Skip to content
Merged
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **Bundled firmware is now leader 1.2.7 / follower 1.2.17 (protocol V2.9).**
Both add time-shared exchange (`0x5E` leader, `0x5D` follower) so host and
gripper never transmit at the same time; an uplink frame that overlaps a host
byte loses the bytes in flight. **Host-breaking:** IMU/encoder `timestamp` moves
from DWT/250 (wraps every 17.18 s) to TIM2 1 MHz (wraps every 71 min 35 s); this
SDK already unwraps the new timebase, older host software does not. Images are
built with arm-none-eabi-gcc 10.3.1 from tc-gu-01 `8c059e9`; size, CRC32 and
notes are in `firmware/manifest.json`. Leader verified on the collector backpack
(0 bad uplink frames under load vs 17/7208 on 1.2.6 push). As before, replug the
leader's USB / power-cycle the follower's 24 V after flashing.

### Fixed

- **A lost follower exchange reply no longer loses IMU samples.** The follower
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ motor's own firmware, checked against the required minimum. It is read-only:

```bash
python python/examples/follower_status.py left
# [fw] FirmwareVersion(1.2.16.0)
# [fw] FirmwareVersion(1.2.17.0)
# [motor] MotorModel(RS00, id=1, from flash, t_max=14.0Nm, v_max=33.0rad/s)
# [motor fw] 0.0.3.32 (...) OK
```
Expand Down
8 changes: 4 additions & 4 deletions docs/FIRMWARE.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ inactive bank and uses the STM32H5 bank swap, so one build serves both banks.
```bash
python python/examples/ota_update.py \
third_party/firmware/tc-gu-01/build/master/tc-gu-01-master.bin \
left --target-version 1.2.6
left --target-version 1.2.7
```

**The two roles have independent version numbers** — at the time of writing the
leader is 1.2.6 and the follower 1.2.16, and neither is "behind" the other. Both
leader is 1.2.7 and the follower 1.2.17, and neither is "behind" the other. Both
carry the same control-UART receive fix, which lives in shared code.
The roles were briefly forced onto one number; that was dropped on 2026-09-24.

Expand All @@ -142,13 +142,13 @@ them apart. If a leader reports 1.2.6, check `g.diagnostics.uart_stats()`: the
fixed image answers with 44 bytes, so `rx_errors` / `rx_rearms` exist and are
meaningful; the old one answers 32 bytes and the SDK reads both as 0 -- which is
indistinguishable from a healthy zero. When in doubt, reflash
`tc-gu-01-master-1.2.6.bin` from `firmware/`; OTA compares no versions
the current leader image from `firmware/`; OTA compares no versions
(`--target-version` is informational), so reflashing the same number is fine.
Compare versions only within one role.

Note the build output keeps the Makefile's unversioned name
(`build/master/tc-gu-01-master.bin`), while the images released under
`firmware/` carry the version (`tc-gu-01-master-1.2.6.bin`). That is deliberate:
`firmware/` carry the version (`tc-gu-01-master-1.2.7.bin`). That is deliberate:
a build artifact is whatever you just compiled, a release is a specific version
someone may still be holding a copy of months later. **If you promote a local
build into `firmware/`, rename it and update `firmware/manifest.json` in the
Expand Down
11 changes: 8 additions & 3 deletions firmware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ part of this SDK.

| Image | Role | Version | Protocol | Size | CRC32 |
| --- | --- | --- | --- | --- | --- |
| `tc-gu-01-master-1.2.6.bin` | leader (SN ends **`m`**) | **1.2.6** | V2.6 | 118,300 B | `0x350393b5` |
| `tc-gu-01-slave-1.2.16.bin` | follower (SN ends **`s`**) | **1.2.16** | V2.8 | 167,544 B | `0x4a8cd360` |
| `tc-gu-01-master-1.2.7.bin` | leader (SN ends **`m`**) | **1.2.7** | V2.9 | 122,848 B | `0x19e881dd` |
| `tc-gu-01-slave-1.2.17.bin` | follower (SN ends **`s`**) | **1.2.17** | V2.9 | 180,100 B | `0xf019df36` |

