No description
This repository has been archived on 2026-06-13. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
Find a file
2026-06-13 18:42:12 +02:00
examples/readout Move main.go into examples directory 2026-06-13 18:42:12 +02:00
protocol Fixes before computer reset 2026-06-08 22:25:57 +02:00
reader Make read handle data arriving in smaller chunks 2026-06-13 11:29:46 +02:00
result Add documentation 2026-06-01 14:51:59 +02:00
go.mod Fix module name again 2026-05-28 22:38:37 +02:00
go.sum Initial commit supporting readouts of SI5, SI6 and SI8 2026-05-19 23:14:55 +02:00
LICENSE Add license 2026-06-02 22:50:52 +02:00
README.md Add documentation 2026-06-01 14:51:59 +02:00

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 startTime argument if non-nil, otherwise the card's start punch, otherwise ErrNoStartTime. A fixed start is typical for mass-start events.
  • Status is OK only when every required control is present in order and a finish punch exists; otherwise NOK. 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 nil split and total; the next control that is present has a nil split (the leg can't be measured across the gap) but a total measured from the start; later legs resume reporting both. nil is 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.