Skip to content

XM10 Extension Module SDK v2.9.0

Release date: 2026-10-19 | Target boards: Rev 2.0 only (Rev 1.1 stays at v2.8.1) | New: struct logging · USB memory logging

🛑 Rev 2.0 only release. The Rev 1.1 SDK has no changes since v2.8.1. If you use a Rev 1.1 board, keep using the Rev1.1.zip you already have.

⚠️ The new behavior in this release has not yet been confirmed on a real board. Please read "Not yet confirmed" below first. (not yet confirmed on the board)


At a glance

  • Struct logging is new (Rev 2.0). Register a struct of your own and the board sends each field's name, unit and type to the PC along with the values. See xm_api_logging.h and the new example Ex.44. (not yet confirmed on the board)
  • USB memory logging is back with a new API (Rev 2.0). It was removed in v2.5.0. The system saves one struct to a USB memory stick at a rate you choose. See xm_api_usb_msc.h (14 functions) and the new examples Ex.10 and Ex.34. (not yet confirmed on the board)
  • A few USB-CDC functions changed (Rev 2.0). The channel-name JSON can be up to 1,007 bytes, and XM_SetUsbCustomMeta() now returns bool. A debug message can be up to 1,021 bytes. You cannot send with a module ID the system uses, and the send functions return false when called inside an interrupt.
  • 3 declarations are gone from the headers (Rev 2.0). The system uses these functions and types by itself, so you never need to use them. If your code used them, building with the new SDK gives an error, so delete those lines. The names are listed below under "3 declarations removed from the headers".
  • The build setting changed (Rev 2.0). Calling a function that has no declaration now stops the build with an error instead of a warning.
  • The .xmlog receive time in the xm10 tool is now UTC. pc_time_us is now microseconds since 1970-01-01 UTC.
  • Example count — Rev 2.0: 48 / Rev 1.1: 40.
  • The firmware version written in the Rev 2.0 SDK is 2.9.0.

Struct logging (Rev 2.0)

Until now you sent a float array with XM_SendUsbDataWithId() and told the PC only the channel names with XM_SetUsbCustomMeta(). Now you can register a whole struct and send it. The board sends each field's name, unit and type along with the data, so the PC decodes the values with the right types. (not yet confirmed on the board) Rev 1.1 (v2.8.1) has no struct logging.

#include "xm_api.h"
#include "xm_api_logging_raw.h"   // always include this in a .c file that uses the XM_LOG_* macros

typedef struct {
    float    hip_angle;
    int16_t  emg[8];
    uint32_t mode;
} MyLog_t;                                          // 24 B — the size is a multiple of 4

XM_LOG_DECLARE(MyLog, MyLog_t);                     // give the struct a short name (token), MyLog

XM_LOG_FIELDS(MyLog) = {                            // fields to send: name · unit · scale
    XM_FIELD(MyLog, hip_angle, "hip_angle", "deg", 1.0f),
    XM_FIELD_ARRAY(MyLog, emg, "emg", "mV", 1.0f),
    XM_FIELD(MyLog, mode, "mode", "", 1.0f),
};

void Control_Setup(void)
{
    if (!XM_LOG_REGISTER(MyLog, 0xF0, 1000)) {          // module ID 0xF0, 1000 Hz
        XM_SetLedEffect(XM_LED_1, XM_LED_BLINK, 200);   // report a failed registration with an LED
    }
}

void Control_Loop(void)
{
    MyLog_t s = { 0 };
    (void)XM_LOG_SEND(MyLog, &s);                       // calling earlier than the registered rate sends nothing and returns false
}

