Notes from real engagements — embedded systems, integration, and modernisation work. Written from the field, not the marketing department.
This is the multi-page printable view of this section. Click here to print.
Field notes
- I built a LoRa analyser because I was sick of guessing
- Zephyr is the DOS-to-Linux moment for embedded
- We open-sourced our Antarctic wave buoys
I built a LoRa analyser because I was sick of guessing
Debugging a LoRa link used to go like this. A node stops reporting. I swap the antenna. I move the gateway. I drop the spreading factor. Something improves, or it doesn’t, and I have no idea why. The band is invisible and every fix is a guess.
So I built the thing I kept wishing I had. LoRaScope is a small radio that listens to the band and shows you what is actually on the air: LoRa, MeshCore and LoRaWAN, with live switching between them. No gateway, no network server, nothing in the path. The radio hears the packet, the box decodes it, and it shows up on a screen.
The product site is lora.dd.com.au. This is the field note version of what it does and why.
What you see
Who is talking, how often, how loud (RSSI and SNR), how busy the channel is and whether the link is healthy. That is the whole point, and it is surprising how much of the guesswork evaporates once you can just look.
For LoRaWAN and MeshCore it reads the headers without any keys. DevAddr, frame counter, port, airtime. You cannot see the payload without the session keys, but you do not need it to find the node that is hogging the channel, or to notice that a device is rejoining every ten minutes. When you do have the keys, it decrypts everything.
Decoding on the box, or in the browser
Plain binary payloads and encrypted ones decode on the device itself and go straight to its screen. That covers the sensors I build and most of what I run into in the field: a few bytes of packed fields, sometimes sealed with a symmetric key.
For anything more involved there is a JavaScript plugin runtime in the browser. You write a small decoder against the live feed, reload, and your packets show up as fields instead of hex. Site-specific formats, odd vendor encodings, a protocol nobody documented. It is the same feed the rest of the dashboard uses, so a plugin’s output goes into the plots and the logs like everything else.
Walking the site
The dashboard is served from the box, so a laptop or a phone on the same WiFi is all you need. But a lot of LoRa problems are about where you are standing, so there is a screen on the device as well.
It also has GPS and a ping test. Walk away from the other end, press ping, and distance, RSSI and SNR come up on the screen and get written to the SD card with the position. A range survey becomes one trip with the box in your hand, and nobody has to sit at the far end reading numbers over the phone.
Everything else gets logged to SD too, raw frames and decoded, and you can browse and plot the captures in the browser afterwards.
The other things I lean on
Bandwidth, duty cycle and airtime per station. That is usually the number you actually need, and it is the one people are most often surprised by.
Channel scanning, so you find out what is where before you pick a frequency. There is a LoRaWAN mode that hunts join requests and uplinks across the sub-band.
Emulation. The box can pretend to be a hub or a repeater so you can test a single node in isolation without touching the field hardware.
Handing it to an agent
Everything the browser can see and do is also exposed over telnet and REST. That was a deliberate choice, and it paid off sooner than I expected.
I had a run of packet corruption on the bench. Rather than sit there reading hex, I gave an AI agent the telnet and REST addresses and let it watch the feed, form a hypothesis, inject a test packet, and look at what came back. It ran that loop on its own and found the fault. The full session is written up on the LoRaScope site: How I let an AI debug my LoRa network.
An analyser you can only look at is a screen. One you can query and drive is an instrument, and that is the difference for anyone who wants to automate their testing.
What building it covered
I am building LoRaScope to sell. I am also building it because it is a compact demonstration of the work I do for clients.
The firmware is C++ on an ESP32. Getting three protocols onto one radio meant pulling apart the binary framing for each and writing decoders that run on a microcontroller. The decrypt path is AES, with the key handling that goes with it. There is a capture store on SD with an API over the top, a web dashboard served from flash, and the telnet and REST surface that makes the agent story possible.
The same skills show up on client work in different clothes: ESP-IDF and Zephyr firmware, LoRa and BLE, binary protocol design and reverse engineering, encryption on constrained devices, device provisioning, and the database and API end that the data lands in.
If you have a flaky mesh, a gateway that hears nothing, or a binary protocol nobody has documented, that is the sort of problem I take on. The LoRa and LoRaWAN work I do is described in more detail here, or just get in touch.
Zephyr is the DOS-to-Linux moment for embedded
I’ve written embedded firmware for a long time — AVR, ESP32, nRF52, RP2040 —
and mostly the default was bare metal or a tiny superloop. It worked. It was
also, honestly, the DOS era of microcontrollers: you own the whole machine, you
reinvent every driver, and “scaling up” means more #ifdef soup. FreeRTOS if
you were feeling fancy, but the same shape.
Zephyr is the move to Linux. It brings what Linux got right down to a 64KB MCU.
I was late to this
I have the receipts. In May I wrote myself a note about build systems: PlatformIO under “Current”, Zephyr filed under “Alternatives watched” with the line “worth it if I move heavier on nRF.” The same note listed the trigger that would change my mind — “a specific project’s complexity outgrows PlatformIO’s project model.”
Then exactly that happened. I started the second-generation OGLAS repo on 23 July. Twenty-four days and 184 commits later, the whole sensor fleet — sensor, repeater and hub tiers, plus a handheld and a remote control — runs on Zephyr.
Devicetree, not #ifdef soup
This was the first thing that sold me. Hardware goes in devicetree overlays, features in Kconfig fragments, product tier in its own app directory. Three orthogonal axes, and variation stops turning into soup.
Concretely: I collapsed a pile of per-role firmware variants into one config-driven image with a runtime node config store. One binary, configured per node, instead of a build matrix that grew every time a customer wanted something slightly different.
The same app on ARM and RISC-V
The same OGLAS sensor code runs on nRF52840 (RAK4631, XIAO BLE, T-Echo Lite), ESP32-S3 (T3-S3, Heltec Wireless Tracker), ESP32-C6 and ESP32-C3. That last pair is RISC-V, not ARM.
Same application, different instruction set, because the HAL is Zephyr’s rather than the vendor’s. I did not expect that to just work. Changing silicon mid-project is normally a rewrite; here it was a board file.
CI for firmware
ztest and twister are the parts most people miss. Twelve test suites that
build and run across a board matrix on every push — firmware gets the same
regression discipline web code has had for years.
It still bit me, mind you. The platform list in CI has to name every board a
sample.yaml mentions, or those tests skip silently rather than failing. That
is how two of my apps went unbuilt for a week without anyone noticing. Silent
skips are worse than red builds.
The rest is quieter but adds up. Networking ships in the box, so BLE, Thread, Wi-Fi and MQTT are maintained subsystems rather than copy-pasted vendor blobs. Power management has real policy hooks — I PM-gated a GNSS UART between acquisitions and stopped the GPS burning current between fixes. And there’s enough visibility into the build to notice a board sitting at 87.6% DRAM, move the big arrays into PSRAM, and get it back to 73.2%.
No flag day
The part I’m most pleased with isn’t a feature.
The interop contract for OGLAS is the wire protocol, not the code: a Zephyr node has to be byte-identical over the air to a legacy Arduino one. The binary framing and radio parameters are frozen invariants, so old and new firmware coexist in the field. There was no migration weekend, no fleet-wide reflash, no moment where the whole network had to be on the new thing at once.
That matters more than any single Zephyr feature. A rewrite you can deploy one node at a time is a rewrite you can actually finish.
The potholes are real
Bringing up a Heltec Wireless Tracker, the SX1262 ignored everything I sent it.
The sync word read back 0x00 0x00 instead of 0x14 0x24, and every transmit
timed out.
I worked down through the obvious suspects — software chip-select, reclaiming
the CS pin from its pinmux, a push-pull reset, dropping SPI from 4 MHz to
1 MHz — and got nowhere. So I bit-banged a GetStatus on the same pins with
plain GPIO, bypassing the driver entirely. It answered 0x52: chip mode RX.
The radio had been in RX exactly as commanded. My writes had been landing all
along; only the read path was dead.
Root cause: the MISO pad’s input buffer was never enabled (FUN_IE = 0 in
IO_MUX), so the GPIO matrix was reading a constant zero. Boards where MISO
happens to land on a strapping pin work by accident, because the boot ROM
enables the input buffer for them — which is presumably why this had gone
unnoticed. Between that and a board-file defect, that in-tree board’s LoRa
radio could never have worked.
Two days of my life. But here’s the thing: I could read the driver, find it, fix it, and write it up for upstream. On a vendor SDK I’d have filed a ticket and waited.
Where the line falls
Kconfig, devicetree and west are a lot to swallow on day one. It’s overkill
for a 2KB ATtiny or a one-off blinky — there I still reach for PlatformIO and a
superloop, and I’d tell a client the same.
But the moment a project has more than one bus, more than one transport, or more than one person on it, Zephyr pays back fast. I resisted it for years and then did it in under a month.
If you’re weighing the same jump, the Zephyr work I take on covers firmware architecture, driver work, LoRa and mesh, low-power design, and getting a team onto Zephyr without the first-month pain. Happy to talk through where the line falls for your hardware — get in touch.
We open-sourced our Antarctic wave buoys
For over a decade I built and maintained the firmware for the Waves In Ice buoys — instruments that get deployed onto Antarctic sea ice, freeze in, and keep reporting wave motion back over Iridium satellite for months at a time. The data from the earlier generations contributed to peer-reviewed science, including a 2014 paper in Nature on storm-induced sea-ice breakup.
That work is now open source as WII5: wii5.sh3d.com.au — AVR firmware, schematics, PCB designs, enclosure notes, and the on-board wave-statistics mathematics, along with deployment logs going back to 2012.
Why open it up?
The honest answer is the bus factor. These buoys have outlived research grants, hardware generations, and more than one institution’s involvement. The knowledge of how they work — why the power budget is shaped the way it is, how the IMU maths turns accelerations into wave spectra on an 8-bit microcontroller, what actually fails in sea ice — lived in too few heads.
Publishing everything means the next research group doesn’t start from zero. They can read the firmware source, see a decade of deployment history, and build on hardware that has already survived the worst environment we could find for it.
What’s in it for an engineer?
Even if you never go near sea ice, the interesting bits are the constraints: ultra-low-power AVR design that runs for months on a fixed battery budget, on-board signal processing because satellite bandwidth is too expensive to ship raw data, and mechanical design that survives being frozen into moving pack ice.
If any of those problems sound like yours — remote monitoring, satellite telemetry, power-constrained firmware — that’s exactly the kind of work I consult on.