74 lines
3.3 KiB
Python
74 lines
3.3 KiB
Python
import re
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
NODE = ROOT / "firmware" / "xiao-esp32c6-sensor-node"
|
|
def source(name): return (NODE / name).read_text()
|
|
|
|
def test_wiring_matches_assembly():
|
|
config = source("sensor_node_config.h")
|
|
expected = {"RS485_DE_PIN":"D2", "RS485_TX_PIN":"D4", "RS485_RX_PIN":"D5",
|
|
"BNO086_SDA_PIN":"D7", "BNO086_SCL_PIN":"D8", "GPS_RX_PIN":"D9", "GPS_TX_PIN":"D10"}
|
|
for name, pin in expected.items(): assert re.search(rf"#define\s+{name}\s+{pin}\b", config)
|
|
|
|
def test_transport_contract():
|
|
text = source("rs485_transport.cpp")
|
|
for token in ['doc["type"] = "sensor_status"', 'doc["schema_version"]', 'createNestedObject("imu")', 'createNestedObject("gps")', "serial.write('\\n')", "serial.flush()"]:
|
|
assert token in text
|
|
assert "USB_TELEMETRY_ENABLED" in text
|
|
assert "serializeJson(doc, Serial)" in text
|
|
assert 'createNestedObject("nmea")' in text
|
|
assert 'nmea["sentences_passed"]' in text
|
|
for field in ["linear_acceleration_mps2", "angular_velocity_rads", "gravity_mps2", "stability", "hdop", "location_age_ms"]:
|
|
assert field in text
|
|
|
|
def test_loop_is_non_blocking():
|
|
loop = source("xiao-esp32c6-sensor-node.ino").split("void loop()", 1)[1]
|
|
assert "updateGps" in loop and "updateImu" in loop and "TELEMETRY_INTERVAL_MS" in loop
|
|
assert not re.search(r"delay\((?:[2-9]|[1-9]\d+)\)", loop)
|
|
|
|
def test_imu_does_not_redeclare_arduino_degrees_macro():
|
|
imu = source("imu.cpp")
|
|
assert "float degrees(" not in imu
|
|
assert "radiansToDegrees" in imu
|
|
|
|
def test_imu_uses_verified_i2c_address():
|
|
config = source("sensor_node_config.h")
|
|
imu = source("imu.cpp")
|
|
assert "BNO086_I2C_ADDRESS 0x4B" in config
|
|
assert "findBnoAddress" not in imu
|
|
assert "BNO086_INIT_ATTEMPTS" in imu
|
|
assert "BNO086_RUNTIME_RETRY_MS" in imu
|
|
assert "bno086.wasReset()" in imu
|
|
for report in ["SH2_LINEAR_ACCELERATION", "SH2_GYROSCOPE_CALIBRATED", "SH2_GRAVITY", "SH2_STABILITY_CLASSIFIER"]:
|
|
assert report in imu
|
|
|
|
def test_gps_uses_verified_uart_baud():
|
|
config = source("sensor_node_config.h")
|
|
gps = source("gps.cpp")
|
|
assert "GPS_BAUD 38400" in config
|
|
assert "baudHasValidNmea" not in gps
|
|
|
|
def test_level_calibration_is_persistent_and_non_blocking():
|
|
calibration = source("calibration.cpp")
|
|
transport = source("rs485_transport.cpp")
|
|
loop = source("xiao-esp32c6-sensor-node.ino").split("void loop()", 1)[1]
|
|
assert 'preferences.begin("sensor_cal"' in calibration
|
|
assert 'putFloat("pitch_zero"' in calibration
|
|
assert 'putFloat("roll_zero"' in calibration
|
|
assert 'type == "calibrate_level"' in transport
|
|
assert 'type == "clear_level_calibration"' in transport
|
|
assert "updateLevelCalibration" in loop
|
|
assert "delay(5000)" not in calibration
|
|
|
|
def test_rs485_echo_cannot_starve_sensor_loop():
|
|
transport = source("rs485_transport.cpp")
|
|
assert "bytesRemaining" in transport
|
|
assert "discardCommandUntilNewline" in transport
|
|
assert 'type == "sensor_status"' in transport
|
|
assert 'publishCalibrationResult(serial, "invalid_json")' not in transport
|
|
|
|
def test_node_does_not_take_cargo_or_ble_authority():
|
|
combined = "\n".join(p.read_text() for p in NODE.glob("*.*"))
|
|
assert "NimBLE" not in combined and "JBD" not in combined and "server.on(" not in combined
|