A C# WinForms application that simulates control software for a medical device, communicating over a real USB serial connection with a Flipper Zero acting as simplified test hardware. Not real clinical software.
|
A treatment mid-run, connected to |
A full run to completion. The event log shows the entire |
- C# / .NET application development, including WinForms desktop UI
- Serial hardware communication (
System.IO.Ports) with real USB hardware - Network hardware communication (
System.Net.Sockets) via a second transport implementation, including a standalone TCP device simulator - A custom line-based command/response protocol, designed and implemented from scratch, shared across both transports
- A workflow/state machine enforcing valid operation sequences independent of the UI
- Automated unit and integration testing, including test doubles for hardware-free testing
- Incremental, evidence-driven engineering: every architectural decision below was made only after observing real behavior, not assumed in advance
The solution is split into several projects, each with one narrow responsibility:
| Project | Responsibility |
|---|---|
MedDeviceSim.Communication |
Serial and TCP transports (SerialTransport, TcpTransport, ITransport), byte-to-line framing (LineReader), and the protocol layer (DeviceCommand, DeviceResponse) |
MedDeviceSim.Workflow |
TreatmentWorkflow - a pure, synchronous state machine with no I/O. Fully testable without hardware or a UI. |
MedDeviceSim.Session |
TreatmentSession - bridges the workflow to a real transport: sends commands, reads responses, feeds them back into the workflow, translates communication failures into safe state transitions |
MedDeviceSim |
The WinForms UI, built on top of the above. Reflects workflow state; does not itself enforce validity - that's the workflow's job |
MedDeviceSim.Simulator |
SimulatedDeviceServer - an independent, stateful implementation of the protocol over TCP, used to test and demonstrate the real application against something that actually speaks it (see Some limitations) |
MedDeviceSim.Simulator.Host |
A minimal standalone console app that runs SimulatedDeviceServer as its own process, for manual testing and demonstration against the live UI |
FlipperSerialExperiment / RawSerialExperiment |
Early diagnostic console apps used to observe real Flipper Zero serial behavior and debug a suspected System.IO.Ports issue before any reusable library code was written |
Dependency direction is strictly one-way: MedDeviceSim → MedDeviceSim.Session → MedDeviceSim.Workflow → MedDeviceSim.Communication. Nothing lower in that chain knows anything about the layer above it. TreatmentWorkflow, in particular, has zero knowledge that a UI, or even a real transport, exists.
Disconnected → Connected → PlanLoaded → Armed → Running → Complete
│ │
└── Stop ─┴──→ Stopped
any state (except Disconnected) ──Error──→ Fault
any state ──disconnect──→ Disconnected
Enforced entirely by TreatmentWorkflow (MedDeviceSim.Workflow), independent of the UI:
ARMis rejected unless a plan has been loaded.STARTis rejected unless the device is armed.LOAD_PLANis rejected while running.- A lost connection, meaning any communication failure and not just an explicit disconnect, immediately forces
Disconnected, regardless of prior state. - A device-reported error (the device replying while still reachable) forces a distinct
Faultstate instead, preserving the reason. This state is reserved specifically for the device telling us something is wrong, not for losing the ability to talk to it at all. Seedocs/state-machine.mdfor the full transition set.
A custom line-based (\r\n-terminated) protocol over the serial connection:
| Command | Response(s) |
|---|---|
CONNECT |
CONNECTED |
LOAD_PLAN <id> |
PLAN_LOADED <id> |
ARM |
READY |
START |
RUNNING, then PROGRESS <percent> (repeated), then COMPLETE |
STOP |
STOPPED |
GET_STATUS |
(not implemented anywhere yet; see Some limitations) |
| any | ERROR <reason> |
Unrecognized or malformed lines parse to a distinct Unknown response rather than throwing. Device output that doesn't match the protocol is treated as an expected possibility, not an exceptional one, since real hardware (see Some Limitations) routinely sends output the protocol doesn't define. See docs/protocol-spec.md for the full specification, including error format and framing rules.
Requires the .NET 10 SDK and Windows (the UI project targets net10.0-windows for WinForms).
dotnet build
dotnet test
Open med-device-sim.slnx in Visual Studio, or run the UI directly:
dotnet run --project MedDeviceSim
68 automated tests across the library projects, including pure logic (protocol parsing, state transitions, formatting) requiring no hardware, integration tests against FakeTransport (a controllable test double simulating timeouts, malformed lines, device errors, and disconnects), and integration tests against MedDeviceSim.Simulator's real, independently-implemented protocol-aware device over a real TCP socket.
Two additional tests are hardware-gated ([Fact(Skip = "requires a real Flipper Zero connected via USB")]) and are run manually, with the Flipper connected. See docs/test-procedures.md for exactly how to run these, what each test project covers, and the manual UI verification procedure.
The WinForms UI itself has been manually driven and verified against real hardware (via Windows UI Automation, not just visual inspection) for the connect/disconnect flow, event logging, and resource cleanup on close.
GET_STATUSis not wired up end-to-end.DeviceCommand.GetStatusexists, butTreatmentWorkflownever got a corresponding request method, since a status query doesn't fit the "valid from exactly one state" pattern the other commands share. Not yet resolved.- Formal design documentation is still in progress. See Documentation below for what exists so far.
Since the real Flipper Zero's stock CLI doesn't understand this custom protocol, the stock-hardware path can never move from Disconnected to Connected (CONNECT gets an unrecognized-command reply, not CONNECTED). What actually works with the Flipper is that the COM port opens, DTR gets asserted, and bytes move both directions. To get a full Connected → Complete run against something real, MedDeviceSim.Simulator implements the protocol statefully (SimulatedDeviceServer, hosted standalone by MedDeviceSim.Simulator.Host), paired with TcpTransport, a second ITransport implementation alongside SerialTransport. The WinForms UI now runs a complete workflow, including live PROGRESS updates, against this simulator over a real TCP socket.
Built as a portfolio/learning project with Claude as a guide to develop C#/.NET, Windows desktop, and serial/network hardware interface skills.
Lightweight, requirements-driven documentation, in progress:
docs/requirements.md: numbered requirements (REQ-NNN), extracted from behavior already implemented and tested, not aspirational.docs/protocol-spec.md: the command/response wire protocol, including framing, commands, responses, error format, and known gaps.docs/state-machine.md:TreatmentWorkflow's states and every transition, split into request validation vs. response-driven change.docs/architecture-decisions.md: why the codebase is layered and built the way it is, decision by decision.docs/test-procedures.md: how to run the automated suite, what each test project covers, and manual verification procedures (hardware-gated tests, UI against the simulator and real hardware).docs/traceability-matrix.md: every requirement mapped to the test(s) that verify it, plus the coverage gaps that fell out of building it (the UI layer has no automated tests).
- The Flipper Zero's USB CDC-ACM serial interface stays silent until the host asserts the DTR control line. This undocumented behavior was discovered empirically, not assumed.
System.IO.Ports.SerialPort's asyncReadAsync/WriteAsyncdo not reliably honorCancellationTokenon Windows in this environment. This was confirmed via an isolated diagnostic probe against real hardware, and worked around by falling back to the synchronousRead/Writemethods (which do respect their configured timeouts) wrapped inTask.Run.- The Flipper's CLI can be driven into an unresponsive state by rapid repeated connect/disconnect cycling, recoverable only by a physical power cycle. This was discovered during debugging, and is now documented as an operational constraint for future hardware testing rather than assumed to be a code bug.


