Skip to content
Draft
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
18 changes: 18 additions & 0 deletions axiomatic_adapter/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ install(TARGETS axiomatic_adapter
add_executable(axiomatic_socketcan_bridge
src/axiomatic_socketcan_bridge.cpp
src/axiomatic_socketcan_bridge_node.cpp
src/serial_port.cpp
)

target_include_directories(axiomatic_socketcan_bridge PUBLIC
Expand Down Expand Up @@ -112,6 +113,23 @@ if(BUILD_TESTING)
add_test(NAME test_axiomatic_model COMMAND test_axiomatic_model)
endif()

add_executable(test_axiomatic_adapter_loopback test/axiomatic_adapter_loopback_test.cpp)
target_link_libraries(test_axiomatic_adapter_loopback PRIVATE axiomatic_adapter Boost::boost Catch2::Catch2WithMain)
if(COMMAND catch_discover_tests)
catch_discover_tests(test_axiomatic_adapter_loopback)
else()
add_test(NAME test_axiomatic_adapter_loopback COMMAND test_axiomatic_adapter_loopback)
endif()

add_executable(test_serial_port test/serial_port_test.cpp src/serial_port.cpp)
target_include_directories(test_serial_port PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(test_serial_port PRIVATE Catch2::Catch2WithMain)
if(COMMAND catch_discover_tests)
catch_discover_tests(test_serial_port)
else()
add_test(NAME test_serial_port COMMAND test_serial_port)
endif()

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

if(HARDWARE_CONNECTED)
Expand Down
66 changes: 58 additions & 8 deletions axiomatic_adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,31 @@ https://www.notion.so/polymathrobotics/Axiomatic-CAN-to-Ethernet-Converter-08e07

## 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 |
| `--model` | Device | Raw (serial) data | Protocol reference |
| --- | --- | --- | --- |
| `ax140900` (default) | [AX140900 CAN/Ethernet Converter](https://www.axiomatic.com/product/canethernet-converter-ax140900/) | no | [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/) | yes | [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.
`decode` returns CAN frames, raw data payloads, and the number of bytes consumed; pass the unconsumed tail again with the next bytes received.

### 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.
If the model forwards raw (serial) data, also override `encodeRawData` and append decoded payloads to `DecodeResult::raw_data`.
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`.
4. Add a decode test of the manual's example bytes in `test/axiomatic_model_test.cpp`; the round trip tests cover every name in `modelNames`.

## Usage
### Socketcan-Axiomatic Bridge

```bash
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
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 [OPTIONAL]--serial-port

# Examples
# generic example to bridge vcan0 with axiomatic using 192.168.50.34:4000
Expand All @@ -38,6 +40,9 @@ 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

# also bridge the AX142100A's serial data to one end of a virtual serial pair (see Serial data below)
ros2 run axiomatic_adapter axiomatic_socketcan_bridge vcan0 192.168.1.20 4000 --model ax142100a --serial-port /dev/ttyAX0

# 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 All @@ -50,6 +55,45 @@ ros2 run axiomatic_adapter axiomatic_socketcan_bridge vcan0 192.168.50.34 4000 -
ros2 run axiomatic_adapter axiomatic_socketcan_bridge vcan0 192.168.50.34 4000 --no-tcp-nodelay
```

### Serial data (AX142100A)

`--serial-port <device>` opens an existing serial device in raw mode; the bridge does not create it.
Bytes read from the device are sent to the converter as Forwarded Data with the raw data flag set.
Raw data the converter forwards to Ethernet is written to the device.
The bridge does not set a baud rate; the converter's Main Settings page sets each serial port's.

For an application on the same host, create a virtual serial pair, give one end to the bridge, and open the other end in the application.
Each end must have exactly one reader; the bridge and the application must not open the same end.

```bash
# create the pair; run this as its own long-lived service
socat -d -d pty,raw,echo=0,link=/dev/ttyAX0 pty,raw,echo=0,link=/dev/ttyAX1

# bridge end
ros2 run axiomatic_adapter axiomatic_socketcan_bridge vcan0 192.168.1.20 4000 --model ax142100a --serial-port /dev/ttyAX0

# application end
picocom /dev/ttyAX1
```

A pair that outlives the bridge keeps the application's end open across bridge restarts.

The protocol message carries no serial port number.
The converter's routing configuration (web interface, UMAX142100A section 4) decides which ports the data goes to and comes from.
Interfaces are numbered RS-232 #1 = 1, RS-232 #2 = 2, RS-422 = 3, CAN = 4, Ethernet = 5.
On Main Settings, set Data Port Type to TCP; the bridge does not speak UDP.
In Ethernet and serial routing rules, leave "Forward all data bytes" at its default, or the rule truncates what it forwards.

- **Ethernet → serial**: on `eth_data_routing.shtml`, list the RS ports in a rule's Output interfaces.
Raw data is forwarded as is, subject only to these rules (UMAX142100A section 4.2).
- **Ethernet → CAN**: CAN frames sent by the bridge pass through the same Ethernet routing rules, so a rule must also list CAN (4).
- **Serial → Ethernet**: on `serial_data_routing.shtml`, list Ethernet (5) in the RS port's rule Output interfaces.
- **CAN → Ethernet**: on `can_data_routing.shtml`, list Ethernet (5) and set Data forwarding to "Forward full message", as in the UMAX142100A CAN routing example.
"Forward all data bytes" appears to forward the data bytes without the CAN ID.

All routed ports share the one serial device.
To tell ports apart, configure distinct start bytes per port in the serial routing rules.

### Library

```cpp
Expand All @@ -72,9 +116,15 @@ polymath::can::AxiomaticAdapter adapter(
// open the socket
adapter.openSocket();

// optionally receive raw (serial) data; set before starting the reception thread
adapter.setOnRawDataCallback([](std::vector<uint8_t> /*data*/) { /* No-op */ });

// start the reception thread. At this point, it's running
adapter.startReceptionThread();

// send raw (serial) data; returns an error for a model without raw data
adapter.sendRawData({'h', 'i', '\r', '\n'});

// on shutdown/destruction, thread will join and socket will close
```

Expand Down Expand Up @@ -103,5 +153,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.
5. AX142100A 11-bit IDs are assumed to be 2 bytes on the wire; the manual only shows a 29-bit example.
6. The AX142100A's maximum raw data payload per message is unconfirmed.
16 changes: 16 additions & 0 deletions axiomatic_adapter/include/axiomatic_adapter/axiomatic_adapter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
#include <poll.h>

#include <chrono>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "axiomatic_adapter/axiomatic_model.hpp"
#include "axiomatic_adapter/models/ax140900.hpp"
Expand Down Expand Up @@ -103,6 +105,12 @@ class AxiomaticAdapter : public std::enable_shared_from_this<AxiomaticAdapter>
/// @return success on started
bool startReceptionThread();

/// @brief Set the callback for raw (serial) data payloads.
/// Called on the thread that read them: the reception thread, or the caller of receive().
/// Unset drops raw data.
/// @return false while the reception thread is running
bool setOnRawDataCallback(std::function<void(std::vector<uint8_t> data)> && callback_function);

/// @brief Stop and join reception thread
/// @param timeout_s INPUT timeout in seconds, <=0 means no timeout
/// @return success on closed and joined thread
Expand All @@ -118,6 +126,14 @@ class AxiomaticAdapter : public std::enable_shared_from_this<AxiomaticAdapter>
/// @return optional error string filled with an error message if any
std::optional<socket_error_string_t> send(const can_frame & frame);

/// @brief Transmit raw (serial) data via socket
/// @param data payload, at most the model's raw data limit
/// @return optional error string, set when the model has no raw data channel or the write fails
std::optional<socket_error_string_t> sendRawData(const std::vector<uint8_t> & data);

/// @brief Whether the model carries raw (serial) data
bool supportsRawData() const;

/// @brief Get state of socket
/// @return TCPSocketState data type detailing OPEN or CLOSED
TCPSocketState get_socket_state();
Expand Down
19 changes: 14 additions & 5 deletions axiomatic_adapter/include/axiomatic_adapter/axiomatic_model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>

Expand All @@ -29,7 +30,7 @@ namespace polymath::can

/// @class polymath::can::AxiomaticModel
/// @brief One Axiomatic converter model and its wire format.
/// Subclasses override protocolId, encode, and decodeMessage.
/// Subclasses override protocolId, encode, and decodeMessage, and encodeRawData when the model forwards raw data.
/// Methods are const and keep no state between calls; one instance may be shared across threads.
class AxiomaticModel
{
Expand All @@ -39,8 +40,16 @@ class AxiomaticModel
/// @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.
/// @brief Encode raw (serial) data as one complete protocol message
/// @return std::nullopt when the model has no raw data channel
virtual std::optional<std::vector<uint8_t>> encodeRawData(const std::vector<uint8_t> & data) const;

/// @brief Whether encodeRawData and decode carry raw data
bool supportsRawData() const;

/// @brief Decode every CAN frame and raw data payload in a buffer of back-to-back protocol messages.
/// Messages with a Message ID that carries neither are skipped.
/// A trailing incomplete message is left unconsumed; pass it again with the bytes that follow.
/// @param data buffer start
/// @param size buffer length in bytes
protocol::DecodeResult decode(const uint8_t * data, size_t size) const;
Expand All @@ -49,8 +58,8 @@ class AxiomaticModel
/// @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
/// @brief Append the CAN frames and raw data in one message to result
/// @return false when the Message ID carries neither
virtual bool decodeMessage(const protocol::MessageView & message, protocol::DecodeResult & result) const = 0;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,19 @@ struct ParsedMessages
{
std::vector<MessageView> messages;
std::vector<std::string> diagnostics;
/// @brief Leading bytes parsed or dropped; the rest is an incomplete message to retry with more data
size_t consumed{0};
};

/// @brief CAN frames decoded from a buffer, plus a description of every byte range skipped or dropped
/// @brief CAN frames and raw data decoded from a buffer, plus a description of every byte range skipped or dropped
struct DecodeResult
{
std::vector<polymath::socketcan::CanFrame> frames;
/// @brief Raw data payloads, in arrival order
std::vector<std::vector<uint8_t>> raw_data;
std::vector<std::string> diagnostics;
/// @brief Leading bytes decoded or dropped; the rest is an incomplete message to retry with more data
size_t consumed{0};
};

/// @brief Read an unsigned little-endian integer
Expand All @@ -82,8 +88,8 @@ 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.
/// Scanning stops at the first header whose tag or Protocol ID does not match, and the rest of the buffer is consumed.
/// A trailing message or header that runs past the buffer is not returned or consumed.
/// @param protocol_id Protocol ID every message must carry
/// @param data buffer start
/// @param size buffer length in bytes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@
#ifndef AXIOMATIC_ADAPTER__AXIOMATIC_SOCKETCAN_BRIDGE_NODE_HPP_
#define AXIOMATIC_ADAPTER__AXIOMATIC_SOCKETCAN_BRIDGE_NODE_HPP_

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

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

namespace polymath::can
{

/// @class polymath::can::AxiomaticSocketcanBridge
/// @brief This class will bridge a socketcan connection and read/write to the can-eth device tcp connection
/// @brief This class will bridge a socketcan connection and read/write to the can-eth device tcp connection.
/// Optionally bridges the device's raw (serial) data to a local serial port.
class AxiomaticSocketcanBridge
{
public:
Expand All @@ -40,19 +45,21 @@ class AxiomaticSocketcanBridge
/// AxiomaticAdapter socket. See AxiomaticAdapter for the
/// full tradeoff discussion.
/// @param model converter model, e.g. std::make_unique<ax142100a::Ax142100a>(); must not be null
/// @param serial_port_path when set, existing serial device bridged to the model's raw data, e.g. /dev/ttyAX0
AxiomaticSocketcanBridge(
const std::string & can_interface_name,
const std::string & ip,
const std::string & port,
bool verbose = false,
bool tcp_nodelay = true,
std::unique_ptr<const AxiomaticModel> model = std::make_unique<ax140900::Ax140900>());
std::unique_ptr<const AxiomaticModel> model = std::make_unique<ax140900::Ax140900>(),
const std::optional<std::string> & serial_port_path = std::nullopt);

/// @brief Destruct axiomatic socketcan bridge
~AxiomaticSocketcanBridge();

/// @brief configures and opens the TCP and CAN sockets for the bridge
/// @return success for opening both sockets
/// @brief configures and opens the TCP and CAN sockets, and the serial port when a path was given
/// @return success for opening all of them; false when a serial port is configured for a model without raw data
bool on_configure();

/// @brief activates and starts the reception threads for the socketcan and axiomatic adapters
Expand All @@ -63,18 +70,23 @@ class AxiomaticSocketcanBridge
/// @return success for joining bpth threads
bool on_deactivate();

/// @brief Shuts down and closes the TCP and CAN sockets
/// @brief Shuts down and closes the TCP and CAN sockets, and the serial port
/// @return success for closing both sockets
bool on_shutdown();

private:
polymath::socketcan::SocketcanAdapter socketcan_adapter_;
polymath::can::AxiomaticAdapter axiomatic_adapter_;
std::optional<std::string> serial_port_path_;
// open from on_configure until on_shutdown
std::unique_ptr<SerialPort> serial_port_;

bool verbose_;

void socketcanReceiveCallback(std::unique_ptr<const polymath::socketcan::CanFrame> frame);
void ethcanReceiveCallback(std::unique_ptr<const polymath::socketcan::CanFrame> frame);
void serialPortReceiveCallback(std::vector<uint8_t> data);
void ethRawDataReceiveCallback(std::vector<uint8_t> data);
};

} // namespace polymath::can
Expand Down
12 changes: 10 additions & 2 deletions axiomatic_adapter/include/axiomatic_adapter/models/ax142100a.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#define AXIOMATIC_ADAPTER__MODELS__AX142100A_HPP_

#include <cstdint>
#include <optional>
#include <vector>

#include "axiomatic_adapter/axiomatic_model.hpp"
Expand All @@ -31,6 +32,10 @@ namespace polymath::can::ax142100a
/// @brief Protocol ID 20008, 0x28 0x4E on the wire
static constexpr uint16_t PROTOCOL_ID = 0x4E28;

/// @brief Largest raw data payload one message can carry; the status byte takes one byte of the 16-bit body length
/// TODO: (David Tarazi) Confirm the device's own limit on hardware; the UMAX142100A example client uses a 256-byte buffer.
static constexpr size_t MAX_RAW_DATA_BYTES = 0xFFFF - 1;

/// @brief Message IDs (UMAX142100A section 4.2)
enum class MessageId : uint16_t
{
Expand All @@ -39,13 +44,16 @@ enum class MessageId : uint16_t
};

/// @class polymath::can::ax142100a::Ax142100a
/// @brief Encodes a Forwarded Data message.
/// Decodes Forwarded Data messages; raw data payloads are skipped.
/// @brief Encodes and decodes Forwarded Data messages carrying a CAN frame or raw data.
/// The device's data routing configuration selects which serial port raw data is forwarded to and from.
class Ax142100a : public AxiomaticModel
{
public:
std::vector<uint8_t> encode(const polymath::socketcan::CanFrame & frame) const override;

/// @throws std::length_error when data exceeds MAX_RAW_DATA_BYTES
std::optional<std::vector<uint8_t>> encodeRawData(const std::vector<uint8_t> & data) const override;

protected:
uint16_t protocolId() const override;
bool decodeMessage(const protocol::MessageView & message, protocol::DecodeResult & result) const override;
Expand Down
Loading
Loading