04 — Receiving Data on Your PC — the xm10 Tool¶
A tool for plotting as a live graph, saving raw, and exporting to CSV the data your board streams out over USB. It lives in the Extension_Module/pc-data-tool/ folder — run it directly with Python, or bundle it into a single executable for a PC that doesn't have Python installed.
Where is it? When you unzip the SDK,
Extension_Module\pc-data-tool\is included. If your ZIP doesn't have that folder (an older release), grab just thepc-data-tool/folder from the GitHub repo and put it anywhere — it's the same files regardless of board revision. In the v2.8.0 ZIP this folder was namedPythonDecoder— it is an earlier version of the same tool.How is this different from PhAI Studio? PhAI Studio is the official tool you use straight from the browser. This is an open-source, local tool. It saves the raw bytes before interpreting them, so you can re-export later even if the channel layout changes, and if you know Python you can modify it however you like. The two tools can't have the same COM port open at the same time — connect only one PC program at a time.
Use this tool to see your own structs. PhAI Studio shows the Total Data (0x20) the board sends automatically. PhAI Studio is still under development, so for now use the
xm10tool to view and save your own (custom) data structs.
| What you want to see | Program to use |
|---|---|
System data the board sends automatically — joint angles, torque, etc. (Total Data 0x20; to fill the 28-byte User_Custom block inside it, see xm_api_user_custom) |
PhAI Studio |
Your own struct sent with XM_SendUsbDataWithId (0xF0–0xFE) — on Rev 2.0, also structs sent with struct logging (XM_LOG_SEND) |
the xm10 tool (this page) |
Text sent with XM_SendUsbDebugMessage |
a serial terminal |
| Files the board saved by itself to a USB memory stick 🟢 Rev 2.0 only | XM10 Log Decoder 2.2.0 or later (Save Data to a USB Memory Stick) |
5-Minute Demo — No Board Required¶
You can watch the whole flow once even without a board yet.
cd Extension_Module/pc-data-tool # inside the unzipped SDK folder
pip install pyserial pyqt5 pyqtgraph numpy # first time only
python xm10.py demo
This feeds data made to look like it came from a real board into the actual receive code, saves it, reads it back, exports it to CSV, and self-checks 17 items. You should see this at the end:
데모 통과 — 17/17 항목. 산출물:
demo_out\demo.xmlog
demo_out\csv\demo_total_0x20.csv
demo_out\csv\demo_user_0xF0.csv
...
Open demo_out\csv\demo_user_0xF0.csv. The first three columns (pc_time_us receive time in µs since 1970-01-01 UTC, seq_id receive sequence number, activation_id the number of the struct description that was valid when the row was received — 0 means "the description hadn't arrived yet", as in the first few rows, which are still decoded with the description that arrived right after) are receive metadata added by the PC; after those come state, contact, emg_rms, angle_x10, tick, torque[0], torque[1] — the field names defined in the board-side C struct became the CSV columns, unchanged. That's exactly what this tool is for.
Connecting a Board¶
1. Find the Port¶
포트 3개:
COM6 0483:5740 USB 직렬 장치(COM6) <- XM10 후보
COM11 - 표준 Bluetooth에서 직렬 링크(COM11)
COM12 - 표준 Bluetooth에서 직렬 링크(COM12)
If no XM10 candidate shows up, check the cable and the board's power. Seeing only Bluetooth virtual ports is common — those aren't the XM10.
2. View as a Graph¶
A window opens. Pick your Port in the top row and click Connect — the board's user channels start plotting live. If you registered names with XM_SetUsbCustomMeta, as in Example 09, those names show up in the graph titles.
Connect only one PC program at a time. Rev 2.0 (v2.9.0 or later) sends the channel names again every time the COM port is opened, so if they are missing, click Disconnect and then Connect again. (not yet confirmed on the board)
Rev 1.1 (v2.8.1) sends the channel names only once, when USB connects. If they are missing, re-plug the USB cable and connect again.
If you send user channels under several Module IDs (0xF0, 0xF1, …), each Module ID gets its own tab. Every tab has its own graphs and its own saved file, and a tab you aren't looking at keeps saving.
| Button | What it does |
|---|---|
| Refresh | Re-read the port list — if you opened the window before plugging in the board, click this or it won't show up |
| Connect / Disconnect | Connect / disconnect |
| Freeze | Pause the graph only (saving keeps going) |
| Output | Folder where files are saved (default data/) |
| Save .xmlog | Save the received bytes as-is (on by default — see below) |
| Show 0x20 tab | A tab that shows Total Data (0x20) as a table (name + latest value, refreshed 5 times a second) instead of a graph — off by default |
| Screenshot | Save the current view as a PNG |
When you disconnect, the status bar at the bottom shows how many frames were received and which file they were saved to.
3. Saved Files¶
Each time you connect, two kinds of files show up in the Output folder.
| File | What it is |
|---|---|
cdc_phai_<timestamp>_user_0xF0.csv |
The user channels plotted on screen, as a table (opens directly in Excel). One file per Module ID |
cdc_<timestamp>.xmlog |
Every frame received, raw bytes (explained below) |
Why save a second file when the CSV should be enough? The CSV is already interpreted at the moment it's received. If the channel names arrived after the data, or you changed the channel layout afterward, or there was a mistake in interpretation, that CSV is final — you're stuck with it. The .xmlog is laid down without any interpretation, so you can always re-export it later. It also includes the system channels that are not plotted as graphs (the 0x20 Total Data, 197 channels — turn on Show 0x20 tab to see them as a table).
4. Export CSV from .xmlog¶
python xm10.py export data/cdc_20260910_143000.xmlog # summary of what happened
python xm10.py export data/cdc_20260910_143000.xmlog --csv out/ # one CSV per channel
CSV:
cdc_..._total_0x20.csv 12000 rows [generated-map] v2.8 ...
cdc_..._user_0xF0.csv 12000 rows [0xEF] JSON 메타 4채널 ⚠ 타입 미상 — float32 가정
total_0x20 is the 197 channels the board always sends (joint angles, torque, IMU, GRF, etc.), and user_0xF0 is the channel you sent from your own example code. The channel names the board sent while you were recording are stored inside the file itself, so there's nothing extra to configure when exporting.
Each row is decoded with the channel description that was valid when the row was received (a row received before any description is decoded with the first one that arrives afterwards on the same connection). If the struct description for a Module ID changes partway through a single file (for example, a script that keeps writing into one file across a reconnect — the graph window opens a new file for every connection), the CSV for that Module ID is split per description — ..._user_0xF0.csv, ..._user_0xF0_schema2.csv — because the same column name can mean something different (the same four bytes are 1.0 as a float but 1065353216 as a uint32_t).
If you connect again, the graph window opens a new .xmlog. (A .xmlog recorded without channel names exports as ch0, ch1, … with values assumed to be float. The name-only method (XM_SetUsbCustomMeta) does not send types. To send types as well, see "Sending Your Own Data (Board Side)" below.)
Console-Only Mode¶
When you don't need the graph window, or you're leaving it running for a long time, console mode is lighter.
python xm10.py recv --cli --port COM6 --log # saves CSV + .xmlog, Ctrl+C to stop
python xm10.py recv --cli --port COM6 --log --total-data # also exports the 0x20 (197-channel) data
Checking for Dropped Frames — soak¶
At 1 kHz, 30 minutes is 1.8 million frames. There's no way to eyeball whether even one of them got dropped, so there's a command that judges it for you automatically.
When it finishes, it reports PASS / FAIL with the reason — frames the board dropped, corrupted frames, missing sequence numbers, frames per second, even whether the board restarted partway through. Run this once during initial setup, after swapping a cable, and after adding channels.
Sending Your Own Data (Board Side)¶
cdc_stream.c in Ex.09 CDC Stream is the reference. You need three pieces — one struct, one line to register names, one line to send. The code below is taken from that file as-is.
/* The struct holding the values to send — all float; field order = channel order */
typedef struct {
float is_connected;
float left_hip_angle;
float right_hip_angle;
float forward_velocity;
} UserDebugData_t;
static UserDebugData_t s_debug;
/* Control_Setup — register channel names (once). Entry order = struct field order */
XM_SetUsbCustomMeta(0xF0,
"[{\"name\":\"H10 Connected\",\"unit\":\"bool\"},"
"{\"name\":\"Left Hip Angle\",\"unit\":\"deg\"},"
"{\"name\":\"Right Hip Angle\",\"unit\":\"deg\"},"
"{\"name\":\"Forward Velocity\",\"unit\":\"m/s\"}]");
/* Control_Loop — fill in values and send (every tick) */
XM_SendUsbDataWithId(&s_debug, sizeof(s_debug), 0xF0);
Five rules to follow:
- The struct must be
float-only — the name-registration mechanism doesn't send types along with it, so the PC reads every field as a float. If a singleuint32_tsneaks in, that column comes out as garbage numbers. - JSON entry count = struct field count, in the same order — even a mismatch of one shifts every column after it. The values still look like plausible numbers, so it's easy to miss.
- Module ID is
0xF0–0xFE— the rest are reserved for the system. - Only one name registration per connection — call
XM_SetUsbCustomMetatwice and only the last one survives. If you have several channel groups, combine them under a single Module ID (Ex.41 sends its six IMUs under a single Module ID). - Keep the JSON at 1,007 bytes or less (Rev 2.0, v2.9.0 or later) — if it is longer,
XM_SetUsbCustomMeta()returnsfalse, the names are not registered, and the columns show up asch0, ch1, …(reopening the port or re-plugging the cable does not fix this). On Rev 1.1 (v2.8.1) the limit is 512 bytes, and a longer string is silently not sent.
The 24 SDK examples follow rules 1–3. Follow the same rules when you write your own.
🟢 Rev 2.0 only — The "struct field names and types become CSV columns" behavior you saw in the demo (
stateas an integer,contactas a bool, and so on) is already built on the PC side. Rev 2.0 (v2.9.0 or later) can use it through struct logging (XM_LOG_REGISTER·XM_LOG_SEND). The board sends the field names, units and types together, and thexm10tool opens a tab per struct and builds the graph and CSV columns from those names. (not yet confirmed on the board) Use thexm10tool that comes with the SDK (thepc-data-toolfolder). With the tool from an earlier release, some values may not appear or may appear wrong. See Struct Logging and Ex.44 CDC Struct Logging for how to use it. The method above (XM_SetUsbCustomMeta) and Rev 1.1 still send names only.
Building a Standalone Executable¶
To carry this to a PC without Python, bundle it into one file.
python build_exe.py # dist/xm10.exe — includes the graph window (~55.6 MB)
python build_exe.py --no-gui # console-only (~23.0 MB)
After building, it actually runs demo and the self-checks against that exe to confirm it works. The commands are the same as with Python — xm10.exe demo, xm10.exe recv, xm10.exe export ....
When Something Goes Wrong¶
| Symptom | What to check |
|---|---|
| XM10 doesn't show up in the port list | If the window was already open, click Refresh. Re-run xm10.py ports. Check the cable and power. See if a COM port appears in Device Manager. On Rev 2.0, the board does not connect to the PC while a USB memory stick is plugged into its USB-C port. Remove the stick, then connect the PC. (not yet confirmed on the board) |
| Registered two name sets but only one shows | Only one registration survives per connection — combine them under one Module ID |
| Connect fails | Make sure PhAI Studio or a serial terminal isn't already holding the same port |
Graph titles show ch0, ch1… |
Check that XM_SetUsbCustomMeta is called in Control_Setup, that the entry count matches the struct, and that the JSON is 1,007 bytes or less (512 on Rev 1.1) |
Names are registered but titles still show ch0, ch1… |
The names may have been missed when you connected. On Rev 2.0 (v2.9.0 or later), click Disconnect and then Connect again. (not yet confirmed on the board) On Rev 1.1 (v2.8.1), re-plug the USB cable and connect again. Connect only one PC program at a time |
| Values come out as garbage numbers | Check for a non-float member in the struct |
| Frames seem to be dropping | Judge it with xm10.py soak. Try sending fewer channels |
| Not sure the tool is installed correctly | python xm10.py selftest — self-check, no board needed |
What's in the Folder¶
pc-data-tool/
├── xm10.py ← Start here. demo / ports / recv / soak / export / selftest
├── build_exe.py ← Build the standalone executable
├── run_tests.py ← Development-time verification (a bit more than selftest)
├── README.md ← Internal structure, file-by-file notes
└── CDC/ ← The actual implementation, for when you want to modify it directly
What each file under CDC/ does is listed in the "파일 한눈에" (files at a glance) table in pc-data-tool/README.md. Normally you only need xm10.py.
Next: move on to Ex.07–09 (USB communication) in Tutorials.