Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
481827a
wip fop edl cmd shell
sneakyaardvark Jun 12, 2026
7fa5356
fix: T1 timeout is triggered on fast responses due to CLCW check orde…
sneakyaardvark Jun 16, 2026
191e143
fix: bypass frames are incorrectly expected to have SDLS
sneakyaardvark Jun 22, 2026
b618586
style: check and format
sneakyaardvark Jun 22, 2026
f485d8b
test: add uslp tests
sneakyaardvark Jun 22, 2026
004c4ed
fix: SDLS should be ignored for command (BC) frames, not BD frames
sneakyaardvark Jun 22, 2026
798b1c1
feat: cop termination and expedited service for edl_cmd_shell
sneakyaardvark Jun 22, 2026
7aaac83
build: bump ccsds-cop version to 0.1.1
sneakyaardvark Jun 23, 2026
43b6541
wip: fop higher and lower procedures
sneakyaardvark Jun 25, 2026
9f91f63
feat: uplinked CLCW handling
sneakyaardvark Jun 28, 2026
5bb9fd0
feat: full routing from users through FOP and back to router
sneakyaardvark Jun 29, 2026
f149b8f
refactor: use _drain util for fop higher and lower signal queues
sneakyaardvark Jun 29, 2026
ca8f104
feat: define FOP recovery states
sneakyaardvark Jun 29, 2026
439097e
fix: typo in enum name
sneakyaardvark Jun 30, 2026
039b105
fix: frames are not held in queue when in state other than BD_FALLBACK
sneakyaardvark Jun 30, 2026
8de48ec
refactor: use set for request tracking
sneakyaardvark Jun 30, 2026
79ec66e
fix: missing route for IDLE VCID
sneakyaardvark Jun 30, 2026
844c9b2
feat: suspend supervisor when CLCWs stop arriving
sneakyaardvark Jun 30, 2026
15c6769
feat: add recovery directive requests
sneakyaardvark Jun 30, 2026
36e667e
feat: BD_FALLBACK recovery
sneakyaardvark Jun 30, 2026
35cacad
feat: log if FDU transmission totally fails
sneakyaardvark Jun 30, 2026
2f48b12
fix: FDUs are drained when in inactive states
sneakyaardvark Jun 30, 2026
845c289
build: bump ccsds-cop version
sneakyaardvark Jun 30, 2026
7deb4b8
test: add FOP supervision tests
sneakyaardvark Jun 30, 2026
720ace8
refactor: loop through FOP instances once per service loop
sneakyaardvark Jun 30, 2026
6e6bb39
feat: automatic AD service resume when in BD fallback mode
sneakyaardvark Jun 30, 2026
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
25 changes: 15 additions & 10 deletions oresat_c3/protocols/uslp.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ def unpack_frame(raw: bytes) -> TransferFrame:

vcid = ((raw[2] & 0b111) << 3) | ((raw[3] >> 5) & 0b111)

sdls_header_len = get_sdls_header_len(vcid)
is_command = bool(raw[6] & 0x40)
sdls_header_len = 0 if is_command else get_sdls_header_len(vcid)

