- Go 100%
| examples/readout | ||
| protocol | ||
| reader | ||
| result | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
sportident-go
A Go implementation of the SportIdent protocol, along with utilities for turning a card readout into race results.
SportIdent is a timing system used in orienteering and similar sports. Competitors carry a contactless card (an "SI-card") that records a punch — a control code and a time of day — at every control they visit. This library reads those cards from a master station, decodes them, reconstructs absolute punch timestamps, and evaluates them against a course.
Installation
go get git.cheesemans.dev/cheesemans/sportident-go
Requires Go 1.25+. Reading from a physical station depends on
go.bug.st/serial; the protocol and
result packages have no external dependencies.
Packages
| Package | Responsibility |
|---|---|
protocol |
Command framing/CRC, card-type detection, and decoding card memory into a SportIdentCard. Also reconstructs absolute punch timestamps. |
reader |
Drives a master station over a serial port: waits for a card, reads its memory, returns a decoded card. |
result |
Evaluates a decoded card against a course and computes split/total times. |
The root package is a small example program that reads cards in a loop and prints them.
Usage
Reading cards from a station
r, err := reader.NewSportIdentReader("/dev/ttyUSB0")
if err != nil {
log.Fatal(err)
}
defer r.Close()
for {
card, err := r.ReadCard() // blocks until a card is inserted
if err != nil {
log.Fatal(err)
}
fmt.Println(card) // punch times are already resolved
}
ReadCard decodes the card and calls ResolveTimes for you, so each
Punch.Time holds an absolute time.Time.
Decoding without a station
If you already have a card-memory command (for example captured from hardware or a test fixture), decode it directly:
card := protocol.DecodeCard(cmd, time.Now())
card.ResolveTimes()
Computing results
course := []uint32{31, 32, 33, 34}
res, err := result.CalculateResult(card, course, nil)
if err != nil {
log.Fatal(err) // e.g. result.ErrNoStartTime
}
if res.Status == result.OK {
fmt.Printf("Finished in %s\n", *res.TotalTime)
}
How timestamps are resolved
A card stores only a time of day per punch — plus a weekday on everything
newer than the SI5. It has no notion of a date. ResolveTimes reconstructs the
real timestamps by walking backwards from DecodedAt (the moment the card
was read): the most recent punch is pinned to the latest matching instant at or
before DecodedAt, and each earlier punch is pinned relative to the punch that
follows it.
This relies on two assumptions:
- The card is read reasonably soon after the race. For the SI5 this matters most: it stores only a 12-hour time with no am/pm flag, so a card read more than 12 hours after the last punch cannot be disambiguated.
- The stations are correctly programmed. A full week is allowed to elapse between punches, so a Tuesday punch sitting between two Monday punches is correctly placed ~6 days earlier.
Timestamps are resolved in the location of the DecodedAt value (local time
when read via reader).
Results model
CalculateResult matches the card's punches against the course in order. Extra
punches in between are ignored, but the required controls must appear in the
course order.
- Start time is resolved as: the explicit
startTimeargument if non-nil, otherwise the card's start punch, otherwiseErrNoStartTime. A fixed start is typical for mass-start events. StatusisOKonly when every required control is present in order and a finish punch exists; otherwiseNOK. The status is intentionally binary — distinctions such as DNS/DNF/DSQ are left to event-administration software.- Missing controls do not stop reporting. A missing control has
nilsplit and total; the next control that is present has anilsplit (the leg can't be measured across the gap) but a total measured from the start; later legs resume reporting both.nilis used rather than a zero duration so a missing control can never be confused with a zero-length split.
Card support
| Card version | Support |
|---|---|
| SI5 | ✅ |
| SI6 | ✅ |
| SI8 | ✅ |
| SI9 | ✅ |
| SI10 | ❌ |
| SI11 | ❌ |
| SIAC | ✅ |
| pCard | ❌ |
Roadmap
Readout of cards is the current focus. Possible future work:
- Control the SI master station (change mode, bump punches from a remote station).
- Program stations (set control number, synchronize time, set beacon mode).
- Full SIAC readout (clear/start/finish reserve, battery voltage, subsecond punches) — low priority.
- Read personal data stored on cards — low priority.