SGA‑OISOpen Interoperability Standard
1.0 Draft
SGA-OIS/Specification

Device lifecycle: admission, updates, enrollment

Normative.

Three problems a cheap accessory ecosystem creates, and none of them is the protocol. A device has to be kept current, refused when it is dangerous, and attachable to a bay by an operator without a player present.


1. The engine updates between rounds, never during one#

The bundle refreshes on the schedule in 04 §2, but a swap only takes effect at a round boundary.

Changing the field catalog mid-round would change validation semantics partway through, and the round's bundle_version — which is what makes it re-examinable years later (04 §6) — would no longer name one thing. A bundle downloaded during play is staged and applied when the round ends.

A bay that is never idle still gets there: if no round boundary occurs within seven days, the swap applies at the next session start instead. The point is never to swap during a round, not to wait for a quiet moment that a busy facility never has.

The engine binary is separate and never self-updates silently (04 §4). Data updates itself; code does not.


2. Refusing a device that is dangerous to everyone else#

A device can be denied admission by a rule in the bundle, matching on vendor, model and firmware range.

JSON
{ "vendor": "acme", "model": "AX-3", "firmware_below": "4.2.0",
  "reason": "shot injection via unauthenticated debug port",
  "advisory": "SGA-2027-004", "since": "2027-03-02" }

2.0 What a rule may match#

dimensionfieldsdenies
vendorvendor (required)every device from that vendor
modelmodelevery unit of that model
firmwarefirmware_below, firmware_inaffected builds
unitserials, serial_prefix, serial_range, device_idsa production batch, or a named unit

Constraint kinds AND together; constraints within a kind OR. So a rule carrying both a serial prefix and a firmware range means this batch, running something older than the fix — which is how a fault that is real only on certain hardware gets expressed without condemning either dimension whole.

A rule with only a vendor denies that vendor entirely. That is a large hammer and it is meant to be visible as one: there is no way to write it by accident, because vendor is the one required field and the resulting refusal says vendor_denied rather than something narrower.

Firmware may be matched as a range, an exact list, or both.

matchesremedy offered
firmware_beloweverything older than the fixupdate forward
firmware_inonly the builds namedmove to any other supported build
botheither — "everything before 4.2.0, and the botched 4.3.1"as applicable

A range is right for a flaw introduced at some point and fixed at a known one. An exact list is right when the fault is one bad release, with sound builds either side: a regression that shipped in 4.2.0, was found in a week, and was gone by 4.2.2. Expressed as firmware_below: "4.2.2" that rule would condemn every build back to the first — versions that were never affected, on units whose owners did nothing wrong.

Two consequences follow from an exact list, and both are deliberate:

  • The remedy is not "update". Off a range you move forward. Off a list you move to anything else, which may legitimately mean rolling back, and the refusal message says so rather than pointing at a version that does not exist yet.
  • A device that will not state its firmware escapes a list, though never a range. Silence counts as older than any firmware_below, so staying quiet is not a way out of one. But a list names specific builds, and silence is not a claim to be one of them. Reading it as a match would refuse every quiet unit of that model over a release most of them may not be running. So: where a fault has a known floor, say so with firmware_below as well. A list alone is the narrower instrument, and narrower cuts both ways.

A known vulnerability is the only thing that refuses a device. Certification does not: a bay certified to a high tier prefers the best-attested source for the fields its certification rests on and admits everything else (14 §3.2). The justification here is different in kind — not what a device can prove about itself, but what it exposes everyone else to.

The case that justifies this is not a device that misbehaves on its own. It is one with a flaw somebody else can use: tampering with another player's data mid-match, or improving their own. A vulnerable unit is a hazard to everyone in the bay, which is what makes refusing it proportionate rather than heavy-handed.

It also answers the revocation question 02 §7 left open. A revocation reaches an offline bay the way everything else does — in the bundle, through whichever vendor's installer touches that machine next. Including, pleasingly, the offending vendor's own.

2.1 The refusal is coded, not prose#

A device is told what happened in the same registered vocabulary we require of everything it sends us. A sentence with a machine-readable prefix would be the standard asking vendors for coded values and answering in English.

JSON
{ "type": "session.denied",
  "code": "firmware_below_minimum",
  "remedy": "update_firmware",
  "required_firmware": "4.2.0",
  "advisory": "SGA-2027-004",
  "retry_after_ms": 300000,
  "detail": "shot injection via unauthenticated debug port" }

Schema: denial.schema.json.