How to use it

  • The four macros (XM_LOG_DECLARE · XM_LOG_FIELDS · XM_LOG_REGISTER · XM_LOG_SEND) are used in the same .c file. They work in C files only.
  • Call XM_LOG_REGISTER() in Control_Setup() and XM_LOG_SEND() in Control_Loop(). Both return bool.
  • You can call XM_LOG_SEND() on every loop; data still goes out only at the registered rate. Before the next period it sends nothing and returns false.
  • A struct size must be a multiple of 4 and at most 1,008 bytes. You can register up to 4 structs, and each one picks its own module ID from 0xF0 to 0xFE.
  • A struct has 1 to 90 fields. A field name can be up to 15 characters and a unit up to 7, using only letters, digits and symbols (ASCII).
  • The field type is decided automatically from the member's C type. Write an array member with XM_FIELD_ARRAY.
  • If you use it wrongly (an unsupported member type, a size that is not a multiple of 4, and so on), the compiler points to that line.
  • To find out why something was not sent, read the counters with XM_Log_GetStats().

Things to know

  • A registered module ID belongs to that struct only. Calling XM_SendUsbDataWithId() with the same ID returns false.
  • Once you register a struct, the amount you can send with XM_SendUsbDataWithId() and XM_SendUsbDebugMessage() is limited. Sending too much in a short time, or too much in one call, returns false. Send large data with struct logging instead.
  • Call XM_USB_SetHostProfile() in Control_Setup() before the first XM_LOG_REGISTER(). After you register even one struct, that call is ignored.
  • After you register a struct, XM_SetUsbCustomMeta() can also be called only inside Control_Setup(). Called anywhere else, it returns false.
  • With the profile for a plain serial terminal (XM_USB_HOST_TERMINAL), struct data is still sent and may appear mixed into the terminal as garbled characters. Do not register a struct when you only want to see text.
  • Control_Setup() runs before USB is ready, so a message you send to the PC from there usually does not go out. Report a failed registration with an LED.
  • Once you register a struct, Total Data (0x20) goes out at most once per 1 ms, even if the control loop runs later than 1 ms. In that stretch the xm_loop_count value is not continuous and some values are skipped. The interval at which the board sends and the time at which data arrives on the PC may not be an even 1 ms. (design goal, not yet confirmed on the board)

Viewing it on the PC

  • The xm10 tool in the SDK opens one tab per module ID (the tab title is the module ID and the struct name), uses the field names for graphs and CSV column names, and decodes the values with the types the board sends. (not yet confirmed on the board)
  • PhAI Studio is still in development, so for your own data structs (custom structs), view and save them with the xm10 tool for now.
  • The existing way (XM_SetUsbCustomMeta() + XM_SendUsbDataWithId()) still works. In that case the board sends only the channel names, as before, and the PC assumes the values are float32.

Example Ex.44 — CDC Struct Logging (Rev 2.0)

It registers two structs (SensorLog and StatusLog) and sends button, expansion-ADC and synthetic-wave values with their names and units. If you set USE_H10 to 1, it reads the H10's hip values and sends them at 1 kHz (read only — it does not output any torque). If registration fails, LED 3 blinks fast. This example has not been confirmed on a board yet. (not yet confirmed on the board) View Ex.44 CDC Struct Logging


Save to a USB memory stick (Rev 2.0)

USB memory logging, removed in v2.5.0, is back with a new API. Register one struct, and while logging runs the system writes that struct to the USB memory stick at the rate you set (1000 Hz if you do not set one). You do not call a write function yourself. (not yet confirmed on the board) v2.5.0 removed it because samples could be dropped silently while saving. This time you can see the number of dropped samples and the longest gap in XM_GetUsbLogStats(), summary.txt and the decoder result. (not yet confirmed on the board) Rev 1.1 (v2.8.1) has no USB memory logging.

// Control_Setup()
XM_SetUsbLogSource(&s_log, sizeof(s_log));   // s_log is a global or static struct
XM_SetUsbLogSampleRateHz(100);               // 1000 Hz if you leave it out

