mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-15 11:12:16 -05:00
Compare commits
74
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3d4d9af9b | ||
|
|
ab730ee8a3 | ||
|
|
8fe4f2f98e | ||
|
|
89f3a1cb65 | ||
|
|
79d36cae93 | ||
|
|
1dcd7e5d7d | ||
|
|
b4bc99ab27 | ||
|
|
c94e1a0508 | ||
|
|
abcb0871b2 | ||
|
|
1ffe680895 | ||
|
|
ed26b90384 | ||
|
|
2b48fee00f | ||
|
|
1284125fff | ||
|
|
28d0b1437d | ||
|
|
9d3b8df4bf | ||
|
|
9d42cb5581 | ||
|
|
e670226f8c | ||
|
|
0669f29a4a | ||
|
|
38d3465bf8 | ||
|
|
6bfbb8ad80 | ||
|
|
ba2f66ebc1 | ||
|
|
fde2fca0a0 | ||
|
|
d36ea4f672 | ||
|
|
4be6e6c9a2 | ||
|
|
7bac2fc447 | ||
|
|
e8a407cd49 | ||
|
|
bee1866a70 | ||
|
|
ddfc889a8c | ||
|
|
c71267873a | ||
|
|
1ba4ebe5ff | ||
|
|
607d327e9c | ||
|
|
1bdca60648 | ||
|
|
ea4af2ebb3 | ||
|
|
b2bbaf2085 | ||
|
|
18e7fb8d1b | ||
|
|
ba71106544 | ||
|
|
a395656056 | ||
|
|
50c2c1f5db | ||
|
|
b59389870e | ||
|
|
cd4aee4e74 | ||
|
|
9de0bfb1e7 | ||
|
|
008c90edf1 | ||
|
|
eed88983ca | ||
|
|
923c04736b | ||
|
|
bdc3eb640d | ||
|
|
01354c56e6 | ||
|
|
0ff03a74a5 | ||
|
|
73a78eef20 | ||
|
|
a157ece816 | ||
|
|
8cb7cc5bdc | ||
|
|
dbcb7f6a7a | ||
|
|
34936ab05c | ||
|
|
7231809140 | ||
|
|
8ca714cf1a | ||
|
|
ffd34de56c | ||
|
|
aeeff6fe1b | ||
|
|
846c9dae55 | ||
|
|
e750712a39 | ||
|
|
ff8b0c530e | ||
|
|
69c5bfe544 | ||
|
|
3ad95b376a | ||
|
|
bda740b08d | ||
|
|
3c8c5f5883 | ||
|
|
c35544e515 | ||
|
|
0df4a5ec89 | ||
|
|
8e301e7db7 | ||
|
|
b712fb89c6 | ||
|
|
fe2c29371f | ||
|
|
61ee692815 | ||
|
|
487666eed8 | ||
|
|
51b42ea778 | ||
|
|
318cad1846 | ||
|
|
f3e3b6a803 | ||
|
|
b48348d89e |
@@ -5,6 +5,7 @@
|
|||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
webui/.cache/mediamtx/
|
||||||
# Neither the legacy state directory nor the Compose-mounted replacement may
|
# Neither the legacy state directory nor the Compose-mounted replacement may
|
||||||
# enter an image build. This prevents credentials and backups from becoming
|
# enter an image build. This prevents credentials and backups from becoming
|
||||||
# image layers after an operator has started using either deployment layout.
|
# image layers after an operator has started using either deployment layout.
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ jobs:
|
|||||||
# replaced on each successful push, just as main replaces `latest`.
|
# replaced on each successful push, just as main replaces `latest`.
|
||||||
- name: Build and optionally publish
|
- name: Build and optionally publish
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
|
env:
|
||||||
|
DOCKER_BUILD_RECORD_RETENTION_DAYS: 3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ create_2_Open_Interface_Spec.txt
|
|||||||
|
|
||||||
logs
|
logs
|
||||||
node_modules/
|
node_modules/
|
||||||
|
webui/.cache/mediamtx/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
.pio
|
.pio
|
||||||
|
|||||||
+43
-19
@@ -9,15 +9,21 @@
|
|||||||
ARG FEDORA_VERSION=43
|
ARG FEDORA_VERSION=43
|
||||||
|
|
||||||
FROM fedora:${FEDORA_VERSION} AS architecture-check
|
FROM fedora:${FEDORA_VERSION} AS architecture-check
|
||||||
|
ARG FEDORA_VERSION
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
# The central server is currently deployed and verified only on amd64. Failing
|
# The central server is currently deployed and verified only on amd64. Failing
|
||||||
# here avoids publishing an ARM image whose native workers and hardware paths
|
# here avoids publishing an ARM image whose native workers and hardware paths
|
||||||
# have never been exercised on a real ARM server.
|
# have never been exercised on a real ARM server.
|
||||||
RUN test "${TARGETARCH}" = "amd64" || (echo "MultiRover server images support only linux/amd64." >&2; exit 1)
|
RUN test "${TARGETARCH}" = "amd64" || (echo "MultiRover server images support only linux/amd64." >&2; exit 1)
|
||||||
|
|
||||||
FROM architecture-check AS webui-build
|
# Both Node builders reuse the same installed toolchain layer.
|
||||||
RUN dnf install -y --setopt=install_weak_deps=False nodejs npm \
|
# Fedora 43 uses DNF5. Cache metadata and RPMs outside image layers, and keep
|
||||||
&& dnf clean all
|
# 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
|
WORKDIR /build
|
||||||
# Copy dependency manifests first so ordinary source edits retain the expensive
|
# Copy dependency manifests first so ordinary source edits retain the expensive
|
||||||
# npm cache layer. npm ci makes the checked-in lockfile the exact dependency
|
# 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/
|
COPY webui/package.json webui/package-lock.json ./webui/
|
||||||
RUN --mount=type=cache,target=/root/.npm \
|
RUN --mount=type=cache,target=/root/.npm \
|
||||||
cd webui && npm ci
|
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
|
COPY webui ./webui
|
||||||
# Vite deliberately emits into ../server/public. Create that destination in the
|
# Vite deliberately emits into ../server/public. Create that destination in the
|
||||||
# isolated builder and copy only its finished files into the runtime stage.
|
# isolated builder and copy only its finished files into the runtime stage.
|
||||||
RUN mkdir -p server/public && cd webui && npm run build
|
RUN mkdir -p server/public && cd webui && npm run build
|
||||||
|
|
||||||
FROM architecture-check AS server-dependencies
|
FROM node-build-base AS server-dependencies
|
||||||
RUN dnf install -y --setopt=install_weak_deps=False nodejs npm gcc-c++ make python3 \
|
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-server-dependencies,target=/var/cache/libdnf5,sharing=locked \
|
||||||
&& dnf clean all
|
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True gcc-c++ make python3
|
||||||
WORKDIR /build/server
|
WORKDIR /build/server
|
||||||
COPY server/package.json server/package-lock.json ./
|
COPY server/package.json server/package-lock.json ./
|
||||||
# Native Node modules compile here when a matching prebuild is unavailable;
|
# 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
|
npm ci --omit=dev
|
||||||
|
|
||||||
FROM architecture-check AS native-workers
|
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 \
|
bluez-libs-devel \
|
||||||
gcc-c++ \
|
gcc-c++ \
|
||||||
libcap \
|
libcap \
|
||||||
@@ -49,8 +59,7 @@ RUN dnf install -y --setopt=install_weak_deps=False \
|
|||||||
libusb1-devel \
|
libusb1-devel \
|
||||||
make \
|
make \
|
||||||
pkgconf-pkg-config \
|
pkgconf-pkg-config \
|
||||||
wiiuse-devel \
|
wiiuse-devel
|
||||||
&& dnf clean all
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY server/src/services/kinectService/native ./kinect
|
COPY server/src/services/kinectService/native ./kinect
|
||||||
COPY server/src/services/balanceBoardService/native ./balance-board
|
COPY server/src/services/balanceBoardService/native ./balance-board
|
||||||
@@ -66,8 +75,8 @@ ARG NEOLINK_VERSION=0.6.2
|
|||||||
ARG NEOLINK_SHA256=0cb963b44dca7ccc5333154092186e1536c687aef0e1ad119b7f310a7471dbbb
|
ARG NEOLINK_SHA256=0cb963b44dca7ccc5333154092186e1536c687aef0e1ad119b7f310a7471dbbb
|
||||||
ARG GOOGLE_TTS_VERSION=26.5
|
ARG GOOGLE_TTS_VERSION=26.5
|
||||||
ARG GOOGLE_TTS_SHA256=6a9eae6726871788da52e767dad964a1c83e7feb7e7dbac508a2574a2345ac24
|
ARG GOOGLE_TTS_SHA256=6a9eae6726871788da52e767dad964a1c83e7feb7e7dbac508a2574a2345ac24
|
||||||
RUN dnf install -y --setopt=install_weak_deps=False curl findutils tar unzip xz \
|
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-packaged-tools,target=/var/cache/libdnf5,sharing=locked \
|
||||||
&& dnf clean all
|
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True curl findutils tar unzip xz
|
||||||
WORKDIR /build/tools
|
WORKDIR /build/tools
|
||||||
# Each external artifact is pinned and checked before extraction. A changed or
|
# Each external artifact is pinned and checked before extraction. A changed or
|
||||||
# truncated upstream download therefore fails the image build instead of being
|
# 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 d -exec chmod 0755 {} + \
|
||||||
&& find /output/opt/roverd/googletts -type f -exec chmod 0644 {} +
|
&& 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
|
FROM fedora:${FEDORA_VERSION} AS runtime
|
||||||
ARG FEDORA_VERSION
|
ARG FEDORA_VERSION
|
||||||
ARG TARGETARCH
|
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
|
# replay output path. Enable RPM Fusion Free before installing runtime packages
|
||||||
# so the image receives the complete FFmpeg build instead of requiring replay
|
# so the image receives the complete FFmpeg build instead of requiring replay
|
||||||
# code to work around a deployment-only codec omission.
|
# 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"
|
"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
|
# This is the complete runtime package set. Build headers and compilers live in
|
||||||
# earlier stages, while media, TTS, USB, and Bluetooth libraries remain here
|
# earlier stages, while media, TTS, USB, and Bluetooth libraries remain here
|
||||||
# because enabled services invoke them after startup. Weak dependencies are
|
# because enabled services invoke them after startup. Weak dependencies are
|
||||||
# deliberately disabled: Fedora otherwise installs desktop portals, graphical
|
# deliberately disabled: Fedora otherwise installs desktop portals, graphical
|
||||||
# themes, and GPU drivers that a headless server neither starts nor uses.
|
# 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 \
|
||||||
bluez-libs \
|
bluez-libs \
|
||||||
espeak \
|
espeak \
|
||||||
@@ -134,20 +155,15 @@ RUN dnf install -y --setopt=install_weak_deps=False \
|
|||||||
tini \
|
tini \
|
||||||
wiiuse \
|
wiiuse \
|
||||||
--allowerasing \
|
--allowerasing \
|
||||||
&& dnf clean all \
|
|
||||||
&& useradd --uid 1000 --create-home --home-dir /home/multirover --shell /sbin/nologin multirover \
|
&& 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
|
&& install -d -o multirover -g multirover -m 0755 /data /opt/multirover/server
|
||||||
|
|
||||||
WORKDIR /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=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/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=native-workers /build/balance-board/balance_board_worker ./src/services/balanceBoardService/native/balance_board_worker
|
||||||
COPY --from=packaged-tools /output/ /
|
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/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
|
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 \
|
&& test -s /tmp/chromegtts-smoke.wav \
|
||||||
&& rm /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 \
|
ENV NODE_ENV=production \
|
||||||
SERVER_DATA_DIR=/data \
|
SERVER_DATA_DIR=/data \
|
||||||
ROVER_SNAPSHOT_WRITER_BIN=/usr/local/bin/rover-snapshot-writer.sh
|
ROVER_SNAPSHOT_WRITER_BIN=/usr/local/bin/rover-snapshot-writer.sh
|
||||||
|
|||||||
+5
-1
@@ -23,9 +23,13 @@ services:
|
|||||||
# Required for Bluetooth hardware such as the Balance Board.
|
# Required for Bluetooth hardware such as the Balance Board.
|
||||||
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro
|
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro
|
||||||
|
|
||||||
# Required for Kinect USB access.
|
# Kinect USB access.
|
||||||
devices:
|
devices:
|
||||||
- /dev/bus/usb:/dev/bus/usb
|
- /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:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -26,6 +26,8 @@ Phase 1 must be complete and verified before Phase 2 begins. Containerization mu
|
|||||||
|
|
||||||
## Decision log
|
## 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-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: 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.
|
- 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.
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ socket.on('connect', async () => {
|
|||||||
// previously stored session with this object instead of merging snapshots.
|
// previously stored session with this object instead of merging snapshots.
|
||||||
socket.on('session:sync', (session) => {
|
socket.on('session:sync', (session) => {
|
||||||
console.log('Session:', session);
|
console.log('Session:', session);
|
||||||
|
console.log('Server mode:', session.mode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// chat:init contains the recent chat history available when the bot connects.
|
// chat:init contains the recent chat history available when the bot connects.
|
||||||
@@ -105,6 +106,21 @@ Run it with:
|
|||||||
node bot.js
|
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
|
## Events used
|
||||||
|
|
||||||
- `nickname:set` sets the bot's visible nickname.
|
- `nickname:set` sets the bot's visible nickname.
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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.")
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
autoChargeTimeout = 1 * time.Second
|
autoChargeTimeout = 10 * time.Second
|
||||||
autoChargeCooldown = 0 * time.Minute
|
autoChargeCooldown = 0 * time.Minute
|
||||||
sourceHomeBase = 1 << 1
|
sourceHomeBase = 1 << 1
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -68,6 +68,7 @@ require('./src/services/batteryManager');
|
|||||||
// subscriptions see fully decoded state without becoming an initialization
|
// subscriptions see fully decoded state without becoming an initialization
|
||||||
// dependency of either control path.
|
// dependency of either control path.
|
||||||
require('./src/services/fleetReportService');
|
require('./src/services/fleetReportService');
|
||||||
|
require('./src/services/roverLearningService');
|
||||||
require('./src/services/replayEngineV2');
|
require('./src/services/replayEngineV2');
|
||||||
// Replay delivery is a core service. It must subscribe before the optional
|
// Replay delivery is a core service. It must subscribe before the optional
|
||||||
// Discord feature so web requests always have a local delivery path.
|
// Discord feature so web requests always have a local delivery path.
|
||||||
|
|||||||
+3
-3
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
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
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
@@ -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
@@ -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
+2
-2
@@ -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};
|
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-CaWv689i.js.map
|
//# sourceMappingURL=SetupApp-DN7XBkOe.js.map
|
||||||
+1
-1
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
@@ -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
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
@@ -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
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+35
@@ -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
+23
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+23
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+35
@@ -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
+18
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
@@ -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
@@ -12,8 +12,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<!-- site-metadata:inject -->
|
<!-- site-metadata:inject -->
|
||||||
<!-- analytics:inject -->
|
<!-- analytics:inject -->
|
||||||
<script type="module" crossorigin src="/assets/index-n0JxE1Mv.js"></script>
|
<script type="module" crossorigin src="/assets/index-dJH5nUWV.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-xCRVLGjq.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DOmNOCX4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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: 'homeAssistant', path: ['homeAssistant', 'enabled'] },
|
||||||
{ key: 'neato', path: ['homeAssistant', 'neato', 'enabled'] },
|
{ key: 'neato', path: ['homeAssistant', 'neato', 'enabled'] },
|
||||||
{ key: 'lift', path: ['homeAssistant', 'lift', '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: 'roomCameras', path: ['roomCameras', 'enabled'] },
|
||||||
{ key: 'ptzCamera', path: ['ptzCamera', 'enabled'] },
|
{ key: 'ptzCamera', path: ['ptzCamera', 'enabled'] },
|
||||||
{ key: 'kinect', path: ['kinect', '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: 'discord', path: ['discord', 'enabled'] },
|
||||||
{ key: 'socials', path: ['socials', 'enabled'] },
|
{ key: 'socials', path: ['socials', 'enabled'] },
|
||||||
{ key: 'fleetReports', path: ['fleetReports', 'enabled'] },
|
{ key: 'fleetReports', path: ['fleetReports', 'enabled'] },
|
||||||
|
{ key: 'roverLearning', path: ['roverLearning', 'enabled'] },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const bandwidthSavings = require('../helpers/bandwidthSavings.configuration');
|
|||||||
const audioForward = require('../services/audioForwardService/configuration');
|
const audioForward = require('../services/audioForwardService/configuration');
|
||||||
const audioLevels = require('../services/audioLevelsService/configuration');
|
const audioLevels = require('../services/audioLevelsService/configuration');
|
||||||
const homeAssistant = require('../services/homeAssistantService/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 roomCameras = require('../services/roomCameraService/configuration');
|
||||||
const ptzCamera = require('../services/ptzCameraService/configuration');
|
const ptzCamera = require('../services/ptzCameraService/configuration');
|
||||||
const kinect = require('../services/kinectService/configuration');
|
const kinect = require('../services/kinectService/configuration');
|
||||||
@@ -21,6 +23,7 @@ const barcodeScanner = require('../services/barcodeScannerService/configuration'
|
|||||||
const commands = require('../services/operatorCommandService/configuration');
|
const commands = require('../services/operatorCommandService/configuration');
|
||||||
const discord = require('../services/discordBotService/configuration');
|
const discord = require('../services/discordBotService/configuration');
|
||||||
const fleetReports = require('../services/fleetReportService/configuration');
|
const fleetReports = require('../services/fleetReportService/configuration');
|
||||||
|
const roverLearning = require('../services/roverLearningService/configuration');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Object property order is preserved by JSON serialization and JSON Schema
|
Object property order is preserved by JSON serialization and JSON Schema
|
||||||
@@ -41,7 +44,10 @@ const definitions = [
|
|||||||
audioForward,
|
audioForward,
|
||||||
audioLevels,
|
audioLevels,
|
||||||
homeAssistant,
|
homeAssistant,
|
||||||
|
// Activities own a separate catalog even though the HA connection is shared.
|
||||||
|
homeAssistantActivities,
|
||||||
roomCameras,
|
roomCameras,
|
||||||
|
roomAudio,
|
||||||
ptzCamera,
|
ptzCamera,
|
||||||
kinect,
|
kinect,
|
||||||
balanceBoard,
|
balanceBoard,
|
||||||
@@ -52,6 +58,7 @@ const definitions = [
|
|||||||
sessionConfiguration.socials,
|
sessionConfiguration.socials,
|
||||||
sessionConfiguration.driverAd,
|
sessionConfiguration.driverAd,
|
||||||
fleetReports,
|
fleetReports,
|
||||||
|
roverLearning,
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultConfig = Object.fromEntries(
|
const defaultConfig = Object.fromEntries(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Configuration Database Migrations
|
// Configuration Database Migrations
|
||||||
// Purpose: Applies ordered, transactional schema changes to the configuration and administration database.
|
// 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 = [
|
const migrations = [
|
||||||
{
|
{
|
||||||
version: 1,
|
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) {
|
function applySchemaMigrations(db) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ module.exports = {
|
|||||||
goal: 400,
|
goal: 400,
|
||||||
async run(ctx) {
|
async run(ctx) {
|
||||||
startStrobe(ctx, { endsAt: Date.now() + STROBE_MS, on: false });
|
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) {
|
async recover(ctx, effect) {
|
||||||
if (!effect || Number(effect.endsAt || 0) <= Date.now()) {
|
if (!effect || Number(effect.endsAt || 0) <= Date.now()) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const logger = require('../../globals/logger').child('assignment');
|
|||||||
const { MODES, getMode, modeEvents } = require('../modeManager');
|
const { MODES, getMode, modeEvents } = require('../modeManager');
|
||||||
const { roleEvents, getRole, isAdmin, isLockdownAdmin } = require('../roleService');
|
const { roleEvents, getRole, isAdmin, isLockdownAdmin } = require('../roleService');
|
||||||
const roverManager = require('../roverManager');
|
const roverManager = require('../roverManager');
|
||||||
|
const { getOperatingMode } = require('../operatingModeService');
|
||||||
const { compareRoversForAssignment } = require('./roverRanking');
|
const { compareRoversForAssignment } = require('./roverRanking');
|
||||||
|
|
||||||
const socketRefs = new Map(); // socketId -> socket
|
const socketRefs = new Map(); // socketId -> socket
|
||||||
@@ -131,7 +132,7 @@ roverManager.managerEvents.on('switch', ({ socketId, roverId }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function assignSocket(socket, options = {}) {
|
function assignSocket(socket, options = {}) {
|
||||||
if (!socket || isAdmin(socket) || getRole(socket) !== 'user') {
|
if (!socket || getOperatingMode(socket) !== 'rover' || isAdmin(socket) || getRole(socket) !== 'user') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// avoid double assignment
|
// avoid double assignment
|
||||||
@@ -311,13 +312,22 @@ function describeAssignment(socketId) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
assignmentEvents,
|
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,
|
describeAssignment,
|
||||||
forceRelease,
|
forceRelease,
|
||||||
forceReleaseWithNotice,
|
forceReleaseWithNotice,
|
||||||
rerollAssignments,
|
rerollAssignments,
|
||||||
getAssignedRover: (socketId) => assignments.get(socketId) || null,
|
getAssignedRover: (socketId) => assignments.get(socketId) || null,
|
||||||
moveAssignment: (socket, roverId, { releasePrevious = true } = {}) => {
|
moveAssignment: (socket, roverId, { releasePrevious = true } = {}) => {
|
||||||
if (!socket || !roverId) return;
|
if (!socket || !roverId || getOperatingMode(socket) !== 'rover') return;
|
||||||
const previous = assignments.get(socket.id);
|
const previous = assignments.get(socket.id);
|
||||||
if (previous && previous !== roverId && releasePrevious) {
|
if (previous && previous !== roverId && releasePrevious) {
|
||||||
roverManager.releaseControl(previous, socket);
|
roverManager.releaseControl(previous, socket);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { subscribe } = require('../eventBus');
|
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) {
|
function registerChargeCompleteSound(deps) {
|
||||||
const {
|
const {
|
||||||
@@ -12,22 +12,18 @@ function registerChargeCompleteSound(deps) {
|
|||||||
playServerAudioFile,
|
playServerAudioFile,
|
||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
subscribe('battery.unlocked', (event = {}) => {
|
subscribe('battery.chargeComplete', (event = {}) => {
|
||||||
const roverId = String(event?.payload?.roverId || '').trim();
|
const roverId = String(event?.payload?.roverId || '').trim();
|
||||||
if (!roverId) return;
|
if (!roverId) return;
|
||||||
|
|
||||||
try {
|
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, {
|
playServerAudioFile(roverId, DONE_CHARGING_SOUND_PATH, {
|
||||||
source: 'charge-complete',
|
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) {
|
} catch (err) {
|
||||||
// A missing/offline rover or unavailable ffmpeg should not affect the
|
// 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.
|
// on top of the state transition, so failures are logged and contained.
|
||||||
logger.warn('Failed to play charging complete sound', {
|
logger.warn('Failed to play charging complete sound', {
|
||||||
roverId,
|
roverId,
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ function grantExternalSpectatorAccessAfterAdminLogin(socket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
io.on('connection', (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.
|
Role is assigned before the browser's full identity heartbeat has completed.
|
||||||
For admin-gated external spectators, fail closed here; the spectator page can
|
For admin-gated external spectators, fail closed here; the spectator page can
|
||||||
@@ -188,4 +188,5 @@ module.exports = {
|
|||||||
isAdmin,
|
isAdmin,
|
||||||
isLockdownAdmin,
|
isLockdownAdmin,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
canBecomeSpectator,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ async function copyDurableTree(sourceDir, destinationDir, relativeDir = '') {
|
|||||||
const relativePath = path.join(relativeDir, entry.name);
|
const relativePath = path.join(relativeDir, entry.name);
|
||||||
if (!relativeDir && EXCLUDED_TOP_LEVEL_NAMES.has(entry.name)) continue;
|
if (!relativeDir && EXCLUDED_TOP_LEVEL_NAMES.has(entry.name)) continue;
|
||||||
if (!relativeDir && DATABASE_FILES.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.isSymbolicLink()) throw new Error(`Backup cannot include symbolic link: ${relativePath}`);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
skipped.push(...await copyDurableTree(sourceDir, destinationDir, relativePath));
|
skipped.push(...await copyDurableTree(sourceDir, destinationDir, relativePath));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
const logger = require('../../globals/logger').child('batteryManager');
|
const logger = require('../../globals/logger').child('batteryManager');
|
||||||
const { publishEvent } = require('../eventBus');
|
const { publishEvent } = require('../eventBus');
|
||||||
const { managerEvents, lockRover, rovers } = require('../roverManager');
|
const { managerEvents, lockRover, rovers } = require('../roverManager');
|
||||||
|
const { issueCommand } = require('../commandService');
|
||||||
|
|
||||||
const STATES = {
|
const STATES = {
|
||||||
NORMAL: 'normal',
|
NORMAL: 'normal',
|
||||||
@@ -29,6 +30,7 @@ function getState(roverId) {
|
|||||||
dockedCharging: false,
|
dockedCharging: false,
|
||||||
charging: false,
|
charging: false,
|
||||||
batteryLocked: false,
|
batteryLocked: false,
|
||||||
|
completionAnnounced: false,
|
||||||
waitingSince: null,
|
waitingSince: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,11 @@ function handleSensorEvent({ roverId, sensors, batteryState }) {
|
|||||||
}
|
}
|
||||||
if (!state.urgent && batteryState?.urgentActive) {
|
if (!state.urgent && batteryState?.urgentActive) {
|
||||||
state.urgent = true;
|
state.urgent = true;
|
||||||
|
try {
|
||||||
|
issueCommand(roverId, { type: 'song', song: { notes: [{ note: 72, duration: 8 }, { note: 0, duration: 8 }, { note: 72, duration: 16 }] } });
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Failed to play urgent battery warning', { roverId, error: err.message });
|
||||||
|
}
|
||||||
publishEvent({
|
publishEvent({
|
||||||
source: 'batteryManager',
|
source: 'batteryManager',
|
||||||
type: 'battery.urgent',
|
type: 'battery.urgent',
|
||||||
@@ -137,6 +144,18 @@ function handleSensorEvent({ roverId, sensors, batteryState }) {
|
|||||||
(batteryState?.charge != null && fullThreshold != null && batteryState.charge >= fullThreshold) ||
|
(batteryState?.charge != null && fullThreshold != null && batteryState.charge >= fullThreshold) ||
|
||||||
(batteryState?.percent != null && batteryState.percent >= 0.99);
|
(batteryState?.percent != null && batteryState.percent >= 0.99);
|
||||||
|
|
||||||
|
// Announce once per dock visit, independently of the charging lock. Keep the
|
||||||
|
// latch at 100% so small charge fluctuations cannot repeat the sound.
|
||||||
|
if (!onDock) state.completionAnnounced = false;
|
||||||
|
if (onDock && batteryState?.percentDisplay >= 100 && !state.completionAnnounced) {
|
||||||
|
state.completionAnnounced = true;
|
||||||
|
publishEvent({
|
||||||
|
source: 'batteryManager',
|
||||||
|
type: 'battery.chargeComplete',
|
||||||
|
payload: { roverId, batteryState },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const needsCharge = state.warned;
|
const needsCharge = state.warned;
|
||||||
|
|
||||||
if (dockedCharging && lockable && needsCharge && !state.batteryLocked && !isFull && !waitingLongEnough) {
|
if (dockedCharging && lockable && needsCharge && !state.batteryLocked && !isFull && !waitingLongEnough) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user