Skip to content

Commit 31288bd

Browse files
authored
Merge pull request #96 from CarletonURocketry/eh/linaccel_angvel
Add datablock parsing for linear acceleration and angular velocity
2 parents 1e00205 + 2121412 commit 31288bd

File tree

3 files changed

+161
-18
lines changed

3 files changed

+161
-18
lines changed

modules/telemetry/v1/data_block.py

+99-1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def parse(block_subtype: DataBlockSubtype, payload: bytes) -> DataBlock:
105105
DataBlockSubtype.TEMPERATURE: TemperatureDB,
106106
DataBlockSubtype.PRESSURE: PressureDB,
107107
DataBlockSubtype.HUMIDITY: HumidityDB,
108+
DataBlockSubtype.ACCELERATION: LinearAccelerationDB,
109+
DataBlockSubtype.ANGULAR_VELOCITY: AngularVelocityDB,
108110
}
109111

110112
subtype = SUBTYPE_CLASSES.get(block_subtype)
@@ -234,7 +236,7 @@ def __init__(self, mission_time: int, pressure: int) -> None:
234236
Constructs a pressure data block.
235237
236238
Args:
237-
mission_time: The mission time the altitude was measured at in milliseconds since launch.
239+
mission_time: The mission time the pressure was measured at in milliseconds since launch.
238240
pressure: The pressure in millibars.
239241
240242
"""
@@ -308,6 +310,98 @@ def __iter__(self):
308310
yield "percentage", round(self.humidity / 100)
309311

310312

313+
class LinearAccelerationDB(DataBlock):
314+
"""Represents a linear acceleration data block"""
315+
316+
def __init__(self, mission_time: int, x_axis: int, y_axis: int, z_axis: int) -> None:
317+
"""
318+
Constructs a linear acceleration data block.
319+
320+
Args:
321+
mission_time: The mission time the linear acceleration was measured in milliseconds since launch.
322+
x_axis: The acceleration about the x axis in meters per second squared.
323+
y_axis: The acceleration about the y axis in meters per second squared.
324+
z_axis: The acceleration about the z axis in meters per second squared.
325+
326+
"""
327+
super().__init__(mission_time)
328+
self.x_axis: int = x_axis
329+
self.y_axis: int = y_axis
330+
self.z_axis: int = z_axis
331+
332+
@classmethod
333+
def from_bytes(cls, payload: bytes) -> Self:
334+
"""
335+
Constructs a linear acceleration data block from bytes.
336+
Returns:
337+
A linear acceleration data block.
338+
"""
339+
parts = struct.unpack("<Ihhhh", payload)
340+
return cls(parts[0], parts[1] / 100, parts[2] / 100, parts[3] / 100)
341+
342+
def __len__(self) -> int:
343+
"""
344+
Get the length of a linear acceleration data block in bytes
345+
Returns:
346+
The length of a linear acceleration data block in bytes not including the block header.
347+
"""
348+
return 10
349+
350+
def __str__(self):
351+
return f"""{self.__class__.__name__} -> time: {self.mission_time} ms, x-axis: {self.x_axis} m/s^2, y-axis:
352+
{self.y_axis} m/s^2, z-axis: {self.z_axis} m/s^2"""
353+
354+
def __iter__(self):
355+
yield "mission_time", self.mission_time
356+
yield "acceleration", {"x_axis": self.x_axis, "y_axis": self.y_axis, "z_axis": self.z_axis}
357+
358+
359+
class AngularVelocityDB(DataBlock):
360+
"""Represents an angular velocity data block"""
361+
362+
def __init__(self, mission_time: int, x_axis: int, y_axis: int, z_axis: int) -> None:
363+
"""
364+
Constructus an angular velocity data block.
365+
366+
Args:
367+
mission_time: The mission time the angular velocity was measured in milliseconds since launch.
368+
x_axis: The velocity about the x axis in degrees per second.
369+
y_axis: The velocity about the y axis in degrees per second.
370+
z_axis: The velocity about the z axis in degrees per second.
371+
372+
"""
373+
super().__init__(mission_time)
374+
self.x_axis: int = x_axis
375+
self.y_axis: int = y_axis
376+
self.z_axis: int = z_axis
377+
378+
@classmethod
379+
def from_bytes(cls, payload: bytes) -> Self:
380+
"""
381+
Constructs an angular velocity data block from bytes.
382+
Returns:
383+
An angular velocity data block.
384+
"""
385+
parts = struct.unpack("<Ihhhh", payload)
386+
return cls(parts[0], parts[1] / 10, parts[2] / 10, parts[3] / 10)
387+
388+
def __len__(self) -> int:
389+
"""
390+
Get the length of an angular velocity data block in bytes
391+
Returns:
392+
The length of an angular velocity data block in bytes not including the block header.
393+
"""
394+
return 10
395+
396+
def __str__(self):
397+
return f"""{self.__class__.__name__} -> time: {self.mission_time} ms, x-axis: {self.x_axis} dps, y-axis:
398+
{self.y_axis} dps, z-axis: {self.z_axis} dps"""
399+
400+
def __iter__(self):
401+
yield "mission_time", self.mission_time
402+
yield "velocity", {"x_axis": self.x_axis, "y_axis": self.y_axis, "z_axis": self.z_axis}
403+
404+
311405
def parse_data_block(type: DataBlockSubtype, payload: bytes) -> DataBlock:
312406
"""
313407
Parses a bytes payload into the correct data block type.
@@ -331,5 +425,9 @@ def parse_data_block(type: DataBlockSubtype, payload: bytes) -> DataBlock:
331425
return PressureDB.from_bytes(payload)
332426
case DataBlockSubtype.HUMIDITY:
333427
return HumidityDB.from_bytes(payload)
428+
case DataBlockSubtype.ACCELERATION:
429+
return LinearAccelerationDB.from_bytes(payload)
430+
case DataBlockSubtype.ANGULAR_VELOCITY:
431+
return AngularVelocityDB.from_bytes(payload)
334432
case _:
335433
raise NotImplementedError

