Skip to content
Husain Bootwala
03Shipped

Spacecraft TC/TM Link

A two-sided EGSE bench for a satellite power distribution unit

Bit-field map of the 4-byte SET_LOAD telecommand and the 8-byte TM_LOAD_STATUS telemetry frame. Every byte is a labelled cell carrying its offset, encoding and example value; the current and bus-voltage fields span two bytes each. Byte 3 of each frame fans out into its eight bits: the State bit alone in the telecommand, State and OCP together in the telemetry, the rest reserved and zero.
Diagram · Frame layout: every field, byte offset and bit position in both frames
Channel
03
Period
Apr 2026
Domain
Software
Role
Contract engineer via Habzeno: test-campaign plan, protocol design, both endpoints, fault scenarios, containerised harness

Stack

  • Python
  • Binary protocol design
  • Sockets
  • Docker Compose
  • CSV telemetry logging
Telecommand frame
4 bytes
Telemetry frame
8 bytes
Scenarios covered
Nominal, OCP fault, shutdown

The problem

Hardware and ground software are never ready together, and the key fault can’t be triggered safely.

  1. Two schedule lanes: ground software is written early, the flight power unit arrives much later, and the software waits in betweenWaitingGround softwareWrittenTestedFlight power unitNot built yetArrivesTime
    BeforeGround software waits for a power unit that isn’t built yet.
  2. A ground client and a power-unit simulator joined by a fixed TC/TM link; either side can later be swapped for the real ground segment or the flight unitTC/TM over TCPGroundclientPDUsimulatorTC 4 BTM 8 BSwapSwapOps groundFlight PDU
    AfterTwo halves, each standing in until the real one arrives.
Bus voltage floor
26.5 V
Current ceiling
2.2 A
Gated test phases
6
Payload channels planned
4

How the ground decides

  1. The ground segment sends four telecommand bytes up to the power unit and receives eight telemetry bytes back; nothing else crosses the linkPower unit, in orbitNo debuggerGround segment10010280TC4 B up2081028007D06D60TM8 Bdown
    ProblemNo debugger in orbit: four bytes up, eight back.
  2. Two inputs to the client: the command it sent is crossed out and ignored; the returned telemetry is what sets its belief that the load is onSentSET_LOAD ONIgnoredTelemetrySTATE = ONClient believesLOAD ONFrom TM only
    FixBelief comes from telemetry, never from the command sent.
  3. An eight-byte fault frame: its status byte reports the load on with overcurrent protection tripped, and its voltage bytes decode below the 26.5 volt floor; both checks fail and the verdict is shutdownTM frame208102C007D06758Status bitsON + OCP = 1FailBus voltage26.456 VFloor 26.5 VVerdictShutdown
    FixA self-contradicting fault frame fails two checks.
  4. One docker-compose command runs the nominal, fault and shutdown scenarios, and each one writes its decision as a row in a CSV log$ docker-compose upNominalONFaultON + OCPShutdownOFFlogs/test.csvKEEP_ONSHUTDOWNOFF_CONFIRMED
    ResultOne command, three scenarios, every decision logged.

Now try it yourself: step through the real frames, or type your own.

Interactive · Ground segment

One command, one reply, and the decision the ground has to make

The real 4-byte telecommand and 8-byte telemetry frame from this project, assembled and decoded field by field. Step through it.

Ground · EGSE

Telecommand · 4 bytes

  1. ——0
  2. ——1
  3. ——2
  4. ——3

Spacecraft · PDU

Telemetry · 8 bytes

  1. ——0
  2. ——1
  3. ——2
  4. ——3
  5. ——4
  6. ——5
  7. ——6
  8. ——7

01 / 41

logs/test.csv

timestampbus_voltage_vcurrent_aocpdecisionreason

The timestamp column is left blank because this is a reconstruction. Inventing a time would put one fabricated value on an otherwise real page.

Your turn: feed it a telemetry frame

Eight bytes, as if the PDU had just sent them. Pick any byte to see what it carries and change it, in any order. Nothing runs until you press Decode & decide: then the ground decodes the frame, applies the same limits, and issues whatever telecommand follows.

  1. 0
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7. 6
  8. 7

Select a byte to see the field it carries. Arrow keys move between bytes; up and down change the value.

Eight bytes. Spaces optional.

Load a frame to start from

Full transcript, all 41 steps
Act 1

Nominal

