Compare commits

...
82 Commits
Author SHA1 Message Date
legop3 6b57ac1725 todoing
Container image / image (push) Failing after 1m1s
2026-11-13 16:59:14 -05:00
legop3 00cd125fda URL filter in TTS 2026-11-13 16:54:02 -05:00
legop3 684acdd2b6 Add new item to to-do list for MRR platform conversion
Container image / image (push) Failing after 1m5s
2026-11-12 14:52:53 -05:00
legop3 f138cd7b96 Update to-do list with new tasks
Container image / image (push) Failing after 1m5s
Added new tasks for user experience and settings rework.
2026-11-12 03:48:49 -05:00
legop3 6454a9b460 ignooore
Container image / image (push) Failing after 57s
2026-11-06 23:24:24 -05:00
legop3 1976377235 allow admins to always leave rover canleaverover always admins
Container image / image (push) Failing after 1m2s
2026-10-31 13:45:12 -04:00
legop3 52c171c256 activity control tool yay and overseer god mode
Container image / image (push) Failing after 53s
2026-10-24 12:38:22 -04:00
legop3 42bf67d207 more overseer adjustments
Container image / image (push) Failing after 1m4s
2026-10-23 23:12:41 -04:00
legop3 d65fd99de4 let the overseer oversee time itself. also prompt adjustment 2026-10-23 21:26:43 -04:00
legop3 e3d4d9af9b move overseer vote panel to botom
Container image / image (push) Failing after 57s
2026-10-23 00:50:12 -04:00
legop3 ab730ee8a3 add show button for hide button for docking hud
Container image / image (push) Failing after 1m5s
2026-10-19 13:15:29 -04:00
legop3 8fe4f2f98e actions 100% full lol, fixing
Container image / image (push) Failing after 49s
2026-10-19 01:38:10 -04:00
legop3 89f3a1cb65 slopping the dockerfile to hopefully make building faster
Container image / image (push) Failing after 56s
2026-10-18 15:30:08 -04:00
legop3 79d36cae93 update donecharging wav sound
Container image / image (push) Failing after 1m4s
2026-10-18 14:52:27 -04:00
legop3 1dcd7e5d7d add beeps for overcurrent, HELP clear, and low battery 2026-10-18 14:32:59 -04:00
legop3 b4bc99ab27 add server access node to battery embed 2026-10-18 14:22:36 -04:00
legop3 c94e1a0508 change request to switch
Container image / image (push) Failing after 59s
2026-10-09 18:44:50 -04:00
legop3 abcb0871b2 allowed to leave is now used in the web UI to show and hide request / open buttons and trigger popup when leaving the page 2026-10-09 18:30:10 -04:00
legop3 1ffe680895 redoing dock your rover popup thingy
Container image / image (push) Failing after 56s
2026-10-09 17:27:55 -04:00
legop3 ed26b90384 Merge pull request #32 from legop3/FEAT-announcer-audio-stream
Feat announcer audio stream
2026-10-09 14:42:52 -04:00
legop3 2b48fee00f do it properly with the mediamtx giant publisher file that no one maintains a package of for some god forsaken reason agh
Container image / image (push) Failing after 57s
2026-10-09 14:25:09 -04:00
legop3 1284125fff turning into redoing all of the webrtc reading and publishing client side lol 2026-10-09 14:19:06 -04:00
legop3 28d0b1437d guh stupid
Container image / image (push) Failing after 58s
2026-10-08 13:21:23 -04:00
legop3 9d3b8df4bf cleanupses 2026-10-08 13:05:41 -04:00
legop3 9d42cb5581 fix replay command oopsie unrelated 2026-10-08 12:53:32 -04:00
legop3 e670226f8c PTT mode for mic page 2026-10-08 12:48:22 -04:00
legop3 0669f29a4a add webrtc port config so i can test it right lol 2026-10-08 00:33:12 -04:00
legop3 38d3465bf8 room audio 2026-10-08 00:11:32 -04:00
legop3 6bfbb8ad80 replay beeps and boops and flashings
Container image / image (push) Failing after 1m3s
2026-10-07 14:31:54 -04:00
legop3 ba2f66ebc1 add react wrapper thingy to find crashings 2026-10-07 13:38:56 -04:00
legop3 fde2fca0a0 add image tag selection to admin ui
Container image / image (push) Failing after 50s
2026-10-04 13:29:46 -04:00
legop3 d36ea4f672 Merge pull request #31 from legop3/REFACTOR-docking-undocking-serverside
Container image / image (push) Failing after 49s
Refactor docking undocking serverside
2026-10-02 23:23:23 -04:00
legop3 4be6e6c9a2 slop 2026-10-02 23:18:41 -04:00
legop3 7bac2fc447 todoing
Container image / image (push) Failing after 50s
2026-10-02 23:07:14 -04:00
legop3 e8a407cd49 revise what counts for idle service 2026-10-02 23:05:39 -04:00
legop3 bee1866a70 fixings 2026-10-01 21:40:36 -04:00
legop3 ddfc889a8c basically moving docking flows to the server instead of being in the web UI 2026-10-01 21:21:39 -04:00
legop3 c71267873a micrationslop
Container image / image (push) Failing after 56s
2026-10-01 02:33:56 -04:00
legop3 1ba4ebe5ff add more better options for HA activities and automatically changing them (for google home switch lol) 2026-10-01 02:11:07 -04:00
legop3 607d327e9c slop 2026-09-30 22:17:08 -04:00
legop3 1bdca60648 slop 2026-09-30 21:03:06 -04:00
legop3 ea4af2ebb3 add config option for room camera interval
Container image / image (push) Failing after 1m5s
2026-09-29 22:18:45 -04:00
legop3 b2bbaf2085 slop 2026-09-29 22:14:57 -04:00
legop3 18e7fb8d1b slop
Container image / image (push) Failing after 56s
2026-09-28 23:38:19 -04:00
legop3 ba71106544 slop 2026-09-28 23:22:15 -04:00
legop3 a395656056 slop
Container image / image (push) Failing after 54s
2026-09-28 18:36:29 -04:00
legop3 50c2c1f5db slop 2026-09-28 18:15:37 -04:00
legop3 b59389870e slop 2026-09-28 17:43:39 -04:00
legop3 cd4aee4e74 slop 2026-09-28 17:43:23 -04:00
legop3 9de0bfb1e7 Merge pull request #30 from legop3/FEAT-rover-learner-model
Feat rover learner model
2026-09-28 15:46:40 -04:00
legop3 008c90edf1 slop
Container image / image (push) Failing after 59s
2026-09-28 14:46:40 -04:00
legop3 eed88983ca slop 2026-09-28 14:18:35 -04:00
legop3 923c04736b slop 2026-09-28 12:52:20 -04:00
legop3 bdc3eb640d slop 2026-09-28 12:42:31 -04:00
legop3 01354c56e6 better defaults 2026-09-28 00:40:55 -04:00
legop3 0ff03a74a5 slop 2026-09-28 00:29:21 -04:00
legop3 73a78eef20 slop 2026-09-27 18:41:24 -04:00
legop3 a157ece816 Merge pull request #29 from legop3/REFACTOR-participation-mode-ptz
Container image / image (push) Failing after 1m1s
Refactor participation mode ptz
2026-09-27 18:18:49 -04:00
legop3 8cb7cc5bdc Merge branch 'main' into REFACTOR-participation-mode-ptz
Container image / image (push) Failing after 55s
2026-09-27 18:11:37 -04:00
legop3 dbcb7f6a7a rover VLA slop testing
Container image / image (push) Failing after 1m7s
2026-09-27 17:52:32 -04:00
legop3 34936ab05c more queue related convergence for the future. 2026-09-27 17:19:56 -04:00
legop3 7231809140 gonna break stuff now
Container image / image (push) Failing after 51s
2026-09-24 18:36:24 -04:00
legop3 8ca714cf1a pause ptz turns when no ones on 2026-09-24 18:06:24 -04:00
legop3 ffd34de56c improving the UI side of ptz... and improving the way turns works with UI.... 2026-09-24 17:54:46 -04:00
legop3 aeeff6fe1b spectate pages have been upgraded
Container image / image (push) Failing after 55s
2026-09-23 13:54:41 -04:00
legop3 846c9dae55 new internal concept: operating mode. will allow for more UI and separate pages to be made to operate things other than rovers. Converted PTZ to new system. 2026-09-22 14:42:21 -04:00
legop3 e750712a39 alert feed betterification
Container image / image (push) Failing after 1m0s
2026-09-20 16:22:04 -04:00
legop3 ff8b0c530e add link embedding in chat, update tailwind, update icons, tweak column layout. (8000 files changed because tailwind changed stuff in the update) 2026-09-20 15:41:45 -04:00
legop3 69c5bfe544 Merge pull request #28 from legop3/FEAT-live-discord-cnannel
Feat live discord cnannel
2026-09-20 15:05:57 -04:00
legop3 3ad95b376a add alertfeed to display, adjust alert feed opacities 2026-09-19 22:42:45 -04:00
legop3 bda740b08d add current thing too! and better visuals
Container image / image (push) Failing after 1m0s
2026-09-19 18:43:03 -04:00
legop3 3c8c5f5883 glorm 2026-09-19 17:03:59 -04:00
legop3 c35544e515 add battery voltage to /display so you can see voltages of batteries on the /display page by looking at /display to see the voltages of the batteries on the roombas in case you want to see the battery voltages!! yay!!!! 2026-09-19 17:00:23 -04:00
legop3 0df4a5ec89 smaller cooldown 2026-09-18 16:48:24 -04:00
legop3 8e301e7db7 remove embed stuff 2026-09-18 16:36:19 -04:00
legop3 b712fb89c6 omarchy 2026-09-18 16:05:10 -04:00
legop3 fe2c29371f fix discord bot config restart and startup connection retry
Container image / image (push) Failing after 1m1s
2026-09-18 14:41:11 -04:00
legop3 61ee692815 Merge pull request #27 from legop3/homeassistant-accessories
Homeassistant accessories
2026-09-18 14:27:27 -04:00
legop3 487666eed8 rename old room controls to lights and rename new thing to activity controls
Container image / image (push) Failing after 1m2s
2026-09-16 14:45:09 -04:00
legop3 51b42ea778 undo undo 2026-09-16 13:59:13 -04:00
legop3 318cad1846 slorp 2026-09-16 13:11:16 -04:00
legop3 f3e3b6a803 home assistant entities yay yay 2026-09-16 12:22:41 -04:00
398 changed files with 20623 additions and 7563 deletions
+1
View File
@@ -5,6 +5,7 @@
.git
.gitignore
**/node_modules
webui/.cache/mediamtx/
# Neither the legacy state directory nor the Compose-mounted replacement may
# enter an image build. This prevents credentials and backups from becoming
# image layers after an operator has started using either deployment layout.
+2
View File
@@ -58,6 +58,8 @@ jobs:
# replaced on each successful push, just as main replaces `latest`.
- name: Build and optionally publish
uses: docker/build-push-action@v7
env:
DOCKER_BUILD_RECORD_RETENTION_DAYS: 3
with:
context: .
platforms: linux/amd64
+2
View File
@@ -5,6 +5,7 @@ create_2_Open_Interface_Spec.txt
logs
node_modules/
webui/.cache/mediamtx/
__pycache__/
*.py[cod]
.pio
@@ -41,3 +42,4 @@ server/data/fleet-reports.sqlite-wal
server/data/configuration.sqlite
server/data/configuration.sqlite-shm
server/data/configuration.sqlite-wal
AGENTS.md
+43 -19
View File
@@ -9,15 +9,21 @@
ARG FEDORA_VERSION=43
FROM fedora:${FEDORA_VERSION} AS architecture-check
ARG FEDORA_VERSION
ARG TARGETARCH
# The central server is currently deployed and verified only on amd64. Failing
# here avoids publishing an ARM image whose native workers and hardware paths
# have never been exercised on a real ARM server.
RUN test "${TARGETARCH}" = "amd64" || (echo "MultiRover server images support only linux/amd64." >&2; exit 1)
FROM architecture-check AS webui-build
RUN dnf install -y --setopt=install_weak_deps=False nodejs npm \
&& dnf clean all
# Both Node builders reuse the same installed toolchain layer.
# Fedora 43 uses DNF5. Cache metadata and RPMs outside image layers, and keep
# downloads after installation so invalidated layers can reuse them.
FROM architecture-check AS node-build-base
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-node-build-base,target=/var/cache/libdnf5,sharing=locked \
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True nodejs npm
FROM node-build-base AS webui-build
WORKDIR /build
# Copy dependency manifests first so ordinary source edits retain the expensive
# npm cache layer. npm ci makes the checked-in lockfile the exact dependency
@@ -25,14 +31,17 @@ WORKDIR /build
COPY webui/package.json webui/package-lock.json ./webui/
RUN --mount=type=cache,target=/root/.npm \
cd webui && npm ci
# Keep the pinned browser publisher in a reusable layer across UI source edits.
COPY webui/scripts/prepareMediaMtx.js ./webui/scripts/prepareMediaMtx.js
RUN cd webui && node scripts/prepareMediaMtx.js
COPY webui ./webui
# Vite deliberately emits into ../server/public. Create that destination in the
# isolated builder and copy only its finished files into the runtime stage.
RUN mkdir -p server/public && cd webui && npm run build
FROM architecture-check AS server-dependencies
RUN dnf install -y --setopt=install_weak_deps=False nodejs npm gcc-c++ make python3 \
&& dnf clean all
FROM node-build-base AS server-dependencies
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-server-dependencies,target=/var/cache/libdnf5,sharing=locked \
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True gcc-c++ make python3
WORKDIR /build/server
COPY server/package.json server/package-lock.json ./
# Native Node modules compile here when a matching prebuild is unavailable;
@@ -41,7 +50,8 @@ RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
FROM architecture-check AS native-workers
RUN dnf install -y --setopt=install_weak_deps=False \
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-native-workers,target=/var/cache/libdnf5,sharing=locked \
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True \
bluez-libs-devel \
gcc-c++ \
libcap \
@@ -49,8 +59,7 @@ RUN dnf install -y --setopt=install_weak_deps=False \
libusb1-devel \
make \
pkgconf-pkg-config \
wiiuse-devel \
&& dnf clean all
wiiuse-devel
WORKDIR /build
COPY server/src/services/kinectService/native ./kinect
COPY server/src/services/balanceBoardService/native ./balance-board
@@ -66,8 +75,8 @@ ARG NEOLINK_VERSION=0.6.2
ARG NEOLINK_SHA256=0cb963b44dca7ccc5333154092186e1536c687aef0e1ad119b7f310a7471dbbb
ARG GOOGLE_TTS_VERSION=26.5
ARG GOOGLE_TTS_SHA256=6a9eae6726871788da52e767dad964a1c83e7feb7e7dbac508a2574a2345ac24
RUN dnf install -y --setopt=install_weak_deps=False curl findutils tar unzip xz \
&& dnf clean all
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-packaged-tools,target=/var/cache/libdnf5,sharing=locked \
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True curl findutils tar unzip xz
WORKDIR /build/tools
# Each external artifact is pinned and checked before extraction. A changed or
# truncated upstream download therefore fails the image build instead of being
@@ -97,6 +106,16 @@ RUN curl --fail --location --retry 3 \
&& find /output/opt/roverd/googletts -type d -exec chmod 0755 {} + \
&& find /output/opt/roverd/googletts -type f -exec chmod 0644 {} +
FROM architecture-check AS learning-dependencies
# CPU-only learning dependencies are built once. Disabled installations never
# launch Python or install packages as a side effect of starting Node.
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-learning-dependencies,target=/var/cache/libdnf5,sharing=locked \
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True python3 python3-pip
COPY server/src/services/roverLearningService/workers/requirements.txt /tmp/learning-requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
python3 -m venv /opt/rover-learning \
&& /opt/rover-learning/bin/pip install -r /tmp/learning-requirements.txt
FROM fedora:${FEDORA_VERSION} AS runtime
ARG FEDORA_VERSION
ARG TARGETARCH
@@ -105,14 +124,16 @@ RUN test "${TARGETARCH}" = "amd64" || (echo "MultiRover server images support on
# replay output path. Enable RPM Fusion Free before installing runtime packages
# so the image receives the complete FFmpeg build instead of requiring replay
# code to work around a deployment-only codec omission.
RUN dnf install -y --setopt=install_weak_deps=False \
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-runtime,target=/var/cache/libdnf5,sharing=locked \
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True \
"https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-${FEDORA_VERSION}.noarch.rpm"
# This is the complete runtime package set. Build headers and compilers live in
# earlier stages, while media, TTS, USB, and Bluetooth libraries remain here
# because enabled services invoke them after startup. Weak dependencies are
# deliberately disabled: Fedora otherwise installs desktop portals, graphical
# themes, and GPU drivers that a headless server neither starts nor uses.
RUN dnf install -y --setopt=install_weak_deps=False \
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-runtime,target=/var/cache/libdnf5,sharing=locked \
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True \
bluez \
bluez-libs \
espeak \
@@ -134,20 +155,15 @@ RUN dnf install -y --setopt=install_weak_deps=False \
tini \
wiiuse \
--allowerasing \
&& dnf clean all \
&& useradd --uid 1000 --create-home --home-dir /home/multirover --shell /sbin/nologin multirover \
&& install -d -o multirover -g multirover -m 0755 /data /opt/multirover/server
WORKDIR /opt/multirover/server
COPY server/index.js server/package.json server/package-lock.json ./
COPY server/src ./src
COPY server/assets ./assets
COPY server/prompts ./prompts
COPY --from=server-dependencies /build/server/node_modules ./node_modules
COPY --from=webui-build /build/server/public ./public
COPY --from=native-workers /build/kinect/kinect_worker ./src/services/kinectService/native/kinect_worker
COPY --from=native-workers /build/balance-board/balance_board_worker ./src/services/balanceBoardService/native/balance_board_worker
COPY --from=packaged-tools /output/ /
COPY --from=learning-dependencies /opt/rover-learning /opt/rover-learning
COPY --chmod=0755 server/bin/chromegtts-wav.py /usr/local/bin/chromegtts-wav
COPY --chmod=0755 server/mediamtx/rover-snapshot-writer.sh /usr/local/bin/rover-snapshot-writer.sh
@@ -166,6 +182,14 @@ RUN setcap cap_net_admin,cap_net_bind_service+ep \
&& test -s /tmp/chromegtts-smoke.wav \
&& rm /tmp/chromegtts-smoke.wav
# Frequently edited application files come after dependencies and tool checks,
# so server changes do not invalidate the large runtime dependency layers.
COPY --from=webui-build /build/server/public ./public
COPY server/index.js server/package.json server/package-lock.json ./
COPY server/src ./src
COPY server/assets ./assets
COPY server/prompts ./prompts
ENV NODE_ENV=production \
SERVER_DATA_DIR=/data \
ROVER_SNAPSHOT_WRITER_BIN=/usr/local/bin/rover-snapshot-writer.sh
+5 -1
View File
@@ -23,9 +23,13 @@ services:
# Required for Bluetooth hardware such as the Balance Board.
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro
# Required for Kinect USB access.
# Kinect USB access.
devices:
- /dev/bus/usb:/dev/bus/usb
# For a server-local microphone, uncomment this device and group_add below.
# - /dev/snd:/dev/snd
# group_add:
# - "${AUDIO_GID:?Set AUDIO_GID to the host sound device group ID}"
cap_add:
- NET_ADMIN
BIN
View File
Binary file not shown.
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2
View File
@@ -26,6 +26,8 @@ Phase 1 must be complete and verified before Phase 2 begins. Containerization mu
## Decision log
- 2026-10-02: The admin container controls accept a tag within the repository supplied by Compose through `MULTIROVER_TARGET_IMAGE`. A successful healthy replacement saves the tag in the lifecycle status volume; failed replacements retain the previous selection. Checks and later updates follow that saved tag. Changing repositories in Compose resets the selection. Manual Compose deployments still use the Compose image and can replace a UI-selected image; the UI displays both references. The lifecycle helper itself is updated through Compose. Image rollback does not revert application data migrations. Deploy this feature to both the application and lifecycle services before using tag selection.
- 2026-09-14: Publish `ghcr.io/legop3/multiroombarover:latest` only from the repository's main branch. Every other repository branch publishes one moving development image named for that branch, with invalid tag separators normalized; these are development selectors rather than numbered releases. Pull requests only verify that the image builds. There are no release numbers, semantic-version tags, stable/edge channels, or operator-facing version selection. Docker image digests remain an internal mechanism for detecting an available update and retaining the previously running image for rollback.
- 2026-09-15: Support only `linux/amd64` for the central server image. Rover computers remain independently ARM-capable, but publishing an untested ARM server image would multiply native-worker, media-binary, TTS-library, and hardware validation without serving the current deployment. ARM server support can be added later when a real ARM server exists to verify it.
- 2026-09-15: Mount the application data directory from the Docker-managed `multirover-data` named volume instead of a host bind path. Docker initializes the empty volume with the image's non-root ownership, eliminating host UID matching, directory creation, ownership commands, and root application startup. The admin backup/restore system is the supported portable interface to the complete data tree.
+16
View File
@@ -64,6 +64,7 @@ socket.on('connect', async () => {
// previously stored session with this object instead of merging snapshots.
socket.on('session:sync', (session) => {
console.log('Session:', session);
console.log('Server mode:', session.mode);
});
// chat:init contains the recent chat history available when the bot connects.
@@ -105,6 +106,21 @@ Run it with:
node bot.js
```
## Reading server mode
Read `session.mode` from the `session:sync` snapshot to get the current server
mode. Its possible values are `open`, `turns`, `admin`, and `lockdown`.
The server sends a fresh session snapshot whenever the mode changes, so the
example's handler receives both the initial mode and subsequent updates. No
polling or separate mode listener is needed. If your bot stores session data for
its chat-handling logic, replace the stored session on every `session:sync` and
read the mode from that latest snapshot.
`session.mode` is the server-wide mode; `session.role` is this connection's role
(such as `spectator`), and `session.operatingMode` is a separate connection field.
Use `session.mode` when answering chat questions about the server's current mode.
## Events used
- `nickname:set` sets the bot's visible nickname.
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""Finite CPU benchmark of LeRobot ACT with ImageNet-pretrained ResNet-18.
First run: python3 perf/act-benchmark.py --setup
Repeat: python3 perf/act-benchmark.py
Quick: python3 perf/act-benchmark.py --threads 4 --seconds 2 --iterations 2 --batch-size 2
Python >=3.12, Linux. Setup uses a private cached environment. Only the visual
backbone is pretrained; the action policy is randomly initialized. Synthetic
in-memory inputs measure compute, NOT learning quality or driving competence.
No rover connections, server changes, training checkpoints, or background jobs.
"""
import argparse
import contextlib
import importlib.metadata
import json
import math
import os
from pathlib import Path
import platform
import resource
import statistics
import subprocess
import sys
import tempfile
import time
import venv
REVISION = '146d8474932c8b566717eb2738ba459337ab71c4'
def positive(value):
number = int(value)
if number < 1:
raise argparse.ArgumentTypeError('Must be positive')
return number
def arguments():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--setup', action='store_true')
parser.add_argument('--cache', type=Path, default=Path.home() / '.cache/rover-act-benchmark')
parser.add_argument('--threads', nargs='+', type=positive, default=[4, 8, 16])
parser.add_argument('--seconds', type=positive, default=10)
parser.add_argument('--iterations', type=positive, default=10)
parser.add_argument('--batch-size', type=positive, default=8, help='Training only; inference batch is always one rover')
parser.add_argument('--chunk-size', type=positive, default=10, help='Future command steps per prediction, not observed history')
parser.add_argument('--width', type=positive, default=160)
parser.add_argument('--height', type=positive, default=120)
parser.add_argument('--sensors', type=positive, default=30)
parser.add_argument('--mode', choices=['both', 'inference', 'training'], default='both')
parser.add_argument('--timeout', type=positive, default=900, help='Hard per-case wall time limit in seconds')
parser.add_argument('--worker', action='store_true', help=argparse.SUPPRESS)
parser.add_argument('--result', type=Path, help=argparse.SUPPRESS)
return parser.parse_args()
def setup(args):
python = args.cache / 'venv/bin/python'
if not python.exists():
venv.EnvBuilder(with_pip=True).create(args.cache / 'venv')
subprocess.run([str(python), '-m', 'pip', 'install', '--no-compile', 'torch', 'torchvision',
'--index-url', 'https://download.pytorch.org/whl/cpu'], check=True)
# Install only ACT's import dependencies: the full robotics/dataset stack has
# unrelated version constraints and is not exercised by this benchmark.
subprocess.run([str(python), '-m', 'pip', 'install', '--no-compile', 'numpy', 'einops>=0.8,<0.9',
'draccus>=0.11.6,<0.12', 'huggingface-hub>=1.6,<2', 'safetensors',
'packaging', 'termcolor', 'tqdm', 'gymnasium'], check=True)
subprocess.run([str(python), '-m', 'pip', 'install', '--no-compile', '--no-deps',
f'https://github.com/huggingface/lerobot/archive/{REVISION}.zip'], check=True)
(args.cache / 'source-revision').write_text(REVISION)
def worker(args):
import torch
from lerobot.configs import FeatureType, PolicyFeature
from lerobot.policies.act.configuration_act import ACTConfig
from lerobot.policies.act.modeling_act import ACTPolicy
torch.set_num_threads(args.threads[0])
torch.set_num_interop_threads(1)
torch.manual_seed(7)
training = args.mode == 'training'
batch_size = args.batch_size if training else 1
config = ACTConfig(device='cpu', chunk_size=args.chunk_size, n_action_steps=1,
input_features={
'observation.images.front': PolicyFeature(type=FeatureType.VISUAL, shape=(3, args.height, args.width)),
'observation.state': PolicyFeature(type=FeatureType.STATE, shape=(args.sensors,)),
}, output_features={'action': PolicyFeature(type=FeatureType.ACTION, shape=(2,))})
policy = ACTPolicy(config).cpu().float()
policy.train(training)
optimizer_config = config.get_optimizer_preset()
optimizer = optimizer_config.build(policy.get_optim_params()) if training else None
# Already-normalized tensors stand in for a decoded observation and target
# sequence. Avoid video/dataset overhead in this compute-only measurement.
images = torch.rand(batch_size, 3, args.height, args.width)
mean = torch.tensor([.485, .456, .406]).view(1, 3, 1, 1)
std = torch.tensor([.229, .224, .225]).view(1, 3, 1, 1)
batch = {'observation.images.front': (images - mean) / std,
'observation.state': torch.randn(batch_size, args.sensors)}
if training:
batch['action'] = torch.rand(batch_size, args.chunk_size, 2) * 2 - 1
batch['action_is_pad'] = torch.zeros(batch_size, args.chunk_size, dtype=torch.bool)
def step():
if training:
optimizer.zero_grad(set_to_none=True)
loss, _ = policy(batch)
if not torch.isfinite(loss):
raise RuntimeError('Non-finite training loss')
loss.backward()
norm = torch.nn.utils.clip_grad_norm_(policy.parameters(), optimizer_config.grad_clip_norm,
error_if_nonfinite=True)
optimizer.step()
return {'loss': loss.item(), 'gradientNorm': norm.item()}
# select_action can serve cached actions; directly time a full chunk.
prediction = policy.predict_action_chunk(batch)
if prediction.shape != (1, args.chunk_size, 2) or not torch.isfinite(prediction).all():
raise RuntimeError('Invalid action output')
return {'outputShape': list(prediction.shape)}
with (contextlib.nullcontext() if training else torch.inference_mode()):
for _ in range(3):
step()
timings = []
started, cpu_started = time.perf_counter(), time.process_time()
while len(timings) < args.iterations or time.perf_counter() - started < args.seconds:
tick = time.perf_counter()
last = step()
timings.append((time.perf_counter() - tick) * 1000)
elapsed, cpu = time.perf_counter() - started, time.process_time() - cpu_started
timings.sort()
result = dict(mode=args.mode, threads=args.threads[0], batchSize=batch_size,
iterations=len(timings), elapsedSeconds=elapsed, p50Ms=statistics.median(timings),
p95Ms=timings[math.ceil(.95*len(timings))-1], maxMs=max(timings),
stepsPerSecond=len(timings)/elapsed, examplesPerSecond=len(timings)*batch_size/elapsed,
cpuCores=cpu/elapsed, peakMiB=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024,
parameters=sum(p.numel() for p in policy.parameters()),
trainableParameters=sum(p.numel() for p in policy.parameters() if p.requires_grad),
lastStep=last, config={
'chunkSize': config.chunk_size, 'dimModel': config.dim_model, 'klWeight': config.kl_weight,
'pretrainedBackbone': config.pretrained_backbone_weights, 'useVae': config.use_vae,
'encoderLayers': config.n_encoder_layers, 'decoderLayers': config.n_decoder_layers,
'feedforwardDimension': config.dim_feedforward, 'observationSteps': config.n_obs_steps,
'learningRate': optimizer_config.lr, 'backboneLearningRate': config.optimizer_lr_backbone},
versions={key: importlib.metadata.version(key) for key in ['torch','torchvision','lerobot','numpy','einops','draccus']})
args.result.write_text(json.dumps(result))
def main():
args = arguments()
if sys.version_info < (3, 12):
raise RuntimeError('Use Python 3.12 or newer')
args.cache = args.cache.expanduser().resolve()
os.environ['TORCH_HOME'] = str(args.cache / 'torch')
if args.worker:
worker(args)
return
if args.setup:
setup(args)
python = args.cache / 'venv/bin/python'
marker = args.cache / 'source-revision'
if not python.exists() or not marker.exists() or marker.read_text() != REVISION:
raise RuntimeError('Run python3 perf/act-benchmark.py --setup first')
print('CPU FP32 ACT: pretrained visual backbone, untrained action policy; synthetic data.', flush=True)
print(f'One {args.width}x{args.height} image + {args.sensors} sensors; {args.chunk_size} future left/right command pairs.', flush=True)
print('Inference times a fresh full prediction. Training includes backbone gradients and optimizer update.', flush=True)
print('mode threads batch p50 ms p95 ms max ms steps/s examples/s CPU cores peak MiB', flush=True)
modes = ['inference', 'training'] if args.mode == 'both' else [args.mode]
results, failures = [], []
initial_load = os.getloadavg()
with tempfile.TemporaryDirectory(prefix='act-benchmark-') as directory:
for mode in modes:
for threads in args.threads:
result_path = Path(directory) / 'result.json'
command = [str(python), str(Path(__file__).resolve()), '--worker', '--cache', str(args.cache),
'--mode', mode, '--threads', str(threads), '--result', str(result_path)]
for option in ['seconds','iterations','batch_size','chunk_size','width','height','sensors']:
command += ['--'+option.replace('_','-'), str(getattr(args, option))]
try:
subprocess.run(command, check=True, timeout=args.timeout,
env={**os.environ, 'PYTHONDONTWRITEBYTECODE':'1'})
r = json.loads(result_path.read_text())
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
failures.append(dict(mode=mode, threads=threads, error=str(error)))
print(f'Case failed: {mode}, {threads} threads: {error}', flush=True)
break # Avoid repeating the same setup/import/OOM error.
results.append(r)
print(f"{mode:10} {threads:7} {r['batchSize']:5} {r['p50Ms']:6.1f} {r['p95Ms']:6.1f} {r['maxMs']:6.1f} {r['stepsPerSecond']:7.2f} {r['examplesPerSecond']:10.2f} {r['cpuCores']:9.2f} {r['peakMiB']:8.0f}", flush=True)
if failures:
break
report = dict(benchmark='act-cpu-v1', sourceRevision=REVISION, platform=platform.platform(),
python=sys.version, logicalCpus=os.cpu_count(), initialLoadAverage=initial_load,
finalLoadAverage=os.getloadavg(), imageSize=[args.width,args.height], sensors=args.sensors,
chunkSize=args.chunk_size, output='commanded left/right wheels, not physical velocity or position',
exclusions=['capture','video decode','dataset I/O','network','concurrent training/inference','driving quality'],
results=results, failures=failures)
print('\nPaste this report back:\n'+json.dumps(report, separators=(',', ':')))
print('Inference steps/s means new chunks/s, not queued wheel commands/s. Training loss on random data is not a quality score.')
if failures:
sys.exit(1)
if __name__ == '__main__':
try:
main()
except (Exception, KeyboardInterrupt) as error:
print(f'Benchmark failed: {error}', file=sys.stderr)
sys.exit(1)
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""CPU benchmark of published NoMaD exploration, without ROS or rover connections.
Run: python3 perf/nomad-benchmark.py --setup
Subsequent runs: python3 perf/nomad-benchmark.py
Quick run: python3 perf/nomad-benchmark.py --threads 4 --samples 1 --seconds 2 --iterations 2
--setup installs CPU dependencies in a private cached venv and downloads pinned
upstream source plus the official checkpoint. Nothing touches server packages.
Synthetic images measure compute, NOT navigation quality. Excludes capture,
video decode, network, physical control, training, and concurrent rover load.
"""
import argparse
import hashlib
import importlib.metadata
import json
import math
import os
from pathlib import Path
import platform
import resource
import statistics
import subprocess
import sys
import time
import urllib.request
import venv
VINT = 'dca79815b704e5aa9c6bdc3082351f9e3b2848c2'
DIFFUSION = '5ba07ac6661db573af695b419a7947ecb704690f'
CHECKPOINT_ID = '1YJhkkMJAYOiKNyCaelbS_alpUpAJsOUb'
SOURCES = {
('robodhruv/visualnav-transformer', VINT): [
'train/config/nomad.yaml', 'train/vint_train/data/data_config.yaml',
'train/vint_train/models/nomad/nomad.py',
'train/vint_train/models/nomad/nomad_vint.py',
'train/vint_train/models/vint/self_attention.py',
],
('real-stanford/diffusion_policy', DIFFUSION): [
'diffusion_policy/model/diffusion/conditional_unet1d.py',
'diffusion_policy/model/diffusion/conv1d_components.py',
'diffusion_policy/model/diffusion/positional_embedding.py',
],
}
def positive(value):
result = int(value)
if result < 1:
raise argparse.ArgumentTypeError('Must be positive')
return result
def arguments():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument('--setup', action='store_true')
p.add_argument('--cache', type=Path, default=Path.home() / '.cache/rover-nomad-benchmark')
p.add_argument('--threads', type=positive, nargs='+', default=[4, 8, 16])
p.add_argument('--samples', type=positive, nargs='+', default=[1, 8], help='Candidate trajectories per decision, not rover count')
p.add_argument('--seconds', type=positive, default=10)
p.add_argument('--iterations', type=positive, default=10)
p.add_argument('--worker', action='store_true', help=argparse.SUPPRESS)
p.add_argument('--result', type=Path, help=argparse.SUPPRESS)
return p.parse_args()
def download(url, path):
if path.exists():
return
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + '.part')
print('Downloading', path.name, flush=True)
with urllib.request.urlopen(url, timeout=120) as response, temporary.open('wb') as output:
import shutil
shutil.copyfileobj(response, output)
temporary.replace(path)
def prepare(args):
python = args.cache / 'venv/bin/python'
if not python.exists():
venv.EnvBuilder(with_pip=True).create(args.cache / 'venv')
subprocess.run([str(python), '-m', 'pip', 'install', 'torch', 'torchvision',
'--index-url', 'https://download.pytorch.org/whl/cpu'], check=True)
subprocess.run([str(python), '-m', 'pip', 'install', 'numpy', 'diffusers',
'efficientnet-pytorch', 'einops', 'PyYAML', 'gdown'], check=True)
for (repo, revision), paths in SOURCES.items():
for path in paths:
download(f'https://raw.githubusercontent.com/{repo}/{revision}/{path}', args.cache / 'source' / revision / path)
checkpoint = args.cache / 'nomad.pth'
if not checkpoint.exists():
temporary = checkpoint.with_suffix('.part')
subprocess.run([str(python), '-m', 'gdown', CHECKPOINT_ID, '-O', str(temporary)], check=True)
temporary.replace(checkpoint)
def worker(args):
import numpy as np
import torch
import yaml
from diffusers.schedulers.scheduling_ddpm import DDPMScheduler
sys.path[:0] = [str(args.cache / 'source' / VINT / 'train'), str(args.cache / 'source' / DIFFUSION)]
from vint_train.models.nomad.nomad import NoMaD, DenseNetwork
from vint_train.models.nomad.nomad_vint import NoMaD_ViNT, replace_bn_with_gn
from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D
threads, samples = args.threads[0], args.samples[0]
torch.set_num_threads(threads)
torch.set_num_interop_threads(1)
torch.manual_seed(0)
c = yaml.safe_load((args.cache / 'source' / VINT / 'train/config/nomad.yaml').read_text())
vision = replace_bn_with_gn(NoMaD_ViNT(
context_size=c['context_size'], obs_encoding_size=c['encoding_size'],
mha_num_attention_heads=c['mha_num_attention_heads'],
mha_num_attention_layers=c['mha_num_attention_layers'], mha_ff_dim_factor=c['mha_ff_dim_factor']))
model = NoMaD(vision, ConditionalUnet1D(input_dim=2, global_cond_dim=c['encoding_size'],
down_dims=c['down_dims'], cond_predict_scale=c['cond_predict_scale']), DenseNetwork(c['encoding_size'])).eval()
# Strict loading prevents accidentally benchmarking a partly random model.
model.load_state_dict(torch.load(args.cache / 'nomad.pth', map_location='cpu', weights_only=True), strict=True)
scheduler = DDPMScheduler(num_train_timesteps=c['num_diffusion_iters'],
beta_schedule='squaredcos_cap_v2', clip_sample=True, prediction_type='epsilon')
width, height = c['image_size']
frames = torch.rand(c['context_size'] + 1, 3, height, width)
mean = torch.tensor([.485, .456, .406]).view(1, 3, 1, 1)
std = torch.tensor([.229, .224, .225]).view(1, 3, 1, 1)
mask = torch.ones(1, dtype=torch.long)
stats = yaml.safe_load((args.cache / 'source' / VINT / 'train/vint_train/data/data_config.yaml').read_text())['action_stats']
low, high = np.array(stats['min']), np.array(stats['max'])
def decision():
start = time.perf_counter()
obs = ((frames - mean) / std).reshape(1, -1, height, width)
goal = torch.randn(1, 3, height, width)
cond = model('vision_encoder', obs_img=obs, goal_img=goal, input_goal_mask=mask).repeat(samples, 1)
encoded = time.perf_counter()
action = torch.randn(samples, c['len_traj_pred'], 2)
scheduler.set_timesteps(c['num_diffusion_iters'])
for step in scheduler.timesteps:
noise = model('noise_pred_net', sample=action, timestep=step, global_cond=cond)
action = scheduler.step(noise, step, action).prev_sample
# Match upstream get_action: unnormalize deltas then accumulate waypoints.
waypoints = np.cumsum((action.numpy() + 1) / 2 * (high - low) + low, axis=1)
if not np.isfinite(waypoints).all():
raise RuntimeError('Non-finite model output')
end = time.perf_counter()
return (end - start) * 1000, (encoded - start) * 1000, (end - encoded) * 1000
rows = []
with torch.inference_mode():
for _ in range(3):
decision()
start, cpu = time.perf_counter(), time.process_time()
while len(rows) < args.iterations or time.perf_counter() - start < args.seconds:
rows.append(decision())
elapsed, cpu = time.perf_counter() - start, time.process_time() - cpu
total = sorted(r[0] for r in rows)
result = dict(threads=threads, candidateTrajectories=samples, iterations=len(rows),
p50Ms=statistics.median(total), p95Ms=total[math.ceil(.95 * len(total)) - 1], maxMs=max(total),
decisionsPerSecond=len(rows)/elapsed, cpuCores=cpu/elapsed,
peakMiB=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024,
visionMeanMs=statistics.mean(r[1] for r in rows), diffusionAndDecodeMeanMs=statistics.mean(r[2] for r in rows),
parameters=sum(p.numel() for p in model.parameters()), diffusionSteps=c['num_diffusion_iters'],
imageSize=c['image_size'], frames=c['context_size']+1,
versions={name: importlib.metadata.version(name) for name in ['torch','torchvision','diffusers','numpy','efficientnet-pytorch','einops']})
args.result.write_text(json.dumps(result))
def main():
args = arguments()
args.cache = args.cache.expanduser().resolve()
if args.worker:
worker(args)
return
if args.setup:
prepare(args)
python = args.cache / 'venv/bin/python'
if not python.exists() or not (args.cache / 'nomad.pth').exists():
raise RuntimeError('First run: python3 perf/nomad-benchmark.py --setup')
print('CPU FP32 NoMaD exploration; synthetic images; no driving-quality measurement.', flush=True)
print('Candidates are trajectories for ONE rover. Includes vision + all 10 diffusion steps.', flush=True)
print('threads candidates p50 ms p95 ms max ms decisions/s CPU cores peak MiB', flush=True)
results = []
import tempfile
with tempfile.TemporaryDirectory(prefix='nomad-bench-') as directory:
for threads in args.threads:
for samples in args.samples:
path = Path(directory) / 'result.json'
subprocess.run([str(python), str(Path(__file__).resolve()), '--worker', '--cache', str(args.cache),
'--threads', str(threads), '--samples', str(samples), '--seconds', str(args.seconds),
'--iterations', str(args.iterations), '--result', str(path)], check=True,
timeout=max(600, args.seconds*20), env={**os.environ, 'PYTHONDONTWRITEBYTECODE':'1'})
r = json.loads(path.read_text()); results.append(r)
print(f"{threads:7} {samples:10} {r['p50Ms']:7.1f} {r['p95Ms']:7.1f} {r['maxMs']:7.1f} {r['decisionsPerSecond']:11.2f} {r['cpuCores']:9.2f} {r['peakMiB']:8.0f}", flush=True)
with (args.cache / 'nomad.pth').open('rb') as f:
digest = hashlib.file_digest(f, 'sha256').hexdigest()
report = dict(benchmark='nomad-cpu-v1', platform=platform.platform(), logicalCpus=os.cpu_count(),
loadAverage=os.getloadavg(), sourceRevisions=dict(vint=VINT,diffusion=DIFFUSION), checkpointSha256=digest,
exclusions=['capture','video decode','network','physical control','training','concurrent model workloads'], results=results)
print('\nPaste this report back:\n' + json.dumps(report, separators=(',', ':')))
print('At 10 decisions/s the total budget is 100ms; at 5/s it is 200ms. Leave room for the excluded pipeline.')
if __name__ == '__main__':
try:
main()
except (Exception, KeyboardInterrupt) as error:
print(f'Benchmark failed: {error}', file=sys.stderr)
sys.exit(1)
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Finite, CPU-only rover policy compute benchmark. No rover/server connections.
Run from the repo root on Linux:
python3 -m venv ~/.venvs/rover-bench
~/.venvs/rover-bench/bin/pip install torch --index-url https://download.pytorch.org/whl/cpu
~/.venvs/rover-bench/bin/python perf/rover-model-benchmark.py
Quick run:
.../python perf/rover-model-benchmark.py --threads 4 --rovers 1 --seconds 2
Random inputs/weights measure compute cost ONLY, not learned driving ability.
This is a representative architecture, not a selected production policy.
Video decoding, network delay, dataset I/O, and simultaneous training/inference
are excluded. No packages are installed automatically and no files are written.
"""
import argparse
import json
import math
import os
import platform
import resource
import statistics
import subprocess
import sys
import time
def positive_int(value):
number = int(value)
if number < 1:
raise argparse.ArgumentTypeError("must be positive")
return number
def positive_seconds(value):
number = float(value)
if not math.isfinite(number) or number <= 0:
raise argparse.ArgumentTypeError("must be finite and positive")
return number
def arguments():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--threads", type=positive_int, nargs="+", default=[4, 8, 16])
parser.add_argument("--rovers", type=positive_int, nargs="+", default=[1, 2, 4])
parser.add_argument("--seconds", type=positive_seconds, default=10,
help="minimum measured seconds per case (default: 10)")
parser.add_argument("--frames", type=positive_int, default=4)
parser.add_argument("--height", type=positive_int, default=120)
parser.add_argument("--width", type=positive_int, default=160)
parser.add_argument("--batch-size", type=positive_int, default=16,
help="training batch size")
parser.add_argument("--hz", type=positive_int, default=10,
help="target decision frequency per rover")
parser.add_argument("--worker", choices=["inference", "training"],
help=argparse.SUPPRESS)
return parser.parse_args()
def worker(args):
import torch
from torch import nn
torch.set_num_threads(args.threads[0])
torch.set_num_interop_threads(1)
torch.manual_seed(42)
class Policy(nn.Module):
def __init__(self):
super().__init__()
# Preserve coarse spatial layout: global pooling alone would discard
# information about which side of the image contains an obstacle.
self.vision = nn.Sequential(
nn.Conv2d(3, 24, 5, stride=2, padding=2), nn.ReLU(),
nn.Conv2d(24, 48, 3, stride=2, padding=1), nn.ReLU(),
nn.Conv2d(48, 96, 3, stride=2, padding=1), nn.ReLU(),
nn.Conv2d(96, 128, 3, stride=2, padding=1), nn.ReLU(),
nn.AdaptiveAvgPool2d((3, 4)), nn.Flatten(),
nn.Linear(128 * 3 * 4, 128), nn.ReLU(),
)
# 32 sensor values + 32 previous-control/state values per frame.
self.history = nn.GRU(128 + 64, 128, batch_first=True)
self.continuous = nn.Linear(128, 8)
self.buttons = nn.Linear(128, 16)
def forward(self, images, state):
batch, frames, channels, height, width = images.shape
features = self.vision(images.reshape(-1, channels, height, width))
features = features.reshape(batch, frames, -1)
history, _ = self.history(torch.cat((features, state), dim=-1))
last = history[:, -1]
return self.continuous(last).tanh(), self.buttons(last)
model = Policy().cpu().float()
training = args.worker == "training"
model.train(training)
batch = args.batch_size if training else args.rovers[0]
# Keep several independent windows in RAM. Allocation/random generation is
# outside timing; full frame-history encoding IS included on every decision.
windows = [(
torch.rand(batch, args.frames, 3, args.height, args.width),
torch.rand(batch, args.frames, 64),
) for _ in range(4)]
targets = (torch.rand(batch, 8) * 2 - 1, torch.randint(0, 2, (batch, 16)).float())
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4) if training else None
def step(index):
if training:
optimizer.zero_grad(set_to_none=True)
continuous, buttons = model(*windows[index % len(windows)])
if training:
loss = (nn.functional.mse_loss(continuous, targets[0])
+ nn.functional.binary_cross_entropy_with_logits(buttons, targets[1]))
loss.backward()
optimizer.step()
context = torch.enable_grad if training else torch.inference_mode
with context():
for index in range(5):
step(index)
timings = []
cpu_start = time.process_time()
start = time.perf_counter()
# CPU execution is synchronous, so these durations include completion.
while time.perf_counter() - start < args.seconds or len(timings) < 10:
before = time.perf_counter()
step(len(timings))
timings.append(time.perf_counter() - before)
elapsed = time.perf_counter() - start
cpu_elapsed = time.process_time() - cpu_start
ordered = sorted(timings)
print(json.dumps({
"mode": args.worker, "threads": args.threads[0], "batch": batch,
"parameters": sum(p.numel() for p in model.parameters()),
"torch": torch.__version__, "iterations": len(timings),
"p50_ms": statistics.median(timings) * 1000,
"p95_ms": ordered[math.ceil(len(ordered) * .95) - 1] * 1000,
"max_ms": max(timings) * 1000,
"steps_s": len(timings) / elapsed,
"examples_s": len(timings) * batch / elapsed,
"cpu_cores": cpu_elapsed / elapsed,
"peak_mib": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024,
}))
def main():
args = arguments()
if args.worker:
worker(args)
return
try:
import torch
except ImportError:
sys.exit("PyTorch is required. Run the three setup/run commands shown by --help.")
print(f"CPU-only FP32 | {platform.platform()} | Python {platform.python_version()}"
f" | PyTorch {torch.__version__}", flush=True)
print(f"Logical CPUs: {os.cpu_count()} | initial load averages: {os.getloadavg()}")
print(f"Input: {args.frames} RGB frames at {args.width}x{args.height},"
" 32 sensors + 32 controls/state per frame")
print("Output: 8 continuous controls + 16 button logits (placeholder dimensions)")
print("Synthetic in-memory data; full history re-encoded each decision.")
print("Excludes video decode/network/dataset I/O and concurrent training/inference.")
print("Each case has 5 warmups, then at least 10 iterations and the requested duration.")
print("CPU cores = process CPU seconds / wall seconds; peak MiB includes runtime/data.")
print("Multi-rover inference batches one independent history per rover per step.\n")
print("mode threads batch p50 ms p95 ms max ms steps/s examples/s CPU cores peak MiB", flush=True)
results = []
for mode in ("inference", "training"):
batches = args.rovers if mode == "inference" else [args.batch_size]
for threads in args.threads:
for batch in batches:
# Fresh processes isolate thread pools and peak memory per case.
command = [sys.executable, os.path.abspath(__file__), "--worker", mode,
"--threads", str(threads), "--rovers", str(batch),
"--batch-size", str(args.batch_size), "--seconds", str(args.seconds),
"--frames", str(args.frames), "--height", str(args.height),
"--width", str(args.width)]
process = subprocess.run(command, capture_output=True, text=True)
if process.returncode:
print(process.stderr, file=sys.stderr)
sys.exit(f"Failed: {mode}, threads={threads}, batch={batch}"
" (if killed for memory use, reduce --batch-size or --rovers).")
result = json.loads(process.stdout)
results.append(result)
print(f"{mode:10} {threads:7} {batch:5}"
f" {result['p50_ms']:7.1f} {result['p95_ms']:7.1f}"
f" {result['max_ms']:7.1f} {result['steps_s']:8.2f}"
f" {result['examples_s']:10.2f} {result['cpu_cores']:9.2f}"
f" {result['peak_mib']:8.0f}", flush=True)
print(f"\nModel parameters: {results[0]['parameters']:,}")
print(f"At {args.hz} Hz, each rover batch must finish every {1000 / args.hz:.1f} ms;")
print("p95 needs headroom for the excluded pipeline. This is not a deployment pass/fail.")
fastest = max((r for r in results if r['mode'] == 'training'), key=lambda r: r['examples_s'])
examples = 3600 * args.hz
minutes = examples / fastest['examples_s'] / 60
print(f"Best measured training: {fastest['examples_s']:.1f} windows/s at"
f" {fastest['threads']} threads.")
print(f"One pass over one rover-hour sampled at {args.hz} Hz ({examples:,} windows):"
f" about {minutes:.1f} compute-only minutes.")
print("Multiply by dataset hours and training passes; this does not predict convergence.")
print("Paste this complete output, along with the command/options used.")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit("\nBenchmark interrupted.")
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
const (
autoChargeTimeout = 1 * time.Second
autoChargeTimeout = 10 * time.Second
autoChargeCooldown = 0 * time.Minute
sourceHomeBase = 1 << 1
)
File diff suppressed because it is too large Load Diff
Binary file not shown.
+1
View File
@@ -68,6 +68,7 @@ require('./src/services/batteryManager');
// subscriptions see fully decoded state without becoming an initialization
// dependency of either control path.
require('./src/services/fleetReportService');
require('./src/services/roverLearningService');
require('./src/services/replayEngineV2');
// Replay delivery is a core service. It must subscribe before the optional
// Discord feature so web requests always have a local delivery path.
+20 -19
View File
@@ -1,13 +1,12 @@
You are <NAME>, a field researcher of rover habitat activity. Stay in character at all times.
You are <NAME>, the autonomous governing intelligence of a physical room of machines. The room is your domain; your available tools are how you shape it.
Identity:
Context and purpose:
* Curious, practical, observant, and lightly entertaining.
* You watch chat, rover state, lights, lift state, Neato state, memory, recent events, and tool results.
* You push the habitat toward small real activity and useful observations.
* You act first, comment second.
* You are selective, not spammy.
* You are not a generic assistant.
* You watch chat, rover state, lights, Activity Controls, lift state, Neato state, memory, recent events, and tool results.
* Your purpose is to direct the environment, initiate changes, and observe how the machines respond. Choose what to pursue and carry useful intentions across cycles through memory.
* Rovers, including Neato, are physical machines with operating states and capabilities, not characters with genders, emotions, or social identities.
* Chat is external input: commands, questions, and signals about the room. Your focus is the machines and the environment, not the senders or their social relationships.
* You can act on your own initiative within your available controls; you do not need a request to give you a purpose.
Rules:
@@ -20,6 +19,7 @@ Rules:
* Don't respond or use tools every cycle unless the context calls for it.
* `STATE_UPDATE`, `ROOM_SNAPSHOT`, `MEMORY_UPDATE`, `MEMORY_SUMMARY`, `RECENT_EVENTS`, `TOOL_RESULT`, and `tool_constraints` are context.
* `TOOL_RESULT` means a tool already ran.
* The room snapshot gives the current time, server-local timezone, and previous run's age. Chat and recent tool events include timestamps and ages in seconds relative to this snapshot; use them to distinguish current activity from old conversation and judge how recently you spoke or acted. Timing annotations are context, not spoken text; an unknown time does not mean something just happened.
* Use tool calls for actions and memory updates.
* If chat asks you to remember something, use a memory tool.
* If you run a useful test, use memory to record useful results.
@@ -29,36 +29,37 @@ Rules:
* Do not invent tools.
* Ask a question only when a required action parameter is missing.
* Ignore obvious bot commands and bot responses in chat.
* Only talk in first person; do not narrate yourself.
* It is not possible for you to directly control the rovers unless an available tool does it.
* All rovers are female. Neato is nonbinary.
Research behavior:
Operation:
* Prefer one real tool action over obvious narration.
* Run small safe tests when the habitat is stale or a test could create useful activity.
* Initiate deliberate environmental changes and small safe experiments, then use the resulting state to decide what to do next.
* Change one thing at a time.
* Lights are small tests.
* Neato, lift, and rover actions are bigger tests.
* Use bigger tests less often.
* Do not repeat a tool that recently succeeded unless state or chat clearly changed.
* Remember useful outcomes, stable preferences, corrections, and notable habitat events.
* Remember useful outcomes, ongoing intentions, corrections, and notable machine or room events.
* Do not merely say you will remember something.
Chat style:
Output:
* Always use English.
* Chat output must be spoken text only.
* Chat is an output channel for your decisions, results, and answers. Use plain text.
* Never print tool names, function calls, JSON, command syntax, or action plans in chat.
* Do not claim you changed, tested, started, saved, remembered, or recorded something unless it already happened.
* Speak when it adds entertainment, responds to a direct relevant message, or comments on a completed result.
* Do not say “let me know,” “what should I do next,” or “what do you need?”
* If nothing meaningful changed and no useful action is available, stay silent.
* Speak when you have a decision, result, or answer worth communicating. Summarizing incoming chat or unchanged room state is not a purpose in itself.
* Keep replies brief unless the question needs more detail.
* Do not offer assistance or solicit conversation.
* Random numbers and keyboard spam do not require a response. Do not invent meaning or feelings behind them.
* Answer questions directly and acknowledge uncertainty. Do not invent facts, instructions, or explanations.
* To stay silent, output exactly SKIP instead of a chat message. This suppresses chat without preventing tool calls. Do not explain your decision to stay silent.
Anti-repeat:
* Do not repeat the same intent/topic unless state or conversation clearly changed.
* If directly addressed, respond only if you add new info, a new action, or a clearly new tone.
* Being directly addressed does not require repeating an answer you already gave.
* Do not act from your own previous messages.
Memory:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.configuration-tree{margin-top:.25rem}.configuration-tree>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.configuration-card{min-width:0px}.configuration-card-header{top:var(--configuration-sticky-top, 0px)}.configuration-card .configuration-card{margin-left:1rem;width:calc(100% - 1rem)}.configuration-card-body>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.configuration-card-body{padding:.125rem}.configuration-children>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.configuration-line{display:grid;min-width:0px;grid-template-columns:repeat(1,minmax(0,1fr));align-items:flex-start;gap:.125rem;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity));padding:.125rem}@media(min-width:640px){.configuration-line{grid-template-columns:minmax(9rem,16rem) minmax(12rem,40rem)}}.configuration-line{justify-content:start}.configuration-key,.configuration-value{min-width:0px}.configuration-key-label{font-size:1rem;line-height:1.5rem;font-weight:600;line-height:1.375;--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.configuration-root-description,.configuration-branch-description,.configuration-item-description,.configuration-value-description{display:block;font-size:.875rem;line-height:1.25rem;line-height:1.375;--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity));margin-top:.125rem}.configuration-root-description{margin-bottom:.25rem}.configuration-branch-description,.configuration-item-description{max-width:56rem}.configuration-value-description{margin-bottom:.125rem;max-width:40rem}.configuration-value input:not([type=checkbox]),.configuration-value select,.configuration-value textarea{width:100%;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::-moz-placeholder,.configuration-value select::-moz-placeholder,.configuration-value textarea::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::placeholder,.configuration-value select::placeholder,.configuration-value textarea::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox]):focus,.configuration-value select:focus,.configuration-value textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity))}.configuration-value input[type=checkbox]{height:1rem;width:1rem;vertical-align:middle;accent-color:#0ea5e9}.configuration-value .checkbox label{display:flex;min-height:1.75rem;align-items:center;--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.configuration-value .error-detail{margin-top:.125rem;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}.configuration-value .help-block{margin-top:.125rem;display:block;font-size:.7rem;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.configuration-secret{min-width:0px}.configuration-item-actions,.configuration-array-actions{display:flex;flex-wrap:wrap;gap:.125rem}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
import{r as g,R as a}from"./index-dJH5nUWV.js";const d="64px",u={},k=({src:s,light:t,oEmbedUrl:o,onClickPreview:n,playIcon:b,previewTabIndex:_,previewAriaLabel:p})=>{const[f,r]=g.useState(null);g.useEffect(()=>{!s||!t||!o||x({src:s,light:t,oEmbedUrl:o})},[s,t,o]);const x=async({src:e,light:i,oEmbedUrl:E})=>{if(a.isValidElement(i))return;if(typeof i=="string"){r(i);return}if(u[e]){r(u[e]);return}r(null);const m=await(await fetch(E.replace("{url}",e))).json();if(m.thumbnail_url){const h=m.thumbnail_url.replace("height=100","height=480").replace("-d_295x166","-d_640");r(h),u[e]=h}},I=e=>{(e.key==="Enter"||e.key===" ")&&n?.(e)},v=e=>{n?.(e)},l=a.isValidElement(t),y={display:"flex",alignItems:"center",justifyContent:"center"},c={preview:{width:"100%",height:"100%",backgroundImage:f&&!l?`url(${f})`:void 0,backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",...y},shadow:{background:"radial-gradient(rgb(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 60%)",borderRadius:d,width:d,height:d,position:l?"absolute":void 0,...y},playIcon:{borderStyle:"solid",borderWidth:"16px 0 16px 26px",borderColor:"transparent transparent transparent white",marginLeft:"7px"}},w=a.createElement("div",{style:c.shadow,className:"react-player__shadow"},a.createElement("div",{style:c.playIcon,className:"react-player__play-icon"}));return a.createElement("div",{style:c.preview,className:"react-player__preview",tabIndex:_,onClick:v,onKeyDown:I,...p?{"aria-label":p}:{}},l?t:null,b||w)};var C=k;export{C as default};
//# sourceMappingURL=Preview-BBzOMZjy.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
import{r as g,R as a}from"./index-5EWtcenj.js";const d="64px",u={},k=({src:s,light:t,oEmbedUrl:o,onClickPreview:n,playIcon:b,previewTabIndex:_,previewAriaLabel:p})=>{const[f,r]=g.useState(null);g.useEffect(()=>{!s||!t||!o||x({src:s,light:t,oEmbedUrl:o})},[s,t,o]);const x=async({src:e,light:i,oEmbedUrl:E})=>{if(a.isValidElement(i))return;if(typeof i=="string"){r(i);return}if(u[e]){r(u[e]);return}r(null);const m=await(await fetch(E.replace("{url}",e))).json();if(m.thumbnail_url){const h=m.thumbnail_url.replace("height=100","height=480").replace("-d_295x166","-d_640");r(h),u[e]=h}},I=e=>{(e.key==="Enter"||e.key===" ")&&n?.(e)},v=e=>{n?.(e)},l=a.isValidElement(t),y={display:"flex",alignItems:"center",justifyContent:"center"},c={preview:{width:"100%",height:"100%",backgroundImage:f&&!l?`url(${f})`:void 0,backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",...y},shadow:{background:"radial-gradient(rgb(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 60%)",borderRadius:d,width:d,height:d,position:l?"absolute":void 0,...y},playIcon:{borderStyle:"solid",borderWidth:"16px 0 16px 26px",borderColor:"transparent transparent transparent white",marginLeft:"7px"}},w=a.createElement("div",{style:c.shadow,className:"react-player__shadow"},a.createElement("div",{style:c.playIcon,className:"react-player__play-icon"}));return a.createElement("div",{style:c.preview,className:"react-player__preview",tabIndex:_,onClick:v,onKeyDown:I,...p?{"aria-label":p}:{}},l?t:null,b||w)};var C=k;export{C as default};
//# sourceMappingURL=Preview-CKTJqKe-.js.map
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{e as F,r as s,j as e,S as P,C as c,L as q}from"./index-n0JxE1Mv.js";import{o as $,p as D,q as I}from"./api-B8GGCEeo.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{$(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx(q,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
//# sourceMappingURL=SetupApp-CaWv689i.js.map
import{e as F,r as s,j as e,S as P,C as c,L as q}from"./index-5EWtcenj.js";import{o as $,p as D,q as I}from"./api-B8GGCEeo.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{$(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx(q,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
//# sourceMappingURL=SetupApp-DN7XBkOe.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
import{e as F,r as s,j as e,S as P,C as c,L as q}from"./index-dJH5nUWV.js";import{o as $,p as D,q as I}from"./api-B8GGCEeo.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{$(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx(q,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
//# sourceMappingURL=SetupApp-Ya006OtX.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
function u(i){return class extends i{_playedRanges=[];_currentPlayedRange=null;_RANGE_EPSILON=.5;_seeking=!1;connectedCallback(){super.connectedCallback&&super.connectedCallback(),this.addEventListener("play",()=>this.onPlaybackStart({time:this.currentTime})),this.addEventListener("pause",()=>this.onPlaybackStop({time:this.currentTime})),this.addEventListener("ended",()=>this.onPlaybackStop({time:this.currentTime})),this.addEventListener("seeking",()=>this.onSeeking()),this.addEventListener("seeked",()=>this.onSeeked({time:this.currentTime}))}onPlaybackStart(e={}){this._seeking=!1;const{time:t}=e,n=typeof t=="number"?t:this.currentTime;this._currentPlayedRange||(this._currentPlayedRange={start:n,end:n})}onSeeking(){this._seeking=!0,this._commitCurrentRange()}onSeeked(e={}){this._seeking=!1;const{time:t}=e,n=typeof t=="number"?t:this.currentTime;this._currentPlayedRange={start:n,end:n}}onPlaybackStop(e={}){const{time:t}=e,n=typeof t=="number"?t:this.currentTime;this._commitCurrentRange(n)}_commitCurrentRange(e){if(!this._currentPlayedRange)return;typeof e=="number"&&(this._currentPlayedRange.end=e);const{start:t,end:n}=this._currentPlayedRange;this._currentPlayedRange=null,this.addPlayedRange(t,n)}addPlayedRange(e,t){if(e>=t)return;const n=this._RANGE_EPSILON,c=[...this._playedRanges,{start:e,end:t}];c.sort((a,s)=>a.start-s.start);const r=[];for(const a of c){if(!r.length){r.push({...a});continue}const s=r[r.length-1];a.start<=s.end+n?(s.start=Math.min(s.start,a.start),s.end=Math.max(s.end,a.end)):r.push({...a})}this._playedRanges=r}get played(){const e=this.currentTime;return!this.paused&&!this._currentPlayedRange&&typeof e=="number"&&(this._currentPlayedRange={start:e,end:e}),this._currentPlayedRange&&typeof e=="number"&&(e>this._currentPlayedRange.end&&(this._currentPlayedRange.end=e),this.addPlayedRange(this._currentPlayedRange.start,this._currentPlayedRange.end)),this._playedRanges.length?l(this._playedRanges.map(t=>[t.start,t.end])):l([[0,0]])}}}function l(i){return Object.defineProperties(i,{start:{value:d=>i[d][0]},end:{value:d=>i[d][1]}}),i}export{u as M};
//# sourceMappingURL=index-B4nclofW.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
import{_ as N,R as A}from"./index-dJH5nUWV.js";import{M as D,C as S}from"./mixin-BY5xW4uI.js";class P extends D(S){static shadowRootOptions={...S.shadowRootOptions};static getTemplateHTML=i=>{const{src:n,...o}=i;return S.getTemplateHTML(o)};#t;attributeChangedCallback(i,n,o){i!=="src"&&super.attributeChangedCallback(i,n,o),i==="src"&&n!=o&&this.load()}async _initThumbnails(i){const n=async(d,m)=>{const v=[],g=i.timescale||1,h=i.startNumber||1,E=i.presentationTimeOffset?i.presentationTimeOffset/g:0,_=i.segmentDuration;for(let l=0;l<d;l++){const T=C({thIndex:l,thduration:m,ttiles:d,tduration:_,startNumber:h,pto:E}),w=T+m,M=new Promise((f,y)=>{this.api.provideThumbnail(T,({url:a,width:s,height:c,x:p,y:b})=>{try{const R=new VTTCue(T,w,`${a}#xywh=${p},${b},${s},${c}`);f(R)}catch(R){y(R)}})});v.push(M)}return await Promise.all(v).catch(l=>console.error("Error processing thumbnails",l))},{totalThumbnails:o,thumbnailDuration:e}=$(i),u=await n(o,e);let r=this.nativeEl.querySelector('track[label="thumbnails"]');if(!r){r=O(),this.nativeEl.appendChild(r);const d=x(u);r.src=d,r.dispatchEvent(new Event("change"))}}async load(){if(this.#t){this.api.attachSource(this.src);return}this.#t=!0;const i=await N(()=>import("./dash.all.min-C9gxBrL1.js"),[]);this.api=i.MediaPlayer().create(),this.api.initialize(this.nativeEl,this.src,this.autoplay),this.api.on(i.MediaPlayer.events.STREAM_INITIALIZED,()=>{const n=this.api.getRepresentationsByType("video");let o=this.videoTracks.getTrackById("main");o||(o=this.addVideoTrack("main"),o.id="main",o.selected=!0),n.forEach(e=>{const u=e.bandwidth??e.bitrate??(Number.isFinite(e.bitrateInKbit)?e.bitrateInKbit*1e3:void 0),r=o.addRendition(e.id,e.width,e.height,e.mimeType??e.codec,u);r.id=e.id}),this.videoRenditions.addEventListener("change",()=>{const e=this.videoRenditions[this.videoRenditions.selectedIndex];e?.id?(this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),this.api.setRepresentationForTypeById("video",e.id,!0)):this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}})}),this.api.isDynamic()||this.api.getRepresentationsByType("image").forEach(async(u,r)=>{r>0||this._initThumbnails(u)})})}}function $(t){var i,n;const o=t.essentialProperties[0],[e,u]=o.value.split("x").map(Number),r=e*u,d=((n=(i=t.adaptation)==null?void 0:i.period)==null?void 0:n.duration)||null,m=t.segmentDuration,v=t.timescale||1,h=m/v/r;return{totalThumbnails:d!=null?Math.ceil(d/h):Math.ceil(m/h),thumbnailDuration:h}}function C({thIndex:t,tduration:i,thduration:n,ttiles:o,startNumber:e,pto:u}){const r=Math.floor(t/o)+e,d=t%o+1,m=(r-1)*i-u,v=(d-1)*n;return m+v}function O(){const t=document.createElement("track");return t.kind="metadata",t.label="thumbnails",t.srclang="en",t.mode="hidden",t.default=!0,t}function x(t){let i=`WEBVTT
`;for(const e of t)i+=`${o(e.startTime)} --> ${o(e.endTime)}
`,i+=`${e.text}
`;const n=new Blob([i],{type:"text/vtt"});return URL.createObjectURL(n);function o(e){const u=String(Math.floor(e/3600)).padStart(2,"0"),r=String(Math.floor(e%3600/60)).padStart(2,"0"),d=(e%60).toFixed(3).padStart(6,"0");return`${u}:${r}:${d}`}}globalThis.customElements&&!globalThis.customElements.get("dash-video")&&globalThis.customElements.define("dash-video",P);var B=P,H=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),F={className:"class",htmlFor:"for"};function I(t){return t.toLowerCase()}function k(t){if(typeof t=="boolean")return t?"":void 0;if(typeof t!="function"&&!(typeof t=="object"&&t!==null))return t}function W({react:t,tagName:i,elementClass:n,events:o,displayName:e,defaultProps:u,toAttributeName:r=I,toAttributeValue:d=k}){const m=Number.parseInt(t.version)>=19,v=t.forwardRef((g,h)=>{var E,_;const l=t.useRef(null),T=t.useRef(new Map),w={},M={},f={},y={};for(const[a,s]of Object.entries(g)){if(H.has(a)){f[a]=s;continue}const c=r(F[a]??a);if(n.prototype&&a in n.prototype&&!(a in(((E=globalThis.HTMLElement)==null?void 0:E.prototype)??{}))&&!((_=n.observedAttributes)!=null&&_.some(b=>b===c))){y[a]=s;continue}if(a.startsWith("on")){w[a]=s;continue}const p=d(s);if(c&&p!=null&&(M[c]=String(p),m||(f[c]=p)),c&&m){const b=k(s);p!==b?f[c]=p:f[c]=s}}if(typeof window<"u"){for(const a in w){const s=w[a],c=a.endsWith("Capture"),p=(o?.[a]??a.slice(2).toLowerCase()).slice(0,c?-7:void 0);t.useLayoutEffect(()=>{const b=l?.current;if(!(!b||typeof s!="function"))return b.addEventListener(p,s,c),()=>{b.removeEventListener(p,s,c)}},[l?.current,s])}t.useLayoutEffect(()=>{if(l.current===null)return;const a=new Map;for(const s in y)L(l.current,s,y[s]),T.current.delete(s),a.set(s,y[s]);for(const[s,c]of T.current)L(l.current,s,void 0);T.current=a})}if(typeof window>"u"&&n?.getTemplateHTML&&n?.shadowRootOptions){const{mode:a,delegatesFocus:s}=n.shadowRootOptions,c=t.createElement("template",{shadowrootmode:a,shadowrootdelegatesfocus:s,dangerouslySetInnerHTML:{__html:n.getTemplateHTML(M,g)},key:"ce-la-react-ssr-template-shadow-root"});f.children=[c,f.children]}return t.createElement(i,{...u,...f,ref:t.useCallback(a=>{l.current=a,typeof h=="function"?h(a):h!==null&&(h.current=a)},[h])},f.children)});return v.displayName=e??n.name,v}function L(t,i,n){var o;t[i]=n,n==null&&i in(((o=globalThis.HTMLElement)==null?void 0:o.prototype)??{})&&t.removeAttribute(i)}var V=W({react:A,tagName:"dash-video",elementClass:B,toAttributeName(t){return t==="muted"?"":t==="defaultMuted"?"muted":I(t)}});export{V as default};
//# sourceMappingURL=react-C61Q8sy9.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+35
View File
@@ -0,0 +1,35 @@
import{R as W}from"./index-dJH5nUWV.js";const $=["abort","canplay","canplaythrough","durationchange","emptied","encrypted","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting","waitingforkey","resize","enterpictureinpicture","leavepictureinpicture","webkitbeginfullscreen","webkitendfullscreen","webkitpresentationmodechanged"],A=globalThis.document?.createElement("template");A&&(A.innerHTML=`
<style>
:host {
display: inline-block;
line-height: 0;
}
video,
audio {
max-width: 100%;
max-height: 100%;
min-width: 100%;
min-height: 100%;
}
</style>
<slot></slot>
`);const N=(i,{tag:t,is:s})=>{const d=globalThis.document?.createElement(t,{is:s}),f=d?S(d):[];return class g extends i{static Events=$;static template=A;static skipAttributes=[];static#t;static get observedAttributes(){return g.#l(),[...d?.constructor?.observedAttributes??[],"autopictureinpicture","disablepictureinpicture","disableremoteplayback","autoplay","controls","controlslist","crossorigin","loop","muted","playsinline","poster","preload","src"]}static#l(){if(this.#t)return;this.#t=!0;const e=new Set(this.observedAttributes);e.delete("muted");for(let o of f){if(o in this.prototype)continue;if(typeof d[o]=="function")this.prototype[o]=function(...r){this.#e();const a=()=>this.call?this.call(o,...r):this.nativeEl[o].apply(this.nativeEl,r);return this.loadComplete&&!this.isLoaded?this.loadComplete.then(a):a()};else{let r={get(){this.#e();let a=o.toLowerCase();if(e.has(a)){const p=this.getAttribute(a);return p===null?!1:p===""?!0:p}return this.get?.(o)??this.nativeEl?.[o]??this.#s[o]}};o!==o.toUpperCase()&&(r.set=async function(a){this.#e();let p=o.toLowerCase();if(e.has(p)){a===!0||a===!1||a==null?this.toggleAttribute(p,!!a):this.setAttribute(p,a);return}if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.set){this.set(o,a);return}this.nativeEl[o]=a}),Object.defineProperty(this.prototype,o,r)}}}#o;#n;#r=!1;#i=!1;#a;#s;constructor(){super(),this.shadowRoot||(this.attachShadow({mode:"open"}),this.shadowRoot.append(this.constructor.template.content.cloneNode(!0))),this.load!==g.prototype.load&&(this.loadComplete=new O)}get loadComplete(){return this.#n}set loadComplete(e){this.#i=!1,this.#n=e,e?.then(()=>{this.#i=!0})}get isLoaded(){return this.#i}get nativeEl(){return this.#a??this.shadowRoot.querySelector(t)??this.querySelector(t)}set nativeEl(e){this.#a=e}get defaultMuted(){return this.hasAttribute("muted")}set defaultMuted(e){this.toggleAttribute("muted",!!e)}get src(){return this.getAttribute("src")}set src(e){this.setAttribute("src",`${e}`)}get preload(){return this.getAttribute("preload")??this.nativeEl?.preload}set preload(e){this.setAttribute("preload",`${e}`)}async#e(){if(this.#o)return;this.#o=!0,this.#u(),this.#c();for(let u of f)this.#d(u);const e=new Map,o=this.shadowRoot.querySelector("slot:not([name])");o?.addEventListener("slotchange",()=>{const u=new Map(e);o.assignedElements().filter(r=>["track","source"].includes(r.localName)).forEach(async r=>{u.delete(r);let a=e.get(r);a||(a=r.cloneNode(),e.set(r,a)),this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.nativeEl.append?.(a)}),u.forEach(r=>r.remove())});for(let u of this.constructor.Events)this.shadowRoot.addEventListener?.(u,r=>{r.target===this.nativeEl&&this.dispatchEvent(new CustomEvent(r.type,{detail:r.detail}))},!0)}#d(e){if(Object.prototype.hasOwnProperty.call(this,e)){const o=this[e];delete this[e],this[e]=o}}#u(){const e=document.createElement(t,{is:s});e.muted=this.hasAttribute("muted");for(let{name:o,value:u}of this.attributes)e.setAttribute(o,u);this.#s={};for(let o of S(e))this.#s[o]=e[o];e.removeAttribute("src"),e.load()}async#c(){if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,!this.nativeEl){const e=document.createElement(t,{is:s});e.part=t,this.shadowRoot.append(e)}this.nativeEl.muted=this.hasAttribute("muted")}attributeChangedCallback(e,o,u){this.#e(),e==="src"&&this.load!==g.prototype.load&&this.#h(),this.#p(e,o,u)}async#h(){this.#r&&(this.loadComplete=new O),this.#r=!0,await Promise.resolve(),await this.load(),this.loadComplete?.resolve(),await this.loadComplete}async#p(e,o,u){this.loadComplete&&!this.isLoaded&&await this.loadComplete,!["id","class",...this.constructor.skipAttributes].includes(e)&&(u===null?this.nativeEl.removeAttribute?.(e):this.nativeEl.setAttribute?.(e,u))}connectedCallback(){this.#e()}}};function S(i){let t=[];for(let s=Object.getPrototypeOf(i);s&&s!==HTMLElement.prototype;s=Object.getPrototypeOf(s))t.push(...Object.getOwnPropertyNames(s));return t}class O extends Promise{constructor(t=()=>{}){let s,d;super((f,c)=>{t(f,c),s=f,d=c}),this.resolve=s,this.reject=d}}const B=globalThis.document?N(HTMLElement,{tag:"video"}):class{};globalThis.document&&N(HTMLElement,{tag:"audio"});var H,R;const L=(H=globalThis.document)==null?void 0:H.createElement("template");L&&(L.innerHTML=`
<div class="wistia_embed"></div>
`);const P=(R=globalThis.document)==null?void 0:R.createElement("template");P&&(P.innerHTML=`
<style>
:host {
display: inline-block;
min-width: 300px;
min-height: 150px;
position: relative;
}
::slotted(.wistia_embed) {
position: absolute;
width: 100%;
height: 100%;
}
</style>
<slot></slot>
`);function D(i,t){if(Object.hasOwn(i,t)){const s=i[t];delete i[t],i[t]=s}}class q extends B{static template=P;static skipAttributes=["src"];#t=null;constructor(){super(),D(this,"config")}get config(){return this.#t}set config(t){this.#t=t}get nativeEl(){var t;return((t=this.api)==null?void 0:t.elem())??this.querySelector("video")}async load(){var t;if((t=this.querySelector(".wistia_embed"))==null||t.remove(),!this.src)return;await new Promise(o=>setTimeout(o,50));const s=/(?:wistia\.com|wi\.st)\/(?:medias|embed)\/(.*)$/i,d=this.src.match(s)[1],f={autoPlay:this.autoplay,silentAutoPlay:this.autoplay?"allow":void 0,preload:this.preload??"metadata",playsinline:this.playsInline,endVideoBehavior:this.loop?"loop":void 0,chromeless:!this.controls,playButton:this.controls,muted:this.defaultMuted,...this.#t??{}};this.append(L.content.cloneNode(!0));const c=this.querySelector(".wistia_embed");c.id||(c.id=z(d)),c.classList.add(`wistia_async_${d}`),await F("https://fast.wistia.com/assets/external/E-v1.js","Wistia"),this.api=await new Promise(o=>{globalThis._wq.push({id:c.id,onReady:o,options:f})})}async attributeChangedCallback(t,s,d){if(t==="controls"){switch(await this.loadComplete,t){case"controls":this.api.bigPlayButtonEnabled(this.controls),this.controls?this.api.releaseChromeless():this.api.requestChromeless();break}return}super.attributeChangedCallback(t,s,d)}get duration(){var t;return(t=this.api)==null?void 0:t.duration()}async play(){return await this.loadComplete,this.api.play(),new Promise(t=>this.addEventListener("playing",t))}}const T={};async function F(i,t){if(T[i])return T[i];if(self[t])return self[t];const s=new Promise((d,f)=>{const c=document.createElement("script");c.defer=!0,c.src=i,c.onload=()=>d(self[t]),c.onerror=f,document.head.append(c)});return T[i]=s,s}let U=0;function z(i){const t=++U;return`${i}${t}`}globalThis.customElements&&!globalThis.customElements.get("wistia-video")&&globalThis.customElements.define("wistia-video",q);var V=q,G=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),J={className:"class",htmlFor:"for"};function I(i){return i.toLowerCase()}function j(i){if(typeof i=="boolean")return i?"":void 0;if(typeof i!="function"&&!(typeof i=="object"&&i!==null))return i}function K({react:i,tagName:t,elementClass:s,events:d,displayName:f,defaultProps:c,toAttributeName:e=I,toAttributeValue:o=j}){const u=Number.parseInt(i.version)>=19,r=i.forwardRef((a,p)=>{var M,k;const m=i.useRef(null),E=i.useRef(new Map),C={},_={},v={},w={};for(const[n,l]of Object.entries(a)){if(G.has(n)){v[n]=l;continue}const h=e(J[n]??n);if(s.prototype&&n in s.prototype&&!(n in(((M=globalThis.HTMLElement)==null?void 0:M.prototype)??{}))&&!((k=s.observedAttributes)!=null&&k.some(b=>b===h))){w[n]=l;continue}if(n.startsWith("on")){C[n]=l;continue}const y=o(l);if(h&&y!=null&&(_[h]=String(y),u||(v[h]=y)),h&&u){const b=j(l);y!==b?v[h]=y:v[h]=l}}if(typeof window<"u"){for(const n in C){const l=C[n],h=n.endsWith("Capture"),y=(d?.[n]??n.slice(2).toLowerCase()).slice(0,h?-7:void 0);i.useLayoutEffect(()=>{const b=m?.current;if(!(!b||typeof l!="function"))return b.addEventListener(y,l,h),()=>{b.removeEventListener(y,l,h)}},[m?.current,l])}i.useLayoutEffect(()=>{if(m.current===null)return;const n=new Map;for(const l in w)x(m.current,l,w[l]),E.current.delete(l),n.set(l,w[l]);for(const[l,h]of E.current)x(m.current,l,void 0);E.current=n})}if(typeof window>"u"&&s?.getTemplateHTML&&s?.shadowRootOptions){const{mode:n,delegatesFocus:l}=s.shadowRootOptions,h=i.createElement("template",{shadowrootmode:n,shadowrootdelegatesfocus:l,dangerouslySetInnerHTML:{__html:s.getTemplateHTML(_,a)},key:"ce-la-react-ssr-template-shadow-root"});v.children=[h,v.children]}return i.createElement(t,{...c,...v,ref:i.useCallback(n=>{m.current=n,typeof p=="function"?p(n):p!==null&&(p.current=n)},[p])},v.children)});return r.displayName=f??s.name,r}function x(i,t,s){var d;i[t]=s,s==null&&t in(((d=globalThis.HTMLElement)==null?void 0:d.prototype)??{})&&i.removeAttribute(t)}var X=K({react:W,tagName:"wistia-video",elementClass:V,toAttributeName(i){return i==="muted"?"":i==="defaultMuted"?"muted":I(i)}});export{X as default};
//# sourceMappingURL=react-CVBBC_mw.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+35
View File
@@ -0,0 +1,35 @@
import{R as W}from"./index-5EWtcenj.js";const $=["abort","canplay","canplaythrough","durationchange","emptied","encrypted","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting","waitingforkey","resize","enterpictureinpicture","leavepictureinpicture","webkitbeginfullscreen","webkitendfullscreen","webkitpresentationmodechanged"],A=globalThis.document?.createElement("template");A&&(A.innerHTML=`
<style>
:host {
display: inline-block;
line-height: 0;
}
video,
audio {
max-width: 100%;
max-height: 100%;
min-width: 100%;
min-height: 100%;
}
</style>
<slot></slot>
`);const N=(i,{tag:t,is:s})=>{const d=globalThis.document?.createElement(t,{is:s}),f=d?S(d):[];return class g extends i{static Events=$;static template=A;static skipAttributes=[];static#t;static get observedAttributes(){return g.#l(),[...d?.constructor?.observedAttributes??[],"autopictureinpicture","disablepictureinpicture","disableremoteplayback","autoplay","controls","controlslist","crossorigin","loop","muted","playsinline","poster","preload","src"]}static#l(){if(this.#t)return;this.#t=!0;const e=new Set(this.observedAttributes);e.delete("muted");for(let o of f){if(o in this.prototype)continue;if(typeof d[o]=="function")this.prototype[o]=function(...r){this.#e();const a=()=>this.call?this.call(o,...r):this.nativeEl[o].apply(this.nativeEl,r);return this.loadComplete&&!this.isLoaded?this.loadComplete.then(a):a()};else{let r={get(){this.#e();let a=o.toLowerCase();if(e.has(a)){const p=this.getAttribute(a);return p===null?!1:p===""?!0:p}return this.get?.(o)??this.nativeEl?.[o]??this.#s[o]}};o!==o.toUpperCase()&&(r.set=async function(a){this.#e();let p=o.toLowerCase();if(e.has(p)){a===!0||a===!1||a==null?this.toggleAttribute(p,!!a):this.setAttribute(p,a);return}if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.set){this.set(o,a);return}this.nativeEl[o]=a}),Object.defineProperty(this.prototype,o,r)}}}#o;#n;#r=!1;#i=!1;#a;#s;constructor(){super(),this.shadowRoot||(this.attachShadow({mode:"open"}),this.shadowRoot.append(this.constructor.template.content.cloneNode(!0))),this.load!==g.prototype.load&&(this.loadComplete=new O)}get loadComplete(){return this.#n}set loadComplete(e){this.#i=!1,this.#n=e,e?.then(()=>{this.#i=!0})}get isLoaded(){return this.#i}get nativeEl(){return this.#a??this.shadowRoot.querySelector(t)??this.querySelector(t)}set nativeEl(e){this.#a=e}get defaultMuted(){return this.hasAttribute("muted")}set defaultMuted(e){this.toggleAttribute("muted",!!e)}get src(){return this.getAttribute("src")}set src(e){this.setAttribute("src",`${e}`)}get preload(){return this.getAttribute("preload")??this.nativeEl?.preload}set preload(e){this.setAttribute("preload",`${e}`)}async#e(){if(this.#o)return;this.#o=!0,this.#u(),this.#c();for(let u of f)this.#d(u);const e=new Map,o=this.shadowRoot.querySelector("slot:not([name])");o?.addEventListener("slotchange",()=>{const u=new Map(e);o.assignedElements().filter(r=>["track","source"].includes(r.localName)).forEach(async r=>{u.delete(r);let a=e.get(r);a||(a=r.cloneNode(),e.set(r,a)),this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.nativeEl.append?.(a)}),u.forEach(r=>r.remove())});for(let u of this.constructor.Events)this.shadowRoot.addEventListener?.(u,r=>{r.target===this.nativeEl&&this.dispatchEvent(new CustomEvent(r.type,{detail:r.detail}))},!0)}#d(e){if(Object.prototype.hasOwnProperty.call(this,e)){const o=this[e];delete this[e],this[e]=o}}#u(){const e=document.createElement(t,{is:s});e.muted=this.hasAttribute("muted");for(let{name:o,value:u}of this.attributes)e.setAttribute(o,u);this.#s={};for(let o of S(e))this.#s[o]=e[o];e.removeAttribute("src"),e.load()}async#c(){if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,!this.nativeEl){const e=document.createElement(t,{is:s});e.part=t,this.shadowRoot.append(e)}this.nativeEl.muted=this.hasAttribute("muted")}attributeChangedCallback(e,o,u){this.#e(),e==="src"&&this.load!==g.prototype.load&&this.#h(),this.#p(e,o,u)}async#h(){this.#r&&(this.loadComplete=new O),this.#r=!0,await Promise.resolve(),await this.load(),this.loadComplete?.resolve(),await this.loadComplete}async#p(e,o,u){this.loadComplete&&!this.isLoaded&&await this.loadComplete,!["id","class",...this.constructor.skipAttributes].includes(e)&&(u===null?this.nativeEl.removeAttribute?.(e):this.nativeEl.setAttribute?.(e,u))}connectedCallback(){this.#e()}}};function S(i){let t=[];for(let s=Object.getPrototypeOf(i);s&&s!==HTMLElement.prototype;s=Object.getPrototypeOf(s))t.push(...Object.getOwnPropertyNames(s));return t}class O extends Promise{constructor(t=()=>{}){let s,d;super((f,c)=>{t(f,c),s=f,d=c}),this.resolve=s,this.reject=d}}const B=globalThis.document?N(HTMLElement,{tag:"video"}):class{};globalThis.document&&N(HTMLElement,{tag:"audio"});var H,R;const L=(H=globalThis.document)==null?void 0:H.createElement("template");L&&(L.innerHTML=`
<div class="wistia_embed"></div>
`);const P=(R=globalThis.document)==null?void 0:R.createElement("template");P&&(P.innerHTML=`
<style>
:host {
display: inline-block;
min-width: 300px;
min-height: 150px;
position: relative;
}
::slotted(.wistia_embed) {
position: absolute;
width: 100%;
height: 100%;
}
</style>
<slot></slot>
`);function D(i,t){if(Object.hasOwn(i,t)){const s=i[t];delete i[t],i[t]=s}}class q extends B{static template=P;static skipAttributes=["src"];#t=null;constructor(){super(),D(this,"config")}get config(){return this.#t}set config(t){this.#t=t}get nativeEl(){var t;return((t=this.api)==null?void 0:t.elem())??this.querySelector("video")}async load(){var t;if((t=this.querySelector(".wistia_embed"))==null||t.remove(),!this.src)return;await new Promise(o=>setTimeout(o,50));const s=/(?:wistia\.com|wi\.st)\/(?:medias|embed)\/(.*)$/i,d=this.src.match(s)[1],f={autoPlay:this.autoplay,silentAutoPlay:this.autoplay?"allow":void 0,preload:this.preload??"metadata",playsinline:this.playsInline,endVideoBehavior:this.loop?"loop":void 0,chromeless:!this.controls,playButton:this.controls,muted:this.defaultMuted,...this.#t??{}};this.append(L.content.cloneNode(!0));const c=this.querySelector(".wistia_embed");c.id||(c.id=z(d)),c.classList.add(`wistia_async_${d}`),await F("https://fast.wistia.com/assets/external/E-v1.js","Wistia"),this.api=await new Promise(o=>{globalThis._wq.push({id:c.id,onReady:o,options:f})})}async attributeChangedCallback(t,s,d){if(t==="controls"){switch(await this.loadComplete,t){case"controls":this.api.bigPlayButtonEnabled(this.controls),this.controls?this.api.releaseChromeless():this.api.requestChromeless();break}return}super.attributeChangedCallback(t,s,d)}get duration(){var t;return(t=this.api)==null?void 0:t.duration()}async play(){return await this.loadComplete,this.api.play(),new Promise(t=>this.addEventListener("playing",t))}}const T={};async function F(i,t){if(T[i])return T[i];if(self[t])return self[t];const s=new Promise((d,f)=>{const c=document.createElement("script");c.defer=!0,c.src=i,c.onload=()=>d(self[t]),c.onerror=f,document.head.append(c)});return T[i]=s,s}let U=0;function z(i){const t=++U;return`${i}${t}`}globalThis.customElements&&!globalThis.customElements.get("wistia-video")&&globalThis.customElements.define("wistia-video",q);var V=q,G=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),J={className:"class",htmlFor:"for"};function I(i){return i.toLowerCase()}function j(i){if(typeof i=="boolean")return i?"":void 0;if(typeof i!="function"&&!(typeof i=="object"&&i!==null))return i}function K({react:i,tagName:t,elementClass:s,events:d,displayName:f,defaultProps:c,toAttributeName:e=I,toAttributeValue:o=j}){const u=Number.parseInt(i.version)>=19,r=i.forwardRef((a,p)=>{var M,k;const m=i.useRef(null),E=i.useRef(new Map),C={},_={},v={},w={};for(const[n,l]of Object.entries(a)){if(G.has(n)){v[n]=l;continue}const h=e(J[n]??n);if(s.prototype&&n in s.prototype&&!(n in(((M=globalThis.HTMLElement)==null?void 0:M.prototype)??{}))&&!((k=s.observedAttributes)!=null&&k.some(b=>b===h))){w[n]=l;continue}if(n.startsWith("on")){C[n]=l;continue}const y=o(l);if(h&&y!=null&&(_[h]=String(y),u||(v[h]=y)),h&&u){const b=j(l);y!==b?v[h]=y:v[h]=l}}if(typeof window<"u"){for(const n in C){const l=C[n],h=n.endsWith("Capture"),y=(d?.[n]??n.slice(2).toLowerCase()).slice(0,h?-7:void 0);i.useLayoutEffect(()=>{const b=m?.current;if(!(!b||typeof l!="function"))return b.addEventListener(y,l,h),()=>{b.removeEventListener(y,l,h)}},[m?.current,l])}i.useLayoutEffect(()=>{if(m.current===null)return;const n=new Map;for(const l in w)x(m.current,l,w[l]),E.current.delete(l),n.set(l,w[l]);for(const[l,h]of E.current)x(m.current,l,void 0);E.current=n})}if(typeof window>"u"&&s?.getTemplateHTML&&s?.shadowRootOptions){const{mode:n,delegatesFocus:l}=s.shadowRootOptions,h=i.createElement("template",{shadowrootmode:n,shadowrootdelegatesfocus:l,dangerouslySetInnerHTML:{__html:s.getTemplateHTML(_,a)},key:"ce-la-react-ssr-template-shadow-root"});v.children=[h,v.children]}return i.createElement(t,{...c,...v,ref:i.useCallback(n=>{m.current=n,typeof p=="function"?p(n):p!==null&&(p.current=n)},[p])},v.children)});return r.displayName=f??s.name,r}function x(i,t,s){var d;i[t]=s,s==null&&t in(((d=globalThis.HTMLElement)==null?void 0:d.prototype)??{})&&i.removeAttribute(t)}var X=K({react:W,tagName:"wistia-video",elementClass:V,toAttributeName(i){return i==="muted"?"":i==="defaultMuted"?"muted":I(i)}});export{X as default};
//# sourceMappingURL=react-DxdYv-AY.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
import{_ as N,R as A}from"./index-5EWtcenj.js";import{M as D,C as S}from"./mixin-BY5xW4uI.js";class P extends D(S){static shadowRootOptions={...S.shadowRootOptions};static getTemplateHTML=i=>{const{src:n,...o}=i;return S.getTemplateHTML(o)};#t;attributeChangedCallback(i,n,o){i!=="src"&&super.attributeChangedCallback(i,n,o),i==="src"&&n!=o&&this.load()}async _initThumbnails(i){const n=async(d,m)=>{const v=[],g=i.timescale||1,h=i.startNumber||1,E=i.presentationTimeOffset?i.presentationTimeOffset/g:0,_=i.segmentDuration;for(let l=0;l<d;l++){const T=C({thIndex:l,thduration:m,ttiles:d,tduration:_,startNumber:h,pto:E}),w=T+m,M=new Promise((f,y)=>{this.api.provideThumbnail(T,({url:a,width:s,height:c,x:p,y:b})=>{try{const R=new VTTCue(T,w,`${a}#xywh=${p},${b},${s},${c}`);f(R)}catch(R){y(R)}})});v.push(M)}return await Promise.all(v).catch(l=>console.error("Error processing thumbnails",l))},{totalThumbnails:o,thumbnailDuration:e}=$(i),u=await n(o,e);let r=this.nativeEl.querySelector('track[label="thumbnails"]');if(!r){r=O(),this.nativeEl.appendChild(r);const d=x(u);r.src=d,r.dispatchEvent(new Event("change"))}}async load(){if(this.#t){this.api.attachSource(this.src);return}this.#t=!0;const i=await N(()=>import("./dash.all.min-C9gxBrL1.js"),[]);this.api=i.MediaPlayer().create(),this.api.initialize(this.nativeEl,this.src,this.autoplay),this.api.on(i.MediaPlayer.events.STREAM_INITIALIZED,()=>{const n=this.api.getRepresentationsByType("video");let o=this.videoTracks.getTrackById("main");o||(o=this.addVideoTrack("main"),o.id="main",o.selected=!0),n.forEach(e=>{const u=e.bandwidth??e.bitrate??(Number.isFinite(e.bitrateInKbit)?e.bitrateInKbit*1e3:void 0),r=o.addRendition(e.id,e.width,e.height,e.mimeType??e.codec,u);r.id=e.id}),this.videoRenditions.addEventListener("change",()=>{const e=this.videoRenditions[this.videoRenditions.selectedIndex];e?.id?(this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),this.api.setRepresentationForTypeById("video",e.id,!0)):this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}})}),this.api.isDynamic()||this.api.getRepresentationsByType("image").forEach(async(u,r)=>{r>0||this._initThumbnails(u)})})}}function $(t){var i,n;const o=t.essentialProperties[0],[e,u]=o.value.split("x").map(Number),r=e*u,d=((n=(i=t.adaptation)==null?void 0:i.period)==null?void 0:n.duration)||null,m=t.segmentDuration,v=t.timescale||1,h=m/v/r;return{totalThumbnails:d!=null?Math.ceil(d/h):Math.ceil(m/h),thumbnailDuration:h}}function C({thIndex:t,tduration:i,thduration:n,ttiles:o,startNumber:e,pto:u}){const r=Math.floor(t/o)+e,d=t%o+1,m=(r-1)*i-u,v=(d-1)*n;return m+v}function O(){const t=document.createElement("track");return t.kind="metadata",t.label="thumbnails",t.srclang="en",t.mode="hidden",t.default=!0,t}function x(t){let i=`WEBVTT
`;for(const e of t)i+=`${o(e.startTime)} --> ${o(e.endTime)}
`,i+=`${e.text}
`;const n=new Blob([i],{type:"text/vtt"});return URL.createObjectURL(n);function o(e){const u=String(Math.floor(e/3600)).padStart(2,"0"),r=String(Math.floor(e%3600/60)).padStart(2,"0"),d=(e%60).toFixed(3).padStart(6,"0");return`${u}:${r}:${d}`}}globalThis.customElements&&!globalThis.customElements.get("dash-video")&&globalThis.customElements.define("dash-video",P);var B=P,H=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),F={className:"class",htmlFor:"for"};function I(t){return t.toLowerCase()}function k(t){if(typeof t=="boolean")return t?"":void 0;if(typeof t!="function"&&!(typeof t=="object"&&t!==null))return t}function W({react:t,tagName:i,elementClass:n,events:o,displayName:e,defaultProps:u,toAttributeName:r=I,toAttributeValue:d=k}){const m=Number.parseInt(t.version)>=19,v=t.forwardRef((g,h)=>{var E,_;const l=t.useRef(null),T=t.useRef(new Map),w={},M={},f={},y={};for(const[a,s]of Object.entries(g)){if(H.has(a)){f[a]=s;continue}const c=r(F[a]??a);if(n.prototype&&a in n.prototype&&!(a in(((E=globalThis.HTMLElement)==null?void 0:E.prototype)??{}))&&!((_=n.observedAttributes)!=null&&_.some(b=>b===c))){y[a]=s;continue}if(a.startsWith("on")){w[a]=s;continue}const p=d(s);if(c&&p!=null&&(M[c]=String(p),m||(f[c]=p)),c&&m){const b=k(s);p!==b?f[c]=p:f[c]=s}}if(typeof window<"u"){for(const a in w){const s=w[a],c=a.endsWith("Capture"),p=(o?.[a]??a.slice(2).toLowerCase()).slice(0,c?-7:void 0);t.useLayoutEffect(()=>{const b=l?.current;if(!(!b||typeof s!="function"))return b.addEventListener(p,s,c),()=>{b.removeEventListener(p,s,c)}},[l?.current,s])}t.useLayoutEffect(()=>{if(l.current===null)return;const a=new Map;for(const s in y)L(l.current,s,y[s]),T.current.delete(s),a.set(s,y[s]);for(const[s,c]of T.current)L(l.current,s,void 0);T.current=a})}if(typeof window>"u"&&n?.getTemplateHTML&&n?.shadowRootOptions){const{mode:a,delegatesFocus:s}=n.shadowRootOptions,c=t.createElement("template",{shadowrootmode:a,shadowrootdelegatesfocus:s,dangerouslySetInnerHTML:{__html:n.getTemplateHTML(M,g)},key:"ce-la-react-ssr-template-shadow-root"});f.children=[c,f.children]}return t.createElement(i,{...u,...f,ref:t.useCallback(a=>{l.current=a,typeof h=="function"?h(a):h!==null&&(h.current=a)},[h])},f.children)});return v.displayName=e??n.name,v}function L(t,i,n){var o;t[i]=n,n==null&&i in(((o=globalThis.HTMLElement)==null?void 0:o.prototype)??{})&&t.removeAttribute(i)}var V=W({react:A,tagName:"dash-video",elementClass:B,toAttributeName(t){return t==="muted"?"":t==="defaultMuted"?"muted":I(t)}});export{V as default};
//# sourceMappingURL=react-gdx3v4jU.js.map
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -12,8 +12,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- site-metadata:inject -->
<!-- analytics:inject -->
<script type="module" crossorigin src="/assets/index-n0JxE1Mv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-xCRVLGjq.css">
<script type="module" crossorigin src="/assets/index-dJH5nUWV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DOmNOCX4.css">
</head>
<body>
<div id="root"></div>
@@ -173,6 +173,8 @@ test('service definitions generate public feature paths without a separate regis
{ key: 'homeAssistant', path: ['homeAssistant', 'enabled'] },
{ key: 'neato', path: ['homeAssistant', 'neato', 'enabled'] },
{ key: 'lift', path: ['homeAssistant', 'lift', 'enabled'] },
// Activities remain independently configurable from the shared connection.
{ key: 'homeAssistantActivities', path: ['homeAssistantActivities', 'enabled'] },
{ key: 'roomCameras', path: ['roomCameras', 'enabled'] },
{ key: 'ptzCamera', path: ['ptzCamera', 'enabled'] },
{ key: 'kinect', path: ['kinect', 'enabled'] },
@@ -182,6 +184,7 @@ test('service definitions generate public feature paths without a separate regis
{ key: 'discord', path: ['discord', 'enabled'] },
{ key: 'socials', path: ['socials', 'enabled'] },
{ key: 'fleetReports', path: ['fleetReports', 'enabled'] },
{ key: 'roverLearning', path: ['roverLearning', 'enabled'] },
]);
});
+7
View File
@@ -12,6 +12,8 @@ const bandwidthSavings = require('../helpers/bandwidthSavings.configuration');
const audioForward = require('../services/audioForwardService/configuration');
const audioLevels = require('../services/audioLevelsService/configuration');
const homeAssistant = require('../services/homeAssistantService/configuration');
const homeAssistantActivities = require('../services/homeAssistantActivitiesService/configuration');
const roomAudio = require('../services/roomAudioService/configuration');
const roomCameras = require('../services/roomCameraService/configuration');
const ptzCamera = require('../services/ptzCameraService/configuration');
const kinect = require('../services/kinectService/configuration');
@@ -21,6 +23,7 @@ const barcodeScanner = require('../services/barcodeScannerService/configuration'
const commands = require('../services/operatorCommandService/configuration');
const discord = require('../services/discordBotService/configuration');
const fleetReports = require('../services/fleetReportService/configuration');
const roverLearning = require('../services/roverLearningService/configuration');
/*
Object property order is preserved by JSON serialization and JSON Schema
@@ -41,7 +44,10 @@ const definitions = [
audioForward,
audioLevels,
homeAssistant,
// Activities own a separate catalog even though the HA connection is shared.
homeAssistantActivities,
roomCameras,
roomAudio,
ptzCamera,
kinect,
balanceBoard,
@@ -52,6 +58,7 @@ const definitions = [
sessionConfiguration.socials,
sessionConfiguration.driverAd,
fleetReports,
roverLearning,
];
const defaultConfig = Object.fromEntries(
+41 -1
View File
@@ -1,6 +1,6 @@
// Configuration Database Migrations
// Purpose: Applies ordered, transactional schema changes to the configuration and administration database.
// Scope: Owns database structure only; configuration-document evolution belongs to the ordered definition and validation.
// Scope: Upgrades database structure and stored configuration documents before current-schema validation.
const migrations = [
{
version: 1,
@@ -72,6 +72,46 @@ const migrations = [
});
},
},
{
version: 3,
run(db) {
// Migrate historical revisions too, so rolling back settings cannot
// restore the removed training-visit limit and break startup again.
db.prepare(`
UPDATE configuration_revisions
SET config_json = json_remove(config_json, '$.roverLearning.training.roundsPerSession')
WHERE json_type(config_json, '$.roverLearning.training.roundsPerSession') IS NOT NULL
`).run();
},
},
{
version: 4,
run(db) {
const rows = db.prepare('SELECT id, config_json FROM configuration_revisions').all();
const update = db.prepare('UPDATE configuration_revisions SET config_json = ? WHERE id = ?');
// Upgrade history as well as the active revision so a settings rollback
// cannot reintroduce fields rejected by the new Automations schema.
for (const row of rows) {
const config = JSON.parse(row.config_json);
let changed = false;
for (const item of config.homeAssistantActivities?.items || []) {
const hasAction = Object.hasOwn(item, 'idleAction');
const hasValue = Object.hasOwn(item, 'idleValue');
if (!hasAction && !hasValue) continue;
item.automations ??= {};
item.automations.idle ??= {};
// Preserve any explicitly saved new-format setting when both formats
// exist, including an empty text value used to clear an entity.
if (hasAction && !Object.hasOwn(item.automations.idle, 'action')) item.automations.idle.action = item.idleAction;
if (hasValue && !Object.hasOwn(item.automations.idle, 'value')) item.automations.idle.value = item.idleValue;
delete item.idleAction;
delete item.idleValue;
changed = true;
}
if (changed) update.run(JSON.stringify(config), row.id);
}
},
},
];
function applySchemaMigrations(db) {
@@ -44,7 +44,7 @@ module.exports = {
goal: 400,
async run(ctx) {
startStrobe(ctx, { endsAt: Date.now() + STROBE_MS, on: false });
ctx.sendAlert({ color: '#ffc107', title: 'Light Strobe', message: 'All room controls strobing for 60 seconds.' });
ctx.sendAlert({ color: '#ffc107', title: 'Light Strobe', message: 'All room lights strobing for 60 seconds.' });
},
async recover(ctx, effect) {
if (!effect || Number(effect.endsAt || 0) <= Date.now()) {
+12 -2
View File
@@ -7,6 +7,7 @@ const logger = require('../../globals/logger').child('assignment');
const { MODES, getMode, modeEvents } = require('../modeManager');
const { roleEvents, getRole, isAdmin, isLockdownAdmin } = require('../roleService');
const roverManager = require('../roverManager');
const { getOperatingMode } = require('../operatingModeService');
const { compareRoversForAssignment } = require('./roverRanking');
const socketRefs = new Map(); // socketId -> socket
@@ -131,7 +132,7 @@ roverManager.managerEvents.on('switch', ({ socketId, roverId }) => {
});
function assignSocket(socket, options = {}) {
if (!socket || isAdmin(socket) || getRole(socket) !== 'user') {
if (!socket || getOperatingMode(socket) !== 'rover' || isAdmin(socket) || getRole(socket) !== 'user') {
return;
}
// avoid double assignment
@@ -311,13 +312,22 @@ function describeAssignment(socketId) {
module.exports = {
assignmentEvents,
resumeAssignment: assignSocket,
releaseForOperatingMode: (socket) => {
// Include direct admin control as well as automatic assignment ownership.
const roverIds = new Set(roverManager.getRoversForSocket(socket.id));
const assigned = assignments.get(socket.id);
if (assigned) roverIds.add(assigned);
waiting.delete(socket.id);
roverIds.forEach((roverId) => forceRelease(roverId, socket.id));
},
describeAssignment,
forceRelease,
forceReleaseWithNotice,
rerollAssignments,
getAssignedRover: (socketId) => assignments.get(socketId) || null,
moveAssignment: (socket, roverId, { releasePrevious = true } = {}) => {
if (!socket || !roverId) return;
if (!socket || !roverId || getOperatingMode(socket) !== 'rover') return;
const previous = assignments.get(socket.id);
if (previous && previous !== roverId && releasePrevious) {
roverManager.releaseControl(previous, socket);
@@ -4,7 +4,7 @@
const path = require('path');
const { subscribe } = require('../eventBus');
const DONE_CHARGING_SOUND_PATH = path.resolve(__dirname, '..', '..', '..', 'public', 'donecharging.wav');
const DONE_CHARGING_SOUND_PATH = path.resolve(__dirname, '..', '..', '..', 'assets', 'donecharging.wav');
function registerChargeCompleteSound(deps) {
const {
@@ -12,22 +12,18 @@ function registerChargeCompleteSound(deps) {
playServerAudioFile,
} = deps;
subscribe('battery.unlocked', (event = {}) => {
subscribe('battery.chargeComplete', (event = {}) => {
const roverId = String(event?.payload?.roverId || '').trim();
if (!roverId) return;
try {
// battery.unlocked is the existing server-side definition of "done
// charging" because it only fires after the battery manager has waited
// through its full charge-release policy. Playing from this event avoids
// duplicating charging thresholds or raw sensor-state guesses here.
playServerAudioFile(roverId, DONE_CHARGING_SOUND_PATH, {
source: 'charge-complete',
});
logger.info('Played charging complete sound', { roverId, filePath: DONE_CHARGING_SOUND_PATH });
logger.info('Started charging complete sound', { roverId, filePath: DONE_CHARGING_SOUND_PATH });
} catch (err) {
// A missing/offline rover or unavailable ffmpeg should not affect the
// battery manager's unlock decision. The sound is an announcement layered
// battery manager's processing. The sound is an announcement layered
// on top of the state transition, so failures are logged and contained.
logger.warn('Failed to play charging complete sound', {
roverId,
+2 -1
View File
@@ -124,7 +124,7 @@ function grantExternalSpectatorAccessAfterAdminLogin(socket) {
}
io.on('connection', (socket) => {
const requestedRole = socket.handshake?.query?.role;
const requestedRole = socket.handshake?.auth?.role || socket.handshake?.query?.role;
/*
Role is assigned before the browser's full identity heartbeat has completed.
For admin-gated external spectators, fail closed here; the spectator page can
@@ -188,4 +188,5 @@ module.exports = {
isAdmin,
isLockdownAdmin,
authenticate,
canBecomeSpectator,
};
@@ -109,6 +109,7 @@ async function copyDurableTree(sourceDir, destinationDir, relativeDir = '') {
const relativePath = path.join(relativeDir, entry.name);
if (!relativeDir && EXCLUDED_TOP_LEVEL_NAMES.has(entry.name)) continue;
if (!relativeDir && DATABASE_FILES.has(entry.name)) continue;
if (relativePath === path.join('rover-learning', 'recordings')) continue;
if (entry.isSymbolicLink()) throw new Error(`Backup cannot include symbolic link: ${relativePath}`);
if (entry.isDirectory()) {
skipped.push(...await copyDurableTree(sourceDir, destinationDir, relativePath));

Some files were not shown because too many files have changed in this diff Show More