code and remedy are separate fields on purpose. One says what was refused, the other says what would fix it, and they do not move together: the same code carries a different remedy as a rule's shape changes. A device should branch on remedy.

coderemedycarries
firmware_below_minimumupdate_firmwarerequired_firmware
firmware_build_deniedchange_firmwaredenied_builds
unit_deniedcontact_vendor—
serial_requiredreport_serial—
model_denied, vendor_deniedcontact_vendor—

detail carries the human sentence for a log or an operator screen, and MUST NOT be parsed. Everything a device acts on is in the coded fields.

Three things make this actually self-remediating rather than merely well-typed:

  • A denied device may always use the update channel in §3. Refusing admission without leaving a route to remediation would strand it at the bay, which helps nobody — and since §3 constrains the destination by address rather than by a list, there is nothing a device needs to be told in advance about where it may fetch from.
  • retry_after_ms is scaled to whether the device can act at all. Minutes where it can fix itself; a day where a person has to be involved. A device that cannot distinguish the two cases retries at the same rate for both.
  • The refusal is sent before the connection closes. A silent disconnect is indistinguishable from a crash, and a device that gets one will retry forever having learned nothing.

2.2 A unit that will not fix itself becomes the operator's problem#

Three refused attempts from the same device is no longer a retry. It is a unit that cannot reach its update server, or a vendor whose self-update is broken, and either way a person has to look.

At that point the engine raises an operator notice at interrupt urgency naming the device, the firmware, the required version and the advisory. A player cannot fix a vendor's firmware, so it is not addressed to them.

2.3 And a fixed build clears itself#

Same property as the notification blocklist: the rule is a range, so a device that updates to 4.2.0 is admitted the moment it does. No republished bundle, no negotiation, nobody's attention. Enforcement that needs our attention to lift is enforcement we will be slow to lift.

2.4 A bad batch is not a bad build#

Firmware matching cannot express the case where the hardware is the problem: a fault in a production run, or units shipped with a signing key that later leaked. No update fixes either, and denying the whole model punishes everyone who bought a good one.

So a rule may name units instead:

JSON
{ "vendor": "acme", "model": "AX-3",
  "serial_prefix": ["AX3-2026W14-"],
  "reason": "signing key exposed in week-14 production run" }
fieldfor
serial_prefixa batch by lot code — how production runs are usually numbered
serial_rangea contiguous batch, from/to
serialsexact units
device_idsa named unit known to be compromised

A serial range is compared lexicographically, never numerically. Serial grammars are the vendor's own and parsing one would be guessing; lexicographic comparison is right for the zero-padded, fixed-width serials that batches actually use. Both ends MUST be the same length, and an engine MUST reject a bundle whose range ends differ in length rather than discover the problem from a device that should have been refused and was not. A signed artifact goes to every bay in the world; a malformed rule in one is not a thing to find out about later.

The honest limit#

A unit denial is only as strong as the attestation behind it. A device at A0 or A1 can claim whatever serial it likes (02 §3), so at those tiers this is an inventory control — it keeps known-bad hardware out of bays where the hardware is cooperating — and a security control only at A2 and above, where the serial is inside something signed. Treating it as the latter everywhere would be self-deception of exactly the kind 02 §8.4 warns about.

The quiet-unit dodge, and the narrow answer to it#

A device that does not report a serial escapes a batch rule, for the same reason it escapes firmware_in: silence is not a claim to be one of the named units, and refusing every anonymous unit of a model would punish the many for the few.

Where that matters — a batch denial for a real hardware fault — a rule may set require_serial, which denies units inside its vendor and model scope that will not identify themselves. The refusal then says serial_required with remedy report_serial, not unit_denied: it may well be a good unit, and the message should say what would settle the question rather than accuse it.

Two guards keep this from spreading:

  • It is per-rule, never global. It applies inside one vendor-and-model scope, so it never becomes a standing demand for serials everywhere — which would cut against the identity posture the rest of the standard keeps.
  • An engine MUST reject require_serial on a rule with no unit constraint. Such a rule denies every quiet unit of a vendor: a model-wide denial written by accident.

3. Firmware updates through the engine#

Two kinds of device cannot patch themselves. A BLE-only accessory with no screen has no realistic way onto the internet — facility wifi means credentials it cannot be typed into and captive portals it cannot render. And a LAN device with no outbound access, which is ordinary in a commercial install with a locked-down network. Both end up as a generation of hardware that never gets patched.

So the engine fetches on a device's behalf:

