# Gauge.S reusable modules — offline author / agent reference

User guide: ./guide.html. Device controls: /wifi/index.htm#modules.
Design editor: /wifi/sim/editor.html, File > Modules.
Platform: V6/V6.1, Module API 1, Lua 5.5.1 with LUA_32BITS (int32 + float32).
These APIs run locally and do not need a hosted service or API key.

START HERE TO CREATE A MODULE: ./authoring.md — step-by-step authoring, Lua sandbox,
transparent OGS overlays, time/state pitfalls, executable WASM verification and
delivery checklist. Read it before writing code. This file is the compact API
reference; ./guide.html is the customer guide. All three ship on the device.
Repository location: deploy/wifi/modules/. No engine-source reading is needed.
Verified complete example: ./shift-crown.ogp (MIT). Progressive shift lights and a
minimum-visible danger window; bind your real RPM source after import. Clean source
in prep/modules/shift-crown/ (private Gauge.Modules submodule); qualification in
testing/modules/AUTHORING_RESULTS.md.
Use the installed/local editor with Module API 1; do not assume the public hosted
editor has the same feature-branch firmware. Gauge design reference in the repo:
prep/gauges/README.md (published as /gauge-guide.md by hosted editor packaging).

## Workflows

Use: select an instance; bind real input parameters; Stop session > Save choices >
Apply saved setup > Start session. Run on boot is a separate saved choice.
Screen navigation adds dedicated views after ordinary gauges while running.
Popup messages independently enables automatic event overlays. Neither starts Lua.
Status is a snapshot: Refresh explicitly. Input picker values update only while open.
Performance collects all 20 supported acceleration/distance/braking metrics per run.
Runs & personal bests previews saved runs; each Reset clears one best, retains
history and excludes the already-started run. Missing/unreached values display —.
Vehicle Speed (km/h) and accel mag are optional default bindings; coherent GPS is
required for standing distances and when GPS timing is selected. No source mixing.
Style preview is a disposable production-WASM renderer with sample values: no Lua
activation, device writes, sensor injection or durable records.

Lap timer: Choose / create / import track loads installed courses automatically.
Import course JSON, or Create a track > choose an offline catalogue outline >
Draw timing line (two points across the route). Check the arrow/Reverse direction;
add optional sectors in driving order. Install & select track > Save choices >
Apply stopped setup > Start session. Catalogue outlines are not timing gates.
New session clears current lap count/last/session best while retaining history and
the track record. A changed GPS UTC date rolls over at the first valid start crossing;
an unavailable date retains the saved session. Stop/Start does not erase it.
Selecting Lap timer/Performance automatically opens Lap history/Run history above
setup fields. View laps/View runs jumps to the list; Track/Session selects its
scope, and View lap/View run opens readable measurements (including sector cards).
Raw journal/file inspection is author/debug API functionality, not customer UI.
Analyse logs reads local/SD CSVs with explicit column mappings and missing-quality
assumptions, runs installed production logic in a disposable WASM realm, and saves
selected byte-verified journals/provenance into ordinary module history. Identity
is committed last; identical retries are idempotent. Start once to refresh device
summaries/bests. This is recovered history; live replay tools are debugging only.

Author: import a module OGP or folder in Module packages > import and author offline.
Use the file selector to edit manifest/Lua/views. Export the complete pack.
The editor source tools can open a view in the normal gauge editor and copy it back.
Stage a package, then register/apply stopped setup, then start explicitly.
Editor agents: await GaugeAgent.loadOgpB64(b64,name) for lazy module imports.
One package id must use one manifest path across its instances. To replace shared
source, stage the new path for all siblings then restart; otherwise live setup
reports "package identity collision" and retains the old runtime.
Do not modify installed content-addressed packages. Stage a new complete pack.

## A complete small module

Create these three files in one folder, import the folder, validate and export.
The manifest must be the first file in the OGP; choose it as the entry file.

warning.json:
```json
{
    "kind": "module", "api": 1, "id": "my-warning", "name": "My warning",
    "inputs": { "temperature": { "param": "Oil Temp", "unit": "C" } },
    "settings": {
        "limit": { "type": "number", "default": 120, "unit": "C", "description": "Warn at or above this temperature." },
        "clear": { "type": "number", "default": 115, "unit": "C", "description": "Clear below this temperature." },
        "minimum": { "type": "integer", "default": 5000, "min": 0, "max": 600000, "unit": "ms" },
        "message": { "type": "string", "default": "OIL HOT", "maxLength": 31, "group": "Appearance" }
    },
    "logic": { "file": "warning.lua" },
    "views": { "warning": { "file": "view.json", "params": { "v0": "temperature" }, "customs": { "heading": "message" } } },
    "presentations": [{ "event": "hot", "clearEvent": "cooled", "view": "warning", "priority": 80, "snapshot": false, "minVisibleMsSetting": "minimum" }],
    "menu": [{ "name": "Warn at", "setting": "limit" }]
}
```

