Code and Life

Programming, electronics and other cool tech stuff

Supported by

Supported by Picotech

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

Recording 433 MHz Radio signals with Seeed XIAO RP2040

Soundcard as an Oscilloscope

Having done 433 Mhz radio signal recording with PicoScope 2208B MSO and Raspberry Pi 4, Arduino Uno and regular USB soundcard, I figured, why not add one more to the mix: Let's try the RP2040!

Compared to Arduino Uno, the RP2040 has major advantages for this project:

  1. Much higher clock frequency of 133 MHz means there's cycles to spare even at ~1 Mhz rates
  2. Relatively vast SRAM memory, 264 kB vs. 2 kB
  3. Native C SDK that is rather easy to work with

I'm using the Seeed XIAO RP2040 for this project. It is extremely compact and has a nice USB-C interface. You can see the wiring, it's just 3.3V and GND to the receiver (which luckily did work fine with that voltage) and signal to GPIO pin 0.

Note that while RP2040 pinout has 5V supply line, the GPIO pins are not 5V tolerant, so you should not power a 5V receiver and directly connect it to pin 0. A voltage divider is strongly recommended to avoid longer term damage to the RP2040 pin.

Setting up RP2040 programming environment

I basically followed the Getting started guide that was linked from the Pico SDK Github to get the Blink example working. After that, it was quite simple to set up a new project following the "Quick-start your own project", setting up CMakeLists.txt like this:

cmake_minimum_required(VERSION 3.13)

# initialize the SDK based on PICO_SDK_PATH
# note: this must happen before project()
include(pico_sdk_import.cmake)

project(joonas-pico)

# initialize the Raspberry Pi Pico SDK
pico_sdk_init()

# rest of your project
add_subdirectory(logic_analyze)

In the logic_analyze subfolder I copied the Interrupt triggered GPIO example to continue from. You can grab the full example as a zip here and run pretty similar set of commands as in the SDK guide:

$ mkdir logic_analyze
$ cd logic_analyze
$ wget https://codeandlife.com/images/2023/logic-analyze-pico.zip
$ unzip logic_analyze-pico.zip
$ mkdir build
$ cd build
$ export PICO_SDK_PATH=../../pico-sdk
$ cmake ..
$ make

Note that this assumes you placed the example directory logic_analyze alongside your pico-sdk directory.

After running make, you should find the logic.uf2 file under logic_analyze directory and you can just drag and drop it to your RP2040 when it is in USB drive mode.

C Code for Recording GPIO Changes

The code is basically combination of what I did for Arduino and Raspberry Pi, and the hello_gpio_irq and hello_timer examples. Basic logic:

  1. Setup stdio_init_all() (over USB, necessary definitions to enable that in CMakeLists.txt file) and wait until stdio_usb_connected() returns true.
  2. Loop forever, asking the user over serial (USB) to press a key to start recording
  3. Clear receive buffer
  4. Set alarm timeout of 5 seconds to end recording if buffer hasn't been filled
  5. Set up GPIO interrupt triggers on rising and falling edges of pin 0
  6. In the interrupt handler, record time elapsed since last edge using time_us_64()
  7. Once timeout is reached or buffer has been filled, disable GPIO interrupt and print out received timings.

Here's the main method:

int main() {
    stdio_init_all();
    while(!stdio_usb_connected());

    while(true) {
        printf("Press any key to start.\n");
        getchar_timeout_us(100*1000000);
        printf("Started!\n");

        for(int i = 0; i < BUFSIZE; i++) buf[i] = 0; // Clear
        pos = 0;
        timer_fired = false;

        add_alarm_in_ms(5000, alarm_callback, NULL, false);

        gpio_set_irq_enabled_with_callback(0,
                                           GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL,
                                           true, &gpio_callback);

        while(!timer_fired && pos <= BUFSIZE) tight_loop_contents();

        gpio_set_irq_enabled(0, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, false);
        printf("Ended.\n");

        for(int i = 0; i < pos; i++) printEdge(buf[i], i&1);
    }
}

