Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSDK-2478: Update encoder driver methods #2161

Merged
merged 11 commits into from
Apr 11, 2023
37 changes: 26 additions & 11 deletions components/board/arduino/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type Encoder struct {
A, B string
name string

positionType encoder.PositionType
generic.Unimplemented
}

Expand All @@ -102,28 +103,42 @@ func (cfg *EncoderConfig) Validate(path string) ([]string, error) {
return deps, nil
}

// TicksCount returns number of ticks since last zeroing.
func (e *Encoder) TicksCount(ctx context.Context, extra map[string]interface{}) (float64, error) {
// GetPosition returns the current position in terms of ticks or
// degrees, and whether it is a relative or absolute position.
func (e *Encoder) GetPosition(
ctx context.Context,
positionType *encoder.PositionType,
extra map[string]interface{},
) (float64, encoder.PositionType, error) {
if positionType != nil && *positionType == encoder.PositionTypeDEGREES {
err := errors.New("Encoder does not support PositionType Angle Degrees, use a different PositionType")
return 0, *positionType, err
}
res, err := e.board.runCommand("motor-position " + e.name)
if err != nil {
return 0, err
return 0, e.positionType, err
}

ticks, err := strconv.ParseInt(res, 10, 64)
if err != nil {
return 0, fmt.Errorf("couldn't parse # ticks (%s) : %w", res, err)
return 0, e.positionType, fmt.Errorf("couldn't parse # ticks (%s) : %w", res, err)
}

return float64(ticks), nil
return float64(ticks), e.positionType, nil
}

// Reset sets the current position of the motor (adjusted by a given offset)
// ResetPosition sets the current position of the motor (adjusted by a given offset)
// to be its new zero position.
func (e *Encoder) Reset(ctx context.Context, offset float64, extra map[string]interface{}) error {
if err := encoder.ValidateIntegerOffset(offset); err != nil {
return err
}
offsetInt := int64(offset)
func (e *Encoder) ResetPosition(ctx context.Context, extra map[string]interface{}) error {
offsetInt := int64(0)
_, err := e.board.runCommand(fmt.Sprintf("motor-zero %s %d", e.name, offsetInt))
return err
}

// GetProperties returns a list of all the position types that are supported by a given encoder.
func (e *Encoder) GetProperties(ctx context.Context, extra map[string]interface{}) (map[encoder.Feature]bool, error) {
return map[encoder.Feature]bool{
encoder.TicksCountSupported: true,
encoder.AngleDegreesSupported: false,
}, nil
}
5 changes: 3 additions & 2 deletions components/board/pi/impl/external_hardware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"go.viam.com/rdk/components/board/genericlinux"
picommon "go.viam.com/rdk/components/board/pi/common"
"go.viam.com/rdk/components/encoder"
"go.viam.com/rdk/components/encoder/incremental"
"go.viam.com/rdk/components/motor"
"go.viam.com/rdk/components/motor/gpio"
"go.viam.com/rdk/components/servo"
Expand Down Expand Up @@ -147,8 +148,8 @@ func TestPiHardware(t *testing.T) {

deps := make(registry.Dependencies)
_, err = encoderReg.Constructor(ctx, deps, config.Component{
Name: "encoder1", ConvertedAttributes: &encoder.IncrementalConfig{
Pins: encoder.IncrementalPins{
Name: "encoder1", ConvertedAttributes: &incremental.AttrConfig{
Pins: incremental.Pins{
A: "a",
B: "b",
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package encoder
// Package ams implements the AMS_AS5048 encoder
package ams

import (
"context"
Expand All @@ -12,6 +13,7 @@ import (
"go.viam.com/utils"

"go.viam.com/rdk/components/board"
"go.viam.com/rdk/components/encoder"
"go.viam.com/rdk/components/generic"
"go.viam.com/rdk/config"
"go.viam.com/rdk/registry"
Expand All @@ -36,7 +38,7 @@ var waitTimeNano = (1.0 / 50.0) * 1000000000.0

func init() {
registry.RegisterComponent(
Subtype,
encoder.Subtype,
modelName,
registry.Component{
Constructor: func(
Expand All @@ -50,19 +52,19 @@ func init() {
},
)
config.RegisterComponentAttributeMapConverter(
Subtype,
encoder.Subtype,
modelName,
func(attributes config.AttributeMap) (interface{}, error) {
var conf AS5048Config
var conf AttrConfig
return config.TransformAttributeMapToStruct(&conf, attributes)
},
&AS5048Config{},
&AttrConfig{},
)
}

// AS5048Config contains the connection information for
// AttrConfig contains the connection information for
// configuring an AS5048 encoder.
type AS5048Config struct {
type AttrConfig struct {
BoardName string `json:"board"`
// We include connection type here in anticipation for
// future SPI support
Expand All @@ -72,7 +74,7 @@ type AS5048Config struct {

// Validate checks the attributes of an initialized config
// for proper values.
func (conf *AS5048Config) Validate(path string) ([]string, error) {
func (conf *AttrConfig) Validate(path string) ([]string, error) {
var deps []string

connType := conf.ConnectionType
Expand Down Expand Up @@ -120,15 +122,16 @@ func (cfg *I2CAttrConfig) ValidateI2C(path string) error {
return nil
}

// AS5048 is a struct representing an instance of a hardware unit
// Encoder is a struct representing an instance of a hardware unit
// in AMS's AS5048 series of Hall-effect encoders.
type AS5048 struct {
type Encoder struct {
mu sync.RWMutex
logger golog.Logger
position float64
positionOffset float64
rotations int
connectionType string
positionType encoder.PositionType
i2cBus board.I2C
i2cAddr byte
cancelCtx context.Context
Expand All @@ -140,17 +143,18 @@ type AS5048 struct {
func newAS5048Encoder(
ctx context.Context, deps registry.Dependencies,
cfg config.Component, logger *zap.SugaredLogger,
) (*AS5048, error) {
attr, ok := cfg.ConvertedAttributes.(*AS5048Config)
) (encoder.Encoder, error) {
attr, ok := cfg.ConvertedAttributes.(*AttrConfig)
if !ok {
return nil, rdkutils.NewUnexpectedTypeError(attr, cfg.ConvertedAttributes)
}
cancelCtx, cancel := context.WithCancel(ctx)
res := &AS5048{
res := &Encoder{
connectionType: attr.ConnectionType,
cancelCtx: cancelCtx,
cancel: cancel,
logger: logger,
positionType: encoder.PositionTypeTICKS,
}
brd, err := board.FromDependencies(deps, attr.BoardName)
if err != nil {
Expand All @@ -176,8 +180,8 @@ func newAS5048Encoder(
return res, nil
}

func (enc *AS5048) startPositionLoop(ctx context.Context) error {
if err := enc.Reset(ctx, 0.0, map[string]interface{}{}); err != nil {
func (enc *Encoder) startPositionLoop(ctx context.Context) error {
if err := enc.ResetPosition(ctx, map[string]interface{}{}); err != nil {
return err
}
enc.activeBackgroundWorkers.Add(1)
Expand All @@ -197,7 +201,7 @@ func (enc *AS5048) startPositionLoop(ctx context.Context) error {
return nil
}

func (enc *AS5048) readPosition(ctx context.Context) (float64, error) {
func (enc *Encoder) readPosition(ctx context.Context) (float64, error) {
// retrieve the 8 most significant bits of the 14-bit resolution
// position
msB, err := enc.readByteDataFromBus(ctx, enc.i2cBus, enc.i2cAddr, byte(0xFE))
Expand All @@ -221,7 +225,7 @@ func convertBytesToAngle(msB, lsB byte) float64 {
return (float64(byteData) * scalingFactor)
}

func (enc *AS5048) updatePosition(ctx context.Context) error {
func (enc *Encoder) updatePosition(ctx context.Context) error {
enc.mu.Lock()
defer enc.mu.Unlock()
angleDeg, err := enc.readPosition(ctx)
Expand All @@ -243,35 +247,37 @@ func (enc *AS5048) updatePosition(ctx context.Context) error {
return nil
}

// TicksCount returns the total number of rotations detected
// GetPosition returns the total number of rotations detected
// by the encoder (rather than a number of pulse state transitions)
// because this encoder is absolute and not incremental. As a result
// a user MUST set ticks_per_rotation on the config of the corresponding
// motor to 1. Any other value will result in completely incorrect
// position measurements by the motor.
func (enc *AS5048) TicksCount(
ctx context.Context, extra map[string]interface{},
) (float64, error) {
func (enc *Encoder) GetPosition(
ctx context.Context, positionType *encoder.PositionType, extra map[string]interface{},
) (float64, encoder.PositionType, error) {
enc.mu.RLock()
defer enc.mu.RUnlock()
if positionType != nil && *positionType == encoder.PositionTypeDEGREES {
enc.positionType = encoder.PositionTypeDEGREES
return enc.position, enc.positionType, nil
}
ticks := float64(enc.rotations) + enc.position/360.0
return ticks, nil
enc.positionType = encoder.PositionTypeTICKS
return ticks, enc.positionType, nil
}

// Reset sets the current position measured by the encoder to be considered
// its new zero position. If the offset provided is not 0.0, it also
// sets the positionOffset attribute and adjusts all future recorded
// positions by that offset (until the function is called again).
func (enc *AS5048) Reset(
ctx context.Context, offset float64, extra map[string]interface{},
// ResetPosition sets the current position measured by the encoder to be
// considered its new zero position.
func (enc *Encoder) ResetPosition(
ctx context.Context, extra map[string]interface{},
) error {
enc.mu.Lock()
defer enc.mu.Unlock()
// NOTE (GV): potential improvement could be writing the offset position
// to the zero register of the encoder rather than keeping track
// on the struct
enc.positionOffset = offset
enc.position = 0.0 + offset
enc.position = 0.0
currentMSB, err := enc.readByteDataFromBus(ctx, enc.i2cBus, enc.i2cAddr, byte(0xFE))
if err != nil {
return err
Expand Down Expand Up @@ -301,16 +307,25 @@ func (enc *AS5048) Reset(
return nil
}

// GetProperties returns a list of all the position types that are supported by a given encoder.
func (enc *Encoder) GetProperties(ctx context.Context, extra map[string]interface{}) (map[encoder.Feature]bool, error) {
return map[encoder.Feature]bool{
encoder.TicksCountSupported: true,
encoder.AngleDegreesSupported: true,
}, nil
}

// Close stops the position loop of the encoder when the component
// is closed.
func (enc *AS5048) Close() {
func (enc *Encoder) Close() error {
enc.cancel()
enc.activeBackgroundWorkers.Wait()
return nil
}

// readByteDataFromBus opens a handle for the bus adhoc to perform a single read
// and returns the result. The handle is closed at the end.
func (enc *AS5048) readByteDataFromBus(ctx context.Context, bus board.I2C, addr, register byte) (byte, error) {
func (enc *Encoder) readByteDataFromBus(ctx context.Context, bus board.I2C, addr, register byte) (byte, error) {
i2cHandle, err := bus.OpenHandle(addr)
if err != nil {
return 0, err
Expand All @@ -325,7 +340,7 @@ func (enc *AS5048) readByteDataFromBus(ctx context.Context, bus board.I2C, addr,

// writeByteDataToBus opens a handle for the bus adhoc to perform a single write.
// The handle is closed at the end.
func (enc *AS5048) writeByteDataToBus(ctx context.Context, bus board.I2C, addr, register, data byte) error {
func (enc *Encoder) writeByteDataToBus(ctx context.Context, bus board.I2C, addr, register, data byte) error {
i2cHandle, err := bus.OpenHandle(addr)
if err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package encoder
package ams

import (
"math"
Expand Down
82 changes: 82 additions & 0 deletions components/encoder/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package encoder

import (
"context"

"github.com/edaniels/golog"
pb "go.viam.com/api/component/encoder/v1"
"go.viam.com/utils/rpc"
"google.golang.org/protobuf/types/known/structpb"

"go.viam.com/rdk/protoutils"
)

// client implements EncoderServiceClient.
type client struct {
name string
conn rpc.ClientConn
client pb.EncoderServiceClient
logger golog.Logger
}

// NewClientFromConn constructs a new Client from connection passed in.
func NewClientFromConn(ctx context.Context, conn rpc.ClientConn, name string, logger golog.Logger) Encoder {
c := pb.NewEncoderServiceClient(conn)
return &client{
name: name,
conn: conn,
client: c,
logger: logger,
}
}

// GetPosition returns the current position in terms of ticks or
// degrees, and whether it is a relative or absolute position.
func (c *client) GetPosition(
ctx context.Context,
positionType *PositionType,
extra map[string]interface{},
) (float64, PositionType, error) {
ext, err := structpb.NewStruct(extra)
if err != nil {
return 0, PositionTypeUNSPECIFIED, err
}
posType := ToProtoPositionType(positionType)
req := &pb.GetPositionRequest{Name: c.name, PositionType: &posType, Extra: ext}
resp, err := c.client.GetPosition(ctx, req)
if err != nil {
return 0, PositionTypeUNSPECIFIED, err
}
posType1 := ToEncoderPositionType(&resp.PositionType)
return float64(resp.Value), posType1, nil
}

// ResetPosition sets the current position of
// the encoder to be its new zero position.
func (c *client) ResetPosition(ctx context.Context, extra map[string]interface{}) error {
ext, err := structpb.NewStruct(extra)
if err != nil {
return err
}
req := &pb.ResetPositionRequest{Name: c.name, Extra: ext}
_, err = c.client.ResetPosition(ctx, req)
return err
}

// GetProperties returns a list of all the position types that are supported by a given encoder.
func (c *client) GetProperties(ctx context.Context, extra map[string]interface{}) (map[Feature]bool, error) {
ext, err := structpb.NewStruct(extra)
if err != nil {
return nil, err
}
req := &pb.GetPropertiesRequest{Name: c.name, Extra: ext}
resp, err := c.client.GetProperties(ctx, req)
if err != nil {
return nil, err
}
return ProtoFeaturesToMap(resp), nil
}

func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) {
return protoutils.DoFromResourceClient(ctx, c.client, c.name, cmd)
}
Loading