Engineering field note

Control systems for robotics.

Notes from building a networked robotic control system: turning vendor constraints into code, tracing timing across the wire, and verifying behaviour on a bench.

The system is the path, not the executable.

A command is not real when a function returns. It is real after crossing threads, queues, translations, bridges, devices, and the feedback path. I debug and verify that path as one thing.

  1. 01

    Intent

    What must happen, what must never happen, and which constraint decides the difference?

  2. 02

    Control

    Which state and which thread own the decision, its timing, and its failure response?

  3. 03

    Translation

    How do commands, observations, and faults cross software boundaries without changing meaning?

  4. 04

    Device

    What actually travelled over UDP and CAN, and what did the physical equipment do with it?

  5. 05

    Evidence

    Can another engineer reproduce the run, inspect the same boundary, and reach the same conclusion?

Working rule If a boundary can change behaviour, it needs both a contract and an observation point.

How I approach the work.

These are not separate phases. On a difficult integration problem I move between them until the design, the software, and the evidence agree.

  1. 01

    Interpret

    Read operational requirements, vendor protocols, and bench behaviour closely enough to find assumptions that have not yet been made explicit.

    Output: state definitions, safe defaults, and testable acceptance conditions.

  2. 02

    Structure

    Give timing, state, queues, and protocol translation one owner each, a thread with a known priority and a queue with a known depth, so that a local change cannot quietly alter the whole control path.

    Output: bounded SCHED_FIFO threads, single-producer queues, allocation-free hot paths, narrow interfaces, reviewable design decisions.

  3. 03

    Investigate

    Observe the same run in code and on the wire, monotonic-clock traces on one side and tshark or candump on the other, compare repeated trials, and change one variable at a time instead of tuning by impression.

    Output: stage timelines, p50/p95/p99 comparisons, pcap and SocketCAN evidence, and a narrower hypothesis.

  4. 04

    Verify

    Pin the decision in automated tests, run the build under the sanitizers, exercise it through simulation and hardware, and state plainly what the evidence does not yet prove.

    Output: gtest regression suites, ASan/TSan-clean builds, bench records, open questions, and a handover another engineer can use.

Where the method mattered.

N-01

A safe shutdown was not just another command.

The actuator documentation required a particular power-down sequence, but the controller did not enforce it. I added a configuration-gated shutdown path: an explicit state machine in the control thread, with a guard and a bounded wait on every transition.

FromEventToGuard / timeout
OPERATIONALshutdown requestedSHUTDOWN_PENDINGmotion stopped, confirmed by feedback
SHUTDOWN_PENDINGstep acknowledgedDISABLEDbounded wait per step; timeout raises a fault
any statefault detectedFAULTEDimmediate; never runs the orderly sequence

Fault shutdown stays a separate path because disabling a brakeless arm changes the hazard: remove drive power and gravity takes over, so "disabled" is not "safe", and the fault response must not share code with the orderly sequence. Then I checked the actual frames on a bench: gtest pins the transitions, packet captures confirm the frames leave in the documented order. The useful result was not only the feature; it was making the assumptions, exception, and evidence visible.

Evidence: state-transition tests, fault-path tests, packet captures, bench handover.

N-02

One latency number hid the problem.

Early timing work treated communication delay as a single result. I instrumented the path instead: every command carries a trace, stamped from one monotonic clock at each boundary it crosses.

using steady_ns = std::chrono::steady_clock::time_point;

struct CmdTrace {      // rides with every command; no allocation on the hot path
  uint64_t  seq;       // echoed by the device; joins host trace to the capture
  steady_ns created;   // control decision taken
  steady_ns enqueued;  // handed to the transmit thread
  steady_ns sent;      // sendmsg() returned; SO_TIMESTAMPING gives the wire time
  steady_ns received;  // feedback datagram arrived
  steady_ns decoded;   // protocol translation done
  steady_ns applied;   // state estimate updated
};

The sequence number joins the host trace and the packet capture into one timeline: tshark gives the per-hop wire times, the struct gives the per-stage host times, and disagreement between the two is itself a finding. One experiment improved the median while making part of the tail worse, the signature of batching: amortised per-packet cost pulls p50 down while queue occupancy under bursts stretches p99. Seeing p50, p95, and p99 per stage stopped us declaring victory on the wrong metric.

Evidence: monotonic timestamps, repeated-run comparison, wire correlation, percentile analysis.

N-03

A twin is useful only if it respects the real boundary.

Physical hardware was shared and slow to iterate against. I connected the production controller to a physics model over the same UDP boundary used by the device path: the twin binds the same socket interface, decodes the production command frames, steps a rigid-body model at a fixed dt, and answers with observation packets on the production schema. At the protocol layer the controller cannot tell the difference, so integration and regression runs exercise serialization, timing, and state handling rather than a mocked function call. The twin deliberately does not model bus arbitration or device firmware behaviour; those claims still belong to the bench, and saying so keeps the evidence honest.

Evidence: production protocol round-trip, shared geometry, integration tests, repeatable off-bench runs.

What transfers, and what does not.

I have not built train-control software. I have built software that controls physical equipment, speaks to vendor devices, fails in consequential ways, and has to be debugged from evidence. That is the relevant overlap, and the distinction matters.

What carries over

The habits used to understand and change an operational system.

Follow behaviour end to end. Make ownership and failure semantics explicit. Validate on the real interface. Separate what a test proves from what still needs a bench. Leave evidence that survives the person who ran the investigation.

What is new

The others domain, its operating rules, and its particular technology stack.

I would need to learn the context rather than pretend robotics is the same thing. My starting point would be one real operational path: understand its requirements and interfaces, reproduce its behaviour, then make small changes behind testable boundaries.