There’s a part of embedded software work that almost nobody sees: making the system work when the hardware isn’t around.
The code compiles, the unit tests pass, but the system only fully exists when the sensors, controllers and data concentrators are connected. And they’re almost never on your desk.
That’s the context where I worked on simulators and relays. I won’t go into architecture, code or business rules, because that isn’t mine to share. But I can talk about what I did, the skills I developed and what I learned.
This post is the behind-the-scenes story of the Simulator & Relay project, which brings the simulator and the relay together.
The context
The projects involved software that had to talk to field equipment over the network. Each device had its own protocol, its own connection lifecycle and its own way of failing. Testing that with real hardware was expensive, slow and limited: the bench can’t reproduce every scenario, isn’t available all the time and doesn’t produce the bad case you need the next day.
The first instinct was to reuse real capture data. But the software under test doesn’t read a capture file; it reads bytes arriving on a socket. Between one thing and the other sits the entire protocol: header, length, field order, number format, checksum, heartbeat and the right time to open and close the connection.
A traditional mock doesn’t solve this. A mock tests a function. What I needed was a behavior double: something that accepted connections, kept state, responded to heartbeats and could fail on purpose.
My role
I worked as an independent contractor, in a consulting arrangement, with technical autonomy to lead the development tracks. In practice, that meant:
- understanding device behavior and turning it into requirements;
- implementing the simulators and relays;
- validating with real data and error scenarios;
- handling the build, packaging and documentation;
- supporting integration and debugging when something didn’t line up.
A big part of the work was communication. I had to explain what the tool did, which scenarios it covered and which ones it didn’t. Clear documentation and error messages saved hours of support.
What I built
Two complementary tracks: a simulator, which stood in for the devices and replayed data in the expected format, and a relay, which sat in the middle to distribute traffic and allow inspection without interfering with the producer.
At different times, I used two stacks. The first was C++ with SDL2 and ImGui, chosen because it produced a native executable with no heavy runtime and could run on modest machines. The second was Rust for the core with Flutter for the UI, for a more testable base and a more comfortable interface to configure.
I won’t detail the architecture of either one. What I can say is that both followed the same idea: separate the protocol from the interface, test the data format without opening a socket and keep the tool observable.
Traditional mock
- answers a call and stops
- keeps no state
- has no concept of time
- doesn’t fail on purpose
Behavior simulator
- accepts connections and keeps state
- respects time and order
- reacts to heartbeat and disconnects
- fails on purpose
Skills and stack
| Layer | What I used |
|---|---|
| Languages | C++, Rust, Dart |
| Interface | SDL2 + ImGui, Flutter |
| Networking | TCP, UDP, HTTP |
| Concurrency | threads, async/await, Tokio, ASIO |
| Integration | FFI, flutter_rust_bridge |
| Build | CMake, cross-compile, packaging |
| Practices | automated tests, protocol validation, documentation, observability |
I also worked with compatibility for old operating systems, offline environments, 32-bit machines and hardware without graphics acceleration. That changes how you choose dependencies, package and test. It’s not just code: it’s product.
How I approached the problem
Understand the device behavior.
Before writing any line, I needed to know what the device did, in what order, and what happened when something went off script.Reproduce the protocol and the connection lifecycle.
Building the data wasn’t enough. I had to open connections, keep state, answer heartbeats and close at the right moment.Validate with real data.
Field captures were the reference. They showed what the device actually sent, not just what the documentation said.Create the bad scenarios.
Incomplete data, values out of range, dropped connections, unexpected behavior. That’s where the tool stops being a convenience and becomes engineering.Make the tool observable.
Status, counters and clear logs save a lot of debugging time. Whoever uses the tool needs to understand its state without opening several terminals.Package and document.
A tool that only runs on the author’s machine doesn’t solve the team’s problem. Build, package and instructions are part of the delivery.
The challenges that stuck with me
Simulating behavior, not just data
The first mistake beginners make is thinking simulation is about building the payload. The hard part is the lifecycle: when the connection opens, how long it stays open, when it closes, what happens if the other side drops and how the device reacts to a heartbeat. A perfect packet on the wrong connection is useless.
Time and concurrency
Multiple channels, delays, simultaneous events and the need to reproduce the same scenario more than once. I learned to prefer deterministic randomness: variation without losing reproducibility. If a case fails, it needs to fail again.
I also learned that time is part of the protocol. A counter that wraps around, a clock difference or an extra delay changes the system’s behavior. You can’t treat time as a detail.
Compatibility
A testing tool has to run in the environment where the team tests, not just on yours. Old operating systems, little memory, no GPU, no internet, sometimes no admin rights. Build, packaging and error messages are part of the delivery.
Validation
Testing the data format with known cases was one of the biggest wins. If you can compare the expected bytes without starting a server, the error shows up in seconds, not hours. I also learned to separate the protocol from the network: the network is a thin shell around pure functions.
Observability
The tool has to show what’s happening. Status, counters, per-channel logs and the next expected action save a lot of console time. When something goes wrong, you want to look at the screen and understand the state.
Bad scenarios
Simulating the happy path is easy. The value is in producing the invalid case, the incomplete data, the dropped connection and the unexpected behavior. That’s when the simulator stopped being a convenience and became an engineering tool.
Outcome
The gain was autonomy. I could test scenarios that real hardware doesn’t produce on demand, reduce dependence on the bench and reach the field with fewer surprises. I also developed a more complete view of integration: protocol, network, interface, build and operations stop being separate boxes.
I can’t share the systems, the implementation details or the specific results. But I can share the approach, the technologies and what this experience taught me. That’s what this post does.
What I learned
- Simulating a device is about simulating behavior, not just payload. Connection, state, heartbeat and order are part of the protocol.
- Time is data. A counter that wraps around, a different clock and an extra delay change the result.
- Testing the data format with known cases catches errors that an integration test would take hours to find.
- Deterministic randomness is the best of both worlds: variation without losing reproducibility.
- The interface is part of the tool. Seeing state, counters and logs saves console time.
- Compatibility is part of the product. The build and the package need to work in the team’s environment.
In the end, the lesson is simple: if the hardware can’t be on your desk, build a double that behaves like it. Not to replace the real test, but to reach it with fewer surprises.