Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/grid-phase-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw-webapp": patch
---

Name the grid's three phase currents and the main fuse in the shared contract, as field ids 11 to 14 after the frozen set. The box sends them for a node's own view; this app does not draw them yet.
23 changes: 23 additions & 0 deletions contract/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ frozen_fields:
# by the site convention: the charger consumes like any other load.
10: { name: ev_w, kind: power, unit: W }

# ---------------------------------------------------------------------------
# Field ids after the frozen set. They travel like the frozen ids, in lane
# 0's delta and in FTW Cloud's live body, under the same rules: integers,
# sent only while the box has the reading, and an id never changes meaning.
# They are depth for a node's own view, not the core view, so an app draws
# Now without them and a box may stop sending one. Lane 0 drops the highest
# id first when a delta does not fit its bucket, so these give way first.
#
# The grid's phase currents come from the site meter in tenths of an ampere,
# signed by the site convention: positive while the phase draws from the
# grid. A meter that gives magnitudes and no power per phase sends positive
# values both ways, so a positive current alone does not prove import. The
# box sends only the phases the site has, each while the meter is online and
# reports it, and their freshness is the grid meter's. grid_fuse_da is the
# main fuse per phase from the box's config, sent with the phases as their
# scale.
# ---------------------------------------------------------------------------
extra_fields:
11: { name: grid_l1_da, kind: current, unit: dA }
12: { name: grid_l2_da, kind: current, unit: dA }
13: { name: grid_l3_da, kind: current, unit: dA }
14: { name: grid_fuse_da, kind: current, unit: dA }

# ---------------------------------------------------------------------------
# Capabilities. A set of names, not version numbers. Absent means the box does
# not have it; the UI hides the feature and never crashes.
Expand Down
6 changes: 6 additions & 0 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ battery power, battery state of charge, load power, and their source ids. Any
app, however old, can draw the core view. Freezing them now is free; doing it
later is not.

Ids after the frozen set live in the registry's `extra_fields`: depth for a
node's own view, such as the grid's phase currents and the main fuse. They
travel in the same `delta` and follow the same rules, but an app draws the
core view without them and a box may stop sending one. A delta that does not
fit drops the highest id first, so they give way before the core view.

## Freshness

Every reading points at a **source**, not at its own timestamp:
Expand Down
30 changes: 30 additions & 0 deletions tests/registry-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,36 @@ describe('frozen field ids', () => {
const table = Object.entries(FID).map(([name, id]) => `${id}: ${name.toLowerCase()}`)
expect(table.sort()).toEqual(declared.sort())
})

/**
* extra_fields share the frozen set's id space on the wire: a delta and
* FTW Cloud's live body key both by id alone. An id or a name in both
* blocks would be two meanings for one number. They also sit above the
* frozen set, because lane 0 drops the highest id first when a delta does
* not fit, and the core view must be the last thing to go.
*/
it('leave the extra fields above the frozen set, with ids and names of their own', () => {
const entries = (name: string) =>
block(name).map((line) => {
const m = /^ {2}(\d+): \{ name: ([a-z0-9_]+),/.exec(line)
expect(m, `unreadable ${name} line: ${line}`).not.toBeNull()
return { id: Number(m![1]), name: m![2]! }
})
const frozen = entries('frozen_fields')
const extra = entries('extra_fields')
expect(extra.length, 'no extra fields were found to check').toBeGreaterThan(0)

const highestFrozen = Math.max(...frozen.map((f) => f.id))
const ids = new Set(frozen.map((f) => f.id))
const names = new Set(frozen.map((f) => f.name))
for (const f of extra) {
expect(f.id, `${f.name} sits among the frozen ids`).toBeGreaterThan(highestFrozen)
expect(ids.has(f.id), `field id ${f.id} is declared twice`).toBe(false)
expect(names.has(f.name), `field name ${f.name} is declared twice`).toBe(false)
ids.add(f.id)
names.add(f.name)
}
})
})

describe('capabilities the app gates on', () => {
Expand Down
Loading