diff --git a/FprimeZephyrReference/Components/CMakeLists.txt b/FprimeZephyrReference/Components/CMakeLists.txt index 6a0b32d..b881a4a 100644 --- a/FprimeZephyrReference/Components/CMakeLists.txt +++ b/FprimeZephyrReference/Components/CMakeLists.txt @@ -1,6 +1,7 @@ # Include project-wide components here add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/Drv/") +add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ComDelay/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/FatalHandler") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ImuManager/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/NullPrmDb/") diff --git a/FprimeZephyrReference/Components/ComDelay/CMakeLists.txt b/FprimeZephyrReference/Components/ComDelay/CMakeLists.txt new file mode 100644 index 0000000..5f5d5ce --- /dev/null +++ b/FprimeZephyrReference/Components/ComDelay/CMakeLists.txt @@ -0,0 +1,36 @@ +#### +# F Prime CMakeLists.txt: +# +# SOURCES: list of source files (to be compiled) +# AUTOCODER_INPUTS: list of files to be passed to the autocoders +# DEPENDS: list of libraries that this module depends on +# +# More information in the F´ CMake API documentation: +# https://fprime.jpl.nasa.gov/latest/docs/reference/api/cmake/API/ +# +#### + +# Module names are derived from the path from the nearest project/library/framework +# root when not specifically overridden by the developer. i.e. The module defined by +# `Ref/SignalGen/CMakeLists.txt` will be named `Ref_SignalGen`. + +register_fprime_library( + AUTOCODER_INPUTS + "${CMAKE_CURRENT_LIST_DIR}/ComDelay.fpp" + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/ComDelay.cpp" +# DEPENDS +# MyPackage_MyOtherModule +) + +### Unit Tests ### +# register_fprime_ut( +# AUTOCODER_INPUTS +# "${CMAKE_CURRENT_LIST_DIR}/ComDelay.fpp" +# SOURCES +# "${CMAKE_CURRENT_LIST_DIR}/test/ut/ComDelayTestMain.cpp" +# "${CMAKE_CURRENT_LIST_DIR}/test/ut/ComDelayTester.cpp" +# DEPENDS +# STest # For rules-based testing +# UT_AUTO_HELPERS +# ) diff --git a/FprimeZephyrReference/Components/ComDelay/ComDelay.cpp b/FprimeZephyrReference/Components/ComDelay/ComDelay.cpp new file mode 100644 index 0000000..2d1c073 --- /dev/null +++ b/FprimeZephyrReference/Components/ComDelay/ComDelay.cpp @@ -0,0 +1,71 @@ +// ====================================================================== +// \title ComDelay.cpp +// \author starchmd +// \brief cpp file for ComDelay component implementation class +// ====================================================================== + +#include "FprimeZephyrReference/Components/ComDelay/ComDelay.hpp" +#include "FprimeZephyrReference/Components/ComDelay/FppConstantsAc.hpp" + +namespace Components { + +// ---------------------------------------------------------------------- +// Component construction and destruction +// ---------------------------------------------------------------------- + +ComDelay ::ComDelay(const char* const compName) + : ComDelayComponentBase(compName), m_last_status_valid(false), m_last_status(Fw::Success::FAILURE) {} + +ComDelay ::~ComDelay() {} + +void ComDelay ::parameterUpdated(FwPrmIdType id) { + switch (id) { + case ComDelay::PARAMID_DIVIDER: { + Fw::ParamValid is_valid; + U8 new_divider = this->paramGet_DIVIDER(is_valid); + if ((is_valid != Fw::ParamValid::INVALID) && (is_valid != Fw::ParamValid::UNINIT)) { + this->log_ACTIVITY_HI_DividerSet(new_divider); + } + } break; + default: + FW_ASSERT(0); + break; // Fallthrough from assert (static analysis) + } +} + +// ---------------------------------------------------------------------- +// Handler implementations for typed input ports +// ---------------------------------------------------------------------- + +void ComDelay ::comStatusIn_handler(FwIndexType portNum, Fw::Success& condition) { + this->m_last_status = condition; + this->m_last_status_valid = true; +} + +void ComDelay ::run_handler(FwIndexType portNum, U32 context) { + // On the cycle after the tick count is reset, attempt to output any current com status + if (this->m_tick_count == 0) { + bool expected = true; + // Receive the current "last status" validity flag and atomically exchange it with false. This effectively + // "consumes" a valid status. When valid, the last status is sent out. + bool valid = this->m_last_status_valid.compare_exchange_strong(expected, false); + if (valid) { + this->comStatusOut_out(0, this->m_last_status); + this->timeout_out(0, 0); + } + } + + // Unless there is corruption, the parameter should always be valid via its default value; however, in the interest + // of failing-safe and continuing some sort of communication we default the current_divisor to the default value. + Fw::ParamValid is_valid; + U8 current_divisor = this->paramGet_DIVIDER(is_valid); + + // Increment and module the tick count by the divisor + if ((is_valid == Fw::ParamValid::INVALID) || (is_valid == Fw::ParamValid::UNINIT)) { + current_divisor = Components::DEFAULT_DIVIDER; + } + // Count this new tick, resetting whenever the current count is at or higher than the current divider. + this->m_tick_count = (this->m_tick_count >= current_divisor) ? 0 : this->m_tick_count + 1; +} + +} // namespace Components diff --git a/FprimeZephyrReference/Components/ComDelay/ComDelay.fpp b/FprimeZephyrReference/Components/ComDelay/ComDelay.fpp new file mode 100644 index 0000000..6c14f0c --- /dev/null +++ b/FprimeZephyrReference/Components/ComDelay/ComDelay.fpp @@ -0,0 +1,51 @@ +module Components { + constant DEFAULT_DIVIDER = 30 + @ A component to delay com status until some further point + passive component ComDelay { + @ Rate schedule port used to trigger radio transmission + sync input port run: Svc.Sched + + @ Rate schedule port used to trigger aggregation timeout + output port timeout: Svc.Sched + + @ Input comStatus from radio component + sync input port comStatusIn: Fw.SuccessCondition + + @ Output comStatus to be called on rate group + output port comStatusOut: Fw.SuccessCondition + + @ Divider of the incoming rate tick + param DIVIDER: U8 default DEFAULT_DIVIDER # Start slow i.e. on a 1S tick, transmit every 30S + + @ Divider set event + event DividerSet(divider: U8) severity activity high \ + format "Set divider to: {}" + + ############################################################################### + # Standard AC Ports: Required for Channels, Events, Commands, and Parameters # + ############################################################################### + @ Port for requesting the current time + time get port timeCaller + + @ Port for sending command registrations + command reg port cmdRegOut + + @ Port for receiving commands + command recv port cmdIn + + @ Port for sending command responses + command resp port cmdResponseOut + + @ Port for sending textual representation of events + text event port logTextOut + + @ Port for sending events to downlink + event port logOut + + @ Port to return the value of a parameter + param get port prmGetOut + + @Port to set the value of a parameter + param set port prmSetOut + } +} diff --git a/FprimeZephyrReference/Components/ComDelay/ComDelay.hpp b/FprimeZephyrReference/Components/ComDelay/ComDelay.hpp new file mode 100644 index 0000000..82368b0 --- /dev/null +++ b/FprimeZephyrReference/Components/ComDelay/ComDelay.hpp @@ -0,0 +1,61 @@ +// ====================================================================== +// \title ComDelay.hpp +// \author starchmd +// \brief hpp file for ComDelay component implementation class +// ====================================================================== + +#ifndef Components_ComDelay_HPP +#define Components_ComDelay_HPP + +#include +#include "FprimeZephyrReference/Components/ComDelay/ComDelayComponentAc.hpp" + +namespace Components { + +class ComDelay final : public ComDelayComponentBase { + public: + // ---------------------------------------------------------------------- + // Component construction and destruction + // ---------------------------------------------------------------------- + + //! Construct ComDelay object + ComDelay(const char* const compName //!< The component name + ); + + //! Destroy ComDelay object + ~ComDelay(); + + private: + void parameterUpdated(FwPrmIdType id //!< The parameter ID + ) override; + + // ---------------------------------------------------------------------- + // Handler implementations for typed input ports + // ---------------------------------------------------------------------- + + //! Handler implementation for comStatusIn + //! + //! Input comStatus from radio component + void comStatusIn_handler(FwIndexType portNum, //!< The port number + Fw::Success& condition //!< Condition success/failure + ) override; + + //! Handler implementation for run + //! + //! Rate schedule port used to trigger radio transmission + void run_handler(FwIndexType portNum, //!< The port number + U32 context //!< The call order + ) override; + + private: + //! Count of incoming run ticks + U8 m_tick_count; + //! Stores if the last status is currently valid + std::atomic m_last_status_valid; + //! Stores the last status + Fw::Success m_last_status; +}; + +} // namespace Components + +#endif diff --git a/FprimeZephyrReference/Components/ComDelay/docs/sdd.md b/FprimeZephyrReference/Components/ComDelay/docs/sdd.md new file mode 100644 index 0000000..dded32e --- /dev/null +++ b/FprimeZephyrReference/Components/ComDelay/docs/sdd.md @@ -0,0 +1,17 @@ +# Components::ComDelay + +`Components::ComDelay` is a parameterized rate group schedule divider. On the initial run invocation and on each multiple of the divider thereafter any received com status is sent out. This effectively delays the com status until the next (divided) run call. + +# 1 Requirements + +| Requirement ID | Description | Validation | +|----------------|----------------------------------------------------------------------|------------| +| COM_DELAY_001 | The `Svc::ComDelay` component shall accept com status in. | Unit-Test | +| COM_DELAY_002 | The `Svc::ComDelay` component shall emit com status once for each DIVIDER number of rate group ticks. | Unit-Test | +| COM_DELAY_003 | The `Svc::ComDelay` component shall set the DIVIDER via a parameter. | Unit-Test | + +# 2 Parameters + +| Name | Description | +|---------|-----------------------------------------------------------| +| DIVIDER | Number of rate group ticks received before sending status | diff --git a/FprimeZephyrReference/ReferenceDeployment/Top/instances.fpp b/FprimeZephyrReference/ReferenceDeployment/Top/instances.fpp index 3a20cd6..64925bc 100644 --- a/FprimeZephyrReference/ReferenceDeployment/Top/instances.fpp +++ b/FprimeZephyrReference/ReferenceDeployment/Top/instances.fpp @@ -73,4 +73,6 @@ module ReferenceDeployment { instance gpioBurnwire1: Zephyr.ZephyrGpioDriver base id 0x10023000 instance prmDb: Components.NullPrmDb base id 0x10024000 + + instance comDelay: Components.ComDelay base id 0x10025000 } diff --git a/FprimeZephyrReference/ReferenceDeployment/Top/topology.fpp b/FprimeZephyrReference/ReferenceDeployment/Top/topology.fpp index 5c58f64..e67efb4 100644 --- a/FprimeZephyrReference/ReferenceDeployment/Top/topology.fpp +++ b/FprimeZephyrReference/ReferenceDeployment/Top/topology.fpp @@ -35,6 +35,7 @@ module ReferenceDeployment { instance lis2mdlManager instance lsm6dsoManager instance bootloaderTrigger + instance comDelay instance burnwire # ---------------------------------------------------------------------- @@ -100,7 +101,8 @@ module ReferenceDeployment { rateGroup1Hz.RateGroupMemberOut[3] -> CdhCore.tlmSend.Run rateGroup1Hz.RateGroupMemberOut[4] -> watchdog.run rateGroup1Hz.RateGroupMemberOut[5] -> imuManager.run - rateGroup1Hz.RateGroupMemberOut[6] -> burnwire.schedIn + rateGroup1Hz.RateGroupMemberOut[6] -> comDelay.run + rateGroup1Hz.RateGroupMemberOut[7] -> burnwire.schedIn } diff --git a/Makefile b/Makefile index f5aff55..64fb2f4 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,10 @@ fprime-venv: ## Create a virtual environment @$(MAKE) uv @echo "Creating virtual environment..." @$(UV) venv fprime-venv - @$(UV) pip install --requirement requirements.txt + @$(UV) pip install --prerelease=allow --requirement requirements.txt + +patch-gps-package: + cp custom_space_data_link.py fprime-venv/lib/python3.13/site-packages/fprime_gds/common/communication/ccsds/space_data_link.py .PHONY: zephyr-setup zephyr-setup: fprime-venv ## Set up Zephyr environment diff --git a/circuit-python-lora-passthrough/boot.py b/circuit-python-lora-passthrough/boot.py new file mode 100644 index 0000000..6776ff3 --- /dev/null +++ b/circuit-python-lora-passthrough/boot.py @@ -0,0 +1,3 @@ +import usb_cdc + +usb_cdc.enable(console=True, data=True) diff --git a/circuit-python-lora-passthrough/code.py b/circuit-python-lora-passthrough/code.py new file mode 100644 index 0000000..4efd89f --- /dev/null +++ b/circuit-python-lora-passthrough/code.py @@ -0,0 +1,41 @@ +""" +CircuitPython Feather RP2350 LoRa Radio forwarder + +This code will forward any received LoRa packets to the serial console (sys.stdout). It cycles through neo pixel colors +to indicate packet reception. +""" + +import time + +import adafruit_rfm9x +import board +import digitalio +import usb_cdc + +# Radio constants +RADIO_FREQ_MHZ = 437.4 +CS = digitalio.DigitalInOut(board.SPI0_CS0) +RESET = digitalio.DigitalInOut(board.RF1_RST) + +rfm95 = adafruit_rfm9x.RFM9x(board.SPI(), CS, RESET, RADIO_FREQ_MHZ) +rfm95.spreading_factor = 8 +rfm95.signal_bandwidth = 125000 +rfm95.coding_rate = 5 +rfm95.preamble_length = 8 +time_start = time.time() +packet_count = 0 +print("[INFO] LoRa Receiver receiving packets") +while True: + # Look for a new packet - wait up to 2 seconds: + packet = rfm95.receive(timeout=2.0) + # If no packet was received during the timeout then None is returned. + if packet is not None: + usb_cdc.data.write(packet) + packet_count += 1 + time_delta = time.time() - time_start + if time_delta > 10: + print(f"[INFO] Packets received: {packet_count}") + time_start = time.time() + data = usb_cdc.data.read(usb_cdc.data.in_waiting) + if len(data) > 0: + rfm95.send(data) diff --git a/custom_space_data_link.py b/custom_space_data_link.py new file mode 100644 index 0000000..0439afd --- /dev/null +++ b/custom_space_data_link.py @@ -0,0 +1,198 @@ +"""F Prime Framer/Deframer Implementation of the CCSDS Space Data Link (TC/TM) Protocols""" + +import copy +import struct +import sys + +import crc +from fprime_gds.common.communication.framing import FramerDeframer +from fprime_gds.plugin.definitions import gds_plugin_implementation + + +class SpaceDataLinkFramerDeframer(FramerDeframer): + """CCSDS Framer/Deframer Implementation for the TC (uplink / framing) and TM (downlink / deframing) + protocols. This FramerDeframer is used for framing TC data for uplink and deframing TM data for downlink. + """ + + SEQUENCE_NUMBER_MAXIMUM = 256 + TC_HEADER_SIZE = 5 + TM_HEADER_SIZE = 6 + TM_FIXED_FRAME_SIZE = 248 + TM_TRAILER_SIZE = 2 + TC_TRAILER_SIZE = 2 + + # As per CCSDS standard, use CRC-16 CCITT config with init value + # all 1s and final XOR value of 0x0000 + CRC_CCITT_CONFIG = crc.Configuration( + width=16, + polynomial=0x1021, + init_value=0xFFFF, + final_xor_value=0x0000, + ) + CRC_CALCULATOR = crc.Calculator(CRC_CCITT_CONFIG) + + def __init__(self, scid, vcid): + """ """ + self.scid = scid + self.vcid = vcid + self.sequence_number = 0 + + def frame(self, data): + """Frame the supplied data in a TC frame""" + space_packet_bytes = data + # CCSDS TC protocol defines the length token as number of bytes in full frame, minus 1 + # so we add to packet size the size of the header and trailer and subtract 1 + length = ( + len(space_packet_bytes) + self.TC_HEADER_SIZE + self.TC_TRAILER_SIZE - 1 + ) + assert length < (pow(2, 10) - 1), "Length too-large for CCSDS format" + + # CCSDS TC Header: + # 2b - 00 - TF version number + # 1b - 0/1 - 0 enable FARM checks, 1 bypass FARM + # 1b - 0/1 - 0 = data (Type-D), 1 = control information (Type-C) + # 2b - 00 - Reserved + # 10b - XX - Spacecraft id + # 6b - XX - Virtual Channel ID + # 10b - XX - Frame length + # 8b - XX - Frame sequence number + + # First 16 bits: + header_val1_u16 = ( + (0 << 14) # TF version number (2 bits) + | (1 << 13) # Bypass FARM (1 bit) + | (0 << 12) # Type-D (1 bit) + | (0 << 10) # Reserved (2 bits) + | (self.scid & 0x3FF) # SCID (10 bits) + ) + # Second 16 bits: + header_val2_u16 = ( + ((self.vcid & 0x3F) << 10) # VCID (6 bits) + | (length & 0x3FF) # Frame length (10 bits) + ) + # 8 bit sequence number - always 0 in bypass FARM mode + header_val3_u8 = 0 + header_bytes = struct.pack( + ">HHB", header_val1_u16, header_val2_u16, header_val3_u8 + ) + full_bytes_no_crc = header_bytes + space_packet_bytes + assert len(header_bytes) == self.TC_HEADER_SIZE, ( + "CCSDS primary header must be 5 octets long" + ) + assert len(full_bytes_no_crc) == self.TC_HEADER_SIZE + len(data), ( + "Malformed packet generated" + ) + + full_bytes = full_bytes_no_crc + struct.pack( + ">H", self.CRC_CALCULATOR.checksum(full_bytes_no_crc) + ) + return full_bytes + + def get_sequence_number(self): + """Get the sequence number and increment - used for TM deframing + + This function will return the current sequence number and then increment the sequence number for the next round. + + Return: + current sequence number + """ + sequence = self.sequence_number + self.sequence_number = (self.sequence_number + 1) % self.SEQUENCE_NUMBER_MAXIMUM + return sequence + + def deframe(self, data, no_copy=False): + """Deframe TM frames""" + discarded = b"" + if not no_copy: + data = copy.copy(data) + # Continue until there is not enough data for the header, or until a packet is found (return) + while len(data) >= self.TM_FIXED_FRAME_SIZE: + # Read header information + sc_and_channel_ids = struct.unpack_from(">H", data) + spacecraft_id = (sc_and_channel_ids[0] & 0x3FF0) >> 4 + virtual_channel_id = (sc_and_channel_ids[0] & 0x000E) >> 1 + # Check if the header is correct with regards to expected spacecraft and VC IDs + if spacecraft_id != self.scid or virtual_channel_id != self.vcid: + # If the header is invalid, rotate away a Byte and keep processing + discarded += data[0:1] + data = data[1:] + continue + # Spacecraft ID and Virtual Channel ID match, so we look at end of frame for CRC + crc_offset = self.TM_FIXED_FRAME_SIZE - self.TM_TRAILER_SIZE + transmitted_crc = struct.unpack_from(">H", data, crc_offset)[0] + if transmitted_crc == self.CRC_CALCULATOR.checksum(data[:crc_offset]): + # CRC is valid, so we return the deframed data + deframed_data_len = ( + self.TM_FIXED_FRAME_SIZE + - self.TM_TRAILER_SIZE + - self.TM_HEADER_SIZE + ) + deframed = struct.unpack_from( + f">{deframed_data_len}s", data, self.TM_HEADER_SIZE + )[0] + # Consume the fixed size frame + data = data[self.TM_FIXED_FRAME_SIZE :] + return deframed, data, discarded + + print( + "[WARNING] Checksum validation failed.", + file=sys.stderr, + ) + # Bad checksum, rotate 1 and keep looking for non-garbage + discarded += data[0:1] + data = data[1:] + continue + return None, data, discarded + + @classmethod + def get_arguments(cls): + """Arguments to request from the CLI""" + return { + ("--scid",): { + "type": lambda input_arg: int(input_arg, 0), + "help": "Spacecraft ID", + "default": 0x44, + "required": False, + }, + ("--vcid",): { + "type": lambda input_arg: int(input_arg, 0), + "help": "Virtual channel ID", + "default": 1, + "required": False, + }, + } + + @classmethod + def check_arguments(cls, scid, vcid): + """Check arguments from the CLI + + Confirms that the input arguments are valid for this framer/deframer. + + Args: + scid: spacecraft id + vcid: virtual channel id + """ + if scid is None: + raise TypeError("Spacecraft ID not specified") + if scid < 0: + raise TypeError(f"Spacecraft ID {scid} is negative") + if scid > 0x3FF: + raise TypeError(f"Spacecraft ID {scid} is larger than {0x3FF}") + + if vcid is None: + raise TypeError("Virtual Channel ID not specified") + if vcid < 0: + raise TypeError(f"Virtual Channel ID {vcid} is negative") + if vcid > 0x3F: + raise TypeError(f"Virtual Channel ID {vcid} is larger than {0x3FF}") + + @classmethod + def get_name(cls): + """Name of this implementation provided to CLI""" + return "raw-space-data-link" + + @classmethod + @gds_plugin_implementation + def register_framing_plugin(cls): + """Register the MyPlugin plugin""" + return cls diff --git a/fprime-gds.yaml b/fprime-gds.yaml deleted file mode 100644 index 0e1f0e2..0000000 --- a/fprime-gds.yaml +++ /dev/null @@ -1,4 +0,0 @@ -command-line-options: - communication-selection: uart - uart-baud: 115200 - no-app: diff --git a/fprime-gds.yml b/fprime-gds.yml new file mode 100644 index 0000000..7145707 --- /dev/null +++ b/fprime-gds.yml @@ -0,0 +1,6 @@ +command-line-options: + communication-selection: uart + uart-baud: 115200 + no-app: + dictionary: build-artifacts/zephyr/fprime-zephyr-deployment/dict/ReferenceDeploymentTopologyDictionary.json + output-unframed-data: "-" diff --git a/prj.conf b/prj.conf index 96d07ad..f171594 100644 --- a/prj.conf +++ b/prj.conf @@ -50,7 +50,7 @@ CONFIG_COMMON_LIBC_MALLOC=y CONFIG_SENSOR=y -CONFIG_LOG=y +CONFIG_LOG=n CONFIG_LOG_DEFAULT_LEVEL=3 CONFIG_CBPRINTF_FP_SUPPORT=y