Skip to content

Commit ec50a3d

Browse files
committed
update: pus21.md, pus_native_arch.md
1 parent d63ead6 commit ec50a3d

2 files changed

Lines changed: 334 additions & 0 deletions

File tree

pus_analysis/pus21.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,3 +761,101 @@ with two categories of workarounds:
761761

762762
The remaining 10 subtypes are fully XTCE-compatible using standard patterns (fixed-string
763763
arguments, zero-payload MetaCommands, AggregateParameterType arrays).
764+
765+
---
766+
767+
## d) YAMCS-Native Implementation (Java)
768+
769+
Given the MDB is generated separately, the following Java-side changes are required.
770+
771+
### New file: `Pus21Service.java`
772+
773+
**Path**: `simulator/src/main/java/org/yamcs/simulator/pus/Pus21Service.java`
774+
775+
Follows the same pattern as `Pus11Service.java`. Key design points:
776+
777+
| Aspect | Decision |
778+
|--------|----------|
779+
| State | `LinkedHashMap<String, RequestSequence> sequenceStore` — keyed on `seq_id` |
780+
| seq_id encoding | 16-byte null-padded ASCII; `readSeqId` / `encodeSeqId` helpers |
781+
| Delay encoding | `uint32 milliseconds` — stored as `int delayMs` in `Entry` |
782+
| TC relay | `pusSimulator.processTc(new PusTcPacket(entry.rawTc))` — same mechanism as ST[11] |
783+
| Thread model | One daemon thread per executing sequence (`"pus21-seq-<id>"`); `seq.active` is `volatile` for abort signalling |
784+
| Synchronization | `executeTc` is `synchronized` on the service instance; `runSequence` re-enters with `synchronized(this)` at completion to reset `active` |
785+
| CRC (TC[21,9]) | `CrcCciitCalculator.compute(raw, 0, len)` over concatenation of `{tc_bytes ‖ delay_ms(uint32)}` for all entries |
786+
| TC[21,2]/TC[21,8] | NACK with `COMPL_ERR_FILE_NOT_SUPPORTED` — no filesystem in the simulator |
787+
788+
**Subtype dispatch table**:
789+
790+
```java
791+
case 1 -> loadDirectly(tc) // TC[21,1]: parse seq_id+N+entries, store inactive
792+
case 2 -> loadByReference(tc, false) // TC[21,2]: NACK (no filesystem)
793+
case 3 -> unloadSequence(tc) // TC[21,3]: remove if inactive
794+
case 4 -> activateSequence(tc) // TC[21,4]: set active, spawn thread
795+
case 5 -> abortSequence(tc) // TC[21,5]: set active=false (thread exits on next check)
796+
case 6 -> reportExecutionStatus(tc) // TC[21,6]: build TM[21,7]
797+
case 8 -> loadByReference(tc, true) // TC[21,8]: NACK (no filesystem)
798+
case 9 -> checksumSequence(tc) // TC[21,9]: build TM[21,10]
799+
case 11 -> reportSequenceContent(tc) // TC[21,11]: build TM[21,12]
800+
case 13 -> abortAll(tc) // TC[21,13]: abort all active, build TM[21,14]
801+
```
802+
803+
TM subtype 7, 10, 12, 14 are generated inside the TC handlers above (not dispatched separately).
804+
805+
---
806+
807+
### Modified file: `PusSimulator.java`
808+
809+
Three changes needed:
810+
811+
**1. Field declaration** (alongside other service fields):
812+
```java
813+
Pus21Service pus21Service;
814+
```
815+
816+
**2. Constructor** (alongside other service instantiations):
817+
```java
818+
pus21Service = new Pus21Service(this);
819+
```
820+
821+
**3. `doStart()`** (alongside other `start()` calls):
822+
```java
823+
pus21Service.start();
824+
```
825+
826+
**4. `executePendingCommands()` switch** (adds case 21):
827+
```java
828+
case 21 -> pus21Service.executeTc(commandPacket);
829+
```
830+
831+
---
832+
833+
### Optional: `yamcs.pus.yaml` MDB entry
834+
835+
Once the `pus21.xml` MDB file is generated, add it to the `mdb` list:
836+
837+
```yaml
838+
- type: "xtce"
839+
spec: "mdb/pus21.xml"
840+
```
841+
842+
---
843+
844+
### Gap summary for Java implementation
845+
846+
| Gap | Impact | How handled |
847+
|-----|--------|-------------|
848+
| TC[21,1] entry body (variable-length TC + delay) | Cannot be parsed by YAMCS MDB alone | `Pus21Service.loadDirectly()` reads the entry block manually from `getUserDataBuffer()` |
849+
| TC[21,2]/TC[21,8] file loading | No filesystem in simulator | NACK with error code 14; ground must use TC[21,1] instead |
850+
| TC relay (gap #5 from §c) | YAMCS has no native ST[21] executor | `runSequence()` calls `pusSimulator.processTc()` — same approach as ST[11] scheduled TC release |
851+
| TM[21,12] variable-length content | Cannot be built by YAMCS alone | Built entirely in `reportSequenceContent()` by manual ByteBuffer assembly |
852+
853+
854+
Configuration needed (not yet changed)
855+
In processors.yaml, replace StreamTcCommandReleaser with:
856+
- class: org.yamcs.pus.Pus21RequestSequencingService
857+
args:
858+
apid: 1
859+
timeEncoding:
860+
implicitPfield: false
861+
pfield: 0x2f

pus_analysis/pus_native_arch.md

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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

Comments
 (0)