morse-code / views /settings.js
RemiFabre
feat: Demo mode for filming two robots conversing
a33a583
Raw
History Blame Contribute Delete
5 kB
/**
* 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 };
}