feat: real fire on the boards, the LA office as a twin, and a night sky worth reading
The world stops being a simulation of California and starts being California. **THE PROMOTION GATE WAS THE FIRST COMMIT, BEFORE ANY ORANGE PIXEL EXISTED.** On today's live store the SoCal board contains 22 incidents. Every one has NULL acreage and fifteen are nameless LA County dispatch numbers. Drawn naively that is 22 orange marks over Los Angeles on a day nothing is burning — in a frame that contains no other warm colour, so one glyph would be the most salient object on the board and twenty-two would spend its credibility permanently. `acres >= 10 AND contained < 80 AND type != 'RX' AND last_seen = max(last_seen)` returns 0 on SoCal, exactly 5 on California, 0 on the Bay — same body, same day, three correct answers. The empty board is a deliverable, not a fallback: it says "No active fire on this board — CAL FIRE and WFIGS, just now", states that 21 records were gated and why, lists the largest fires burning OUTSIDE the frame with distances, and counts the hot pixels it is deliberately not drawing. **The privacy leak is structurally impossible rather than carefully avoided.** cloud-1 serves a projection; the four home-relative columns never leave that box. `observations.threat` was the one that nearly got through — it is `(16/distance)^2 x log10(acres) x momentum x containment x wind-alignment`, so with acreage and containment public it inverts to a distance circle around a house and three fires give an intersection. A grep of the built bundle for distance_km, bearing_deg, threat, 7762 and the street name returns nothing. **Deliberately not used, and both would have produced a confident wrong answer:** the store's `air` table retains only the last parameter of each poll, so all 78 rows read "Good" while the live feed reports ozone 101 "Unhealthy for Sensitive Groups" — haze driven off it would clear the sky during a smoke event. And `weather` is written only inside the NWS alerts loop, so a quiet day stores no wind at all. Tera's own per-region NWS wind is already correct and already what the clouds drift on. Satellite detections are drawn as evidence and never as incidents. The permanent industrial heat source 4.7 km from the owner's house is flagged persistent and dropped, asserted by a test that first proves it is present in the fixture. MODIS integer confidence and VIIRS string confidence are branched on `sat`. **The LA office is a twin.** Its entire authored second storey — Model Loft, Model Bay, The Materials Room, 430 lines nobody had ever stood in — is reachable on foot: a walker crosses level-1 to level-2 in 73 fixed steps, floorY 0 to 5, verified against the real pack rather than a synthetic plan. Its two studio devices read real hardware through a field-allowlisted bridge: mute, volume and reachability only. Never level, because there is no passive level upstream and obtaining one would record a room with people in it. Never dB, because upstream is gainPct across four different native scales. The bridge refuses all writes. Fixed at its root: an anonymous visitor was getting permanently at-rest instruments backing off against a 401. The tier moves into `createDeviceSource`, so anon gets the living simulator three file headers already promised. **Item 8 is closed, not fixed, and the correction is the point.** The Bay Area "stutter" was GPU power management — the card sat at 500 MHz of 2725 through every run that reproduced it, 4096/2048/1024/256 shadow maps all render in 1.21-1.31 ms, and two consecutive runs over a byte-identical dist gave 33.4 then 16.7. The allowance is removed and the cell is back to 16.7. Geometry is the gate; frame time is advisory. Item 7 was re-scoped after measuring: 1,069,006 of the Bay Area's 2,265,056 triangles were the second submission of the same buildings into the shadow pass. Mobile now has its own triangle caps and bay-area mobile draws 1,266,096. Also: bridges and the freeway corridor light up at night as emission, not lights — 1,614 deck lamps and 18 tower heads on the Bay in two draw calls. The single change that made US-101 legible was moving its edge lines from the lit material to the unlit one: retroreflective paint, the argument the SFO night frame already makes. California went 21,991 lamps to 4,051, clustered at the 17 town districts, because a rural interurban corridor genuinely is unlit. Tests 1137 -> 1340, server 280. All ten budget cells pass on first attempt with no cap raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* A read-only bridge to an operator's own hardware, over their own network.
|
||||
*
|
||||
* The first source in this build that reports something nobody in this process
|
||||
* invented. It is also the first that reads a machine in a room with people in
|
||||
* it, so the two most important things in this file are both refusals.
|
||||
*
|
||||
* ### It refuses to write
|
||||
*
|
||||
* There is no command path through here, and that is a product decision rather
|
||||
* than an unfinished one. Reading the state of a room is the demonstration; a
|
||||
* POST from a public web page that unmutes a microphone in an occupied room is a
|
||||
* different product and nobody has decided to build it. `commandRefusal()` says
|
||||
* so out loud and `devices/index.ts` reports it, because a refusal that is only
|
||||
* an absent function is a refusal somebody adds by accident.
|
||||
*
|
||||
* ### It refuses to invent a level, and it refuses to invent decibels
|
||||
*
|
||||
* Two separate refusals that both look like missing features.
|
||||
*
|
||||
* **There is no passive level upstream.** `GET /state` reports, per microphone,
|
||||
* `{ muted, gainPct, gainRaw, reachable, error, checkedAt }` and nothing else. A
|
||||
* level requires `POST /levels`, which records one and a half to three seconds
|
||||
* of audio per microphone to measure it. Mirroring `level` at a device TTL would
|
||||
* be a permanently open microphone in somebody's room, and it would look like a
|
||||
* feature the entire time it was doing it. So `levelDb` is never mapped, the
|
||||
* panel's meter row simply has no reading, and the row hides itself — which is
|
||||
* what `undefined` on `DeviceState` has always meant.
|
||||
*
|
||||
* **There is no honest decibel figure.** Upstream speaks `gainPct`, normalised
|
||||
* over four different native scales: a Blue Yeti Nano's ALSA range is 0–50, an
|
||||
* SMY18's and an Anker C200's are 0–100, a ThinkPad's internal is 0–63. "68%" is
|
||||
* a mixer position. Rendering it as "+20.6 dB" would present a guess in the
|
||||
* typography of a measurement, and it would look completely plausible. So the
|
||||
* gain reading is emitted **only** when the declaration itself supplies a
|
||||
* `ranges.gain` that is not in decibels — a pack that says "0–100 %" gets its
|
||||
* number, and a pack that says nothing gets no gain row at all. Fail-closed, in
|
||||
* the direction where the missing thing is visible.
|
||||
*
|
||||
* That leaves mute, volume and reachability as the fields this build actually
|
||||
* puts on a public wire, which is the smallest set that still makes the room
|
||||
* real.
|
||||
*
|
||||
* ### The mapper is an allowlist, never a spread
|
||||
*
|
||||
* `/state` carries far more than the readings: `positionNote` (which describes
|
||||
* where hardware sits relative to furniture), ALSA and PulseAudio device paths,
|
||||
* sink names, host labels, free-text `error` strings, a room `layout`
|
||||
* description, and a `recommendedNote`. A `...mic.state` anywhere in here is the
|
||||
* bug, and it is the kind of bug that ships. Every field is named, one at a time,
|
||||
* below.
|
||||
*
|
||||
* ### Endpoints that must never be read from here
|
||||
*
|
||||
* The same server exposes `GET /sleep` — which answers whether the owner is
|
||||
* asleep, with the camera activity and lux readings behind it — and
|
||||
* `GET /automations`, which returns log tails including a voice assistant's
|
||||
* transcribed speech and occupancy edges. They are one path segment away, they
|
||||
* are rich, and they would make a room feel astonishingly alive. Nothing here
|
||||
* reads them, nothing here should, and a public 3D world that renders whether
|
||||
* its owner is asleep is not a feature with a privacy setting.
|
||||
*
|
||||
* ### Its own cache, longer than the device TTL
|
||||
*
|
||||
* `/state` cold-probes three machines over SSH — measured at half a second, with
|
||||
* karti-os alone taking 498 ms — and holds its own ten-second cache. Tera's
|
||||
* device TTL is five seconds. Polling the bridge on that clock, per open tab,
|
||||
* would fan SSH out to three machines the operator actually uses, forever. So
|
||||
* this holds thirty seconds of its own (`TERA_STUDIO_TTL`), and a miss serves
|
||||
* the last good snapshot rather than re-probing.
|
||||
*
|
||||
* ### It demotes to `live: false`, never to `sim`
|
||||
*
|
||||
* A bridge that quietly started inventing readings when the room stopped
|
||||
* answering would be the `first-party-sensor`/`simulated` confusion
|
||||
* `DeviceProvenance` exists to prevent, in the one direction that matters. When
|
||||
* nothing answers, the devices this bridge covers report their last known state
|
||||
* with `reachable: false`, and a device it has never heard about reports nothing
|
||||
* at all.
|
||||
*/
|
||||
|
||||
import { getJson } from "../http.ts";
|
||||
import { createUpstream } from "../upstream.ts";
|
||||
import { deviceRange, hasCapability, type DeviceDeclaration } from "../../../src/devices/types.ts";
|
||||
import type { DeviceState } from "../../../src/devices/types.ts";
|
||||
import type { StudioConfig } from "../config.ts";
|
||||
|
||||
/** One reading, keyed by the id a pack declares. */
|
||||
export type FirstPartyReadings = ReadonlyMap<string, FirstPartyReading>;
|
||||
|
||||
/**
|
||||
* The allowlist, as a type.
|
||||
*
|
||||
* Everything this build is willing to learn about somebody's room. Growing it is
|
||||
* a privacy decision and the questions to answer first are the two the header
|
||||
* asks: does obtaining it record anybody, and is the number in a unit this
|
||||
* repo can name without guessing.
|
||||
*/
|
||||
export interface FirstPartyReading {
|
||||
/** The upstream id, so a mismatch is diagnosable. Never rendered. */
|
||||
id: string;
|
||||
muted?: boolean;
|
||||
/** 0–100, in the upstream's own percent. Emitted only under a declared range. */
|
||||
gainPct?: number;
|
||||
/** 0–1. `sinkVolumePct / 100`, which is exact rather than a conversion. */
|
||||
volume?: number;
|
||||
reachable: boolean;
|
||||
/** Epoch ms, from the upstream's `checkedAt`. */
|
||||
observedAt: number;
|
||||
}
|
||||
|
||||
export interface FirstPartySnapshot {
|
||||
readings: FirstPartyReadings;
|
||||
/** Epoch ms at which this box completed the fetch. */
|
||||
fetchedAt: number;
|
||||
}
|
||||
|
||||
export interface FirstPartyLog {
|
||||
warn(msg: string): void;
|
||||
}
|
||||
|
||||
export interface FirstPartySource {
|
||||
/** The freshest snapshot, or `null` if nothing has ever answered. */
|
||||
read(): Promise<FirstPartySnapshot | null>;
|
||||
/**
|
||||
* Turn one declaration plus one reading into a state.
|
||||
*
|
||||
* On this source, and not on `DeviceState`, because the mapping is where the
|
||||
* two refusals live: which fields are carried at all, and under what
|
||||
* conditions a gain figure may be named.
|
||||
*/
|
||||
stateFor(
|
||||
declaration: DeviceDeclaration,
|
||||
reading: FirstPartyReading | undefined,
|
||||
observedAt: number,
|
||||
): DeviceState;
|
||||
}
|
||||
|
||||
/** The whole of the write surface. Stated, not merely absent. */
|
||||
export function commandRefusal(): string {
|
||||
return (
|
||||
"this deployment reads its first-party hardware and does not command it — turning a " +
|
||||
"microphone on in an occupied room from a public page is a decision nobody has made"
|
||||
);
|
||||
}
|
||||
|
||||
/** How long the bridge fetch may take. Three SSH probes cold. */
|
||||
const TIMEOUT_MS = 6_000;
|
||||
|
||||
/** Bounds on the operator's dial, so neither end can become a probe storm. */
|
||||
const MIN_TTL_SECONDS = 15;
|
||||
const MAX_TTL_SECONDS = 600;
|
||||
|
||||
/** One key: the bridge answers about one studio. Same argument as satellites. */
|
||||
const STUDIO_KEY = "studio";
|
||||
|
||||
/** A ceiling on how many devices one bridge may describe. `store.ts`'s number. */
|
||||
const MAX_READINGS = 64;
|
||||
|
||||
export function createFirstPartySource(
|
||||
config: StudioConfig,
|
||||
log: FirstPartyLog,
|
||||
): FirstPartySource {
|
||||
const ttl = Math.min(MAX_TTL_SECONDS, Math.max(MIN_TTL_SECONDS, config.ttlSeconds));
|
||||
const upstream = createUpstream<FirstPartySnapshot>({
|
||||
label: "devices:first-party",
|
||||
ttlSeconds: ttl,
|
||||
log,
|
||||
});
|
||||
|
||||
return {
|
||||
async read(): Promise<FirstPartySnapshot | null> {
|
||||
if (config.url === "") return null;
|
||||
return upstream.get(STUDIO_KEY, () => fetchState(config));
|
||||
},
|
||||
stateFor: mapState,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One GET, mapped field by named field.
|
||||
*
|
||||
* `null` on every failure, which `upstream.ts` turns into "serve the last good
|
||||
* snapshot" — and, once the last good snapshot has aged, into a set of readings
|
||||
* that still carry `reachable: false`.
|
||||
*/
|
||||
async function fetchState(config: StudioConfig): Promise<FirstPartySnapshot | null> {
|
||||
const headers = config.key === "" ? undefined : { "x-studio-key": config.key };
|
||||
const body = await getJson<Record<string, unknown>>(`${config.url}/state`, {
|
||||
timeoutMs: TIMEOUT_MS,
|
||||
...(headers === undefined ? {} : { headers }),
|
||||
});
|
||||
if (body === null) return null;
|
||||
|
||||
const state = record(body.state);
|
||||
if (state === null) return null;
|
||||
|
||||
const readings = new Map<string, FirstPartyReading>();
|
||||
const now = Date.now();
|
||||
|
||||
// --- microphones -------------------------------------------------------
|
||||
//
|
||||
// `info` is read for exactly one field — the id — and `state` for four. Not
|
||||
// `positionNote`, which describes where hardware sits relative to furniture;
|
||||
// not `device`, which is an ALSA or PulseAudio path; not `host` or
|
||||
// `hostLabel`; not `recommendedNote`; and not `error`, which is free text
|
||||
// from somebody else's shell.
|
||||
for (const entry of array(state.mics)) {
|
||||
if (readings.size >= MAX_READINGS) break;
|
||||
const mic = record(entry);
|
||||
if (mic === null) continue;
|
||||
const info = record(mic.info);
|
||||
const reading = record(mic.state);
|
||||
if (reading === null) continue;
|
||||
const id = str(info?.id) ?? str(reading.id);
|
||||
if (id === null) continue;
|
||||
|
||||
readings.set(id, {
|
||||
id,
|
||||
...(typeof reading.muted === "boolean" ? { muted: reading.muted } : {}),
|
||||
...(finite(reading.gainPct) === null ? {} : { gainPct: finite(reading.gainPct) as number }),
|
||||
reachable: reading.reachable === true,
|
||||
observedAt: epoch(reading.checkedAt) ?? now,
|
||||
});
|
||||
}
|
||||
|
||||
// --- the speaker -------------------------------------------------------
|
||||
//
|
||||
// Upstream has one speaker object with a chosen sink, not a list. It is
|
||||
// published under two ids — the literal `speaker` and the active sink's own id
|
||||
// — so a pack may declare whichever reads better in its own floorplan without
|
||||
// this file having to know which one it chose. `sinks[]`, `pulseName`,
|
||||
// `alsaMaster`, `paplayVolume` and `ladder` are all left where they are:
|
||||
// they are the operator's audio plumbing, not a reading about a room.
|
||||
const speaker = record(state.speaker);
|
||||
if (speaker !== null) {
|
||||
const volumePct = finite(speaker.sinkVolumePct);
|
||||
const reading: FirstPartyReading = {
|
||||
id: str(speaker.sink) ?? "speaker",
|
||||
...(typeof speaker.muted === "boolean" ? { muted: speaker.muted } : {}),
|
||||
// Percent to fraction is exact arithmetic in the same unit, which is why
|
||||
// it is allowed here and a percent-to-decibel conversion is not.
|
||||
...(volumePct === null ? {} : { volume: Math.min(1, Math.max(0, volumePct / 100)) }),
|
||||
reachable: speaker.reachable === true,
|
||||
observedAt: epoch(speaker.checkedAt) ?? now,
|
||||
};
|
||||
readings.set("speaker", reading);
|
||||
const sinkId = str(speaker.sink);
|
||||
if (sinkId !== null && readings.size < MAX_READINGS) readings.set(sinkId, reading);
|
||||
}
|
||||
|
||||
return { readings, fetchedAt: now };
|
||||
}
|
||||
|
||||
/**
|
||||
* One declaration plus one reading, as a `DeviceState`.
|
||||
*
|
||||
* Exported through the source rather than as a free function so that the two
|
||||
* refusals stay attached to the thing that makes them: `levelDb` is never set,
|
||||
* and `gainDb` is set only under a declared non-decibel range.
|
||||
*/
|
||||
function mapState(
|
||||
declaration: DeviceDeclaration,
|
||||
reading: FirstPartyReading | undefined,
|
||||
observedAt: number,
|
||||
): DeviceState {
|
||||
const state: DeviceState = {
|
||||
id: declaration.id,
|
||||
kind: declaration.kind,
|
||||
// A device the bridge could reach is a device that is on. There is no
|
||||
// separate power reading upstream — a microphone the host can enumerate is
|
||||
// powered, and one on a machine that will not answer is unreachable rather
|
||||
// than off. The two are different sentences and `reachable` carries the
|
||||
// second one.
|
||||
powered: reading?.reachable === true,
|
||||
reachable: reading?.reachable ?? false,
|
||||
observedAt: reading?.observedAt ?? observedAt,
|
||||
// The whole reason this source exists. Nothing here was invented.
|
||||
synthetic: false,
|
||||
};
|
||||
|
||||
if (hasCapability(declaration, "mute") && reading?.muted !== undefined) {
|
||||
state.muted = reading.muted;
|
||||
}
|
||||
if (hasCapability(declaration, "volume") && reading?.volume !== undefined) {
|
||||
state.volume = reading.volume;
|
||||
}
|
||||
if (hasCapability(declaration, "gain") && reading?.gainPct !== undefined) {
|
||||
// The refusal, in one condition. Upstream's number is a percent of a mixer
|
||||
// travel; the global default range is decibels. Emitting it under the
|
||||
// default would mean printing "+68 dB" beside a microphone, which is both
|
||||
// wrong and plausible. A declaration that states its own non-decibel range
|
||||
// is a pack author saying what the number means, and only then is it named.
|
||||
const range = deviceRange(declaration, "gain");
|
||||
if (range.unit !== DEVICE_DEFAULT_GAIN_UNIT) {
|
||||
state.gainDb = Math.min(range.max, Math.max(range.min, reading.gainPct));
|
||||
}
|
||||
}
|
||||
// `levelDb` is never set. There is no passive level upstream and obtaining one
|
||||
// records the room. The panel's meter row hides itself on `undefined`, which
|
||||
// is what `undefined` has always meant here: this device has no such reading.
|
||||
return state;
|
||||
}
|
||||
|
||||
/** The unit the global default gain range is in. Anything else is the pack's own. */
|
||||
const DEVICE_DEFAULT_GAIN_UNIT = "dB";
|
||||
|
||||
// ---- Reading somebody else's JSON -----------------------------------------
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function array(value: unknown): unknown[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function str(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed === "" ? null : trimmed;
|
||||
}
|
||||
|
||||
function finite(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
/** An ISO timestamp as epoch ms, or `null` where it will not parse. */
|
||||
function epoch(value: unknown): number | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const ms = Date.parse(value);
|
||||
return Number.isFinite(ms) ? ms : null;
|
||||
}
|
||||
@@ -21,6 +21,11 @@
|
||||
* 3. **The declarations come from the pack, never from the caller.**
|
||||
* `devices/store.ts` resolves them against a `Plan` this process built, which
|
||||
* is what makes a command checkable at all.
|
||||
* 4. **A source that reads real hardware never falls back to inventing it.**
|
||||
* `first-party` demotes to `live: false` readings with `reachable: false`,
|
||||
* never to `sim`. Substituting a state machine for a bridge is the one
|
||||
* confusion `DeviceProvenance` exists to prevent, and it would be doing it in
|
||||
* the direction that matters.
|
||||
*
|
||||
* ### Commands are memory-only and bounded
|
||||
*
|
||||
@@ -32,6 +37,11 @@
|
||||
*/
|
||||
|
||||
import { resolveDevices } from "./store.ts";
|
||||
import {
|
||||
commandRefusal,
|
||||
createFirstPartySource,
|
||||
type FirstPartySource,
|
||||
} from "./firstParty.ts";
|
||||
import { createDeviceRuntime, type DeviceRuntime } from "./sim.ts";
|
||||
import {
|
||||
normalizeDeviceCommand,
|
||||
@@ -46,8 +56,11 @@ export interface DevicesService {
|
||||
/**
|
||||
* Every device in one office, right now. Never throws; an office with no
|
||||
* declarations, or a box with no source, is an empty list and a 200.
|
||||
*
|
||||
* A promise because one source is a network call. The `none` and `sim` paths
|
||||
* resolve without awaiting anything, so nothing about their cadence changed.
|
||||
*/
|
||||
current(office: Office): DevicesBody;
|
||||
current(office: Office): Promise<DevicesBody>;
|
||||
/**
|
||||
* Apply one command.
|
||||
*
|
||||
@@ -82,6 +95,15 @@ const NO_ATTRIBUTION: string[] = [];
|
||||
|
||||
export function createDevicesService(config: Config, log: DevicesLog): DevicesService {
|
||||
const { source, ttlSeconds, seed } = config.devices;
|
||||
/**
|
||||
* Built only for the source that uses it, unlike the simulator runtime below.
|
||||
*
|
||||
* The runtime is built unconditionally because it costs an empty `Map`. This
|
||||
* one holds a URL and a cache in front of somebody's house, and a box that
|
||||
* never asked for it should not have constructed one.
|
||||
*/
|
||||
const bridge: FirstPartySource | null =
|
||||
source === "first-party" ? createFirstPartySource(config.studio, log) : null;
|
||||
// Built even for `none`, because it costs one empty `Map` and it means the
|
||||
// two branches below differ by a single condition rather than by a structure.
|
||||
const runtime: DeviceRuntime = createDeviceRuntime({ seed });
|
||||
@@ -119,22 +141,41 @@ export function createDevicesService(config: Config, log: DevicesLog): DevicesSe
|
||||
devices,
|
||||
observedAt,
|
||||
source,
|
||||
// Never `false` in this build. The only implemented source is a state
|
||||
// machine, and a body that claimed observation would be a lie told by a
|
||||
// constructor — the same sentence `initialDeviceState` carries.
|
||||
synthetic: true,
|
||||
// `false` on exactly one source, and it is derived rather than asserted: a
|
||||
// body is observed only if every reading in it is. An empty list on the
|
||||
// bridge is still `false` — a real bridge with nothing plugged into it is a
|
||||
// different picture from a box making it all up, which is the distinction
|
||||
// this field carries separately from the per-device one.
|
||||
synthetic: source !== "first-party",
|
||||
ttlSeconds,
|
||||
...(NO_ATTRIBUTION.length > 0 ? { attribution: NO_ATTRIBUTION } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
current(office: Office): DevicesBody {
|
||||
async current(office: Office): Promise<DevicesBody> {
|
||||
const now = Date.now();
|
||||
if (source === "none") return body(office, [], now);
|
||||
const resolved = resolveDevices(office);
|
||||
report(office, resolved);
|
||||
const { declarations } = resolved;
|
||||
if (declarations.length === 0) return body(office, [], now);
|
||||
|
||||
if (bridge !== null) {
|
||||
// A snapshot the bridge has never obtained is `null`, and every device
|
||||
// then reports `reachable: false` with its readings absent. That is the
|
||||
// honest picture — the instruments are declared, nobody has answered —
|
||||
// and it is emphatically not an empty list, which would say this office
|
||||
// declares no hardware.
|
||||
const snapshot = await bridge.read();
|
||||
return body(
|
||||
office,
|
||||
declarations.map((declaration) =>
|
||||
bridge.stateFor(declaration, snapshot?.readings.get(declaration.id), now),
|
||||
),
|
||||
now,
|
||||
);
|
||||
}
|
||||
|
||||
const simulator = runtime.advance(office.id, declarations, now);
|
||||
// Restamped with the request's clock: the simulator's own `observedAt` is
|
||||
// its epoch plus its simulated elapsed time, which lags by up to a step
|
||||
@@ -149,6 +190,11 @@ export function createDevicesService(config: Config, log: DevicesLog): DevicesSe
|
||||
|
||||
command(office: Office, command: DeviceCommand): DeviceCommandOutcome {
|
||||
if (source === "none") return { ok: false, reason: "this deployment has no device source" };
|
||||
// The whole write surface of the first-party bridge, and it is a refusal.
|
||||
// Stated here rather than left implicit in a missing branch, because a
|
||||
// refusal that is only an absence is a refusal somebody removes by
|
||||
// accident. See `devices/firstParty.ts`.
|
||||
if (bridge !== null) return { ok: false, reason: commandRefusal() };
|
||||
const { declarations } = resolveDevices(office);
|
||||
const declaration = declarations.find((d) => d.id === command.deviceId);
|
||||
// The whole of the authorisation for a write, in two lines. The device
|
||||
|
||||
Reference in New Issue
Block a user