tests/parsing/test_block_data.py

+49-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
__author__ = "Elias Hawa"
33

44
import pytest
5-
from modules.telemetry.v1.data_block import PressureDB
6-
from modules.telemetry.v1.data_block import TemperatureDB
5+
from modules.telemetry.v1.data_block import PressureDB, TemperatureDB, LinearAccelerationDB, AngularVelocityDB
76

87

98
@pytest.fixture
@@ -26,6 +25,34 @@ def temperature_data_content() -> bytes:
2625
return b"\x00\x00\x00\x00\xf0\x55\x00\x00"
2726

2827

28+
@pytest.fixture
29+
def linear_acceleration_data_content() -> bytes:
30+
"""
31+
Returns a linear acceleration sensor reading with the following attributes
32+
mission time: 0ms
33+
x axis acceleration: 3cm/s^2
34+
y axis acceleration: -4cm/s^2
35+
z axis acceleration: 1032cm/s^2
36+
Note that LinearAccelerationDB from_bytes method should convert the axis values
37+
from cm/s^2 to m/s^2
38+
"""
39+
return b"\x00\x00\x00\x00\x03\x00\xfc\xff\x08\x04\x00\x00"
40+
41+
42+
@pytest.fixture
43+
def angular_velocity_data_content() -> bytes:
44+
"""
45+
Returns an angular velocity sensor reading with the following attributes
46+
mission time: 0ms
47+
x axis velocity: 60 tenths of a degree per second
48+
y axis velocity: 110 tenths of a degree per second
49+
z axis velocity -30 tenths of a degree per second
50+
Note that the AngularVelocityDb from_bytes method should convert the axis values
51+
from tenths of a degree per second to degrees per second
52+
"""
53+
return b"\x00\x00\x00\x00\x06\x00\x0b\x00\xfd\xff\x00\x00"
54+
55+
2956
def test_pressure_data_block(pressure_data_content: bytes) -> None:
3057
"""Test that the pressure data block is parsed correctly."""
3158
pdb = PressureDB.from_bytes(pressure_data_content)
@@ -40,3 +67,23 @@ def test_temperature_data_block(temperature_data_content: bytes) -> None:
4067

