|
| 1 | +# PUS Native YAMCS-Core Architecture |
| 2 | + |
| 3 | +Architecture learnings for implementing PUS services natively in yamcs-core |
| 4 | +(i.e. YAMCS acts as the on-board service — no relay to a spacecraft required). |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## 1. YAMCS Commanding Pipeline |
| 9 | + |
| 10 | +``` |
| 11 | +Ground (REST / WebSocket) |
| 12 | + → CommandingManager.sendCommand() |
| 13 | + → CommandQueueManager.addCommand() |
| 14 | + → CommandQueue (HOLD / BYPASS) |
| 15 | + → CommandingManager.releaseCommand() |
| 16 | + → CommandReleaser.releaseCommand(PreparedCommand) |
| 17 | + → StreamTcCommandReleaser → TC stream → TcDataLink → spacecraft |
| 18 | +``` |
| 19 | + |
| 20 | +**Key interception point**: `CommandReleaser.releaseCommand()`. |
| 21 | +If a service implements `CommandReleaser`, it becomes the sole command releaser |
| 22 | +for the processor. Everything else is transparent. |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## 2. How to Implement a Native PUS Service |
| 27 | + |
| 28 | +### Pattern: Extend `StreamTcCommandReleaser` |
| 29 | + |
| 30 | +The cleanest approach is to subclass `StreamTcCommandReleaser` and override |
| 31 | +`releaseCommand()`: |
| 32 | + |
| 33 | +```java |
| 34 | +public class PusXxService extends StreamTcCommandReleaser { |
| 35 | + |
| 36 | + @Override |
| 37 | + public synchronized void releaseCommand(PreparedCommand pc) { |
| 38 | + byte[] binary = pc.getBinary(); |
| 39 | + if (binary != null && (binary[7] & 0xFF) == MY_SERVICE_TYPE) { |
| 40 | + handleLocally(pc); |
| 41 | + } else { |
| 42 | + super.releaseCommand(pc); // forward to TC stream as normal |
| 43 | + } |
| 44 | + } |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +Non-intercepted commands flow to the TC stream normally via `super.releaseCommand()`. |
| 49 | +Intercepted commands are handled in-process. |
| 50 | + |
| 51 | +### How to identify a PUS TC from raw binary |
| 52 | + |
| 53 | +``` |
| 54 | +binary[0..1] = CCSDS primary header word 0 (version + type + sh_flag + APID) |
| 55 | +binary[2..3] = sequence flags + sequence count |
| 56 | +binary[4..5] = packet data length |
| 57 | +binary[6] = PUS secondary header byte 0 (version + ack flags) |
| 58 | +binary[7] = service type |
| 59 | +binary[8] = service subtype |
| 60 | +binary[9..10] = source ID |
| 61 | +binary[11+] = application data |
| 62 | +``` |
| 63 | + |
| 64 | +`APP_DATA_OFFSET = 11` |
| 65 | + |
| 66 | +### TC relay during sequence execution |
| 67 | + |
| 68 | +To release an embedded TC (raw bytes) back through the normal pipeline: |
| 69 | + |
| 70 | +```java |
| 71 | +CommandId cmdId = CommandId.newBuilder() |
| 72 | + .setCommandName("/pus21/relay") |
| 73 | + .setOrigin("pus21-sequencer") |
| 74 | + .setSequenceNumber(relaySeqCounter.getAndIncrement()) |
| 75 | + .setGenerationTime(processor.getCurrentTime()) |
| 76 | + .build(); |
| 77 | +PreparedCommand relayPc = new PreparedCommand(cmdId); |
| 78 | +relayPc.setBinary(rawTc); |
| 79 | +relayPc.setRaw(true); // skip post-processing |
| 80 | +super.releaseCommand(relayPc); // goes to TC stream → datalink |
| 81 | +``` |
| 82 | + |
| 83 | +**Limitation**: bypasses YAMCS `CommandingManager` (no command queue, no verifiers). |
| 84 | +Appropriate for sequence-released commands that are "best effort fire-and-forget". |
| 85 | + |
| 86 | +--- |
| 87 | + |
| 88 | +## 3. Injecting TM Responses into YAMCS |
| 89 | + |
| 90 | +To inject TM back from a native service (so YAMCS decodes it via MDB and parameters |
| 91 | +appear in the archive), emit a Tuple directly on the realtime TM stream. |
| 92 | + |
| 93 | +### Get the TM stream |
| 94 | + |
| 95 | +```java |
| 96 | +String yamcsInstance = proc.getInstance(); |
| 97 | +YarchDatabaseInstance ydb = YarchDatabase.getInstance(yamcsInstance); |
| 98 | +for (StreamConfig.TmStreamConfigEntry entry : StreamConfig.getInstance(yamcsInstance).getTmEntries()) { |
| 99 | + if ("realtime".equals(entry.getProcessor())) { |
| 100 | + tmStream = ydb.getStream(entry.getName()); |
| 101 | + break; |
| 102 | + } |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +### Emit a TM Tuple |
| 107 | + |
| 108 | +The TM stream expects tuples with these columns (`StandardTupleDefinitions.TM`): |
| 109 | +- `gentime` (TIMESTAMP / long) |
| 110 | +- `seqNum` (INT / int) — CCSDS sequence count |
| 111 | +- `rectime` (TIMESTAMP / long) |
| 112 | +- `status` (INT / int) — 0 = OK |
| 113 | +- `packet` (BINARY / byte[]) — full raw packet bytes |
| 114 | + |
| 115 | +```java |
| 116 | +int seqCount = ((pkt[2] & 0x3F) << 8) | (pkt[3] & 0xFF); |
| 117 | +TupleDefinition td = StandardTupleDefinitions.TM.copy(); |
| 118 | +Tuple t = new Tuple(td, new Object[]{ |
| 119 | + now, // gentime |
| 120 | + seqCount, // seqNum |
| 121 | + now, // rectime |
| 122 | + 0, // status |
| 123 | + pkt // raw packet bytes |
| 124 | +}); |
| 125 | +tmStream.emitTuple(t); |
| 126 | +``` |
| 127 | + |
| 128 | +**Note**: injecting via Tuple bypasses `PacketPreprocessor`. The gentime must be |
| 129 | +set by the service. The raw packet bytes still go through MDB extraction for |
| 130 | +parameter decoding. |
| 131 | + |
| 132 | +### Building a PUS TM Packet |
| 133 | + |
| 134 | +PUS TM packet structure: |
| 135 | + |
| 136 | +``` |
| 137 | +[0..5] CCSDS primary header (6 bytes) |
| 138 | +[6] PUS secondary header: 0x21 (version=2, scRefStatus=1) |
| 139 | +[7] service type |
| 140 | +[8] service subtype |
| 141 | +[9..10] message type counter (uint16, big-endian) |
| 142 | +[11..12] destination ID (uint16, big-endian, 0 = ground) |
| 143 | +[13+] absolute time (CUC format, see timeEncoding config) |
| 144 | +[13+T+] application data |
| 145 | +[-2..-1] CRC-16-CCIIT |
| 146 | +``` |
| 147 | + |
| 148 | +Use `CucTimeEncoder` (same class as `PusCommandPostprocessor`) to encode time. |
| 149 | +Apply `CrcCciitCalculator.compute(pkt, 0, totalLen - 2)` for the CRC. |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## 4. Command History Integration |
| 154 | + |
| 155 | +The native service should publish to command history to keep audit trails: |
| 156 | + |
| 157 | +```java |
| 158 | +// Ack that the TC was "sent" (received and accepted by the service) |
| 159 | +commandHistory.publishAck(pc.getCommandId(), AcknowledgeSent_KEY, |
| 160 | + processor.getCurrentTime(), AckStatus.OK); |
| 161 | + |
| 162 | +// Report completion (success or failure) |
| 163 | +commandHistory.publishAck(pc.getCommandId(), CommandComplete_KEY, |
| 164 | + processor.getCurrentTime(), AckStatus.OK /* or NOK */, errorMsg /* nullable */); |
| 165 | +``` |
| 166 | + |
| 167 | +Override `setCommandHistory(CommandHistoryPublisher chp)` to receive this reference. |
| 168 | +`StreamTcCommandReleaser.setCommandHistory()` is a no-op by default. |
| 169 | + |
| 170 | +--- |
| 171 | + |
| 172 | +## 5. Processor Configuration |
| 173 | + |
| 174 | +Replace `StreamTcCommandReleaser` with the native service in the processor config: |
| 175 | + |
| 176 | +```yaml |
| 177 | +# In processors.yaml (or in yamcs.<instance>.yaml under processorConfig) |
| 178 | +- class: org.yamcs.pus.Pus21RequestSequencingService |
| 179 | + args: |
| 180 | + apid: 1 # APID for generated TM packets |
| 181 | + timeEncoding: # must match the PusPacketPreprocessor time config |
| 182 | + implicitPfield: false |
| 183 | + pfield: 0x2f |
| 184 | +``` |
| 185 | +
|
| 186 | +The service registers itself as the processor's `CommandReleaser` because it |
| 187 | +implements `CommandReleaser` (via `StreamTcCommandReleaser`). No other |
| 188 | +`CommandReleaser` should be present for the same processor. |
| 189 | + |
| 190 | +If `StreamTcCommandReleaser` was previously in the service list, remove it and |
| 191 | +replace with this class. Non-ST[21] commands are forwarded to the TC stream |
| 192 | +via `super.releaseCommand()` exactly as before. |
| 193 | + |
| 194 | +--- |
| 195 | + |
| 196 | +## 6. XTCE / MDB Notes for Native Services |
| 197 | + |
| 198 | +MDB is still required for: |
| 199 | +- Ground tool encoding of TC commands (argument types, fixed values, etc.) |
| 200 | +- YAMCS TM decoding of injected TM packets (container hierarchy, parameters) |
| 201 | + |
| 202 | +The native service handles the _on-board_ logic; the MDB handles the |
| 203 | +_ground-side_ encoding/decoding contract. |
| 204 | + |
| 205 | +For TM packets injected by the service, the raw bytes must match the XTCE |
| 206 | +`SequenceContainer` definitions (correct service type / subtype in the right |
| 207 | +byte positions, correct data types and sizes for all fields). |
| 208 | + |
| 209 | +--- |
| 210 | + |
| 211 | +## 7. Limitations of the Native Approach |
| 212 | + |
| 213 | +| Limitation | Explanation | |
| 214 | +|------------|-------------| |
| 215 | +| Load by reference (TC[21,2]/TC[21,8]) | Requires a filesystem / on-board storage service; not implemented in the YAMCS native service. Ground must use TC[21,1] (direct load) instead. | |
| 216 | +| Embedded TC command history | Relayed TCs use synthetic CommandIds (`/pus21/relay`). They appear in command history but without full argument decoding. | |
| 217 | +| TM time accuracy | `processor.getCurrentTime()` is YAMCS mission time, which may differ slightly from on-board time used by the spacecraft. | |
| 218 | +| No persistence | Sequence store is in-memory; sequences are lost on YAMCS restart. For persistence, use a `Table`-backed store. | |
| 219 | +| No broadcast | If multiple YAMCS instances are running, sequence state is not shared. | |
| 220 | + |
| 221 | +--- |
| 222 | + |
| 223 | +## 8. Pattern Reuse for Other PUS Services |
| 224 | + |
| 225 | +The same pattern applies to other PUS services that benefit from native YAMCS handling: |
| 226 | + |
| 227 | +| Service | Native benefit | |
| 228 | +|---------|---------------| |
| 229 | +| ST[11] Time-based scheduling | YAMCS manages the schedule; avoids duplicating it on-board | |
| 230 | +| ST[21] Request sequencing | YAMCS executes sequences; sequences visible in command history | |
| 231 | +| ST[14] Packet forwarding control | YAMCS manages routing; no on-board routing table needed | |
| 232 | +| ST[20] On-board parameter management | YAMCS parameter DB is the on-board store | |
| 233 | + |
| 234 | +For any of these, the entry point is the same: extend `StreamTcCommandReleaser`, |
| 235 | +intercept on `releaseCommand()`, manage state, relay via `super.releaseCommand()`, |
| 236 | +inject TM via stream Tuple emission. |
0 commit comments