frame_props = VarFrameProperties(
has_insert_zone=sdls_header_len != 0,
Expand All @@ -92,6 +93,7 @@ def make_frame(
control_word: Optional[bytes] = None,
sequence_number: int = 0,
bypass: bool = False,
command: bool = False,
) -> TransferFrame:
"""Create and pack a USLP

Expand All @@ -113,6 +115,8 @@ def make_frame(
The anti-replay sequence number for SDLS.
bypass
Specify if this frame is bypass (Type-BD) frame.
command
Specify if this frame is for protocol command.

Returns
-------
Expand All @@ -127,29 +131,29 @@ def make_frame(
tfdz=payload,
)

# USLP transfer frame total length - 1
frame_len = len(payload) + PRIMARY_HEADER_LEN + DFH_LEN + FECF_LEN - 1
vcf_count_len = 1 if vcf_count is not None else 0

has_clcw = control_word is not None
if has_clcw:
frame_len += len(control_word)

# USLP transfer frame total length - 1
frame_len = len(payload) + PRIMARY_HEADER_LEN + DFH_LEN + FECF_LEN - 1
frame_len = len(payload) + PRIMARY_HEADER_LEN + vcf_count_len + DFH_LEN + FECF_LEN - 1
if has_clcw:
frame_len += len(control_word)
frame_len += get_sdls_len(vcid)
if not command:
frame_len += get_sdls_len(vcid)

frame_header = PrimaryHeader(
scid=SPACECRAFT_ID,
map_id=0,
vcid=vcid,
src_dest=src_dest,
frame_len=frame_len,
vcf_count_len=bool(vcf_count),
vcf_count_len=vcf_count_len,
vcf_count=vcf_count,
op_ctrl_flag=has_clcw,
prot_ctrl_cmd_flag=ProtocolCommandFlag.USER_DATA,
prot_ctrl_cmd_flag=(
ProtocolCommandFlag.PROTOCOL_INFORMATION if command else ProtocolCommandFlag.USER_DATA
),
bypass_seq_ctrl_flag=(
BypassSequenceControlFlag.EXPEDITED_QOS
if bypass
Expand All @@ -159,6 +163,7 @@ def make_frame(

frame = TransferFrame(header=frame_header, tfdf=tfdf, op_ctrl_field=control_word)

apply_sdls(frame, sequence_number, hmac_key)
if not command:
apply_sdls(frame, sequence_number, hmac_key)

return frame
41 changes: 26 additions & 15 deletions oresat_c3/services/channel_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ def __init__(self, radios_service: RadiosService, cop_service: CopManagerService
self._downlink_routes: dict[EdlVcid, SimpleQueue[bytes]] = {}
self._last_clcw_time = 0.0

def on_start(self) -> None:
self._send_clcws()

def on_loop(self) -> None:
for dl in self._downlink_routes.values():
while True:
Expand All @@ -47,16 +50,7 @@ def on_loop(self) -> None:
if self.node.od["status"].value == C3State.EDL:
now = monotonic()
if now - self._last_clcw_time >= self._CLCW_INTERVAL:
for clcw in self._get_all_clcw():
frame = make_frame(
payload=bytes(1),
vcid=EdlVcid.IDLE,
src_dest=SourceOrDestField.SOURCE,
control_word=clcw.pack(),
)
self._radios_service.send_edl_response(
frame.pack(frame_type=FrameType.VARIABLE)
)
self._send_clcws()
self._last_clcw_time = now

try:
Expand All @@ -66,8 +60,10 @@ def on_loop(self) -> None:

try:
frame = unpack_frame(message)
if frame.op_ctrl_field is not None:
self._cop_service.dispatch_clcw(ControlWord.unpack(frame.op_ctrl_field))
vcid = frame.header.vcid
if vcid in self._uplink_routes:
if vcid != EdlVcid.IDLE and vcid in self._uplink_routes:
self._uplink_routes[vcid].put_nowait(frame)
else:
logger.error(f"No route for VCID {frame.header.vcid}")
Expand All @@ -77,6 +73,16 @@ def on_loop(self) -> None:
except Exception as e:
logger.exception(f"Failed to unpack frame: {e}")

def _send_clcws(self) -> None:
for clcw in self._get_all_clcw():
frame = make_frame(
payload=bytes(1),
vcid=EdlVcid.IDLE,
src_dest=SourceOrDestField.SOURCE,
control_word=clcw.pack(),
)
self._radios_service.send_edl_response(frame.pack(frame_type=FrameType.VARIABLE))

def request_uplink_route(
self, vcid: EdlVcid, use_cop: bool = False
) -> SimpleQueue[TransferFrame]:
Expand Down Expand Up @@ -109,13 +115,15 @@ def request_uplink_route(
self._uplink_routes[vcid] = q
return q

def request_downlink_route(self, vcid: EdlVcid) -> SimpleQueue[bytes]:
def request_downlink_route(self, vcid: EdlVcid, use_cop: bool = False) -> SimpleQueue[bytes]:
"""Request a downlink Virtual Channel route.

Parameters
----------
vcid
The VCID used to identify the route.
use_cop
True enables COP-1 (FOP-1) on this route.

Returns
-------
Expand All @@ -130,11 +138,14 @@ def request_downlink_route(self, vcid: EdlVcid) -> SimpleQueue[bytes]:

if vcid in self._downlink_routes:
raise KeyError(f"Downlink route for VCID={vcid} already exists")
if use_cop:
send_queue: SimpleQueue[bytes] = SimpleQueue()
q = self._cop_service.create_fop_service(vcid, send_queue)
self._downlink_routes[vcid] = send_queue
else:
q: SimpleQueue[bytes] = SimpleQueue()
q = SimpleQueue()
self._downlink_routes[vcid] = q
logger.info(f"Created downlink route for VCID {vcid}")
return q
return q

def _get_all_clcw(self) -> list[ControlWord]:
clcws = []
Expand Down
Loading
Loading