Add AxiomaticFdBackend for the AX140970 (ICanBackend impl, Classical-CAN only for now)#14
Draft
iliabaranov wants to merge 4 commits into
Draft
Conversation
Adds socketcan_adapter/include/socketcan_adapter/i_can_backend.hpp — the cross-cutting CAN transport contract: open/close, send, receive, reception thread lifecycle, and on-receive/on-error callback hooks. Linux-specific knobs (filter vector, error mask, JOIN flag) stay concrete-only on SocketcanAdapter since they have no analog on non-SocketCAN backends. SocketcanAdapter now inherits from ICanBackend, with `override` annotations on the matching methods. The interface signatures were chosen to match SocketcanAdapter's existing public surface byte-for-byte, so this is a zero-behavior-change refactor: every existing call site continues to work, all 19 existing tests still pass, all three packages in the workspace (socketcan_adapter, socketcan_adapter_ros, axiomatic_adapter) build without modification. Why this is needed: enables a forthcoming AxiomaticFdBackend (Axiomatic AX140970 Dual CAN FD over Ethernet converter) to plug into SocketcanBridgeNode and other consumers via ICanBackend* without forking the runtime path. The legacy AxiomaticAdapter will be retrofitted in a follow-up patch (its API needs minor unification — currently uses chrono::milliseconds vs the interface's chrono::duration<float>, and lacks runtime setOnReceiveCallback / setOnErrorCallback).
Companion to the prior commit that extracted the ICanBackend interface and
made SocketcanAdapter implement it. With this change, both transport
adapters share a polymorphic base, so consumers can hold a non-owning
ICanBackend* and select the transport (kernel SocketCAN, Axiomatic
ETH-CAN bridge) at construction time without forking the runtime path.
Purely additive — every existing call site keeps working:
* TCPSocketState becomes an alias of polymath::socketcan::SocketState.
The two enums had identical numeric values; aliasing is source-compat
for any caller spelling out the legacy name.
* Existing methods get `override` annotations where they match the
interface byte-for-byte: openSocket, closeSocket, receive(CanFrame&),
startReceptionThread, send(const CanFrame&), is_thread_running,
get_socket_state.
* Four new methods added to satisfy the interface:
- joinReceptionThread(const std::chrono::duration<float>&) — converts
to the existing milliseconds-based overload.
- send(std::shared_ptr<const CanFrame>) — delegates to send(CanFrame&).
- setOnReceiveCallback(...) — runtime setter; mirrors SocketcanAdapter's
plain-assignment semantics. Set BEFORE startReceptionThread() to
avoid racing the rx thread (same convention as SocketcanAdapter).
- setOnErrorCallback(...) — same pattern.
* The existing constructor that takes receive + error callbacks remains
the canonical setup path. The new setters are additive — useful when
constructing through an ICanBackend* factory but not required.
Build: all 3 packages build clean (socketcan_adapter, socketcan_adapter_ros,
axiomatic_adapter). Tests: 19/19 existing tests pass — same as baseline.
Pure formatter output from running the new pre-commit hook (polymathrobotics/polymath_code_standard@v2.1.2 introduced upstream in polymathrobotics#9) over the four files touched by the prior two commits: socketcan_adapter/include/socketcan_adapter/i_can_backend.hpp socketcan_adapter/include/socketcan_adapter/socketcan_adapter.hpp axiomatic_adapter/include/axiomatic_adapter/axiomatic_adapter.hpp axiomatic_adapter/src/axiomatic_adapter.cpp Plus one cpplint suggestion: add #include <memory> in axiomatic_adapter.cpp for the std::shared_ptr usage in the new send(shared_ptr<const CanFrame>) overload. No logic changes; build remains clean and 19/19 existing tests still pass.
Lands the production-ready bits of the Axiomatic AX140970 (Dual CAN FD over
Ethernet) integration into the existing axiomatic_adapter package. The
backend implements ICanBackend (from the prior PR), so consumers — including
SocketcanBridgeNode and other downstream code — can select between kernel
SocketCAN and this Ethernet transport at construction time without forking
the runtime path.
What's added:
* `axiomatic_protocol.{hpp,cpp}` — wire codec. Pure data-in / data-out: no
sockets, no threads, no ROS. Implements the device's proprietary protocol
per Axiomatic's "Ethernet to CAN Converter Communication Protocol" v6:
- 11-byte AXIO-tagged message envelope (Protocol ID 14010 LSB-first)
- CAN FD Frame record (17-byte header + 0–64 data bytes) — encode +
parse, all flag bits (FDF/BRS/ESI/EID/RTR/ERR), every valid DLC,
standard + extended CAN IDs
- Heartbeat v2 (encode for host-side keepalive, parse for device-side
monitoring; UDP idle timeout is 10 s device-side)
- Supported Features bitmask, channel routing addresses (CG, CIDS)
42 GoogleTest cases including a parity fixture against captured device
wire bytes from bench testing.
* `udp_client.{hpp,cpp}` — UDP transport. Owns one connected UDP socket plus
two threads: tx (1 Hz Heartbeat for device-side keepalive) and rx (poll +
recvfrom + parse + dispatch by Message ID). Type-erased callbacks for
frames, error notifications, and inbound heartbeats. Atomic Stats
counters (frames sent/received, parse_errors, tx_errors). 10 lifecycle
tests (no hardware needed) covering start/stop idempotency, restart,
encode validation, callback safety, heartbeat pacing.
* `axiomatic_fd_backend.{hpp,cpp}` — the ICanBackend implementation. Wraps
UdpClient and translates between the codec's CanFdFrameRecord and the
socketcan::CanFrame value type. Lifecycle mirrors SocketcanAdapter
(openSocket / setOn{Receive,Error}Callback / startReceptionThread /
joinReceptionThread / closeSocket). Routing addresses are configurable
via Options (tx_address defaults to CAN1; local_address advertised in
our heartbeats so the device filters which frames it forwards us).
Classical-CAN-only in this revision. socketcan::CanFrame currently wraps
Linux `struct can_frame` with an 8-byte payload limit; full CAN FD
support (64-byte payloads + BRS / ESI) lands when CanFrame is generalized
to wrap `canfd_frame` (separate PR — Phase 2 of the integration design).
Inbound FD frames are dropped + counted on `fd_frames_dropped()` so
callers can detect the limitation rather than silently lose data.
`receive(CanFrame &)` left unimplemented (returns an error string) — the
device's UDP stream is naturally asynchronous, all real consumers use the
on-receive callback path. Mirrors SocketcanAdapter's blocking `receive()`
as a legacy affordance that we don't need to support up-front.
Build + test: 73 tests pass across socketcan_adapter (19, unchanged) and
axiomatic_adapter (54 = 42 codec + 10 udp_client + 2 existing). Existing
Catch2 test suite and HARDWARE_CONNECTED gate are untouched; new GoogleTest
suites run unconditionally via ament_cmake_gtest. Catch2 + GoogleTest will
coexist during the transition; future migration is a separate concern.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch is built on top of
feat/i-can-backend-extraction(PR #12)because it implements the
ICanBackendinterface that PR adds. Since GitHubwon't let me base a PR against a branch that only exists in my fork, the
diff here shows PR #12's commits too. Please review #12 first; the
new work in this PR is just the last commit (
6d5607e — Add AxiomaticFdBackend ...).Once #12 lands I'll rebase this branch onto
main, which will collapse thediff to just the new commit.
What
ICanBackendimplementation for the Axiomatic AX140970 Dual CAN FD toEthernet Converter, wrapping a UDP transport. Three pieces:
axiomatic_protocol.{hpp,cpp}— pure data-in / data-out wire codecfor the device's proprietary protocol (11-byte AXIO-tagged envelope,
CAN FD Frame Stream, Heartbeat v2). No sockets, no threads, no ROS.
udp_client.{hpp,cpp}— UDP transport with 1 Hz heartbeat keepalive(the device times UDP connections out at 10 s of host inactivity per the
protocol spec §2.3.1.1).
axiomatic_fd_backend.{hpp,cpp}— theICanBackendimplementationitself, translating between the codec's
CanFdFrameRecordandsocketcan::CanFrame.Known limitation — Classical-CAN-only in this PR
socketcan::CanFramecurrently wraps Linuxstruct can_frame(8-bytepayload limit). The AX140970 hardware supports 64-byte CAN FD frames with
BRS / ESI, but they can't fit through
CanFrameyet. Inbound FD framesare dropped + counted on
fd_frames_dropped()— observably, not silently.Full FD support is a separate follow-up PR that generalizes
CanFrametowrap
canfd_frame.receive(CanFrame &)is intentionally unimplemented (returns an errorstring). The device's UDP stream is naturally asynchronous; real consumers
should use the callback path (
setOnReceiveCallback+startReceptionThread). This mirrorsSocketcanAdapter's blockingreceive()as a legacy affordance we don't need to support up front.Tests
device wire bytes from bench testing (Heartbeat from S/N
0010525274).UdpClientlifecycle cases (no hardware required — usesTEST-NET-1 address 192.0.2.1 as a black hole).
socketcan_adaptertests still pass.axiomatic_adapterCatch2 tests still pass.Total: 73 tests, 0 failures.
GoogleTest sits alongside the existing Catch2 framework via
ament_cmake_gtest; both run viacolcon test. Future migration to asingle framework is out of scope here.
Build / test
```sh
colcon build --packages-select socketcan_adapter axiomatic_adapter
colcon test --packages-select socketcan_adapter axiomatic_adapter
73 tests, 0 failures.
```
Why this is needed
Groundwork for the bench-validated end-to-end CAN integration on the
AX140970 Dual CAN FD → Ethernet converter. The codec, transport, and
backend in this PR have already been used to drive a 17-cell loopback
matrix (every Classical baud + every FD preset × ISO + non-ISO) at
~95% theoretical busload with zero loss and 1–15 ms p99 latency end to
end. Subsequent PRs will add the ROS2 lifecycle bridge node that consumes
this backend and the
CanFrame→ CAN FD generalization.