add framed binary telemetry transport
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "trikke_protocol.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t packet[TRIKKE_WIRE_MAX_PACKET_SIZE] = {0};
|
||||
const trikke_wire_metadata_t metadata = {
|
||||
.sample_rate_hz = 100,
|
||||
.accel_range_g = 8,
|
||||
.gyro_range_dps = 500,
|
||||
.flags = TRIKKE_METADATA_FLAG_ACCEL_Y_NEGX_Z |
|
||||
TRIKKE_METADATA_FLAG_GYRO_IDENTITY,
|
||||
.accel_offset_counts = {-1.5f, -4.5f, 12.0f},
|
||||
.accel_counts_per_g = {259.0f, 260.0f, 246.0f},
|
||||
.gyro_bias_counts = {9.0f, -154.0f, -7.0f},
|
||||
.gyro_mdps_per_lsb = 17.5f,
|
||||
};
|
||||
size_t size = trikke_encode_metadata_packet(
|
||||
packet, sizeof(packet), 41, 1234567, 2, 3, &metadata);
|
||||
if (size == 0 || fwrite(packet, 1, size, stdout) != size) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const trikke_wire_sample_t samples[] = {
|
||||
{
|
||||
.sequence = 1000,
|
||||
.timestamp_us = 2000000,
|
||||
.accel_x = 1,
|
||||
.accel_y = -2,
|
||||
.accel_z = 258,
|
||||
.gyro_x = -10,
|
||||
.gyro_y = 20,
|
||||
.gyro_z = -30,
|
||||
.accel_status = 0x82,
|
||||
.gyro_status = 0x0F,
|
||||
},
|
||||
{
|
||||
.sequence = 1001,
|
||||
.timestamp_us = 2010000,
|
||||
.accel_x = 4,
|
||||
.accel_y = -5,
|
||||
.accel_z = 257,
|
||||
.gyro_x = 40,
|
||||
.gyro_y = -50,
|
||||
.gyro_z = 60,
|
||||
.accel_status = 0x02,
|
||||
.gyro_status = 0xFF,
|
||||
},
|
||||
};
|
||||
size = trikke_encode_sample_packet(packet, sizeof(packet), 42, 2, 3,
|
||||
samples, 2);
|
||||
if (size == 0 || fwrite(packet, 1, size, stdout) != size) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
|
||||
from trikke_protocol import ( # noqa: E402
|
||||
PACKET_TYPE_METADATA,
|
||||
PACKET_TYPE_SAMPLES,
|
||||
StreamParser,
|
||||
sample_to_csv_row,
|
||||
)
|
||||
|
||||
|
||||
class ProtocolContractTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
compiler = shutil.which("cc")
|
||||
if compiler is None:
|
||||
raise unittest.SkipTest("host C compiler is unavailable")
|
||||
cls.tempdir = tempfile.TemporaryDirectory()
|
||||
executable = Path(cls.tempdir.name) / "protocol_fixture"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-I",
|
||||
str(ROOT / "main"),
|
||||
str(ROOT / "main" / "trikke_protocol.c"),
|
||||
str(ROOT / "tests" / "protocol_fixture.c"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
cls.encoded = subprocess.run(
|
||||
[str(executable)], check=True, capture_output=True
|
||||
).stdout
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls.tempdir.cleanup()
|
||||
|
||||
def test_c_encoder_to_python_parser_contract(self) -> None:
|
||||
parser = StreamParser()
|
||||
frames = []
|
||||
stream = b"startup text\r\n" + self.encoded
|
||||
for offset in range(0, len(stream), 7):
|
||||
frames.extend(parser.feed(stream[offset : offset + 7]))
|
||||
|
||||
self.assertEqual(2, len(frames))
|
||||
metadata_frame, sample_frame = frames
|
||||
self.assertEqual(PACKET_TYPE_METADATA, metadata_frame.packet_type)
|
||||
self.assertEqual(41, metadata_frame.packet_sequence)
|
||||
self.assertEqual(2, metadata_frame.dropped_sample_count)
|
||||
self.assertEqual(3, metadata_frame.loop_overrun_count)
|
||||
self.assertEqual(100, metadata_frame.metadata.sample_rate_hz)
|
||||
self.assertEqual((-1.5, -4.5, 12.0), metadata_frame.metadata.accel_offset_counts)
|
||||
|
||||
self.assertEqual(PACKET_TYPE_SAMPLES, sample_frame.packet_type)
|
||||
self.assertEqual(42, sample_frame.packet_sequence)
|
||||
self.assertEqual(2, len(sample_frame.samples))
|
||||
self.assertEqual(1000, sample_frame.samples[0].sequence)
|
||||
self.assertEqual(2_000_000, sample_frame.samples[0].timestamp_us)
|
||||
self.assertEqual((1, -2, 258), sample_frame.samples[0].accel)
|
||||
self.assertEqual(2_010_000, sample_frame.samples[1].timestamp_us)
|
||||
self.assertEqual((40, -50, 60), sample_frame.samples[1].gyro)
|
||||
self.assertEqual(0xFF, sample_frame.samples[1].gyro_status)
|
||||
self.assertEqual(0, parser.crc_errors)
|
||||
self.assertEqual(len(b"startup text\r\n"), parser.skipped_bytes)
|
||||
|
||||
row = sample_to_csv_row(
|
||||
sample_frame.samples[0], metadata_frame.metadata, 3
|
||||
)
|
||||
self.assertEqual(23, len(row))
|
||||
self.assertEqual((2, 1, 258), tuple(row[14:17]))
|
||||
self.assertEqual(3, row[-1])
|
||||
|
||||
def test_crc_failure_resynchronizes_to_next_frame(self) -> None:
|
||||
first_size = 36 + 48
|
||||
damaged = bytearray(self.encoded[:first_size])
|
||||
damaged[-1] ^= 0x80
|
||||
parser = StreamParser()
|
||||
frames = parser.feed(bytes(damaged) + self.encoded[first_size:])
|
||||
self.assertEqual(1, parser.startup_crc_errors)
|
||||
self.assertEqual(0, parser.crc_errors)
|
||||
self.assertEqual(1, len(frames))
|
||||
self.assertEqual(PACKET_TYPE_SAMPLES, frames[0].packet_type)
|
||||
|
||||
def test_crc_failure_after_sync_is_stream_error(self) -> None:
|
||||
first_size = 36 + 48
|
||||
damaged = bytearray(self.encoded[first_size:])
|
||||
damaged[-1] ^= 0x80
|
||||
parser = StreamParser()
|
||||
frames = parser.feed(self.encoded[:first_size] + bytes(damaged))
|
||||
self.assertEqual(0, parser.startup_crc_errors)
|
||||
self.assertEqual(1, parser.crc_errors)
|
||||
self.assertEqual(1, len(frames))
|
||||
self.assertEqual(PACKET_TYPE_METADATA, frames[0].packet_type)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user