From 98c012a9e0e46952d8fedbbc60766561921f4fcc Mon Sep 17 00:00:00 2001 From: Waheed Brown Date: Thu, 3 Sep 2026 20:45:47 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20add=20AGENTS.md=20=E2=80=94=20guidance?= =?UTF-8?q?=20for=20coding=20agents=20driving=20DEEPRobotics=20robots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a root AGENTS.md (and the matching AGENTS_CN.md) recording the SDK behaviours that a coding agent has to know before it writes a command, and that produce no error when they are got wrong. Structure is a common section that should hold for any DEEPRobotics robot, then one section per product, so further products can be added without reorganising the file. Starts with DR02 (Pro and Std) and Lite3. Content is of two kinds and is marked as such throughout: values quoted from files in this repository, which can be checked in a diff; and observations measured on hardware, each carrying the date it was taken. Nothing was verified on a DR02 Std, and that is said where it applies. Also adds a short pointer to the file from both READMEs. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 957 +++++++++++++++++++++++++++++++++++++++++++++++++++ AGENTS_CN.md | 792 ++++++++++++++++++++++++++++++++++++++++++ README.md | 4 + README_CN.md | 4 + 4 files changed, 1757 insertions(+) create mode 100644 AGENTS.md create mode 100644 AGENTS_CN.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c501b3f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,957 @@ +# AGENTS.md — instructions for a coding agent working with this SDK + +**English** | [简体中文](AGENTS_CN.md) + +Read this before writing code that talks to a DEEPRobotics robot. It is written to be +followed literally. + +This SDK commands machines heavy enough to injure someone. The rules below are not style +preferences. **Each one is here because breaking it produces a robot that does the wrong +thing without any error message.** That is the shape of almost every entry in this file: not +a crash, not an exception, not a rejected message — a topic that lists, reports a type and +delivers nothing; an array that is accepted in full and moves a joint you did not mean; a +command that is published, received, and quietly ignored. + +Those failures are expensive for a coding agent specifically. An agent reads a successful +return value as success, and there is nothing in the return value to read. **When a robot +does not do what you commanded, the first hypothesis to test is not that your value was +wrong. It is that your command never took effect.** + +--- + +## Contents + +**Common to every robot** + +- [1. Scope and provenance — how much to trust this file](#1-scope-and-provenance--how-much-to-trust-this-file) +- [2. The five that cost the most](#2-the-five-that-cost-the-most) +- [3. Common rules — every DEEPRobotics robot](#3-common-rules--every-deeprobotics-robot) +- [4. Transport: the failures that produce no error](#4-transport-the-failures-that-produce-no-error) + +**Per robot** + +- [5. DR02 (Pro and Std)](#5-dr02-pro-and-std) — + [modes](#51-developer-modes-decide-what-your-program-can-do-at-all) · + [arm control while walking](#52-arm-control-while-the-robot-still-walks-and-balances) · + [zero gain](#53-the-legs-are-kept-by-zero-gain-not-by-not-naming-them) · + [gains](#54-the-gains-are-a-table-and-so-is-the-damping) · + [variants and indices](#55-read-the-document-for-the-variant-in-front-of-you) · + [`/STEER`](#56-steer-preconditions-and-a-header-finding-we-cannot-fully-explain) · + [feedback](#57-feedback-joints_data_10hz-decoded-where-joints_data-did-not) · + [sensors](#58-sensors) · [build](#59-build) +- [6. Lite3](#6-lite3) — + [what carries across](#61-what-carries-across-from-3-and-what-does-not) · + [balance controller](#62-taking-the-low-level-sdk-means-taking-the-balance-controller) · + [two velocity interfaces](#63-there-are-two-velocity-interfaces-and-the-wrong-one-fails-silently) · + [sign-only velocity](#64-a-sign-only-velocity-mapping-silently-invalidates-every-speed-measurement) · + [dead sender](#65-two-command-interfaces-two-different-answers-to-a-dead-sender) · + [state frames](#66-the-state-stream-check-the-frame-length-and-where-it-is-being-sent) · + [addressing](#67-two-robots-one-address-never-default-to-another-robots-ip) · + [the stop](#68-a-network-change-can-silently-disable-the-emergency-stop) · + [RGB only](#69-rgb-only-what-the-robot-cannot-see) · + [mode authority](#610-mode-authority-stays-with-the-operator) + +**Extending this file** + +- [7. Adding another robot to this file](#7-adding-another-robot-to-this-file) + +--- + +## 1. Scope and provenance — how much to trust this file + +This file has two kinds of content and they should be trusted differently. + +**Quoted from this repository.** Joint index tables, gain arrays, mode numbers, motion-state +values and topic names are cited to the file they come from, in this repository, and can be +checked in a diff. If a vendor document and this file ever disagree, **the vendor document is +right and this file is stale** — fix this file. + +**Measured on a robot.** Everything else was observed on hardware and is marked with the +date. Two bodies of observation feed this file: + +| Marker | What it was | How much of it there is | +| --- | --- | --- | +| *[measured 2026-09-03]* | A one-day DR02 Pro bring-up session | One robot, one afternoon, one network | +| *[measured 2026-08/09]* | Integration work on two Lite3 Venture units, August–September 2026 | Two robots, several sessions, and a stack that is not the vendor's | + +Each such statement gives the observation rather than a conclusion, so that it can be re-run +and disagreed with. + +**Read the second kind as a place to look, not as a specification.** One robot on one day is +not a population. Several of these findings involve robot-side software that is outside this +SDK's documented surface — the DDS profile on the robot image, a `10 Hz` feedback topic, an +RTSP camera stream — and the SDK could not reasonably have anticipated them. They are +recorded here because an agent *will* meet them and will otherwise spend hours attributing +them to its own code. + +**Nothing here has been verified on a DR02 Std.** Where a Std statement appears, it is quoted +from `src/dr02_std/` and is labelled as unverified on hardware. + +--- + +## 2. The five that cost the most + +If you read nothing else, read these. Each is expanded below. + +| # | The trap | Where | +| --- | --- | --- | +| 1 | **Arm work requires Upper-Body Joint Control Mode.** High-Level mode's `/ACTION` vocabulary is canned social gestures with no grasp, lift or carry, and modes cannot be switched from inside a running program | [§5.1](#51-developer-modes-decide-what-your-program-can-do-at-all) | +| 2 | **The legs are kept by ZERO GAIN, not by not naming them.** `/JOINTS_CMD` is full length; a position entry with a non-zero `kp` is an active command that fights the walking policy | [§5.3](#53-the-legs-are-kept-by-zero-gain-not-by-not-naming-them) | +| 3 | **Gains are a table, and so is damping.** The Pro example uses `2800` on `waist_x_joint` and `90` on a wrist — a factor of thirty-one inside one array. `kd` is quoted, not derived from `kp` | [§5.4](#54-the-gains-are-a-table-and-so-is-the-damping) | +| 4 | **Read the document for the variant in front of you.** `dr02_pro/` and `dr02_std/` are siblings with different joint counts, arm degrees of freedom and gains, and neither says which robot you are holding | [§5.5](#55-read-the-document-for-the-variant-in-front-of-you) | +| 5 | **A velocity command is renewed, not set.** And on the wire, several things are silently discarded rather than rejected | [§3.5](#35-a-velocity-command-is-renewed-not-set), [§4](#4-transport-the-failures-that-produce-no-error) | + +--- + +## 3. Common rules — every DEEPRobotics robot + +These apply whatever robot is in front of you and are the parts of this file least likely to +go stale. + +### 3.1 Identify the robot and the variant before you write a line + +Product directories in this repository are siblings with the same filenames. A document is +correct for exactly one product and says nothing about the others, and **no document tells +you which robot is in front of you.** Copying a value across the boundary is not caught by +anything: the message formats are shared, the topic names are shared, and only the numbers +differ. + +Establish the variant **by measurement**, not by assumption, and make the answer explicit in +your code. On a DR02 the joint count is the discriminator — see [§5.5](#55-read-the-document-for-the-variant-in-front-of-you). +Refuse to run when the measurement matches no known variant rather than guessing the nearer +one. + +### 3.2 Developer Mode is a mode of the robot, not a flag in your program + +Entering Developer Mode is an operator action on the gamepad, or a service call, and it +changes what the robot's own controller is doing. Two consequences that a program cannot +see: + +- **After entering any Developer Mode, perception, localisation and obstacle avoidance are + disabled** (`src/dr02_pro/docs/DEVELOPER_MODE.md`). The robot is no longer watching for + people. Whatever your code does is then the only thing that is. +- **The modes cannot be switched directly.** Changing mode means stopping your program, + fully exiting the current mode, and driving the gamepad menu. Do not design a run that + needs a mid-run mode change; it is an operator sequence, not an API call. + +Never write a program that assumes it is in a mode. Read the mode, and refuse to publish if +it is not the one you need. + +### 3.3 Never write a joint index as a literal + +```python +cmd[6] = 0.4 # FORBIDDEN +targets = {"left_elbow_joint": 0.4} # correct +``` + +`/JOINTS_CMD` carries a bare array with **no model field**. A wrong index is not rejected and +logs nothing — the robot simply moves a different joint. On a DR02, index 9 is a wrist on a +Pro and a **hip** on a Std ([§5.5](#55-read-the-document-for-the-variant-in-front-of-you)), +which means the same literal that nudges an arm on one robot commands a leg the walking +policy is standing on, on the other. + +Address joints **by name**, against an index table selected by the measured variant. Keep +exactly one place in your code where a name becomes an index, and let everything else pass +names. + +### 3.4 Gains are tables. Quote them; never derive them. + +Do not invent a gain, do not reuse one joint's gain for another, and do not compute `kd` from +`kp`. Copy the vendor array for the variant you are on, verbatim, from the example that ships +in this repository, and cite the file you copied it from in a comment. If a joint you need is +not in that array, that is a question for the vendor, not a value to interpolate. + +A single number applied across a whole array is the specific failure this rule exists to +prevent, and it is not a small error: see [§5.4](#54-the-gains-are-a-table-and-so-is-the-damping). + +### 3.5 A velocity command is renewed, not set + +A velocity intent is not a setting. It has to be re-published on every tick of a live control +loop, and your loop must publish zeros when it has nothing to say rather than falling silent. + +This is not a quirk to work around — it *is* the deadman, and it is the right design. It +means a control loop that has died, blocked or lost its link stops the robot instead of +leaving a velocity latched. Write the loop so that stopping the sender is a stop. + +```python +# WRONG — sets the intent once, then spins +commander.set_velocity(x=0.35) +while time.monotonic() < end: + commander.tick() + time.sleep(period) + +# RIGHT — the intent is renewed every tick +while time.monotonic() < end: + commander.set_velocity(x=0.35) + commander.tick() + time.sleep(period) +``` + +**Silence is not a stop.** To stop, publish a burst of zeros — not one message, and not +nothing. + +**And establish, per robot and per interface, what happens when the sender dies. Do not +assume, and do not carry the answer across.** The two we have looked at differ, and they +differ in the direction that matters: + +| Interface | What happens when the sending process is killed outright | +| --- | --- | +| DR02 `/STEER` *[measured 2026-09-03]* | The robot came to rest on its own — but travelled roughly a further 5 cm over about 5 seconds. Judged by eye; re-measure rather than quoting these numbers. Stopping distance is not zero. | +| Lite3 legacy velocity UDP *[see §6]* | The command **latches**. There is no receiver-side timeout on that interface, so a killed sender leaves the robot walking with nothing left to stop it. | + +So the standard advice "prove the deadman with `SIGKILL`, not `Ctrl-C`" is correct on a DR02 +and **actively dangerous as a habit on a Lite3's legacy interface**, where `SIGKILL` is the +one thing that guarantees your zero is never sent. Find out which case you are in on a gantry, +with a person on the stop button, before it matters. Where the interface latches, send zeros +in a cleanup path *and* keep the operator's stop as the real answer. + +### 3.6 Check the payload, not the request + +**This is the general lesson of the two falls behind this file, and it outlives any one +robot.** A guard that inspects what the caller *asked for* is not a guard on what leaves your +process. + +`/JOINTS_CMD` is full length. Every joint gets an entry whether you named it or not, so a +seven-joint arm request becomes a 31-entry array — and the twenty-four entries you did not +write are the ones that dropped a robot ([§5.3](#53-the-legs-are-kept-by-zero-gain-not-by-not-naming-them)). +The check has to run **after** the expansion, on the array itself, per index. + +Anything that expands a caller's intent into a wider structure — a full-length array, a +broadcast, a batch write — needs its safety check on the expanded thing. Assert on the +payload. Test on the payload. + +### 3.7 Never wrap a publish in a bare `try`/`except` + +A publish that fails while your node is running is **a stop that did not happen**. It must +propagate and it must be visible. Swallowing it converts a transport fault into a robot that +keeps moving while your logs look clean. + +There is one defensible exception — skipping the publish when the ROS context is already torn +down, because it is then genuinely impossible. Write that one narrowly and do not generalise +it. + +### 3.8 Never weaken a safety constant to make something work + +Timeouts, stop-repeat counts, staleness limits, confidence thresholds and drift thresholds +are load-bearing. If one fires too often, **measure the real rate and report the mismatch**; +do not raise the limit until it stops complaining. A threshold that has been tuned until it +is quiet is a threshold that no longer protects anything. + +The same applies to a failing test. A failing test is information about the code or about the +world. Deleting it, loosening its assertion or skipping it produces a green suite that means +nothing. + +### 3.9 Nothing in your program is an emergency stop + +The red stop button on the gamepad is the emergency stop, and a person whose only job is +holding it must be present for every run on a real robot. Software that publishes a stop can +only stop the robot while the software is still running — which is exactly the condition that +fails first. + +Also, from this repository's own safety notes, and worth restating because a program cannot +enforce them: + +- Only **one** `/JOINTS_CMD` publisher may run at a time. Running the state machine and a + joint example together means two processes fighting for the robot. +- Test with a reliable safety suspension in place, and know that **suspended and standing are + different states with different safe transitions** — the vendor requires both feet firmly on + the ground before entering `RLControl`, and warns that switching while suspended may cause + sudden motion. + +--- + +## 4. Transport: the failures that produce no error + +**Scope: this section is about the ROS 2 / DDS interface**, so it applies to the DR02 family +and to anything else using the vendor's `drdds` messages. The Lite3 speaks UDP and has its own +equivalents in [§6](#6-lite3). + +Everything below is a way for a message to be published successfully and have no effect, or +for a topic to exist and deliver nothing. Check these **before** you start doubting your +control logic — on the day this file records, most of the lost time went to debugging correct +code. + +### 4.1 Source the robot's DDS bootstrap first, in every shell, on every host + +*[measured 2026-09-03]* On the DR02 Pro used for this session the robot image carried a DDS +profile script that had to be sourced **before** the ROS 2 environment: + +```bash +set +u +source /opt/robot/scripts/setup_ros2.sh # sets FASTRTPS_DEFAULT_PROFILES_FILE +source /opt/ros/humble/setup.bash +``` + +It points `FASTRTPS_DEFAULT_PROFILES_FILE` at the robot's Fast DDS profile, which carries a +transport whitelist containing the host's own address. **Without it, several robot topics +list, report a type, and deliver zero bytes** — which is indistinguishable from a dead sensor. +Sourcing it took one topic from `0` bytes to `1219` bytes per message with no other change. + +`set +u` is required because the ROS setup scripts read undefined variables and abort under +`nounset` before ROS is reached. + +The exact path is robot-image specific and is not part of this SDK; look for the equivalent +on the machine in front of you and source it. If topics list but deliver nothing, this is the +first thing to check. + +### 4.2 Match the vendor's QoS: publishers are BEST_EFFORT + +*[measured 2026-09-03]* The robot's publishers are BEST_EFFORT. **A RELIABLE subscriber never +connects to them, and nothing is logged on either side.** Your subscriber shows zero messages +and every diagnostic looks healthy. + +```bash +ros2 topic echo /SOME_TOPIC --once --qos-reliability best_effort +``` + +In code: subscribe BEST_EFFORT to vendor topics. Note also that `ros2 topic hz` and +`ros2 topic bw` subscribe RELIABLE and **do not accept `--qos-reliability`** on Humble, so +they report zero on every best-effort topic. Only `echo` takes the flag. A zero from `hz` is +not evidence of a silent topic. + +### 4.3 Judge a topic by bytes, not by publisher count + +*[measured 2026-09-03]* Robot-side publishers appear as bare DDS applications rather than ROS +nodes, so ROS tooling does not count them. One topic reported `Publisher count: 0` while +delivering 1219 bytes in the same second. `ros2 node list` will not show them either. + +The converse also misleads: **two publishers on a topic does not mean two robots.** Compare +the first 12 bytes of each endpoint GID — if the participant prefixes match, it is one process: + +```bash +ros2 topic info --verbose /JOINTS_DATA +``` + +### 4.4 The `name` field is reserved and arrives empty + +`src/dr02_pro/docs/JOINT_CONTROL.md` states that `name` is a reserved field and that SDK +applications should not rely on it. *[measured 2026-09-03]* On the robot, all 31 `name` +entries arrived empty. + +Take this literally when designing: **the robot will not tell you what its joints are +called.** The name-to-index map comes from the vendor table, selected by the measured joint +count. That is why the count is the first thing to measure. + +### 4.5 One robot can carry two hosts, two ROS distributions, and an asymmetric topic view + +*[measured 2026-09-03]* On this robot the control host ran Ubuntu 24.04 / ROS 2 Jazzy and the +perception host ran Ubuntu 22.04 / ROS 2 Humble, on one DDS domain. They saw each other's +topics, **but not symmetrically**: joint topics were usable only from the perception host, and +the LiDAR topic delivered data only from the control host until §4.1 was applied. + +Two consequences. Build and install the message package for the distribution on the host you +are building on. And **check a topic from the host you intend to use it from** — a topic +listing on the other host proves nothing about yours. + +### 4.6 If your program runs off-robot, its clock is not the robot's clock + +*[measured 2026-09-03]* See [§5.6](#56-steer-preconditions-and-a-header-finding-we-cannot-fully-explain). +A message that carries a timestamp from a host whose clock disagrees with the robot's is a +candidate explanation for commands that are received and ignored. This is a hypothesis, not +an established mechanism, but it is cheap to rule out: synchronise the clocks, or reproduce +the failure from a robot-side host and see whether it persists. + +--- + +## 5. DR02 (Pro and Std) + +Applies to `src/dr02_pro/` and `src/dr02_std/`. Everything marked *[measured 2026-09-03]* was +observed on a **DR02 Pro**; the Std statements are quoted from `src/dr02_std/` and have not +been verified on hardware. + +### 5.1 Developer Modes decide what your program can do at all + +**Choose the mode before you choose the approach.** Three modes exist +(`src/dr02_pro/docs/DEVELOPER_MODE.md`), and for most SDK work only two of them are usable: + +| Mode | `set_mode` value | What the SDK owns | What the robot owns | +| --- | --- | --- | --- | +| **High-Level Motion Control** | `3` | `/MOTION_STATE`, `/GAIT`, `/STEER`, `/ACTION` | **all joints** | +| **Whole-Body Joint Control** | `2` | every joint, via `/JOINTS_CMD` | nothing — including balance | +| **Upper-Body Joint Control** | `1` | waist and both arms, via `/JOINTS_CMD` | the legs, under its own policy | + +**High-Level mode cannot manipulate anything.** Its `/ACTION` interface takes a single +integer and its entire vocabulary is canned social gestures — twelve on the Pro +(`0x3000` `greeting` … `0x300b` `clap`) and five on the Std (`0x3000`–`0x3004`). There is no +grasp, no lift and no carry, and in High-Level mode the robot's internal policy owns the +joints. **If your task involves the arms doing anything other than a preset gesture, High-Level +is the wrong mode and no amount of work in it will get you there.** + +**Whole-Body mode is not a shortcut to arm control either.** It hands you the legs as well, +which means you are now responsible for balancing a standing humanoid. This repository warns +directly that running an upper-body example in Whole-Body mode may cause the legs to lose +support and the robot to fall, because the leg joints receive no valid command. + +**So arm work requires Upper-Body Joint Control Mode.** That is the whole of +[§5.2](#52-arm-control-while-the-robot-still-walks-and-balances). + +**Modes cannot be switched from inside a running program.** They cannot be switched directly +at all: you stop the SDK program, confirm its publishers have stopped, fully exit the current +mode, and re-enter through the gamepad menu. Design the run as a single mode from start to +finish. A plan that says "walk in High-Level, then switch to Upper-Body and grasp" is a plan +with an operator, a gamepad and a full stop in the middle of it — and if the robot must stay +standing across that boundary, it does not work at all. + +Read the current mode from `/DEVELOPER_MODE_STATUS`, which publishes JSON such as +`{"Enabled":true,"Mode":2,"Frequency":500}`, and refuse to publish if it is not the mode you +need. + +### 5.2 Arm control while the robot still walks and balances + +**This is the most useful thing in this file.** It is the configuration in which an autonomous +manipulation task is actually possible on a DR02, and it needs no mid-run mode change. + +`/STEER` is accepted in **Upper-Body Joint Control Mode** as well as High-Level — this +repository's own example table lists `steer_example` under both +(`src/dr02_pro/docs/EXAMPLES.md`). So in one mode, with no switch: + +- the robot walks and balances on its **own leg policy**, driven by `/STEER`; +- your program owns the **waist and both arms** through `/JOINTS_CMD`. + +*[measured 2026-09-03]* In this configuration on a DR02 Pro, a single-arm reach tracked its +commanded angle to **0.11°** while the walking policy held the legs underneath it, and the +knees moved 0.07° and 0.57° across a 1076-frame, eight-second ramp with no abort. + +Two things make it work, and both are easy to get wrong: + +1. **The mode must be entered correctly.** Upper-Body mode entry expects the robot in the + suspended-standing posture, so being on a gantry is correct *for mode entry*. It is not + correct for `RLControl`, which the vendor requires be entered with **both feet firmly on the + ground**. These are different steps in the same sequence; do not collapse them. +2. **The legs must be given zero gain.** Not "left out" — zero gain. This is §5.3, and getting + it wrong is what dropped the robot. + +### 5.3 The legs are kept by ZERO GAIN, not by not naming them + +**This one dropped a robot. It is the least obvious thing in this file.** + +`/JOINTS_CMD` is a **full-length array**: on a Pro, all 31 entries are sent whether you named +the joint or not. The natural safe-looking choice is to fill the unnamed entries with the +robot's own measured positions — the robot is being told to stay exactly where it already is. + +**That is not the safe choice. A position entry with a non-zero `kp` is an ACTIVE command.** +The joint controller will hold that angle against anything — including against the robot's own +walking policy, which in Upper-Body mode is the thing keeping the legs underneath it. + +*[measured 2026-09-03]* On a DR02 Pro: a right-arm reach naming seven joints, with `kp=40`, +`kd=2` applied uniformly to all 31 entries, ramped over eight seconds. The arm moved a little +and **the legs collapsed.** A guard asserting that the caller had named only upper-body joints +passed, and could not have caught it — the legs were never named. They arrived in the array +anyway. + +**The gains are the guard.** This repository's own example already does the right thing +(`src/dr02_pro/low_level/arm_joint_example.cpp`): the array is full length, every one of the 31 +entries is populated, and **every index at or beyond the upper-body count carries `kp = 0` and +`kd = 0`.** + +``` +kUpperBodyKp[17..30] = 0.0f // legs and neck: present in the array, exerting no torque +kUpperBodyKd[17..30] = 0.0f +``` + +**Read that carefully, because the load-bearing part is the gain and not the position.** The +example writes `position = 0` into those leg entries, and that is harmless *only* because the +gains beside them are zero. The same entries with a non-zero `kp` would command every leg joint +to its calibrated zero at once. So: + +- **The value in an uncommanded entry is irrelevant while its gain is zero, and catastrophic + the moment it is not.** Whether you fill unnamed entries with `0` as the example does, or with + the robot's measured positions, the thing keeping the robot up is the zero gain. Assert on the + gains, per index, in a test. +- **Never treat position `0` as "no command".** `0` is the product's calibrated joint zero + (`src/dr02_pro/docs/JOINT_CONTROL.md`), which is a specific pose, not an absence of one. +- **A measured position re-read every tick follows; it does not hold.** *[measured + 2026-09-03]* If you fill unnamed entries from live feedback, they track rather than resist. + During a successful reach the torso leaned `waist_x` by `-0.177 rad` to counterbalance the + extended arm, because the waist was unnamed. That is harmless unloaded, and it means "return + to rest" has to command the whole upper body — all three waist joints included — or it leaves + the torso wherever it drifted. + +A leg-drift watchdog is worth having: snapshot every policy-owned joint at the start of a ramp +and abort if one moves too far. Its threshold is a judgement, not a constant to copy. *[measured +2026-09-03]* 0.05 rad tripped on a hip moving 2.9° that was almost certainly the policy +balancing correctly; 0.12 rad caught a fold without aborting on compensation. In Upper-Body +mode the hips and knees **should** move while an arm extends. + +### 5.4 The gains are a table, and so is the damping + +The authoritative source is **`src/dr02_pro/low_level/arm_joint_example.cpp`** for the Pro and +**`src/dr02_std/low_level/arm_joint_example.cpp`** for the Std. Copy from the file; do not +retype from here without checking. + +DR02 Pro, upper body (indices `0`–`16`): + +| Joints | `kp` | `kd` | +| --- | --- | --- | +| `waist_z_joint` | 600 | 6 | +| `waist_x_joint` | **2800** | **15** | +| `waist_y_joint` | 2300 | 20 | +| shoulders and elbows, both arms | 600 | 6 | +| wrists, both arms | **90** | **2** | +| legs and neck (`17`–`30`) | **0** | **0** | + +**There is a factor of thirty-one between two joints in the same array.** A waist asked to +hold a torso upright and a wrist are not remotely the same actuator, and there is no single +number that serves both. + +*[measured 2026-09-03]* A uniform `kp=40` across all joints folded the robot twice, and +produced **two symptoms that looked unrelated**: the waist sagged — 40 against a specified +2800, seventy times too little, and on a standing humanoid the legs follow the torso down — +while the arm "barely moved", because 40 was also pushing against a shoulder that wants 600. +One wrong number, two symptoms, and the second one sends you looking at the pose rather than +the gains. + +**`kd` is quoted, not derived.** With the gains corrected, `kd` was then scaled as `kp/20` — a +guess. Against the vendor's array that guess is up to **9.3× too high** (`waist_x_joint`: 140 +against a specified 15), and *[measured 2026-09-03]* the waist bent sideways during a two-armed +reach, on exactly the joint that was furthest out. Excessive derivative gain in a discrete +high-rate loop does not simply damp; it drives drift and oscillation. + +**There is no `kp`/`kd` ratio to discover here. Both are tables.** If you keep these values in +your own code, add a test that asserts the waist's ratio differs from a wrist's, so that a +future well-meaning refactor cannot re-derive one from the other. + +DR02 Std, upper body (indices `0`–`8`), quoted from `src/dr02_std/low_level/arm_joint_example.cpp` +and **not verified on hardware**: + +| Joints | `kp` | `kd` | +| --- | --- | --- | +| `waist_z_joint` | 600 | 6 | +| shoulders and elbows, both arms | 200 | 5 | +| legs (`9`–`20`) | 0 | 0 | + +Note that a Std shoulder is `200` where a Pro shoulder is `600`. **The tables are not +interchangeable in either direction.** + +### 5.5 Read the document for the variant in front of you + +**This is what caused both falls.** The gains that folded the robot came from +`dr02_std/low_level/arm_joint_example.cpp`; the robot was a Pro. + +`src/dr02_pro/` and `src/dr02_std/` are sibling directories with identical filenames and +different contents, and **neither says which robot is in front of you.** They differ in joint +count, arm degrees of freedom, gains, `/ACTION` vocabulary and available IMU topics. The +message types and topic names are identical, so nothing rejects a cross-variant value. + +**Measure the joint count first.** It is the discriminator, and it comes straight off the wire: + +```bash +ros2 topic echo /JOINTS_DATA_10HZ --once --qos-reliability best_effort | grep -c data_id +``` + +*[measured 2026-09-03]* Returned `31` on three independent samples → a Pro. **31 is a Pro, 21 +is a Std, and anything else means stop and ask** — do not round to the nearer variant. + +The two index tables, from `src/dr02_pro/docs/JOINT_CONTROL.md` and +`src/dr02_std/docs/JOINT_CONTROL.md`: + +| Group | Pro (31 joints) | Std (21 joints) | +| --- | --- | --- | +| Waist | `0`–`2` (`waist_z`, `waist_x`, `waist_y`) | `0` (`waist_z` only) | +| Left arm | `3`–`9` (7 DoF, incl. 3 wrist) | `1`–`4` (4 DoF, no wrist) | +| Right arm | `10`–`16` (7 DoF, incl. 3 wrist) | `5`–`8` (4 DoF, no wrist) | +| Left leg | `17`–`22` | `9`–`14` | +| Right leg | `23`–`28` | `15`–`20` | +| Neck | `29`–`30` (locked, not controllable) | none | +| Upper-body controllable range | `0`–`16` | `0`–`8` | + +**The two variants share 21 joint names, and 20 of them sit at different indices.** Only +`waist_z_joint` at index `0` agrees. Index `9` is `left_wrist_x_joint` on a Pro and +`left_hip_y_joint` on a Std. On a Std, the Pro's arm indices `9`–`16` are **all legs the +walking policy is standing on.** + +Because `/JOINTS_CMD` carries a bare array with no model field, a Pro-shaped command sent to a +Std is not rejected, logs nothing, and moves legs. + +### 5.6 `/STEER` preconditions, and a header finding we cannot fully explain + +`/STEER` is the correct topic for programmatic walking in both High-Level and Upper-Body modes. +`x`, `y` and `yaw` are **normalised ratios in `[-1.0, 1.0]`, not physical velocities** +(`src/dr02_pro/docs/EXAMPLES.md`), and `steer_example` uses a fixed `0.6`. + +**Precondition: the robot must already be standing in `RLControl` with a gait selected.** +Nothing reports the absence of this as an error — the publish succeeds, the topic shows +traffic, the subscribers show as connected, and the robot stands still. *[measured 2026-09-03]* +104 `/STEER` messages at 20 Hz with `x=0.15` produced no motion at all. + +Read the robot's actual state from **`/MOTION_INFO`**. `/MOTION_STATE` and `/GAIT` are +*command* topics and are silent — echoing them tells you nothing, which reads exactly like a +dead link. + +```bash +ros2 topic echo /MOTION_INFO --once --qos-reliability best_effort +``` + +`/MOTION_INFO` reports these in **decimal** while `src/dr02_pro/docs/EXAMPLES.md` lists them in +**hex**, which is its own small trap: `17` and `0x11` are the same state, and `131078` is +`SuspendedStand` rather than anything wrong. + +| State | Hex | Decimal | +| --- | --- | --- | +| `Idle` | `0x0` | 0 | +| `JointDamping` | `0x2` | 2 | +| **`RLControl`** | `0x11` | **17** — `/STEER` only means anything here | +| `SuspendedStand` | `0x20006` | 131078 | +| `HumanWALKAMP` (gait) | `0x21001` | 135169 | +| `HumanWALKTERRAIN` (gait) | `0x21006` | 135174 | + +**The order is: lower the robot, both feet firmly on the ground, then `RLControl`, then a +gait, then `/STEER`.** + +**Two open observations, offered as questions rather than claims.** + +*[measured 2026-09-03]* **Small magnitudes produced no motion.** `x=0.15` moved nothing; +`0.35` walked. `steer_example` uses `0.6`. Whether there is a deadband on the normalised input +was not established. + +*[measured 2026-09-03]* **`/STEER` was ignored until the header was left unset, and this cost +more time than anything else in the session.** With the robot in `RLControl` with a gait, feet +on the ground, QoS matched, no competing publisher, and `ros2 topic echo` showing the correct +values on the wire, **293 commands across three attempts produced no motion and no log line of +any kind.** A capture of the handheld controller driving the robot successfully showed every +message carrying `frame_id: 0` and an unset `stamp`. Publishing with `frame_id = 0` and +`header.stamp` left at its default moved the robot on the first attempt. + +Both fields were changed together, so **we cannot say whether the stamp, the frame id, or both +are responsible.** Our best guess is that the consumer validates the stamp against the robot's +own clock and discards what disagrees — and our publisher was on a development host whose clock +did. That would be consistent with `steer_example` working, since it does set both fields +(`msg.header.frame_id = frame_id++; msg.header.stamp = node->now();`) and is typically run +where the clocks agree. **We would value a correction here** — if the consumer's behaviour is +documented somewhere we did not find, this whole entry can be replaced with a link. + +Until then, the practical advice is: if `/STEER` is being received and ignored, try +`frame_id = 0` with an unset stamp before you go looking at gaits, deadbands and mode +arbitration — all of which were correct the whole time here. + +### 5.7 Feedback: `/JOINTS_DATA_10HZ` decoded where `/JOINTS_DATA` did not + +*[measured 2026-09-03]* Both topics carry `drdds/msg/Joints`. Over five attempts each, from +`ros2 topic echo` on the perception host: + +| Topic | Decoded | Joints | Rate | +| --- | --- | --- | --- | +| `/JOINTS_DATA` | **0 / 5** | — | — | +| `/JOINTS_DATA_10HZ` | **5 / 5** | 31 | 10.000 Hz | + +`/JOINTS_DATA` failed inside the DDS reader with `sequence size exceeds remaining buffer`, +before any ROS message conversion; `--raw` failed identically. It persisted after the DDS +bootstrap (§4.1), so it is not a transport or QoS problem, and it looks like a wire-format +disagreement between a native publisher and the ROS-generated IDL rather than anything a +subscriber can configure. + +**Reported honestly and with a caveat.** This was observed only through `ros2 topic echo` from +a Python-side subscriber. **We did not run this repository's own `arm_joint_example`, which +subscribes `/JOINTS_DATA` in C++**, so we cannot say whether it is affected, and it may well +not be. If it is not, then the difference between the two paths is itself worth knowing and we +would be glad to be told what it is. + +The workaround we used was to read `/JOINTS_DATA_10HZ`. **The cost is real and should be +stated:** the measured positions used for the unnamed entries of a `/JOINTS_CMD` array then +refresh every 100 ms rather than at the control rate. Note this at the constant in your own +code; it is a reduction in feedback bandwidth, not a free substitution. + +### 5.8 Sensors + +*[measured 2026-09-03]* These concern robot-image sensors rather than this SDK, and are +recorded because they change what a safety gate can promise. + +**The LiDAR is blind behind the robot.** A bearing histogram of one sweep in the body frame +showed returns from roughly −120° to +120° and **exactly zero beyond ±120°** in either +direction. That is the field of view, not a filtering artefact — the distribution was +symmetric about forward. The consequence is direct: **a proximity stop gate built on this +LiDAR cannot see anyone standing behind the robot.** Disable backward motion in code rather +than merely avoiding it, and do not let anyone stand in the rear cone expecting the robot to +detect them. During this session the operators sat 1.5–2 m behind the robot and produced no +returns at all. + +A self-return floor is needed regardless — a humanoid's LiDAR sees its own structure, and +returns closer than about 0.35 m were the robot itself. Without a floor the nearest range +always reads zero. + +**Cameras: the USB descriptor serial is not the camera's firmware serial.** +`src/dr02_pro/docs/CAMERA.md` already warns that the serial-to-position mapping cannot be +determined from the serials and must be established empirically — that warning is correct and +should be followed. Two things to add: + +- The serial in `/sys/.../serial` (a value like `350423023842`) is **not** the firmware serial + the driver wants (a value like `254622074040`). Launching with the wrong one **does not + fail** — the driver falls back to another device, so you silently open a different camera + than you asked for. Prefer `usb_port_id`, which is positional and stable across reflashes. +- The empirical method is weaker than it sounds. A downward-angled camera showing bare floor + looks much like any other downward-angled camera showing bare floor. **Someone who can see + where each unit is bolted outranks any inference from the images**, and a mislabelled rear + camera is a blind spot with a reassuring name on it — see the LiDAR note above for why the + rear view specifically matters. +- If a colour pixel is to index the depth image, `align_depth.enable:=true` is required. + Without it, sampling the depth image at a colour coordinate returns the range of whatever + is at that pixel in a **different** camera: a plausible wrong answer rather than an error. + +**An H.265 RTSP stream shows grey frames before it shows a picture.** The robot also serves a +forward-facing RGB camera over RTSP. Opened with default settings it connects, reports +1280x720, and returns a uniform grey frame that looks exactly like a covered lens. It is not: +the stream is H.265 and its parameter sets are sent infrequently, so no decoder can produce a +picture until one arrives, and UDP transport loses fragments while it waits. Set TCP transport +before the capture is opened, and **judge a frame by its variance, not by whether the read +succeeded** — the read returns success for the grey frames. Measured across one capture: frame +0 had `std=1.06`; from frame 20 onward `std≈68`. + +Also, the lens is strongly wide-angle — a straight ceiling beam renders as a pronounced arc. +That matters for fiducial detection, which fits straight-edged quads: barrel distortion can +defeat the quad test while the marker stays perfectly legible to a person. + +### 5.9 Build + +`src/dr02_pro/CMakeLists.txt` sets `BUILD_PLATFORM` to `x86` by default with no detection, and +`BUILD_PLATFORM` selects the `third_party/onnxruntime//` binaries. The robot-side +hosts are aarch64, so a bare `colcon build` there fails in the linker. `src/dr02_pro/docs/REAL_ROBOT.md` +gives the right invocation per host; if you are scripting it, derive the value from `uname -m` +rather than hardcoding a constant, so the same script works on a development host and on the +robot. + +`BUILD_SIM=ON` must not be enabled for real-robot builds. + +--- + +## 6. Lite3 + +**The Lite3 is not in this repository, and almost nothing above transfers to it.** Its +interfaces are the vendor's separate `Lite3_MotionSDK` and `Lite3_ROS` projects, and the +transport is **UDP datagrams to the motion host, not ROS 2 topics**. A coding agent that has +read [§5](#5-dr02-pro-and-std) and then meets a Lite3 has the wrong model of the machine in +almost every particular: no `/JOINTS_CMD`, no `/STEER`, no Developer Mode, a different joint +count, and a different answer to what happens when the sender dies. + +This section is here so that the boundary is explicit rather than discovered. + +**Provenance.** These observations come from two Lite3 Venture units during integration work +in **August–September 2026**, and are marked *[measured 2026-08/09]*. They are less complete +than the DR02 material and involve a stack that is not the vendor's: read them as places to +look. Where a value was never measured, that is said, because an unmeasured value is exactly +what this file exists to stop anyone inventing. + +### 6.1 What carries across from §3, and what does not + +**Carries across unchanged:** identify the unit before you write a line ([§3.1](#31-identify-the-robot-and-the-variant-before-you-write-a-line)); +never invent a value ([§3.4](#34-gains-are-tables-quote-them-never-derive-them)); check the +payload, not the request ([§3.6](#36-check-the-payload-not-the-request)); never wrap a send in +a bare `try`/`except` ([§3.7](#37-never-wrap-a-publish-in-a-bare-tryexcept)); never weaken a +safety constant ([§3.8](#38-never-weaken-a-safety-constant-to-make-something-work)); the +operator's handset is the emergency stop ([§3.9](#39-nothing-in-your-program-is-an-emergency-stop)). + +**Does not carry across:** every topic name, QoS note and joint table in +[§4](#4-transport-the-failures-that-produce-no-error) and [§5](#5-dr02-pro-and-std). And most +importantly the deadman behaviour — see [§6.5](#65-two-command-interfaces-two-different-answers-to-a-dead-sender). + +### 6.2 Taking the low-level SDK means taking the balance controller + +`Lite3_MotionSDK` exposes the twelve leg joints with position targets and gains. **Using it +replaces the vendor's own gait and balance controller** — the thing keeping the robot upright. +That is the same trade as DR02 Whole-Body mode ([§5.1](#51-developer-modes-decide-what-your-program-can-do-at-all)), +and the same answer applies: unless writing a balance controller is the actual task, stay on +the high-level velocity interface and let the vendor's policy have the legs. + +The corollary is a real limitation and should be planned around rather than worked around: +**motor temperatures are not exposed on the high-level interface at all.** Software on that +interface cannot see accumulated heat. Let the robot cool physically between runs, and cap the +duration of any run you cannot thermally monitor. + +### 6.3 There are two velocity interfaces, and the wrong one fails silently + +*[measured 2026-08/09]* The Lite3 accepts velocity commands on UDP port `43893` in two +different framings, and **which one works depends on the robot's current mode, with no +distinguishing symptom when you pick wrong:** + +- The **legacy velocity** framing (a fixed-size datagram carrying one `double` per axis) + requires the vendor's **autonomous mode**. A unit in AI Motion Mode cannot enter autonomous + mode. *[measured 2026-08]* A correctly formed `vx = 0.10 m/s` pulse arrived at the motion + host, `error_state` stayed `0`, and the **world-pose delta was zero.** Nothing reported a + rejection. +- The **simple-axis** framing (mode plus per-axis integer commands, driven at ≥20 Hz with a + documented 250 ms receiver-side timeout) is the one that moved a robot. + +**A packet that arrives and is accepted is not a packet that will be acted on.** As with +`/STEER` on a DR02 ([§5.6](#56-steer-preconditions-and-a-header-finding-we-cannot-fully-explain)), +the precondition lives in the robot's mode, and the failure is silence. Read the state stream +and confirm the mode before concluding your command was wrong. + +### 6.4 A sign-only velocity mapping silently invalidates every speed measurement + +*[measured 2026-08/09]* **This is a trap in the integration rather than in the robot, and it +is recorded because it is the Lite3 equivalent of the zero-gain trap: a whole class of +measurement that looks like it worked and measured nothing.** + +The vendor's axis interface carries an integer magnitude per axis, so it *can* express speed. +But a client can only send a magnitude it has evidence for, and if exactly one raw value per +direction has ever been observed to produce a known displacement, then that is the only value +the client can honestly emit. The mapping collapses to the **sign** of the requested velocity. +A request for 0.05 m/s and a request for 0.30 m/s then put the identical value on the wire, +and the robot walks at whatever speed that one primitive produces. + +None of that is wrong as a conservative design — inventing intermediate magnitudes would be +worse. **What is dangerous is forgetting it upstream**, because nothing downstream reports that +the requested magnitude was discarded. + +Three consequences that each produce a plausible wrong answer rather than an error: + +- **A speed ladder measures the ladder, not the robot.** Every rung above the dead zone walks + at the same speed, so the probe "passes" at its lowest rung and reports that rung as the + robot's minimum gait speed. The number describes the script. +- **A planner that assumes commanded velocity equals executed velocity commits the wrong + distance.** A policy creeping at 0.05 m/s validated 0.125 m of travel while the legs + actually covered 0.75 m in the same window. The robot goes where the primitive takes it, not + where the plan says. +- **A dead zone must be applied to the velocity vector, not per axis.** Gate `hypot(vx, vy)` + and snap to a bearing. Applied per axis, a diagonal request just below the forward dead zone + and just above the lateral one zeroes forward and fires a **full-speed 90° strafe**. + +Also: *[measured 2026-08/09]* on the vendor axis convention, **positive is right** for lateral +and yaw. If your navigation stack uses positive-left, invert exactly once, at the transport +boundary, and test it — a sign error here is a robot walking confidently the wrong way with no +diagnostic. + +**What was never measured, and must not be invented.** Forward and lateral speed ceilings, the +yaw rate in physical units, and the minimum gait speed were not established on these units. +Requiring these as explicit arguments with **no default** is the right design: an earlier +version silently defaulted them to *a different robot's* published figures, which is how a +number with no relationship to the machine in front of you ends up bounding its motion. + +### 6.5 Two command interfaces, two different answers to a dead sender + +*[measured 2026-08/09]* **The legacy velocity interface has no heartbeat and no receiver-side +timeout: a command latches until something explicitly sends zero.** The axis interface has a +documented 250 ms timeout and does stop. + +So on the latching interface, killing the controlling process outright is **the opposite of a +stop** — it is the one action that guarantees your cleanup never runs and your zero is never +sent. This inverts the standard advice, and it inverts what the DR02 measurement in +[§3.5](#35-a-velocity-command-is-renewed-not-set) would lead you to expect. Send zeros +repeatedly on the way out, for a bounded interval, in a path that runs on every exit; and treat +the operator's handset, not any software path, as the stop that is guaranteed. + +### 6.6 The state stream: check the frame length, and where it is being sent + +*[measured 2026-08/09]* Two Lite3 units in the same fleet emitted **state frames of different +lengths — 220 bytes on one, 212 on the other.** The shorter frame omits one field, which +shifts every subsequent field by eight bytes. + +**Decoding the short frame with the long layout does not crash.** It shears every field into +its neighbour: battery reads a calm, plausible `0.0`, and gravity lands on the wrong axis. +Key the decoder on the **exact** frame length and refuse an unknown one; a tolerant parse here +is a parse that lies. And decode a field the firmware never sent as *absent*, never as a +substituted `0` — a downstream gate that reads `0` as "measured and fine" will let the robot +move on a value that does not exist. + +*[measured 2026-08/09]* The state stream also goes to **exactly one destination address**, +configured on the robot (`~/jy_exe/conf/network.toml`, port `43897`). If that address is not +the host you are debugging from, every tool reports the robot as silent for a reason that has +nothing to do with the robot. This was the first trap of the first deployment, and it is worth +checking before anything else. + +*[measured 2026-08/09]* One more, and it is a trap of the same family as §5.6's decimal-vs-hex +confusion: **the angular-rate unit is ambiguous.** The ROS bridge copies the vendor's +`rpy_vel` into a ROS field with no stated conversion, while the low-level SDK documents +angular rate in **degrees per second**. Resolve it empirically on the firmware in front of +you — divide the observed yaw-pose change by the reported rate: about `1` means degrees per +second, about `57.3` means radians per second. Do not use a measured yaw rate downstream until +that check has been done. + +### 6.7 Two robots, one address: never default to another robot's IP + +*[measured 2026-09-02]* Units ship with the same factory address on the motion host. In one +fleet, a default motion-host address was left at the first robot's IP. On robot 1 that +"worked", because it pointed at itself — which is exactly what hid the problem. On robot 2 the +same default pointed **across the network at robot 1**: robot 2 reported `37/37 ticks driven, +moved 0.00 m` while **robot 1 walked in response**. + +Two rules fall straight out of that: + +- Anything that **runs on** the robot it drives should target `127.0.0.1`, not the robot's own + LAN address. +- Anything that runs **off** the robot must name its target explicitly, with no default. A + wrong explicit address fails loudly. A wrong default reaches a live robot. + +Readdress units before putting more than one on a shared network. + +### 6.8 A network change can silently disable the emergency stop + +**This is the most serious item in this section.** *[measured 2026-08/09]* The vendor handset +— which is the emergency stop — connects through an access point on a virtual interface that +shares radio hardware with the robot's station interface. That AP profile ships with +autoconnect disabled, so **applying a network configuration change deactivates the AP and it +does not come back.** + +Every service stays running. The robot stays reachable over Ethernet and WiFi. The only +symptom is that **the handset can no longer find its SSID**, which reads as a hardware radio +fault rather than as a consequence of a network change made an hour earlier. It cost an hour +on each of two robots, a day apart — and for that hour the operator's stop was not there. + +Check the AP interface is up and in AP mode **before and after** any network change, and never +make one between the pre-run check and the run. Note also that the radio permits a **single +channel across the AP and the station connection combined**, so the handset's channel and the +site network's channel are not independent — a "tidy" split of the two bands breaks handset +pairing outright. + +### 6.9 RGB only: what the robot cannot see + +The Lite3 Venture used here carried **one forward RGB camera: no LiDAR, no depth, no onboard +map.** Range came from apparent size against known object dimensions. + +**There is no lateral or rear sensing at all.** Where the DR02's LiDAR at least sees ±120° +([§5.8](#58-sensors)), this robot sees a single forward cone and nothing else. Clear both +sides of the lane, not only the ends, and treat any object outside that cone — including a +second, powered-off robot parked nearby — as something the robot cannot see rather than +something it will avoid. + +Three camera-path traps *[measured 2026-08/09]*, all of which present as something other than +what they are: + +- **The camera device is owned by the vendor's own publisher.** Do not compete for the device + node; consume the local stream it already serves. +- **Decode-time timestamps make stale frames look fresh.** OpenCV stamps a frame when it is + decoded, not when the shutter fired, so a buffered stream can hand you a several-second-old + frame that reports as current. Serve only the newest frame from a background reader, and + measure end-to-end frame age per installation rather than trusting the timestamp. +- **A tight retry loop on a dead stream starves everything else in the process.** Re-opening a + capture in a loop holds the interpreter lock across each blocking call: the port keeps + listening, every request times out, and the log stays empty. It reads as a network fault and + is a missing sleep. + +### 6.10 Mode authority stays with the operator + +AI Motion Control Mode is enabled by the vendor on a specific physical unit. **Do not attempt +to bypass, alter or reverse-engineer that activation.** There is also no documented high-level +"stand down": a program's cleanup can only send zeros, and returning the robot to a resting +posture goes through the vendor's own interface, operated by a person. + +Write code that **refuses** an unexpected mode rather than correcting it. On this platform the +robot's posture and mode are the operator's, and a program that quietly transitions the robot +is a program that moved it while nobody expected motion. + +--- + +## 7. Adding another robot to this file + +Keep the shape. **[§3](#3-common-rules--every-deeprobotics-robot) is common to every robot** +and should hold whatever arrives next. +**[§4](#4-transport-the-failures-that-produce-no-error) is common to the ROS 2 / DDS +products** and applies to any robot that speaks `drdds`. Everything else belongs in a section +of its own, holding only the values that differ. + +A product section is worth writing when it can answer these, with a citation to a file in this +repository or a measurement with a date: + +1. **How do I tell this robot apart from its siblings, from the wire?** +2. **What are the control modes, and which one can do the task I have?** +3. **What is the joint index table, and what is the full command payload?** +4. **What are the gains, and where is the authoritative array?** +5. **What must already be true before a motion command means anything?** +6. **What fails silently — accepted, or delivered, and ignored?** + +Two conventions that keep the file honest: + +- **Cite the file, or date the measurement.** Every number here is either quoted from a path in + this repository or marked with the date it was observed on a robot. A number with neither is + a number nobody can check. +- **Say what you did not test.** An unverified statement clearly labelled is useful. An + unverified statement presented as fact is the exact failure this whole file is about. diff --git a/AGENTS_CN.md b/AGENTS_CN.md new file mode 100644 index 0000000..69d7152 --- /dev/null +++ b/AGENTS_CN.md @@ -0,0 +1,792 @@ +# AGENTS_CN.md —— 给使用本 SDK 的编码代理的说明 + +[English](AGENTS.md) | **简体中文** + +> 英文版 [AGENTS.md](AGENTS.md) 为权威版本;如两版有出入,以英文版为准。 + +在编写与 DEEPRobotics 机器人通信的代码之前,请先读本文。本文按字面执行即可。 + +本 SDK 控制的是足以造成人员伤害的机器。下面的规则不是风格偏好。**每一条的存在,都是因为违反 +它会让机器人做错事,而且不会有任何报错。** 本文几乎每一条的失效形态都是这样:不是崩溃、不是 +异常、也不是被拒绝的消息,而是一个能列出、能报告类型、却不传输任何数据的 Topic;一个被完整接受、 +却驱动了你并不打算驱动的关节的数组;一条已发布、已接收、却被静默忽略的指令。 + +**这类失效对编码代理尤其昂贵。** 代理会把成功的返回值读作成功,而返回值里根本没有可读的信息。 +**当机器人没有按你的指令动作时,第一个要检验的假设不是"我的数值错了",而是"我的指令根本没有 +生效"。** + +--- + +## 目录 + +**通用部分** + +- [1. 范围与出处 —— 本文各部分的可信程度](#1-范围与出处--本文各部分的可信程度) +- [2. 代价最大的五条](#2-代价最大的五条) +- [3. 通用规则 —— 适用于每一台 DEEPRobotics 机器人](#3-通用规则--适用于每一台-deeprobotics-机器人) +- [4. 通信层:不产生任何报错的失效](#4-通信层不产生任何报错的失效) + +**分机型部分** + +- [5. DR02(Pro 与 Std)](#5-dr02pro-与-std) +- [6. Lite3](#6-lite3) + +**扩展本文** + +- [7. 向本文添加新机型](#7-向本文添加新机型) + +--- + +## 1. 范围与出处 —— 本文各部分的可信程度 + +本文有两类内容,可信程度不同,请区别对待。 + +**引自本仓库的内容。** 关节索引表、增益数组、模式编号、运动状态取值和 Topic 名称,均标注了它们 +在本仓库中的来源文件,可以在 diff 中核对。**如果厂商文档与本文出现分歧,以厂商文档为准,本文 +即为过时** —— 请修正本文。 + +**在实机上实测的内容。** 其余内容均为实机观测,并标注日期。本文有两批观测数据: + +| 标记 | 来源 | 数据量 | +| --- | --- | --- | +| *[实测 2026-09-03]* | 一次为期一天的 DR02 Pro 调试上电 | 一台机器人、一个下午、一个网络 | +| *[实测 2026-08/09]* | 2026 年 8—9 月对两台 Lite3 Venture 的集成工作 | 两台机器人、若干次实验,且软件栈非厂商提供 | + +每一条都只陈述观测结果而非结论,以便他人复现并提出异议。 + +**请把第二类内容当作"值得排查的方向",而不是规格说明。** 一台机器人在一天里的表现不构成统计 +样本。其中若干发现涉及本 SDK 文档范围之外的机器人侧软件 —— 机器人镜像中的 DDS 配置、一个 10 Hz +的反馈 Topic、一路 RTSP 相机流 —— **本 SDK 没有理由预先考虑到它们**。记录在此,是因为代理 +**一定会**遇到,而且否则会花上数小时把问题归咎于自己的代码。 + +**本文没有任何内容在 DR02 Std 上验证过。** 凡涉及 Std 的陈述,均引自 `src/dr02_std/`,并已标注 +为未经实机验证。 + +--- + +## 2. 代价最大的五条 + +如果只读一段,就读这一段。每条在下文都有展开。 + +| # | 陷阱 | 位置 | +| --- | --- | --- | +| 1 | **手臂作业必须使用上半身关节控制模式。** 高层运动控制模式的 `/ACTION` 词汇表只是若干预置的社交动作,没有抓取、举起或搬运;而且模式无法在程序运行中切换 | §5.1 | +| 2 | **保住腿部靠的是"零增益",不是"不点名"。** `/JOINTS_CMD` 是全长数组;一个 `kp` 非零的位置项就是一条**主动指令**,会与行走策略对抗 | §5.3 | +| 3 | **增益是一张表,阻尼也是一张表。** Pro 示例中 `waist_x_joint` 用 `2800`,腕关节用 `90` —— 同一个数组内相差三十一倍。`kd` 是照抄的,不是从 `kp` 推导出来的 | §5.4 | +| 4 | **看你手上这台机器对应的那份文档。** `dr02_pro/` 与 `dr02_std/` 是并列目录,关节数、手臂自由度和增益都不同,而且**两者都不会告诉你手上是哪一台** | §5.5 | +| 5 | **速度指令需要持续刷新,不是设置一次。** 而在通信层上,有若干东西是被静默丢弃而不是被拒绝的 | §3.5、§4 | + +--- + +## 3. 通用规则 —— 适用于每一台 DEEPRobotics 机器人 + +无论面前是哪台机器人,这些都适用;它们也是本文中最不容易过时的部分。 + +### 3.1 动手写第一行代码之前,先确认机型和型号 + +本仓库的产品目录是并列的,文件名完全相同。**一份文档只对一个产品正确,对其他产品什么也没说,而 +且没有任何文档会告诉你面前是哪一台。** 跨型号复制一个数值不会被任何机制拦下:消息格式相同、 +Topic 名称相同,只有数值不同。 + +请**通过测量**确定型号,而不是靠假设,并把结论显式写进代码。DR02 上的判别依据是关节数 —— +见 §5.5。当测量结果不匹配任何已知型号时,应当拒绝运行,而不是就近取一个。 + +### 3.2 开发者模式是机器人的状态,不是你程序里的一个开关 + +进入开发者模式是操作者在手柄上的动作(或一次服务调用),它改变的是机器人自身控制器的行为。有两 +个后果是程序看不见的: + +- **进入任何开发者模式后,感知、定位和避障功能都会关闭** + (`src/dr02_pro/docs/DEVELOPER_MODE_CN.md`)。机器人不再监视行人。此时你的代码是唯一在监视的 + 东西。 +- **三种模式不能直接互相切换。** 切换模式意味着停止你的程序、完整退出当前模式、再通过手柄菜单 + 重新进入。不要设计需要中途切换模式的流程 —— 那是一套人工操作步骤,不是一次 API 调用。 + +绝不要写一个"假定自己处在某个模式"的程序。**读取模式,如果不是你需要的那个,就拒绝发布。** + +### 3.3 绝不把关节索引写成字面量 + +```python +cmd[6] = 0.4 # 禁止 +targets = {"left_elbow_joint": 0.4} # 正确 +``` + +`/JOINTS_CMD` 传输的是**不含机型字段**的裸数组。错误的索引不会被拒绝、也不会有日志 —— 机器人 +只是动了另一个关节。在 DR02 上,索引 9 在 Pro 上是腕关节,在 Std 上是**髋关节**(§5.5):同一个 +字面量,在一台机器上只是轻推手臂,在另一台机器上却是在指挥行走策略正支撑着的腿。 + +请**按名称寻址**,依据由实测型号选定的索引表。代码中只保留**一处**把名称转换成索引的地方,其余 +一律传名称。 + +### 3.4 增益是表。照抄,不要推导。 + +不要臆造增益、不要把一个关节的增益套用到另一个关节、也不要用 `kp` 计算 `kd`。请从本仓库随附的 +示例中,逐字复制你所用型号的厂商数组,并在注释中注明来源文件。如果你需要的关节不在那张表里,那 +是一个应当向厂商确认的问题,而不是一个可以插值得到的数值。 + +**用一个数值套用整个数组,正是本条规则要防止的具体失效**,而且它不是小误差:见 §5.4。 + +### 3.5 速度指令需要持续刷新,不是设置一次 + +速度意图不是一项设置。它必须在实时控制循环的**每一拍**重新发布;当循环没有新的意图时,应当发布 +零值,而不是保持沉默。 + +这不是需要绕过的怪癖 —— **它本身就是失能保护(deadman),而且是正确的设计。** 它使得一个已经 +死亡、阻塞或断链的控制循环让机器人停下来,而不是让某个速度被锁存。请让"发送方停止"等价于 +"机器人停止"。 + +```python +# 错误 —— 只设置一次意图,之后空转 +commander.set_velocity(x=0.35) +while time.monotonic() < end: + commander.tick() + time.sleep(period) + +# 正确 —— 每一拍都刷新意图 +while time.monotonic() < end: + commander.set_velocity(x=0.35) + commander.tick() + time.sleep(period) +``` + +**沉默不等于停止。** 要停止,请连续发布一串零值 —— 不是一条,更不是什么都不发。 + +**并且要逐机型、逐接口地确认"发送进程被杀死时会发生什么"。不要假设,也不要把结论跨机型搬运。** +我们看过的两个接口在这一点上正好相反,而且是朝着要命的方向相反: + +| 接口 | 发送进程被直接杀死后的行为 | +| --- | --- | +| DR02 `/STEER` *[实测 2026-09-03]* | 机器人自行停了下来 —— 但又前进了约 5 cm,用了约 5 秒。此数值为目测,请重新测量而不要直接引用。**制动距离不为零。** | +| Lite3 传统速度 UDP 接口 *[见 §6]* | 指令会被**锁存**。该接口没有接收端超时,因此被杀死的发送进程会让机器人继续行走,且已无任何东西能让它停下。 | + +也就是说,"用 `SIGKILL` 而不是 `Ctrl-C` 来验证失能保护"这条常见建议在 DR02 上是正确的, +**而在 Lite3 的传统接口上作为习惯是危险的** —— 在那里 `SIGKILL` 恰恰是唯一能保证你的零值永远 +发不出去的操作。请在有人握住急停按钮、机器人挂在龙门架上的条件下先弄清楚自己处于哪一种情况。 +若接口会锁存,就在清理路径中发送零值,**同时**把操作者的急停当作真正的答案。 + +### 3.6 检查载荷,而不是检查请求 + +**这是本文背后两次跌倒的通用教训,其价值超出任何一台机器人。** 检查调用方"要求了什么"的防护, +不是对"离开你的进程的东西"的防护。 + +`/JOINTS_CMD` 是全长数组。无论你是否点名,每个关节都会得到一个条目 —— 于是一次点名七个关节的 +手臂请求,会变成一个 31 项的数组,而**把机器人放倒的正是那二十四个你没有写的条目**(§5.3)。 +检查必须在**展开之后**、针对数组本身、**逐索引**进行。 + +任何把调用方意图展开成更大结构的东西 —— 全长数组、广播、批量写入 —— 都必须在展开后的对象上做 +安全检查。**断言载荷,测试载荷。** + +### 3.7 绝不用裸 `try`/`except` 包住一次发布 + +节点运行期间发布失败,意味着**一次没有发生的停止**。它必须向上抛出,而且必须可见。吞掉它,会把 +一次通信故障变成"机器人继续运动,而日志看起来很干净"。 + +只有一个站得住脚的例外 —— 当 ROS 上下文已经销毁时跳过发布,因为此时发布确实不可能。请把这个 +例外写得足够窄,不要推广它。 + +### 3.8 绝不为了让某件事跑通而削弱安全常量 + +超时、停止重发次数、数据陈旧上限、置信度阈值和漂移阈值都是承重的。如果某个阈值触发得太频繁, +**请测量真实速率并上报这个不一致**,不要把上限一路调高到它不再报警为止。**一个被调到不再出声的 +阈值,就是一个不再保护任何东西的阈值。** + +对失败的测试同理。失败的测试是关于代码或关于世界的信息。删除它、放宽断言或跳过它,只会得到一套 +毫无意义的绿色结果。 + +### 3.9 你的程序里没有任何东西是急停 + +**手柄上的红色停止按钮才是急停**,并且每一次实机运行都必须有一个人在场、只负责握住它。发布停止 +指令的软件,只有在软件本身仍在运行时才能停下机器人 —— 而这恰恰是最先失效的条件。 + +另外,以下几条出自本仓库自身的安全说明,值得重申,因为程序无法强制它们: + +- 同一时间只允许**一个** `/JOINTS_CMD` 发布者。同时运行状态机和某个关节示例,等于两个进程在 + 争夺机器人。 +- 首次实机测试要有可靠的安全悬吊,并且要知道**悬吊与站立是两种不同的状态、有不同的安全转换** —— + 厂商要求进入 `RLControl` 前必须把机器人放下、双脚牢固触地,并警告悬吊状态下切换可能导致突发 + 运动。 + +--- + +## 4. 通信层:不产生任何报错的失效 + +**范围:本节讨论的是 ROS 2 / DDS 接口**,因此适用于 DR02 系列以及任何使用厂商 `drdds` 消息的 +机型。Lite3 使用 UDP,其对应内容见 [§6](#6-lite3)。 + +下面每一条,都是"消息发布成功却毫无效果"或"Topic 存在却不传输数据"的一种方式。**请在开始怀疑 +自己的控制逻辑之前先检查它们** —— 本文所记录的那一天,绝大部分时间都花在调试本来就正确的代码上。 + +### 4.1 每台主机、每个 shell,都要先 source 机器人的 DDS 引导脚本 + +*[实测 2026-09-03]* 本次使用的 DR02 Pro,其机器人镜像带有一个 DDS 配置脚本,必须在 ROS 2 环境 +**之前** source: + +```bash +set +u +source /opt/robot/scripts/setup_ros2.sh # 设置 FASTRTPS_DEFAULT_PROFILES_FILE +source /opt/ros/humble/setup.bash +``` + +它把 `FASTRTPS_DEFAULT_PROFILES_FILE` 指向机器人的 Fast DDS 配置文件,其中包含一份含有本机地址 +的传输白名单。**不执行它,若干机器人 Topic 会"能列出、能报告类型、却传输零字节"** —— 这与传感器 +损坏完全无法区分。仅仅 source 它,就让某个 Topic 从 `0` 字节变为每条 `1219` 字节,其余条件未变。 + +`set +u` 是必需的:ROS 的 setup 脚本会读取未定义变量,在 `nounset` 下会在到达 ROS 之前中止。 + +该路径属于机器人镜像,不属于本 SDK;请在你面前的机器上寻找对应的脚本并 source 它。**如果 Topic +能列出却没有数据,这是第一个要检查的地方。** + +### 4.2 与厂商的 QoS 保持一致:发布端是 BEST_EFFORT + +*[实测 2026-09-03]* 机器人侧的发布者是 BEST_EFFORT。**RELIABLE 的订阅者永远连不上,而且两侧都 +不会有任何日志。** 你的订阅者收到零条消息,而所有诊断看起来都很健康。 + +```bash +ros2 topic echo /SOME_TOPIC --once --qos-reliability best_effort +``` + +在代码中,请以 BEST_EFFORT 订阅厂商 Topic。另需注意:`ros2 topic hz` 和 `ros2 topic bw` 以 +RELIABLE 订阅,且在 Humble 上**不接受 `--qos-reliability`**,因此它们在所有 best-effort Topic +上都报告零。只有 `echo` 接受该参数。**`hz` 报零不能作为 Topic 静默的证据。** + +### 4.3 用"是否有字节到达"判断 Topic,而不是用发布者计数 + +*[实测 2026-09-03]* 机器人侧的发布者是裸 DDS 应用而非 ROS 节点,因此 ROS 工具不会统计它们。曾有 +一个 Topic 报告 `Publisher count: 0`,而在同一秒内传输了 1219 字节。`ros2 node list` 同样看不到 +它们。 + +反过来也会误导:**一个 Topic 上有两个发布者,并不意味着有两台机器人。** 请比较各端点 GID 的前 +12 字节 —— 如果 participant 前缀一致,那就是同一个进程: + +```bash +ros2 topic info --verbose /JOINTS_DATA +``` + +### 4.4 `name` 字段是保留字段,实际到达时为空 + +`src/dr02_pro/docs/JOINT_CONTROL_CN.md` 已说明 `name` 是保留字段,SDK 不应依赖它。 +*[实测 2026-09-03]* 在实机上,全部 31 个 `name` 条目都是空的。 + +**请把这句话按字面理解来做设计:机器人不会告诉你它的关节叫什么。** 名称到索引的映射只能来自厂商 +表格,并以实测关节数为键。这正是"关节数是第一个要测量的东西"的原因。 + +### 4.5 一台机器人可能有两台主机、两个 ROS 发行版,以及不对称的 Topic 视图 + +*[实测 2026-09-03]* 本机器人的控制主机运行 Ubuntu 24.04 / ROS 2 Jazzy,感知主机运行 +Ubuntu 22.04 / ROS 2 Humble,同处一个 DDS 域。它们能看到彼此的 Topic,**但并不对称**:关节 +Topic 只能从感知主机使用;在应用 §4.1 之前,LiDAR Topic 只在控制主机上有数据。 + +两个推论:为你正在编译的那台主机所用的发行版编译并安装消息包;以及 **要从哪台主机使用某个 +Topic,就从哪台主机去验证它** —— 在另一台主机上能列出,并不能证明你这台也行。 + +### 4.6 如果你的程序不在机器人上运行,它的时钟就不是机器人的时钟 + +*[实测 2026-09-03]* 见 §5.6。一条携带了"与机器人时钟不一致的主机时间戳"的消息,是"指令被接收却 +被忽略"的一个候选解释。这是假设而非已确立的机制,但排除它的成本很低:同步时钟,或者从机器人侧 +主机复现该故障,看它是否仍然存在。 + +--- + +## 5. DR02(Pro 与 Std) + +适用于 `src/dr02_pro/` 与 `src/dr02_std/`。所有标注 *[实测 2026-09-03]* 的内容均在一台 +**DR02 Pro** 上观测;涉及 Std 的陈述引自 `src/dr02_std/`,**未经实机验证**。 + +### 5.1 开发者模式决定了你的程序究竟能做什么 + +**先选模式,再选方案。** 共有三种模式(`src/dr02_pro/docs/DEVELOPER_MODE_CN.md`),而对大多数 +SDK 工作而言只有两种可用: + +| 模式 | `set_mode` 取值 | SDK 拥有 | 机器人拥有 | +| --- | --- | --- | --- | +| **高层运动控制模式** | `3` | `/MOTION_STATE`、`/GAIT`、`/STEER`、`/ACTION` | **全部关节** | +| **全身关节控制模式** | `2` | 全部关节,经由 `/JOINTS_CMD` | 无 —— 包括平衡在内 | +| **上半身关节控制模式** | `1` | 腰部和双臂,经由 `/JOINTS_CMD` | 腿部,由其内部策略控制 | + +**高层运动控制模式无法进行任何操作作业。** 它的 `/ACTION` 接口只接受一个整数,全部词汇表是预置 +的社交动作 —— Pro 上十二个(`0x3000` `greeting` … `0x300b` `clap`),Std 上五个 +(`0x3000`–`0x3004`)。**没有抓取、没有举起、没有搬运**,并且在该模式下关节由机器人内部策略控制。 +**如果你的任务需要手臂做出预置动作之外的任何事,高层模式就是错的模式,在其中投入再多工作也到不了 +目的地。** + +**全身关节控制模式也不是手臂控制的捷径。** 它把腿也交给你,也就是说你现在要负责让一台站立的人形 +机器人保持平衡。本仓库明确警告:在全身模式下运行上半身示例,可能因腿部关节收不到有效指令而失去 +支撑,导致机器人摔倒。 + +**因此手臂作业必须使用上半身关节控制模式。** §5.2 讲的就是这件事。 + +**模式无法在程序运行中切换。** 事实上它们根本不能直接切换:你必须停止 SDK 程序、确认其发布者已 +停止、完整退出当前模式,再通过手柄菜单重新进入。**请把一次运行设计为从头到尾只用一个模式。** +"先在高层模式下走过去,再切到上半身模式抓取"这样的方案,中间夹着一个操作者、一个手柄和一次完全 +停止 —— 如果机器人必须在这个边界上保持站立,那么这个方案根本不成立。 + +请从 `/DEVELOPER_MODE_STATUS` 读取当前模式(它发布形如 +`{"Enabled":true,"Mode":2,"Frequency":500}` 的 JSON),若不是你需要的模式就拒绝发布。 + +### 5.2 让机器人一边行走保持平衡,一边由 SDK 控制手臂 + +**这是本文中最有用的一条。** 它是在 DR02 上真正能够开展自主操作作业的配置,而且全程无需切换模式。 + +**`/STEER` 在上半身关节控制模式下同样被接受**,不只是高层模式 —— 本仓库自己的示例表格就把 +`steer_example` 同时列在这两种模式下(`src/dr02_pro/docs/EXAMPLES_CN.md`)。因此在同一个模式内、 +不做任何切换: + +- 机器人依靠**自身的腿部策略**行走和保持平衡,由 `/STEER` 驱动; +- 你的程序通过 `/JOINTS_CMD` 拥有**腰部和双臂**。 + +*[实测 2026-09-03]* 在 DR02 Pro 上采用该配置时,单臂伸展的跟踪误差为 **0.11°**,同时行走策略稳稳 +托住了下方的双腿;在一次 1076 帧、八秒的斜坡插值过程中,双膝分别移动了 0.07° 和 0.57°,全程未 +中止。 + +有两件事使它成立,而且都很容易做错: + +1. **进入模式的方式必须正确。** 上半身模式的进入流程要求机器人处于**悬吊起立**姿态,因此挂在龙门 + 架上对于**进入模式**是正确的。但这对 `RLControl` 并不正确 —— 厂商要求进入 `RLControl` 时 + **双脚牢固触地**。它们是同一套流程中的不同步骤,不要把它们合并。 +2. **必须给腿部零增益。** 不是"不写进去",而是**零增益**。这就是 §5.3,做错它正是机器人被放倒的 + 原因。 + +### 5.3 保住腿部靠的是"零增益",不是"不点名" + +**这一条放倒过一台机器人,也是本文中最不显然的一条。** + +`/JOINTS_CMD` 是**全长数组**:在 Pro 上,无论你是否点名,全部 31 个条目都会被发送。看起来最安全 +的自然选择,是把未点名的条目填上机器人自身的实测位置 —— 相当于告诉机器人"保持你现在的位置"。 + +**这并不是安全的选择。一个 `kp` 非零的位置项就是一条主动指令。** 关节控制器会不惜代价地把该角度 +保持住 —— 包括对抗机器人自身的行走策略,而在上半身模式下,正是那个策略在托住它的双腿。 + +*[实测 2026-09-03]* 在一台 DR02 Pro 上:一次点名七个关节的右臂伸展,对全部 31 个条目统一施加 +`kp=40`、`kd=2`,用八秒斜坡插值。手臂只动了一点,**而双腿塌了下去。** 一个"断言调用方只点名了 +上半身关节"的防护通过了检查,而且它**不可能**捕获这个问题 —— 腿从来没有被点名,它们是随数组一起 +到达的。 + +**增益本身就是防护。** 本仓库自己的示例已经做对了这件事 +(`src/dr02_pro/low_level/arm_joint_example.cpp`):数组是全长的,31 个条目全部被填充,而且 +**上半身关节数及其之后的每一个索引都携带 `kp = 0` 和 `kd = 0`。** + +``` +kUpperBodyKp[17..30] = 0.0f // 腿部与颈部:存在于数组中,但不产生力矩 +kUpperBodyKd[17..30] = 0.0f +``` + +**请仔细读这一点,因为承重的是增益,而不是位置。** 该示例向那些腿部条目写入 `position = 0`, +而这之所以无害,**仅仅**是因为旁边的增益是零。同样的条目若配上非零的 `kp`,就会命令全部腿部关节 +同时回到标定零位。因此: + +- **只要增益为零,未点名条目里的数值就无关紧要;一旦增益不为零,它就是灾难性的。** 无论你像示例 + 那样把未点名条目填 `0`,还是填机器人的实测位置,让机器人站住的都是那个零增益。请在测试中 + **逐索引断言增益**。 +- **绝不要把位置 `0` 当作"没有指令"。** `0` 是产品标定定义的关节零位 + (`src/dr02_pro/docs/JOINT_CONTROL_CN.md`),它是一个具体的姿态,不是姿态的缺席。 +- **每拍重读的实测位置是"跟随",不是"保持"。** *[实测 2026-09-03]* 如果你用实时反馈填充未点名 + 条目,它们会跟随而不产生回复力。在一次成功的伸展中,躯干的 `waist_x` 倾斜了 `-0.177 rad` 去 + 配平伸出的手臂,因为腰部未被点名。空载时这是无害的;它同时意味着"回到休息位"必须命令**整个 + 上半身 —— 包括全部三个腰关节**,否则它会把躯干丢在漂移到的位置上。 + +值得加一个腿部漂移看门狗:在斜坡插值开始时对每个由策略控制的关节拍快照,若某个关节偏移过大就中止。 +**它的阈值是一个判断,不是一个可以照抄的常量。** *[实测 2026-09-03]* 0.05 rad 会在一个髋关节移动 +2.9° 时触发,而那几乎肯定是策略在正确地保持平衡;0.12 rad 既能捕获折叠,又不会因配平动作而误中止。 +在上半身模式下,手臂伸展时髋和膝**本来就应该**动。 + +### 5.4 增益是一张表,阻尼也是一张表 + +权威来源是 Pro 的 **`src/dr02_pro/low_level/arm_joint_example.cpp`** 和 Std 的 +**`src/dr02_std/low_level/arm_joint_example.cpp`**。请从文件里复制;不要不加核对地照抄本文。 + +DR02 Pro,上半身(索引 `0`–`16`): + +| 关节 | `kp` | `kd` | +| --- | --- | --- | +| `waist_z_joint` | 600 | 6 | +| `waist_x_joint` | **2800** | **15** | +| `waist_y_joint` | 2300 | 20 | +| 双臂肩部与肘部 | 600 | 6 | +| 双臂腕部 | **90** | **2** | +| 腿部与颈部(`17`–`30`) | **0** | **0** | + +**同一个数组中的两个关节相差三十一倍。** 一个要托住躯干的腰关节和一个腕关节,根本不是同一类 +执行器,**不存在一个能同时服务两者的数值**。 + +*[实测 2026-09-03]* 对所有关节统一使用 `kp=40`,让机器人折叠了两次,并且产生了**两个看似无关的 +症状**:腰部下沉 —— 规格要求 2800 而只给了 40,相差七十倍,而在一台站立的人形机器人上,腿会随着 +躯干一起下去;与此同时手臂"几乎没动" —— 因为 40 同时也在对抗一个需要 600 的肩关节。**一个错误的 +数值,两个症状,而第二个症状会把你引向姿态,而不是增益。** + +**`kd` 是照抄的,不是推导的。** 增益改对之后,`kd` 曾被按 `kp/20` 缩放 —— 这是一个猜测。相对厂商 +数组,这个猜测最高偏大 **9.3 倍**(`waist_x_joint`:140 对规格值 15), +*[实测 2026-09-03]* 在一次双臂伸展中腰部向侧向弯曲,恰好发生在偏差最大的那个关节上。在离散的高频 +控制环中,过大的微分增益并不会简单地起阻尼作用,而是会驱动漂移和振荡。 + +**这里没有什么 `kp`/`kd` 比值可供发现。两者都是表。** 如果你在自己的代码里保存这些数值,请补一个 +测试,断言腰部的比值与腕部的比值不同,使未来某次善意的重构无法把其中一个从另一个推导出来。 + +DR02 Std,上半身(索引 `0`–`8`),引自 `src/dr02_std/low_level/arm_joint_example.cpp`, +**未经实机验证**: + +| 关节 | `kp` | `kd` | +| --- | --- | --- | +| `waist_z_joint` | 600 | 6 | +| 双臂肩部与肘部 | 200 | 5 | +| 腿部(`9`–`20`) | 0 | 0 | + +请注意:Std 的肩关节是 `200`,而 Pro 的肩关节是 `600`。**两张表在任何方向上都不可互换。** + +### 5.5 看你手上这台机器对应的那份文档 + +**这就是两次跌倒的成因。** 让机器人折叠的那组增益来自 +`dr02_std/low_level/arm_joint_example.cpp`;而机器人是一台 Pro。 + +`src/dr02_pro/` 与 `src/dr02_std/` 是并列目录,文件名完全相同、内容不同,而且**两者都不会告诉你 +面前是哪一台**。它们在关节数、手臂自由度、增益、`/ACTION` 词汇表和可用 IMU Topic 上都不同。消息 +类型和 Topic 名称完全一致,因此没有任何机制会拒绝一个跨型号的数值。 + +**先测关节数。** 它就是判别依据,而且可以直接从通信上读出来: + +```bash +ros2 topic echo /JOINTS_DATA_10HZ --once --qos-reliability best_effort | grep -c data_id +``` + +*[实测 2026-09-03]* 三次独立采样均返回 `31` → 是 Pro。**31 是 Pro,21 是 Std,其他任何数值都意味 +着停下来提问** —— 不要就近取一个型号。 + +两张索引表,引自 `src/dr02_pro/docs/JOINT_CONTROL_CN.md` 和 +`src/dr02_std/docs/JOINT_CONTROL_CN.md`: + +| 分组 | Pro(31 关节) | Std(21 关节) | +| --- | --- | --- | +| 腰部 | `0`–`2`(`waist_z`、`waist_x`、`waist_y`) | `0`(仅 `waist_z`) | +| 左臂 | `3`–`9`(7 自由度,含 3 个腕关节) | `1`–`4`(4 自由度,无腕关节) | +| 右臂 | `10`–`16`(7 自由度,含 3 个腕关节) | `5`–`8`(4 自由度,无腕关节) | +| 左腿 | `17`–`22` | `9`–`14` | +| 右腿 | `23`–`28` | `15`–`20` | +| 颈部 | `29`–`30`(锁死,不可控制) | 无 | +| 上半身可控范围 | `0`–`16` | `0`–`8` | + +**两个型号共有 21 个同名关节,其中 20 个位于不同索引。** 只有索引 `0` 上的 `waist_z_joint` 一致。 +索引 `9` 在 Pro 上是 `left_wrist_x_joint`,在 Std 上是 `left_hip_y_joint`。在 Std 上,Pro 的手臂 +索引 `9`–`16` **全部是行走策略正支撑着的腿部关节。** + +由于 `/JOINTS_CMD` 传输的是不含机型字段的裸数组,一条按 Pro 构造的指令发到 Std 上不会被拒绝、 +不会有日志,而且会驱动腿部。 + +### 5.6 `/STEER` 的前置条件,以及一个我们无法完全解释的消息头现象 + +在高层模式和上半身模式下,`/STEER` 都是进行程序化行走的正确 Topic。`x`、`y` 和 `yaw` 是 +**`[-1.0, 1.0]` 的归一化比例,不是实际速度**(`src/dr02_pro/docs/EXAMPLES_CN.md`), +`steer_example` 使用固定值 `0.6`。 + +**前置条件:机器人必须已经站立、处于 `RLControl` 状态、并已选定步态。** 不满足时没有任何东西会 +报错 —— 发布成功、Topic 上有流量、订阅方显示已连接,而机器人只是站着不动。*[实测 2026-09-03]* +以 20 Hz 发布 104 条 `x=0.15` 的 `/STEER` 指令,机器人完全没有动作。 + +请从 **`/MOTION_INFO`** 读取机器人的实际状态。`/MOTION_STATE` 和 `/GAIT` 是**指令** Topic,本身 +是静默的 —— echo 它们什么也得不到,而这看起来恰好像链路已死。 + +```bash +ros2 topic echo /MOTION_INFO --once --qos-reliability best_effort +``` + +`/MOTION_INFO` 以**十进制**报告这些取值,而 `src/dr02_pro/docs/EXAMPLES_CN.md` 用**十六进制** +列出它们,这本身就是一个小陷阱:`17` 和 `0x11` 是同一个状态,`131078` 是 `SuspendedStand`, +而不是什么异常。 + +| 状态 | 十六进制 | 十进制 | +| --- | --- | --- | +| `Idle` | `0x0` | 0 | +| `JointDamping` | `0x2` | 2 | +| **`RLControl`** | `0x11` | **17** —— `/STEER` 只在这里有意义 | +| `SuspendedStand` | `0x20006` | 131078 | +| `HumanWALKAMP`(步态) | `0x21001` | 135169 | +| `HumanWALKTERRAIN`(步态) | `0x21006` | 135174 | + +**顺序是:把机器人放下、双脚牢固触地,然后 `RLControl`,然后步态,然后 `/STEER`。** + +**两条开放的观测,作为问题而非结论提出。** + +*[实测 2026-09-03]* **小幅值没有产生运动。** `x=0.15` 毫无动作,`0.35` 走了起来,而 +`steer_example` 用的是 `0.6`。归一化输入上是否存在死区,未能确定。 + +*[实测 2026-09-03]* **在把消息头留空之前,`/STEER` 一直被忽略;这是本次实验中代价最大的一件事。** +在机器人处于 `RLControl`、已选步态、双脚触地、QoS 匹配、无其他发布者,且 `ros2 topic echo` 能在 +通信上看到正确数值的条件下,**三次尝试共 293 条指令没有产生任何运动,也没有任何一行日志。** 对 +手柄成功驱动机器人时的抓包显示,每条消息都携带 `frame_id: 0` 和未设置的 `stamp`。改为 +`frame_id = 0` 且 `header.stamp` 保持默认值后,第一次尝试机器人就动了。 + +两个字段是一起改的,因此**我们无法判断责任在 stamp、在 frame id、还是两者都有。** 我们最合理的 +猜测是:消费方会用机器人自身的时钟校验时间戳并丢弃不一致的消息 —— 而我们的发布端在一台开发主机 +上,其时钟确实不一致。这也能解释为什么 `steer_example` 是正常工作的:它确实设置了这两个字段 +(`msg.header.frame_id = frame_id++; msg.header.stamp = node->now();`),而它通常运行在时钟一致 +的环境里。**我们非常欢迎在这一点上被纠正** —— 如果消费方的行为在某处已有文档而我们没有找到, +这一整段都可以替换成一个链接。 + +在此之前,实用建议是:如果 `/STEER` 正在被接收却被忽略,**先试试 `frame_id = 0` 加未设置的 +stamp**,再去查步态、死区和模式仲裁 —— 在我们这里,那些从头到尾都是对的。 + +### 5.7 反馈:`/JOINTS_DATA_10HZ` 能解析,而 `/JOINTS_DATA` 不能 + +*[实测 2026-09-03]* 两个 Topic 都承载 `drdds/msg/Joints`。在感知主机上用 `ros2 topic echo` 各尝试 +五次: + +| Topic | 解析成功 | 关节数 | 频率 | +| --- | --- | --- | --- | +| `/JOINTS_DATA` | **0 / 5** | — | — | +| `/JOINTS_DATA_10HZ` | **5 / 5** | 31 | 10.000 Hz | + +`/JOINTS_DATA` 在 DDS reader 内部就以 `sequence size exceeds remaining buffer` 失败,尚未进入 +ROS 消息转换;`--raw` 同样失败。它在执行 §4.1 之后依然存在,因此不是传输或 QoS 问题,更像是原生 +发布端与 ROS 生成的 IDL 之间的线格式不一致,而不是订阅端可以配置的东西。 + +**如实说明,并附带保留。** 这只是通过 Python 侧订阅者用 `ros2 topic echo` 观察到的。**我们没有 +运行本仓库自己的 `arm_joint_example`(它以 C++ 订阅 `/JOINTS_DATA`)**,因此无法判断它是否受影响, +而且很可能并不受影响。如果它不受影响,那么这两条路径之间的差异本身就值得了解,我们很乐意被告知 +原因。 + +我们采用的规避方式是改读 `/JOINTS_DATA_10HZ`。**这个代价是真实的,应当被写明:** 用于填充 +`/JOINTS_CMD` 数组中未点名条目的实测位置,此后每 100 ms 刷新一次,而不是按控制频率刷新。请在你 +自己代码中的相应常量处注明这一点;**这是反馈带宽的下降,不是一次免费的替换。** + +### 5.8 传感器 + +*[实测 2026-09-03]* 以下涉及机器人镜像中的传感器而非本 SDK,记录在此是因为它们改变了一个安全门 +所能承诺的内容。 + +**LiDAR 看不见机器人背后。** 对一次扫描在机体坐标系下做方位角直方图,显示回波大致分布在 −120° +到 +120° 之间,而**在 ±120° 之外恰好为零**。这是传感器的视场,不是滤波产物 —— 该分布关于正前方 +对称。后果非常直接:**基于该 LiDAR 构建的接近停止门,看不见站在机器人后方的任何人。** 请在代码中 +**禁用**后向运动,而不是仅仅避免它;也不要让任何人站在后方锥区内、指望机器人能检测到自己。在本次 +实验中,操作者坐在机器人后方 1.5–2 m 处,完全没有产生回波。 + +无论如何都需要一个自身回波下限:人形机器人的 LiDAR 会看到自己的结构,约 0.35 m 以内的回波就是 +机器人本身;没有下限的话,最近距离永远读作零。 + +**相机:USB 描述符序列号不是相机的固件序列号。** +`src/dr02_pro/docs/CAMERA_CN.md` 已经警告"序列号与安装位置的对应关系无法从序列号本身确定,必须 +凭经验建立" —— 这个警告是正确的,应当遵守。此外补充三点: + +- `/sys/.../serial` 中的序列号(形如 `350423023842`)**不是**驱动需要的固件序列号(形如 + `254622074040`)。用错的那个启动**不会失败** —— 驱动会退回到它找到的其他设备,于是你会在毫无 + 察觉的情况下打开另一台相机。建议优先使用 `usb_port_id`,它按位置确定且稳定。 +- 凭经验建立对应关系这件事,比听上去更脆弱:一台朝下的相机拍到的空地板,和另一台朝下的相机拍到的 + 空地板长得很像。**能看到每台相机实际安装在哪里的人,胜过任何从图像做出的推断**;而一个标错的 + 后向相机,是一个带着令人安心名字的盲区 —— 参见上面关于 LiDAR 的说明,那正是后向视角格外重要的 + 原因。 +- 如果要用彩色像素坐标去索引深度图,必须设置 `align_depth.enable:=true`。否则,在彩色坐标处采样 + 深度图,返回的是**另一台相机**在该像素处的距离:这是一个貌似合理的错误答案,而不是一个报错。 + +**H.265 的 RTSP 流在出图之前会先给出灰帧。** 机器人还通过 RTSP 提供一路前向 RGB 相机。用默认设置 +打开时,它能连上、报告 1280x720,并返回一张均匀的灰色画面,看起来和镜头被盖住一模一样。**它不是:** +该流是 H.265,其参数集发送间隔较长,在参数集到达之前任何解码器都无法出图,而等待期间 UDP 传输还会 +丢分片。请在打开采集之前设置 TCP 传输,并且**用方差判断一帧是否可用,而不是用读取是否成功** —— +对灰帧而言读取是返回成功的。单次采集实测:第 0 帧 `std=1.06`,从第 20 帧起 `std≈68`。 + +另外,该镜头广角很强 —— 一根笔直的天花板梁会被渲染成明显的弧线。这对基准标记检测有影响:标记检测 +拟合的是直边四边形,桶形畸变足以让四边形检验失败,而标记对人眼仍然完全清晰可读。 + +### 5.9 编译 + +`src/dr02_pro/CMakeLists.txt` 把 `BUILD_PLATFORM` 默认设为 `x86` 且不做检测,而 +`BUILD_PLATFORM` 决定选用 `third_party/onnxruntime//` 下的哪份二进制。机器人侧主机是 +aarch64,因此在那里直接 `colcon build` 会在链接阶段失败。`src/dr02_pro/docs/REAL_ROBOT_CN.md` +给出了每台主机的正确命令;如果你要把它写进脚本,请从 `uname -m` 推导该值,而不要写死常量,这样 +同一个脚本在开发主机和机器人上都能用。 + +实机编译不得启用 `BUILD_SIM=ON`。 + +--- + +## 6. Lite3 + +**Lite3 不在本仓库中,而且上面的内容几乎没有一条能直接搬过去。** 它的接口是厂商另外的 +`Lite3_MotionSDK` 与 `Lite3_ROS` 项目,传输方式是**发往运动主机的 UDP 数据报,而不是 ROS 2 +Topic**。一个读完 §5 之后遇到 Lite3 的编码代理,对这台机器的模型几乎在每个方面都是错的:没有 +`/JOINTS_CMD`、没有 `/STEER`、没有开发者模式、关节数不同,而且"发送方死亡时会发生什么"的答案 +也不同。 + +本节存在的意义,就是让这条边界**被明确说明,而不是被踩出来**。 + +**出处。** 这些观测来自 **2026 年 8—9 月**对两台 Lite3 Venture 的集成工作,标记为 +*[实测 2026-08/09]*。它们比 DR02 部分更不完整,且涉及一套并非厂商提供的软件栈:请把它们当作值得 +排查的方向。凡是没有测量过的数值,本文都会说明 —— 因为"未经测量的数值"正是本文要阻止任何人臆造的 +东西。 + +### 6.1 §3 中哪些适用,哪些不适用 + +**原样适用:** 动手前先确认机型(§3.1);绝不臆造数值(§3.4);检查载荷而非请求(§3.6);绝不用 +裸 `try`/`except` 包住一次发送(§3.7);绝不削弱安全常量(§3.8);操作者手中的遥控器才是急停 +(§3.9)。 + +**不适用:** §4 与 §5 中的每一个 Topic 名称、QoS 说明和关节表。最重要的是失能保护行为的差异 —— +见 §6.5。 + +### 6.2 采用低层 SDK,就等于接管了平衡控制器 + +`Lite3_MotionSDK` 以位置目标和增益的形式开放十二个腿部关节。**使用它会取代厂商自己的步态与平衡 +控制器** —— 也就是让机器人保持直立的那个东西。这与 DR02 的全身关节控制模式是同一笔交易(§5.1), +答案也相同:除非"编写一个平衡控制器"本身就是任务,否则请留在高层速度接口上,把腿交给厂商策略。 + +由此带来一个真实的限制,应当在设计上绕开而不是硬扛:**高层接口根本不暴露电机温度。** 运行在该接口 +上的软件看不到累积的热量。请让机器人在两次运行之间**物理散热**,并对任何无法进行热监控的运行设置 +时长上限。 + +### 6.3 存在两套速度接口,用错的那套会静默失效 + +*[实测 2026-08/09]* Lite3 在 UDP 端口 `43893` 上接受两种不同帧格式的速度指令,而 +**哪一种有效取决于机器人当前的模式;选错时没有任何可区分的症状:** + +- **传统速度**帧格式(一个定长数据报,每轴携带一个 `double`)要求机器人处于厂商的**自主模式**。 + 处于 AI 运动模式的机器无法进入自主模式。*[实测 2026-08]* 一条格式完全正确的 `vx = 0.10 m/s` + 脉冲到达了运动主机,`error_state` 保持为 `0`,而**世界坐标位姿变化为零**。没有任何东西报告拒绝。 +- **简单轴**帧格式(模式指令加上逐轴整数指令,以 ≥20 Hz 发送,接收端有文档记载的 250 ms 超时) + 才是真正让机器人动起来的那一套。 + +**一个到达并被接受的数据包,不等于一个会被执行的数据包。** 与 DR02 上的 `/STEER` 一样(§5.6), +前置条件存在于机器人的模式中,而失效表现为沉默。**在断定自己的指令有问题之前,先读状态流、确认 +模式。** + +### 6.4 只保留符号的速度映射,会静默地让所有速度测量失效 + +*[实测 2026-08/09]* **这是集成层面的陷阱,而不是机器人的问题**;记录它,是因为它是 Lite3 上与 +"零增益陷阱"对应的东西:一整类看起来做成了、实际什么也没测到的测量。 + +厂商的轴接口每轴携带一个整数幅值,因此它**能够**表达速度。但客户端只能发送自己有证据支持的幅值; +如果每个方向上只有一个原始值曾被观测到能产生已知位移,那么它就是客户端唯一能诚实发出的值。于是 +这个映射退化为只取请求速度的**符号**。此时请求 0.05 m/s 与请求 0.30 m/s 在通信上发出的是同一个 +值,机器人以那一个原语所对应的速度行走。 + +作为一种保守设计,这本身并没有错 —— 臆造中间幅值只会更糟。**危险的是在上游忘记这件事**,因为 +下游不会有任何东西报告"你请求的幅值已被丢弃"。 + +三个后果,每一个都产生貌似合理的错误答案而不是报错: + +- **速度阶梯测的是这个脚本,而不是机器人。** 死区之上的每一档都以同一速度行走,于是探测程序在 + 最低一档就"通过",并把该档报告为机器人的最低步态速度。这个数字描述的是脚本。 +- **假定"指令速度等于执行速度"的规划器会提交错误的距离。** 一个以 0.05 m/s 缓行的策略验证了 + 0.125 m 的行程,而同一时间窗内双腿实际走过了 0.75 m。机器人走到的是原语带它去的地方,不是规划 + 说的地方。 +- **死区必须作用于速度矢量,而不是逐轴作用。** 请对 `hypot(vx, vy)` 设门限并吸附到某个方位。若 + 逐轴作用,一个刚好低于前向死区、又刚好高于侧向死区的斜向请求,会把前向清零并触发一次 + **全速 90° 横移**。 + +另外:*[实测 2026-08/09]* 在厂商的轴向约定中,侧向和偏航**正方向为右**。如果你的导航栈使用 +"正方向为左",请**只在传输边界处取反一次**,并做测试 —— 这里的符号错误意味着机器人信心十足地朝 +错误方向走,而没有任何诊断信息。 + +**从未测量、也不得臆造的量。** 前向与侧向的速度上限、以物理单位表示的偏航角速率,以及最低步态 +速度,在这两台机器上都没有确立。把它们要求为**没有默认值**的显式参数是正确的设计:早先的版本曾 +把它们静默地默认为**另一款机器人**公布的数值 —— 一个与面前这台机器毫无关系的数字,就是这样成为 +它运动边界的。 + +### 6.5 两套指令接口,对"发送方死亡"给出两种相反的答案 + +*[实测 2026-08/09]* **传统速度接口没有心跳、也没有接收端超时:指令会一直锁存,直到有东西显式发送 +零值。** 简单轴接口则有文档记载的 250 ms 超时,会真正停下来。 + +因此在会锁存的那个接口上,直接杀死控制进程是**停止的反面** —— 它恰恰是唯一能保证你的清理代码 +永远不会运行、你的零值永远发不出去的操作。这与常见建议相反,也与 §3.5 中 DR02 的实测结论相反。 +请在退出路径中**反复**发送零值、持续一段有界的时间,并且这条路径要在每一种退出方式下都执行;同时 +把操作者手中的遥控器(而非任何软件路径)当作唯一有保证的停止手段。 + +### 6.6 状态流:核对帧长度,以及它被发往何处 + +*[实测 2026-08/09]* 同一批次的两台 Lite3 发出的**状态帧长度不同 —— 一台 220 字节,另一台 +212 字节**。较短的帧缺少一个字段,导致其后每一个字段都平移八个字节。 + +**用长帧布局解析短帧不会崩溃。** 它会把每个字段都错位读成相邻字段:电池读出一个平静而貌似合理的 +`0.0`,重力落到错误的轴上。请让解码器以**精确**帧长为键,并拒绝未知长度;**在这里"宽容的解析"就是 +"会撒谎的解析"。** 另外,对固件根本没有发送的字段,请解码为**缺失**,绝不要替换成 `0` —— 下游一个 +把 `0` 读作"已测量且正常"的门限,会允许机器人基于一个并不存在的数值开始运动。 + +*[实测 2026-08/09]* 状态流还**只发往唯一一个目的地址**,在机器人上配置 +(`~/jy_exe/conf/network.toml`,端口 `43897`)。如果那个地址不是你正在调试的主机,所有工具都会 +报告机器人静默,而原因与机器人毫无关系。**这是第一次部署时遇到的第一个陷阱**,值得在其他一切之前 +先检查。 + +*[实测 2026-08/09]* 还有一条,与 §5.6 的"十进制对十六进制"属于同一类:**角速度单位是有歧义的。** +公开的 ROS 桥接把厂商的 `rpy_vel` 拷贝进一个 ROS 字段,没有说明任何换算,而低层 SDK 文档记载角速率 +的单位是**度每秒**。请在你面前的固件上凭经验确认 —— 用观测到的偏航位姿变化除以上报的速率:结果约 +为 `1` 说明是度每秒,约为 `57.3` 说明是弧度每秒。**在这项检查完成之前,不要在下游使用测得的偏航 +角速率。** + +### 6.7 两台机器人共用一个地址:绝不要把默认值设成另一台机器人的 IP + +*[实测 2026-09-02]* 出厂时各机器的运动主机使用相同地址。在某个机队中,运动主机地址的默认值仍然是 +第一台机器人的 IP。在 1 号机上它"能用",因为它指向自己 —— 而这恰恰掩盖了问题。在 2 号机上,同一个 +默认值**穿过网络指向了 1 号机**:2 号机报告 `37/37 ticks driven, moved 0.00 m`,而 +**1 号机应声走了起来**。 + +由此直接得出两条规则: + +- **运行在**它所驱动的那台机器人上的程序,应当以 `127.0.0.1` 为目标,而不是机器人自己的局域网 + 地址。 +- 运行在机器人**之外**的程序,必须显式指定目标,且不设默认值。**写错的显式地址会响亮地失败; + 写错的默认值会悄悄够到一台活着的机器人。** + +在把多台机器人接入同一网络之前,请先重新分配地址。 + +### 6.8 一次网络变更可能会静默地让急停失效 + +**这是本节中最严重的一条。** *[实测 2026-08/09]* 厂商遥控器 —— 也就是急停 —— 通过一个虚拟接口上 +的接入点连接,而该虚拟接口与机器人的 station 接口共用同一套射频硬件。该 AP 配置出厂时禁用了自动 +连接,因此**执行一次网络配置变更会让 AP 停用,并且它不会自行恢复。** + +所有服务都还在运行。机器人通过以太网和 WiFi 依然可达。**唯一的症状是遥控器再也找不到它的 SSID**, +而这看起来像是射频硬件故障,而不是一小时前那次网络变更的后果。它在两台机器人上各花掉一小时,前后 +相隔一天 —— **而在那一小时里,操作者的急停并不存在。** + +请在任何网络变更**前后**都确认 AP 接口处于开启且为 AP 模式,并且绝不要在运行前检查与实际运行 +之间做网络变更。另请注意:该射频**在 AP 与 station 连接合计只允许一个信道**,因此遥控器的信道和 +现场网络的信道并不独立 —— 一次"整洁地"把两个频段分开的操作,会直接破坏遥控器配对。 + +### 6.9 只有 RGB:机器人看不见什么 + +本次使用的 Lite3 Venture 只带**一路前向 RGB 相机:没有 LiDAR、没有深度、没有机载地图**。距离来自 +已知物体尺寸与其成像大小的比对。 + +**它完全没有侧向和后向感知。** DR02 的 LiDAR 至少能看到 ±120°(§5.8),而这台机器人只能看到一个 +前向锥区,此外什么都看不到。请清空通道的**两侧**,而不只是两端;并把该锥区之外的任何物体 —— +包括停在附近、已断电的第二台机器人 —— 当作机器人**看不见**的东西,而不是它会避开的东西。 + +三个相机路径上的陷阱 *[实测 2026-08/09]*,它们呈现出来的样子都不是其真实原因: + +- **相机设备由厂商自己的发布程序占用。** 不要去争抢设备节点,请消费它已经提供的本地流。 +- **解码时间戳会让陈旧的帧看起来很新。** OpenCV 在帧被解码时打时间戳,而不是在快门触发时,因此 + 一路带缓冲的流可能把几秒前的帧交给你,而它报告为当前帧。请只从一个后台读取线程提供最新帧,并 + **逐部署实测端到端帧龄**,而不是相信时间戳。 +- **对一路已死的流做紧凑重试循环,会饿死进程中的其他一切。** 循环重开采集会在每次阻塞调用期间持有 + 解释器全局锁:端口仍在监听、每个请求都超时、日志一片空白。它读起来像网络故障,实际上是少了一个 + sleep。 + +### 6.10 模式权限始终属于操作者 + +AI 运动控制模式由厂商针对具体的物理单机开启。**不要试图绕过、修改或逆向该激活过程。** 同时,也 +没有文档记载的高层"趴下"指令:程序的清理逻辑只能发送零值,让机器人回到休息姿态要通过厂商自己的 +界面、由人来操作。 + +请把代码写成在遇到非预期模式时**拒绝**,而不是去纠正它。在这个平台上,机器人的姿态和模式属于 +操作者,而**一个悄悄切换机器人状态的程序,就是一个在无人预期时让机器人动起来的程序。** + +--- + +## 7. 向本文添加新机型 + +请保持结构。**§3 通用于每一台机器人**,无论下一台是什么都应当成立。 +**§4 通用于 ROS 2 / DDS 类产品**,适用于任何使用 `drdds` 的机器人。其余内容都应当放进各自独立的 +一节,只保留互不相同的数值。 + +当一节能够回答下面这些问题、并且**每个答案都能指向本仓库中的一个文件路径、或一次带日期的测量** +时,它才值得写: + +1. **我怎样从通信上把这台机器人和它的同系列机型区分开?** +2. **有哪些控制模式,其中哪一个能完成我手上的任务?** +3. **关节索引表是什么?完整的指令载荷是什么样的?** +4. **增益是多少?权威数组在哪里?** +5. **在一条运动指令产生意义之前,必须已经成立的条件是什么?** +6. **什么会静默失效 —— 被接受、或被送达,然后被忽略?** + +两条让本文保持诚实的约定: + +- **要么引用文件,要么标注测量日期。** 本文中的每一个数字,要么引自本仓库中的某个路径,要么标注了 + 它在实机上被观测到的日期。**两者都没有的数字,是没有人能核对的数字。** +- **写明你没有测试过什么。** 一条被清楚标注的未验证陈述是有用的;**一条被当作事实呈现的未验证 + 陈述,正是本文通篇要防止的那种失效。** diff --git a/README.md b/README.md index 02f0d0d..0f385a3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ src// Product-specific ROS 2 packages, deployment instructions, and third_party/ Third-party source and integration files ``` +## Working with Coding Agents + +[`AGENTS.md`](AGENTS.md) ([中文](AGENTS_CN.md)) collects the rules a coding agent should follow when writing code that drives a DEEPRobotics robot: which Developer Mode a task needs, how joint commands and gains are structured per product, and the interface behaviours that fail without producing an error. It is organised as a common section followed by one section per product. + ## Related Repositories - [deep-robotics-msg](https://github.com/DeepRoboticsLab/deep-robotics-msg): ROS 2 message interface package used by the SDK. Its ROS 2 package name is `drdds`. diff --git a/README_CN.md b/README_CN.md index 8214150..9a7f844 100644 --- a/README_CN.md +++ b/README_CN.md @@ -19,6 +19,10 @@ src// 产品专属 ROS 2 软件包、部署说明和使用文档 third_party/ 第三方源码和集成文件 ``` +## 与编码代理协作 + +[`AGENTS.md`](AGENTS.md)([中文](AGENTS_CN.md))汇总了编码代理在编写驱动 DEEPRobotics 机器人的代码时应当遵循的规则:任务需要哪种开发者模式、各产品的关节指令与增益如何组织,以及哪些接口行为在失效时不会产生任何报错。全文按"通用部分 + 分产品部分"组织。 + ## 相关仓库 - [deep-robotics-msg](https://github.com/DeepRoboticsLab/deep-robotics-msg):SDK 使用的 ROS 2 消息接口库,ROS 2 包名为 `drdds`。