mirror of
https://github.com/hybridgroup/gobot.git
synced 2025-04-24 13:48:49 +08:00
raspi(pwm): add support for sysfs and fix pi-blaster
This commit is contained in:
parent
a2690d2b15
commit
ab4a76ba8f
@ -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)
|
||||
|
92
examples/raspi_servo.go
Normal file
92
examples/raspi_servo.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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 (
|
||||
|
118
platforms/adaptors/piblasterpwm_pin.go
Normal file
118
platforms/adaptors/piblasterpwm_pin.go
Normal file
@ -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
|
||||
}
|
79
platforms/adaptors/piblasterpwm_pin_test.go
Normal file
79
platforms/adaptors/piblasterpwm_pin_test.go
Normal file
@ -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())
|
||||
}
|
@ -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".
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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{},
|
||||
}
|
||||
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,
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.pwmPins = nil
|
||||
|
||||
if e := c.I2cBusAdaptor.Finalize(); e != nil {
|
||||
if e := a.PWMPinsAdaptor.Finalize(); e != nil {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
|
||||
if e := c.SpiBusAdaptor.Finalize(); e != nil {
|
||||
if e := a.I2cBusAdaptor.Finalize(); e != nil {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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):
|
||||
|
@ -31,6 +31,8 @@ const (
|
||||
pwmPeriodPath = pwmPwmDir + "period"
|
||||
pwmDutyCyclePath = pwmPwmDir + "duty_cycle"
|
||||
pwmPolarityPath = pwmPwmDir + "polarity"
|
||||
|
||||
pwmInvertedIdentifier = "inversed"
|
||||
)
|
||||
|
||||
var pwmMockPaths = []string{
|
||||
|
@ -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
|
||||
|
||||
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:
|
||||
### Using Linux Kernel sysfs implementation
|
||||
|
||||
[https://github.com/sarfata/pi-blaster](https://github.com/sarfata/pi-blaster)
|
||||
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: <https://github.com/sarfata/pi-blaster>
|
||||
|
||||
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 <https://github.com/sarfata/pi-blaster#how-to-adjust-the-frequency-and-the-resolution-of-the-pwm>.
|
||||
|
||||
It is not possible to change the period from gobot side.
|
||||
|
||||
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)
|
||||
...
|
||||
```
|
||||
|
@ -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
|
||||
}
|
@ -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())
|
||||
}
|
@ -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,
|
||||
}
|
||||
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,
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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 := 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
|
||||
}
|
||||
|
||||
if err := c.AnalogPinsAdaptor.Connect(); err != nil {
|
||||
if err := a.AnalogPinsAdaptor.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.pwmPins = nil
|
||||
|
||||
if e := c.AnalogPinsAdaptor.Finalize(); e != nil {
|
||||
if e := a.PWMPinsAdaptor.Finalize(); e != nil {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
|
||||
if e := c.I2cBusAdaptor.Finalize(); e != nil {
|
||||
if e := a.AnalogPinsAdaptor.Finalize(); e != nil {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
|
||||
if e := c.SpiBusAdaptor.Finalize(); e != nil {
|
||||
if e := a.I2cBusAdaptor.Finalize(); e != nil {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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) {
|
||||
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())
|
||||
}
|
||||
|
||||
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, fs := initTestAdaptorWithMockedFilesystem(mockedPaths)
|
||||
a.PiBlasterPeriod = 20000000
|
||||
|
||||
require.NoError(t, a.PwmWrite("7", 4))
|
||||
|
||||
pin, _ := a.PWMPin("7")
|
||||
period, _ := pin.Period()
|
||||
assert.Equal(t, uint32(20000000), period)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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):
|
||||
|
@ -31,6 +31,8 @@ const (
|
||||
pwmPeriodPath = pwmPwmDir + "period"
|
||||
pwmDutyCyclePath = pwmPwmDir + "duty_cycle"
|
||||
pwmPolarityPath = pwmPwmDir + "polarity"
|
||||
|
||||
pwmInvertedIdentifier = "inversed"
|
||||
)
|
||||
|
||||
var pwmMockPaths = []string{
|
||||
|
@ -14,7 +14,7 @@ var _ gobot.PWMPinner = (*pwmPinSysFs)(nil)
|
||||
|
||||
const (
|
||||
normal = "normal"
|
||||
inverted = "inverted"
|
||||
inverted = "inversed"
|
||||
)
|
||||
|
||||
func initTestPWMPinSysFsWithMockedFilesystem(mockPaths []string) (*pwmPinSysFs, *MockFilesystem) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user