Skip to content

Commit be13dbd

Browse files
committed
feat(tmdl): add TM frame support for data link
1 parent f9e2906 commit be13dbd

File tree

3 files changed

+241
-0
lines changed

3 files changed

+241
-0
lines changed

pkg/tmdl/frame.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package tmdl
2+
3+
import (
4+
"encoding/binary"
5+
"errors"
6+
"fmt"
7+
)
8+
9+
// TMTransferFrame represents a CCSDS TM Space Data Link Protocol Transfer Frame.
10+
type TMTransferFrame struct {
11+
VersionNumber uint8 // 2 bits
12+
SpacecraftID uint16 // 10 bits
13+
VirtualChannelID uint8 // 6 bits
14+
FrameLength uint16 // Length of the frame
15+
FrameSecondaryHeader []byte // Optional secondary header
16+
DataField []byte // Main telemetry data
17+
OperationalControl []byte // 4-byte OCF (if used)
18+
FrameErrorControl uint16 // 16-bit CRC (Error Control)
19+
}
20+
21+
// NewTMTransferFrame initializes a new TM Transfer Frame.
22+
func NewTMTransferFrame(scid uint16, vcid uint8, data []byte, secondaryHeader []byte, ocf []byte) (*TMTransferFrame, error) {
23+
if len(data) > 65535 {
24+
return nil, errors.New("data field exceeds maximum frame length")
25+
}
26+
27+
frame := &TMTransferFrame{
28+
VersionNumber: 0b01, // Default CCSDS TM version
29+
SpacecraftID: scid & 0x03FF, // Mask to 10 bits
30+
VirtualChannelID: vcid & 0x3F, // Mask to 6 bits
31+
FrameLength: uint16(5 + len(secondaryHeader) + len(data) + len(ocf) + 2), // Total frame length including headers and CRC
32+
FrameSecondaryHeader: secondaryHeader,
33+
DataField: data,
34+
OperationalControl: ocf,
35+
}
36+
37+
// Compute Frame Error Control (CRC-16)
38+
frame.FrameErrorControl = ComputeCRC(frame.EncodeWithoutFEC())
39+
40+
return frame, nil
41+
}
42+
43+
// Encode converts the TM Transfer Frame to a byte slice.
44+
func (tf *TMTransferFrame) Encode() []byte {
45+
frameData := tf.EncodeWithoutFEC()
46+
47+
// Append CRC-16
48+
crcBytes := make([]byte, 2)
49+
binary.BigEndian.PutUint16(crcBytes, tf.FrameErrorControl)
50+
return append(frameData, crcBytes...)
51+
}
52+
53+
// EncodeWithoutFEC converts the frame to bytes excluding the CRC field.
54+
func (tf *TMTransferFrame) EncodeWithoutFEC() []byte {
55+
header := make([]byte, 5)
56+
57+
// First 5 bytes: TFVN, SCID, VCID, Length
58+
header[0] = (tf.VersionNumber << 6) | byte(tf.SpacecraftID>>8)
59+
header[1] = byte(tf.SpacecraftID & 0xFF)
60+
header[2] = tf.VirtualChannelID
61+
binary.BigEndian.PutUint16(header[3:], tf.FrameLength)
62+
63+
// Assemble full frame
64+
frameData := append(header, tf.FrameSecondaryHeader...)
65+
frameData = append(frameData, tf.DataField...)
66+
frameData = append(frameData, tf.OperationalControl...)
67+
68+
return frameData
69+
}
70+
71+
// DecodeTMTransferFrame parses a byte slice into a TM Transfer Frame.
72+
func DecodeTMTransferFrame(data []byte) (*TMTransferFrame, error) {
73+
if len(data) < 7 {
74+
return nil, errors.New("frame too short to be a valid TM Transfer Frame")
75+
}
76+
77+
// Extract Version Number, SCID, and VCID
78+
version := (data[0] >> 6) & 0x03
79+
scid := (uint16(data[0]&0x03) << 8) | uint16(data[1])
80+
vcid := data[2]
81+
82+
// Extract Frame Length
83+
frameLength := binary.BigEndian.Uint16(data[3:5])
84+
85+
// Check if the received frame length matches the actual data length
86+
if int(frameLength) != len(data) {
87+
return nil, fmt.Errorf("frame length mismatch: expected %d, got %d", frameLength, len(data))
88+
}
89+
90+
// Compute and verify CRC-16
91+
receivedCRC := binary.BigEndian.Uint16(data[len(data)-2:])
92+
computedCRC := ComputeCRC(data[:len(data)-2])
93+
if receivedCRC != computedCRC {
94+
return nil, fmt.Errorf("CRC mismatch: expected %04X, got %04X", receivedCRC, computedCRC)
95+
}
96+
97+
// Extract Data Field
98+
dataStart := 5
99+
dataEnd := len(data) - 2
100+
frameSecondaryHeader := []byte{}
101+
operationalControl := []byte{}
102+
103+
// Check if Secondary Header exists (Mission-dependent)
104+
if dataStart < dataEnd {
105+
frameSecondaryHeader = data[dataStart : dataStart+2] // Assuming a 2-byte header
106+
dataStart += 2
107+
}
108+
109+
// Extract Operational Control Field (OCF) if present
110+
if dataEnd-dataStart >= 4 {
111+
operationalControl = data[dataEnd-4 : dataEnd]
112+
dataEnd -= 4
113+
}
114+
115+
// Extract the main Data Field
116+
dataField := data[dataStart:dataEnd]
117+
118+
// Construct the TMTransferFrame object
119+
frame := &TMTransferFrame{
120+
VersionNumber: version,
121+
SpacecraftID: scid,
122+
VirtualChannelID: vcid,
123+
FrameLength: frameLength,
124+
FrameSecondaryHeader: frameSecondaryHeader,
125+
DataField: dataField,
126+
OperationalControl: operationalControl,
127+
FrameErrorControl: receivedCRC,
128+
}
129+
130+
return frame, nil
131+
}

