serialforge 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. serialforge/__init__.py +56 -0
  2. serialforge/advanced.py +272 -0
  3. serialforge/connection/__init__.py +5 -0
  4. serialforge/connection/command_dispatcher.py +671 -0
  5. serialforge/connection/command_registry.py +193 -0
  6. serialforge/connection/connection_worker.py +380 -0
  7. serialforge/connection/pending_command.py +37 -0
  8. serialforge/connection/read_thread.py +72 -0
  9. serialforge/connection/response_parser.py +92 -0
  10. serialforge/connection/serial_handler.py +284 -0
  11. serialforge/diagnostics/__init__.py +6 -0
  12. serialforge/diagnostics/log_file_manager.py +352 -0
  13. serialforge/diagnostics/traffic_logger.py +286 -0
  14. serialforge/discovery/__init__.py +5 -0
  15. serialforge/discovery/async_scan_thread.py +39 -0
  16. serialforge/discovery/baud_cache.py +117 -0
  17. serialforge/discovery/baud_prober.py +187 -0
  18. serialforge/discovery/device_finder.py +161 -0
  19. serialforge/discovery/port_scanner.py +48 -0
  20. serialforge/enums.py +129 -0
  21. serialforge/errors.py +47 -0
  22. serialforge/models.py +419 -0
  23. serialforge/protocols.py +53 -0
  24. serialforge/py.typed +0 -0
  25. serialforge/settings.py +14 -0
  26. serialforge/testing/__init__.py +12 -0
  27. serialforge/testing/fake_backend.py +54 -0
  28. serialforge/testing/fake_transport.py +128 -0
  29. serialforge/testing/simulated_device.py +67 -0
  30. serialforge/transport/__init__.py +19 -0
  31. serialforge/transport/backend_switch.py +36 -0
  32. serialforge/transport/checksum_calculator.py +77 -0
  33. serialforge/transport/frame_splitter.py +134 -0
  34. serialforge/transport/latency_tracker.py +65 -0
  35. serialforge/transport/port_options.py +31 -0
  36. serialforge/transport/port_registry.py +57 -0
  37. serialforge/transport/serial_transport.py +90 -0
  38. serialforge-0.0.1.dist-info/METADATA +37 -0
  39. serialforge-0.0.1.dist-info/RECORD +41 -0
  40. serialforge-0.0.1.dist-info/WHEEL +4 -0
  41. serialforge-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,67 @@
