diff --git a/experimental/devices/ccs811/ccs811.go b/experimental/devices/ccs811/ccs811.go new file mode 100644 index 0000000..3494b6a --- /dev/null +++ b/experimental/devices/ccs811/ccs811.go @@ -0,0 +1,423 @@ +// Copyright 2019 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 ccs811 + +import ( + "fmt" + + "periph.io/x/periph/conn/physic" + + "periph.io/x/periph/conn" + "periph.io/x/periph/conn/i2c" +) + +// MeasurementMode represents different ways how data is read +type MeasurementMode byte + +// Different measurement mode constants: +// +// - Mode 0: Idle, low current mode. +// +// - Mode 1: Constant power mode, IAQ measurement every second. +// +// - Mode 2: Pulse heating mode IAQ measurement every 10 seconds. +// +// - Mode 3: Low power pulse heating mode IAQ measurement every 60 seconds. +// +// - Mode 4: Constant power mode, sensor measurement every 250ms. +const ( + MeasurementModeIdle MeasurementMode = 0 + MeasurementModeConstant1000 MeasurementMode = 1 + MeasurementModePulse MeasurementMode = 2 + MeasurementModeLowPower MeasurementMode = 3 + MeasurementModeConstant250 MeasurementMode = 4 +) + +// NeededData represents set of data read from the sensor. +type NeededData byte + +// What data should be read from the sensor. +const ( + ReadCO2 NeededData = 2 + ReadCO2VOC NeededData = 4 + ReadCO2VOCStatus NeededData = 5 + ReadAll NeededData = 8 +) + +// SensorErrorID represents error reported by the sensor. +type SensorErrorID byte + +// Error constants, applicable if status registers signals error. +const ( + //The CCS811 received an I2C write request addressed to this station but with invalid register address ID. + writeRegInvalid SensorErrorID = 0x1 + //The CCS811 received an I2C read request to a mailbox ID that is invalid. + readRegInvalid SensorErrorID = 0x2 + //The CCS811 received an I2C request to write an unsupported mode to MEAS_MODE. + measModeInvalid SensorErrorID = 0x4 + //The sensor resistance measurement has reached or exceeded the maximum range. + maxResistance SensorErrorID = 0x8 + //The Heater current in the CCS811 is not in range. + heaterFault SensorErrorID = 0x10 + //The Heater voltage is not being applied correctly. + heaterSupply SensorErrorID = 0x20 +) + +func (d *Dev) errorCodeToError(errorCode SensorErrorID) error { + if errorCode == 0 { + return nil + } + errorText := "" + switch errorCode { + case writeRegInvalid: + errorText = "WRITE_REG_INVALID: The CCS811 received an I2C write request addressed to this station but with invalid register address ID." + case readRegInvalid: + errorText = "READ_REG_INVALID: The CCS811 received an I2C read request to a mailbox ID that is invalid." + case measModeInvalid: + errorText = "MEASMODE_INVALID: The CCS811 received an I2C request to write an unsupported mode to MEAS_MODE." + case maxResistance: + errorText = "MAX_RESISTANCE: The sensor resistance measurement has reached or exceeded the maximum range." + case heaterFault: + errorText = "HEATER_FAULT: The Heater current in the CCS811 is not in range." + case heaterSupply: + errorText = "HEATER_SUPPLY: The Heater voltage is not being applied correctly." + default: + errorText = fmt.Sprintf("Uknwown error, code: %d", errorCode) + } + return fmt.Errorf("Sensor error: %s", errorText) +} + +// Opts holds the configuration options. The address must be 0x5A or 0x5B. +type Opts struct { + Addr uint16 + MeasurementMode MeasurementMode + InterruptWhenReady bool + UseThreshold bool +} + +// DefaultOpts are the safe default options. +var DefaultOpts = Opts{ + Addr: 0x5A, + MeasurementMode: MeasurementModeConstant1000, + InterruptWhenReady: false, + UseThreshold: false, +} + +// New creates a new driver for CCS811 VOC sensor. +func New(bus i2c.Bus, opts *Opts) (*Dev, error) { + if opts.Addr != 0x5A && opts.Addr != 0x5B { + return nil, fmt.Errorf("Invalid device address, only 0x5A or 0x5B are allowed") + } + + if opts.MeasurementMode > MeasurementModeConstant250 { + return nil, fmt.Errorf("Invalid measurement mode") + } + + dev := &Dev{ + c: &i2c.Dev{Bus: bus, Addr: opts.Addr}, + opts: *opts, + } + + // From boot mode to measurement mode. + if err := dev.StartSensorApp(); err != nil { + return nil, fmt.Errorf("Error transitioning from boot do app mode: %v", err) + } + mmp := &MeasurementModeParams{MeasurementMode: opts.MeasurementMode, + GenerateInterrupt: opts.InterruptWhenReady, + UseThreshold: opts.UseThreshold} + + if err := dev.SetMeasurementModeRegister(*mmp); err != nil { + return nil, fmt.Errorf("Error setting measurement mode: %v", err) + } + + return dev, nil +} + +// Dev is an handle to an CCS811 sensor. +type Dev struct { + c conn.Conn + opts Opts +} + +const ( //Sensor's registers. + statusReg byte = 0x00 + measurementModeReg byte = 0x01 + algoResultsReg byte = 0x02 + rawDataReg byte = 0x03 + environmentReg byte = 0x05 + baselineReg byte = 0x11 + resetReg byte = 0xFF +) + +func (d *Dev) String() string { + return "CCS811" +} + +// StartSensorApp initializes sensor to application mode. +func (d *Dev) StartSensorApp() error { + return d.c.Tx([]byte{0xf4}, nil) +} + +// SetMeasurementModeRegister sets one of the 5 measurement modes, interrupt generation +// and interrupt threshold. +func (d *Dev) SetMeasurementModeRegister(mmp MeasurementModeParams) error { + mesModeValue := (mmp.MeasurementMode << 4) + if mmp.GenerateInterrupt { + mesModeValue = mesModeValue | (0x1 << 3) + } + if mmp.UseThreshold { + mesModeValue = mesModeValue | (0x1 << 2) + } + + return d.c.Tx([]byte{measurementModeReg, byte(mesModeValue)}, nil) +} + +// MeasurementModeParams is a structure representing Measuremode register of the sensor. +type MeasurementModeParams struct { + MeasurementMode MeasurementMode + GenerateInterrupt bool // True if sensor should generate interrupts on new measurement. + UseThreshold bool // True if sensor should use thresholds from threshold register. +} + +// GetMeasurementModeRegister returns current measurement mode of the sensor. +func (d *Dev) GetMeasurementModeRegister() (MeasurementModeParams, error) { + r := make([]byte, 1) + + if err := d.c.Tx([]byte{measurementModeReg}, r); err != nil { + return MeasurementModeParams{}, err + } + mode := MeasurementMode(r[0] >> 4) + threshold := (r[0]&4 == 4) + interrupt := (r[0]&8 == 8) + + return MeasurementModeParams{MeasurementMode: mode, GenerateInterrupt: interrupt, UseThreshold: threshold}, nil +} + +// Reset sets device into the BOOT mode. +func (d *Dev) Reset() error { + if err := d.c.Tx([]byte{resetReg, 0x11, 0xE5, 0x72, 0x8A}, nil); err != nil { + return err + } + return nil +} + +// ReadStatus returns value of status register. +func (d *Dev) ReadStatus() (byte, error) { + r := make([]byte, 1) + if err := d.c.Tx([]byte{statusReg}, r); err != nil { + return 0, err + } + return r[0], nil +} + +// ReadRawData provides current and voltage on the sensor. +// Current is in range of 0-63uA. Voltage is in range 0-1.65V. +func (d *Dev) ReadRawData() (physic.ElectricCurrent, physic.ElectricPotential, error) { + r := make([]byte, 2) + if err := d.c.Tx([]byte{rawDataReg}, r); err != nil { + return 0, 0, err + } + current, voltage := valuesFromRawData(r) + return current, voltage, nil +} + +// SetEnvironmentData allows to provide temperature and humidity so +// sensor can compensate it's measurement. +func (d *Dev) SetEnvironmentData(temp, humidity float32) error { + rawTemp := uint16((temp + 25) * 512) + rawHum := uint16(humidity * 512) + w := []byte{environmentReg, + byte(rawHum >> 8), + byte(rawHum), + byte(rawTemp >> 8), + byte(rawTemp)} + + if err := d.c.Tx(w, nil); err != nil { + return err + } + return nil +} + +// GetBaseline provides current baseline used by internal measurement alogrithm. +// For better understanding how to use this value, check the SetBaseline and documentation. +func (d *Dev) GetBaseline() ([]byte, error) { + r := make([]byte, 2) + if err := d.c.Tx([]byte{baselineReg}, r); err != nil { + return nil, err + } + return r, nil +} + +// SetBaseline sets current baseline for internal measurement algorithm. +// For more details check sensor's specification. +// +// Manual Baseline Correction. +// +// There is a mechanism within CCS811 to manually save and restore a previously +// saved baseline value using the BASELINE register. The correct time to save +// the baseline will depend on the customer use-case and application. +// +// For devices which are powered for >24 hours at a time: +// +// - During the first 500 hours – save the baseline every 24-48 hours. +// +// - After the first 500 hours – save the baseline every 5-7 days. +// +// For devices which are powered <24 hours at a time: +// +// - If the device is run in, save the baseline before power down. +// +// - If multiple operating modes are used, a separate baseline should be stored for each. +// +// - The baseline should only be restored when the resistance is stable +// (typically 20-30 minutes). +// +// - If changing from a low to high power mode (without spending at least +// 10 minutes in idle), the sensor resistance should be allowed to settle again +// before restoring the baseline. +// +// Note(s): +// +// 1) If a value is written to the BASELINE register while the sensor +// is stabilising, the output of the TVOC and eCO2 calculations may be higher +// than expected. +// +// 2) The baseline must be written after the conditioning period +func (d *Dev) SetBaseline(baseline []byte) error { + w := []byte{baselineReg, baseline[0], baseline[1]} + if err := d.c.Tx(w, nil); err != nil { + return err + } + return nil +} + +// SensorValues represents data read from the sensor. +// Data are populated based on NeededData parameter. +// +// Sensor provides eCO2 measurement in range: 400ppm to 8192ppm, +// and VOC measurement in range: 0ppb to 1187ppb. +// +// Sensing resistor's current is between 0-63uA, and voltage 0-1.65V. +// +// Status represents sensor's status register. +// 1001 0110 +// ||||||||| +// ||||||| \- 1 = There is an error. +// |||||| \- Reserved. +// ||||| \- Reserved. +// |||| \- 1 = Data ready. +// ||| \- 1 = Valid application firmware loaded. +// || \- Reserved. +// | \- Reserved. +// \- 0 = Firmware in boot mode, 1 Firmware in application mode. +// +// Error represents error state of the sensor if available, otherwise is nil. +type SensorValues struct { + ECO2 int + VOC int + Status byte + Error error + RawDataCurrent physic.ElectricCurrent + RawDataVoltage physic.ElectricPotential +} + +// Sense provides data from the sensor. +// This function read all 8 available bytes including error, raw data etc. +// If you want just eCO2 and/or VOC, use SensePartial. +func (d *Dev) Sense(values *SensorValues) error { + return d.SensePartial(ReadAll, values) +} + +// SensePartial provides marginaly more efficient reading from the sensor. +// You can specify what subset of data you want through NeededData constants. +func (d *Dev) SensePartial(requested NeededData, values *SensorValues) error { + read := make([]byte, requested) + if err := d.c.Tx([]byte{algoResultsReg}, read); err != nil { + return err + } + if requested >= ReadCO2 { + // Exptected range: 400ppm to 8192ppm. + // 0x3F is used to erase randomly set top bits, + // causing value out of range given by specs. + values.ECO2 = int(uint32(read[0]&0x3F)<<8 | uint32(read[1])) + } + if requested >= ReadCO2VOC { + // Expected range: 0ppb to 1187ppb. + // 0x7 is used to erase randomly set top bits + // causing value out of range given by specs. + values.VOC = int(uint32(read[2]&0x7)<<8 | uint32(read[3])) + } + if requested >= ReadCO2VOCStatus { + values.Status = read[4] + } + if requested == ReadAll { + values.Error = d.errorCodeToError(SensorErrorID(read[5])) + values.RawDataCurrent, values.RawDataVoltage = valuesFromRawData(read[6:]) + } + + return nil +} + +// Parse current and voltage from raw data. +func valuesFromRawData(data []byte) (physic.ElectricCurrent, physic.ElectricPotential) { + c := physic.ElectricCurrent(int64(data[0]>>2) * 1000) + sensorsVoltageUnits := int64((uint16(data[0]&0x03) << 8) | uint16(data[1])) + // 1.65V = 1023 + // sensorsVoltageUnits is converted to V, and after that to nV. + // 165 is used instead of 1.65 to prevent types truncation. + p := physic.ElectricPotential((sensorsVoltageUnits * 165 * (1000 * 1000 * 1000) / 102300)) + return c, p +} + +// FwVersions is a strcutre which aggregates all different versions of sensors features. +// +// HWIdentifier - for family of CCS81x should be 0x81. +// +// HWVersion - hardware major and minor version: 0x1X. +// +// BootVersion - version of firmware bootloader in form Major.Minor.Trivial. +// +// ApplicationVersion - version of firmware application in form Major.Minor.Trivial. +type FwVersions struct { + HWIdentifier byte + HWVersion byte + BootVersion string + ApplicationVersion string +} + +// GetFirmwareData populates FwVersions structure with data. +func (d *Dev) GetFirmwareData() (*FwVersions, error) { + version := &FwVersions{} + buffer1 := make([]byte, 1) + + if err := d.c.Tx([]byte{0x20}, buffer1); err != nil { + return version, err + } + version.HWIdentifier = buffer1[0] + + if err := d.c.Tx([]byte{0x21}, buffer1); err != nil { + return version, err + } + version.HWVersion = buffer1[0] + + buffer2 := make([]byte, 2) + if err := d.c.Tx([]byte{0x23}, buffer2); err != nil { + return version, err + } + minor := buffer2[0] & 0x0F + major := (buffer2[0] & 0xF0) >> 4 + trivial := buffer2[1] + version.BootVersion = fmt.Sprintf("%d.%d.%d", major, minor, trivial) + + if err := d.c.Tx([]byte{0x24}, buffer2); err != nil { + return version, err + } + minor = buffer2[0] & 0x0F + major = (buffer2[0] & 0xF0) >> 4 + trivial = buffer2[1] + version.ApplicationVersion = fmt.Sprintf("%d.%d.%d", major, minor, trivial) + + return version, nil +} diff --git a/experimental/devices/ccs811/ccs811_test.go b/experimental/devices/ccs811/ccs811_test.go new file mode 100644 index 0000000..63d7213 --- /dev/null +++ b/experimental/devices/ccs811/ccs811_test.go @@ -0,0 +1,214 @@ +// Copyright 2019 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 ccs811 + +import ( + "fmt" + "testing" + + "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/physic" +) + +func TestBasicInitialisationAndDataRead(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x5A, W: []byte{0xf4}, R: nil}, + {Addr: 0x5A, W: []byte{measurementModeReg, 0x1C}, R: nil}, + {Addr: 0x5A, W: []byte{algoResultsReg}, R: []byte{0x1, 0x2, 0x2, 0x3, 0xF, 0x8, 0xF, 0xF}}, + }, + DontPanic: true, + } + + opts := DefaultOpts + opts.InterruptWhenReady = true + opts.UseThreshold = true + + dev, err := New(&bus, &opts) + if err != nil { + t.Fatal(err) + } + + data := &SensorValues{} + if err := dev.Sense(data); err == nil { + var vExpected physic.ElectricPotential + vExpected.Set("1.65V") // 682 units + var cExpected physic.ElectricCurrent + cExpected.Set("63uA") + if data.ECO2 != 0x102 && + data.VOC != 0x203 && + data.Status != 0xF && + data.Error != fmt.Errorf("Sensor error: %s", "HEATER_FAULT: The Heater current in the CCS811 is not in range.") && + data.RawDataCurrent != cExpected && + data.RawDataVoltage != vExpected { + t.Fatalf("Data parsed incorrectly, got %v", data) + } + } else { + t.Fatal(err) + } +} + +func TestMeasurementModeRegisterRead(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x5A, W: []byte{0xf4}, R: nil}, + {Addr: 0x5A, W: []byte{measurementModeReg, 0x4C}, R: nil}, + {Addr: 0x5A, W: []byte{measurementModeReg}, R: []byte{0x4C}}, + }, + DontPanic: true, + } + + opts := DefaultOpts + opts.MeasurementMode = MeasurementModeConstant250 + opts.InterruptWhenReady = true + opts.UseThreshold = true + + dev, err := New(&bus, &opts) + if err != nil { + t.Fatal(err) + } + mode, err := dev.GetMeasurementModeRegister() + if err != nil || + mode.GenerateInterrupt != true || + mode.UseThreshold != true || + mode.MeasurementMode != MeasurementModeConstant250 { + t.Fatalf("Parsing of Measurement Mode register failed. Got: %+v", mode) + } + +} +func TestGetFirmwareData(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x5A, W: []byte{0xf4}, R: nil}, + {Addr: 0x5A, W: []byte{measurementModeReg, 0x4C}, R: nil}, + {Addr: 0x5A, W: []byte{0x20}, R: []byte{0x81}}, + {Addr: 0x5A, W: []byte{0x21}, R: []byte{0x15}}, + {Addr: 0x5A, W: []byte{0x23}, R: []byte{0x12, 0x03}}, + {Addr: 0x5A, W: []byte{0x24}, R: []byte{0x89, 0x20}}, + }, + DontPanic: true, + } + + opts := DefaultOpts + opts.MeasurementMode = MeasurementModeConstant250 + opts.InterruptWhenReady = true + opts.UseThreshold = true + + dev, err := New(&bus, &opts) + if err != nil { + t.Fatal(err) + } + versions, err := dev.GetFirmwareData() + if err != nil || + versions.HWIdentifier != 0x81 || + versions.HWVersion != 0x15 || + versions.BootVersion != "1.2.3" || + versions.ApplicationVersion != "8.9.32" { + t.Fatalf("Parsing of firmware version data failed. Got: %+v", versions) + } + +} + +func TestInvalidSensorAddress(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x0, W: nil, R: nil}, + }, + } + if dev, err := New(&bus, &Opts{Addr: 0xFF, MeasurementMode: MeasurementModeConstant1000}); dev != nil || err == nil { + t.Fatal("New should have failed") + } +} + +func TestSetEnvironmentData(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x5A, W: []byte{0xf4}, R: nil}, + {Addr: 0x5A, W: []byte{measurementModeReg, 0x10}, R: nil}, + {Addr: 0x5A, W: []byte{environmentReg, 0x61, 0x00, 0x64, 0x00}, R: nil}, + {Addr: 0x5A, W: []byte{environmentReg, 0x64, 0x00, 0x61, 0x00}, R: nil}, + }, + } + dev, err := New(&bus, &DefaultOpts) + if err != nil { + t.Fatal(err) + } + dev.SetEnvironmentData(25, 48.5) + dev.SetEnvironmentData(23.5, 50) +} + +func TestBaseline(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x5A, W: []byte{0xf4}, R: nil}, + {Addr: 0x5A, W: []byte{measurementModeReg, 0x10}, R: nil}, + {Addr: 0x5A, W: []byte{baselineReg}, R: []byte{0xAA, 0xDD}}, + {Addr: 0x5A, W: []byte{baselineReg, 0xAA, 0xDD}, R: nil}, + }, + } + dev, err := New(&bus, &DefaultOpts) + if err != nil { + t.Fatal(err) + } + base, err := dev.GetBaseline() + if err != nil { + t.Fatal(err) + } + dev.SetBaseline(base) +} + +func TestReadRawData(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x5A, W: []byte{0xf4}, R: nil}, + {Addr: 0x5A, W: []byte{measurementModeReg, 0x10}, R: nil}, + {Addr: 0x5A, W: []byte{rawDataReg}, R: []byte{0x96, 0xAA}}, + }, + } + dev, err := New(&bus, &DefaultOpts) + if err != nil { + t.Fatal(err) + } + cur, vol, err := dev.ReadRawData() + if err != nil { + t.Fatal(err) + } + var vExpected physic.ElectricPotential + vExpected.Set("1.1V") // 682 units + var cExpected physic.ElectricCurrent + cExpected.Set("37uA") + + if cur != cExpected || vol != vExpected { + t.Fatalf("Raw data reading failed got values: %d, %d", cur, vol) + } +} + +func TestRawDataParsing(t *testing.T) { + var vExpected physic.ElectricPotential + vExpected.Set("0.825806451V") // 512 units + var cExpected physic.ElectricCurrent + cExpected.Set("62uA") + c, v := valuesFromRawData([]byte{0xFA, 0x0}) + if c != cExpected && v != vExpected { + t.Fatal("current and/or voltage data parsed incorrectly") + } +} + +func TestReset(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x5B, W: []byte{0xf4}, R: nil}, + {Addr: 0x5B, W: []byte{measurementModeReg, 0x10}, R: nil}, + {Addr: 0x5B, W: []byte{resetReg, 0x11, 0xE5, 0x72, 0x8A}, R: nil}, + }, + } + opts := &DefaultOpts + opts.Addr = 0x5B + dev, err := New(&bus, opts) + if err != nil { + t.Fatal(err) + } + dev.Reset() +} diff --git a/experimental/devices/ccs811/doc.go b/experimental/devices/ccs811/doc.go new file mode 100644 index 0000000..d5ed0d3 --- /dev/null +++ b/experimental/devices/ccs811/doc.go @@ -0,0 +1,11 @@ +// Copyright 2019 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 ccs811 controls CCS811 Volatile Organic Compounds sensor via +// I²C interface. +// +// Datasheet +// +// https://ams.com/documents/20143/36005/CCS811_DS000459_6-00.pdf +package ccs811