pkg/tmdl/frame_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package tmdl_test
2+
3+
import (
4+
"github.com/ravisuhag/astro/pkg/tmdl"
5+
"testing"
6+
)
7+
8+
func TestNewTMTransferFrame(t *testing.T) {
9+
scid := uint16(0x3FF)
10+
vcid := uint8(0x3F)
11+
data := []byte{0x01, 0x02, 0x03, 0x04}
12+
secondaryHeader := []byte{0x05, 0x06}
13+
ocf := []byte{0x07, 0x08, 0x09, 0x0A}
14+
15+
frame, err := tmdl.NewTMTransferFrame(scid, vcid, data, secondaryHeader, ocf)
16+
if err != nil {
17+
t.Fatalf("Expected no error, got %v", err)
18+
}
19+
20+
if frame.SpacecraftID != scid&0x03FF {
21+
t.Errorf("Expected SpacecraftID %v, got %v", scid&0x03FF, frame.SpacecraftID)
22+
}
23+
24+
if frame.VirtualChannelID != vcid&0x3F {
25+
t.Errorf("Expected VirtualChannelID %v, got %v", vcid&0x3F, frame.VirtualChannelID)
26+
}
27+
28+
expectedLength := uint16(5 + len(secondaryHeader) + len(data) + len(ocf) + 2)
29+
if frame.FrameLength != expectedLength {
30+
t.Errorf("Expected FrameLength %v, got %v", expectedLength, frame.FrameLength)
31+
}
32+
}
33+
34+
func TestTMTransferFrame_Encode(t *testing.T) {
35+
scid := uint16(0x3FF)
36+
vcid := uint8(0x3F)
37+
data := []byte{0x01, 0x02, 0x03, 0x04}
38+
secondaryHeader := []byte{0x05, 0x06}
39+
ocf := []byte{0x07, 0x08, 0x09, 0x0A}
40+
41+
frame, err := tmdl.NewTMTransferFrame(scid, vcid, data, secondaryHeader, ocf)
42+
if err != nil {
43+
t.Fatalf("Expected no error, got %v", err)
44+
}
45+
46+
bytes := frame.Encode()
47+
if len(bytes) != int(frame.FrameLength) {
48+
t.Errorf("Expected byte slice length %v, got %v", frame.FrameLength, len(bytes))
49+
}
50+
}
51+
52+
func TestDecodeTMTransferFrame(t *testing.T) {
53+
scid := uint16(0x3FF)
54+
vcid := uint8(0x3F)
55+
data := []byte{0x01, 0x02, 0x03, 0x04}
56+
secondaryHeader := []byte{0x05, 0x06}
57+
ocf := []byte{0x07, 0x08, 0x09, 0x0A}
58+
59+
frame, err := tmdl.NewTMTransferFrame(scid, vcid, data, secondaryHeader, ocf)
60+
if err != nil {
61+
t.Fatalf("Expected no error, got %v", err)
62+
}
63+
64+
bytes := frame.Encode()
65+
decodedFrame, err := tmdl.DecodeTMTransferFrame(bytes)
66+
if err != nil {
67+
t.Fatalf("Expected no error, got %v", err)
68+
}
69+
70+
if decodedFrame.SpacecraftID != frame.SpacecraftID {
71+
t.Errorf("Expected SpacecraftID %v, got %v", frame.SpacecraftID, decodedFrame.SpacecraftID)
72+
}
73+
74+
if decodedFrame.VirtualChannelID != frame.VirtualChannelID {
75+
t.Errorf("Expected VirtualChannelID %v, got %v", frame.VirtualChannelID, decodedFrame.VirtualChannelID)
76+
}
77+
78+
if decodedFrame.FrameLength != frame.FrameLength {
79+
t.Errorf("Expected FrameLength %v, got %v", frame.FrameLength, decodedFrame.FrameLength)
80+
}
81+
82+
if string(decodedFrame.DataField) != string(frame.DataField) {
83+
t.Errorf("Expected DataField %v, got %v", frame.DataField, decodedFrame.DataField)
84+
}
85+
86+
if string(decodedFrame.FrameSecondaryHeader) != string(frame.FrameSecondaryHeader) {
87+
t.Errorf("Expected FrameSecondaryHeader %v, got %v", frame.FrameSecondaryHeader, decodedFrame.FrameSecondaryHeader)
88+
}
89+
90+
if string(decodedFrame.OperationalControl) != string(frame.OperationalControl) {
91+
t.Errorf("Expected OperationalControl %v, got %v", frame.OperationalControl, decodedFrame.OperationalControl)
92+
}
93+
}

pkg/tmdl/utils.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package tmdl
2+
3+
// ComputeCRC calculates a CRC-16 checksum for the given data.
4+
func ComputeCRC(data []byte) uint16 {
5+
var crc uint16 = 0xFFFF
6+
for _, b := range data {
7+
crc ^= uint16(b) << 8
8+
for i := 0; i < 8; i++ {
9+
if (crc & 0x8000) != 0 {
10+
crc = (crc << 1) ^ 0x1021
11+
} else {
12+
crc <<= 1
13+
}
14+
}
15+
}
16+
return crc
17+
}

0 commit comments

Comments
 (0)