warning.lua:
```lua
local function update(s, us)
    local value, _, _, ps, pu = ctx.read("temperature")
    local fresh = value and ps and ctx.elapsed_ms(s, us, ps, pu) <= 15000
    if fresh then out.temperature = value end
    if fresh and value >= cfg.limit and out.active == 0 then
        out.active = 1
        ctx.emit("hot")
    elseif out.active == 1 and (not fresh or value < cfg.clear) then
        out.active = 0
        ctx.emit("cooled")
    end
end
return {
    on_setup = function()
        assert(cfg.clear <= cfg.limit)
        ctx.parameter("temperature", {unit="C", dec=0})
        ctx.parameter("active", {noLog=true})
        ctx.on_periodic(20, update)
    end,
    on_enable = function() out.active = 0 end
}
```

view.json:
```json
{
    "name": "Warning", "screenW": 320, "screenH": 120,
    "params": { "v0": "temperature" },
    "customs": { "heading": { "type": "string", "default": "OIL HOT" } },
    "vars": { "heading": "@heading" },
    "elements": [
        { "type": "rect", "layer": 1, "x": 50, "y": 50, "w": 320, "h": 120, "color": "#E6282A36" },
        { "type": "label", "layer": 2, "x": 50, "y": 25, "text": "$heading", "font": "visitor", "size": 3, "align": "center", "color": "#FFFF5555" },
        { "type": "valueText", "layer": 2, "x": 50, "y": 66, "defaultParam": "[v0]", "font": "visitor", "size": 4, "format": "%.0f C", "align": "center", "smoothing": 0 }
    ]
}
```

## Manifest and visual contracts

settings types: number, integer, bool, string, choice (choices array), param, track.
inputs.<role>.optional:true permits a missing CDS source; ctx.read then returns nil
without publication metadata. Required inputs still fail setup if missing. Binding
resolution happens at explicit setup, not automatically when new sources appear.
Defaults are validated even when overridden. Optional name/unit/description are
generated-form labels/help; group:"Appearance" separates display choices.
String maxLength counts UTF-8 bytes; control:"color" validates #RRGGBB/#AARRGGBB
and exposes the shared RGB/alpha picker. ValueText format holds 15 bytes; prefer
author-defined safe format choices. Label text holds 31 bytes. Font size is an
integer multiplier (the JSON key is "size"). Use ordinary OGS Customs/vars for style.

views.<name>.params maps local OGS roles to this instance's declared output names.
views.<name>.customs maps OGS custom names to scalar setting names. At explicit
setup these resolve to copied values (1024-byte JSON limit per view), overriding
embedded/sidecar customs without changing package bytes. Settings apply on setup.
Assets and presets are relative to the view file; preserve all files in export.
Views share the parent Canvas, centred with their authored dimensions. They do not
start or own a separate runtime. Ordinary reusable components bind existing values.
Set views.<name>.transparent:true to preserve the gauge when explicitly opening a
view/screen-mode popup. Overlay popups already retain it. There is one winning
central popup and one independent perimeter indicator (see modes below).
Live status/blinking views require snapshot:false. Emit eligibility
transitions, not a new event every tick. Use visible:false/expression, never a dark
"off" shape, when an inactive indicator must expose the exact underlying pixels.
Bool customs: use direct visible:"$showValue" on a group and numeric runtime
conditions on its children. Compound bool-custom expressions can hide true content
on the current renderer. See authoring.md for the tested grouping pattern.

