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.
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.
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:
REGISTER_MODULE macroThe 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.
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/.
For the example, we'll create a simple module that controls laser power based on motion.
The module will:
Good examples of existing V2 modules are in the src/modules/ directory, particularly Laser, TemperatureControl, and Extruder.
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.
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.
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.
In V2, modules don't use a global kernel. Instead, they use:
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
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 receivedon_gcode_execute - called right before the movement corresponding to that G-code line is executedThe one that's of interest to us now is on_gcode_execute.
In V2, laser power control works differently than V1. Instead of registering for G-code events, the laser module uses a timer-based approach:
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));
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.
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.
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
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.
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.
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.
As you can see, V2's module architecture is significantly different from V1. The key differences are:
REGISTER_MODULE macro with a static create method, instantiated from config filesTHEDISPATCHERStepTicker::getInstance()->get_current_block()Despite these architectural changes, the fundamental principle remains: add functionality without modifying the core firmware, through a well-defined module interface.