Accessory and single-purpose device integration
Read Integrate with SGA in an afternoon first — it is one page and it is most of this.
A device that contributes one field is a first-class participant. There is no minimum. If you know one thing nobody else in the bay knows, the standard wants it.
Why one field is enough#
Shot records are composed by the engine, not emitted whole by any single device. The launch monitor sends ball data. The simulator sends lie and outcome. You send your one thing. The engine assembles the record and decides, field by field, whose value wins.
So you never need to know the hole number, the player, the score, or what any other device is doing. Send what you know, when you know it.
Examples#
An RFID reader in the playfield that knows which club just passed over it:
{ "type": "session.describe", "protocol_version": "1.0",
"identity": { "device_id": "sga:device:tagmat-0001", "vendor": "acme",
"model": "TagMat", "serial": "TM-0001",
"attestation_tier": "A0", "sensors": ["rfid_nfc"] },
"roles": ["sensor.club_id"],
"provides": [
{ "field": "club.club_id", "provenance": "measured",
"id_methods": ["rfid_nfc"],
"timing": "standing", "max_age_ms": 600000 }
] }That device outranks a simulator's UI dropdown for club.club_id, because selection for that field ranks by strength of evidence about what was actually swung — rfid_nfc > marker > imaged > holder_absence > inferred_from_impact > bag_sequence > ui_selected. It does not have to be a launch monitor, or be made by one, or coordinate with one.
A sensed club rack — one switch per slot, and it knows which club was taken:
{ "roles": ["sensor.club_id"],
"provides": [
{ "field": "club.club_id", "provenance": "measured",
"id_methods": ["holder_absence"],
"timing": "standing",
"max_age_ms": 600000 }
] }timing: "standing" is the important part. You are not producing a value at the moment of a shot — you know the club long before anyone swings, and you have to be able to change your mind. Say so, and declare how long your answer stays good without renewal.
Send state when it changes, not when a shot happens:
{ "type": "record.state", "field": "club.club_id", "value": "7i",
"confidence": 0.9, "t_ms": 812440 }When you no longer know, say so. Two empty slots means you cannot tell which club is in play:
{ "type": "record.state", "field": "club.club_id", "value": null,
"reason": "ambiguous", "t_ms": 845102 }value: null means I no longer know — which is different from "nothing changed" and different from "the device died". Going quiet cannot express it, so withdrawal is an explicit message.
Without a retraction, every later shot is filed under a club nobody is holding. Never guess between two candidates; send null.
The engine takes your most recent unretracted assertion inside your declared window, records how old it was when the stroke happened, and composes nothing at all if there is none.
Holders compared. A rack, a bag or something nobody has built yet: what matters is whether it identifies clubs by tag or infers them from position.
| device | identifies by | declares requires_correct_replacement | can carry club.spec |
|---|---|---|---|
| tag-reading bag | tag | no | yes, and it travels with the player: their own clubs at any bay |
| tag-reading rack | tag | no | yes: program the house set once, and a walk-in gets per-club statistics |
| slotted rack or bag | position | yes | no |
Tags need every club modified and are exact; switches work with any clubs and assume they go back in the right place. The standard ranks by sensing method, never by conditions: a device that declares its limits honestly must never lose to one that declared none. Where two sources tie on method, the operator or the certification decides, through priority.
A rack like this is a few dollars of switches, and a tagged one not much more. Any of them gives a golfer per-club statistics that otherwise require a launch monitor with club detection or a subscription tier. That is the kind of product this standard is meant to make possible.
A weather module: roles: ["sensor.environment"]. Indoors it measures the room: provide ambient.temperature_c (06 §4.1). Outdoors, also provide environment.* with environment.conditions_source: "measured", so it reads as weather rather than a game setting.
A leveling platform — both directions:
{ "roles": ["actuator", "sensor.environment"],
"provides": [ { "field": "origin.lie_slope_along_deg", "provenance": "measured" },
{ "field": "origin.lie_slope_across_deg", "provenance": "measured" } ],
"wants": [ { "field": "origin.lie_surface", "importance": "required" },
{ "field": "status.sim_ready" } ] }It asks the simulator what the lie is, moves, and reports the angle it actually achieved. Nobody wrote an integration for that pair — the connection matrix resolved it because both sides declared what they had and what they needed.
A display, HUD or projector: roles: ["display"], provides nothing, wants only the handful of fields it renders. You receive only what you ask for, which is what keeps a wall display off a 200 Hz kinematics stream.
Two rules#
If your field is state rather than an event, declare it. Anything you assert between shots and maintain until it changes — a club drawn, an angle reached, a surface presented, conditions in the room — is timing: "standing". Declare a max_age_ms, and retract with value: null the moment you cannot support your own answer.
Ask for things that do not exist yet. An unmet want is reported back, not treated as an error — and in aggregate, unmet wants tell us what the ecosystem needs. If your product would be better with a field nobody supplies, declare the want anyway.
Need a field the standard does not have? Work with the community to add it to the common namespace, so every product uses the same name. Your own x-yourvendor. namespace is always available meanwhile, so you are never waiting on us (extending the standard).
Devices that travel#
A bag or a wearable belongs to a person and moves between bays. It joins exactly like a fixture does -- a bay token and nothing else. You never tell the bay whose you are, and you never need to: a club specification is a fact about a club, not about its owner, and friends share clubs constantly. The member registers you in the app; the cloud resolves it from your device_id when it matters.
If you have no camera, the phone relays the bay token. The member scans the placard, their app hands it to you over BLE, and you present it yourself. The phone is a courier, not a voucher.
placard --scan--> phone --BLE--> your device --DESCRIBE--> engine
Wearables and anything else that describes a person#
If your device measures a person rather than a thing, say so:
{ "roles": ["sensor.swing"],
"scope": "person",
"bay_token": "BAY-0482-07" }The difference decides whose record your data lands in. A rack describes a club, so its data follows whoever is swinging. A wearable describes its wearer, so its data follows them and nobody else — because you keep producing motion while your wearer stands about watching someone else hit, and that motion must never be filed as the other player's swing.
sensor.swing, sensor.pressure and sensor.player are treated as person-scoped even if you forget to declare it. Forgetting should not give you the dangerous default.
You do not bind yourself to a player. The member's phone does that at check-in, once they have picked their in-game name. Until then your data is recorded and attributed to nobody — kept, not discarded, in case a binding arrives later.
You do not declare a sharing policy. Whether your device may change hands mid-round is set by its owner when they bind it, not by you -- the same hardware is personal on one player and a loaner in a teaching pro's fleet, and you cannot know which one you are today. One less thing to get wrong.
Several holders at once#
You are not the only holder in the bay. Assert only what you know, and the engine resolves across all of them:
- One club out across the whole bay -- unambiguous, whoever it belongs to.
- Two clubs out -- ambiguous. The engine composes nothing, whether they came from one holder or two.
- One goes back -- it resolves again on its own.
So you never need to reason about other devices, and you never need to know which player is which. Say what is missing from your own inventory, retract when you cannot, and let the engine do the rest.
Your trust level only matters if a result uses you#
A result is only as trusted as the lowest level in its chain: the simulator, the launch monitor, the bay, and any sensor whose value changed the result (02 §2.3). A display, a swing camera or a club rack that no result field resolves to never lowers anything, so an A0 accessory is safe in a championship bay. If you do change results — a putting sensor, a leveling platform — the same A0–A3 ladder applies to you as to a launch monitor (the full ladder).
Want to hear what is going wrong?#
Ask for notices and the engine tells you when something it can see is affecting the round -- two clubs out of tracked holders, a source that just degraded, a wearable nobody has linked yet:
"wants": [ { "field": "sga.notice", "reason_code": "display" } ]Most of it arrives before the shot rather than after, because the engine knows the moment a second club leaves a bag. A device that does not ask never sees one.
You can talk to your player#
Not just listen. A swing monitor can ask how that one felt; an accessory can say it needs attention:
{ "type": "notice.post", "code": "device_question",
"audience": "slot", "slot": 2, "urgency": "background",
"text": "How did that one feel?",
"choices": [ {"id":"1","label":"Poor"}, {"id":"3","label":"Fine"},
{"id":"5","label":"Great"} ],
"expires_ms": 120000 }The answer comes back to you as a notice.response carrying the slot that answered — never a name.
Four limits, and none of them will trouble an honest device:
- A slot you are bound to, or the active one. Not the round, not the operator.
device_messageanddevice_questiononly. Engine codes cannot be spoofed.- Never
interrupturgency. Interrupting a golfer mid-swing is the engine's alone. - The player can switch you off in their app, next to the sharing switch. You are not told, and you cannot find out.
And the rule certification enforces that a license cannot: this is not a marketing channel. A device that advertises through it loses its mark.
Subjective answers are worth asking for. A shot that felt terrible and scored well is exactly what no sensor can see.
Transport#
One persistent connection for the session. BLE is fine for a device with no IP stack — GATT notify inbound, write-without-response outbound. Never connect per message; on BLE that costs 100–1000 ms.
Check yourself#
cd sga-ois/conformance
python -m sgaconform your-descriptor.jsonTesting your integration · Become a technology partner