Code and Life

Programming, electronics and other cool tech stuff

Supported by

Supported by Picotech

Using a Soundcard as an Oscilloscope to Decode 433 MHz Smart Power Plug Remote Control Signals

I previously covered how to decode Nexa smart power plug remote control signals using Arduino Uno. The drawback of Arduino was the limited capture time and somewhat inaccurate timing information (when using a delay function instead of an actual hardware interrupt timer).

Another way to look at signals is an oscilloscope, and a soundcard can work as a "poor man's oscilloscope" in a pinch. You wire the signal to left/right channel (you could even wire two signals at the same time) and "measure" by recording the audio. It helps if ground level is shared between the soundcard and the signal source. In this case I achieved it pretty well by plugging the Arduino that still provides 3.3V voltage for the 433 MHz receiver chip to the same USB hub as the soundcard.

Another thing to consider is the signal level. Soundcard expects the voltage to be around 0.5V, whereas our receiver is happily pushing full 3.3V of VCC when the signal is high. This can be fixed with a voltage divider -- I wired the signal via a 4.7 kOhm resistor to the audio plug tip, and a 1 kOhm resistor continues to GND -- effectively dropping the voltage to less than a fifth of original.

The audio plug "sleeve" should be connected to GND as L/R channel voltages are relative to that. There was a 0.1V difference in voltage between soundcard sleeve and Arduino GND so I decided to put a 1 kOhm resistor also here to avoid too much current flowing through a "ground loop".

Note that I'm using a SparkFun breakout for the plug that makes connecting the plug to a breadboard quite easy. If you need to wire it directly to the connector (signal to "tip" and ground to "sleeve"), refer to diagram here: https://en.wikipedia.org/wiki/Phone_connector_(audio). A word of caution: You can break stuff if you wire it incorrectly or provide too high voltage to some parts, so be careful and proceed at your own risk!

Soundcard as an Oscilloscope

You can see the circuit in the picture above. Note that due to receiver needing GND in two places, I've been able to connect the sleeve to another GND point (rightmost resistor) than the other 1 kOhm resistor that is part of the voltage divider (leftmost resistor)

Trying it out with Audacity

Once you have power to the receiver, and 1 kOhm resistors to sleeve and tip and signal through a 4.7 kOhm (anything between that and 22 kOhm works pretty well with 16 bits for our ON/OFF purposes), you can start a recording software like Audacity to capture some signals. Here is a sample recording where I've pressed the remote a couple of times:

Audacity example

As you can see, our signal is one-sided (audio signals should be oscillating around GND, where as our is between GND and +0.5V) which causes the signal to waver around a bit. If you want a nicer signal you can build a somewhat more elaborate circuit but this should do for our purposes.

By the way, you can use ctrl-A to select all of the recorded audio and use the "Amplify" effect (with default settings) maximize signal to available audio headroom. Makes looking at the signals a little easier. I've done that before the captures below. Another issue becomes apparent when we zoom in a bit closer:

Audacity example two: zoomed in

Due to automatic gain in the receiver chip (or maybe some other anomaly), the receiver consistently captures some "garbage signal" some 200 ms after the "official" Nexa signal we got a glimpse of in the [previous part of this series]. It is not pure noise but seems to contain some other signal, probably coming somewhere else in the building but at a much lower power level -- the receiver records this as well, but blocks it for a while once it gets the more powerful Nexa signal.

To analyze the Nexa signal, I suggest deleting other parts of audio capture than the actual signals. I actually tore open the Nexa remote and measured the signal straight from the antenna to make sure this recurring "tail" is not coming from the remote. :D

Below you can see a zoom-in of the signal at various levels of magnification. Note that I'm not zooming in on the first signal that has the "spike" before the actual signal starts.

Nexa remote signal zoomed in Nexa remote signal zoomed in more Nexa remote signal zoomed in even more

As you can see, the 44.1 kHz signal has enough resolution to deduce the signal protocol just with Audacity. But countint the individual samples from HIGH and LOW segments to get an average length is pretty tedious. If only we could do it programmatically...

Using Python to analyze the recorded signal

I made a handy Python script to analyze the recorded signal. Basic procedure is:

Read post

Analyzing 433 MHz Nexa Smart Power Plug Remote Control Signal with Arduino Uno

A friend recently started a project to remotely boot his router (which tends to hang randomly) with Raspberry Pi. Unfortunately, the rpi-rf tool was not quite recognizing the signals. I pitched in to help, and as he did not have access to an oscilloscope, but had an Arduino Uno, I thought maybe I could figure it out with that.

Fast forward a few weeks later, I have been experimenting with four methods analyzing my own Nexa 433 MHz remote controller:

  1. Arduino Uno
  2. Soundcard a.k.a. "poor man's oscilloscope"
  3. Raspberry Pi
  4. An actual oscilloscope, namely my Picoscope 2208B

Having learned a lot, I thought to document the process for others to learn from, or maybe even hijack to analyze their smart remotes. In this first part, I will cover the process with Arduino Uno, and the following posts will go through the other three methods.