// Control_Loop() — when XM_IsUsbLogReady() and XM_IsUsbLogSessionIdle() are both true
XM_StartUsbDataLog("", "loop_count(uint32_t), hip_L(float), hip_R(float), contact(uint8_t), _pad(3bytes)");
  1. In Control_Setup(), register the struct and set the rate.
  2. In Control_Loop(), fill in the struct's values. The system saves it after Control_Loop() finishes.
  3. When XM_IsUsbLogReady() and XM_IsUsbLogSessionIdle() are both true, start with XM_StartUsbDataLog(). If it returns false, read the reason with XM_GetUsbLogLastStartError().
  4. Stop with XM_StopUsbDataLog() and wait until XM_IsUsbLogSessionIdle() becomes true. Once it is true, all files are closed, so you can pull out the USB memory stick. (not yet confirmed on the board)
Kind Functions Call from
Setup XM_SetUsbLogSource() · XM_SetUsbLogSampleRateHz() · XM_SetUsbLogAutoTimestamp() · XM_SetUsbLogRollingSize() Control_Setup() · Control_Loop()
Start · stop · marker · statistics XM_StartUsbDataLog() · XM_StopUsbDataLog() · XM_InsertUsbLogMarker() · XM_GetUsbLogStats() Control_Setup() · Control_Loop()
Status XM_IsUsbLogReady() · XM_IsUsbLogSessionIdle() · XM_IsUsbLogRecording() · XM_GetUsbLogLastStartError() · XM_GetUsbDiskFreeMB() · XM_GetUsbDiskTotalMB() Anywhere
  • If you call a setup, start, stop, marker or statistics function from another task or from an interrupt, it does nothing and returns false.
  • In the metadata line (the second argument of XM_StartUsbDataLog()), list the struct's fields in declaration order as name(type), separated by commas. The decoder uses this line to decode the data.
  • Write unused bytes with a name that starts with _, such as _pad(3bytes). If the field sizes do not add up to the registered struct size, logging does not start (XM_USB_LOG_START_METADATA_MISMATCH).
  • Format the USB memory stick as FAT32. exFAT and NTFS are not recognized.
  • Pick the logging rate from the divisors of 1000; if you do not set one, it is 1000 Hz. If the struct is too big for the rate, logging does not start (XM_USB_LOG_START_TOO_BIG_FOR_RATE).
  • Logging at 1000 Hz can drop some samples. The number dropped and the longest gap appear in XM_GetUsbLogStats() and in the decoder result.
  • Files go into a /LOGS/<session name>/ folder: metadata.txt, data_000_part_000.bin …, and summary.txt. If you leave the name empty, the clock (RTC) makes a YYMMDD_HHMMSS name.
  • A file is split into a new part at each set size (10 MB by default). (not yet confirmed on the board)
  • Do not pull out the USB memory stick while logging.

There is only one USB-C port

  • The XM10 has one USB-C port. While a USB memory stick is plugged in, you cannot use PhAI Studio, the xm10 tool, or firmware upload over USB.
  • During that time the USB link to the PC is down, so XM_IsUsbStreamConnected() is false and the functions that send to the PC (such as XM_SendUsbDataWithId()) return false.
  • When logging is done, pull out the USB memory stick and connect the PC. (not yet confirmed on the board)

Examples Ex.10 · Ex.34 (Rev 2.0)

Example What it does
Ex.10 USB Memory Log Button 1 starts and stops logging. It saves one struct (16 B) holding a loop counter, a synthetic sine wave, a synthetic ramp wave, the button states and whether the H10 is connected, at 100 Hz. Button 2 inserts a marker. After you pull out the USB memory stick, connect the PC and open a serial terminal, press button 3 and it sends the last statistics (not yet confirmed on the board)
Ex.34 USB Memory Gait Log It saves H10 gait data in one struct (96 B) at 1 kHz. Button 1 starts and stops, and buttons 2 and 3 insert markers

Neither example has been confirmed on a board yet. (not yet confirmed on the board)

⚠️ Ex.34 safety: With the default setting (USE_H10_ASSIST 0) it only records and sends no torque or control command. If you change USE_H10_ASSIST to 1, the H10's own assist algorithm turns on and assist torque is applied to the wearer. Turn it on only after you have checked the safety of the wearer and the surroundings.

