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 estop on 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_timer restores 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 error events. Most rejections are timer_active — a period is already running.

Source

Firmware, web application, hardware design and this documentation are in one repository.

github.com/Elevation-Robotics/OpenFC