SGA‑OISOpen Interoperability Standard
1.0 Draft

Launch monitor integration

You measure what the stroke did to the ball — how fast it left, at what angle and direction, and how it was spinning — and most units also read the club that sent it. Everything else in the bay is downstream of those numbers.

That makes this the shortest integration in the standard, and the one with the most leverage. Four fields make you conformant. About a dozen more make the bay noticeably better to play in.

Read Integrate with SGA in an afternoon first — it is one page and it is most of the protocol. This page is the order to build things in, and what each one buys.


The whole integration, in outline#

open a socket
  ← session.hello                 the engine introduces itself
  → session.describe              who you are, what you provide, what you want
  ← session.manifest              your sources, your subscribers, your unmet wants
  → record.state × 2              status.lm_ready, player.lm_handedness
  ⇄ data flows
  → record.shot                   one per stroke, naming the field it carries

That is it. There is no registration step, no key exchange for an unattested device, and no polling. A minimal conformant launch monitor is a socket, a JSON encoder and about two hundred lines.


1. Say what you are — and send a serial#

Your session.describe carries identity and roles. For a launch monitor the role is sensor.ball, plus sensor.club if you read the club too. That message — your descriptor — is how the bay learns what you are, and you send it once.

JSON
"identity": { "device_id": "sga:device:ax3-00119423", "vendor": "acme",
              "model": "AX-3", "serial": "AX3-00119423", "firmware": "4.2.1",
              "firmware_digest": "sha256:…", "attestation_tier": "A0",
              "sensors": ["radar_doppler", "camera"] }

Send identity.serial. It is optional in the schema and you should send it anyway, because it is what the engine uses to tell this unit reconnecting from a second unit configured with the same device id (03 §2.0d).

The case that matters is mundane and happens in every bay eventually: your unit drops off Wi-Fi, the TCP connection half-opens, and neither end notices. You reconnect. With a serial, the engine sees the same unit, closes the stale connection and admits you immediately. Without one, it cannot distinguish you from an impostor and keeps the incumbent — and you wait for the dead socket to be reaped before the bay will take your shots again.

One string in a descriptor you send once. It is the highest benefit-per-character in this document.


2. Declare honestly, and declare only what you send#

Your provides list is a contract, and it can be broken two ways.

Declaring a field you never publish#

The engine names you as the source of every field in your provides, so no subscriber is told it is unmet. If you then never send it, they receive nothing and no warning: they cannot tell "not ready yet" from "never coming". Had you left it out, the manifest would have told them at handshake that nobody provides it, and they could plan around that.

If it is in provides, something in your code must send it. At the end of every round the engine journals each declared field you never sent (03 §2.0b). That is not an error — a field can depend on something the round never had, such as a tagged ball — but a field that is never sent in any round tells SGA your descriptor and your device disagree, and we will ask you about it.

Sending a field you never declared#

You may not send a field you did not declare in your session.describe. The engine refuses it outright with not_your_field (03 §2.0b), and the frame is neither forwarded nor journaled. The refusal names the field, so the fix is one line in your descriptor.

Provenance is not modesty#

Every provided field carries measured, calculated or inferred (01). Declare what is true.

An inferred spin value still wins when nothing better is present. It then gives way the moment a device that actually measures spin joins the bay. That is the connection matrix — the engine working out, field by field, which device is the best source for each one — and it can only do that if you were honest.

Under-declaring costs you directly. Sources are ranked by provenance first, so a real measurement you labeled inferred loses to another device's measured value. A certified bay also excludes devices below its required trust level, but it never ranks by trust level (14 §2.3).


3. Send a shot#

JSON
{
  "type": "record.shot",
  "field": "ball.speed_mps",
  "ball":  { "speed_mps": 64.8, "launch_angle_deg": 12.4,
             "launch_direction_deg": -1.7, "spin_rpm": 2810 },
  "club":  { "speed_mps": 48.9, "path_deg": -1.2 }
}

Two things about that frame are easy to get wrong.

field is what the engine routes on. It names the field this frame is about; the rest of the payload rides along. A frame with no field cannot be routed, attributed or recorded, and is refused.

Do not invent a shot_id. The engine allocates one on the first evidence of a swing and hands it out in record.swing_detected. A device that makes one up is telling the engine this contribution belongs to a shot that already closed, and it will be filed as late enrichment for a stroke that never existed (06 §10.2).

Required#

Four fields. That is the entire bar for a conformant sensor.ball device.

fieldunit
ball.speed_mpsmeters/second
ball.launch_angle_degdegrees, positive up
ball.launch_direction_degdegrees, positive right of target
ball.spin_rpmrev/min (or back_spin_rpm + side_spin_rpm)

If you cannot measure spin, declare it inferred with your model name. That is conformant. Omitting it is also conformant.

Optional — send what you have#

Ball spin_axis_deg · back_spin_rpm · side_spin_rpm · smash_ratio · carry_m · total_m · roll_m · curve_m · side_carry_m · side_total_m · landing_angle_deg

Club speed_mps · speed_at_impact_mps · path_deg · face_angle_deg · face_to_path_deg · angle_of_attack_deg · dynamic_loft_deg · dynamic_lie_deg · spin_loft_deg · swing_plane_vertical_deg · swing_plane_horizontal_deg · closure_rate_dps · low_point_m · strike_horizontal_mm · strike_vertical_mm

Trajectory apex_height_m · flight_time_ms · descent_angle_deg · the events array if you model the flight path

Putting stroke_length_m · backswing_time_ms · forward_swing_time_ms · tempo_ratio · skid_m · roll_speed_mps · speed_drop_pct · roll_pct · effective_stimp · break_m · putter path, face, loft, lie