4168
assert tdb.mission_time == 0
4269
assert tdb.temperature == 22000
70+
71+
72+
def test_linear_acceleration_data_block(linear_acceleration_data_content: bytes) -> None:
73+
"""Test that the linear acceleration is parsed correctly."""
74+
lin_acc = LinearAccelerationDB.from_bytes(linear_acceleration_data_content)
75+
76+
assert lin_acc.mission_time == 0
77+
assert lin_acc.x_axis == 0.03
78+
assert lin_acc.y_axis == -0.04
79+
assert lin_acc.z_axis == 10.32
80+
81+
82+
def test_angular_velocity_data_block(angular_velocity_data_content: bytes) -> None:
83+
"""Test that the angular velocity is parsed correctly."""
84+
ang_vel = AngularVelocityDB.from_bytes(angular_velocity_data_content)
85+
86+
assert ang_vel.mission_time == 0
87+
assert ang_vel.x_axis == 0.6
88+
assert ang_vel.y_axis == 1.1
89+
assert ang_vel.z_axis == -0.3

tests/parsing/test_full_telemetry_parsing.py

+13-15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from modules.telemetry.telemetry_utils import parse_radio_block, from_approved_callsign
1111
from modules.misc.config import load_config
1212

13+
# Fixtures and tests to ensure that parse_radio_block works as expected
14+
1315

1416
@pytest.fixture
1517
def pkt_version() -> int:
@@ -41,21 +43,20 @@ def test_radio_block(pkt_version: int, block_header: BlockHeader, hex_block_cont
4143
"""
4244
prb = parse_radio_block(pkt_version, block_header, hex_block_contents)
4345
assert prb is not None
44-
if prb is not None:
45-
assert prb.block_header.length == 12
46-
assert prb.block_header.message_type == 0
47-
assert prb.block_header.message_subtype == 2
48-
assert prb.block_header.destination == 0
49-
assert prb.block_name == "temperature"
50-
assert prb.block_contents["mission_time"] == 0
46+
assert prb.block_header.length == 12
47+
assert prb.block_header.message_type == 0
48+
assert prb.block_header.message_subtype == 2
49+
assert prb.block_header.destination == 0
50+
assert prb.block_name == "temperature"
51+
assert prb.block_contents["mission_time"] == 0
5152

5253

53-
# fixtures
54+
# Fixtures and tests to ensure that parse_radio_block handles errors as expected
5455

5556

5657
@pytest.fixture
5758
def not_implemented_datablock_subtype() -> BlockHeader:
58-
return BlockHeader.from_hex("02000400")
59+
return BlockHeader.from_hex("02000600")
5960

6061

6162
def test_invalid_datablock_subtype(pkt_version: int, hex_block_contents: str):
@@ -80,7 +81,7 @@ def test_not_implemented_error(
8081

8182
config = load_config("config.json")
8283

83-
# Fixtures
84+
# Fixtures and tests to ensure that from_approved_callsign works as expected
8485

8586

8687
@pytest.fixture
@@ -103,9 +104,6 @@ def non_approved_callsign() -> PacketHeader:
103104
return pkt_hdr
104105

105106

106-
# Tests
107-
108-
109107
# Test valid header
110108
def test_is_approved_pkt_hdr(
111109
valid_packet_header: PacketHeader, approved_callsigns: dict[str, str], caplog: LogCaptureFixture
@@ -131,12 +129,12 @@ def test_is_unauthorized_callsign(
131129
def test_is_invalid_hdr(approved_callsigns: dict[str, str]) -> None:
132130
hdr = "564133494e490000000c000137000000"
133131
with pytest.raises(UnsupportedEncodingVersionError, match="Unsupported encoding version: 0"):
134-
from_approved_callsign(PacketHeader.from_hex(hdr), approved_callsigns)
132+
PacketHeader.from_hex(hdr)
135133

136134

137135
# Test an invalid header: non approved callsign and incorrect version number
138136
def test_is_invalid_hdr2(approved_callsigns: dict[str, str]) -> None:
139137
hdr = "52415454204D4F53530c0b0137000000"
140138

141139
with pytest.raises(UnsupportedEncodingVersionError, match="Unsupported encoding version: 11"):
142-
from_approved_callsign(PacketHeader.from_hex(hdr), approved_callsigns)
140+
PacketHeader.from_hex(hdr)

0 commit comments

Comments
 (0)