diff --git a/examples/raspi_led_brightness.go b/examples/raspi_led_brightness.go index ae3da2c21..1ae9a6dc9 100644 --- a/examples/raspi_led_brightness.go +++ b/examples/raspi_led_brightness.go @@ -16,7 +16,7 @@ import ( func main() { r := raspi.NewAdaptor() - led := gpio.NewLedDriver(r, "11") + led := gpio.NewLedDriver(r, "pwm0") work := func() { brightness := uint8(0) diff --git a/examples/raspi_servo.go b/examples/raspi_servo.go new file mode 100644 index 000000000..d1f88ff65 --- /dev/null +++ b/examples/raspi_servo.go @@ -0,0 +1,92 @@ +//go:build example +// +build example + +// Do not build by default. + +package main + +import ( + "fmt" + "log" + "time" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/drivers/gpio" + "gobot.io/x/gobot/v2/platforms/adaptors" + "gobot.io/x/gobot/v2/platforms/raspi" +) + +// Wiring +// PWM Raspi: header pin 12 (GPIO18-PWM0), please refer to the README.md, located in the folder of raspi platform, on +// how to activate the pwm support. +// Servo: orange (PWM), black (GND), red (VCC) 4-6V (please read the manual of your device) +func main() { + const ( + pwmPin = "pwm0" + wait = 3 * time.Second + + fiftyHzNanos = 20 * 1000 * 1000 // 50Hz = 0.02 sec = 20 ms + ) + // usually a frequency of 50Hz is used for servos, most servos have 0.5 ms..2.5 ms for 0-180°, however the mapping + // can be changed with options... + // + // for usage of pi-blaster driver just add the option "adaptors.WithPWMUsePiBlaster()" and use your pin number + // instead of "pwm0" + adaptor := raspi.NewAdaptor(adaptors.WithPWMDefaultPeriodForPin(pwmPin, fiftyHzNanos)) + servo := gpio.NewServoDriver(adaptor, pwmPin) + + work := func() { + fmt.Printf("first move to minimal position for %s...\n", wait) + if err := servo.ToMin(); err != nil { + log.Println(err) + } + + time.Sleep(wait) + + fmt.Printf("second move to center position for %s...\n", wait) + if err := servo.ToCenter(); err != nil { + log.Println(err) + } + + time.Sleep(wait) + + fmt.Printf("third move to maximal position for %s...\n", wait) + if err := servo.ToMax(); err != nil { + log.Println(err) + } + + time.Sleep(wait) + + fmt.Println("finally move 0-180° (or what your servo do for the new mapping) and back forever...") + angle := 0 + fadeAmount := 45 + + gobot.Every(time.Second, func() { + if err := servo.Move(byte(angle)); err != nil { + log.Println(err) + } + angle = angle + fadeAmount + if angle < 0 || angle > 180 { + if angle < 0 { + angle = 0 + } + if angle > 180 { + angle = 180 + } + // change direction and recalculate + fadeAmount = -fadeAmount + angle = angle + fadeAmount + } + }) + } + + robot := gobot.NewRobot("motorBot", + []gobot.Connection{adaptor}, + []gobot.Device{servo}, + work, + ) + + if err := robot.Start(); err != nil { + panic(err) + } +} diff --git a/examples/tinkerboard_servo.go b/examples/tinkerboard_servo.go index 4e81503b7..3c26b52b7 100644 --- a/examples/tinkerboard_servo.go +++ b/examples/tinkerboard_servo.go @@ -17,7 +17,7 @@ import ( ) // Wiring -// PWR Tinkerboard: 1 (+3.3V, VCC), 2(+5V), 6, 9, 14, 20 (GND) +// PWR Tinkerboard: 1 (+3.3V, VCC), 2(+5V), 6, 9, 14, 20 (GND) // PWM Tinkerboard: header pin 33 (PWM2) or pin 32 (PWM3) func main() { const ( diff --git a/platforms/adaptors/piblasterpwm_pin.go b/platforms/adaptors/piblasterpwm_pin.go new file mode 100644 index 000000000..15f4b65c1 --- /dev/null +++ b/platforms/adaptors/piblasterpwm_pin.go @@ -0,0 +1,118 @@ +package adaptors + +import ( + "fmt" + "os" + "strconv" + + "gobot.io/x/gobot/v2/system" +) + +const ( + piBlasterPath = "/dev/pi-blaster" + piBlasterMinDutyNano = 10000 // 10 us +) + +// piBlasterPWMPin is the Raspberry Pi implementation of the PWMPinner interface. +// It uses Pi Blaster. +type piBlasterPWMPin struct { + sys *system.Accesser + pin string + dc uint32 + period uint32 +} + +// newPiBlasterPWMPin returns a new PWM pin for pi-blaster access. +func newPiBlasterPWMPin(sys *system.Accesser, pinNo int) *piBlasterPWMPin { + return &piBlasterPWMPin{ + sys: sys, + pin: strconv.Itoa(pinNo), + } +} + +// Export exports the pin for use by the Raspberry Pi +func (p *piBlasterPWMPin) Export() error { + return nil +} + +// Unexport releases the pin from the operating system +func (p *piBlasterPWMPin) Unexport() error { + return p.writeValue(fmt.Sprintf("release %v\n", p.pin)) +} + +// Enabled returns always true for "enabled" +func (p *piBlasterPWMPin) Enabled() (bool, error) { + return true, nil +} + +// SetEnabled do nothing for PiBlaster +func (p *piBlasterPWMPin) SetEnabled(e bool) error { + return nil +} + +// Polarity returns always true for "normal" +func (p *piBlasterPWMPin) Polarity() (bool, error) { + return true, nil +} + +// SetPolarity does not do anything when using PiBlaster +func (p *piBlasterPWMPin) SetPolarity(bool) error { + return nil +} + +// Period returns the cached PWM period for pin +func (p *piBlasterPWMPin) Period() (uint32, error) { + return p.period, nil +} + +// SetPeriod uses PiBlaster setting and cannot be changed. We allow setting once here to define a base period for +// ServoWrite(). see https://github.com/sarfata/pi-blaster#how-to-adjust-the-frequency-and-the-resolution-of-the-pwm +func (p *piBlasterPWMPin) SetPeriod(period uint32) error { + if p.period != 0 { + return fmt.Errorf("the period of PWM pins needs to be set to '%d' in pi-blaster source code", period) + } + p.period = period + return nil +} + +// DutyCycle returns the duty cycle for the pin +func (p *piBlasterPWMPin) DutyCycle() (uint32, error) { + return p.dc, nil +} + +// SetDutyCycle writes the duty cycle to the pin +func (p *piBlasterPWMPin) SetDutyCycle(dutyNanos uint32) error { + if p.period == 0 { + return fmt.Errorf("pi-blaster PWM pin period not set while try to set duty cycle to '%d'", dutyNanos) + } + + if dutyNanos > p.period { + return fmt.Errorf("the duty cycle (%d) exceeds period (%d) for pi-blaster", dutyNanos, p.period) + } + + // never go below minimum allowed duty for pi blaster unless the duty equals to 0 + if dutyNanos < piBlasterMinDutyNano && dutyNanos != 0 { + dutyNanos = piBlasterMinDutyNano + fmt.Printf("duty cycle value limited to '%d' ns for pi-blaster", dutyNanos) + } + + duty := float64(dutyNanos) / float64(p.period) + if err := p.writeValue(fmt.Sprintf("%v=%v\n", p.pin, duty)); err != nil { + return err + } + + p.dc = dutyNanos + return nil +} + +func (p *piBlasterPWMPin) writeValue(data string) error { + fi, err := p.sys.OpenFile(piBlasterPath, os.O_WRONLY|os.O_APPEND, 0o644) + defer fi.Close() //nolint:staticcheck // for historical reasons + + if err != nil { + return err + } + + _, err = fi.WriteString(data) + return err +} diff --git a/platforms/adaptors/piblasterpwm_pin_test.go b/platforms/adaptors/piblasterpwm_pin_test.go new file mode 100644 index 000000000..bd4c48210 --- /dev/null +++ b/platforms/adaptors/piblasterpwm_pin_test.go @@ -0,0 +1,79 @@ +package adaptors + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/system" +) + +var _ gobot.PWMPinner = (*piBlasterPWMPin)(nil) + +func TestPiBlasterPWMPin(t *testing.T) { + // arrange + const path = "/dev/pi-blaster" + a := system.NewAccesser() + a.UseMockFilesystem([]string{path}) + pin := newPiBlasterPWMPin(a, 1) + // act & assert: activate pin for usage + require.NoError(t, pin.Export()) + require.NoError(t, pin.SetEnabled(true)) + // act & assert: get and set polarity + val, err := pin.Polarity() + require.NoError(t, err) + assert.True(t, val) + require.NoError(t, pin.SetPolarity(false)) + polarity, err := pin.Polarity() + assert.True(t, polarity) + require.NoError(t, err) + // act & assert: get and set period + period, err := pin.Period() + require.NoError(t, err) + assert.Equal(t, uint32(0), period) + require.NoError(t, pin.SetPeriod(20000000)) + period, err = pin.Period() + require.NoError(t, err) + assert.Equal(t, uint32(20000000), period) + err = pin.SetPeriod(10000000) + require.EqualError(t, err, "the period of PWM pins needs to be set to '10000000' in pi-blaster source code") + // act & assert: cleanup + require.NoError(t, pin.Unexport()) +} + +func TestPiBlasterPWMPin_DutyCycle(t *testing.T) { + // arrange + const path = "/dev/pi-blaster" + a := system.NewAccesser() + a.UseMockFilesystem([]string{path}) + pin := newPiBlasterPWMPin(a, 1) + // act & assert: activate pin for usage + require.NoError(t, pin.Export()) + require.NoError(t, pin.SetEnabled(true)) + // act & assert zero + dc, err := pin.DutyCycle() + require.NoError(t, err) + assert.Equal(t, uint32(0), dc) + // act & assert error without period set, the value remains zero + err = pin.SetDutyCycle(10000) + require.EqualError(t, err, "pi-blaster PWM pin period not set while try to set duty cycle to '10000'") + dc, err = pin.DutyCycle() + require.NoError(t, err) + assert.Equal(t, uint32(0), dc) + // arrange, act & assert a value + pin.period = 20000000 + require.NoError(t, pin.SetDutyCycle(10000)) + dc, err = pin.DutyCycle() + require.NoError(t, err) + assert.Equal(t, uint32(10000), dc) + // act & assert error on over limit, the value remains + err = pin.SetDutyCycle(20000001) + require.EqualError(t, err, "the duty cycle (20000001) exceeds period (20000000) for pi-blaster") + dc, err = pin.DutyCycle() + require.NoError(t, err) + assert.Equal(t, uint32(10000), dc) + // act & assert: cleanup + require.NoError(t, pin.Unexport()) +} diff --git a/platforms/adaptors/pwmpinsadaptor.go b/platforms/adaptors/pwmpinsadaptor.go index baa8bc0dd..16388bba2 100644 --- a/platforms/adaptors/pwmpinsadaptor.go +++ b/platforms/adaptors/pwmpinsadaptor.go @@ -34,7 +34,10 @@ type pwmPinServoScale struct { // pwmPinConfiguration contains all changeable attributes of the adaptor. type pwmPinsConfiguration struct { initialize pwmPinInitializer + usePiBlasterPin bool periodDefault uint32 + periodMinimum uint32 + dutyRateMinimum float64 // is the minimal relation of duty/period (except 0.0) polarityNormalIdentifier string polarityInvertedIdentifier string adjustDutyOnSetPeriod bool @@ -73,7 +76,7 @@ func NewPWMPinsAdaptor(sys *system.Accesser, t pwmPinTranslator, opts ...PwmPins pinsDefaultPeriod: make(map[string]uint32), pinsServoScale: make(map[string]pwmPinServoScale), polarityNormalIdentifier: "normal", - polarityInvertedIdentifier: "inverted", + polarityInvertedIdentifier: "inversed", adjustDutyOnSetPeriod: true, }, } @@ -91,12 +94,28 @@ func WithPWMPinInitializer(pc pwmPinInitializer) pwmPinsInitializeOption { return pwmPinsInitializeOption(pc) } +// WithPWMUsePiBlaster substitute the default sysfs-implementation for PWM-pins by the implementation for pi-blaster. +func WithPWMUsePiBlaster() pwmPinsUsePiBlasterPinOption { + return pwmPinsUsePiBlasterPinOption(true) +} + // WithPWMDefaultPeriod substitute the default period of 10 ms (100 Hz) for all created pins. func WithPWMDefaultPeriod(periodNanoSec uint32) pwmPinsPeriodDefaultOption { return pwmPinsPeriodDefaultOption(periodNanoSec) } -// WithPWMPolarityInvertedIdentifier use the given identifier, which will replace the default "inverted". +// WithPWMMinimumPeriod substitute the default minimum period limit of 0 nanoseconds. +func WithPWMMinimumPeriod(periodNanoSec uint32) pwmPinsPeriodMinimumOption { + return pwmPinsPeriodMinimumOption(periodNanoSec) +} + +// WithPWMMinimumDutyRate substitute the default minimum duty rate of 1/period. The given limit only come into effect, +// if the rate is > 0, because a rate of 0.0 is always allowed. +func WithPWMMinimumDutyRate(dutyRate float64) pwmPinsDutyRateMinimumOption { + return pwmPinsDutyRateMinimumOption(dutyRate) +} + +// WithPWMPolarityInvertedIdentifier use the given identifier, which will replace the default "inversed". func WithPWMPolarityInvertedIdentifier(identifier string) pwmPinsPolarityInvertedIdentifierOption { return pwmPinsPolarityInvertedIdentifierOption(identifier) } @@ -132,6 +151,11 @@ func (a *PWMPinsAdaptor) Connect() error { defer a.mutex.Unlock() a.pins = make(map[string]gobot.PWMPinner) + + if a.pwmPinsCfg.dutyRateMinimum == 0 && a.pwmPinsCfg.periodDefault > 0 { + a.pwmPinsCfg.dutyRateMinimum = 1 / float64(a.pwmPinsCfg.periodDefault) + } + return nil } @@ -164,12 +188,18 @@ func (a *PWMPinsAdaptor) PwmWrite(id string, val byte) error { if err != nil { return err } - period, err := pin.Period() + periodNanos, err := pin.Period() if err != nil { return err } - duty := gobot.FromScale(float64(val), 0, 255.0) - return pin.SetDutyCycle(uint32(float64(period) * duty)) + + dutyNanos := float64(periodNanos) * gobot.FromScale(float64(val), 0, 255.0) + + if err := a.validateDutyCycle(id, dutyNanos, float64(periodNanos)); err != nil { + return err + } + + return pin.SetDutyCycle(uint32(dutyNanos)) } // ServoWrite writes a servo signal to the specified pin. The given angle is between 0 and 180°. @@ -181,13 +211,14 @@ func (a *PWMPinsAdaptor) ServoWrite(id string, angle byte) error { if err != nil { return err } - period, err := pin.Period() // nanoseconds + periodNanos, err := pin.Period() if err != nil { return err } - if period != fiftyHzNanos { - log.Printf("WARNING: the PWM acts with a period of %d, but should use %d (50Hz) for servos\n", period, fiftyHzNanos) + if periodNanos != fiftyHzNanos { + log.Printf("WARNING: the PWM acts with a period of %d, but should use %d (50Hz) for servos\n", + periodNanos, fiftyHzNanos) } scale, ok := a.pwmPinsCfg.pinsServoScale[id] @@ -195,10 +226,15 @@ func (a *PWMPinsAdaptor) ServoWrite(id string, angle byte) error { return fmt.Errorf("no scaler found for servo pin '%s'", id) } - duty := gobot.ToScale(gobot.FromScale(float64(angle), + dutyNanos := gobot.ToScale(gobot.FromScale(float64(angle), scale.minDegree, scale.maxDegree), float64(scale.minDuty), float64(scale.maxDuty)) - return pin.SetDutyCycle(uint32(duty)) + + if err := a.validateDutyCycle(id, dutyNanos, float64(periodNanos)); err != nil { + return err + } + + return pin.SetDutyCycle(uint32(dutyNanos)) } // SetPeriod adjusts the period of the specified PWM pin immediately. @@ -286,7 +322,13 @@ func (a *PWMPinsAdaptor) pwmPin(id string) (gobot.PWMPinner, error) { if err != nil { return nil, err } - pin = a.sys.NewPWMPin(path, channel, a.pwmPinsCfg.polarityNormalIdentifier, a.pwmPinsCfg.polarityInvertedIdentifier) + + if a.pwmPinsCfg.usePiBlasterPin { + pin = newPiBlasterPWMPin(a.sys, channel) + } else { + pin = a.sys.NewPWMPin(path, channel, a.pwmPinsCfg.polarityNormalIdentifier, + a.pwmPinsCfg.polarityInvertedIdentifier) + } if err := a.pwmPinsCfg.initialize(id, pin); err != nil { return nil, err } @@ -296,6 +338,28 @@ func (a *PWMPinsAdaptor) pwmPin(id string) (gobot.PWMPinner, error) { return pin, nil } +func (a *PWMPinsAdaptor) validateDutyCycle(id string, dutyNanos, periodNanos float64) error { + if periodNanos == 0 { + return nil + } + + if dutyNanos > periodNanos { + return fmt.Errorf("duty cycle (%d) exceeds period (%d) for PWM pin id '%s'", + uint32(dutyNanos), uint32(periodNanos), id) + } + + if dutyNanos == 0 { + return nil + } + + rate := dutyNanos / periodNanos + if rate < a.pwmPinsCfg.dutyRateMinimum { + return fmt.Errorf("duty rate (%.8f) is lower than allowed (%.8f) for PWM pin id '%s'", + rate, a.pwmPinsCfg.dutyRateMinimum, id) + } + return nil +} + // setPeriod adjusts the PWM period of the given pin. If duty cycle is already set and this feature is not suppressed, // also this value will be adjusted in the same ratio. The order of writing the values must be observed, otherwise an // error occur "write error: Invalid argument". diff --git a/platforms/adaptors/pwmpinsadaptor_test.go b/platforms/adaptors/pwmpinsadaptor_test.go index 4635c90fb..3dfc595fe 100644 --- a/platforms/adaptors/pwmpinsadaptor_test.go +++ b/platforms/adaptors/pwmpinsadaptor_test.go @@ -17,23 +17,32 @@ import ( ) const ( - pwmDir = "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/" //nolint:gosec // false positive - pwmPwm0Dir = pwmDir + "pwm44/" - pwmExportPath = pwmDir + "export" - pwmUnexportPath = pwmDir + "unexport" - pwmEnablePath = pwmPwm0Dir + "enable" - pwmPeriodPath = pwmPwm0Dir + "period" - pwmDutyCyclePath = pwmPwm0Dir + "duty_cycle" - pwmPolarityPath = pwmPwm0Dir + "polarity" + pwmDir = "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/" //nolint:gosec // false positive + pwmPwm44Dir = pwmDir + "pwm44/" + pwmPwm47Dir = pwmDir + "pwm47/" + pwmExportPath = pwmDir + "export" + pwmUnexportPath = pwmDir + "unexport" + pwm44EnablePath = pwmPwm44Dir + "enable" + pwm44PeriodPath = pwmPwm44Dir + "period" + pwm44DutyCyclePath = pwmPwm44Dir + "duty_cycle" + pwm44PolarityPath = pwmPwm44Dir + "polarity" + pwm47EnablePath = pwmPwm47Dir + "enable" + pwm47PeriodPath = pwmPwm47Dir + "period" + pwm47DutyCyclePath = pwmPwm47Dir + "duty_cycle" + pwm47PolarityPath = pwmPwm47Dir + "polarity" ) var pwmMockPaths = []string{ pwmExportPath, pwmUnexportPath, - pwmEnablePath, - pwmPeriodPath, - pwmDutyCyclePath, - pwmPolarityPath, + pwm44EnablePath, + pwm44PeriodPath, + pwm44DutyCyclePath, + pwm44PolarityPath, + pwm47EnablePath, + pwm47PeriodPath, + pwm47DutyCyclePath, + pwm47PolarityPath, } // make sure that this PWMPinsAdaptor fulfills all the required interfaces @@ -47,10 +56,10 @@ func initTestPWMPinsAdaptorWithMockedFilesystem(mockPaths []string) (*PWMPinsAda sys := system.NewAccesser() fs := sys.UseMockFilesystem(mockPaths) a := NewPWMPinsAdaptor(sys, testPWMPinTranslator) - fs.Files[pwmEnablePath].Contents = "0" - fs.Files[pwmPeriodPath].Contents = "0" - fs.Files[pwmDutyCyclePath].Contents = "0" - fs.Files[pwmPolarityPath].Contents = a.pwmPinsCfg.polarityInvertedIdentifier + fs.Files[pwm44EnablePath].Contents = "0" + fs.Files[pwm44PeriodPath].Contents = "0" + fs.Files[pwm44DutyCyclePath].Contents = "0" + fs.Files[pwm44PolarityPath].Contents = a.pwmPinsCfg.polarityInvertedIdentifier if err := a.Connect(); err != nil { panic(err) } @@ -62,7 +71,7 @@ func testPWMPinTranslator(id string) (string, int, error) { if err != nil { return "", -1, fmt.Errorf("'%s' is not a valid id of a PWM pin", id) } - channel = channel + 11 // just for tests + channel = channel + 11 // just for tests, 33=>pwm0, 36=>pwm3 return pwmDir, channel, err } @@ -74,7 +83,7 @@ func TestNewPWMPinsAdaptor(t *testing.T) { // assert assert.Equal(t, uint32(pwmPeriodDefault), a.pwmPinsCfg.periodDefault) assert.Equal(t, "normal", a.pwmPinsCfg.polarityNormalIdentifier) - assert.Equal(t, "inverted", a.pwmPinsCfg.polarityInvertedIdentifier) + assert.Equal(t, "inversed", a.pwmPinsCfg.polarityInvertedIdentifier) assert.True(t, a.pwmPinsCfg.adjustDutyOnSetPeriod) } @@ -97,8 +106,8 @@ func TestPWMPinsFinalize(t *testing.T) { sys := system.NewAccesser() fs := sys.UseMockFilesystem(pwmMockPaths) a := NewPWMPinsAdaptor(sys, testPWMPinTranslator) - fs.Files[pwmPeriodPath].Contents = "0" - fs.Files[pwmDutyCyclePath].Contents = "0" + fs.Files[pwm44PeriodPath].Contents = "0" + fs.Files[pwm44DutyCyclePath].Contents = "0" // assert that finalize before connect is working require.NoError(t, a.Finalize()) // arrange @@ -140,30 +149,136 @@ func TestPWMPinsReConnect(t *testing.T) { assert.Empty(t, a.pins) } -func TestPwmWrite(t *testing.T) { - a, fs := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths) - - err := a.PwmWrite("33", 100) +func TestPWMPinsCache(t *testing.T) { + // arrange + a, _ := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths) + // act + firstSysPin, err := a.PWMPin("33") require.NoError(t, err) + secondSysPin, err := a.PWMPin("33") + require.NoError(t, err) + otherSysPin, err := a.PWMPin("36") + require.NoError(t, err) + // assert + assert.Equal(t, secondSysPin, firstSysPin) + assert.NotEqual(t, otherSysPin, firstSysPin) +} - assert.Equal(t, "44", fs.Files[pwmExportPath].Contents) - assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents) - //nolint:perfsprint // ok here - assert.Equal(t, fmt.Sprintf("%d", a.pwmPinsCfg.periodDefault), fs.Files[pwmPeriodPath].Contents) - assert.Equal(t, "3921568", fs.Files[pwmDutyCyclePath].Contents) - assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents) - - err = a.PwmWrite("notexist", 42) - require.ErrorContains(t, err, "'notexist' is not a valid id of a PWM pin") - - fs.WithWriteError = true - err = a.PwmWrite("33", 100) - require.ErrorContains(t, err, "write error") - fs.WithWriteError = false - - fs.WithReadError = true - err = a.PwmWrite("33", 100) - require.ErrorContains(t, err, "read error") +func TestPwmWrite(t *testing.T) { + tests := map[string]struct { + pin string + value byte + minimumRate float64 + simulateWriteErr bool + simulateReadErr bool + wantExport string + wantEnable string + wantPeriod string + wantDutyCycle string + wantErr string + }{ + "write_max": { + pin: "33", + value: 255, + wantExport: "44", + wantEnable: "1", + wantPeriod: "10000000", + wantDutyCycle: "10000000", + }, + "write_nearmax": { + pin: "33", + value: 254, + wantExport: "44", + wantEnable: "1", + wantPeriod: "10000000", + wantDutyCycle: "9960784", + }, + "write_mid": { + pin: "33", + value: 100, + wantExport: "44", + wantEnable: "1", + wantPeriod: "10000000", + wantDutyCycle: "3921568", + }, + "write_near min": { + pin: "33", + value: 1, + wantExport: "44", + wantEnable: "1", + wantPeriod: "10000000", + wantDutyCycle: "39215", + }, + "write_min": { + pin: "33", + value: 0, + minimumRate: 0.05, + wantExport: "44", + wantEnable: "1", + wantPeriod: "10000000", + wantDutyCycle: "0", + }, + "error_min_rate": { + pin: "33", + value: 1, + minimumRate: 0.05, + wantExport: "44", + wantEnable: "1", + wantPeriod: "10000000", + wantDutyCycle: "0", + wantErr: "is lower than allowed (0.05", + }, + "error_non_existent_pin": { + pin: "notexist", + wantEnable: "0", + wantPeriod: "0", + wantDutyCycle: "0", + wantErr: "'notexist' is not a valid id of a PWM pin", + }, + "error_write_error": { + pin: "33", + value: 10, + simulateWriteErr: true, + wantEnable: "0", + wantPeriod: "0", + wantDutyCycle: "0", + wantErr: "write error", + }, + "error_read_error": { + pin: "33", + value: 11, + simulateReadErr: true, + wantExport: "44", + wantEnable: "0", + wantPeriod: "0", + wantDutyCycle: "0", + wantErr: "read error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a, fs := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths) + if tc.minimumRate > 0 { + a.pwmPinsCfg.dutyRateMinimum = tc.minimumRate + } + fs.WithWriteError = tc.simulateWriteErr + fs.WithReadError = tc.simulateReadErr + // act + err := a.PwmWrite(tc.pin, tc.value) + // assert + assert.Equal(t, tc.wantExport, fs.Files[pwmExportPath].Contents) + assert.Equal(t, tc.wantEnable, fs.Files[pwm44EnablePath].Contents) + assert.Equal(t, tc.wantPeriod, fs.Files[pwm44PeriodPath].Contents) + assert.Equal(t, tc.wantDutyCycle, fs.Files[pwm44DutyCyclePath].Contents) + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + assert.Equal(t, "normal", fs.Files[pwm44PolarityPath].Contents) + } + }) + } } func TestServoWrite(t *testing.T) { @@ -172,15 +287,15 @@ func TestServoWrite(t *testing.T) { err := a.ServoWrite("33", 0) require.NoError(t, err) assert.Equal(t, "44", fs.Files[pwmExportPath].Contents) - assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents) + assert.Equal(t, "1", fs.Files[pwm44EnablePath].Contents) //nolint:perfsprint // ok here - assert.Equal(t, fmt.Sprintf("%d", a.pwmPinsCfg.periodDefault), fs.Files[pwmPeriodPath].Contents) - assert.Equal(t, "250000", fs.Files[pwmDutyCyclePath].Contents) - assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents) + assert.Equal(t, fmt.Sprintf("%d", a.pwmPinsCfg.periodDefault), fs.Files[pwm44PeriodPath].Contents) + assert.Equal(t, "250000", fs.Files[pwm44DutyCyclePath].Contents) + assert.Equal(t, "normal", fs.Files[pwm44PolarityPath].Contents) err = a.ServoWrite("33", 180) require.NoError(t, err) - assert.Equal(t, "1250000", fs.Files[pwmDutyCyclePath].Contents) + assert.Equal(t, "1250000", fs.Files[pwm44DutyCyclePath].Contents) err = a.ServoWrite("notexist", 42) require.ErrorContains(t, err, "'notexist' is not a valid id of a PWM pin") @@ -209,15 +324,15 @@ func TestSetPeriod(t *testing.T) { // assert require.NoError(t, err) assert.Equal(t, "44", fs.Files[pwmExportPath].Contents) - assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents) - assert.Equal(t, fmt.Sprintf("%d", newPeriod), fs.Files[pwmPeriodPath].Contents) //nolint:perfsprint // ok here - assert.Equal(t, "0", fs.Files[pwmDutyCyclePath].Contents) - assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents) + assert.Equal(t, "1", fs.Files[pwm44EnablePath].Contents) + assert.Equal(t, fmt.Sprintf("%d", newPeriod), fs.Files[pwm44PeriodPath].Contents) //nolint:perfsprint // ok here + assert.Equal(t, "0", fs.Files[pwm44DutyCyclePath].Contents) + assert.Equal(t, "normal", fs.Files[pwm44PolarityPath].Contents) // arrange test for automatic adjustment of duty cycle to lower value err = a.PwmWrite("33", 127) // 127 is a little bit smaller than 50% of period require.NoError(t, err) - assert.Equal(t, strconv.Itoa(1270000), fs.Files[pwmDutyCyclePath].Contents) + assert.Equal(t, strconv.Itoa(1270000), fs.Files[pwm44DutyCyclePath].Contents) newPeriod = newPeriod / 10 // act @@ -225,7 +340,7 @@ func TestSetPeriod(t *testing.T) { // assert require.NoError(t, err) - assert.Equal(t, strconv.Itoa(127000), fs.Files[pwmDutyCyclePath].Contents) + assert.Equal(t, strconv.Itoa(127000), fs.Files[pwm44DutyCyclePath].Contents) // arrange test for automatic adjustment of duty cycle to higher value newPeriod = newPeriod * 20 @@ -235,7 +350,7 @@ func TestSetPeriod(t *testing.T) { // assert require.NoError(t, err) - assert.Equal(t, strconv.Itoa(2540000), fs.Files[pwmDutyCyclePath].Contents) + assert.Equal(t, strconv.Itoa(2540000), fs.Files[pwm44DutyCyclePath].Contents) // act err = a.SetPeriod("not_exist", newPeriod) @@ -255,7 +370,7 @@ func Test_PWMPin(t *testing.T) { wantErr string }{ "pin_ok": { - mockPaths: []string{pwmExportPath, pwmEnablePath, pwmPeriodPath, pwmDutyCyclePath, pwmPolarityPath}, + mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44PeriodPath, pwm44DutyCyclePath, pwm44PolarityPath}, period: "0", dutyCycle: "0", translate: translator, @@ -269,7 +384,7 @@ func Test_PWMPin(t *testing.T) { "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/export: no such file", }, "init_setenabled_error": { - mockPaths: []string{pwmExportPath, pwmPeriodPath}, + mockPaths: []string{pwmExportPath, pwm44PeriodPath}, period: "1000", translate: translator, pin: "33", @@ -277,14 +392,14 @@ func Test_PWMPin(t *testing.T) { "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/pwm44/enable: no such file", }, "init_setperiod_dutycycle_no_error": { - mockPaths: []string{pwmExportPath, pwmEnablePath, pwmPeriodPath, pwmDutyCyclePath, pwmPolarityPath}, + mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44PeriodPath, pwm44DutyCyclePath, pwm44PolarityPath}, period: "0", dutyCycle: "0", translate: translator, pin: "33", }, "init_setperiod_error": { - mockPaths: []string{pwmExportPath, pwmEnablePath, pwmDutyCyclePath}, + mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44DutyCyclePath}, dutyCycle: "0", translate: translator, pin: "33", @@ -292,7 +407,7 @@ func Test_PWMPin(t *testing.T) { "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/pwm44/period: no such file", }, "init_setpolarity_error": { - mockPaths: []string{pwmExportPath, pwmEnablePath, pwmPeriodPath, pwmDutyCyclePath}, + mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44PeriodPath, pwm44DutyCyclePath}, period: "0", dutyCycle: "0", translate: translator, @@ -311,10 +426,10 @@ func Test_PWMPin(t *testing.T) { sys := system.NewAccesser() fs := sys.UseMockFilesystem(tc.mockPaths) if tc.period != "" { - fs.Files[pwmPeriodPath].Contents = tc.period + fs.Files[pwm44PeriodPath].Contents = tc.period } if tc.dutyCycle != "" { - fs.Files[pwmDutyCyclePath].Contents = tc.dutyCycle + fs.Files[pwm44DutyCyclePath].Contents = tc.dutyCycle } a := NewPWMPinsAdaptor(sys, tc.translate) if err := a.Connect(); err != nil { diff --git a/platforms/adaptors/pwmpinsadaptoroptions.go b/platforms/adaptors/pwmpinsadaptoroptions.go index c6b336d92..168840470 100644 --- a/platforms/adaptors/pwmpinsadaptoroptions.go +++ b/platforms/adaptors/pwmpinsadaptoroptions.go @@ -10,12 +10,22 @@ type PwmPinsOptionApplier interface { // pwmPinInitializeOption is the type for applying another than the default initializer. type pwmPinsInitializeOption pwmPinInitializer +// pwmPinsUsePiBlasterPinOption is the type for applying the usage of the pi-blaster PWM pin implementation, which will +// replace the default sysfs-implementation for PWM-pins. +type pwmPinsUsePiBlasterPinOption bool + // pwmPinPeriodDefaultOption is the type for applying another than the default period of 10 ms (100 Hz) for all // created pins. type pwmPinsPeriodDefaultOption uint32 +// pwmPinsPeriodMinimumOption is the type for applying another than the default minimum period of "0". +type pwmPinsPeriodMinimumOption uint32 + +// pwmPinsDutyRateMinimumOption is the type for applying another than the default minimum rate of 1/period. +type pwmPinsDutyRateMinimumOption float64 + // pwmPinPolarityInvertedIdentifierOption is the type for applying another identifier, which will replace the default -// "inverted". +// "inversed". type pwmPinsPolarityInvertedIdentifierOption string // pwmPinsAdjustDutyOnSetPeriodOption is the type for applying the automatic adjustment of duty cycle on setting @@ -49,12 +59,24 @@ func (o pwmPinsInitializeOption) String() string { return "pin initializer option for PWM's" } +func (o pwmPinsUsePiBlasterPinOption) String() string { + return "pi-blaster pin implementation option for PWM's" +} + func (o pwmPinsPeriodDefaultOption) String() string { return "default period option for PWM's" } +func (o pwmPinsPeriodMinimumOption) String() string { + return "minimum period option for PWM's" +} + +func (o pwmPinsDutyRateMinimumOption) String() string { + return "minimum duty rate option for PWM's" +} + func (o pwmPinsPolarityInvertedIdentifierOption) String() string { - return "inverted identifier option for PWM's" + return "identifier for 'inversed' option for PWM's" } func (o pwmPinsAdjustDutyOnSetPeriodOption) String() string { @@ -77,10 +99,22 @@ func (o pwmPinsInitializeOption) apply(cfg *pwmPinsConfiguration) { cfg.initialize = pwmPinInitializer(o) } +func (o pwmPinsUsePiBlasterPinOption) apply(cfg *pwmPinsConfiguration) { + cfg.usePiBlasterPin = bool(o) +} + func (o pwmPinsPeriodDefaultOption) apply(cfg *pwmPinsConfiguration) { cfg.periodDefault = uint32(o) } +func (o pwmPinsPeriodMinimumOption) apply(cfg *pwmPinsConfiguration) { + cfg.periodMinimum = uint32(o) +} + +func (o pwmPinsDutyRateMinimumOption) apply(cfg *pwmPinsConfiguration) { + cfg.dutyRateMinimum = float64(o) +} + func (o pwmPinsPolarityInvertedIdentifierOption) apply(cfg *pwmPinsConfiguration) { cfg.polarityInvertedIdentifier = string(o) } diff --git a/platforms/adaptors/pwmpinsadaptoroptions_test.go b/platforms/adaptors/pwmpinsadaptoroptions_test.go index 64341272d..cf0175cb4 100644 --- a/platforms/adaptors/pwmpinsadaptoroptions_test.go +++ b/platforms/adaptors/pwmpinsadaptoroptions_test.go @@ -25,6 +25,15 @@ func TestWithPWMPinInitializer(t *testing.T) { assert.Equal(t, wantErr, err) } +func TestWithPWMUsePiBlaster(t *testing.T) { + // arrange + cfg := &pwmPinsConfiguration{usePiBlasterPin: false} + // act + WithPWMUsePiBlaster().apply(cfg) + // assert + assert.True(t, cfg.usePiBlasterPin) +} + func TestWithPWMDefaultPeriod(t *testing.T) { // arrange const newPeriod = uint32(10) @@ -35,6 +44,26 @@ func TestWithPWMDefaultPeriod(t *testing.T) { assert.Equal(t, newPeriod, cfg.periodDefault) } +func TestWithPWMMinimumPeriod(t *testing.T) { + // arrange + const newMinPeriod = uint32(10) + cfg := &pwmPinsConfiguration{periodMinimum: 23} + // act + WithPWMMinimumPeriod(newMinPeriod).apply(cfg) + // assert + assert.Equal(t, newMinPeriod, cfg.periodMinimum) +} + +func TestWithPWMMinimumDutyRate(t *testing.T) { + // arrange + const newRate = 11.0 + cfg := &pwmPinsConfiguration{dutyRateMinimum: 33} + // act + WithPWMMinimumDutyRate(newRate).apply(cfg) + // assert + assert.InDelta(t, newRate, cfg.dutyRateMinimum, 0.0) +} + func TestWithPWMPolarityInvertedIdentifier(t *testing.T) { // arrange const newPolarityIdent = "pwm_invers" @@ -146,3 +175,16 @@ func TestWithPWMServoAngleRangeForPin(t *testing.T) { }) } } + +func TestStringer(t *testing.T) { + assert.NotEmpty(t, pwmPinsInitializeOption(nil).String()) + assert.NotEmpty(t, pwmPinsUsePiBlasterPinOption(true).String()) + assert.NotEmpty(t, pwmPinsPeriodDefaultOption(1).String()) + assert.NotEmpty(t, pwmPinsPeriodMinimumOption(1).String()) + assert.NotEmpty(t, pwmPinsDutyRateMinimumOption(1).String()) + assert.NotEmpty(t, pwmPinsPolarityInvertedIdentifierOption("1").String()) + assert.NotEmpty(t, pwmPinsAdjustDutyOnSetPeriodOption(true).String()) + assert.NotEmpty(t, pwmPinsDefaultPeriodForPinOption{}.String()) + assert.NotEmpty(t, pwmPinsServoDutyScaleForPinOption{}.String()) + assert.NotEmpty(t, pwmPinsServoAngleScaleForPinOption{}.String()) +} diff --git a/platforms/jetson/jetson_adaptor.go b/platforms/jetson/jetson_adaptor.go index dacd9ede4..6ef35239a 100644 --- a/platforms/jetson/jetson_adaptor.go +++ b/platforms/jetson/jetson_adaptor.go @@ -1,7 +1,6 @@ package jetson import ( - "errors" "fmt" "sync" @@ -13,7 +12,9 @@ import ( ) const ( - pwmPeriodDefault = 3000000 // 3 ms = 333 Hz + pwmPeriodDefault = 3000000 // 3 ms = 333 Hz + pwmPeriodMinimum = 5334 + pwmDutyRateMinimum = 0.0005 // minimum duty of 1500 for default period, ~3 for minimum period defaultI2cBusNumber = 1 @@ -26,11 +27,11 @@ const ( // Adaptor is the Gobot adaptor for the Jetson Nano type Adaptor struct { - name string - sys *system.Accesser - mutex sync.Mutex - pwmPins map[string]gobot.PWMPinner + name string + sys *system.Accesser + mutex *sync.Mutex *adaptors.DigitalPinsAdaptor + *adaptors.PWMPinsAdaptor *adaptors.I2cBusAdaptor *adaptors.SpiBusAdaptor } @@ -41,143 +42,99 @@ type Adaptor struct { // // adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# -func NewAdaptor(opts ...func(adaptors.DigitalPinsOptioner)) *Adaptor { +// +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +func NewAdaptor(opts ...interface{}) *Adaptor { sys := system.NewAccesser() - c := &Adaptor{ - name: gobot.DefaultName("JetsonNano"), - sys: sys, + a := &Adaptor{ + name: gobot.DefaultName("JetsonNano"), + sys: sys, + mutex: &sync.Mutex{}, + } + + var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) + pwmPinsOpts := []adaptors.PwmPinsOptionApplier{ + adaptors.WithPWMDefaultPeriod(pwmPeriodDefault), + adaptors.WithPWMMinimumPeriod(pwmPeriodMinimum), + adaptors.WithPWMMinimumDutyRate(pwmDutyRateMinimum), + } + for _, opt := range opts { + switch o := opt.(type) { + case func(adaptors.DigitalPinsOptioner): + digitalPinsOpts = append(digitalPinsOpts, o) + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } } - c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateDigitalPin, opts...) - c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, defaultI2cBusNumber) - c.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, c.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, + + a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.translateDigitalPin, digitalPinsOpts...) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.translatePWMPin, pwmPinsOpts...) + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, defaultI2cBusNumber) + a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, a.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed) - return c + return a } // Name returns the Adaptor's name -func (c *Adaptor) Name() string { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Name() string { + a.mutex.Lock() + defer a.mutex.Unlock() - return c.name + return a.name } // SetName sets the Adaptor's name -func (c *Adaptor) SetName(n string) { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) SetName(n string) { + a.mutex.Lock() + defer a.mutex.Unlock() - c.name = n + a.name = n } // Connect create new connection to board and pins. -func (c *Adaptor) Connect() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() - if err := c.SpiBusAdaptor.Connect(); err != nil { + if err := a.SpiBusAdaptor.Connect(); err != nil { return err } - if err := c.I2cBusAdaptor.Connect(); err != nil { + if err := a.I2cBusAdaptor.Connect(); err != nil { return err } - c.pwmPins = make(map[string]gobot.PWMPinner) - return c.DigitalPinsAdaptor.Connect() + if err := a.PWMPinsAdaptor.Connect(); err != nil { + return err + } + + return a.DigitalPinsAdaptor.Connect() } // Finalize closes connection to board and pins -func (c *Adaptor) Finalize() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() - err := c.DigitalPinsAdaptor.Finalize() + err := a.DigitalPinsAdaptor.Finalize() - for _, pin := range c.pwmPins { - if pin != nil { - if perr := pin.Unexport(); err != nil { - err = multierror.Append(err, perr) - } - } + if e := a.PWMPinsAdaptor.Finalize(); e != nil { + err = multierror.Append(err, e) } - c.pwmPins = nil - if e := c.I2cBusAdaptor.Finalize(); e != nil { + if e := a.I2cBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.SpiBusAdaptor.Finalize(); e != nil { + if e := a.SpiBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } return err } -// PWMPin returns a Jetson Nano. PWMPin which provides the gobot.PWMPinner interface -func (c *Adaptor) PWMPin(pin string) (gobot.PWMPinner, error) { - c.mutex.Lock() - defer c.mutex.Unlock() - - return c.pwmPin(pin) -} - -// PwmWrite writes a PWM signal to the specified pin -func (c *Adaptor) PwmWrite(pin string, val byte) error { - c.mutex.Lock() - defer c.mutex.Unlock() - - sysPin, err := c.pwmPin(pin) - if err != nil { - return err - } - - duty := uint32(gobot.FromScale(float64(val), 0, 255) * float64(pwmPeriodDefault)) - return sysPin.SetDutyCycle(duty) -} - -// ServoWrite writes a servo signal to the specified pin -func (c *Adaptor) ServoWrite(pin string, angle byte) error { - c.mutex.Lock() - defer c.mutex.Unlock() - - sysPin, err := c.pwmPin(pin) - if err != nil { - return err - } - - duty := uint32(gobot.FromScale(float64(angle), 0, 180) * float64(pwmPeriodDefault)) - return sysPin.SetDutyCycle(duty) -} - -func (c *Adaptor) pwmPin(pin string) (gobot.PWMPinner, error) { - if c.pwmPins == nil { - return nil, fmt.Errorf("not connected") - } - - if c.pwmPins[pin] != nil { - return c.pwmPins[pin], nil - } - - fn, err := c.translatePwmPin(pin) - if err != nil { - return nil, err - } - - c.pwmPins[pin] = NewPWMPin(c.sys, "/sys/class/pwm/pwmchip0", fn) - if err := c.pwmPins[pin].Export(); err != nil { - return nil, err - } - if err := c.pwmPins[pin].SetPeriod(pwmPeriodDefault); err != nil { - return nil, err - } - if err := c.pwmPins[pin].SetEnabled(true); err != nil { - return nil, err - } - - return c.pwmPins[pin], nil -} - -func (c *Adaptor) validateSpiBusNumber(busNr int) error { +func (a *Adaptor) validateSpiBusNumber(busNr int) error { // Valid bus numbers are [0,1] which corresponds to /dev/spidev0.x through /dev/spidev1.x. // x is the chip number <255 if (busNr < 0) || (busNr > 1) { @@ -186,7 +143,7 @@ func (c *Adaptor) validateSpiBusNumber(busNr int) error { return nil } -func (c *Adaptor) validateI2cBusNumber(busNr int) error { +func (a *Adaptor) validateI2cBusNumber(busNr int) error { // Valid bus number is [0..1] which corresponds to /dev/i2c-0 through /dev/i2c-1. if (busNr < 0) || (busNr > 1) { return fmt.Errorf("Bus number %d out of range", busNr) @@ -194,16 +151,16 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error { return nil } -func (c *Adaptor) translateDigitalPin(id string) (string, int, error) { +func (a *Adaptor) translateDigitalPin(id string) (string, int, error) { if line, ok := gpioPins[id]; ok { return "", line, nil } return "", -1, fmt.Errorf("'%s' is not a valid id for a digital pin", id) } -func (c *Adaptor) translatePwmPin(pin string) (string, error) { - if fn, ok := pwmPins[pin]; ok { - return fn, nil +func (a *Adaptor) translatePWMPin(id string) (string, int, error) { + if channel, ok := pwmPins[id]; ok { + return "/sys/class/pwm/pwmchip0", channel, nil } - return "", errors.New("Not a valid pin") + return "", 0, fmt.Errorf("'%s' is not a valid pin id for PWM on '%s'", id, a.name) } diff --git a/platforms/jetson/jetson_adaptor_test.go b/platforms/jetson/jetson_adaptor_test.go index 11c9e3338..7f3599b06 100644 --- a/platforms/jetson/jetson_adaptor_test.go +++ b/platforms/jetson/jetson_adaptor_test.go @@ -66,15 +66,12 @@ func TestFinalize(t *testing.T) { func TestPWMPinsConnect(t *testing.T) { a := NewAdaptor() - assert.Equal(t, (map[string]gobot.PWMPinner)(nil), a.pwmPins) err := a.PwmWrite("33", 1) require.ErrorContains(t, err, "not connected") err = a.Connect() require.NoError(t, err) - assert.NotEqual(t, (map[string]gobot.PWMPinner)(nil), a.pwmPins) - assert.Empty(t, a.pwmPins) } func TestPWMPinsReConnect(t *testing.T) { @@ -84,18 +81,16 @@ func TestPWMPinsReConnect(t *testing.T) { "/sys/class/pwm/pwmchip0/unexport", "/sys/class/pwm/pwmchip0/pwm2/duty_cycle", "/sys/class/pwm/pwmchip0/pwm2/period", + "/sys/class/pwm/pwmchip0/pwm2/polarity", "/sys/class/pwm/pwmchip0/pwm2/enable", } a, _ := initTestAdaptorWithMockedFilesystem(mockPaths) - assert.Empty(t, a.pwmPins) require.NoError(t, a.PwmWrite("33", 1)) - assert.Len(t, a.pwmPins, 1) require.NoError(t, a.Finalize()) // act err := a.Connect() // assert require.NoError(t, err) - assert.Empty(t, a.pwmPins) } func TestDigitalIO(t *testing.T) { @@ -240,3 +235,35 @@ func Test_validateI2cBusNumber(t *testing.T) { }) } } + +func Test_translatePWMPin(t *testing.T) { + tests := map[string]struct { + pin string + wantDir string + wantChannel int + wantErr error + }{ + "32_pwm0": { + pin: "32", + wantDir: "/sys/class/pwm/pwmchip0", + wantChannel: 0, + }, + "33_pwm2": { + pin: "33", + wantDir: "/sys/class/pwm/pwmchip0", + wantChannel: 2, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := NewAdaptor() + // act + dir, channel, err := a.translatePWMPin(tc.pin) + // assert + assert.Equal(t, tc.wantErr, err) + assert.Equal(t, tc.wantDir, dir) + assert.Equal(t, tc.wantChannel, channel) + }) + } +} diff --git a/platforms/jetson/jetson_pin_map.go b/platforms/jetson/jetson_pin_map.go index 7aae07d84..7abff1e99 100644 --- a/platforms/jetson/jetson_pin_map.go +++ b/platforms/jetson/jetson_pin_map.go @@ -25,7 +25,8 @@ var gpioPins = map[string]int{ "40": 78, } -var pwmPins = map[string]string{ - "32": "0", - "33": "2", +// pin to pwm channel (pwm0, pwm2) +var pwmPins = map[string]int{ + "32": 0, + "33": 2, } diff --git a/platforms/jetson/pwm_pin.go b/platforms/jetson/pwm_pin.go deleted file mode 100644 index 558e832ef..000000000 --- a/platforms/jetson/pwm_pin.go +++ /dev/null @@ -1,148 +0,0 @@ -package jetson - -import ( - "errors" - "fmt" - "os" - "path" - "strconv" - - "gobot.io/x/gobot/v2" - "gobot.io/x/gobot/v2/system" -) - -const ( - minimumPeriod = 5334 - minimumRate = 0.05 -) - -// PWMPin is the Jetson Nano implementation of the PWMPinner interface. -// It uses gpio pwm. -type PWMPin struct { - sys *system.Accesser - path string - fn string - dc uint32 - period uint32 - enabled bool -} - -// NewPWMPin returns a new PWMPin -// pin32 pwm0, pin33 pwm2 -func NewPWMPin(sys *system.Accesser, path string, fn string) *PWMPin { - p := &PWMPin{ - sys: sys, - path: path, - fn: fn, - } - return p -} - -// Export exports the pin for use by the Jetson Nano -func (p *PWMPin) Export() error { - return p.writeFile("export", p.fn) -} - -// Unexport releases the pin from the operating system -func (p *PWMPin) Unexport() error { - return p.writeFile("unexport", p.fn) -} - -// Enabled returns the cached enabled state of the PWM pin -func (p *PWMPin) Enabled() (bool, error) { - return p.enabled, nil -} - -// SetEnabled enables/disables the PWM pin -func (p *PWMPin) SetEnabled(e bool) error { - if err := p.writeFile(fmt.Sprintf("pwm%s/enable", p.fn), strconv.Itoa(bool2int(e))); err != nil { - return err - } - p.enabled = e - return nil -} - -// Polarity returns always the polarity "true" for normal -func (p *PWMPin) Polarity() (bool, error) { - return true, nil -} - -// SetPolarity does not do anything when using Jetson Nano -func (p *PWMPin) SetPolarity(bool) error { - return nil -} - -// Period returns the cached PWM period for pin -func (p *PWMPin) Period() (uint32, error) { - if p.period == 0 { - return p.period, errors.New("Jetson PWM pin period not set") - } - - return p.period, nil -} - -// SetPeriod uses Jetson Nano setting and cannot be changed once set -func (p *PWMPin) SetPeriod(period uint32) error { - if p.period != 0 { - return errors.New("Cannot set the period of individual PWM pins on Jetson") - } - // JetsonNano Minimum period - if period < minimumPeriod { - return errors.New("Cannot set the period more then minimum") - } - //nolint:perfsprint // ok here - if err := p.writeFile(fmt.Sprintf("pwm%s/period", p.fn), fmt.Sprintf("%v", period)); err != nil { - return err - } - p.period = period - return nil -} - -// DutyCycle returns the cached duty cycle for the pin -func (p *PWMPin) DutyCycle() (uint32, error) { - return p.dc, nil -} - -// SetDutyCycle writes the duty cycle to the pin -func (p *PWMPin) SetDutyCycle(duty uint32) error { - if p.period == 0 { - return errors.New("Jetson PWM pin period not set") - } - - if duty > p.period { - return errors.New("Duty cycle exceeds period") - } - - rate := gobot.FromScale(float64(duty), 0, float64(p.period)) - // never go below minimum allowed duty because very short duty - if rate < minimumRate { - duty = uint32(minimumRate * float64(p.period) / 100) - } - //nolint:perfsprint // ok here - if err := p.writeFile(fmt.Sprintf("pwm%s/duty_cycle", p.fn), fmt.Sprintf("%v", duty)); err != nil { - return err - } - - p.dc = duty - return nil -} - -func (p *PWMPin) writeFile(subpath string, value string) error { - sysfspath := path.Join(p.path, subpath) - fi, err := p.sys.OpenFile(sysfspath, os.O_WRONLY|os.O_APPEND, 0o644) - defer fi.Close() //nolint:staticcheck // for historical reasons - - if err != nil { - return err - } - - _, err = fi.WriteString(value) - return err -} - -func bool2int(b bool) int { - if b { - return 1 - } - return 0 -} diff --git a/platforms/jetson/pwm_pin_test.go b/platforms/jetson/pwm_pin_test.go deleted file mode 100644 index 14cf878e0..000000000 --- a/platforms/jetson/pwm_pin_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package jetson - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "gobot.io/x/gobot/v2" - "gobot.io/x/gobot/v2/system" -) - -var _ gobot.PWMPinner = (*PWMPin)(nil) - -func TestPwmPin(t *testing.T) { - a := system.NewAccesser() - const ( - exportPath = "/sys/class/pwm/pwmchip0/export" - unexportPath = "/sys/class/pwm/pwmchip0/unexport" - enablePath = "/sys/class/pwm/pwmchip0/pwm3/enable" - periodPath = "/sys/class/pwm/pwmchip0/pwm3/period" - dutyCyclePath = "/sys/class/pwm/pwmchip0/pwm3/duty_cycle" - ) - mockPaths := []string{ - exportPath, - unexportPath, - enablePath, - periodPath, - dutyCyclePath, - } - fs := a.UseMockFilesystem(mockPaths) - - pin := NewPWMPin(a, "/sys/class/pwm/pwmchip0", "3") - require.Equal(t, "", fs.Files[exportPath].Contents) - require.Equal(t, "", fs.Files[unexportPath].Contents) - require.Equal(t, "", fs.Files[enablePath].Contents) - require.Equal(t, "", fs.Files[periodPath].Contents) - require.Equal(t, "", fs.Files[dutyCyclePath].Contents) - - require.NoError(t, pin.Export()) - assert.Equal(t, "3", fs.Files[exportPath].Contents) - - require.NoError(t, pin.SetEnabled(true)) - assert.Equal(t, "1", fs.Files[enablePath].Contents) - - val, _ := pin.Polarity() - assert.True(t, val) - require.NoError(t, pin.SetPolarity(false)) - val, _ = pin.Polarity() - assert.True(t, val) - - _, err := pin.Period() - require.ErrorContains(t, err, "Jetson PWM pin period not set") - require.ErrorContains(t, pin.SetDutyCycle(10000), "Jetson PWM pin period not set") - assert.Equal(t, "", fs.Files[dutyCyclePath].Contents) - - require.NoError(t, pin.SetPeriod(20000000)) - assert.Equal(t, "20000000", fs.Files[periodPath].Contents) - period, _ := pin.Period() - assert.Equal(t, uint32(20000000), period) - require.ErrorContains(t, pin.SetPeriod(10000000), "Cannot set the period of individual PWM pins on Jetson") - assert.Equal(t, "20000000", fs.Files[periodPath].Contents) - - dc, _ := pin.DutyCycle() - assert.Equal(t, uint32(0), dc) - - require.NoError(t, pin.SetDutyCycle(10000)) - assert.Equal(t, "10000", fs.Files[dutyCyclePath].Contents) - dc, _ = pin.DutyCycle() - assert.Equal(t, uint32(10000), dc) - - require.ErrorContains(t, pin.SetDutyCycle(999999999), "Duty cycle exceeds period") - dc, _ = pin.DutyCycle() - assert.Equal(t, "10000", fs.Files[dutyCyclePath].Contents) - assert.Equal(t, uint32(10000), dc) - - require.NoError(t, pin.Unexport()) - assert.Equal(t, "3", fs.Files[unexportPath].Contents) -} diff --git a/platforms/nanopi/nanopi_adaptor.go b/platforms/nanopi/nanopi_adaptor.go index ab78758d8..d0da14c1d 100644 --- a/platforms/nanopi/nanopi_adaptor.go +++ b/platforms/nanopi/nanopi_adaptor.go @@ -12,8 +12,6 @@ import ( ) const ( - pwmInvertedIdentifier = "inversed" - defaultI2cBusNumber = 0 defaultSpiBusNumber = 0 @@ -83,7 +81,7 @@ func NewNeoAdaptor(opts ...interface{}) *Adaptor { } var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) - pwmPinsOpts := []adaptors.PwmPinsOptionApplier{adaptors.WithPWMPolarityInvertedIdentifier(pwmInvertedIdentifier)} + var pwmPinsOpts []adaptors.PwmPinsOptionApplier for _, opt := range opts { switch o := opt.(type) { case func(adaptors.DigitalPinsOptioner): diff --git a/platforms/nanopi/nanopi_adaptor_test.go b/platforms/nanopi/nanopi_adaptor_test.go index 5410323bc..79f46018f 100644 --- a/platforms/nanopi/nanopi_adaptor_test.go +++ b/platforms/nanopi/nanopi_adaptor_test.go @@ -31,6 +31,8 @@ const ( pwmPeriodPath = pwmPwmDir + "period" pwmDutyCyclePath = pwmPwmDir + "duty_cycle" pwmPolarityPath = pwmPwmDir + "polarity" + + pwmInvertedIdentifier = "inversed" ) var pwmMockPaths = []string{ diff --git a/platforms/raspi/README.md b/platforms/raspi/README.md index 34d42a34d..8db99b238 100644 --- a/platforms/raspi/README.md +++ b/platforms/raspi/README.md @@ -51,9 +51,7 @@ func main() { } ``` -## How to Connect - -### Compiling +## Compiling Compile your Gobot program on your workstation like this: @@ -74,9 +72,48 @@ scp raspi_blink pi@192.168.1.xxx:/home/pi/ ssh -t pi@192.168.1.xxx "./raspi_blink" ``` -### Enabling PWM output on GPIO pins +## Enabling PWM output on GPIO pins + +### Using Linux Kernel sysfs implementation + +The PWM needs to be enabled in the device tree. Please read `/boot/overlays/README` of your device. Usually "pwm0" can +be activated for all raspi variants with a line `dtoverlay=pwm,pin=18,func=2` added to `/boot/config.txt`. The number +relates to "GPIO18", not the header number, which is "12" in this case. + +Now the pin can be used with gobot by the pwm channel name, e.g. for our example above: + +```go +... +// create the adaptor with a 50Hz default frequency for usage with servos +a := NewAdaptor(adaptors.WithPWMDefaultPeriod(20000000)) +// move servo connected with header pin 12 to 90° +a.ServoWrite("pwm0", 90) +... +``` + +> If the activation fails or something strange happen, maybe the audio driver conflicts with the PWM. Please deactivate +> the audio device tree overlay in `/boot/config.txt` to avoid conflicts. + +### Using pi-blaster + +For support PWM on all pins, you may use a program called pi-blaster. You can follow the instructions for install in +the pi-blaster repo here: + +For using a PWM for servo, the default 100Hz period needs to be adjusted to 50Hz in the source code of the driver. +Please refer to . -For extended PWM support on the Raspberry Pi, you will need to use a program called pi-blaster. You can follow the -instructions for pi-blaster install in the pi-blaster repo here: +It is not possible to change the period from gobot side. -[https://github.com/sarfata/pi-blaster](https://github.com/sarfata/pi-blaster) +Now the pin can be used with gobot by the header number, e.g.: + +```go +... +// create the adaptor with usage of pi-blaster instead of default sysfs, 50Hz default is given for calculate +// duty cycle for servos but will not change everything for the pi-blaster driver, see description above +a := NewAdaptor(adaptors.WithPWMUsePiBlaster(), adaptors.WithPWMDefaultPeriod(20000000)) +// move servo to 90° +a.ServoWrite("11", 90) +// this will not work like expected, see description +a.SetPeriod("11", 20000000) +... +``` diff --git a/platforms/raspi/pwm_pin.go b/platforms/raspi/pwm_pin.go deleted file mode 100644 index 70aa59452..000000000 --- a/platforms/raspi/pwm_pin.go +++ /dev/null @@ -1,119 +0,0 @@ -package raspi - -import ( - "errors" - "fmt" - "os" - - "gobot.io/x/gobot/v2" - "gobot.io/x/gobot/v2/system" -) - -// PWMPin is the Raspberry Pi implementation of the PWMPinner interface. -// It uses Pi Blaster. -type PWMPin struct { - sys *system.Accesser - path string - pin string - dc uint32 - period uint32 -} - -// NewPWMPin returns a new PWMPin -func NewPWMPin(sys *system.Accesser, path string, pin string) *PWMPin { - return &PWMPin{ - sys: sys, - path: path, - pin: pin, - } -} - -// Export exports the pin for use by the Raspberry Pi -func (p *PWMPin) Export() error { - return nil -} - -// Unexport releases the pin from the operating system -func (p *PWMPin) Unexport() error { - return p.writeValue(fmt.Sprintf("release %v\n", p.pin)) -} - -// Enabled returns always true for "enabled" -func (p *PWMPin) Enabled() (bool, error) { - return true, nil -} - -// SetEnabled do nothing for PiBlaster -func (p *PWMPin) SetEnabled(e bool) error { - return nil -} - -// Polarity returns always true for "normal" -func (p *PWMPin) Polarity() (bool, error) { - return true, nil -} - -// SetPolarity does not do anything when using PiBlaster -func (p *PWMPin) SetPolarity(bool) error { - return nil -} - -// Period returns the cached PWM period for pin -func (p *PWMPin) Period() (uint32, error) { - if p.period == 0 { - return p.period, errors.New("Raspi PWM pin period not set") - } - - return p.period, nil -} - -// SetPeriod uses PiBlaster setting and cannot be changed once set -func (p *PWMPin) SetPeriod(period uint32) error { - if p.period != 0 { - return errors.New("Cannot set the period of individual PWM pins on Raspi") - } - p.period = period - return nil -} - -// DutyCycle returns the duty cycle for the pin -func (p *PWMPin) DutyCycle() (uint32, error) { - return p.dc, nil -} - -// SetDutyCycle writes the duty cycle to the pin -func (p *PWMPin) SetDutyCycle(duty uint32) error { - if p.period == 0 { - return errors.New("Raspi PWM pin period not set") - } - - if duty > p.period { - return errors.New("Duty cycle exceeds period") - } - - val := gobot.FromScale(float64(duty), 0, float64(p.period)) - // never go below minimum allowed duty for pi blaster - // unless the duty equals to 0 - if val < 0.05 && val != 0 { - val = 0.05 - } - - if err := p.writeValue(fmt.Sprintf("%v=%v\n", p.pin, val)); err != nil { - return err - } - - p.dc = duty - return nil -} - -func (p *PWMPin) writeValue(data string) error { - fi, err := p.sys.OpenFile(p.path, os.O_WRONLY|os.O_APPEND, 0o644) - defer fi.Close() //nolint:staticcheck // for historical reasons - - if err != nil { - return err - } - - _, err = fi.WriteString(data) - return err -} diff --git a/platforms/raspi/pwm_pin_test.go b/platforms/raspi/pwm_pin_test.go deleted file mode 100644 index f25cd0c08..000000000 --- a/platforms/raspi/pwm_pin_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package raspi - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "gobot.io/x/gobot/v2" - "gobot.io/x/gobot/v2/system" -) - -var _ gobot.PWMPinner = (*PWMPin)(nil) - -func TestPwmPin(t *testing.T) { - const path = "/dev/pi-blaster" - a := system.NewAccesser() - a.UseMockFilesystem([]string{path}) - - pin := NewPWMPin(a, path, "1") - - require.NoError(t, pin.Export()) - require.NoError(t, pin.SetEnabled(true)) - - val, _ := pin.Polarity() - assert.True(t, val) - - require.NoError(t, pin.SetPolarity(false)) - - val, _ = pin.Polarity() - assert.True(t, val) - - _, err := pin.Period() - require.ErrorContains(t, err, "Raspi PWM pin period not set") - require.ErrorContains(t, pin.SetDutyCycle(10000), "Raspi PWM pin period not set") - - require.NoError(t, pin.SetPeriod(20000000)) - period, _ := pin.Period() - assert.Equal(t, uint32(20000000), period) - require.ErrorContains(t, pin.SetPeriod(10000000), "Cannot set the period of individual PWM pins on Raspi") - - dc, _ := pin.DutyCycle() - assert.Equal(t, uint32(0), dc) - - require.NoError(t, pin.SetDutyCycle(10000)) - - dc, _ = pin.DutyCycle() - assert.Equal(t, uint32(10000), dc) - - require.ErrorContains(t, pin.SetDutyCycle(999999999), "Duty cycle exceeds period") - dc, _ = pin.DutyCycle() - assert.Equal(t, uint32(10000), dc) - - require.NoError(t, pin.Unexport()) -} diff --git a/platforms/raspi/raspi_adaptor.go b/platforms/raspi/raspi_adaptor.go index a98ff9fe1..1d917b19a 100644 --- a/platforms/raspi/raspi_adaptor.go +++ b/platforms/raspi/raspi_adaptor.go @@ -1,7 +1,6 @@ package raspi import ( - "errors" "fmt" "strconv" "strings" @@ -37,12 +36,11 @@ type Adaptor struct { mutex sync.Mutex sys *system.Accesser revision string - pwmPins map[string]gobot.PWMPinner *adaptors.AnalogPinsAdaptor *adaptors.DigitalPinsAdaptor + *adaptors.PWMPinsAdaptor *adaptors.I2cBusAdaptor *adaptors.SpiBusAdaptor - PiBlasterPeriod uint32 } // NewAdaptor creates a Raspi Adaptor @@ -56,83 +54,95 @@ type Adaptor struct { // adaptors.WithGpiosOpenDrain/Source(pin's): sets the output behavior // adaptors.WithGpioDebounce(pin, period): sets the input debouncer // adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection -func NewAdaptor(opts ...func(adaptors.DigitalPinsOptioner)) *Adaptor { +func NewAdaptor(opts ...interface{}) *Adaptor { sys := system.NewAccesser(system.WithDigitalPinGpiodAccess()) - c := &Adaptor{ - name: gobot.DefaultName("RaspberryPi"), - sys: sys, - PiBlasterPeriod: 10000000, + a := &Adaptor{ + name: gobot.DefaultName("RaspberryPi"), + sys: sys, + } + + var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) + var pwmPinsOpts []adaptors.PwmPinsOptionApplier + for _, opt := range opts { + switch o := opt.(type) { + case func(adaptors.DigitalPinsOptioner): + digitalPinsOpts = append(digitalPinsOpts, o) + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } } - c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin) - c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.getPinTranslatorFunction(), opts...) - c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, 1) - c.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, c.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, + + a.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, a.translateAnalogPin) + a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.getPinTranslatorFunction(), digitalPinsOpts...) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.getPinTranslatorFunction(), pwmPinsOpts...) + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, 1) + a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, a.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed) - return c + return a } // Name returns the Adaptor's name -func (c *Adaptor) Name() string { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Name() string { + a.mutex.Lock() + defer a.mutex.Unlock() - return c.name + return a.name } // SetName sets the Adaptor's name -func (c *Adaptor) SetName(n string) { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) SetName(n string) { + a.mutex.Lock() + defer a.mutex.Unlock() - c.name = n + a.name = n } // Connect create new connection to board and pins. -func (c *Adaptor) Connect() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() + + if err := a.SpiBusAdaptor.Connect(); err != nil { + return err + } - if err := c.SpiBusAdaptor.Connect(); err != nil { + if err := a.I2cBusAdaptor.Connect(); err != nil { return err } - if err := c.I2cBusAdaptor.Connect(); err != nil { + if err := a.AnalogPinsAdaptor.Connect(); err != nil { return err } - if err := c.AnalogPinsAdaptor.Connect(); err != nil { + if err := a.PWMPinsAdaptor.Connect(); err != nil { return err } - c.pwmPins = make(map[string]gobot.PWMPinner) - return c.DigitalPinsAdaptor.Connect() + return a.DigitalPinsAdaptor.Connect() } // Finalize closes connection to board and pins -func (c *Adaptor) Finalize() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() - err := c.DigitalPinsAdaptor.Finalize() + err := a.DigitalPinsAdaptor.Finalize() - for _, pin := range c.pwmPins { - if pin != nil { - if perr := pin.Unexport(); err != nil { - err = multierror.Append(err, perr) - } - } + if e := a.PWMPinsAdaptor.Finalize(); e != nil { + err = multierror.Append(err, e) } - c.pwmPins = nil - if e := c.AnalogPinsAdaptor.Finalize(); e != nil { + if e := a.AnalogPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.I2cBusAdaptor.Finalize(); e != nil { + if e := a.I2cBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.SpiBusAdaptor.Finalize(); e != nil { + if e := a.SpiBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } return err @@ -140,51 +150,15 @@ func (c *Adaptor) Finalize() error { // DefaultI2cBus returns the default i2c bus for this platform. // This overrides the base function due to the revision dependency. -func (c *Adaptor) DefaultI2cBus() int { - rev := c.readRevision() +func (a *Adaptor) DefaultI2cBus() int { + rev := a.readRevision() if rev == "2" || rev == "3" { return 1 } return 0 } -// PWMPin returns a raspi.PWMPin which provides the gobot.PWMPinner interface -func (c *Adaptor) PWMPin(id string) (gobot.PWMPinner, error) { - c.mutex.Lock() - defer c.mutex.Unlock() - - return c.pwmPin(id) -} - -// PwmWrite writes a PWM signal to the specified pin -func (c *Adaptor) PwmWrite(pin string, val byte) error { - c.mutex.Lock() - defer c.mutex.Unlock() - - sysPin, err := c.pwmPin(pin) - if err != nil { - return err - } - - duty := uint32(gobot.FromScale(float64(val), 0, 255) * float64(c.PiBlasterPeriod)) - return sysPin.SetDutyCycle(duty) -} - -// ServoWrite writes a servo signal to the specified pin -func (c *Adaptor) ServoWrite(pin string, angle byte) error { - c.mutex.Lock() - defer c.mutex.Unlock() - - sysPin, err := c.pwmPin(pin) - if err != nil { - return err - } - - duty := uint32(gobot.FromScale(float64(angle), 0, 180) * float64(c.PiBlasterPeriod)) - return sysPin.SetDutyCycle(duty) -} - -func (c *Adaptor) validateSpiBusNumber(busNr int) error { +func (a *Adaptor) validateSpiBusNumber(busNr int) error { // Valid bus numbers are [0,1] which corresponds to /dev/spidev0.x through /dev/spidev1.x. // x is the chip number <255 if (busNr < 0) || (busNr > 1) { @@ -193,7 +167,7 @@ func (c *Adaptor) validateSpiBusNumber(busNr int) error { return nil } -func (c *Adaptor) validateI2cBusNumber(busNr int) error { +func (a *Adaptor) validateI2cBusNumber(busNr int) error { // Valid bus number is [0..1] which corresponds to /dev/i2c-0 through /dev/i2c-1. if (busNr < 0) || (busNr > 1) { return fmt.Errorf("Bus number %d out of range", busNr) @@ -201,14 +175,14 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error { return nil } -func (c *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, error) { +func (a *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, error) { pinInfo, ok := analogPinDefinitions[id] if !ok { return "", false, false, 0, fmt.Errorf("'%s' is not a valid id for a analog pin", id) } path := pinInfo.path - info, err := c.sys.Stat(path) + info, err := a.sys.Stat(path) if err != nil { return "", false, false, 0, fmt.Errorf("Error (%v) on access '%s'", err, path) } @@ -219,28 +193,37 @@ func (c *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, err return path, pinInfo.r, pinInfo.w, pinInfo.bufLen, nil } -func (c *Adaptor) getPinTranslatorFunction() func(string) (string, int, error) { +// getPinTranslatorFunction returns a function to be able to translate GPIO and PWM pins. +// This means for pi-blaster usage, each pin can be used and therefore the pin is given as number, like a GPIO pin. +// For sysfs-PWM usage, the pin will be given as "pwm0" or "pwm1", because the real pin number depends on the user +// configuration in "/boot/config.txt". For further details, see "/boot/overlays/README". +func (a *Adaptor) getPinTranslatorFunction() func(string) (string, int, error) { return func(pin string) (string, int, error) { var line int - if val, ok := pins[pin][c.readRevision()]; ok { + if val, ok := pins[pin][a.readRevision()]; ok { line = val } else if val, ok := pins[pin]["*"]; ok { line = val } else { - return "", 0, errors.New("Not a valid pin") + return "", 0, fmt.Errorf("'%s' is not a valid pin id for raspi revision %s", pin, a.revision) } - // TODO: Pi1 model B has only this single "gpiochip0", a change of the translator is needed, - // to support different chips with different revisions - return "gpiochip0", line, nil + // We always use "gpiochip0", because currently all pins are available with this approach. A change of the + // translator would be needed to support different chips (e.g. gpiochip1) with different revisions. + path := "gpiochip0" + if strings.HasPrefix(pin, "pwm") { + path = "/sys/class/pwm/pwmchip0" + } + + return path, line, nil } } -func (c *Adaptor) readRevision() string { - if c.revision == "" { - c.revision = "0" - content, err := c.sys.ReadFile(infoFile) +func (a *Adaptor) readRevision() string { + if a.revision == "" { + a.revision = "0" + content, err := a.sys.ReadFile(infoFile) if err != nil { - return c.revision + return a.revision } for _, v := range strings.Split(string(content), "\n") { if strings.Contains(v, "Revision") { @@ -248,34 +231,15 @@ func (c *Adaptor) readRevision() string { version, _ := strconv.ParseInt("0x"+s[len(s)-1], 0, 64) switch { case version <= 3: - c.revision = "1" + a.revision = "1" case version <= 15: - c.revision = "2" + a.revision = "2" default: - c.revision = "3" + a.revision = "3" } } } } - return c.revision -} - -func (c *Adaptor) pwmPin(id string) (gobot.PWMPinner, error) { - pin := c.pwmPins[id] - - if pin == nil { - tf := c.getPinTranslatorFunction() - _, i, err := tf(id) - if err != nil { - return nil, err - } - pin = NewPWMPin(c.sys, "/dev/pi-blaster", strconv.Itoa(i)) - if err := pin.SetPeriod(c.PiBlasterPeriod); err != nil { - return nil, err - } - c.pwmPins[id] = pin - } - - return pin, nil + return a.revision } diff --git a/platforms/raspi/raspi_adaptor_test.go b/platforms/raspi/raspi_adaptor_test.go index c48f7ae46..a463963a6 100644 --- a/platforms/raspi/raspi_adaptor_test.go +++ b/platforms/raspi/raspi_adaptor_test.go @@ -16,9 +16,32 @@ import ( "gobot.io/x/gobot/v2/drivers/gpio" "gobot.io/x/gobot/v2/drivers/i2c" "gobot.io/x/gobot/v2/drivers/spi" + "gobot.io/x/gobot/v2/platforms/adaptors" "gobot.io/x/gobot/v2/system" ) +const ( + pwmDir = "/sys/class/pwm/pwmchip0/" //nolint:gosec // false positive + pwmPwmDir = pwmDir + "pwm0/" + pwmExportPath = pwmDir + "export" + pwmUnexportPath = pwmDir + "unexport" + pwmEnablePath = pwmPwmDir + "enable" + pwmPeriodPath = pwmPwmDir + "period" + pwmDutyCyclePath = pwmPwmDir + "duty_cycle" + pwmPolarityPath = pwmPwmDir + "polarity" + + pwmInvertedIdentifier = "inversed" +) + +var pwmMockPaths = []string{ + pwmExportPath, + pwmUnexportPath, + pwmEnablePath, + pwmPeriodPath, + pwmDutyCyclePath, + pwmPolarityPath, +} + // make sure that this Adaptor fulfills all the required interfaces var ( _ gobot.Adaptor = (*Adaptor)(nil) @@ -33,10 +56,19 @@ var ( _ spi.Connector = (*Adaptor)(nil) ) +func preparePwmFs(fs *system.MockFilesystem) { + fs.Files[pwmEnablePath].Contents = "0" + fs.Files[pwmPeriodPath].Contents = "0" + fs.Files[pwmDutyCyclePath].Contents = "0" + fs.Files[pwmPolarityPath].Contents = pwmInvertedIdentifier +} + func initTestAdaptorWithMockedFilesystem(mockPaths []string) (*Adaptor, *system.MockFilesystem) { a := NewAdaptor() fs := a.sys.UseMockFilesystem(mockPaths) - _ = a.Connect() + if err := a.Connect(); err != nil { + panic(err) + } return a, fs } @@ -133,35 +165,96 @@ func TestAnalog(t *testing.T) { require.NoError(t, a.Finalize()) } -func TestDigitalPWM(t *testing.T) { - mockedPaths := []string{"/dev/pi-blaster"} - a, fs := initTestAdaptorWithMockedFilesystem(mockedPaths) - a.PiBlasterPeriod = 20000000 - - require.NoError(t, a.PwmWrite("7", 4)) +func TestPwmWrite(t *testing.T) { + // arrange + a, fs := initTestAdaptorWithMockedFilesystem(pwmMockPaths) + preparePwmFs(fs) + // act + err := a.PwmWrite("pwm0", 100) + // assert + require.NoError(t, err) + assert.Equal(t, "0", fs.Files[pwmExportPath].Contents) + assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents) + assert.Equal(t, "10000000", fs.Files[pwmPeriodPath].Contents) + assert.Equal(t, "3921568", fs.Files[pwmDutyCyclePath].Contents) + assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents) + // act & assert invalid pin + err = a.PwmWrite("pwm1", 42) + require.ErrorContains(t, err, "'pwm1' is not a valid pin id for raspi revision 0") + require.NoError(t, a.Finalize()) +} - pin, _ := a.PWMPin("7") - period, _ := pin.Period() - assert.Equal(t, uint32(20000000), period) +func TestServoWrite(t *testing.T) { + // arrange: prepare 50Hz for servos + const ( + pin = "pwm0" + fiftyHzNano = 20000000 + ) + a := NewAdaptor(adaptors.WithPWMDefaultPeriodForPin(pin, fiftyHzNano)) + fs := a.sys.UseMockFilesystem(pwmMockPaths) + preparePwmFs(fs) + require.NoError(t, a.Connect()) + // act & assert for 0° (min default value) + err := a.ServoWrite(pin, 0) + require.NoError(t, err) + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents) + assert.Equal(t, "500000", fs.Files[pwmDutyCyclePath].Contents) + // act & assert for 180° (max default value) + err = a.ServoWrite(pin, 180) + require.NoError(t, err) + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents) + assert.Equal(t, "2500000", fs.Files[pwmDutyCyclePath].Contents) + // act & assert invalid pins + err = a.ServoWrite("3", 120) + require.ErrorContains(t, err, "'3' is not a valid pin id for raspi revision 0") + require.NoError(t, a.Finalize()) +} +func TestPWMWrite_piPlaster(t *testing.T) { + // arrange + const hundredHzNano = 10000000 + mockedPaths := []string{"/dev/pi-blaster"} + a := NewAdaptor(adaptors.WithPWMUsePiBlaster()) + fs := a.sys.UseMockFilesystem(mockedPaths) + require.NoError(t, a.Connect()) + // act & assert: Write & Pin & Period require.NoError(t, a.PwmWrite("7", 255)) - assert.Equal(t, "4=1", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0]) - - require.NoError(t, a.ServoWrite("11", 90)) - - assert.Equal(t, "17=0.5", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0]) - - require.ErrorContains(t, a.PwmWrite("notexist", 1), "Not a valid pin") - require.ErrorContains(t, a.ServoWrite("notexist", 1), "Not a valid pin") - + pin, _ := a.PWMPin("7") + period, err := pin.Period() + require.NoError(t, err) + assert.Equal(t, uint32(hundredHzNano), period) + // act & assert: nonexistent pin + require.ErrorContains(t, a.PwmWrite("notexist", 1), "'notexist' is not a valid pin id for raspi revision 0") + // act & assert: SetDutyCycle pin, _ = a.PWMPin("12") - period, _ = pin.Period() - assert.Equal(t, uint32(20000000), period) - require.NoError(t, pin.SetDutyCycle(1.5*1000*1000)) + assert.Equal(t, "18=0.15", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0]) +} - assert.Equal(t, "18=0.075", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0]) +func TestPWM_piPlaster(t *testing.T) { + // arrange + const fiftyHzNano = 20000000 // 20 ms + mockedPaths := []string{"/dev/pi-blaster"} + a := NewAdaptor(adaptors.WithPWMUsePiBlaster(), adaptors.WithPWMDefaultPeriod(fiftyHzNano)) + fs := a.sys.UseMockFilesystem(mockedPaths) + require.NoError(t, a.Connect()) + // act & assert: Pin & Period + pin, _ := a.PWMPin("7") + period, err := pin.Period() + require.NoError(t, err) + assert.Equal(t, uint32(fiftyHzNano), period) + // act & assert for 180° (max default value), 2.5 ms => 12.5% + require.NoError(t, a.ServoWrite("11", 180)) + assert.Equal(t, "17=0.125", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0]) + // act & assert for 90° (center value), 1.5 ms => 7.5% duty + require.NoError(t, a.ServoWrite("11", 90)) + assert.Equal(t, "17=0.075", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0]) + // act & assert for 0° (min default value), 0.5 ms => 2.5% duty + require.NoError(t, a.ServoWrite("11", 0)) + assert.Equal(t, "17=0.025", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0]) + // act & assert: nonexistent pin + require.ErrorContains(t, a.ServoWrite("notexist", 1), "'notexist' is not a valid pin id for raspi revision 0") } func TestDigitalIO(t *testing.T) { @@ -187,7 +280,7 @@ func TestDigitalIO(t *testing.T) { require.NoError(t, err) assert.Equal(t, 1, i) - require.ErrorContains(t, a.DigitalWrite("notexist", 1), "Not a valid pin") + require.ErrorContains(t, a.DigitalWrite("notexist", 1), "'notexist' is not a valid pin id for raspi revision 2") require.NoError(t, a.Finalize()) } @@ -214,55 +307,6 @@ func TestDigitalPinConcurrency(t *testing.T) { } } -func TestPWMPin(t *testing.T) { - a := NewAdaptor() - if err := a.Connect(); err != nil { - panic(err) - } - - assert.Empty(t, a.pwmPins) - - a.revision = "3" - firstSysPin, err := a.PWMPin("35") - require.NoError(t, err) - assert.Len(t, a.pwmPins, 1) - - secondSysPin, err := a.PWMPin("35") - - require.NoError(t, err) - assert.Len(t, a.pwmPins, 1) - assert.Equal(t, secondSysPin, firstSysPin) - - otherSysPin, err := a.PWMPin("36") - - require.NoError(t, err) - assert.Len(t, a.pwmPins, 2) - assert.NotEqual(t, otherSysPin, firstSysPin) -} - -func TestPWMPinsReConnect(t *testing.T) { - // arrange - a := NewAdaptor() - a.revision = "3" - if err := a.Connect(); err != nil { - panic(err) - } - - _, err := a.PWMPin("35") - require.NoError(t, err) - assert.Len(t, a.pwmPins, 1) - require.NoError(t, a.Finalize()) - // act - err = a.Connect() - // assert - require.NoError(t, err) - assert.Empty(t, a.pwmPins) - _, _ = a.PWMPin("35") - _, err = a.PWMPin("36") - require.NoError(t, err) - assert.Len(t, a.pwmPins, 2) -} - func TestSpiDefaultValues(t *testing.T) { a := NewAdaptor() @@ -407,3 +451,73 @@ func Test_translateAnalogPin(t *testing.T) { }) } } + +func Test_getPinTranslatorFunction(t *testing.T) { + tests := map[string]struct { + id string + revision string + wantPath string + wantLine int + wantErr string + }{ + "translate_12_rev0": { + id: "12", + wantPath: "gpiochip0", + wantLine: 18, + }, + "translate_13_rev0": { + id: "13", + wantErr: "'13' is not a valid pin id for raspi revision 0", + }, + "translate_13_rev1": { + id: "13", + revision: "1", + wantPath: "gpiochip0", + wantLine: 21, + }, + "translate_29_rev1": { + id: "29", + revision: "1", + wantErr: "'29' is not a valid pin id for raspi revision 1", + }, + "translate_29_rev3": { + id: "29", + revision: "3", + wantPath: "gpiochip0", + wantLine: 5, + }, + "translate_pwm0_rev0": { + id: "pwm0", + wantPath: "/sys/class/pwm/pwmchip0", + wantLine: 0, + }, + "translate_pwm1_rev0": { + id: "pwm1", + wantErr: "'pwm1' is not a valid pin id for raspi revision 0", + }, + "translate_pwm1_rev3": { + id: "pwm1", + revision: "3", + wantPath: "/sys/class/pwm/pwmchip0", + wantLine: 1, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := NewAdaptor() + a.revision = tc.revision + // act + f := a.getPinTranslatorFunction() + path, line, err := f(tc.id) + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, tc.wantPath, path) + assert.Equal(t, tc.wantLine, line) + }) + } +} diff --git a/platforms/raspi/raspi_pin_map.go b/platforms/raspi/raspi_pin_map.go index 6fdda4835..6a7bdf7ea 100644 --- a/platforms/raspi/raspi_pin_map.go +++ b/platforms/raspi/raspi_pin_map.go @@ -85,6 +85,12 @@ var pins = map[string]map[string]int{ "40": { "3": 21, }, + "pwm0": { // pin 12 (GPIO18) and pin 32 (GPIO12) can be configured for "pwm0" + "*": 0, + }, + "pwm1": { // pin 33 (GPIO13) and pin 35 (GPIO19) can be configured for "pwm1" + "3": 1, + }, } var analogPinDefinitions = map[string]analogPinDefinition{ diff --git a/platforms/tinkerboard/adaptor.go b/platforms/tinkerboard/adaptor.go index 01f926660..8dc9a4b5b 100644 --- a/platforms/tinkerboard/adaptor.go +++ b/platforms/tinkerboard/adaptor.go @@ -12,8 +12,6 @@ import ( ) const ( - pwmInvertedIdentifier = "inversed" - defaultI2cBusNumber = 1 defaultSpiBusNumber = 0 @@ -80,7 +78,7 @@ func NewAdaptor(opts ...interface{}) *Adaptor { } var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) - pwmPinsOpts := []adaptors.PwmPinsOptionApplier{adaptors.WithPWMPolarityInvertedIdentifier(pwmInvertedIdentifier)} + var pwmPinsOpts []adaptors.PwmPinsOptionApplier for _, opt := range opts { switch o := opt.(type) { case func(adaptors.DigitalPinsOptioner): diff --git a/platforms/tinkerboard/adaptor_test.go b/platforms/tinkerboard/adaptor_test.go index 0b6a5fe6d..9667dcea1 100644 --- a/platforms/tinkerboard/adaptor_test.go +++ b/platforms/tinkerboard/adaptor_test.go @@ -31,6 +31,8 @@ const ( pwmPeriodPath = pwmPwmDir + "period" pwmDutyCyclePath = pwmPwmDir + "duty_cycle" pwmPolarityPath = pwmPwmDir + "polarity" + + pwmInvertedIdentifier = "inversed" ) var pwmMockPaths = []string{ diff --git a/system/pwmpin_sysfs_test.go b/system/pwmpin_sysfs_test.go index a9460667f..51a7f7747 100644 --- a/system/pwmpin_sysfs_test.go +++ b/system/pwmpin_sysfs_test.go @@ -14,7 +14,7 @@ var _ gobot.PWMPinner = (*pwmPinSysFs)(nil) const ( normal = "normal" - inverted = "inverted" + inverted = "inversed" ) func initTestPWMPinSysFsWithMockedFilesystem(mockPaths []string) (*pwmPinSysFs, *MockFilesystem) {