Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion axiomatic_adapter/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ find_package(CLI11 REQUIRED)
find_package(socketcan_adapter REQUIRED)

# Axiomatic Adapter Library
add_library(axiomatic_adapter SHARED src/axiomatic_adapter.cpp)
add_library(axiomatic_adapter SHARED
src/axiomatic_adapter.cpp
src/axiomatic_model.cpp
src/axiomatic_protocol.cpp
src/models/ax140900.cpp
src/models/ax142100a.cpp
)

target_compile_features(axiomatic_adapter PUBLIC c_std_99 cxx_std_17)
target_include_directories(axiomatic_adapter PUBLIC
Expand Down Expand Up @@ -98,6 +104,14 @@ if(BUILD_TESTING)
endif()
include(Catch OPTIONAL)

add_executable(test_axiomatic_model test/axiomatic_model_test.cpp)
target_link_libraries(test_axiomatic_model PRIVATE axiomatic_adapter Catch2::Catch2WithMain)
if(COMMAND catch_discover_tests)
catch_discover_tests(test_axiomatic_model)
else()
add_test(NAME test_axiomatic_model COMMAND test_axiomatic_model)
endif()

option(HARDWARE_CONNECTED "Enable tests that require a connected Axiomatic device and CAN interface" OFF)

if(HARDWARE_CONNECTED)
Expand Down
34 changes: 29 additions & 5 deletions axiomatic_adapter/README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
# Axiomatic Adapter
Library and Adapter for the Axiomatic CAN-ETH converter.
Library and Adapter for Axiomatic CAN-Ethernet converters.

For more information on the decoding/endcoding and product, see the links below:
For more information on the decoding/encoding, see:

https://www.notion.so/polymathrobotics/Axiomatic-CAN-to-Ethernet-Converter-08e078d8914f40d6b7cd99ebf39fe1b0

https://products.axiomatic.com/viewitems/connectivity/ethernet-can-converters
## Supported Models

| `--model` | Device | Protocol reference |
| --- | --- | --- |
| `ax140900` (default) | [AX140900 CAN/Ethernet Converter](https://www.axiomatic.com/product/canethernet-converter-ax140900/) | [Ethernet to CAN Converter Communication Protocol](https://www.axiomatic.com/wp-content/uploads/Ethernet-to-CAN-Converter-Communication-Protocol.pdf) |
| `ax142100a` | [AX142100A Protocol Converter, Ethernet/RS-422/2x RS-232/CAN](https://www.axiomatic.com/product/protocol-converter-ethernet-rs-422-2-rs-232-can-sae-j1939-ax142100a/) | [UMAX142100A](https://www.axiomatic.com/wp-content/uploads/UMAX142100A.pdf), section 4.2 |

Every model shares the 11-byte `AXIO` message header in `axiomatic_protocol.hpp`.
Each model is a subclass of `AxiomaticModel` (`axiomatic_model.hpp`) in `include/axiomatic_adapter/models/` and `src/models/`.
A model takes bytes and `CanFrame`s only, so it can be used without a socket.

### Adding a model

1. Add `include/axiomatic_adapter/models/<model>.hpp` and `src/models/<model>.cpp` with a `<model>::<Model>` class deriving `AxiomaticModel`.
Override `protocolId`, `encode` (build on `protocol::encodeMessage`), and `decodeMessage` (one message body; return `false` for Message IDs that carry no CAN frames).
The base class `decode` splits the buffer into messages and calls `decodeMessage` for each.
2. Add the source to the `axiomatic_adapter` library in `CMakeLists.txt`.
3. Add its entry to `modelFactories` in `src/axiomatic_model.cpp`.
4. Add a decode test of the manual's example bytes in `test/axiomatic_model_test.cpp`; the round trip test covers every name in `modelNames`.

## Usage
### Socketcan-Axiomatic Bridge

```bash
ros2 run axiomatic_adapter axiomatic_socketcan_bridge [CAN_INTERFACE_NAME] [IP_ADDRESS] [PORT] [OPTIONAL]--retry-connection[-r] [OPTIONAL]--max-retry-attempts [OPTIONAL]--verbose[-v] [OPTIONAL]--no-tcp-nodelay
ros2 run axiomatic_adapter axiomatic_socketcan_bridge [CAN_INTERFACE_NAME] [IP_ADDRESS] [PORT] [OPTIONAL]--model[-m] [OPTIONAL]--retry-connection[-r] [OPTIONAL]--max-retry-attempts [OPTIONAL]--verbose[-v] [OPTIONAL]--no-tcp-nodelay

# Examples
# generic example to bridge vcan0 with axiomatic using 192.168.50.34:4000
ros2 run axiomatic_adapter axiomatic_socketcan_bridge vcan0 192.168.50.34 4000

# bridge vcan0 with an AX142100A
ros2 run axiomatic_adapter axiomatic_socketcan_bridge vcan0 192.168.50.34 4000 --model ax142100a

# this will continue retrying to connect forever and not exit on first failure. It will also print more detailed logs
ros2 run axiomatic_adapter axiomatic_socketcan_bridge vcan0 192.168.50.34 4000 -r -v

Expand Down Expand Up @@ -44,7 +65,8 @@ polymath::can::AxiomaticAdapter adapter(
[](std::unique_ptr<const CanFrame> /*frame*/) { /* No-op */ },
[](polymath::can::AxiomaticAdapter::socket_error_string_t /*error*/) { /*do nothing*/ },
receive_timeout_ms,
/*tcp_nodelay=*/true // optional, defaults to true; see below
/*tcp_nodelay=*/true, // optional, defaults to true; see below
std::make_unique<polymath::can::ax142100a::Ax142100a>() // optional, defaults to ax140900::Ax140900
);

// open the socket
Expand Down Expand Up @@ -81,3 +103,5 @@ If you have any doubt, leave it at the default.
2. Axiomatic-specific status messages are ignored and deliberately skipped. In future revisions this should be handled and reported as necessary.
3. CAN FD is not supported
4. Only TCP mode is supported; no UDP support (heartbeats are required for UDP support)
5. AX142100A raw data (serial) payloads are skipped; only CAN frames are delivered.
6. AX142100A 11-bit IDs are assumed to be 2 bytes on the wire; the manual only shows a 29-bit example.
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@
#include <optional>
#include <string>

#include "axiomatic_adapter/axiomatic_model.hpp"
#include "axiomatic_adapter/models/ax140900.hpp"
#include "socketcan_adapter/can_frame.hpp"

namespace polymath
{
namespace can
namespace polymath::can
{

/// @brief State of TCP socket, error, open or closed
Expand Down Expand Up @@ -66,6 +66,7 @@ class AxiomaticAdapter : public std::enable_shared_from_this<AxiomaticAdapter>
/// on both sides, more switch/NIC PPS load, and worse bytes-on-wire
/// efficiency for bulk transfers). Better for real-time, but can, in constrained
/// resource environments, cause issues
/// @param model converter model, e.g. std::make_unique<ax142100a::Ax142100a>(); must not be null
AxiomaticAdapter(
const std::string & ip_address,
const std::string & port,
Expand All @@ -74,7 +75,8 @@ class AxiomaticAdapter : public std::enable_shared_from_this<AxiomaticAdapter>
const std::function<void(socket_error_string_t error)> && error_callback_function =
[](socket_error_string_t /*error*/) { /*do nothing*/ },
const std::chrono::milliseconds & receive_timeout_ms = AxiomaticAdapter::DEFAULT_SOCKET_RECEIVE_TIMEOUT_MS,
bool tcp_nodelay = true);
bool tcp_nodelay = true,
std::unique_ptr<const AxiomaticModel> model = std::make_unique<ax140900::Ax140900>());

/// @brief Destructor for AxiomaticAdapter
virtual ~AxiomaticAdapter();
Expand Down Expand Up @@ -129,7 +131,6 @@ class AxiomaticAdapter : public std::enable_shared_from_this<AxiomaticAdapter>
class AxiomaticAdapterImpl;
std::unique_ptr<AxiomaticAdapterImpl> pimpl_;
};
} // namespace can
} // namespace polymath
} // namespace polymath::can

#endif // AXIOMATIC_ADAPTER__AXIOMATIC_ADAPTER_HPP_
66 changes: 66 additions & 0 deletions axiomatic_adapter/include/axiomatic_adapter/axiomatic_model.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2025-present Polymath Robotics, Inc. All rights reserved
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef AXIOMATIC_ADAPTER__AXIOMATIC_MODEL_HPP_
#define AXIOMATIC_ADAPTER__AXIOMATIC_MODEL_HPP_

#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>

#include "axiomatic_adapter/axiomatic_protocol.hpp"
#include "socketcan_adapter/can_frame.hpp"

namespace polymath::can
{

/// @class polymath::can::AxiomaticModel
/// @brief One Axiomatic converter model and its wire format.
/// Subclasses override protocolId, encode, and decodeMessage.
/// Methods are const and keep no state between calls; one instance may be shared across threads.
class AxiomaticModel
{
public:
virtual ~AxiomaticModel() = default;

/// @brief Encode one CAN frame as one complete protocol message
virtual std::vector<uint8_t> encode(const polymath::socketcan::CanFrame & frame) const = 0;

/// @brief Decode every CAN frame in a buffer of back-to-back protocol messages.
/// Messages with a Message ID that carries no CAN frames are skipped.
/// @param data buffer start
/// @param size buffer length in bytes
protocol::DecodeResult decode(const uint8_t * data, size_t size) const;

protected:
/// @brief Protocol ID every message of this model carries
virtual uint16_t protocolId() const = 0;

/// @brief Append the CAN frames in one message to result
/// @return false when the Message ID carries no CAN frames
virtual bool decodeMessage(const protocol::MessageView & message, protocol::DecodeResult & result) const = 0;
};

/// @brief Construct the model with a lower-case name, e.g. "ax142100a"
/// @throws std::invalid_argument for a name not in modelNames()
std::unique_ptr<const AxiomaticModel> makeModel(const std::string & model);

/// @brief Every model name makeModel accepts
std::vector<std::string> modelNames();

} // namespace polymath::can

#endif // AXIOMATIC_ADAPTER__AXIOMATIC_MODEL_HPP_
94 changes: 94 additions & 0 deletions axiomatic_adapter/include/axiomatic_adapter/axiomatic_protocol.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) 2025-present Polymath Robotics, Inc. All rights reserved
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef AXIOMATIC_ADAPTER__AXIOMATIC_PROTOCOL_HPP_
#define AXIOMATIC_ADAPTER__AXIOMATIC_PROTOCOL_HPP_

#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

#include "socketcan_adapter/can_frame.hpp"

namespace polymath::can::protocol
{

/// @brief Message Header framing shared by every Axiomatic Ethernet converter.
/// Wire layout, multi-byte fields LSB first:
/// bytes 0-3 : Axiomatic Tag "AXIO"
/// bytes 4-5 : Protocol ID (model specific)
/// bytes 6-7 : Message ID
/// byte 8 : Message Version
/// bytes 9-10 : Message Data Length (body bytes that follow the header)
static constexpr std::array<uint8_t, 4> AXIOMATIC_TAG = {'A', 'X', 'I', 'O'};
static constexpr size_t HEADER_BYTES = 11;
static constexpr size_t PROTOCOL_ID_OFFSET = 4;
static constexpr size_t MESSAGE_ID_OFFSET = 6;
static constexpr size_t MESSAGE_VERSION_OFFSET = 8;
static constexpr size_t MESSAGE_DATA_LENGTH_OFFSET = 9;

/// @brief One protocol message located inside a receive buffer
struct MessageView
{
uint16_t message_id;
uint8_t message_version;
/// @brief points into the buffer passed to parseMessages; valid only while that buffer is
const uint8_t * body;
size_t body_size;
};

/// @brief Protocol messages found in a buffer, plus a description of every byte range skipped
struct ParsedMessages
{
std::vector<MessageView> messages;
std::vector<std::string> diagnostics;
};

/// @brief CAN frames decoded from a buffer, plus a description of every byte range skipped or dropped
struct DecodeResult
{
std::vector<polymath::socketcan::CanFrame> frames;
std::vector<std::string> diagnostics;
};

/// @brief Read an unsigned little-endian integer
/// @param data first byte, LSB
/// @param num_bytes field width, at most 4
uint32_t readLittleEndian(const uint8_t * data, size_t num_bytes);

/// @brief Append the low num_bytes of value, LSB first
void appendLittleEndian(std::vector<uint8_t> & out, uint32_t value, size_t num_bytes);

/// @brief Frame a message body with the Axiomatic Message Header
/// @param protocol_id model specific Protocol ID
/// @param message_id Message ID for the body type
/// @param message_version Message Version
/// @param body Message Data, at most 65535 bytes
/// @return header followed by body
std::vector<uint8_t> encodeMessage(
uint16_t protocol_id, uint16_t message_id, uint8_t message_version, const std::vector<uint8_t> & body);

/// @brief Split a buffer into back-to-back protocol messages carrying protocol_id.
/// Scanning stops at the first header whose tag or Protocol ID does not match.
/// A message whose declared length runs past the buffer is returned truncated to the bytes present.
/// @param protocol_id Protocol ID every message must carry
/// @param data buffer start
/// @param size buffer length in bytes
ParsedMessages parseMessages(uint16_t protocol_id, const uint8_t * data, size_t size);

} // namespace polymath::can::protocol

#endif // AXIOMATIC_ADAPTER__AXIOMATIC_PROTOCOL_HPP_
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
#ifndef AXIOMATIC_ADAPTER__AXIOMATIC_SOCKETCAN_BRIDGE_NODE_HPP_
#define AXIOMATIC_ADAPTER__AXIOMATIC_SOCKETCAN_BRIDGE_NODE_HPP_

#include <memory>
#include <string>

#include "axiomatic_adapter/axiomatic_adapter.hpp"
#include "axiomatic_adapter/models/ax140900.hpp"
#include "socketcan_adapter/can_frame.hpp"
#include "socketcan_adapter/socketcan_adapter.hpp"

namespace polymath
{
namespace can
namespace polymath::can
{

/// @class polymath::can::AxiomaticSocketcanBridge
Expand All @@ -39,12 +39,14 @@ class AxiomaticSocketcanBridge
/// @param tcp_nodelay when true (default), sets TCP_NODELAY on the underlying
/// AxiomaticAdapter socket. See AxiomaticAdapter for the
/// full tradeoff discussion.
/// @param model converter model, e.g. std::make_unique<ax142100a::Ax142100a>(); must not be null
AxiomaticSocketcanBridge(
const std::string & can_interface_name,
const std::string & ip,
const std::string & port,
bool verbose = false,
bool tcp_nodelay = true);
bool tcp_nodelay = true,
std::unique_ptr<const AxiomaticModel> model = std::make_unique<ax140900::Ax140900>());

/// @brief Destruct axiomatic socketcan bridge
~AxiomaticSocketcanBridge();
Expand Down Expand Up @@ -75,7 +77,6 @@ class AxiomaticSocketcanBridge
void ethcanReceiveCallback(std::unique_ptr<const polymath::socketcan::CanFrame> frame);
};

} // namespace can
} // namespace polymath
} // namespace polymath::can

#endif // AXIOMATIC_ADAPTER__AXIOMATIC_SOCKETCAN_BRIDGE_NODE_HPP_
Loading
Loading