Adding commands
The following is a guide to adding commands to ensure that they work in the python and c sections of this code base. (To navigate quickly, github provides a table of contents via the little list icon on the README header)
Adding commands to the C side
Section titled “Adding commands to the C side”Step 1: Adding the command id
Section titled “Step 1: Adding the command id”- Navigate to
obc_gs_command_id.h
and in thecmd_callback_id_t
enum struct, add your command id. This must be beforeNUM_CMD_CALLBACKS
(since we use it to count the number of commands throughout the codebase). The command should be written in uppercase with underscores as spaces and should be prefixed withCMD
. As an example,CMD_MAPLE_LEAFS
is correct butCmdMapleLeafs
,CMDMAPLELEAFS
,cmd_maple_leafs
are not.
typedef enum { CMD_END_OF_FRAME = 0x00, CMD_EXEC_OBC_RESET, CMD_RTC_SYNC, CMD_DOWNLINK_LOGS_NEXT_PASS, CMD_MICRO_SD_FORMAT, CMD_PING, CMD_DOWNLINK_TELEM, CMD_UPLINK_DISC, CMD_SET_PROGRAMMING_SESSION, CMD_ERASE_APP, CMD_DOWNLOAD_DATA, CMD_VERIFY_CRC, CMD_RESET_BL, // Add command here NUM_CMD_CALLBACKS} cmd_callback_id_t;
Step 2: Adding command data
Section titled “Step 2: Adding command data”If your command has no data associated with it you may skip this step. An example of a command that has data is CMD_RTC_SYNC
which takes a unixtime as data and syncs the board’s RTC to said unixtime.
- If your command does have data, create a struct to house the data in
obc_gs_command_data.h
. Continuing theCMD_RTC_SYNC
example, the following is a struct that houses the unixtime data the command requires…
// CMD_RTC_SYNCtypedef struct { uint32_t unixTime;} rtc_sync_cmd_data_t;
- Then add the struct you just created to the union in
cmd_msg_t
.
typedef struct { union { rtc_sync_cmd_data_t rtcSync; downlink_logs_next_pass_cmd_data_t downlinkLogsNextPass; download_data_cmd_data_t downloadData; set_programming_session_cmd_data_t setProgrammingSession; // Add command struct here };
uint32_t timestamp; // Unix timestamp in seconds bool isTimeTagged; // If true, command will be executed at timestamp
cmd_callback_id_t id; // Command ID} cmd_msg_t;
Step 3: Adding your command’s pack function
Section titled “Step 3: Adding your command’s pack function”Each command has its respective pack function to turn the command into transmittable bytes. The conversion to bytes is done by packing information in the cmd_msg_t
struct in a specific order and then bitshifting the data in certain ways. You can learn more about this by looking at the code in obc_gs_command_pack.c
and data_pack_utils.c
.
- Create a static decleration for the command’s pack function on the line right before the type def that defines
pack_func_t
(typedef void (*pack_func_t)(uint8_t*, uint32_t*, const cmd_msg_t*);
). The delecration should be done in the format used in the code snippet below. Make sure to replaceCmdName
with the name of the command being packed. Additionally, add a comment a line before the delecration that spells out the command enum the function is for.
// CMD_NAMEstatic void packCmdNameCmdData(uint8_t* buffer, uint32_t* offset, const cmd_msg_t* msg);
// Example: Pack function for CMD_RTC_SYNC// CMD_RTC_SYNCstatic void packRtcSyncCmdData(uint8_t* buffer, uint32_t* offset, const cmd_msg_t* msg);
- Now define the actual function body at the end of the file. Again make sure to add a comment the line before with the command enum that the function is for.
// CMD_NAMEstatic void packCmdNameCmdData(uint8_t* buffer, uint32_t* offset, const cmd_msg_t* msg) {
}
// Example: Pack function body for CMD_RTC_SYNC// CMD_RTC_SYNCstatic void packRtcSyncCmdData(uint8_t* buffer, uint32_t* offset, const cmd_msg_t* cmdMsg) {
}
- The manner in which you do this step depends on if the command your packing has additional data or not.
- If your command has no data, simply add a comment saying
// No data to pack
.
// CMD_NAMEstatic void packCmdNameCmdData(uint8_t* buffer, uint32_t* offset, const cmd_msg_t* msg) {// No data to pack}- If your command does have data, then we have to call specific data pack functions to pack said data. Make sure to call the right data pack function for the right datatype. For example,
CMD_RTC_SYNC
has additional data,rtcSync.unixTime
which is auint32_t
. As such, we would call thepackUint32()
function to packrtcSync.unixTime
for us into bytes.
// CMD_RTC_SYNCstatic void packRtcSyncCmdData(uint8_t* buffer, uint32_t* offset, const cmd_msg_t* cmdMsg) {packUint32(cmdMsg->rtcSync.unixTime, buffer, offset);// You can pack any extra data by calling one of the pack functions with the data you want to pack while leaving the rest of the arguments the same} - If your command has no data, simply add a comment saying
Step 4: Adding the your command’s unpack function
Section titled “Step 4: Adding the your command’s unpack function”In addition to a pack function, each command has it’s own unpack function. Contrary to pack, the unpack function decodes an array of bytes into the cmd_msg_t
struct. This is done by just reversing the bitshifts applied. To learn more you can look at obc_gs_command_unpack.c
and data_unpack_utils.c
.
- Create a static decleration for the command’s unpack function on the line right before the type def that defines
unpack_func_t
(typedef void (*unpack_func_t)(const uint8_t*, uint32_t*, cmd_msg_t*);
). The delecration should be done in the format used in the code snippet below. Make sure to replaceCmdName
with the name of the command being packed. Additionally, add a comment a line before the delecration that spells out the command enum the function is for.
// CMD_NAMEstatic void unpackCmdNameCmdData(const uint8_t* buffer, uint32_t* offset, cmd_msg_t* msg);
// Example: Unpack function for CMD_RTC_SYNC// CMD_RTC_SYNCstatic void unpackRtcSyncCmdData(const uint8_t* buffer, uint32_t* offset, cmd_msg_t* msg);
- Now let’s define the actual function body at the end of the file. Again make sure to add a comment the line before with the command enum that the function is for.
// CMD_NAMEstatic void unpackCmdNameCmdData(const uint8_t* buffer, uint32_t* offset, cmd_msg_t* cmdMsg) {
}
// Example: Unpack function body for CMD_RTC_SYNC// CMD_RTC_SYNCstatic void unpackRtcSyncCmdData(const uint8_t* buffer, uint32_t* offset, cmd_msg_t* cmdMsg) {
}
- The manner in which you do this step depends on if the command your unpacking has additional data or not.
- If your command has no data, simply add a comment saying
// No data to unpack
.
// CMD_NAMEstatic void unpackCmdNameCmdData(const uint8_t* buffer, uint32_t* offset, cmd_msg_t* cmdMsg) {// No data to unpack}- If your command has data, then we need to call data unpack functions in a specific order to unpack the necessary data. Like we did in the pack function, make sure to call the right data unpack function for the right datatype. For example,
CMD_RTC_SYNC
has additional data,rtcSync.unixTime
which is auint32_t
. As such, we would call theunpackUint32()
function to packrtcSync.unixTime
for us into bytes. We will be assigning the return of the data unpack functions to their respective variable in thecmdMsg
struct (of typecmd_msg_t
) which is passed into the unpack function as a parameter. Be sure to call the respective data unpack functions in the order you called the data pack functions in the pack function of the commands.
// CMD_RTC_SYNCstatic void unpackRtcSyncCmdData(const uint8_t* buffer, uint32_t* offset, cmd_msg_t* cmdMsg) {cmdMsg->rtcSync.unixTime = unpackUint32(buffer, offset);// You can unpack any extra data by calling additional data unpack functions and assigning their returns to the variables necessary. The arguments passed in remain the same.} - If your command has no data, simply add a comment saying
Step 5: Adding a command callback
Section titled “Step 5: Adding a command callback”To make the command actually do something, the code base uses callbacks. Callbacks, in this case, are simply the functions, with the command’s functionality, that are called when the commands are processed. Since the required commands and the functionality of those commands varies from bootloader to app, we have two files that define callbacks: command_callbacks.c
(for the app) and bl_command_callbacks.c
(for the bootloader).
-
To define a callback first determine if the command will run on the booloader or the app or both. If the command only runs on bootloader, do the following steps in
bl_command_callbacks.c
only; if the command only runs on the app, do the following steps incommand_callbacks.c
; and if the app runs on both the bootloader and app do the following steps in both files. -
Before the definition of the function pointer array,
cmdsConfig[]
define the callback function in the format outlined in the code snippet. Be sure to replacecmdName
with the actual name of the command.
static obc_error_code_t cmdNameCmdCallback(cmd_msg_t *cmd, uint8_t *responseData, uint8_t *responseDataLen) { if (cmd == NULL || responseData == NULL || responseDataLen == NULL) { return OBC_ERR_CODE_INVALID_ARG; }
return OBC_ERR_CODE_SUCCESS;}
- Then in
cmdsConfig
we add the command and it’s respective callback, while specifying some options as seen in the following snippet. ReplaceCMD_NAME
with the command enum andcmdNameCmdCallback
with the correct callback function- You will notice that we specify
CMD_POLICY_PROD
.CMD_POLICY_PROD
is acmd_policy_t
enum that just specifies if the command is for research and development (CMD_POLICY_RND
) or if the command is meant to be used in production (CMD_POLICY_PROD
). While this is currently not implemented, it is good to follow the convention so everything works as expected when this does get implemented. - We also specify
CMD_TYPE_NORMAL
which is acmd_opt_t
enum that defines the type the command. As of writing this procedure there are three command types:CMD_TYPE_NORMAL
(regular commands),CMD_TYPE_CRITICAL
(mission critical commands) andCMD_TYPE_ARO
(aro commands).
- You will notice that we specify
const cmd_info_t cmdsConfig[] = { [CMD_END_OF_FRAME] = {NULL, CMD_POLICY_PROD, CMD_TYPE_NORMAL}, [CMD_EXEC_OBC_RESET] = {execObcResetCmdCallback, CMD_POLICY_PROD, CMD_TYPE_CRITICAL}, [CMD_RTC_SYNC] = {rtcSyncCmdCallback, CMD_POLICY_PROD, CMD_TYPE_NORMAL}, [CMD_DOWNLINK_LOGS_NEXT_PASS] = {downlinkLogsNextPassCmdCallback, CMD_POLICY_PROD, CMD_TYPE_CRITICAL}, [CMD_MICRO_SD_FORMAT] = {microSDFormatCmdCallback, CMD_POLICY_PROD, CMD_TYPE_CRITICAL}, [CMD_PING] = {pingCmdCallback, CMD_POLICY_PROD, CMD_TYPE_NORMAL}, [CMD_DOWNLINK_TELEM] = {downlinkTelemCmdCallback, CMD_POLICY_PROD, CMD_TYPE_NORMAL}, [CMD_I2C_PROBE] = {I2CProbeCmdCallback, CMD_POLICY_PROD, CMD_TYPE_NORMAL}, [CMD_NAME] = {cmdNameCmdCallback, CMD_POLICY_PROD, CMD_TYPE_NORMAL},};
- Finally, you can add the functionality of your command in the callback function body.
static obc_error_code_t cmdNameCmdCallback(cmd_msg_t *cmd, uint8_t *responseData, uint8_t *responseDataLen) { if (cmd == NULL || responseData == NULL || responseDataLen == NULL) { return OBC_ERR_CODE_INVALID_ARG; }
// Implement functionality here
return OBC_ERR_CODE_SUCCESS;}
Step 6: Adding a command response
Section titled “Step 6: Adding a command response”Each command sends back its own command response. In this response you have an option to add data. If you do not have any data to send back as a response to the command, you can skip this step.
- Specify the length of the data that you want to send back by setting the
responseDataLen
variable.
static obc_error_code_t cmdNameCmdCallback(cmd_msg_t *cmd, uint8_t *responseData, uint8_t *responseDataLen) { if (cmd == NULL || responseData == NULL || responseDataLen == NULL) { return OBC_ERR_CODE_INVALID_ARG; }
// You can change 200 to be any number from 1 to 220 *resonseDataLen = 200; // Implement functionality here
return OBC_ERR_CODE_SUCCESS;}
- Then you can populate data by using the
responseData
pointer as an array pointer.
static obc_error_code_t cmdNameCmdCallback(cmd_msg_t *cmd, uint8_t *responseData, uint8_t *responseDataLen) { if (cmd == NULL || responseData == NULL || responseDataLen == NULL) { return OBC_ERR_CODE_INVALID_ARG; }
// Example of setting one byte *resonseDataLen = 1; responseData[0] = 0xFF;
// Implement functionality here
return OBC_ERR_CODE_SUCCESS;}
Adding commands to the Python side
Section titled “Adding commands to the Python side”For our ground station to effectively use commands, we re-define and add some logic for commands in the python side of our code base. However, the core functions are actually just wrapped from their c implementations using ctypes
.
Step 1: Adding the command id
Section titled “Step 1: Adding the command id”Just like the c-side, the python side also defines an enum for the commands.
- Navigate to
commands/__init__.py
and find theCmdCallbackId
struct. Insert your command just beforeNUM_CMD_CALLBACKS
and change the enum integer assignments so that the integers remain consecutive (in python you have to explicitly assign the integers that C just assigns implicitly). You can replaceCMD_NAME
with your command enum name.
# Original Structureclass CmdCallbackId(IntEnum): """ Enums corresponding to the C implementation of cmd_callback_id_t """
CMD_END_OF_FRAME = 0 CMD_EXEC_OBC_RESET = 1 CMD_RTC_SYNC = 2 CMD_DOWNLINK_LOGS_NEXT_PASS = 3 CMD_MICRO_SD_FORMAT = 4 CMD_PING = 5 CMD_DOWNLINK_TELEM = 6 CMD_UPLINK_DISC = 7 CMD_SET_PROGRAMMING_SESSION = 8 CMD_ERASE_APP = 9 CMD_DOWNLOAD_DATA = 10 CMD_VERIFY_CRC = 11 CMD_RESET_BL = 12 NUM_CMD_CALLBACKS = 13
# Structure with added command (Notice how the numbering changes)class CmdCallbackId(IntEnum): """ Enums corresponding to the C implementation of cmd_callback_id_t """
CMD_END_OF_FRAME = 0 CMD_EXEC_OBC_RESET = 1 CMD_RTC_SYNC = 2 CMD_DOWNLINK_LOGS_NEXT_PASS = 3 CMD_MICRO_SD_FORMAT = 4 CMD_PING = 5 CMD_DOWNLINK_TELEM = 6 CMD_UPLINK_DISC = 7 CMD_SET_PROGRAMMING_SESSION = 8 CMD_ERASE_APP = 9 CMD_DOWNLOAD_DATA = 10 CMD_VERIFY_CRC = 11 CMD_RESET_BL = 12 CMD_NAME = 13 NUM_CMD_CALLBACKS = 14
Step 2: Adding command data
Section titled “Step 2: Adding command data”Just like in the C version, we need to tell python the data that a command may store. This definition helps ctypes
wrap the c-side functions.
- Locate the union class named
_U
. Before that class you can add the data that your command will store as a child class of thectypes
classStructure
. Continuing the example ofCMD_RTC_SYNC
, it’s data would be defined as the following in python:
class RtcSyncCmdData(Structure): """ The python equivalent class for the rtc_sync_cmd_data_t structure in the C implementation """
_fields_ = [("unixTime", c_uint32)]
- Now go to the child class of the
ctypes
Union
class named_U
and add the class you just created to store the command’s data as a field which is a tuple of a string name and the corresponding class.
class _U(Union): """ Union class needed to create the CmdMsgType Class """
_fields_ = [ # Note how we define a tuple with the name of the variable on the c-side # as a string and then we add it's corresponding python class. ("rtcSync", RtcSyncCmdData), ("downlinkLogsNextPass", DownlinkLogsNextPassCmdData), ("downloadData", DownloadDataCmdData), ("setProgrammingSession", SetProgrammingSessionCmdData), ]
Step 3: Adding a command factory function
Section titled “Step 3: Adding a command factory function”To integrate the commands in a more pythonic style, we create command factories which are functions that take in specific commands parameters and return the generated CmdMsg
class. Note, CmdMsg
is the python equivalent of the cmd_msg_t
class on the c-side.
- Locate the command block header (the big rectangles of hashtags with text in the middle) and locate the one that says “Command Pack and Unpack Implementations”. Right before that block we can start creating the command factory function as seen in the code snippet below. Be sure to replace
cmd_name
with the name of the command your creating the factory for and update the doc string accordingly (refer to the mainREADME.md
for docstring style).
def create_cmd_name(unixtime_of_execution: int | None = None) -> CmdMsg: """ Function to create a CmdMsg structure for CMD_NAME
:param unixtime_of_execution: A time of when to execute a certain event, by default, it is set to None (i.e. a specific time is not needed) :return: CmdMsg structure for CMD_NAME """ cmd_msg = CmdMsg(unixtime_of_execution) cmd_msg.id = CmdCallbackId.CMD_NAME return cmd_msg
- If your command does not have any associated data with it, you can skip this step. If your command does have data we need to add that to your function.
- Define the data your command takes in the function parameters. Be sure to just use regular python types here. If your command has any integer data fields, no matter the size, declare it as an
int
when writing a function parameter to define it. Remember to update the docstring as well. Again usingCMD_RTC_SYNC
as an example…
# Note how the time parameter is defined as a regular intdef create_cmd_rtc_sync(time: int, unixtime_of_execution: int | None = None) -> CmdMsg:"""Function to create a CmdMsg structure for CMD_RTC_SYNC:param time: Unixtime as an integer:param unixtime_of_execution: A time of when to execute a certain event,by default, it is set to None (i.e. a specifictime is not needed):return: CmdMsg structure for CMD_RTC_SYNC"""cmd_msg = CmdMsg(unixtime_of_execution)cmd_msg.id = CmdCallbackId.CMD_RTC_SYNCreturn cmd_msg- This step only applies if your integer data field is less than 32 bits (i.e.
uint8_t
oruint16_t
). To the function, we need to add some validation that verifies that the number passed into the function can actually be stored as an 8-bit or 16-bit integer, otherwise ctypes will throw an error. Note, we did not need to do this forCMD_RTC_SYNC
because, by default, a python int is 32-bit. Thus, for this example we will be using another command calledCMD_DOWNLINK_LOGS_NEXT_PASS
which takes in alogLevel
that is defined as auint8_t
on the c-side. To validate the value passed into the function we add a simple conditional that throws aValueError
if the number is too large as seen int he code snippet below:
# Notice the log level parameter follows python naming and is defined as a regular python intdef create_cmd_downlink_logs_next_pass(log_level: int, unixtime_of_execution: int | None = None) -> CmdMsg:"""Function to create a CmdMsg structure for CMD_DOWNLINK_LOGS_NEXT_PASS:param log_level: The Log Level for the logs:param unixtime_of_execution: A time of when to execute a certain event,by default, it is set to None (i.e. a specifictime is not needed):return: CmdMsg structure for CMD_DOWNLINK_LOGS_NEXT_PASS"""# The conditional checks if the number passed in is larger than what a uint8_t can actually stores# and throws a ValueError with a message (Don't make these messages too long but they should be# descriptive so that developers can debug)if log_level > 255:raise ValueError("Log level passed is too large (cannot be encoded into a c_uint8)")cmd_msg = CmdMsg(unixtime_of_execution)cmd_msg.id = CmdCallbackId.CMD_DOWNLINK_LOGS_NEXT_PASS# Don't worry, this is explained in the next stepcmd_msg.downlinkLogsNextPass.logLevel = c_uint8(log_level)return cmd_msg- Finally, we assign the parameter to the right variable in the
cmd_msg
class variable defined in the function. You’ll notice that the data fields incmd_msg
are made of types fromctypes
while the parameter is a regular pythonint
. Thus we use functions provided byctypes
to convert them to the right type. Note that these functions will fail if the value being converted is invalid (i.e. too large/small). ForCMD_RTC_SYNC
, this means converting thetime
variable to ac_uint32
using thec_uint32()
function. Note that we access the variable by using the string names we defined in the tuples for thectypes
class_fields_
and regular python dot syntax.
- Define the data your command takes in the function parameters. Be sure to just use regular python types here. If your command has any integer data fields, no matter the size, declare it as an
```pythondef create_cmd_rtc_sync(time: int, unixtime_of_execution: int | None = None) -> CmdMsg: """ Function to create a CmdMsg structure for CMD_RTC_SYNC
:param time: Unixtime as an integer :param unixtime_of_execution: A time of when to execute a certain event, by default, it is set to None (i.e. a specific time is not needed) :return: CmdMsg structure for CMD_RTC_SYNC """ cmd_msg = CmdMsg(unixtime_of_execution) cmd_msg.id = CmdCallbackId.CMD_RTC_SYNC # Here we convert and assign the parameter. # If you recall, we defined the union field and gave it the name "rtcSync" # and then gave the variable in that class the name "unixTime", corresponding # to the c names. cmd_msg.rtcSync.unixTime = c_uint32(time) return cmd_msg```
Step 4: Adding command responses
Section titled “Step 4: Adding command responses”If the command response being sent by the command has no data, you can skip this step. Otherwise we define a class in python to store the command response data nicely and in a more usable format.
- Locate the
command_response_classes.py
file. Right after the last class we can define a new one in the format specified in the code snippet. Make sure to replaceCmdName
andCMD_NAME
with the name of your command. Note, this is a child class of the base class used to store command responses,CmdRes
, and is defined with thedataclass
decorator which automatically handles some boilerplate functions for us. Additionally, we override the string method to help us better format the response (this is especially useful with the ground station CLI).
@dataclassclass CmdCmdNameRes(CmdRes): """ Class for storing the response to CMD_NAME """
def __str__(self) -> str: """ Overriding the str method for a better representation of what's happening """ formatted_string = super().__str__() return formatted_string
- Before the override for the string method, add in the data fields that your command response will contain. Note, we do add a type hint since this is specifically a dataclass. Note, these type hints are python types, not types define by
ctypes
. For example,CMD_RTC_SYNC
will send a response that contains the unixtime of the board when the command was sent. Make sure to update the docstring as seen in the code snippet below:
@dataclassclass CmdRtcSyncRes(CmdRes): """ Child class for storing the response to the CMD_RTC_SYNC :param board_unixtime: The time on the board when the sync command was sent :type board_unixtime: int """
board_unixtime: int
def __str__(self) -> str: """ Overriding the str method for a better representation of what's happening """ formatted_string = super().__str__()
return formatted_string
- Now we fully define the string override by appending the data the class has as a formatted string to the
formatted_string
variable. Ideally each variable is it’s own line in theformatted_string
. For example, forCMD_RTC_SYNC
we add the unixtime of the board to theformatted_string
. Remember to add the newline character so that each of the variables are formatted as their own lines.
@dataclassclass CmdRtcSyncRes(CmdRes): """ Child class for storing the response to the CMD_RTC_SYNC :param board_unixtime: The time on the board when the sync command was sent :type board_unixtime: int """
board_unixtime: int
def __str__(self) -> str: """ Overriding the str method for a better representation of what's happening """ formatted_string = super().__str__() # We just add the unixtime from the board into the string formatted_string += "Unixtime from Board: " + str(self.board_unixtime) + "\n"
return formatted_string
Step 5: Parsing command responses
Section titled “Step 5: Parsing command responses”If your command response has no data, you can skip this step. If your command response does have data we have to define a parsing callback function since the python side will receive the data as a array of bytes (or bytestring in python).
- Locate the
command_response_callbacks.py
. Before theparse_func_dict
we can define the function callback as seen in the code snippet below. Make sure to replaceCmdName
,name
,cmd_name
andCMD_NAME
with the name of the command your parsing a response for.
def parse_cmd_name(cmd_response: CmdRes, data: bytes) -> CmdCmdNameRes: """ A function to parse the raw data from the response of CMD_NAME :param cmd_response: Basic command response :param data: The raw bytes containing the data that needs to be parsed :return: CmdCmdNameRes (i.e. A command response with no data for CMD_NAME) """ if cmd_response.cmd_id != CmdCallbackId.CMD_NAME: raise ValueError("Wrong command id for parsing the name command")
return CmdCmdNameRes(cmd_response.cmd_id, cmd_response.error_code, cmd_response.response_length)
- In between the conditional and the return, add in the logic that takes a bytestring from the
data
parameter and converts it to data that can be stored in the command’s respective response class. An example for the command response forCMD_RTC_SYNC
is provided below.
def parse_cmd_rtc_sync(cmd_response: CmdRes, data: bytes) -> CmdRtcSyncRes: """ A function to parse the raw data from the response of CMD_RTC_SYNC :param cmd_response: Basic command response :param data: The raw bytes containing the data that needs to be parsed :return: CmdRes (i.e. A command response with no data for CMD_RTC_SYNC) """ # TODO: Implement this callback properly if cmd_response.cmd_id != CmdCallbackId.CMD_RTC_SYNC: raise ValueError("Wrong command id for parsing the rtc sync command")
# The first four bytes store an integer defining the board's unixtime # so we convert it back to an integer board_unixtime = int.from_bytes(data[:4], "little")
# We use the data that we just created and construct the command response class for CMD_RTC_SYNC return CmdRtcSyncRes(cmd_response.cmd_id, cmd_response.error_code, cmd_response.response_length, board_unixtime)
- Add the function callback you just created to the
parse_func_dict
, using the command enum in python as an index.
parse_func_dict: dict[CmdCallbackId, Callable[..., CmdRes]] = defaultdict(lambda: parse_cmd_with_no_data)parse_func_dict[CmdCallbackId.CMD_VERIFY_CRC] = parse_cmd_verify_crcparse_func_dict[CmdCallbackId.CMD_RTC_SYNC] = parse_cmd_rtc_syncparse_func_dict[CmdCallbackId.CMD_I2C_PROBE] = parse_cmd_i2c_probe# Add entry here
Step 6: Adding functions to command utils
Section titled “Step 6: Adding functions to command utils”We need to add the command factory functions to a specific list in our command utils file so that everything works as intended.
- Locate
command_utils.py
. In thegenerate_commands
function, there will be acommand_factories
list. Add the command factory for your newly created command there.
def generate_command(args: str) -> CmdMsg | None: """ A function that parsed command arguments and returns the corresponding command frame
:param args: The arguments to parse to create the command :return: CmdMsg structure with the requested command if the command is valid, else none """ arguments = args.split() command = CmdMsg()
# These are a list of parsers for commands that require additional arguments # NOTE: Update this list when another command with a specific parser is required child_parsers = [parse_cmd_downlink_logs_next_pass, parse_cmd_rtc_time_sync]
# A list of Command factories for all commands # NOTE: Update these when a command is added and make sure to keep them in the order that the commands are described # in the CmdCallbackId Enum commmand_factories: list[Callable[..., CmdMsg]] = [ create_cmd_end_of_frame, create_cmd_exec_obc_reset, create_cmd_rtc_sync, create_cmd_downlink_logs_next_pass, create_cmd_mirco_sd_format, create_cmd_ping, create_cmd_downlink_telem, create_cmd_uplink_disc, # Add command factory function here ]
That was a long list of steps that hopefully will be simplified in the future. But, you’ve successfully added your command. If something is wrong with the guide or something needs changing, bring it up on the discord!