Feat/4 9 face recognition - #665
Open
WarutC wants to merge 45 commits into
Open
Conversation
fix: safe JSON parsing and sequential button polling in SparkPeripheral test: add failing tests for scratch3_spark extension feat: implement getInfo with LED, Button, Buzzer, Camera block definitions feat: implement Spark block handlers (LED, Button, Buzzer, Camera) fix: guard against NaN in LED brightness and tone block handlers feat: register spark extension in builtinExtensions feat: add placeholder icons for Spark extension library card feat: register Spark extension in GUI extension library feat: add Thai translations for Spark extension fix: align WS protocol with middleware — pending map, correct message schema, send(cmd, data) fix: guard against overlapping button polls, slow interval to 200ms fix: update webpack-cli and dependencies, adjust polling mechanism for Spark extension
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- imuAccelX/Y/Z, imuGyroX/Y/Z, imuPitch, imuRoll: all REPORTER blocks, no arguments. Reads via SparkPeripheral._readImuField(cmd, field) which sends the cmd over WS to the middleware (which serves from its imuCache, no firmware round-trip per block). - Graceful degradation: on status:error or timeout, returns last-known cached value (or 0 if no prior sample). Logs one warn per degradation transition (not per-read). - translations.js: 8 Thai labels added (ความเร่งแกน X/Y/Z, ความเร็ว เชิงมุมแกน X/Y/Z, มุมก้มเงย, มุมเอียงข้าง). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Story 2.3 firmware now emits btn_press/btn_release events instead of relying on Scratch to poll cmd:btn for HAT-block detection. Wire up the consumer side: - _onMessage now dispatches type:"event" frames to a new _onEvent handler (was previously ignored). - _onEvent updates _buttonState[pin] from press/release events AND sets _buttonEdgeLatch[pin]=true on press. - whenButtonPressed HAT consumes the edge latch — fires ONCE per debounced press instead of every frame the button is held (was the polling-path behavior that conflicted with Story 2.3 AC c). - isButtonPressed boolean block keeps using the polling path (touchButtonPoll → cmd:btn) for continuous-state queries. Falls back gracefully on older firmware that doesn't emit events: the HAT block returns false until events start arriving. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The whenButtonPressed block previously called touchButtonPoll() when the edge latch was empty, which spawned a continuous cmd:btn poll loop on the wire even when no button was pressed. The fallback was kept "in case events haven't arrived yet (older firmware)". Now that GPIO event push works on Spark-Baseboard (spark-firmware d129ee2: task_button_gpio_input emits btn_press / btn_release per pin) and Waveshare touchscreen has had it since Story 2.3, the fallback is dead weight. Removing it: - Idle wire when a script just contains the HAT block — no traffic until the user actually presses - One firing per debounced press, never per polling-tick The isButtonPressed BOOLEAN block keeps its own touchButtonPoll() — continuous-state queries genuinely need polling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
R2 amendment per sprint-change-proposal-2026-05-08.md §4.5. Pairs
with spark-firmware af6ee01 (firmware enum + dispatch + drivers).
Files:
- index.js:
* LedColor const: drop BLUE/WHITE, add YELLOW
* LED_COLOR_MAP: drop blue/white triplets, add yellow {255,255,0}
* ledColors menu: 4 items now (red/green/yellow/off);
blue/white items removed
- translations.js: drop spark.color.blue/white, add spark.color.yellow
('เหลือง')
Closed schema visible to the child via Scratch dropdown matches the
firmware contract 1:1. Saved Scratch projects with setLedColor blue
will surface as undefined-menu blocks on reload — per spec, no
migration story (no project corpus we control).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the gesture HAT block ("เมื่อเขย่า") and a sensitivity COMMAND
("ตั้งความไวการเขย่าเป็น [เบา|ปานกลาง|แรง]") to scratch3_spark/.
whenShake mirrors the Story 2.3 whenButtonPressed edge-latch pattern:
SparkPeripheral._onEvent() flips _shakeEdgeLatch on receipt of the
{event:"shake"} frame; the HAT's opcode method consumes the latch and
returns true exactly once per debounced gesture. No polling fallback —
the firmware-side 500 ms refractory (task_imu_sampler) is the single
source of debouncing.
setShakeSensitivity sends cmd:"set_shake_threshold" with {level:1|2|3}
mapped via the shakeLevels menu. Validation is firmware-side
(invalid_param closed-enum) — JS layer just guards against parseInt
NaN before issuing the request.
5 Thai labels appended to translations.js (whenShake, setShakeSensitivity,
shakeLevel.1/2/3). All 15 existing blocks + their order preserved.
node --check syntax-validates both files.
Story 3.3, Tier C (Tasks 8-9).
The Spark card in scratch-gui's extension library rendered with a
broken-image placeholder + no banner art (vs Music/Pen siblings which
have colorful hero illustrations). Root cause: spark.png was actually
an SVG file with .png extension (216 bytes — same content as
spark-small.svg), so the browser's raster image loader fell back to
the broken-image glyph.
Fix:
- New spark-banner.svg (600x372) — amber gradient + stylized dev
board with 2 LEDs, 2 push-buttons, buzzer, IMU chip, SPARK wordmark.
mix-blend-mode: screen on the LED glows so the lit halo reads
through the dark fill. Font-family fallback chain (Archivo Black →
Arial Black → system bold-sans) so wordmark renders consistently
on Linux/Chromebook classroom builds. <title> for a11y.
- Import in extensions/index.jsx flipped from spark/spark.png to
spark/spark-banner.svg.
- Deleted the corrupted spark/spark.png.
Bundling: Webpack treats .svg and .png identically as type: 'asset'
(precedent: microbit-illustration.svg, wedo-illustration.svg, etc.
already import as iconURL the same way). No bundler risk.
Adversarial review (1 reviewer): 3 patches applied (a11y title,
mix-blend-mode for glows, font-family fallback + bumped trace strokes
for thumbnail crispness). 2 deferred (spark-small.svg palette tweak;
stale doc references to deleted spark.png).
Spec trace:
_bmad-output/implementation-artifacts/spec-spark-extension-banner.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pairs with the previous library-card banner fix (7376e80). The peripheral-connect success modal showed a 5-line placeholder SVG (just a unicode lightning bolt + "Spark IoT Kit" text) when other extensions ship full hardware illustrations. Replaces spark-illustration.svg with a 355x278 SVG matching the same visual language as spark-banner.svg: - Warm peach background preserves Spark brand color - Stylized dev board centerpiece (LEDs, buttons, buzzer, IMU, traces) - USB cable trailing off the top edge — visual cue for "plugged in" - mix-blend-mode: screen LED halos so the lit effect reads through the dark board fill - Font-family fallback chain (Archivo Black → Arial Black → system bold-sans) so the wordmark renders consistently on Linux/Chromebook classroom builds - <title> + role/aria-label for a11y No code changes — single asset content swap. Both connectionIconURL and connectionSmallIconURL bindings in extensions/index.jsx already point here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous 355x278 connection illustration painted its own peach
background + "SPARK IoT KIT" wordmark inside the SVG, which collided
with the modal's own framing — board ended up dominating ~74% of the
canvas with the wordmark squeezed at the bottom edge.
Mirror microbit-illustration.svg's pattern: compact 120x90 viewBox,
hardware silhouette only, transparent background. Scratch's connection
modal owns the framing, peach backdrop, and the extension name label
(coming from the manifest's `name` field, not embedded in the SVG).
Removed:
- 355x278 peach background rect
- "SPARK IoT KIT" embedded wordmark
- USB cable trailing + connector
- Decorative dots + sound waves
Kept:
- Dev-board silhouette (96x56) with 2 LEDs, 2 buttons, buzzer, IMU,
decorative traces — the recognizable kit
- mix-blend-mode: screen LED glow halos
- <title> + role/aria-label for a11y
- Gradient IDs renamed with `spk` prefix to avoid collision if both
illustrations load on the same page
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oasts (FR16)
scratch3_spark: +6 blocks (reporters micLevel/lightLevel/tofDistance, inert HATs
whenLoud/whenBright/whenNear) appended after the IMU section, preserving all 15
existing blocks + order. Reporters delegate to a new SparkPeripheral._readStubField
which returns the declared mock 0 and fires a one-shot Thai toast per family per
session (runtime.emit('SPARK_STUB_WARNING') + a log.warn); _stubWarningShown Set
cleared on disconnect/_handleDisconnect so a re-connect re-arms. 6 Thai labels in
translations.js (the "(stub)" prefix is the UX-DR10 fallback pending-HW indicator).
node --check clean on both files. HATs never fire; firmware emits no events.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o (code-review MED-1) The middleware now forwards the firmware's hw_not_present error frame, so send() resolves promptly (no 3 s timeout). Defensive: if a future change ever makes send() reject, still treat it as first-touch → fire the one-shot Thai "sensor not present" toast once, per the Story 3.4 contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…reshold blocks (3.7-3.10) SCP #4. Mic/Light/TOF reporters/HATs were Story-3.4 stubs (mock 0 + toast, inert HATs); the firmware now returns live {value} on a board with the Spark-Sensors module. - _readStubField -> _readSensorField(cmd, family): branch on the response, not a build flag — {status:ok, value:number} returns the value (no toast); hw_not_present/timeout falls back to mock 0 + the one-shot Thai warning toast per family. Same .scratch works on both substrates. - _setSensorThreshold + setMicThreshold/setLightThreshold/setTofThreshold COMMAND blocks (level 1/2/3 -> set_mic|light|tof_threshold) + a shared sensorLevels menu. - whenLoud/whenBright/whenNear edge-latch the firmware mic_loud/light_bright/tof_near events (mirror whenShake) — fire once per debounced event; _loudEdgeLatch/_brightEdgeLatch/ _nearEdgeLatch handled in _onEvent. - _resetEdgeLatches() on disconnect/handleDisconnect — clears all HAT latches incl. the previously-deferred _shakeEdgeLatch reset + the button latches. - Labels: dropped the "(stub) " prefix; translations.js updated + new threshold-block / sensorLevel.* Thai strings. eslint --fix run on index.js; residual max-len matches the file's pre-existing one-liner style. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…per en/th bilingual) The Spark-Sensors blocks (micLevel/whenLoud/lightLevel/whenBright/tofDistance/whenNear) had Thai 'default:' strings (from the Story-3.4 stub-era defaults), so a non-Thai GUI locale rendered Thai. The rest of the extension is English 'default:' + Thai in translations.js (th map already complete). Changed the 6 reporter/HAT defaults to English: sound level / when loud / light level / when bright / nearest distance / when object near. setMicThreshold/setLightThreshold/setTofThreshold + the sensorLevels menu were already English. translations.js unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vm.setLocale
Root cause of 'Spark blocks always render in English even in a Thai GUI':
scratch-vm's VirtualMachine.setLocale(locale, messages) calls
formatMessage.setup({translations: {[locale]: messages}}), and format-message's
setup *replaces* the whole translations table (translations = opt.translations).
The GUI passes the scratch-l10n editor messages (no spark.* keys), so it wipes
the th strings our module registered at load time, and every block falls back to
its English default:. (Pre-existing — affects all Spark blocks, not just the new
sensor ones; the M1 '31 Thai-labelled blocks' verification doesn't hold for this
build.)
Fix: applySparkTranslations() merges translations.js into format-message's current
table (preserving every other locale/key), and is called at the top of getInfo()
— which the GUI invokes after setLocale, on every palette refresh — so our strings
are re-applied no matter the ordering. Idempotent; ~10 lines + one call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Story 3-11. _readImuField caches yaw for imu_angle; new imuYaw REPORTER
(returns imu_angle.yaw); new setImuFusion COMMAND + fusionAlgos menu
(none->raw, complementary->smooth, kalman->Kalman, madgwick, mahony) ->
send('set_imu_fusion', {algo}), no-op if not one of the five. English
default: strings; Thai (มุมหันซ้ายขวา / ตั้งการรวมเซ็นเซอร์ IMU เป็น [ALGO] /
ดิบ·นุ่มนวล·คาลมาน·Madgwick·Mahony) added to translations.js — picked up by
the existing applySparkTranslations().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Dockerfile, .dockerignore, and docker/ (nginx.conf, env-config template, entrypoint, README) so scratch-editor ships as a hardened OCI image consumed by Helm/ArgoCD on VHI K8saaS. - Multi-stage: node:24.15.0-alpine builder, nginxinc/nginx-unprivileged:1.27- alpine runtime, both pinned to sha256 digest (CLAUDE.md container guard). - Runtime as UID 101 (non-root). One brief USER root in build to chown /usr/share/nginx/html so entrypoint can write env-config.js; final USER 101. - Build-once-deploy-many: MIDDLEWARE_WS_URL injected at start via envsubst into /env-config.js; Spark extension reads window.SPARK_ENV with Electron- desktop fallback to ws://localhost:8080 (already in 61747b2). - nginx hardening: server_tokens off, X-Content-Type-Options, Referrer-Policy, CSP frame-ancestors 'none', gzip for JS/CSS, /healthz returns 200 ok, no proxy_pass. Security headers explicitly re-declared in each add_header- bearing location (nginx inheritance gotcha). - Drops standalone/player/blocks-only/compatibility HTML entries + their ~16MB sibling bundles from the final image. - docker/README.md is bilingual (Thai-primary) and documents pinned digests, Helm securityContext contract for Story 10.2, and the smoke checklist. Local smoke (M-series macOS, Docker 29.4.0): build 6m48s, image 151MB uncompressed; / /healthz /env-config.js all return 200 with security headers; env value flips correctly across two containers proving build-once-deploy-many; docker stop returns in 0.19s with clean nginx shutdown. Refs story 10.1 (epic 10).
…p /static immutable
Applies 4 patches from the bmad-code-review pass on story 10.1:
- (P1, Med) Validate MIDDLEWARE_WS_URL in docker/30-spark-env.sh via an
allow-list regex (ws://|wss:// + [A-Za-z0-9._:/?#=&%@~+-]+); reject with
exit 1 and a clear error in docker logs. Blocks the special-char injection
path (', `, $, newline) that would break the single-quoted JS string in
env-config.js and silently fall back to localhost.
- (P2, Med) Render env-config.js to /tmp/env-config.js + nginx alias instead
of writing into /usr/share/nginx/html. Drops the brief USER root + chown
ceremony from the Dockerfile (template moves to /usr/share/spark.env-
config.js.template, world-readable). /usr/share/nginx/html is now strictly
read-only at runtime, so story 10.2's Helm chart can set readOnlyRoot
Filesystem: true without subPath-on-a-single-file gymnastics; only /tmp
needs to be writable. docker/README.md §7 updated accordingly + new §11
on input validation.
- (P3, Low) Test cleanup in extension_spark.js uses delete on globals it
added; only restores props that pre-existed. Avoids leaking
global.window=undefined / global.WebSocket=undefined into other tests.
- (P4, Low) Drop immutable from Cache-Control on location /static/ in
nginx.conf — /static/ mixes content-hashed assets with stable-name ones
(favicon.ico, blocks-media). Keeps expires 1y so mtime validation still
hits cache on unchanged files.
Smoke (re-built and re-tested on M-series, Docker 29.4.0): build 7m10s,
container starts cleanly, /env-config.js renders in /tmp and is served via
nginx alias with all security headers, /static returns Cache-Control: public
(no immutable), invalid MIDDLEWARE_WS_URL values (special chars + http://)
correctly exit non-zero with a logged error. scratch-vm tap 18/18.
Refs story 10.1 code-review (epic 10).
The monorepo-wide `npm run build` aborts because the sibling @scratch/task-herder builds with rolldown-vite and rolldown 1.0.0-beta.53 fails to resolve its entry module ([UNRESOLVED_ENTRY]) on both alpine(musl) and debian(glibc). task-herder is not a dependency of scratch-gui and is not shipped in this image, so the build now compiles only scratch-gui's dependency closure (scratch-svg-renderer -> scratch-render -> scratch-vm -> scratch-gui) plus an explicit verification gate that fails the build if the deployable artifact (index.html/gui.js/chunks/static) is missing — correct scoping, not error suppression. task-herder's build defect is tracked separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thai category label สปาร์ก→สปาร์กี้; scratch-gui extension card name and connecting message Spark→Sparky. EXTENSION_ID / loader key 'spark' and builtin map unchanged (no .sb3 opcode compat break). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move UAT_SSH_KEY to env: (no secret in rendered run: script); SHA-pin actions/checkout, docker/login-action, docker/build-push-action per guardrail; --short=12 tag; surface ssh-keyscan failure; timeout-minutes 20 + ssh ConnectTimeout 15. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ld-push v6) Previous harden commit pinned latest-overall (v6/v4/v7) — a major bump outside the reviewed scope (build-push-action v7 changes provenance defaults). Re-pin to the latest patch within the reviewed majors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spark extension LED color enum/map/menu + message id spark.color.yellow -> spark.color.amber; Thai translations updated and reformatted. (User-approved WIP; categoryName remains สปาร์กี้.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T6.5 shipped amber={r:100,g:255,b:0} (low-red/full-green = lime, the
opposite of amber) on the red+green bi-color matrix. Set #FFBF00
{r:255,g:191,b:0} = full red + ~75% green = true warm amber, distinct
from yellow (255,255,0) and green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LED_COLOR_MAP is the single source of truth; ledColorMenuItems() derives
the block menu and the default is the literal 'red'. Removed the now-
redundant LedColor enum (only menu/default referenced it). Wire payload
unchanged ({pin,r,g,b}); setLedColor untouched. Add a color = one map
row + one spark.color.<name> translation line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Asserts every LED_COLOR_MAP key has a spark.color.<name> translation and the menu derives from the map (no hardcoded items, no LedColor enum), so adding a color = one map row + one translation line stays enforced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssertions Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ction-modal hang) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (M3) last Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Story 7.6 — consolidate the OTA host onto this image (one host serves Scratch + firmware). deploy-uat now also triggers on a repository_dispatch (firmware-release) from spark-firmware's GitLab CI, fetches the highest published firmware (sha256 fail-closed), and generates firmware/manifest.json (carrying the Ed25519 signature) into the build context; the Dockerfile bakes it and nginx serves /firmware/ (manifest no-cache, .bin immutable). - scripts/fetch-firmware.sh (resolve highest semver, sha256 verify, manifest) - deploy-uat.yml: repository_dispatch[firmware-release] + fetch step - Dockerfile: COPY firmware/ into nginx html - docker/nginx.conf: /firmware/ locations (security-header trio re-declared) Node24 action bump (backlog 7-5) intentionally left to that story's PR. Not deploy-verified here (no Docker / GitLab secrets / runner). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitHub deprecated Node20-runtime actions on 2026-06-02. Re-pin the three actions in deploy-uat.yml to releases whose action.yml declares runs.using: 'node24' (SHAs from feat/7-5-deploy-uat-node24-bump, re-applied on top of the Story 7.6 dispatch+fetch changes): - actions/checkout v4.3.1 -> v5 08c6903cd8c0fde910a37f88322edcfb5dd907a8 - docker/login-action v3.7.0 -> v4.2.0 650006c6eb7dba73a995cc03b0b2d7f5ca915bee - docker/build-push-action v6.19.2 -> v7.2.0 f9f3042f7e2789586610d6e8b85c8f03e5195baf Verified: YAML valid; all node24 confirmed by fetching each action.yml at the pinned SHA; checkout SHA already used by 8 sibling workflows. Live workflow_dispatch smoke (AC2) intentionally DEFERRED — demo tomorrow, do not deploy yet (Warut). On develop only; deploy-uat fires on push to `uat`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Story 4.5 — 5 REPORTER blocks in the Sparky extension: aiClassifyFace,
aiClassifyColor [TARGET], aiClassifyMotion, aiClassifyImuGesture, aiConfidence.
Each sends cmd "ai.classify" {primitive, params} via the existing peripheral
send() and returns the inference label; aiConfidence reads the last result.
FR28 graceful degradation: hw_not_present/model_load_failed/inference_timeout/
timeout → mock label + one-shot Thai 'ai' toast (SPARK_STUB_WARNING), never a
Scratch error. Thai translations + single-source aiColorTargets menu. Changes
confined to scratch3_spark/ (NFR25).
8 new tap tests (extension_spark_ai.js); 48/48 spark suite green. New code is
lint-clean (pre-existing max-len debt in index.js untouched). Real on-device
inference label -> sprite reaction = HIL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…reporter, cache fix (#3) Addresses the 2026-07-25 code-review of Story 4-5 (acceptance-incomplete): - AC3: aiClassifyMotion gains a user-settable THRESHOLD arg (0..100, default 50, clamped) instead of the hard-coded 20. - AC3: aiClassifyImuGesture gains the aiGestures menu (any/shake/tilt/flat/flip); 'any' → params.gesture:null, else the selected gesture. - AC4: new aiBbox [FIELD] reporter reads x/y/w/h of the last detection's bbox (0 when no bbox) — the previously-dead _lastAi.bbox cache is now reachable. - Bug fix: _classify now caches the mock state (confidence 0, bbox null) into _lastAi on the degrade/disconnected path, so aiConfidence/aiBbox stay coherent with the block that just ran (previously left a prior success's values stale). - Single-source menus (AI_GESTURE_TARGETS / AI_BBOX_FIELDS) + Thai translations. - Tests: threshold/gesture/bbox coverage, the stale-cache regression, and a menu↔translation single-source check. Full spark suite green (78 assertions). Claude-Session: https://claude.ai/code/session_01R9k76rqzDdLRRV63FfyQRW Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uning (#4) Story 2.8 — the board's two bi-color LEDs are now independently addressable from Scratch, backward-compatibly: - setLedColor + setLedBrightness gain a "ดวงไหน" (which LED) dropdown: ทั้งคู่ / ดวงที่ 1 / ดวงที่ 2. 'both' (default) omits the wire `index` (drives both, unchanged); led1/led2 send index 0/1 to the firmware. - Single-source LED_TARGETS + ledTargets menu + Thai translations. - Fix amber: restore the bench-tuned {r:100,g:255,b:0} (green-dominant, reads amber on the red+green LED). A 2026-05-18 refactor had regressed it to the web-standard {255,191,0}, which is red-dominant and looked wrong on hardware. - Tests: per-LED index for both blocks + menu↔translation single-source + amber value. Full spark unit suite green (90 assertions). HIL-verified e2e on board v2 via the full stack (firmware + middleware + Scratch UI). Claude-Session: https://claude.ai/code/session_01R9k76rqzDdLRRV63FfyQRW Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Story 12.6 + 12.3 editor side. Two neutral primitives on the Spark
extension:
- HAT "เมื่อสแกนได้ [ข้อความ]" — edge-latch, exact-match-after-trim,
fires once per sighting (mirror of whenShake)
- reporter "ข้อความที่สแกนล่าสุด" — last decoded text, empty before any
scan, reset on disconnect
setQrScan sends qr_scan_enable {enable}. FR45 graceful fallback: a board
that announced its features but lacks qr_scan -> one-shot Thai toast, no
send (distinct from the silent no-board case); a legacy board (caps
unknown) still works. FR49 editor: one-shot hint if nothing decodes >5s.
Capability handshake (12.3): _queryCapabilities on connect gates the QR
blocks. Thai labels in translations.js. Tests: extension_spark_qr 32/32.
…raw RMS The mic_level wire value is now a 0..100 dB-mapped sound level (firmware build_mic_level), so the translator-facing description '(RMS)' was stale. Reporter passes the value through unchanged; block label is untouched.
…w reporter cache Scratch-side findings from the Epic-12 3-layer review (decisions D5a): - P22: whenScanned latch now clears per VM STEP (runtime.currentMSecs), not per first consumer — two scripts with the same target both fire on one sighting (the old consume-once starved whichever polled second). - P23: sightings are a small queue (cap 4), so two qr_seen inside one VM tick both reach their HATs instead of the first being overwritten. - P20 (FR43): lastScannedText caches the RAW payload — trimming is a HAT-matching concern only, the student sees the text as scanned. - P12: whitespace-only payloads are junk — no cache, no latch, and a blank-target HAT can no longer fire on them. - P21 (FR49): the no-decode hint is one-shot per SESSION (re-armed on disconnect), matching the sibling toast semantics. - P9: a rejected qr_scan_enable send now clears the armed 5 s hint timer (it used to fire for a scan that never started). - P17: capabilities queries carry a generation token — a settlement from a dead session can no longer stomp the fresh session's Set. extension_spark_qr tap reworked for the new semantics: 39/39; full spark suites 129/129.
Story 7.4 authored .gitlab-ci.yml on feat/7-4-ci-cd-pipelines-and-release-automation (scratch-editor bc84cd2). That branch was never merged into the delivery line and the file is absent here entirely — this restores it verbatim rather than merging the stale branch, whose submodule-pointer lineage predates the 12.x work. Scope stays 7.4: test + build only. The UAT deploy remains on GitHub Actions (.github/workflows/deploy-uat.yml), which Story 7.6 has since extended with the firmware-release repository_dispatch trigger — untouched here. Includes the 7.4 code-review patches: `--if-present` on the workspace test run (parity with lint) and `~/.npm` caching instead of node_modules. Verified against the current tree: .nvmrc 24.15.0 matches NODE_IMAGE node:24, the top-level `build` script and all six workspaces still exist, YAML parses.
Scratch half of Story 4.9. Adds the aiClassifyFaceId reporter returning the opaque slot label person_1..person_10 / person_none. The block takes no arguments by design. Anything that let a project name or select a person would put personal data inside the .sb3 file children share with each other, so the only question a project may ask is "which enrolled slot is this?". Enrolment is not a block at all — it lives in the teacher's Advanced panel, and the middleware refuses faceEnroll/faceForget on this channel. The Thai label carries the duration — "จำใบหน้าได้ (~3 วินาที)" — because the block really does take about three seconds and a child watching a sprite do nothing for that long will conclude the board is broken unless told. That is also why the wait is documented rather than hidden behind a spinner we do not have. send() gains a per-call timeout. The shared 3,000 ms default was measured against a ~2,750 ms round trip (worst 2,832 ms) — ~170 ms of margin, so it would have expired intermittently and returned the mock label as if the board had answered, a failure that looks exactly like flaky hardware. face_id uses 8,000 ms, deliberately above the middleware's 6,000 ms router timeout for this primitive, so a slow board yields a real error the block can degrade on instead of the block giving up first. AI_MOCK_LABEL.face_id = 'person_none' so FR28 degradation returns the same answer as an empty enrolment store: a project written against face_id behaves identically on a board without the capability. 11 unit tests, incl. the timeout ordering and an assertion that no opcode exposes an enrolment affordance. Spark suites 184/184.
The examples folder was a flat `Game/{ADV,IMU,LIGHT,MIC,TOF,Tuning}` tree whose
names did not match its contents: `ADV` holds three projects with no Sparky block
at all, and `Tuning` is an authoring system prompt plus an empty project template,
not a lesson. Regroup by what a teacher actually picks from:
1-sensors/{imu,light,mic,tof} one sensor at a time (customer-supplied)
2-ai-camera/{face-play,face-id} camera + on-device AI
3-scratch-only/ no board required
_authoring/ tools for whoever writes examples
New in 2-ai-camera/face-id — three projects for the Story 4.9 `recognise face`
block, each isolating one thing the block's shape forces on a project:
d-face-id-door compare the opaque slot label as TEXT against a
teacher-owned allow-list, gated on AI confidence.
person_none is in no list, so unknown faces and boards
that cannot run the model both fail closed for free.
e-face-id-checkin slot -> name mapping lives in the project (two parallel
lists). The block never returns a human name, because a
name typed into a block travels inside the .sb3 children
share with each other.
f-face-id-greeting the cheap `detect face` gates the ~3 s `recognise face`,
plus a latch so one visitor is greeted once.
2-ai-camera/face-play moves here from design-artifacts: those are shipping
examples for teachers, not design evidence, so they belong where the editor
loads from. Both sets are generated — the generators, structural validators and
behavioural simulators stay in design-artifacts/ and now write here directly, so
there is exactly one copy of every .sb3. Hand edits will be overwritten.
Verified: both suites regenerate green (validate + simulate, 19 new behavioural
assertions), and all six projects load in the editor at :8601.
Contributor
|
I have read the CLA Document and I hereby sign the CLA You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves
What Github issue does this resolve (please include link)?
Proposed Changes
Describe what this Pull Request does
Reason for Changes
Explain why these changes should be made
Test Coverage
Please show how you have added tests to cover your changes