Alarm and interrupt callbacks are rather straightforward:

Read post

Recreating Chris Veness' AES256 CTR decryption with CryptoJS for fun and profit

A quick one tonight. Having just spent enjoyable hacking time to reverse engineer Chris Veness' AES 256 encryption library enough to be able to decrypt some old data I had using CryptoJS, I thought I will share it with the world to enjoy.

Now Chris' library is nice and simple, you can just encrypt stuff with AES 256 counter mode with a single line of code:

> AESEncryptCtr('Well this is fun!', 'password', 256)
'SdzeY4GBgYHDEWay4JdHr/CnwwnAoBfjQA=='

Now AES 256 is a super standard cipher, so it should be pretty easy to decrypt that with another libray, right?

WRONG!

Wrong, standard AES crypto is not always easy to decrypt

Turns out that Chris' library does not use the 'password' in a way most other libraries use it, but instead chooses to:

  1. Decode the string into UTF8
  2. Create a 16 byte array and put the decoded string into beginning of the array
  3. Initialize AES encryption ("key expansion") using this array
  4. Encrypt a copy of the array as a single AES block using the key expansion (that the library has internally just been initialized with)
  5. Expand the AES encrypted 16 bytes by doubling the array into 32 byte one
  6. Use the 32 byte array as the AES key

Now if you think that any other library would use the same method, you would be dead wrong. Also, most libraries do not expose the same set of functions to replicate this process in any simple manner.

To add insult to injury, JavaScript has so poor support for byte data that it seems each crypto library uses its own internal representation of binary data. For example, CryptoJS likes to make a uint32 array, so [1, 2, 3, 4, 5, 6, 7, 8] is represented as {words: [0x01020304, 0x05060708], sigBytes: 8}!

Thankfully, yours truly is a true master and after just 2 hours of trial and error, I managed to produce this golden nugget:

const crypto = require('crypto-js');

function VanessKey(password) {
  const iv = { words: [0,0,0,0], sigBytes: 16};
  const encrypted = crypto.AES.encrypt(
    crypto.enc.Utf8.parse(password.padEnd(16, '\0')),
    crypto.enc.Utf8.parse(password.padEnd(32, '\0')), { iv }
  ).ciphertext;
  const ew = encrypted.words.slice(0, 4);
  // double the first 4 words of the encrypted
  encrypted.words = ew.concat(ew);
  return encrypted;
}

const key = VanessKey('password');
console.log(key);
console.log(crypto.enc.Hex.stringify(key));

Saving it as decrypt.cjs and running node decrypt.cjs should produce the "Vaness key" for 'password':

$ node poista.cjs
{
  words: [
    233507778, -213013704,
     65782802, -856145110,
    233507778, -213013704,
     65782802, -856145110
  ],
  sigBytes: 32
}
0deb0bc2f34dab3803ebc412ccf8432a0deb0bc2f34dab3803ebc412ccf8432a

Using the key to decrypt the encrypted data

Now we only need the magic incantation to decrypt the Base64 encoded data SdzeY4GBgYHDEWay4JdHr/CnwwnAoBfjQA== produced in the intro! This is also pretty non-straightforward:

Read post

Decoding 433 MHz Radio Signals with Raspberry Pi 4 and Picoscope Digital Oscilloscope

PicoScope 2208B MSO and Raspberry Pi 4 Finale time! After analyzing the Nexa 433 MHz smart power plug remote control signal with Arduino Uno and regular USB soundcard, it is time to try some heavier guns: Raspberry Pi 4 and PicoScope 2208B MSO.

I was initially sceptical on using a Raspberry Pi for analyzing signals, due to several reasons:

  1. Most GPIO projects on RaspPi seem to use Python, which is definitely not a low-latency solution, especially compared to raw C.
  2. Having done a raw C GPIO benchmark on RaspPi in the past, the libraries were indeed quite... low level.
  3. I had serious doubts that a multitasking operating system like Linux running on the side of time-critical signal measurements might impact performance (it is not a RTOS after all).

