Nori L2
This page is for the Nori L2
Everything here — electrical specifications, leader-arm setup, calibration, and power diagnostics — applies to the L2 only. Do not follow it on an A3.
Power is the root cause of a surprising share of "the robot is flaky" reports. Symptoms that look like software — a camera that vanishes mid-session, a speaker that disconnects, an arm that stops responding — are often the Pi's USB rail running out of current.
Electrical specifications
| System | Specification |
|---|---|
| Battery | Two 24 V lithium-ion batteries connected in parallel |
| Total battery capacity | 576 Wh |
| Motor supply | 12 V, 40 A maximum |
| Required charger | 24 V lithium-ion battery charger |
| Included charger | 24 V, 2 A |
| Maximum battery charging rate | 4 A |
The two batteries form one 24 V pack: connecting them in parallel increases capacity, not voltage. All motors run from the robot's 12 V motor supply. The 40 A figure is the maximum for the motor power system, not a per-motor rating.
Use only a compatible 24 V lithium-ion charger
The robot includes a 24 V, 2 A charger. Do not exceed the battery pack's 4 A maximum charging rate, and do not use a charger intended for a different voltage or battery chemistry.
Power controls
The two physical power controls do different jobs:
- Master switch: turns power to the entire robot on or off.
- Physical E-stop: cuts power only to the motors. The robot's computer and other non-motor systems remain powered.
Use the master switch for a complete shutdown. Use the physical E-stop when motor power must be removed immediately. The app and SDK also provide a separate software E-stop; it blocks motion but does not replace the physical motor-power cutoff. See Safety states for how the two differ in what happens to motor torque.
Charging
Use the included 24 V, 2 A charger, or any compatible 24 V lithium-ion charger up to the pack's 4 A maximum — see the specifications and warning above.
The pack's state of charge is reported to the operator app as telemetry.batteryPercent — see SDK: Telemetry.
The rules
The Pi 5 caps aggregate USB current at 600 mA unless it's explicitly told the supply can do more. On a robot Pi this must be raised, or peripherals will brown out under load:
usb_max_current_enable=1Use a powered USB hub for anything hungry — speakerphones especially.
Motor torque is limited so that a peak draw stays within the motor power system's limits.
Confirm rather than guess. vcgencmd get_throttled on the Pi tells you whether you're actually looking at a power problem — and distinguishes an undervoltage history from a thermal one, which needs cooling instead. Bit-by-bit decode.
Devices that re-enumerate
When a USB device browns out and comes back, it comes back as a new card number. Anything configured by number (hw:0) is then pointing at nothing, permanently, until you restart.
This is why the robot's speaker must be configured by name — a dmix alias (nori_out) or hw:CARD=<name>, never hw:<number>.
Brownouts and throttling
Read this section whenever a peripheral disappears rather than misbehaves. A device that vanishes mid-session — a camera, the speaker, an arm — is usually being starved of current, not failing.
The symptom pattern
You're looking at a brownout if:
- A USB device works, then disconnects mid-session, often under load.
- The robot logs show a device re-enumerating (
… device has been disconnected, then a new device appearing). - It's reproducible under load (loud audio playback, all cameras streaming, arms moving) and fine when idle.
Software bugs don't usually correlate with load like that. Power does.
Confirming it rather than guessing
On the Pi:
vcgencmd get_throttledthrottled=0x0 is healthy. Anything else is the firmware telling you what went wrong:
| Bit | Value | Meaning |
|---|---|---|
| 0 | 0x1 | Undervoltage right now. The live signal — this is the one to watch. |
| 1 | 0x2 | Arm frequency capped now. |
| 2 | 0x4 | Currently throttled. |
| 3 | 0x8 | Soft temperature limit active now. |
| 16 | 0x10000 | Undervoltage has occurred since boot. |
| 17 | 0x20000 | Frequency cap has occurred since boot. |
| 18 | 0x40000 | Throttling has occurred since boot. |
| 19 | 0x80000 | Soft temp limit has been hit since boot. |
So 0x50000 = "undervoltage and throttling both happened at some point since boot"; 0xe0000 = "frequency-capped, throttled, and soft-temp-limited since boot" — a thermal history, not a power one. That distinction matters: a Pi dropping camera frames with 0xe0000 and no bit 0 needs cooling, not a bigger supply.
The high bits are sticky until reboot
Bits 16–19 persist for the life of the boot, so a sag from yesterday pollutes today's reading. Reboot before you measure, then watch bit 0 (the reading ending in an odd digit) in a live loop — that's the real-time signal.
Log it alongside temperature and the 5 V rail while you reproduce:
while true; do
printf '%s %s %s 5V=%s\n' "$(date +%T)" "$(vcgencmd get_throttled)" \
"$(vcgencmd measure_temp)" \
"$(vcgencmd pmic_read_adc | grep -E 'EXT5V_V' | awk '{print $2}')"
sleep 2
doneEXT5V_V sagging below ~4.8 V under load means the rail is marginal — note which activity did it.
Audio clips brown out the speaker
A near-full-scale audio clip drives the speaker amp and the hardware-AEC reference far harder than call speech. On a full-speed USB DSP speakerphone that's enough to brown the device out into a mid-stream USB re-enumeration.
The robot already clamps playback gain (NORI_SPEAKER_GAIN, default 0.7) to defend against this. If you're still hitting it: attenuate further client-side, add a powered USB hub, and use a more robust speaker. More: Audio.
Motor torque and the power station
Motor torque is deliberately limited so a peak draw stays within the motor power system's limits. If you're seeing the whole robot cut out under aggressive motion rather than a single peripheral dropping, that's a different problem from a USB brownout — it's the supply rail.
Leader arms
The leader arms are the physical pair you hold to puppet the robot's follower arms. They plug into your laptop over USB and are driven by the desktop app.
This is the setup that most often needs troubleshooting, because it's the one that touches serial ports, USB hubs, and motor IDs.
How the app sets them up
The desktop app has a leader-setup wizard (/nori/leader-setup) that walks the whole thing. The same steps exist as a CLI, which is useful when the wizard can't see an arm:
python -m lelab.nori_leader_setup plan # what needs doing
python -m lelab.nori_leader_setup ports --save # find and remember the serial ports
python -m lelab.nori_leader_setup set-id --wizard # assign motor IDs
python -m lelab.nori_leader_setup calibrate # calibrate the armsCalibration lands in:
~/.cache/huggingface/lerobot/calibration/teleoperators/nori_l2_dual_leader/Calibration
It is essential that when you start calibration, you hold the arm in the position where all motors are in the middle of their motion range. This is also where the Nori arm calibration starts, and the two must start in the same position for the leader arms to properly mirror motion. If you see strange issues with inverted or skewed motion when using the leader arms, starting calibration at the wrong position is likely the culprit.
Common issues
Worth knowing before you start, and the first thing to check if something breaks or feels off:
A charge-only USB cable looks exactly like a data cable. It carries no data lines, and it is the single most common cause of "no arm found."
Hubs swallow devices. An unpowered hub, or a hub daisy-chained off another hub, will intermittently drop the arms.
Calibration is cached on disk and reused across sessions. An arm that behaves as though it's calibrated for a different arm usually is.
Leader arm troubleshooting
The app says no leader arm is found
Rule these out in order — listed by how often they're the answer.
1. The cable is charge-only. Swap it for a cable you know carries data.
2. A hub is swallowing it. Plug the arm directly into the laptop while debugging — add the hub back only once it works.
3. It's genuinely unplugged. Worth actually checking.
If the wizard still can't see it, the CLI takes the same steps and is more verbose:
python -m lelab.nori_leader_setup ports --saveOnly one of the two arms is found
Both arms share a serial bus. If two motors have the same ID, they collide and the bus misbehaves — which frequently looks like "one arm is missing" rather than a clean error.
Re-run the ID assignment:
python -m lelab.nori_leader_setup set-id --wizardThe wrong joint moves, or a joint moves backwards
Two candidates:
Motor IDs are wrong. If a motor has the ID that belongs to a different joint, commands land on the wrong joint. Re-run set-id --wizard.
Calibration is stale. An arm calibrated for a different arm — or before a mechanical change — will behave consistently wrongly. Re-run calibration:
python -m lelab.nori_leader_setup calibrateSuspect the cache before the hardware
A stale calibration file produces symptoms that look exactly like a broken arm. If an arm started misbehaving without anything physically changing, check the cache first.
The arms disconnect randomly during a session
Usually power or the hub, not the arms. See Brownouts and throttling.
Cameras
The robot carries four cameras, each with a fixed role name:
| Role | Where it is | What it sees |
|---|---|---|
left_wrist | On the left gripper | What the left hand is doing |
right_wrist | On the right gripper | What the right hand is doing |
overhead | Above the workspace | Looks down over both arms |
front | Facing forward | The scene ahead — primarily your driving view |
The roles are identical on the L2 and the A3 — nothing here is model-specific. They are the vocabulary everything else keys off: the tiles in the composite video feed, cameraView(role) in the SDK, per-camera stills, and the per-camera streams recorded into episodes all use these four names. All four are 720p RGB, up to 30 fps.
If some cameras aren't found
Each role is pinned to a physical USB port, not to the camera itself — the cameras all report the same serial number, so the port is the only stable identity. If the robot comes up with cameras missing, or two roles swapped, the usual cause is USB re-enumeration: the ports were re-shuffled (a camera moved to a different port, a hub changed) and the saved port→role mapping no longer matches reality. The cameras are safe — nothing is broken; they just need to be re-identified.
Run the identification script on the robot, with the camera stack stopped (cameras can only be open in one place at a time):
sudo systemctl stop nori-cameras
ros2 run nori_cameras nori-identify-cameras
sudo systemctl start nori-camerasIt asks you to cover each camera in turn and picks the one that went dark, then writes the confirmed mapping back to the config — no guessing which port is which.
A camera that was working and then vanishes mid-session is a different problem — power, not identity. See Brownouts and throttling.
For how the video feed itself works — the composite grid, and the resolution and frame rate to expect — see Video.