presentations: event, view, optional clearEvent, priority 0..255, snapshot bool,
mode "overlay", "screen" or "indicator", dismiss "grace". One pending event per rule.
Optional invertParam names a declared output: >0 requests whole-panel RGB inversion
only while that selected presentation actually draws. Live snapshot:false rules
use current values. Hidden/preempted/menu-suppressed/disabled views do not invert;
clear/expiry removes the request. The host reuses panel inversion; scripts own the
blink phase/rate. Manual views/components/style previews do not drive this effect.
For whole-segment smoothing, publish a separate eased/quantized display count;
visibility expressions bypass OGS easing. Keep raw RPM and all timers independent.
Overlay/screen share one central popup slot. Indicator has a separate bounded slot
above it for perimeter lights beside other modules' warnings/results. One winner
per slot; menus, grace and the instance Popups switch hide both. Older hosts reject
the indicator mode during setup.
holdMs: timed result lifetime from event time, even while hidden; 0 means persistent.
minVisibleMs (0..600000) OR minVisibleMsSetting (integer setting name): minimum
warning time from its first successful draw. Requires clearEvent and holdMs=0.
Clear before first draw cancels the unseen warning. Clear after first draw retains
it until the minimum expires; active warnings remain afterward. Re-trigger starts
a new event. Disable/fault, menu ownership and ten-second interaction grace win.
Higher priority wins within each slot; newest breaks ties. snapshot:false reads current values;
snapshot:true freezes the values copied by ctx.emit, while measurement continues.
Minimum visibility retains a view, not children hidden by a cleared live output.
For independent warning/indicator lifetimes, use mode:"indicator", snapshot:false
for lights and an overlay message whose text stays present until the host ends it.
Do not duplicate the lights inside that message (double alpha blending).
See authoring.md for lifetime and manual-view caveats.

Active automatic views prepare ahead of events and stay hot through clear/blink,
preemption and menu suppression. No event-time file reads/JSON parsing. Automatic
view metadata/arenas share a 1-MiB quota per renderer; Canvas objects/tiles have
separate fixed limits. Status.visuals exposes wanted/ready, hotBytes, loads,
failures and elapsed render-work maxima. Check ready==wanted and failures==0.
Rejected preparation retries on reactivation/explicit reload; minimum visibility
starts on actual appearance, never during preparation. Explicit screens still load
on request. Neither rendering nor its cache changes measurement timers.

Optional configUi:{file:"configure.html",name:"Open my panel"} uses self-contained
HTML/inline JS/CSS in an opaque sandbox. GaugeModuleUi.read() explicitly returns
settings, config, enabled, values, storage and needsSetup for this instance only.
save(settings), action(menuAction), records({op,key,after?,offset?}) share the scoped
bridge. Reads never poll while idle. Records are paged and read-only; no direct
device, network or origin-storage API is exposed by the bridge.

## Lua lifetime and limits

Each instance runs globally across gauge switches. Setup is bounded/transactional.
Create parameters/calculations and callbacks only in on_setup. Declarations replay
on enable; a fresh VM retains committed outputs. Disable/fault closes that VM;
published cells remain bindable until reboot. Live replacement requires stopped
current-generation instance and drained storage. Setup never enables automatically.
Declare outputs in deterministic order (explicit calls or ipairs over a dense array).
The Lua sandbox exposes assert/error/ipairs/next/pairs/select/tonumber/tostring/type
and math except random/randomseed. There is NO string, table, os, io, print, require,
pcall or coroutine library. Lua operators, #length, concatenation and tables work.
out.* writes are forbidden during source load, on_setup and on_disable; use
declaration initial values and publish resets in on_enable. Stop hides the view
through the host and retains outputs. Expression outputs are always read-only.

on_setup, on_enable, on_disable, on_action(name), on_storage(request,status,result)
are lifecycle callbacks. Use ctx.on_periodic(hz,callback) and
ctx.on_update(roleOrDenseRoleArray,hz,callback) during setup. Callbacks receive
(seconds,micros) and run serially after ordinary expressions, never in CAN/GPS ISR.
ctx.read(role) -> valueOrNil, observationSeconds, observationMicros,
publicationSecondsOrNil, publicationMicrosOrNil. ctx.now() is the observation pair.
ctx.updated(role) reports coalesced publications for this registration.
Reading or re-evaluating a calculated parameter does not prove physical freshness.
Leave acceptance/freshness/rearm policy to the author.

ctx.parameter(name,{unit,dec,initial,noLog,...}) creates a finite scalar exposed as
instance.name. Write only declared finite out.name values. ctx.emit(name) freezes a
snapshot at that statement. Callback faults roll back outputs/events/storage.
ctx.disable() is a committed self-disable request. cfg contains copied settings.
Module outputs skip automatic Historic; external consumers can explicitly use it.
Ordinary CSV includes published outputs unless noLog; membership changes rotate it.

