From d1d3b72147c3cf64c5918143ba919483358a807e Mon Sep 17 00:00:00 2001 From: NeuralSpaz <6887502+NeuralSpaz@users.noreply.github.com> Date: Thu, 20 Dec 2018 13:06:03 +1100 Subject: [PATCH] mcp9808: add experimental support for digital temperature sensor (#356) * mcp9808: add support for digital temperature sensor Add support for microchip i2c MCP9808 temperature sensor. --- experimental/devices/mcp9808/doc.go | 18 + experimental/devices/mcp9808/example_test.go | 81 ++ experimental/devices/mcp9808/mcp9808.go | 426 ++++++++ experimental/devices/mcp9808/mcp9808_test.go | 947 ++++++++++++++++++ .../mcp9808smoketest/mcp9808smoketest.go | 76 ++ 5 files changed, 1548 insertions(+) create mode 100644 experimental/devices/mcp9808/doc.go create mode 100644 experimental/devices/mcp9808/example_test.go create mode 100644 experimental/devices/mcp9808/mcp9808.go create mode 100644 experimental/devices/mcp9808/mcp9808_test.go create mode 100644 experimental/devices/mcp9808/mcp9808smoketest/mcp9808smoketest.go diff --git a/experimental/devices/mcp9808/doc.go b/experimental/devices/mcp9808/doc.go new file mode 100644 index 0000000..6e89d6e --- /dev/null +++ b/experimental/devices/mcp9808/doc.go @@ -0,0 +1,18 @@ +// Copyright 2018 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +// Package mcp9808 controls a Microchip MCP9808 digital I²C temperature sensor. +// +// Features +// +// -40°C and +125°C Operating Range. +// +// User-Selectable Measurement Resolution: +0.5°C, +0.25°C, +0.125°C, +0.0625°C. +// +// User-Programmable Temperature Alerts. +// +// Datasheet +// +// http://ww1.microchip.com/downloads/en/DeviceDoc/25095A.pdf +package mcp9808 diff --git a/experimental/devices/mcp9808/example_test.go b/experimental/devices/mcp9808/example_test.go new file mode 100644 index 0000000..ae30a78 --- /dev/null +++ b/experimental/devices/mcp9808/example_test.go @@ -0,0 +1,81 @@ +// Copyright 2018 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package mcp9808_test + +import ( + "fmt" + "log" + + "periph.io/x/periph/conn/i2c/i2creg" + "periph.io/x/periph/conn/physic" + "periph.io/x/periph/experimental/devices/mcp9808" + "periph.io/x/periph/host" +) + +func ExampleDev_SenseTemp() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Open default I²C bus. + bus, err := i2creg.Open("") + if err != nil { + log.Fatalf("failed to open I²C: %v", err) + } + defer bus.Close() + + // Create a new temperature sensor. + sensor, err := mcp9808.New(bus, &mcp9808.DefaultOpts) + if err != nil { + log.Fatalln(err) + } + + // Read values from sensor. + measurement, err := sensor.SenseTemp() + + if err != nil { + log.Fatalln(err) + } + + fmt.Println(measurement) +} + +func ExampleDev_SenseWithAlerts() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Open default I²C bus. + bus, err := i2creg.Open("") + if err != nil { + log.Fatalf("failed to open I²C: %v", err) + } + defer bus.Close() + + // Create a new temperature sensor. + sensor, err := mcp9808.New(bus, &mcp9808.DefaultOpts) + if err != nil { + log.Fatalln(err) + } + + lower := physic.ZeroCelsius + upper := physic.ZeroCelsius + 25*physic.Celsius + critical := physic.ZeroCelsius + 32*physic.Celsius + + // Read values from sensor. + temperature, alerts, err := sensor.SenseWithAlerts(lower, upper, critical) + + if err != nil { + log.Fatalln(err) + } + + for _, alert := range alerts { + fmt.Println(alert) + } + + fmt.Println(temperature) +} diff --git a/experimental/devices/mcp9808/mcp9808.go b/experimental/devices/mcp9808/mcp9808.go new file mode 100644 index 0000000..e4c20db --- /dev/null +++ b/experimental/devices/mcp9808/mcp9808.go @@ -0,0 +1,426 @@ +// Copyright 2018 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package mcp9808 + +import ( + "encoding/binary" + "errors" + "sync" + "time" + + "periph.io/x/periph/conn" + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/mmr" + "periph.io/x/periph/conn/physic" +) + +// Opts holds the configuration options. +// +// Slave Address +// +// Depending which pins the A0, A1 and A2 pins are connected to will change the +// slave address. Default configuration is address 0x18 (Ax pins to GND). For a +// full address table see datasheet. +type Opts struct { + Addr int + Res resolution +} + +// DefaultOpts is the recommended default options. +var DefaultOpts = Opts{ + Addr: 0x18, + Res: Maximum, +} + +// New opens a handle to an mcp9808 sensor. +func New(bus i2c.Bus, opts *Opts) (*Dev, error) { + i2cAddress := DefaultOpts.Addr + if opts.Addr != 0 { + if opts.Addr < 0x18 || opts.Addr > 0x1f { + return nil, errAddressOutOfRange + } + i2cAddress = opts.Addr + } + + dev := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: bus, Addr: uint16(i2cAddress)}, + Order: binary.BigEndian, + }, + stop: make(chan struct{}, 1), + res: opts.Res, + enabled: false, + } + + if err := dev.setResolution(opts.Res); err != nil { + return nil, err + } + if err := dev.enable(); err != nil { + return nil, err + } + return dev, nil +} + +// Dev is a handle to the mcp9808 sensor. +type Dev struct { + m mmr.Dev8 + stop chan struct{} + res resolution + + mu sync.Mutex + sensing bool + critical physic.Temperature + upper physic.Temperature + lower physic.Temperature + enabled bool +} + +// Sense reads the current temperature. +func (d *Dev) Sense(e *physic.Env) error { + t, _, err := d.readTemperature() + e.Temperature = t + return err +} + +// SenseContinuous returns measurements as °C, on a continuous basis. +// The application must call Halt() to stop the sensing when done to stop the +// sensor and close the channel. +// It's the responsibility of the caller to retrieve the values from the channel +// as fast as possible, otherwise the interval may not be respected. +func (d *Dev) SenseContinuous(interval time.Duration) (<-chan physic.Env, error) { + switch d.res { + case Maximum: + if interval < 250*time.Millisecond { + return nil, errTooShortInterval + } + case High: + if interval < 130*time.Millisecond { + return nil, errTooShortInterval + } + case Medium: + if interval < 65*time.Millisecond { + return nil, errTooShortInterval + } + case Low: + if interval < 30*time.Millisecond { + return nil, errTooShortInterval + } + } + + env := make(chan physic.Env) + d.mu.Lock() + d.sensing = true + d.mu.Unlock() + var wg sync.WaitGroup + wg.Add(1) + + go func() { + for { + select { + case <-time.After(interval): + t, _, _ := d.readTemperature() + env <- physic.Env{Temperature: t} + case <-d.stop: + wg.Done() + return + } + } + }() + + go func() { + wg.Wait() + close(env) + d.mu.Lock() + d.sensing = false + d.mu.Unlock() + }() + return env, nil +} + +func (d *Dev) Precision(e *physic.Env) { + switch d.res { + case Maximum: + e.Temperature = 62500 * physic.MicroKelvin + case High: + e.Temperature = 125 * physic.MilliKelvin + case Medium: + e.Temperature = 250 * physic.MilliKelvin + case Low: + e.Temperature = 500 * physic.MilliKelvin + } +} + +// SenseTemp reads the current temperature. +func (d *Dev) SenseTemp() (physic.Temperature, error) { + t, _, err := d.readTemperature() + return t, err +} + +// SenseWithAlerts reads the ambient temperature and returns an slice of any +// alerts that have been tripped. Lower must be less than upper which must be +// less than critical. +func (d *Dev) SenseWithAlerts(lower, upper, critical physic.Temperature) (physic.Temperature, []Alert, error) { + if critical > upper && upper > lower { + if err := d.setCriticalAlert(critical); err != nil { + return 0, nil, err + } + if err := d.setUpperAlert(upper); err != nil { + return 0, nil, err + } + if err := d.setLowerAlert(lower); err != nil { + return 0, nil, err + } + } else { + return 0, nil, errAlertInvalid + } + + t, alertBits, err := d.readTemperature() + if err != nil { + return 0, nil, err + } + + // Check for Alerts. + if alertBits&0xe0 > 0 { + var as []Alert + if alertBits&0x80 > 0 { + // Critical Alert bit set. + crit, err := d.m.ReadUint16(critAlert) + if err != nil { + return t, nil, errReadCriticalAlert + } + t := alertBitsToTemperature(crit) + as = append(as, Alert{"critical", t}) + } + + if alertBits&0x40 > 0 { + // Upper Alert bit set. + upper, err := d.m.ReadUint16(upperAlert) + if err != nil { + return t, nil, errReadUpperAlert + } + t := alertBitsToTemperature(upper) + as = append(as, Alert{"upper", t}) + } + + if alertBits&0x20 > 0 { + // Lower Alert bit set. + lower, err := d.m.ReadUint16(lowerAlert) + if err != nil { + return t, nil, errReadLowerAlert + } + t := alertBitsToTemperature(lower) + as = append(as, Alert{"lower", t}) + } + + return t, as, nil + } + return t, nil, nil +} + +// Halt put the mcp9808 into shutdown mode. It will not read temperatures while +// in shutdown mode. +func (d *Dev) Halt() error { + d.mu.Lock() + if d.sensing { + d.stop <- struct{}{} + } + d.mu.Unlock() + + if err := d.m.WriteUint16(configuration, 0x0100); err != nil { + return errWritingConfiguration + } + + d.mu.Lock() + d.enabled = false + d.mu.Unlock() + return nil +} + +func (d *Dev) String() string { + return "MCP9808" +} + +func (d *Dev) enable() error { + d.mu.Lock() + defer d.mu.Unlock() + if !d.enabled { + if err := d.m.WriteUint16(configuration, 0x0000); err != nil { + return errWritingConfiguration + } + d.enabled = true + } + return nil +} + +func (d *Dev) readTemperature() (physic.Temperature, uint8, error) { + if err := d.enable(); err != nil { + return 0, 0, err + } + + tbits, err := d.m.ReadUint16(temperature) + if err != nil { + return 0, 0, errReadTemperature + } + // Convert to physic.Temperature 0.0625°C per bit + t := physic.Temperature(tbits&0x0FFF) * 62500 * physic.MicroKelvin + if tbits&0x1000 > 0 { + // Check for bit sign. + t = -t + } + t += physic.ZeroCelsius + return t, uint8(tbits>>8) & 0xe0, nil +} + +func (d *Dev) setResolution(r resolution) error { + switch r { + case Low: + if err := d.m.WriteUint8(resolutionConfig, 0x00); err != nil { + return errWritingResolution + } + case Medium: + if err := d.m.WriteUint8(resolutionConfig, 0x01); err != nil { + return errWritingResolution + } + case High: + if err := d.m.WriteUint8(resolutionConfig, 0x02); err != nil { + return errWritingResolution + } + case Maximum: + if err := d.m.WriteUint8(resolutionConfig, 0x03); err != nil { + return errWritingResolution + } + default: + return errInvalidResolution + } + return nil +} + +func (d *Dev) setCriticalAlert(t physic.Temperature) error { + d.mu.Lock() + defer d.mu.Unlock() + if t == d.critical { + return nil + } + crit, err := alertTemperatureToBits(t) + if err != nil { + return err + } + if err := d.m.WriteUint16(critAlert, crit); err != nil { + return errWritingCritAlert + } + d.critical = t + return nil +} + +func (d *Dev) setUpperAlert(t physic.Temperature) error { + d.mu.Lock() + defer d.mu.Unlock() + if t == d.upper { + return nil + } + upper, err := alertTemperatureToBits(t) + if err != nil { + return err + } + if err := d.m.WriteUint16(upperAlert, upper); err != nil { + return errWritingUpperAlert + } + d.upper = t + return nil +} + +func (d *Dev) setLowerAlert(t physic.Temperature) error { + d.mu.Lock() + defer d.mu.Unlock() + if t == d.lower { + return nil + } + lower, err := alertTemperatureToBits(t) + if err != nil { + return err + } + if err := d.m.WriteUint16(lowerAlert, lower); err != nil { + return errWritingLowerAlert + } + d.lower = t + return nil +} + +type Alert struct { + AlertMode string + AlertLevel physic.Temperature +} + +const ( + // Register addresses. + configuration byte = 0x01 + upperAlert byte = 0x02 + lowerAlert byte = 0x03 + critAlert byte = 0x04 + temperature byte = 0x05 + manifactureID byte = 0x06 + deviceID byte = 0x07 + resolutionConfig byte = 0x08 +) + +var ( + errReadTemperature = errors.New("failed to read ambient temperature") + errReadCriticalAlert = errors.New("failed to read critical temperature") + errReadUpperAlert = errors.New("failed to read upper temperature") + errReadLowerAlert = errors.New("failed to read lower temperature") + errAddressOutOfRange = errors.New("i2c address out of range") + errInvalidResolution = errors.New("invalid resolution") + errWritingResolution = errors.New("failed to write resolution configuration") + errWritingConfiguration = errors.New("failed to write configuration") + errWritingCritAlert = errors.New("failed to write critical alert configuration") + errWritingUpperAlert = errors.New("failed to write upper alert configuration") + errWritingLowerAlert = errors.New("failed to write lower alert configuration") + errAlertOutOfRange = errors.New("alert setting exceeds operating conditions") + errAlertInvalid = errors.New("invalid alert temperature configuration") + errTooShortInterval = errors.New("too short interval for resolution") +) + +func alertBitsToTemperature(b uint16) physic.Temperature { + b = (b >> 2) & 0x07FF + t := physic.Temperature(b&0x03FF) * 250 * physic.MilliKelvin + if b&0x400 > 0 { + t = -t + } + t += physic.ZeroCelsius + return t +} + +func alertTemperatureToBits(t physic.Temperature) (uint16, error) { + const maxAlert = 125*physic.Kelvin + physic.ZeroCelsius + const minAlert = -40*physic.Kelvin + physic.ZeroCelsius + + if t > maxAlert || t < minAlert { + return 0, errAlertOutOfRange + } + t -= physic.ZeroCelsius + // 0.25°C per bit. + t /= 250 * physic.MilliKelvin + + var bits uint16 + if t < 0 { + t = -t + bits |= 0x400 + } + bits |= uint16(t) + bits = bits << 2 + return bits, nil +} + +type resolution uint8 + +const ( + Maximum resolution = 0 + Low resolution = 1 + Medium resolution = 2 + High resolution = 3 +) + +var _ conn.Resource = &Dev{} +var _ physic.SenseEnv = &Dev{} diff --git a/experimental/devices/mcp9808/mcp9808_test.go b/experimental/devices/mcp9808/mcp9808_test.go new file mode 100644 index 0000000..fab8198 --- /dev/null +++ b/experimental/devices/mcp9808/mcp9808_test.go @@ -0,0 +1,947 @@ +// Copyright 2018 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package mcp9808 + +import ( + "encoding/binary" + "reflect" + "testing" + "time" + + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/mmr" + "periph.io/x/periph/conn/physic" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + opts Opts + tx []i2ctest.IO + want error + }{ + { + name: "defaults", + opts: DefaultOpts, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{resolutionConfig, 0x03}, R: []byte{}}, + {Addr: 0x18, W: []byte{configuration, 0x00, 0x00}, R: []byte{}}, + }, + want: nil, + }, + { + name: "bad address", + opts: Opts{Addr: 0x40}, + want: errAddressOutOfRange, + }, + { + name: "io error", + opts: Opts{Addr: 0x18}, + want: errWritingResolution, + }, + { + name: "enabled error", + opts: DefaultOpts, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{resolutionConfig, 0x03}, R: []byte{}}, + }, + want: errWritingConfiguration, + }, + } + + for _, tt := range tests { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + + _, err := New(&bus, &tt.opts) + if err != tt.want { + t.Errorf("New(%s) expected %v but got %v ", tt.name, tt.want, err) + } + } +} + +func TestSense(t *testing.T) { + tests := []struct { + name string + want physic.Temperature + tx []i2ctest.IO + enabled bool + err error + }{ + { + name: "0C", + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x00, 0x00}}, + }, + enabled: true, + want: physic.ZeroCelsius, + err: nil, + }, + { + name: "10C", + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x00, 0xa0}}, + }, + enabled: true, + want: physic.ZeroCelsius + 10*physic.Kelvin, + err: nil, + }, + { + name: "-10C", + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x10, 0xa0}}, + }, + enabled: true, + want: physic.ZeroCelsius - 10*physic.Kelvin, + err: nil, + }, + { + name: "io error", + tx: []i2ctest.IO{}, + enabled: true, + err: errReadTemperature, + }, + { + name: "enable error", + enabled: false, + want: physic.ZeroCelsius + 10*physic.Kelvin, + err: errWritingConfiguration, + }, + } + + for _, tt := range tests { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + enabled: tt.enabled, + } + e := &physic.Env{} + err := mcp9808.Sense(e) + if err == nil && tt.want != e.Temperature { + t.Errorf("%s Sense() expected %v but got %v ", tt.name, tt.want, e.Temperature) + } + if err != tt.err { + t.Errorf("%s Sense() expected %v but got %v ", tt.name, tt.err, err) + } + } +} + +func TestSenseContinuous(t *testing.T) { + tests := []struct { + name string + want physic.Temperature + res resolution + interval time.Duration + tx []i2ctest.IO + enabled bool + Halt bool + err error + }{ + { + name: "errTooShortInterval Max", + res: Maximum, + interval: 249 * time.Millisecond, + err: errTooShortInterval, + }, + { + name: "errTooShortInterval High", + res: High, + interval: 129 * time.Millisecond, + err: errTooShortInterval, + }, + { + name: "errTooShortInterval Med", + res: Medium, + interval: 64 * time.Millisecond, + err: errTooShortInterval, + }, + { + name: "errTooShortInterval Low", + res: Low, + interval: 29 * time.Millisecond, + err: errTooShortInterval, + }, + { + name: "Halt", + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x00, 0xa0}}, + {Addr: 0x18, W: []byte{configuration, 0x01, 0x00}, R: nil}, + }, + want: physic.ZeroCelsius + 10*physic.Celsius, + res: Low, + interval: 30 * time.Millisecond, + enabled: true, + Halt: true, + err: nil, + }, + } + for _, tt := range tests { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian, + }, + res: tt.res, + enabled: tt.enabled, + stop: make(chan struct{}, 1), + } + + env, err := mcp9808.SenseContinuous(tt.interval) + + if tt.Halt { + e := <-env + err := mcp9808.Halt() + if err != tt.err { + t.Errorf("SenseContinuous() %s wanted err: %v, but got: %v", tt.name, tt.err, err) + } + if err == nil && e.Temperature != tt.want { + t.Errorf("SenseContinuous() %s wanted %v, but got: %v", tt.name, tt.want, e.Temperature) + } + } + + if err != tt.err { + t.Errorf("SenseContinuous() %s wanted err: %v, but got: %v", tt.name, tt.err, err) + } + mcp9808.Halt() + } +} + +func TestPrecision(t *testing.T) { + tests := []struct { + name string + want physic.Temperature + res resolution + }{ + { + name: "Maximum", + want: 62500 * physic.MicroKelvin, + res: Maximum, + }, + { + name: "High", + want: 125 * physic.MilliKelvin, + res: High, + }, + { + name: "Medium", + want: 250 * physic.MilliKelvin, + res: Medium, + }, + { + name: "Low", + want: 500 * physic.MilliKelvin, + res: Low, + }, + } + + for _, tt := range tests { + d := &Dev{res: tt.res} + e := &physic.Env{} + d.Precision(e) + if e.Temperature != tt.want { + t.Errorf("Precision(%s) wanted %v but got %v", tt.name, tt.want, e.Temperature) + } + } +} + +func TestSenseTemp(t *testing.T) { + tests := []struct { + name string + want physic.Temperature + tx []i2ctest.IO + err error + }{ + { + name: "0C", + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x00, 0x00}}, + }, + want: physic.ZeroCelsius, + err: nil, + }, + { + name: "10C", + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x00, 0xa0}}, + }, + want: physic.ZeroCelsius + 10*physic.Kelvin, + err: nil, + }, + { + name: "-10C", + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x10, 0xa0}}, + }, + want: physic.ZeroCelsius - 10*physic.Kelvin, + err: nil, + }, + { + name: "io error", + tx: []i2ctest.IO{}, + err: errReadTemperature, + }, + } + + for _, tt := range tests { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + enabled: true, + } + got, err := mcp9808.SenseTemp() + if tt.want != got { + t.Errorf("%s SenseTemp() expected %v but got %v ", tt.name, tt.want, got) + } + if err != tt.err { + t.Errorf("%s SenseTemp() expected %v but got %v ", tt.name, tt.err, err) + } + } +} + +func TestSenseWithAlerts(t *testing.T) { + tests := []struct { + name string + critical physic.Temperature + upper physic.Temperature + lower physic.Temperature + temp physic.Temperature + alerts []Alert + tx []i2ctest.IO + err error + }{ + { + name: "read no alert", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + temp: physic.ZeroCelsius + 1*physic.Kelvin, + alerts: nil, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x50}, R: nil}, + {Addr: 0x18, W: []byte{lowerAlert, 0x00, 0x00}, R: nil}, + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x00, 0x10}}, + }, + err: nil, + }, + { + name: "get lower Alert", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + temp: physic.ZeroCelsius - 1*physic.Kelvin, + alerts: []Alert{ + {"lower", physic.ZeroCelsius}, + }, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x50}, R: nil}, + {Addr: 0x18, W: []byte{lowerAlert, 0x00, 0x00}, R: nil}, + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x30, 0x10}}, + {Addr: 0x18, W: []byte{lowerAlert}, R: []byte{0x00, 0x00}}, + }, + err: nil, + }, + { + name: "get upper Alert", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + temp: physic.ZeroCelsius + 7*physic.Kelvin, + alerts: []Alert{ + {"upper", physic.ZeroCelsius + 5*physic.Kelvin}, + }, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x50}, R: nil}, + {Addr: 0x18, W: []byte{lowerAlert, 0x00, 0x00}, R: nil}, + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x40, 0x70}}, + {Addr: 0x18, W: []byte{upperAlert}, R: []byte{0x00, 0x50}}, + }, + err: nil, + }, + { + name: "get critical Alert", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + temp: physic.ZeroCelsius + 15*physic.Kelvin, + alerts: []Alert{ + {"critical", physic.ZeroCelsius + 10*physic.Kelvin}, + {"upper", physic.ZeroCelsius + 5*physic.Kelvin}, + }, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x50}, R: nil}, + {Addr: 0x18, W: []byte{lowerAlert, 0x00, 0x00}, R: nil}, + {Addr: 0x18, W: []byte{temperature}, R: []byte{0xc0, 0xf0}}, + {Addr: 0x18, W: []byte{critAlert}, R: []byte{0x00, 0xa0}}, + {Addr: 0x18, W: []byte{upperAlert}, R: []byte{0x00, 0x50}}, + }, + err: nil, + }, + { + name: "set critical error", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + err: errWritingCritAlert, + }, + { + name: "set upper error", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + }, + err: errWritingUpperAlert, + }, + { + name: "set lower error", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x50}, R: nil}, + }, + err: errWritingLowerAlert, + }, + { + name: "invalid alert config", + critical: physic.ZeroCelsius + 1*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + tx: []i2ctest.IO{}, + err: errAlertInvalid, + }, + { + name: "temperature io error", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x50}, R: nil}, + {Addr: 0x18, W: []byte{lowerAlert, 0x00, 0x00}, R: nil}, + }, + err: errReadTemperature, + }, + { + name: "read critical io error", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + temp: physic.ZeroCelsius + 15*physic.Kelvin, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x50}, R: nil}, + {Addr: 0x18, W: []byte{lowerAlert, 0x00, 0x00}, R: nil}, + {Addr: 0x18, W: []byte{temperature}, R: []byte{0xc0, 0xf0}}, + }, + err: errReadCriticalAlert, + }, + { + name: "read upper io error", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + temp: physic.ZeroCelsius + 15*physic.Kelvin, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x50}, R: nil}, + {Addr: 0x18, W: []byte{lowerAlert, 0x00, 0x00}, R: nil}, + {Addr: 0x18, W: []byte{temperature}, R: []byte{0xc0, 0xf0}}, + {Addr: 0x18, W: []byte{critAlert}, R: []byte{0x00, 0xa0}}, + }, + err: errReadUpperAlert, + }, + { + name: "read lower io error", + critical: physic.ZeroCelsius + 10*physic.Kelvin, + upper: physic.ZeroCelsius + 5*physic.Kelvin, + lower: physic.ZeroCelsius, + temp: physic.ZeroCelsius - 1*physic.Kelvin, + alerts: nil, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0xa0}, R: nil}, + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x50}, R: nil}, + {Addr: 0x18, W: []byte{lowerAlert, 0x00, 0x00}, R: nil}, + {Addr: 0x18, W: []byte{temperature}, R: []byte{0x30, 0x10}}, + }, + err: errReadLowerAlert, + }, + } + + for _, tt := range tests { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + enabled: true, + } + temp, alerts, err := mcp9808.SenseWithAlerts(tt.lower, tt.upper, tt.critical) + if err != tt.err { + t.Errorf("SenseWithAlerts(%s) wanted err: %v but got: %v", tt.name, tt.err, err) + } + if temp != tt.temp { + t.Errorf("SenseWithAlerts(%s) wanted temp: %s but got: %s", tt.name, tt.temp, temp) + } + if !reflect.DeepEqual(alerts, tt.alerts) { + t.Errorf("SenseWithAlerts(%s) expected alerts %+v but got %+v ", tt.name, tt.alerts, alerts) + } + } +} + +func TestDevHalt(t *testing.T) { + tests := []struct { + name string + tx []i2ctest.IO + enabled bool + want bool + err error + }{ + { + name: "shutdown", + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{configuration, 0x01, 0x00}, R: nil}, + }, + enabled: true, + want: false, + err: nil, + }, + { + name: "io error", + tx: []i2ctest.IO{ + // {Addr: 0x18, W: []byte{configuration, 0x01, 0x00}, R: nil}, + }, + enabled: true, + want: true, + err: errWritingConfiguration, + }, + } + for _, tt := range tests { + + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + enabled: tt.enabled, + } + + if err := mcp9808.Halt(); err != tt.err { + t.Errorf("Halt(%s) wanted error: %v but got: %v", tt.name, tt.err, err) + } + if mcp9808.enabled != tt.want { + t.Errorf("Halt(%s) expected dev to be: %t but is: %t", tt.name, tt.want, mcp9808.enabled) + } + } +} + +func TestDevString(t *testing.T) { + mcp9808 := &Dev{} + want := "MCP9808" + if want != mcp9808.String() { + t.Errorf("mpc9808.String() expected %s but got %s", want, mcp9808.String()) + } +} + +func TestDev_enable(t *testing.T) { + tests := []struct { + name string + tx []i2ctest.IO + enabled bool + want bool + err error + }{ + { + name: "shutdown", + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{configuration, 0x00, 0x00}, R: nil}, + }, + enabled: false, + want: true, + err: nil, + }, + { + name: "io error", + tx: []i2ctest.IO{ + // {Addr: 0x18, W: []byte{configuration, 0x01, 0x00}, R: nil}, + }, + enabled: false, + want: false, + err: errWritingConfiguration, + }, + } + for _, tt := range tests { + + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + enabled: tt.enabled, + } + + if err := mcp9808.enable(); err != tt.err { + t.Errorf("enable(%s) wanted error: %v but got: %v", tt.name, tt.err, err) + } + if mcp9808.enabled != tt.want { + t.Errorf("enable(%s) expected dev to be: %t but is: %t", tt.name, tt.want, mcp9808.enabled) + } + } +} + +func TestDev_setResolution(t *testing.T) { + succeeds := []struct { + name string + res resolution + tx []i2ctest.IO + err error + }{ + { + name: "Low", + res: Low, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{resolutionConfig, 0x00}, R: nil}, + }, + err: nil, + }, { + name: "Medium", + res: Medium, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{resolutionConfig, 0x01}, R: nil}, + }, + err: nil, + }, { + name: "High", + res: High, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{resolutionConfig, 0x02}, R: nil}, + }, + err: nil, + }, { + name: "Maximum", + res: Maximum, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{resolutionConfig, 0x03}, R: nil}, + }, + err: nil, + }, + } + + fails := []struct { + name string + res resolution + tx []i2ctest.IO + err error + }{ + { + name: "Low", + res: Low, + err: errWritingResolution, + }, { + name: "Medium", + res: Medium, + err: errWritingResolution, + }, { + name: "High", + res: High, + err: errWritingResolution, + }, { + name: "Maximum", + res: Maximum, + err: errWritingResolution, + }, { + name: "Unknown", + res: resolution(5), + err: errInvalidResolution, + }, + } + + for _, tt := range succeeds { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + } + if err := mcp9808.setResolution(tt.res); err != tt.err { + t.Errorf("setResolution(%s) expected %v but got %v", tt.name, tt.err, err) + } + + } + + for _, tt := range fails { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + } + if err := mcp9808.setResolution(tt.res); err != tt.err { + t.Errorf("setResolution(%s) expected %v but got %v", tt.name, tt.err, err) + } + } +} + +func TestDev_setCriticalAlert(t *testing.T) { + tests := []struct { + name string + alert physic.Temperature + crit physic.Temperature + tx []i2ctest.IO + err error + }{ + { + name: "0C", + alert: physic.ZeroCelsius, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{critAlert, 0x00, 0x00}, R: nil}, + }, + err: nil, + }, + { + name: "126C", + alert: physic.ZeroCelsius + 126*physic.Kelvin, + tx: []i2ctest.IO{}, + err: errAlertOutOfRange, + }, + { + name: "io error", + alert: physic.ZeroCelsius, + tx: []i2ctest.IO{}, + err: errWritingCritAlert, + }, + { + name: "nop", + alert: physic.ZeroCelsius, + crit: physic.ZeroCelsius, + tx: []i2ctest.IO{}, + err: nil, + }, + } + + for _, tt := range tests { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + critical: tt.crit, + } + if err := mcp9808.setCriticalAlert(tt.alert); err != tt.err { + t.Errorf("setCriticalAlert(%s) expected %v but got %v", tt.name, tt.err, err) + } + } +} + +func TestDev_setUpperAlert(t *testing.T) { + tests := []struct { + name string + alert physic.Temperature + tx []i2ctest.IO + upper physic.Temperature + err error + }{ + { + name: "0C", + alert: physic.ZeroCelsius, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{upperAlert, 0x00, 0x00}, R: nil}, + }, + err: nil, + }, + { + name: "126C", + alert: physic.ZeroCelsius + 126*physic.Kelvin, + tx: []i2ctest.IO{}, + err: errAlertOutOfRange, + }, + { + name: "io error", + alert: physic.ZeroCelsius, + tx: []i2ctest.IO{}, + err: errWritingUpperAlert, + }, + { + name: "nop", + alert: physic.ZeroCelsius, + upper: physic.ZeroCelsius, + tx: []i2ctest.IO{}, + err: nil, + }, + } + + for _, tt := range tests { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + upper: tt.upper, + } + if err := mcp9808.setUpperAlert(tt.alert); err != tt.err { + t.Errorf("setUpperAlert(%s) expected %v but got %v", tt.name, tt.err, err) + } + } +} + +func TestDev_setLowerAlert(t *testing.T) { + tests := []struct { + name string + alert physic.Temperature + tx []i2ctest.IO + lower physic.Temperature + err error + }{ + { + name: "0C", + alert: physic.ZeroCelsius, + tx: []i2ctest.IO{ + {Addr: 0x18, W: []byte{lowerAlert, 0x00, 0x00}, R: nil}, + }, + err: nil, + }, + { + name: "126C", + alert: physic.ZeroCelsius + 126*physic.Kelvin, + tx: []i2ctest.IO{}, + err: errAlertOutOfRange, + }, + { + name: "io error", + alert: physic.ZeroCelsius, + tx: []i2ctest.IO{}, + err: errWritingLowerAlert, + }, + { + name: "nop", + alert: physic.ZeroCelsius, + lower: physic.ZeroCelsius, + tx: []i2ctest.IO{}, + err: nil, + }, + } + + for _, tt := range tests { + bus := i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + + mcp9808 := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x18}, + Order: binary.BigEndian}, + lower: tt.lower, + } + if err := mcp9808.setLowerAlert(tt.alert); err != tt.err { + t.Errorf("setLowerAlert(%s) expected %v but got %v", tt.name, tt.err, err) + } + } +} + +func Test_alertBitsToTemperature(t *testing.T) { + tests := []struct { + name string + bits uint16 + want physic.Temperature + }{ + {"0°C", 0x0000, physic.ZeroCelsius}, + {"0.25°C", 0x0004, physic.ZeroCelsius + 250*physic.MilliKelvin}, + {"-0.25°C", 0x1004, physic.ZeroCelsius - 250*physic.MilliKelvin}, + } + + for _, tt := range tests { + if got := alertBitsToTemperature(tt.bits); got != tt.want { + t.Errorf("alertBitsToTemperature(%s) = %v, want %v", tt.name, got, tt.want) + } + } +} + +func Test_temperatureToAlertBits(t *testing.T) { + succeeds := []struct { + name string + alert physic.Temperature + want uint16 + }{ + {"0°C", physic.ZeroCelsius, 0x0000}, + {"0.25°C", physic.ZeroCelsius + 250*physic.MilliKelvin, 0x0004}, + {"-0.25°C", physic.ZeroCelsius - 250*physic.MilliKelvin, 0x1004}, + {"124.75°C", physic.ZeroCelsius + 124750*physic.MilliKelvin, 0x07cc}, + {"-39.75°C", physic.ZeroCelsius - 39750*physic.MilliKelvin, 0x127c}, + } + + fails := []struct { + name string + alert physic.Temperature + want error + }{ + {"126°C", physic.ZeroCelsius + 126*physic.Kelvin, errAlertOutOfRange}, + {"-41°C", physic.ZeroCelsius - 41*physic.Kelvin, errAlertOutOfRange}, + } + + for _, tt := range succeeds { + got, err := alertTemperatureToBits(tt.alert) + if got != tt.want && err == nil { + t.Errorf("alertBitsToTemperature(%s) = %x, want %x", tt.name, got, tt.want) + } + if err != nil { + t.Errorf("alertBitsToTemperature(%s) got unexpected error: %v", tt.name, err) + } + } + + for _, tt := range fails { + if _, err := alertTemperatureToBits(tt.alert); err == nil { + t.Errorf("alertBitsToTemperature(%s) expected error %v", tt.name, tt.want) + } + } +} diff --git a/experimental/devices/mcp9808/mcp9808smoketest/mcp9808smoketest.go b/experimental/devices/mcp9808/mcp9808smoketest/mcp9808smoketest.go new file mode 100644 index 0000000..1e854cd --- /dev/null +++ b/experimental/devices/mcp9808/mcp9808smoketest/mcp9808smoketest.go @@ -0,0 +1,76 @@ +// Copyright 2018 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package mcp9808smoketest + +import ( + "errors" + "flag" + "fmt" + + "periph.io/x/periph/conn/i2c/i2creg" + "periph.io/x/periph/experimental/devices/mcp9808" + "periph.io/x/periph/host" +) + +// SmokeTest is imported by periph-smoketest. +type SmokeTest struct { +} + +// Name implements the SmokeTest interface. +func (s *SmokeTest) Name() string { + return "mcp9808" +} + +// Description implements the SmokeTest interface. +func (s *SmokeTest) Description() string { + return "Tests MCP9808 over I²C" +} + +// Run implements the SmokeTest interface. +func (s *SmokeTest) Run(f *flag.FlagSet, args []string) (err error) { + i2cID := f.String("i2c", "", "I²C bus to use") + i2cAddr := f.Int("ia", 0x18, "I²C bus address use: 0x18 to 0x1f") + if err := f.Parse(args); err != nil { + return err + } + if f.NArg() != 0 { + f.Usage() + return errors.New("unrecognized arguments") + } + + fmt.Println("Starting MCP9808 Temperature Sensor\nctrl+c to exit") + if _, err := host.Init(); err != nil { + return err + } + + // Open default i2c bus. + bus, err := i2creg.Open(*i2cID) + if err != nil { + return err + } + defer func() { + if err2 := bus.Close(); err == nil { + err = err2 + } + }() + + // Create a new temperature sensor a with maximum resolution. + config := mcp9808.Opts{ + Addr: *i2cAddr, + Res: mcp9808.Maximum, + } + + sensor, err := mcp9808.New(bus, &config) + if err != nil { + return err + } + t, err := sensor.SenseTemp() + if err != nil { + return err + } + fmt.Println(t) + + return nil +}