refactor: reorganize tests/tools, share python protocol helpers
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-08-17 23:22:44 +08:00
parent 5f5477d658
commit 2a23168728
9 changed files with 266 additions and 174 deletions

View File

@@ -4,115 +4,10 @@
import argparse
import math
import socket
import struct
import threading
import time
PLOT_ESCAPE = 0x1E
PLOT_MARKER = ord("P")
DISCONNECT_WINERRORS = {
10053, # Software caused connection abort
10054, # Connection reset by peer
10058, # Socket shutdown race on Windows
}
def build_frame(
sample_index: int,
channels: int,
plot_format: str,
samples_per_channel: int,
amplitude: float,
frequency_hz: float,
sample_rate_hz: float,
) -> bytes:
values = []
if plot_format == "xy":
for offset in range(samples_per_channel):
t = (sample_index + offset) / sample_rate_hz
x = amplitude * math.cos(2 * math.pi * frequency_hz * t)
y = amplitude * math.sin(2 * math.pi * frequency_hz * t)
values.extend([x, y])
return struct.pack(f"<{len(values)}f", *values)
for offset in range(samples_per_channel):
t = (sample_index + offset) / sample_rate_hz
for ch in range(channels):
phase = 2 * math.pi * ch / max(channels, 1)
value = amplitude * math.sin(2 * math.pi * frequency_hz * t + phase)
values.append(value)
return struct.pack(f"<{len(values)}f", *values)
def cobs_encode(payload: bytes) -> bytes:
if not payload:
return b"\x01"
out = bytearray([0])
code_index = 0
code = 1
for byte in payload:
if byte == 0:
out[code_index] = code
code_index = len(out)
out.append(0)
code = 1
else:
out.append(byte)
code += 1
if code == 0xFF:
out[code_index] = code
code_index = len(out)
out.append(0)
code = 1
out[code_index] = code
return bytes(out)
def build_plot_packet(
payload: bytes,
channels: int,
samples_per_channel: int,
plot_format: str,
) -> bytes:
format_id = {"interleaved": 0, "block": 1, "xy": 2}[plot_format]
header = bytearray()
header.extend(b"XP")
header.append(1) # version
header.append(format_id)
header.append(8) # f32
header.append(0) # little-endian
header.append(channels)
header.extend(struct.pack("<H", samples_per_channel))
header.extend(struct.pack("<I", len(payload)))
header.extend(payload)
return bytes(header)
def build_mixed_plot_frame(
payload: bytes,
channels: int,
samples_per_channel: int,
plot_format: str,
) -> bytes:
packet = build_plot_packet(
payload=payload,
channels=channels,
samples_per_channel=samples_per_channel,
plot_format=plot_format,
)
return bytes([PLOT_ESCAPE, PLOT_MARKER]) + cobs_encode(packet) + b"\x00"
def is_disconnect_error(err: OSError) -> bool:
return isinstance(
err,
(
BrokenPipeError,
ConnectionAbortedError,
ConnectionResetError,
),
) or getattr(err, "winerror", None) in DISCONNECT_WINERRORS
import pv_protocol
def main() -> None:
@@ -217,8 +112,8 @@ def main() -> None:
else:
print(" framer = Line")
print(" decoder = Text")
print(" lua test = Lua framer tests/lua_line_framer.lua")
print(" Lua decoder tests/lua_text_decoder.lua\n")
print(" lua test = Lua framer crates/pipeview-client/tests/fixtures/lua_line_framer.lua")
print(" Lua decoder crates/pipeview-client/tests/fixtures/lua_text_decoder.lua\n")
if args.wire_format == "text":
print(f"sending text only at {args.text_interval:.3f}s intervals")
print(f"sample clock: {args.rate:.1f} samples/sec\n")
@@ -268,7 +163,7 @@ def main() -> None:
next_text_at += args.text_interval
continue
plot_payload = build_frame(
plot_payload = pv_protocol.build_plot_payload(
sample_index=sample_index,
channels=args.channels,
plot_format=args.format,
@@ -278,7 +173,7 @@ def main() -> None:
sample_rate_hz=args.rate,
)
if args.wire_format == "mixed":
frame = build_mixed_plot_frame(
frame = pv_protocol.build_mixed_plot_frame(
payload=plot_payload,
channels=args.channels,
samples_per_channel=samples_per_channel,
@@ -308,7 +203,7 @@ def main() -> None:
if wait > 0:
time.sleep(wait)
except OSError as err:
if not is_disconnect_error(err):
if not pv_protocol.is_disconnect_error(err):
raise
if args.wire_format == "text":
print(f"[conn -] {addr} ({text_count} text lines)")