Skip to content

Commit

Permalink
RSDK-7152 Analog Reader API rdk changes (#3946)
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviamiller authored May 16, 2024
1 parent 18d6caf commit 1f3cbd0
Show file tree
Hide file tree
Showing 22 changed files with 215 additions and 86 deletions.
13 changes: 12 additions & 1 deletion components/board/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,23 @@ type Board interface {
// An Analog represents an analog pin that resides on a board.
type Analog interface {
// Read reads off the current value.
Read(ctx context.Context, extra map[string]interface{}) (int, error)
Read(ctx context.Context, extra map[string]interface{}) (AnalogValue, error)

// Write writes a value to the analog pin.
Write(ctx context.Context, value int, extra map[string]interface{}) error
}

// AnalogValue contains all info about the analog reading.
// Value represents the reading in bits.
// Min and Max represent the range of raw analog values.
// StepSize is the precision per bit of the reading.
type AnalogValue struct {
Value int
Min float32
Max float32
StepSize float32
}

// FromDependencies is a helper for getting the named board from a collection of
// dependencies.
func FromDependencies(deps resource.Dependencies, name string) (Board, error) {
Expand Down
8 changes: 4 additions & 4 deletions components/board/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,10 @@ type analogClient struct {
analogName string
}

func (ac *analogClient) Read(ctx context.Context, extra map[string]interface{}) (int, error) {
func (ac *analogClient) Read(ctx context.Context, extra map[string]interface{}) (AnalogValue, error) {
ext, err := protoutils.StructToStructPb(extra)
if err != nil {
return 0, err
return AnalogValue{}, err
}
// the api method is named ReadAnalogReader, it is named differently than
// the board interface functions.
Expand All @@ -147,9 +147,9 @@ func (ac *analogClient) Read(ctx context.Context, extra map[string]interface{})
Extra: ext,
})
if err != nil {
return 0, err
return AnalogValue{}, err
}
return int(resp.Value), nil
return AnalogValue{Value: int(resp.Value), Min: resp.MinRange, Max: resp.MaxRange, StepSize: resp.StepSize}, nil
}

func (ac *analogClient) Write(ctx context.Context, value int, extra map[string]interface{}) error {
Expand Down
11 changes: 7 additions & 4 deletions components/board/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,16 @@ func TestWorkingClient(t *testing.T) {
test.That(t, injectBoard.AnalogByNameCap(), test.ShouldResemble, []interface{}{"analog1"})

// Analog: Read
injectAnalog.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) {
injectAnalog.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (board.AnalogValue, error) {
actualExtra = extra
return 6, nil
return board.AnalogValue{Value: 6, Min: 0, Max: 10, StepSize: 0.1}, nil
}
readVal, err := analog1.Read(context.Background(), expectedExtra)
analogVal, err := analog1.Read(context.Background(), expectedExtra)
test.That(t, err, test.ShouldBeNil)
test.That(t, readVal, test.ShouldEqual, 6)
test.That(t, analogVal.Value, test.ShouldEqual, 6)
test.That(t, analogVal.Min, test.ShouldEqual, 0)
test.That(t, analogVal.Max, test.ShouldEqual, 10)
test.That(t, analogVal.StepSize, test.ShouldEqual, 0.1)
test.That(t, actualExtra, test.ShouldResemble, expectedExtra)
actualExtra = nil

Expand Down
9 changes: 6 additions & 3 deletions components/board/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ func newAnalogCollector(resource interface{}, params data.CollectorParams) (data
}

cFunc := data.CaptureFunc(func(ctx context.Context, arg map[string]*anypb.Any) (interface{}, error) {
var value int
var analogValue AnalogValue
if _, ok := arg[analogReaderNameKey]; !ok {
return nil, data.FailedToReadErr(params.ComponentName, analogs.String(),
errors.New("Must supply reader_name in additional_params for analog collector"))
}
if reader, err := board.AnalogByName(arg[analogReaderNameKey].String()); err == nil {
value, err = reader.Read(ctx, data.FromDMExtraMap)
analogValue, err = reader.Read(ctx, data.FromDMExtraMap)
if err != nil {
// A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore
// is used in the datamanager to exclude readings from being captured and stored.
Expand All @@ -57,7 +57,10 @@ func newAnalogCollector(resource interface{}, params data.CollectorParams) (data
}
}
return pb.ReadAnalogReaderResponse{
Value: int32(value),
Value: int32(analogValue.Value),
MinRange: analogValue.Min,
MaxRange: analogValue.Max,
StepSize: analogValue.StepSize,
}, nil
})
return data.NewCollector(cFunc, params)
Expand Down
9 changes: 6 additions & 3 deletions components/board/collectors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ func TestCollectors(t *testing.T) {
},
collector: board.NewAnalogCollector,
expected: tu.ToProtoMapIgnoreOmitEmpty(pb.ReadAnalogReaderResponse{
Value: 1,
Value: 1,
MinRange: 0,
MaxRange: 10,
StepSize: 0.1,
}),
shouldError: false,
},
Expand Down Expand Up @@ -96,8 +99,8 @@ func TestCollectors(t *testing.T) {
func newBoard() board.Board {
b := &inject.Board{}
analog := &inject.Analog{}
analog.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) {
return 1, nil
analog.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (board.AnalogValue, error) {
return board.AnalogValue{Value: 1, Min: 0, Max: 10, StepSize: 0.1}, nil
}
b.AnalogByNameFunc = func(name string) (board.Analog, error) {
return analog, nil
Expand Down
7 changes: 4 additions & 3 deletions components/board/fake/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,14 +289,15 @@ func (a *Analog) reset(pin string) {
a.Mu.Unlock()
}

func (a *Analog) Read(ctx context.Context, extra map[string]interface{}) (int, error) {
func (a *Analog) Read(ctx context.Context, extra map[string]interface{}) (board.AnalogValue, error) {
a.Mu.RLock()
defer a.Mu.RUnlock()
if a.pin != analogTestPin {
a.Value = a.fakeValue
a.fakeValue++
a.fakeValue %= 1001
a.Value = a.fakeValue
}
return a.Value, nil
return board.AnalogValue{Value: a.Value, Min: 0, Max: 1000, StepSize: 1}, nil
}

func (a *Analog) Write(ctx context.Context, value int, extra map[string]interface{}) error {
Expand Down
4 changes: 2 additions & 2 deletions components/board/genericlinux/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,11 +351,11 @@ func newWrappedAnalogReader(ctx context.Context, chipSelect string, reader *pinw
return &wrapped
}

func (a *wrappedAnalogReader) Read(ctx context.Context, extra map[string]interface{}) (int, error) {
func (a *wrappedAnalogReader) Read(ctx context.Context, extra map[string]interface{}) (board.AnalogValue, error) {
a.mu.RLock()
defer a.mu.RUnlock()
if a.reader == nil {
return 0, errors.New("closed")
return board.AnalogValue{}, errors.New("closed")
}
return a.reader.Read(ctx, extra)
}
Expand Down
12 changes: 8 additions & 4 deletions components/board/mcp3008helper/mcp3008.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"go.uber.org/multierr"

"go.viam.com/rdk/components/board"
"go.viam.com/rdk/components/board/genericlinux/buses"
"go.viam.com/rdk/grpc"
"go.viam.com/rdk/resource"
Expand Down Expand Up @@ -37,29 +38,32 @@ func (config *MCP3008AnalogConfig) Validate(path string) error {
return nil
}

func (mar *MCP3008AnalogReader) Read(ctx context.Context, extra map[string]interface{}) (value int, err error) {
func (mar *MCP3008AnalogReader) Read(ctx context.Context, extra map[string]interface{}) (
analogVal board.AnalogValue, err error,
) {
var tx [3]byte
tx[0] = 1 // start bit
tx[1] = byte((8 + mar.Channel) << 4) // single-ended
tx[2] = 0 // extra clocks to receive full 10 bits of data

bus, err := mar.Bus.OpenHandle()
if err != nil {
return 0, err
return board.AnalogValue{}, err
}
defer func() {
err = multierr.Combine(err, bus.Close())
}()

rx, err := bus.Xfer(ctx, 1000000, mar.Chip, 0, tx[:])
if err != nil {
return 0, err
return board.AnalogValue{}, err
}
// Reassemble the 10-bit value. Do not include bits before the final 10, because they contain
// garbage and might be non-zero.
val := 0x03FF & ((int(rx[1]) << 8) | int(rx[2]))

return val, nil
// returning no analog range since mcp3008 will be removed soon.
return board.AnalogValue{Value: val}, nil
}

// Close does nothing.
Expand Down
70 changes: 61 additions & 9 deletions components/board/numato/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ type numatoBoard struct {
sent map[string]bool
sentMu sync.Mutex
workers rdkutils.StoppableWorkers

maxAnalogVoltage float32
stepSize float32
}

func (b *numatoBoard) addToSent(msg string) {
Expand Down Expand Up @@ -354,12 +357,18 @@ type analog struct {
pin string
}

func (a *analog) Read(ctx context.Context, extra map[string]interface{}) (int, error) {
// Read returns the analog value with the range and step size in V/bit.
func (a *analog) Read(ctx context.Context, extra map[string]interface{}) (board.AnalogValue, error) {
res, err := a.b.doSendReceive(ctx, fmt.Sprintf("adc read %s", a.pin))
if err != nil {
return 0, err
return board.AnalogValue{}, err
}
reading, err := strconv.Atoi(res)
if err != nil {
return board.AnalogValue{}, err
}
return strconv.Atoi(res)

return board.AnalogValue{Value: reading, Min: 0, Max: a.b.maxAnalogVoltage, StepSize: a.b.stepSize}, nil
}

func (a *analog) Write(ctx context.Context, value int, extra map[string]interface{}) error {
Expand All @@ -384,6 +393,49 @@ func connect(ctx context.Context, name resource.Name, conf *Config, logger loggi
path = devs[0].Path
}

// Find the numato board's productid
products := getSerialDevices()

var productID int
for _, product := range products {
if product.ID.Vendor != 0x2a19 {
continue
}
// we can safely get the first numato productID we find because
// we only support one board being used at a time
productID = product.ID.Product
break
}

// Find the max analog voltage and stepSize based on the productID.
var max float32
var stepSize float32
switch productID {
case 0x800:
// 8 and 16 pin usb versions have the same product ID but different voltage ranges
// both have 10 bit resolution
if conf.Pins == 8 {
max = 5.0
} else if conf.Pins == 16 {
max = 3.3
}
stepSize = max / 1024
case 0x802:
// 32 channel usb numato has 10 bit resolution
max = 3.3
stepSize = max / 1024
case 0x805:
// 128 channel usb numato has 12 bit resolution
max = 3.3
stepSize = max / 4096
case 0xC05:
// 1 channel usb relay module numato - 10 bit resolution
max = 5.0
stepSize = max / 1024
default:
logger.Warnf("analog range and step size are not supported for numato with product id %d", productID)
}

options := goserial.OpenOptions{
PortName: path,
BaudRate: 19200,
Expand All @@ -396,12 +448,13 @@ func connect(ctx context.Context, name resource.Name, conf *Config, logger loggi
if err != nil {
return nil, err
}

b := &numatoBoard{
Named: name.AsNamed(),
pins: pins,
port: device,
logger: logger,
Named: name.AsNamed(),
pins: pins,
port: device,
logger: logger,
maxAnalogVoltage: max,
stepSize: stepSize,
}

b.analogs = map[string]*pinwrappers.AnalogSmoother{}
Expand All @@ -419,6 +472,5 @@ func connect(ctx context.Context, name resource.Name, conf *Config, logger loggi
return nil, multierr.Combine(b.Close(ctx), err)
}
b.logger.CDebugw(ctx, "numato startup", "version", ver)

return b, nil
}
10 changes: 7 additions & 3 deletions components/board/numato/board_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,25 @@ func TestNumato1(t *testing.T) {

res2, err := ar.Read(ctx, nil)
test.That(t, err, test.ShouldBeNil)
test.That(t, res2, test.ShouldBeLessThan, 100)
test.That(t, res2.Value, test.ShouldBeLessThan, 100)

// Only testing these since the values depend on what board version is connected.
test.That(t, res2.Min, test.ShouldEqual, 0)
test.That(t, res2.Max, test.ShouldBeGreaterThanOrEqualTo, 3.3)

err = zeroPin.Set(context.Background(), true, nil)
test.That(t, err, test.ShouldBeNil)

res2, err = ar.Read(ctx, nil)
test.That(t, err, test.ShouldBeNil)
test.That(t, res2, test.ShouldBeGreaterThan, 1000)
test.That(t, res2.Value, test.ShouldBeGreaterThan, 1000)

err = zeroPin.Set(context.Background(), false, nil)
test.That(t, err, test.ShouldBeNil)

res2, err = ar.Read(ctx, nil)
test.That(t, err, test.ShouldBeNil)
test.That(t, res2, test.ShouldBeLessThan, 100)
test.That(t, res2.Value, test.ShouldBeLessThan, 100)
}

func TestConfigValidate(t *testing.T) {
Expand Down
13 changes: 13 additions & 0 deletions components/board/numato/darwin_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build darwin

package numato

import "go.viam.com/utils/usb"

// getSerialDevices returns all device descriptions connected by USB on mac. This is used to get the
// productID of the numato board being used.
func getSerialDevices() []usb.Description {
return usb.Search(usb.NewSearchFilter("AppleUSBACMData", "usbmodem"), func(vendorID, productID int) bool {
return true
})
}
13 changes: 13 additions & 0 deletions components/board/numato/linux_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build linux

package numato

import "go.viam.com/utils/usb"

// getSerialDevices returns all devices connected by USB on a linux machine.
// This is used to get the productID of the numato board being used.
func getSerialDevices() []usb.Description {
return usb.Search(usb.SearchFilter{}, func(vendorID, productID int) bool {
return true
})
}
6 changes: 3 additions & 3 deletions components/board/pi/impl/external_hardware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,23 @@ func TestPiHardware(t *testing.T) {

v, err := reader.Read(ctx, nil)
test.That(t, err, test.ShouldBeNil)
test.That(t, v, test.ShouldAlmostEqual, 0, 150)
test.That(t, v.Value, test.ShouldAlmostEqual, 0, 150)

// try to set high
err = p.SetGPIOBcom(26, true)
test.That(t, err, test.ShouldBeNil)

v, err = reader.Read(ctx, nil)
test.That(t, err, test.ShouldBeNil)
test.That(t, v, test.ShouldAlmostEqual, 1023, 150)
test.That(t, v.Value, test.ShouldAlmostEqual, 1023, 150)

// back to low
err = p.SetGPIOBcom(26, false)
test.That(t, err, test.ShouldBeNil)

v, err = reader.Read(ctx, nil)
test.That(t, err, test.ShouldBeNil)
test.That(t, v, test.ShouldAlmostEqual, 0, 150)
test.That(t, v.Value, test.ShouldAlmostEqual, 0, 150)
})

t.Run("basic interrupts", func(t *testing.T) {
Expand Down
Loading

0 comments on commit 1f3cbd0

Please sign in to comment.