Open Meet Display

Version 1.0. A minor version only adds; only a major breaks. Comments to [email protected].

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 results is 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

  1. Run an HTTP server on port 43339 that accepts requests at /omd.
  2. Answer OPTIONS with the CORS headers in Required endpoint behavior.
  3. Answer GET /omd with the probe reply: { "accepts": ["1.0"], "generator": { "id": "yourboard", "name": "YourBoard" } }.
  4. Reply 200 { "ok": true } to every POST. Read omd.kind. For results (or start_list), draw results.rows (or start_list.rows) top to bottom: place, participant_name, team.short_name, result. For any kind you do not show yet, change nothing.
  5. 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 results message 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.
  • clear blanks the target. See clear.
  • 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_at is 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_assignment is 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).
  • seed is 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 a status (dnf, dns, dq, fs, scr, or nr). 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 by status.

  • place is the printed place within what the message covers: within the heat or flight when event.section_name is set, otherwise within the round. Ties repeat the place and skip the next one (3, 3, 5). For a row with a status, place holds what a results sheet prints for it (DNF, DQ, DNS, and NT, NM, or NH for nr), so a simple board can print the place column as it is.

  • result is 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_name is then the relay team name (for example Riverside High School A), and relay_legs lists 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 no event.
  • event: standings for one race, as in cross country. The message carries that race’s event block, the same fields a start list or results message carries, without round, section, or wind. Match event.id to show the standings beside the race’s results, or title the board with event.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 /omd and the port 43339 unless 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. 200 with the reply above when you are ready to show messages; 503 with Retry-After when 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 carries Access-Control-Allow-Origin. A plain GET needs 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 no accepts), 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-Origin names the producer origins your operator allows, such as https://manager.2timer.app. Echo back the request’s Origin when it is on your list, and send Vary: Origin whenever you echo it, so a cache never serves one site’s answer to another. Use * only for testing. See Security for why.
  • OPTIONS and GET are listed in Access-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 POST response 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 2xx is 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 curl ignore CORS, so on a shared venue network you may let the operator choose which computers can send.
  • Treat display.connection_id as 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.generator and omd.sent_at makes 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’s name says it; its code is 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.version is MAJOR.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 422 and accepts, rather than showing what it half-understands.
  • One schema URL per major, republished in place with each minor.
  • A receiver that accepts 1.0 is sent any 1.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 omd block, like OMX’s omx block. omd.version, omd.kind, omd.generator, and omd.sent_at replace format, display.kind, source, and sent_at. source.connection_id is now display.connection_id.

  • OMX’s names for the same facts:

    Draft 0.3 1.0
    format: "meet.display.v1" omd.version: "1.0"
    display.kind omd.kind
    source.name, source.version omd.generator.name, version
    sent_at omd.sent_at
    source.connection_id display.connection_id
    meet.date_start, date_end meet.start_date, end_date
    event_number, event_name, event_code event.number, name, code
    round_number, round_name event.round, round_label
    bib_number bib
    affiliation team
    mark result
    team_points points
  • Statuses are OMX’s lowercase codes, and a row with a place has none: OK is gone.

  • One letter per gender, as in OMX: M, F, X (non-binary), and C for a mixed event or combined standings; X now means non-binary. team_scores.gender is always present. Changed in 1.0 in place on 2026-11-10, before any receiver read 1.0 (it was mixed, and left out for combined standings).

  • event.code is OMX’s event code, left out when OMX has none.

  • The probe: a receiver answers GET with accepts and generator.

  • Replies are OMX’s: accept with 200 or 204; refuse with 400 or 422 and an optional accepts and errors body; another major or no omd block is 422.

  • Vary: Origin when 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.rows and results.rows, as team standings’ were already team_scores.rows; Draft 0.3 sent them as top-level rows. 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:43339 and accepts messages at /omd and 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 /last returns the latest message, GET /requests the full history, DELETE /requests clears it, and GET /health returns { "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, and generator, and the CORS header (OMD-C20, OMD-C21)
  • [ ] Answers every OPTIONS with 204, Access-Control-Allow-Origin, Access-Control-Allow-Methods: GET, POST, OPTIONS, and Access-Control-Allow-Headers: Content-Type (OMD-C12)
  • [ ] Answers CORS only for producer origins the operator allows (* only while testing), with Vary: Origin when it echoes the origin (OMD-C14)
  • [ ] Includes Access-Control-Allow-Origin on every response, success or error (OMD-C13)
  • [ ] Replies 200 or 204 within 5 seconds to every accepted message, including ignored ones (OMD-C1, OMD-C15)
  • [ ] Replies 400 to a body that is not JSON, and 422 with accepts to a message with no omd block 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 null one the same way (OMD-C6)
  • [ ] Handles UTF-8 names (OMD-C8)
  • [ ] Documents its address, port, and path (path /omd and port 43339 suggested), 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:

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:

  1. Which kinds you will support first, and in what order.
  2. Which display.target names your product uses (boards, layers, scenes).
  3. What your product needs that is not here (see Not in this version).
  4. 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 is 127.0.0.1 when the receiver runs on the same computer. The connection starts with port 43339 and 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.target value sent with every message (default main). Each connection sends to one target, so a venue with two boards sets up two connections. When your probe reply lists targets, 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. clear is 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_name format 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 each relay_legs name 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_list or results message from 2Timer holds at most 10 rows.

What 2Timer puts in a message

  • omd.generator is { "id": "2t", "name": "2Timer", "version": "<its version>" }.
  • meet.id is the meet’s stable identity, a UUID that stays the same across days and restores.
  • display.connection_id is <meet.id>_<the connection's number in that meet>. It can repeat across two computers restored from the same meet file.
  • announcement.priority is normal, and announcement.expires_at is 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

  1. Start your receiver (or the reference receiver) on the computer running the browser.
  2. 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.
  3. Press Test connection, and check it names your product and says it reads 1.0.
  4. 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.
  5. Repeat with the receiver on another computer: enter that computer’s IP address, send from Chrome, and click Allow on the local-network prompt.