NoneadChina/dsh-nonead-universal-robots

dsh-nonead-universal-robots

让 DSH 用自然语言直接控制 Universal Robots(UR)机械臂的插件,由拓德科技(Nonead)基于自研的 nUR MCP Server 同源逻辑开发。(Suzhou Nonead Robot Technology Co., Ltd.)

未分類

dsh-nonead-universal-robots

A DeepSeek Harness (DSH) plugin that lets you control Universal Robots (UR) collaborative arms directly from natural language, developed by Suzhou Nonead Robot Technology Co., Ltd. based on the same logic as the company's own nUR MCP Server. (Suzhou Nonead Robot Technology Co., Ltd.)

The plugin starts a persistent Python worker (which internally reuses the vendored URBasic library) and exposes robot control as native DSH tools: the tools talk to the robot through that worker, so after a single connect you can keep issuing commands to the same IP. Repository:

⚠️ Safety notice: this plugin drives a real robotic arm directly. Always keep the robot in sight, keep the emergency stop within reach, and keep the workspace clear of people and obstacles. Treat it with the same care as granting a model the bash tool. You (or the model) bear full responsibility for any motion command.
🛡️ Motion approval gate: the 53 commands that physically move the arm, run a program, take the arm out of program control, remove its rigidity, overwrite/delete a program stored on the robot, drive a fieldbus device over Modbus, change the TCP / tool electrical configuration or switch digital/analog outputs, or push against a workpiece under force control pause and wait for human confirmation before being sent to the robot:
| Group | Tools |
|---|---|
| Motion | ur_movej ur_movel ur_movep ur_movec ur_servoj ur_move_optimized ur_move_x ur_move_y ur_move_z ur_move_tool_x ur_move_tool_y ur_move_tool_z ur_move_force |
| Drawing | ur_draw_circle ur_draw_square ur_draw_rectangle ur_draw_star |
| Programs & scripts | ur_load_program ur_run_program ur_send_script ur_reset_error |
| Control mode / power / safety | ur_set_freedrive ur_set_teach_mode ur_power_on ur_power_off ur_brake_release ur_unlock_protective_stop ur_shutdown |
| Force & velocity | ur_force_mode ur_force_mode_settings ur_speedj ur_speedl |
| Tool & I/O configuration | ur_set_tcp ur_set_tool_voltage ur_set_tool_output_mode ur_set_tool_communication ur_set_digital_out ur_set_analog_out ur_set_conveyor_tick |
| Program storage | ur_upload_program ur_delete_program |
| Physical state changes | ur_set_payload ur_set_payload_inertia ur_set_gravity ur_zero_ftsensor ur_motion_version ur_set_speed_scaling ur_set_conveyor_tracking |
| Modbus writes | ur_modbus_add_signal ur_modbus_delete_signal ur_modbus_send_custom_command ur_modbus_set_output_register ur_modbus_set_output_signal |
Decelerations and exits (ur_stopj, ur_stopl, ur_end_force_mode) are deliberately not gated: they are how an operator (or the model, after a cancellation) makes the arm stop, and a confirmation prompt would only slow that down. Gating rules live in one place (APPROVAL_OPS in lib/index.js) and are asserted by scripts/check-approval-gate.py + test/approval-gate.test.mjs.
In an interactive deployment (approval policy ask) a confirmation dialog appears in the UI. A call made without an approval service, without an agent, or answered with anything other than allowed-once fails closed — the command is rejected and never reaches the robot. The default lives in code (config.requireApprovalForMotion ?? true), so it cannot be turned off by a caller that skips schema parsing; set requireApprovalForMotion: false to disable the gate deliberately. test/approval-gate.test.mjs drives the real tool registry and proves all of the above, including that an extra op argument cannot re-route a call.
🧭 Pre-flight safety gate: a second layer that caps what a *single* command may ask for, independent of the approval gate. Limits: safetyMaxStepM (0.5 m — axis moves and drawing dimensions), safetyMaxLinearSpeed (0.5 m/s), safetyMaxJointSpeed (1.05 rad/s), safetyMaxAccel (2.0 m/s²), safetyMaxDurationS (60 s), safetyMaxForceN / safetyMaxTorqueNm (40 N / 10 N·m, force mode), plus an optional safetyWorkspace box ("x:-1,1 y:-1,1 z:0,1.2") checked against explicit Cartesian targets — including the center/origin of the four ur_draw_* tools (and their r / border / width / height / side are capped by safetyMaxStepM). It runs before the approval prompt — an impossible request should not bother a human — and is decoupled from it: requireApprovalForMotion: false turns off the human confirmation only, never these physical caps; that requires safetyGate: false. A malformed safetyWorkspace fails closed (every motion op is refused) instead of being silently ignored. It is a guard against typos and unit mistakes, not a safety boundary (the controller's own safety configuration still is), and it deliberately does not cover ur_send_script, which is arbitrary URScript this gate does not parse.
📝 Motion audit ledger: every gated command appends one NDJSON line (/nonead-ur/audit.ndjson by default; auditLogPath to move it, auditLog: false to disable, auditLogAll: true to also record unchecked reads) as {ts, ip, op, tool_name, args, decision, reason} with decision ∈ {approved, denied, blocked, unchecked} — so "what did the model actually do to the robot" is answerable after the fact. Writing to it never affects the robot path: an audit failure is reported, never thrown.

Feature overview

104 tools (ur_*) in total.

Category · Tool(s) (ur_*) · Description Connection · ur_connect / ur_disconnect · Connect / disconnect a UR robot by IP Status · ur_get_status · One-shot read of TCP, joints, model, serial, version, safety mode, run/program state, voltage, current, temperatures, uptime, joint currents/voltages/speeds, TCP speed and wrench, speed scaling, plus rtde_resync_bytes (how many bytes the RTDE receiver had to drop to re-synchronise; 0 means a clean stream) Pose · ur_get_tcp_pose / ur_get_joint_pose · Read current TCP pose / joint angles Targets · ur_get_target_values / ur_check_pose · Read where the controller is taking the arm: target joint positions/velocities/accelerations and target TCP pose/speed, plus the matching actual values for comparison. Comes from the RTDE target_* fields, so it is free — and it is the only direct evidence for "command sent but not executed yet / being blended / held back by the safety limit". ur_check_pose answers the question worth asking *before* moving: is this TCP pose inside the controller's safety limits, and does inverse kinematics solve (is it reachable)? A read-only pre-check: the controller runs is_within_safety_limits + get_inverse_kin and the answers come back through output registers behind start/finish sentinels, so "the controller never ran it" is reported rather than guessed Device info · ur_get_robot_model / ur_get_serial_number / ur_get_uptime / ur_get_software_version / ur_get_safety_mode / ur_get_safety_status / ur_get_robot_mode · Model (with a remote_control field) / serial / uptime / software version / safety mode / safety and robot status bits (which safety function fired, incl. violation / fault; the numeric limits themselves are not exposed by RTDE or the dashboard and must be read from PolyScope's Safety page) / run state Programs · ur_get_program_state / ur_load_program / ur_run_program / ur_stop_program / ur_pause_program / ur_list_programs / ur_upload_program / ur_download_program / ur_delete_program · Program state, load, run, stop, pause, SSH listing, and program file transfer over SFTP. The three transfer tools each verify what they did instead of trusting that a transfer was requested: upload reads the remote size back, download reads the local size back, and delete confirms the file is really gone; a download never silently overwrites (an existing target is renamed to .bak first, and a failed backup refuses the download). Their local paths are fenced to programDir — see "Local file access" below. ur_list_programs scans the real robot's /programs or the URSim ~/URSim_Linux-*/programs.* layout (override with programs_dir); it reports used_default_password when no password was given and the host_key_policy in force, and an optional known_hosts file switches SSH to strict `RejectPolicy` host-key verification (a missing file is an error, never a silent fallback). Run/stop/pause check the controller's reply and report the run state Operator messaging · ur_popup / ur_close_popup / ur_add_log · Talk to the person standing at the robot: show a modal popup on the teach pendant, close it, and write one line into the controller's own log history. None of them moves the arm, so none is behind the approval gate; the text is injection-checked (dashboard parses its commands line by line, so a newline would inject another command) and capped at 300 characters. Note that ur_add_log leaves a permanent trace on the controller — that is the point of it, so do not write secrets there Modbus · ur_modbus_add_signal / ur_modbus_delete_signal / ur_modbus_get_signal_status / ur_modbus_send_custom_command / ur_modbus_set_output_register / ur_modbus_set_output_signal / ur_modbus_set_runstate_dependent_choice / ur_modbus_set_signal_update_frequency · Fieldbus I/O: add/delete the signals the controller supervises, read one back, set output registers and digital outputs, send a raw Modbus command, and tune a signal's run-state behaviour and refresh rate. ⚠️ The vendored urScript.py's eight modbus_* methods are stubs (raise NotImplementedError), so these build the URScript themselves. ⚠️ Modbus writes get no response — all these tools can prove is that the URScript *ran on the controller* (start/finish sentinels), never that the PLC acted, so every reply carries fieldbus_confirmed: false; only ur_modbus_get_signal_status reads a value back (through an output register, behind its own sentinels). Signal names are whitelist-checked — they are interpolated into a URScript string literal, so a quote or a newline would inject script — and a read must declare kind because URScript is strongly typed (a digital signal returns a bool, a register an int, and one script cannot safely write both). The five write-side tools are behind the approval gate; the read and the two signal-configuration tools are not Named poses · ur_save_pose / ur_get_pose / ur_list_poses / ur_delete_pose · A local table of named poses — one JSON file per pose, /.json, default /nonead-ur/poses: save the current pose (or explicit numbers), read one back, list them, delete one. The name becomes a file name, so it is whitelist-checked (Unicode letters/digits are fine, so Chinese names work; path separators, Windows-illegal characters, reserved device names and leading/trailing whitespace are refused), and the fence is applied after realpath — the same host-fences-it / worker-writes-it split as the program files. Writes are atomic (tmp + os.replace) and are then read back and compared; VERIFY_FAILED means do not trust the file. A read separates "never saved" (`NOT_FOUND`) from "saved but corrupt" (`CORRUPT`) — conflating them would send you off to re-save something that already exists. One corrupt entry never breaks a listing: it is reported per entry, with its file name in broken. ⚠️ There is deliberately no "move to named pose": the numbers would be invisible to the safety gate and the approval gate until after the call, so the safe flow is two steps — ur_get_pose(name) then ur_movel(pose, …) — which puts the real numbers in front of both gates Registers · ur_get_int_register / ur_get_double_register / ur_get_bit_register · Read Int / Double / Bool registers Health · ur_ping · Check worker / Python / URBasic readiness without a robot I/O · ur_get_digital_in / ur_set_digital_out / ur_get_digital_in_bits / ur_get_digital_out_bits / ur_get_analog_in / ur_set_analog_out / ur_get_tool_analog_in · Digital in/out (incl. bit-mask reads, and which="tool" for the tool-flange I/O) and standard analog in/out (in engineering units: URScript's set_analog_out takes a relative level [0,1], and full_scale may be set to 20 for a current-domain port). Reading a tool digital input is sentinel-verified — if the controller never reports the script finished, the tool refuses to give a number instead of reporting one it cannot vouch for Tool config · ur_set_tool_voltage / ur_set_tcp / ur_set_payload / ur_set_payload_inertia / ur_set_gravity / ur_zero_ftsensor / ur_set_tool_output_mode / ur_set_tool_communication · Tool voltage (0/12/24 V), TCP, payload mass / centre of gravity, mass + CoG + inertia matrix in one call (set_target_payload, PolyScope 5.10+; unlike set_payload it does not reset the inertia matrix and leave the three parameters inconsistent), gravity direction (for non-horizontal mounting), force/torque sensor zeroing, tool output mode (normal / power dual-pin), tool serial port (TCI/RS-485 — ⚠️ enabling it disables the tool analog inputs). ⚠️ These are settings with no read-back channel: a successful reply says 「已下发(未校验)」 and carries verified: false, because all the plugin can prove is that the script reached the controller — UR accepts URScript only in remote-control mode and silently discards it otherwise Control mode / power / safety · ur_set_freedrive / ur_set_teach_mode / ur_power_on / ur_power_off / ur_brake_release / ur_unlock_protective_stop / ur_shutdown · Freedrive / teach mode (move the arm by hand), power on / off / brake release (⚠️ the arm may fall under gravity), protective-stop unlock only (no power-on and no brake release — that is what distinguishes it from ur_reset_error), controller shutdown Force control · ur_force_mode / ur_end_force_mode / ur_force_mode_settings / ur_move_force · Force Mode: the arm becomes compliant along/about the selected axes and keeps applying the requested force/torque. Arguments match the manual one by one — task_frame / selection_vector (1 = compliant) / wrench / type (1–3) / limits (compliant axes = max TCP speed, stiff axes = max deviation) / damping / gain_scaling. The script inserts the manual's recommended sleep(0.02) before entering force mode; exit with ur_end_force_mode. ⚠️ damping/gain_scaling cannot be read back from the controller, so the tool reports what it set instead of pretending to read the current value. `ur_move_force` is the assembly move built on top of it: one URScript program does force_mode(...) → movel(...) → stopl(...) → end_force_mode(), so the arm advances toward the target and stops at the workpiece. ⚠️ It is not the vendored move_force_2stop, which streams the wrench through RTDE input registers every 8 ms — this plugin deliberately has no send recipe, so those fields cannot be claimed here (claiming one makes the controller throw and kills the RTDE thread). Instead the whole motion runs on the controller, the start/finish sentinels prove it ran, and the actual TCP pose before/after is read back over read-only RTDE: "stopped short of the target" then means the controller could not advance further (which is what force assembly wants) — but it does not prove the force actually reached the workpiece, so the reply also carries stopped_due_to_safety and leaves the distinction between "held back by the force limit" and "stopped by safety" to you Velocity control · ur_speedj / ur_speedl / ur_stopj / ur_stopl / ur_wait_steady · Joint/TCP velocity commands (speedj / speedl) and the matching decelerations (stopj / stopl). ⚠️ Velocity commands are open-ended: with t=0 (the default) the function returns once the target speed is reached while the arm keeps moving — always finish with a stop* or ur_wait_steady. ur_wait_steady polls the RTDE speed fields rather than using URScript's is_steady() (which is documented to return false in force/teach mode) Live telemetry · ur_get_runtime_telemetry / ur_get_robot_voltage / ur_get_robot_current / ur_get_joint_temperatures / ur_get_speed_scaling / ur_get_tcp_force / ur_get_tool_telemetry · Joint currents/voltages/speeds, TCP speed and wrench, tool accelerometer, speed scaling, robot voltage/current, joint temperatures, tool current/voltage, I/O current. These fields are already in the 500 Hz RTDE stream, so reading them is free and needs no reconfiguration. Single-value reads and the one-shot ur_get_runtime_telemetry summary coexist: poll a single value when that is all you need, use the summary for diagnosis Conveyor · ur_get_conveyor / ur_set_conveyor_tick / ur_set_conveyor_tracking · Conveyor tick read / set, and starting or stopping linear or circular tracking. ur_get_conveyor self-verifies the script it depends on: the tick is only reported after the controller was observed finishing it. ⚠️ ur_set_conveyor_tracking is not hardware-verified — its signature and units follow the official manual, but tracking behaviour needs a real encoder-equipped conveyor to confirm; try it on URSim first Motion · ur_movej / ur_movel / ur_movep / ur_movec / ur_servoj / ur_move_optimized / ur_move_x / ur_move_y / ur_move_z / ur_move_tool_x / ur_move_tool_y / ur_move_tool_z · Joint-space / linear / path / circular motion (mode 0 interpolates orientation, mode 1 keeps it fixed) / continuous servo / OptiMove (optimovej/optimovel: jerk-limited, smoother, less vibration — ⚠️ its a/v are fractions of capability in (0,1], not rad/s or m/s) / axis-aligned motion: the _move_* tools move along base axes and the _move_tool_* ones along the current tool axes (converted in the worker with a rotation matrix, so they do not depend on URScript pose_trans). Defaults are the manual's (movej 1.4 / 1.05, movel 1.2 / 0.25); with t > 0 the duration takes precedence over a/v Motion planning · ur_motion_version / ur_set_speed_scaling / ur_get_freedrive_status · Set the Motion Version (manual chapter 14) and the jerk gain (0.01–1.0, which only affects jerk-limited profiles: version-2 movej/movel and optimovej/optimovel). Version 2 clamps velocities/accelerations to the hardware limits while planning and shrinks blend radii dynamically instead of skipping the whole move with an "Overlapping Blends" warning. ⚠️ Newer robot models and PolyScope X support only version 2; CB3 has no such setting. ⚠️ Neither setting has a read-back channel. ur_set_speed_scaling sets the speed slider (set_speed_slider: 0 = stopped, 1 = full speed) — the most direct way to slow everything down before handing over a delicate motion. Unlike the other settings it does have a read-back channel (target_speed_fraction), so it reports verified: true only when the read-back matches, and hardware_verified: false because that mapping has never been confirmed on a real robot. ur_get_freedrive_status returns how close the current pose is to a singularity during freedrive (0 normal / 1 near / 2 too close — not an on/off flag), which is what tells an operator to pick another path Drawing · ur_draw_circle / ur_draw_square / ur_draw_rectangle / ur_draw_star · Draw a circle / square / rectangle / pentagram. "Completed" is only reported after the script was actually observed running Script / emergency · ur_send_script / ur_reset_error · Send URScript (execution-verified: sentinels are injected inside the function body / between top-level statements and read back, so "sent" never masquerades as "ran"; with several functions and no visible call site it refuses to verify and returns verified: null) / reset errors

