Module Example

Developer Guide: This guide provides a step-by-step overview of how to create a basic module for use with Smoothie.

The aim of this guide is to provide a step-by-step overview of how to create a basic module for use with Smoothie.

The example here will be a laser control module.

What is a module

In Smoothie, everything is a module.

A module is essentially a piece of code (an object) that connects to the rest of the code only through event calls and event handlers.

The core of Smoothie (serial communication, motion planning, actual stepping) is divided into modules.

And then you can write modules for additional tasks, like controlling a laser, 3D printing, or whatever cool idea you have.

The main idea is to be able to add these additional functionalities in the simplest way possible, without having to edit the core, just connect to/call events.

What is a module

In Smoothieware V2, modules are configuration-driven components that register themselves and communicate via a request/response system.

A module is a piece of code (an object/class) that:

  • Registers itself during startup via the REGISTER_MODULE macro
  • Processes its configuration section from the config file
  • Communicates with other modules through explicit lookup and request calls
  • Optionally handles M-codes and custom commands via the Dispatcher system
  • Can use timer callbacks (SlowTicker/FastTicker) for periodic operations

The core of Smoothieware V2 (serial communication, motion planning, stepping) is divided into modules, and you can write modules for additional tasks like controlling a laser, temperature control, or custom hardware.

The main idea is to add functionality without editing the core, through explicit configuration and direct module communication.

About this example

For the example, we'll create a simple module that turns the laser on and off depending on received G codes.

PWM adjusts the laser power to the robot's speed so that it plays nicely with look-ahead acceleration management.

This should actually be sufficient to do basic laser cutting.

Also, there'll be a lot of explaining here, but if you landed on this page, you will probably understand everything just by looking at the code; it's not that complicated.

Good examples of existing modules are in /modules/communication/ and /modules/robot/.

About this example

For the example, we'll create a simple module that controls laser power based on motion.

The module will:

  • Register itself with the module system using a static create method
  • Read configuration from the config file (pin, power settings)
  • Handle M-codes via the Dispatcher system (e.g., M221 for power scaling)
  • Use a timer callback to continuously adjust PWM based on motion speed

Good examples of existing V2 modules are in the src/modules/ directory, particularly Laser, TemperatureControl, and Extruder.

Code style

For this example, we’ll put all of the function code within the class definition. You should obviously not do that for real-life coding, but it makes life easier for an example.

Our base module

So here is what your basic module skeleton looks like:

// Basic laser control module
class Laser : public Module {
    public:
        Laser(){}
        void on_module_loaded() { }
};

We make a Laser class, extending the base Module class. The on_module_loaded method will be called automatically when the kernel is done loading the module. You shouldn’t call the kernel, like to register for an event, before on_module_loaded is called, like in the constructor.

About the Kernel

The kernel (libs/Kernel.h) is basically what you talk to when registering for an event, calling an event, and what calls you (the Module) when you have registered for an event, and another Module calls this event.

Your module must be added to it like this, in main.cpp:

// Add Laser module to Kernel
Laser laser = Laser();
kernel->add_module(&laser);

Once the module is added, on_module_loaded() is called so that you can register for events.

About the Module Registry

In V2, modules don't use a global kernel. Instead, they use:

  1. Module Registry: A map-based system that manages all modules by group/instance name
  2. REGISTER_MODULE macro: Registers a static create function that's called at startup
  3. Configuration-driven loading: Modules are instantiated and configured based on the config file

Your module registers itself through a static create method:

// In Laser.cpp
REGISTER_MODULE(Laser, Laser::create)

bool Laser::create(ConfigReader& cr)
{
    Laser *laser = new Laser();
    if(!laser->configure(cr)) {
        delete laser;
        return false;
    }
    return true;
}

Laser::Laser() : Module("laser")
{
    // Initialize member variables
    laser_on = false;
}

The REGISTER_MODULE macro places the create function pointer in a special linker section, so it's automatically called during firmware startup. The config file controls whether the module is enabled:

[laser]
enable = true
pwm_pin = 2.4
maximum_power = 1.0
minimum_power = 0.0

On the two G code events

So in our example, we want to turn the laser on/off depending on the G codes we receive.

But because of the acceleration management (G code look-ahead, see the Planner class), received G codes are not executed when they are received.

They are pushed into a queue, and then popped out when the previous movement has finished executing.

So there are two events:

  • on_gcode_received - called when a G-code is received
  • on_gcode_execute - called right before the movement corresponding to that G-code line is executed

The one that's of interest to us now is on_gcode_execute.

On motion and laser power

In V2, laser power control works differently than V1. Instead of registering for G-code events, the laser module uses a timer-based approach:

  1. G1/G2/G3 moves include an S-value (power) that's stored in the motion block
  2. A timer callback runs periodically (up to 1kHz) to check the current motion block
  3. The callback reads the block's S-value and current speed ratio to calculate proportional power
  4. PWM output is adjusted in real-time as the motion accelerates/decelerates

This design allows the laser to respond to speed changes during motion without requiring explicit event broadcasts.

For M-codes and custom commands (like manual fire), modules register handlers with the Dispatcher:

// Register M221 handler for power scaling
THEDISPATCHER->add_handler(Dispatcher::MCODE_HANDLER, 221,
    std::bind(&Laser::handle_M221, this, _1, _2));

// Register custom "fire" command
THEDISPATCHER->add_handler("fire",
    std::bind(&Laser::handle_fire_cmd, this, _1, _2));

Registering for an event

So here is how we register for an event in our code:

class Laser : public Module {
    public:
        Laser(){}

        void on_module_loaded() {
            this->register_for_event(ON_GCODE_EXECUTE);
        }

        void on_gcode_execute(void* argument){
            Gcode* gcode = static_cast<Gcode*>(argument);
        }
};