Before you build, change the body information in _SetupBodyData() (weight, height, leg length) to the wearer's values. With the example values left as they are, the knee and thigh angles are inaccurate.

The PC program that decodes saved logs

Copy the /LOGS/<session name>/ folder from the USB memory stick to the PC and decode it with XM10 Log Decoder. If samples were dropped, it tells you where and for how long.

  • It is an installable Windows program, and Python is not needed.
  • Get XM10 Log Decoder 2.2.1 from the attachments on the release page.

What changed in the USB-CDC functions (Rev 2.0)

Channel names — XM_SetUsbCustomMeta()

  • The JSON can be up to 1,007 bytes (XM_USB_CUSTOM_META_MAX_LEN). Before, anything over 512 bytes was silently not sent.
  • The return type changed from void to bool. It returns true when registered. If the string is NULL or too long it returns false, and the names you registered earlier stay.
  • Code that ignores the return value builds as before. Code that passed NULL to clear the names can no longer clear them (it returns false).
  • Unless you use the XM_USB_HOST_TERMINAL profile, the board sends the channel names again every time a PC program opens the COM port. You no longer need to re-plug the USB cable when the names do not show up. (not yet confirmed on the board)
  • The limit described in v2.8.1 (a .xmlog recorded after you only press Disconnect and then Connect in the window has no channel names) is also designed to go away on Rev 2.0. (not yet confirmed on the board)
  • What to do: if your name list is long, check the return value. If it is false, put the units inside the names (for example "IMU0 Roll [deg]") to shorten the JSON.
  • Rev 1.1 is the same as v2.8.1: no return value, JSON up to 512 bytes, and the names are sent once when USB connects. If the names do not show up, re-plug the cable.

Debug messages — XM_SendUsbDebugMessage()

  • One 0x00 byte is added after the string. This keeps the data packets sent alongside from being corrupted. It is added under the XM_USB_HOST_TERMINAL profile too. (not yet confirmed on the board)
  • It sends up to 1,021 bytes at a time (XM_USB_DEBUG_MESSAGE_MAX_LEN). If the message is longer or NULL, it returns false and sends nothing. The length is in bytes, so Korean text takes more bytes than characters.
  • An empty string ("") sends only the one 0x00 byte and returns true. Before, it returned false.
  • Before, sending text while a stream was being received lost one data packet per message. On Rev 2.0 this loss is designed out. (not yet confirmed on the board)
  • What to do: split a message longer than 1,021 bytes into parts. With struct logging a shorter limit applies.
  • Rev 1.1 is the same as v2.8.1. It does not add the end byte (0x00), has no 1,021-byte limit, and sending while a stream is received loses one data packet per message.

Module IDs the system uses — XM_SendUsbDataWithId()

  • Sending with 0x20 · 0xB2 · 0xED · 0xEE · 0xEF · 0xFF returns false.
  • XM_SetUsbStreamModuleId() ignores these values and keeps using the previous ID.
  • What to do: use 0xF0 to 0xFE for your own data. The SDK examples use only 0xF0 to 0xF3.
  • Rev 1.1 is the same as v2.8.1. It accepts every ID.

Calling from an interrupt

  • If you call XM_SendUsbData(), XM_SendUsbDataWithId() or XM_SendUsbDebugMessage() inside an interrupt, nothing is sent and it returns false.
  • What to do: call them inside Control_Loop(). Do not call them from a background task you created yourself (XM_Task_*) either, because the data you send can be corrupted.
  • Rev 1.1 is the same as v2.8.1.

3 declarations removed from the headers

  • Removed: XM_USB_ProcessPeriodic() · XM_GetUsbLogStatus() · XmLogStatus_e (XM_LOG_STATUS_*). The system uses these functions and types by itself, so you never need to use them.
  • If your code used them, delete those lines. A function gives an implicit declaration of function error with the new setting described in "Build setting" below. The type XmLogStatus_e gives an unknown type name error with any setting.
  • If you need the state of USB memory logging, use XM_IsUsbLogRecording() and XM_IsUsbLogSessionIdle().
  • Rev 1.1 is the same as v2.8.1. All three declarations are still there.

