refactor: rewrite and fix pca9685 controllers
* implement throttle controller directly with pca9685 pin * close actuator controllers on exit * add trace logs * handle TERM signals to properly exit * add info logs at startup
This commit is contained in:
		@@ -1,39 +1,112 @@
 | 
			
		||||
package actuator
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/cyrilix/robocar-pca9685/pkg/util"
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
	"periph.io/x/conn/v3/gpio"
 | 
			
		||||
	"periph.io/x/conn/v3/i2c/i2creg"
 | 
			
		||||
	"periph.io/x/conn/v3/physic"
 | 
			
		||||
	"periph.io/x/devices/v3/pca9685"
 | 
			
		||||
	"periph.io/x/host/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	device *pca9685.Dev
 | 
			
		||||
const (
 | 
			
		||||
	MinPercent = -1.
 | 
			
		||||
	MaxPercent = 1.
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	zap.S().Info("init pca9685 controller")
 | 
			
		||||
type PWM int
 | 
			
		||||
 | 
			
		||||
func NewDevice(freq physic.Frequency) *pca9685.Dev {
 | 
			
		||||
	zap.S().Info("NewDevice pca9685 controller")
 | 
			
		||||
	_, err := host.Init()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		zap.S().Fatalf("unable to init host: %v", err)
 | 
			
		||||
		zap.S().Fatalf("unable to NewDevice host: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	zap.S().Info("open i2c bus")
 | 
			
		||||
	bus, err := i2creg.Open("")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		zap.S().Fatalf("unable to init i2c bus: %v", err)
 | 
			
		||||
		zap.S().Fatalf("unable to NewDevice i2c bus: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	zap.S().Info("i2c bus opened")
 | 
			
		||||
	zap.S().Infof("i2c bus opened: %v", bus)
 | 
			
		||||
 | 
			
		||||
	device, err = pca9685.NewI2C(bus, pca9685.I2CAddr)
 | 
			
		||||
	device, err := pca9685.NewI2C(bus, pca9685.I2CAddr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		zap.S().Fatalf("unable to init pca9685 bus: %v", err)
 | 
			
		||||
		zap.S().Fatalf("unable to NewDevice pca9685 bus: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	zap.S().Infof("set pwm frequency to %d", 60)
 | 
			
		||||
	err = device.SetPwmFreq(60 * physic.Hertz)
 | 
			
		||||
	err = device.SetPwmFreq(freq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		zap.S().Panicf("unable to set pwm frequency: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	zap.S().Info("init done")
 | 
			
		||||
	zap.S().Info("NewDevice done")
 | 
			
		||||
	return device
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertToDuty(percent float32, freq physic.Frequency, centerPWM, minPWM, maxPWM PWM) (gpio.Duty, error) {
 | 
			
		||||
	// map absolute angle to angle that vehicle can implement.
 | 
			
		||||
	pw := int(centerPWM)
 | 
			
		||||
	if percent > 0 {
 | 
			
		||||
		pw = util.MapRange(float64(percent), 0, MaxPercent, float64(centerPWM), float64(maxPWM))
 | 
			
		||||
	} else if percent < 0 {
 | 
			
		||||
		pw = util.MapRange(float64(percent), MinPercent, 0, float64(minPWM), float64(centerPWM))
 | 
			
		||||
	}
 | 
			
		||||
	zap.S().Debugf("convert value %v to pw: %v", percent, pw)
 | 
			
		||||
 | 
			
		||||
	per := freq.Period().Microseconds()
 | 
			
		||||
 | 
			
		||||
	draw := float64(pw) / float64(per)
 | 
			
		||||
	d, err := gpio.ParseDuty(fmt.Sprintf("%.f%%", draw*100))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, fmt.Errorf("unable to parse duty, probably bad compute: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return d, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Pca9685Controller struct {
 | 
			
		||||
	pin                        gpio.PinIO
 | 
			
		||||
	minPWM, maxPWM, neutralPWM PWM
 | 
			
		||||
	freq                       physic.Frequency
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Pca9685Controller) Close() error {
 | 
			
		||||
	return c.pin.Halt()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Pca9685Controller) SetDuty(d gpio.Duty) error {
 | 
			
		||||
	err := c.pin.PWM(d, c.freq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to set pwm value: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetPercentValue Set percent value
 | 
			
		||||
func (c *Pca9685Controller) SetPercentValue(p float32) error {
 | 
			
		||||
	d, err := convertToDuty(p, c.freq, c.neutralPWM, c.minPWM, c.maxPWM)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to compute Duty value for steering: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = c.SetDuty(d)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to apply duty value '%v' for pin '%v': '%w' ", d, c.pin.Name(), err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPca9685Controller(device *pca9685.Dev, channel int, minPWM, maxPWM, neutralPWM PWM, freq physic.Frequency) (*Pca9685Controller, error) {
 | 
			
		||||
	p, err := device.CreatePin(channel)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to create pin for channel %v: %w", channel, err)
 | 
			
		||||
	}
 | 
			
		||||
	s := Pca9685Controller{
 | 
			
		||||
		pin:        p,
 | 
			
		||||
		minPWM:     minPWM,
 | 
			
		||||
		maxPWM:     maxPWM,
 | 
			
		||||
		neutralPWM: neutralPWM,
 | 
			
		||||
		freq:       freq,
 | 
			
		||||
	}
 | 
			
		||||
	return &s, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										81
									
								
								pkg/actuator/pca9685_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								pkg/actuator/pca9685_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
package actuator
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
	"periph.io/x/conn/v3/physic"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	l, _ := zap.NewDevelopment()
 | 
			
		||||
	zap.ReplaceGlobals(l)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_convertToDuty(t *testing.T) {
 | 
			
		||||
	type fields struct {
 | 
			
		||||
	}
 | 
			
		||||
	type args struct {
 | 
			
		||||
		percent   float32
 | 
			
		||||
		leftPWM   PWM
 | 
			
		||||
		rightPWM  PWM
 | 
			
		||||
		centerPWM PWM
 | 
			
		||||
		freq      physic.Frequency
 | 
			
		||||
	}
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		fields  fields
 | 
			
		||||
		args    args
 | 
			
		||||
		want    string
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "center",
 | 
			
		||||
			args: args{
 | 
			
		||||
				percent:   0.,
 | 
			
		||||
				leftPWM:   1000,
 | 
			
		||||
				rightPWM:  2000,
 | 
			
		||||
				centerPWM: 1500,
 | 
			
		||||
				freq:      60 * physic.Hertz,
 | 
			
		||||
			},
 | 
			
		||||
			want: "9%",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "left",
 | 
			
		||||
			args: args{
 | 
			
		||||
				percent:   -1.,
 | 
			
		||||
				leftPWM:   1000,
 | 
			
		||||
				rightPWM:  2000,
 | 
			
		||||
				centerPWM: 1500,
 | 
			
		||||
				freq:      60 * physic.Hertz,
 | 
			
		||||
			},
 | 
			
		||||
			want: "6%",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "right",
 | 
			
		||||
			args: args{
 | 
			
		||||
				percent:   1.,
 | 
			
		||||
				leftPWM:   1000,
 | 
			
		||||
				rightPWM:  2000,
 | 
			
		||||
				centerPWM: 1500,
 | 
			
		||||
				freq:      60 * physic.Hertz,
 | 
			
		||||
			},
 | 
			
		||||
			want: "12%",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
			if got, err := convertToDuty(tt.args.percent, tt.args.freq, tt.args.centerPWM, tt.args.leftPWM, tt.args.rightPWM); got.String() != tt.want {
 | 
			
		||||
				if tt.wantErr && err == nil {
 | 
			
		||||
					t.Errorf("an error is expected")
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Errorf("unexpected error: %v", err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				t.Errorf("convertToDuty() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
package actuator
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/cyrilix/robocar-pca9685/pkg/util"
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
	"periph.io/x/conn/v3/gpio"
 | 
			
		||||
	"periph.io/x/devices/v3/pca9685"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	LeftAngle  = -1.
 | 
			
		||||
	RightAngle = 1.
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Steering struct {
 | 
			
		||||
	channel                      int
 | 
			
		||||
	leftPWM, rightPWM, centerPWM int
 | 
			
		||||
	dev                          *pca9685.Dev
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Steering) SetPulse(pulse int) {
 | 
			
		||||
	err := s.dev.SetPwm(s.channel, 0, gpio.Duty(pulse))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		zap.S().Warnf("unable to set steering pwm value: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetPercentValue Set percent value steering
 | 
			
		||||
func (s *Steering) SetPercentValue(p float32) {
 | 
			
		||||
	// map absolute angle to angle that vehicle can implement.
 | 
			
		||||
 | 
			
		||||
	pulse := 0
 | 
			
		||||
	if p > 0 {
 | 
			
		||||
		pulse = util.MapRange(float64(p), 0, RightAngle, float64(s.centerPWM), float64(s.rightPWM))
 | 
			
		||||
	} else if p < 0 {
 | 
			
		||||
		pulse = util.MapRange(float64(p), LeftAngle, 0, float64(s.leftPWM), float64(s.centerPWM))
 | 
			
		||||
	}
 | 
			
		||||
	zap.S().Debugf("convert steering %v to %v", p, pulse)
 | 
			
		||||
	s.SetPulse(pulse)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSteering(channel, leftPWM, rightPWM, centerPWM int) *Steering {
 | 
			
		||||
	s := Steering{
 | 
			
		||||
		channel:   channel,
 | 
			
		||||
		dev:       device,
 | 
			
		||||
		leftPWM:   leftPWM,
 | 
			
		||||
		rightPWM:  rightPWM,
 | 
			
		||||
		centerPWM: centerPWM,
 | 
			
		||||
	}
 | 
			
		||||
	return &s
 | 
			
		||||
}
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
package actuator
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/cyrilix/robocar-pca9685/pkg/util"
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
	"periph.io/x/conn/v3/gpio"
 | 
			
		||||
	"periph.io/x/devices/v3/pca9685"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	MinThrottle = -1
 | 
			
		||||
	MaxThrottle = 1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Throttle struct {
 | 
			
		||||
	channel                       int
 | 
			
		||||
	zeroPulse, minPulse, maxPulse int
 | 
			
		||||
	dev                           *pca9685.Dev
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Throttle) SetPulse(pulse int) {
 | 
			
		||||
	err := t.dev.SetPwm(t.channel, 0, gpio.Duty(pulse))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		zap.S().Errorf("unable to set throttle pwm value: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set percent value throttle
 | 
			
		||||
func (t *Throttle) SetPercentValue(p float32) {
 | 
			
		||||
	var pulse int
 | 
			
		||||
	if p > 0 {
 | 
			
		||||
		pulse = util.MapRange(float64(p), 0, MaxThrottle, float64(t.zeroPulse), float64(t.maxPulse))
 | 
			
		||||
	} else {
 | 
			
		||||
		pulse = util.MapRange(float64(p), MinThrottle, 0, float64(t.minPulse), float64(t.zeroPulse))
 | 
			
		||||
	}
 | 
			
		||||
	zap.S().Debugf("set throttle to %v-> %v (%v, %v, %v, %v, %v)", p, pulse, LeftAngle, RightAngle, t.minPulse, t.maxPulse, t.zeroPulse)
 | 
			
		||||
	t.SetPulse(pulse)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewThrottle(channel, zeroPulse, minPulse, maxPulse int) *Throttle {
 | 
			
		||||
	t := Throttle{
 | 
			
		||||
		channel:   channel,
 | 
			
		||||
		dev:       device,
 | 
			
		||||
		zeroPulse: zeroPulse,
 | 
			
		||||
		minPulse:  minPulse,
 | 
			
		||||
		maxPulse:  maxPulse,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	zap.S().Info("send zero pulse to calibrate ESC")
 | 
			
		||||
	t.SetPercentValue(0)
 | 
			
		||||
 | 
			
		||||
	return &t
 | 
			
		||||
}
 | 
			
		||||
@@ -3,18 +3,24 @@ package part
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/cyrilix/robocar-base/service"
 | 
			
		||||
	"github.com/cyrilix/robocar-pca9685/pkg/actuator"
 | 
			
		||||
	"github.com/cyrilix/robocar-protobuf/go/events"
 | 
			
		||||
	MQTT "github.com/eclipse/paho.mqtt.golang"
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
	"google.golang.org/protobuf/proto"
 | 
			
		||||
	"io"
 | 
			
		||||
	"sync"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ActuatorController interface {
 | 
			
		||||
	// SetPercentValue Set percent value
 | 
			
		||||
	SetPercentValue(p float32) error
 | 
			
		||||
	io.Closer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Pca9685Part struct {
 | 
			
		||||
	client       MQTT.Client
 | 
			
		||||
	throttleCtrl *actuator.Throttle
 | 
			
		||||
	steeringCtrl *actuator.Steering
 | 
			
		||||
	throttleCtrl ActuatorController
 | 
			
		||||
	steeringCtrl ActuatorController
 | 
			
		||||
 | 
			
		||||
	muSteering    sync.Mutex
 | 
			
		||||
	steeringValue float32
 | 
			
		||||
@@ -29,7 +35,7 @@ type Pca9685Part struct {
 | 
			
		||||
	cancel chan interface{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPca9685Part(client MQTT.Client, throttleCtrl *actuator.Throttle, steeringCtrl *actuator.Steering, updateFrequency int, throttleTopic, steeringTopic string) *Pca9685Part {
 | 
			
		||||
func NewPca9685Part(client MQTT.Client, throttleCtrl ActuatorController, steeringCtrl ActuatorController, updateFrequency int, throttleTopic, steeringTopic string) *Pca9685Part {
 | 
			
		||||
	return &Pca9685Part{
 | 
			
		||||
		client:          client,
 | 
			
		||||
		throttleCtrl:    throttleCtrl,
 | 
			
		||||
@@ -46,8 +52,12 @@ func (p *Pca9685Part) Start() error {
 | 
			
		||||
		return fmt.Errorf("unable to start service: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	p.steeringCtrl.SetPercentValue(0)
 | 
			
		||||
	p.throttleCtrl.SetPercentValue(0)
 | 
			
		||||
	if err := p.steeringCtrl.SetPercentValue(0); err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to start steering controller: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := p.throttleCtrl.SetPercentValue(0); err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to set init value '0%%': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
@@ -59,6 +69,12 @@ func (p *Pca9685Part) Start() error {
 | 
			
		||||
 | 
			
		||||
func (p *Pca9685Part) Stop() {
 | 
			
		||||
	close(p.cancel)
 | 
			
		||||
	if err := p.steeringCtrl.Close(); err != nil {
 | 
			
		||||
		zap.S().Errorf("unable to close steering controller: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := p.throttleCtrl.Close(); err != nil {
 | 
			
		||||
		zap.S().Errorf("unable to close throttle controller: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	service.StopService("pca9685", p.client, p.throttleTopic, p.steeringTopic)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -74,7 +90,10 @@ func (p *Pca9685Part) onThrottleChange(_ MQTT.Client, message MQTT.Message) {
 | 
			
		||||
	zap.S().Debugf("new throttle value: %v", throttle.GetThrottle())
 | 
			
		||||
	p.muThrottle.Lock()
 | 
			
		||||
	defer p.muThrottle.Unlock()
 | 
			
		||||
	p.throttleCtrl.SetPercentValue(throttle.GetThrottle())
 | 
			
		||||
	err = p.throttleCtrl.SetPercentValue(throttle.GetThrottle())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		zap.S().Errorf("unable to set steering value '%v': %w", throttle.GetThrottle(), err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Pca9685Part) onSteeringChange(_ MQTT.Client, message MQTT.Message) {
 | 
			
		||||
@@ -90,7 +109,10 @@ func (p *Pca9685Part) onSteeringChange(_ MQTT.Client, message MQTT.Message) {
 | 
			
		||||
	zap.S().Debugf("new steering value: %v", steering.GetSteering())
 | 
			
		||||
	p.muSteering.Lock()
 | 
			
		||||
	defer p.muSteering.Unlock()
 | 
			
		||||
	p.steeringCtrl.SetPercentValue(steering.GetSteering())
 | 
			
		||||
	err = p.steeringCtrl.SetPercentValue(steering.GetSteering())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		zap.S().Errorf("unable to set steering value '%v': %w", steering.GetSteering(), err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Pca9685Part) registerCallbacks() error {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user