Skip to main content

Hello Main Loop Example

This introductory example covers getting set up, interacting with the Amiga, and using auto-control mode to drive your Amiga from a computer using the farm-ng microcontroller kit.

This example enables driving the Amiga by entering simple fwd / rev / left / right keyboard commands the serial port, which the app sends over the CAN bus.

Parts required

Code Breakdown

Imports from lib/


The MainLoop class is used throughout the application layer of the farm-ng firmware. MainLoop contains generic functionality we use on our pendant, dashboard, and auxiliary components for constant looping, receiving of CAN messages, sending of regular status updates called Heartbeats, and more. The MainLoop takes an AppClass in the constructor, and the AppClass is expected to contain a method called iter that is called every in every iteration (also called iter) of the MainLoop.

The _register_message_handlers() method is an important feature to note. This method adds parsing directly into the MainLoop so the App only receives the desired CAN messages. Because messages sent on the CAN bus are seen by all other components it is important to efficiently filter out irrelevant messages on the resource constrained microcontrollers.

Take it further:

Try to add an additional message parser for one of the other messages on the CAN bus. For instance, if you have a pendant connected to your CAN bus you could add something like:

from farm_ng.utils.packet import  PENDANT_NODE_ID, PendantState

def _register_message_handlers(self):
self.main_loop.command_handlers[CanOpenObject.TPDO1 |
DASHBOARD_NODE_ID] = self._handle_amiga_tpdo1
self.main_loop.command_handlers[CanOpenObject.TPDO1 | PENDANT_NODE_ID] = self._handle_pendant_state

def _handle_pendant_state(self, message):
pendant_state = PendantState.from_can_data(

All messages on the bus can be found by using the cansniffer example app. You can compare the detected CAN ID's to those in CanOpenObject. But keep in mind, node id is added to the function code for the full CAN Id, as you'll see below in CanOpenObject / DASHBOARD_NODE_ID.


The TickRepeater class is a useful utility that we recommend taking advantage of throughout your custom implementations. We use "repeaters" to limit the frequency of certain actions, by only performing the action once the period of the repeater has past, when compared to the last time the action was performed. The check() method returns False until the checkpoint has past, and True once the checkpoint is past. When True is returned, the repeater is updated to the next checkpoint, so you really only need the check() method in most applications.

The TickRepeater is what we call a "catch-up" repeater, in which the the next checkpoint is the ticks_period_ms (period in ms) added to the last checkpoint (rather than the next checkpoint being the ticks_period_ms added to the time of last execution). As you can infer, there's no reason to use one of these catch-up repeaters if the check() will be called less frequently than the ticks_period_ms used in the constructor.


We use ticks_ms which wraps every 2^29 ms (~6.2 days). Our logic handles a single wrap, but we do not detect two wraps as we use this in periods more on the 100 ms timescale. If you are creating a long duration application, just make sure your period is less than 6 days and that the check is called at least that often.

See the supervisor.ticks_ms() docs for more details about ticks_ms.


Wrapper for CAN packet used for auto mode controls of the Amiga. Provide theAmigaRpdo1 object with a requested AmigaControlState, speed, and angular rate. Then pack this into a canio.Message and send this message over the bus.


This is a request for a specific AmigaControlState, angular rate, and linear velocity sent to the dashboard. The dashboard, operating as the vehicle control unit (VCU), has built-in logic to prevent unsafe speeds, accelerations, control state transitions, etc.


Wrapper for CAN packet used for sending state of the Amiga, including AmigaControlState. Unpack the message to see the current AmigaControlState, speed, and angular rate of the robot.

There is a convenient util function from_can_data that unpacks the message directly into an AmigaTpdo1 object.


Control state of the Amiga.


We mostly follow the CANopen standards. A recommended reading is the CSS Electronics CANopen tutorial.


Some of the third-party, auxiliary components we have integrated into the system do not allow for strict adherence to the CANopen standards. For our core system, we adhere closely to the standards.

In this standard, messages are passed using function codes based on their use. Each component has a node ID identifier used to identify either the intended recipient or the source component of each message sent on the CAN bus. In the current example, we send requested commands to the Amiga on the RPDO1 channel, and receive responses streamed from the Amiga on the TPDO1 channel. These are differentiated from pendant or motor controller RPDO/ TPDO command sets by sending them with the dashboard node ID.

tip (or is the default name for the executable Python file on microcontrollers flashed with CircuitPython. You'll see we stick to the convention with our files.


Here we create HelloMainLoopApp as a simple example of the types of AppClass you can create.

In our HelloMainLoopApp constructor, we create a TickRepeater that will stream the automatic control command to the dashboard every 50 ms (at a 20hz rate).

In our iter() call, we:

  • Check for control keys entered into the serial console [<space bar> for toggling auto mode, & w / a / s / d[fwd / left / rev / right] for adjusting velocities].
  • Parse through all received CAN messages, sorting only for the AmigaTpdo1 responses coming from the dashboard.
  • Send the most up-to-date auto control commands, based on serial console entries, in an AmigaRpdo1 formatted packet.


1. Connection

Connect your microcontroller as in the following diagram:


2. Load the code

From amiga-dev-kit/circuitpy/, drop the file and the lib/ folder directly into the root of the mounted CIRCUITPY drive, as seen below.


This assumes you have already cloned the amiga-dev-kit repo.

cd <to_your_base_directory>
git clone


3. Open the Serial Console


Mu is the recommended serial console program by adafruit on their CircuitPython serial console page. Mu has a built in plotter for tuples printed to the serial console (print statements in the python code on your microcontroller).

We've found that Mu can be a little unstable and freezes occasionally, so we'd recommend checking out their links for the "advanced" serial console:

You should see an output of the current state of the robot, similar to the screenshot below, and you should see the values update as the robot drives around.


4. Enable AUTO

On the dashboard

Navigate to the Auto mode tab on your dashboard, and click the [AUTO CONTROL] button. The [AUTO READY] icon should turn yellow, indicating the dashboard is ready for a component to take Auto Control.

In the serial console

Hit the space bar in your serial console to request auto control, and you should see the [AUTO READY] turn green, indicating the dashboard is in Auto Control mode.

NOTE: The space bar may not register every press, so use the dash indicators!

5. Drive the robot

In the serial console, increase / decrease the robot forward / reverse speed with the w & s keys, and increase / decrease the robot angular rate with the a & d keys.

6. Release AUTO

Hit the space bar in the serial console to release auto control and return to the [AUTO READY] state. Or hit the E-Stop on your Amiga!