21 steps
  1. ground

    Act 1 · Nominal

    The ground commands the load on and the PDU reports a healthy bus. This is what agreement between the two sides looks like.

  2. ground

    Byte 0 · APID

    The first byte says who this packet is for. A spacecraft runs dozens of processes and the router has to make that decision without opening the payload, so identity comes before content.

  3. ground

    Byte 1 · Command ID

    Which command it is. 0x01 is SET_LOAD, and it is what tells the PDU how to read the remaining two bytes. Get this byte wrong and everything after it is misinterpreted rather than rejected.

  4. ground

    Byte 2 · Load ID

    Which switched output to act on. Fixed at 2 here, but a full byte wide because a real power distribution unit has many, and addressing them is not something you want to redesign later.

  5. ground

    Byte 3 · one bit of payload

    The command carries its entire payload in bit 7: 1 means ON. Shifting a 1 up seven places gives 0x80.

  6. ground

    Byte 3 · the seven reserved bits

    Bits 6 to 0 are reserved and always zero. Spending seven bits to carry one looks wasteful until you need to decode a frame with no parser and no room for ambiguity: a fixed layout is readable by inspection, and the spare bits cost nothing until a future flag needs them.

  7. ground

    Telecommand ready

    Four bytes, fully determined, no length field and no delimiter. Both ends already agree a SET_LOAD is exactly 4 bytes, so the frame does not need to describe itself.

  8. link

    Uplink

    The bytes go out over a TCP socket. What arrives at the other end is a stream of bytes with no message boundaries in it. The frame is a fiction both ends agree to maintain.

  9. spacecraft

    Three bytes arrive, and the reader blocks

    TCP delivered three bytes, not four. A single read returns whatever happened to arrive, so the receiver loops until it has the count it asked for. Treating one read as one message is the most common way to get this wrong, and it fails intermittently rather than immediately.

  10. spacecraft

    Frame complete

    The fourth byte arrives and the read returns. Only now does the PDU have something it is allowed to call a telecommand.

  11. spacecraft

    The PDU decodes it

    Shift bit 7 down, mask off the rest: state = 1. The PDU switches load 2 on and measures the result.

  12. spacecraft

    Telemetry header

    A different APID for the downlink, and a TM ID of 0x81, LOAD_STATUS. That second byte is the promise that bytes 4 to 7 are a current and a voltage; without it the numbers are unlabelled.

  13. spacecraft

    Byte 3 · two flags, one byte

    State in bit 7, OCP in bit 6, combined with an OR. Here the byte is 0x80: the load is on and protection has not tripped. Packing status as bits rather than bytes saves downlink bandwidth, which is the scarcest resource a spacecraft has.

  14. spacecraft

    Bytes 4-5 · current

    Two bytes, high byte first, because the convention is big-endian. 0x07 0xD0 is 2000, and the unit is milliamps, so 2.0 A. Integers rather than floats: the wire has no decimal point, and milli-units give a thousandth of resolution inside a 16-bit range.

  15. spacecraft

    Bytes 6-7 · bus voltage

    Same encoding: 28000 mV, or 28.0 V. A 16-bit millivolt field tops out at 65.5 V, which is comfortable headroom over a 28 V bus while still resolving a single millivolt.

  16. link

    Downlink

    Eight bytes back the other way. The ground reads all eight before acting on any of them, for the same reason the PDU read all four.

  17. ground

    What the ground now believes

    State 1, OCP 0, 2.0 A, 28.0 V, decoded from the frame rather than the command that provoked it. That distinction carries the design: an acknowledged telecommand is not an executed one, so the ground never updates its model of the spacecraft from what it sent.

  18. ground

    Check 1 of 3 · Bus voltage

    28.0 V clears the 26.5 V floor.

  19. ground

    Check 2 of 3 · Load current

    2.0 A against a 1.8–2.2 A window. The window has a floor as well as a ceiling: a load drawing far less than expected is as much a fault as one drawing too much.

  20. ground

    Check 3 of 3 · OCP flag

    Overcurrent protection has not tripped.

  21. ground

    Verdict · KEEP_ON

    Every limit holds, so the load stays on and the row is written: "all nominal". The log is the deliverable. You cannot go and look at a spacecraft, so an unrecorded decision may as well never have been made.

Act 2

Fault — OCP tripped

