openvibers/openvibe.link Commits

install.sh --setup: a card made on openvibe.bot/setup/sd-card adds its robot with no code (v0.13.1) (#53)

OP OpenVibers committed 20 days ago · fd2e71fdb9 · 4 files, +86 −13

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex 3b5c378..541dc37 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -5,8 +5,8 @@
"wave": "T15",
"plan": "Plan track T15 (Bot complete) and ADR-043 (Bot: devices, pairing, control and safety); the computer plugin and the job worker serve OpenVibe.Actor and the Codes job runner (tracks T14/T17)",
"kind": "client",
-- "version": "0.13.0",
-- "release": "v0.13.0, published 2026-10-20 as a GitHub release: binaries for linux amd64/arm64/armv7, darwin amd64/arm64 and windows amd64, the plugins tarball, install.sh and SHA256SUMS",
++ "version": "0.13.1",
++ "release": "v0.13.1, published 2026-10-24 as a GitHub release (install.sh --setup: a card made on openvibe.bot/setup/sd-card adds its robot with no code): binaries for linux amd64/arm64/armv7, darwin amd64/arm64 and windows amd64, the plugins tarball, install.sh and SHA256SUMS",
"runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library",
"database": "none",
"deployed": false,
ddiff --git a/docs/install.md b/docs/install.md
iindex 68efeef..b0f9d38 100644
---- a/docs/install.md
++++ b/docs/install.md
@@@ -38,6 +38,21 @@ The code lasts 15 minutes; a denied or expired one adds nothing, and running the
already-paired machine keeps its pairing. `--driver` still overrides what was found, and `--no-claim` installs without
asking for a code. To drive the robot from your Live chat, use **Use it in my OpenVibe.Live chat** on the approval page.
++## From a blank microSD card (no screen, no keyboard)
++
++[openvibe.bot/setup/sd-card](https://openvibe.bot/setup/sd-card) makes a Raspberry Pi card's first-boot files: you
++write Raspberry Pi OS Lite (64-bit) with Raspberry Pi Imager, copy the page's `user-data` and `network-config` onto the
++card's `bootfs` drive, and switch the robot on. On first boot the Pi joins your Wi-Fi, turns on SSH, and runs
++
++```sh
++curl -fsSL https://openvibe.bot/install | sh -s -- --setup osk_…
++```
++
++with the card's setup key (`OPENVIBE_SETUP_KEY` works too). openvibe.bot approves that claim at once for the person who
++made the files, with the name, model, who may drive and Live they chose there, so no code is shown and nobody needs to
++be at the robot. A key adds one robot and lasts 7 days; a retry before the robot has paired pairs the same robot again.
++Its output is in `journalctl -u openvibe-setup` on the Pi.
++
## Quick start with a code from openvibe.bot (Linux, Raspberry Pi, macOS)
1. On openvibe.bot, add a robot. You get a code like `ABCD-1234` (valid 10 minutes) and the installer command with
ddiff --git a/install/install.sh b/install/install.sh
iindex 300c25d..8b71c15 100755
---- a/install/install.sh
++++ b/install/install.sh
@@@ -5,6 +5,7 @@
# curl -fsSL https://openvibe.bot/install | sh -s -- --robot <rob_…> --code <CODE> [--driver adeept|adeept-mecanum|cozmo|looi|ros2|ros2-holonomic|none]
# curl -fsSL https://openvibe.bot/install | sh -s -- --network <URL> --pairing <pair_…> --code <CODE> [--driver …]
# curl -fsSL https://openvibe.bot/install | sh -s -- --network <URL> --pairing <pair_…> --code <CODE> --worker
++# curl -fsSL https://openvibe.bot/install | sh -s -- --setup <osk_…>
#
# Detects the OS and CPU, downloads the openvibe-node binary and the plugin bundle (checking their SHA-256), creates
# a Python virtual environment for the plugins, disables the Adeept kit's stock control server if it finds it,
@@@ -17,6 +18,10 @@
# openvibe.bot/link (signed in), sees what was found and approves it; the robot is made, this machine pairs with it,
# and the installer checks the driver and the camera are running and prints the robot's panel.
#
++# With --setup (the last line) it is that same command with the setup key of a card made on openvibe.bot/setup/sd-card:
++# the card's first boot runs it with nobody at a screen, so openvibe.bot approves the claim at once for the key's
++# owner, with the robot's name, model and the rest chosen when the card files were made. No code is shown or needed.
++#
# Options:
# --robot ROBOT the rob_… id of the robot to pair with (from openvibe.bot). A driver kind here (adeept, …) is the
# old meaning of --robot: still accepted, but deprecated; use --driver.
@@@ -36,6 +41,8 @@
# (the one that ran sudo), never root, and its folders are your home. Linux only; not with --worker.
# --no-service install and pair, but do not install the service.
# --no-claim with no robot and no code: install without pairing (no code is asked for on openvibe.bot).
++# --setup KEY the osk_… setup key from card files made on openvibe.bot/setup/sd-card (or OPENVIBE_SETUP_KEY):
++# the claim is approved by the key, with no code to enter. Used on its own.
# --local DIR install from DIR (openvibe-node-<os>-<arch> and openvibe-node-plugins.tar.gz) instead of
# downloading.
# --version TAG release to download (default: latest).
@@@ -67,6 +74,7 @@ WORKER=0
COMPUTER=""
CLAIM_OK=1
DRIVER_GIVEN=0
++SETUP="${OPENVIBE_SETUP_KEY:-}"
# The OpenVibe.Codes release whose `openvibe-codes job` the worker runs (protocol [email protected]).
CODES_VERSION="${OPENVIBE_CODES_VERSION:-v0.3.1}"
CODES_DIR=/opt/openvibe-codes
@@@ -74,7 +82,7 @@ CODES_DIR=/opt/openvibe-codes
usage() {
echo "usage: install.sh [--robot rob_… | --network URL --pairing pair_…] [--code CODE | CODE] [--name NAME]"
echo " [--driver adeept|adeept-mecanum|cozmo|looi|ros2|ros2-holonomic|none] [--worker]"
-- echo " [--computer observe|ask|trusted|full] [--no-service] [--no-claim]"
++ echo " [--computer observe|ask|trusted|full] [--no-service] [--no-claim] [--setup osk_…]"
echo " [--local DIR] [--version TAG]"
echo " curl -fsSL https://openvibe.bot/install | sh (finds the kit, then shows a code to approve on openvibe.bot/link)"
echo " curl -fsSL https://openvibe.bot/install | sh -s -- --robot rob_… --code ABCD-1234 --driver adeept"
@@@ -109,6 +117,8 @@ while [ $# -gt 0 ]; do
--computer=*) COMPUTER="${1#*=}"; shift ;;
--no-service) SERVICE=0; shift ;;
--no-claim) CLAIM_OK=0; shift ;;
++ --setup) SETUP="${2:-}"; shift 2 ;;
++ --setup=*) SETUP="${1#*=}"; shift ;;
--local) LOCAL="${2:-}"; shift 2 ;;
--version) TAG="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
@@@ -124,6 +134,12 @@ case "$PAIRING" in ""|pair_*) ;; *) [ -z "$CODE" ] || die "--pairing must be a p
if [ -n "$PAIRING" ] && [ -z "$NETWORK" ]; then die "--pairing needs --network, the OpenVibe.Network URL"; fi
if [ -n "$NETWORK" ] && [ -z "$CODE" ]; then die "--network needs the pairing code (--code)"; fi
case "$NETWORK" in ""|https://*|http://localhost*|http://127.0.0.1*) ;; *) die "--network must be an https:// URL" ;; esac
++# A card's setup key: the one command below, approved by the key (openvibe.bot/setup/sd-card), so it stands alone.
++if [ -n "$SETUP" ]; then
++ printf '%s' "$SETUP" | grep -Eq '^osk_[A-Za-z0-9_-]{43}$' || die "--setup takes the osk_… key from your card files (openvibe.bot/setup/sd-card)"
++ [ -z "$CODE$ROBOT_ID$NETWORK$PAIRING" ] || die "--setup is used on its own (no --robot, --code, --network or --pairing)"
++ if [ "$CLAIM_OK" = 0 ] || [ "$WORKER" = 1 ] || [ -n "$COMPUTER" ]; then die "--setup cannot be used with --no-claim, --worker or --computer"; fi
++fi
# No robot, no code, no Network pairing, not a worker or a computer: the one command (openvibe.bot/link, see the top).
CLAIM=0
if [ "$CLAIM_OK" = 1 ] && [ -z "$CODE$ROBOT_ID$NETWORK$PAIRING" ] && [ "$WORKER" = 0 ] && [ -z "$COMPUTER" ]; then CLAIM=1; fi
@@@ -178,6 +194,7 @@ claim_robot() {
OV_HW_MODEL="$({ tr -d '\0' < /proc/device-tree/model; } 2>/dev/null || true)" \
OV_HW_OS="$( (. /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-}") || true)" \
OV_HW_ARCH="$(uname -m)" OV_HW_KIT="${KIT:-}" OV_HW_KIT_NAME="${KIT_NAME:-}" OV_HW_FOUND="${FOUND:-}" OV_HW_CAMERA="${CAMERA:-}" \
++ OV_SETUP_KEY="${SETUP:-}" \
python3 - "${OPENVIBE_SERVER:-https://openvibe.bot}" "$_answer" "${NAME:-$(hostname)}" <<'PY' || return 1
import json, os, re, subprocess, sys, time, urllib.error, urllib.request
server, out, host = sys.argv[1].rstrip('/'), sys.argv[2], sys.argv[3]
@@@ -194,16 +211,32 @@ def call(method, path, body=None, headers=None):
def die(msg):
print(f'\nopenvibe-node: error: {msg}', file=sys.stderr)
sys.exit(1)
++setup_key = os.environ.get('OV_SETUP_KEY', '')
++body = {'hostname': host, 'hardware': hw}
++if setup_key:
++ body['setup_key'] = setup_key
try:
-- c = call('POST', '/api/v1/claims', {'hostname': host, 'hardware': hw})
++ c = call('POST', '/api/v1/claims', body)
++except urllib.error.HTTPError as e:
++ # openvibe.bot says why (a used or old setup key, too many tries): show that, not just the status.
++ detail = ''
++ try:
++ j = json.load(e)
++ detail = j.get('detail') or j.get('error') or ''
++ except Exception: # noqa: BLE001 - no JSON body: the status is all there is
++ pass
++ die(f'{server} refused: {detail}' if detail else f'could not ask {server} for a code: {e}')
except Exception as e: # noqa: BLE001 - say what happened and stop
die(f'could not ask {server} for a code: {e}')
--print(f"\n Approve this robot on openvibe.bot (signed in as you):\n open {c['verification_url']}\n code {c['user_code']}\n")
--try:
-- subprocess.run(['qrencode', '-t', 'ANSIUTF8', '-m', '2', c['verification_url_complete']], check=True, timeout=10)
-- print(' (or scan this with your phone)\n')
--except Exception: # noqa: BLE001 - no QR, the link is enough
-- print(f" or open {c['verification_url_complete']}\n")
++if c.get('approved'):
++ print("\n Adding this robot to openvibe.bot with your card's setup key (no code needed).\n")
++else:
++ print(f"\n Approve this robot on openvibe.bot (signed in as you):\n open {c['verification_url']}\n code {c['user_code']}\n")
++ try:
++ subprocess.run(['qrencode', '-t', 'ANSIUTF8', '-m', '2', c['verification_url_complete']], check=True, timeout=10)
++ print(' (or scan this with your phone)\n')
++ except Exception: # noqa: BLE001 - no QR, the link is enough
++ print(f" or open {c['verification_url_complete']}\n")
interval, deadline = max(2, int(c.get('interval') or 3)), time.time() + 16 * 60
sys.stdout.write(' waiting for you to approve it')
sys.stdout.flush()
ddiff --git a/scripts/test-install-claim.sh b/scripts/test-install-claim.sh
iindex 1a75368..466cdc9 100755
---- a/scripts/test-install-claim.sh
++++ b/scripts/test-install-claim.sh
@@@ -56,12 +56,14 @@ class H(BaseHTTPRequestHandler):
body = self.rfile.read(int(self.headers.get('Content-Length') or 0))
open(f'{work}/claim-body.json', 'wb').write(body)
if self.path != '/api/v1/claims': return self.send(404, {})
-- self.send(201, {'id': 'clm_01JZTEST', 'user_code': 'KQ7M-2XRD', 'secret': 's3cret', 'interval': 1, 'expires_at': '2099-01-01T00:00:00Z',
++ if mode == 'spent':
++ return self.send(403, {'code': 'bot.setup_key_invalid', 'detail': "this card's setup key was used, revoked or is more than 7 days old"})
++ self.send(201, {**({'approved': True} if mode == 'setup' else {}), 'id': 'clm_01JZTEST', 'user_code': 'KQ7M-2XRD', 'secret': 's3cret', 'interval': 1, 'expires_at': '2099-01-01T00:00:00Z',
'verification_url': 'https://openvibe.bot/link', 'verification_url_complete': 'https://openvibe.bot/link?code=KQ7M-2XRD'})
def do_GET(self):
if self.path != '/api/v1/claims/clm_01JZTEST' or self.headers.get('X-OV-Claim-Secret') != 's3cret': return self.send(404, {})
polls.append(1)
-- self.send(200, {'state': 'pending'} if len(polls) == 1 else answers[mode])
++ self.send(200, {'state': 'pending'} if len(polls) == 1 and mode != 'setup' else answers['approved' if mode == 'setup' else mode])
srv = HTTPServer(('127.0.0.1', 0), H)
open(f'{work}/port', 'w').write(str(srv.server_port))
srv.serve_forever()
@@@ -106,4 +108,27 @@ if claim >/dev/null; then fail "a name that could break out of the shell's quote
grep -q "unexpected ROBOT_NAME" "$work/err" || fail "an odd answer says which value: $(cat "$work/err")"
stop
--echo "install.sh one command (detection, claim): ok"
++# A card's setup key (openvibe.bot/setup/sd-card): sent with the claim, approved at once, no code shown.
++key="osk_$(printf 'k%.0s' $(seq 1 43))"
++serve setup
++out="$(OPENVIBE_INSTALL_CLAIM_ONLY=1 OPENVIBE_SERVER="http://127.0.0.1:$(cat "$work/port")" sh "$installer" --name mecanum --setup "$key" 2>"$work/err")" ||
++ fail "the setup-key claim failed: $(cat "$work/err")"
++stop
++has "$out" "ROBOT_ID=rob_01JZTEST"
++has "$out" "CODE=ABCD-1234"
++printf '%s\n' "$out" | grep -q "setup key (no code needed)" || fail "it says the key added it"
++if printf '%s\n' "$out" | grep -q "code KQ7M-2XRD"; then fail "no code is shown with a setup key"; fi
++python3 -c 'import json,sys; b=json.load(open(sys.argv[1])); assert b["setup_key"] == sys.argv[2] and b["hostname"] == "mecanum", b' "$work/claim-body.json" "$key" ||
++ fail "the claim carries the setup key"
++
++serve spent
++if OPENVIBE_INSTALL_CLAIM_ONLY=1 OPENVIBE_SERVER="http://127.0.0.1:$(cat "$work/port")" sh "$installer" --setup "$key" >/dev/null 2>"$work/err"; then fail "a spent key was taken"; fi
++grep -q "used, revoked or is more than 7 days old" "$work/err" || fail "a refused key says why: $(cat "$work/err")"
++stop
++
++if OPENVIBE_INSTALL_CLAIM_ONLY=1 sh "$installer" --setup osk_short >/dev/null 2>"$work/err"; then fail "a malformed key was taken"; fi
++grep -q "takes the osk_" "$work/err" || fail "a malformed key says so: $(cat "$work/err")"
++if OPENVIBE_INSTALL_CLAIM_ONLY=1 sh "$installer" --setup "$key" --code ABCD-1234 >/dev/null 2>"$work/err"; then fail "--setup with a code was taken"; fi
++grep -q "on its own" "$work/err" || fail "--setup with a code says so: $(cat "$work/err")"
++
++echo "install.sh one command (detection, claim, setup key): ok"