Skip to content

Commit 48c4ada

Browse files
Fix bug of Beacon with Magnetometer Proto (#312)
Co-authored-by: Michael Pham <[email protected]>
1 parent dc0ba45 commit 48c4ada

File tree

2 files changed

+163
-0
lines changed
  • circuitpython-workspaces/flight-software/src/pysquared
  • cpython-workspaces/flight-software-unit-tests/src/unit-tests

2 files changed

+163
-0
lines changed

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from .nvm.counter import Counter
2727
from .nvm.flag import Flag
2828
from .protos.imu import IMUProto
29+
from .protos.magnetometer import MagnetometerProto
2930
from .protos.power_monitor import PowerMonitorProto
3031
from .protos.radio import RadioProto
3132
from .protos.temperature_sensor import TemperatureSensorProto
@@ -49,6 +50,7 @@ def __init__(
4950
*args: PowerMonitorProto
5051
| RadioProto
5152
| IMUProto
53+
| MagnetometerProto
5254
| TemperatureSensorProto
5355
| Flag
5456
| Counter
@@ -71,6 +73,7 @@ def __init__(
7173
PowerMonitorProto
7274
| RadioProto
7375
| IMUProto
76+
| MagnetometerProto
7477
| TemperatureSensorProto
7578
| Flag
7679
| Counter
@@ -228,6 +231,8 @@ def _add_sensor_data(self, state: OrderedDict[str, object]) -> None:
228231
self._add_radio_data(state, sensor, index)
229232
elif isinstance(sensor, IMUProto):
230233
self._add_imu_data(state, sensor, index)
234+
elif isinstance(sensor, MagnetometerProto):
235+
self._add_magnetometer_data(state, sensor, index)
231236
elif isinstance(sensor, PowerMonitorProto):
232237
self._add_power_monitor_data(state, sensor, index)
233238
elif isinstance(sensor, TemperatureSensorProto):
@@ -283,6 +288,21 @@ def _add_imu_data(
283288
index,
284289
)
285290

291+
def _add_magnetometer_data(
292+
self, state: OrderedDict[str, object], sensor: MagnetometerProto, index: int
293+
) -> None:
294+
"""Adds magnetometer data to the beacon state."""
295+
sensor_name = sensor.__class__.__name__
296+
297+
self._safe_add_sensor_reading(
298+
state,
299+
f"{sensor_name}_{index}_magnetic_field",
300+
lambda: sensor.get_magnetic_field().to_dict(),
301+
"Error retrieving magnetic field",
302+
sensor_name,
303+
index,
304+
)
305+
286306
def _add_power_monitor_data(
287307
self, state: OrderedDict[str, object], sensor: PowerMonitorProto, index: int
288308
) -> None:
@@ -471,6 +491,12 @@ def _add_template_for_sensor(
471491
for i in range(3):
472492
state[f"{sensor_name}_{index}_acceleration_value_{i}"] = 0.0
473493
state[f"{sensor_name}_{index}_angular_velocity_value_{i}"] = 0.0
494+
elif isinstance(sensor, MagnetometerProto):
495+
sensor_name = sensor.__class__.__name__
496+
# Add template data for all magnetometer fields that would be created
497+
state[f"{sensor_name}_{index}_magnetic_field_timestamp"] = 0.0
498+
for i in range(3):
499+
state[f"{sensor_name}_{index}_magnetic_field_value_{i}"] = 0.0
474500
elif isinstance(sensor, PowerMonitorProto):
475501
sensor_name = sensor.__class__.__name__
476502
state[f"{sensor_name}_{index}_current_avg"] = 0.0

cpython-workspaces/flight-software-unit-tests/src/unit-tests/test_beacon.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
from pysquared.nvm.counter import Counter
2121
from pysquared.nvm.flag import Flag
2222
from pysquared.protos.imu import IMUProto
23+
from pysquared.protos.magnetometer import MagnetometerProto
2324
from pysquared.protos.power_monitor import PowerMonitorProto
2425
from pysquared.protos.radio import RadioProto
2526
from pysquared.protos.temperature_sensor import TemperatureSensorProto
2627
from pysquared.sensor_reading.acceleration import Acceleration
2728
from pysquared.sensor_reading.angular_velocity import AngularVelocity
2829
from pysquared.sensor_reading.avg import avg_readings
2930
from pysquared.sensor_reading.current import Current
31+
from pysquared.sensor_reading.magnetic import Magnetic
3032
from pysquared.sensor_reading.temperature import Temperature
3133
from pysquared.sensor_reading.voltage import Voltage
3234

@@ -128,6 +130,14 @@ def get_acceleration(self) -> Acceleration:
128130
return Acceleration(5.4, 3.2, 1.0)
129131

130132

133+
class MockMagnetometer(MagnetometerProto):
134+
"""Mocks the MagnetometerProto for testing."""
135+
136+
def get_magnetic_field(self) -> Magnetic:
137+
"""Mocks the get_magnetic_field method."""
138+
return Magnetic(25.5, -12.3, 8.7)
139+
140+
131141
def test_beacon_init(mock_logger, mock_packet_manager):
132142
"""Tests Beacon initialization.
133143
@@ -1041,3 +1051,130 @@ def test_beacon_encode_sensor_dict_with_non_numeric_values():
10411051
# Verify encoding completed without error
10421052
encoded_data = encoder.to_bytes()
10431053
assert isinstance(encoded_data, bytes)
1054+
1055+
1056+
@patch("pysquared.nvm.flag.microcontroller")
1057+
@patch("pysquared.nvm.counter.microcontroller")
1058+
def test_beacon_send_with_magnetometer(
1059+
mock_flag_microcontroller,
1060+
mock_counter_microcontroller,
1061+
mock_logger,
1062+
mock_packet_manager,
1063+
):
1064+
"""Tests sending a beacon with magnetometer sensor.
1065+
1066+
Args:
1067+
mock_flag_microcontroller: Mocked microcontroller for Flag.
1068+
mock_counter_microcontroller: Mocked microcontroller for Counter.
1069+
mock_logger: Mocked Logger instance.
1070+
mock_packet_manager: Mocked PacketManager instance.
1071+
"""
1072+
mock_flag_microcontroller.nvm = setup_datastore
1073+
mock_counter_microcontroller.nvm = setup_datastore
1074+
1075+
magnetometer = MockMagnetometer()
1076+
1077+
beacon = Beacon(
1078+
mock_logger,
1079+
"test_beacon",
1080+
mock_packet_manager,
1081+
0,
1082+
magnetometer,
1083+
)
1084+
1085+
result = beacon.send()
1086+
1087+
# Verify the beacon was sent successfully
1088+
assert result == mock_packet_manager.send.return_value
1089+
mock_packet_manager.send.assert_called_once()
1090+
1091+
# Decode the sent data to verify magnetometer data is included
1092+
sent_data = mock_packet_manager.send.call_args[0][0]
1093+
decoded_data = Beacon.decode_binary_beacon(sent_data)
1094+
1095+
# Verify magnetometer data is present in the decoded data
1096+
values = list(decoded_data.values())
1097+
1098+
# Should contain the magnetic field components (25.5, -12.3, 8.7)
1099+
# Use approximate comparison for floating point values
1100+
assert any(abs(v - 25.5) < 0.01 for v in values if isinstance(v, (int, float)))
1101+
assert any(abs(v - (-12.3)) < 0.01 for v in values if isinstance(v, (int, float)))
1102+
assert any(abs(v - 8.7) < 0.01 for v in values if isinstance(v, (int, float)))
1103+
1104+
1105+
def test_beacon_send_with_magnetometer_error(mock_logger, mock_packet_manager):
1106+
"""Tests sending a beacon when magnetometer sensor fails.
1107+
1108+
Args:
1109+
mock_logger: Mocked Logger instance.
1110+
mock_packet_manager: Mocked PacketManager instance.
1111+
"""
1112+
magnetometer = MockMagnetometer()
1113+
# Mock the get_magnetic_field method to raise an exception
1114+
magnetometer.get_magnetic_field = MagicMock(
1115+
side_effect=Exception("Magnetometer sensor failure")
1116+
)
1117+
1118+
beacon = Beacon(
1119+
mock_logger,
1120+
"test_beacon",
1121+
mock_packet_manager,
1122+
0,
1123+
magnetometer,
1124+
)
1125+
1126+
_ = beacon.send()
1127+
1128+
# Verify the error was logged
1129+
mock_logger.error.assert_called_with(
1130+
"Error retrieving magnetic field",
1131+
magnetometer.get_magnetic_field.side_effect,
1132+
sensor="MockMagnetometer",
1133+
index=0,
1134+
)
1135+
1136+
# Verify beacon was still sent (despite the error)
1137+
mock_packet_manager.send.assert_called_once()
1138+
1139+
1140+
@patch("pysquared.nvm.flag.microcontroller")
1141+
@patch("pysquared.nvm.counter.microcontroller")
1142+
def test_beacon_generate_key_mapping_with_magnetometer(
1143+
mock_flag_microcontroller,
1144+
mock_counter_microcontroller,
1145+
mock_logger,
1146+
mock_packet_manager,
1147+
):
1148+
"""Tests the generate_key_mapping method includes magnetometer template data.
1149+
1150+
Args:
1151+
mock_flag_microcontroller: Mocked microcontroller for Flag.
1152+
mock_counter_microcontroller: Mocked microcontroller for Counter.
1153+
mock_logger: Mocked Logger instance.
1154+
mock_packet_manager: Mocked PacketManager instance.
1155+
"""
1156+
mock_flag_microcontroller.nvm = setup_datastore
1157+
mock_counter_microcontroller.nvm = setup_datastore
1158+
1159+
magnetometer = MockMagnetometer()
1160+
1161+
beacon = Beacon(
1162+
mock_logger,
1163+
"test_beacon",
1164+
mock_packet_manager,
1165+
0,
1166+
magnetometer,
1167+
)
1168+
1169+
key_map = beacon.generate_key_mapping()
1170+
1171+
# Verify comprehensive key mapping is generated
1172+
assert isinstance(key_map, dict)
1173+
1174+
# Check that magnetometer template keys are present in the mapping
1175+
# The keys should include magnetometer timestamp and 3D magnetic field components
1176+
key_names = list(key_map.values())
1177+
magnetometer_keys = [key for key in key_names if "magnetic_field" in str(key)]
1178+
1179+
# Should have at least 4 keys: timestamp + 3 components (x, y, z)
1180+
assert len(magnetometer_keys) >= 4

0 commit comments

Comments
 (0)