Documentation
OpenFC is open hardware, open firmware and an open protocol. Nothing here is proprietary — you can build the board, build the firmware, and drive it from your own software.
Where to start
- Serial protocol — every command and event, and the safety model behind them.
- Firmware API — verify a board is running official, competition-certified firmware.
- Hardware — schematic, pin map and build notes.
- License — Apache-2.0 for software, CERN-OHL-P-2.0 for hardware; free to build, modify and sell.
How integration works
There is no daemon, no service and no SDK requirement. The board is a USB serial device that exchanges JSON lines at 115200 baud. Your application opens the port and writes commands.
The only rule that really matters:
To run a match, use
start_period. The board owns the countdown, so the match completes even if your application crashes or the cable is unplugged. If you instead enable manually with set, you must send a command at least every 600 ms or the board disables itself.Python
pip install pyserial
import json, serial
port = serial.Serial('COM3', 115200, timeout=0.1)
port.dtr = port.rts = True # assert both, or neither — see the protocol page
def send(**kw):
port.write((json.dumps(kw) + '\n').encode())
def read():
line = port.readline().decode(errors='replace').strip()
return json.loads(line) if line.startswith('{') else None
send(cmd='get_info') # identity + fw_hash
send(cmd='start_period', mode='auto', duration_ms=15000)
while True:
evt = read()
if not evt:
continue
if evt.get('evt') == 'timer_expired':
print('autonomous complete')
break
if evt.get('evt') == 'error':
raise SystemExit(f"rejected: {evt['reason']}")Node.js
npm install serialport
import { SerialPort } from 'serialport';
import { ReadlineParser } from '@serialport/parser-readline';
const port = new SerialPort({ path: '/dev/ttyUSB0', baudRate: 115200 });
const lines = port.pipe(new ReadlineParser({ delimiter: '\n' }));
const send = (obj) => port.write(JSON.stringify(obj) + '\n');
lines.on('data', (line) => {
let evt;
try { evt = JSON.parse(line); } catch { return; } // ignore boot chatter
if (evt.evt === 'hello') console.log('board', evt.device_id, evt.fw_version);
if (evt.evt === 'timer_expired') console.log('period over');
if (evt.evt === 'estop') console.log('disabled:', evt.source);
});
send({ cmd: 'get_info' });
send({ cmd: 'start_period', mode: 'driver', duration_ms: 105000 });Browser (Web Serial)
Chrome, Edge and Opera can talk to the board directly. This is how this site works — no installation, no native code.
requires a user gesture and a secure context
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 }); // do not set flowControl
const encoder = new TextEncoderStream();
encoder.readable.pipeTo(port.writable);
const writer = encoder.writable.getWriter();
const send = (obj) => writer.write(JSON.stringify(obj) + '\n');
const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();
await send({ cmd: 'start_period', mode: 'auto', duration_ms: 15000 });Chrome remembers the permission, so navigator.serial.getPorts() can reopen a previously approved board without prompting — which is what makes automatic reconnection mid-match possible.
Manual control with a keepalive
If you need free-running enable rather than a fixed period, you own the keepalive. Ping well inside the 600 ms window.
python
import threading
send(cmd='set', enable=True, mode='driver')
stop = threading.Event()
def keepalive():
while not stop.wait(0.25): # 250 ms, comfortably under the 600 ms limit
send(cmd='ping')
threading.Thread(target=keepalive, daemon=True).start()
# ... on shutdown, always disable explicitly rather than relying on the watchdog
stop.set()
send(cmd='estop')Practical notes
- Always
estopon exit. The watchdog will catch a crash, but an explicit disable is immediate. - Ignore non-JSON lines. The ESP32 bootloader prints plain text at reset.
- Re-query after reconnecting.
get_info+get_state+get_timerrestores your view of a match already in progress. - Trust
remaining_ms, not your own clock. The board is authoritative; a host-side timer will drift. - Handle
errorevents. Most rejections aretimer_active— a period is already running.
Source
Firmware, web application, hardware design and this documentation are in one repository.