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)
Changes
@@@ -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,@@@ -38,6 +38,21 @@ The code lasts 15 minutes; a denied or expired one adds nothing, and running thealready-paired machine keeps its pairing. `--driver` still overrides what was found, and `--no-claim` installs withoutasking 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@@@ -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=0COMPUTER=""CLAIM_OK=1DRIVER_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-codesusage() {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 pif [ -n "$PAIRING" ] && [ -z "$NETWORK" ]; then die "--pairing needs --network, the OpenVibe.Network URL"; fiif [ -n "$NETWORK" ] && [ -z "$CODE" ]; then die "--network needs the pairing code (--code)"; ficase "$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=0if [ "$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 1import json, os, re, subprocess, sys, time, urllib.error, urllib.requestserver, 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_keytry:-- 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 stopdie(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 * 60sys.stdout.write(' waiting for you to approve it')sys.stdout.flush()@@@ -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 quotegrep -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"
@@ -5,8 +5,8 @@
@@ -5,8 +5,8 @@
"wave": "T15",
"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)",
"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",
"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",
"runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library",
"database": "none",
"database": "none",
"deployed": false,
"deployed": false,
@@ -38,6 +38,21 @@ The code lasts 15 minutes; a denied or expired one adds nothing, and running the
@@ -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
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.
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)
## 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
1. On openvibe.bot, add a robot. You get a code like `ABCD-1234` (valid 10 minutes) and the installer command with
@@ -5,6 +5,7 @@
@@ -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 -- --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> [--driver …]
# curl -fsSL https://openvibe.bot/install | sh -s -- --network <URL> --pairing <pair_…> --code <CODE> --worker
# 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
# 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,
# a Python virtual environment for the plugins, disables the Adeept kit's stock control server if it finds it,
@@ -17,6 +18,10 @@
@@ -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,
# 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.
# 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:
# Options:
# --robot ROBOT the rob_… id of the robot to pair with (from openvibe.bot). A driver kind here (adeept, …) is the
# --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.
# old meaning of --robot: still accepted, but deprecated; use --driver.
@@ -36,6 +41,8 @@
@@ -36,6 +41,8 @@
# (the one that ran sudo), never root, and its folders are your home. Linux only; not with --worker.
# (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-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).
# --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
# --local DIR install from DIR (openvibe-node-<os>-<arch> and openvibe-node-plugins.tar.gz) instead of
# downloading.
# downloading.
# --version TAG release to download (default: latest).
# --version TAG release to download (default: latest).
@@ -67,6 +74,7 @@ WORKER=0
@@ -67,6 +74,7 @@ WORKER=0
COMPUTER=""
COMPUTER=""
CLAIM_OK=1
CLAIM_OK=1
DRIVER_GIVEN=0
DRIVER_GIVEN=0
+SETUP="${OPENVIBE_SETUP_KEY:-}"
# The OpenVibe.Codes release whose `openvibe-codes job` the worker runs (protocol [email protected]).
# The OpenVibe.Codes release whose `openvibe-codes job` the worker runs (protocol [email protected]).
CODES_VERSION="${OPENVIBE_CODES_VERSION:-v0.3.1}"
CODES_VERSION="${OPENVIBE_CODES_VERSION:-v0.3.1}"
CODES_DIR=/opt/openvibe-codes
CODES_DIR=/opt/openvibe-codes
@@ -74,7 +82,7 @@ CODES_DIR=/opt/openvibe-codes
@@ -74,7 +82,7 @@ CODES_DIR=/opt/openvibe-codes
usage() {
usage() {
echo "usage: install.sh [--robot rob_… | --network URL --pairing pair_…] [--code CODE | CODE] [--name NAME]"
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 " [--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 " [--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 (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"
echo " curl -fsSL https://openvibe.bot/install | sh -s -- --robot rob_… --code ABCD-1234 --driver adeept"
@@ -109,6 +117,8 @@ while [ $# -gt 0 ]; do
@@ -109,6 +117,8 @@ while [ $# -gt 0 ]; do
--computer=*) COMPUTER="${1#*=}"; shift ;;
--computer=*) COMPUTER="${1#*=}"; shift ;;
--no-service) SERVICE=0; shift ;;
--no-service) SERVICE=0; shift ;;
--no-claim) CLAIM_OK=0; shift ;;
--no-claim) CLAIM_OK=0; shift ;;
+ --setup) SETUP="${2:-}"; shift 2 ;;
+ --setup=*) SETUP="${1#*=}"; shift ;;
--local) LOCAL="${2:-}"; shift 2 ;;
--local) LOCAL="${2:-}"; shift 2 ;;
--version) TAG="${2:-}"; shift 2 ;;
--version) TAG="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
-h|--help) usage; exit 0 ;;
@@ -124,6 +134,12 @@ case "$PAIRING" in ""|pair_*) ;; *) [ -z "$CODE" ] || die "--pairing must be a p
@@ -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 "$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
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
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).
# No robot, no code, no Network pairing, not a worker or a computer: the one command (openvibe.bot/link, see the top).
CLAIM=0
CLAIM=0
if [ "$CLAIM_OK" = 1 ] && [ -z "$CODE$ROBOT_ID$NETWORK$PAIRING" ] && [ "$WORKER" = 0 ] && [ -z "$COMPUTER" ]; then CLAIM=1; fi
if [ "$CLAIM_OK" = 1 ] && [ -z "$CODE$ROBOT_ID$NETWORK$PAIRING" ] && [ "$WORKER" = 0 ] && [ -z "$COMPUTER" ]; then CLAIM=1; fi
@@ -178,6 +194,7 @@ claim_robot() {
@@ -178,6 +194,7 @@ claim_robot() {
OV_HW_MODEL="$({ tr -d '\0' < /proc/device-tree/model; } 2>/dev/null || true)" \
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_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_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
python3 - "${OPENVIBE_SERVER:-https://openvibe.bot}" "$_answer" "${NAME:-$(hostname)}" <<'PY' || return 1
import json, os, re, subprocess, sys, time, urllib.error, urllib.request
import json, os, re, subprocess, sys, time, urllib.error, urllib.request
server, out, host = sys.argv[1].rstrip('/'), sys.argv[2], sys.argv[3]
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):
@@ -194,16 +211,32 @@ def call(method, path, body=None, headers=None):
def die(msg):
def die(msg):
print(f'\nopenvibe-node: error: {msg}', file=sys.stderr)
print(f'\nopenvibe-node: error: {msg}', file=sys.stderr)
sys.exit(1)
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:
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
except Exception as e: # noqa: BLE001 - say what happened and stop
die(f'could not ask {server} for a code: {e}')
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
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.write(' waiting for you to approve it')
sys.stdout.flush()
sys.stdout.flush()
@@ -56,12 +56,14 @@ class H(BaseHTTPRequestHandler):
@@ -56,12 +56,14 @@ class H(BaseHTTPRequestHandler):
body = self.rfile.read(int(self.headers.get('Content-Length') or 0))
body = self.rfile.read(int(self.headers.get('Content-Length') or 0))
open(f'{work}/claim-body.json', 'wb').write(body)
open(f'{work}/claim-body.json', 'wb').write(body)
if self.path != '/api/v1/claims': return self.send(404, {})
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'})
'verification_url': 'https://openvibe.bot/link', 'verification_url_complete': 'https://openvibe.bot/link?code=KQ7M-2XRD'})
def do_GET(self):
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, {})
if self.path != '/api/v1/claims/clm_01JZTEST' or self.headers.get('X-OV-Claim-Secret') != 's3cret': return self.send(404, {})
polls.append(1)
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)
srv = HTTPServer(('127.0.0.1', 0), H)
open(f'{work}/port', 'w').write(str(srv.server_port))
open(f'{work}/port', 'w').write(str(srv.server_port))
srv.serve_forever()
srv.serve_forever()
@@ -106,4 +108,27 @@ if claim >/dev/null; then fail "a name that could break out of the shell's quote
@@ -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")"
grep -q "unexpected ROBOT_NAME" "$work/err" || fail "an odd answer says which value: $(cat "$work/err")"
stop
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"