Build setting — calls to undeclared functions are errors (Rev 2.0)

The Rev 2.0 SDK build settings (Debug and Release in STM32CubeIDE, and CMake) now include -Werror=implicit-function-declaration.

  • Calling a function that is not declared in any header is now a build error, not a warning. Before, you only got a warning and the build went through, and the function was called as if its return value were an int.
  • On screen it appears as error: implicit declaration of function 'function name'.
  • Fix it like this.
    1. Add #include for the header that declares the function. For an SDK function, #include "xm_api.h" is usually enough. For a function you wrote yourself, declare its prototype before you call it.
    2. Check that the function name is right. It may be a typo, or a name that was removed from the SDK.
    3. If you are using the 3 removed declarations described above, delete those lines.
  • This setting is in the newly downloaded Rev2.0.zip. The settings of a project you made from an earlier ZIP do not change.
  • The Rev 1.1 SDK is unchanged.

PC data tool — .xmlog receive time

The time the PC received data (pc_time_us) that the xm10 tool writes into .xmlog has changed.

v2.8.0 · v2.8.1 v2.9.0
pc_time_us in .xmlog µs since receiving started µs since 1970-01-01 UTC
pc_time_s in the live CSV seconds since receiving started unchanged
  • When saving starts, the tool reads the PC clock once to set a base time, and after that adds the elapsed time from the PC's monotonic clock. So the value does not jump or go backward while saving, even if the PC clock is adjusted. In exchange, after a long recording it can slowly drift from the PC clock.
  • pc_time_us in a CSV exported from .xmlog is also a UTC value. Use it when you need the date and time of day.
  • A .xmlog made by an older tool (v2.8.0 · v2.8.1) keeps the old value (µs since receiving started). Do not mix the two values when comparing.
  • A new document explains every field of .xmlog. It also includes the rule that reading stops when a record does not match the format — pc-data-tool/spec/xmlog.md (in Korean)
  • This change is in the pc-data-tool/ folder of Rev2.0.zip.
  • Rev1.1.zip is the same file as v2.8.1, so it contains the older tool. If you need the new tool, get it from Rev2.0.zip or from the pc-data-tool/ folder in the GitHub repository. The folder has the same files regardless of board revision.

Example count

Rev 2.0 48 / Rev 1.1 40.

  • 3 new examples — all are 🛑 Rev 2.0 only. Ex.10 USB Memory Log · Ex.34 USB Memory Gait Log · Ex.44 CDC Struct Logging
  • Ex.10 and Ex.34 are new examples, with different names and different content from the USB memory examples that existed up to v2.4.1 (removed in v2.5.0).
  • The Rev 1.1 SDK stays at 40. The examples that exist only on Rev 2.0 are Ex.10 · 34 · 36 · 40 · 41 · 42 · 43 · 44, 8 in all.

Not yet confirmed — please read