8 instances, 64 KiB Lua heap each, 32 outputs, 32 settings, 16 inputs, 16 callbacks,
8 views, 8 presentations, 4 events and 4 queued storage requests per instance.
Lua source <=16 KiB; manifest <=32 KiB; UI schema/config <=16 KiB; settings text
<=127 bytes. 10,000 instructions per callback, 50,000 setup; emergency wall ceiling
1 second. Shared adaptive cadence is max(10 ms, twice the one-second mean active
phase cost), targeting 50% duty. This is cooperative, not preemptive. Native ordinary
expression functions retain their behavior outside Lua's instruction guard.

Time is two int32 values: seconds 0..2147483647, micros 0..999999. Do not flatten
absolute timestamps into float32. ctx.elapsed(endS,endUs,startS,startUs) returns a
normalized pair; ctx.elapsed_ms returns bounded integer milliseconds. ctx.crossing
interpolates ordered upward scalar crossings natively. GPS timing uses coherent
ctx.gps() E7 fixes and native geo_line/geo_crossing, not legacy coordinate display floats.
ctx.geo_line(aLatE7,aLonE7,bLatE7,bLonE7) creates a finite gate;
ctx.geo_crossing(line,fromLatE7,fromLonE7,fromS,fromUs,
toLatE7,toLonE7,toS,toUs,direction) returns crossing seconds/micros or nil.
Direction +1 crosses negative-to-positive side of ordered A/B; -1 reverses it.
ctx.gps() trailing utcDate/utcTimeCs are YYYYMMDD/centiseconds since midnight, or nil
when unavailable. ctx.session_id() and ctx.storage_id() return configured storage
session/current-identity keys. Menu {value:"last",format:"duration",name:"Last lap"}
shows a read-only output formatted m:ss.mmm; full history belongs in the web UI.

ctx.append(schemaId,row) and ctx.save() stage independent transactions. A nonzero
request means acceptance, not durability; on_storage reports serialized completion.
Handle busy/limits explicitly. Record schemas, state migration and retention belong
to the module. No automatic deletion or exactly-once guarantee. Browser records
are separate from SD and require a lifetime Web Lock for persistence; plain HTTP
AP previews use explicit page-local memory. Configuration files stay project-owned.

## Device HTTP (read status before each write; wait for the matching receipt)

GET /modules -> instances, storageSession, setup/configuration receipts.
GET /modules/config?id=ID -> schema, saved config, generation, editRevision, session.
POST /modules/config?id=ID&generation=N&revision=R&session=S with JSON patch
{settings:{...}, inputs:{...}, name:"...", runOnBoot:false, screen:false, popups:true}.
Maps are patches here; save preserves omitted keys. Settings/bindings need setup.
Response request token -> /modules.configuration.lastRequest/ok/error.
POST /modules?id=ID&generation=N&op=setup|enable|disable|action&action=NAME&session=S.
Setup receipt: /modules.setup.lastRequest/ok/error. Controls: instance.lastRequest/
requestOk. Generation 0 adds a saved instance. Do not mistake an old receipt for
completion. HTTP 202 is queue acceptance. A timeout can still complete afterward.

Install immutable package files under /modules/packages/<sha256>/, verify every
received byte, then POST /modules/install?id=ID&generation=N&revision=R&session=S
with {before:EXACT_REGISTRY_TEXT_OR_NULL,module:REGISTRY_RELATIVE_MANIFEST_PATH}.
Installation inputs/settings, if supplied, REPLACE their complete maps; omission
preserves saved choices and {} selects defaults. Never start implicitly.
Use ModulePackages.stageDevice in the shared local library for the established
hash/install flow. Do not invent a hash from manifest text alone: it pins full OGP.

POST /modules/records?id=ID&generation=N&session=S&op=files|record|file&key=KEY
queues a bounded read, optional after/offset cursor. GET /modules/records?
request=N&session=S retrieves the receipt/page. No automatic repair or writes.
POST /modules/courses?id=ID&generation=N&session=S: empty body lists installed
courses; course JSON installs an immutable revision. GET /modules/courses?
request=N&session=S retrieves completion. Selection/save/setup stay explicit.

## Course and licence notes

Course JSON kind:"course", api:1, id, revision, name, mode:"lap"|"stage", gates[]:
{id,a:{latE7,lonE7},b:{latE7,lonE7},direction:"negativeToPositive"|"positiveToNegative"}.
At most 16 finite ordered gates. Coordinates are degrees multiplied by 10,000,000.
Lap finish is first; stage start first/finish last. Validate real placement/direction.
OSM catalogue is only dated names/locations/outlines, licensed ODbL with attribution.
It is not a database of validated timing gates. First-party module code is MIT;
keep the code licence separate from map/course data provenance.
