Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c761c510a6 | ||
|
|
feeff6ac9b |
+28
-6
@@ -6,9 +6,11 @@
|
|||||||
# showing a fresh random claim code the API never received. This makes it all
|
# showing a fresh random claim code the API never received. This makes it all
|
||||||
# consistent again in one pass:
|
# consistent again in one pass:
|
||||||
# 1. re-forward the adb tunnels (:8080 gateway, :9999 matrix relay)
|
# 1. re-forward the adb tunnels (:8080 gateway, :9999 matrix relay)
|
||||||
# 2. wait for the node daemon (start the App Lab app if it isn't up)
|
# 2. assert the single-app invariant (canonical app is the boot default + the
|
||||||
# 3. re-register the node with the local API (restores its in-memory binding)
|
# only thing on :8080/:9999; stop any stray duplicate)
|
||||||
# 4. scroll a fixed claim code on the matrix so the board + API agree
|
# 3. wait for the node daemon (start the App Lab app if it isn't up)
|
||||||
|
# 4. re-register the node with the local API (restores its in-memory binding)
|
||||||
|
# 5. scroll a fixed claim code on the matrix so the board + API agree
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./recover.sh # one recovery pass, then exit
|
# ./recover.sh # one recovery pass, then exit
|
||||||
@@ -27,6 +29,7 @@ KIT_ID="${KIT_ID:-crimson-node}"
|
|||||||
CLAIM_CODE="${CLAIM_CODE:-7777}"
|
CLAIM_CODE="${CLAIM_CODE:-7777}"
|
||||||
NODE_URL="${NODE_URL:-http://127.0.0.1:8080}"
|
NODE_URL="${NODE_URL:-http://127.0.0.1:8080}"
|
||||||
APP="${APP:-/home/arduino/ArduinoApps/apess-onboard}"
|
APP="${APP:-/home/arduino/ArduinoApps/apess-onboard}"
|
||||||
|
CONTAINER="${APP##*/}-main-1" # App Lab names the container <app>-main-1
|
||||||
PORTS=(8080 9999)
|
PORTS=(8080 9999)
|
||||||
|
|
||||||
# Resolve adb even under launchd's minimal PATH.
|
# Resolve adb even under launchd's minimal PATH.
|
||||||
@@ -61,7 +64,26 @@ recover_once() {
|
|||||||
done
|
done
|
||||||
ok "tunnels forwarded (${PORTS[*]})"
|
ok "tunnels forwarded (${PORTS[*]})"
|
||||||
|
|
||||||
# 2 · wait for the node daemon (auto-starts on boot; start it if not)
|
# 2 · single-app invariant: the canonical node app owns :8080/:9999. Make it
|
||||||
|
# the boot default, stop any OTHER user app (a stray duplicate would collide on
|
||||||
|
# our ports), and ensure it's the running container.
|
||||||
|
"$ADB" -s "$SERIAL" shell "arduino-app-cli properties set default $APP" >/dev/null 2>&1
|
||||||
|
local strays
|
||||||
|
strays="$("$ADB" -s "$SERIAL" shell \
|
||||||
|
"docker ps --format '{{.Names}}' 2>/dev/null | grep -E '\-main-1$' | grep -v '^${CONTAINER}$'" 2>/dev/null | tr -d '\r')"
|
||||||
|
if [ -n "$strays" ]; then
|
||||||
|
warn "stopping stray app container(s): $strays"
|
||||||
|
for c in $strays; do
|
||||||
|
"$ADB" -s "$SERIAL" shell "docker stop $c" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if ! "$ADB" -s "$SERIAL" shell "docker ps --format '{{.Names}}'" 2>/dev/null | grep -q "^${CONTAINER}$"; then
|
||||||
|
warn "node app not running — starting ${APP##*/}"
|
||||||
|
"$ADB" -s "$SERIAL" shell "arduino-app-cli app start $APP" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
ok "single-app invariant (default + only ${APP##*/} on ${PORTS[*]})"
|
||||||
|
|
||||||
|
# 3 · wait for the node daemon (auto-starts on boot; start it if not)
|
||||||
local n=0
|
local n=0
|
||||||
until curl -s -m2 "$NODE_URL/health" -o /dev/null 2>/dev/null; do
|
until curl -s -m2 "$NODE_URL/health" -o /dev/null 2>/dev/null; do
|
||||||
n=$((n + 1))
|
n=$((n + 1))
|
||||||
@@ -74,7 +96,7 @@ recover_once() {
|
|||||||
done
|
done
|
||||||
ok "node daemon healthy"
|
ok "node daemon healthy"
|
||||||
|
|
||||||
# 3 · re-register with the local API (restores the in-memory node binding)
|
# 4 · re-register with the local API (restores the in-memory node binding)
|
||||||
local r
|
local r
|
||||||
r="$(curl -s -m5 -X POST "$API/nodes/self-register" \
|
r="$(curl -s -m5 -X POST "$API/nodes/self-register" \
|
||||||
-H "x-fleet-secret: $FLEET_SECRET" -H 'content-type: application/json' \
|
-H "x-fleet-secret: $FLEET_SECRET" -H 'content-type: application/json' \
|
||||||
@@ -82,7 +104,7 @@ recover_once() {
|
|||||||
if echo "$r" | grep -q '"url"'; then ok "re-registered with API (code $CLAIM_CODE)"
|
if echo "$r" | grep -q '"url"'; then ok "re-registered with API (code $CLAIM_CODE)"
|
||||||
else warn "API self-register failed — is the API up at $API? ($r)"; return 1; fi
|
else warn "API self-register failed — is the API up at $API? ($r)"; return 1; fi
|
||||||
|
|
||||||
# 4 · sync the claim code onto the matrix so board + API agree
|
# 5 · sync the claim code onto the matrix so board + API agree
|
||||||
matrix_code && ok "matrix showing $CLAIM_CODE" || warn "could not set matrix code (relay :9999)"
|
matrix_code && ok "matrix showing $CLAIM_CODE" || warn "could not set matrix code (relay :9999)"
|
||||||
log "recovered — bind in the UI with code $CLAIM_CODE"
|
log "recovered — bind in the UI with code $CLAIM_CODE"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export function CockpitRail() {
|
|||||||
{tel.live ? (
|
{tel.live ? (
|
||||||
<WaveformCanvas wave={tel.wave} impact={tel.event === 'impact'} />
|
<WaveformCanvas wave={tel.wave} impact={tel.event === 'impact'} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[78px] items-center justify-center rounded-[10px] border border-rail-line bg-rail-inset font-mono text-[10px] text-rail-dim3">
|
<div className="flex h-[100px] items-center justify-center rounded-[10px] border border-rail-line bg-rail-inset font-mono text-[10px] text-rail-dim3">
|
||||||
adxl355_stream — awaiting board
|
adxl355_stream — awaiting board
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { useEffect, useRef } from 'react'
|
|||||||
/**
|
/**
|
||||||
* The live-acceleration waveform. Reads the magnitude history from a ref and
|
* The live-acceleration waveform. Reads the magnitude history from a ref and
|
||||||
* redraws via requestAnimationFrame — independent of React renders, so it stays
|
* redraws via requestAnimationFrame — independent of React renders, so it stays
|
||||||
* smooth. Line turns to the spike colour during an impact event.
|
* smooth. A ResizeObserver keeps the drawing buffer exactly matched to the
|
||||||
|
* element's pixel size (so the trace is always centered and peaks never clip),
|
||||||
|
* and the line turns to the spike colour during an impact event.
|
||||||
*/
|
*/
|
||||||
export function WaveformCanvas({
|
export function WaveformCanvas({
|
||||||
wave,
|
wave,
|
||||||
@@ -21,16 +23,23 @@ export function WaveformCanvas({
|
|||||||
if (!cv) return
|
if (!cv) return
|
||||||
const ctx = cv.getContext('2d')
|
const ctx = cv.getContext('2d')
|
||||||
if (!ctx) return
|
if (!ctx) return
|
||||||
const dpr = window.devicePixelRatio || 1
|
const dpr = Math.max(1, window.devicePixelRatio || 1)
|
||||||
const resize = () => {
|
|
||||||
cv.width = cv.clientWidth * dpr
|
// Keep the drawing buffer matched to the element's actual size. Setting
|
||||||
cv.height = cv.clientHeight * dpr
|
// width/height clears the canvas, so only assign when it actually changed.
|
||||||
|
const sync = () => {
|
||||||
|
const w = Math.round(cv.clientWidth * dpr)
|
||||||
|
const h = Math.round(cv.clientHeight * dpr)
|
||||||
|
if (w && cv.width !== w) cv.width = w
|
||||||
|
if (h && cv.height !== h) cv.height = h
|
||||||
}
|
}
|
||||||
resize()
|
sync()
|
||||||
window.addEventListener('resize', resize)
|
const ro = new ResizeObserver(sync)
|
||||||
|
ro.observe(cv)
|
||||||
|
|
||||||
let raf = 0
|
let raf = 0
|
||||||
const draw = () => {
|
const draw = () => {
|
||||||
|
sync()
|
||||||
const w = cv.width
|
const w = cv.width
|
||||||
const h = cv.height
|
const h = cv.height
|
||||||
ctx.clearRect(0, 0, w, h)
|
ctx.clearRect(0, 0, w, h)
|
||||||
@@ -41,26 +50,30 @@ export function WaveformCanvas({
|
|||||||
ctx.moveTo(0, h / 2)
|
ctx.moveTo(0, h / 2)
|
||||||
ctx.lineTo(w, h / 2)
|
ctx.lineTo(w, h / 2)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
// waveform
|
// waveform — centered, clamped to ±45% of height so peaks never clip
|
||||||
const buf = wave.current
|
const buf = wave.current
|
||||||
const n = buf.length
|
const n = buf.length
|
||||||
ctx.strokeStyle = impactRef.current ? '#ff8a5c' : '#7fa8ff'
|
if (n > 1) {
|
||||||
ctx.lineWidth = 1.5 * dpr
|
ctx.strokeStyle = impactRef.current ? '#ff8a5c' : '#7fa8ff'
|
||||||
ctx.beginPath()
|
ctx.lineWidth = 1.6 * dpr
|
||||||
for (let i = 0; i < n; i++) {
|
ctx.lineJoin = 'round'
|
||||||
const x = (i / (n - 1)) * w
|
ctx.beginPath()
|
||||||
const y = h / 2 - buf[i] * (h * 0.42)
|
for (let i = 0; i < n; i++) {
|
||||||
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)
|
const x = (i / (n - 1)) * w
|
||||||
|
const v = Math.max(-1, Math.min(1, buf[i]))
|
||||||
|
const y = h / 2 - v * (h * 0.45)
|
||||||
|
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
}
|
}
|
||||||
ctx.stroke()
|
|
||||||
raf = requestAnimationFrame(draw)
|
raf = requestAnimationFrame(draw)
|
||||||
}
|
}
|
||||||
raf = requestAnimationFrame(draw)
|
raf = requestAnimationFrame(draw)
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(raf)
|
cancelAnimationFrame(raf)
|
||||||
window.removeEventListener('resize', resize)
|
ro.disconnect()
|
||||||
}
|
}
|
||||||
}, [wave])
|
}, [wave])
|
||||||
|
|
||||||
return <canvas ref={ref} className="block h-[78px] w-full rounded-[10px] border border-rail-line bg-rail-inset" />
|
return <canvas ref={ref} className="block h-[100px] w-full rounded-[10px] border border-rail-line bg-rail-inset" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
import { getNodeMatrix } from './api'
|
import { getNodeMatrix } from './api'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Poll the board's real LED-matrix framebuffer (~8 fps) for a pixel-perfect
|
* Poll the board's real LED-matrix framebuffer for a pixel-perfect mirror.
|
||||||
* mirror. Returns the 104 on/off dots, or null until a frame is read. Skips a
|
* Returns the 104 on/off dots, or null until a frame is read. Skips a poll while
|
||||||
* poll while the previous one is in flight so a slow board can't stack requests.
|
* the previous one is in flight so a slow board can't stack requests.
|
||||||
|
*
|
||||||
|
* Default 15 fps: the board renders its animations at ~12 fps (the sketch's 80 ms
|
||||||
|
* loop), and a matrix_get round-trip is only ~15 ms, so 15 fps slightly
|
||||||
|
* oversamples the source — smooth, with no dropped frames — while staying far
|
||||||
|
* under the ~68 fps round-trip ceiling. Going higher mostly re-reads identical
|
||||||
|
* frames (the board isn't rendering faster), so it's wasted requests.
|
||||||
*/
|
*/
|
||||||
export function useMatrixMirror(teamId: string, enabled: boolean, fps = 8): boolean[] | null {
|
export function useMatrixMirror(teamId: string, enabled: boolean, fps = 15): boolean[] | null {
|
||||||
const [dots, setDots] = useState<boolean[] | null>(null)
|
const [dots, setDots] = useState<boolean[] | null>(null)
|
||||||
const inflight = useRef(false)
|
const inflight = useRef(false)
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export function useTelemetry(enabled: boolean): Telemetry {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
let phase = 0
|
let phase = 0
|
||||||
|
let k = 0
|
||||||
let impactUntil = 0
|
let impactUntil = 0
|
||||||
const jog = (base: number, amp: number) => base + (rand() - 0.5) * amp
|
const jog = (base: number, amp: number) => base + (rand() - 0.5) * amp
|
||||||
|
|
||||||
@@ -79,10 +80,15 @@ export function useTelemetry(enabled: boolean): Telemetry {
|
|||||||
setAcc1({ x: jog(0, a), y: jog(0, a), z: jog(1, z) })
|
setAcc1({ x: jog(0, a), y: jog(0, a), z: jog(1, z) })
|
||||||
setAcc2({ x: jog(0, a * 0.9), y: jog(0, a * 0.9), z: jog(1, z) })
|
setAcc2({ x: jog(0, a * 0.9), y: jog(0, a * 0.9), z: jog(1, z) })
|
||||||
|
|
||||||
// magnitude for the waveform
|
// Signed oscilloscope sample: a gentle idle wave at rest, a big decaying
|
||||||
const m = impact ? 0.55 + rand() * 0.85 : 0.04 + rand() * 0.06
|
// ring on impact. Bounded to ±0.95 so it never leaves the canvas.
|
||||||
|
k += 1
|
||||||
|
const env = impact ? Math.max(0, (impactUntil - now) / 1400) : 0
|
||||||
|
const s = impact
|
||||||
|
? Math.sin(k * 1.05) * env * 0.9 + (rand() - 0.5) * 0.12
|
||||||
|
: Math.sin(k * 0.4) * 0.16 + (rand() - 0.5) * 0.06
|
||||||
const buf = wave.current
|
const buf = wave.current
|
||||||
buf.push(m)
|
buf.push(Math.max(-0.95, Math.min(0.95, s)))
|
||||||
if (buf.length > WAVE_LEN) buf.shift()
|
if (buf.length > WAVE_LEN) buf.shift()
|
||||||
|
|
||||||
setEvent(impact ? 'impact' : 'nominal')
|
setEvent(impact ? 'impact' : 'nominal')
|
||||||
|
|||||||
Reference in New Issue
Block a user