Skip to content

How MARLIN works

Advanced

Nothing on this page is needed to write a robot program. Read it when you are debugging, or when you want to know why the extension has to stay open.

MARLIN has four active pieces during a match:

  1. your program;
  2. the MARLIN VS Code extension on the laptop;
  3. the Controller, connected to the laptop over USB serial; and
  4. the robot, connected to the Controller over a Bluetooth (BLE) link.

The most important thing to understand is that the laptop stays in the loop. Your program runs on the laptop. The extension runs on the laptop. The extension owns the USB connection to the Controller. Your program talks to the extension through a local connection, not directly to hardware.

Big picture

flowchart LR
    subgraph laptop["Laptop / VS Code"]
        ui["MARLIN control station"]
        extension["Extension runtime"]
        link["Local MARLIN link"]
        script["Your program"]
        api["marlin Python API"]

        ui <--> extension
        extension <--> link
        script <--> api
        api <--> link
    end

    controller["Controller"]
    robot["Robot"]

    extension <-->|"USB serial"| controller
    controller <-->|"Bluetooth (BLE) link"| robot

The extension has two jobs at the same time:

  • it receives state from the Controller and robot
  • it sends commands from your program and the control station.

That means packets are moving in both directions throughout a match.

Why the extension owns USB

Only one program should own the Controller serial port. MARLIN gives that job to the VS Code extension so it can coordinate:

  • program launch and stop
  • Controller serial discovery and connection
  • robot status packets
  • Controller input packets
  • match commands such as Pause, Auton, and Driver
  • E-Stop and clear E-Stop commands
  • pool server integration.

If each program opened USB directly, the extension could not reliably stop programs, show live status, or send emergency commands independently of your code.

What happens when you start a phase

sequenceDiagram
    participant You
    participant UI as MARLIN control station
    participant Py as Your Python process
    participant API as marlin API
    participant Ext as MARLIN extension
    participant Ctl as Controller
    participant Bot as Robot

    You->>UI: Press Auton or Driver
    UI->>Ext: Start the open project's program
    Ext->>Ext: Open the local MARLIN link
    Ext->>Py: Launch src/main.py
    Py->>API: import marlin and create Robot()
    API->>Ext: Connect to the local link
    API->>Ext: Sync time with the robot brain
    Ext-->>API: Report the laptop-to-robot time offset
    API->>Ext: Send hardware configuration
    Ext->>Ctl: Send config over USB serial
    Ctl->>Bot: Forward config
    Bot-->>Ctl: Echo parsed config/status
    Ctl-->>Ext: Forward robot packet
    Ext-->>API: Report config echo/state
    API->>Py: Run initialize/autonomous/driver callbacks

After that startup path, the system becomes a continuous loop: the extension keeps reading packets, the Python API keeps caching state, and your code keeps updating command setpoints.

Continuous packet flow

During a match, state travels one way and commands travel the other. Solid arrows carry state towards your code. Dotted arrows carry commands back to the hardware:

flowchart LR
    robot["Robot\nbattery, sensors, status"]
    controller["Controller\nbuttons, joysticks"]
    extension["Extension"]
    api["marlin API\nstate cache"]
    code["Your code"]
    station["Control station\nPause / Auton / Driver / E-Stop"]

    robot --> controller --> extension --> api --> code
    code -.-> api -.-> extension -.-> controller -.-> robot
    station --> extension

This is why reads in the Python API are non-blocking. When your code asks for robot.controller.joystick("ONE").value or robot.imu("S1").read(), it receives the most recent value cached by the API. It does not stop and wait for a fresh hardware packet.

Controller packets and robot packets

The Controller can send state about itself and also forward state from the robot. MARLIN treats those as different kinds of information:

Packet source Examples Used by
Controller joystick axes, pressed buttons robot.controller
Robot battery voltage, link health, sensor data robot.battery_voltage, robot.link_ok, sensor reads

Threads and “many things at once”

It can feel like MARLIN is doing many things simultaneously because it is:

  • VS Code keeps the extension UI responsive.
  • The extension reads and writes serial data.
  • The extension listens for the Python API over the local MARLIN link.
  • Your program runs in its own Python process.
  • The Python API maintains cached state and command buffers.

The rule you use while programming is simpler:

Write your robot behaviour as normal Python callbacks. MARLIN handles the communication loops around your code.

Inside a callback, use while robot.running: for behaviour that should continue through the current phase. MARLIN exits that loop when the phase ends.

What your code should not do

Your code should not:

  • open the Controller serial port
  • speak the USB packet protocol directly
  • try to connect to the robot's Bluetooth (BLE) link
  • implement its own E-Stop path
  • run long blocking operations inside a phase loop without checking robot.running.

Your code should:

  • declare hardware with robot.configure(...)
  • read cached state from robot.controller, sensors, and robot properties
  • command effectors through robot.thruster(...) and robot.servo(...)
  • let the extension own hardware communication and safety controls.