Skip to content

Add AxiomaticFdBackend for the AX140970 (ICanBackend impl, Classical-CAN only for now)#14

Draft
iliabaranov wants to merge 4 commits into
polymathrobotics:mainfrom
iliabaranov:feat/axiomatic-fd-backend
Draft

Add AxiomaticFdBackend for the AX140970 (ICanBackend impl, Classical-CAN only for now)#14
iliabaranov wants to merge 4 commits into
polymathrobotics:mainfrom
iliabaranov:feat/axiomatic-fd-backend

Conversation

@iliabaranov
Copy link
Copy Markdown

⚠️ Stacked on #12

This branch is built on top of feat/i-can-backend-extraction (PR #12)
because it implements the ICanBackend interface that PR adds. Since GitHub
won'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 the
diff to just the new commit.


What

ICanBackend implementation for the Axiomatic AX140970 Dual CAN FD to
Ethernet Converter, wrapping a UDP transport. Three pieces:

  • axiomatic_protocol.{hpp,cpp} — pure data-in / data-out wire codec
    for 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} — the ICanBackend implementation
    itself, translating between the codec's CanFdFrameRecord and
    socketcan::CanFrame.

Known limitation — Classical-CAN-only in this PR

socketcan::CanFrame currently wraps Linux struct can_frame (8-byte
payload limit). The AX140970 hardware supports 64-byte CAN FD frames with
BRS / ESI, but they can't fit through CanFrame yet. Inbound FD frames
are dropped + counted on fd_frames_dropped()
— observably, not silently.

Full FD support is a separate follow-up PR that generalizes CanFrame to
wrap canfd_frame.

receive(CanFrame &) is intentionally unimplemented (returns an error
string). The device's UDP stream is naturally asynchronous; real consumers
should use the callback path (setOnReceiveCallback +
startReceptionThread). This mirrors SocketcanAdapter's blocking
receive() as a legacy affordance we don't need to support up front.

Tests

  • 42 GoogleTest codec cases including a parity fixture against captured
    device wire bytes from bench testing (Heartbeat from S/N 0010525274).
  • 10 GoogleTest UdpClient lifecycle cases (no hardware required — uses
    TEST-NET-1 address 192.0.2.1 as a black hole).
  • All 19 existing socketcan_adapter tests still pass.
  • All 2 existing axiomatic_adapter Catch2 tests still pass.

Total: 73 tests, 0 failures.

GoogleTest sits alongside the existing Catch2 framework via
ament_cmake_gtest; both run via colcon test. Future migration to a
single 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.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant