From 3b3ad4df0ecb284c14b53c42ba54e3761bc9d294 Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Thu, 24 Mar 2022 18:40:26 +0100 Subject: [PATCH] add AnalogActuatorDriver, analog temperature sensor, driver for PCF8591 (with 400kbit stabilization), driver for YL-40 --- drivers/aio/README.md | 2 + drivers/aio/aio.go | 12 +- drivers/aio/analog_actuator_driver.go | 111 +++++ drivers/aio/analog_actuator_driver_test.go | 105 +++++ drivers/aio/analog_sensor_driver.go | 85 +++- drivers/aio/analog_sensor_driver_test.go | 67 ++- .../aio/grove_temperature_sensor_driver.go | 97 +---- .../grove_temperature_sensor_driver_test.go | 83 ++-- drivers/aio/helpers_test.go | 29 +- drivers/aio/temperature_sensor_driver.go | 127 ++++++ drivers/aio/temperature_sensor_driver_test.go | 214 ++++++++++ drivers/i2c/README.md | 2 + drivers/i2c/pcf8591_driver.go | 388 ++++++++++++++++++ drivers/i2c/pcf8591_driver_test.go | 165 ++++++++ drivers/i2c/yl40_driver.go | 324 +++++++++++++++ drivers/i2c/yl40_driver_test.go | 259 ++++++++++++ examples/tinkerboard_pcf8591.go | 91 ++++ examples/tinkerboard_yl40.go | 76 ++++ 18 files changed, 2088 insertions(+), 149 deletions(-) create mode 100644 drivers/aio/analog_actuator_driver.go create mode 100644 drivers/aio/analog_actuator_driver_test.go create mode 100644 drivers/aio/temperature_sensor_driver.go create mode 100644 drivers/aio/temperature_sensor_driver_test.go create mode 100644 drivers/i2c/pcf8591_driver.go create mode 100644 drivers/i2c/pcf8591_driver_test.go create mode 100644 drivers/i2c/yl40_driver.go create mode 100644 drivers/i2c/yl40_driver_test.go create mode 100644 examples/tinkerboard_pcf8591.go create mode 100644 examples/tinkerboard_yl40.go diff --git a/drivers/aio/README.md b/drivers/aio/README.md index 146fffc76..3123cfdd7 100644 --- a/drivers/aio/README.md +++ b/drivers/aio/README.md @@ -12,9 +12,11 @@ go get -d -u gobot.io/x/gobot/... ## Hardware Support Gobot has a extensible system for connecting to hardware devices. The following AIO devices are currently supported: - Analog Sensor + - Analog Actuator - Grove Light Sensor - Grove Rotary Dial - Grove Sound Sensor - Grove Temperature Sensor + - Temperature Sensor (supports linear and NTC thermistor in normal and inverse mode) More drivers are coming soon... diff --git a/drivers/aio/aio.go b/drivers/aio/aio.go index 12fc03a90..4b8bd9638 100644 --- a/drivers/aio/aio.go +++ b/drivers/aio/aio.go @@ -15,12 +15,20 @@ const ( Error = "error" // Data event Data = "data" + // Value event + Value = "value" // Vibration event Vibration = "vibration" ) -// AnalogReader interface represents an Adaptor which has Analog capabilities +// AnalogReader interface represents an Adaptor which has AnalogRead capabilities type AnalogReader interface { //gobot.Adaptor - AnalogRead(string) (val int, err error) + AnalogRead(pin string) (val int, err error) +} + +// AnalogWriter interface represents an Adaptor which has AnalogWrite capabilities +type AnalogWriter interface { + //gobot.Adaptor + AnalogWrite(pin string, val int) (err error) } diff --git a/drivers/aio/analog_actuator_driver.go b/drivers/aio/analog_actuator_driver.go new file mode 100644 index 000000000..0cc615c9a --- /dev/null +++ b/drivers/aio/analog_actuator_driver.go @@ -0,0 +1,111 @@ +package aio + +import ( + "strconv" + + "gobot.io/x/gobot" +) + +// AnalogActuatorDriver represents an analog actuator +type AnalogActuatorDriver struct { + name string + pin string + connection AnalogWriter + gobot.Eventer + gobot.Commander + scale func(input float64) (value int) + lastValue float64 + lastRawValue int +} + +// NewAnalogActuatorDriver returns a new AnalogActuatorDriver given by an AnalogWriter and pin. +// The driver supports customizable scaling from given float64 value to written int. +// The default scaling is 1:1. An adjustable linear scaler is provided by the driver. +// +// Adds the following API Commands: +// "Write" - See AnalogActuator.Write +// "RawWrite" - See AnalogActuator.RawWrite +func NewAnalogActuatorDriver(a AnalogWriter, pin string) *AnalogActuatorDriver { + d := &AnalogActuatorDriver{ + name: gobot.DefaultName("AnalogActuator"), + connection: a, + pin: pin, + Commander: gobot.NewCommander(), + scale: func(input float64) (value int) { return int(input) }, + } + + d.AddCommand("Write", func(params map[string]interface{}) interface{} { + val, err := strconv.ParseFloat(params["val"].(string), 64) + if err != nil { + return err + } + return d.Write(val) + }) + + d.AddCommand("RawWrite", func(params map[string]interface{}) interface{} { + val, _ := strconv.Atoi(params["val"].(string)) + return d.RawWrite(val) + }) + + return d +} + +// Start starts driver +func (a *AnalogActuatorDriver) Start() (err error) { return } + +// Halt is for halt +func (a *AnalogActuatorDriver) Halt() (err error) { return } + +// Name returns the drivers name +func (a *AnalogActuatorDriver) Name() string { return a.name } + +// SetName sets the drivers name +func (a *AnalogActuatorDriver) SetName(n string) { a.name = n } + +// Pin returns the drivers pin +func (a *AnalogActuatorDriver) Pin() string { return a.pin } + +// Connection returns the drivers Connection +func (a *AnalogActuatorDriver) Connection() gobot.Connection { return a.connection.(gobot.Connection) } + +// RawWrite write the given raw value to the actuator +func (a *AnalogActuatorDriver) RawWrite(val int) (err error) { + a.lastRawValue = val + return a.connection.AnalogWrite(a.Pin(), val) +} + +// SetScaler substitute the default 1:1 return value function by a new scaling function +func (a *AnalogActuatorDriver) SetScaler(scaler func(float64) int) { + a.scale = scaler +} + +// Write writes the given value to the actuator +func (a *AnalogActuatorDriver) Write(val float64) (err error) { + a.lastValue = val + rawValue := a.scale(val) + return a.RawWrite(rawValue) +} + +// RawValue returns the last written raw value +func (a *AnalogActuatorDriver) RawValue() (val int) { + return a.lastRawValue +} + +// Value returns the last written value +func (a *AnalogActuatorDriver) Value() (val float64) { + return a.lastValue +} + +func AnalogActuatorLinearScaler(fromMin, fromMax float64, toMin, toMax int) func(input float64) (value int) { + m := float64(toMax-toMin) / (fromMax - fromMin) + n := float64(toMin) - m*fromMin + return func(input float64) (value int) { + if input <= fromMin { + return toMin + } + if input >= fromMax { + return toMax + } + return int(input*m + n) + } +} diff --git a/drivers/aio/analog_actuator_driver_test.go b/drivers/aio/analog_actuator_driver_test.go new file mode 100644 index 000000000..06bfe39c0 --- /dev/null +++ b/drivers/aio/analog_actuator_driver_test.go @@ -0,0 +1,105 @@ +package aio + +import ( + "strings" + "testing" + + "gobot.io/x/gobot/gobottest" +) + +func TestAnalogActuatorDriver(t *testing.T) { + a := newAioTestAdaptor() + d := NewAnalogActuatorDriver(a, "47") + + gobottest.Refute(t, d.Connection(), nil) + gobottest.Assert(t, d.Pin(), "47") + + err := d.RawWrite(100) + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], 100) + + err = d.Write(247.0) + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(a.written), 2) + gobottest.Assert(t, a.written[1], 247) + gobottest.Assert(t, d.RawValue(), 247) + gobottest.Assert(t, d.Value(), 247.0) +} + +func TestAnalogActuatorDriverWithScaler(t *testing.T) { + // commands + a := newAioTestAdaptor() + d := NewAnalogActuatorDriver(a, "7") + d.SetScaler(func(input float64) int { return int((input + 3) / 2.5) }) + + err := d.Command("RawWrite")(map[string]interface{}{"val": "100"}) + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], 100) + + err = d.Command("Write")(map[string]interface{}{"val": "247.0"}) + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(a.written), 2) + gobottest.Assert(t, a.written[1], 100) +} + +func TestAnalogActuatorDriverLinearScaler(t *testing.T) { + var tests = map[string]struct { + fromMin float64 + fromMax float64 + input float64 + want int + }{ + "byte_range_min": {fromMin: 0, fromMax: 255, input: 0, want: 0}, + "byte_range_max": {fromMin: 0, fromMax: 255, input: 255, want: 255}, + "signed_percent_range_min": {fromMin: -100, fromMax: 100, input: -100, want: 0}, + "signed_percent_range_mid": {fromMin: -100, fromMax: 100, input: 0, want: 127}, + "signed_percent_range_max": {fromMin: -100, fromMax: 100, input: 100, want: 255}, + "voltage_range_min": {fromMin: 0, fromMax: 5.1, input: 0, want: 0}, + "voltage_range_nearmin": {fromMin: 0, fromMax: 5.1, input: 0.02, want: 1}, + "voltage_range_mid": {fromMin: 0, fromMax: 5.1, input: 2.55, want: 127}, + "voltage_range_nearmax": {fromMin: 0, fromMax: 5.1, input: 5.08, want: 254}, + "voltage_range_max": {fromMin: 0, fromMax: 5.1, input: 5.1, want: 255}, + "upscale": {fromMin: 0, fromMax: 24, input: 12, want: 127}, + "below_min": {fromMin: -10, fromMax: 10, input: -11, want: 0}, + "exceed_max": {fromMin: 0, fromMax: 20, input: 21, want: 255}, + } + a := newAioTestAdaptor() + d := NewAnalogActuatorDriver(a, "7") + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d.SetScaler(AnalogActuatorLinearScaler(tt.fromMin, tt.fromMax, 0, 255)) + a.written = []int{} // reset previous write + // act + err := d.Write(tt.input) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], tt.want) + }) + } +} + +func TestAnalogActuatorDriverStart(t *testing.T) { + d := NewAnalogActuatorDriver(newAioTestAdaptor(), "1") + gobottest.Assert(t, d.Start(), nil) +} + +func TestAnalogActuatorDriverHalt(t *testing.T) { + d := NewAnalogActuatorDriver(newAioTestAdaptor(), "1") + gobottest.Assert(t, d.Halt(), nil) +} + +func TestAnalogActuatorDriverDefaultName(t *testing.T) { + d := NewAnalogActuatorDriver(newAioTestAdaptor(), "1") + gobottest.Assert(t, strings.HasPrefix(d.Name(), "AnalogActuator"), true) +} + +func TestAnalogActuatorDriverSetName(t *testing.T) { + d := NewAnalogActuatorDriver(newAioTestAdaptor(), "1") + d.SetName("mybot") + gobottest.Assert(t, d.Name(), "mybot") +} diff --git a/drivers/aio/analog_sensor_driver.go b/drivers/aio/analog_sensor_driver.go index 4221c488d..1d4df4116 100644 --- a/drivers/aio/analog_sensor_driver.go +++ b/drivers/aio/analog_sensor_driver.go @@ -15,16 +15,22 @@ type AnalogSensorDriver struct { connection AnalogReader gobot.Eventer gobot.Commander + rawValue int + value float64 + scale func(input int) (value float64) } // NewAnalogSensorDriver returns a new AnalogSensorDriver with a polling interval of // 10 Milliseconds given an AnalogReader and pin. +// The driver supports customizable scaling from read int value to returned float64. +// The default scaling is 1:1. An adjustable linear scaler is provided by the driver. // // Optionally accepts: // time.Duration: Interval at which the AnalogSensor is polled for new information // // Adds the following API Commands: -// "Read" - See AnalogSensor.Read +// "Read" - See AnalogDriverSensor.Read +// "ReadValue" - See AnalogDriverSensor.ReadValue func NewAnalogSensorDriver(a AnalogReader, pin string, v ...time.Duration) *AnalogSensorDriver { d := &AnalogSensorDriver{ name: gobot.DefaultName("AnalogSensor"), @@ -34,6 +40,7 @@ func NewAnalogSensorDriver(a AnalogReader, pin string, v ...time.Duration) *Anal Commander: gobot.NewCommander(), interval: 10 * time.Millisecond, halt: make(chan bool), + scale: func(input int) (value float64) { return float64(input) }, } if len(v) > 0 { @@ -41,6 +48,7 @@ func NewAnalogSensorDriver(a AnalogReader, pin string, v ...time.Duration) *Anal } d.AddEvent(Data) + d.AddEvent(Value) d.AddEvent(Error) d.AddCommand("Read", func(params map[string]interface{}) interface{} { @@ -48,25 +56,42 @@ func NewAnalogSensorDriver(a AnalogReader, pin string, v ...time.Duration) *Anal return map[string]interface{}{"val": val, "err": err} }) + d.AddCommand("ReadValue", func(params map[string]interface{}) interface{} { + val, err := d.ReadValue() + return map[string]interface{}{"val": val, "err": err} + }) + return d } -// Start starts the AnalogSensorDriver and reads the Analog Sensor at the given interval. +// Start starts the AnalogSensorDriver and reads the sensor at the given interval. // Emits the Events: -// Data int - Event is emitted on change and represents the current reading from the sensor. +// Data int - Event is emitted on change and represents the current raw reading from the sensor. +// Value float64 - Event is emitted on change and represents the current reading from the sensor. // Error error - Event is emitted on error reading from the sensor. func (a *AnalogSensorDriver) Start() (err error) { - var value int = 0 + if a.interval == 0 { + // cyclic reading deactivated + return + } + var oldRawValue = 0 + var oldValue = 0.0 go func() { timer := time.NewTimer(a.interval) timer.Stop() for { - newValue, err := a.Read() + _, err := a.ReadValue() if err != nil { a.Publish(a.Event(Error), err) - } else if newValue != value && newValue != -1 { - value = newValue - a.Publish(a.Event(Data), value) + } else { + if a.rawValue != oldRawValue && a.rawValue != -1 { + a.Publish(a.Event(Data), a.rawValue) + oldRawValue = a.rawValue + } + if a.value != oldValue && a.value != -1 { + a.Publish(a.Event(Value), a.value) + oldValue = a.value + } } timer.Reset(a.interval) @@ -83,6 +108,10 @@ func (a *AnalogSensorDriver) Start() (err error) { // Halt stops polling the analog sensor for new information func (a *AnalogSensorDriver) Halt() (err error) { + if a.interval == 0 { + // cyclic reading deactivated + return + } a.halt <- true return } @@ -99,7 +128,45 @@ func (a *AnalogSensorDriver) Pin() string { return a.pin } // Connection returns the AnalogSensorDrivers Connection func (a *AnalogSensorDriver) Connection() gobot.Connection { return a.connection.(gobot.Connection) } -// Read returns the current reading from the Analog Sensor +// Read returns the current reading from the sensor without scaling func (a *AnalogSensorDriver) Read() (val int, err error) { return a.connection.AnalogRead(a.Pin()) } + +// SetScaler substitute the default 1:1 return value function by a new scaling function +func (a *AnalogSensorDriver) SetScaler(scaler func(int) float64) { + a.scale = scaler +} + +// ReadValue returns the current reading from the sensor +func (a *AnalogSensorDriver) ReadValue() (val float64, err error) { + if a.rawValue, err = a.Read(); err != nil { + return + } + a.value = a.scale(a.rawValue) + return a.value, nil +} + +// Value returns the last read value from the sensor +func (a *AnalogSensorDriver) Value() float64 { + return a.value +} + +// RawValue returns the last read raw value from the sensor +func (a *AnalogSensorDriver) RawValue() int { + return a.rawValue +} + +func AnalogSensorLinearScaler(fromMin, fromMax int, toMin, toMax float64) func(input int) (value float64) { + m := (toMax - toMin) / float64(fromMax-fromMin) + n := toMin - m*float64(fromMin) + return func(input int) (value float64) { + if input <= fromMin { + return toMin + } + if input >= fromMax { + return toMax + } + return float64(input)*m + n + } +} diff --git a/drivers/aio/analog_sensor_driver_test.go b/drivers/aio/analog_sensor_driver_test.go index eed5da6f6..942efcfc2 100644 --- a/drivers/aio/analog_sensor_driver_test.go +++ b/drivers/aio/analog_sensor_driver_test.go @@ -20,8 +20,10 @@ func TestAnalogSensorDriver(t *testing.T) { // default interval gobottest.Assert(t, d.interval, 10*time.Millisecond) + // commands a = newAioTestAdaptor() d = NewAnalogSensorDriver(a, "42", 30*time.Second) + d.SetScaler(func(input int) float64 { return 2.5*float64(input) - 3 }) gobottest.Assert(t, d.Pin(), "42") gobottest.Assert(t, d.interval, 30*time.Second) @@ -30,15 +32,69 @@ func TestAnalogSensorDriver(t *testing.T) { return }) ret := d.Command("Read")(nil).(map[string]interface{}) - gobottest.Assert(t, ret["val"].(int), 100) gobottest.Assert(t, ret["err"], nil) + + ret = d.Command("ReadValue")(nil).(map[string]interface{}) + gobottest.Assert(t, ret["val"].(float64), 247.0) + gobottest.Assert(t, ret["err"], nil) + + // refresh value on read + a = newAioTestAdaptor() + d = NewAnalogSensorDriver(a, "3") + a.TestAdaptorAnalogRead(func() (val int, err error) { + val = 150 + return + }) + gobottest.Assert(t, d.Value(), 0.0) + val, err := d.ReadValue() + gobottest.Assert(t, err, nil) + gobottest.Assert(t, val, 150.0) + gobottest.Assert(t, d.Value(), 150.0) + gobottest.Assert(t, d.RawValue(), 150) +} + +func TestAnalogSensorDriverWithLinearScaler(t *testing.T) { + // the input scales per default from 0...255 + var tests = map[string]struct { + toMin float64 + toMax float64 + input int + want float64 + }{ + "single_byte_range_min": {toMin: 0, toMax: 255, input: 0, want: 0}, + "single_byte_range_max": {toMin: 0, toMax: 255, input: 255, want: 255}, + "single_below_min": {toMin: 3, toMax: 121, input: -1, want: 3}, + "single_is_max": {toMin: 5, toMax: 6, input: 255, want: 6}, + "single_upscale": {toMin: 337, toMax: 5337, input: 127, want: 2827.196078431373}, + "grd_int_range_min": {toMin: -180, toMax: 180, input: 0, want: -180}, + "grd_int_range_minus_one": {toMin: -180, toMax: 180, input: 127, want: -0.7058823529411598}, + "grd_int_range_max": {toMin: -180, toMax: 180, input: 255, want: 180}, + "upscale": {toMin: -10, toMax: 1234, input: 255, want: 1234}, + } + a := newAioTestAdaptor() + d := NewAnalogSensorDriver(a, "7") + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d.SetScaler(AnalogSensorLinearScaler(0, 255, tt.toMin, tt.toMax)) + a.TestAdaptorAnalogRead(func() (val int, err error) { + return tt.input, nil + }) + // act + got, err := d.ReadValue() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, got, tt.want) + }) + } } func TestAnalogSensorDriverStart(t *testing.T) { sem := make(chan bool, 1) a := newAioTestAdaptor() d := NewAnalogSensorDriver(a, "1") + d.SetScaler(func(input int) float64 { return float64(input * input) }) // expect data to be received d.Once(d.Event(Data), func(data interface{}) { @@ -46,6 +102,11 @@ func TestAnalogSensorDriverStart(t *testing.T) { sem <- true }) + d.Once(d.Event(Value), func(data interface{}) { + gobottest.Assert(t, data.(float64), 10000.0) + sem <- true + }) + // send data a.TestAdaptorAnalogRead(func() (val int, err error) { val = 100 @@ -83,6 +144,10 @@ func TestAnalogSensorDriverStart(t *testing.T) { sem <- true }) + d.Once(d.Event(Value), func(data interface{}) { + sem <- true + }) + a.TestAdaptorAnalogRead(func() (val int, err error) { val = 200 return diff --git a/drivers/aio/grove_temperature_sensor_driver.go b/drivers/aio/grove_temperature_sensor_driver.go index 5beb73563..d52d8481a 100644 --- a/drivers/aio/grove_temperature_sensor_driver.go +++ b/drivers/aio/grove_temperature_sensor_driver.go @@ -1,7 +1,6 @@ package aio import ( - "math" "time" "gobot.io/x/gobot" @@ -9,103 +8,35 @@ import ( var _ gobot.Driver = (*GroveTemperatureSensorDriver)(nil) -// GroveTemperatureSensorDriver represents a Temperature Sensor +// GroveTemperatureSensorDriver represents a temperature sensor // The temperature is reported in degree Celsius type GroveTemperatureSensorDriver struct { - name string - pin string - halt chan bool - temperature float64 - interval time.Duration - connection AnalogReader - gobot.Eventer + *TemperatureSensorDriver } // NewGroveTemperatureSensorDriver returns a new GroveTemperatureSensorDriver with a polling interval of // 10 Milliseconds given an AnalogReader and pin. // // Optionally accepts: -// time.Duration: Interval at which the TemperatureSensor is polled for new information +// time.Duration: Interval at which the sensor is polled for new information (given 0 switch the polling off) // // Adds the following API Commands: -// "Read" - See AnalogSensor.Read +// "Read" - See AnalogDriverSensor.Read +// "ReadValue" - See AnalogDriverSensor.ReadValue func NewGroveTemperatureSensorDriver(a AnalogReader, pin string, v ...time.Duration) *GroveTemperatureSensorDriver { - d := &GroveTemperatureSensorDriver{ - name: gobot.DefaultName("GroveTemperatureSensor"), - connection: a, - pin: pin, - Eventer: gobot.NewEventer(), - interval: 10 * time.Millisecond, - halt: make(chan bool), - } + t := NewTemperatureSensorDriver(a, pin, v...) + ntc := TemperatureSensorNtcConf{TC0: 25, R0: 10000.0, B: 3975} //Ohm, R25=10k + t.SetNtcScaler(1023, 10000, false, ntc) //Ohm, reference value: 1023, series R: 10k - if len(v) > 0 { - d.interval = v[0] + d := &GroveTemperatureSensorDriver{ + TemperatureSensorDriver: t, } - - d.AddEvent(Data) - d.AddEvent(Error) + d.SetName(gobot.DefaultName("GroveTemperatureSensor")) return d } -// Start starts the GroveTemperatureSensorDriver and reads the Sensor at the given interval. -// Emits the Events: -// Data int - Event is emitted on change and represents the current temperature in celsius from the sensor. -// Error error - Event is emitted on error reading from the sensor. -func (a *GroveTemperatureSensorDriver) Start() (err error) { - thermistor := 3975.0 - a.temperature = 0 - - go func() { - for { - rawValue, err := a.Read() - - resistance := float64(1023.0-rawValue) * 10000 / float64(rawValue) - newValue := 1/(math.Log(resistance/10000.0)/thermistor+1/298.15) - 273.15 - - if err != nil { - a.Publish(Error, err) - } else if newValue != a.temperature && newValue != -1 { - a.temperature = newValue - a.Publish(Data, a.temperature) - } - select { - case <-time.After(a.interval): - case <-a.halt: - return - } - } - }() - return -} - -// Halt stops polling the analog sensor for new information -func (a *GroveTemperatureSensorDriver) Halt() (err error) { - a.halt <- true - return -} - -// Name returns the GroveTemperatureSensorDrivers name -func (a *GroveTemperatureSensorDriver) Name() string { return a.name } - -// SetName sets the GroveTemperatureSensorDrivers name -func (a *GroveTemperatureSensorDriver) SetName(n string) { a.name = n } - -// Pin returns the GroveTemperatureSensorDrivers pin -func (a *GroveTemperatureSensorDriver) Pin() string { return a.pin } - -// Connection returns the GroveTemperatureSensorDrivers Connection -func (a *GroveTemperatureSensorDriver) Connection() gobot.Connection { - return a.connection.(gobot.Connection) -} - -// Read returns the current Temperature from the Sensor -func (a *GroveTemperatureSensorDriver) Temperature() (val float64) { - return a.temperature -} - -// Read returns the raw reading from the Sensor -func (a *GroveTemperatureSensorDriver) Read() (val int, err error) { - return a.connection.AnalogRead(a.Pin()) +// Temperature returns the last read temperature from the sensor. +func (t *TemperatureSensorDriver) Temperature() (val float64) { + return t.Value() } diff --git a/drivers/aio/grove_temperature_sensor_driver_test.go b/drivers/aio/grove_temperature_sensor_driver_test.go index 379b8376a..2a093eadb 100644 --- a/drivers/aio/grove_temperature_sensor_driver_test.go +++ b/drivers/aio/grove_temperature_sensor_driver_test.go @@ -1,7 +1,6 @@ package aio import ( - "errors" "fmt" "strings" "testing" @@ -21,6 +20,39 @@ func TestGroveTemperatureSensorDriver(t *testing.T) { gobottest.Assert(t, d.interval, 10*time.Millisecond) } +func TestGroveTemperatureSensorDriverScaling(t *testing.T) { + var tests = map[string]struct { + input int + want float64 + }{ + "min": {input: 0, want: -273.15}, + "nearMin": {input: 1, want: -76.96736464322436}, + "T-25C": {input: 65, want: -25.064097201780044}, + "T0C": {input: 233, want: -0.014379114122164083}, + "T25C": {input: 511, want: 24.956285721537938}, + "585": {input: 585, want: 31.61532462352477}, + "nearMax": {input: 1022, want: 347.6819764792606}, + "max": {input: 1023, want: 347.77682140097613}, + "biggerThanMax": {input: 5000, want: 347.77682140097613}, + } + a := newAioTestAdaptor() + d := NewGroveTemperatureSensorDriver(a, "54") + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a.TestAdaptorAnalogRead(func() (val int, err error) { + val = tt.input + return + }) + // act + got, err := d.ReadValue() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, got, tt.want) + }) + } +} + func TestGroveTempSensorPublishesTemperatureInCelsius(t *testing.T) { sem := make(chan bool, 1) a := newAioTestAdaptor() @@ -30,7 +62,7 @@ func TestGroveTempSensorPublishesTemperatureInCelsius(t *testing.T) { val = 585 return }) - d.Once(d.Event(Data), func(data interface{}) { + d.Once(d.Event(Value), func(data interface{}) { gobottest.Assert(t, fmt.Sprintf("%.2f", data.(float64)), "31.62") sem <- true }) @@ -45,54 +77,7 @@ func TestGroveTempSensorPublishesTemperatureInCelsius(t *testing.T) { gobottest.Assert(t, d.Temperature(), 31.61532462352477) } -func TestGroveTempSensorPublishesError(t *testing.T) { - sem := make(chan bool, 1) - a := newAioTestAdaptor() - d := NewGroveTemperatureSensorDriver(a, "1") - - // send error - a.TestAdaptorAnalogRead(func() (val int, err error) { - err = errors.New("read error") - return - }) - - gobottest.Assert(t, d.Start(), nil) - - // expect error - d.Once(d.Event(Error), func(data interface{}) { - gobottest.Assert(t, data.(error).Error(), "read error") - sem <- true - }) - - select { - case <-sem: - case <-time.After(1 * time.Second): - t.Errorf("Grove Temperature Sensor Event \"Error\" was not published") - } -} - -func TestGroveTempSensorHalt(t *testing.T) { - d := NewGroveTemperatureSensorDriver(newAioTestAdaptor(), "1") - done := make(chan struct{}) - go func() { - <-d.halt - close(done) - }() - gobottest.Assert(t, d.Halt(), nil) - select { - case <-done: - case <-time.After(100 * time.Millisecond): - t.Errorf("Grove Temperature Sensor was not halted") - } -} - func TestGroveTempDriverDefaultName(t *testing.T) { d := NewGroveTemperatureSensorDriver(newAioTestAdaptor(), "1") gobottest.Assert(t, strings.HasPrefix(d.Name(), "GroveTemperatureSensor"), true) } - -func TestGroveTempDriverSetName(t *testing.T) { - d := NewGroveTemperatureSensorDriver(newAioTestAdaptor(), "1") - d.SetName("mybot") - gobottest.Assert(t, d.Name(), "mybot") -} diff --git a/drivers/aio/helpers_test.go b/drivers/aio/helpers_test.go index 06ebe92e9..9c0079376 100644 --- a/drivers/aio/helpers_test.go +++ b/drivers/aio/helpers_test.go @@ -10,10 +10,12 @@ func (t *aioTestBareAdaptor) Name() string { return "" } func (t *aioTestBareAdaptor) SetName(n string) {} type aioTestAdaptor struct { - name string - port string - mtx sync.Mutex - testAdaptorAnalogRead func() (val int, err error) + name string + port string + mtx sync.Mutex + testAdaptorAnalogRead func() (val int, err error) + testAdaptorAnalogWrite func(val int) (err error) + written []int } func (t *aioTestAdaptor) TestAdaptorAnalogRead(f func() (val int, err error)) { @@ -22,11 +24,25 @@ func (t *aioTestAdaptor) TestAdaptorAnalogRead(f func() (val int, err error)) { t.testAdaptorAnalogRead = f } -func (t *aioTestAdaptor) AnalogRead(string) (val int, err error) { +func (t *aioTestAdaptor) TestAdaptorAnalogWrite(f func(val int) (err error)) { + t.mtx.Lock() + defer t.mtx.Unlock() + t.testAdaptorAnalogWrite = f +} + +func (t *aioTestAdaptor) AnalogRead(pin string) (val int, err error) { t.mtx.Lock() defer t.mtx.Unlock() return t.testAdaptorAnalogRead() } + +func (t *aioTestAdaptor) AnalogWrite(pin string, val int) (err error) { + t.mtx.Lock() + defer t.mtx.Unlock() + t.written = append(t.written, val) + return t.testAdaptorAnalogWrite(val) +} + func (t *aioTestAdaptor) Connect() (err error) { return } func (t *aioTestAdaptor) Finalize() (err error) { return } func (t *aioTestAdaptor) Name() string { return t.name } @@ -39,5 +55,8 @@ func newAioTestAdaptor() *aioTestAdaptor { testAdaptorAnalogRead: func() (val int, err error) { return 99, nil }, + testAdaptorAnalogWrite: func(val int) (err error) { + return nil + }, } } diff --git a/drivers/aio/temperature_sensor_driver.go b/drivers/aio/temperature_sensor_driver.go new file mode 100644 index 000000000..6d17a54a5 --- /dev/null +++ b/drivers/aio/temperature_sensor_driver.go @@ -0,0 +1,127 @@ +package aio + +import ( + "math" + "time" + + "gobot.io/x/gobot" +) + +const kelvinOffset = 273.15 + +type TemperatureSensorNtcConf struct { + TC0 int // °C + R0 float64 // same unit as resistance of NTC (Ohm is recommended) + B float64 // 2000..5000K + TC1 int // used if B is not given, °C + R1 float64 // used if B is not given, same unit as R0 needed + t0 float64 + r float64 +} + +// TemperatureSensorDriver represents an Analog Sensor +type TemperatureSensorDriver struct { + *AnalogSensorDriver +} + +// NewTemperatureSensorDriver is a gobot driver for analog temperature sensors +// with a polling interval 10 Milliseconds given an AnalogReader and pin. +// For further details please refer to AnalogSensorDriver. +// Linear scaling and NTC scaling is supported. +// +// Optionally accepts: +// time.Duration: Interval at which the sensor is polled for new information (given 0 switch the polling off) +// +// Adds the following API Commands: +// "Read" - See AnalogDriverSensor.Read +// "ReadValue" - See AnalogDriverSensor.ReadValue +func NewTemperatureSensorDriver(a AnalogReader, pin string, v ...time.Duration) *TemperatureSensorDriver { + ad := NewAnalogSensorDriver(a, pin, v...) + + d := &TemperatureSensorDriver{AnalogSensorDriver: ad} + d.SetName(gobot.DefaultName("TemperatureSensor")) + + return d +} + +// SetNtcSaler sets a function for typical NTC scaling the read value. +// The read value is related to the voltage over the thermistor in an series connection to a resistor. +// If the thermistor is connected to ground, the reverse flag must be set to true. +// This means the voltage decreases when temperature gets higher. +// Currently no negative values are supported. +func (t *TemperatureSensorDriver) SetNtcScaler(vRef uint, rOhm uint, reverse bool, ntc TemperatureSensorNtcConf) { + t.SetScaler(TemperatureSensorNtcScaler(vRef, rOhm, reverse, ntc)) +} + +// SetLinearScaler sets a function for linear scaling the read value. +// This can be applied for some silicon based PTC sensors or e.g. PT100, +// and in a small temperature range also for NTC. +func (t *TemperatureSensorDriver) SetLinearScaler(fromMin, fromMax int, toMin, toMax float64) { + t.SetScaler(AnalogSensorLinearScaler(fromMin, fromMax, toMin, toMax)) +} + +func TemperatureSensorNtcScaler(vRef uint, rOhm uint, reverse bool, ntc TemperatureSensorNtcConf) func(input int) (value float64) { + ntc.initialize() + return (func(input int) (value float64) { + if input < 0 { + input = 0 + } + rTherm := temperaturSensorGetResistance(uint(input), vRef, rOhm, reverse) + temp := ntc.getTemp(rTherm) + return temp + }) +} + +// getResistance calculates the value of the series thermistor by given value +// and reference value (e.g. the voltage value over the complete series circuit) +// The unit of the returned thermistor value equals the given series resistor unit. +func temperaturSensorGetResistance(value uint, vRef uint, rSeries uint, reverse bool) float64 { + if value > vRef { + value = vRef + } + valDiff := vRef - value + if reverse { + // rSeries thermistor + // vRef o--|==|--o--|=/=|----| GND + // |-> value <-| + if value == 0 { + // prevent jump to -273.15 + value = 1 + } + return float64(rSeries*value) / float64(valDiff) + } + + // thermistor rSeries + // vRef o--|=/=|--o--|==|-----| GND + // |-> value <-| + if valDiff == 0 { + // prevent jump to -273.15 + valDiff = 1 + } + return float64(rSeries*valDiff) / float64(value) +} + +// getTemp calculates the temperature from the given resistance of the NTC resistor +func (n *TemperatureSensorNtcConf) getTemp(rntc float64) float64 { + // 1/T = 1/T0 + 1/B * ln(R/R0) + // + // B/T = B/T0 + ln(R/R0) = k, B/T0 = r + // T = B/k, Tc = T - 273 + + k := n.r + math.Log(rntc/n.R0) + return n.B/k - kelvinOffset +} + +// initialize is used to calculate some constants for the NTC algorithm. +// If B is unknown (given as 0), the function needs a second pair to calculate +// B from the both pairs (R1, TC1), (R0, TC0) +func (n *TemperatureSensorNtcConf) initialize() { + n.t0 = float64(n.TC0) + kelvinOffset + if n.B <= 0 { + //B=[ln(R0)-ln(R1)]/(1/T0-1/T1) + T1 := float64(n.TC1) + kelvinOffset + n.B = (1/n.t0 - 1/T1) + n.B = (math.Log(n.R0) - math.Log(n.R1)) / n.B // 2000K...5000K + } + n.r = n.B / n.t0 +} diff --git a/drivers/aio/temperature_sensor_driver_test.go b/drivers/aio/temperature_sensor_driver_test.go new file mode 100644 index 000000000..4d6f342d1 --- /dev/null +++ b/drivers/aio/temperature_sensor_driver_test.go @@ -0,0 +1,214 @@ +package aio + +import ( + "errors" + "fmt" + "strings" + "testing" + "time" + + "gobot.io/x/gobot/gobottest" +) + +func TestTemperatureSensorDriver(t *testing.T) { + testAdaptor := newAioTestAdaptor() + d := NewTemperatureSensorDriver(testAdaptor, "123") + gobottest.Assert(t, d.Connection(), testAdaptor) + gobottest.Assert(t, d.Pin(), "123") + gobottest.Assert(t, d.interval, 10*time.Millisecond) +} + +func TestTemperatureSensorDriverNtcScaling(t *testing.T) { + var tests = map[string]struct { + input int + want float64 + }{ + "smaller_than_min": {input: -1, want: 457.720219684306}, + "min": {input: 0, want: 457.720219684306}, + "near_min": {input: 1, want: 457.18923673420545}, + "mid_range": {input: 127, want: 87.9784401845593}, + "T25C": {input: 232, want: 24.805280460718336}, + "T0C": {input: 248, want: -0.9858175109026774}, + "T-25C": {input: 253, want: -22.92863536929451}, + "near_max": {input: 254, want: -33.51081663999781}, + "max": {input: 255, want: -273.15}, + "bigger_than_max": {input: 256, want: -273.15}, + } + a := newAioTestAdaptor() + d := NewTemperatureSensorDriver(a, "4") + ntc1 := TemperatureSensorNtcConf{TC0: 25, R0: 10000.0, B: 3950} //Ohm, R25=10k, B=3950 + d.SetNtcScaler(255, 1000, true, ntc1) //Ohm, reference value: 3300, series R: 1k + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a.TestAdaptorAnalogRead(func() (val int, err error) { + val = tt.input + return + }) + // act + got, err := d.ReadValue() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, got, tt.want) + }) + } +} + +func TestTemperatureSensorDriverLinearScaling(t *testing.T) { + var tests = map[string]struct { + input int + want float64 + }{ + "smaller_than_min": {input: -129, want: -40}, + "min": {input: -128, want: -40}, + "near_min": {input: -127, want: -39.450980392156865}, + "T-25C": {input: -101, want: -25.17647058823529}, + "T0C": {input: -55, want: 0.07843137254902288}, + "T25C": {input: -10, want: 24.7843137254902}, + "mid_range": {input: 0, want: 30.274509803921575}, + "near_max": {input: 126, want: 99.45098039215688}, + "max": {input: 127, want: 100}, + "bigger_than_max": {input: 128, want: 100}, + } + a := newAioTestAdaptor() + d := NewTemperatureSensorDriver(a, "4") + d.SetLinearScaler(-128, 127, -40, 100) + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a.TestAdaptorAnalogRead(func() (val int, err error) { + val = tt.input + return + }) + // act + got, err := d.ReadValue() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, got, tt.want) + }) + } +} + +func TestTempSensorPublishesTemperatureInCelsius(t *testing.T) { + sem := make(chan bool, 1) + a := newAioTestAdaptor() + d := NewTemperatureSensorDriver(a, "1") + ntc := TemperatureSensorNtcConf{TC0: 25, R0: 10000.0, B: 3975} //Ohm, R25=10k + d.SetNtcScaler(1023, 10000, false, ntc) //Ohm, reference value: 1023, series R: 10k + + a.TestAdaptorAnalogRead(func() (val int, err error) { + val = 585 + return + }) + d.Once(d.Event(Value), func(data interface{}) { + gobottest.Assert(t, fmt.Sprintf("%.2f", data.(float64)), "31.62") + sem <- true + }) + gobottest.Assert(t, d.Start(), nil) + + select { + case <-sem: + case <-time.After(1 * time.Second): + t.Errorf(" Temperature Sensor Event \"Data\" was not published") + } + + gobottest.Assert(t, d.Value(), 31.61532462352477) +} + +func TestTempSensorPublishesError(t *testing.T) { + sem := make(chan bool, 1) + a := newAioTestAdaptor() + d := NewTemperatureSensorDriver(a, "1") + + // send error + a.TestAdaptorAnalogRead(func() (val int, err error) { + err = errors.New("read error") + return + }) + + gobottest.Assert(t, d.Start(), nil) + + // expect error + d.Once(d.Event(Error), func(data interface{}) { + gobottest.Assert(t, data.(error).Error(), "read error") + sem <- true + }) + + select { + case <-sem: + case <-time.After(1 * time.Second): + t.Errorf(" Temperature Sensor Event \"Error\" was not published") + } +} + +func TestTempSensorHalt(t *testing.T) { + d := NewTemperatureSensorDriver(newAioTestAdaptor(), "1") + done := make(chan struct{}) + go func() { + <-d.halt + close(done) + }() + gobottest.Assert(t, d.Halt(), nil) + select { + case <-done: + case <-time.After(100 * time.Millisecond): + t.Errorf(" Temperature Sensor was not halted") + } +} + +func TestTempDriverDefaultName(t *testing.T) { + d := NewTemperatureSensorDriver(newAioTestAdaptor(), "1") + gobottest.Assert(t, strings.HasPrefix(d.Name(), "TemperatureSensor"), true) +} + +func TestTempDriverSetName(t *testing.T) { + d := NewTemperatureSensorDriver(newAioTestAdaptor(), "1") + d.SetName("mybot") + gobottest.Assert(t, d.Name(), "mybot") +} + +func TestTempDriver_initialize(t *testing.T) { + var tests = map[string]struct { + input TemperatureSensorNtcConf + want TemperatureSensorNtcConf + }{ + "B_low_tc0": { + input: TemperatureSensorNtcConf{TC0: -13, B: 2601.5}, + want: TemperatureSensorNtcConf{TC0: -13, B: 2601.5, t0: 260.15, r: 10}, + }, + "B_low_tc0_B": { + input: TemperatureSensorNtcConf{TC0: -13, B: 5203}, + want: TemperatureSensorNtcConf{TC0: -13, B: 5203, t0: 260.15, r: 20}, + }, + "B_mid_tc0": { + input: TemperatureSensorNtcConf{TC0: 25, B: 3950}, + want: TemperatureSensorNtcConf{TC0: 25, B: 3950, t0: 298.15, r: 13.248364916988095}, + }, + "B_mid_tc0_r0_no_change": { + input: TemperatureSensorNtcConf{TC0: 25, R0: 1234.5, B: 3950}, + want: TemperatureSensorNtcConf{TC0: 25, R0: 1234.5, B: 3950, t0: 298.15, r: 13.248364916988095}, + }, + "B_high_tc0": { + input: TemperatureSensorNtcConf{TC0: 100, B: 3731.5}, + want: TemperatureSensorNtcConf{TC0: 100, B: 3731.5, t0: 373.15, r: 10}, + }, + "T1_low": { + input: TemperatureSensorNtcConf{TC0: 25, R0: 2500.0, TC1: -13, R1: 10000}, + want: TemperatureSensorNtcConf{TC0: 25, R0: 2500.0, TC1: -13, R1: 10000, B: 2829.6355560320544, t0: 298.15, r: 9.490644159087891}, + }, + "T1_high": { + input: TemperatureSensorNtcConf{TC0: 25, R0: 2500.0, TC1: 100, R1: 371}, + want: TemperatureSensorNtcConf{TC0: 25, R0: 2500.0, TC1: 100, R1: 371, B: 2830.087381913779, t0: 298.15, r: 9.49215959052081}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // arrange + ntc := tt.input + // act + ntc.initialize() + // assert + gobottest.Assert(t, ntc, tt.want) + }) + } +} diff --git a/drivers/i2c/README.md b/drivers/i2c/README.md index 9dae07f08..010e11a7f 100644 --- a/drivers/i2c/README.md +++ b/drivers/i2c/README.md @@ -38,11 +38,13 @@ Gobot has a extensible system for connecting to hardware devices. The following - MPL115A2 Barometer - MPU6050 Accelerometer/Gyroscope - PCA9685 16-channel 12-bit PWM/Servo Driver +- PCF8591 8-bit 4xA/D & 1xD/A converter - SHT2x Temperature/Humidity - SHT3x-D Temperature/Humidity - SSD1306 OLED Display Controller - TSL2561 Digital Luminosity/Lux/Light Sensor - Wii Nunchuck Controller +- Y-40 Brightness/Temperature sensor, Potentiometer, analog input, analog output Driver More drivers are coming soon... diff --git a/drivers/i2c/pcf8591_driver.go b/drivers/i2c/pcf8591_driver.go new file mode 100644 index 000000000..4b5f47232 --- /dev/null +++ b/drivers/i2c/pcf8591_driver.go @@ -0,0 +1,388 @@ +package i2c + +import ( + "fmt" + "log" + "strings" + "sync" + "time" + + "gobot.io/x/gobot" +) + +// PCF8591 supports addresses from 0x48 to 0x4F +// The default address applies when all address pins connected to ground. +const pcf8591DefaultAddress = 0x48 + +const ( + pcf8591Debug = false +) + +type pcf8591Mode uint8 +type PCF8591Channel uint8 + +const ( + pcf8591_CHAN0 PCF8591Channel = 0x00 + pcf8591_CHAN1 PCF8591Channel = 0x01 + pcf8591_CHAN2 PCF8591Channel = 0x02 + pcf8591_CHAN3 PCF8591Channel = 0x03 +) + +const pcf8591_AION = 0x04 // auto increment, only relevant for ADC + +const ( + pcf8591_ALLSINGLE pcf8591Mode = 0x00 + pcf8591_THREEDIFF pcf8591Mode = 0x10 + pcf8591_MIXED pcf8591Mode = 0x20 + pcf8591_TWODIFF pcf8591Mode = 0x30 + pcf8591_ANAON pcf8591Mode = 0x40 +) + +const pcf8591_ADMASK = 0x33 // channels and mode + +type pcf8591ModeChan struct { + mode pcf8591Mode + channel PCF8591Channel +} + +// modeMap is to define the relation between a given description and the mode and channel +// beside the long form there are some short forms available without risk of confusion +// +// pure single mode +// "s.0"..."s.3": read single value of input n => channel n +// pure differential mode +// "d.0-1": differential value between input 0 and 1 => channel 0 +// "d.2-3": differential value between input 2 and 3 => channel 1 +// mixed mode +// "m.0": single value of input 0 => channel 0 +// "m.1": single value of input 1 => channel 1 +// "m.2-3": differential value between input 2 and 3 => channel 2 +// three differential inputs, related to input 3 +// "t.0-3": differential value between input 0 and 3 => channel 0 +// "t.1-3": differential value between input 1 and 3 => channel 1 +// "t.2-3": differential value between input 1 and 3 => channel 2 +var pcf8591ModeMap = map[string]pcf8591ModeChan{ + "s.0": {pcf8591_ALLSINGLE, pcf8591_CHAN0}, + "0": {pcf8591_ALLSINGLE, pcf8591_CHAN0}, + "s.1": {pcf8591_ALLSINGLE, pcf8591_CHAN1}, + "1": {pcf8591_ALLSINGLE, pcf8591_CHAN1}, + "s.2": {pcf8591_ALLSINGLE, pcf8591_CHAN2}, + "2": {pcf8591_ALLSINGLE, pcf8591_CHAN2}, + "s.3": {pcf8591_ALLSINGLE, pcf8591_CHAN3}, + "3": {pcf8591_ALLSINGLE, pcf8591_CHAN3}, + "d.0-1": {pcf8591_TWODIFF, pcf8591_CHAN0}, + "0-1": {pcf8591_TWODIFF, pcf8591_CHAN0}, + "d.2-3": {pcf8591_TWODIFF, pcf8591_CHAN1}, + "m.0": {pcf8591_MIXED, pcf8591_CHAN0}, + "m.1": {pcf8591_MIXED, pcf8591_CHAN1}, + "m.2-3": {pcf8591_MIXED, pcf8591_CHAN2}, + "t.0-3": {pcf8591_THREEDIFF, pcf8591_CHAN0}, + "0-3": {pcf8591_THREEDIFF, pcf8591_CHAN0}, + "t.1-3": {pcf8591_THREEDIFF, pcf8591_CHAN1}, + "1-3": {pcf8591_THREEDIFF, pcf8591_CHAN1}, + "t.2-3": {pcf8591_THREEDIFF, pcf8591_CHAN2}, +} + +// PCF8591Driver is a Gobot Driver for the PCF8591 8-bit 4xA/D & 1xD/A converter with i2c (100 kHz) and 3 address pins. +// The analog inputs can be used as differential inputs in different ways. +// +// All values are linear scaled to 3.3V by default. This can be changed, see example "tinkerboard_pcf8591.go". +// +// Address specification: +// 1 0 0 1 A2 A1 A0|rd +// Lowest bit (rd) is mapped to switch between write(0)/read(1), it is not part of the "real" address. +// +// Example: A1,A2=1, others are 0 +// Address mask => 1001110|1 => real 7-bit address mask 0100 1110 = 0x4E +// +// For example, here is the Adafruit board that uses this chip: +// https://www.adafruit.com/product/4648 +// +// This driver was tested with Tinkerboard and the YL-40 driver. +// +type PCF8591Driver struct { + name string + connector Connector + connection Connection + Config + gobot.Commander + lastCtrlByte byte + lastAnaOut byte + additionalReadWrite uint8 + additionalRead uint8 + forceRefresh bool + LastRead [][]byte // for debugging purposes + mutex *sync.Mutex // mutex needed to ensure write-read sequence of AnalogRead() is not interrupted +} + +// NewPCF8591Driver creates a new driver with specified i2c interface +// Params: +// conn Connector - the Adaptor to use with this Driver +// +// Optional params: +// i2c.WithBus(int): bus to use with this driver +// i2c.WithAddress(int): address to use with this driver +// i2c.WithPCF8591With400kbitStabilization(uint8, uint8): stabilize read in 400 kbit mode +// +func NewPCF8591Driver(a Connector, options ...func(Config)) *PCF8591Driver { + p := &PCF8591Driver{ + name: gobot.DefaultName("PCF8591"), + connector: a, + Config: NewConfig(), + Commander: gobot.NewCommander(), + mutex: &sync.Mutex{}, + } + + for _, option := range options { + option(p) + } + + return p +} + +// WithPCF8591With400kbitStabilisation option sets the PCF8591 additionalReadWrite and additionalRead value +func WithPCF8591With400kbitStabilization(additionalReadWrite, additionalRead int) func(Config) { + return func(c Config) { + p, ok := c.(*PCF8591Driver) + if ok { + if additionalReadWrite < 0 { + additionalReadWrite = 1 // works in most cases + } + if additionalRead < 0 { + additionalRead = 2 // works in most cases + } + p.additionalReadWrite = uint8(additionalReadWrite) + p.additionalRead = uint8(additionalRead) + if pcf8591Debug { + log.Printf("400 kbit stabilization for PCF8591Driver set rw: %d, r: %d", p.additionalReadWrite, p.additionalRead) + } + } else if pcf8591Debug { + log.Printf("trying to set 400 kbit stabilization for non-PCF8591Driver %v", c) + } + } +} + +// WithPCF8591ForceWrite option modifies the PCF8591Driver forceRefresh option +// Setting to true (1) will force refresh operation to register, although there is no change. +// Normally this is not needed, so default is off (0). +// When there is something flaky, there is a small chance to stabilize by setting this flag to true. +// However, setting this flag to true slows down each IO operation up to 100%. +func WithPCF8591ForceRefresh(val uint8) func(Config) { + return func(c Config) { + d, ok := c.(*PCF8591Driver) + if ok { + d.forceRefresh = val > 0 + } else if pcf8591Debug { + log.Printf("Trying to set forceRefresh for non-PCF8591Driver %v", c) + } + } +} + +// Name returns the Name for the Driver +func (p *PCF8591Driver) Name() string { return p.name } + +// SetName sets the Name for the Driver +func (p *PCF8591Driver) SetName(n string) { p.name = n } + +// Connection returns the connection for the Driver +func (p *PCF8591Driver) Connection() gobot.Connection { return p.connector.(gobot.Connection) } + +// Start initializes the PCF8591 +func (p *PCF8591Driver) Start() (err error) { + bus := p.GetBusOrDefault(p.connector.GetDefaultBus()) + address := p.GetAddressOrDefault(pcf8591DefaultAddress) + + p.connection, err = p.connector.GetConnection(address, bus) + if err != nil { + return err + } + + if err := p.AnalogOutputState(false); err != nil { + return err + } + return +} + +// Halt stops the device +func (p *PCF8591Driver) Halt() (err error) { + return p.AnalogOutputState(false) +} + +// AnalogRead returns value from analog reading of given input description +// +// Vlsb = (Vref-Vagnd)/256, value = (Van+ - Van-)/Vlsb, Van-=Vagnd for single mode +// +// The first read contains the last converted value (usually the last read). +// After the channel was switched this means the value of the previous read channel. +// After power on, the first byte read will be 80h, because the read is one cycle behind. +// +// Important note for 440 kbit mode: +// With a bus speed of 100 kBit/sec, the ADC conversion has ~80 us + ACK (time to transfer the previous value). +// This time is the limit for A-D conversion (datasheet 90 us). +// An i2c bus extender (LTC4311) don't fix it (it seems rather the opposite). +// +// This leads to following behavior: +// * the control byte is not written correctly +// * the transition process takes an additional cycle, very often +// * some circuits takes one cycle longer transition time in addition +// * reading more than one byte by Read([]byte), e.g. to calculate an average, is not sufficient, +// because some missing integration steps in each conversion (each byte value is a little bit lower than expected) +// +// So, for default, we drop the first three bytes to get the right value. +func (p *PCF8591Driver) AnalogRead(description string) (value int, err error) { + p.mutex.Lock() + defer p.mutex.Unlock() + + mc, err := PCF8591ParseModeChan(description) + if err != nil { + return 0, err + } + + // reset channel and mode + ctrlByte := p.lastCtrlByte & ^uint8(pcf8591_ADMASK) + // set to current channel and mode, AI must be off, because we need reading twice + ctrlByte = ctrlByte | uint8(mc.mode) | uint8(mc.channel) & ^uint8(pcf8591_AION) + + var uval byte + p.LastRead = make([][]byte, p.additionalReadWrite+1) + // repeated write and read cycle to stabilize value in 400 kbit mode + for writeReadCycle := uint8(1); writeReadCycle <= p.additionalReadWrite+1; writeReadCycle++ { + if err = p.writeCtrlByte(ctrlByte, p.forceRefresh || writeReadCycle > 1); err != nil { + return 0, err + } + + // initiate read but skip some bytes + if err := p.readBuf(writeReadCycle, 1+p.additionalRead); err != nil { + return 0, err + } + + // additional relax time + time.Sleep(1 * time.Millisecond) + + // real used read + if uval, err = p.connection.ReadByte(); err != nil { + return 0, err + } + + if pcf8591Debug { + p.LastRead[writeReadCycle-1] = append(p.LastRead[writeReadCycle-1], uval) + } + } + + // prepare return value + value = int(uval) + if mc.pcf8591IsDiff() { + if uval > 127 { + // first bit is set, means negative + value = int(uval) - 256 + } + } + + return value, err +} + +// AnalogWrite writes the given value to the analog output (DAC) +// Vlsb = (Vref-Vagnd)/256, Vaout = Vagnd+Vlsb*value +// implements the aio.AnalogWriter interface, pin is unused here +func (p *PCF8591Driver) AnalogWrite(pin string, value int) (err error) { + p.mutex.Lock() + defer p.mutex.Unlock() + + byteVal := byte(value) + + if p.lastAnaOut == byteVal { + if pcf8591Debug { + log.Printf("write skipped because value unchanged: 0x%X\n", byteVal) + } + return nil + } + + ctrlByte := p.lastCtrlByte | byte(pcf8591_ANAON) + err = p.connection.WriteByteData(ctrlByte, byteVal) + if err != nil { + return err + } + + p.lastCtrlByte = ctrlByte + p.lastAnaOut = byteVal + return nil +} + +// AnalogOutputState enables or disables the analog output +// Please note that in case of using the internal oscillator +// and the auto increment mode the output should not switched off. +// Otherwise conversion errors could occur. +func (p *PCF8591Driver) AnalogOutputState(state bool) (err error) { + p.mutex.Lock() + defer p.mutex.Unlock() + + var ctrlByte uint8 + if state { + ctrlByte = p.lastCtrlByte | byte(pcf8591_ANAON) + } else { + ctrlByte = p.lastCtrlByte & ^uint8(pcf8591_ANAON) + } + + if err = p.writeCtrlByte(ctrlByte, p.forceRefresh); err != nil { + return err + } + return nil +} + +// PCF8591ParseModeChan is used to get a working combination between mode (single, mixed, 2 differential, 3 differential) +// and the related channel to read from, parsed from the given description string. +func PCF8591ParseModeChan(description string) (*pcf8591ModeChan, error) { + mc, ok := pcf8591ModeMap[description] + if !ok { + descriptions := []string{} + for k := range pcf8591ModeMap { + descriptions = append(descriptions, k) + } + ds := strings.Join(descriptions, ", ") + return nil, fmt.Errorf("Unknown description '%s' for read analog value, accepted values: %s", description, ds) + } + + return &mc, nil +} + +func (p *PCF8591Driver) writeCtrlByte(ctrlByte uint8, forceRefresh bool) error { + if p.lastCtrlByte != ctrlByte || forceRefresh { + if err := p.connection.WriteByte(ctrlByte); err != nil { + return err + } + p.lastCtrlByte = ctrlByte + } else { + if pcf8591Debug { + log.Printf("write skipped because control byte unchanged: 0x%X\n", ctrlByte) + } + } + return nil +} + +func (p *PCF8591Driver) readBuf(nr uint8, cntBytes uint8) error { + buf := make([]byte, cntBytes) + cntRead, err := p.connection.Read(buf) + if err != nil { + return err + } + if cntRead != len(buf) { + return fmt.Errorf("Not enough bytes (%d of %d) read", cntRead, len(buf)) + } + if pcf8591Debug { + p.LastRead[nr-1] = buf + } + return nil +} + +func (mc pcf8591ModeChan) pcf8591IsDiff() bool { + switch mc.mode { + case pcf8591_TWODIFF: + return true + case pcf8591_THREEDIFF: + return true + case pcf8591_MIXED: + return mc.channel == pcf8591_CHAN2 + default: + return false + } +} diff --git a/drivers/i2c/pcf8591_driver_test.go b/drivers/i2c/pcf8591_driver_test.go new file mode 100644 index 000000000..b724cc929 --- /dev/null +++ b/drivers/i2c/pcf8591_driver_test.go @@ -0,0 +1,165 @@ +package i2c + +import ( + "testing" + + "gobot.io/x/gobot/gobottest" +) + +func initTestPCF8591DriverWithStubbedAdaptor() (*PCF8591Driver, *i2cTestAdaptor) { + adaptor := newI2cTestAdaptor() + pcf := NewPCF8591Driver(adaptor, WithPCF8591With400kbitStabilization(0, 2)) + pcf.lastCtrlByte = 0xFF // prevent skipping of write + pcf.Start() + return pcf, adaptor +} + +func TestPCF8591DriverWithPCF8591With400kbitStabilization(t *testing.T) { + pcf := NewPCF8591Driver(newI2cTestAdaptor(), WithPCF8591With400kbitStabilization(5, 6)) + gobottest.Assert(t, pcf.additionalReadWrite, uint8(5)) + gobottest.Assert(t, pcf.additionalRead, uint8(6)) +} + +func TestPCF8591DriverAnalogReadSingle(t *testing.T) { + // sequence to read the input channel: + // * prepare value (with channel and mode) and write control register + // * read 3 values to drop (see description in implementation) + // * read the analog value + // + // arrange + pcf, adaptor := initTestPCF8591DriverWithStubbedAdaptor() + adaptor.written = []byte{} // reset writes of Start() and former test + description := "s.1" + pcf.lastCtrlByte = 0x00 + ctrlByteOn := uint8(pcf8591_ALLSINGLE) | uint8(pcf8591_CHAN1) + returnRead := []uint8{0x01, 0x02, 0x03, 0xFF} + want := int(returnRead[3]) + // arrange reads + numCallsRead := 0 + adaptor.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + if numCallsRead == 1 { + b = returnRead[0:len(b)] + } + if numCallsRead == 2 { + b[0] = returnRead[len(returnRead)-1] + } + return len(b), nil + } + // act + got, err := pcf.AnalogRead(description) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(adaptor.written), 1) + gobottest.Assert(t, adaptor.written[0], ctrlByteOn) + gobottest.Assert(t, numCallsRead, 2) + gobottest.Assert(t, got, want) +} + +func TestPCF8591DriverAnalogReadDiff(t *testing.T) { + // sequence to read the input channel: + // * prepare value (with channel and mode) and write control register + // * read 3 values to drop (see description in implementation) + // * read the analog value + // * convert to 8-bit two's complement (-127...128) + // + // arrange + pcf, adaptor := initTestPCF8591DriverWithStubbedAdaptor() + adaptor.written = []byte{} // reset writes of Start() and former test + description := "m.2-3" + pcf.lastCtrlByte = 0x00 + ctrlByteOn := uint8(pcf8591_MIXED) | uint8(pcf8591_CHAN2) + // some two' complements + // 0x80 => -128 + // 0xFF => -1 + // 0x00 => 0 + // 0x7F => 127 + returnRead := []uint8{0x01, 0x02, 0x03, 0xFF} + want := -1 + // arrange reads + numCallsRead := 0 + adaptor.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + if numCallsRead == 1 { + b = returnRead[0:len(b)] + } + if numCallsRead == 2 { + b[0] = returnRead[len(returnRead)-1] + } + return len(b), nil + } + // act + got, err := pcf.AnalogRead(description) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(adaptor.written), 1) + gobottest.Assert(t, adaptor.written[0], ctrlByteOn) + gobottest.Assert(t, numCallsRead, 2) + gobottest.Assert(t, got, want) +} + +func TestPCF8591DriverAnalogWrite(t *testing.T) { + // sequence to write the output: + // * create new value for the control register (ANAON) + // * write the control register and value + // + // arrange + pcf, adaptor := initTestPCF8591DriverWithStubbedAdaptor() + adaptor.written = []byte{} // reset writes of Start() and former test + pcf.lastCtrlByte = 0x00 + pcf.lastAnaOut = 0x00 + ctrlByteOn := uint8(pcf8591_ANAON) + want := uint8(0x15) + // arrange writes + adaptor.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // act + err := pcf.AnalogWrite("", int(want)) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(adaptor.written), 2) + gobottest.Assert(t, adaptor.written[0], ctrlByteOn) + gobottest.Assert(t, adaptor.written[1], want) +} + +func TestPCF8591DriverAnalogOutputState(t *testing.T) { + // sequence to set the state: + // * create the new value (ctrlByte) for the control register (ANAON) + // * write the register value + // + // arrange + pcf, adaptor := initTestPCF8591DriverWithStubbedAdaptor() + for bitState := 0; bitState <= 1; bitState++ { + adaptor.written = []byte{} // reset writes of Start() and former test + // arrange some values + pcf.lastCtrlByte = uint8(0x00) + wantCtrlByteVal := uint8(pcf8591_ANAON) + if bitState == 0 { + pcf.lastCtrlByte = uint8(0xFF) + wantCtrlByteVal = uint8(0xFF & ^pcf8591_ANAON) + } + // act + err := pcf.AnalogOutputState(bitState == 1) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(adaptor.written), 1) + gobottest.Assert(t, adaptor.written[0], wantCtrlByteVal) + } +} + +func TestPCF8591DriverStart(t *testing.T) { + yl := NewPCF8591Driver(newI2cTestAdaptor()) + gobottest.Assert(t, yl.Start(), nil) +} + +func TestPCF8591DriverHalt(t *testing.T) { + yl := NewPCF8591Driver(newI2cTestAdaptor()) + gobottest.Assert(t, yl.Halt(), nil) +} + +func TestPCF8591DriverSetName(t *testing.T) { + d := NewPCF8591Driver(newI2cTestAdaptor()) + d.SetName("TESTME") + gobottest.Assert(t, d.Name(), "TESTME") +} diff --git a/drivers/i2c/yl40_driver.go b/drivers/i2c/yl40_driver.go new file mode 100644 index 000000000..58a6d4ff6 --- /dev/null +++ b/drivers/i2c/yl40_driver.go @@ -0,0 +1,324 @@ +package i2c + +import ( + "fmt" + "log" + "strings" + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/drivers/aio" +) + +// All address pins are connected to ground. +const yl40DefaultAddress = 0x48 + +const yl40Debug = false + +type YL40Pin string + +const ( + // brightness sensor, high brightness - low raw value, scaled to 0..1000 (high brightness - high value) + YL40Bri YL40Pin = "brightness" + // temperature sensor, high temperature - low raw value, scaled to °C + YL40Temp YL40Pin = "temperature" + // wired to AOUT, scaled to voltage 3.3V + YL40AIN2 YL40Pin = "analog input AIN2" + // adjustable resistor, turn clockwise will lower the raw value, scaled to -100..+100% (clockwise) + YL40Poti YL40Pin = "potentiometer" + YL40AOUT YL40Pin = "analog output" +) + +const ( + // the LED light is visible above ~1.7V + yl40LedDefaultVal = 1.7 + // default refresh rate, set to zero (cyclic reading deactivated) + yl40DefaultRefresh = 0 +) + +type yl40Sensor struct { + interval time.Duration + scaler func(input int) (value float64) +} + +type yl40Config struct { + sensors map[YL40Pin]*yl40Sensor + aOutScaler func(input float64) (value int) +} + +var yl40Pins = map[YL40Pin]string{ + YL40Bri: "s.0", + YL40Temp: "s.1", + YL40AIN2: "s.2", + YL40Poti: "s.3", + YL40AOUT: "aOut.0", +} + +// YL40Driver is a Gobot i2c bus driver for the YL-40 module with light dependent resistor (LDR), thermistor (NTC) +// and an potentiometer, one additional analog input and one analog output with an connected LED. +// The module is based on PCF8591 with 4xADC, 1xDAC. For detailed documentation refer to PCF8591Driver. +// +// All values are linear scaled to 3.3V by default. This can be changed, see example "tinkerboard_yl40.go". +// +// This driver was tested with Tinkerboard and this board with temperature & brightness sensor: +// https://www.makershop.de/download/YL_40_yl40.pdf +// +type YL40Driver struct { + *PCF8591Driver + conf yl40Config + + aBri *aio.AnalogSensorDriver + aTemp *aio.TemperatureSensorDriver + aAIN2 *aio.AnalogSensorDriver + aPoti *aio.AnalogSensorDriver + aOut *aio.AnalogActuatorDriver +} + +// NewYL40Driver creates a new driver with specified i2c interface +// Params: +// conn Connector - the Adaptor to use with this Driver +// +// Optional parameters: +// refer to PCF8591Driver for i2c specific options +// refer to TemperatureSensorDriver for temperature sensor specific options +// refer to AnalogSensorDriver for analog input specific options +// refer to AnalogActuatorDriver for analog output specific options +// +func NewYL40Driver(a Connector, options ...func(Config)) *YL40Driver { + options = append(options, WithAddress(yl40DefaultAddress)) + pcf := NewPCF8591Driver(a, options...) + + ntc := aio.TemperatureSensorNtcConf{TC0: 25, R0: 10000.0, B: 3950} //Ohm, R25=10k, B=3950 + defTempScaler := aio.TemperatureSensorNtcScaler(255, 1000, true, ntc) + + defConf := yl40Config{ + sensors: map[YL40Pin]*yl40Sensor{ + YL40Bri: { + interval: yl40DefaultRefresh, + scaler: aio.AnalogSensorLinearScaler(0, 255, 1000, 0), + }, + YL40Temp: { + interval: yl40DefaultRefresh, + scaler: defTempScaler, + }, + YL40AIN2: { + interval: yl40DefaultRefresh, + scaler: aio.AnalogSensorLinearScaler(0, 255, 0, 3.3), + }, + YL40Poti: { + interval: yl40DefaultRefresh, + scaler: aio.AnalogSensorLinearScaler(0, 255, 100, -100), + }, + }, + aOutScaler: aio.AnalogActuatorLinearScaler(0, 3.3, 0, 255), + } + + y := &YL40Driver{ + PCF8591Driver: pcf, + conf: defConf, + } + + y.SetName(gobot.DefaultName("YL-40")) + + for _, option := range options { + option(y) + } + + // initialize analog drivers + y.aBri = aio.NewAnalogSensorDriver(pcf, yl40Pins[YL40Bri], y.conf.sensors[YL40Bri].interval) + y.aTemp = aio.NewTemperatureSensorDriver(pcf, yl40Pins[YL40Temp], y.conf.sensors[YL40Temp].interval) + y.aAIN2 = aio.NewAnalogSensorDriver(pcf, yl40Pins[YL40AIN2], y.conf.sensors[YL40AIN2].interval) + y.aPoti = aio.NewAnalogSensorDriver(pcf, yl40Pins[YL40Poti], y.conf.sensors[YL40Poti].interval) + y.aOut = aio.NewAnalogActuatorDriver(pcf, yl40Pins[YL40AOUT]) + + // set input scalers + y.aBri.SetScaler(y.conf.sensors[YL40Bri].scaler) + y.aTemp.SetScaler(y.conf.sensors[YL40Temp].scaler) + y.aAIN2.SetScaler(y.conf.sensors[YL40AIN2].scaler) + y.aPoti.SetScaler(y.conf.sensors[YL40Poti].scaler) + + // set output scaler + y.aOut.SetScaler(y.conf.aOutScaler) + + return y +} + +// WithYL40Interval option sets the interval for refresh of given pin in YL40 driver +func WithYL40Interval(pin YL40Pin, val time.Duration) func(Config) { + return func(c Config) { + y, ok := c.(*YL40Driver) + if ok { + if sensor, ok := y.conf.sensors[pin]; ok { + sensor.interval = val + } + } else if yl40Debug { + log.Printf("trying to set interval for '%s' refresh for non-YL40Driver %v", pin, c) + } + } +} + +// WithYL40InputScaler option sets the input scaler of given input pin in YL40 driver +func WithYL40InputScaler(pin YL40Pin, scaler func(input int) (value float64)) func(Config) { + return func(c Config) { + y, ok := c.(*YL40Driver) + if ok { + if sensor, ok := y.conf.sensors[pin]; ok { + sensor.scaler = scaler + } + } else if yl40Debug { + log.Printf("trying to set input scaler for '%s' for non-YL40Driver %v", pin, c) + } + } +} + +// WithYL40OutputScaler option sets the output scaler in YL40 driver +func WithYL40OutputScaler(scaler func(input float64) (value int)) func(Config) { + return func(c Config) { + y, ok := c.(*YL40Driver) + if ok { + y.conf.aOutScaler = scaler + } else if yl40Debug { + log.Printf("trying to set output scaler for '%s' for non-YL40Driver %v", YL40AOUT, c) + } + } +} + +// Start initializes the driver +func (y *YL40Driver) Start() (err error) { + // must be the first one + if err := y.PCF8591Driver.Start(); err != nil { + return err + } + if err := y.aBri.Start(); err != nil { + return err + } + if err := y.aTemp.Start(); err != nil { + return err + } + if err := y.aAIN2.Start(); err != nil { + return err + } + if err := y.aPoti.Start(); err != nil { + return err + } + if err := y.aOut.Start(); err != nil { + return err + } + return y.Write(yl40LedDefaultVal) +} + +// Halt stops the driver +func (y *YL40Driver) Halt() (err error) { + // we try halt on each device, not stopping on the first error + var errors []string + if err := y.aBri.Halt(); err != nil { + errors = append(errors, err.Error()) + } + if err := y.aTemp.Halt(); err != nil { + errors = append(errors, err.Error()) + } + if err := y.aAIN2.Halt(); err != nil { + errors = append(errors, err.Error()) + } + if err := y.aPoti.Halt(); err != nil { + errors = append(errors, err.Error()) + } + if err := y.aOut.Halt(); err != nil { + errors = append(errors, err.Error()) + } + // must be the last one + if err := y.PCF8591Driver.Halt(); err != nil { + errors = append(errors, err.Error()) + } + if len(errors) > 0 { + return fmt.Errorf("Halt the driver %s", strings.Join(errors, ", ")) + } + return nil +} + +// Read returns the current reading from the given pin of the driver +// For the analog output pin the last written value is returned +func (y *YL40Driver) Read(pin YL40Pin) (val float64, err error) { + switch pin { + case YL40Bri: + return y.aBri.ReadValue() + case YL40Temp: + return y.aTemp.ReadValue() + case YL40AIN2: + return y.aAIN2.ReadValue() + case YL40Poti: + return y.aPoti.ReadValue() + case YL40AOUT: + return y.aOut.Value(), nil + default: + return 0, fmt.Errorf("Analog reading from pin '%s' not supported", pin) + } +} + +// ReadBrightness returns the current reading from brightness pin of the driver +func (y *YL40Driver) ReadBrightness() (val float64, err error) { + return y.Read(YL40Bri) +} + +// ReadTemperature returns the current reading from temperature pin of the driver +func (y *YL40Driver) ReadTemperature() (val float64, err error) { + return y.Read(YL40Temp) +} + +// ReadAIN2 returns the current reading from analog input pin 2 pin of the driver +func (y *YL40Driver) ReadAIN2() (val float64, err error) { + return y.Read(YL40AIN2) +} + +// ReadPotentiometer returns the current reading from potentiometer pin of the driver +func (y *YL40Driver) ReadPotentiometer() (val float64, err error) { + return y.Read(YL40Poti) +} + +// Value returns the last read or written value from the given pin of the driver +func (y *YL40Driver) Value(pin YL40Pin) (val float64, err error) { + switch pin { + case YL40Bri: + return y.aBri.Value(), nil + case YL40Temp: + return y.aTemp.Value(), nil + case YL40AIN2: + return y.aAIN2.Value(), nil + case YL40Poti: + return y.aPoti.Value(), nil + case YL40AOUT: + return y.aOut.Value(), nil + default: + return 0, fmt.Errorf("Get analog value from pin '%s' not supported", pin) + } +} + +// Brightness returns the last read brightness of the driver +func (y *YL40Driver) Brightness() (val float64, err error) { + return y.Value(YL40Bri) +} + +// Temperature returns the last read temperature of the driver +func (y *YL40Driver) Temperature() (val float64, err error) { + return y.Value(YL40Temp) +} + +// AIN2 returns the last read analog input value of the driver +func (y *YL40Driver) AIN2() (val float64, err error) { + return y.Value(YL40AIN2) +} + +// Potentiometer returns the last read potentiometer value of the driver +func (y *YL40Driver) Potentiometer() (val float64, err error) { + return y.Value(YL40Poti) +} + +// AOUT returns the last written value of the driver +func (y *YL40Driver) AOUT() (val float64, err error) { + return y.Value(YL40AOUT) +} + +// Write writes the given value to the analog output +func (y *YL40Driver) Write(val float64) (err error) { + return y.aOut.Write(val) +} diff --git a/drivers/i2c/yl40_driver_test.go b/drivers/i2c/yl40_driver_test.go new file mode 100644 index 000000000..598f67b40 --- /dev/null +++ b/drivers/i2c/yl40_driver_test.go @@ -0,0 +1,259 @@ +package i2c + +import ( + "fmt" + "testing" + "time" + + "gobot.io/x/gobot/gobottest" +) + +func initTestYL40DriverWithStubbedAdaptor() (*YL40Driver, *i2cTestAdaptor) { + adaptor := newI2cTestAdaptor() + yl := NewYL40Driver(adaptor, WithPCF8591With400kbitStabilization(0, 2)) + WithPCF8591ForceRefresh(1)(yl.PCF8591Driver) + yl.Start() + return yl, adaptor +} + +func TestYL40Driver(t *testing.T) { + // arrange, act + yl := NewYL40Driver(newI2cTestAdaptor()) + //assert + gobottest.Refute(t, yl.PCF8591Driver, nil) + gobottest.Assert(t, yl.conf.sensors[YL40Bri].interval, time.Duration(0)) + gobottest.Refute(t, yl.conf.sensors[YL40Bri].scaler, nil) + gobottest.Assert(t, yl.conf.sensors[YL40Temp].interval, time.Duration(0)) + gobottest.Refute(t, yl.conf.sensors[YL40Temp].scaler, nil) + gobottest.Assert(t, yl.conf.sensors[YL40AIN2].interval, time.Duration(0)) + gobottest.Refute(t, yl.conf.sensors[YL40AIN2].scaler, nil) + gobottest.Assert(t, yl.conf.sensors[YL40Poti].interval, time.Duration(0)) + gobottest.Refute(t, yl.conf.sensors[YL40Poti].scaler, nil) + gobottest.Refute(t, yl.conf.aOutScaler, nil) + gobottest.Refute(t, yl.aBri, nil) + gobottest.Refute(t, yl.aTemp, nil) + gobottest.Refute(t, yl.aAIN2, nil) + gobottest.Refute(t, yl.aPoti, nil) + gobottest.Refute(t, yl.aOut, nil) +} + +func TestYL40DriverWithYL40Interval(t *testing.T) { + // arrange, act + yl := NewYL40Driver(newI2cTestAdaptor(), + WithYL40Interval(YL40Bri, 100), + WithYL40Interval(YL40Temp, 101), + WithYL40Interval(YL40AIN2, 102), + WithYL40Interval(YL40Poti, 103), + ) + // assert + gobottest.Assert(t, yl.conf.sensors[YL40Bri].interval, time.Duration(100)) + gobottest.Assert(t, yl.conf.sensors[YL40Temp].interval, time.Duration(101)) + gobottest.Assert(t, yl.conf.sensors[YL40AIN2].interval, time.Duration(102)) + gobottest.Assert(t, yl.conf.sensors[YL40Poti].interval, time.Duration(103)) +} + +func TestYL40DriverWithYL40InputScaler(t *testing.T) { + // arrange + yl := NewYL40Driver(newI2cTestAdaptor()) + f1 := func(input int) (value float64) { return 0.1 } + f2 := func(input int) (value float64) { return 0.2 } + f3 := func(input int) (value float64) { return 0.3 } + f4 := func(input int) (value float64) { return 0.4 } + //act + WithYL40InputScaler(YL40Bri, f1)(yl) + WithYL40InputScaler(YL40Temp, f2)(yl) + WithYL40InputScaler(YL40AIN2, f3)(yl) + WithYL40InputScaler(YL40Poti, f4)(yl) + // assert + gobottest.Assert(t, fEqual(yl.conf.sensors[YL40Bri].scaler, f1), true) + gobottest.Assert(t, fEqual(yl.conf.sensors[YL40Temp].scaler, f2), true) + gobottest.Assert(t, fEqual(yl.conf.sensors[YL40AIN2].scaler, f3), true) + gobottest.Assert(t, fEqual(yl.conf.sensors[YL40Poti].scaler, f4), true) +} + +func TestYL40DriverWithYL40WithYL40OutputScaler(t *testing.T) { + // arrange + yl := NewYL40Driver(newI2cTestAdaptor()) + fo := func(input float64) (value int) { return 123 } + //act + WithYL40OutputScaler(fo)(yl) + // assert + gobottest.Assert(t, fEqual(yl.conf.aOutScaler, fo), true) +} + +func TestYL40DriverReadBrightness(t *testing.T) { + // sequence to read the input with PCF8591, see there tests + // arrange + yl, adaptor := initTestYL40DriverWithStubbedAdaptor() + adaptor.written = []byte{} // reset writes of Start() and former test + // ANAOUT was switched on by Start() + ctrlByteOn := uint8(pcf8591_ANAON) | uint8(pcf8591_ALLSINGLE) | uint8(pcf8591_CHAN0) + returnRead := []uint8{0x01, 0x02, 0x03, 73} + // scaler for brightness is 255..0 => 0..1000 + want := float64(255-returnRead[3]) * 1000 / 255 + // arrange reads + numCallsRead := 0 + adaptor.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + if numCallsRead == 1 { + b = returnRead[0:len(b)] + } + if numCallsRead == 2 { + b[0] = returnRead[len(returnRead)-1] + } + return len(b), nil + } + // act + got, err := yl.ReadBrightness() + got2, err2 := yl.Brightness() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(adaptor.written), 1) + gobottest.Assert(t, adaptor.written[0], ctrlByteOn) + gobottest.Assert(t, numCallsRead, 2) + gobottest.Assert(t, got, want) + gobottest.Assert(t, err2, nil) + gobottest.Assert(t, got2, want) +} + +func TestYL40DriverReadTemperature(t *testing.T) { + // sequence to read the input with PCF8591, see there tests + // arrange + yl, adaptor := initTestYL40DriverWithStubbedAdaptor() + adaptor.written = []byte{} // reset writes of Start() and former test + // ANAOUT was switched on by Start() + ctrlByteOn := uint8(pcf8591_ANAON) | uint8(pcf8591_ALLSINGLE) | uint8(pcf8591_CHAN1) + returnRead := []uint8{0x01, 0x02, 0x03, 232} + // scaler for temperature is 255..0 => NTC °C, 232 relates to nearly 25°C + // in TestTemperatureSensorDriverNtcScaling we have already used this NTC values + want := 24.805280460718336 + // arrange reads + numCallsRead := 0 + adaptor.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + if numCallsRead == 1 { + b = returnRead[0:len(b)] + } + if numCallsRead == 2 { + b[0] = returnRead[len(returnRead)-1] + } + return len(b), nil + } + // act + got, err := yl.ReadTemperature() + got2, err2 := yl.Temperature() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(adaptor.written), 1) + gobottest.Assert(t, adaptor.written[0], ctrlByteOn) + gobottest.Assert(t, numCallsRead, 2) + gobottest.Assert(t, got, want) + gobottest.Assert(t, err2, nil) + gobottest.Assert(t, got2, want) +} + +func TestYL40DriverReadAIN2(t *testing.T) { + // sequence to read the input with PCF8591, see there tests + // arrange + yl, adaptor := initTestYL40DriverWithStubbedAdaptor() + adaptor.written = []byte{} // reset writes of Start() and former test + // ANAOUT was switched on by Start() + ctrlByteOn := uint8(pcf8591_ANAON) | uint8(pcf8591_ALLSINGLE) | uint8(pcf8591_CHAN2) + returnRead := []uint8{0x01, 0x02, 0x03, 72} + // scaler for analog input 2 is 0..255 => 0..3.3 + want := float64(returnRead[3]) * 33 / 2550 + // arrange reads + numCallsRead := 0 + adaptor.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + if numCallsRead == 1 { + b = returnRead[0:len(b)] + } + if numCallsRead == 2 { + b[0] = returnRead[len(returnRead)-1] + } + return len(b), nil + } + // act + got, err := yl.ReadAIN2() + got2, err2 := yl.AIN2() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(adaptor.written), 1) + gobottest.Assert(t, adaptor.written[0], ctrlByteOn) + gobottest.Assert(t, numCallsRead, 2) + gobottest.Assert(t, got, want) + gobottest.Assert(t, err2, nil) + gobottest.Assert(t, got2, want) +} + +func TestYL40DriverReadPotentiometer(t *testing.T) { + // sequence to read the input with PCF8591, see there tests + // arrange + yl, adaptor := initTestYL40DriverWithStubbedAdaptor() + adaptor.written = []byte{} // reset writes of Start() and former test + // ANAOUT was switched on by Start() + ctrlByteOn := uint8(pcf8591_ANAON) | uint8(pcf8591_ALLSINGLE) | uint8(pcf8591_CHAN3) + returnRead := []uint8{0x01, 0x02, 0x03, 63} + // scaler for potentiometer is 255..0 => -100..100 + want := float64(returnRead[3])*-200/255 + 100 + // arrange reads + numCallsRead := 0 + adaptor.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + if numCallsRead == 1 { + b = returnRead[0:len(b)] + } + if numCallsRead == 2 { + b[0] = returnRead[len(returnRead)-1] + } + return len(b), nil + } + // act + got, err := yl.ReadPotentiometer() + got2, err2 := yl.Potentiometer() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(adaptor.written), 1) + gobottest.Assert(t, adaptor.written[0], ctrlByteOn) + gobottest.Assert(t, numCallsRead, 2) + gobottest.Assert(t, got, want) + gobottest.Assert(t, err2, nil) + gobottest.Assert(t, got2, want) +} + +func TestYL40DriverAnalogWrite(t *testing.T) { + // sequence to write the output of PCF8591, see there + // arrange + pcf, adaptor := initTestYL40DriverWithStubbedAdaptor() + adaptor.written = []byte{} // reset writes of Start() and former test + ctrlByteOn := uint8(pcf8591_ANAON) + want := uint8(175) + // write is scaled by 0..3.3V => 0..255 + write := float64(want) * 33 / 2550 + // arrange writes + adaptor.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // act + err := pcf.Write(write) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(adaptor.written), 2) + gobottest.Assert(t, adaptor.written[0], ctrlByteOn) + gobottest.Assert(t, adaptor.written[1], want) +} + +func TestYL40DriverStart(t *testing.T) { + yl := NewYL40Driver(newI2cTestAdaptor()) + gobottest.Assert(t, yl.Start(), nil) +} + +func TestYL40DriverHalt(t *testing.T) { + yl := NewYL40Driver(newI2cTestAdaptor()) + gobottest.Assert(t, yl.Halt(), nil) +} + +func fEqual(want interface{}, got interface{}) bool { + return fmt.Sprintf("%v", want) == fmt.Sprintf("%v", got) +} diff --git a/examples/tinkerboard_pcf8591.go b/examples/tinkerboard_pcf8591.go new file mode 100644 index 000000000..32977c6b2 --- /dev/null +++ b/examples/tinkerboard_pcf8591.go @@ -0,0 +1,91 @@ +// +build example +// +// Do not build by default. + +package main + +import ( + "fmt" + "log" + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/drivers/aio" + "gobot.io/x/gobot/drivers/i2c" + "gobot.io/x/gobot/platforms/tinkerboard" +) + +func main() { + // This driver was tested with Tinkerboard and this board with temperature & brightness sensor: + // https://www.makershop.de/download/YL_40_PCF8591.pdf + // + // Wiring + // PWR Tinkerboard: 1 (+3.3V, VCC), 6, 9, 14, 20 (GND) + // I2C1 Tinkerboard: 3 (SDA), 5 (SCL) + // PCF8591 plate: wire AOUT --> AIN2 for this example + board := tinkerboard.NewAdaptor() + pcf := i2c.NewPCF8591Driver(board, i2c.WithBus(1)) + aout := aio.NewAnalogActuatorDriver(pcf, "AOUT") + aout.SetScaler(aio.AnalogActuatorLinearScaler(0, 3.3, 0, 255)) + + var val int + var err error + + // brightness sensor, high brightness - low raw value + descLight := "s.0" + // temperature sensor, high temperature - low raw value + // sometimes buggy, because not properly grounded + descTemp := "s.1" + // wired to AOUT + descAIN2 := "s.2" + // adjustable resistor, turn clockwise will lower the raw value + descResi := "s.3" + // the LED light is visible above ~1.7V, this means ~127 (half of 3.3V) + writeVal := 1.7 + + work := func() { + gobot.Every(1000*time.Millisecond, func() { + if err := aout.Write(writeVal); err != nil { + fmt.Println(err) + } else { + log.Printf("Write AOUT: %.1f V [0..3.3]", writeVal) + writeVal = writeVal + 0.1 + if writeVal > 3.3 { + writeVal = 0 + } + } + + if val, err = pcf.AnalogRead(descLight); err != nil { + fmt.Println(err) + } else { + log.Printf("Brightness (%s): %d [255..0]", descLight, val) + } + + if val, err = pcf.AnalogRead(descTemp); err != nil { + fmt.Println(err) + } else { + log.Printf("Temperature (%s): %d [255..0]", descTemp, val) + } + + if val, err = pcf.AnalogRead(descAIN2); err != nil { + fmt.Println(err) + } else { + log.Printf("Read AOUT (%s): %d [0..255]", descAIN2, val) + } + + if val, err = pcf.AnalogRead(descResi); err != nil { + fmt.Println(err) + } else { + log.Printf("Resistor (%s): %d [255..0]", descResi, val) + } + }) + } + + robot := gobot.NewRobot("pcfBot", + []gobot.Connection{board}, + []gobot.Device{pcf}, + work, + ) + + robot.Start() +} diff --git a/examples/tinkerboard_yl40.go b/examples/tinkerboard_yl40.go new file mode 100644 index 000000000..c947d3a17 --- /dev/null +++ b/examples/tinkerboard_yl40.go @@ -0,0 +1,76 @@ +// +build example +// +// Do not build by default. + +package main + +import ( + "fmt" + "log" + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/drivers/i2c" + "gobot.io/x/gobot/platforms/tinkerboard" +) + +func main() { + // Wiring + // PWR Tinkerboard: 1 (+3.3V, VCC), 6, 9, 14, 20 (GND) + // I2C1 Tinkerboard: 3 (SDA), 5 (SCL) + // YL-40 module: wire AOUT --> AIN2 for this example + // + // Note: temperature measurement is often buggy, because sensor is not properly grounded + // fix it by soldering a small bridge to the adjacent ground pin of brightness sensor + board := tinkerboard.NewAdaptor() + yl := i2c.NewYL40Driver(board, i2c.WithBus(1)) + + work := func() { + // the LED light is visible above ~1.7V + writeVal, _ := yl.AOUT() + + gobot.Every(1000*time.Millisecond, func() { + if err := yl.Write(writeVal); err != nil { + fmt.Println(err) + } else { + log.Printf(" %.1f V written", writeVal) + writeVal = writeVal + 0.1 + if writeVal > 3.3 { + writeVal = 0 + } + } + + if brightness, err := yl.ReadBrightness(); err != nil { + fmt.Println(err) + } else { + log.Printf("Brightness: %.0f [0..1000]", brightness) + } + + if temperature, err := yl.ReadTemperature(); err != nil { + fmt.Println(err) + } else { + log.Printf("Temperature: %.1f °C", temperature) + } + + if ain2, err := yl.ReadAIN2(); err != nil { + fmt.Println(err) + } else { + log.Printf("Read back AOUT: %.1f [0..3.3]", ain2) + } + + if potiState, err := yl.ReadPotentiometer(); err != nil { + fmt.Println(err) + } else { + log.Printf("Resistor: %.0f %% [-100..+100]", potiState) + } + }) + } + + robot := gobot.NewRobot("yl40Bot", + []gobot.Connection{board}, + []gobot.Device{yl}, + work, + ) + + robot.Start() +}