1
+ """Scriptable device description used by the fake serial backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping, Sequence
6
+ from dataclasses import dataclass, field
7
+
8
+ ResponseScript = Callable[[bytes], bytes | Sequence[bytes] | None]
9
+
10
+
11
+ @dataclass
12
+ # pylint: disable=too-many-instance-attributes
13
+ class SimulatedDevice:
14
+ """Describe a fake port and its deterministic wire behavior.
15
+
16
+ ``responses`` maps request bytes to response chunks. ``script`` can
17
+ provide dynamic behavior and takes precedence.
18
+ """
19
+
20
+ vid: int
21
+ pid: int
22
+ port: str = "SIM1"
23
+ serial_number: str | None = None
24
+ location: str | None = None
25
+ description: str = "serialforge simulated device"
26
+ responses: Mapping[bytes | str, bytes | Sequence[bytes]] = field(
27
+ default_factory=dict
28
+ )
29
+ script: ResponseScript | None = None
30
+ latency_s: float = 0.0
31
+ chunk_size: int | None = None
32
+ echo: bool = False
33
+ disconnect_after_writes: int | None = None
34
+ baudrate: int | None = None
35
+
36
+ @property
37
+ def device(self) -> str:
38
+ """Return the pyserial-style port name."""
39
+ return self.port
40
+
41
+ def response_for(self, data: bytes) -> list[bytes]:
42
+ """Return scripted response chunks for a transmitted request."""
43
+ if self.script is not None:
44
+ result = self.script(data)
45
+ else:
46
+ key = data.decode("utf-8", errors="replace")
47
+ result = self.responses.get(data, self.responses.get(key))
48
+ if result is None:
49
+ chunks: list[bytes] = []
50
+ elif isinstance(result, bytes):
51
+ chunks = [result]
52
+ else:
53
+ chunks = [bytes(item) for item in result]
54
+ if self.echo:
55
+ chunks.insert(0, data)
56
+ if self.chunk_size is None or self.chunk_size <= 0:
57
+ return chunks
58
+ split: list[bytes] = []
59
+ for chunk in chunks:
60
+ split.extend(
61
+ chunk[index : index + self.chunk_size]
62
+ for index in range(0, len(chunk), self.chunk_size)
63
+ )
64
+ return split
65
+
66
+
67
+ __all__ = ["SimulatedDevice", "ResponseScript"]
@@ -0,0 +1,19 @@
1
+ """Transport-layer implementations and test injection points."""
2
+
3
+ from .backend_switch import BackendSwitch
4
+ from .checksum_calculator import ChecksumCalculator
5
+ from .frame_splitter import FrameSplitter
6
+ from .latency_tracker import LatencyTracker
7
+ from .port_options import PortOptions
8
+ from .port_registry import PortRegistry
9
+ from .serial_transport import SerialTransport
10
+
11
+ __all__ = [
12
+ "BackendSwitch",
13
+ "ChecksumCalculator",
14
+ "FrameSplitter",
15
+ "LatencyTracker",
16
+ "PortRegistry",
17
+ "PortOptions",
18
+ "SerialTransport",
19
+ ]
@@ -0,0 +1,36 @@
1
+ """Process-wide selection of the serial port backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from contextlib import contextmanager
7
+
8
+ from ..protocols import PortBackend
9
+ from .serial_transport import SerialTransport
10
+
11
+
12
+ class BackendSwitch:
13
+ """Select a port backend and restore it safely after a test."""
14
+
15
+ _backend: PortBackend | None = None
16
+
17
+ @classmethod
18
+ def current(cls) -> PortBackend:
19
+ """Return the active backend, creating the real backend lazily."""
20
+ if cls._backend is None:
21
+ cls._backend = SerialTransport()
22
+ return cls._backend
23
+
24
+ @classmethod
25
+ @contextmanager
26
+ def use(cls, backend: PortBackend) -> Iterator[PortBackend]:
27
+ """Temporarily replace the active backend."""
28
+ previous = cls._backend
29
+ cls._backend = backend
30
+ try:
31
+ yield backend
32
+ finally:
33
+ cls._backend = previous
34
+
35
+
36
+ __all__ = ["BackendSwitch"]
@@ -0,0 +1,77 @@
1
+ """Checksum calculation and verification for binary frames."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import binascii
6
+ from collections.abc import Callable
7
+
8
+ from ..enums import Checksum
9
+
10
+
11
+ class ChecksumCalculator:
12
+ """Calculate or verify supported checksums with a stable byte order."""
13
+
14
+ # The explicit branches keep algorithm-specific byte order visible.
15
+ # pylint: disable=too-many-return-statements
16
+
17
+ @staticmethod
18
+ def calculate(
19
+ data: bytes,
20
+ algorithm: Checksum,
21
+ custom: Callable[[bytes], bytes] | None = None,
22
+ ) -> bytes:
23
+ """Return the checksum bytes for ``data``."""
24
+ if custom is not None:
25
+ return bytes(custom(data))
26
+ if algorithm is Checksum.NONE:
27
+ return b""
28
+ if algorithm is Checksum.SUM8:
29
+ return bytes((sum(data) & 0xFF,))
30
+ if algorithm is Checksum.XOR:
31
+ value = 0
32
+ for item in data:
33
+ value ^= item
34
+ return bytes((value,))
35
+ if algorithm is Checksum.CRC16_MODBUS:
36
+ return ChecksumCalculator.crc16(data, 0xA001, 0xFFFF)
37
+ if algorithm is Checksum.CRC16_CCITT:
38
+ return ChecksumCalculator.crc16_ccitt(data)
39
+ if algorithm is Checksum.CRC32:
40
+ return (binascii.crc32(data) & 0xFFFFFFFF).to_bytes(4, "little")
41
+ raise ValueError(f"unsupported checksum algorithm: {algorithm}")
42
+
43
+ @staticmethod
44
+ def verify(
45
+ data: bytes,
46
+ expected: bytes,
47
+ algorithm: Checksum,
48
+ custom: Callable[[bytes], bytes] | None = None,
49
+ ) -> bool:
50
+ """Return whether ``expected`` matches the calculated checksum."""
51
+ return ChecksumCalculator.calculate(data, algorithm, custom) == expected
52
+
53
+ @staticmethod
54
+ def crc16(data: bytes, polynomial: int, initial: int) -> bytes:
55
+ """Calculate reflected CRC-16 and return little-endian bytes."""
56
+ value = initial
57
+ for item in data:
58
+ value ^= item
59
+ for _ in range(8):
60
+ value = (value >> 1) ^ polynomial if value & 1 else value >> 1
61
+ return value.to_bytes(2, "little")
62
+
63
+ @staticmethod
64
+ def crc16_ccitt(data: bytes) -> bytes:
65
+ """Calculate CRC-16/CCITT-FALSE and return big-endian bytes."""
66
+ value = 0xFFFF
67
+ for item in data:
68
+ value ^= item << 8
69
+ for _ in range(8):
70
+ if value & 0x8000:
71
+ value = ((value << 1) ^ 0x1021) & 0xFFFF
72
+ else:
73
+ value = (value << 1) & 0xFFFF
74
+ return value.to_bytes(2, "big")
75
+
76
+
77
+ __all__ = ["ChecksumCalculator"]
@@ -0,0 +1,134 @@
1
+ """Incremental byte framing for text and binary serial protocols."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Callable
7
+
8
+ from ..advanced import FramingConfig
9
+ from ..enums import FramingMode
10
+ from ..errors import FramingError
11
+
12
+
13
+ class FrameSplitter:
14
+ """Split arbitrary byte chunks into complete frames without decoding."""
15
+
16
+ _config: FramingConfig
17
+ _buffer: bytearray
18
+ _last_byte_at: float | None
19
+
20
+ def __init__(
21
+ self,
22
+ config: FramingConfig | None = None,
23
+ *,
24
+ clock: Callable[[], float] = time.monotonic,
25
+ ) -> None:
26
+ """Create a splitter with bounded buffering."""
27
+ self._config = config or FramingConfig()
28
+ self._buffer = bytearray()
29
+ self._last_byte_at = None
30
+ self._clock: Callable[[], float] = clock
31
+
32
+ def feed(self, data: bytes) -> list[bytes]:
33
+ """Append bytes and return every complete frame now available."""
34
+ if data:
35
+ self._buffer.extend(data)
36
+ self._last_byte_at = self._clock()
37
+ if len(self._buffer) > self._config.max_buffer_bytes:
38
+ self._resynchronise()
39
+ raise FramingError("receive buffer exceeded max_buffer_bytes")
40
+ if self._config.mode is FramingMode.LINE:
41
+ return self.split_terminator(self._config.terminator)
42
+ if self._config.mode is FramingMode.DELIMITED:
43
+ return self.split_delimited()
44
+ if self._config.mode is FramingMode.LENGTH_PREFIX:
45
+ return self.split_length_prefix()
46
+ return self.split_silence_gap()
47
+
48
+ def flush(self, *, now: float | None = None) -> list[bytes]:
49
+ """Emit a silence-gap frame after the configured idle period."""
50
+ if self._config.mode is not FramingMode.SILENCE_GAP:
51
+ return []
52
+ if not self._buffer or self._last_byte_at is None:
53
+ return []
54
+ current = self._clock() if now is None else now
55
+ gap = self._config.silence_gap_s
56
+ # FramingConfig validates SILENCE_GAP at construction.
57
+ assert gap is not None
58
+ if current - self._last_byte_at < gap:
59
+ return []
60
+ frame = bytes(self._buffer)
61
+ self._buffer.clear()
62
+ return [self.limit_frame(frame)]
63
+
64
+ def split_terminator(self, terminator: bytes) -> list[bytes]:
65
+ """Split line-style frames, preserving no terminator bytes."""
66
+ frames: list[bytes] = []
67
+ while terminator:
68
+ index = self._buffer.find(terminator)
69
+ if index < 0:
70
+ break
71
+ frames.append(self.limit_frame(bytes(self._buffer[:index])))
72
+ del self._buffer[: index + len(terminator)]
73
+ return frames
74
+
75
+ def split_delimited(self) -> list[bytes]:
76
+ """Split frames enclosed by start and end markers."""
77
+ start = self._config.start_marker
78
+ end = self._config.end_marker
79
+ frames: list[bytes] = []
80
+ while start and end:
81
+ begin = self._buffer.find(start)
82
+ if begin < 0:
83
+ self._resynchronise()
84
+ break
85
+ if begin:
86
+ del self._buffer[:begin]
87
+ finish = self._buffer.find(end, len(start))
88
+ if finish < 0:
89
+ break
90
+ stop = finish + len(end)
91
+ frames.append(self.limit_frame(bytes(self._buffer[:stop])))
92
+ del self._buffer[:stop]
93
+ return frames
94
+
95
+ def split_length_prefix(self) -> list[bytes]:
96
+ """Split frames whose length is encoded in the header."""
97
+ offset = self._config.length_offset
98
+ width = self._config.length_size
99
+ frames: list[bytes] = []
100
+ while len(self._buffer) >= offset + width:
101
+ length_start = offset
102
+ length_end = length_start + width
103
+ payload_length = int.from_bytes(
104
+ self._buffer[length_start:length_end],
105
+ "little" if self._config.byteorder == "little" else "big",
106
+ )
107
+ frame_length = length_end + payload_length
108
+ if frame_length > self._config.max_frame_length:
109
+ self._resynchronise()
110
+ raise FramingError("frame exceeded max_frame_length")
111
+ if len(self._buffer) < frame_length:
112
+ break
113
+ frames.append(self.limit_frame(bytes(self._buffer[:frame_length])))
114
+ del self._buffer[:frame_length]
115
+ return frames
116
+
117
+ def split_silence_gap(self) -> list[bytes]:
118
+ """Return no frames until ``flush`` observes a gap."""
119
+ return []
120
+
121
+ def limit_frame(self, frame: bytes) -> bytes:
122
+ """Validate one frame against the configured maximum."""
123
+ if len(frame) > self._config.max_frame_length:
124
+ raise FramingError("frame exceeded max_frame_length")
125
+ return frame
126
+
127
+ def _resynchronise(self) -> None:
128
+ """Drop bounded garbage while retaining possible marker prefixes."""
129
+ limit = self._config.max_frame_length
130
+ if len(self._buffer) > limit:
131
+ del self._buffer[:-limit]
132
+
133
+
134
+ __all__ = ["FrameSplitter"]
@@ -0,0 +1,65 @@
1
+ """EWMA latency and adaptive read interval tracking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..advanced import HandlerStats, ReadLoopConfig
6
+
7
+
8
+ class LatencyTracker:
9
+ """Track uncontended round trips and derive a bounded poll interval."""
10
+
11
+ _config: ReadLoopConfig
12
+ _sample_count: int
13
+ _srtt_s: float | None
14
+ _rttvar_s: float | None
15
+ _rto_s: float | None
16
+
17
+ def __init__(self, config: ReadLoopConfig | None = None) -> None:
18
+ """Create a cold-start tracker."""
19
+ self._config = config or ReadLoopConfig()
20
+ self._sample_count = 0
21
+ self._srtt_s = None
22
+ self._rttvar_s = None
23
+ self._rto_s = None
24
+
25
+ def observe(self, latency_s: float) -> None:
26
+ """Record one positive uncontended round-trip sample."""
27
+ if latency_s <= 0:
28
+ raise ValueError("latency_s must be positive")
29
+ if self._srtt_s is None or self._rttvar_s is None:
30
+ self._srtt_s = latency_s
31
+ self._rttvar_s = latency_s / 2
32
+ else:
33
+ self._rttvar_s = 0.75 * self._rttvar_s + 0.25 * abs(
34
+ self._srtt_s - latency_s
35
+ )
36
+ self._srtt_s = 0.875 * self._srtt_s + 0.125 * latency_s
37
+ self._rto_s = self._srtt_s + 4 * self._rttvar_s
38
+ self._sample_count += 1
39
+
40
+ def reset(self) -> None:
41
+ """Forget all samples after reconnect or device changes."""
42
+ self._sample_count = 0
43
+ self._srtt_s = None
44
+ self._rttvar_s = None
45
+ self._rto_s = None
46
+
47
+ def poll_interval_s(self) -> float:
48
+ """Return the cold-start or adaptive bounded read interval."""
49
+ if self._sample_count < self._config.min_samples:
50
+ return self._config.poll_initial_s
51
+ assert self._srtt_s is not None
52
+ value = self._srtt_s * self._config.poll_factor
53
+ return min(self._config.poll_max_s, max(self._config.poll_min_s, value))
54
+
55
+ def stats(self) -> HandlerStats:
56
+ """Return a frozen snapshot for the public diagnostics API."""
57
+ return HandlerStats(
58
+ srtt_s=self._srtt_s,
59
+ rttvar_s=self._rttvar_s,
60
+ poll_interval_s=self.poll_interval_s(),
61
+ rto_s=self._rto_s,
62
+ )
63
+
64
+
65
+ __all__ = ["LatencyTracker"]
@@ -0,0 +1,31 @@
1
+ """Translate immutable device serial settings into pyserial options."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..advanced import SerialConfig
6
+ from ..enums import FlowControl
7
+
8
+ # This utility intentionally exposes one static conversion operation.
9
+ # pylint: disable=too-few-public-methods
10
+
11
+
12
+ class PortOptions:
13
+ """Translate one serial configuration for either opening path."""
14
+
15
+ @staticmethod
16
+ def build(config: SerialConfig, baudrate: int) -> dict[str, object]:
17
+ """Build pyserial options for a known baudrate."""
18
+ return {
19
+ "baudrate": baudrate,
20
+ "bytesize": int(config.data_bits),
21
+ "parity": config.parity.value,
22
+ "stopbits": float(config.stop_bits.value),
23
+ "xonxoff": config.flow_control is FlowControl.XON_XOFF,
24
+ "rtscts": config.flow_control is FlowControl.RTS_CTS,
25
+ "dsrdtr": config.flow_control is FlowControl.DSR_DTR,
26
+ "write_timeout": config.write_timeout_s,
27
+ "timeout": 0.05,
28
+ }
29
+
30
+
31
+ __all__ = ["PortOptions"]
@@ -0,0 +1,57 @@
1
+ """Process-local exclusive ownership of serial ports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from contextlib import contextmanager
7
+ from threading import RLock
8
+
9
+ from ..errors import PortBusyError
10
+
11
+
12
+ class PortRegistry:
13
+ """Track port ownership with re-entrant, process-local leases."""
14
+
15
+ _owners: dict[str, object] = {}
16
+ _lock = RLock()
17
+
18
+ @classmethod
19
+ def acquire(cls, port: str, owner: object) -> None:
20
+ """Claim ``port`` or raise ``PortBusyError``."""
21
+ with cls._lock:
22
+ current = cls._owners.get(port)
23
+ if current is not None and current is not owner:
24
+ raise PortBusyError(f"serial port is busy: {port}")
25
+ cls._owners[port] = owner
26
+
27
+ @classmethod
28
+ def release(cls, port: str, owner: object) -> None:
29
+ """Release a port only when owned by ``owner``."""
30
+ with cls._lock:
31
+ if cls._owners.get(port) is owner:
32
+ del cls._owners[port]
33
+
34
+ @classmethod
35
+ def is_owned(cls, port: str) -> bool:
36
+ """Return whether the port is currently leased."""
37
+ with cls._lock:
38
+ return port in cls._owners
39
+
40
+ @classmethod
41
+ @contextmanager
42
+ def lease(cls, port: str, owner: object) -> Iterator[object]:
43
+ """Acquire a port for a context and release it afterwards."""
44
+ cls.acquire(port, owner)
45
+ try:
46
+ yield owner
47
+ finally:
48
+ cls.release(port, owner)
49
+
50
+ @classmethod
51
+ def clear(cls) -> None:
52
+ """Clear all leases; intended for isolated test cleanup."""
53
+ with cls._lock:
54
+ cls._owners.clear()
55
+
56
+
57
+ __all__ = ["PortRegistry"]
@@ -0,0 +1,90 @@
1
+ """pyserial-backed port backend and byte transport."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any, cast
7
+
8
+ import serial
9
+ from serial.tools import list_ports
10
+
11
+ from ..protocols import TransportProtocol
12
+
13
+
14
+ class SerialTransport:
15
+ """Provide the replaceable backend and one opened serial connection."""
16
+
17
+ _serial: serial.Serial | None
18
+
19
+ def __init__(self, connection: serial.Serial | None = None) -> None:
20
+ """Create a backend or wrap an already opened connection."""
21
+ self._serial = connection
22
+
23
+ def list_ports(self) -> Sequence[Any]:
24
+ """Return pyserial port descriptors."""
25
+ return tuple(list_ports.comports())
26
+
27
+ def open(
28
+ self, port: str, params: Mapping[str, object]
29
+ ) -> TransportProtocol:
30
+ """Open ``port`` with a mapping of pyserial keyword arguments."""
31
+ values = dict(params)
32
+ values["port"] = port
33
+ return SerialTransport(serial.Serial(**values))
34
+
35
+ def read(self, size: int, timeout_s: float) -> bytes:
36
+ """Read up to ``size`` bytes with a bounded timeout."""
37
+ connection = self._require_connection()
38
+ connection.timeout = timeout_s
39
+ return bytes(connection.read(size))
40
+
41
+ def write(self, data: bytes) -> int:
42
+ """Write bytes and return the accepted byte count."""
43
+ return cast(int, self._require_connection().write(data))
44
+
45
+ def close(self) -> None:
46
+ """Close the connection if it is open."""
47
+ if self._serial is not None:
48
+ self._serial.close()
49
+
50
+ def cancel_read(self) -> None:
51
+ """Interrupt a read where the pyserial platform supports it."""
52
+ connection = self._serial
53
+ if connection is not None and hasattr(connection, "cancel_read"):
54
+ connection.cancel_read()
55
+
56
+ def cancel_write(self) -> None:
57
+ """Interrupt a write where the pyserial platform supports it."""
58
+ connection = self._serial
59
+ if connection is not None and hasattr(connection, "cancel_write"):
60
+ connection.cancel_write()
61
+
62
+ def reset_input_buffer(self) -> None:
63
+ """Discard queued input after an exclusive command timeout."""
64
+ self._require_connection().reset_input_buffer()
65
+
66
+ def set_buffer_size(self, rx_size: int) -> None:
67
+ """Request a driver receive buffer size."""
68
+ connection = self._require_connection()
69
+ setter = getattr(connection, "set_buffer_size", None)
70
+ if setter is not None:
71
+ setter(rx_size=rx_size)
72
+
73
+ def set_control_lines(
74
+ self, *, dtr: bool | None = None, rts: bool | None = None
75
+ ) -> None:
76
+ """Set DTR/RTS only when the caller explicitly supplied them."""
77
+ connection = self._require_connection()
78
+ if dtr is not None:
79
+ connection.dtr = dtr
80
+ if rts is not None:
81
+ connection.rts = rts
82
+
83
+ def _require_connection(self) -> serial.Serial:
84
+ """Return the wrapped connection or fail before touching pyserial."""
85
+ if self._serial is None:
86
+ raise RuntimeError("serial transport is not open")
87
+ return self._serial
88
+
89
+
90
+ __all__ = ["SerialTransport"]
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.5
2
+ Name: serialforge
3
+ Version: 0.0.1
4
+ Summary: PySide6 serial communication toolkit for Windows
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 10
8
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Topic :: Terminals :: Serial
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: loguru>=0.7
17
+ Requires-Dist: pyserial>=3.5
18
+ Requires-Dist: pyside6-essentials>=6.5
19
+ Description-Content-Type: text/markdown
20
+
21
+ # serialforge
22
+
23
+ > 适用版本: 0.0.1 | 当前里程碑: M1
24
+
25
+ `serialforge` 是面向 Windows 10/11 64 位应用的 PySide6 串口通讯库。设计目标是用 QtCore 信号槽和线程封装设备发现、命令调度、自动重连、流量日志和无硬件测试后端。
26
+
27
+ 当前版本完成包骨架与定义层,真实串口传输和调度将在后续里程碑实现。设计契约见 `串口通讯模块_方案_v10定稿.md`,AI 协作入口见 `AGENTS.md`。
28
+
29
+ ## 开发
30
+
31
+ ```powershell
32
+ uv sync
33
+ uv run python scripts/ci.py --fast
34
+ uv run python scripts/ci.py
35
+ ```
36
+
37
+ 项目仅支持 Windows 10/11 64 位;当前未执行 PyPI 上传或真实硬件验证。
@@ -0,0 +1,41 @@
1
+ serialforge/__init__.py,sha256=EQzlRJq8K1jPVdIU9eXGYKyOxEcfY6YlVt1OwwKsIdo,1189
2
+ serialforge/advanced.py,sha256=Q-_qALa_vkODd_-BLIkQupBd4u4zGNgY4RhwBBp7lL4,9224
3
+ serialforge/enums.py,sha256=v1ZNPYPciMS40KowAV-GN76tJ7btcB3cPXF7rHrOHjE,2381
4
+ serialforge/errors.py,sha256=UCkppyxCsX8VKytCqw0X4D5Vti0gq8hwb_iKpGievhA,1072
5
+ serialforge/models.py,sha256=5K0VP2NyA8kfYsBqXVL0SocDH1wVylqY5AOEBN5O_rY,15854
6
+ serialforge/protocols.py,sha256=YQOeLxFZdDqowrdEN2lfDVnmMswRKtP6H0KN-lnwCEs,1581
7
+ serialforge/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ serialforge/settings.py,sha256=Dy2JfDbfNU2z1qyVKDvYsXMo5MGgXSRml6NrBsYUC1g,357
9
+ serialforge/connection/__init__.py,sha256=6PZGXCoW79Kf5EUU_UNTNOGCOCQYsWfMeilpELTN780,105
10
+ serialforge/connection/command_dispatcher.py,sha256=rn20zJN7olRTkdotRIiD54qwC3-_X4BD8gM8UXWGi4Q,27159
11
+ serialforge/connection/command_registry.py,sha256=DntLWB83wE2MYResIBQnwi_Bu856BdMq32PW2eKzVl8,7221
12
+ serialforge/connection/connection_worker.py,sha256=m2q_GI5veuJTSok9ibacKhilkD4GCb-TuUMMXM4J0n8,14726
13
+ serialforge/connection/pending_command.py,sha256=rZYFXIQmJrJDWHJo4hGQRWQg6e3TgIs_RK3m7Mnfjik,1067
14
+ serialforge/connection/read_thread.py,sha256=yvqMvBhnFM1aT7swjvEgRi19-kFidIkw9bkm3cjMftg,2421
15
+ serialforge/connection/response_parser.py,sha256=ZxPdTtoe2wgetMotkZhAHIuXo2Qb_KDu1DxndinNB7M,3560
16
+ serialforge/connection/serial_handler.py,sha256=WabGF7m4Rs7JNJKIlg7ad-yL8YXP4baDr6-elOzKZ_0,11135
17
+ serialforge/diagnostics/__init__.py,sha256=0HmHZGXcf4nRsMCRzvucQL7PLKJTdNyygJF0IalTK4Q,197
18
+ serialforge/diagnostics/log_file_manager.py,sha256=uxB0q8mWIXqHxuWhOsdYNjxIw7p0G4Q7KZxED97JXGs,12864
19
+ serialforge/diagnostics/traffic_logger.py,sha256=dmEvDbBn1ChgeqpvCFwKaE16dD-jX21R1Vhe_3Vvf_g,10083
20
+ serialforge/discovery/__init__.py,sha256=c6Qs5uB52P__gtryKQnVK_GCJipnHSEk1xj5HvqzOJA,101
21
+ serialforge/discovery/async_scan_thread.py,sha256=epn2UPiVw7dnJzNih19xZRemTtgFE5w7vKgoK9-fTQg,1054
22
+ serialforge/discovery/baud_cache.py,sha256=-bAAmy9-9HOYdJrk4rFfHbNvmawkoxxWerDMuuvT15E,4019
23
+ serialforge/discovery/baud_prober.py,sha256=Yo2rwE_CwkzESwigYKVSN_G4j50WBRO3GJPeExX-mFc,7008
24
+ serialforge/discovery/device_finder.py,sha256=OQaKDBd6q5ej6ZDf_SDws_-9ORIpBkE9AnXvEtVQfLY,6120
25
+ serialforge/discovery/port_scanner.py,sha256=vLPIbmN5gvxofS7ePN0VlorN3SlOO9n5788NdrcsYf4,1811
26
+ serialforge/testing/__init__.py,sha256=x0lpcoUm3Q6LFz1PNJtUbfpx3EcefrrW-LKwSCMDdQE,307
27
+ serialforge/testing/fake_backend.py,sha256=Q_uOTXfb1gKO3BApqeGAvO1_720rASNhGfRu6IUCRVY,1804
28
+ serialforge/testing/fake_transport.py,sha256=nAyECllGz8kPzBJ2MgcU0EEwka9BWxLmBspofzutrz8,4330
29
+ serialforge/testing/simulated_device.py,sha256=tXqAzGirAfPuRBm2yQrSRkYrTLiEGUml1t8xMaLwMzA,2139
30
+ serialforge/transport/__init__.py,sha256=xGN_JfA2BSgILiNQAcZ6yOommBKpyyRfnjBluVgUxgE,537
31
+ serialforge/transport/backend_switch.py,sha256=hRk7Vc2wDSgOaEVCItl0YLISXFs3-BEXjcWbmxQQ8NM,962
32
+ serialforge/transport/checksum_calculator.py,sha256=4DjKej_PaaEbZj2BuaOJRIAxrVrIywTvpsDcWXtbR0I,2614
33
+ serialforge/transport/frame_splitter.py,sha256=p6Pq4Spn8TPWy7pjvMlVBveXQtv5vTeKiWvNu-hjbYI,5060
34
+ serialforge/transport/latency_tracker.py,sha256=Pe0x6SkD-u-jfFrk3wgv3WZsyjNR8yg88NvWkUiRToY,2226
35
+ serialforge/transport/port_options.py,sha256=62NOq30vn_0Pu6TAq3Wpq6dRvBCuUwCYDwei67rrzEE,1047
36
+ serialforge/transport/port_registry.py,sha256=GHJfRI0EsubvhXlOfAWUQ9v5596tP1oaKaFnuKUYgAk,1669
37
+ serialforge/transport/serial_transport.py,sha256=rwK90-WJFED-aowUsTEZ_Tyav69F0r5n75C5C0sPMR4,3172
38
+ serialforge-0.0.1.dist-info/METADATA,sha256=ewJTgk0VlfSLiFI0gkV3evsiE3oPc8CDJVKfWSssvZ0,1378
39
+ serialforge-0.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
40
+ serialforge-0.0.1.dist-info/licenses/LICENSE,sha256=s6YNvjBbs8bqr2t4zQUyYysyU-iiVnJQtTGJNzw3tTE,1081
41
+ serialforge-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any