device --update.fetch(url)--> engine --https--> vendor's update host
       <-- update.begin
       <-- update.data (seq 0..n)
       <-- update.end(bytes, sha256)

       ... or update.refused(reason, retry_after_ms), and nothing else

Schema: update.schema.json.

Chunks are base64 in ordinary JSON messages, at a 33% cost on the wire. That is a deliberate trade: every device already has a JSON parser for the rest of the protocol, and streaming base64 into flash is a few lines on an MCU. A binary side-channel would save bandwidth on a transfer that is not bandwidth-bound, in exchange for every vendor implementing a second framing mode correctly. chunk_bytes lets a device with little RAM ask for smaller pieces.

Four rules about the exchange itself:

  • update.begin is sent only once the first bytes are in hand. A device should not erase flash for a transfer that is refused a moment later.
  • A vendor's error is passed through, not swallowed. A 404 means the vendor moved the file and this device's updater has a stale URL — the device's problem to solve, and it can only solve it if it is told.
  • update.end carries a SHA-256 of what was relayed. A check on the transfer, not a warrant on the image — see §3.2, which this must not be read against.
  • A device that was refused admission stays connected for this. Hanging up on a device denied for vulnerable firmware would refuse admission and remediation in the same move.

The engine's own connection is tried first, and a paired member's phone only if the bay cannot reach the internet (07 §1). The phone is a fallback, never the default: it is somebody's data plan.

This makes a pairing to an SGA member sufficient for a device to stay current for its lifetime, which is what makes a thirty-dollar accessory maintainable rather than abandoned.

3.1 Refusing the facility's own network#

A device that can name a URL can otherwise reach things it could never reach itself — the facility's router, its admin pages, the other bays, the cloud metadata endpoint. The engine sits inside that network, so proxying for a device hands it the engine's network position. That is request forgery, and it would be ours.

An SGA-maintained allowlist of vendor update hosts looks like the answer. It is the wrong instrument. Hundreds of partners, CDN endpoints that move, regional mirrors and vendors changing domains make such a list permanently stale — and a stale allowlist breaks legitimate updates until somebody turns it off, at which point it protects nothing. Enumerating good does not scale here.

The rule is instead to exclude bad by address, because the dangerous space is small, fixed, and defined by RFCs:

  • HTTPS only, with a valid certificate. A router's admin page does not have one for the name being requested, which alone closes most of this.
  • No connection to a private or reserved address. 10/8, 172.16/12, 192.168/16, 127/8, 169.254/16 — which is also the cloud metadata endpoint — 100.64/10, ::1, fc00::/7, fe80::/10, multicast, and the IANA reserved ranges. IPv4-mapped IPv6 forms are normalized first, since ::ffff:192.168.1.1 is the obvious way around a naive check.
  • Checked at connect time, against the address actually being dialed. Validating a resolved address and then connecting separately is defeated by DNS rebinding: the name answers public for the check and private for the connection a moment later.
  • Redirects re-run every check, and a redirect to plain HTTP is a refusal.

This needs no maintenance, ever. No vendor's updates break when they change CDN, and nothing has to be added when a partner joins.

3.1a Limits#

Volume, not files.

limitdefaultwhy
one image15 MBgenerous for the class: an nRF52 image is 100–500 KB, an ESP32 OTA partition 1–2 MB. Raisable per vendor
per device / 24h60 MBrolling window
per bay / 24h180 MB — hardper-device limits multiply; ten devices at 60 MB is 600 MB off a facility's connection
between requests10 s

Bytes rather than a file count. A device off a shelf several versions behind may legitimately need a sequence of images plus a bootloader and a softdevice; a file count fails it for doing exactly the right thing, while letting a different device pull five 15 MB files when it needed five 200 KB ones. Bytes are the resource.

But a byte budget alone does not stop ten thousand 1 KB requests, or a device polling an endpoint that answers 304 and costs no budget at all while costing a TLS handshake every time — on a machine that is also running a golf simulator and must not stutter during a shot. The minimum interval covers that, and is invisible to a real updater, which has to flash each image before asking for the next.

Three properties that decide whether the budget holds:

  • Bytes transferred are charged, not bytes delivered. A download that dies at 99% still spends what it moved. Otherwise a device aborts near the end forever and consumes unlimited bandwidth while never completing anything.
  • The window rolls; it does not reset at midnight. A reset hands a device a fresh allowance at 00:00 and another at 00:01 on the far side of the boundary, which is how a daily quota turns out to be twice what it says.
  • The allowance shrinks to what is actually left. A device with 3 MB of budget is offered 3 MB, not 15, so it does not start a transfer it cannot finish.

