# Terminator Beeper

> Self-contained beeping multi-layer data transfer magical leprechaun Hackathon project

Published 2026-07-30 by Zsombor Paróczi · tags: m5stick, terminator, hackathon
Source: https://blog.stormregion.hu/posts/beeping/

At [DONE.](https://thisisdone.com/) we had a nice Hackathon back in July, where every team got an M5Stick (I do love those small ESP32 devices), and they had one day to create something that might help with day-to-day work. The limits were that they could not use any US-based LLMs, forcing them to use free, mostly Chinese solutions like Qwen, DeepSeek, etc.

Now, that is interesting and all, and there were great products at the end of the day, but since I am not allowed to join any team (part of the ruleset), I needed to come up with my own idea. I always had this "two-factor authentication using sound and beeps" idea, where nearby machines can just beep to each other, creating ad-hoc channels. In 2024, some guys even ran with my idea for a TDK at BME. But I never tried it myself, and since it's a small enough problem to solve in a day, let's make it a bit more challenging: having JUST the device and somehow making a normal modern computer communicate with it, without using any other channel. A good metaphor is probably a message in a bottle, where you want to read the message but first need to somehow get it out of the bottle.

The M5Stick I used for this one is the M5StickC PLUS2, so there are WAY more limitations than you want to admit. :D

## Communication idea

Breaking down this challenge is the key to solving it. So:

1. Somehow bootstrap the channel between the computer and the stick (in a secure-ish way)
2. Write the message (using sound)
3. Keep it on the device.

And when the time for the readback comes, same-ish process:

1. Bootstrap channel
2. Read message (using sound)

The bootstrap part is probably more interesting than the sound part, since that one is more like an old dial-up modem in a simplified way.

## Bootstrapping

Let's assume there is no internet and we don't want to use Bluetooth or Wi-Fi, to make it more challenging. That leaves a couple of ways a computer can receive input:

1. Physical wire things, like USB or so
2. Camera, let's assume we are talking about a laptop
3. Microphone

Since the camera seems like a good idea, I started with the obvious "does not need anything else, just the OS" approach with QR-code-based communication. Well... a 1.14-inch, 135 × 240 LCD is not the largest thing in the world. :D I managed to squeeze 515 bytes of code into a 97×97 pixel grid, and mostly failed to read it as a QR code.

This is one of the test QR codes:

![QR](https://blog.stormregion.hu/posts/beeping/qr_clean.png)

It can barely fit the 515-byte payload, but to be honest, it's mostly non-working, and the payload itself got larger.

That was the stage where I decided to do a multi-stage deal. The **first stage** is just minimal, small code that can receive data: small footprint, slow data transfer, but enough to receive the **second stage**, which is the actual code that we need for the fast(er) communication. Man, if that LCD had been just 50% larger, it might have worked.

Since there is no "audio to code" type tool (as far as I know) on a standard machine, and I did not want to mess around with implementing a USB protocol AND writing some things to my Mac, I fell back to **let's type in the first stage**.

Still, I needed a first stage. My solution was to create a super-minimal HTML page for this:

```html
data:text/html,<i onclick=\"a=new AudioContext;N=a.sampleRate/100|0;navigator.mediaDevices.getUserMedia({audio:1}).then(s=>{sp=a.createScriptProcessor(256);a.createMediaStreamSource(s).connect(sp);st=b=n=k=x=0;o='';sp.onaudioprocess=e=>{for(v of e.inputBuffer.getChannelData(0)){x+=v*v;w=st?N:44;if(++k<w)continue;st&&x>M&&(M=x);q=x>(st?M/2:w/1e3);k=x=0;if(!st){st=q;M=0;continue}b=b*2+q;if(++n&7)continue;st<2?st++:st<3?b!=85?st=0:st++:st<4?(l=b,st=4):st<5?(l=l*256+b,st=5):st<6?(o+=String.fromCharCode(b),--l?0:st=6):0;b=n=0;if(st>5)this.innerHTML=o}};sp.connect(a.destination)})\">RECV</i>
```

Which looks awesome on the device BTW (a bit older screenshot, but still):

![M5 in the Fridge](https://blog-files.stormregion.hu/beeping/fridge.jpg)

I know, I know. It will need some explaining:

```js
const audioContext = new AudioContext();

const preambleWindowSamples =
    Math.floor(audioContext.sampleRate / 100);

navigator.mediaDevices
    .getUserMedia({ audio: true })
    .then(stream => {
        const processor =
            audioContext.createScriptProcessor(256);

        const source =
            audioContext.createMediaStreamSource(stream);

        source.connect(processor);

        let state = 0;
        let currentByte = 0;
        let bitCount = 0;

        let sampleCount = 0;
        let energy = 0;

        let output = '';
        let maxPreambleEnergy = 0;
        let remainingLength = 0;

        processor.onaudioprocess = event => {
            const samples =
                event.inputBuffer.getChannelData(0);

            for (const sample of samples) {
                energy += sample * sample;

                const windowSize = state
                    ? preambleWindowSamples
                    : 44;

                sampleCount++;

                if (sampleCount < windowSize) {
                    continue;
                }

                if (
                    state &&
                    energy > maxPreambleEnergy
                ) {
                    maxPreambleEnergy = energy;
                }

                const threshold = state
                    ? maxPreambleEnergy / 2
                    : windowSize / 1000;

                const bit = energy > threshold;

                sampleCount = 0;
                energy = 0;

                /*
                 * Wait for the first energetic block.
                 */
                if (!state) {
                    state = bit;
                    maxPreambleEnergy = 0;
                    continue;
                }

                /*
                 * Assemble one byte, MSB-first.
                 */
                currentByte =
                    currentByte * 2 + bit;

                bitCount++;

                if (bitCount & 7) {
                    continue;
                }

                /*
                 * Parse protocol bytes.
                 */
                switch (state) {
                    case 1:
                        // Guard byte.
                        state = 2;
                        break;

                    case 2:
                        // Sync byte = 0x55.
                        if (currentByte !== 0x55) {
                            state = 0;
                        } else {
                            state = 3;
                        }
                        break;

                    case 3:
                        // Length high byte.
                        remainingLength =
                            currentByte;

                        state = 4;
                        break;

                    case 4:
                        // Length low byte.
                        remainingLength =
                            (remainingLength << 8) |
                            currentByte;

                        state = 5;
                        break;

                    case 5:
                        // Payload.
                        output +=
                            String.fromCharCode(
                                currentByte
                            );

                        remainingLength--;

                        if (remainingLength === 0) {
                            state = 6;
                        }
                        break;
                }

                currentByte = 0;
                bitCount = 0;

                if (state === 6) {
                    this.innerHTML = output;
                }
            }
        };

        processor.connect(
            audioContext.destination
        );
    });
```

A small state machine with a simple "on-off" signal that is converted into one tone or silence. One long flow, no error correction, no checks, nothing. But at least it can wait for the first part of the transfer, the long beep, and then the sync. Also, it's super slow, since it should be somewhat robust to other noises, hence the 256 samples turning into 1 byte.

Giving a small ChatGPT explanation of this:

![Bootstrap html](https://blog-files.stormregion.hu/beeping/bootstrap.png)

More than 3 minutes just to put 2413 bytes of HTML from point A to point B. The beeper on the device is not the best, and during the Hackathon I annoyed everyone enough that, for such a short time, this should be enough. I bet some improvements can be made.

The resulting HTML is a bit uglier than the original (CSS and debug strings cut from that), but this is how the page should have looked:

![HTML of stage2](https://blog-files.stormregion.hu/beeping/write.png)

## Stage 2

The Stage 2 interface is basically a huge textarea with 2 buttons (functions). The first is "COPY": whatever the user enters into the textarea gets converted to sound and played. For example, this 25,187-byte-long fan fiction in the Terminator universe. Yes, 1.4 hours of beeping for roughly 5 bytes/sec. It uses the fancier, more robust, 4-tone version: checksum, everything fancy that I did learn at the university. :D ChatGPT explanation:

![4-FSK protocol](https://blog-files.stormregion.hu/beeping/4fsk.png)

The beeper itself is limited, the microphone is limited, and it's an open-air communication protocol hacked together in 2413 bytes. Once the HTML is loaded onto a machine, it can act both as a receiver and sender, so if you are lucky, you only need to load the initial 3 minutes once.

A function of the stick itself is to store data persistently, so you can record messages (stored as messages, not voice, since we just have a couple of usable kBytes).

![Storage](https://blog-files.stormregion.hu/beeping/storage.jpg)

## A hint of style

Of course, presentation is half of the story in Hackathons, so slapping a Terminator story around it (keeping the communication secure-ish by using an obscure sound protocol, no internet, etc.), adding some design, and we got a crazy Hackathon project. :)

![Background for dramatic effect](https://blog-files.stormregion.hu/beeping/bg.gif)

## Credits

- OpenCode (Big Pickle, DeepSeek V4 Flash)
- Z.AI GLM 5.2
- Qwen Image 2.0
- Me as the host, the guy orchestrating agents and pushing random buttons on a small beeping device