However, there are projects like rpi-rf that seem to work, so I dug in and found out some promising aspects:

  • There is a interrupt driven GPIO edge detection capability in the RaspPi.GPIO library that should trigger immediately on level changes.
  • Python has a sub-microsecond precision time.perf_counter_ns() that is suitable for recording the time of the interrupt

Python code to record GPIO signals in Raspberry Pi

While rpi-rf did not work properly with my Nexa remote, taking hints from the implementation allowed me to write pretty concise Python script to capture GPIO signals:

from RPi import GPIO
import time, argparse

parser = argparse.ArgumentParser(description='Analyze RF signal for Nexa codes')
parser.add_argument('-g', dest='gpio', type=int, default=27, help='GPIO pin (Default: 27)')
parser.add_argument('-s', dest='secs', type=int, default=3, help='Seconds to record (Default: 3)')
parser.add_argument('--raw', dest='raw', action='store_true', default=False, help='Output raw samples')
args = parser.parse_args()

times = []

GPIO.setmode(GPIO.BCM)
GPIO.setup(args.gpio, GPIO.IN)
GPIO.add_event_detect(args.gpio, GPIO.BOTH,
        callback=lambda ch: times.append(time.perf_counter_ns()//1000))

time.sleep(args.secs)
GPIO.remove_event_detect(args.gpio)
GPIO.cleanup()

# Calculate difference between consecutive times
diff = [b-a for a,b in zip(times, times[1:])]

if args.raw: # Print a raw dump
    for d in diff: print(d, end='\n' if d>5e3 else ' ')

The code basically parses command line arguments (defaulting to GPIO pin 27 for input) and sets up GPIO.add_event_detect() to record (microsecond) timings of the edge changes on the pin. In the end, differences between consecutive times will be calculated to yield edge lengths instead of timings.

Raspberry Pi is nice also due to the fact that it has 3.3V voltage available on GPIO. Wiring up the 433 MHz receiver was a pretty elegant matter (again, there is a straight jumper connection between GND and last pin of the receiver):

Raspberry Pi wired to 433 MHz receiver

  1. Wire the receiver in
  2. Start the script with python scan.py --raw (use -h option instead for help on command)
  3. Press the Nexa remote button 1 during the 3 second recording interval

Here's how the output looks like (newline is added after delays longer than 5 ms):

Read post

Stop Writing Scripts: Create and automatically run ChatGPT generated Python code

I previously covered how to run your own ChatGPT script with Python and Golang. But what if you want to create a script that automatically runs the ChatGPT generated code? That is what I will cover in this post. The idea is really simple:

  1. Create a script that asks for user input
  2. Pass the input to ChatGPT
  3. Run the ChatGPT generated code

NOTE: Please read the caveats at the end of the post before using this!

Calling The OpenAI API

First step is simple, just call the OpenAI API with the user input. If "-v" is given as the first argument, print the raw answer and usage statistics as well.

#!/usr/bin/python3

import openai
import sys

openai.api_key = 'sk-xxx'

verbose = sys.argv[1] == '-v'
prompt = ' '.join(sys.argv[2 if verbose else 1:])

resp = openai.ChatCompletion.create(
  model="gpt-3.5-turbo",
  messages=[
        {"role": "system", "content": "You are Python code generator. Answer with just the Python code."},
        {"role": "user", "content": prompt},
    ]
)

data = resp['choices'][0]['message']['content']

if verbose: print(data, 'Usage was:', resp['usage'], sep='\n')

Parsing the Code From Response

A rather simplistic implementation looks for set of start tokens and end tokens and returns the code between them. This is not perfect, but it works as long as the ChatGPT response does not contain multiple code blocks.

# Extract lines of code from the response tagged with ``` or ```python
lines = []
ok_start = ['```', '```python', '```py', '```python3', '```py3']
started = False
for line in data.splitlines():
    if not started:
        if line.strip() in ok_start:
            started = True
    else:
        if line.strip() == '```': break
        lines.append(line)

Running The Code

The code is then written to a temporary file and executed. After running, the file is deleted.

import os
import tempfile

# Create a temporary file
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tf:
    tf.write("#!/usr/bin/env python3\n")

    # Write lines to the file
    for line in lines:
        tf.write(line + '\n')

# Make the file executable
os.chmod(tf.name, 0o755)

# Run the file with python3
os.system(tf.name)

# Delete the file
os.unlink(tf.name)

Trying It Out

The whole script is available here for your convenience. If you are on Linux, you can chmod +x the script and run it directly. Otherwise, you can run it with python3 pyai.py:

$ pyai -v simple hello world app
print("Hello, world!")
Usage was:
{
  "completion_tokens": 6,
  "prompt_tokens": 30,
  "total_tokens": 36
}
Hello, world!

Omitting the -v argument will only run the generated code:

$ pyai print out 10 first primes
2
3
5
7
11
13
17
19
23
29
$ pyai calculate sha1 checksum of file pyai
SHA1 checksum of pyai: db5ab334ca2af7508460d5ade8dac7fedd48bcf0
$ sha1sum pyai
db5ab334ca2af7508460d5ade8dac7fedd48bcf0  pyai

Python has pretty powerful libraries, so you can access the web as well:

$ pyai print out longest h1 tag content text from https://codeandlife.com
Using a Soundcard as an Oscilloscope to Decode 433 MHz Smart Power Plug Remote Control Signals
$ # If you want to know what the code (and usage) was, use -v in the beginning
$ pyai -v print out longest h1 tag content text from https://codeandlife.com
```python
import requests
from bs4 import BeautifulSoup

url = "https://codeandlife.com"
response = requests.get(url)
soup = BeautifulSoup(response.content, "html.parser")

h1_list = soup.find_all("h1")
longest_h1 = max(h1_list, key=lambda x: len(x.text))

print(longest_h1.text.strip())
```
Usage was:
{
  "completion_tokens": 76,
  "prompt_tokens": 41,
  "total_tokens": 117
}
Using a Soundcard as an Oscilloscope to Decode 433 MHz Smart Power Plug Remote Control Signals

Yes you can read files too:

$ pyai print out contents of /etc/passwd
# And the contents of /etc/passwd come here :D

$ pyai read /etc/passwd, split by colon and print the largest 3rd entry value
65534

Conclusions and Caveats

While the code is simple and has many limitations, it is staggeringly powerful. You can do pretty much anything you can do with Python with this. But the downside is it's like the "I'm feeling lucky" version of scripting. You don't know what the code is going to do. You can't even see the code before running it. So, use this with caution! Potential drawbacks may include, but are not limited to:

  1. Racking up your OpenAI API usage and costing you money
  2. ChatGPT getting it wrong and the code not working
  3. Code working but doing something you didn't expect
  4. Overwriting (important) files
  5. Getting really malicious code that does something bad

Currently code injection to LLMs is pretty hard, but it is not unthinkable that someone will find a way to do it. So, please be careful when using this. I am not responsible for any damage caused by this script!

Read post

Python on-the-fly AES encryption/decryption and transfer to AWS S3

So, I started writing a file database and toolset called fileson to take advantage of AWS S3 Glacier Deep Archive (let's just call it GDA from now on). With 1 €/mo/TB storage cost, it is essentially a dirt cheap option to store very infrequently accessed data like offsite backups.

Why not just use rclone? Well, I disliked the fact that all tools do a ton of (paid) queries against S3 when syncing. I thought a simple JSON file database should work to keep track what to copy and delete. Well, that work is progressing, but as a part of that...

Encrypting on the fly with Python and Pycrypto(dome)

I started thinking that client side encryption would be useful as well. AES is tried and tested, and it's easy to find sample code to do it. But it seems wasteful to first create encrypted files on your hard drive, then upload them to AWS and finally delete everything.

Luckily, the Python AWS SDK boto3 has a great example on how to upload a file to S3 with upload_fileobj that accepts "a readable file-like object". What does that mean? Let's find out!

Read post