Skip to content

Commit 9af3cbe

Browse files
authored
Add Amateur Radio Repeat Code (#316)
1 parent 6118f20 commit 9af3cbe

File tree

5 files changed

+374
-13
lines changed

5 files changed

+374
-13
lines changed

circuitpython-workspaces/flight-software/src/pysquared/cdh.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class CommandDataHandler:
3434
command_change_radio_modulation: str = "change_radio_modulation"
3535
command_send_joke: str = "send_joke"
3636

37+
oscar_password: str = "Hello World!" # Default password for OSCAR commands
38+
3739
def __init__(
3840
self,
3941
logger: Logger,
@@ -71,6 +73,29 @@ def listen_for_commands(self, timeout: int) -> None:
7173

7274
msg: dict[str, str] = json.loads(json_str)
7375

76+
# Check for OSCAR password first
77+
if msg.get("password") == self.oscar_password:
78+
self._log.debug("OSCAR command received", msg=msg)
79+
cmd = msg.get("command")
80+
if cmd is None:
81+
self._log.warning("No OSCAR command found in message", msg=msg)
82+
self._packet_manager.send(
83+
f"No OSCAR command found in message: {msg}".encode("utf-8")
84+
)
85+
return
86+
87+
args: list[str] = []
88+
raw_args = msg.get("args")
89+
if isinstance(raw_args, list):
90+
args: list[str] = raw_args
91+
92+
# Delay to give the ground station time to switch to listening mode
93+
time.sleep(self._send_delay)
94+
self._packet_manager.send_acknowledgement()
95+
96+
self.oscar_command(cmd, args)
97+
return
98+
7499
# If message has password field, check it
75100
if msg.get("password") != self._config.super_secret_code:
76101
self._log.debug(
@@ -170,3 +195,33 @@ def reset(self) -> None:
170195
self._packet_manager.send(data="Resetting satellite".encode("utf-8"))
171196
microcontroller.on_next_reset(microcontroller.RunMode.NORMAL)
172197
microcontroller.reset()
198+
199+
def oscar_command(self, command: str, args: list[str]) -> None:
200+
"""Handles OSCAR commands.
201+
202+
Args:
203+
command: The OSCAR command to execute.
204+
args: A list of arguments for the command.
205+
"""
206+
if command == "ping":
207+
self._log.info("OSCAR ping command received. Sending pong response.")
208+
self._packet_manager.send(
209+
f"Pong! {self._packet_manager.get_last_rssi()}".encode("utf-8")
210+
)
211+
212+
elif command == "repeat":
213+
if len(args) < 1:
214+
self._log.warning("No message specified for repeat command")
215+
self._packet_manager.send(
216+
"No message specified for repeat command.".encode("utf-8")
217+
)
218+
return
219+
repeat_message = " ".join(args)
220+
self._log.info("OSCAR repeat command received. Repeating message.")
221+
self._packet_manager.send(repeat_message.encode("utf-8"))
222+
223+
else:
224+
self._log.warning("Unknown OSCAR command received", command=command)
225+
self._packet_manager.send(
226+
f"Unknown OSCAR command received: {command}".encode("utf-8")
227+
)

circuitpython-workspaces/flight-software/src/pysquared/hardware/radio/packetizer/packet_manager.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ def send_acknowledgement(self) -> None:
201201
self.send(b"ACK")
202202
self._logger.debug("Sent acknowledgment packet")
203203

204+
def get_last_rssi(self) -> int:
205+
"""Gets the RSSI of the last received packet.
206+
207+
Returns:
208+
The RSSI of the last received packet.
209+
"""
210+
return self._radio.get_rssi()
211+
204212
def _unpack_data(self, packets: list[bytes]) -> bytes:
205213
"""Unpacks a list of packets and reassembles the original data.
206214

circuitpython-workspaces/ground-station/src/ground_station/ground_station.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def send_receive(self):
5858
| 1: Reset |
5959
| 2: Change radio modulation |
6060
| 3: Send joke |
61+
| 4: OSCAR commands |
6162
===============================
6263
"""
6364
)
@@ -74,10 +75,15 @@ def handle_input(self, cmd_selection):
7475
Args:
7576
cmd_selection: The command selection input by the user.
7677
"""
77-
if cmd_selection not in ["1", "2", "3"]:
78+
if cmd_selection not in ["1", "2", "3", "4"]:
7879
self._log.warning("Invalid command selection. Please try again.")
7980
return
8081

82+
# Handle OSCAR commands separately
83+
if cmd_selection == "4":
84+
self.handle_oscar_commands()
85+
return
86+
8187
message: dict[str, object] = {
8288
"name": self._config.cubesat_name,
8389
"password": self._config.super_secret_code,
@@ -128,6 +134,79 @@ def handle_input(self, cmd_selection):
128134
self._log.info("Received response", response=b.decode("utf-8"))
129135
break
130136

137+
def handle_oscar_commands(self):
138+
"""
139+
Handle OSCAR command selection and sending.
140+
"""
141+
try:
142+
oscar_selection = input(
143+
"""
144+
===============================
145+
| Select OSCAR command |
146+
| 1: Ping |
147+
| 2: Repeat message |
148+
===============================
149+
"""
150+
)
151+
152+
if oscar_selection not in ["1", "2"]:
153+
self._log.warning("Invalid OSCAR command selection. Please try again.")
154+
return
155+
156+
message: dict[str, object] = {
157+
"password": self._cdh.oscar_password,
158+
}
159+
160+
if oscar_selection == "1":
161+
message["command"] = "ping"
162+
message["args"] = []
163+
elif oscar_selection == "2":
164+
repeat_message = input("Enter message to repeat: ")
165+
if not repeat_message.strip():
166+
self._log.warning("Empty message provided. Please try again.")
167+
return
168+
message["command"] = "repeat"
169+
message["args"] = repeat_message.split()
170+
171+
while True:
172+
# Turn on the radio so that it captures any received packets to buffer
173+
self._packet_manager.listen(1)
174+
175+
# Send the OSCAR message
176+
self._log.info(
177+
"Sending OSCAR command",
178+
cmd=message["command"],
179+
args=message.get("args", []),
180+
)
181+
self._packet_manager.send(json.dumps(message).encode("utf-8"))
182+
183+
# Listen for ACK response
184+
b = self._packet_manager.listen(1)
185+
if b is None:
186+
self._log.info("No response received, retrying...")
187+
continue
188+
189+
if b != b"ACK":
190+
self._log.info(
191+
"No ACK response received, retrying...",
192+
response=b.decode("utf-8"),
193+
)
194+
continue
195+
196+
self._log.info("Received ACK")
197+
198+
# Now listen for the actual response
199+
b = self._packet_manager.listen(1)
200+
if b is None:
201+
self._log.info("No response received, retrying...")
202+
continue
203+
204+
self._log.info("Received OSCAR response", response=b.decode("utf-8"))
205+
break
206+
207+
except KeyboardInterrupt:
208+
self._log.debug("Keyboard interrupt received, exiting OSCAR mode.")
209+
131210
def run(self):
132211
"""Run the ground station interface."""
133212
while True:

0 commit comments

Comments
 (0)