A small C library and example program for the HASOMED MotionSensor 2.0, an IMU that communicates over a serial link (Bluetooth SPP or USB-CDC, e.g. /dev/ttyACM0) using a binary, package-based protocol.
This library implements the following commands, each including its acknowledgment handling: Init, GetSerial, GetSensorPosition, GetCalibrationData, GetScaleFactor, GetAvailableMeasurementModes, PrepareMeasurement, UnPrepareMeasurement, StartMeasurement, StopMeasurement, SetMeasurementMode, GetMeasurementMode, MeasurementActive, SensorStatsExt, RawData14, RawData15, Error, KeepAlive.
include/motionsensor.h Public API (types, constants, function declarations)
src/ms_transport.c Serial port setup, byte-stuffing/checksum framing, send/receive
src/ms_commands.c High-level command wrappers (Init, GetSerial, ...)
src/ms_measdata.c Parsing of RawData14/RawData15/Error, calibration math
src/ms_error.c Human readable error strings
src/ms_internal.h Internal shared definitions
examples/example_use.c Example program (see below)
Makefile Builds libmotionsensor.a and the example program
Requires a C11 compiler and Linux (uses termios/poll).
makeThis produces libmotionsensor.a and the example binary example_use.
make cleanremoves all build artifacts.
The example follows the "Example of use" chapter of the user manual: it initializes the connection, reads sensor information, configures a measurement mode, starts a measurement, prints incoming data packages for a few seconds, then stops it.
./example_use /dev/ttyACM0The device path defaults to /dev/ttyACM0 if omitted. Make sure your user has permission to access the serial device (usually the dialout group on Linux).
Link against libmotionsensor.a and include the header:
cc -Iinclude myprogram.c libmotionsensor.a -o myprogram#include "motionsensor.h"
MotionSensor *ms = ms_open("/dev/ttyACM0");
if (!ms) { /* handle error */ }
MsInitInfo info;
if (ms_init(ms, &info, 2000) != MS_OK) { /* handle error */ }
ms_close(ms);- Every command function returns an
int:MS_OK(0) on success.- A negative
MS_ERR_*code for local failures (see below). MS_ERR_DEVICE(-5) if the sensor answered with anErrorpackage; callms_last_device_error(ms)to get the sensor's error code andms_device_strerror(code)for a human readable description.
- Every command function takes a
timeout_msparameter: the maximum time to wait for the sensor's acknowledgment. - Output values are written through pointer parameters; passing
NULLfor an output parameter you don't need is fine. ms_strerror(rc)turns any library return code into a readable string.
int rc = ms_get_serial(ms, &serial, 2000);
if (rc != MS_OK) {
if (rc == MS_ERR_DEVICE) {
fprintf(stderr, "device error: %s\n", ms_device_strerror(ms_last_device_error(ms)));
} else {
fprintf(stderr, "error: %s\n", ms_strerror(rc));
}
}| Code | Meaning |
|---|---|
MS_OK |
Success |
MS_ERR_TIMEOUT |
No (matching) response within timeout_ms |
MS_ERR_IO |
Serial port read/write failed |
MS_ERR_CHECKSUM |
Received a package with a wrong checksum |
MS_ERR_PROTOCOL |
Malformed or unexpected package |
MS_ERR_DEVICE |
Sensor answered with an Error package (see ms_last_device_error) |
MS_ERR_ARG |
Invalid argument passed to a library call |
MotionSensor *ms_open(const char *device);Opens the serial/USB-CDC device and configures the line (460800 baud, 8N1, no flow control, as specified by the protocol). Returns NULL on failure (e.g. wrong path or missing permissions).
MotionSensor *ms = ms_open("/dev/ttyACM0");void ms_close(MotionSensor *ms);Closes the device and frees the handle.
ms_close(ms);Every command below must be called after a successful ms_init(). timeout_ms is the time to wait for the sensor's response, e.g. 2000 (2 seconds).
int ms_init(MotionSensor *ms, MsInitInfo *info, int timeout_ms);Initializes the connection. Must be called once at the beginning, before any other command. Returns the protocol version and a welcome text via info.
MsInitInfo info;
if (ms_init(ms, &info, 2000) == MS_OK) {
printf("protocol version %u: %s\n", info.protocol_version, info.welcome_text);
}int ms_keep_alive(MotionSensor *ms, int timeout_ms);Keeps the connection alive and resets the sensor's StandBy timer (the sensor turns itself off after a period of inactivity, see ConfigStandBy in the manual). Call this periodically if you are not otherwise communicating with the sensor (e.g. during a long idle period).
ms_keep_alive(ms, 2000);int ms_get_serial(MotionSensor *ms, uint32_t *serial, int timeout_ms);Reads the sensor's unique serial number (also printed on the sensor's label).
uint32_t serial;
if (ms_get_serial(ms, &serial, 2000) == MS_OK) {
printf("serial: %u\n", serial);
}int ms_get_sensor_position(MotionSensor *ms, uint32_t *position, int timeout_ms);Reads the body position the sensor is configured for (see the MsSensorPosition enum: MS_POS_FOOT_LEFT, MS_POS_FOOT_RIGHT, MS_POS_SHANK_LEFT, MS_POS_SHANK_RIGHT, MS_POS_THIGH_LEFT, MS_POS_THIGH_RIGHT, MS_POS_PELVIS, MS_POS_STERNUM, MS_POS_WRIST_LEFT, MS_POS_WRIST_RIGHT, MS_POS_PELVIS_DAY, MS_POS_PELVIS_NIGHT).
uint32_t position;
if (ms_get_sensor_position(ms, &position, 2000) == MS_OK) {
printf("position: %u\n", position);
}int ms_get_calibration_data(MotionSensor *ms, uint32_t channel, uint32_t mode,
MsCalibrationRaw *calib, int timeout_ms);Reads the factory calibration data (bias, rotation matrix, Cg1/Cg2 polynomial coefficients) for a given sensor chip (channel: MS_CHANNEL_ACC, MS_CHANNEL_GYRO, MS_CHANNEL_MAG, MS_CHANNEL_PRESSURE) and measurement range (mode, e.g. MS_ACC_MODE_8G for the accelerometer or MS_GYRO_MODE_1000DPS for the gyroscope). Convert the result to physical (double) values with ms_calibration_to_double() and use ms_apply_calibration() to calibrate raw samples (see Converting measurement data below).
MsCalibrationRaw raw;
MsCalibration acc_calib;
if (ms_get_calibration_data(ms, MS_CHANNEL_ACC, MS_ACC_MODE_8G, &raw, 2000) == MS_OK) {
ms_calibration_to_double(&raw, &acc_calib);
}int ms_get_scale_factor(MotionSensor *ms, uint32_t channel, uint32_t mode,
double *scale_factor, int timeout_ms);Reads the scale factor (LSB per physical unit) for a channel/mode combination, needed to convert raw RawDataXX values into physical units (scaled_value = LSB / scale_factor).
double acc_scale;
if (ms_get_scale_factor(ms, MS_CHANNEL_ACC, MS_ACC_MODE_8G, &acc_scale, 2000) == MS_OK) {
printf("acc scale factor: %f LSB per m/s^2\n", acc_scale);
}int ms_get_available_measurement_modes(MotionSensor *ms, uint32_t *modes,
uint32_t max_modes, uint32_t *num_modes,
int timeout_ms);Lists the measurement data commands (e.g. MS_CMD_RAW_DATA14) supported by this particular sensor's hardware revision. modes must point to an array with room for at least max_modes entries; *num_modes receives the number of modes the sensor actually reported (which may be larger than max_modes if the array was too small).
uint32_t modes[64], num_modes;
if (ms_get_available_measurement_modes(ms, modes, 64, &num_modes, 2000) == MS_OK) {
for (uint32_t i = 0; i < num_modes; ++i) {
printf("mode %u\n", modes[i]);
}
}int ms_set_measurement_mode(MotionSensor *ms, const MsMeasurementMode *mode, int timeout_ms);Configures the sample rate and the measurement data package to be produced, plus the range of each sensor chip. The sensor must not be prepared (call ms_unprepare_measurement() first if needed).
Fields of MsMeasurementMode:
| Field | Meaning |
|---|---|
sample_rate |
Sample rate in Hz (max. 600, see manual for hardware-dependent limits) |
measurement_mode |
Command number of the data package to stream, e.g. MS_CMD_RAW_DATA14 |
acc_mode |
Accelerometer range, e.g. MS_ACC_MODE_8G |
gyro_mode |
Gyroscope range, e.g. MS_GYRO_MODE_1000DPS |
mag_mode |
Magnetometer range, e.g. MS_MAG_MODE_1_3GS |
send_storage_mode |
MS_SEND_STORAGE_SEND_NOT_STORE, ..._SEND_AND_STORE, or ..._NOT_SEND_STORE |
MsMeasurementMode mode = {
.sample_rate = 100,
.measurement_mode = MS_CMD_RAW_DATA14,
.acc_mode = MS_ACC_MODE_8G,
.gyro_mode = MS_GYRO_MODE_1000DPS,
.mag_mode = MS_MAG_MODE_1_3GS,
.send_storage_mode = MS_SEND_STORAGE_SEND_NOT_STORE,
};
ms_set_measurement_mode(ms, &mode, 2000);int ms_get_measurement_mode(MotionSensor *ms, MsMeasurementMode *mode, int timeout_ms);Reads back the currently configured measurement mode (same fields as above).
MsMeasurementMode mode;
if (ms_get_measurement_mode(ms, &mode, 2000) == MS_OK) {
printf("sample rate: %u Hz\n", mode.sample_rate);
}int ms_prepare_measurement(MotionSensor *ms, uint32_t *session_id, int timeout_ms);Prepares the sensor for a measurement (resets the package counter, starts an internal SD-card session, etc.). Must be called before every ms_start_measurement(). Returns a session_id needed to request retransmission of lost packages.
uint32_t session_id;
ms_prepare_measurement(ms, &session_id, 2000);int ms_unprepare_measurement(MotionSensor *ms, int timeout_ms);Cancels a preparation made with ms_prepare_measurement() without starting a measurement (e.g. to change the measurement mode again). Not needed after ms_stop_measurement(), which un-prepares implicitly.
ms_unprepare_measurement(ms, 2000);int ms_start_measurement(MotionSensor *ms, uint32_t *start_time_ms, int timeout_ms);Starts a previously prepared measurement. From this point on the sensor streams measurement data packages (e.g. RawData14) until ms_stop_measurement() is called; read them with ms_receive() (see Receiving measurement data). start_time_ms receives the sensor's internal clock value (ms since power-on) at the moment the measurement started.
uint32_t start_time;
ms_start_measurement(ms, &start_time, 2000);int ms_stop_measurement(MotionSensor *ms, uint32_t *stop_time_ms,
uint32_t *num_packages, int timeout_ms);Stops a running measurement. stop_time_ms receives the sensor's clock value at the time of stopping, num_packages the total number of measurement data packages the sensor sent.
uint32_t stop_time, num_packages;
ms_stop_measurement(ms, &stop_time, &num_packages, 2000);int ms_measurement_active(MotionSensor *ms, int *active, int timeout_ms);Checks whether a measurement is currently running (*active is set to 1 or 0).
int active;
if (ms_measurement_active(ms, &active, 2000) == MS_OK) {
printf("measurement active: %d\n", active);
}int ms_sensor_stats_ext(MotionSensor *ms, MsSensorStatsExt *stats, int timeout_ms);Reads extended battery/link statistics: csoc (state of charge, %), ttecp (time to empty at constant power, minutes), ai (average current, mA), rsoc (relative state of charge, %), volt (voltage, mV).
MsSensorStatsExt stats;
if (ms_sensor_stats_ext(ms, &stats, 2000) == MS_OK) {
printf("battery: %d%%, %d mV\n", stats.csoc, stats.volt);
}Once a measurement is started, the sensor streams data packages asynchronously (not as command/acknowledgment pairs). Read them in a loop with the low-level ms_receive() function and dispatch on pkt.command:
MsPacket pkt;
int rc = ms_receive(ms, &pkt, 2000);
if (rc != MS_OK) {
/* MS_ERR_TIMEOUT, MS_ERR_IO, MS_ERR_CHECKSUM, or MS_ERR_PROTOCOL */
}int ms_parse_rawdata_imu(const MsPacket *pkt, MsRawDataImu *out);Decodes a RawData14 (21 accelerometer+gyroscope samples per package) or RawData15 (1 sample per package) measurement data package. pkt->command must be MS_CMD_RAW_DATA14 or MS_CMD_RAW_DATA15. out->num_samples tells you how many entries of out->samples[] are valid (21 or 1); each sample holds raw LSB values acc[3] and gyro[3].
if (pkt.command == MS_CMD_RAW_DATA14 || pkt.command == MS_CMD_RAW_DATA15) {
MsRawDataImu imu;
if (ms_parse_rawdata_imu(&pkt, &imu) == MS_OK) {
printf("package #%u, %u samples, first AccX = %d\n",
imu.package_number, imu.num_samples, imu.samples[0].acc[0]);
}
}int ms_parse_error(const MsPacket *pkt, uint32_t *error_code);Decodes an Error package (pkt->command == MS_CMD_ERROR), which the sensor can send instead of an acknowledgment, or asynchronously while a measurement is running. ms_transact() (used internally by every command function above) already handles Error responses to a command automatically and returns MS_ERR_DEVICE; use ms_parse_error() yourself only when reading raw packages during a measurement.
if (pkt.command == MS_CMD_ERROR) {
uint32_t err;
ms_parse_error(&pkt, &err);
fprintf(stderr, "sensor error: %s\n", ms_device_strerror(err));
}Raw RawDataXX samples are signed LSB values. To turn them into physical units:
- Divide by the scale factor from
ms_get_scale_factor():ms_scale_lsb(lsb, scale_factor). - Apply the calibration data from
ms_get_calibration_data()(converted to double withms_calibration_to_double()):ms_apply_calibration(scaled, &calib, out).
double scale_factor; /* from ms_get_scale_factor() */
MsCalibration calib; /* from ms_get_calibration_data() + ms_calibration_to_double() */
int16_t lsb[3] = { imu.samples[0].acc[0], imu.samples[0].acc[1], imu.samples[0].acc[2] };
double scaled[3], physical[3];
for (int i = 0; i < 3; ++i) {
scaled[i] = ms_scale_lsb(lsb[i], scale_factor);
}
ms_apply_calibration(scaled, &calib, physical);
/* physical[] now holds AccX/Y/Z in m/s^2 (or GyroX/Y/Z in deg/s for the gyroscope) */If no calibration data is available (or you want uncalibrated values), you may skip ms_apply_calibration() and use the scaled values directly.
For advanced use cases (e.g. commands not wrapped by this library), the underlying framing functions are also exposed:
int ms_send(MotionSensor *ms, uint16_t command, const uint8_t *data, uint16_t data_len);
int ms_receive(MotionSensor *ms, MsPacket *pkt, int timeout_ms);
int ms_transact(MotionSensor *ms, uint16_t command, const uint8_t *data, uint16_t data_len,
uint16_t expected_ack, MsPacket *response, int timeout_ms);ms_transact() sends a command and waits for either the expected acknowledgment or an Error package, discarding any unrelated packages (such as measurement data) received while waiting. It's what every command function in this library uses internally.
- This code is fully written and tested by AI