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:
- your program;
- the MARLIN VS Code extension on the laptop;
- the Controller, connected to the laptop over USB serial; and
- 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(...)androbot.servo(...) - let the extension own hardware communication and safety controls.