/** * 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 }; }