The tmdl package implements the CCSDS 132.0-B-3 TM Space Data Link Protocol — the Data Link Layer protocol used for transferring telemetry data from spacecraft to ground stations.
import "github.com/ravisuhag/astro/pkg/tmdl"
// Create and encode a TM Transfer Frame
frame, _ := tmdl.NewTMTransferFrame(0x1A, 1, []byte("telemetry"), nil, nil)
encoded, _ := frame.Encode()
// Decode a received frame
decoded, _ := tmdl.DecodeTMTransferFrame(encoded)
fmt.Println(decoded.Header.Humanize())The package follows a layered architecture mapping to the CCSDS data plane:
┌─────────────────────────────────────────────┐
│ Service Layer │
│ VCP (Packet) · VCF (Frame) · VCA (Access) │
│ TMServiceManager │
├─────────────────────────────────────────────┤
│ Master Channel Layer │
│ MasterChannel · VirtualChannelMultiplexer │
├─────────────────────────────────────────────┤
│ Virtual Channel Layer │
│ VirtualChannel (frame buffer per VCID) │
├─────────────────────────────────────────────┤
│ Frame Layer │
│ TMTransferFrame · PrimaryHeader │
│ SecondaryHeader · FrameCounter · CRC-16 │
├─────────────────────────────────────────────┤
│ Physical Layer │
│ PhysicalChannel (MC multiplexing) │
└─────────────────────────────────────────────┘
Note: The sync and channel coding layer (ASM, pseudo-randomization, CADU framing) is handled by the
tmscpackage, which implements CCSDS 131.0-B-4. See tmsc for details.
The TMTransferFrame is the fundamental data unit. Each frame has a fixed length on a given physical channel and carries telemetry data identified by Spacecraft ID and Virtual Channel ID.
// Basic frame with SCID=0x1A, VCID=1
frame, err := tmdl.NewTMTransferFrame(0x1A, 1, data, nil, nil)
// Frame with a secondary header
frame, err := tmdl.NewTMTransferFrame(0x1A, 1, data, secondaryHeaderBytes, nil)
// Frame with Operational Control Field (4 bytes)
frame, err := tmdl.NewTMTransferFrame(0x1A, 1, data, nil, ocfBytes)
// Idle frame (all-ones data, FHP=0x07FF)
idle, err := tmdl.NewIdleFrame(0x1A, 7, config)// Encode to bytes (includes CRC-16)
encoded, err := frame.Encode()
// Encode without Frame Error Control
raw, err := frame.EncodeWithoutFEC()
// Decode bytes back to a frame (validates CRC)
frame, err := tmdl.DecodeTMTransferFrame(encoded)
// Check if a frame is idle
if tmdl.IsIdleFrame(frame) { ... }// Human-readable header dump
fmt.Println(frame.Header.Humanize())
// Access identifiers
mcid := frame.Header.MCID() // Master Channel ID (TFVN + SCID)
gvcid := frame.Header.GVCID() // Global Virtual Channel ID (MCID + VCID)+----------------+----------------+----------------+----------------+
| Version (2b) | Spacecraft ID (10b) | VCID (3b) |
+----------------+----------------+----------------+----------------+
| OCF Flag (1b) | MC Frame Count (8b) | VC Frame Count |
+----------------+----------------+----------------+----------------+
| FSH (1b) | Sync (1b) | PktOrd (1b) | SegLen (2b) | FHP (11b) |
+----------------+----------------+----------------+----------------+
| Secondary Header (optional, 1–64 bytes) |
+----------------+----------------+----------------+----------------+
| Transfer Frame Data Field (variable) |
+----------------+----------------+----------------+----------------+
| Operational Control Field (optional, 4 bytes) |
+----------------+----------------+----------------+----------------+
| Frame Error Control (CRC-16-CCITT, 2 bytes) |
+----------------+----------------+----------------+----------------+
The 6-byte primary header identifies and routes each frame:
| Field | Bits | Range | Description |
|---|---|---|---|
| Version Number | 2 | 0 | Transfer Frame Version (00 for TM) |
| Spacecraft ID | 10 | 0–1023 | Identifies the spacecraft |
| Virtual Channel ID | 3 | 0–7 | Identifies the virtual channel |
| OCF Flag | 1 | 0–1 | Operational Control Field present |
| MC Frame Count | 8 | 0–255 | Master Channel sequence counter |
| VC Frame Count | 8 | 0–255 | Virtual Channel sequence counter |
| FSH Flag | 1 | 0–1 | Frame Secondary Header present |
| Sync Flag | 1 | 0–1 | Synchronization flag (VCA sets to 1) |
| Packet Order Flag | 1 | 0–1 | Must be 0 when Sync Flag is 0 |
| Segment Length ID | 2 | 0–3 | Must be 11 when Sync Flag is 0 |
| First Header Pointer | 11 | 0–2047 | Offset to first packet start in data field |
First Header Pointer special values:
0x07FE— no packet starts in this frame (continuation only)0x07FF— idle frame (VCP) or VCA service data
Optional mission-defined header (1 prefix byte + up to 64 data bytes):
// Included when secondaryHeaderData is non-nil in NewTMTransferFrame
frame, err := tmdl.NewTMTransferFrame(scid, vcid, data, myHeaderBytes, nil)| Field | Bits | Description |
|---|---|---|
| Version Number | 2 | Always 00 for Version 1 |
| Header Length | 6 | Length of data field minus 1 (0–63) |
| Data Field | variable | Mission-specific content (1–64 bytes) |
ChannelConfig defines the fixed parameters shared by all frames on a physical channel:
config := tmdl.ChannelConfig{
FrameLength: 1024, // Total frame length in octets
HasOCF: true, // Operational Control Field (4 bytes)
HasFEC: true, // Frame Error Control (2-byte CRC)
}
// Calculate available space for user data
capacity := config.DataFieldCapacity(0) // No secondary header
capacity := config.DataFieldCapacity(len(secHeaderData)) // With secondary headerDataFieldCapacity accounts for the 6-byte primary header, optional secondary header (1 + N bytes), optional OCF (4 bytes), and optional FEC (2 bytes).
A VirtualChannel is a buffered frame queue identified by a VCID (0–7). It provides thread-safe FIFO storage for frames within a single data stream.
// Create with VCID=1 and buffer capacity of 100 frames
vc := tmdl.NewVirtualChannel(1, 100)
// Add and retrieve frames
err := vc.AddFrame(frame) // ErrBufferFull if at capacity
frame, err := vc.GetNextFrame() // ErrNoFramesAvailable if empty
hasFrames := vc.HasFrames()
count := vc.Len()Three service types provide different data transfer models over Virtual Channels:
Multiplexes CCSDS Space Packets into fixed-length frames using FirstHeaderPointer for packet boundary detection.
counter := tmdl.NewFrameCounter()
vc := tmdl.NewVirtualChannel(1, 100)
vcp := tmdl.NewVirtualChannelPacketService(0x1A, 1, vc, config, counter)
// Send packets — automatically packed into frames
err := vcp.Send(packet1)
err = vcp.Send(packet2)
err = vcp.Flush() // Emit remaining partial frame with idle fill
// Receive — extracts packets using FHP and PacketSizer
pkt, err := vcp.Receive()Packet packing behavior:
- When
ChannelConfig.FrameLength > 0: packets are buffered and packed into fixed-length frames. Multiple small packets can share a frame; large packets span multiple frames.FirstHeaderPtrmarks where each new packet begins. - When
ChannelConfig.FrameLength == 0: legacy mode, one frame per packet.
Packet sizer:
A packet sizer must be set before calling Receive. For CCSDS Space Packets, use the sizer from the spp package:
vcp.SetPacketSizer(spp.PacketSizer)For non-CCSDS packet formats, provide a custom sizer function:
vcp.SetPacketSizer(func(data []byte) int {
if len(data) < 4 { return -1 }
length := int(binary.BigEndian.Uint32(data[0:4]))
return 4 + length
})Receive-side resync: After a frame gap is detected (via FrameGapDetector), the receiver discards its buffer and resyncs at the next FirstHeaderPtr offset.
Pass-through service — sends and receives pre-encoded frames without modification.
vc := tmdl.NewVirtualChannel(2, 100)
vcf := tmdl.NewVirtualChannelFrameService(2, vc)
// Send a pre-encoded frame
err := vcf.Send(encodedFrameBytes)
// Receive an encoded frame
data, err := vcf.Receive()Fixed-length SDU service for housekeeping data or fixed-rate streams. Sets SyncFlag=true and FirstHeaderPtr=0x07FF per CCSDS spec.
counter := tmdl.NewFrameCounter()
vc := tmdl.NewVirtualChannel(3, 100)
vca := tmdl.NewVirtualChannelAccessService(0x1A, 3, 256, vc, config, counter)
// Send a fixed-length SDU (padded to data field capacity)
err := vca.Send(sduData)
// Receive SDU and check status
data, err := vca.Receive()
status := vca.LastStatus() // VCAStatus{SyncFlag, PacketOrderFlag, SegmentLengthID}Manages 8-bit MC and VC frame counters. Share a single counter across all services for the same spacecraft:
counter := tmdl.NewFrameCounter()
mc, vc := counter.Next(vcid) // Returns current counts, then increments bothBoth counters wrap at 255.
Groups Virtual Channels for a single spacecraft (identified by SCID) and provides weighted round-robin multiplexing:
mc := tmdl.NewMasterChannel(0x1A, config)
// Register Virtual Channels with priority weights
mc.AddVirtualChannel(vc1, 3) // Higher priority
mc.AddVirtualChannel(vc2, 1) // Lower priority
// Send path: retrieve next frame from multiplexer
frame, err := mc.GetNextFrame()
frame, err := mc.GetNextFrameOrIdle() // Returns idle frame if none available
// Receive path: route inbound frame to correct VC
err := mc.AddFrame(frame)
// Frame gap detection
mcGap := mc.MCFrameGap() // MC frame gap from last AddFrame
vcGap := mc.VCFrameGap() // VC frame gap from last AddFrame
// Check pending state
hasPending := mc.HasPendingFrames()Represents the physical communication link. Handles MC-level multiplexing across Master Channels:
pc := tmdl.NewPhysicalChannel("TM-68", config)
// Register Master Channels with priority weights
pc.AddMasterChannel(mc1, 2)
pc.AddMasterChannel(mc2, 1)
// Send path
frame, err := pc.GetNextFrame() // Weighted round-robin across MCs
frame, err := pc.GetNextFrameOrIdle() // Idle frame if no data
// Receive path: demux inbound frame to correct MC by SCID
err := pc.AddFrame(frame)The tmsc package (CCSDS 131.0-B-4) handles the sync layer — ASM, pseudo-randomization, and CADU framing. Use it alongside tmdl for a complete send/receive pipeline:
import "github.com/ravisuhag/astro/pkg/tmsc"
// Send: get next frame from MC multiplexer, then wrap as CADU
frame, _ := pc.GetNextFrame()
encoded, _ := frame.Encode()
cadu := tmsc.WrapCADU(encoded, nil, true) // nil=default ASM, true=randomize
// Receive: unwrap CADU, then decode frame
unwrapped, _ := tmsc.UnwrapCADU(cadu, nil, true) // nil=default ASM, true=derandomize
frame, _ := tmdl.DecodeTMTransferFrame(unwrapped)TMServiceManager provides a high-level API that wires the full pipeline:
mgr := tmdl.NewTMServiceManager()
// Register services and channels
mgr.RegisterVirtualService(1, tmdl.VCP, vcp)
mgr.RegisterVirtualService(3, tmdl.VCA, vca)
mgr.RegisterMasterChannel(0x1A, mc)
// Send data through a service
err := mgr.SendData(1, tmdl.VCP, packetBytes)
// Receive data from a service
data, err := mgr.ReceiveData(1, tmdl.VCP)
// Flush a service
err := mgr.FlushService(1, tmdl.VCP)
// Route frames through Master Channels
err := mgr.AddFrameToMasterChannel(0x1A, frame)
frame, err := mgr.GetNextFrameFromMasterChannel(0x1A)
hasPending := mgr.HasPendingFramesInMasterChannel(0x1A)// 1. Configure the physical channel
config := tmdl.ChannelConfig{
FrameLength: 1024,
HasOCF: true,
HasFEC: true,
}
// 2. Create channel hierarchy
counter := tmdl.NewFrameCounter()
vc1 := tmdl.NewVirtualChannel(1, 100)
vcp := tmdl.NewVirtualChannelPacketService(0x1A, 1, vc1, config, counter)
mc := tmdl.NewMasterChannel(0x1A, config)
mc.AddVirtualChannel(vc1, 1)
pc := tmdl.NewPhysicalChannel("TM-68", config)
pc.AddMasterChannel(mc, 1)
// 3. Send packets
vcp.Send(packet1)
vcp.Send(packet2)
vcp.Flush()
// 4. Transmit frames as CADUs (using tmsc for sync layer)
for pc.HasPendingFrames() {
frame, _ := pc.GetNextFrame()
encoded, _ := frame.Encode()
cadu := tmsc.WrapCADU(encoded, nil, true) // nil=default ASM, true=randomize
transmit(cadu)
}// 1. Create matching channel hierarchy
counter := tmdl.NewFrameCounter()
vc1 := tmdl.NewVirtualChannel(1, 100)
vcp := tmdl.NewVirtualChannelPacketService(0x1A, 1, vc1, config, counter)
vcp.SetPacketSizer(spp.PacketSizer)
mc := tmdl.NewMasterChannel(0x1A, config)
mc.AddVirtualChannel(vc1, 1)
pc := tmdl.NewPhysicalChannel("TM-68", config)
pc.AddMasterChannel(mc, 1)
// 2. Process incoming CADUs (using tmsc for sync layer)
unwrapped, err := tmsc.UnwrapCADU(cadu, nil, true) // nil=default ASM, true=derandomize
if err != nil { /* handle sync marker or data errors */ }
frame, err := tmdl.DecodeTMTransferFrame(unwrapped)
if err != nil { /* handle CRC or frame errors */ }
// 3. Route to Master Channel → Virtual Channel
err = pc.AddFrame(frame)
// 4. Extract packets
pkt, err := vcp.Receive()All errors are exported package-level variables, suitable for use with errors.Is:
| Error | Meaning |
|---|---|
ErrDataTooShort |
Data too short to decode |
ErrInvalidVersion |
Version is not 0 |
ErrInvalidSpacecraftID |
SCID outside 0–1023 |
ErrInvalidVCID |
VCID outside 0–7 |
ErrInvalidPacketOrderFlag |
Packet order flag set when sync flag is 0 |
ErrInvalidSegmentLengthID |
Segment length ID not 11 when sync flag is 0 |
ErrInvalidFirstHeaderPtr |
FHP outside 0–2047 or inconsistent with sync flag |
ErrInvalidSecondaryHeaderVersion |
Secondary header version is not 0 |
ErrInvalidHeaderLength |
Secondary header length outside 0–63 |
ErrCRCMismatch |
CRC integrity check failed |
ErrDataTooLarge |
Data exceeds maximum frame length |
ErrEmptyData |
Empty data provided |
ErrNoFramesAvailable |
No frames in buffer |
ErrBufferFull |
Virtual channel buffer at capacity |
ErrSCIDMismatch |
Frame SCID doesn't match master channel |
ErrSizeMismatch |
VCA data size doesn't match expected fixed size |
ErrServiceNotFound |
No service for specified VCID and type |
ErrMasterChannelNotFound |
No master channel for specified SCID |
ErrNoVirtualChannels |
No virtual channels registered |
ErrVirtualChannelNotFound |
No virtual channel for specified VCID |
ErrDataFieldTooSmall |
Data field capacity too small for framing |
ErrNoMasterChannels |
No master channels on physical channel |
ErrInvalidOCFLength |
OCF not exactly 4 bytes |
Note: Sync-layer errors such as
ErrSyncMarkerMismatchandErrDataTooShortare defined in thetmscpackage.
- CCSDS 132.0-B-3 — TM Space Data Link Protocol Blue Book
- CCSDS 131.0-B-5 — TM Synchronization and Channel Coding
tmscpackage — Sync and Channel Coding (ASM, randomization, CADU framing)