Many Arduino projects are written in a style that closely resembles the simpler, though more verbose, C language, including the demonstration program provided as a reference in this project. This approach is advantageous in applications where dynamic memory and processing power are severely constrained, as is often the case with lower-spec Arduino boards. However, the Raspberry Pi Pico offers a significant performance boost compared to similarly sized boards such as the Arduino Nano, including more than four times the processor clock speed, over one hundred times the random-access memory, and approximately sixty times the flash storage capacity. This massive increase in hardware resources enables projects of a much larger scale and complexity than would otherwise be possible.

Given these capabilities, I decided from the outset that I’d structure the project using C++ classes. Classes allow individual program components, particularly those that appear multiple times, to encapsulate both data and the functions that operate on that data. For example, adjusting the target speed of a motor and handling all associated operations can be managed through a single method call on a motor object. Unlike regular functions, each object (for example, the left motor) maintains its own internal state, enabling it to store and manage its behaviour independently of other instances (such as the right motor). This structure greatly improves both code clarity and scalability in a project of this size. If two more motors needed to be attached last minute, the code would only need to be changed to include two more object declarations with the relevant pins for each passed as parameters.

The root of the whole program is the main class, Micromouse.cpp, which serves as the central controller for all components of the Micromouse. This class acts as a container for all of the other subsystems, each represented by its own specialized class. The Micromouse class is responsible for managing the overall state of the robot, including tracking and executing the current selected program. Each program is also encapsulated in its own class, making it easy to switch between them.

A key design feature is the selected_program member variable, which serves as a pointer to one of the four program classes, allowing for dynamic assignment of different programs to the selected_program variable. Once a program is selected the setup and line methods of the chosen program could be invoked with the following syntax: selected_program->method(); This meant that after all of the hardware and their relevant classes are initialized within the Micromouse class setup method, each program class could effectively act as a separate Arduino project with their own separate setup and loop.

This setup provided several advantages:

Separation – Each program was isolated within its own class, making it easier to independently develop, debug, and modify each program without affecting any of the other programs.

Modularity – New programs could be added simply by creating a new class, which could then be assigned to selected_program and automatically work out of the box.

Clarity – The structure of the code is clearer since all of the logic encapsulated in each program was simply dealing with the manipulation of already declared and defined components, which simplified and massively reduced the size of each program.

Class structure diagram

File structure of project

Condensed Micromouse.cpp File

#include "Micromouse.hpp"
#include "Programs/Whiteline.hpp"
#include "Programs/Combat.hpp"
#include "Programs/Calibration.hpp"
#include "Programs/Avoidance.hpp"

#include <EEPROM.h>

// Define the member variables
Tachometer Micromouse::left_tacho;
Tachometer Micromouse::right_tacho;
Motor Micromouse::left_motor;
Motor Micromouse::right_motor;
Touchbar Micromouse::fl_touchbar;
Touchbar Micromouse::fr_touchbar;
Touchbar Micromouse::bl_touchbar;
Touchbar Micromouse::br_touchbar;
InfraredSensor Micromouse::left_irsensor;
InfraredSensor Micromouse::right_irsensor;
Adafruit_seesaw Micromouse::seesaw(&Wire);
LightDetector Micromouse::fr_LDR;
LightDetector Micromouse::fl_LDR;
LightDetector Micromouse::rr_LDR;
LightDetector Micromouse::rl_LDR;
Display Micromouse::display;
NewPing Micromouse::rear_ultrasonic(17, 16, 100);
NewPing Micromouse::front_ultrasonic(28, 27, 100);

...

// Initialise hardware on micrmouse
void Micromouse::begin() 
{
    // Iniitalise display
    display.begin(14, 15, 0x3C);
    display.debug_message(">>Display ready");

    // Initialise the hardware and assign the pins
    left_tacho.begin(2);
    //left_tacho.setPulsesPerSeconds(793);
    right_tacho.begin(3);
    //right_tacho.setPulsesPerSeconds(799);
    display.debug_message(">>Tachos ready");

    left_motor.begin(7, 6, &left_tacho);
    right_motor.begin(8, 9, &right_tacho);
    display.debug_message(">>Motors ready");
    
    ...

    while(true) {
        if(fr_touchbar.isPressed()) { selected_program = Calibration::getInstance(); }
        else if(fl_touchbar.isPressed()) { selected_program = Whiteline::getInstance(); }
        else if(br_touchbar.isPressed()) { selected_program = Combat::getInstance(); }
        else if(bl_touchbar.isPressed()) { selected_program = Avoidance::getInstance(); }

        if(selected_program != nullptr) { break; }
        delay(5);
    }
    
    selected_program->begin();
}


void Micromouse::loop() {
    selected_program->loop();

}


Full Whiteline.cpp File

#include "Whiteline.hpp"

Whiteline Whiteline::instance;

// Preserve Singleton usage
Whiteline* Whiteline::getInstance() 
{
    return &instance;
}

/* ─── Default Functions ─────────────────────────────────────────────────── */

void Whiteline::begin() {
    display.debug_message(">>Whiteline ready", 200);
    display.getController().clearDisplay();
    display.getController().display();

    forwards();

    display.horizontal_align("Whiteline", 16, 2);
    display.horizontal_align("Following", 32, 2);
    display.getController().display();
}

void Whiteline::loop() {
  // Check sensor states and turn accordingly
  setDirection();
}
 
/* ─── Helper Functions ─────────────────────────────────────────────────── */

/// @brief If driving forwards, check sensors and turn accordingly
void Whiteline::setDirection() {
  right_check = fr_LDR.getState();
  left_check = fl_LDR.getState();

  // Check if turn can end
  if((direction == Direction::RIGHT && !right_check) || (direction == Direction::LEFT && !left_check)) { 
    direction = Direction::FORWARDS; 
    forwards();
    return;
  }

  // Otherwise, if we are not going forwards...
  if(direction != Direction::FORWARDS) { return; }

  // ...then check if turn is needed
  if(right_check) { 
    direction = Direction::RIGHT; 
    rightturn();
    return;
  } 
  
  if(left_check) { 
    direction = Direction::LEFT; 
    leftturn();
    return;
  }
}

/* ─── Change Driving Direction ─────────────────────────────────────────────────── */

/// @brief Drive forwards
void Whiteline::forwards() {
  left_motor.setDrivemode(Motor::Drivemode::FORWARDS);
  left_motor.setTargetSpeed(0.6);
  right_motor.setDrivemode(Motor::Drivemode::FORWARDS);
  right_motor.setTargetSpeed(0.6);
}

/// @brief Hard turn right
void Whiteline::leftturn() {
  right_motor.setTargetSpeed(0.5);
  right_motor.setDrivemode(Motor::Drivemode::FORWARDS);
  left_motor.setDrivemode(Motor::Drivemode::BRAKE);
}

/// @brief Hard turn left
void Whiteline::rightturn() {
  left_motor.setTargetSpeed(0.5);
  left_motor.setDrivemode(Motor::Drivemode::FORWARDS);
  right_motor.setDrivemode(Motor::Drivemode::BRAKE);
}