Skip to content

Commit a0d78fb

Browse files
authored
Add Command Responses (#523)
# Purpose This PR closes #508 and #528. This PR adds a command response flow to the app with an additional command called `CMD_I2C_PROBE` # New Changes - Refactored `downlink_encoder.c` to support sending command responses - Added command responses to the bootloaders - Updated `app_update.py` to support bootloader command responses - Implemented command parsing callbacks and classes on python side - Implemented `packCmdResponse` and `unpackCmdResponse` - Changed the C++ tests for command responses to reflect the new updates to the command response infrastructure. - Implemented python side for command responses with command response classes and callbacks - Fixed a ground station cli bug where a help command would exit the program # Testing Explain tests that you ran to verify code functionality. - [x] I have unit-tested this PR. Otherwise, explain why it cannot be unit-tested. - [x] I have tested this PR on a board if the code will run on a board (Only required for firmware developers). - [x] I have tested this PR by running the ARO website (Only required if the code will impact the ARO website). - [x] I have tested this PR by running the MCC website (Only required if the code will impact the MCC website). - [ ] I have included screenshots of the tests performed below. # Outstanding Changes - [x] Add command responses to bootloader once #431 is merged - Investigate 'COMMAND_I2C_PROBE' --------- Co-authored-by: Remote-Flashing-Orbital <flashing!>
1 parent 717141e commit a0d78fb

27 files changed

+696
-275
lines changed

gs/backend/ground_station_cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from serial import Serial, SerialException
77

88
from gs.backend.obc_utils.command_utils import LOG_PATH, arg_parse, poll, send_command, send_conn_request
9+
from interfaces.obc_gs_interface.commands import CmdCallbackId
910

1011

1112
class GroundStationShell(Cmd):
@@ -85,7 +86,10 @@ def do_send_command(self, line: str) -> None:
8586
if self.background_logging is not None:
8687
self.background_logging.kill()
8788

88-
send_command(line, self._com_port, 1)
89+
cmd_response = send_command(line, self._com_port, 1)
90+
print(cmd_response)
91+
if cmd_response is not None and cmd_response.cmd_id == CmdCallbackId.CMD_EXEC_OBC_RESET:
92+
self._conn_request_sent = False
8993

9094
self._restart_logging()
9195

gs/backend/obc_utils/command_utils.py

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,29 @@
99
from gs.backend.obc_utils.encode_decode import CommsPipeline
1010
from interfaces import (
1111
OBC_UART_BAUD_RATE,
12+
RS_DECODED_DATA_SIZE,
1213
)
1314
from interfaces.command_framing import command_multi_pack
1415
from interfaces.obc_gs_interface.commands import (
1516
CmdCallbackId,
1617
CmdMsg,
18+
CmdResponseErrorCode,
1719
create_cmd_downlink_logs_next_pass,
1820
create_cmd_downlink_telem,
21+
create_cmd_download_data,
1922
create_cmd_end_of_frame,
23+
create_cmd_erase_app,
2024
create_cmd_exec_obc_reset,
25+
create_cmd_i2c_probe,
2126
create_cmd_mirco_sd_format,
2227
create_cmd_ping,
2328
create_cmd_rtc_sync,
29+
create_cmd_set_programming_session,
2430
create_cmd_uplink_disc,
31+
create_cmd_verify_crc,
2532
)
33+
from interfaces.obc_gs_interface.commands.command_response_callbacks import parse_command_response
34+
from interfaces.obc_gs_interface.commands.command_response_classes import CmdRes
2635

2736
# This is a constant value set in the python and OBC side as to what length of I Frame the OBC will be waiting to
2837
# receive. This must be followed or the obc will not function as expected
@@ -31,7 +40,7 @@
3140
LOG_PATH: Path = (Path(__file__).parent / "../logs.log").resolve()
3241

3342

34-
def send_command(args: str, com_port: str, timeout: int = 0) -> Frame | None:
43+
def send_command(args: str, com_port: str, timeout: int = 0) -> CmdRes | type[CmdRes] | None:
3544
"""
3645
A function to send a command up to the cube satellite and awaits a response
3746
@@ -40,7 +49,7 @@ def send_command(args: str, com_port: str, timeout: int = 0) -> Frame | None:
4049
:return: A decoded frame if command is valid and has a response else None
4150
"""
4251
# Using generate commands, we generate a command based on the arguments passed in
43-
command = generate_command(args)
52+
command, is_timetagged = generate_command(args)
4453

4554
# We do a check to see if the arguments were properly passed in otherwise we return None
4655
if command is None:
@@ -76,6 +85,10 @@ def send_command(args: str, com_port: str, timeout: int = 0) -> Frame | None:
7685
start_index = read_bytes.find(b"\x7e")
7786
end_index = read_bytes.rfind(b"\x7e")
7887

88+
if command.id == CmdCallbackId.CMD_EXEC_OBC_RESET.value:
89+
print(read_bytes)
90+
return CmdRes(CmdCallbackId.CMD_EXEC_OBC_RESET, CmdResponseErrorCode.CMD_RESPONSE_SUCCESS, 0)
91+
7992
# Check if a frame is what is sent back
8093
if start_index != -1:
8194
# These are all the bytes from other tasks that are not a part of the frame
@@ -89,10 +102,16 @@ def send_command(args: str, com_port: str, timeout: int = 0) -> Frame | None:
89102

90103
rcv_frame = comms.decode_frame(rcv_frame_bytes)
91104
# TODO: Handle these return frames
92-
return rcv_frame
105+
if rcv_frame is not None and not is_timetagged:
106+
return parse_command_response(rcv_frame.data[:RS_DECODED_DATA_SIZE])
107+
else:
108+
return None
109+
elif is_timetagged:
110+
print("Command is time tagged, enable and check logs for a response")
111+
return None
93112
else:
94113
# TODO: Handle bootloader recieve
95-
return None
114+
return parse_command_response(read_bytes[:RS_DECODED_DATA_SIZE])
96115

97116

98117
def send_conn_request(com_port: str, timeout: int = 0) -> Frame:
@@ -174,7 +193,7 @@ def parse_cmd_rtc_time_sync() -> ArgumentParser:
174193
A function to parse the argument for the rtc_time_sync command
175194
"""
176195
parent_parser = arg_parse()
177-
parser = ArgumentParser(parents=[parent_parser], exit_on_error=False)
196+
parser = ArgumentParser(parents=[parent_parser], add_help=False, exit_on_error=False)
178197
parser.add_argument(
179198
"-rtc",
180199
"--rtc_sync_time",
@@ -191,7 +210,7 @@ def parse_cmd_downlink_logs_next_pass() -> ArgumentParser:
191210
A function to parse the argument for the downlink_logs_next_pass command
192211
"""
193212
parent_parser = arg_parse()
194-
parser = ArgumentParser(parents=[parent_parser], exit_on_error=False)
213+
parser = ArgumentParser(parents=[parent_parser], add_help=False, exit_on_error=False)
195214
parser.add_argument(
196215
"-lnp",
197216
"--log_next_pass",
@@ -206,7 +225,7 @@ def parse_cmd_downlink_logs_next_pass() -> ArgumentParser:
206225
# End of specific command parsers
207226

208227

209-
def generate_command(args: str) -> CmdMsg | None:
228+
def generate_command(args: str) -> tuple[CmdMsg | None, bool]:
210229
"""
211230
A function that parsed command arguments and returns the corresponding command frame
212231
@@ -219,6 +238,7 @@ def generate_command(args: str) -> CmdMsg | None:
219238
# These are a list of parsers for commands that require additional arguments
220239
# NOTE: Update this list when another command with a specific parser is required
221240
child_parsers = [parse_cmd_downlink_logs_next_pass, parse_cmd_rtc_time_sync]
241+
is_timetagged = False
222242

223243
# A list of Command factories for all commands
224244
# NOTE: Update these when a command is added and make sure to keep them in the order that the commands are described
@@ -232,6 +252,11 @@ def generate_command(args: str) -> CmdMsg | None:
232252
create_cmd_ping,
233253
create_cmd_downlink_telem,
234254
create_cmd_uplink_disc,
255+
create_cmd_set_programming_session,
256+
create_cmd_erase_app,
257+
create_cmd_download_data,
258+
create_cmd_verify_crc,
259+
create_cmd_i2c_probe,
235260
]
236261

237262
# Loop through each of the specific parses and see if we get a valid parse on any of them
@@ -248,7 +273,7 @@ def generate_command(args: str) -> CmdMsg | None:
248273
command_enum = CmdCallbackId[command_args.command]
249274
except KeyError:
250275
print("Invalid Command")
251-
return None
276+
return None, False
252277

253278
# We check how many arguments are in the parsed object and call functions accordingly.
254279
# This is the reason why it's important to use the arg1, arg2, arg3 naming convention when creating
@@ -266,25 +291,31 @@ def generate_command(args: str) -> CmdMsg | None:
266291
)
267292
elif hasattr(command_args, "arg1"):
268293
command = commmand_factories[command_enum.value](command_args.arg1, command_args.timestamp)
269-
return command
294+
295+
if command_args.timestamp is not None and command_args.timestamp > 0:
296+
is_timetagged = True
297+
return command, is_timetagged
270298

271299
parser = arg_parse()
272300
# If the command did not pass any of the specific parsers, we try the general one
273301
try:
274302
command_args = parser.parse_args(arguments)
275303
except ArgumentError:
276304
print("Invalid Commands")
277-
return None
305+
return None, False
278306

279307
# Same thing as before, we try to convert to a CmdCallbackId Enum to see if the command if valid
280308
try:
281309
command_enum = CmdCallbackId[command_args.command]
282310
except KeyError:
283311
print("Invalid Command")
284-
return None
312+
return None, False
285313

286314
command = commmand_factories[command_enum.value](command_args.timestamp)
287-
return command
315+
316+
if command_args.timestamp is not None and command_args.timestamp > 0:
317+
is_timetagged = True
318+
return command, is_timetagged
288319

289320

290321
def poll(com_port: str, file_path: str | Path, timeout: int = 0, print_console: bool = False) -> None:
@@ -295,6 +326,9 @@ def poll(com_port: str, file_path: str | Path, timeout: int = 0, print_console:
295326
:param print_console: Whether the function should print to console or not. By default, this is set to False. This is
296327
useful for the CLI where sometimes we want to print out the received logs from the board
297328
"""
329+
330+
comms = CommsPipeline()
331+
298332
with (
299333
Serial(
300334
com_port,
@@ -306,8 +340,31 @@ def poll(com_port: str, file_path: str | Path, timeout: int = 0, print_console:
306340
open(file_path, "a") as file,
307341
):
308342
while True:
309-
data = ser.read(10000).decode("utf-8")
310-
file.write(data)
343+
data = ser.read(100000)
344+
start_index = data.find(b"\x7e")
345+
end_index = data.rfind(b"\x7e")
346+
347+
# Check if a frame is in what is sent back
348+
if start_index != -1:
349+
# These are all the bytes from other tasks that are not a part of the frame
350+
command_res = None
351+
outer_bytes_left = data[:start_index]
352+
outer_bytes_right = data[end_index + 1 :]
353+
354+
# Isolate the frame
355+
rcv_frame_bytes = data[start_index : end_index + 1]
356+
357+
rcv_frame = comms.decode_frame(rcv_frame_bytes)
358+
# TODO: Handle these return frames
359+
if rcv_frame is not None and rcv_frame.data is not None:
360+
command_res = parse_command_response(bytes(rcv_frame.data[:RS_DECODED_DATA_SIZE]))
361+
362+
data_string = outer_bytes_left.decode("utf-8") + str(command_res) + outer_bytes_right.decode("utf-8")
363+
print("Time Tagged Command Response:")
364+
else:
365+
data_string = data.decode("utf-8")
366+
367+
file.write(data_string)
311368
file.flush()
312-
if print_console and len(data) != 0:
313-
print(data)
369+
if print_console and len(data_string) != 0:
370+
print(data_string)

interfaces/obc_gs_interface/commands/__init__.py

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
Structure,
44
Union,
55
c_bool,
6-
c_float,
76
c_uint,
87
c_uint8,
98
c_uint16,
@@ -13,7 +12,7 @@
1312
from enum import IntEnum
1413
from typing import Final
1514

16-
from interfaces import MAX_CMD_MSG_SIZE, MAX_REPONSE_PACKED_SIZE, RS_DECODED_DATA_SIZE
15+
from interfaces import MAX_CMD_MSG_SIZE, RS_DECODED_DATA_SIZE
1716
from interfaces.obc_gs_interface import interface
1817

1918
# ######################################################################
@@ -113,43 +112,24 @@ def __init__(self, unixtime_of_execution: int | None = None) -> None:
113112
# ######################################################################
114113

115114

116-
# NOTE: Just like the C implementation, this is a sample implementation. Add implemetnations as command responses are
117-
# made
118-
class ObcCmdResetResponse(Structure):
119-
"""
120-
The python equivalent class for the obc_cmd_reset_response_t structure in the C implementation
121-
NOTE: This is a sample
122-
NOTE: This class uses floats which means it has floating point precision. This should not be a problem in most cases
123-
"""
124-
125-
_fields_ = [("data1", c_float), ("data2", c_uint32)]
126-
127-
128-
# NOTE: This only has the sample response so add more response structures as they get implemented to this union
129-
class _UR(Union):
130-
"""
131-
The union needed to create the cmd_unpacked_response_t type in python
132-
NOTE: Add response structures as they get implemented to this union
133-
"""
134-
135-
_fields_ = [("obcResetResponse", ObcCmdResetResponse)]
136-
137-
138115
# NOTE: No modifications to this class are necessary when adding new responses
139-
class CmdUnpackedReponse(Structure):
116+
class CmdResponseHeader(Structure):
140117
"""
141118
The python equivalent class for the cmd_unpacked_response_t structure in the C implementation
142119
"""
143120

144-
_anonymous_ = ("u",)
145-
_fields_ = [("errCode", c_uint), ("cmdId", c_uint), ("u", _UR)]
121+
_fields_ = [("cmdId", c_uint), ("errCode", c_uint), ("dataLen", c_uint8)]
146122

147123

148-
interface.packCommandResponse.argtypes = (POINTER(CmdUnpackedReponse), POINTER(c_uint8 * MAX_REPONSE_PACKED_SIZE))
149-
interface.packCommandResponse.restype = c_uint
124+
interface.packCmdResponse.argtypes = (POINTER(CmdResponseHeader), POINTER(c_uint8 * RS_DECODED_DATA_SIZE))
125+
interface.packCmdResponse.restype = c_uint
150126

151-
interface.unpackCommandResponse.argtypes = (POINTER(c_uint8 * MAX_REPONSE_PACKED_SIZE), POINTER(CmdUnpackedReponse))
152-
interface.unpackCommandResponse.restype = c_uint
127+
interface.unpackCmdResponse.argtypes = (
128+
POINTER(c_uint8 * RS_DECODED_DATA_SIZE),
129+
POINTER(CmdResponseHeader),
130+
POINTER(c_uint8 * RS_DECODED_DATA_SIZE),
131+
)
132+
interface.unpackCmdResponse.restype = c_uint
153133

154134

155135
# ######################################################################
@@ -178,7 +158,8 @@ class CmdCallbackId(IntEnum):
178158
CMD_ERASE_APP = 9
179159
CMD_DOWNLOAD_DATA = 10
180160
CMD_VERIFY_CRC = 11
181-
NUM_CMD_CALLBACKS = 12
161+
CMD_I2C_PROBE = 12
162+
NUM_CMD_CALLBACKS = 13
182163

183164

184165
# Path to File: interfaces/obc_gs_interface/commands/obc_gs_commands_response.h
@@ -187,8 +168,8 @@ class CmdResponseErrorCode(IntEnum):
187168
Enums corresponding to the C implementation of the cmd_response_error_code_t
188169
"""
189170

190-
CMD_RESPONSE_SUCCESS = 0
191-
CMD_RESPONSE_ERROR = 1
171+
CMD_RESPONSE_SUCCESS = 0x01
172+
CMD_RESPONSE_ERROR = 0x7F
192173

193174

194175
class ProgrammingSession(IntEnum):
@@ -398,6 +379,20 @@ def create_cmd_verify_crc(unixtime_of_execution: int | None = None) -> CmdMsg:
398379
return cmd_msg
399380

400381

382+
def create_cmd_i2c_probe(unixtime_of_execution: int | None = None) -> CmdMsg:
383+
"""
384+
Function to create a CmdMsg structure for CMD_I2C_PROBE
385+
386+
:param unixtime_of_execution: A time of when to execute a certain event,
387+
by default, it is set to None (i.e. a specific
388+
time is not needed)
389+
:return: CmdMsg structure for CMD_I2C_PROBE
390+
"""
391+
cmd_msg = CmdMsg(unixtime_of_execution)
392+
cmd_msg.id = CmdCallbackId.CMD_I2C_PROBE
393+
return cmd_msg
394+
395+
401396
# ######################################################################
402397
# || ||
403398
# || Command Pack and Unpack Implementations ||
@@ -479,14 +474,14 @@ def unpack_command(cmd_msg_packed: bytes) -> tuple[list[CmdMsg], bytes]:
479474
# ######################################################################
480475

481476

482-
def pack_command_response(cmd_msg_response: CmdUnpackedReponse) -> bytes:
477+
def pack_command_response(cmd_msg_response: CmdResponseHeader) -> bytes:
483478
"""
484479
This takes a command message reponse to pack it (see the C implementation for more on how that's exactly done)
485480
486481
:param cmd_msg_response: A c-style structure that hold the unpacked command message response
487482
:return: Bytes of the packed commmand response
488483
"""
489-
buffer = (c_uint8 * MAX_REPONSE_PACKED_SIZE)(*([0] * MAX_REPONSE_PACKED_SIZE))
484+
buffer = (c_uint8 * RS_DECODED_DATA_SIZE)(*([0] * RS_DECODED_DATA_SIZE))
490485
res = interface.packCommandResponse(pointer(cmd_msg_response), pointer(buffer))
491486

492487
if res != 0:
@@ -495,24 +490,26 @@ def pack_command_response(cmd_msg_response: CmdUnpackedReponse) -> bytes:
495490
return bytes(buffer).rstrip(b"\x00")
496491

497492

498-
def unpack_command_response(cmd_msg_packed: bytes) -> CmdUnpackedReponse:
493+
def unpack_command_response(cmd_msg_packed: bytes) -> tuple[CmdResponseHeader, bytes]:
499494
"""
500495
This takes in a bytes of data to be unpacked into a command response (see the C implementation for more on how
501496
that's exactly done)
502497
503498
:param cmd_msg_packed: Bytes of an already encoded message
504499
:return: An unpacked command message in the form of a structure
505500
"""
506-
if len(cmd_msg_packed) > MAX_REPONSE_PACKED_SIZE:
501+
if len(cmd_msg_packed) > RS_DECODED_DATA_SIZE:
507502
raise ValueError("The encoded command reponse data to unpack is too long")
508503

509504
buffer_elements = list(cmd_msg_packed)
510-
buff = (c_uint8 * MAX_REPONSE_PACKED_SIZE)(*buffer_elements)
511-
cmd_msg_response = CmdUnpackedReponse()
505+
buff = (c_uint8 * RS_DECODED_DATA_SIZE)(*buffer_elements)
506+
data_buffer = (c_uint8 * RS_DECODED_DATA_SIZE)()
507+
cmd_msg_response = CmdResponseHeader()
512508

513-
res = interface.unpackCommandResponse(pointer(buff), pointer(cmd_msg_response))
509+
res = interface.unpackCmdResponse(pointer(buff), pointer(cmd_msg_response), pointer(data_buffer))
510+
data_bytes = bytes(data_buffer)
514511

515512
if res != 0:
516513
raise ValueError("Could not unpack command response. OBC Error Code: " + str(res))
517514

518-
return cmd_msg_response
515+
return cmd_msg_response, data_bytes

0 commit comments

Comments
 (0)