From c8a4a1183d0211dbde017e29bed5e70d19f0e582 Mon Sep 17 00:00:00 2001 From: Su Yang Date: Fri, 6 Nov 2026 20:18:07 +0800 Subject: [PATCH] Refresh transport documentation and comments against current native behavior --- CHANGELOG.en.md | 2 ++ CHANGELOG.md | 4 ++++ README.md | 2 ++ README.zh-CN.md | 1 + docs/README.md | 11 ++++++++-- docs/en/api.md | 22 ++++++++++++++----- docs/en/build.md | 10 ++++++--- docs/en/can-metadata.md | 2 ++ docs/en/ci.md | 11 ++++++++++ docs/en/device-sessions.md | 4 ++-- docs/en/minicanfd.md | 7 +++++- docs/en/operations.md | 7 +++++- docs/en/pcan.md | 10 +++++++-- docs/en/session.md | 12 +++++----- docs/en/slcan.md | 4 ++++ docs/en/socketcan.md | 12 ++++++++-- docs/zh/api.md | 16 ++++++++++---- docs/zh/build.md | 9 +++++--- docs/zh/can-metadata.md | 2 +- docs/zh/ci.md | 8 +++++++ docs/zh/device-sessions.md | 4 ++-- docs/zh/minicanfd.md | 5 ++++- docs/zh/operations.md | 6 ++++- docs/zh/pcan.md | 8 +++++-- docs/zh/session.md | 7 +++--- docs/zh/slcan.md | 4 +++- docs/zh/socketcan.md | 9 +++++++- include/linkerbot/can/frame.hpp | 11 +++++----- include/linkerbot/can/message.hpp | 6 ++--- include/linkerbot/can/slcan_transport.hpp | 3 ++- include/linkerbot/can/socketcan_transport.hpp | 8 ++++--- .../linkerbot/can/vendor_canfd_transport.hpp | 5 +++-- include/linkerbot/transport/session.hpp | 2 +- tests/test_socketcan_lifecycle.cpp | 2 +- validation/prepare-report-db.py | 2 +- .../baseline-inventory.json | 10 +++++---- 36 files changed, 185 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index a4eb918..4a04ee8 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -4,6 +4,8 @@ English · [简体中文](CHANGELOG.md) ## Unreleased +- Synchronize source guides, backend contracts, API comments and validation inventory with native receive metadata, explicit frame formats and per-device Runtime ownership. Retain fixed-version evidence and distinguish Unreleased APIs from published SDKs. + - Add explicit per-message classic/FD selection while preserving backend defaults, native receive timestamp metadata and opt-in SocketCAN/PCAN receive-own configuration. Reject unsupported vendor/SLCAN echo opt-in, preserve driver counters without epoch conversion, and add encoding, rollback, metadata routing and deployment-evidence regressions. No hand/arm integration or physical hardware acceptance is included; consumers must rebuild with matching headers and library. - Reject unsupported FIFO capacity units, send policies and deadline policies with `ValidationError` before configuration changes or send admission. Preserve valid defaults and existing signatures; add configuration, queued-latest, device/owned-operation and installed-consumer regressions. diff --git a/CHANGELOG.md b/CHANGELOG.md index d93c4ec..374c417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ## 未发布 +- 同步源码指南、后端契约、接口注释和验证清单,修正原生接收元数据、显式帧格式及每设备 Runtime 所有权的过时说明;保留固定版本证据,并区分 Unreleased 接口与已发布 SDK。 + +- 新增逐帧显式经典/FD 类型、原生接收时间戳元数据和 SocketCAN/PCAN 自身发送帧接收选项,保留后端默认行为;vendor/SLCAN 拒绝不支持的回显开启,驱动计数器不推定纪元。补充编码、初始化回滚、元数据路由和部署证据回归。本次不接入 hand/arm、不计为实机验收,使用方须用匹配的头文件和库重新编译。 + - 在配置修改或发送准入前,以 `ValidationError` 拒绝不支持的 FIFO 计量单位、发送策略和 deadline 策略;保留合法默认值和既有签名,增加配置、等待 latest、设备/已登记操作及安装消费者回归。 - 修正设备提交中显式全 Session 队龄检查被改写的问题;新增 `PendingAgeScope::kDefault`,保留设备局部与旧 Session 全局默认行为、既有枚举值和签名,增加作用域、已登记操作、替换、cooldown 及安装消费者回归。 diff --git a/README.md b/README.md index dc35a5f..35b3d25 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ See the [serial and RTU guide](docs/en/modbus-rtu.md). Current source version: **0.3.3**, including optional serial and Modbus RTU modules. Published v0.3.2 packages do not contain those modules. See the [latest published stable release](https://github.com/linker-bot/linkerbot-transport-cpp/releases/latest). See [downloads, checksums and release documentation](docs/en/releases.md). +Source guides also cover [Unreleased changes](CHANGELOG.en.md); use the matching tag, +headers and bundled documentation to determine a published SDK's actual interfaces. The canonical package is `linkerbot-transport`; former `linkerbot-comm` calls and archives remain available as compatibility entries. See [name migration](docs/en/naming.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index 166964f..bba04f4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -15,6 +15,7 @@ RTU 模块处理寄存器事务,设备寄存器映射和运动控制仍由上 当前源码版本:**0.3.3**,包含可选串口和 Modbus RTU 模块。已发布的 v0.3.2 包不包含这两项。下载[最新已发布的稳定版](https://github.com/linker-bot/linkerbot-transport-cpp/releases/latest)。 下载、校验及版本文档见[发行说明](docs/zh/releases.md)。 +源码指南也涵盖[未发布变更](CHANGELOG.md);已发布 SDK 的实际接口以对应标签、头文件和随包文档为准。 规范包名为 `linkerbot-transport`;原 `linkerbot-comm` 查找入口和库文件继续兼容。 参见[命名迁移](docs/zh/naming.md)。 diff --git a/docs/README.md b/docs/README.md index a33f4f9..53107c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,9 @@ This index and the [English](../README.md) and [Chinese](../README.zh-CN.md) READMEs describe the current implementation. Each guide has an English and a Chinese edition. -Historical hardware statements are separated in the validation guide. +Historical hardware statements are separated in the validation guide. Source guides +include the Changelog's Unreleased APIs; published SDKs must be checked against their +matching tag, headers and bundled documentation, rather than the version number alone. ## English guides @@ -17,6 +19,8 @@ require parameters confirmed for the actual hardware; offline CI does not run th - [Windows builds and standalone packages](en/windows-build.md) - [Public API, threading and ownership](en/api.md) - [Session, routing and lifecycle](en/session.md) +- [Per-device sessions and shared Runtime](en/device-sessions.md) +- [CAN frame selection, timestamps and receive-own messages](en/can-metadata.md) - [Serial byte streams and Modbus RTU](en/modbus-rtu.md) - Backends: [SocketCAN](en/socketcan.md), [MiniCANFD](en/minicanfd.md), [PCAN](en/pcan.md), [SLCAN](en/slcan.md) @@ -32,7 +36,8 @@ commands or validation scope must update both editions together. Keep identifier and fixed-commit evidence unchanged when translating. Historical translations belong under `docs/archive/`, with a historical-status notice and a link to this index. -本索引对应当前源码。先完成 FakeTransport 的离线构建和接入,再选择真实后端。 +本索引对应当前源码,包含 Changelog 的 Unreleased 接口;已发布 SDK 应核对对应标签、头文件 +和随包文档,不能只按版本号推定包含所有新能力。先完成 FakeTransport 的离线构建和接入,再选择真实后端。 文档中的真实设备示例需要按已确认的硬件参数调整,不会由离线 CI 自动执行。 ## 维护状态 @@ -57,6 +62,8 @@ under `docs/archive/`, with a historical-status notice and a link to this index. - [公共 API 与线程/所有权契约](zh/api.md):帧、传输层、Dispatcher、共享总线、Pool、取消和测试替身。 - [Session、路由与生命周期](zh/session.md):所有权、回执、共享、取消和恢复。 +- [每设备会话与共享 Runtime](zh/device-sessions.md):独立生命周期与单发送器。 +- [CAN 帧格式与接收元数据](zh/can-metadata.md):显式经典/FD、时间戳及回显限制。 - [通用串口与 Modbus RTU](zh/modbus-rtu.md):独立模块、寄存器事务、方向控制及恢复边界。 - [SocketCAN](zh/socketcan.md):Linux 网络接口、Classic/FD、超时及权限。 - [MiniCANFD](zh/minicanfd.md):平台 ABI、厂商库、枚举及资源重试。 diff --git a/docs/en/api.md b/docs/en/api.md index d75af70..94c6b57 100644 --- a/docs/en/api.md +++ b/docs/en/api.md @@ -19,10 +19,14 @@ header search paths and the C++17 requirement only; it does not replace the impl | Reception and physical batch boundaries | `can::CANDispatcher` | Dedicated receive thread, subscription broadcasts and serialized physical transmit batches | | Recoverable shared bus | `can::ManagedCANBus` | Stores a factory, explicit `start/stop/recover`, legacy transmit queue | | In-process connection pool | `can::SharedCANBusPool`, `CANBusLease` | Same-key bus sharing; leases retain ownership needed to retry the final close | -| Device SDK session | `transport::Session`, `RouteInbox`, `SendReceipt` | Exact routing, one transmit scheduler, deadlines, cancellation and generation-based recovery | +| Shared multi-device runtime | `transport::TransportRuntime`, `DeviceSession`, `DeviceOperation` | Independent device registrations and lifetimes on one shared physical Session/writer | +| Direct Session and legacy sharing | `transport::Session`, `RouteInbox`, `SendReceipt` | Exact routing, one transmit scheduler, deadlines, cancellation and generation-based recovery | | Offline testing | `can::FakeTransport` | In-memory I/O, error injection and synchronization gates; does not represent an adapter | -Device SDKs normally use Session. Use lower layers when implementing a backend or maintaining legacy code. +New multi-device integrations use `TransportRuntime` and one `DeviceSession` per +logical device on the existing Session writer; see [device sessions](device-sessions.md). +Direct Session use remains available. Use lower layers when implementing a backend +or maintaining legacy code. ## Frames and backend interpretation @@ -31,12 +35,18 @@ are at most `0x7ff`; extended identifiers are at most `0x1fffffff` and require `is_extended=true`. Classic frames carry at most 8 bytes and FD frames at most 64; `is_brs=true` requires `is_fd=true`. -| Backend / mode | Transmit interpretation of `is_fd=false,is_brs=false` | +| Backend / mode | Transmit interpretation when `format` is unspecified and `is_fd=false,is_brs=false` | | --- | --- | | SocketCAN Classic, PCAN Classic, Lawicel Classic | Classic | | SocketCAN FD, MiniCANFD | Compatibility behavior: FD without BRS | | PCAN FD, CANable FD | Classic; set `is_fd=true` explicitly for FD | +`Message::format` explicitly selects classic/FD encoding without changing these +legacy defaults. Optional `timestamp` and `is_rx` preserve backend receive metadata; +TX ignores them. Clock domains and per-backend limits are specified in +[CAN metadata](can-metadata.md). Public structure additions require consumers to +rebuild with matching headers and libraries. + Backends may round lengths such as 9, 10 or 11 bytes up to a representable wire length and pad with zeros. Upper-layer parsers must use the returned length and validate the business format. Backend acceptance does not establish CAN acknowledgment or device execution. @@ -99,8 +109,10 @@ to 512 frames. An executing batch does not occupy waiting-queue capacity but cou `pending_tx`. Defaults are FIFO, priority 0 and no finite deadline. See the [Session guide](session.md) for sharing, routing, receipts, recovery and exit contracts. -`transport::Clock` is `std::chrono::steady_clock`; deadlines and timestamps are not UTC -or Unix time. `CancellationSource` and its tokens share an atomic, one-way state. +`transport::Clock` is `std::chrono::steady_clock`; Session deadlines, receipt times +and `RoutedMessage::received_at` are not UTC or Unix time. `Message::timestamp` is +separate backend metadata whose clock and unit must be inspected. +`CancellationSource` and its tokens share an atomic, one-way state. Cancellation is repeatable but cannot be reversed. Cancellation, deadline expiry or a wait timeout does not establish that a frame already passed to a driver was not delivered. Inspect `sent_frames`, `current_frame_uncertain` and device state at the business layer. diff --git a/docs/en/build.md b/docs/en/build.md index 099e055..965b616 100644 --- a/docs/en/build.md +++ b/docs/en/build.md @@ -267,8 +267,9 @@ The separate `C++ Report Card` workflow runs [cppreportcard-action](https://github.com/soulteary/cppreportcard-action) v1.0.2, pinned to a full commit SHA, on Linux ARM64. It runs clang-format 18, clang-tidy 18 and Cppcheck in `full` mode after building the project, quickstart, installed -consumer and shared-library consumer. Their four compilation databases are combined -so the standalone consumers are analyzed with the main project's sources and tests. +consumer, shared-library consumer and installed-library Python validation probe. +Their five compilation databases are combined so all standalone consumers and the +probe are analyzed with the main project's sources and tests. The configuration is `.cppreportcard.json`. Platform-dependent exclusions are `tests/vendor_fixture_dependency.cpp` (Windows-only), `validation/pcan_header_abi.cpp` @@ -281,7 +282,10 @@ The report uses `clang-analyzer-*` and `bugprone-*`; it does not replace the existing style workflow's different rule set or platform checks. `validation/prepare-report-db.py` merges the databases and exposes project -`-isystem` directories as `-I` directories for Cppcheck. Through +`-isystem` directories as `-I` directories for Cppcheck. The workflow also records +the compiler's actual target macros. Cppcheck preincludes the installed multiarch +Python configuration to traverse CPython's same-name forwarding header; no probe +source or preprocessor error is excluded. Through `validation/cppcheck-report.sh`, Cppcheck uses its `std` library model and `validation/catch2-cppcheck.cfg` for the Catch2 macros used by the project. The wrapper suppresses only diff --git a/docs/en/can-metadata.md b/docs/en/can-metadata.md index 718c305..61be26a 100644 --- a/docs/en/can-metadata.md +++ b/docs/en/can-metadata.md @@ -18,6 +18,8 @@ frames stay classic). Existing `is_fd`/`is_brs` callers need no changes. Receive frames keep the existing flags and leave the new TX override unspecified. A forwarder that needs classic-on-FD must explicitly choose the classic override. `Session::submit` validates the override before admission or latest replacement. +Receive route keys use the arbitration ID and standard/extended-ID flag, not this +TX classic/FD override. ## Receive timestamps diff --git a/docs/en/ci.md b/docs/en/ci.md index adaf32c..78bda60 100644 --- a/docs/en/ci.md +++ b/docs/en/ci.md @@ -125,6 +125,17 @@ bytes and downloads them to compare hashes. It alone has repository write permis automatically publishes or moves tags. Inspect an incomplete draft before retrying; released bytes require a new version instead of replacement. Changing these workflows does not itself create a tag or Release. +## Python pre-integration validation + +The separate `python-preintegration.yml` workflow builds an isolated CPython probe +against the installed native library. It tests conventional GIL-enabled CPython +3.10–3.13 on Linux x86_64/ARM64, Windows x64 with 3.12 and Windows ARM64 with 3.13. +The source fixture is `validation/python-preintegration`; it is not a production +binding or a wheel. Subinterpreters and free-threaded builds are rejected. +Allocation/receipt, callback, lifecycle and deployment-gate checks do not establish +actual hand/arm Runtime sharing or hardware compatibility. See [CAN metadata](can-metadata.md) +for backend-specific capabilities and physical validation limits. + ## CI timing and reproducible comparisons See the [measured results and limitations](ci-performance.md), including all baseline and diff --git a/docs/en/device-sessions.md b/docs/en/device-sessions.md index bd4c84d..45b367a 100644 --- a/docs/en/device-sessions.md +++ b/docs/en/device-sessions.md @@ -44,7 +44,7 @@ The Python source baselines use a shared hand domain of 256 request batches and ## Routes and diagnostics -`subscribe_routes()` owns exact ID/format pairs. `subscribe_rules()` owns masked ID/format ranges, including extended A7 Lite routing. Rules preserve the original frame and record their logical key separately. Overlap is rejected using actual wire ranges, including exact-versus-masked conflicts. Group registration rolls back after allocation failure. Masked routes cannot use the legacy global extractor. +`subscribe_routes()` owns exact ID/standard-versus-extended-ID pairs. `subscribe_rules()` owns masked ID ranges with the same extended-ID flag, including extended A7 Lite routing. Rules preserve the original frame and record their logical key separately. Overlap is rejected using actual wire ranges, including exact-versus-masked conflicts. Group registration rolls back after allocation failure. Masked routes cannot use the legacy global extractor. `observe_rules(rules, capacity)` provides bounded read-only discovery observation. It does not take route ownership or ordinary receive-budget slots; a full observer drops its own copy. Normal owners receive their frame first. Discovery requests still use the same device writer. Observation inbox health records its own backlog and drops. @@ -52,4 +52,4 @@ The Python source baselines use a shared hand domain of 256 request batches and Channel health includes route count, receive backlog/high-water mark, routed frames, stale frames, registration quota rejections and close drops. Device health reports admission state, generation, instance, registered operations, send-error streak, queued FIFO frames/latest slots, in-flight jobs and oldest owned work. -This transport implementation does not provide Python bindings, Python Future execution, protocol transactions, SDK migration or motor safety policy. Hardware timestamps and arbitrary Python backend plugins are not provided by these APIs. Software tests do not establish hardware or firmware compatibility; deployments need the device/driver/topology acceptance checks described in the migration plan. +This transport implementation does not provide Python bindings, Python Future execution, protocol transactions, SDK migration or motor safety policy. Native receive timestamps and backend-specific echo options are described in [CAN metadata](can-metadata.md). Arbitrary Python backend plugins are not implied by the native backends. Software tests do not establish hardware or firmware compatibility; deployments need the device/driver/topology acceptance checks described in the migration plan. diff --git a/docs/en/minicanfd.md b/docs/en/minicanfd.md index eb9c476..2647dee 100644 --- a/docs/en/minicanfd.md +++ b/docs/en/minicanfd.md @@ -94,6 +94,7 @@ confirm indices, bitrates and vendor fields against enumeration and vendor docum | `frame_type` | `0x04`; retained for aggregate-initialization source compatibility only; unused by TX or `CANFD_Init` | | `send_timeout_ms` | 10 ms, accepts 0–100 ms | | `receive_timeout_ms` | 10 ms, accepts 0–10 ms | +| `receive_own_messages` | `false`; `true` is rejected before library loading or channel initialization | | Native receive buffer per call | Requests at most 64 frames | | Native send batch per call | At most 100 frames | @@ -101,7 +102,11 @@ Default arbitration/data rates are 1 Mbit/s / 5 Mbit/s. Standard/extended IDs, F handled per message. Untagged messages on an FD Transport retain the existing FD-without-BRS behavior. The upper-layer device protocol selects IDs and BRS; the Transport does not infer flags from model names. `Message.dlc` counts 0–64 bytes and is converted to the vendor's 0–15 DLC code. -Unrepresentable lengths round up and are zero-padded. `Config::frame_type` does not override send flags. +Unrepresentable lengths round up and are zero-padded. `Config::frame_type` does not override send flags. Set `Message::format` to +`FrameFormat::kClassic` for an explicit classic frame on the FD channel, with at most +eight bytes and no FD/BRS flags. Receive timestamps preserve the raw 32-bit vendor +counter; units, epoch and wrap extension are not inferred. Direction remains unknown, +and receive-own opt-in is unsupported by this ABI. See [CAN metadata](can-metadata.md). A direct `write_batch()` call with more than 100 frames synchronously throws `ValidationError`. When Session selects a native batch with `gap=0`, it passes the entire `MessageBatch` to that diff --git a/docs/en/operations.md b/docs/en/operations.md index 737e26d..42a98a8 100644 --- a/docs/en/operations.md +++ b/docs/en/operations.md @@ -16,7 +16,12 @@ Expose only the interfaces the container uses: a visible CAN network interface f ## Sharing, shutdown, and failed cleanup -Devices can share a Session, but it is not an independent connection handle for each caller. Any sharer calling `close()` closes the entire Session, cleans up resources held by that Session, and invalidates the other sharers' inboxes. Ownership of an external bus or borrowed dispatcher does not transfer to the Session. When one device finishes, close its inbox and cancel its endpoint first; a common owner should close the Session last. +For independent device lifetimes, use `TransportRuntime` with one `DeviceSession` +per device. Closing a device releases its own work, subscriptions and counted lease; +peers remain connected, and the last device releases the channel. Retry ownership +and coordinated recovery are described in [device sessions](device-sessions.md). + +For legacy callers sharing a raw Session, it is not an independent connection handle for each caller. Any sharer calling `close()` closes the entire Session, cleans up resources held by that Session, and invalidates the other sharers' inboxes. Ownership of an external bus or borrowed dispatcher does not transfer to the Session. When one device finishes, close its inbox and cancel its endpoint first; a common owner should close the Session last. Shutdown order: stop producing application tasks → cancel endpoints no longer needed → check in-flight receipts and device state → close inboxes/Session → handle retryable cleanup errors → release borrowed dispatchers or external buses. Do not synchronously call Session `close()`/`recover()` from callbacks. Before destroying a borrowed dispatcher, call `Session::drain_deferred_cleanup()` from an external thread to wait for deferred cleanup. diff --git a/docs/en/pcan.md b/docs/en/pcan.md index 85d0d2a..14bb9bf 100644 --- a/docs/en/pcan.md +++ b/docs/en/pcan.md @@ -75,7 +75,8 @@ accepted on specified hardware. When changing bitrate, also set prescalers, segm Configuration validates ranges, SJW ≤ TSEG2 and exact bitrates. PCAN sends and receives explicit frame flags: `is_fd=false,is_brs=false` remains Classic on an -FD channel. FD requires `is_fd=true`; BRS additionally requires `is_brs=true`. The device protocol +FD channel when `format` is unspecified. `FrameFormat::kClassic` and `kFd` explicitly +select the frame type; BRS still requires both `is_fd=true` and `is_brs=true`. The device protocol selects standard/extended IDs and BRS; this library has no hand-model encoder. Wire DLC 0–15 maps to 0–64 bytes. Unrepresentable send lengths round up to a legal DLC and are zero-padded. Classic rejects payloads above 8 bytes, FD/BRS and invalid IDs without truncation. @@ -103,7 +104,12 @@ combined error statuses are not suppressed. `read()` also returns empty after fi frames until the deadline or after at most 256 vendor read attempts, so an empty optional does not by itself prove that the full receive wait elapsed. `PcanTransportError::reason()` distinguishes bus-off, unavailable/uninitialized adapters, TX queue full and bus errors, retaining raw status bits in `native_status()`. -RTR/echo frames are filtered; status/error messages produce errors instead of application data. +RTR frames are filtered. Echo frames are filtered by default; opt-in +`PcanConfig::receive_own_messages=true` enables the native setter and delivers them with +`Message::is_rx=false`. Missing setter support fails before initialization; driver +rejection follows the channel cleanup/retry protocol. Status/error messages produce +errors instead of application data. Driver timestamps retain microsecond values and +a driver-specific clock, without epoch conversion. See [CAN metadata](can-metadata.md). Synchronous driver calls have no cancellation API, so a stuck driver cannot be forcibly interrupted. Windows driver call timing and load behavior still require hardware validation. diff --git a/docs/en/session.md b/docs/en/session.md index 6666ffb..55b2691 100644 --- a/docs/en/session.md +++ b/docs/en/session.md @@ -94,9 +94,11 @@ preserves the original registration on conflict. `submit(batch, options)` enqueues and immediately returns `transport::SendReceipt`; a full queue throws `QueueFullError`. By default, capacity counts waiting batches, not the executing batch; SDK options below can instead count FIFO frames and separate latest slots. -There is no global bound on frames per batch or total route groups, so a bounded queue -does not bound total memory. Applications must also limit batch sizes, submission rate -and total routes. `send(batch, options)` submits then waits through the same scheduler. +By default there is no global bound on batch frames or registered route keys. +Configure `SessionOptions::max_batch_frames`, `max_route_keys` and `max_pending_rx` +to add those limits; zero retains the unbounded legacy behavior. Queue capacity +alone does not bound total memory. Applications must also bound submission rate +and inbox capacities. `send(batch, options)` submits then waits through the same scheduler. Submission validates the batch before enqueueing. An already-cancelled token, an already-expired deadline or closed session throws synchronously, without returning a receipt. Backend-specific @@ -113,7 +115,7 @@ Use explicit endpoints: `cancel_endpoint("")` matches unnamed tasks and does not one particular device. Backends supporting native batching may combine sends with no gap. MiniCANFD accepts at most 100 frames in one native batch; exceeding this with `gap=0` completes the receipt as failed. See [MiniCANFD](minicanfd.md). Other backends may impose -driver-specific limits; Session's lack of a global frame limit does not imply unlimited backend capacity. +driver-specific limits; a zero `max_batch_frames` limit does not imply unlimited backend capacity. `SendOptions.deadline` is an absolute monotonic-clock time. The default execution policy covers queueing, transmit-lock waiting and inter-frame waits. `cancellation` cancels unfinished tasks; `cancel_endpoint(name)` @@ -293,7 +295,7 @@ count as backend write failures. `set_route_key_extractor()` accepts a native function that maps a received `can::Message` to a `RouteKey`. Configure it before the first successful route registration or accepted send. Unmatched unsolicited RX before registration does not freeze it; recovery does not unfreeze it. An empty -function restores exact ID/format routing. The function runs under the Session state lock: +function restores exact ID/standard-versus-extended-ID routing. The function runs under the Session state lock: it must be fast, nonblocking and must not reenter the Session. It should not invoke Python or perform I/O. Exceptions or invalid returned keys increment `route_key_errors` and drop only that frame. The delivered `message` retains its original ID, flags and payload; diff --git a/docs/en/slcan.md b/docs/en/slcan.md index bed8d52..729f97d 100644 --- a/docs/en/slcan.md +++ b/docs/en/slcan.md @@ -95,6 +95,10 @@ The parser supports fragmented and concatenated serial input. Incomplete lines a requests at most 256 bytes. Invalid ASCII, BEL, overlong lines, queue overflow, send failure and serial ACK timeout enter the faulted state. Close and reconstruct the transport to avoid using a late ACK. RTR, timestamp suffixes and command echo are unsupported. +`SlcanConfig::receive_own_messages` defaults to false; true is rejected before serial +initialization. No receive timestamp or direction is synthesized. `Message::format` +can select classic/FD explicitly within the configured profile and link capability; +see [CAN metadata](can-metadata.md). Windows uses exclusive `CreateFileW`, 8N1, no software/hardware flow control, disabled DTR/RTS, and overlapped I/O / CancelIoEx. After cancellation it waits for OS completion before freeing diff --git a/docs/en/socketcan.md b/docs/en/socketcan.md index 4395b4b..c2e24f4 100644 --- a/docs/en/socketcan.md +++ b/docs/en/socketcan.md @@ -78,8 +78,16 @@ unexpected read sizes and system I/O failures produce errors. `Message.dlc` counts bytes; extended identifiers require `is_extended=true`. For compatibility, an untagged message sent in FD mode is FD without BRS. Setting `is_fd=false` -cannot force a Classic transmission on this backend in FD mode. A Classic backend rejects FD/BRS -and payloads larger than 8 bytes. +alone cannot force a Classic transmission in FD mode. Set `Message::format` to +`FrameFormat::kClassic` to select it explicitly; FD/BRS flags must be false and the +payload must not exceed 8 bytes. `kUnspecified` retains the old FD default. A Classic +backend rejects FD/BRS and payloads larger than 8 bytes. + +The options constructor is `SocketCANTransport(options, interface_name)`. +`SocketCANOptions::receive_own_messages` defaults to false and `timestamp_mode` to +`kDisabled`; opt in to native echo or software/raw hardware timestamps explicitly. +`read()` uses `recvmsg()` to preserve available metadata. See [CAN metadata](can-metadata.md) +for clock domains, missing timestamps, direction flags and hardware-validation limits. The socket receive timeout is 10 ms and the send timeout is 50 ms. A receive timeout returns an empty optional, but an empty result does not prove that the full receive wait elapsed. A negative system `write()` result throws `TransportIOError` with `system_error()`; diff --git a/docs/zh/api.md b/docs/zh/api.md index 369308b..39b2f88 100644 --- a/docs/zh/api.md +++ b/docs/zh/api.md @@ -18,10 +18,12 @@ CAN 请求/回复关联及业务幂等由上层 SDK 实现。串口和 RTU 见[ | 接收与物理批次边界 | `can::CANDispatcher` | 单独接收线程、订阅广播,并串行化物理发送批次 | | 可恢复共享总线 | `can::ManagedCANBus` | 保存 factory,显式 `start/stop/recover`,兼容旧发送队列 | | 进程内连接池 | `can::SharedCANBusPool`、`CANBusLease` | 相同 key 共享总线;lease 明确持有和重试最后一次关闭 | -| 设备 SDK 会话 | `transport::Session`、`RouteInbox`、`SendReceipt` | 精确路由、单发送调度器、deadline、取消和 generation 恢复 | +| 多设备共享运行时 | `transport::TransportRuntime`、`DeviceSession`、`DeviceOperation` | 独立设备注册与生命周期,共用物理 Session/发送器 | +| 直接 Session 与兼容共享 | `transport::Session`、`RouteInbox`、`SendReceipt` | 精确路由、单发送调度器、deadline、取消和 generation 恢复 | | 离线测试 | `can::FakeTransport` | 内存收发、错误注入和并发门,不代表真实适配器 | -通常由设备 SDK 使用 Session;只有实现新后端或兼容旧代码时才直接使用更低层接口。 +新多设备接入使用 `TransportRuntime`,每个逻辑设备独立持有 `DeviceSession`,共用现有 Session +发送器,见[设备会话](device-sessions.md)。直接使用 Session 的接口继续保留;实现新后端或兼容旧代码时可使用更低层接口。 ## 帧与后端解释 @@ -29,12 +31,16 @@ CAN 请求/回复关联及业务幂等由上层 SDK 实现。串口和 RTU 见[ 最大 `0x1fffffff`;扩展帧必须显式设置 `is_extended=true`。Classic 最多 8 字节,FD 最多 64 字节;`is_brs=true` 必须同时设置 `is_fd=true`。 -| 后端 / 模式 | `is_fd=false,is_brs=false` 的发送解释 | +| 后端 / 模式 | `format` 未指定且 `is_fd=false,is_brs=false` 的发送解释 | | --- | --- | | SocketCAN Classic、PCAN Classic、Lawicel Classic | Classic | | SocketCAN FD、MiniCANFD | 兼容行为:FD、无 BRS | | PCAN FD、CANable FD | Classic;FD 帧必须显式设置 `is_fd=true` | +`Message::format` 可显式选择经典/FD 编码,不改变上述默认值。可选 `timestamp` 和 `is_rx` +保存后端接收元数据,发送忽略这两项;时钟及后端限制见 [CAN 元数据说明](can-metadata.md)。 +公共结构新增字段后须用匹配的头文件和库重新编译。 + 后端可能把 9、10、11 等 FD 字节长度向上补零到可表示的线长。上层解析必须使用返回的实际 长度并校验业务格式,不能假定后端接受等于 CAN 对端 ACK 或设备已执行。 @@ -83,7 +89,9 @@ Session 默认发送队列容量为 2000 个**等待批次**,路由 inbox 默 批次不占等待队列容量,但计入 `pending_tx`。默认策略为 FIFO、priority 0、deadline 无上限。 完整的共享、路由、回执、恢复和退出契约见 [Session 指南](session.md)。 -`transport::Clock` 是 `std::chrono::steady_clock`,所有 deadline 和时间戳都不是 UTC/Unix 时间。 +`transport::Clock` 是 `std::chrono::steady_clock`,Session deadline、回执时间和 +`RoutedMessage::received_at` 不是 UTC/Unix 时间。`Message::timestamp` 是独立的后端元数据, +须按其时钟和单位解释。 `CancellationSource` 与其 token 共享一个原子、单向状态;取消可重复调用但不可撤销。取消、deadline 或等待超时都不能证明已经进入驱动的帧未送达,业务层仍须核对 `sent_frames`、 `current_frame_uncertain` 和设备状态。 diff --git a/docs/zh/build.md b/docs/zh/build.md index 1dd959b..bbb7b99 100644 --- a/docs/zh/build.md +++ b/docs/zh/build.md @@ -237,8 +237,9 @@ env TSAN_OPTIONS="halt_on_error=1:second_deadlock_stack=1:external_symbolizer_pa 独立的 `C++ Report Card` 工作流在 Linux ARM64 上运行固定完整提交 SHA 的 [cppreportcard-action](https://github.com/soulteary/cppreportcard-action) v1.0.2。 -构建项目、quickstart、安装后消费者和共享库消费者后,合并四份编译数据库,以 `full` -模式运行 clang-format 18、clang-tidy 18 和 Cppcheck,覆盖主工程源码、测试及独立消费者。 +构建项目、quickstart、安装后消费者、共享库消费者和使用安装库的 Python 验证模块后, +合并五份编译数据库,以 `full` 模式运行 clang-format 18、clang-tidy 18 和 Cppcheck, +覆盖主工程源码、测试、独立消费者和验证模块。 配置位于 `.cppreportcard.json`。平台依赖排除项包括 Windows 专用文件 `tests/vendor_fixture_dependency.cpp`、需要厂商头文件的 `validation/pcan_header_abi.cpp`, @@ -249,7 +250,9 @@ env TSAN_OPTIONS="halt_on_error=1:second_deadlock_stack=1:external_symbolizer_pa 或平台验证。 `validation/prepare-report-db.py` 合并编译数据库,并将项目的 `-isystem` 目录显式表示为 -`-I`,供 Cppcheck 使用。`validation/cppcheck-report.sh` 让 Cppcheck 使用 `std` 标准库模型, +`-I`,供 Cppcheck 使用。工作流同时记录编译器的实际目标宏,并让 Cppcheck 预包含已安装的 +多架构 Python 配置头,以解析 CPython 同名转发头;不排除验证模块源码或预处理错误。 +`validation/cppcheck-report.sh` 让 Cppcheck 使用 `std` 标准库模型, 并通过 `validation/catch2-cppcheck.cfg` 为项目使用的 Catch2 宏建模。 包装脚本仅抑制 `missingIncludeSystem`:编译器标准库头文件不是 Cppcheck 的分析输入。 完整模式中的 clang-tidy 仍检查每个编译单元的缺失头文件,包括尖括号形式的项目头文件; diff --git a/docs/zh/can-metadata.md b/docs/zh/can-metadata.md index 0208040..af4eff8 100644 --- a/docs/zh/can-metadata.md +++ b/docs/zh/can-metadata.md @@ -2,7 +2,7 @@ 新增字段保持现有源码调用和聚合初始化方式兼容。使用方须用匹配的头文件和库重新编译;`Message` 和配置结构大小增加,不保证已编译二进制的 ABI 兼容。本次不接入 hand/arm,也不修改 Python 生产绑定。 -`Message::format` 默认 `FrameFormat::kUnspecified`,保留原有后端行为:SocketCAN/vendor 的 FD 链路将未标记帧视为 FD,PCAN/SLCAN 将其视为经典帧。显式 `kClassic` 可在 FD 通道发送经典帧,要求不超过 8 字节且无 FD/BRS 标志;`kFd` 在经典链路上会被拒绝。非法枚举、冲突标志和超长经典帧在 Session 入队或替换 latest 任务之前被拒绝。接收帧只保留既有 flags,新的发送覆盖字段保持未指定。 +`Message::format` 默认 `FrameFormat::kUnspecified`,保留原有后端行为:SocketCAN/vendor 的 FD 链路将未标记帧视为 FD,PCAN/SLCAN 将其视为经典帧。显式 `kClassic` 可在 FD 通道发送经典帧,要求不超过 8 字节且无 FD/BRS 标志;`kFd` 在经典链路上会被拒绝。非法枚举、冲突标志和超长经典帧在 Session 入队或替换 latest 任务之前被拒绝。接收帧只保留既有 flags,新的发送覆盖字段保持未指定。接收路由 key 使用仲裁 ID 和标准/扩展标志,不使用这个发送端经典/FD 覆盖字段。 `Message::timestamp` 可为空,携带原生数值、单位、时钟、来源和计数器位宽。它与 Session 的本地单调时钟 `received_at` 分开保存;发送忽略接收元数据,不用主机读取时间填补缺失值。 diff --git a/docs/zh/ci.md b/docs/zh/ci.md index f62ce00..6d3ce66 100644 --- a/docs/zh/ci.md +++ b/docs/zh/ci.md @@ -101,6 +101,14 @@ gh workflow run release.yml --repo linker-bot/linkerbot-transport-cpp \ job 有仓库写权限,不自动正式发布或移动标签。不完整草稿须先检查再重试,正式产物修订 需新版本。修改工作流本身不会创建标签或 Release。 +## Python 接入前验证 + +独立的 `python-preintegration.yml` 使用已安装原生库构建 CPython 验证模块,覆盖 Linux +x86_64/ARM64 的常规 GIL CPython 3.10–3.13、Windows x64 的 3.12 及 Windows ARM64 的 3.13。 +源码入口为 `validation/python-preintegration`;它不是生产绑定或 wheel,拒绝子解释器和 +free-threaded 构建。分配失败、回执、回调、生命周期和部署证据校验不证明实际 hand/arm 已共用 +Runtime,也不代表实机兼容,后端能力与实机边界见 [CAN 元数据说明](can-metadata.md)。 + ## CI 耗时对照 [实测结果与限制(英文)](../en/ci-performance.md)记录全部基线/优化运行链接、三轮中位数、 diff --git a/docs/zh/device-sessions.md b/docs/zh/device-sessions.md index 333b277..13e78fb 100644 --- a/docs/zh/device-sessions.md +++ b/docs/zh/device-sessions.md @@ -42,7 +42,7 @@ SDK 在关闭前用 `begin_operation()` 登记已有工作。可移动的 `Devic ## 路由、发现与诊断 -`subscribe_routes()` 注册精确 ID 和帧格式,`subscribe_rules()` 注册掩码范围,支持 A7 Lite 扩展 ID。始终交付原始帧,逻辑 key 单独记录。按实际 wire 范围拒绝精确与掩码的重叠,整组注册在分配失败后回滚。掩码规则不能与旧全局 extractor 混用。 +`subscribe_routes()` 注册精确 ID 和标准/扩展 ID 标志,`subscribe_rules()` 注册掩码范围,支持 A7 Lite 扩展 ID。始终交付原始帧,逻辑 key 单独记录。按实际 wire 范围拒绝精确与掩码的重叠,整组注册在分配失败后回滚。掩码规则不能与旧全局 extractor 混用。 `observe_rules(rules, capacity)` 提供显式、有界、只读的发现观察。它不占用普通路由所有权或普通接收总容量,原始 owner 优先收到自己的帧;观察流满时仅丢弃其观察副本。发现请求发送仍使用设备的同一 writer。观察 inbox 的容量与丢弃情况通过自身 health 查看。 @@ -50,4 +50,4 @@ SDK 在关闭前用 `begin_operation()` 登记已有工作。可移动的 `Devic 通道 health 包含路由数、普通接收积压与高水位、已路由帧、过期帧、额度拒绝和关闭丢弃。设备 health 包含准入、generation、instance、已登记操作、错误连续计数、等待 FIFO 帧/latest 槽、in-flight Job 和最老工作。设备故障原因由 inbox 保留,旧式 inbox 的关闭异常约定不变。 -本次不交付 Python 绑定、Future 执行、协议事务、hand/arm SDK 接入或电机安全政策。硬件时间戳和任意 Python 插件后端仍需各自兼容实现。软件 fake 测试不代表实机、固件与驱动已验收;部署继续遵循迁移方案的型号、驱动和拓扑验收要求。 +本次不交付 Python 绑定、Future 执行、协议事务、hand/arm SDK 接入或电机安全政策。原生接收时间戳和后端回显选项见 [CAN 元数据说明](can-metadata.md);任意 Python 插件后端不自动获得原生支持。软件 fake 测试不代表实机、固件与驱动已验收;部署继续遵循迁移方案的型号、驱动和拓扑验收要求。 diff --git a/docs/zh/minicanfd.md b/docs/zh/minicanfd.md index 1447b17..f8e03c4 100644 --- a/docs/zh/minicanfd.md +++ b/docs/zh/minicanfd.md @@ -88,6 +88,7 @@ Session 创建后已经启动,没有额外的 open 方法。 | `frame_type` | `0x04`;只保留聚合初始化源码兼容,不参与 TX 或 `CANFD_Init` | | `send_timeout_ms` | 10 ms,允许 0–100 ms | | `receive_timeout_ms` | 10 ms,允许 0–10 ms | +| `receive_own_messages` | `false`;开启会在加载库或初始化通道前被拒绝 | | 单次原生接收缓冲 | 最多请求 64 帧 | | 单次原生发送批次 | 最多 100 帧 | @@ -95,7 +96,9 @@ Session 创建后已经启动,没有额外的 open 方法。 未标记帧在 FD Transport 上沿用已有的 FD、无 BRS 语义。标准/扩展帧和 BRS 应按上层设备协议设置,Transport 不根据型号推断这些标志。 `Message.dlc` 为 0–64 字节,后端转换为厂商 0–15 的 DLC 编码; -不可直接表示的长度向上取整并清零填充。`Config::frame_type` 不覆盖发送标志。 +不可直接表示的长度向上取整并清零填充。`Config::frame_type` 不覆盖发送标志。显式 `Message::format = FrameFormat::kClassic` +可在 FD 通道发送不超过 8 字节且无 FD/BRS 标志的经典帧。接收保留厂商原始 32 位时间戳计数器, +不推定单位、纪元或回绕扩展;方向保持未知,ABI 不支持开启自身发送帧接收,见 [CAN 元数据说明](can-metadata.md)。 直接调用 `write_batch()` 超过 100 帧会同步抛 `ValidationError`。Session 在 `gap=0` 且后端 选择原生批次发送时,会把整个 `MessageBatch` 交给该接口;超过 100 帧时回执以失败完成, diff --git a/docs/zh/operations.md b/docs/zh/operations.md index ea5d1ca..7a50a9c 100644 --- a/docs/zh/operations.md +++ b/docs/zh/operations.md @@ -26,7 +26,11 @@ MiniCANFD 和 PCAN 的库选择优先级为配置参数 → 各自环境变量 ## 共享、退出与异常清理 -多个设备可共享同一 Session,但 Session 不是每个调用者独立的连接句柄。 +设备需要独立生命周期时,使用 `TransportRuntime` 和每设备一个 `DeviceSession`。关闭一个设备 +只结束其自身工作、订阅和计数租约,其余设备继续连接;最后一个设备释放通道。重试所有权和 +协调恢复见[设备会话](device-sessions.md)。 + +以下为直接共享原始 Session 的兼容接口规则:Session 不是每个调用者独立的连接句柄。 共享使用者中任一方 `close()` 会关闭整个会话、清理该会话持有的资源,并使其他使用者的 inbox 失效; 外部总线或借用 dispatcher 的所有权不因此转移给 Session。 每个设备结束时优先关闭自身 inbox、取消自身 endpoint;最后由统一的所有者关闭 Session。 diff --git a/docs/zh/pcan.md b/docs/zh/pcan.md index d4c7774..4e1f044 100644 --- a/docs/zh/pcan.md +++ b/docs/zh/pcan.md @@ -67,7 +67,8 @@ data = 1/11/4/4,精确产生 1M/5M bit/s。此时序尚未在指定硬件验 更改速率时须同时设置分频、段长和 SJW;配置会校验范围、SJW ≤ TSEG2 及精确速率。 PCAN 按显式帧标志收发:`is_fd=false,is_brs=false` 在 FD 通道上仍是 Classic; -FD 需 `is_fd=true`,BRS 需额外 `is_brs=true`。设备协议决定标准/扩展 ID 和 BRS, +上述解释适用于 `format` 未指定时;`FrameFormat::kClassic` / `kFd` 可显式选择帧类型, +BRS 仍要求同时设置 `is_fd=true` 和 `is_brs=true`。设备协议决定标准/扩展 ID 和 BRS, 本库不提供手型号编码器。DLC 0–15 转换为 0–64 字节;发送不能直接表示的长度向上取合法 DLC 并零填充。 Classic 拒绝大于 8 字节、FD/BRS 或非法 ID,不截断。 @@ -89,7 +90,10 @@ initialized,也不会误清理其他连接。 仅纯 `QRCVEMPTY` 按接收队列为空处理,组合错误不吞掉;过滤非数据帧直到截止时间或完成最多 256 次厂商读取尝试后, `read()` 也会返回空值,因此空值不表示已经等待完整接收时限。bus-off、adapter unavailable/uninitialized、 TX queue full、bus error 分别由 `PcanTransportError::reason()` 表达,原始位码保留在 -`native_status()`。RTR/echo 过滤,status/error 消息报错,不作为业务数据交付。 +`native_status()`。RTR 帧被过滤;echo 默认过滤,显式开启 +`PcanConfig::receive_own_messages=true` 后由原生 setter 配置,并以 `Message::is_rx=false` +交付。缺少 setter 时在初始化前拒绝;驱动拒绝时执行通道清理/重试协议。status/error 消息仍报错。 +驱动时间戳保留微秒值及驱动时钟标识,不作纪元转换,见 [CAN 元数据说明](can-metadata.md)。 同步驱动调用没有取消 API,卡住的驱动不能强行中断;Windows 驱动调用时限和负载测试待验收。 ## 验证 diff --git a/docs/zh/session.md b/docs/zh/session.md index 296713a..212e5ab 100644 --- a/docs/zh/session.md +++ b/docs/zh/session.md @@ -76,8 +76,9 @@ factory 可以在同一线程重入不同的注册表 key(例如不同 `Manage `MessageBatch` 包含 frames、帧间 `gap` 和首帧前 `initial_delay`。 `submit(batch, options)` 入队并立即返回 `transport::SendReceipt`;队列满抛 `QueueFullError`。 默认容量按等待中的批次计,不含正在执行的批次;下方 SDK 选项可改为按 FIFO 帧数计并独立限制 latest 槽位。 -单个批次的帧数、所有路由组数量没有全局上限, -因此有界队列不等于总内存有界;应用还需限制单批次帧数、提交速率和总路由量。 +默认不限制单批次帧数和全局注册路由 key 数量。可配置 `SessionOptions::max_batch_frames`、 +`max_route_keys` 和 `max_pending_rx` 增加相应限制,零保留原有不设上限的行为。 +因此有界发送队列不等于总内存有界,应用还需限制提交速率和 inbox 容量。 `send(batch, options)` 提交后等待,使用相同调度路径。 提交会在入队前校验批次;token 已取消、deadline 已过期或 Session 已关闭时同步抛异常, @@ -240,7 +241,7 @@ priority 加等待老化、较早 deadline、较久未服务的 endpoint 和队 `set_route_key_extractor()` 接受将收到的 `can::Message` 映射为 `RouteKey` 的原生函数, 须在第一次成功注册路由或接受发送前配置。此前未匹配路由的主动 RX 不会冻结配置;恢复后 -也不会解除冻结。空函数恢复精确 ID/帧格式路由。提取器在 Session 状态锁 +也不会解除冻结。空函数恢复精确 ID/标准或扩展标志路由。提取器在 Session 状态锁 内执行,必须快速、不阻塞、不重入 Session,不应调用 Python 或执行 I/O。异常或非法返回 key 累计 `route_key_errors`,只丢弃该帧。交付的 `message` 保留原 ID、标志和载荷, `RoutedMessage::route_key` 记录提取后的 key。 diff --git a/docs/zh/slcan.md b/docs/zh/slcan.md index c31fe07..3b246eb 100644 --- a/docs/zh/slcan.md +++ b/docs/zh/slcan.md @@ -86,7 +86,9 @@ CANable 的接收和发送独立,不等待不存在的串口 ACK。 解析器支持拆包/粘包,Classic 未完成行上限 26 字符,FD 上限 138 字符, 最多缓存 256 帧,单次串口读取最多 256 字节。非法 ASCII、BEL、超长行、队列溢出、 发送失败和 ACK 超时进入故障态,必须关闭重建,避免使用迟到 ACK。 -不支持 RTR、时间戳后缀和命令回显。 +不支持 RTR、时间戳后缀和命令回显。`SlcanConfig::receive_own_messages` 默认 false; +true 会在串口初始化前被拒绝,不生成接收时间戳或方向替代值。`Message::format` 可在已配置 +固件及链路能力范围内显式选择经典/FD,见 [CAN 元数据说明](can-metadata.md)。 Windows 使用独占 `CreateFileW`、8N1、无软硬件流控、DTR/RTS 禁用, 采用 overlapped I/O / CancelIoEx。取消后等待 OS 完成请求才释放缓冲区; diff --git a/docs/zh/socketcan.md b/docs/zh/socketcan.md index 9d3e94d..d26ba92 100644 --- a/docs/zh/socketcan.md +++ b/docs/zh/socketcan.md @@ -68,7 +68,14 @@ CAN 错误帧与 RTR 不作为应用帧交付,过滤一帧后立即返回空 `Message.dlc` 是字节数;扩展 ID 需要显式 `is_extended=true`。 FD 模式发送未标记帧会沿用 FD、无 BRS 的兼容行为,不能靠 `is_fd=false` 在这个后端的 -FD 模式强制发 Classic。Classic 后端拒绝 FD/BRS 或超过 8 字节的载荷。 +FD 模式强制发 Classic。显式设置 `Message::format = FrameFormat::kClassic` 可以发送经典帧, +要求 FD/BRS 标志为 false 且载荷不超过 8 字节。`kUnspecified` 保留原有 FD 默认行为; +Classic 后端拒绝 FD/BRS 或超过 8 字节的载荷。 + +配置构造入口为 `SocketCANTransport(options, interface_name)`。`SocketCANOptions` 中 +`receive_own_messages` 默认 false,`timestamp_mode` 默认 `kDisabled`;原生回显及软件/原始硬件 +时间戳须显式开启。`read()` 通过 `recvmsg()` 保留可用元数据,时钟、方向标志和实机验证边界 +见 [CAN 元数据说明](can-metadata.md)。 socket 接收超时设为 10 ms,发送超时设为 50 ms;接收超时返回空值,但空值不表示已经等待完整接收时限。系统 `write()` 以负值 失败时抛出的 `TransportIOError` 保留 `system_error()`;异常短写也抛 `TransportIOError`,但 diff --git a/include/linkerbot/can/frame.hpp b/include/linkerbot/can/frame.hpp index 14f876f..95310df 100644 --- a/include/linkerbot/can/frame.hpp +++ b/include/linkerbot/can/frame.hpp @@ -27,16 +27,17 @@ namespace linkerbot::can { bool is_brs = false; }; - // P0.1 public FD/BRS rules: + // Per-frame format and FD/BRS rules: // // - Message::format explicitly overrides the legacy per-backend FD default. // Unspecified keeps the existing is_fd / is_brs behavior. // - BRS is opt-in: only is_brs=true enables it. Backends never upgrade. // - is_brs && !is_fd is illegal on every backend (rejected, not normalized). - // - bool cannot express "unspecified". Untagged (false, false) is resolved by - // link kind: CanFd => FD without BRS (FrameType 0x04); Classic => classic CAN. + // - Unspecified format with false/false flags uses the backend default: + // SocketCAN FD/vendor => FD without BRS; PCAN/SLCAN => classic CAN. + // Explicit kClassic/kFd selects the format subject to link capability. // - Config::frame_type is not applied to TX frames and is not a CANFD_Init - // field. Session-wide BRS belongs on Message (or a later SDK stamp). + // field. The device protocol must select BRS on each Message. // - SocketCAN and vendor encode the same resolved flags: is_fd => FD frame / // FrameType bit 0x04; is_brs => CANFD_BRS / FrameType bit 0x08. @@ -113,7 +114,7 @@ namespace linkerbot::can { return FrameFlags{}; } - // CanFd link: untagged (false, false) means FD without BRS. + // FD-capable link: retain explicit flags, otherwise use the backend default. if (message.is_fd || message.is_brs) { return FrameFlags{true, message.is_brs}; } diff --git a/include/linkerbot/can/message.hpp b/include/linkerbot/can/message.hpp index 68459f6..fece972 100644 --- a/include/linkerbot/can/message.hpp +++ b/include/linkerbot/can/message.hpp @@ -27,8 +27,8 @@ namespace linkerbot::can { // payload length in bytes (NOT the 0-15 wire DLC nibble); backends that speak // wire DLC convert at their boundary. // - // `is_fd` / `is_brs` are the per-frame FD/BRS flags. Encoding, illegal-combo - // rejection, and untagged defaults are in frame.hpp. + // `format` selects an explicit TX format; kUnspecified retains the backend + // interpretation of is_fd/is_brs. Validation and defaults are in frame.hpp. struct Message { uint32_t id = 0; std::array data = {}; @@ -37,7 +37,7 @@ namespace linkerbot::can { bool is_fd = false; bool is_brs = false; FrameFormat format = FrameFormat::kUnspecified; - // RX metadata only. TX ignores it; device clocks are not wall clocks. + // RX metadata only. TX ignores it; interpret the recorded clock and unit. std::optional timestamp = std::nullopt; std::optional is_rx = std::nullopt; }; diff --git a/include/linkerbot/can/slcan_transport.hpp b/include/linkerbot/can/slcan_transport.hpp index 01c00c4..1d333d1 100644 --- a/include/linkerbot/can/slcan_transport.hpp +++ b/include/linkerbot/can/slcan_transport.hpp @@ -20,7 +20,8 @@ namespace linkerbot::can { std::string port; // Explicit COM1..COM65535 or Linux /dev/... path. uint32_t serial_baud = 115200; uint32_t nominal_bitrate = 1'000'000; - bool fd = false, brs = false; // BRS requires FD; Message flags select each frame type. + // BRS requires FD; each Message selects its format/flags. + bool fd = false, brs = false; std::chrono::milliseconds receive_timeout{10}; // 1..100 ms std::chrono::milliseconds command_timeout{500}; // 1..5000 ms SlcanProtocol protocol = SlcanProtocol::kLawicelClassic; diff --git a/include/linkerbot/can/socketcan_transport.hpp b/include/linkerbot/can/socketcan_transport.hpp index 8fdb96e..11d8d05 100644 --- a/include/linkerbot/can/socketcan_transport.hpp +++ b/include/linkerbot/can/socketcan_transport.hpp @@ -50,7 +50,9 @@ namespace linkerbot::can { // and in-flight I/O coordination without a physical CAN interface. // Takes ownership of fd; SO_RCVTIMEO/SO_SNDTIMEO are applied same as // the production constructor (both are generic socket options, not - // CAN-specific, so they also apply to a socketpair fd). + // CAN-specific, so they also apply to a socketpair fd). Optional timestamp + // mode is applied; receive-own opt-in requires an actual CAN socket. + // The existing false/disabled defaults leave those options unchanged. struct AdoptFd { int fd = -1; bool fd_mode = false; @@ -281,7 +283,7 @@ namespace linkerbot::can { // number, so it can never be reused by unrelated code while a // syscall might still reference it (R18). The mutex is only // held for this bookkeeping, never across the blocking - // read()/write() syscalls themselves, so a slow write cannot + // recvmsg()/write() syscalls themselves, so a slow write cannot // starve RX (or vice versa). io_cv_.wait(lock, [this] { return in_flight_ == 0; }); const int fd = fd_; @@ -320,7 +322,7 @@ namespace linkerbot::can { private: // Bookkeeping-only RAII guard: reserves the fd for the duration of - // one read()/write() syscall so close() cannot free (and let + // one recvmsg()/write() syscall so close() cannot free (and let // something else reuse) the fd number while that syscall is still // outstanding. Never held across the blocking syscall itself. class IoInFlightGuard { diff --git a/include/linkerbot/can/vendor_canfd_transport.hpp b/include/linkerbot/can/vendor_canfd_transport.hpp index a4ab612..e135c4b 100644 --- a/include/linkerbot/can/vendor_canfd_transport.hpp +++ b/include/linkerbot/can/vendor_canfd_transport.hpp @@ -28,8 +28,9 @@ namespace linkerbot::can { inline constexpr uint8_t kVendorCanFdDefaultConfigFlags = 0x01 | 0x02 | 0x04; inline constexpr uint8_t kVendorCanFdDefaultModel = 0; inline constexpr uint8_t kVendorCanFdDefaultCanType = 1; - // Documented default only. TX FrameType is encoded from Message flags - // (untagged => FD without BRS). Not passed to CANFD_Init. + // Documented legacy default only. TX FrameType uses Message format/flags; + // unspecified format with false/false flags => FD without BRS. + // This constant is not passed to CANFD_Init. inline constexpr uint8_t kVendorCanFdDefaultFrameType = kFrameTypeFd; inline constexpr unsigned kVendorCanFdMaxTransmitBatch = 100; inline constexpr const char *kVendorCanFdLibEnv = "LINKERBOT_CANFD_LIB"; diff --git a/include/linkerbot/transport/session.hpp b/include/linkerbot/transport/session.hpp index e11b3b6..7ae5af8 100644 --- a/include/linkerbot/transport/session.hpp +++ b/include/linkerbot/transport/session.hpp @@ -297,7 +297,7 @@ namespace linkerbot::transport { size_t capacity = 512); // Configure before the first route registration or accepted send. Unmatched RX is allowed. // Must be fast, non-blocking and non-reentrant; runs under the state lock. - // Empty restores exact ID/format routing. Failures drop only that frame. + // Empty restores exact ID/extended-flag routing. Failures drop only that frame. void set_route_key_extractor(std::function extractor); [[nodiscard]] std::shared_ptr notification() const; void update_routes(const std::shared_ptr &inbox, diff --git a/tests/test_socketcan_lifecycle.cpp b/tests/test_socketcan_lifecycle.cpp index 60b0b1d..290e05c 100644 --- a/tests/test_socketcan_lifecycle.cpp +++ b/tests/test_socketcan_lifecycle.cpp @@ -71,7 +71,7 @@ TEST_CASE("close() waits for an in-flight blocking read before releasing the fd" auto fds = make_socketpair(); // test_on_io_in_flight fires after in_flight_ is already incremented but - // before the real ::read() syscall runs, and we hold it there until the + // before the real ::recvmsg() syscall runs, and we hold it there until the // test releases it. That makes "a read is genuinely in flight" — and // "close() must not be able to finish while that is true" — a // deterministic fact instead of something inferred from elapsed wall- diff --git a/validation/prepare-report-db.py b/validation/prepare-report-db.py index 762ea38..aa28d6d 100644 --- a/validation/prepare-report-db.py +++ b/validation/prepare-report-db.py @@ -1,4 +1,4 @@ -"""Merge compilation databases and expose project system-include directories.""" +"""Merge compilation databases and preserve analyzer include/target configuration.""" import argparse import json diff --git a/validation/python-preintegration/baseline-inventory.json b/validation/python-preintegration/baseline-inventory.json index 68522af..b0f6d40 100644 --- a/validation/python-preintegration/baseline-inventory.json +++ b/validation/python-preintegration/baseline-inventory.json @@ -9,7 +9,7 @@ }, "deployed_revisions": "not provided; inspected source revisions are not asserted to be deployed revisions" }, - "hardware_timestamp": "Python CANFDMessage exposes timestamp; native can::Message has no corresponding backend timestamp field. Dependency in actual deployment is not yet specified.", + "hardware_timestamp": "Native Message preserves optional timestamp value, unit, clock, source and counter width. SocketCAN can request raw hardware timestamps; PCAN preserves driver microseconds and vendor preserves raw 32-bit ticks. Deployment-specific provenance, clock correlation and hardware acceptance remain unverified.", "historical_evidence": [ { "path": "../hand_cpp/docs/hardware-acceptance/l6/report.md", @@ -20,7 +20,9 @@ "status": "deployment-specific native hardware acceptance remains unverified", "inspected_native_base_revision": "e1d65e5961754a22c9575863d63337931d6dbeae", "additional_source_gaps": [ - "Explicit classic TX on an FD link is not representable by the current untagged native Message defaults.", - "Python receive_own_messages has no equivalent exposed native configuration option." - ] + "Arbitrary python-can plugins and options are not implied by the native backend set.", + "Vendor and SLCAN interfaces reject receive_own_messages=true; these SLCAN profiles do not expose receive timestamps.", + "The isolated Python probe is not a production binding, exception/clock compatibility layer or actual hand/arm integration." + ], + "capability_reference": "backend-contracts.json and ../../docs/en/can-metadata.md describe current source capabilities; inspected_native_base_revision records the starting revision, not the current feature set." }