Decoding the Daktronics Omnisport 2000
January 8th, 2013
Projects
This is an extended version of the README on github.
At our pool, we had a Daktronics Omnisport 2000 timing console that we wanted to link up to broadcast graphics. This hardware-based 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.
signal
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:
0.0 .1D..00000000.0042100000. 0.1 .BD..00000000.0042101025. 0.1 .C5..00000000.0042101036. 0.68..00000000.0042100222. 1 .05..00000000.0042101100. .90..00000000.0042100000. 0.2 .BE..00000000.0042101025. 0.2 .C6..00000000.0042100258.Matsuyama, Neo BUCS 2 .1C..00000000.0042101109. .99..00000000.0042100000. 0.3 .BF..00000000.0042101025. 0.3 .C7..00000000.0042100294.Hodowany, Kyle TAS 3 .02..00000000.0042101118. .99..00000000.0042100000. 0.4 .C0..00000000.0042101025. 0.4 .C8..00000000.0042100330.Huang, Miles ISB-D 4 .70..00000000.0042101127. .99..00000000.0042100000. 0.5 .C1..00000000.0042101025. 0.5 .C9..00000000.0042100366.Nakazawa, Kai SAS 5 .88..00000000.0042101136. .99..00000000.0042100000. 0.6 .C2..00000000.0042101025. 0.6 .CA..00000000.0042100402.Fitzgerald, AnnISHCM 6 .73..00000000.0042101145. .99..00000000.0042100000. 0.7 .C3..00000000.0042101025. 0.7 .CB..00000000.0042100438.Anderson, JeremTAS 7 .35..00000000.0042101154. .99..00000000.0042100000. 0.8 .C4..00000000.0042101025. 0.8 .CC..00000000.0042100474. .FD..00000000.0042101163. .99..00000000.0042100000. 0.9 .C5..00000000.0042101025. 0.9 .CD..00000000.0042100510. .F4..00000000.0042101172. .99..00000000.0042100000. 1.0 .BD..00000000.0042101025. 1.0 .C5..00000000.0042100546. .FD..00000000.0042101181. .99..00000000.0042100000. 1.1 .BE..00000000.0042101025. 1.1 .C6..00000000.0042100968. .85..00000000.0042100000. 1.2 .BF..00000000.0042101025. 1.2 .C7..00000000.0042100009.Mixed 10&U 100 Fly .E6..00000000.0042100039.Timed Finals .2A..00000000.0042100000. 1.3 .C0..00000000.0042101025. 1.3 .C8..00000000.0042100069.X 10&U 100 Fly Timed Finals .BD..00000000.0042100099. 5 1 F 4.80..00000000.0042100000. 1.4 .C1..00000000.0042101025. 1.4 .C9..00000000.0042100128. 0.C9..00000000.0042100130. .52..00000000.0042100000. 1.5 .C2..00000000.0042101025. 1.5 .CA..00000000.0042100153. .57..00000000.0042100176. .5C..00000000.0042100199. .61..00000000.0042100000. 1.6 .C3..00000000.0042101025. 1.6 .CB..00000000.0042100000. 1.7 .C4..00000000.0042101025. 1.7 .CC..00000000.0042100000. 1.8 .C5..00000000.0042101025. 1.8 .CD..00000000.0042100000. 1.9 .C6..00000000.0042101025. 1.9 .CE..00000000.0042100000. 2.0 .BE..00000000.0042101025. 2.0 .C6..00000000.0042100000. 2.1 .BF..00000000.0042101025. 2.1 .C7..00000000.0042100000. 2.2 .C0..00000000.0042101025. 2.2 .C8..00000000.0042100000. 2.3 .C1..00000000.0042101025. 2.3 .C9..00000000.0042100000. 2.4 .C2..00000000.0042101025. 2.4 .CA..00000000.0042100000. 2.5 .C3..00000000.0042101025. 2.5 .CB..00000000.0042100000. 2.6 .C4..00000000.0042101025. 2.6 .CC..00000000.0042100000. 2.7 .C5..00000000.0042101025. 2.7 .CD..00000000.0042100000. 2.8 .C6..00000000.0042101025. 2.8 .CE..00000000.0042100000. 2.9 .C7..00000000.0042101025. 2.9 .CF..00000000.0042100000. 3.0 .BF..00000000.0042101025
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:
␖00000000␁0042100000␂ 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:
- Remove data between SYN and STX (inclusive)
- Remove data between EOT and ETB (inclusive)
- Trim leading and trailing whitespace
- 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:
- Remove packets that do not have a period (ASCII 0x2E)
This cleans out everything except running times and splits.
hardware
Not a lot in terms of hardware for this one, just a simple RS232 to TTL converter.
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.
software
The latest version will always be on github.
#define BUFFER_STRING_SIZE 50
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);
RUN_TEST_MODE();
while(1) {}; // enter infinite loop
}
inputString.reserve(BUFFER_STRING_SIZE);
oldString.reserve(BUFFER_STRING_SIZE);
}
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
Serial.println(inputString);
}
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.print(“t”);
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”);
delay(100);
}
}
void flash_status_led(byte number_of_flashes, unsigned int milliseconds) {
for(byte x = 0; x<number_of_flashes; x++) {
PORTB = (1<<PORTB0);
delay(milliseconds);
PORTB = (0<<PORTB0);
delay(milliseconds);
}
}
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 explanatorys3 1 1:11.63 2
- four data points, which are ordered as follows:
- Lane number
- Place
- Time (split times are accurate to a hundredth of a second)
- Laps of the pool completed
Decoded data is fed into Autobahn (a websockets bridge) 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.