Spaces:
Running
Running
| /** | |
| * Settings sheet: speed, beep tone, mic sensitivity, and the robot clap | |
| * calibration knobs (contact angle / hold / latency lead). These are exactly | |
| * the values we tune against real hardware, surfaced so calibration needs no | |
| * code edit. | |
| */ | |
| import { el, clear } from "./dom.js"; | |
| import { SPEED_PRESETS, makeTiming } from "../lib/timing.js"; | |
| import { APP_VERSION } from "../lib/version.js"; | |
| export function createSettings(ctx) { | |
| const c = ctx.state; | |
| function rangeRow(label, get, set, { min, max, step, fmt = (v) => v }) { | |
| const out = el("span.range-val", {}, fmt(get())); | |
| const slider = el("input.range", { | |
| type: "range", min, max, step, value: get(), | |
| oninput: (e) => { set(parseFloat(e.target.value)); out.textContent = fmt(get()); }, | |
| }); | |
| return el("div.field", {}, [ | |
| el("div.row.between", {}, [el("label.field-label", {}, label), out]), | |
| slider, | |
| ]); | |
| } | |
| const body = el("div.settings-body", {}, [ | |
| el("h3", {}, "Transmission"), | |
| rangeRow("Speed (unit)", () => c.unitMs, (v) => { c.unitMs = v; c.speed = "custom"; }, | |
| { min: 60, max: 240, step: 10, fmt: (v) => `${v} ms` }), | |
| rangeRow("Beep tone", () => c.beepFreq, (v) => { c.beepFreq = v; }, | |
| { min: 600, max: 4000, step: 50, fmt: (v) => `${v} Hz` }), | |
| el("h3", {}, "Microphone / detection"), | |
| rangeRow("High-pass", () => c.detector.highpassHz, (v) => { c.detector.highpassHz = v; }, | |
| { min: 500, max: 4000, step: 100, fmt: (v) => `${v} Hz` }), | |
| rangeRow("Sensitivity", () => c.detector.thresholdFactor, | |
| (v) => { c.detector.thresholdFactor = v; }, | |
| { min: 1.5, max: 10, step: 0.5, fmt: (v) => `${v}×` }), | |
| el("h3", {}, "Robot clap (calibration)"), | |
| rangeRow("Contact angle", () => c.tap.collisionDeg, (v) => { c.tap.collisionDeg = v; }, | |
| { min: 10, max: 60, step: 1, fmt: (v) => `${v}°` }), | |
| rangeRow("Held antenna", () => c.tap.heldDeg, (v) => { c.tap.heldDeg = v; }, | |
| { min: -55, max: 0, step: 1, fmt: (v) => `${v}°` }), | |
| rangeRow("Hold", () => c.tap.holdMs, (v) => { c.tap.holdMs = v; }, | |
| { min: 20, max: 150, step: 10, fmt: (v) => `${v} ms` }), | |
| rangeRow("Latency lead", () => c.tap.leadMs, (v) => { c.tap.leadMs = v; }, | |
| { min: -50, max: 250, step: 10, fmt: (v) => `${v} ms` }), | |
| el("div.row", {}, [ | |
| el("button.btn.ghost", { onclick: () => testBeep() }, "Test beep"), | |
| el("button.btn.ghost", { onclick: () => testClap(), disabled: !ctx.reachy }, | |
| ctx.reachy ? "Test clap" : "Test clap (no robot)"), | |
| ]), | |
| el("div.gap-preview muted"), | |
| el("div.version muted", {}, APP_VERSION), | |
| ]); | |
| function testBeep() { | |
| ctx.synth().play([0, makeTiming(c.unitMs).dahGapMs], { clickMs: makeTiming(c.unitMs).clickMs }); | |
| } | |
| function testClap() { | |
| if (ctx.reachy) ctx.tapper().testClap(); | |
| } | |
| // ── Demo mode: hidden conversation rules + kickoff sends ─────────── | |
| if (c.mode === "demo") { | |
| c.demo = c.demo || { rules: [] }; | |
| const list = el("div.rule-list"); | |
| const ruleRow = (rule) => { | |
| const inp = el("input.rule-input", { | |
| type: "text", value: rule.input, placeholder: "hears…", | |
| oninput: (e) => { rule.input = e.target.value; }, | |
| }); | |
| const out = el("input.rule-input", { | |
| type: "text", value: rule.output, placeholder: "replies…", | |
| oninput: (e) => { rule.output = e.target.value; }, | |
| }); | |
| const send = el("button.btn.ghost.small", { | |
| title: "Send this phrase now", | |
| onclick: () => { | |
| ctx._closeSettings?.(); | |
| setTimeout(() => ctx.transmit(rule.input, { via: ctx.reachy ? "robot" : "device" }), 150); | |
| }, | |
| }, "Send ▶"); | |
| const del = el("button.icon-btn.tiny", { | |
| title: "Remove", onclick: () => { c.demo.rules.splice(c.demo.rules.indexOf(rule), 1); rebuild(); }, | |
| }, "✕"); | |
| return el("div.rule-row", {}, [inp, el("span.rule-arrow", {}, "→"), out, send, del]); | |
| }; | |
| const rebuild = () => { clear(list); c.demo.rules.forEach((r) => list.append(ruleRow(r))); }; | |
| rebuild(); | |
| const addBtn = el("button.btn.ghost.small", { | |
| onclick: () => { c.demo.rules.push({ input: "", output: "" }); rebuild(); }, | |
| }, "+ Add rule"); | |
| body.prepend(el("div.demo-setup", {}, [ | |
| el("h3", {}, "Conversation rules"), | |
| el("p.muted.tiny", {}, "When this robot hears the left phrase, it taps the right one back. “Send ▶” kicks one off now (the sheet closes so the camera sees only the demo)."), | |
| list, | |
| addBtn, | |
| ])); | |
| } | |
| return { node: body }; | |
| } | |