Module System
Introduction
This document describes how the Prunt module system is designed, and why it is designed the way that it is. This document is aimed at anyone who needs to add a new module to Prunt or modify an existing one.
A module in Prunt is a self-contained part of the controller that is responsible for one area of printer behaviour, including g-code commands and configuration parameters for that area. Prunt is built from modules for things like motion, homing, heaters, fans, and other machine features.
Core Types
Prunt.Modules.Module
type Module is abstract tagged private;
function Config_Schema (This : Module) return Config.Versioned_Config_Schema;
function Gcode_Commands (This : Module) return Gcode_Command_Vectors.Vector;
function Status_Schema (This : Module) return Status_Manager.Status_Group_Maps.Map;
function Initialize
(This : Module;
Config_Data : Config.Config_Data;
Report_Config_Error : access procedure (...);
Status_Emitter : Status_Manager.Status_Emitter;
Get_Other_Instance : access function (Tag : Ada.Tags.Tag)
return Module_Instance_Shared_Pointers.Ref)
return Module_Instance'Class
is abstract;A Module lasts for the life of the printer controller and serves as a descriptor of the module.
It exists purely to describe the a module to the controller using the functions shown above and to allow instantiation of an instance.
The module descriptor is separate from the instance because the controller needs to be able to emit available configuration, g-code, and status values to the user before the printer is started.
Gcode_Commands and the internals of Config_Schema can be automatically generated by the build system, so do not worry about the types used there.
Prunt.Modules.Module_Instance
type Module_Instance is synchronized interface;
procedure Gcode_Dispatch
(This : in out Module_Instance;
Args : in out Gcode_Arguments.Arguments;
Planner : Planner_Interface'Class;
Command_Identifier : Gcode_Command_Identifier)
is abstract;
procedure Start
(This : in out Module_Instance;
Self_Ref : Module_Instance_Shared_Pointers.Weak_Ref;
Planner : Planner_Interface'Class)
is abstract;A Module_Instance is created for a specific configuration of the printer, either temporarily to check that a configuration is valid, or for a longer time when the user starts the printer.
If a configuration is being checked then Start is never called.
Lifecycle
Initialize
Initialize is called with the configuration for the module.
Here the module needs to do the following:
- Parse
Config_Data. This is generally done using generated code that will be described later. - Go over the parsed configuration and call
Report_Config_Errorfor all errors which make the configuration invalid. - Call
Get_Other_Instancefor any other modules that are required either during config checking or later and possibly save the returned values. - Return the constructed instance.
At this point the module should not touch any hardware as Initialize may be called purely to validate a configuration.
There may be another instance active at this time.
Get_Other_Instance will detect any cycles or missing module dependencies.
Start
After every module has been initialized and no configuration errors were reported, Start is called on each module.
These calls occur in an order such that any instances returned by Get_Other_Instance will be started before the caller.
At this point the instance may start touching hardware to set default values as determined by the configuration.
Finalize
If a module instance does touch hardware then it should return that hardware to a safe state during finalization. Finalization generally occurs in the reverse order of start, assuming strong references have not been passed around between modules directly.
Configuration Parameters
Modules have the option to define their configuration parameters as regular Ada records with annotations for the code generator:
type User_Config_Heater_Control_Method (Kind : ... := Disabled) is record
-- Select how this heater is controlled.
case Kind is
when Disabled =>
Disabled : User_Config_Empty;
-- Disable this heater. The output remains off and the heater cannot be used.
when PID =>
PID : User_Config_Heater_PID;
end case;
end record
with Annotate => (Prunt_Config, User_Config);
type User_Config_Heater is record
-- This section contains the configuration for a single heater.
Thermistor : Thermistor_Name := Thermistor_Name'First;
-- Select the thermistor used to measure this heater's temperature.
Check_Gain_Time : Time range 0.0 * s .. 1.0E100 * s := 20.0 * s;
-- Time window used when checking that the heater is gaining temperature.
Control_Method : User_Config_Heater_Control_Method := (others => <>);
-- Select the control method for this heater.
end record
with Annotate => (Prunt_Config, User_Config);
type User_Config_Heater_Array is array (Heater_Name) of User_Config_Heater
with Annotate => (Prunt_Config, Tabbed), Annotate => (Prunt_Config, User_Config);
type User_Config is record
Heaters : User_Config_Heater_Array := [others => <>];
end record
with Annotate => (Prunt_Config, Root_User_Config);If the build system detects these annotations then the following procedures are generated:
function Build_Schema return Config.Config_Property_Maps.Map;
overriding
function Config_Data_To_User_Config (Data : Config.Config_Data) return User_Config;
procedure User_Config_To_Config_Data (Data : in out Config.Config_Data; Config : User_Config);These should be defined is separate is the package body if they are required.
G-code Dispatch
Modules also have the option to declare g-code commands in a way that will be picked up by the build system.
procedure Set_Fan_Speed
(Planner : Planner_Interface'Class;
P : Virtual_String;
-- Fan name.
S : Dimensionless := 255.0
-- Fan speed from 0 to 255 where 255 is full speed. Uses full speed if not specified.
)
with Annotate => (Prunt_Config, Gcode_Command, "M106");
-- Set the speed of a fan by name. The speed is scaled according to the maximum speed configured for the
-- selected fan. It is an error to attempt to set the speed of a fan that is configured to be always on.If the build system detects these annotations then the following procedures are generated:
overriding
procedure Gcode_Dispatch
(This : in out Module_Instance;
Args : in out Gcode_Arguments.Arguments;
Planner : Planner_Interface'Class;
Command_Identifier : Gcode_Command_Identifier);
overriding
function Gcode_Commands (This : Module) return Gcode_Command_Vectors.Vector;These should be defined is separate is the package body if they are required.
When implementing a g-code command it is important that the command procedure does not perform any part of the command.
The command procedure should instead output to the planner as there could be other commands waiting in the queue which must occur before the new command.
The command procedure should validate arguments and raise Gcode_Bad_Inputs_Error without enqueuing anything in the planner if the command is invalid.