Starting Simple: Arduino and 433 MHz receiver

Having purchased a rather basic Hope Microelectronics (RFM210LCF-433D) 3.3V receiver for the 433 MHz spectrum signals, it was easy to wire to Arduino:

  1. Connect GND and 3.3V outputs from Arduino to GND and VCC
  2. Connect Arduino PIN 8 to DATA on the receiver
  3. Connect a fourth "enable" pin to GND as well to turn the receiver on

You can see the setup here (larger version):

Arduino wired to Hope 433 MHz rf receiver

I wrote a simple Arduino script that measures the PIN 8 voltage every 50 microseconds (20 kHz), recording the length of HIGH/LOW pulses in a unsigned short array. Due to memory limitation of 2 kB, there is only space for about 850 edges, and the maximum length of a single edge is about 65 000 samples, i.e. bit more than three seconds.

Once the buffer is filled with edge data or maximum "silence" is reached, the code prints out the data over serial, resets the buffer and starts again, blinking a LED for 5 seconds so you know when you should start pressing those remote control buttons. Or perhaps "press a button", as at least my Nexa pretty much fills the buffer with a single key press, as it sends the same data of about 130 edges a minimum of 5 times, taking almost 700 edges!

It also turned out that the "silence" limit is rarely reached, as the Hope receiver is pretty good at catching stray signals from other places when there is nothing transmitting nearby (it likely has automatic sensitivity to "turn up the volume" if it doesn't hear anything).

#define inputPin 8 // Which pin to monitor

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(38400);
}

#define BUFSIZE 850
#define WAIT_US 50 // 20 kHz minus processing overhead

unsigned short buf[BUFSIZE];
short pos = 0;
bool measure = false;

void loop() {
  if(measure) {
    unsigned char data = digitalRead(inputPin) == HIGH ? 1 : 0;
    if(data == (pos&1))
      pos++; // data HIGH and odd pos or LOW and even -> advance
    
    if(buf[pos] > 65000 || pos >= BUFSIZE) {
      measure = false;
      return; // avoid overflow
    }
    
    buf[pos]++;
    delayMicroseconds(WAIT_US); // Wait until next signal
  } else { // end measure
    digitalWrite(LED_BUILTIN, LOW); // LED OFF = dumping
    Serial.println();
    Serial.print("N =");
    Serial.println(pos, DEC);
    for(int i=0; i<pos && i<BUFSIZE; i++) {
      Serial.print(buf[i], DEC);
      if(buf[i] > 100000/WAIT_US)
        Serial.println(); // 100 ms delays get a newline
      else
        Serial.print(' ');
      buf[i] = 0; // reset
    }
    pos = 0; // restart
    measure = true;
    for(int i=0; i<10; i++) { // 5s blink before next measurement 
      digitalWrite(LED_BUILTIN, (i&1) ? LOW : HIGH); 
      delay(500); 
    }
    Serial.println("\nGO");
    digitalWrite(LED_BUILTIN, HIGH); // LED ON = measuring
  }
}

Analyzing the data

Here is some sample data from the serial monitor:

Read post

WebSocket Magic: Create a Simple Server and Client in Go and JavaScript

Title image, generated with Stable Diffusion

WebSocket is a protocol that allows for real-time, bidirectional communication between a client and a server. It is often used in web applications to enable features such as chat, live updates, and multiplayer games.

In this tutorial, I will show you how to create a minimalistic WebSocket server using Go and the nhooyr websocket library, and a JavaScript client to test it out. You will learn how to handle WebSocket connections, send and receive messages, and close the connection when necessary.

By the end of this tutorial, you will have a working WebSocket server and client that you can use as a starting point for your own WebSocket-based applications.

Setting up the project

You should first set up a simple "Hello world" go project, something along the lines of this tutorial. After you have a project going, let's install nhooyr.io/websocket WebSocket library (Go's own seems deprecated and Gorilla development has ceased some years ago):

$ go get nhooyr.io/websocket

The whole system will consist of main.go that will contain a simple net/http server that will:

  1. Serve a simple WebSocket echo server at /echo
  2. Serve static files from static subfolder – essentially other addresses including / will try content from there. We'll put index.html under that subfolder.

Basic webserver stuff:

func main() {
	address := "localhost:1234"
	http.HandleFunc("/echo", echoHandler)
	log.Printf("Starting server, go to http://%s/ to try it out!", address)
	http.Handle("/", http.FileServer(http.Dir("static")))
	err := http.ListenAndServe(address, nil)
	log.Fatal(err)
}

Now the echoHandler will do a few essential items:

  1. Upgrade the connection into a WebSocket one with websocket.Accept
  2. Log errors and defer connection close in case of errors
  3. Loop forever (or actually 10 minutes in this sample), reading messages from the socket and writing them back.

Note that I've used InsecureSkipVerify to accept connections from any origin, you might want to modify the code for a tighter policy:

Read post

Seeed Studio XIAO nRF52840 (Sense) test drive on Arduino

Seeed Studio XIAO nRF52840