So now, whenever a module calls the on_gcode_execute event, this callback function will be called. In this case, the Stepper module calls this upon deleting a move it has just finished stepping.

Because of the way C++ works, arguments to events here must be passed as void pointers and then manually cast in the callback function. You can see how that's done: here we cast a Gcode object.

You can find more information about the different events in ListOfEvents.

Registering timer callbacks and handlers

In V2, we use timer callbacks for continuous operations and the Dispatcher for commands:

bool Laser::configure(ConfigReader& cr)
{
    ConfigReader::section_map_t m;
    if(!cr.get_section("laser", m)) return false;

    // Check if enabled in config
    if(!cr.get_bool(m, "enable", false)) {
        return false;
    }

    // Read pin configuration
    pwm_pin = new Pwm(cr.get_string(m, "pwm_pin", "nc"));
    if(!pwm_pin->is_valid()) {
        delete pwm_pin;
        return false;
    }

    // Read power settings
    laser_maximum_power = cr.get_float(m, "maximum_power", 1.0f);
    laser_minimum_power = cr.get_float(m, "minimum_power", 0.0f);

    // Register M-code handler
    using std::placeholders::_1;
    using std::placeholders::_2;
    THEDISPATCHER->add_handler(Dispatcher::MCODE_HANDLER, 221,
        std::bind(&Laser::handle_M221, this, _1, _2));

    // Register timer callback for proportional power
    uint32_t freq = std::min(1000UL, pwm_pin->get_frequency());
    SlowTicker::getInstance()->attach(freq,
        std::bind(&Laser::set_proportional_power, this));

    return true;
}

Handlers are type-safe - they receive GCode& and OutputStream& references directly, no casting needed. The Dispatcher calls only registered handlers, which is more efficient than V1's broadcast model.

Doing something useful

We have to modify the class a bit to add a PwmOut to it. Then we can do some useful stuff:

class Laser : public Module {
    public:
        Laser(PinName pin) : laser_pin(pin) {
            this->laser_pin.period_us(10);
        }

        void on_module_loaded() {
            this->register_for_event(ON_GCODE_EXECUTE);
            this->register_for_event(ON_SPEED_CHANGE);
        }

        void on_gcode_execute(void* argument) {
            Gcode* gcode = static_cast<Gcode*>(argument);
            if (gcode->has_letter('G')) {
                int code = gcode->get_value('G');
                if (code == 0) { // G0
                    this->laser_pin = 0;
                    this->laser_on = false;
                } else if (code > 0 && code < 4) { // G1, G2, G3
                    this->laser_on = true;
                }
            }
        }

        void on_speed_change(void* argument) {
            Stepper* stepper = static_cast<Stepper*>(argument);
            if (this->laser_on) {
                this->laser_pin = double(stepper->trapezoid_adjusted_rate)
                    / double(stepper->current_block->nominal_rate);
            }
        }

        PwmOut laser_pin;
        bool   laser_on;
};

And also change a bit the way we instantiate the module:

Laser laser = Laser(p21);

That's it, now the Laser pin will be LOW during G0 moves, and HIGH during G1, G2, and G3 moves.

But that's not enough. Because we use acceleration, the speed is not constant. And thus if the power of the laser stays constant, that power will be too much when accelerating and decelerating.

So we need to have a laser power that is proportional to the instant speed of the robot. That's the kind of thing the on_speed_change event is for.

Doing something useful

In V2, proportional laser power is handled by a timer callback that reads from the current motion block:

// Called periodically by SlowTicker (up to 1kHz)
void Laser::set_proportional_power()
{
    // Get the currently executing motion block
    const Block *block = StepTicker::getInstance()->get_current_block();

    // Check if we have a valid block that's a G1/G2/G3 move
    if(block != nullptr && block->is_ready && block->is_g123) {
        // Get requested power from the block's S-value
        float requested_power = block->s_value / laser_maximum_s_value;

        // Calculate current speed ratio (0 to 1)
        float ratio = current_speed_ratio(block);

        // Apply proportional power
        float power = requested_power * ratio * scale;
        set_laser_power(power);
    }
    else if(laser_on) {
        // No motion block - turn laser off
        set_laser_power(0);
    }
}

// Calculate speed ratio based on trapezoid position
float Laser::current_speed_ratio(const Block *block) const
{
    // Find primary axis (most steps)
    size_t pm = 0;
    uint32_t max_steps = 0;
    for (size_t i = 0; i < Robot::getInstance()->get_number_registered_motors(); i++) {
        if(block->steps[i] > max_steps) {
            max_steps = block->steps[i];
            pm = i;
        }
    }

    // Return ratio of current rate to nominal rate
    return block->get_trapezoid_rate(pm) / block->nominal_rate;
}

The key difference from V1 is that V2 polls the motion system rather than receiving events. This allows real-time power adjustment as the motion accelerates and decelerates through the trapezoid profile.

Conclusion

As you can see here, we have added functionality to Smoothie without having to modify the core, which is the whole point of the modular design.

Conclusion

As you can see, V2's module architecture is significantly different from V1. The key differences are:

  • Configuration-driven: Modules use REGISTER_MODULE macro with a static create method, instantiated from config files
  • Timer-based updates: Continuous operations (like laser power) use SlowTicker/FastTicker callbacks instead of events
  • Dispatcher for commands: M-codes and custom commands register handlers with THEDISPATCHER
  • Direct block access: Modules can read from the current motion block via StepTicker::getInstance()->get_current_block()
  • Type-safe handlers: No void pointer casting - handlers receive typed references

Despite these architectural changes, the fundamental principle remains: add functionality without modifying the core firmware, through a well-defined module interface.

This is a wiki! If you'd like to improve this page, you can edit it on GitHub.