mti610
Overview
Talking to an Xsens MTi-610 over a serial port: a ByteStream abstraction with termios and file-replay implementations, the Config/Measurement handshake as a synchronous helper, the reader thread that owns the port and runs both, and the desired-versus-actual comparison that decides what, if anything, to write to the device. Everything here owns a file descriptor, a thread or a file; nothing here knows a field offset. That is what lets xbus be constexpr, and it is why this target depends on xbus and xbus may never depend on this one.
It is the first serial code in the tree. Every other device here is TCP, libusb, hidapi or SocketCAN. It is termios rather than libusb deliberately: an MTi-600 development kit presents as a USB-serial adapter, so the whole stack builds and runs on macOS as well as on the vehicle. It is free of zenoh and capnp; the node, mti610_bridge, maps items onto schemas. Why the threading looks the way it does is in the design notes.
Public headers
| Header | |
|---|---|
mti610/byte_stream.h | ByteStream: the four methods everything above is written against, never a file descriptor. |
mti610/serial_stream.h | SerialStream::open(path, options): a ByteStream over a termios port, opened O_NONBLOCK and O_NOCTTY. |
mti610/replay_stream.h | ReplayStream: a ByteStream over a file of captured bytes, in small chunks. |
mti610/device_session.h | DeviceSession: goToConfig, goToMeasurement, identify, configure, exchange, sendWakeUpAck, pump. Synchronous, single-threaded. |
mti610/stream_client.h | StreamClient: the reader thread. Owns the port, runs the handshake, delivers MTData2, requestReconfigure. |
mti610/output_config.h | diff, plan_writes and normalise_frequency: free functions over two lists, no port. |
mti610/error.h | mti610::Error: a device-and-port error with a message and an errno. NotConnected, Timeout and Refused want different responses. |
Using it
Link the CMake target mti610. StreamClient takes a StreamFactory, its options (which include the SessionOptions for the handshake and the desired output list) and a handler that receives every MTData2 message on the reader thread:
mti610::StreamClient::StreamFactory open = [&] {
return mti610::SerialStream::open(port, serialOptions);
};
mti610::StreamClient client(open, options, [&](const xbus::MessageView& message) {
xbus::ItemIterator items(message.data);
while (auto item = items.next()) { /* visit_item */ }
});
client.start();
// Later, from any thread: ask the reader to re-run the handshake at the top
// of its next loop. Data stops while it does.
client.requestReconfigure();
setByteTap is what --dump-xbus uses: every received byte, before framing. measuring() says whether the device is currently in Measurement.
Behaviour worth knowing
The device has two states and one port. It answers configuration messages only in Config and emits MTData2 only in Measurement, so whoever owns the port owns the handshake, or the two race for the same bytes. StreamClient runs both on its reader thread, and reconfiguration is a request: requestReconfigure() raises a flag acted on at the top of the next loop. Every service call therefore costs data. This is not the shape bd992::StreamClient has, and the difference comes from the device rather than from taste.
An unsolicited WakeUp means the device reset. It must be answered within 500 ms or the device enters Measurement with its stored configuration, which is exactly what the handshake spent its time replacing. Miss this and the node keeps publishing, with the wrong outputs at the wrong rates, and nothing anywhere says so. sendWakeUpAck is the answer, and the reader thread sends it.
SetOutputConfiguration replaces the whole list. XBus has no way to change one output, so port_policy: additive (“leave what I did not mention alone”) means plan_writes re-sends those entries. Getting it backwards switches off somebody else’s data with no error anywhere. This is the opposite of how the BD992’s APPFILE works, and it is the one thing that shapes everything in output_config.h.
The device ignores the frequency on packet metadata, answering 0xFFFF whatever was asked. A configuration check comparing raw numbers would find drift on every pass, rewrite, and find it again a minute later, forever. normalise_frequency() exists for this: zero and 0xFFFF both mean maximum, a real rate is left alone, and the plan writes the normalised value so the read-back matches.
The device clamps rather than refusing a rate the link cannot carry, so a rate that does not fit becomes a lower rate and no error. configure returns what the device echoed so a caller can warn when it differs.
The library never changes the device’s baud rate. SetPortConfig’s word layout is unknown, so PortConfig words are read and round-tripped and never composed. Above 230400 on macOS there is no termios constant and the rate is applied by IOSSIOSPEED; that path exists and has never carried a byte.
The ByteStream::recvSome contract is that 0 and -1 stay distinct, for the reason bd992/byte_stream.h gives.
Tests
ctest --test-dir build -L mti610 --output-on-failure # mti610_test_output_config, mti610_test_stream, mti610_test_config
All three are unit. mti610_test_output_config is the comparison over lists, including the test named after normalise_frequency. mti610_test_stream runs a scripted MTi on the far end of a real pty, so termios setup, partial reads and end-of-file are exercised for real. The fake device enforces the protocol rather than assuming it: it answers only valid BIDs, only in the state each message is valid in, refuses configuration while measuring, and brown-outs mid-stream to keep the WakeUp handling honest. Each of those is a rule this library has to obey and none fails loudly if it does not; a device that does not answer is what a wrong assumption looks like. It is the same argument libs/xpr’s fake radio makes. A pty ignores baud, so the termios path is exercised but the rate is not. mti610_test_config lives in the node and recompiles node_config.cpp.