The follower's RobStride motor runs its own firmware; its images are listed
under `motors` in the manifest (see [Motor firmware](#motor-firmware)):
Expand All @@ -23,6 +23,11 @@ Leader and follower firmware are **versioned independently**; different numbers
on the two halves of a pair are normal. The SDK refuses to drive a follower
below **1.2.5**.

> **1.2.7 / 1.2.17 change the IMU/encoder `timestamp` timebase** (DWT/250, wrapping every
> 17.18 s → TIM2 1 MHz, wrapping every 71 min 35 s). This SDK handles both; older host
> software that unwraps with the 17.18 s period will produce wrong times. Upgrade host
> software together with the firmware. Protocol: tc-gu-01 `docs/PROTOCOL.md` V2.9.

## Flashing

```bash
Expand Down Expand Up @@ -53,7 +58,7 @@ To verify an image file before flashing:

```bash
python -c "from xense.taccap import crc32_iso_hdlc
print(hex(crc32_iso_hdlc(open('firmware/tc-gu-01-master-1.2.6.bin','rb').read())))"
print(hex(crc32_iso_hdlc(open('firmware/tc-gu-01-master-1.2.7.bin','rb').read())))"
# -> 0x350393b5
```

Expand Down
32 changes: 16 additions & 16 deletions firmware/manifest.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
{
"images": {
"master": {
"file": "tc-gu-01-master-1.2.6.bin",
"version": "1.2.6",
"source_commit": "1336ebb",
"size_bytes": 118300,
"crc32": "0x350393b5",
"file": "tc-gu-01-master-1.2.7.bin",
"version": "1.2.7",
"source_commit": "8c059e9",
"size_bytes": 122848,
"crc32": "0x19e881dd",
"sn_suffix": "m",
"protocol": "V2.6",
"protocol": "V2.9",
"build": "local",
"toolchain": "arm-none-eabi-gcc (15:13.2.rel1-2) 13.2.1 20231009",
"note": "Leader line. Same fix as follower 1.2.11 in the shared receive path: the control UART (3 Mbaud, one interrupt per byte) stopped receiving for good after a single overrun, because the error callback never restarted reception -- the stream kept flowing while every command went unanswered. Reception is now restarted, and GET_UART_STATS reports rx_errors / rx_rearms. Verified on 0115m and 0116m (upgraded from 1.2.2): 15 min each of a 100 Hz encoder+IMU stream with a 100 Hz acknowledged command, ~87,000 commands, zero failures. AFTER FLASHING, unplug and replug the leader's USB cable."
"toolchain": "arm-none-eabi-gcc (15:10.3-2021.07-4) 10.3.1 20210621",
"note": "Leader time-shared exchange (STREAM_MODE_POLLED + LEADER_EXCHANGE 0x5E): the leader queues uplink records and only answers, so host and leader never transmit at the same time -- an uplink frame being sent while a host byte arrives loses the bytes in flight. Verified on the taccap-collector backpack (0029m/0030m): 0 bad uplink frames under full host load with ~20 Hz LED commands vs 17/7208 pushing on 1.2.6. HOST-BREAKING: IMU/encoder timestamp is now TIM2 1 MHz (low 32 bits, wraps every 71 min 35 s), no longer DWT/250 (17.18 s); upgrade host software together. Known: after a power-up the leader can stop answering ~0.5 s after the first stream start until the port is reopened (also on 1.2.6). AFTER FLASHING, unplug and replug the leader's USB cable."
},
"slave": {
"file": "tc-gu-01-slave-1.2.16.bin",
"version": "1.2.16",
"source_commit": "a7dc07f",
"size_bytes": 167544,
"crc32": "0x4a8cd360",
"file": "tc-gu-01-slave-1.2.17.bin",
"version": "1.2.17",
"source_commit": "8c059e9",
"size_bytes": 180100,
"crc32": "0xf019df36",
"sn_suffix": "s",
"protocol": "V2.8 + motion envelope",
"protocol": "V2.9 + motion envelope",
"build": "local",
"toolchain": "arm-none-eabi-gcc (15:13.2.rel1-2) 13.2.1 20231009",
"note": "Stops decoding the motor's echo frames as status. A RobStride motor takes an MIT motion frame whose byte 0 equals its own CAN ID for another command and answers on 0xFD with the frame's first four bytes and zeros in bytes 4-7; byte 0 is the target position's high byte, and the factory ID 127 (0x7F) is -0.098..0 rad -- a LEFT-hand RS00's closed end. 1.2.15 decoded each echo as status (temperature 0, torque -t_max, position clamped to the far end), the envelope clamp then dragged the target toward the open end and the jaw was thrown open: force-position never closed to 0.0 on 0089s. The real fix is the CAN ID (RS00 followers: left 17, right 18, written by the factory config tool); this image is the backstop. Carries 1.2.15 (RS00 open direction from the SN's side). Verified on 0089s (left) / 0094s (right). AFTER FLASHING, unplug the 24 V power cable for ~2 s and plug it back; the USB cable can stay in."
"toolchain": "arm-none-eabi-gcc (15:10.3-2021.07-4) 10.3.1 20210621",
"note": "Follower time-shared exchange (SLAVE_EXCHANGE 0x5D, queue 32), optional 1-100 Hz IMU with accel/gyro calibration and magnetometer sessions, uplink collision detect-and-resend, cold-start homing order fix. HOST-BREAKING: IMU timestamp is now TIM2 1 MHz (wraps every 71 min 35 s). Carries 1.2.16. AFTER FLASHING, unplug the 24 V power cable for ~2 s and plug it back; the USB cable can stay in."
}
},
"motors": {
Expand Down
Binary file removed firmware/tc-gu-01-master-1.2.6.bin
Binary file not shown.
Binary file added firmware/tc-gu-01-master-1.2.7.bin
Binary file not shown.
Binary file removed firmware/tc-gu-01-slave-1.2.16.bin
Binary file not shown.
Binary file added firmware/tc-gu-01-slave-1.2.17.bin
Binary file not shown.
10 changes: 5 additions & 5 deletions python/examples/ota_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
of the update, not as troubleshooting.

The released images ship in this repo under `firmware/`, and their filenames
carry the version (tc-gu-01-slave-1.2.16.bin). Prefer the role selectors below:
carry the version (tc-gu-01-slave-1.2.17.bin). Prefer the role selectors below:
they read the current filename from firmware/manifest.json, so they keep
working across releases, while a literal filename goes stale the next time the
firmware is bumped. A name you do pass resolves against firmware/ from any
Expand All @@ -43,7 +43,7 @@

# An explicit image, when you mean a specific build rather than the
# current release.
python python/examples/ota_update.py tc-gu-01-master-1.2.6.bin
python python/examples/ota_update.py tc-gu-01-master-1.2.7.bin

# The version sent to the firmware (it writes bank metadata and the
# post-install verification log with it) is taken from manifest.json by
Expand Down Expand Up @@ -167,7 +167,7 @@ def _gripper_role(eps) -> str:
def _default_firmware_for_role(role: str) -> str:
"""The shipped image for a role, named by firmware/manifest.json.

The filename carries the version (tc-gu-01-slave-1.2.16.bin), so it cannot be
The filename carries the version (tc-gu-01-slave-1.2.17.bin), so it cannot be
derived from the role alone — the manifest is the single place the current
release's filename is written, and `file` there is what this reads. That
also means the manifest is load-bearing for every role-selector and --all
Expand Down Expand Up @@ -363,7 +363,7 @@ def _resolve_firmware(path: str) -> Optional[str]:
"""Find the image whether `path` is relative to the cwd or to this repo.

The images ship inside this repo, but the repo is usually vendored as a
submodule of something else — so `firmware/tc-gu-01-master-1.2.6.bin`, the path
submodule of something else — so `firmware/tc-gu-01-master-1.2.7.bin`, the path
our docs print because it works from the SDK root, is not the path that
works from the parent repo's root. Rather than making every downstream
README carry its own prefix, accept both: the literal path first, then the
Expand Down Expand Up @@ -414,7 +414,7 @@ def _identify_image(fw_bytes: bytes):
"""Look up the CRC32 in firmware/manifest.json; return (role, meta) or (None, None).

**Identify by content, not by file name.** Released image file names carry
the version (tc-gu-01-slave-1.2.16.bin), but file names can change — copy
the version (tc-gu-01-slave-1.2.17.bin), but file names can change — copy
it, rename it, download it from somewhere else, and the name starts lying.
CRC32 does not: it identifies these exact bytes.

Expand Down
Loading