Back to Projects
Deutsche-Hydrapro's Delta brake actuator

TowPilot & Delta

May 2024 - Present Product Page
EmbeddedRTOSBLEESP32-C6Rust

TowPilot and Delta are trailer brake controllers for vehicles that were never wired for towing. The controller gets power, ground and the trailer’s two lamp lines, and works out braking from those lines and an IMU. Deutsche Hydrapro sells them and Aston Dynamics does the engineering. Neither has shipped yet: the TowPilot is about to enter a closed beta and the Delta is still being prototyped.

Task scheduling

Each task runs on the scheduler that fits its requirements. The control loop needs consistent timing, so it is a preemptive FreeRTOS task paced by the hardware, and other work can’t delay it. Work that mostly waits on I/O, like streaming logs to the app, runs as cooperative async tasks on a single executor. They don’t need their own stacks, so adding one is cheap. Cooperative tasks only switch at await points, so a blocking call would stall every task on the executor. Blocking work, like writing to flash, gets its own preemptive task instead. This is the same split I used on my wireless telemetry board.

I check the result with FreeRTOS runtime stats, which report each task’s CPU share and stack headroom. Sampling the stats pauses the scheduler, so they only run in bench builds. Production builds keep the measurements that are cheap to collect: loop timing, dropped ADC frames and free heap, logged to flash every minute. A late loop is logged as soon as it happens.

Control loop timing

The ADC samples continuously from its own hardware clock, and the control loop wakes each time a frame of samples is complete. Since the same clock drives sampling and the loop, every iteration gets exactly one fresh frame and the two never drift apart. If the loop runs late and a second frame is already waiting, it takes the newer one and drops the old one. A brake controller should act on current data, not catch up on stale data.

Shared code

I have pushed to keep core logic platform-independent since I started. When the controls engineer started on the braking math, I set its shape: a library with no hardware code in it, so it builds and is unit tested on a laptop without a board. The app’s native Rust library also runs under Node, so we test features with scripts before the UI for them exists, instead of by hand in the app or with UI automation like Maestro.

The original app protocol was a Google Doc of byte layouts over one RX/TX characteristic pair. That is easy to design but leaves the application to multiplex and dispatch every message. My first version kept that design, as I was asked to, in a Rust library shared by the firmware and the app through UniFFI. Once I took over the firmware and the app’s native code, I moved to one characteristic per message, which an intern I hired built under my review. Each device’s services are declared once, with a name and a Rust type for each characteristic. Macros turn that declaration into firmware that sets up the BLE stack to route each message, and inject serialization code so both ends get a type-safe API. Each UUID is a hash of the service definition, so mismatched firmware and app versions can’t misread each other, and services that haven’t changed stay compatible automatically. Adding a message means choosing a name and a type.

BLE OTA

A write with response allows only one write in flight. Each one waits at least a connection interval, plus however long the phone’s BLE stack takes to report back to the app. Writes without response let the phone stack buffer data and send several packets every connection interval, but they give the app no back pressure. I added back pressure at the application level. The phone keeps a window of data in flight, and the device acks as it buffers each packet. The window also keeps the phone’s queue short, so a brake command sent during an update doesn’t wait behind the whole image. A 2 MB image went from about 3 minutes with my first version to about 30 seconds in good conditions, including flash erase and write.

Radio and certification

As an intern I led RF and EMC certification research for the US, Canada and Australia/NZ. The original design, a bare microcontroller with a custom PCB antenna, would have needed certification as a new intentional radiator, which was prohibitively expensive. I selected the pre-certified ESP32-C6 over an ST module mainly for its software. Espressif’s wireless stack was more modern with more features and is built to run on ESP-IDF’s RTOS, which made it much easier to work with. I now choose the certification route for each product and am the company’s contact with the test lab. Our hand-held remote has passed testing, the TowPilot and Delta have not been tested yet.

Factory testing

I designed the factory test system and am now building it. Five to ten test rigs run in parallel, each connected over USB to a coordination server that bridges them to our cloud provisioning system and hosts a web UI for technicians. New boards run a small factory app that sends the board’s MAC address as UART on its brake-light output. With many boards advertising in the same room, this tells each rig which board is plugged into it. The rig then connects over BLE using the same protocol library as the app, installs production firmware, tests the board and configures it for its product and region. So far a rig can exchange messages with the coordination server, read a new board’s MAC address from its brake-light output, and connect to that board over BLE. The tests and provisioning are next. The same rig hardware will later run hardware-in-the-loop tests of the firmware in CI, as a separate project.