test_parser.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import struct
  2. from pathlib import Path
  3. import pandas as pd
  4. import pytest
  5. from mlog_tool.constants import MLOG_BEGIN_MSG1, MLOG_BEGIN_MSG2, MLOG_END_MSG
  6. from mlog_tool.models import BusFrame, LogHeader, ParsedLog, PlotTraceSpec
  7. from mlog_tool.parser import MLogParser
  8. from mlog_tool.plotting import LogPlotter
  9. from mlog_tool.utils import normalize_field_name
  10. def _pack_fixed_string(value: str, length: int) -> bytes:
  11. raw = value.encode("utf-8")[:length]
  12. return raw + (b"\x00" * (length - len(raw)))
  13. def _write_synthetic_log(path: Path, message_count: int = 2048) -> None:
  14. max_name_len = 16
  15. max_desc_len = 24
  16. max_model_info_len = 24
  17. payload = bytearray()
  18. payload.extend(struct.pack("<H I H H H", 1, 123456789, max_name_len, max_desc_len, max_model_info_len))
  19. payload.extend(_pack_fixed_string("synthetic", max_desc_len))
  20. payload.extend(_pack_fixed_string("unit-test", max_model_info_len))
  21. payload.extend(struct.pack("<B", 1))
  22. payload.extend(_pack_fixed_string("IMU0", max_name_len))
  23. payload.extend(struct.pack("<B B", 7, 3))
  24. payload.extend(_pack_fixed_string("timestamp_ms", max_name_len))
  25. payload.extend(struct.pack("<H H", 5, 1))
  26. payload.extend(_pack_fixed_string("ax", max_name_len))
  27. payload.extend(struct.pack("<H H", 6, 1))
  28. payload.extend(_pack_fixed_string("gyro", max_name_len))
  29. payload.extend(struct.pack("<H H", 6, 3))
  30. payload.extend(struct.pack("<B", 0))
  31. for index in range(message_count):
  32. payload.extend(bytes([MLOG_BEGIN_MSG1, MLOG_BEGIN_MSG2, 7]))
  33. payload.extend(struct.pack("<I", index * 10))
  34. payload.extend(struct.pack("<f", index * 0.5))
  35. payload.extend(struct.pack("<fff", index + 0.1, index + 0.2, index + 0.3))
  36. payload.extend(bytes([MLOG_END_MSG]))
  37. path.write_bytes(payload)
  38. def test_parser_type_exists() -> None:
  39. parser = MLogParser()
  40. assert parser is not None
  41. def test_message_markers_match_matlab_definition() -> None:
  42. assert MLOG_BEGIN_MSG1 == 0x92
  43. assert MLOG_BEGIN_MSG2 == 0x05
  44. assert MLOG_END_MSG == 0x26
  45. def test_normalize_field_name() -> None:
  46. assert normalize_field_name("timestamp_ms") == "timestamp_ms"
  47. assert normalize_field_name("gyro[0]") == "gyro_0"
  48. assert normalize_field_name(" att roll ") == "att_roll"
  49. def test_parsed_log_parameter_lookup_returns_none_when_missing() -> None:
  50. header = LogHeader(
  51. version=1,
  52. timestamp=0,
  53. max_name_len=16,
  54. max_desc_len=16,
  55. max_model_info_len=16,
  56. description="demo",
  57. model_info="demo",
  58. )
  59. parsed_log = ParsedLog(source_path=Path("demo.bin"), header=header)
  60. assert parsed_log.get_parameter("INS", "acc_bias") is None
  61. def test_parser_can_parse_sample_log() -> None:
  62. parser = MLogParser()
  63. sample_log = Path(__file__).resolve().parents[2] / "mlog16.bin"
  64. if not sample_log.exists():
  65. pytest.skip("sample log mlog16.bin is not present in this workspace")
  66. parsed_log = parser.parse(sample_log)
  67. assert parsed_log.header.version >= 1
  68. assert len(parsed_log.header.buses) == 48
  69. assert len(parsed_log.header.parameter_groups) == 9
  70. assert len(parsed_log.buses) == 19
  71. assert "IMU0" in parsed_log.buses
  72. assert "GPS" in parsed_log.buses
  73. assert "time_s" in parsed_log.buses["IMU0"].frame.columns
  74. assert parsed_log.get_parameter_group("SYSTEM") is not None
  75. assert parsed_log.get_parameter("SYSTEM", "MAV_SYS_ID") == 2
  76. def test_plotter_can_compare_traces_across_subplots() -> None:
  77. header = LogHeader(
  78. version=1,
  79. timestamp=0,
  80. max_name_len=16,
  81. max_desc_len=16,
  82. max_model_info_len=16,
  83. description="demo",
  84. model_info="demo",
  85. )
  86. frame = pd.DataFrame(
  87. {
  88. "time_s": [0.0, 0.1, 0.2],
  89. "ax": [1.0, 2.0, 3.0],
  90. "ay": [4.0, 5.0, 6.0],
  91. }
  92. )
  93. parsed_log = ParsedLog(
  94. source_path=Path("demo.bin"),
  95. header=header,
  96. buses={
  97. "IMU0": BusFrame(name="IMU0", frame=frame, timestamp_field="time_s"),
  98. "GPS": BusFrame(name="GPS", frame=frame.rename(columns={"ax": "ve", "ay": "vn"}), timestamp_field="time_s"),
  99. },
  100. )
  101. figure = LogPlotter().build_comparison_figure(
  102. parsed_log,
  103. [
  104. [
  105. PlotTraceSpec(bus_name="IMU0", field_name="ax", subplot_index=0, label="IMU0.ax"),
  106. PlotTraceSpec(bus_name="GPS", field_name="ve", subplot_index=0, label="GPS.ve"),
  107. ],
  108. [PlotTraceSpec(bus_name="IMU0", field_name="ay", subplot_index=1, label="IMU0.ay")],
  109. ],
  110. subplot_count=2,
  111. )
  112. assert len(figure.axes) == 2
  113. assert figure.axes[0].get_title() == "Subplot 1"
  114. assert figure.axes[1].get_title() == "Subplot 2"
  115. assert figure.axes[0].get_shared_x_axes().joined(figure.axes[0], figure.axes[1])
  116. assert len(figure._mlog_cursor_payload) == 2
  117. assert len(figure._mlog_cursor_payload[0]["traces"]) == 2
  118. assert len(figure._mlog_cursor_payload[1]["traces"]) == 1
  119. def test_parser_handles_many_messages_without_losing_column_shape(tmp_path: Path) -> None:
  120. parser = MLogParser()
  121. log_path = tmp_path / "synthetic_mlog.bin"
  122. _write_synthetic_log(log_path, message_count=4096)
  123. parsed_log = parser.parse(log_path)
  124. imu = parsed_log.get_bus("IMU0")
  125. assert imu is not None
  126. assert imu.timestamp_field == "time_s"
  127. assert list(imu.frame.columns[:6]) == ["timestamp_ms", "time_s", "ax", "gyro_0", "gyro_1", "gyro_2"]
  128. assert len(imu.frame) == 4096
  129. assert imu.frame.iloc[0]["time_s"] == 0.0
  130. assert imu.frame.iloc[-1]["timestamp_ms"] == 40950
  131. assert imu.frame.iloc[-1]["gyro_2"] == pytest.approx(4095.3)