openvibers/openvibe.link Commits
Installer, release packaging, and stdout reserved for the plugin protocol
Changes
nnew file mode 100755@@@ -0,0 +1,202 @@++#!/bin/sh++# OpenVibe Node installer.++#++# curl -fsSL https://openvibe.bot/install | sh -s -- <CODE> [--robot adeept|adeept-mecanum|cozmo|none]++#++# 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,++# installs the system service and pairs the device with <CODE>. Run it again to upgrade; the credential is kept.++#++# Options:++# --robot KIND adeept (ordinary wheels), adeept-mecanum, cozmo (bridge), none (dry run). Default: none.++# --no-service install and pair, but do not install the service.++# --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).++# Environment:++# OPENVIBE_NODE_BASE download base URL (default: GitHub releases of OpenVibers/OpenVibe.Node).++# OPENVIBE_SERVER OpenVibe.Bot origin (default https://openvibe.bot).++set -eu++++REPO_URL="https://github.com/OpenVibers/OpenVibe.Node"++CODE=""++ROBOT="none"++SERVICE=1++LOCAL=""++TAG="latest"++++usage() {++ echo "usage: install.sh [CODE] [--robot adeept|adeept-mecanum|cozmo|none] [--no-service] [--local DIR] [--version TAG]"++ echo " curl -fsSL https://openvibe.bot/install | sh -s -- <CODE> --robot adeept"++}++say() { printf '%s\n' "openvibe-node: $*"; }++die() { printf '%s\n' "openvibe-node: error: $*" >&2; exit 1; }++++while [ $# -gt 0 ]; do++ case "$1" in++ --robot) ROBOT="${2:-}"; shift 2 ;;++ --robot=*) ROBOT="${1#*=}"; shift ;;++ --no-service) SERVICE=0; shift ;;++ --local) LOCAL="${2:-}"; shift 2 ;;++ --version) TAG="${2:-}"; shift 2 ;;++ -h|--help) usage; exit 0 ;;++ -*) die "unknown option $1" ;;++ *) CODE="$1"; shift ;;++ esac++done++++case "$ROBOT" in adeept|adeept-mecanum|cozmo|none) ;; *) die "--robot must be adeept, adeept-mecanum, cozmo or none" ;; esac++++# Root for the service, /usr/local/bin and /etc.++SUDO=""++if [ "$(id -u)" -ne 0 ]; then++ command -v sudo >/dev/null 2>&1 || die "run as root (sudo is not installed)"++ SUDO="sudo"++fi++++# ---- platform ----++OS="$(uname -s)"++case "$OS" in++Linux) OS=linux; BIN_DIR=/usr/local/bin; STATE=/var/lib/openvibe-node; CONF=/etc/openvibe-node ;;++Darwin) OS=darwin; BIN_DIR=/usr/local/bin; STATE="/Library/Application Support/OpenVibe Node"; CONF="$STATE" ;;++*) die "unsupported OS $OS (on Windows, download openvibe-node-windows-amd64.exe from $REPO_URL/releases)" ;;++esac++ARCH="$(uname -m)"++case "$ARCH" in++x86_64|amd64) ARCH=amd64 ;;++aarch64|arm64) ARCH=arm64 ;;++armv7l|armv7*) ARCH=armv7 ;;++armv6l) die "ARMv6 (Raspberry Pi Zero / 1) is not supported; use a Pi 3, 4, 5 or Zero 2 with a 64-bit OS" ;;++*) die "unsupported CPU $ARCH" ;;++esac++if [ "$OS" = darwin ] && [ "$ARCH" = armv7 ]; then die "unsupported platform"; fi++IS_PI=0++if [ -r /proc/device-tree/model ] && grep -q "Raspberry Pi" /proc/device-tree/model 2>/dev/null; then IS_PI=1; fi++say "platform $OS/$ARCH$( [ $IS_PI = 1 ] && printf ' (Raspberry Pi)')"++++# ---- download ----++TMP="$(mktemp -d)"++trap 'rm -rf "$TMP"' EXIT++BIN_NAME="openvibe-node-$OS-$ARCH"++if [ -n "$LOCAL" ]; then++ cp "$LOCAL/$BIN_NAME" "$TMP/$BIN_NAME" || die "$LOCAL/$BIN_NAME not found"++ cp "$LOCAL/openvibe-node-plugins.tar.gz" "$TMP/" || die "$LOCAL/openvibe-node-plugins.tar.gz not found"++else++ if [ "$TAG" = latest ]; then++ BASE="${OPENVIBE_NODE_BASE:-$REPO_URL/releases/latest/download}"++ else++ BASE="${OPENVIBE_NODE_BASE:-$REPO_URL/releases/download/$TAG}"++ fi++ fetch() {++ if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 -o "$2" "$1"++ elif command -v wget >/dev/null 2>&1; then wget -q -O "$2" "$1"++ else die "curl or wget is needed"; fi++ }++ say "downloading $BIN_NAME"++ fetch "$BASE/$BIN_NAME" "$TMP/$BIN_NAME" || die "download failed: $BASE/$BIN_NAME"++ fetch "$BASE/openvibe-node-plugins.tar.gz" "$TMP/openvibe-node-plugins.tar.gz" || die "download failed: plugin bundle"++ fetch "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" || die "download failed: SHA256SUMS"++ ( cd "$TMP"++ grep -E " ($BIN_NAME|openvibe-node-plugins\.tar\.gz)\$" SHA256SUMS > sums || die "no checksums for $BIN_NAME"++ if command -v sha256sum >/dev/null 2>&1; then sha256sum -c sums >/dev/null++ else shasum -a 256 -c sums >/dev/null; fi ) || die "checksum mismatch: refusing to install"++fi++++# ---- binary ----++$SUDO install -m 0755 "$TMP/$BIN_NAME" "$BIN_DIR/openvibe-node"++say "installed $BIN_DIR/openvibe-node ($("$BIN_DIR/openvibe-node" version))"++++# ---- service account (Linux): hardware groups, no login ----++SVC_USER=""++if [ "$OS" = linux ] && [ "$SERVICE" = 1 ]; then++ SVC_USER=openvibe-node++ if ! id "$SVC_USER" >/dev/null 2>&1; then++ $SUDO useradd --system --home-dir "$STATE" --no-create-home --shell /usr/sbin/nologin "$SVC_USER" 2>/dev/null ||++ $SUDO adduser --system --home "$STATE" --no-create-home --shell /usr/sbin/nologin "$SVC_USER"++ fi++ for g in gpio i2c spi video audio dialout plugdev render; do++ if getent group "$g" >/dev/null 2>&1; then $SUDO usermod -a -G "$g" "$SVC_USER"; fi++ done++fi++$SUDO mkdir -p "$CONF" "$STATE"++$SUDO chmod 700 "$CONF" "$STATE"++++# ---- plugins and their Python environment ----++command -v python3 >/dev/null 2>&1 || die "python3 is needed for the plugins (sudo apt install python3 python3-venv)"++$SUDO rm -rf "$STATE/plugins"++$SUDO mkdir -p "$STATE/plugins"++$SUDO tar -xzf "$TMP/openvibe-node-plugins.tar.gz" -C "$STATE/plugins"++if [ ! -x "$STATE/venv/bin/python" ]; then++ say "creating the plugin environment in $STATE/venv"++ # --system-site-packages lets the Pi use the OS's picamera2/lgpio builds.++ $SUDO python3 -m venv --system-site-packages "$STATE/venv" || die "python3 -m venv failed (sudo apt install python3-venv)"++fi++PIP="$STATE/venv/bin/pip"++$SUDO "$PIP" install --quiet --upgrade pip >/dev/null 2>&1 || true++$SUDO "$PIP" install --quiet "$STATE/plugins/sdk" "$STATE/plugins/dryrun"++PLUGINS='[{"name": "dryrun"}]'++KIND=onboard++VIDEO='"source": "auto"'++case "$ROBOT" in++adeept|adeept-mecanum)++ [ $IS_PI = 1 ] || say "warning: this is not a Raspberry Pi; the Adeept plugin will report a hardware fault"++ $SUDO "$PIP" install --quiet "$STATE/plugins/adeept_adr036[pi]"++ WHEELS=ordinary; [ "$ROBOT" = adeept-mecanum ] && WHEELS=mecanum++ PLUGINS="[{\"name\": \"adeept_adr036\", \"config\": {\"backend\": \"real\", \"wheels\": \"$WHEELS\"}}]"++ ;;++cozmo)++ $SUDO "$PIP" install --quiet "$STATE/plugins/cozmo[robot]"++ PLUGINS='[{"name": "cozmo"}]'++ KIND=bridge++ command -v espeak-ng >/dev/null 2>&1 || say "tip: install espeak-ng so Cozmo can speak (sudo apt install espeak-ng)"++ ;;++esac++if [ ! -f "$CONF/config.json" ]; then++ $SUDO sh -c "umask 077; cat > '$CONF/config.json'" <<EOF++{++ "server": "${OPENVIBE_SERVER:-https://openvibe.bot}",++ "device_kind": "$KIND",++ "plugins": $PLUGINS,++ "video": {$VIDEO}++}++EOF++ say "wrote $CONF/config.json"++else++ say "keeping the existing $CONF/config.json"++fi++++# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ----++if command -v systemctl >/dev/null 2>&1; then++ unit=Adeept_Robot.service++ if systemctl is-enabled "$unit" >/dev/null 2>&1 || systemctl is-active "$unit" >/dev/null 2>&1; then++ say "disabling the Adeept kit's stock server ($unit): it listens on 0.0.0.0:8888 with a fixed password"++ $SUDO systemctl disable --now "$unit" || true++ fi++fi++if command -v pgrep >/dev/null 2>&1 && pgrep -f "Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py" >/dev/null 2>&1; then++ say "stopping a running Adeept stock server process"++ $SUDO pkill -f "Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py" || true++fi++++# ---- service ----++if [ "$SERVICE" = 1 ]; then++ if [ -n "$SVC_USER" ]; then++ $SUDO "$BIN_DIR/openvibe-node" install --user "$SVC_USER" >/dev/null 2>&1 || $SUDO "$BIN_DIR/openvibe-node" install --user "$SVC_USER"++ else++ $SUDO "$BIN_DIR/openvibe-node" install >/dev/null 2>&1 || $SUDO "$BIN_DIR/openvibe-node" install++ fi++ say "service installed and started"++fi++++# ---- pair ----++if [ -n "$CODE" ]; then++ if $SUDO "$BIN_DIR/openvibe-node" status --json 2>/dev/null | grep -q '"paired":true'; then++ say "already paired; keeping the credential (to pair again: sudo openvibe-node pair --force <CODE>)"++ else++ $SUDO "$BIN_DIR/openvibe-node" pair "$CODE"++ fi++else++ say "not paired yet: get a code on openvibe.bot and run: sudo openvibe-node pair <CODE>"++fi++++say "done. Check it with: sudo openvibe-node status Stop everything with: sudo openvibe-node stop"@@@ -13,6 +13,7 @@ See docs/plugins.md for the wire format."""import json++import osimport queueimport sysimport threading@@@ -328,10 +329,20 @@ class Runtime:return 0++def protocol_stdout():++ """Reserve stdout for the protocol.++++ Hardware libraries print to stdout (Adafruit Blinka warns about the board there), which would corrupt the JSON++ lines. Keep a private duplicate of file descriptor 1 for the protocol, then point descriptor 1 and sys.stdout at++ stderr, so every other print, from Python or from C, lands in the Node's log instead.++ """++ sys.stdout.flush()++ proto = os.fdopen(os.dup(1), "w", buffering=1, encoding="utf-8")++ os.dup2(2, 1)++ sys.stdout = sys.stderr++ return proto++++def run(plugin):"""Entry point for a plugin's __main__."""-- try:-- sys.stdout.reconfigure(line_buffering=True)-- except AttributeError:-- pass-- return Runtime(plugin).run()++ return Runtime(plugin, outfile=protocol_stdout()).run()@@@ -162,12 +162,13 @@ class ProcessTest(unittest.TestCase):"""The real thing: a child process whose stdin closes must stop and exit."""def test_child_exits_on_eof(self):-- code = ("import sys; sys.path.insert(0, %r)\n"++ code = ("import os, sys; sys.path.insert(0, %r)\n""from openvibe_plugin import Plugin, run\n""class P(Plugin):\n"" driver='t'\n"" def handle(self, cmd): pass\n"" def stop(self): sys.stderr.write('STOPPED\\n'); sys.stderr.flush()\n"++ " def setup(self, config): print('library noise on stdout'); os.write(1, b'C noise\\n')\n""sys.exit(run(P()))\n") % os.path.dirname(HERE)proc = subprocess.Popen([sys.executable, "-c", code], stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE, text=True)@@@ -182,6 +183,11 @@ class ProcessTest(unittest.TestCase):self.assertEqual(rc, 0)self.assertIn('"op":"ack"', out)self.assertIn("STOPPED", err)++ # Everything on stdout is protocol; library prints went to stderr.++ for line in out.splitlines():++ json.loads(line)++ self.assertIn("library noise on stdout", err)++ self.assertIn("C noise", err)if __name__ == "__main__":nnew file mode 100755@@@ -0,0 +1,30 @@++#!/bin/sh++# Build the release artifacts into dist/: one binary per platform, the plugin bundle, install.sh and SHA256SUMS.++# scripts/dist.sh [VERSION]++set -eu++VERSION="${1:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}"++cd "$(dirname "$0")/.."++rm -rf dist++mkdir -p dist++for target in linux/amd64 linux/arm64 linux/arm/7 darwin/amd64 darwin/arm64 windows/amd64; do++ os="${target%%/*}"++ rest="${target#*/}"++ arch="${rest%%/*}"++ arm=""++ name="openvibe-node-$os-$arch"++ if [ "$arch" = arm ]; then++ arm="${rest#*/}"++ name="openvibe-node-$os-armv$arm"++ fi++ [ "$os" = windows ] && name="$name.exe"++ CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" GOARM="$arm" \++ go build -trimpath -ldflags "-s -w -X main.version=$VERSION" -o "dist/$name" ./cmd/openvibe-node++ echo "built dist/$name"++done++# The plugin bundle: the Python packages without their tests.++tar -czf dist/openvibe-node-plugins.tar.gz -C plugins \++ --exclude=tests --exclude=__pycache__ --exclude='*.egg-info' --exclude=.pytest_cache --exclude=build \++ sdk dryrun adeept_adr036 cozmo++cp install/install.sh dist/install.sh++(cd dist && if command -v sha256sum >/dev/null 2>&1; then sha256sum -- * >SHA256SUMS; else shasum -a 256 -- * >SHA256SUMS; fi)++echo "dist/ ready for $VERSION"
new file mode 100755
new file mode 100755
@@ -0,0 +1,202 @@
@@ -0,0 +1,202 @@
+#!/bin/sh
+# OpenVibe Node installer.
+#
+# curl -fsSL https://openvibe.bot/install | sh -s -- <CODE> [--robot adeept|adeept-mecanum|cozmo|none]
+#
+# 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,
+# installs the system service and pairs the device with <CODE>. Run it again to upgrade; the credential is kept.
+#
+# Options:
+# --robot KIND adeept (ordinary wheels), adeept-mecanum, cozmo (bridge), none (dry run). Default: none.
+# --no-service install and pair, but do not install the service.
+# --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).
+# Environment:
+# OPENVIBE_NODE_BASE download base URL (default: GitHub releases of OpenVibers/OpenVibe.Node).
+# OPENVIBE_SERVER OpenVibe.Bot origin (default https://openvibe.bot).
+set -eu
+
+REPO_URL="https://github.com/OpenVibers/OpenVibe.Node"
+CODE=""
+ROBOT="none"
+SERVICE=1
+LOCAL=""
+TAG="latest"
+
+usage() {
+ echo "usage: install.sh [CODE] [--robot adeept|adeept-mecanum|cozmo|none] [--no-service] [--local DIR] [--version TAG]"
+ echo " curl -fsSL https://openvibe.bot/install | sh -s -- <CODE> --robot adeept"
+}
+say() { printf '%s\n' "openvibe-node: $*"; }
+die() { printf '%s\n' "openvibe-node: error: $*" >&2; exit 1; }
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --robot) ROBOT="${2:-}"; shift 2 ;;
+ --robot=*) ROBOT="${1#*=}"; shift ;;
+ --no-service) SERVICE=0; shift ;;
+ --local) LOCAL="${2:-}"; shift 2 ;;
+ --version) TAG="${2:-}"; shift 2 ;;
+ -h|--help) usage; exit 0 ;;
+ -*) die "unknown option $1" ;;
+ *) CODE="$1"; shift ;;
+ esac
+done
+
+case "$ROBOT" in adeept|adeept-mecanum|cozmo|none) ;; *) die "--robot must be adeept, adeept-mecanum, cozmo or none" ;; esac
+
+# Root for the service, /usr/local/bin and /etc.
+SUDO=""
+if [ "$(id -u)" -ne 0 ]; then
+ command -v sudo >/dev/null 2>&1 || die "run as root (sudo is not installed)"
+ SUDO="sudo"
+fi
+
+# ---- platform ----
+OS="$(uname -s)"
+case "$OS" in
+Linux) OS=linux; BIN_DIR=/usr/local/bin; STATE=/var/lib/openvibe-node; CONF=/etc/openvibe-node ;;
+Darwin) OS=darwin; BIN_DIR=/usr/local/bin; STATE="/Library/Application Support/OpenVibe Node"; CONF="$STATE" ;;
+*) die "unsupported OS $OS (on Windows, download openvibe-node-windows-amd64.exe from $REPO_URL/releases)" ;;
+esac
+ARCH="$(uname -m)"
+case "$ARCH" in
+x86_64|amd64) ARCH=amd64 ;;
+aarch64|arm64) ARCH=arm64 ;;
+armv7l|armv7*) ARCH=armv7 ;;
+armv6l) die "ARMv6 (Raspberry Pi Zero / 1) is not supported; use a Pi 3, 4, 5 or Zero 2 with a 64-bit OS" ;;
+*) die "unsupported CPU $ARCH" ;;
+esac
+if [ "$OS" = darwin ] && [ "$ARCH" = armv7 ]; then die "unsupported platform"; fi
+IS_PI=0
+if [ -r /proc/device-tree/model ] && grep -q "Raspberry Pi" /proc/device-tree/model 2>/dev/null; then IS_PI=1; fi
+say "platform $OS/$ARCH$( [ $IS_PI = 1 ] && printf ' (Raspberry Pi)')"
+
+# ---- download ----
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+BIN_NAME="openvibe-node-$OS-$ARCH"
+if [ -n "$LOCAL" ]; then
+ cp "$LOCAL/$BIN_NAME" "$TMP/$BIN_NAME" || die "$LOCAL/$BIN_NAME not found"
+ cp "$LOCAL/openvibe-node-plugins.tar.gz" "$TMP/" || die "$LOCAL/openvibe-node-plugins.tar.gz not found"
+else
+ if [ "$TAG" = latest ]; then
+ BASE="${OPENVIBE_NODE_BASE:-$REPO_URL/releases/latest/download}"
+ else
+ BASE="${OPENVIBE_NODE_BASE:-$REPO_URL/releases/download/$TAG}"
+ fi
+ fetch() {
+ if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 -o "$2" "$1"
+ elif command -v wget >/dev/null 2>&1; then wget -q -O "$2" "$1"
+ else die "curl or wget is needed"; fi
+ }
+ say "downloading $BIN_NAME"
+ fetch "$BASE/$BIN_NAME" "$TMP/$BIN_NAME" || die "download failed: $BASE/$BIN_NAME"
+ fetch "$BASE/openvibe-node-plugins.tar.gz" "$TMP/openvibe-node-plugins.tar.gz" || die "download failed: plugin bundle"
+ fetch "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" || die "download failed: SHA256SUMS"
+ ( cd "$TMP"
+ grep -E " ($BIN_NAME|openvibe-node-plugins\.tar\.gz)\$" SHA256SUMS > sums || die "no checksums for $BIN_NAME"
+ if command -v sha256sum >/dev/null 2>&1; then sha256sum -c sums >/dev/null
+ else shasum -a 256 -c sums >/dev/null; fi ) || die "checksum mismatch: refusing to install"
+fi
+
+# ---- binary ----
+$SUDO install -m 0755 "$TMP/$BIN_NAME" "$BIN_DIR/openvibe-node"
+say "installed $BIN_DIR/openvibe-node ($("$BIN_DIR/openvibe-node" version))"
+
+# ---- service account (Linux): hardware groups, no login ----
+SVC_USER=""
+if [ "$OS" = linux ] && [ "$SERVICE" = 1 ]; then
+ SVC_USER=openvibe-node
+ if ! id "$SVC_USER" >/dev/null 2>&1; then
+ $SUDO useradd --system --home-dir "$STATE" --no-create-home --shell /usr/sbin/nologin "$SVC_USER" 2>/dev/null ||
+ $SUDO adduser --system --home "$STATE" --no-create-home --shell /usr/sbin/nologin "$SVC_USER"
+ fi
+ for g in gpio i2c spi video audio dialout plugdev render; do
+ if getent group "$g" >/dev/null 2>&1; then $SUDO usermod -a -G "$g" "$SVC_USER"; fi
+ done
+fi
+$SUDO mkdir -p "$CONF" "$STATE"
+$SUDO chmod 700 "$CONF" "$STATE"
+
+# ---- plugins and their Python environment ----
+command -v python3 >/dev/null 2>&1 || die "python3 is needed for the plugins (sudo apt install python3 python3-venv)"
+$SUDO rm -rf "$STATE/plugins"
+$SUDO mkdir -p "$STATE/plugins"
+$SUDO tar -xzf "$TMP/openvibe-node-plugins.tar.gz" -C "$STATE/plugins"
+if [ ! -x "$STATE/venv/bin/python" ]; then
+ say "creating the plugin environment in $STATE/venv"
+ # --system-site-packages lets the Pi use the OS's picamera2/lgpio builds.
+ $SUDO python3 -m venv --system-site-packages "$STATE/venv" || die "python3 -m venv failed (sudo apt install python3-venv)"
+fi
+PIP="$STATE/venv/bin/pip"
+$SUDO "$PIP" install --quiet --upgrade pip >/dev/null 2>&1 || true
+$SUDO "$PIP" install --quiet "$STATE/plugins/sdk" "$STATE/plugins/dryrun"
+PLUGINS='[{"name": "dryrun"}]'
+KIND=onboard
+VIDEO='"source": "auto"'
+case "$ROBOT" in
+adeept|adeept-mecanum)
+ [ $IS_PI = 1 ] || say "warning: this is not a Raspberry Pi; the Adeept plugin will report a hardware fault"
+ $SUDO "$PIP" install --quiet "$STATE/plugins/adeept_adr036[pi]"
+ WHEELS=ordinary; [ "$ROBOT" = adeept-mecanum ] && WHEELS=mecanum
+ PLUGINS="[{\"name\": \"adeept_adr036\", \"config\": {\"backend\": \"real\", \"wheels\": \"$WHEELS\"}}]"
+ ;;
+cozmo)
+ $SUDO "$PIP" install --quiet "$STATE/plugins/cozmo[robot]"
+ PLUGINS='[{"name": "cozmo"}]'
+ KIND=bridge
+ command -v espeak-ng >/dev/null 2>&1 || say "tip: install espeak-ng so Cozmo can speak (sudo apt install espeak-ng)"
+ ;;
+esac
+if [ ! -f "$CONF/config.json" ]; then
+ $SUDO sh -c "umask 077; cat > '$CONF/config.json'" <<EOF
+{
+ "server": "${OPENVIBE_SERVER:-https://openvibe.bot}",
+ "device_kind": "$KIND",
+ "plugins": $PLUGINS,
+ "video": {$VIDEO}
+}
+EOF
+ say "wrote $CONF/config.json"
+else
+ say "keeping the existing $CONF/config.json"
+fi
+
+# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ----
+if command -v systemctl >/dev/null 2>&1; then
+ unit=Adeept_Robot.service
+ if systemctl is-enabled "$unit" >/dev/null 2>&1 || systemctl is-active "$unit" >/dev/null 2>&1; then
+ say "disabling the Adeept kit's stock server ($unit): it listens on 0.0.0.0:8888 with a fixed password"
+ $SUDO systemctl disable --now "$unit" || true
+ fi
+fi
+if command -v pgrep >/dev/null 2>&1 && pgrep -f "Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py" >/dev/null 2>&1; then
+ say "stopping a running Adeept stock server process"
+ $SUDO pkill -f "Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py" || true
+fi
+
+# ---- service ----
+if [ "$SERVICE" = 1 ]; then
+ if [ -n "$SVC_USER" ]; then
+ $SUDO "$BIN_DIR/openvibe-node" install --user "$SVC_USER" >/dev/null 2>&1 || $SUDO "$BIN_DIR/openvibe-node" install --user "$SVC_USER"
+ else
+ $SUDO "$BIN_DIR/openvibe-node" install >/dev/null 2>&1 || $SUDO "$BIN_DIR/openvibe-node" install
+ fi
+ say "service installed and started"
+fi
+
+# ---- pair ----
+if [ -n "$CODE" ]; then
+ if $SUDO "$BIN_DIR/openvibe-node" status --json 2>/dev/null | grep -q '"paired":true'; then
+ say "already paired; keeping the credential (to pair again: sudo openvibe-node pair --force <CODE>)"
+ else
+ $SUDO "$BIN_DIR/openvibe-node" pair "$CODE"
+ fi
+else
+ say "not paired yet: get a code on openvibe.bot and run: sudo openvibe-node pair <CODE>"
+fi
+
+say "done. Check it with: sudo openvibe-node status Stop everything with: sudo openvibe-node stop"
@@ -13,6 +13,7 @@ See docs/plugins.md for the wire format.
@@ -13,6 +13,7 @@ See docs/plugins.md for the wire format.
"""
"""
import json
import json
+import os
import queue
import queue
import sys
import sys
import threading
import threading
@@ -328,10 +329,20 @@ class Runtime:
@@ -328,10 +329,20 @@ class Runtime:
return 0
return 0
+def protocol_stdout():
+ """Reserve stdout for the protocol.
+
+ Hardware libraries print to stdout (Adafruit Blinka warns about the board there), which would corrupt the JSON
+ lines. Keep a private duplicate of file descriptor 1 for the protocol, then point descriptor 1 and sys.stdout at
+ stderr, so every other print, from Python or from C, lands in the Node's log instead.
+ """
+ sys.stdout.flush()
+ proto = os.fdopen(os.dup(1), "w", buffering=1, encoding="utf-8")
+ os.dup2(2, 1)
+ sys.stdout = sys.stderr
+ return proto
+
+
def run(plugin):
def run(plugin):
"""Entry point for a plugin's __main__."""
"""Entry point for a plugin's __main__."""
- try:
- sys.stdout.reconfigure(line_buffering=True)
- except AttributeError:
- pass
- return Runtime(plugin).run()
+ return Runtime(plugin, outfile=protocol_stdout()).run()
@@ -162,12 +162,13 @@ class ProcessTest(unittest.TestCase):
@@ -162,12 +162,13 @@ class ProcessTest(unittest.TestCase):
"""The real thing: a child process whose stdin closes must stop and exit."""
"""The real thing: a child process whose stdin closes must stop and exit."""
def test_child_exits_on_eof(self):
def test_child_exits_on_eof(self):
- code = ("import sys; sys.path.insert(0, %r)\n"
+ code = ("import os, sys; sys.path.insert(0, %r)\n"
"from openvibe_plugin import Plugin, run\n"
"from openvibe_plugin import Plugin, run\n"
"class P(Plugin):\n"
"class P(Plugin):\n"
" driver='t'\n"
" driver='t'\n"
" def handle(self, cmd): pass\n"
" def handle(self, cmd): pass\n"
" def stop(self): sys.stderr.write('STOPPED\\n'); sys.stderr.flush()\n"
" def stop(self): sys.stderr.write('STOPPED\\n'); sys.stderr.flush()\n"
+ " def setup(self, config): print('library noise on stdout'); os.write(1, b'C noise\\n')\n"
"sys.exit(run(P()))\n") % os.path.dirname(HERE)
"sys.exit(run(P()))\n") % os.path.dirname(HERE)
proc = subprocess.Popen([sys.executable, "-c", code], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
proc = subprocess.Popen([sys.executable, "-c", code], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
stderr=subprocess.PIPE, text=True)
@@ -182,6 +183,11 @@ class ProcessTest(unittest.TestCase):
@@ -182,6 +183,11 @@ class ProcessTest(unittest.TestCase):
self.assertEqual(rc, 0)
self.assertEqual(rc, 0)
self.assertIn('"op":"ack"', out)
self.assertIn('"op":"ack"', out)
self.assertIn("STOPPED", err)
self.assertIn("STOPPED", err)
+ # Everything on stdout is protocol; library prints went to stderr.
+ for line in out.splitlines():
+ json.loads(line)
+ self.assertIn("library noise on stdout", err)
+ self.assertIn("C noise", err)
if __name__ == "__main__":
if __name__ == "__main__":
new file mode 100755
new file mode 100755
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+#!/bin/sh
+# Build the release artifacts into dist/: one binary per platform, the plugin bundle, install.sh and SHA256SUMS.
+# scripts/dist.sh [VERSION]
+set -eu
+VERSION="${1:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}"
+cd "$(dirname "$0")/.."
+rm -rf dist
+mkdir -p dist
+for target in linux/amd64 linux/arm64 linux/arm/7 darwin/amd64 darwin/arm64 windows/amd64; do
+ os="${target%%/*}"
+ rest="${target#*/}"
+ arch="${rest%%/*}"
+ arm=""
+ name="openvibe-node-$os-$arch"
+ if [ "$arch" = arm ]; then
+ arm="${rest#*/}"
+ name="openvibe-node-$os-armv$arm"
+ fi
+ [ "$os" = windows ] && name="$name.exe"
+ CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" GOARM="$arm" \
+ go build -trimpath -ldflags "-s -w -X main.version=$VERSION" -o "dist/$name" ./cmd/openvibe-node
+ echo "built dist/$name"
+done
+# The plugin bundle: the Python packages without their tests.
+tar -czf dist/openvibe-node-plugins.tar.gz -C plugins \
+ --exclude=tests --exclude=__pycache__ --exclude='*.egg-info' --exclude=.pytest_cache --exclude=build \
+ sdk dryrun adeept_adr036 cozmo
+cp install/install.sh dist/install.sh
+(cd dist && if command -v sha256sum >/dev/null 2>&1; then sha256sum -- * >SHA256SUMS; else shasum -a 256 -- * >SHA256SUMS; fi)
+echo "dist/ ready for $VERSION"