Refusals are coded — scheme_not_https, private_address_refused, file_too_large, device_budget_exhausted, bay_budget_exhausted, too_soon — with a retry_after_ms computed from when the oldest spend actually ages out rather than guessed. And spend is journalled: a device burning its whole budget every day is either broken or doing something it should not, and that is only visible if somebody records it.

Bulk path only (09 §3). A firmware download must never share a connection with a shot.

3.1b The phone carries the same rules#

Where the fetch rides a member's phone, the app applies everything in §3.1 and §3.1a itself.

Saying so matters because the offline path would otherwise silently drop the control the online path calls mandatory: if the phone makes the request, the engine never does, and nothing enforces the address rules unless the app does. The phone is also on a network — often the facility's guest wifi — so "no private addresses" means exactly as much there.

Carrying firmware is a separate opt-in from carrying the journal. The two look alike and are not: the journal is SGA's own signed record traveling outward, while this is a third party's binary traveling inward into facility equipment, on the member's data. Wifi by default, never cellular unless the member says so, and never during a round.

3.2 The engine is a pipe, not a warrant#

The engine does not verify a firmware image and must not appear to. It cannot: the format and the signing are the vendor's own.

The vendor signs the image; the device verifies it before flashing. The engine moves bytes between two parties who already trust each other, and the trust does not route through SGA at any point.

The sha256 in update.end is not an exception to this. It covers the bytes the engine relayed and answers exactly one question — did the transfer arrive intact. It says nothing about whether the image is genuine, and a device that treated it as a substitute for checking the vendor's signature would be trusting SGA for something SGA never checked. A device that flashes an unverified image was going to do that over wifi too.

Saying so plainly matters. If relaying an image were ever read as endorsing it, SGA would have taken on liability for every vendor's firmware in exchange for being helpful about bandwidth.


4. Bay-attached devices#

A ceiling camera, an environment sensor, a fixed club rack are bay infrastructure. They should be present whether or not anybody has checked in, and an operator should not have to re-pair them every session.

So a bay keeps a roster of enrolled devices, managed in the configuration application (05 §4a) — which is password-locked, so only an operator can change it.

addpair a device once and it joins every session from then on
name"Ceiling camera", "Left rack" — the operator's words, not a device id
removeit stops joining, and its standing assertions go with it
statusconnected, last seen, firmware, whether policy denies it

An enrolled device presents no bay token. Its enrollment is the binding, recorded by an authenticated operator at the machine, which is a stronger claim than a placard scan rather than a weaker one.

Enrollment is journaled and appears in the bay profile (10), so certification sees the room's permanent equipment as the equipment inventory it is.

A player-owned device is never enrolled. It travels, it leaves when its owner does, and it is admitted by its owner's check-in rather than by an operator — see §4.1. The roster is for things bolted to the room.

4.0 At a facility, enrollment also answers "which bay am I?"#

A single bay does not need to ask. Its engine is the only one a device can see, and an enrolled device connecting to it is at the bay it is enrolled in by construction.

A facility with two hundred bays is different, and the difference is not one of degree. The device sees two hundred engines, it cannot scan a placard to prove which room it is standing in, and its enrollment is the only thing that knows. So the roster is replicated to every engine at the site, and a device that connects to the wrong one is refused and told where it belongs — 03 §2.0f has the frame.

Three consequences an operator sees:

  • A device moved between bays in the configuration application moves for real, without touching the device. It is refused at its old bay on its next connection and redirected to its new one.
  • An unenrolled device at a facility does not join anything. It waits, and it keeps advertising itself, which is what makes a sensor with one LED show up in the configuration application as something to enroll rather than as nothing at all.
  • A device enrolled at two bays at once joins neither until somebody decides. That happens when two operators enroll the same unit while the engines cannot reach each other, and the standard refuses to resolve it: a device that silently picked one bay would file a round's footage against the wrong room, and nobody would know to look.

4.1 A member's own device, admitted by their check-in#

A member arrives with equipment: a BLE bag sensor that knows which club left the bag, a kinematic suit, a putting sensor, a wearable. It is theirs, it travels with them, and they expect it to work at whatever bay they walk into without anybody fetching an operator.

The obstacle is that none of these things can scan a placard. 03 §5 makes physical presence the pairing authority — a device joining a bay presents a bay token obtained from the bay itself, which is what stops a device joining the bay next door. A bag sensor has no screen, no camera, and frequently no UI at all. It cannot obtain a token and it cannot be told which room it is in.