Every tool except connect takes an ip argument and requires that IP to be connected first.

Read-only 3D digital twin

The plugin also ships a read-only 3D digital twin of the robot, rendered with three.js: the entry is a card in the right sidebar's Start panel, directly below the "Workspace files / New terminal / Browser" cards, and selecting it fills the right sidebar's content area with the live 3D view. The view subscribes to the host's SSE stream (/twin/stream, one frame every 100 ms) and stays in sync with the live robot's joint poses, tool (TCP) coordinate frame and recent motion trajectory. The twin is strictly read-only — it never sends a command to the robot: it only subscribes to the host's read-only routes and renders what it receives. A toolbar offers a reset-view control plus isometric / front / side / top presets, and the camera and base grid are framed from the model's bounding box, so a UR3 and a UR20 are both framed correctly. The numeric panel also shows the dashboard-side state (safety mode, robot mode, program state, speed scaling, joint temperatures, bus voltage/current), taken from the same route on a slower cadence.

The feed does not stall while the robot moves: telemetry is pushed by the worker at 20 Hz, and every twin frame reads that push cache first — without touching the single-threaded command queue, which is what a running motion confirmation loop occupies. On the wire a push is simply a JSON line without id, so an older host treats it as an unmatched response and nothing breaks. Only when the cache is stale (>300 ms) or missing does the twin fall back to an explicit read, and twin reads still use a dedicated 2.5 s budget with `killOnTimeout:false` — a read-only visualisation read must never destroy the robot session (RTDE/Dashboard) just because it was busy. If a stall does happen, the stream emits stale events and the panel says "data stopped updating Ns · retrying" rather than leaving a silently frozen arm on screen. After a disconnect the stream reconnects indefinitely (exponential backoff capped at 30 s) and retries immediately when the window becomes visible again or the feed has been stalled too long. Set DSH_UR_TWIN_PUSH=0 to turn the push channel off for comparison.