11 steps
  1. ground

    Act 2 · Fault — OCP tripped

    The same command, a very different answer: the load still reports ON while its overcurrent protection has tripped and the bus is sagging.

  2. ground

    The same four bytes again

    An identical telecommand: same APID, same command, same load, bit 7 set. Nothing about the request has changed, which is what makes the answer interesting.

  3. link

    Up and decoded

    Same four bytes, same read-until-you-have-four on the far side. The PDU switches load 2 on and measures the result.

  4. spacecraft

    Byte 3 · a frame that contradicts itself

    Both flags are set, so the status byte is 0xC0. The load claims to be ON while its overcurrent protection has tripped. Nothing in the protocol prevents a frame from carrying a contradiction, which is why the ground checks more than one field before believing it.

  5. spacecraft

    The measurements

    2.0 A at 26.456 V. The current is unremarkable; the bus has sagged below where the ground will tolerate it. Encoded the same way as before, since the layout does not change because the news is bad.

  6. link

    Down and decoded

    Eight bytes back to the ground, read the same way.

  7. ground

    What the ground now believes

    State 1, OCP 1, 2.0 A, 26.456 V, decoded from the frame rather than the command that provoked it. The command it sent has no vote here.

  8. ground

    Check 1 of 3 · Bus voltage

    26.456 V is under the 26.5 V floor by 44 millivolts, the kind of margin a limit exists to catch.

  9. ground

    Check 2 of 3 · Load current

    2.0 A, still inside the window. The current is the one thing about this frame that looks entirely normal.

  10. ground

    Check 3 of 3 · OCP flag

    The OCP flag is set. It is independent of the voltage reading and catches the same fault a second way, which is why the frame carries both.

  11. ground

    Verdict · SHUTDOWN

    Two independent limits failed, and the reason string carries both: "voltage 26.456V < 26.5V; OCP flag set". The ground commands the load off without asking permission or waiting for an operator. That decision is what causes act 3.

Act 3

Shutdown confirmed

9 steps
  1. ground

    Act 3 · Shutdown confirmed

    This act happens only because the ground decided it should. The load reports OFF and the bus recovers.

  2. ground

    One bit changes

    The same frame with bit 7 cleared. Byte 3 goes from 0x80 to 0x00, and that single bit is the entire difference between switch on and switch off.

  3. link

    Up and decoded

    Same four bytes, same read-until-you-have-four on the far side. The PDU switches load 2 off and measures the result.

  4. spacecraft

    Byte 3 · the load reports off

    Bit 7 is clear: the load is off. The status byte is 0x00, and that is positive evidence the shutdown took effect, rather than a record that it was sent.

  5. spacecraft

    The measurements

    0.0 A at 28.0 V. No current, and the bus has recovered to its nominal value now the load is off.

  6. link

    Down and decoded

    Eight bytes back to the ground, read the same way.

  7. ground

    What the ground now believes

    State 0, OCP 0, 0.0 A, 28.0 V, decoded from the frame rather than the command that provoked it. The command it sent has no vote here.

  8. ground

    Not evaluated, confirmed

    The limits are not applied here, on purpose. An idle load draws 0 A, which would fail the current window and be logged as a fresh fault. The ground commanded this shutdown, so the row records a confirmation instead. Re-evaluating your own instruction turns a success into an alarm.

  9. ground

    Verdict · OFF_CONFIRMED

    The load reports OFF and the bus has recovered. The row is logged as a confirmation: the ground now has evidence that the shutdown it ordered took effect, which is a different thing from having sent the command.

Where a bench like this gets used

In a test campaign, which I planned first.

Part 1 · The test campaign it serves

  1. The planned test rig: a 28 volt supply and an EGSE computer drive the power unit under test, which feeds four 14 ohm dummy loads; a fault relay sits in the L2 lineSupply28 VEGSE PCScriptTCPPDUUndertestL1 · 14 ΩL2 · 14 ΩL3 · 14 ΩL4 · 14 ΩRelayPayloads not yet integrated
    setupFour dummy loads, and a relay to short L2.
  2. Six gated test phases from pre-test checks to power-down; phase three shorts L2 on purpose, and overcurrent protection must trip or the bench powers off0Pre-test checks1Power on + housekeeping2L1 → L4, one at a time3Short L2 on purpose4Re-verify every load5Power down, archiveOCP musttripElse: poweroffGatebetweeneach
    sequenceSix gated phases; the short must trip protection.
  3. Estimated campaign time: about 35 to 48 minutes as planned, about 27 to 32 minutes optimised, with the same test stepsCampaign time, estimatedAs planned35–48 minOptimised27–32 min01020304050Minutes
    ResultFaster by polling, not by skipping tests.
Read the engineering detail4 min read

Why the constraint shapes everything

Spacecraft do not have a debugger attached. Every interaction with a subsystem in flight is a command sent up and a telemetry frame that comes back, and the ground segment’s entire job is to decide, from those bytes alone, whether what just happened was acceptable.

That sets the rest. Frames are small and rigidly laid out, the failure modes have to be reachable on the ground, and every decision has to be recorded because you cannot go and look.

What I built

Two endpoints and the protocol between them.

The PDU simulator stands in for a satellite power distribution unit. It accepts load-control telecommands and answers with a status telemetry frame, including the case where overcurrent protection has tripped and the frame comes back reporting the load ON and its protection TRIPPED at the same time.