It does not have to. Two facts already exist, and together they are stronger than a placard scan: the member's account says the device is theirs, and the member's check-in says the member is here.

Registered devices, admitted on sight#

A member registers equipment to their account, once, in the app. From then on:

  1. The member checks in at a bay (03 §3.4a). That is the physical-presence proof, and it is a person scanning a placard, which is what the placard rule was always reaching for.
  2. The check-in response carries the member's registered equipment for this session.
  3. The engine admits such a device when it observes it. No pairing gesture, no operator, no per-session consent.
  • An engine MUST admit a registered device only for the session whose check-in carried it, and MUST release it when that member's participation ends (03 §3.4d).
  • An engine MUST NOT admit a device on the strength of observing it alone. Hearing a bag sensor proves a bag is nearby, and bags are nearby in the next bay too.
  • Admission MUST be journaled, and the device MUST appear in the connection matrix like any other participant (03 §2.2). A device nobody can see in the manifest is a device contributing values nobody agreed to.

The handle the engine is given is not the device's identity#

A member's registered device identifier is, functionally, a member identifier. Consumer BLE equipment is not a phone: it generally advertises a stable address and does not rotate it. So an identifier that both names a device and is linked to an account would, once journaled, let anyone holding two rounds from two different facilities establish that the same person played both — which is precisely the property 00 §6 removes from every other part of the record, and it would walk back in through the equipment list.

  • The check-in response MUST give the engine a session-scoped handle for each registered device, not an account-level identifier. The construction is the one already used for checkin_ref (03 §3.4): opaque, deterministic within a session, and not reversible by the bay.
  • An engine MUST NOT journal a raw hardware address, serial number or vendor identifier for a member-owned device.
  • The engine MAY observe a stable address on air — it has no choice — but what it writes down is the handle. What it hears is plumbing; what it records is the record.

Equipment class is not identity and is recorded normally. "A kinematic suit contributed these fields" is exactly what a consumer needs and names nobody.

When the cloud cannot be asked#

The check-in response is a cloud round trip, and 09 §6 spends a long time on the case where there is no cloud. A bay on rung 3 has no registered-device list and never will until the uplink returns.

The member's phone is the fallback, in one of two shapes:

shapewhat the phone doeswhat reaches the engine
introductionpasses the bay token to the device, or presents the device to the engine on its behalfthe device's own frames, over 09 §2.3. The phone leaves the path
relaystays paired to the device, translates its vendor protocol into SGA-OISframes composed by the phone

These are also the path for equipment the member never registered, and the relay is not merely a fallback: most consumer BLE golf hardware speaks a vendor protocol whose only implementation is an SDK inside a phone app, so for a large part of the market it is the only shape available at any time.

A relay is not a courier, and the difference is not cosmetic#

The courier (07 §1) is safe because it carries sealed envelopes: hash-chained, signed, alterable only detectably. That is the entire security argument for handing a member's phone a round.

A relay has no such property. It is composing SGA-OIS values from a vendor protocol — deciding what the ball speed was, which club left the bag, what the body did — on a device the member owns, controls, and has an interest in the output of. Nothing is sealed, because the seal happens downstream of it.

  • A relayed value MUST name the phone as the participant that asserted it, and MUST name the originating device separately. "Which sensor" and "who told us about it" are two facts, and a record carrying only one of them cannot be evaluated.
  • An engine MUST NOT present a relayed value as though it arrived from the device directly. That is the specific misreading this rule exists to prevent.

Custody is recorded, not adjudicated#

A member had unsupervised physical access to equipment that produced their own competition data. That is a real consideration and it is not a tier question: the tiers in 02 §3a describe how well a signing key is held, and a member's sensor may hold its key perfectly well. What differs is custody of the hardware, which is a second axis — the same shape as the vendor-assurance axis in 02 §2.1.

So the engine records the facts and takes no view:

factvalues
how the device was admittedoperator_enrollment (§4) · member_registered · member_introduced
whether a phone composed the valuesdirect · relayed, naming the relaying participant
  • An engine MUST record both, for every device and every value they carry.
  • An engine MUST NOT suppress, downgrade or refuse a value on the strength of either (00 §7). What a member-owned sensor is worth in a sanctioned event is for the competition ruleset (08) and the officiator, who need the facts in front of them and are ill served by an engine that quietly decided for them.
Simulated Golf Association · SGA-OIS · generated from the specification sources