Open Meet Display
Overview
Open Meet Display (OMD) is a small JSON message format for live meet displays: scoreboards, LED and video boards, broadcast and live-stream graphics, and announcer screens. Meet software sends one message that says this is what should be on the board now, and the board shows it. One receiver works with every meet program that sends OMD, so neither side writes a connector per product:
Producer ──────── messages ────────▶ Receiver
(meet software) (a board, a graphics system, an announcer screen)
- The producer is meet-management software. It sends a start list, results, team scores, an announcement, or a blank board, whenever the operator sends one or a board follows the meet.
- The receiver is your display product. It shows each message on the display target it names.
Three design choices shape the rest:
- Every message is the whole board. A message replaces what its target shows. There are no row patches, no deltas, and nothing to keep in step.
- Every value arrives ready to print. Places, marks, names, and wind come as display strings, so a receiver needs no sport-specific formatting.
- Start with one kind. A receiver that only shows
resultsis a complete receiver, as long as it accepts and ignores the other kinds.
OMD is a sibling of Open Meet Exchange (OMX), the format for moving a whole meet from
registration to timing to a results website. The two share names for the same facts, the
generator block, version numbers, the accepts list, and replies. OMD’s shape differs: flat,
display-ready rows, because a board shows what it is sent and keeps no database.
What OMD is not
- Not the race clock. The running clock comes from the timing system on its own channel. OMD carries what surrounds it: start lists, results, team scores, and messages.
- Not a way to hand results to a website. A message is what one board shows now, not a meet. To publish results, use Open Meet Exchange.
- Not a remote control. A receiver cannot ask the producer to show something or run anything. The producer decides what is sent; the receiver decides how it looks.
Terms
| Term | Meaning |
|---|---|
| message | One JSON object: a complete snapshot of what one display target should show |
| kind | omd.kind: start_list, results, team_scores, announcement, or clear |
| producer | The meet software that sends messages |
| receiver | The display product that accepts messages and shows them |
| target | display.target: which board, layer, or scene a message is for, such as main or field |
| snapshot | Every message replaces what its target shows; nothing is merged |
| row | One line on the board: an athlete or relay in a start list or results, or a team in team scores |
| page | The rows that fit on a board at once. The producer pages; a receiver never does |
| probe | A GET on the receiver’s URL that says which versions it reads and who it is |
generator |
The product that wrote a message or a probe reply: its id, name, and optional version, as in OMX |
Roles
| Role | What you do | Typical products |
|---|---|---|
| Receiver | Run a small HTTP server, answer the probe, accept messages, and show the kinds your product shows | Scoreboard and LED controllers, video and broadcast graphics, announcer screens |
| Producer | Probe the receiver, send each message to the URL the operator entered, and page the rows to fit the board | Meet-management and timing software |
Quick start
- Run an HTTP server on port
43339that accepts requests at/omd. - Answer
OPTIONSwith the CORS headers in Required endpoint behavior. - Answer
GET /omdwith the probe reply:{ "accepts": ["1.0"], "generator": { "id": "yourboard", "name": "YourBoard" } }. - Reply
200 { "ok": true }to everyPOST. Readomd.kind. Forresults(orstart_list), drawresults.rows(orstart_list.rows) top to bottom:place,participant_name,team.short_name,result. For any kind you do not show yet, change nothing. - Send it the examples with
curl, then send from 2Timer at a real meet.
Part 1: Messages
The envelope
Every message, whatever its kind, has these top-level blocks. A clear message is exactly this
envelope:
{
"omd": {
"version": "1.0",
"kind": "clear",
"generator": {
"id": "2t",
"name": "2Timer",
"version": "1.0.62"
},
"sent_at": "2026-11-14T07:45:00.000Z"
},
"meet": {
"id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4",
"name": "District Schools Championship",
"start_date": "2026-11-14",
"end_date": "2026-11-15",
"sport": "tf-outdoor"
},
"display": {
"target": "main",
"mode": "replace",
"connection_id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4_3"
}
}
A kind’s content sits under its own name: every message except clear carries one block named
exactly as omd.kind (start_list, results, team_scores, or announcement), and a list of
board lines is always that block’s rows. Read message[message.omd.kind]. event, when a message
has one, sits beside the block at the top level, so a race’s start list, results, and team scores carry the same event.
| Field | Required | Notes |
|---|---|---|
omd.version |
yes | MAJOR.MINOR, such as 1.0. A 1.0 receiver reads every 1.x message. See Versioning. |
omd.kind |
yes | start_list, results, team_scores, announcement, or clear. Switch on this. |
omd.generator |
yes | The product that wrote the message: id, a short name it picks for itself and never changes; name; and optional version. The same shape as OMX’s. |
omd.sent_at |
yes | When the producer built the message, ISO 8601 UTC with milliseconds. |
meet.id |
no | The meet’s stable identity. The same meet keeps it across messages and days. Opaque. |
meet.name |
yes | Display-ready meet name. |
meet.start_date, end_date |
yes | YYYY-MM-DD. Equal for a one-day meet. |
meet.sport |
yes | OMX’s sports: tf-outdoor, tf-indoor, xc (cross country), run (road race). Open: treat another value as context, never a reason to reject. |
display.target |
yes | Which board or surface this snapshot is for. See below. |
display.mode |
yes | Always replace in 1.x. |
display.connection_id |
no | The producer connection that sent the message. Opaque. It names the sender; it is not a credential. |
display.target is a free-text name chosen by whoever sets up the producer (default main).
It lets one receiver drive several surfaces, for example main, field, awards, or stream.
Your product decides which names it understands and maps them to boards, layers, or scenes. List
them in your probe reply so the producer can offer them, and tell your users.
Snapshot semantics
- Each message replaces what the target shows. The latest accepted message for a target is
what that target should display. A
resultsmessage arriving while a start list is up replaces the start list. - Never merge rows across messages. A results message with 3 rows means “show these 3 rows”, not “add these 3 rows”. If a row disappears between messages (a result was deleted or moved), it disappears from the board.
- Resends are normal. Producers resend on request and rebuild the current page whenever a result or entry changes. Receiving the same content twice re-renders it.
clearblanks the target. Seeclear.- Ordering. Messages normally arrive in the order they were sent, but delivery order is not
guaranteed (a manual send can overlap an automatic page flip). If that matters to your product,
drop a message whose
omd.sent_atis older than the one already showing for that target. - Rows render in the order given. Producers sort rows (start order, or place order) before sending.
Ignore-unknown rules
These rules let a minor version add fields, kinds, and values without breaking receivers. Accept and refuse map to the replies in Part 2.
| You receive | Do this |
|---|---|
| A field you do not recognize, at any level | Ignore it. |
An omd.kind you do not show |
Accept the message; change nothing on the board. |
A display.mode other than replace |
Accept the message; change nothing on the board. |
An unfamiliar value in meet.sport, event.gender, event.entry_type, results.rows[].status, results.rows[].scoring_role, team_scores.scope, or announcement.priority |
Accept the message; show the value as plain text or ignore it. |
A display.target you do not recognize |
Your choice: show it on a default surface, or refuse it with an error naming the target so the operator can fix the setting. |
An optional field that is missing, or null |
Treat both the same: “not applicable”. Producers leave such fields out; keep accepting null too. |
An omd.version whose major you do not read, or no omd block |
Refuse with 422, naming the versions you read in accepts. |
Why unknown kinds must be accepted: a producer may send start lists before results as a board follows the meet. A results-only receiver that refused start lists would show the operator an error on every event.
Message kinds
The examples are invented: the track and field ones share one meet, the cross country ones another. Each is also a file under /omd/examples/.
start_list
Who is in which lane (or flight position) before a race or field event. Sent when the operator shows a start list, or automatically when a board that follows the meet reaches a seeded event.
{
"omd": {
"version": "1.0",
"kind": "start_list",
"generator": {
"id": "2t",
"name": "2Timer",
"version": "1.0.62"
},
"sent_at": "2026-11-14T01:58:40.502Z"
},
"meet": {
"id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4",
"name": "District Schools Championship",
"start_date": "2026-11-14",
"end_date": "2026-11-15",
"sport": "tf-outdoor"
},
"display": {
"target": "main",
"mode": "replace",
"connection_id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4_3"
},
"event": {
"id": "42",
"number": "3",
"name": "Girls 100 Meter Dash",
"round": "1",
"round_label": "Prelims",
"section_name": "Heat 2",
"gender": "F",
"code": "100",
"entry_type": "individual"
},
"start_list": {
"rows": [
{
"entry_id": "884",
"start_assignment": "2",
"bib": "318",
"participant_name": "Tan Mei Ling",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"seed": "13.02"
},
{
"entry_id": "885",
"start_assignment": "3",
"bib": "205",
"participant_name": "Nur Aisyah Binti Rahman",
"team": {
"name": "Hillcrest Secondary",
"short_name": "HCS"
},
"seed": "12.71"
},
{
"entry_id": "886",
"start_assignment": "4",
"bib": "412",
"participant_name": "Priya Devi Subramaniam",
"team": {
"name": "Lakeview Academy",
"short_name": "LVA"
},
"seed": "12.48"
},
{
"entry_id": "887",
"start_assignment": "5",
"bib": "127",
"participant_name": "Siti Hajar Binti Osman",
"team": {
"name": "Eastgate College",
"short_name": "EGC"
},
"seed": "12.39"
},
{
"entry_id": "888",
"start_assignment": "6",
"bib": "331",
"participant_name": "Wong Jia Xin",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"seed": "12.55"
},
{
"entry_id": "889",
"start_assignment": "7",
"bib": "219",
"participant_name": "Kavitha Raj",
"team": {
"name": "Hillcrest Secondary",
"short_name": "HCS"
},
"seed": "12.84"
},
{
"entry_id": "890",
"start_assignment": "8",
"bib": "140",
"participant_name": "Lim Hui Min",
"team": {
"name": "Eastgate College",
"short_name": "EGC"
}
}
]
}
}
Notes:
- Rows arrive in start order (lane, or flight position for field events).
start_assignmentis the display-ready lane or position, left out when the athlete has none yet (common in distance races). There is no row for an empty lane (lanes 1 and 9 above).seedis the entry mark, display-ready, and is left out when the athlete has none (Lim Hui Min above).- A start list has no
result,status,place,wind, or points.
results
Places and results for one heat or flight, one round, or a whole event. Sent when the operator shows results, or automatically as results are recorded on a board that follows the meet.
This example is one heat of a track race. Wind applies to the whole heat: it is sent in
event.wind and repeated on each row.
{
"omd": {
"version": "1.0",
"kind": "results",
"generator": {
"id": "2t",
"name": "2Timer",
"version": "1.0.62"
},
"sent_at": "2026-11-14T02:09:13.884Z"
},
"meet": {
"id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4",
"name": "District Schools Championship",
"start_date": "2026-11-14",
"end_date": "2026-11-15",
"sport": "tf-outdoor"
},
"display": {
"target": "main",
"mode": "replace",
"connection_id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4_3"
},
"event": {
"id": "42",
"number": "3",
"name": "Girls 100 Meter Dash",
"round": "1",
"round_label": "Prelims",
"section_name": "Heat 2",
"gender": "F",
"code": "100",
"entry_type": "individual",
"wind": "+1.4"
},
"results": {
"rows": [
{
"entry_id": "887",
"start_assignment": "5",
"bib": "127",
"participant_name": "Siti Hajar Binti Osman",
"team": {
"name": "Eastgate College",
"short_name": "EGC"
},
"result": "12.31",
"place": "1",
"wind": "+1.4"
},
{
"entry_id": "886",
"start_assignment": "4",
"bib": "412",
"participant_name": "Priya Devi Subramaniam",
"team": {
"name": "Lakeview Academy",
"short_name": "LVA"
},
"result": "12.44",
"place": "2",
"wind": "+1.4"
},
{
"entry_id": "885",
"start_assignment": "3",
"bib": "205",
"participant_name": "Nur Aisyah Binti Rahman",
"team": {
"name": "Hillcrest Secondary",
"short_name": "HCS"
},
"result": "12.69",
"place": "3",
"wind": "+1.4"
},
{
"entry_id": "888",
"start_assignment": "6",
"bib": "331",
"participant_name": "Wong Jia Xin",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"result": "12.69",
"place": "3",
"wind": "+1.4"
},
{
"entry_id": "884",
"start_assignment": "2",
"bib": "318",
"participant_name": "Tan Mei Ling",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"result": "13.10",
"place": "5",
"wind": "+1.4"
},
{
"entry_id": "889",
"start_assignment": "7",
"bib": "219",
"participant_name": "Kavitha Raj",
"team": {
"name": "Hillcrest Secondary",
"short_name": "HCS"
},
"status": "dnf",
"place": "DNF",
"wind": "+1.4"
},
{
"entry_id": "890",
"start_assignment": "8",
"bib": "140",
"participant_name": "Lim Hui Min",
"team": {
"name": "Eastgate College",
"short_name": "EGC"
},
"status": "dq",
"place": "DQ",
"wind": "+1.4"
}
]
}
}
A field event looks the same. This long jump final covers the whole round, so there is no section_name; each row carries the wind of that athlete’s best jump, and scored
places carry points:
{
"omd": {
"version": "1.0",
"kind": "results",
"generator": {
"id": "2t",
"name": "2Timer",
"version": "1.0.62"
},
"sent_at": "2026-11-14T05:31:47.020Z"
},
"meet": {
"id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4",
"name": "District Schools Championship",
"start_date": "2026-11-14",
"end_date": "2026-11-15",
"sport": "tf-outdoor"
},
"display": {
"target": "field",
"mode": "replace",
"connection_id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4_3"
},
"event": {
"id": "57",
"number": "18",
"name": "Boys Long Jump",
"round": "1",
"round_label": "Finals",
"gender": "M",
"code": "LJ",
"entry_type": "individual"
},
"results": {
"rows": [
{
"entry_id": "1203",
"start_assignment": "6",
"bib": "512",
"participant_name": "Muhammad Hafiz Bin Yusof",
"team": {
"name": "Lakeview Academy",
"short_name": "LVA"
},
"result": "6.92",
"place": "1",
"wind": "+1.8",
"points": "10"
},
{
"entry_id": "1204",
"start_assignment": "2",
"bib": "236",
"participant_name": "Daniel Chong",
"team": {
"name": "Hillcrest Secondary",
"short_name": "HCS"
},
"result": "6.71",
"place": "2",
"wind": "+0.6",
"points": "8"
},
{
"entry_id": "1205",
"start_assignment": "4",
"bib": "355",
"participant_name": "Arjun Nair",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"result": "6.58",
"place": "3",
"wind": "-0.3",
"points": "6"
},
{
"entry_id": "1206",
"start_assignment": "1",
"bib": "149",
"participant_name": "Haziq Bin Kamal",
"team": {
"name": "Eastgate College",
"short_name": "EGC"
},
"status": "dns",
"place": "DNS"
}
]
}
}
A cross country or road race has no lanes, rounds, or wind, so those fields are left out. This
message is the third page of a finished Varsity Boys 5K, places 17 to 24, on a board that shows
eight rows. Each runner who holds a scoring position carries its points, and scoring_role says
whether those points count for the runner’s own team (scorer) or only push other teams back
(displacer). A position is not a place: runners who hold none are skipped.
Positions here already run one behind places, because a runner earlier in the race holds none
(Hakim Bin Rosli, 19th, holds position 18). Daniel Teo runs unattached and holds none, so Ong Kai
Xuan, 21st, holds position 19. Arif Bin Salleh is Riverside’s eighth runner, past its displacers, so he holds no
position either. Points never add up to a team score on their own: the race’s team standings
arrive as their own team_scores message (see below).
{
"omd": {
"version": "1.0",
"kind": "results",
"generator": {
"id": "2t",
"name": "2Timer",
"version": "1.0.62"
},
"sent_at": "2026-10-24T03:52:07.615Z"
},
"meet": {
"id": "b83e5d17-2c9a-4f60-8e14-7a5c0f9d3b62",
"name": "District Cross Country Championship",
"start_date": "2026-10-24",
"end_date": "2026-10-24",
"sport": "xc"
},
"display": {
"target": "main",
"mode": "replace",
"connection_id": "b83e5d17-2c9a-4f60-8e14-7a5c0f9d3b62_1"
},
"event": {
"id": "2",
"number": "2",
"name": "Varsity Boys 5K",
"gender": "M",
"code": "5000",
"entry_type": "individual"
},
"results": {
"rows": [
{
"entry_id": "231",
"bib": "1067",
"participant_name": "Faris Bin Hamdan",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"result": "17:41.2",
"place": "17",
"points": "16",
"scoring_role": "displacer"
},
{
"entry_id": "268",
"bib": "1270",
"participant_name": "Vikram Pillai",
"team": {
"name": "Lakeview Academy",
"short_name": "LVA"
},
"result": "17:44.9",
"place": "18",
"points": "17",
"scoring_role": "scorer"
},
{
"entry_id": "255",
"bib": "1219",
"participant_name": "Hakim Bin Rosli",
"team": {
"name": "Hillcrest Secondary",
"short_name": "HCS"
},
"result": "17:47.5",
"place": "19",
"points": "18",
"scoring_role": "scorer"
},
{
"entry_id": "277",
"bib": "1301",
"participant_name": "Daniel Teo",
"result": "17:52.0",
"place": "20"
},
{
"entry_id": "246",
"bib": "1196",
"participant_name": "Ong Kai Xuan",
"team": {
"name": "Eastgate College",
"short_name": "EGC"
},
"result": "17:55.3",
"place": "21",
"points": "19",
"scoring_role": "displacer"
},
{
"entry_id": "233",
"bib": "1072",
"participant_name": "Lee Wei Jie",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"result": "18:01.8",
"place": "22",
"points": "20",
"scoring_role": "displacer"
},
{
"entry_id": "236",
"bib": "1078",
"participant_name": "Arif Bin Salleh",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"result": "18:06.4",
"place": "23"
},
{
"entry_id": "271",
"bib": "1275",
"participant_name": "Suresh Kumar",
"team": {
"name": "Lakeview Academy",
"short_name": "LVA"
},
"result": "18:09.1",
"place": "24",
"points": "21",
"scoring_role": "displacer"
}
]
}
}
Notes:
-
Rows arrive in display order: finishers by place, then non-finishers.
-
Every row has an outcome: a
place, or astatus(dnf,dns,dq,fs,scr, ornr). A row with a place carries no status. An athlete still competing, such as a runner on the course, is not in the message until they finish or are given a status, so a board following a race shows the finishers so far. Style rows bystatus. -
placeis the printed place within what the message covers: within the heat or flight whenevent.section_nameis set, otherwise within the round. Ties repeat the place and skip the next one (3,3,5). For a row with a status,placeholds what a results sheet prints for it (DNF,DQ,DNS, andNT,NM, orNHfornr), so a simple board can print theplacecolumn as it is. -
resultis display-ready:12.31,1:02.47,6.92, or feet and inches when the meet uses imperial units. Do not parse it. -
Relay events use the same shape with
event.entry_type: "relay";participant_nameis then the relay team name (for exampleRiverside High School A), andrelay_legslists the leg runners in running order when they are recorded, for example["Tan", "Lee", "Ong", "Raj"].
team_scores
Team standings, one division and gender at a time, in team_scores.rows.
team_scores.scope says what the standings cover:
meet: standings summed across a division’s events, as in a track and field meet. There is noevent.event: standings for one race, as in cross country. The message carries that race’seventblock, the same fields a start list or results message carries, without round, section, or wind. Matchevent.idto show the standings beside the race’s results, or title the board withevent.name.
Meet standings for one division and gender:
{
"omd": {
"version": "1.0",
"kind": "team_scores",
"generator": {
"id": "2t",
"name": "2Timer",
"version": "1.0.62"
},
"sent_at": "2026-11-15T09:12:30.415Z"
},
"meet": {
"id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4",
"name": "District Schools Championship",
"start_date": "2026-11-14",
"end_date": "2026-11-15",
"sport": "tf-outdoor"
},
"display": {
"target": "main",
"mode": "replace",
"connection_id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4_3"
},
"team_scores": {
"scope": "meet",
"division_name": "Under 18",
"gender": "F",
"rows": [
{
"place": "1",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"points": "112"
},
{
"place": "2",
"team": {
"name": "Eastgate College",
"short_name": "EGC"
},
"points": "96.5"
},
{
"place": "3",
"team": {
"name": "Hillcrest Secondary",
"short_name": "HCS"
},
"points": "71"
},
{
"place": "3",
"team": {
"name": "Lakeview Academy",
"short_name": "LVA"
},
"points": "71"
},
{
"place": "5",
"team": {
"name": "Northfield School"
},
"points": "18"
}
]
}
}
Standings for one cross country race:
{
"omd": {
"version": "1.0",
"kind": "team_scores",
"generator": {
"id": "2t",
"name": "2Timer",
"version": "1.0.62"
},
"sent_at": "2026-10-24T03:41:18.206Z"
},
"meet": {
"id": "b83e5d17-2c9a-4f60-8e14-7a5c0f9d3b62",
"name": "District Cross Country Championship",
"start_date": "2026-10-24",
"end_date": "2026-10-24",
"sport": "xc"
},
"display": {
"target": "main",
"mode": "replace",
"connection_id": "b83e5d17-2c9a-4f60-8e14-7a5c0f9d3b62_1"
},
"event": {
"id": "2",
"number": "2",
"name": "Varsity Boys 5K",
"gender": "M",
"code": "5000",
"entry_type": "individual"
},
"team_scores": {
"scope": "event",
"division_name": "Varsity",
"gender": "M",
"rows": [
{
"place": "1",
"team": {
"name": "Riverside High School",
"short_name": "RHS"
},
"points": "42"
},
{
"place": "2",
"team": {
"name": "Eastgate College",
"short_name": "EGC"
},
"points": "57"
},
{
"place": "3",
"team": {
"name": "Hillcrest Secondary",
"short_name": "HCS"
},
"points": "81"
},
{
"place": "4",
"team": {
"name": "Lakeview Academy",
"short_name": "LVA"
},
"points": "96"
}
]
}
}
| Field | Notes |
|---|---|
event |
The race, when scope is event. Absent when scope is meet. |
team_scores.scope |
meet (summed across a division’s events) or event (one race). Open. |
team_scores.division_name |
The division’s own name, such as Varsity or Under 18. Left out when there is no division. |
team_scores.gender |
Whose standings these are, always present: M, F, or X for one gender, C for combined. |
team_scores.rows[].place |
Display-ready place; ties repeat. |
team_scores.rows[].team.name / short_name |
Full name and what fits on a narrow board. short_name may be missing. |
team_scores.rows[].points |
Display-ready total; may include halves (96.5). In cross country, low scores win: rows still arrive in place order. |
Not paged: every team in the standings is sent.
announcement
A message the operator types: awards calls, weather delays, schedule changes.
{
"omd": {
"version": "1.0",
"kind": "announcement",
"generator": {
"id": "2t",
"name": "2Timer",
"version": "1.0.62"
},
"sent_at": "2026-11-14T07:20:02.733Z"
},
"meet": {
"id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4",
"name": "District Schools Championship",
"start_date": "2026-11-14",
"end_date": "2026-11-15",
"sport": "tf-outdoor"
},
"display": {
"target": "main",
"mode": "replace",
"connection_id": "6f1c2a9e-4b7d-4e2a-9c51-0d8e3f7a12b4_3"
},
"announcement": {
"title": "Weather delay",
"message": "Competition is paused for lightning. Next update at 3:30 PM.",
"priority": "normal"
}
}
| Field | Notes |
|---|---|
announcement.title |
Short heading. Optional. |
announcement.message |
Always present, never empty. Plain text; wrap it to fit. |
announcement.priority |
normal or high. Open. |
announcement.expires_at |
ISO 8601 timestamp after which to stop showing it. Missing means show it until replaced. |
clear
Blank the target. Envelope only, no content fields; the example is under
The envelope. Show your product’s blank, idle, or logo state for that target. A
receiver with no blank state may accept clear and ignore it.
Event row reference
start_list.rows and results.rows share one row shape. Every value is a string (numbers are never sent as JSON
numbers), except team, an object, and relay_legs, an array of strings. A field with no value is
left out. participant_name is in every row, and entry_id is in every start list row.
Where OMX carries a mark as { value, unit, display }, OMD sends only the display string, under
the same name (seed, result; wind for OMX’s wind_mps).
In the Start list and Results columns: always means every row has it; yes and usually mean it is there when it has a value; no means it is never sent in that kind; anything else names the rows that carry it.
| Field | Start list | Results | Notes |
|---|---|---|---|
participant_name |
always | always | The label to display, already formatted. |
bib |
usually | usually | Bib or hip number. |
team |
usually | usually | { "name", "short_name" }; missing for an unattached athlete. short_name is what fits on a narrow board, such as the team’s code, and may be missing. Choose by available width. |
start_assignment |
yes | yes | Lane or flight position, display-ready; missing when none is assigned. |
seed |
yes | usually not | Entry mark. |
result |
no | yes | Time, distance, or height, display-ready. A disqualified athlete keeps the result they made; other non-finishers have none. |
status |
no | yes | Left out when the row has a place. Otherwise one of the statuses. Open: show an unknown value as text. |
place |
no | yes | Printed place, or what a results sheet prints for a row with a status. |
wind |
no | wind events | Signed m/s without unit: +1.4, 0.0, -0.3. NWI means no wind reading was recorded for a wind-measured event. |
points |
no | scored places | Team points this place carries, as a number string such as 10 or 4.5. In cross country, scoring_role says whose team they count for. Never add rows into a team score (a page shows only some rows); use team_scores. |
scoring_role |
no | cross country | scorer when the runner’s points count toward the team score, displacer when the runner holds a scoring position after the team’s scorers. Open. |
multi_points |
no | multi-events | Combined-event points total. |
relay_legs |
no | relays | Leg runners’ names in running order, formatted like participant_name. Legs with no runner yet are left out, and so is the field when no legs are recorded. |
entry_id |
always | usually | Opaque entry identifier. The same athlete or relay keeps the same entry_id across messages while the meet runs, so you can match rows between snapshots (for example to animate a new finisher). Do not store it beyond the meet. Missing for rows that are not one entry. |
event fields:
| Field | Notes |
|---|---|
id |
Opaque event identifier, unique within the meet. |
number |
Display event number, such as 3. |
name |
Display-ready, such as Girls 100 Meter Dash. |
code |
The event’s code in OMX’s event table, such as 100, 110H, 4X400, LJ, HJ, or SP. Left out when OMX has no code for the event. Useful for choosing a layout (track or field) without parsing names. |
round / round_label |
Such as 1 / Prelims, 2 / Finals. Both missing when the message is not narrowed to a round. |
section_name |
Heat 2, Section 1, or Flight 1 when the message is one heat, section, or flight; missing for a whole round or event. |
gender |
M, F, X (non-binary), or C (everyone, such as a mixed relay), as in OMX; missing when the producer does not know it. Open. |
entry_type |
individual, relay, or multi. Open. |
wind |
In results for one heat of a wind-measured track race, that heat’s reading (same format as row wind). Otherwise missing, including field events, where each row carries its own. Reading wind from the rows always works. |
Statuses
A row’s status uses OMX’s codes and meanings. A row with a place has none.
status |
Meaning | OMX status | place prints |
|---|---|---|---|
dnf |
Did not finish | dnf |
DNF |
dns |
Did not start | dns |
DNS |
dq |
Disqualified; keeps the result it was recorded with | dq |
DQ |
fs |
False start in a timed track race; a non-finisher like dq |
fs |
FS |
scr |
Scratched before competing | scr |
SCR |
nr |
No result: competed with no valid time, distance, or height | nr |
NT, NM, or NH |
exh |
Reserved for exhibition | (none) | as the producer prints |
Row limits and paging
Receivers never paginate. A start_list or results message holds exactly the rows that should be
on screen now. When an event has more rows than a board can show, the producer pages: it sends
each page as an ordinary replacement snapshot, and there are no page-number fields.
Pages never mix heats or flights in a start list: a page holds rows from one section, and
event.section_name names it.
team_scores and announcement are not paged. A team_scores message contains every team in the
standings, so fit or truncate on your side.
Part 2: Transport
Every transport carries the Part 1 messages unchanged. HTTP is the one defined in 1.0. See Planned transports for what may come next.
HTTP POST
The producer sends each message as the JSON body of an HTTP POST to a small HTTP server that your
receiver runs:
Producer (meet software, e.g. in a browser) Your receiver (same computer or network)
─────────────────────────────────────────── ──────────────────────────────────────
asks what you read ──GET───▶ http://<address>:<port>/omd
◀─200─── { "accepts": ["1.0"], "generator": … }
operator sends, or a board follows the ──POST──▶ http://<address>:<port>/omd
meet body = one Part 1 message
◀─200─── { "ok": true }
Why JSON over HTTP, and not gRPC, protobuf, or a socket protocol:
- Producers run in browsers. A web page cannot open raw TCP or UDP sockets, talk to serial ports, or listen for incoming connections. It cannot speak native gRPC either: gRPC-Web needs a proxy beside the receiver, one more thing to install and keep running at a venue. A web page can make HTTP requests.
- Size is not the limit. A message is a few KB, and a busy meet sends a few a minute, so a binary encoding saves nothing a board would notice.
- Anyone can read it. A message can be sent with
curl, read in a log, and checked against the JSON Schema. The TypeScript and Python types give typed code without a code generator. - The transport can change without the message. A receiver whose system takes gRPC or another protocol can run a small bridge that accepts OMD over HTTP and passes each message on. A later transport carries the same Part 1 messages unchanged (see Planned transports).
How HTTP delivery works:
- The producer is the client. It pushes. It does not poll or hold a connection open.
- One request per message. Requests are small (typically 2–10 KB, well under 1 MB).
- Your receiver chooses its URL. Document the address, port, and path your receiver listens on;
the producer’s operator enters that whole destination. For a new receiver, use the path
/omdand the port43339unless you have a reason not to; matching the defaults saves the operator a step. Producers let the operator change the path, not only the address and port. - Other computers. To accept sends from another computer, listen on the network interface (not
only
127.0.0.1) and allow the port through the firewall. See Where the receiver can run.
Request:
POST /omd HTTP/1.1
Host: 127.0.0.1:43339
Origin: https://manager.2timer.app
Content-Type: application/json
{ ...one OMD message... }
- The body is UTF-8 JSON. Names may contain non-ASCII characters.
- No authentication header and no cookies are sent. See Security.
- Reply within 5 seconds, as soon as the message is parsed and accepted, and render afterward. Producers wait a few seconds before telling the operator the send failed; 2Timer waits 5.
The probe
Before it sends, a producer asks the receiver what it reads. The receiver answers GET on the same
URL it takes messages on, with 200 and:
{
"accepts": ["1.0"],
"generator": {
"id": "exampleboard",
"name": "ExampleBoard",
"version": "2.4"
},
"targets": ["main", "field", "stream"]
}
| Field | Required | Meaning |
|---|---|---|
accepts |
yes | The versions you read, MAJOR.MINOR, as in OMX. ["1.0"] reads 1.0 and every later 1.x. |
generator |
yes | Who answered: your product’s id (a short name you pick once and never change), name, and optional version. The same shape as a message’s. |
targets |
no | The display.target names you understand, so the producer can offer them instead of the operator typing one. You may take others too. |
- Every 1.0 receiver answers the probe. That is how a producer tells a 1.0 receiver from one built against a draft of this format: a receiver that answers is 1.0 or later.
- The status code says whether you are ready.
200with the reply above when you are ready to show messages;503withRetry-Afterwhen you are running but cannot show anything yet. - A probe changes nothing on the board. It is never a message.
- CORS as for
POST. The reply carriesAccess-Control-Allow-Origin. A plainGETneeds no preflight, and Chrome’s local-network prompt covers it as it does a send. - Later versions only add fields to the reply.
- A producer sends the newest version both of you read. A receiver that does not answer, or
answers with something that is not a probe reply (a
404, an HTML page, JSON with noaccepts), was built before 1.0; a producer may send it the draft message it was built against, or 1.0.
Required endpoint behavior
When the producer is a web page, such as https://manager.2timer.app, the browser applies CORS,
because your receiver is a different origin. A receiver that does not answer CORS correctly looks
“unreachable” to the operator, even though the request reached your machine.
1. Answer OPTIONS (preflight)
The browser sends an OPTIONS request before a POST because the POST carries
Content-Type: application/json. A producer’s connection test may send its own OPTIONS too.
Answer any OPTIONS on your path with:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://manager.2timer.app
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
Vary: Origin
Access-Control-Allow-Originnames the producer origins your operator allows, such ashttps://manager.2timer.app. Echo back the request’sOriginwhen it is on your list, and sendVary: Originwhenever you echo it, so a cache never serves one site’s answer to another. Use*only for testing. See Security for why.OPTIONSandGETare listed inAccess-Control-Allow-Methods.
2. Answer GET (the probe)
Reply 200 with the probe reply and the CORS header:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://manager.2timer.app
Vary: Origin
Content-Type: application/json
{ "accepts": ["1.0"], "generator": { "id": "yourboard", "name": "YourBoard" } }
3. Accept: reply 200 or 204
For every message you accept, including ones you deliberately ignore under the
Ignore-unknown rules, reply 200 or 204, with the CORS header:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://manager.2timer.app
Vary: Origin
Content-Type: application/json
{ "ok": true }
- The CORS header is on the
POSTresponse too. Without it the browser hides the reply from the producer, and the operator sees a failed send even though you received the message. - Producers read the status code: any
2xxis success. A body is optional, and{ "ok": true }is a good one.
4. Refuse: reply 400 or 422
Refuse only for problems the operator can fix, never for a kind you do not show yet. Reply with the CORS header and an optional body naming what was wrong:
| Status | When |
|---|---|
400 |
The body is not JSON |
422 |
No omd block, an omd.version major you do not read, or a target you refuse |
{ "accepts": ["1.0"], "errors": [{ "path": "/omd/version", "message": "2.0 is not read here" }] }
The producer shows the operator the status (and the message, when there is one), and the board keeps its previous content.
Where the receiver can run
When the producer runs in a browser, where your receiver runs decides whether the browser lets the request out.
| Receiver runs on | Destination the operator enters | Works from |
|---|---|---|
| The same computer as the producer | http://127.0.0.1:<port>/<path> (or localhost) |
Chrome |
| Another computer on the local network | http://<that computer's IP>:<port>/<path> |
Chrome, after one permission prompt |
Chrome asks once. Chrome asks the operator before a public website talks to a device on the local network. The first time the producer reaches your receiver, Chrome shows a local-network permission prompt. The operator clicks Allow once on that computer, and Chrome remembers it for that site in that browser profile. If the operator clicks Block, requests fail until they allow local network access again in the address bar’s site controls.
Receivers need nothing extra. The CORS headers above are enough. No HTTPS certificate and no special local-network header are needed.
Use Chrome for browser-based producers. Safari (including iPad) does not let an HTTPS page reach
a plain http:// address, and Firefox is untested. Nothing about your receiver would change if a helper for those browsers comes later.
If something does not connect, tell us the browser and version and what happened. The browser developer console (F12 → Console) usually says why.
Planned transports
Not defined in 1.0. Each would carry the same Part 1 messages unchanged.
- WebSocket. The receiver runs a WebSocket server, and the producer keeps one connection open and sends each message as one text frame. The receiver could also answer, for example to confirm a message or to ask for what should be showing now.
- File. The producer writes each message to a file in a folder the receiver watches, one file per display target.
A receiver that talks back names the producer connection it means with
display.connection_id.
Security
OMD over HTTP is meant for a venue’s own network and has no login or token, like most scoreboard
protocols, so a receiver names the producer sites it allows instead of answering any origin (*).
These habits keep a live board showing only what the operator sent:
- Allow your producers by origin. Answer CORS for the producer sites your operator uses, such
as
https://manager.2timer.app, rather than*, and let the operator edit that list. Then other web pages open in a browser on the network cannot post to your board. - Optionally limit senders. Tools like
curlignore CORS, so on a shared venue network you may let the operator choose which computers can send. - Treat
display.connection_idas a label. It tells you which connection sent a message, not that the sender is trusted. - Log what you receive. Keeping each message with its
omd.generatorandomd.sent_atmakes a surprising board easy to explain.
Not in this version
Each of these is a known gap, left out until a receiver needs it. Each can be added in a minor version without changing what anything already in the format means:
- Per-attempt field marks, and the athlete now up in a field event.
- Photos, records, country codes, and age group on a row.
- A results status (Unofficial, Under Review, Official) on a results message.
- A page number and page count. The producer pages; a receiver sees each page as a snapshot.
- An event’s adaptive form, such as seated or wheelchair, which OMX carries as
adaptive.mode. The event’snamesays it; itscodeis the ordinary event’s. - Which kinds a board shows, in the probe reply. A receiver accepts and ignores kinds it does not show.
- WebSocket and File transports (see Planned transports).
- A helper for Safari and iPad, so a browser that cannot reach a plain
http://address can still send.
Versioning
omd.versionisMAJOR.MINOR.- Minor versions only add: optional fields, kinds, and values in open lists. A receiver reads any minor within a major it implements and ignores what it does not know.
- Major versions may break. A receiver that meets a major it does not read refuses the message with
422andaccepts, rather than showing what it half-understands. - One schema URL per major, republished in place with each minor.
- A receiver that
accepts1.0is sent any1.x.
Open and closed lists
Every list of values is either closed or open, and an open one says what a receiver does with a value it does not know.
Closed lists change only in a major version: display.mode (replace).
Open lists gain values in minor versions:
| Open list | A value the receiver does not know |
|---|---|
omd.kind |
Accept the message; change nothing on the board |
meet.sport |
Show it as text, or ignore it |
event.gender |
Show it as text, or ignore it |
event.entry_type |
Show it as text, or ignore it |
event.code |
Choose a layout by name; never refuse the message |
results.rows[].status |
Show it as text; treat the row as having no place |
results.rows[].scoring_role |
Show it as text, or ignore it |
team_scores.scope |
Show the standings without labeling what they cover |
announcement.priority |
Treat as normal |
display.target names are the receiver’s own, not a list of the format’s (see
The envelope).
Draft receivers
Receivers built against Draft 0.1 to 0.3 read messages with "format": "meet.display.v1" and no
omd block, and do not answer the probe. A producer that still serves them sends the draft message
to a receiver that gives no valid probe answer. The drafts’ raw files stay published at
/omd/0.2/ and /omd/0.3/.
Pinned versions
- Every version’s raw files stay at
/omd/<version>/, for example/omd/1.0/spec.md, so an implementation can cite and re-check exactly what it was built against. The unversioned URLs always serve the newest version.
Changelog
1.0. The first published version. What changed from Draft 0.3:
-
The envelope is an
omdblock, like OMX’somxblock.omd.version,omd.kind,omd.generator, andomd.sent_atreplaceformat,display.kind,source, andsent_at.source.connection_idis nowdisplay.connection_id. -
OMX’s names for the same facts:
Draft 0.3 1.0 format: "meet.display.v1"omd.version: "1.0"display.kindomd.kindsource.name,source.versionomd.generator.name,versionsent_atomd.sent_atsource.connection_iddisplay.connection_idmeet.date_start,date_endmeet.start_date,end_dateevent_number,event_name,event_codeevent.number,name,coderound_number,round_nameevent.round,round_labelbib_numberbibaffiliationteammarkresultteam_pointspoints -
Statuses are OMX’s lowercase codes, and a row with a place has none:
OKis gone. -
One letter per gender, as in OMX:
M,F,X(non-binary), andCfor a mixed event or combined standings;Xnow means non-binary.team_scores.genderis always present. Changed in 1.0 in place on 2026-11-10, before any receiver read 1.0 (it wasmixed, and left out for combined standings). -
event.codeis OMX’s event code, left out when OMX has none. -
The probe: a receiver answers
GETwithacceptsandgenerator. -
Replies are OMX’s: accept with
200or204; refuse with400or422and an optionalacceptsanderrorsbody; another major or noomdblock is422. -
Vary: Originwhen a receiver echoes the request’s origin. -
A kind’s content sits under its own name. A start list’s and results’ rows are
start_list.rowsandresults.rows, as team standings’ were alreadyteam_scores.rows; Draft 0.3 sent them as top-levelrows. Changed in 1.0 in place on 2026-11-09, the day 1.0 was published and before any receiver read it.
Draft 0.3. A field with no value is left out instead of sent as null. A Quick start, and
plainer wording.
Draft 0.2. team_scores.scope can be event, with that race’s event block, for cross country
standings. A results message holds only rows with an outcome. status can be FS. Cross country
rows carry their scoring position’s points, and scoring_role says scorer or displacer.
Draft 0.1. The first draft.
Testing and conformance
1. With curl, no producer needed
Download start-list.json (or any example), then send what a
browser sends.
Probe (must return 200 with accepts and generator, and Access-Control-Allow-Origin):
curl -i http://127.0.0.1:43339/omd -H "Origin: https://manager.2timer.app"
Preflight (must return 204 with the CORS headers):
curl -i -X OPTIONS http://127.0.0.1:43339/omd -H "Origin: https://manager.2timer.app" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type"
Send (must return 200 or 204 with Access-Control-Allow-Origin):
curl -i -X POST http://127.0.0.1:43339/omd -H "Origin: https://manager.2timer.app" -H "Content-Type: application/json" --data-binary @start-list.json
Also try a made-up kind, and confirm you still answer 200 and leave the board unchanged:
curl -i -X POST http://127.0.0.1:43339/omd -H "Origin: https://manager.2timer.app" -H "Content-Type: application/json" --data-binary '{"omd":{"version":"1.0","kind":"schedule","generator":{"id":"test","name":"Test"},"sent_at":"2026-11-14T07:45:00.000Z"},"meet":{"name":"Test","start_date":"2026-11-14","end_date":"2026-11-14","sport":"tf-outdoor"},"display":{"target":"main","mode":"replace"}}'
2. The reference receiver
omd-test-receiver.mjs is a reference receiver: one Node.js
file with no dependencies (Node 18 or newer). It answers the probe, implements the required endpoint
behavior, and records what it receives, so you can compare its replies with yours or capture real
messages as test fixtures. It accepts every kind and renders nothing.
node omd-test-receiver.mjs --verbose
- Listens on
127.0.0.1:43339and accepts messages at/omdand at/api/2timer, the path 2Timer’s older scoreboard connection uses. - Options:
--port 43340,--host 0.0.0.0(accept requests from other computers),--path /scores(accept only that path),--origin https://manager.2timer.app(answer CORS for that origin only instead of*),--targets main,field(list those targets in the probe reply),--draft(behave like a receiver built against Draft 0.3: no probe answer, and messages carry"format": "meet.display.v1"), and--json(print each full message). GET /lastreturns the latest message,GET /requeststhe full history,DELETE /requestsclears it, andGET /healthreturns{ "ok": true }.
3. Check messages against the schema
Validate what you receive, or your own producer’s output, against
meet-display-v1.schema.json (JSON Schema 2020-12)
with any validator, or run omd-check.mjs
(node omd-check.mjs message.json, or --probe reply.json), which also names the conformance test
each problem fails. Check your probe reply against
probe-reply-v1.schema.json. The schemas allow unknown
fields and kinds, as receivers must, so a misspelled key passes too: check names against the
field reference. The test page sends every conformance fixture to
your receiver from your browser.
Checklist
Work through this before you ship. You do not need all of it to start: the Quick start is enough to show results. The conformance tests give each line a check and a pass condition.
A receiver:
- [ ] Answers the probe with
200,accepts, andgenerator, and the CORS header (OMD-C20, OMD-C21) - [ ] Answers every
OPTIONSwith204,Access-Control-Allow-Origin,Access-Control-Allow-Methods: GET, POST, OPTIONS, andAccess-Control-Allow-Headers: Content-Type(OMD-C12) - [ ] Answers CORS only for producer origins the operator allows (
*only while testing), withVary: Originwhen it echoes the origin (OMD-C14) - [ ] Includes
Access-Control-Allow-Originon every response, success or error (OMD-C13) - [ ] Replies
200or204within 5 seconds to every accepted message, including ignored ones (OMD-C1, OMD-C15) - [ ] Replies
400to a body that is not JSON, and422withacceptsto a message with noomdblock or a major it does not read (OMD-C16 to OMD-C18) - [ ] Treats each message as a full replacement for its
display.target, and never merges rows (OMD-C2) - [ ] Accepts and ignores kinds and modes it does not show, without changing the board (OMD-C3, OMD-C4)
- [ ] Ignores unknown fields, and shows unknown values in open lists as text or ignores them (OMD-C5, OMD-C7, OMD-C10)
- [ ] Renders rows in the order received and shows string values as sent (OMD-C9)
- [ ] Treats a missing optional field and a
nullone the same way (OMD-C6) - [ ] Handles UTF-8 names (OMD-C8)
- [ ] Documents its address, port, and path (path
/omdand port43339suggested), and its targets (OMD-C11, OMD-C23) - [ ] Is reachable from another computer when it is meant to be: listens on the network interface, and the port is open in the firewall (OMD-C19)
- [ ] Is tested from a browser-based producer, such as 2Timer or the test page, not only with
curl
A producer:
- [ ] Sends messages that pass
omd-check.mjs(OMD-C25) - [ ] Probes the receiver and sends the newest version both read, and still sends to a receiver that does not answer (OMD-C26, OMD-C27)
- [ ] Sends every message whole, with rows in display order and paged to the board (OMD-C29)
- [ ] Leaves out a field with no value (OMD-C28)
- [ ] Lets the operator set the address, port, path, and display target, and test the connection (OMD-C30)
Files
Rendered pages:
- Specification: this document
- Worked examples: every example message and the probe reply
- Conformance tests: every rule above with a pass condition
- Field reference: every field in the schema, as tables
- Test page: probe your receiver and send it the conformance fixtures from your browser
- Integration prompt: ready to paste into a coding assistant
Raw files, for tools and coding assistants. /omd/llms.txt lists them all.
| File | URL |
|---|---|
| Message schema (JSON Schema 2020-12) | /omd/schemas/meet-display-v1.schema.json |
| Probe reply schema | /omd/schemas/probe-reply-v1.schema.json |
| Reference receiver (Node 18+) | /omd/tools/omd-test-receiver.mjs |
| Checker (Node 18+, no dependencies) | /omd/tools/omd-check.mjs |
| Fixtures for the conformance tests | /omd/fixtures/index.json |
| TypeScript types | /omd/types/omd.d.ts |
| Python types (3.11+) | /omd/types/omd.py |
| This specification | /omd/spec.md |
| Conformance tests | /omd/conformance.md |
| Integration prompt | /omd/integration-prompt.md |
| Examples index | /omd/examples.md, listing each message under /omd/examples/ |
| This version, pinned | /omd/1.0/: the same raw files, never changed after a later version replaces it |
Governance
OMD was started by 2Timer, which hosts this page, and is edited in the open with the products that implement it.
- Anyone may propose a change by email. A proposal says what problem it solves for which receiver or producer.
- A change is decided with the implementers it affects. Nothing that changes what a receiver reads is made without asking the receivers who build on it, and every change is published here with its reason in the changelog.
- The versioning rules apply: a minor only adds, and only a major breaks.
License
The text of this specification is licensed under Creative Commons Attribution 4.0 (CC BY 4.0): copy, translate, and adapt it, crediting Open Meet Display. The schemas, the example messages, the reference receiver, and the integration prompt are dedicated to the public domain under CC0 1.0: copy them into your product or your test suite with no notice required.
Feedback
Write to [email protected], especially about:
- Which kinds you will support first, and in what order.
- Which
display.targetnames your product uses (boards, layers, scenes). - What your product needs that is not here (see Not in this version).
- Whether you need more than 10 rows per page (2Timer’s current maximum).
Appendix: Sending from 2Timer
How 2Timer, the first producer, sends to your receiver.
The scoreboard connection
In 2Timer the operator opens a meet, goes to Connections, and adds Open Meet Display under Scoreboard. Tell your users to choose Open Meet Display: it sends every kind. The settings are:
- Receiver IP address, Port, and Path of your receiver. 2Timer sends to
http://<IP address>:<Port><Path>. The IP address is127.0.0.1when the receiver runs on the same computer. The connection starts with port43339and path/omd, so a receiver that listens there needs only the IP address. If yours listens elsewhere, tell your users your port and path. - Display target: the
display.targetvalue sent with every message (defaultmain). Each connection sends to one target, so a venue with two boards sets up two connections. When your probe reply liststargets, 2Timer offers them as choices, and the operator can still type another. - Screens to send: Start lists, Results, Team scores, and Announcements, all on to start.
The operator can turn some off for a board, and at least one stays on.
clearis always sent when the operator clears the board. Every kind your receiver does not show must still be accepted and ignored (see Ignore-unknown rules). - Name shown on board: the
participant_nameformat for start lists and results: Last name, First initial + last name, Last name, first initial, First name + last initial, First name + last name, or Last name, first name. It starts at First name + last name, which keeps names that are not written first-name/surname, such as many Malay and Chinese names, whole. Tell your users to choose Last name only for narrow boards. Relay rows always send the relay team name, and eachrelay_legsname uses the same format. - Rows per page (default 8, maximum 10) and When there are more rows than fit: Show only the
top rows, or Cycle through all rows every N seconds (default 5). So a
start_listorresultsmessage from 2Timer holds at most 10 rows.
What 2Timer puts in a message
omd.generatoris{ "id": "2t", "name": "2Timer", "version": "<its version>" }.meet.idis the meet’s stable identity, a UUID that stays the same across days and restores.display.connection_idis<meet.id>_<the connection's number in that meet>. It can repeat across two computers restored from the same meet file.announcement.priorityisnormal, andannouncement.expires_atis not sent yet.
Which version 2Timer sends
2Timer probes your receiver when the operator presses Test connection, and before a board connection’s first send in a session, and keeps the answer for that session.
- A valid probe reply that reads 1.0: 2Timer sends 1.0.
- No reply, or one that is not a probe reply: 2Timer sends the Draft 0.3 message
(
"format": "meet.display.v1"), which every receiver built before 1.0 reads. This is temporary; a later 2Timer release sends 1.0 to every receiver.
Test connection
Test connection sends OPTIONS, then the probe, and tells the operator in plain words what it
learned, for example Reached ExampleBoard 2.4. It reads Open Meet Display 1.0., or Reached the
receiver. It did not say which version it reads, so 2Timer sends the earlier draft.
When a send fails
2Timer tells the operator one of: Chrome blocked the request (local network access not allowed),
the receiver could not be reached (not running, wrong address, firewall, or CORS; the browser does
not say which), or the status your receiver replied with, with your errors message when you send
one. The board keeps whatever it showed before.
End to end
- Start your receiver (or the reference receiver) on the computer running the browser.
- In 2Timer, add Open Meet Display under Scoreboard with IP address
127.0.0.1, your port and path, and a Display target your product understands. - Press Test connection, and check it names your product and says it reads 1.0.
- Send a start list and then results from the Scoreboard page, and check they render. Then try team scores, an announcement, and Clear board if your receiver supports them.
- Repeat with the receiver on another computer: enter that computer’s IP address, send from Chrome, and click Allow on the local-network prompt.
