I've been working on adding support for stepper motors, and I'm interested in some feedback on the best way to move forward with changes to the reef-pi codebase.
My basic approach is to use an arduino to produce step/dir signals for a stepper driver, and to use the Firmata protocol to communicate between the rpi and the arduino. Firmata is a serial protocol designed to allow a computer (in our case the rpi) to control an arduino; it provides messages for things like setting & reading digital pins. I'm using an extension to firmata that adds messages for interacting the the AccelStepper library. This library provides configuration options for step/dir controls and provides a acceleration and deceleration when the stepper is starting or approaching a setpoint.
I've adapted some existing go Firmata libraries to provide the necessary command interface for the AccelStepper messages:
https://github.com/kerinin/gomata. I'm now looking at how best to integrate with the existing reef-pi code organization.
Steppers require fundamentally different control configuration that a simple time & speed based pump: multiple pins need to be configured, the path of the arduino's serial port needs to defined, a firmata "device ID" needs to be specified, etc. The existing "dosing" code is configured using a "jack", a "pin" and a dosing duration. I have a branch that attempts to use the doser resource in either case by proving configurations for both types of pump:
Code:
type Pump struct {
ID string `json:"id"`
Name string `json:"name"`
TimeConfig *TimeConfig `json:"time"`
FirmataStepsConfig *FirmataStepsConfig `json:"firmataSteps"`
Regiment DosingRegiment `json:"regiment"`
}
type TimeConfig struct {
Jack string `json:"jack"`
Pin int `json:"pin"`
Speed float64 `json:"speed"`
}
type FirmataStepsConfig struct {
Firmata string `json:"firmata"`
DeviceID int `json:"deviceID"`
WireCount byte `json:"wireCount"`
StepType byte `json:"stepType"`
HasEnable byte `json:"hasEnable"`
Pin1 int `json:"pin1"`
Pin2 int `json:"pin2"`
Pin3 int `json:"pin3"`
Pin4 int `json:"pin4"`
EnablePin int `json:"enablePin"`
Invert int `json:"invert"`
Speed float32 `json:"speed"`
Acceleration float32 `json:"acceleration"`
}
This solution re-uses a decent amount of code, the difference between the two implementations mostly boils down to a switch in the runner's dose function:
Code:
func (r *Runner) Dose(volume float64) error {
if cfg := r.pump.TimeConfig; cfg != nil {
var duration = volume / cfg.Speed
return r.timeDose(cfg, duration)
}
if cfg := r.pump.FirmataStepsConfig; cfg != nil {
return r.stepDose(cfg, int32(volume))
}
return fmt.Errorf("ERROR: dosing sub-system. Unconfigured pump")
}
func (r *Runner) timeDose(cfg *TimeConfig, duration float64) error {
//...
}
func (r *Runner) stepDose(cfg *FirmataStepsConfig, steps int32) error {
//...
}
I've just started looking at how this change would affect the UI - I think the doser configuration would need some type of modal, maybe with a "doser type" radio button to switch between stepper and timer config.
Another solution would be to introduce a new "doser" implementation, like "stepper-doser". This would cause some code duplication, and creating a new top-level resource to work around implementation details of different physical pumps seems non-ideal. The benefit of this approach is that it would be less likely to cause migration problems for existing users.
So I'm interested in feedback on the general approach - does this seem like the right way to go? What type of forwards-compatibility guarantees are expected - if this introduces breaking changes to the doser data schema is that a problem?
This is the branch I'm working on fwiw (no guarantees this link will remain usable for long):
https://github.com/kerinin/reef-pi/compare/master...kerinin:rm/stepper2