I have to confess I have a thing for small prototyping boards, especially ones with Bluetooth or WLAN connectivity. So when I was offered the opportunity to get a couple of Seeed Studio's tiny Bluetooth devboards with Nordic's nRF52840 in them to try out, I jumped at the opportunity. So full disclosure, I did not buy these myself, but neither did I get any compensation, so what follows will be rather unbiased first impressions! I will cover:

  1. The basic specifications of the two units
  2. How to (re)program the device with Arduino
  3. Help to troubleshoot upload.tool.serial errors on Arduino
  4. Tips and notes on using the USB mass storage mode
  5. Initial summary

I'm interested in trying out the PDM microphone, accelerometer and BLE functionality later on, so check back for updates!

Basic specifications of the Seeed XIAO BLE nrf52840

The Seeed XIAO BLE units come in two varieties, both sharing quite beefy specs:

  • Bluetooth 5.0 with an onboard antenna
  • Nordic nRF52840, ARM Cortex-M4 32-bit processor with FPU, 64 MHz
  • Low power consumption and battery charging chip for untethered IoT use cases
  • Onboard 2 MB flash

Additionally, the Sense variant contains a PDM microphone and a 6-axis accelerometer. The units arrived from China quite quickly and came in sweet little Seeed plastic packages, pin headers included (not soldered in):

Seeed Studio XIAO nRF52840 and Sense

You can get both directly from Seeed, with very reasonable $9.90 and $15.99 price points. Nordic's chips are quite hard to source from AliExpress cheaply (yes I have looked :), so I'd consider both pretty much a bargain.

Board quality seems very good, pads are shiny and components well placed. The USB port is of modern USB-C variety, and the form factor is really small, just 20 x 17.5 mm or the size of a nickel x dime. and the thickness of a half dollar or so (U.S. readers, you're welcome!). The PCB is one-sided which makes it easy to embed in various configurations.

Outside differences of the basic model and Sense variant is one additional chip that contains the PDM microphone. I think the accelerometer is hidden inside the (seemingly FCC and CE compliant) shielding.

Seeed Studio XIAO nRF52840

There is also an absurdly tiny reset button on the opposite corner to the microphone pad (top left above) that is a bit tricky to press. I'd prefer a slightly larger one, but it beats shorting pins any day.

Classic blink test with Arduino

You can follow the instructions on Seeed Studio wiki to install the necessary development tools to build firmware for the device. Short version:

  1. Get Arduino
  2. In File > Preferences, add https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json to Additional Boards Manager URLs
  3. Go to Tools > Board > Boards Manager, search for "seeed nrf52" and install the two libraries.
  4. Now you can select your board and port.

Read post

CORS middleware for julienschmidt/httprouter with Golang

Just a small note / Gist type of thing for today: I got tired of adding w.Header().Set("Access-Control-Allow-Origin", "*") to every handler function in my small Golang web app. I'm using Julien Schmidt's excellent httprouter module for simple routing. Turns out the Basic Authentication example is quite simple to adjust for a set-and-forget type of httprouter.Handle middleware:

// https://github.com/julienschmidt/httprouter middleware to set CORS header
func MiddleCORS(next httprouter.Handle) httprouter.Handle {
	return func(w http.ResponseWriter,
    r *http.Request, ps httprouter.Params) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		next(w, r, ps)
	}
}

Using the middleware is simple, just wrap your normal handler function:

router.GET("/someurl", MiddleCORS(SomeURLFunc))

Or both the middleware and the function it takes implement httprouter.Handle, you can just chain multiple middleware with MiddleCORS(AnotherMiddleware(SomeURLFunc)).

Read post

Control Philips Hue Color (RGB) Lights With Python

I got tired of the fact, that Philips Hue application does not seem to have any easy way to set multiple lights to different colors (and possibly brightnesses) at once. Thankfully, there is a great "REST API" to query light and set status! Read on how to query your lights, set them, and do RBG to Philips Hue XY (or X, Y) colorspace conversion with Python!

Connecting and configuring your Philips Hue for API access

This is a prerequisite step. Find out your Hue Brige's IP and create a user (simple method of authentication). It's all covered in Get started.

Write down your IP and the username, and proceed to activate your user and test it according to instructions. Once you are done, you should be able to also open the lights list in your browser and see a lot of info:

https:///api//lights

Listing Your Connected Philips Hue Lights with Python

Alright, with the prerequisites done, let's do a simple test with Python, and query that same address parse the returned JSON, and pretty print it. You'll need Python 3.10+ for this:

import urllib.request
import ssl, json, pprint

context = ssl._create_unverified_context()

def get_json(url):
    """Do a HTTP GET request and return response parsed as JSON."""
    print(url)
    req = urllib.request.Request(url=url, method='GET')
    f = urllib.request.urlopen(req, context=context)
    print(f.status, f.reason)
    return json.loads(f.read())

# Replace these with your config
user = 'yourverylongusernamestring'
ip = '192.168.1.123'

data = get_json(f'https://{ip}/api/{user}/lights')

pp = pprint.PrettyPrinter(indent=4)   
pp.pprint(data)

for k in data: print(k, data[k]['name'])

Read post