Every one of these is in the field catalog with its units and sign convention. Check the sign conventions before you ship — spin_axis_deg and wind_direction_deg are the two places where two correct implementations most often disagree by 180°.


4. Publish the two things only you know#

These cost almost nothing and they are what turns a bay from working into pleasant.

status.lm_ready — you can see the ball#

A true-or-false standing value — one that stays in force until you change it, rather than an event that happens once. It means: sensors live and settled, ball detected in view, no recalibration running.

Publish it and a simulator can stop asking players to wait for a monitor that is not looking yet; a capture rig can arm its buffer; a unit that sleeps between shots gets told when to wake. Nothing in the standard forces anyone to act on it — there is deliberately no interlock and the engine composes nothing from it (03 §3.4e) — but everything that wants to prepare can.

Subscribe to status.sim_ready for the other half: the round is at a point where a shot means something. That is your cue to come out of low power.

player.lm_handedness — you already computed this#

If you arm on one side of the ball, or you see the swing, you know which hand the player is using. Publish it.

The simulator subscribes, corrects its setup, and the bay follows the player with nobody touching a menu.

It is worth subscribing to player.sim_handedness as well, so you can warn an operator when the simulator is set for the other hand — before the round is scored on it. A simulator configured for a right-hander while a left-hander plays mirrors every number that follows, and nobody notices for eighteen holes (03 §3.4f).

The two are separate fields on purpose. An engine never echoes a field back to the device that produced it. So if both halves shared one field name, your observation would reach the simulator, but the simulator's setting could never reach you — the exchange would only ever run one way.


5. Ask for what makes your numbers better#

Put these in wants. Each is a real improvement to what you report, not a courtesy.

fieldwhy you might want it
origin.lie_surfacea ball in sand leaves the face differently from one on a tee
round.player_uprecalibrate on player change, without anyone touching the unit
club.club_idcross-check your own club detection against what the bay believes
status.sim_readywake the sensors when the round can actually take a shot
ambient.temperature_ccompensation, if you do any — the room, not the game (06 §4.1)

If nobody provides one, you are told. The manifest lists it under unmet with reason no_provider, at handshake, before a shot is struck. A want that goes unmet is not an error and must not stop you measuring — a bay that answered no questions about the lie has not told you the ball is on a tee, it has told you nothing.


6. Two device-specific notes#

Sensor fusion#

Declare every modality in identity.sensors. Real units combine radar, camera and infrared, and the standard does not care which you are: "launch monitor" here means the participant that measures the ball's launch, whatever technology it uses to do so.

Where different modalities produce the same field at different quality, that is what per-field provenance is for. Declare each field at the quality the modality that produces it actually achieves, not at the best your unit can do for anything.

Club identification#

You may be able to identify the club. So can a bag sensor with RFID, and so can the simulator from its own menu. All three publish club.club_id, and they are ranked by identification method, not by provenance (06 §5.1) — a tag read beats a menu selection, and both beat an inference from impact.

Carry your own confidence in club.identification and let the matrix sort it out. You do not need to know what else is in the bay.


7. Refusals you will see#

A session.denied frame always carries a code and a remedy. The four you are most likely to meet:

codewhat happenedwhat to do
describe_requiredyou sent something before session.describedescribe first; the connection closes on this one
not_your_fieldyou published a field you did not declareadd it to provides, or stop sending it
device_id_in_useanother unit is connected under your device idgive this unit an id of its own
serial_requiredyour device id is in use and nothing distinguishes the twosend identity.serial

A refusal names what is wrong and what would fix it. If you get silence instead, that is a bug worth reporting — the standard's position is that a silent disconnect is indistinguishable from a crash, so a device that gets one retries forever.


Transport#

Implement one binding. You are conformant on the one you choose and under no obligation to support the others — pick whichever costs your hardware least.

bindingfor
WebSocket over TLSsoftware, and anything that wants a browser to join
Raw TCP, length-prefixedembedded units where a TLS stack is a real cost
BLE GATTcable-free units

Every engine must accept raw TCP (09 §2.0), so a microcontroller with a TCP stack and a JSON encoder can always join. That is a floor you can build against and rely on.

The binding is the only thing that differs. Same handshake, same message names, same field catalog, same refusal codes — no binding adds, removes or alters anything. Porting between them changes your socket layer and nothing else; your descriptor, your shot frames and your conformance results all carry over. Today the reference engine implements raw TCP, so that is what to develop against, and it constrains nothing about what you ship.

Keep one connection open for the life of the session. Nothing in this standard opens a connection in order to send a message, because on BLE that would cost 100–1000 ms per shot and even on a LAN it spends an order of magnitude more time on ceremony than on data.


8. How far your numbers count#

Every stroke you measure is one link in a chain — launch monitor, simulator, bay — and the result takes the lowest link's level (the full ladder).

levelfor a launch monitor this meansunlocks
A0ship it; no agreementcasual play
A1the technology partner agreement, and a published firmware_digest for each releaseSGA Player Rating, leagues
A2a signing key in firmware or NVRAM; you sign what you sendsanctioned events
A3the key in a secure element or TPMchampionships

Hardware reaches A3 more easily than software can, so for a launch monitor the top level is a firmware design decision, not a deployment problem. Send firmware_digest even at A0: without it, an event's firmware-version check falls back to A1 (kit warning R041).


Check yourself#

shell
cd sga-ois/conformance
python -m sgaconform your-descriptor.json

The kit checks your descriptor. Shot frames are not documents; watch them live against the reference bay.

Testing your integration →

Simulated Golf Association · SGA-OIS · generated from the specification sources