The new behavior in this release was checked only with builds and PC-side tests; it has not yet been confirmed on a real board. If anything looks off while you use it, please let us know.

  • USB memory logging as a whole — saving, stopping, pulling out the stick, and Ex.10 and Ex.34. This includes the point that you can pull out the stick once XM_IsUsbLogSessionIdle() becomes true after you stop. (not yet confirmed on the board)
  • Pulling out the stick and reconnecting the PC — there is only one USB-C port, so it has not been tested whether the PC link (PhAI Studio, the xm10 tool, firmware upload over USB) works again after you pull out the stick. (not yet confirmed on the board)
  • Kinds of USB memory sticks and dropped samples — we have not compared different kinds of USB memory sticks. The number of samples dropped when logging at 1000 Hz can differ from stick to stick, and there is no number measured with this version yet. We have not measured how logging affects the Control_Loop() period either. (not yet confirmed on the board)
  • Allowed gaps in the decoder — for logging rates other than 1000 Hz and 100 Hz, the gaps the decoder allows when it judges a log are calculated values that have not been checked against real board logs. (not yet confirmed on the board)
  • Struct logging as a whole — registering structs, sending the field information, sending data at the registered rate, and the xm10 tool building tab titles and column names and decoding the values by type, together with Ex.44. We have not confirmed it with data received from a real board. (not yet confirmed on the board)
  • Design goals of struct logging — that the board keeps data without dropping any even if the PC program pauses briefly (50 ms by default; you can change it with XM_Log_SetMinStallMs()), and that Total Data is sent at most once per 1 ms, are design goals. 50 ms is an initial value not yet measured on the board, so it is a goal, not a guarantee. (design goal)
  • Resending channel names and field information — resending the channel names (and the field information, if you use struct logging) each time a PC program opens the COM port, and their arrival within a few seconds after the port opens, are design goals. (design goal, not yet confirmed on the board)
  • The 0x00 at the end of a debug message — we have not confirmed that data packets still arrive intact while messages are being sent. (not yet confirmed on the board)
  • Code that uses none of the new features — we have not re-confirmed on the board that the data sent to the PC from code that uses neither struct logging nor USB memory logging is the same as in v2.8.1 (apart from the changes above). (not yet confirmed on the board)
  • Version number — we have not confirmed that a board running the uploaded firmware reports 2.9.0. (not yet confirmed on the board)
  • Installing XM10 Log Decoder — we have not installed and run the installer on a Windows PC without Python.

Compatibility

Item Notes
Existing firmware code (Rev 2.0) Mostly just rebuild. Fix the following — code that uses the 3 declarations removed from the headers · code that calls an undeclared function (now an error) · code that sends with a module ID the system uses (such as 0x20) · code that calls the send functions (XM_SendUsbData() · XM_SendUsbDataWithId() · XM_SendUsbDebugMessage()) inside an interrupt · code that sends a debug message longer than 1,021 bytes · code that passed NULL to XM_SetUsbCustomMeta() to clear the names
New headers xm_api.h now pulls in xm_api_logging.h and xm_api_usb_msc.h. If you defined your own names starting with XM_LOG_ · XM_FIELD · XM_Log_ · XM_USB_LOG_, they may clash. xm_api.h does not pull in xm_api_logging_raw.h, so include it directly in a .c file that uses the XM_LOG_* macros
Rev 1.1 Same as v2.8.1 — the SDK, headers, 40 examples and the behavior of the USB-CDC functions are all the same. There are no new headers, so struct logging and USB memory logging code does not compile on Rev 1.1
PC data tool The receive time in .xmlog (pc_time_us) changed. The required Python packages are the same (pyserial pyqt5 pyqtgraph numpy)
Bootloader Still v1.1.0. A bootloader that is already installed does not need to be changed

Downloads

File For
Rev2.0.zip SDK for Rev 2.0 boards (48 examples, includes pc-data-tool/)
Rev1.1.zip SDK for Rev 1.1 boards — the same file as v2.8.1 (no change, 40 examples)
XM10LogDecoder_Setup_v2.2.1_dbb8525.exe (.zip · SHA256SUMS.txt) XM10 Log Decoder 2.2.1 — PC program that decodes logs saved to a USB memory stick (Windows)
AGR_Bootloader.bin XM10 bootloader v1.1.0 — the same file as v2.8.1 (not yet confirmed on a board)
SUIT_H10_Binary_20260820.zip KIT H10 firmware — same file as v2.6.0
SUIT_ContentsFiles_20260820.zip H10 content files — same file as v2.6.0

This change applies to Rev 2.0 only. If you use Rev 1.1, keep using the Rev1.1.zip (v2.8.1) you already have.

Extract to a short path (e.g. C:\xm10\). A deep path can hit the Windows path-length limit and break the build — that is a path-length problem, not a code problem.