Skip to content

TM Data Link Support #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,044 changes: 1,044 additions & 0 deletions docs/tmdl.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/spp/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (ph *PrimaryHeader) Validate() error {
if ph.SequenceCount > 16383 {
return errors.New("invalid SequenceCount: must be in range 0-16383 (14 bits)")
}
// PacketLength is already a uint16, no need for further validation
// PacketLength is already uint16, no need for further validation
return nil
}

Expand Down
45 changes: 45 additions & 0 deletions pkg/tmdl/channel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package tmdl

import (
"errors"
)

// VirtualChannel represents a logical data stream within a spacecraft.
type VirtualChannel struct {
VCID uint8
FrameBuffer []*TMTransferFrame // Stores received frames
MaxBufferSize int // Max frames that can be stored
}

// NewVirtualChannel initializes a new Virtual Channel.
func NewVirtualChannel(vcid uint8, bufferSize int) *VirtualChannel {
return &VirtualChannel{
VCID: vcid,
FrameBuffer: make([]*TMTransferFrame, 0, bufferSize),
MaxBufferSize: bufferSize,
}
}

// AddFrame stores a new frame in the Virtual Channel buffer.
func (vc *VirtualChannel) AddFrame(f *TMTransferFrame) error {
if len(vc.FrameBuffer) >= vc.MaxBufferSize {
return errors.New("virtual channel buffer full, dropping frame")
}
vc.FrameBuffer = append(vc.FrameBuffer, f)
return nil
}

// GetNextFrame retrieves and removes the oldest frame from the buffer.
func (vc *VirtualChannel) GetNextFrame() (*TMTransferFrame, error) {
if len(vc.FrameBuffer) == 0 {
return nil, errors.New("no frames in buffer")
}
f := vc.FrameBuffer[0]
vc.FrameBuffer = vc.FrameBuffer[1:]
return f, nil
}

// HasFrames checks if there are frames available in the Virtual Channel.
func (vc *VirtualChannel) HasFrames() bool {
return len(vc.FrameBuffer) > 0
}
130 changes: 130 additions & 0 deletions pkg/tmdl/channel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package tmdl_test

import (
"github.com/ravisuhag/astro/pkg/tmdl"
"testing"
)

func TestNewVirtualChannel(t *testing.T) {
vcid := uint8(0x01)
bufferSize := 10

vc := tmdl.NewVirtualChannel(vcid, bufferSize)

if vc.VCID != vcid {
t.Errorf("Expected VCID %v, got %v", vcid, vc.VCID)
}

if len(vc.FrameBuffer) != 0 {
t.Errorf("Expected FrameBuffer length 0, got %v", len(vc.FrameBuffer))
}

if cap(vc.FrameBuffer) != bufferSize {
t.Errorf("Expected FrameBuffer capacity %v, got %v", bufferSize, cap(vc.FrameBuffer))
}

if vc.MaxBufferSize != bufferSize {
t.Errorf("Expected MaxBufferSize %v, got %v", bufferSize, vc.MaxBufferSize)
}
}

func TestAddFrame(t *testing.T) {
vcid := uint8(0x01)
bufferSize := 2
vc := tmdl.NewVirtualChannel(vcid, bufferSize)

frame := &tmdl.TMTransferFrame{}
err := vc.AddFrame(frame)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

if len(vc.FrameBuffer) != 1 {
t.Errorf("Expected FrameBuffer length 1, got %v", len(vc.FrameBuffer))
}

err = vc.AddFrame(frame)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

err = vc.AddFrame(frame)
if err == nil {
t.Fatalf("Expected error, got nil")
}

if len(vc.FrameBuffer) != bufferSize {
t.Errorf("Expected FrameBuffer length %v, got %v", bufferSize, len(vc.FrameBuffer))
}
}

func TestGetNextFrame(t *testing.T) {
vcid := uint8(0x01)
bufferSize := 2
vc := tmdl.NewVirtualChannel(vcid, bufferSize)

frame1 := &tmdl.TMTransferFrame{}
frame2 := &tmdl.TMTransferFrame{}

err := vc.AddFrame(frame1)
if err != nil {
t.Errorf("Failed to add frame1: %v", err)
}

err = vc.AddFrame(frame2)
if err != nil {
t.Errorf("Failed to add frame2: %v", err)
}

retrievedFrame, err := vc.GetNextFrame()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

if retrievedFrame != frame1 {
t.Errorf("Expected frame1, got %v", retrievedFrame)
}

retrievedFrame, err = vc.GetNextFrame()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

if retrievedFrame != frame2 {
t.Errorf("Expected frame2, got %v", retrievedFrame)
}

_, err = vc.GetNextFrame()
if err == nil {
t.Fatalf("Expected error, got nil")
}
}

func TestHasFrames(t *testing.T) {
vcid := uint8(0x01)
bufferSize := 2
vc := tmdl.NewVirtualChannel(vcid, bufferSize)

if vc.HasFrames() {
t.Errorf("Expected HasFrames to be false, got true")
}

frame := &tmdl.TMTransferFrame{}
err := vc.AddFrame(frame)
if err != nil {
t.Errorf("Failed to add frame: %v", err)
}

if !vc.HasFrames() {
t.Errorf("Expected HasFrames to be true, got false")
}

_, err = vc.GetNextFrame()
if err != nil {
t.Errorf("Failed to retrieve frame: %v", err)
}

if vc.HasFrames() {
t.Errorf("Expected HasFrames to be false, got true")
}
}
152 changes: 152 additions & 0 deletions pkg/tmdl/frame.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package tmdl