The ground client encodes commands, decodes the response, evaluates the reported state against safe operating limits (bus voltage at least 26.5 V, current between 1.8 and 2.2 A, OCP flag clear), and writes its verdict to a log.

Three scenarios run end to end: a nominal load enable, a fault where the telemetry contradicts itself and has to be caught on two independent checks, and a commanded shutdown that has to be positively confirmed rather than assumed.

The rule the client is built around

The socket code is the boring half. The design rule underneath it is that the client must never assume its command worked. A telecommand that is acknowledged is not a telecommand that was executed, and a system built on that assumption fails silently in the situation where you can least afford it.

So the client’s model of the spacecraft is only ever updated from returned telemetry, and never from what it just sent. The confirm-gates in my robotics work come from the same principle: the state of the world is whatever the sensor reports, not whatever you told it to be.

Where it fits in integration

This is EGSE, electrical ground support equipment: two tools built from the same protocol pointed in opposite directions.

During spacecraft assembly, integration and test, the flight hardware and the ground software are almost never ready at the same time. The subsystem simulator lets ground software be written and exercised months before a real power distribution unit exists. The ground client does the reverse: when the PDU arrives on the bench, it can be commanded and verified before anyone connects it to the real mission control stack. On a FlatSat, the integration bench where subsystems are laid out flat and wired over their real interfaces, both halves run at once.

The fault scenario is where the bench earns its keep. You cannot ask an expensive power unit to trip its overcurrent protection on demand just to check that the ground reacts correctly. A simulator can produce that frame on every run, which makes the failure path testable instead of hypothetical. The same applies to hardware-in-the-loop work on the power system itself, and to procedure rehearsal, where operators practise responding to a fault nobody wants to stage for real.

The CSV earns its place too. In a verification campaign the artifact you hand over is evidence that a decision was made, on what data, and at what time, which is what that file records.

The test campaign (Part 1)

Before the code, I planned the campaign that would verify regulated power to four payload channels, L1 to L4, with the PDU on the bus and payloads not yet integrated:

  • Rig. A current-limited 28 V supply, four 14 Ω load simulators (28 V ÷ 2 A), an EGSE computer on TC/TM over TCP, and a solid-state relay on L2 behind a hardware key-switch interlock.
  • Sequence. Six phases, each behind a gate: pre-test checks, power-on and housekeeping, each load enabled and disabled in turn, a deliberate short on L2, re-verification of every load, then power-down. If overcurrent protection does not trip, the bench powers off.
  • Optimisation. Polling telemetry continuously and combining each load’s enable and disable cycle cut the estimate from about 35–48 minutes to about 27–32. Two faster options were rejected: testing loads in parallel hides which load caused an anomaly, and skipping re-verification after the fault ignores what the short may have done to the other channels.
  • Feedback to the client’s systems engineering. Their brief left the overcurrent reset behaviour, the fault-injection method and the idle-current limit undefined. I raised each as an open item rather than guessing.

This was contract work, done through my company Habzeno for a satellite company that needed a bench to test its parts. The problem statement was theirs. The test plan, the protocol design, both implementations, the fault scenarios and the containerised harness are mine.

Media

Flowchart. A ground client and a PDU simulator, both built in this project, sit either side of a fixed TC/TM-over-TCP interface carrying a 4-byte telecommand up and an 8-byte telemetry frame down. Either can be replaced without the other changing: the client by an operational ground segment, the simulator by the flight PDU. Below, the three benches this yields: ground software first, hardware bring-up, and FlatSat or hardware-in-the-loop.
Diagram · Where the bench sits during integration
The committed logs/test.csv rendered as a table with its six columns: timestamp, bus voltage, current, OCP flag, decision and reason. Row one is 28.0 V, 2.0 A, OCP 0, KEEP_ON, all nominal. Row two, highlighted, is 26.456 V, 2.0 A, OCP 1, SHUTDOWN, voltage 26.456V below 26.5V and OCP flag set. Row three is 28.0 V, 0.0 A, OCP 0, OFF_CONFIRMED, shutdown command executed.
Screen capture · The decision log the client wrote: three frames, three decisions
Video · One command, both endpoints, all three scenarios

How this was built

The spec sheet above lists the stack. This section covers how the work was run, which a screenshot cannot show you.

Where AI did the work

  • Both endpoints generated from one written frame spec.
  • Fault scenarios treated as the definition of correct.

The discipline around it

  • One Docker Compose command, no host dependencies.
  • Every decision logged to CSV for audit.

What stayed human

  • Safe limits are hand-specified. The client applies them, never infers them.