NorrDog Studio Connector
Write Python for the NorrBot Studio robot dog in VS Code instead of the browser.
Press Connect to Desktop, run a .py file, and the same norrdog SDK
students already use in the Code Studio drives either the simulator running in
their browser or a real Unitree Go2 over rosbridge — except that now the code
runs in their own Python, so pip, breakpoints, NumPy, OpenCV and
frame.save("shot.jpg") all work.
Currently supports the Robot Dog (Unitree Go2). The robot picker is where
future platforms will appear.
How it fits together
VS Code the desktop
┌────────────────────────┐
│ NorrDog panel │
│ Robot Robot Dog ▾ │
│ Target Simulator ▾ │ ws://127.0.0.1:8010 ┌──────────────────┐
│ [ Connect to Desktop ]│◄───────────────────────────►│ Studio page │
│ │ │ window.Norrdog │
│ link hub ────────────┤ └──────────────────┘
└────────┬───────────────┘ ws://<ros-laptop>:9090 ┌──────────────────┐
│ ◄───────────────────────────►│ rosbridge → Go2 │
│ 127.0.0.1:8011 └──────────────────┘
▼
┌────────────────────────┐
│ your program.py │
│ from norrdog import * │
└────────────────────────┘
The extension hosts a small hub. The Studio page dials it (a browser can open a
socket but not accept one); your Python dials it too, over plain TCP so the SDK
needs nothing but the standard library. Op names are the ones in
go2_simulator/web/code.js's BRIDGE table, which is what norrdog.py calls
inside Pyodide — so a program written for the Code Studio runs here
unchanged.
Getting started
- Install the extension.
- Open the NorrDog Studio view in the activity bar and press
Connect to Desktop, then Open Studio. Sign in as usual — it is the
live site at
academy.norrspect.com, just served from your own machine.
- The status dot turns green once the page attaches.
- Run NorrDog: Create Example Project, then press ▶ on
01_motion.py.
Nothing else to install. No simulate.py, no local copy of the Studio.
from norrdog import dog, wait
dog.stand()
for _ in range(4):
dog.move(1.0)
dog.turn(90)
frame = dog.camera("front")
frame.save("shot.jpg")
print(frame.dominant_color())
Why the Studio is served from 127.0.0.1
The extension mirrors the hosted Studio at http://127.0.0.1:8009 and opens
that, rather than sending you to https://academy.norrspect.com directly. The
content is identical and live — it is fetched from the site on every request.
This is not a preference. A page served from a public https origin is not
allowed to open ws://127.0.0.1: Chrome refuses it with
net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS
and granting the localNetworkAccess permission does not lift it. (Mixed
Content is not the rule involved — ws://127.0.0.1 is a potentially
trustworthy URL, so that check passes. Local Network Access is a separate,
newer rule about public sites reaching your machine.) Serving the page from
loopback makes the whole exchange local-to-local, which no browser objects to.
Two details make the mirror work rather than merely load: absolute references
to the live origin are rewritten so Supabase at /sb stays same-origin and
passes the site's own connect-src 'self' CSP, and binary assets (the ~25 MB
of robot meshes) stream through with their compression intact.
To point at a different Studio, set norrdog.studioUrl. If you are running
simulate.py yourself, set it to http://127.0.0.1:8000 — the extension
detects a loopback address and skips the mirror.
Keep the Studio window beside VS Code, not behind it
The simulator advances motion inside the browser's requestAnimationFrame
loop, and browsers stop that loop for a window that is hidden, minimised or
completely covered. So with the Studio buried behind VS Code, dog.move() has
nothing to run in.
Arrange the two side by side. The panel says "window hidden, motion paused"
when the loop has stopped, and a motion command fails straight away with an
explanation instead of hanging until it times out. Sensor reads and
dog.camera() are plain function calls and keep working either way.
Only one Studio page can hold the link at a time — the most recent one to
connect wins. If several tabs are open, close the extras so commands go to the
window you are actually watching.
What you get
|
|
| Connect to Desktop |
One button, at the top of the panel. Simulator or real robot. |
| Run File on Robot |
▶ in the editor title bar for any .py. Runs in your interpreter, in a real terminal. |
| Stop |
Kills the program and halts the dog. Ctrl+C in the terminal does the same. |
| Camera |
Live preview in the panel at 4 fps, or NorrDog: Open Camera Preview for a full-size frame. |
| Activity log |
Link state, MQTT traffic, program start/stop. |
| Snippets |
ndimport, ndsquare, ndcapture, ndframes, nduntil, ndfind, ndvelocity, ndsubscribe. |
The SDK
Identical to the browser's norrdog, method for method:
motion move turn strafe velocity jump sit stand stop wait
sensors imu pose twist ultrasonic battery state
camera camera() -> Image
Image: save to_bytes array show dominant_color brightness find format
mqtt publish subscribe get_message
world set_terrain reset
Four differences, all of them because your code is no longer inside a browser:
- No
await, and no AST rewriting. The browser version rewrites your
program so synchronous-looking code can drive an async simulator living in the
render loop. Here the calls really are synchronous.
time.sleep() is just time.sleep(). There is no render loop to freeze,
so nothing is rewritten. wait() still exists and is interruptible by Stop.
Image.save() writes a real file, and Image.array() gives you a NumPy
RGB array (needs pillow and numpy).
dog.frames(seconds, fps) is new — a generator that yields frames, which
is the short way to pull a clip off the camera.
Stop raises RobotStopped in your program, exactly as the Stop button does in
the Code Studio.
MQTT reaches across both
The hub carries the studio's virtual broker, with the same +/# wildcards.
A Blockly script in the Studio page and a Python file in your editor share the
same topics in both directions, and everything published from VS Code shows up
in the Studio's MQTT panel.
Installing the SDK into your own Python
Run File on Robot needs no install — it injects the bundled copy on
PYTHONPATH for that one run. Install it properly when you want import norrdog to work everywhere else: a plain terminal, a notebook, Code Runner,
pytest, or another editor.
pip install norrdog # https://pypi.org/project/norrdog/
pip install "norrdog[vision]" # + Pillow/NumPy for Image.array()
Install it into the same interpreter VS Code has selected, so the editor and
your terminal agree. Inside a virtualenv, activate it first.
Once installed, nothing else is needed — the SDK dials 127.0.0.1:8011 by
default, which is where the extension listens:
python3 my_program.py
Set NORRDOG_LINK=host:port only if you changed norrdog.clientPort. The link
still has to be connected either way — the extension owns the hub.
NorrDog: Show Python SDK Path prints the bundled copy's path and copies it
to the clipboard, for the PYTHONPATH route if you would rather not install.
Settings
| Setting |
Default |
|
norrdog.robot |
dog |
Which robot. Only the dog today. |
norrdog.target |
simulator |
simulator or robot. |
norrdog.studioUrl |
https://academy.norrspect.com |
The Studio to mirror and open. |
norrdog.serveStudioLocally |
true |
Mirror it on loopback. Required for any https Studio. |
norrdog.proxyPort |
8009 |
Port the mirrored Studio is served on. |
norrdog.rosbridgeUrl |
ws://127.0.0.1:9090 |
Used when the target is robot. |
norrdog.linkPort |
8010 |
Loopback port the Studio page dials. |
norrdog.clientPort |
8011 |
Loopback port the Python SDK dials. |
norrdog.pythonPath |
(empty) |
Interpreter for Run. Empty uses the Python extension's. |
norrdog.autoConnect |
true |
Start the link when the workspace imports norrdog. |
norrdog.openStudioOnConnect |
true |
Offer to open a Studio page if none attaches. |
Both listeners bind to 127.0.0.1 only — nothing on your network can reach
them.
Real robot
Selecting Real robot talks to rosbridge_websocket with the topology
LIVE_MODE_PLAN.md §4.1 specifies:
VS Code --/cmd_vel_teleop--> [cmd_vel_watchdog_node] --/cmd_vel--> driver --> Go2
<--camera/image_raw------------------------------------------------
It never publishes to /cmd_vel directly, so the dead-man's switch always stays
in the path. A move streams its twist at 10 Hz for the whole travel time —
publishing once and sleeping (as live-link.js does) lets the 1.0 s watchdog
stop the dog early on anything longer than a second.
The driver exposes /cmd_vel_teleop and camera/image_raw only, so
move, turn, strafe, velocity, stop, wait and camera work, and
pose/twist/imu work if something is publishing /odom or /imu.
Everything else — jump, sit, stand, ultrasonic, battery, state,
label, set_terrain, reset — raises an error saying so rather than
pretending. Closing the link publishes a zero twist first.
Turn rate is still TURN_RATE_DEG_S = 45 in src/link/backends/rosbridge.ts,
carried over from live-link.js and not yet measured on hardware. Tune it
once the Go2 is in the room.
Building
cd vscode-extension
npm install
npm run compile # bundles to dist/extension.js
npm test # typecheck + both end-to-end suites
npx vsce package # -> norrdog-studio-connector-0.1.0.vsix
Press F5 in VS Code to launch an Extension Development Host.
Tests
Neither suite mocks the parts that matter. test/e2e.mjs runs the real hub and
the real Python SDK in a real interpreter against a stand-in Studio page;
test/rosbridge.mjs does the same against a stand-in rosbridge, and checks the
twist stream, the raw-pixels-to-PNG path and the colour classifier against
runtime.js's exact thresholds.
The browser half
go2_simulator/web/desktop-link.js is the other end of the link, loaded by both
index.html and code.html. It retries the loopback port quietly and is a
complete no-op when the extension is not running — the same pattern main.js
already uses for the camera bridge on port 8001.