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`。