import (
"encoding/binary"
"errors"
)

// TMTransferFrame represents a CCSDS TM Space Data Link Protocol Transfer Frame.
type TMTransferFrame struct {
Header PrimaryHeader
SecondaryHeader SecondaryHeader
DataField []byte // Main telemetry data
OperationalControl []byte // 4-byte OCF (if used)
FrameErrorControl uint16 // 16-bit CRC (Error Control)
}

// NewTMTransferFrame initializes a new TM Transfer Frame.
func NewTMTransferFrame(scid uint16, vcid uint8, data []byte, secondaryHeaderData []byte, ocf []byte) (*TMTransferFrame, error) {
if len(data) > 65535 {
return nil, errors.New("data field exceeds maximum frame length")
}

secondaryHeader := SecondaryHeader{
DataField: secondaryHeaderData,
}

frame := &TMTransferFrame{
Header: PrimaryHeader{
VersionNumber: 0b01, // Default CCSDS TM version
SpacecraftID: scid & 0x03FF, // Mask to 10 bits
VirtualChannelID: vcid & 0x3F, // Mask to 6 bits
OCFFlag: len(ocf) > 0, // Set OCF flag if present
FSHFlag: len(secondaryHeaderData) > 0,
MCFrameCount: 0, // To be set dynamically
VCFrameCount: 0, // To be set dynamically
SyncFlag: false,
PacketOrderFlag: false,
SegmentLengthID: 0, // Default segment length ID
FirstHeaderPtr: 0, // Default "no packet start" pointer
},
SecondaryHeader: secondaryHeader,
DataField: data,
OperationalControl: ocf,
}
if !frame.Header.SyncFlag {
frame.Header.FirstHeaderPtr = uint16(len(secondaryHeaderData))
} else {
frame.Header.FirstHeaderPtr = 0xFFFF // Undefined when SyncFlag is set
}

// Compute Frame Error Control (CRC-16)
frame.FrameErrorControl = ComputeCRC(frame.EncodeWithoutFEC())

return frame, nil
}

// Encode converts the TM Transfer Frame to a byte slice.
func (tf *TMTransferFrame) Encode() []byte {
frameData := tf.EncodeWithoutFEC()

// Append CRC-16
crcBytes := make([]byte, 2)
binary.BigEndian.PutUint16(crcBytes, tf.FrameErrorControl)
return append(frameData, crcBytes...)
}

// EncodeWithoutFEC converts the frame to bytes excluding the CRC field.
func (tf *TMTransferFrame) EncodeWithoutFEC() []byte {
header := tf.Header.Encode()
var secondaryHeader []byte
var err error

// Only encode secondary header if FSHFlag is set
if tf.Header.FSHFlag {
secondaryHeader, err = tf.SecondaryHeader.Encode()
if err != nil {
// Handle error or truncate to FirstHeaderPtr length
secondaryHeader = secondaryHeader[:tf.Header.FirstHeaderPtr]
}
}

// Assemble full frame
frameData := append(header, secondaryHeader...)
frameData = append(frameData, tf.DataField...)
if tf.Header.OCFFlag {
frameData = append(frameData, tf.OperationalControl...)
}

return frameData
}

// DecodeTMTransferFrame parses a byte slice into a TM Transfer Frame.
func DecodeTMTransferFrame(data []byte) (*TMTransferFrame, error) {
if len(data) < 7 {
return nil, errors.New("frame too short to be a valid TM Transfer Frame")
}

// Decode Primary Header
header, err := (&PrimaryHeader{}).Decode(data[:6])
if err != nil {
return nil, err
}

// Compute and verify CRC-16
receivedCRC := binary.BigEndian.Uint16(data[len(data)-2:])
computedCRC := ComputeCRC(data[:len(data)-2])
if receivedCRC != computedCRC {
return nil, errors.New("CRC mismatch: received CRC does not match computed CRC")
}

// Extract Data Field
primaryHeaderLength := 6
dataStart := primaryHeaderLength
dataEnd := len(data) - 2
frameSecondaryData := []byte{}
operationalControl := []byte{}

// Extract Secondary Header if present
if header.FSHFlag {
if int(header.FirstHeaderPtr) > len(data)-primaryHeaderLength {
return nil, errors.New("invalid FirstHeaderPtr value")
}
frameSecondaryData = data[dataStart : dataStart+int(header.FirstHeaderPtr)]
dataStart += int(header.FirstHeaderPtr)
}

// Decode Secondary Header
var secondaryHeader SecondaryHeader
if header.FSHFlag {
if err := secondaryHeader.Decode(frameSecondaryData); err != nil {
return nil, err
}
}

// Extract OCF if present
if header.OCFFlag && dataEnd-dataStart >= 4 {
operationalControl = data[dataEnd-4 : dataEnd]
dataEnd -= 4
}

// Extract main Data Field
dataField := data[dataStart:dataEnd]

// Construct and return the TMTransferFrame object
return &TMTransferFrame{
Header: *header,
SecondaryHeader: secondaryHeader,
DataField: dataField,
OperationalControl: operationalControl,
FrameErrorControl: receivedCRC,
}, nil
}
Loading
Loading