mirror of
https://github.com/hybridgroup/gobot.git
synced 2025-04-24 13:48:49 +08:00
gpio: fix data race in StepperDriver (#1029)
This commit is contained in:
parent
9e311b28e4
commit
a04ce8a7f2
1
Makefile
1
Makefile
@ -58,6 +58,7 @@ fmt_check:
|
||||
fmt_fix:
|
||||
$(MAKE) version_check || true
|
||||
gofumpt -l -w .
|
||||
golangci-lint run -v --fix
|
||||
|
||||
examples: $(EXAMPLES)
|
||||
|
||||
|
@ -2,257 +2,111 @@ package gpio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"gobot.io/x/gobot/v2"
|
||||
)
|
||||
|
||||
// EasyDriver object
|
||||
const easyDriverDebug = false
|
||||
|
||||
// EasyDriver is an driver for stepper hardware board from SparkFun (https://www.sparkfun.com/products/12779)
|
||||
// This should also work for the BigEasyDriver (untested). It is basically a wrapper for the common StepperDriver{}
|
||||
// with the specific additions for the board, e.g. direction, enable and sleep outputs.
|
||||
type EasyDriver struct {
|
||||
*Driver
|
||||
*StepperDriver
|
||||
|
||||
stepPin string
|
||||
dirPin string
|
||||
enPin string
|
||||
sleepPin string
|
||||
stepPin string
|
||||
dirPin string
|
||||
enPin string
|
||||
sleepPin string
|
||||
anglePerStep float32
|
||||
|
||||
angle float32
|
||||
rpm uint
|
||||
dir int8
|
||||
stepNum int
|
||||
enabled bool
|
||||
sleeping bool
|
||||
runStopChan chan struct{}
|
||||
runStopWaitGroup *sync.WaitGroup
|
||||
sleeping bool
|
||||
}
|
||||
|
||||
// NewEasyDriver returns a new driver for EasyDriver from SparkFun (https://www.sparkfun.com/products/12779)
|
||||
// NewEasyDriver returns a new driver
|
||||
// TODO: Support selecting phase input instead of hard-wiring MS1 and MS2 to board truth table
|
||||
// This should also work for the BigEasyDriver (untested)
|
||||
// A - DigitalWriter
|
||||
// anglePerStep - Step angle of motor
|
||||
// stepPin - Pin corresponding to step input on EasyDriver
|
||||
// dirPin - Pin corresponding to dir input on EasyDriver. Optional
|
||||
// enPin - Pin corresponding to enabled input on EasyDriver. Optional
|
||||
// sleepPin - Pin corresponding to sleep input on EasyDriver. Optional
|
||||
// angle - Step angle of motor
|
||||
func NewEasyDriver(
|
||||
a DigitalWriter,
|
||||
angle float32,
|
||||
anglePerStep float32,
|
||||
stepPin string,
|
||||
dirPin string,
|
||||
enPin string,
|
||||
sleepPin string,
|
||||
) *EasyDriver {
|
||||
if angle <= 0 {
|
||||
panic("angle needs to be greater than zero")
|
||||
if anglePerStep <= 0 {
|
||||
panic("angle per step needs to be greater than zero")
|
||||
}
|
||||
d := &EasyDriver{
|
||||
Driver: NewDriver(a.(gobot.Connection), "EasyDriver"),
|
||||
stepPin: stepPin,
|
||||
dirPin: dirPin,
|
||||
enPin: enPin,
|
||||
sleepPin: sleepPin,
|
||||
angle: angle,
|
||||
rpm: 1,
|
||||
dir: 1,
|
||||
enabled: true,
|
||||
sleeping: false,
|
||||
}
|
||||
d.beforeHalt = func() error {
|
||||
if err := d.Stop(); err != nil {
|
||||
fmt.Printf("no need to stop motion: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// panic if step pin isn't set
|
||||
if stepPin == "" {
|
||||
panic("Step pin is not set")
|
||||
}
|
||||
|
||||
// 1/4 of max speed. Not too fast, not too slow
|
||||
d.rpm = d.MaxSpeed() / 4
|
||||
stepper := NewStepperDriver(a, [4]string{}, nil, 1)
|
||||
stepper.name = gobot.DefaultName("EasyDriver")
|
||||
stepper.stepperDebug = easyDriverDebug
|
||||
stepper.haltIfRunning = false
|
||||
stepper.stepsPerRev = 360.0 / anglePerStep
|
||||
d := &EasyDriver{
|
||||
StepperDriver: stepper,
|
||||
stepPin: stepPin,
|
||||
dirPin: dirPin,
|
||||
enPin: enPin,
|
||||
sleepPin: sleepPin,
|
||||
anglePerStep: anglePerStep,
|
||||
|
||||
d.AddCommand("Move", func(params map[string]interface{}) interface{} {
|
||||
degs, _ := strconv.Atoi(params["degs"].(string))
|
||||
return d.Move(degs)
|
||||
})
|
||||
d.AddCommand("Step", func(params map[string]interface{}) interface{} {
|
||||
return d.Step()
|
||||
})
|
||||
d.AddCommand("Run", func(params map[string]interface{}) interface{} {
|
||||
return d.Run()
|
||||
})
|
||||
d.AddCommand("Stop", func(params map[string]interface{}) interface{} {
|
||||
return d.Stop()
|
||||
})
|
||||
sleeping: false,
|
||||
}
|
||||
d.stepFunc = d.onePinStepping
|
||||
d.sleepFunc = d.sleepWithSleepPin
|
||||
d.beforeHalt = d.shutdown
|
||||
|
||||
// 1/4 of max speed. Not too fast, not too slow
|
||||
d.speedRpm = d.MaxSpeed() / 4
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Move the motor given number of degrees at current speed. The move can be stopped asynchronously.
|
||||
func (d *EasyDriver) Move(degs int) error {
|
||||
// ensure that move and run can not interfere
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
if !d.enabled {
|
||||
return fmt.Errorf("motor '%s' is disabled and can not be running", d.name)
|
||||
// SetDirection sets the direction to be moving.
|
||||
func (d *EasyDriver) SetDirection(direction string) error {
|
||||
direction = strings.ToLower(direction)
|
||||
if direction != StepperDriverForward && direction != StepperDriverBackward {
|
||||
return fmt.Errorf("Invalid direction '%s'. Value should be '%s' or '%s'",
|
||||
direction, StepperDriverForward, StepperDriverBackward)
|
||||
}
|
||||
|
||||
if d.runStopChan != nil {
|
||||
return fmt.Errorf("motor '%s' already running or moving", d.name)
|
||||
}
|
||||
|
||||
d.runStopChan = make(chan struct{})
|
||||
d.runStopWaitGroup = &sync.WaitGroup{}
|
||||
d.runStopWaitGroup.Add(1)
|
||||
|
||||
defer func() {
|
||||
close(d.runStopChan)
|
||||
d.runStopChan = nil
|
||||
d.runStopWaitGroup.Done()
|
||||
}()
|
||||
|
||||
steps := int(float32(degs) / d.angle)
|
||||
if steps <= 0 {
|
||||
fmt.Printf("steps are smaller than zero, no move for '%s'\n", d.name)
|
||||
}
|
||||
|
||||
for i := 0; i < steps; i++ {
|
||||
select {
|
||||
case <-d.runStopChan:
|
||||
// don't continue to step if driver is stopped
|
||||
log.Println("stop happen")
|
||||
return nil
|
||||
default:
|
||||
if err := d.step(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run the stepper continuously.
|
||||
func (d *EasyDriver) Run() error {
|
||||
// ensure that run, can not interfere with step or move
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
if !d.enabled {
|
||||
return fmt.Errorf("motor '%s' is disabled and can not be moving", d.name)
|
||||
}
|
||||
|
||||
if d.runStopChan != nil {
|
||||
return fmt.Errorf("motor '%s' already running or moving", d.name)
|
||||
}
|
||||
|
||||
d.runStopChan = make(chan struct{})
|
||||
d.runStopWaitGroup = &sync.WaitGroup{}
|
||||
d.runStopWaitGroup.Add(1)
|
||||
|
||||
go func(name string) {
|
||||
defer d.runStopWaitGroup.Done()
|
||||
for {
|
||||
select {
|
||||
case <-d.runStopChan:
|
||||
d.runStopChan = nil
|
||||
return
|
||||
default:
|
||||
if err := d.step(); err != nil {
|
||||
fmt.Printf("motor step skipped for '%s': %v\n", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(d.name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsMoving returns a bool stating whether motor is currently in motion
|
||||
func (d *EasyDriver) IsMoving() bool {
|
||||
return d.runStopChan != nil
|
||||
}
|
||||
|
||||
// Stop running the stepper
|
||||
func (d *EasyDriver) Stop() error {
|
||||
if !d.IsMoving() {
|
||||
return fmt.Errorf("motor '%s' is not yet started", d.name)
|
||||
}
|
||||
|
||||
d.runStopChan <- struct{}{}
|
||||
d.runStopWaitGroup.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step the stepper 1 step
|
||||
func (d *EasyDriver) Step() error {
|
||||
// ensure that move and step can not interfere
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
if d.IsMoving() {
|
||||
return fmt.Errorf("motor '%s' already running or moving", d.name)
|
||||
}
|
||||
|
||||
return d.step()
|
||||
}
|
||||
|
||||
// SetDirection sets the direction to be moving. Valid directions are "cw" or "ccw"
|
||||
func (d *EasyDriver) SetDirection(dir string) error {
|
||||
// can't change direct if dirPin isn't set
|
||||
if d.dirPin == "" {
|
||||
return fmt.Errorf("dirPin is not set for '%s'", d.name)
|
||||
}
|
||||
|
||||
if dir == "ccw" {
|
||||
d.dir = -1
|
||||
// high is ccw
|
||||
return d.connection.(DigitalWriter).DigitalWrite(d.dirPin, 1)
|
||||
writeVal := byte(0) // low is forward
|
||||
if direction == StepperDriverBackward {
|
||||
writeVal = 1 // high is backward
|
||||
}
|
||||
|
||||
// default to cw, even if user specified wrong value
|
||||
d.dir = 1
|
||||
// low is cw
|
||||
return d.connection.(DigitalWriter).DigitalWrite(d.dirPin, 0)
|
||||
}
|
||||
|
||||
// SetSpeed sets the speed of the motor in RPMs. 1 is the lowest and GetMaxSpeed is the highest
|
||||
func (d *EasyDriver) SetSpeed(rpm uint) error {
|
||||
if rpm < 1 {
|
||||
d.rpm = 1
|
||||
} else if rpm > d.MaxSpeed() {
|
||||
d.rpm = d.MaxSpeed()
|
||||
} else {
|
||||
d.rpm = rpm
|
||||
if err := d.connection.(DigitalWriter).DigitalWrite(d.dirPin, writeVal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure that write of variable can not interfere with read in step()
|
||||
d.valueMutex.Lock()
|
||||
defer d.valueMutex.Unlock()
|
||||
d.direction = direction
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaxSpeed returns the max speed of the stepper
|
||||
func (d *EasyDriver) MaxSpeed() uint {
|
||||
return uint(360 / d.angle)
|
||||
}
|
||||
|
||||
// CurrentStep returns current step number
|
||||
func (d *EasyDriver) CurrentStep() int {
|
||||
return d.stepNum
|
||||
}
|
||||
|
||||
// Enable enables all motor output
|
||||
func (d *EasyDriver) Enable() error {
|
||||
// can't enable if enPin isn't set. This is fine normally since it will be enabled by default
|
||||
if d.enPin == "" {
|
||||
d.enabled = true
|
||||
d.disabled = false
|
||||
return fmt.Errorf("enPin is not set - board '%s' is enabled by default", d.name)
|
||||
}
|
||||
|
||||
@ -261,58 +115,34 @@ func (d *EasyDriver) Enable() error {
|
||||
return err
|
||||
}
|
||||
|
||||
d.enabled = true
|
||||
d.disabled = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disable disables all motor output
|
||||
func (d *EasyDriver) Disable() error {
|
||||
// can't disable if enPin isn't set
|
||||
if d.enPin == "" {
|
||||
return fmt.Errorf("enPin is not set for '%s'", d.name)
|
||||
}
|
||||
|
||||
// stop the motor if running
|
||||
err := d.tryStop()
|
||||
_ = d.stopIfRunning() // drop step errors
|
||||
|
||||
// enPin is active low
|
||||
if e := d.connection.(DigitalWriter).DigitalWrite(d.enPin, 1); e != nil {
|
||||
err = multierror.Append(err, e)
|
||||
} else {
|
||||
d.enabled = false
|
||||
if err := d.connection.(DigitalWriter).DigitalWrite(d.enPin, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
d.disabled = true
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled returns a bool stating whether motor is enabled
|
||||
func (d *EasyDriver) IsEnabled() bool {
|
||||
return d.enabled
|
||||
}
|
||||
|
||||
// Sleep puts the driver to sleep and disables all motor output. Low power mode.
|
||||
func (d *EasyDriver) Sleep() error {
|
||||
// can't sleep if sleepPin isn't set
|
||||
if d.sleepPin == "" {
|
||||
return fmt.Errorf("sleepPin is not set for '%s'", d.name)
|
||||
}
|
||||
|
||||
// stop the motor if running
|
||||
err := d.tryStop()
|
||||
|
||||
// sleepPin is active low
|
||||
if e := d.connection.(DigitalWriter).DigitalWrite(d.sleepPin, 0); e != nil {
|
||||
err = multierror.Append(err, e)
|
||||
} else {
|
||||
d.sleeping = true
|
||||
}
|
||||
|
||||
return err
|
||||
return !d.disabled
|
||||
}
|
||||
|
||||
// Wake wakes up the driver
|
||||
func (d *EasyDriver) Wake() error {
|
||||
// can't wake if sleepPin isn't set
|
||||
if d.sleepPin == "" {
|
||||
return fmt.Errorf("sleepPin is not set for '%s'", d.name)
|
||||
}
|
||||
@ -335,31 +165,43 @@ func (d *EasyDriver) IsSleeping() bool {
|
||||
return d.sleeping
|
||||
}
|
||||
|
||||
func (d *EasyDriver) step() error {
|
||||
stepsPerRev := d.MaxSpeed()
|
||||
func (d *EasyDriver) onePinStepping() error {
|
||||
// ensure that read and write of variables (direction, stepNum) can not interfere
|
||||
d.valueMutex.Lock()
|
||||
defer d.valueMutex.Unlock()
|
||||
|
||||
// a valid steps occurs for a low to high transition
|
||||
if err := d.connection.(DigitalWriter).DigitalWrite(d.stepPin, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
// 1 minute / steps per revolution / revolutions per minute
|
||||
// let's keep it as Microseconds so we only have to do integer math
|
||||
time.Sleep(time.Duration(60*1000*1000/stepsPerRev/d.rpm) * time.Microsecond)
|
||||
|
||||
time.Sleep(d.getDelayPerStep())
|
||||
if err := d.connection.(DigitalWriter).DigitalWrite(d.stepPin, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// increment or decrement the number of steps by 1
|
||||
d.stepNum += int(d.dir)
|
||||
if d.direction == StepperDriverForward {
|
||||
d.stepNum++
|
||||
} else {
|
||||
d.stepNum--
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryStop stop the stepper if moving or running
|
||||
func (d *EasyDriver) tryStop() error {
|
||||
if !d.IsMoving() {
|
||||
return nil
|
||||
// sleepWithSleepPin puts the driver to sleep and disables all motor output. Low power mode.
|
||||
func (d *EasyDriver) sleepWithSleepPin() error {
|
||||
if d.sleepPin == "" {
|
||||
return fmt.Errorf("sleepPin is not set for '%s'", d.name)
|
||||
}
|
||||
|
||||
return d.Stop()
|
||||
_ = d.stopIfRunning() // drop step errors
|
||||
|
||||
// sleepPin is active low
|
||||
if err := d.connection.(DigitalWriter).DigitalWrite(d.sleepPin, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
d.sleeping = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package gpio
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -11,22 +10,21 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
stepAngle = 0.5 // use non int step angle to check int math
|
||||
stepsPerRev = 720
|
||||
)
|
||||
|
||||
func initTestEasyDriverWithStubbedAdaptor() (*EasyDriver, *gpioTestAdaptor) {
|
||||
const anglePerStep = 0.5 // use non int step angle to check int math
|
||||
|
||||
a := newGpioTestAdaptor()
|
||||
d := NewEasyDriver(a, stepAngle, "1", "2", "3", "4")
|
||||
d := NewEasyDriver(a, anglePerStep, "1", "2", "3", "4")
|
||||
return d, a
|
||||
}
|
||||
|
||||
func TestNewEasyDriver(t *testing.T) {
|
||||
// arrange
|
||||
const anglePerStep = 0.5 // use non int step angle to check int math
|
||||
|
||||
a := newGpioTestAdaptor()
|
||||
// act
|
||||
d := NewEasyDriver(a, stepAngle, "1", "2", "3", "4")
|
||||
d := NewEasyDriver(a, anglePerStep, "1", "2", "3", "4")
|
||||
// assert
|
||||
assert.IsType(t, &EasyDriver{}, d)
|
||||
assert.True(t, strings.HasPrefix(d.name, "EasyDriver"))
|
||||
@ -39,30 +37,18 @@ func TestNewEasyDriver(t *testing.T) {
|
||||
assert.Equal(t, "2", d.dirPin)
|
||||
assert.Equal(t, "3", d.enPin)
|
||||
assert.Equal(t, "4", d.sleepPin)
|
||||
assert.Equal(t, float32(stepAngle), d.angle)
|
||||
assert.Equal(t, uint(180), d.rpm)
|
||||
assert.Equal(t, int8(1), d.dir)
|
||||
assert.Equal(t, float32(anglePerStep), d.anglePerStep)
|
||||
assert.Equal(t, uint(14), d.speedRpm)
|
||||
assert.Equal(t, "forward", d.direction)
|
||||
assert.Equal(t, 0, d.stepNum)
|
||||
assert.Equal(t, true, d.enabled)
|
||||
assert.Equal(t, false, d.disabled)
|
||||
assert.Equal(t, false, d.sleeping)
|
||||
assert.Nil(t, d.runStopChan)
|
||||
assert.Nil(t, d.stopAsynchRunFunc)
|
||||
}
|
||||
|
||||
func TestEasyDriverHalt(t *testing.T) {
|
||||
// arrange
|
||||
d, _ := initTestEasyDriverWithStubbedAdaptor()
|
||||
require.NoError(t, d.Run())
|
||||
require.True(t, d.IsMoving())
|
||||
// act
|
||||
err := d.Halt()
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, d.IsMoving())
|
||||
}
|
||||
|
||||
func TestEasyDriverMove(t *testing.T) {
|
||||
func TestEasyDriverMoveDeg_IsMoving(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
inputSteps int
|
||||
inputDeg int
|
||||
simulateDisabled bool
|
||||
simulateAlreadyRunning bool
|
||||
simulateWriteErr bool
|
||||
@ -72,13 +58,13 @@ func TestEasyDriverMove(t *testing.T) {
|
||||
wantErr string
|
||||
}{
|
||||
"move_one": {
|
||||
inputSteps: 1,
|
||||
inputDeg: 1,
|
||||
wantWrites: 4,
|
||||
wantSteps: 2,
|
||||
wantMoving: false,
|
||||
},
|
||||
"move_more": {
|
||||
inputSteps: 20,
|
||||
inputDeg: 20,
|
||||
wantWrites: 80,
|
||||
wantSteps: 40,
|
||||
wantMoving: false,
|
||||
@ -94,9 +80,9 @@ func TestEasyDriverMove(t *testing.T) {
|
||||
wantErr: "already running or moving",
|
||||
},
|
||||
"error_write": {
|
||||
inputSteps: 1,
|
||||
inputDeg: 1,
|
||||
simulateWriteErr: true,
|
||||
wantWrites: 1,
|
||||
wantWrites: 0,
|
||||
wantMoving: false,
|
||||
wantErr: "write error",
|
||||
},
|
||||
@ -105,21 +91,23 @@ func TestEasyDriverMove(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, a := initTestEasyDriverWithStubbedAdaptor()
|
||||
d.enabled = !tc.simulateDisabled
|
||||
if tc.simulateAlreadyRunning {
|
||||
d.runStopChan = make(chan struct{})
|
||||
defer func() { close(d.runStopChan); d.runStopChan = nil }()
|
||||
}
|
||||
var numCallsWrite int
|
||||
a.digitalWriteFunc = func(string, byte) error {
|
||||
numCallsWrite++
|
||||
if tc.simulateWriteErr {
|
||||
return fmt.Errorf("write error")
|
||||
defer func() {
|
||||
// for cleanup dangling channels
|
||||
if d.stopAsynchRunFunc != nil {
|
||||
err := d.stopAsynchRunFunc(true)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
// arrange: different behavior
|
||||
d.disabled = tc.simulateDisabled
|
||||
if tc.simulateAlreadyRunning {
|
||||
d.stopAsynchRunFunc = func(bool) error { return nil }
|
||||
}
|
||||
// arrange: writes
|
||||
a.written = nil // reset writes of Start()
|
||||
a.simulateWriteError = tc.simulateWriteErr
|
||||
// act
|
||||
err := d.Move(tc.inputSteps)
|
||||
err := d.MoveDeg(tc.inputDeg)
|
||||
// assert
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
@ -127,7 +115,7 @@ func TestEasyDriverMove(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tc.wantSteps, d.stepNum)
|
||||
assert.Equal(t, tc.wantWrites, numCallsWrite)
|
||||
assert.Equal(t, tc.wantWrites, len(a.written))
|
||||
assert.Equal(t, tc.wantMoving, d.IsMoving())
|
||||
})
|
||||
}
|
||||
@ -163,10 +151,10 @@ func TestEasyDriverRun_IsMoving(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, a := initTestEasyDriverWithStubbedAdaptor()
|
||||
d.enabled = !tc.simulateDisabled
|
||||
d.skipStepErrors = true
|
||||
d.disabled = tc.simulateDisabled
|
||||
if tc.simulateAlreadyRunning {
|
||||
d.runStopChan = make(chan struct{})
|
||||
defer func() { close(d.runStopChan); d.runStopChan = nil }()
|
||||
d.stopAsynchRunFunc = func(bool) error { return nil }
|
||||
}
|
||||
simWriteErr := tc.simulateWriteErr // to prevent data race in write function (go-called)
|
||||
a.digitalWriteFunc = func(string, byte) error {
|
||||
@ -201,45 +189,250 @@ func TestEasyDriverStop_IsMoving(t *testing.T) {
|
||||
assert.False(t, d.IsMoving())
|
||||
}
|
||||
|
||||
func TestEasyDriverStep(t *testing.T) {
|
||||
func TestEasyDriverHalt_IsMoving(t *testing.T) {
|
||||
// arrange
|
||||
d, _ := initTestEasyDriverWithStubbedAdaptor()
|
||||
require.NoError(t, d.Run())
|
||||
require.True(t, d.IsMoving())
|
||||
// act
|
||||
err := d.Halt()
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, d.IsMoving())
|
||||
}
|
||||
|
||||
func TestEasyDriverSetDirection(t *testing.T) {
|
||||
const anglePerStep = 0.5 // use non int step angle to check int math
|
||||
|
||||
tests := map[string]struct {
|
||||
countCallsForth int
|
||||
countCallsBack int
|
||||
simulateAlreadyRunning bool
|
||||
simulateWriteErr bool
|
||||
wantSteps int
|
||||
wantWritten []byte
|
||||
wantErr string
|
||||
input string
|
||||
dirPin string
|
||||
simulateWriteErr bool
|
||||
wantVal string
|
||||
wantWritten byte
|
||||
wantErr string
|
||||
}{
|
||||
"forward": {
|
||||
input: "forward",
|
||||
dirPin: "10",
|
||||
wantWritten: 0,
|
||||
wantVal: "forward",
|
||||
},
|
||||
"backward": {
|
||||
input: "backward",
|
||||
dirPin: "11",
|
||||
wantWritten: 1,
|
||||
wantVal: "backward",
|
||||
},
|
||||
"unknown": {
|
||||
input: "unknown",
|
||||
dirPin: "12",
|
||||
wantWritten: 0xFF,
|
||||
wantVal: "forward",
|
||||
wantErr: "Invalid direction 'unknown'",
|
||||
},
|
||||
"error_no_pin": {
|
||||
input: "forward",
|
||||
dirPin: "",
|
||||
wantWritten: 0xFF,
|
||||
wantVal: "forward",
|
||||
wantErr: "dirPin is not set",
|
||||
},
|
||||
"error_write": {
|
||||
input: "backward",
|
||||
dirPin: "13",
|
||||
simulateWriteErr: true,
|
||||
wantWritten: 0xFF,
|
||||
wantVal: "forward",
|
||||
wantErr: "write error",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := newGpioTestAdaptor()
|
||||
d := NewEasyDriver(a, anglePerStep, "1", tc.dirPin, "3", "4")
|
||||
a.written = nil // reset writes of Start()
|
||||
a.simulateWriteError = tc.simulateWriteErr
|
||||
require.Equal(t, "forward", d.direction)
|
||||
// act
|
||||
err := d.SetDirection(tc.input)
|
||||
// assert
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.dirPin, a.written[0].pin)
|
||||
assert.Equal(t, tc.wantWritten, a.written[0].val)
|
||||
}
|
||||
assert.Equal(t, tc.wantVal, d.direction)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyDriverMaxSpeed(t *testing.T) {
|
||||
const delayForMaxSpeed = 1428 * time.Microsecond // 1/700Hz
|
||||
|
||||
tests := map[string]struct {
|
||||
anglePerStep float32
|
||||
want uint
|
||||
}{
|
||||
"maxspeed_for_20spr": {
|
||||
anglePerStep: 360.0 / 20.0,
|
||||
want: 2100,
|
||||
},
|
||||
"maxspeed_for_36spr": {
|
||||
anglePerStep: 360.0 / 36.0,
|
||||
want: 1166,
|
||||
},
|
||||
"maxspeed_for_50spr": {
|
||||
anglePerStep: 360.0 / 50.0,
|
||||
want: 840,
|
||||
},
|
||||
"maxspeed_for_100spr": {
|
||||
anglePerStep: 360.0 / 100.0,
|
||||
want: 420,
|
||||
},
|
||||
"maxspeed_for_400spr": {
|
||||
anglePerStep: 360.0 / 400.0,
|
||||
want: 105,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, _ := initTestEasyDriverWithStubbedAdaptor()
|
||||
d.anglePerStep = tc.anglePerStep
|
||||
d.stepsPerRev = 360.0 / tc.anglePerStep
|
||||
// act
|
||||
got := d.MaxSpeed()
|
||||
d.speedRpm = got
|
||||
got2 := d.getDelayPerStep()
|
||||
// assert
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.Equal(t, delayForMaxSpeed.Microseconds()/10, got2.Microseconds()/10)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyDriverSetSpeed(t *testing.T) {
|
||||
const (
|
||||
anglePerStep = 10
|
||||
maxRpm = 1166
|
||||
)
|
||||
|
||||
tests := map[string]struct {
|
||||
input uint
|
||||
want uint
|
||||
wantErr string
|
||||
}{
|
||||
"below_minimum": {
|
||||
input: 0,
|
||||
want: 0,
|
||||
wantErr: "RPM (0) cannot be a zero or negative value",
|
||||
},
|
||||
"minimum": {
|
||||
input: 1,
|
||||
want: 1,
|
||||
},
|
||||
"maximum": {
|
||||
input: maxRpm,
|
||||
want: maxRpm,
|
||||
},
|
||||
"above_maximum": {
|
||||
input: maxRpm + 1,
|
||||
want: maxRpm,
|
||||
wantErr: "cannot be greater then maximal value 1166",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, _ := initTestEasyDriverWithStubbedAdaptor()
|
||||
d.speedRpm = 0
|
||||
d.anglePerStep = anglePerStep
|
||||
d.stepsPerRev = 360.0 / anglePerStep
|
||||
// act
|
||||
err := d.SetSpeed(tc.input)
|
||||
// assert
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tc.want, d.speedRpm)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyDriver_onePinStepping(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
countCallsForth int
|
||||
countCallsBack int
|
||||
simulateWriteErr bool
|
||||
wantSteps int
|
||||
wantWritten []gpioTestWritten
|
||||
wantErr string
|
||||
}{
|
||||
"single": {
|
||||
countCallsForth: 1,
|
||||
wantSteps: 1,
|
||||
wantWritten: []byte{0x00, 0x01},
|
||||
wantWritten: []gpioTestWritten{
|
||||
{pin: "1", val: 0x00},
|
||||
{pin: "1", val: 0x01},
|
||||
},
|
||||
},
|
||||
"many": {
|
||||
countCallsForth: 4,
|
||||
wantSteps: 4,
|
||||
wantWritten: []byte{0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1},
|
||||
wantWritten: []gpioTestWritten{
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
},
|
||||
},
|
||||
"forth_and_back": {
|
||||
countCallsForth: 5,
|
||||
countCallsBack: 3,
|
||||
wantSteps: 2,
|
||||
wantWritten: []byte{0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1},
|
||||
wantWritten: []gpioTestWritten{
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
},
|
||||
},
|
||||
"reverse": {
|
||||
countCallsBack: 3,
|
||||
wantSteps: -3,
|
||||
wantWritten: []byte{0x0, 0x1, 0x0, 0x1, 0x0, 0x1},
|
||||
},
|
||||
"error_already_running": {
|
||||
countCallsForth: 1,
|
||||
simulateAlreadyRunning: true,
|
||||
wantErr: "already running or moving",
|
||||
wantWritten: []gpioTestWritten{
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
{pin: "1", val: 0x0},
|
||||
{pin: "1", val: 0x1},
|
||||
},
|
||||
},
|
||||
"error_write": {
|
||||
simulateWriteErr: true,
|
||||
wantWritten: []byte{0x00, 0x00},
|
||||
countCallsBack: 2,
|
||||
wantErr: "write error",
|
||||
},
|
||||
@ -248,29 +441,18 @@ func TestEasyDriverStep(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, a := initTestEasyDriverWithStubbedAdaptor()
|
||||
if tc.simulateAlreadyRunning {
|
||||
d.runStopChan = make(chan struct{})
|
||||
defer func() { close(d.runStopChan); d.runStopChan = nil }()
|
||||
}
|
||||
var writtenValues []byte
|
||||
a.digitalWriteFunc = func(pin string, val byte) error {
|
||||
assert.Equal(t, d.stepPin, pin)
|
||||
writtenValues = append(writtenValues, val)
|
||||
if tc.simulateWriteErr {
|
||||
return fmt.Errorf("write error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
a.written = nil // reset writes of Start()
|
||||
a.simulateWriteError = tc.simulateWriteErr
|
||||
var errs []string
|
||||
// act
|
||||
for i := 0; i < tc.countCallsForth; i++ {
|
||||
if err := d.Step(); err != nil {
|
||||
if err := d.onePinStepping(); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
}
|
||||
d.dir = -1
|
||||
d.direction = "backward"
|
||||
for i := 0; i < tc.countCallsBack; i++ {
|
||||
if err := d.Step(); err != nil {
|
||||
if err := d.onePinStepping(); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
}
|
||||
@ -282,127 +464,14 @@ func TestEasyDriverStep(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, tc.wantSteps, d.stepNum)
|
||||
assert.Equal(t, tc.wantSteps, d.CurrentStep())
|
||||
assert.Equal(t, tc.wantWritten, writtenValues)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyDriverSetDirection(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
dirPin string
|
||||
input string
|
||||
wantVal int8
|
||||
wantErr string
|
||||
}{
|
||||
"cw": {
|
||||
input: "cw",
|
||||
dirPin: "10",
|
||||
wantVal: 1,
|
||||
},
|
||||
"ccw": {
|
||||
input: "ccw",
|
||||
dirPin: "11",
|
||||
wantVal: -1,
|
||||
},
|
||||
"unknown": {
|
||||
input: "unknown",
|
||||
dirPin: "12",
|
||||
wantVal: 1,
|
||||
},
|
||||
"error_no_pin": {
|
||||
dirPin: "",
|
||||
wantVal: 1,
|
||||
wantErr: "dirPin is not set",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := newGpioTestAdaptor()
|
||||
d := NewEasyDriver(a, stepAngle, "1", tc.dirPin, "3", "4")
|
||||
require.Equal(t, int8(1), d.dir)
|
||||
// act
|
||||
err := d.SetDirection(tc.input)
|
||||
// assert
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tc.wantVal, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyDriverSetSpeed(t *testing.T) {
|
||||
const (
|
||||
angle = 10
|
||||
max = 36 // 360/angle
|
||||
)
|
||||
|
||||
tests := map[string]struct {
|
||||
input uint
|
||||
want uint
|
||||
}{
|
||||
"below_minimum": {
|
||||
input: 0,
|
||||
want: 1,
|
||||
},
|
||||
"minimum": {
|
||||
input: 1,
|
||||
want: 1,
|
||||
},
|
||||
"maximum": {
|
||||
input: max,
|
||||
want: max,
|
||||
},
|
||||
"above_maximum": {
|
||||
input: max + 1,
|
||||
want: max,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d := EasyDriver{angle: angle}
|
||||
// act
|
||||
err := d.SetSpeed(tc.input)
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.want, d.rpm)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyDriverMaxSpeed(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
angle float32
|
||||
want uint
|
||||
}{
|
||||
"180": {
|
||||
angle: 2.0,
|
||||
want: 180,
|
||||
},
|
||||
"360": {
|
||||
angle: 1.0,
|
||||
want: 360,
|
||||
},
|
||||
"720": {
|
||||
angle: 0.5,
|
||||
want: 720,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d := EasyDriver{angle: tc.angle}
|
||||
// act & assert
|
||||
assert.Equal(t, tc.want, d.MaxSpeed())
|
||||
assert.Equal(t, tc.wantWritten, a.written)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyDriverEnable_IsEnabled(t *testing.T) {
|
||||
const anglePerStep = 0.5 // use non int step angle to check int math
|
||||
|
||||
tests := map[string]struct {
|
||||
enPin string
|
||||
simulateWriteErr bool
|
||||
@ -429,7 +498,7 @@ func TestEasyDriverEnable_IsEnabled(t *testing.T) {
|
||||
"error_write": {
|
||||
enPin: "12",
|
||||
simulateWriteErr: true,
|
||||
wantWrites: 1,
|
||||
wantWrites: 0,
|
||||
wantEnabled: false,
|
||||
wantErr: "write error",
|
||||
},
|
||||
@ -438,42 +507,34 @@ func TestEasyDriverEnable_IsEnabled(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := newGpioTestAdaptor()
|
||||
d := NewEasyDriver(a, stepAngle, "1", "2", tc.enPin, "4")
|
||||
var numCallsWrite int
|
||||
var writtenPin string
|
||||
writtenValue := byte(0xFF)
|
||||
a.digitalWriteFunc = func(pin string, val byte) error {
|
||||
numCallsWrite++
|
||||
writtenPin = pin
|
||||
writtenValue = val
|
||||
if tc.simulateWriteErr {
|
||||
return fmt.Errorf("write error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
d.enabled = false
|
||||
d := NewEasyDriver(a, anglePerStep, "1", "2", tc.enPin, "4")
|
||||
a.written = nil // reset writes of Start()
|
||||
a.simulateWriteError = tc.simulateWriteErr
|
||||
d.disabled = true
|
||||
require.False(t, d.IsEnabled())
|
||||
// act
|
||||
err := d.Enable()
|
||||
// assert
|
||||
assert.Equal(t, tc.wantWrites, len(a.written))
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, byte(0), writtenValue) // enable pin is active low
|
||||
assert.Equal(t, tc.enPin, a.written[0].pin)
|
||||
assert.Equal(t, byte(0), a.written[0].val) // enable pin is active low
|
||||
}
|
||||
assert.Equal(t, tc.wantEnabled, d.IsEnabled())
|
||||
assert.Equal(t, tc.wantWrites, numCallsWrite)
|
||||
assert.Equal(t, tc.enPin, writtenPin)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyDriverDisable_IsEnabled(t *testing.T) {
|
||||
const anglePerStep = 0.5 // use non int step angle to check int math
|
||||
|
||||
tests := map[string]struct {
|
||||
enPin string
|
||||
runBefore bool
|
||||
simulateWriteErr string
|
||||
simulateWriteErr bool
|
||||
wantWrites int
|
||||
wantEnabled bool
|
||||
wantErr string
|
||||
@ -497,7 +558,7 @@ func TestEasyDriverDisable_IsEnabled(t *testing.T) {
|
||||
},
|
||||
"error_write": {
|
||||
enPin: "12",
|
||||
simulateWriteErr: "write error",
|
||||
simulateWriteErr: true,
|
||||
wantWrites: 1,
|
||||
wantEnabled: true,
|
||||
wantErr: "write error",
|
||||
@ -507,14 +568,11 @@ func TestEasyDriverDisable_IsEnabled(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := newGpioTestAdaptor()
|
||||
d := NewEasyDriver(a, stepAngle, "1", "2", tc.enPin, "4")
|
||||
writeMutex := sync.Mutex{}
|
||||
d := NewEasyDriver(a, anglePerStep, "1", "2", tc.enPin, "4")
|
||||
var numCallsWrite int
|
||||
var writtenPin string
|
||||
writtenValue := byte(0xFF)
|
||||
a.digitalWriteFunc = func(pin string, val byte) error {
|
||||
writeMutex.Lock()
|
||||
defer writeMutex.Unlock()
|
||||
if pin == d.stepPin {
|
||||
// we do not consider call of step()
|
||||
return nil
|
||||
@ -522,8 +580,8 @@ func TestEasyDriverDisable_IsEnabled(t *testing.T) {
|
||||
numCallsWrite++
|
||||
writtenPin = pin
|
||||
writtenValue = val
|
||||
if tc.simulateWriteErr != "" {
|
||||
return fmt.Errorf(tc.simulateWriteErr)
|
||||
if tc.simulateWriteErr {
|
||||
return fmt.Errorf("write error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -532,7 +590,7 @@ func TestEasyDriverDisable_IsEnabled(t *testing.T) {
|
||||
require.True(t, d.IsMoving())
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
d.enabled = true
|
||||
d.disabled = false
|
||||
require.True(t, d.IsEnabled())
|
||||
// act
|
||||
err := d.Disable()
|
||||
@ -552,37 +610,68 @@ func TestEasyDriverDisable_IsEnabled(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEasyDriverSleep_IsSleeping(t *testing.T) {
|
||||
const anglePerStep = 0.5 // use non int step angle to check int math
|
||||
|
||||
tests := map[string]struct {
|
||||
sleepPin string
|
||||
runBefore bool
|
||||
wantSleep bool
|
||||
wantErr string
|
||||
sleepPin string
|
||||
runBefore bool
|
||||
simulateWriteErr bool
|
||||
wantWrites int
|
||||
wantSleep bool
|
||||
wantErr string
|
||||
}{
|
||||
"basic": {
|
||||
sleepPin: "10",
|
||||
wantSleep: true,
|
||||
sleepPin: "10",
|
||||
wantWrites: 1,
|
||||
wantSleep: true,
|
||||
},
|
||||
"with_run": {
|
||||
sleepPin: "11",
|
||||
runBefore: true,
|
||||
wantSleep: true,
|
||||
sleepPin: "11",
|
||||
runBefore: true,
|
||||
wantWrites: 1,
|
||||
wantSleep: true,
|
||||
},
|
||||
"error_no_pin": {
|
||||
sleepPin: "",
|
||||
wantSleep: false,
|
||||
wantErr: "sleepPin is not set",
|
||||
sleepPin: "",
|
||||
wantSleep: false,
|
||||
wantWrites: 0,
|
||||
wantErr: "sleepPin is not set",
|
||||
},
|
||||
"error_write": {
|
||||
sleepPin: "12",
|
||||
simulateWriteErr: true,
|
||||
wantWrites: 1,
|
||||
wantSleep: false,
|
||||
wantErr: "write error",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := newGpioTestAdaptor()
|
||||
d := NewEasyDriver(a, stepAngle, "1", "2", "3", tc.sleepPin)
|
||||
d := NewEasyDriver(a, anglePerStep, "1", "2", "3", tc.sleepPin)
|
||||
d.sleeping = false
|
||||
require.False(t, d.IsSleeping())
|
||||
// arrange: writes
|
||||
var numCallsWrite int
|
||||
var writtenPin string
|
||||
writtenValue := byte(0xFF)
|
||||
a.digitalWriteFunc = func(pin string, val byte) error {
|
||||
if pin == d.stepPin {
|
||||
// we do not consider call of step()
|
||||
return nil
|
||||
}
|
||||
numCallsWrite++
|
||||
writtenPin = pin
|
||||
writtenValue = val
|
||||
if tc.simulateWriteErr {
|
||||
return fmt.Errorf("write error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if tc.runBefore {
|
||||
require.NoError(t, d.Run())
|
||||
}
|
||||
d.sleeping = false
|
||||
require.False(t, d.IsSleeping())
|
||||
// act
|
||||
err := d.Sleep()
|
||||
// assert
|
||||
@ -590,35 +679,68 @@ func TestEasyDriverSleep_IsSleeping(t *testing.T) {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, byte(0), writtenValue) // sleep pin is active low
|
||||
}
|
||||
assert.Equal(t, tc.wantSleep, d.IsSleeping())
|
||||
assert.Equal(t, tc.wantWrites, numCallsWrite)
|
||||
assert.Equal(t, tc.sleepPin, writtenPin)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyDriverWake_IsSleeping(t *testing.T) {
|
||||
const anglePerStep = 0.5 // use non int step angle to check int math
|
||||
|
||||
tests := map[string]struct {
|
||||
sleepPin string
|
||||
wantSleep bool
|
||||
wantErr string
|
||||
sleepPin string
|
||||
simulateWriteErr bool
|
||||
wantWrites int
|
||||
wantSleep bool
|
||||
wantErr string
|
||||
}{
|
||||
"basic": {
|
||||
sleepPin: "10",
|
||||
wantSleep: false,
|
||||
sleepPin: "10",
|
||||
wantWrites: 1,
|
||||
wantSleep: false,
|
||||
},
|
||||
"error_no_pin": {
|
||||
sleepPin: "",
|
||||
wantSleep: true,
|
||||
wantErr: "sleepPin is not set",
|
||||
sleepPin: "",
|
||||
wantWrites: 0,
|
||||
wantSleep: true,
|
||||
wantErr: "sleepPin is not set",
|
||||
},
|
||||
"error_write": {
|
||||
sleepPin: "12",
|
||||
simulateWriteErr: true,
|
||||
wantWrites: 1,
|
||||
wantSleep: true,
|
||||
wantErr: "write error",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
a := newGpioTestAdaptor()
|
||||
d := NewEasyDriver(a, stepAngle, "1", "2", "3", tc.sleepPin)
|
||||
d := NewEasyDriver(a, anglePerStep, "1", "2", "3", tc.sleepPin)
|
||||
d.sleeping = true
|
||||
require.True(t, d.IsSleeping())
|
||||
// arrange: writes
|
||||
var numCallsWrite int
|
||||
var writtenPin string
|
||||
writtenValue := byte(0xFF)
|
||||
a.digitalWriteFunc = func(pin string, val byte) error {
|
||||
if pin == d.stepPin {
|
||||
// we do not consider call of step()
|
||||
return nil
|
||||
}
|
||||
numCallsWrite++
|
||||
writtenPin = pin
|
||||
writtenValue = val
|
||||
if tc.simulateWriteErr {
|
||||
return fmt.Errorf("write error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// act
|
||||
err := d.Wake()
|
||||
// assert
|
||||
@ -626,8 +748,11 @@ func TestEasyDriverWake_IsSleeping(t *testing.T) {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, byte(1), writtenValue) // sleep pin is active low
|
||||
}
|
||||
assert.Equal(t, tc.wantSleep, d.IsSleeping())
|
||||
assert.Equal(t, tc.wantWrites, numCallsWrite)
|
||||
assert.Equal(t, tc.sleepPin, writtenPin)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -18,15 +18,22 @@ type digitalPinMock struct {
|
||||
writeFunc func(val int) (err error)
|
||||
}
|
||||
|
||||
type gpioTestWritten struct {
|
||||
pin string
|
||||
val byte
|
||||
}
|
||||
|
||||
type gpioTestAdaptor struct {
|
||||
name string
|
||||
pinMap map[string]gobot.DigitalPinner
|
||||
port string
|
||||
mtx sync.Mutex
|
||||
digitalReadFunc func(ping string) (val int, err error)
|
||||
digitalWriteFunc func(pin string, val byte) (err error)
|
||||
pwmWriteFunc func(pin string, val byte) (err error)
|
||||
servoWriteFunc func(pin string, val byte) (err error)
|
||||
name string
|
||||
pinMap map[string]gobot.DigitalPinner
|
||||
port string
|
||||
written []gpioTestWritten
|
||||
simulateWriteError bool
|
||||
mtx sync.Mutex
|
||||
digitalReadFunc func(ping string) (val int, err error)
|
||||
digitalWriteFunc func(pin string, val byte) (err error)
|
||||
pwmWriteFunc func(pin string, val byte) (err error)
|
||||
servoWriteFunc func(pin string, val byte) (err error)
|
||||
}
|
||||
|
||||
func newGpioTestAdaptor() *gpioTestAdaptor {
|
||||
@ -62,6 +69,11 @@ func (t *gpioTestAdaptor) DigitalRead(pin string) (val int, err error) {
|
||||
func (t *gpioTestAdaptor) DigitalWrite(pin string, val byte) (err error) {
|
||||
t.mtx.Lock()
|
||||
defer t.mtx.Unlock()
|
||||
if t.simulateWriteError {
|
||||
return fmt.Errorf("write error")
|
||||
}
|
||||
w := gpioTestWritten{pin: pin, val: val}
|
||||
t.written = append(t.written, w)
|
||||
return t.digitalWriteFunc(pin, val)
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
package gpio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -11,30 +14,39 @@ import (
|
||||
"gobot.io/x/gobot/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
stepperDriverDebug = false
|
||||
|
||||
// StepperDriverForward is to set the stepper to run in forward direction (e.g. turn clock wise)
|
||||
StepperDriverForward = "forward"
|
||||
// StepperDriverBackward is to set the stepper to run in backward direction (e.g. turn counter clock wise)
|
||||
StepperDriverBackward = "backward"
|
||||
)
|
||||
|
||||
type phase [][4]byte
|
||||
|
||||
// StepperModes to decide on Phase and Stepping
|
||||
var StepperModes = struct {
|
||||
SinglePhaseStepping [][4]byte
|
||||
DualPhaseStepping [][4]byte
|
||||
HalfStepping [][4]byte
|
||||
SinglePhaseStepping phase
|
||||
DualPhaseStepping phase
|
||||
HalfStepping phase
|
||||
}{
|
||||
// 1 cycle = 4 steps with lesser torque
|
||||
SinglePhaseStepping: [][4]byte{
|
||||
SinglePhaseStepping: phase{
|
||||
{1, 0, 0, 0},
|
||||
{0, 1, 0, 0},
|
||||
{0, 0, 1, 0},
|
||||
{0, 0, 0, 1},
|
||||
},
|
||||
// 1 cycle = 4 steps with higher torque and current
|
||||
DualPhaseStepping: [][4]byte{
|
||||
DualPhaseStepping: phase{
|
||||
{1, 0, 0, 1},
|
||||
{1, 1, 0, 0},
|
||||
{0, 1, 1, 0},
|
||||
{0, 0, 1, 1},
|
||||
},
|
||||
// 1 cycle = 8 steps with lesser torque than full stepping
|
||||
HalfStepping: [][4]byte{
|
||||
HalfStepping: phase{
|
||||
{1, 0, 0, 1},
|
||||
{1, 0, 0, 0},
|
||||
{1, 1, 0, 0},
|
||||
@ -46,19 +58,26 @@ var StepperModes = struct {
|
||||
},
|
||||
}
|
||||
|
||||
// StepperDriver object
|
||||
// StepperDriver is a common driver for stepper motors. It supports 3 different stepping modes.
|
||||
type StepperDriver struct {
|
||||
name string
|
||||
*Driver
|
||||
|
||||
pins [4]string
|
||||
connection DigitalWriter
|
||||
phase phase
|
||||
stepsPerRev uint
|
||||
moving bool
|
||||
direction string
|
||||
stepNum int
|
||||
speed uint
|
||||
mutex *sync.Mutex
|
||||
gobot.Commander
|
||||
stepsPerRev float32
|
||||
|
||||
stepperDebug bool
|
||||
speedRpm uint
|
||||
direction string
|
||||
skipStepErrors bool
|
||||
haltIfRunning bool // stop automatically if run is called
|
||||
disabled bool
|
||||
valueMutex *sync.Mutex // to ensure that read and write of values do not interfere
|
||||
|
||||
stepFunc func() error
|
||||
sleepFunc func() error
|
||||
stepNum int
|
||||
stopAsynchRunFunc func(bool) error
|
||||
}
|
||||
|
||||
// NewStepperDriver returns a new StepperDriver given a
|
||||
@ -67,193 +86,401 @@ type StepperDriver struct {
|
||||
// Phase - Defined by StepperModes {SinglePhaseStepping, DualPhaseStepping, HalfStepping}
|
||||
// Steps - No of steps per revolution of Stepper motor
|
||||
func NewStepperDriver(a DigitalWriter, pins [4]string, phase phase, stepsPerRev uint) *StepperDriver {
|
||||
s := &StepperDriver{
|
||||
name: gobot.DefaultName("Stepper"),
|
||||
connection: a,
|
||||
pins: pins,
|
||||
phase: phase,
|
||||
stepsPerRev: stepsPerRev,
|
||||
moving: false,
|
||||
direction: "forward",
|
||||
stepNum: 0,
|
||||
speed: 1,
|
||||
mutex: &sync.Mutex{},
|
||||
Commander: gobot.NewCommander(),
|
||||
if stepsPerRev <= 0 {
|
||||
panic("steps per revolution needs to be greater than zero")
|
||||
}
|
||||
s.speed = s.GetMaxSpeed()
|
||||
d := &StepperDriver{
|
||||
Driver: NewDriver(a.(gobot.Connection), "Stepper"),
|
||||
pins: pins,
|
||||
phase: phase,
|
||||
stepsPerRev: float32(stepsPerRev),
|
||||
stepperDebug: stepperDriverDebug,
|
||||
skipStepErrors: false,
|
||||
haltIfRunning: true,
|
||||
direction: StepperDriverForward,
|
||||
stepNum: 0,
|
||||
speedRpm: 1,
|
||||
valueMutex: &sync.Mutex{},
|
||||
}
|
||||
d.speedRpm = d.MaxSpeed()
|
||||
d.stepFunc = d.phasedStepping
|
||||
d.sleepFunc = d.sleepOuputs
|
||||
d.beforeHalt = d.shutdown
|
||||
|
||||
s.AddCommand("Move", func(params map[string]interface{}) interface{} {
|
||||
d.AddCommand("MoveDeg", func(params map[string]interface{}) interface{} {
|
||||
degs, _ := strconv.Atoi(params["degs"].(string))
|
||||
return d.MoveDeg(degs)
|
||||
})
|
||||
d.AddCommand("Move", func(params map[string]interface{}) interface{} {
|
||||
steps, _ := strconv.Atoi(params["steps"].(string))
|
||||
return s.Move(steps)
|
||||
return d.Move(steps)
|
||||
})
|
||||
s.AddCommand("Run", func(params map[string]interface{}) interface{} {
|
||||
return s.Run()
|
||||
d.AddCommand("Step", func(params map[string]interface{}) interface{} {
|
||||
return d.Move(1)
|
||||
})
|
||||
s.AddCommand("Halt", func(params map[string]interface{}) interface{} {
|
||||
return s.Halt()
|
||||
d.AddCommand("Run", func(params map[string]interface{}) interface{} {
|
||||
return d.Run()
|
||||
})
|
||||
d.AddCommand("Sleep", func(params map[string]interface{}) interface{} {
|
||||
return d.Sleep()
|
||||
})
|
||||
d.AddCommand("Stop", func(params map[string]interface{}) interface{} {
|
||||
return d.Stop()
|
||||
})
|
||||
d.AddCommand("Halt", func(params map[string]interface{}) interface{} {
|
||||
return d.Halt()
|
||||
})
|
||||
|
||||
return s
|
||||
return d
|
||||
}
|
||||
|
||||
// Name of StepperDriver
|
||||
func (s *StepperDriver) Name() string { return s.name }
|
||||
// Move moves the motor for given number of steps.
|
||||
func (d *StepperDriver) Move(stepsToMove int) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
// SetName sets name for StepperDriver
|
||||
func (s *StepperDriver) SetName(n string) { s.name = n }
|
||||
|
||||
// Connection returns StepperDriver's connection
|
||||
func (s *StepperDriver) Connection() gobot.Connection { return s.connection.(gobot.Connection) }
|
||||
|
||||
// Start implements the Driver interface and keeps running the stepper till halt is called
|
||||
func (s *StepperDriver) Start() error { return nil }
|
||||
|
||||
// Run continuously runs the stepper
|
||||
func (s *StepperDriver) Run() error {
|
||||
// halt if already moving
|
||||
if s.moving {
|
||||
if err := s.Halt(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.stepAsynch(float64(stepsToMove)); err != nil {
|
||||
// something went wrong with preparation
|
||||
return err
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
s.moving = true
|
||||
s.mutex.Unlock()
|
||||
err := d.stopAsynchRunFunc(false) // wait to finish with err or nil
|
||||
d.stopAsynchRunFunc = nil
|
||||
|
||||
delay := s.getDelayPerStep()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if !s.moving {
|
||||
break
|
||||
}
|
||||
if err := s.step(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Halt implements the Driver interface and halts the motion of the Stepper
|
||||
func (s *StepperDriver) Halt() error {
|
||||
s.mutex.Lock()
|
||||
s.moving = false
|
||||
s.mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
// MoveDeg moves the motor given number of degrees at current speed. Negative values cause to move in backward direction.
|
||||
func (d *StepperDriver) MoveDeg(degs int) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
// SetDirection sets the direction in which motor should be moving, Default is forward
|
||||
func (s *StepperDriver) SetDirection(direction string) error {
|
||||
direction = strings.ToLower(direction)
|
||||
if direction != "forward" && direction != "backward" {
|
||||
return errors.New("Invalid direction. Value should be forward or backward")
|
||||
stepsToMove := float64(degs) * float64(d.stepsPerRev) / 360
|
||||
|
||||
if err := d.stepAsynch(stepsToMove); err != nil {
|
||||
// something went wrong with preparation
|
||||
return err
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
s.direction = direction
|
||||
s.mutex.Unlock()
|
||||
return nil
|
||||
err := d.stopAsynchRunFunc(false) // wait to finish with err or nil
|
||||
d.stopAsynchRunFunc = nil
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Run runs the stepper continuously. Stop needs to be done with call Stop().
|
||||
func (d *StepperDriver) Run() error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
return d.stepAsynch(float64(math.MaxInt) + 1)
|
||||
}
|
||||
|
||||
// IsMoving returns a bool stating whether motor is currently in motion
|
||||
func (s *StepperDriver) IsMoving() bool {
|
||||
return s.moving
|
||||
func (d *StepperDriver) IsMoving() bool {
|
||||
return d.stopAsynchRunFunc != nil
|
||||
}
|
||||
|
||||
// Step moves motor one step in giving direction
|
||||
func (s *StepperDriver) step() error {
|
||||
if s.direction == "forward" {
|
||||
s.stepNum++
|
||||
} else {
|
||||
s.stepNum--
|
||||
// Stop running the stepper
|
||||
func (d *StepperDriver) Stop() error {
|
||||
if d.stopAsynchRunFunc == nil {
|
||||
return fmt.Errorf("'%s' is not yet started", d.name)
|
||||
}
|
||||
|
||||
if s.stepNum >= int(s.stepsPerRev) {
|
||||
s.stepNum = 0
|
||||
} else if s.stepNum < 0 {
|
||||
s.stepNum = int(s.stepsPerRev) - 1
|
||||
err := d.stopAsynchRunFunc(true)
|
||||
d.stopAsynchRunFunc = nil
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Sleep release all pins to the same output level, so no current is consumed anymore.
|
||||
func (d *StepperDriver) Sleep() error {
|
||||
return d.sleepFunc()
|
||||
}
|
||||
|
||||
// SetDirection sets the direction in which motor should be moving, default is forward.
|
||||
// Changing the direction affects the next step, also for asynchronous running.
|
||||
func (d *StepperDriver) SetDirection(direction string) error {
|
||||
direction = strings.ToLower(direction)
|
||||
if direction != StepperDriverForward && direction != StepperDriverBackward {
|
||||
return fmt.Errorf("Invalid direction '%s'. Value should be '%s' or '%s'",
|
||||
direction, StepperDriverForward, StepperDriverBackward)
|
||||
}
|
||||
|
||||
r := int(math.Abs(float64(s.stepNum))) % len(s.phase)
|
||||
|
||||
for i, v := range s.phase[r] {
|
||||
if err := s.connection.DigitalWrite(s.pins[i], v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// ensure that write of variable can not interfere with read in step()
|
||||
d.valueMutex.Lock()
|
||||
defer d.valueMutex.Unlock()
|
||||
d.direction = direction
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Move moves the motor for given number of steps
|
||||
func (s *StepperDriver) Move(stepsToMove int) error {
|
||||
if stepsToMove == 0 {
|
||||
return s.Halt()
|
||||
// MaxSpeed gives the max RPM of motor
|
||||
// max. speed is limited by:
|
||||
// * motor friction, inertia and inductance, load inertia
|
||||
// * full step rate is normally below 1000 per second (1kHz), typically not more than ~400 per second
|
||||
// * mostly not more than 1000-2000rpm (20-40 revolutions per second) are possible
|
||||
// * higher values can be achieved only by ramp-up the velocity
|
||||
// * duration of GPIO write (PI1 can reach up to 70kHz, typically 20kHz, so this is most likely not the limiting factor)
|
||||
// * the hardware driver, to force the high current transitions for the max. speed
|
||||
// * there are CNC steppers with 1000..20.000 steps per revolution, which works with faster step rates (e.g. 200kHz)
|
||||
func (d *StepperDriver) MaxSpeed() uint {
|
||||
const maxStepsPerSecond = 700 // a typical value for a normal, lightly loaded motor
|
||||
return uint(float32(60*maxStepsPerSecond) / d.stepsPerRev)
|
||||
}
|
||||
|
||||
// SetSpeed sets the rpm for the next move or run. A valid value is between 1 and MaxSpeed().
|
||||
// The run needs to be stopped and called again after set this value.
|
||||
func (d *StepperDriver) SetSpeed(rpm uint) error {
|
||||
var err error
|
||||
if rpm <= 0 {
|
||||
rpm = 0
|
||||
err = fmt.Errorf("RPM (%d) cannot be a zero or negative value", rpm)
|
||||
}
|
||||
|
||||
if s.moving {
|
||||
// stop previous motion
|
||||
if err := s.Halt(); err != nil {
|
||||
maxRpm := d.MaxSpeed()
|
||||
if rpm > maxRpm {
|
||||
rpm = maxRpm
|
||||
err = fmt.Errorf("RPM (%d) cannot be greater then maximal value %d", rpm, maxRpm)
|
||||
}
|
||||
|
||||
d.valueMutex.Lock()
|
||||
defer d.valueMutex.Unlock()
|
||||
d.speedRpm = rpm
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CurrentStep gives the current step of motor
|
||||
func (d *StepperDriver) CurrentStep() int {
|
||||
// ensure that read can not interfere with write in step()
|
||||
d.valueMutex.Lock()
|
||||
defer d.valueMutex.Unlock()
|
||||
|
||||
return d.stepNum
|
||||
}
|
||||
|
||||
// SetHaltIfRunning with the given value. Normally a call of Run() returns an error if already running. If set this
|
||||
// to true, the next call of Run() cause a automatic stop before.
|
||||
func (d *StepperDriver) SetHaltIfRunning(val bool) {
|
||||
d.haltIfRunning = val
|
||||
}
|
||||
|
||||
// shutdown the driver
|
||||
func (d *StepperDriver) shutdown() error {
|
||||
// stops the continuous motion of the stepper, if running
|
||||
return d.stopIfRunning()
|
||||
}
|
||||
|
||||
func (d *StepperDriver) stepAsynch(stepsToMove float64) error {
|
||||
if d.disabled {
|
||||
return fmt.Errorf("'%s' is disabled and can not be running or moving", d.name)
|
||||
}
|
||||
|
||||
// if running, return error or stop automatically
|
||||
if d.stopAsynchRunFunc != nil {
|
||||
if !d.haltIfRunning {
|
||||
return fmt.Errorf("'%s' already running or moving", d.name)
|
||||
}
|
||||
d.debug("stop former run forcefully")
|
||||
if err := d.stopAsynchRunFunc(true); err != nil {
|
||||
d.stopAsynchRunFunc = nil
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
s.moving = true
|
||||
s.direction = "forward"
|
||||
|
||||
if stepsToMove < 0 {
|
||||
s.direction = "backward"
|
||||
// prepare stepping behavior
|
||||
stepsLeft := uint64(math.Abs(stepsToMove))
|
||||
if stepsLeft == 0 {
|
||||
return fmt.Errorf("no steps to do for '%s'", d.name)
|
||||
}
|
||||
s.mutex.Unlock()
|
||||
|
||||
stepsLeft := int64(math.Abs(float64(stepsToMove)))
|
||||
delay := s.getDelayPerStep()
|
||||
// t [min] = steps [st] / (steps_per_revolution [st/u] * speed [u/min]) or
|
||||
// t [min] = steps [st] * delay_per_step [min/st], use safety factor 2 and a small offset of 100 ms
|
||||
// prepare this timeout outside of stop function to prevent data race with stepsLeft
|
||||
stopTimeout := time.Duration(2*stepsLeft)*d.getDelayPerStep() + 100*time.Millisecond
|
||||
endlessMovement := false
|
||||
|
||||
for stepsLeft > 0 {
|
||||
if err := s.step(); err != nil {
|
||||
return err
|
||||
if stepsLeft > math.MaxInt {
|
||||
stopTimeout = 100 * time.Millisecond
|
||||
endlessMovement = true
|
||||
} else {
|
||||
d.direction = "forward"
|
||||
if stepsToMove < 0 {
|
||||
d.direction = "backward"
|
||||
}
|
||||
stepsLeft--
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
||||
s.moving = false
|
||||
// prepare new asynchronous stepping
|
||||
onceDoneChan := make(chan struct{})
|
||||
runStopChan := make(chan struct{})
|
||||
runErrChan := make(chan error)
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt)
|
||||
|
||||
d.stopAsynchRunFunc = func(forceStop bool) error {
|
||||
defer func() {
|
||||
d.debug("RUN: cleanup stop channel")
|
||||
if runStopChan != nil {
|
||||
close(runStopChan)
|
||||
}
|
||||
runStopChan = nil
|
||||
d.debug("STOP: cleanup err channel")
|
||||
if runErrChan != nil {
|
||||
close(runErrChan)
|
||||
}
|
||||
runErrChan = nil
|
||||
d.debug("STOP: cleanup done")
|
||||
}()
|
||||
|
||||
d.debug("STOP: wait for once done")
|
||||
<-onceDoneChan // wait for the first step was called
|
||||
|
||||
// send stop for endless movement or a forceful stop happen
|
||||
if endlessMovement || forceStop {
|
||||
d.debug("STOP: send stop channel")
|
||||
runStopChan <- struct{}{}
|
||||
}
|
||||
|
||||
if !endlessMovement && forceStop {
|
||||
// do not wait if an normal movement was stopped forcefully
|
||||
log.Printf("'%s' was forcefully stopped\n", d.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// wait for go routine is finished and cleanup
|
||||
d.debug(fmt.Sprintf("STOP: wait %s for err channel", stopTimeout))
|
||||
select {
|
||||
case err := <-runErrChan:
|
||||
return err
|
||||
case <-time.After(stopTimeout):
|
||||
return fmt.Errorf("'%s' was not finished in %s", d.name, stopTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
d.debug(fmt.Sprintf("going to start go routine - endless=%t, steps=%d", endlessMovement, stepsLeft))
|
||||
go func(name string) {
|
||||
var err error
|
||||
var onceDone bool
|
||||
defer func() {
|
||||
// some cases here:
|
||||
// * stop by stop channel: error should be send as nil
|
||||
// * count of steps reached: error should be send as nil
|
||||
// * write error occurred
|
||||
// * for Run(): caller needs to send stop channel and read the error
|
||||
// * for Move(): caller waits for the error, but don't send stop channel
|
||||
//
|
||||
d.debug(fmt.Sprintf("RUN: write '%v' to err channel", err))
|
||||
runErrChan <- err
|
||||
}()
|
||||
for stepsLeft > 0 {
|
||||
select {
|
||||
case <-sigChan:
|
||||
d.debug("RUN: OS signal received")
|
||||
err = fmt.Errorf("OS signal received")
|
||||
return
|
||||
case <-runStopChan:
|
||||
d.debug("RUN: stop channel received")
|
||||
return
|
||||
default:
|
||||
if err == nil {
|
||||
err = d.stepFunc()
|
||||
if err != nil {
|
||||
if d.skipStepErrors {
|
||||
fmt.Printf("step skipped for '%s': %v\n", name, err)
|
||||
err = nil
|
||||
} else {
|
||||
d.debug("RUN: write error occurred")
|
||||
}
|
||||
}
|
||||
if !onceDone {
|
||||
close(onceDoneChan) // to inform that we are ready for stop now
|
||||
onceDone = true
|
||||
d.debug("RUN: once done")
|
||||
}
|
||||
if !endlessMovement {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
stepsLeft--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}(d.name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDelayPerStep gives the delay per step
|
||||
func (s *StepperDriver) getDelayPerStep() time.Duration {
|
||||
// Do not remove *1000 and change duration to time.Millisecond. It has been done for a reason
|
||||
return time.Duration(60000*1000/(s.stepsPerRev*s.speed)) * time.Microsecond
|
||||
// formula: delay_per_step [min] = 1/(steps_per_revolution * speed [rpm])
|
||||
func (d *StepperDriver) getDelayPerStep() time.Duration {
|
||||
// considering a max. speed of 1000 rpm and max. 1000 steps per revolution, a microsecond resolution is needed
|
||||
// if the motor or application needs bigger values, switch to nanosecond is needed
|
||||
return time.Duration(60*1000*1000/(d.stepsPerRev*float32(d.speedRpm))) * time.Microsecond
|
||||
}
|
||||
|
||||
// GetCurrentStep gives the current step of motor
|
||||
func (s *StepperDriver) GetCurrentStep() int {
|
||||
return s.stepNum
|
||||
}
|
||||
// phasedStepping moves the motor one step with the configured speed and direction. The speed can be adjusted by SetSpeed()
|
||||
// and the direction can be changed by SetDirection() asynchronously.
|
||||
func (d *StepperDriver) phasedStepping() error {
|
||||
// ensure that read and write of variables (direction, stepNum) can not interfere
|
||||
d.valueMutex.Lock()
|
||||
defer d.valueMutex.Unlock()
|
||||
|
||||
// GetMaxSpeed gives the max RPM of motor
|
||||
func (s *StepperDriver) GetMaxSpeed() uint {
|
||||
// considering time for 1 rev as no of steps per rev * 1.5 (min time req between each step)
|
||||
return uint(60000 / (float64(s.stepsPerRev) * 1.5))
|
||||
}
|
||||
oldStepNum := d.stepNum
|
||||
|
||||
// SetSpeed sets the rpm
|
||||
func (s *StepperDriver) SetSpeed(rpm uint) error {
|
||||
if rpm <= 0 {
|
||||
return errors.New("RPM cannot be a zero or negative value")
|
||||
if d.direction == StepperDriverForward {
|
||||
d.stepNum++
|
||||
} else {
|
||||
d.stepNum--
|
||||
}
|
||||
|
||||
m := s.GetMaxSpeed()
|
||||
if rpm > m {
|
||||
rpm = m
|
||||
if d.stepNum >= int(d.stepsPerRev) {
|
||||
d.stepNum = 0
|
||||
} else if d.stepNum < 0 {
|
||||
d.stepNum = int(d.stepsPerRev) - 1
|
||||
}
|
||||
|
||||
s.speed = rpm
|
||||
r := int(math.Abs(float64(d.stepNum))) % len(d.phase)
|
||||
|
||||
for i, v := range d.phase[r] {
|
||||
if err := d.connection.(DigitalWriter).DigitalWrite(d.pins[i], v); err != nil {
|
||||
d.stepNum = oldStepNum
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
delay := d.getDelayPerStep()
|
||||
time.Sleep(delay)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *StepperDriver) sleepOuputs() error {
|
||||
for _, pin := range d.pins {
|
||||
if err := d.connection.(DigitalWriter).DigitalWrite(pin, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stopIfRunning stop the stepper if moving or running
|
||||
func (d *StepperDriver) stopIfRunning() error {
|
||||
// stops the continuous motion of the stepper, if running
|
||||
if d.stopAsynchRunFunc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := d.stopAsynchRunFunc(true)
|
||||
d.stopAsynchRunFunc = nil
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *StepperDriver) debug(text string) {
|
||||
if d.stepperDebug {
|
||||
fmt.Println(text)
|
||||
}
|
||||
}
|
||||
|
@ -1,104 +1,429 @@
|
||||
package gpio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
stepsInRev = 32
|
||||
)
|
||||
func initTestStepperDriverWithStubbedAdaptor() (*StepperDriver, *gpioTestAdaptor) {
|
||||
const stepsPerRev = 32
|
||||
|
||||
func initStepperMotorDriver() *StepperDriver {
|
||||
return NewStepperDriver(newGpioTestAdaptor(), [4]string{"7", "11", "13", "15"}, StepperModes.DualPhaseStepping, stepsInRev)
|
||||
a := newGpioTestAdaptor()
|
||||
d := NewStepperDriver(a, [4]string{"7", "11", "13", "15"}, StepperModes.DualPhaseStepping, stepsPerRev)
|
||||
return d, a
|
||||
}
|
||||
|
||||
func TestStepperDriverRun(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
_ = d.Run()
|
||||
assert.True(t, d.IsMoving())
|
||||
}
|
||||
func TestNewStepperDriver(t *testing.T) {
|
||||
// arrange
|
||||
const stepsPerRev = 32
|
||||
|
||||
func TestStepperDriverHalt(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
_ = d.Run()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
_ = d.Halt()
|
||||
assert.False(t, d.IsMoving())
|
||||
}
|
||||
|
||||
func TestStepperDriverDefaultName(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
assert.True(t, strings.HasPrefix(d.Name(), "Stepper"))
|
||||
}
|
||||
|
||||
func TestStepperDriverSetName(t *testing.T) {
|
||||
name := "SomeStepperSriver"
|
||||
d := initStepperMotorDriver()
|
||||
d.SetName(name)
|
||||
assert.Equal(t, name, d.Name())
|
||||
}
|
||||
|
||||
func TestStepperDriverSetDirection(t *testing.T) {
|
||||
dir := "backward"
|
||||
d := initStepperMotorDriver()
|
||||
_ = d.SetDirection(dir)
|
||||
assert.Equal(t, dir, d.direction)
|
||||
}
|
||||
|
||||
func TestStepperDriverDefaultDirection(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
a := newGpioTestAdaptor()
|
||||
// act
|
||||
d := NewStepperDriver(a, [4]string{"7", "11", "13", "15"}, StepperModes.DualPhaseStepping, stepsPerRev)
|
||||
// assert
|
||||
assert.IsType(t, &StepperDriver{}, d)
|
||||
assert.True(t, strings.HasPrefix(d.name, "Stepper"))
|
||||
assert.Equal(t, a, d.connection)
|
||||
assert.NoError(t, d.afterStart())
|
||||
assert.NoError(t, d.beforeHalt())
|
||||
assert.NotNil(t, d.Commander)
|
||||
assert.NotNil(t, d.mutex)
|
||||
assert.Equal(t, "forward", d.direction)
|
||||
assert.Equal(t, StepperModes.DualPhaseStepping, d.phase)
|
||||
assert.Equal(t, float32(stepsPerRev), d.stepsPerRev)
|
||||
assert.Equal(t, 0, d.stepNum)
|
||||
assert.Nil(t, d.stopAsynchRunFunc)
|
||||
}
|
||||
|
||||
func TestStepperDriverInvalidDirection(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
err := d.SetDirection("reverse")
|
||||
assert.ErrorContains(t, err, "Invalid direction. Value should be forward or backward")
|
||||
func TestStepperMove_IsMoving(t *testing.T) {
|
||||
const stepsPerRev = 32
|
||||
|
||||
tests := map[string]struct {
|
||||
inputSteps int
|
||||
noAutoStopIfRunning bool
|
||||
simulateAlreadyRunning bool
|
||||
simulateWriteErr bool
|
||||
wantWrites int
|
||||
wantSteps int
|
||||
wantMoving bool
|
||||
wantErr string
|
||||
}{
|
||||
"move_forward": {
|
||||
inputSteps: 2,
|
||||
wantWrites: 8,
|
||||
wantSteps: 2,
|
||||
wantMoving: false,
|
||||
},
|
||||
"move_more_forward": {
|
||||
inputSteps: 10,
|
||||
wantWrites: 40,
|
||||
wantSteps: 10,
|
||||
wantMoving: false,
|
||||
},
|
||||
"move_forward_full_revolution": {
|
||||
inputSteps: stepsPerRev,
|
||||
wantWrites: 128,
|
||||
wantSteps: 0, // will be reset after each revision
|
||||
wantMoving: false,
|
||||
},
|
||||
"move_backward": {
|
||||
inputSteps: -2,
|
||||
wantWrites: 8,
|
||||
wantSteps: stepsPerRev - 2,
|
||||
wantMoving: false,
|
||||
},
|
||||
"move_more_backward": {
|
||||
inputSteps: -10,
|
||||
wantWrites: 40,
|
||||
wantSteps: stepsPerRev - 10,
|
||||
wantMoving: false,
|
||||
},
|
||||
"move_backward_full_revolution": {
|
||||
inputSteps: -stepsPerRev,
|
||||
wantWrites: 128,
|
||||
wantSteps: 0, // will be reset after each revision
|
||||
wantMoving: false,
|
||||
},
|
||||
"already_running_autostop": {
|
||||
inputSteps: 3,
|
||||
simulateAlreadyRunning: true,
|
||||
wantWrites: 12,
|
||||
wantSteps: 3,
|
||||
wantMoving: false,
|
||||
},
|
||||
"error_already_running": {
|
||||
noAutoStopIfRunning: true,
|
||||
simulateAlreadyRunning: true,
|
||||
wantMoving: true,
|
||||
wantErr: "already running or moving",
|
||||
},
|
||||
"error_no_steps": {
|
||||
inputSteps: 0,
|
||||
wantWrites: 0,
|
||||
wantSteps: 0,
|
||||
wantMoving: false,
|
||||
wantErr: "no steps to do",
|
||||
},
|
||||
"error_write": {
|
||||
inputSteps: 1,
|
||||
simulateWriteErr: true,
|
||||
wantWrites: 0,
|
||||
wantMoving: false,
|
||||
wantErr: "write error",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, a := initTestStepperDriverWithStubbedAdaptor()
|
||||
defer func() {
|
||||
// for cleanup dangling channels
|
||||
if d.stopAsynchRunFunc != nil {
|
||||
err := d.stopAsynchRunFunc(true)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}()
|
||||
// arrange: different behavior
|
||||
d.haltIfRunning = !tc.noAutoStopIfRunning
|
||||
if tc.simulateAlreadyRunning {
|
||||
d.stopAsynchRunFunc = func(bool) error { log.Println("former run stopped"); return nil }
|
||||
}
|
||||
// arrange: writes
|
||||
a.written = nil // reset writes of Start()
|
||||
a.simulateWriteError = tc.simulateWriteErr
|
||||
// act
|
||||
err := d.Move(tc.inputSteps)
|
||||
// assert
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tc.wantSteps, d.stepNum)
|
||||
assert.Equal(t, tc.wantWrites, len(a.written))
|
||||
assert.Equal(t, tc.wantMoving, d.IsMoving())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepperDriverMoveForward(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
_ = d.Move(1)
|
||||
assert.Equal(t, 1, d.GetCurrentStep())
|
||||
|
||||
_ = d.Move(10)
|
||||
assert.Equal(t, 11, d.GetCurrentStep())
|
||||
func TestStepperRun_IsMoving(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
noAutoStopIfRunning bool
|
||||
simulateAlreadyRunning bool
|
||||
simulateWriteErr bool
|
||||
wantMoving bool
|
||||
wantErr string
|
||||
}{
|
||||
"run": {
|
||||
wantMoving: true,
|
||||
},
|
||||
"error_write": {
|
||||
simulateWriteErr: true,
|
||||
wantMoving: true,
|
||||
},
|
||||
"error_already_running": {
|
||||
noAutoStopIfRunning: true,
|
||||
simulateAlreadyRunning: true,
|
||||
wantMoving: true,
|
||||
wantErr: "already running or moving",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, a := initTestStepperDriverWithStubbedAdaptor()
|
||||
defer func() {
|
||||
// for cleanup dangling channels
|
||||
if d.stopAsynchRunFunc != nil {
|
||||
err := d.stopAsynchRunFunc(true)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}()
|
||||
// arrange: different behavior
|
||||
writeChan := make(chan struct{})
|
||||
if tc.noAutoStopIfRunning {
|
||||
// in this case no write should be called
|
||||
close(writeChan)
|
||||
writeChan = nil
|
||||
d.haltIfRunning = false
|
||||
} else {
|
||||
d.haltIfRunning = true
|
||||
}
|
||||
if tc.simulateAlreadyRunning {
|
||||
d.stopAsynchRunFunc = func(bool) error { return nil }
|
||||
}
|
||||
// arrange: writes
|
||||
simWriteErr := tc.simulateWriteErr // to prevent data race in write function (go-called)
|
||||
var firstWriteDone bool
|
||||
a.digitalWriteFunc = func(string, byte) error {
|
||||
if firstWriteDone {
|
||||
return nil // to prevent to much output and write to channel
|
||||
}
|
||||
writeChan <- struct{}{}
|
||||
firstWriteDone = true
|
||||
if simWriteErr {
|
||||
return fmt.Errorf("write error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// act
|
||||
err := d.Run()
|
||||
// assert
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tc.wantMoving, d.IsMoving())
|
||||
if writeChan != nil {
|
||||
// wait until the first write was called and a little bit longer
|
||||
<-writeChan
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
var asynchErr error
|
||||
if d.stopAsynchRunFunc != nil {
|
||||
asynchErr = d.stopAsynchRunFunc(false)
|
||||
d.stopAsynchRunFunc = nil
|
||||
}
|
||||
if tc.simulateWriteErr {
|
||||
assert.Error(t, asynchErr)
|
||||
} else {
|
||||
assert.NoError(t, asynchErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepperDriverMoveBackward(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
_ = d.Move(-1)
|
||||
assert.Equal(t, stepsInRev-1, d.GetCurrentStep())
|
||||
|
||||
_ = d.Move(-10)
|
||||
assert.Equal(t, stepsInRev-11, d.GetCurrentStep())
|
||||
func TestStepperStop_IsMoving(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
running bool
|
||||
wantErr string
|
||||
}{
|
||||
"stop_running": {
|
||||
running: true,
|
||||
},
|
||||
"errro_not_started": {
|
||||
running: false,
|
||||
wantErr: "is not yet started",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, _ := initTestStepperDriverWithStubbedAdaptor()
|
||||
if tc.running {
|
||||
require.NoError(t, d.Run())
|
||||
require.True(t, d.IsMoving())
|
||||
}
|
||||
// act
|
||||
err := d.Stop()
|
||||
// assert
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.False(t, d.IsMoving())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepperDriverMoveFullRotation(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
_ = d.Move(stepsInRev)
|
||||
assert.Equal(t, 0, d.GetCurrentStep())
|
||||
func TestStepperHalt_IsMoving(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
running bool
|
||||
}{
|
||||
"halt_running": {
|
||||
running: true,
|
||||
},
|
||||
"halt_not_started": {
|
||||
running: false,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, _ := initTestStepperDriverWithStubbedAdaptor()
|
||||
if tc.running {
|
||||
require.NoError(t, d.Run())
|
||||
require.True(t, d.IsMoving())
|
||||
}
|
||||
// act
|
||||
err := d.Halt()
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, d.IsMoving())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepperDriverMotorSetSpeedMoreThanMax(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
m := d.GetMaxSpeed()
|
||||
|
||||
_ = d.SetSpeed(m + 1)
|
||||
assert.Equal(t, d.speed, m)
|
||||
func TestStepperSetDirection(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input string
|
||||
wantVal string
|
||||
wantErr string
|
||||
}{
|
||||
"direction_forward": {
|
||||
input: "forward",
|
||||
wantVal: "forward",
|
||||
},
|
||||
"direction_backward": {
|
||||
input: "backward",
|
||||
wantVal: "backward",
|
||||
},
|
||||
"error_invalid_direction": {
|
||||
input: "reverse",
|
||||
wantVal: "forward",
|
||||
wantErr: "Invalid direction 'reverse'",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, _ := initTestStepperDriverWithStubbedAdaptor()
|
||||
require.Equal(t, "forward", d.direction)
|
||||
// act
|
||||
err := d.SetDirection(tc.input)
|
||||
// assert
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tc.wantVal, d.direction)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepperDriverMotorSetSpeedLessOrEqualMax(t *testing.T) {
|
||||
d := initStepperMotorDriver()
|
||||
m := d.GetMaxSpeed()
|
||||
func TestStepperMaxSpeed(t *testing.T) {
|
||||
const delayForMaxSpeed = 1428 * time.Microsecond // 1/700Hz
|
||||
|
||||
_ = d.SetSpeed(m - 1)
|
||||
assert.Equal(t, d.speed, m-1)
|
||||
|
||||
_ = d.SetSpeed(m)
|
||||
assert.Equal(t, d.speed, m)
|
||||
tests := map[string]struct {
|
||||
stepsPerRev float32
|
||||
want uint
|
||||
}{
|
||||
"maxspeed_for_20spr": {
|
||||
stepsPerRev: 20,
|
||||
want: 2100,
|
||||
},
|
||||
"maxspeed_for_50spr": {
|
||||
stepsPerRev: 50,
|
||||
want: 840,
|
||||
},
|
||||
"maxspeed_for_100spr": {
|
||||
stepsPerRev: 100,
|
||||
want: 420,
|
||||
},
|
||||
"maxspeed_for_400spr": {
|
||||
stepsPerRev: 400,
|
||||
want: 105,
|
||||
},
|
||||
"maxspeed_for_1000spr": {
|
||||
stepsPerRev: 1000,
|
||||
want: 42,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d := StepperDriver{stepsPerRev: tc.stepsPerRev}
|
||||
// act
|
||||
got := d.MaxSpeed()
|
||||
d.speedRpm = got
|
||||
got2 := d.getDelayPerStep()
|
||||
// assert
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.Equal(t, delayForMaxSpeed, got2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepperSetSpeed(t *testing.T) {
|
||||
const maxRpm = 1166
|
||||
|
||||
tests := map[string]struct {
|
||||
input uint
|
||||
want uint
|
||||
wantErr string
|
||||
}{
|
||||
"below_minimum": {
|
||||
input: 0,
|
||||
want: 0,
|
||||
wantErr: "RPM (0) cannot be a zero or negative value",
|
||||
},
|
||||
"minimum": {
|
||||
input: 1,
|
||||
want: 1,
|
||||
},
|
||||
"maximum": {
|
||||
input: maxRpm,
|
||||
want: maxRpm,
|
||||
},
|
||||
"above_maximum": {
|
||||
input: maxRpm + 1,
|
||||
want: maxRpm,
|
||||
wantErr: "cannot be greater then maximal value 1166",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// arrange
|
||||
d, _ := initTestStepperDriverWithStubbedAdaptor()
|
||||
d.stepsPerRev = 36
|
||||
// act
|
||||
err := d.SetSpeed(tc.input)
|
||||
// assert
|
||||
if tc.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tc.want, d.speedRpm)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gobot.io/x/gobot/v2"
|
||||
"gobot.io/x/gobot/v2/drivers/gpio"
|
||||
@ -15,22 +17,59 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
const (
|
||||
coilA1 = "7"
|
||||
coilA2 = "13"
|
||||
coilB1 = "11"
|
||||
coilB2 = "15"
|
||||
|
||||
degPerStep = 1.875
|
||||
countRot = 10
|
||||
)
|
||||
stepPerRevision := int(360.0 / degPerStep)
|
||||
|
||||
r := raspi.NewAdaptor()
|
||||
stepper := gpio.NewStepperDriver(r, [4]string{"7", "11", "13", "15"}, gpio.StepperModes.DualPhaseStepping, 2048)
|
||||
stepper := gpio.NewStepperDriver(r, [4]string{coilA1, coilB1, coilA2, coilB2}, gpio.StepperModes.DualPhaseStepping,
|
||||
uint(stepPerRevision))
|
||||
|
||||
work := func() {
|
||||
// set spped
|
||||
stepper.SetSpeed(15)
|
||||
defer func() {
|
||||
ec := 0
|
||||
// set current to zero to prevent overheating
|
||||
if err := stepper.Sleep(); err != nil {
|
||||
ec = 1
|
||||
log.Println("work done", err)
|
||||
} else {
|
||||
log.Println("work done")
|
||||
}
|
||||
|
||||
// Move forward one revolution
|
||||
if err := stepper.Move(2048); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(ec)
|
||||
}()
|
||||
|
||||
gobot.After(5*time.Second, func() {
|
||||
// this stops only the current movement and the next will start immediately (if any)
|
||||
// this means for the example, that the first rotation stops after ~5 rotations
|
||||
log.Println("asynchron stop after 5 sec.")
|
||||
if err := stepper.Stop(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
})
|
||||
|
||||
// one rotation per second
|
||||
if err := stepper.SetSpeed(60); err != nil {
|
||||
log.Println("set speed", err)
|
||||
}
|
||||
|
||||
// Move backward one revolution
|
||||
if err := stepper.Move(-2048); err != nil {
|
||||
fmt.Println(err)
|
||||
// Move forward N revolution
|
||||
if err := stepper.Move(stepPerRevision * countRot); err != nil {
|
||||
log.Println("move forward", err)
|
||||
}
|
||||
|
||||
// Move backward N revolution
|
||||
if err := stepper.MoveDeg(-360 * countRot); err != nil {
|
||||
log.Println("move backward", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
robot := gobot.NewRobot("stepperBot",
|
||||
|
Loading…
x
Reference in New Issue
Block a user