Decoding the Daktronics Omnisport 2000

January 8, 2013


This is an extended version of the README on github.

Daktronics Omnisport 2000 timing console and television graphics.

This project came out of neccessity after a software-based attempt failed. I really wanted to avoid working with hardware on the system because it takes much longer than pure software, but in the end I had to for the sake of simplicity.

timing console

The timing console has three RS232 serial ports: one for the meet manager, one for a venus scoreboard, and another to connect to the PC control software. The PC software from daktronics costs a ton, so I didn't bother trying to work with that. The meet manager port is obviously used for the results, so I couldn't use that either. That left the RTD scoreboard port, which is what is used for this decoder.


The port is for a scoreboard, so logically it should at least have a running-time signal. After looking at it on a serial terminal, I figured out that it sends split times and place data as well.

Raw data from the console looks like this:


It looks like a mess, because it is. Actual data is embedded between keepalive signals, which are then contained within control characters. The periods in the example are not actually ASCII periods (0x2E), but rather control characters that cannot be rendered in the serial terminal. Browsers, however, can render them properly, and this is what a typical data packet looks like:

000000000042100000␂    4.0  C0

The control characters are bolded. Everything between SYN (0x16) and STX (0x02) is irrelevant to the actual data, so it is dropped from the decoded signal. I still haven't figured out what the 0042100000 represents, but it might just be another sync signal like the string of zeroes after SYN. The C0 is probably a checksum, but I didn't bother working it out. 


\n and sequential sorting

first stage

The first stage of sorting applies these filters to all packets:

  1. Remove data between SYN and STX (inclusive)
  2. Remove data between EOT and ETB (inclusive)
  3. Trim leading and trailing whitespace
  4. Add a linefeed and carriage return to the end




secondary stage

The first stage of filtering returns these (names are starred out). Significantly better than the raw data, but still a lot of noise considering all I want is the running time and splits. 

One more filter is applied after the first stage:

  1. Remove packets that do not have a period (ASCII 0x2E)


This cleans out everything except running times and splits.



MAX232 to TTL signal schematic.

Not a lot in terms of hardware for this one.


Out of the 9 pins from the console, only two are used: TX and ground. The signal is fed into the classic MAX232 to   convert it down to TTL logic levels. 







The latest version will always be on github.

String inputString = "";         // a string to hold incoming data
String oldString = "";
char rxbuffer[BUFFER_STRING_SIZE];

void setup() {
  UCSR0A = 1<<U2X0; // async
  UCSR0B = (1<<RXCIE0) | (1<<UDRIE0) | (1<<RXEN0) | (1<<TXEN0); // enable RXTX, register interrupts
  UCSR0C = (1<<UCSZ01) | (1<<UCSZ00); // 8 data bits
  UBRR0L = 103; // 19.2k baud

  DDRB |= (1<<PORTB0); // status LED output
  DDRB |= (0<<PORTB1); // split switch input  
  if((PINB & (1<<PORTB1))) {
    flash_status_led(4, 80);
    while(1) {}; // enter infinite loop

void loop() {
  while(1) {
    while (Serial.available()) {
      if(Serial.read() == 2 && Serial.readBytesUntil((char)4, rxbuffer, BUFFER_STRING_SIZE)) {
          for(int x = 0; x < BUFFER_STRING_SIZE; x++) { // add chars to string
            if((int)rxbuffer[x] != 0) inputString += rxbuffer[x]; // skip null values; add to string
          inputString.trim(); // remove leading and trailing whitespace
          if(inputString.indexOf(".") != -1) { // if string does not include time, skip
            if(inputString.length() <= 7) {
              Serial.print("t"); // t for time
            else if((PINB & (1<<PORTB1)) == 0 && oldString != inputString) { // if split is different from last split transmission
              PORTB = (1<<PORTB0);
              Serial.print("s"); // s for split
              Serial.println(inputString.substring(0,4) + ' ' + inputString.substring(4));
              oldString = inputString;
              PORTB = (0<<PORTB0);
          inputString = "";
          memset(rxbuffer,0, BUFFER_STRING_SIZE); 

void RUN_TEST_MODE(void) {
  for(unsigned int x=0; x<5000; x++) {
    Serial.println(x*0.1, 1);
    if(x == 33) Serial.println("s5 1 3.36 2"); 
    if(x == 45) Serial.println("s3 2 4.52 2"); 
    if(x == 65) Serial.println("s7 3 6.50 2"); 
    if(x == 150) Serial.println("s3 1 15.05 4"); 
    if(x == 165) Serial.println("s5 2 16.54 4"); 
    if(x == 166) Serial.println("s7 3 16.60 4");        

void flash_status_led(byte number_of_flashes, unsigned int milliseconds) {
  for(byte x = 0; x<number_of_flashes; x++) {
    PORTB = (1<<PORTB0);
    PORTB = (0<<PORTB0);


decoded data

There are two types of data packets: running time and splits. Each packet is composed of a prefix (t for running time and s for splits), the packet data, and a carriage return, which serves as the delimiting character.

Typical data packets would look like this:

t1:02.1 - pretty self explanatory

s3 1 1:11.63 2 - four data points, which are ordered as follows:

  1. Lane number
  2. Place
  3. Time (split times are accurate to a hundredth of a second)
  4. Laps of the pool completed

Decoded data is fed into Autobahn with python and then to the browser for graphic rendering. The websockets server and rendering engine is in the IASAS_swimming_2013 repository.


test mode

The decoder can be put into test mode by resetting with the split button set to the OFF position. This mode ignores data from the console and simulates a test race, which is useful for testing the rendering engine.



comments powered by Disqus