It follows the desktop app's colour theme: the panel's DOM half is coloured with the host's design tokens (--dsw-alias-*), so the toolbar, HUD and status line flip automatically with whatever theme is selected under Settings → Appearance (and with third-party themes) — no JS needed. The base grid lines and the fallback arm's material colour inside the WebGL canvas are repainted on a theme change by src/client/theme.js, which watches body[data-ds-dark-theme] (CSS cannot reach values inside WebGL). Theme detection only trusts that attribute and never falls back to the system preference.

Host-side routes (all fenced to loopback callers):

Route · Description GET /dsh-nonead-ur/twin/stream[?ip=] · SSE stream (what the client uses by default): one full state frame every 100 ms, plus stale (this frame's read has not returned yet) and frame-error (this frame failed) events GET /dsh-nonead-ur/twin/state[?ip=][&detail=1] · Live pose (joint angles / TCP / model). detail=1 adds the dashboard-side state (safety mode, run state, speed scaling, joint temperatures/currents); the pose channel keeps polling independently and a failing detail query never takes it down GET /dsh-nonead-ur/twin/asset?model= · GLB mesh, with a content-hash ETag + immutable and If-None-Match → 304 support GET /dsh-nonead-ur/twin/models · The model list actually present locally

Failure responses carry a **machine-rea