diff --git a/web/public/pattern-sandbox.html b/web/public/pattern-sandbox.html index aae0aac82..28d5cdcf8 100644 --- a/web/public/pattern-sandbox.html +++ b/web/public/pattern-sandbox.html @@ -19,7 +19,7 @@ diff --git a/web/src/lib/guide/deviceSim.ts b/web/src/lib/guide/deviceSim.ts index dae2d1da0..ce21954d0 100644 --- a/web/src/lib/guide/deviceSim.ts +++ b/web/src/lib/guide/deviceSim.ts @@ -13,13 +13,14 @@ // device prints on its KNOB MAP screen. Where each one sits on the front is // the 3D stage's business. -import { PATTERN_DETENTS_PER_TURN, PatternRuntime, type ColorRamp, type PatternInput } from "@/lib/pattern/harness"; +import { PatternRuntime, type ColorRamp, type PatternInput } from "@/lib/pattern/harness"; import { parseRampAnnotation } from "@/lib/pattern/ramp"; import { hexToRgb } from "@/lib/pattern/color"; import { knobSetupFromCode } from "@/lib/community/knobs"; import { LOGICAL_KNOB_DEFAULTS, knobUnitsPerTurn } from "@/lib/pattern/controls"; import { livePresets } from "@/lib/presets"; import { BASICS_PACK } from "@/lib/pattern/packs"; +import { BUS_MAX, BUS_UNITS_PER_CLICK, DETENTS_PER_TURN, EDGES_PER_DETENT, KNOB_COUNT } from "@/lib/shared/constants"; import { BASICS_NAMES } from "./basicsNames"; import { brightnessPercent, @@ -145,7 +146,6 @@ export type SimConsoleState = { // write. A channel is held once written; a change of the held value reaches // a pattern as clicks, ten bus units each, the remainder carried; a hand on // that encoder takes it back after a grace. -const BUS_UNITS_PER_CLICK = 10; const BUS_RELEASE_GRACE_MS = 250; // The lanes (docs/audio-ws-spec.md): what sound writes. A lane is an absolute @@ -197,7 +197,7 @@ export class DeviceSim { private now = 0; private time = 0; - private knobs: Knob[] = Array.from({ length: 4 }, () => ({ + private knobs: Knob[] = Array.from({ length: KNOB_COUNT }, () => ({ down: false, downAt: 0, longFired: false, @@ -330,7 +330,7 @@ export class DeviceSim { * the AUD row switched off stops all four. */ setLane(source: SimLaneSource, knob: number, value: number) { - if (source !== this.laneSource || !Number.isInteger(knob) || knob < 0 || knob > 3 || !Number.isFinite(value)) return; + if (source !== this.laneSource || !Number.isInteger(knob) || knob < 0 || knob >= KNOB_COUNT || !Number.isFinite(value)) return; this.laneValue[knob] = clamp(value, 0, 1); this.laneAt[knob] = this.now; } @@ -499,8 +499,8 @@ export class DeviceSim { * (core_bus.h applyRemoteParam). The 3D knob does not turn — nothing did. */ applyRemoteParam(knob: number, value: number) { - if (knob < 0 || knob > 3 || !Number.isFinite(value)) return; - const v = clamp(Math.round(value), 0, 1000); + if (knob < 0 || knob >= KNOB_COUNT || !Number.isFinite(value)) return; + const v = clamp(Math.round(value), 0, BUS_MAX); if (this.busHeld[knob]) { const acc = this.busResidual[knob] + (v - this.busValue[knob]); const d = Math.trunc(acc / BUS_UNITS_PER_CLICK); @@ -520,9 +520,9 @@ export class DeviceSim { /** The /knobs page: which way each encoder counts and how many of its 4 edges make a click. */ setKnobSettings(invert: boolean[], edgesPerClick: number[]) { - for (let i = 0; i < 4; i++) { - const sub = [1, 2, 4].includes(edgesPerClick[i]) ? edgesPerClick[i] : 4; - this.clickScale[i] = (invert[i] ? -1 : 1) * (4 / sub); + for (let i = 0; i < KNOB_COUNT; i++) { + const sub = [1, 2, 4].includes(edgesPerClick[i]) ? edgesPerClick[i] : EDGES_PER_DETENT; + this.clickScale[i] = (invert[i] ? -1 : 1) * (EDGES_PER_DETENT / sub); } } @@ -700,7 +700,7 @@ export class DeviceSim { const d = detents[i]; if (!d) continue; const [min, max] = this.ranges[i] ?? [0, 1]; - const next = values[i] + d * (knobUnitsPerTurn([min, max]) / PATTERN_DETENTS_PER_TURN); + const next = values[i] + d * (knobUnitsPerTurn([min, max]) / DETENTS_PER_TURN); values[i] = this.wraps[i] ? wrap(next, min, max) : clamp(next, min, max); } } @@ -710,7 +710,7 @@ export class DeviceSim { // SELECT clears the name card (patternflow.ino:1731). if (mode === "select") this.noticeUntil = -1; // SELECT drops every held bus channel, so browsing never fights one (patternflow.ino). - if (mode === "select") for (let i = 0; i < 4; i++) if (this.busHeld[i]) this.releaseBus(i); + if (mode === "select") for (let i = 0; i < KNOB_COUNT; i++) if (this.busHeld[i]) this.releaseBus(i); this.mode = mode; this.idleAt = this.now; this.onModeChange?.(mode); diff --git a/web/src/lib/guide/panelScreens.ts b/web/src/lib/guide/panelScreens.ts index 5fdced609..59048832e 100644 --- a/web/src/lib/guide/panelScreens.ts +++ b/web/src/lib/guide/panelScreens.ts @@ -10,8 +10,9 @@ // firmware/patternflow/src/hub75/ESP32-HUB75-MatrixPanel-I2S-DMA.h // drawPixel/fillRect overrides, the // rotation transform, RGB565 -> 888 -// Line numbers cited here are from those files at v3.10.4. When a screen -// changes there, change it here - the test pins a few pixels so drift shows. +// Firmware constants are cited by name. Other line numbers cited here are +// from those files at v3.10.4. When a screen changes there, change it here - +// the test pins a few pixels so drift shows. // // Things the port reproduces on purpose, because the panel does them: // - Adafruit GFX's text wrap is on (the default; nothing in the firmware turns @@ -36,11 +37,18 @@ // License: MIT. The two font tables this draws with are BSD - see glcdFont.ts // and tomThumbFont.ts for their notices. +import { + BUTTON_DEBOUNCE_MS, + EDGES_PER_DETENT, + LONG_PRESS_MS, + PANEL_GEOMETRIES, + PANEL_GEOM_DEFAULT, +} from "@/lib/shared/constants"; import { GLCD_FONT } from "./glcdFont"; import { TOM_THUMB, type GfxFont } from "./tomThumbFont"; -export const PANEL_W = 128; -export const PANEL_H = 64; +export const PANEL_W: number = PANEL_GEOMETRIES[PANEL_GEOM_DEFAULT].resW; +export const PANEL_H: number = PANEL_GEOMETRIES[PANEL_GEOM_DEFAULT].resH; const FRAME_BYTES = PANEL_W * PANEL_H * 4; export type Rgb = readonly [number, number, number]; @@ -71,15 +79,15 @@ export type PanelOverlay = wifi: WifiStatusText | (string & {}); /** ipString(): the dotted IP, or "-" when not connected (src/core_wifi.h:629-632). */ ip: string; - /** Feature toggle rows; a core build has none. At most two are drawn (patternflow.ino:759). */ + /** Feature toggle rows; a core build has none. At most two are drawn (MAX_FEATURE_ROWS, patternflow.ino). */ rows?: { name: string; on: boolean }[]; /** The hotspot's name, e.g. "patternflow-a1b2", when the panel's own AP is up. Shown only while not connected. */ hotspotName?: string; /** PatternflowWifi::isConnected(). Defaults to wifi === "CONNECTED". */ connected?: boolean; - /** "K1 = SLEEP" line; true unless a build compiles sleep out (config.h:247-248). */ + /** "K1 = SLEEP" line; true unless a build compiles sleep out (PF_SLEEP_ENABLED, config.h). */ sleepHint?: boolean; - /** "K4=UPDATE" line; true unless a build compiles the web update out (net_config.h:172-173). */ + /** "K4=UPDATE" line; true unless a build compiles the web update out (PF_WEBUPDATE_ENABLED, net_config.h). */ updateHint?: boolean; } | { @@ -123,7 +131,7 @@ export type PanelOverlay = // ── Firmware constants ────────────────────────────────────────────────────── -const DEFAULT_BRIGHTNESS = 204; // firmware/patternflow/config.h:241 ("80%") +const DEFAULT_BRIGHTNESS = 204; // DEFAULT_BRIGHTNESS, firmware/patternflow/config.h ("80%") /** The percentage the panel prints for a brightness byte (patternflow.ino:693). */ export function brightnessPercent(level: number): number { @@ -155,50 +163,53 @@ export interface FirmwareConstants { } export const FIRMWARE: Readonly = Object.freeze({ - // MODE_HOLD_MS, patternflow.ino:269. Fires WHILE still held, on the first - // frame the hold exceeds it (strict >, src/core_encoders.h:172); the release - // that follows is not a click (core_encoders.h:151-154). - longPressMs: 1000, - // Button edges closer than this are ignored (src/core_encoders.h:143). - buttonDebounceMs: 50, - // Quadrature edges per detent (knobSubSteps default, src/core_encoders.h:76); - // one detent = one knob delta (patternflow.ino:1168-1182, no acceleration). - edgesPerDetent: 4, - // BRIGHTNESS_IDLE_MS, patternflow.ino:270; checked with > at :1566. The timer - // restarts on entry (:1539) and only when a turn actually changes the level - // (:1555-1559) - turning against the 5/255 clamp does not keep it open. + // MODE_HOLD_MS in patternflow.ino, LONG_PRESS_MS in shared/constants.json. + // Fires WHILE still held, on the first frame the hold exceeds it (strict >, + // Button::longPressed in src/core_encoders.h); the release that follows is + // not a click (Button::pressed). + longPressMs: LONG_PRESS_MS, + // Button edges closer than this are ignored (Button::pressed in + // src/core_encoders.h, BUTTON_DEBOUNCE_MS in shared/constants.json). + buttonDebounceMs: BUTTON_DEBOUNCE_MS, + // Quadrature edges per detent (the knobSubSteps default in + // src/core_encoders.h, EDGES_PER_DETENT in shared/constants.json); one + // detent = one knob delta (readInputFrame in patternflow.ino, no acceleration). + edgesPerDetent: EDGES_PER_DETENT, + // BRIGHTNESS_IDLE_MS in patternflow.ino, checked with > in loop(). The timer + // restarts on entry and only when a turn actually changes the level - + // turning against the 5/255 clamp does not keep it open. brightnessIdleExitMs: 5000, - // BRIGHTNESS_SAVE_DELAY_MS, patternflow.ino:273 (NVS write after the mode closes). + // BRIGHTNESS_SAVE_DELAY_MS in patternflow.ino (NVS write after the mode closes). brightnessSaveDelayMs: 3000, - // OSC_INFO_IDLE_MS (the NETWORK screen's timer despite the name), patternflow.ino:274, :1658. + // OSC_INFO_IDLE_MS in patternflow.ino (the NETWORK screen's timer despite the name). networkIdleExitMs: 8000, - // KNOB_MAP_IDLE_MS, patternflow.ino:276, :1687. + // KNOB_MAP_IDLE_MS in patternflow.ino. knobmapIdleExitMs: 8000, - // KNOB_MAP_HILITE_MS, patternflow.ino:277; a knob stays lit this long after its last detent (:1005-1006). + // KNOB_MAP_HILITE_MS in patternflow.ino: a knob stays lit this long after its last detent (drawKnobMap). knobmapHighlightMs: 600, - // UPDATE_IDLE_MS, patternflow.ino:278, :1711 (not while flashing). + // UPDATE_IDLE_MS in patternflow.ino (not while flashing). updateIdleExitMs: 600000, - // NET_INFO_REDRAW_MS, patternflow.ino:275: info screens repaint at most this often (or on a change). + // NET_INFO_REDRAW_MS in patternflow.ino: info screens repaint at most this often (or on a change). infoRedrawMs: 250, - // SELECT_DETENTS_PER_STEP, patternflow.ino:240; see stepSelect(). + // SELECT_DETENTS_PER_STEP in patternflow.ino; see stepSelect(). selectDetentsPerStep: 3, - // SELECT_SETTLE_MS, patternflow.ino:234: the highlighted pattern loads once K4 has rested this long (:1943). + // SELECT_SETTLE_MS in patternflow.ino: the highlighted pattern loads once K4 has rested this long. selectSettleMs: 350, - // PATTERN_SAVE_DELAY_MS, patternflow.ino:207 (only once SELECT is left, :313). + // PATTERN_SAVE_DELAY_MS in patternflow.ino (only once SELECT is left, savePatternIfSettled). patternSaveDelayMs: 3000, - // CONTENT_NOTICE_SECONDS, patternflow.ino:279 - the name card after a remote pick; SELECT never shows it. + // CONTENT_NOTICE_SECONDS in patternflow.ino - the name card after a remote pick; SELECT never shows it. contentNoticeMs: 1000, - // constrain(currentBrightness + d * 5, 5, 255), patternflow.ino:1554. + // constrain(currentBrightness + d * 5, 5, 255) in loop(), patternflow.ino. brightnessMin: 5, brightnessMax: 255, brightnessStepPerDetent: 5, - // DEFAULT_BRIGHTNESS, config.h:241 - a byte, not a percent. + // DEFAULT_BRIGHTNESS in config.h - a byte, not a percent. defaultBrightness: DEFAULT_BRIGHTNESS, // brightnessPercent(204) = 80. defaultBrightnessPercent: brightnessPercent(DEFAULT_BRIGHTNESS), - // MAX_FEATURE_ROWS, patternflow.ino:759. + // MAX_FEATURE_ROWS in drawNetworkInfo, patternflow.ino. networkMaxFeatureRows: 2, - // WAKE_GUARD_MS, src/core_sleep.h:64: input ignored this long after falling asleep. + // WAKE_GUARD_MS in src/core_sleep.h: input ignored this long after falling asleep. wakeGuardMs: 800, }); @@ -224,7 +235,7 @@ export function wrapIndex(index: number, count: number): number { return ((index % count) + count) % count; } -// Fixed-size name buffers: MODULE_NAME_BYTES, firmware/patternflow/pattern_registry.h:139. +// Fixed-size name buffers: MODULE_NAME_BYTES, firmware/patternflow/pattern_registry.h. const MODULE_NAME_BYTES = 40; const LATIN1_FOLD = "AAAAAAACEEEEIIII" + // C0-CF @@ -235,8 +246,8 @@ const LATIN1_FOLD = /** * A pattern name as the panel prints it: asciiFold (patternflow.ino:577-601) * over the UTF-8 bytes. Latin-1 letters lose their accent, anything else - * becomes one '?'. Stored names live in 40-byte buffers (pattern_registry.h:139, - * snprintf at :217), so at most 39 bytes of the name survive. + * becomes one '?'. Stored names live in 40-byte buffers (MODULE_NAME_BYTES in + * pattern_registry.h, filled with snprintf), so at most 39 bytes of the name survive. */ export function asciiFold(name: string): string { const bytes = new TextEncoder().encode(name).subarray(0, MODULE_NAME_BYTES - 1); @@ -282,17 +293,17 @@ const rgb565 = (r: number, g: number, b: number): Rgb => color565to888(color565( /** The firmware's panel palette, as it lands in the frame buffer. */ export const PANEL_COLORS = Object.freeze({ black: rgb565(0, 0, 0), - led: rgb565(232, 85, 46), // pfLedC, patternflow.ino:635 - white: rgb565(255, 255, 255), // pfWhiteC, :636 - gray: rgb565(140, 140, 140), // pfGrayC, :637 - dim: rgb565(90, 90, 90), // pfDimC, :638 - rule: rgb565(60, 60, 60), // pfRuleC, :639 - green: rgb565(80, 220, 130), // pfGreenC, :640 - red: rgb565(255, 80, 80), // pfRedC, :641 - blue: rgb565(120, 180, 255), // pfBlueC, :642 - selectPage: rgb565(190, 190, 190), // "i / N", :1054 - selectHint: rgb565(200, 200, 200), // "HOLD TO SELECT", :1084 - knobActiveFill: rgb565(74, 27, 15), // KNOB MAP active disc, :1009 + led: rgb565(232, 85, 46), // pfLedC in patternflow.ino + white: rgb565(255, 255, 255), // pfWhiteC + gray: rgb565(140, 140, 140), // pfGrayC + dim: rgb565(90, 90, 90), // pfDimC + rule: rgb565(60, 60, 60), // pfRuleC + green: rgb565(80, 220, 130), // pfGreenC + red: rgb565(255, 80, 80), // pfRedC + blue: rgb565(120, 180, 255), // pfBlueC + selectPage: rgb565(190, 190, 190), // "i / N", drawSelectingMode + selectHint: rgb565(200, 200, 200), // "HOLD TO SELECT", drawSelectingMode + knobActiveFill: rgb565(74, 27, 15), // KNOB MAP active disc, drawKnobMap }); // ── A small Adafruit GFX over an RGBA frame ───────────────────────────────── @@ -729,8 +740,8 @@ function drawUiLine(g: PanelGfx, font: GfxFont | null, text: string, yTop: numbe } // PatternflowUiText::drawWrappedName, src/core_ui_text.h:447-449 -> drawWrapped -// (:388-445) with PF_UI_FONT_SELECT = the built-in 5x7 (core_ui_fonts.h:50-52) -// and pitch 9 (:64-66). +// (:388-445) with PF_UI_FONT_SELECT = the built-in 5x7 and PF_UI_PITCH_SELECT +// = 9 (both in core_ui_fonts.h). // // Only drawWrapped's first branch is ported, because it is the only one that // runs: it measures the whole name with getTextBounds, and with GFX wrap on, diff --git a/web/src/lib/pattern/controls.ts b/web/src/lib/pattern/controls.ts index 7c945677b..0f84f834e 100644 --- a/web/src/lib/pattern/controls.ts +++ b/web/src/lib/pattern/controls.ts @@ -1,16 +1,17 @@ -import { PATTERN_DETENTS_PER_TURN } from "./harness"; +import { BUS_MAX, DETENTS_PER_TURN, TURNS_PER_FULL_RANGE } from "@/lib/shared/constants"; // Detents in one full turn of the physical encoder. One fact, one constant: -// harness.ts owns it (see the note there — the reference Bourns -// PEC11R-...-S0024 is 24, not the 20 assumed for a long time) and this is the -// name the knob math has always used for it. -export const ENCODER_CLICKS_PER_TURN = PATTERN_DETENTS_PER_TURN; +// shared/constants.json owns it (see the note in harness.ts — the reference +// Bourns PEC11R-...-S0024 is 24, not the 20 assumed for a long time) and this +// is the name the knob math has always used for it. +export const ENCODER_CLICKS_PER_TURN = DETENTS_PER_TURN; // The device's absolute bus speaks integers 0..1000 (PF_BUS_MAX in -// firmware/patternflow/src/core_bus.h). Everything here that writes a wire -// value — the Director's bake, the .pfs codec, the MIDI export — uses this -// rather than the literal, so the two sides can only disagree in one place. -export const BUS_WIRE_MAX = 1000; +// firmware/patternflow/src/core_bus.h, BUS_MAX in shared/constants.json). +// Everything here that writes a wire value — the Director's bake, the .pfs +// codec, the MIDI export — uses this rather than the literal, so the two sides +// can only disagree in one place. +export const BUS_WIRE_MAX = BUS_MAX; // How many full turns cross a parameter's ENTIRE range. // @@ -21,7 +22,7 @@ export const BUS_WIRE_MAX = 1000; // Deriving travel from the range makes every knob feel the same regardless of // what it controls, and keeps the web preview and the encoder in step because // both read the value from here. -export const TURNS_PER_FULL_RANGE = 2; +export { TURNS_PER_FULL_RANGE }; /** Value units covered by one full turn, for a parameter spanning `range`. */ export function knobUnitsPerTurn(range: readonly [number, number]) { diff --git a/web/src/lib/pattern/harness.ts b/web/src/lib/pattern/harness.ts index 8fbf77296..e1aa9564d 100644 --- a/web/src/lib/pattern/harness.ts +++ b/web/src/lib/pattern/harness.ts @@ -1,14 +1,16 @@ -export const PATTERN_MATRIX_WIDTH = 128; -export const PATTERN_MATRIX_HEIGHT = 64; -export const PATTERN_KNOB_COUNT = 4; +import { DETENTS_PER_TURN, KNOB_COUNT, PANEL_GEOMETRIES, PANEL_GEOM_DEFAULT } from "@/lib/shared/constants"; + +export const PATTERN_MATRIX_WIDTH: number = PANEL_GEOMETRIES[PANEL_GEOM_DEFAULT].resW; +export const PATTERN_MATRIX_HEIGHT: number = PANEL_GEOMETRIES[PANEL_GEOM_DEFAULT].resH; +export const PATTERN_KNOB_COUNT = KNOB_COUNT; // Detents in one full turn of the physical encoder. The BOM reference part is // Bourns PEC11R-4220F-S0024 — the S0024 suffix means 24 detents / 24 PPR, which // distributors confirm. This was 20 for a long time, which silently put the web // preview 20 % out of step with the hardware it claims to mirror. // Sourcing a different encoder? Generic EC11s ship as 20, 24 or 30 detents — -// this constant has to match whatever is actually in the build. -// Keep in sync with ENCODER_CLICKS_PER_TURN in patternflowControls.ts. -export const PATTERN_DETENTS_PER_TURN = 24; +// DETENTS_PER_TURN in shared/constants.json has to match whatever is actually +// in the build. controls.ts reads the same value as ENCODER_CLICKS_PER_TURN. +export const PATTERN_DETENTS_PER_TURN = DETENTS_PER_TURN; export type PatternParams = Record; diff --git a/web/src/lib/pattern/matrix.ts b/web/src/lib/pattern/matrix.ts index 10b328c4f..4ecc7ce43 100644 --- a/web/src/lib/pattern/matrix.ts +++ b/web/src/lib/pattern/matrix.ts @@ -24,10 +24,15 @@ // public/pattern-sandbox.html is built from this module // (scripts/build-sandbox.ts), so there is no second parser to keep in step. +import { PANEL_GEOMETRIES, PANEL_GEOM_DEFAULT } from "@/lib/shared/constants"; + export type MatrixSize = { width: number; height: number }; /** The stock Patternflow panel. Used whenever a pattern carries no @matrix. */ -export const DEFAULT_MATRIX: MatrixSize = { width: 128, height: 64 }; +export const DEFAULT_MATRIX: MatrixSize = { + width: PANEL_GEOMETRIES[PANEL_GEOM_DEFAULT].resW, + height: PANEL_GEOMETRIES[PANEL_GEOM_DEFAULT].resH, +}; /** * The stock panel in either orientation — the board is mounted whichever way @@ -69,7 +74,7 @@ export function clampMatrixDimension(value: number): number | null { * Pixels per frame beyond which the JS preview starts to struggle — four times * the stock panel. Not a limit, just the point worth warning about. */ -export const MATRIX_HEAVY_PIXELS = 128 * 64 * 4; +export const MATRIX_HEAVY_PIXELS = DEFAULT_MATRIX.width * DEFAULT_MATRIX.height * 4; export function isValidMatrix(size: Partial | null | undefined): boolean { if (!size) return false;