diff --git a/README.md b/README.md new file mode 100644 index 0000000..48375f9 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Sensirion SCD30 CO2 sensor i2c driver module for Golang + +## Overview + +With this module Sensirion SCD30 CO2 sensor can be accessed throug i2c bus. +Implemented: +- starting and stopping continuous measurements +- checking for ready measurement +- reading ready measurement +- getting and setting temperature compensation + +## Example + +```go +package main + +import ( + "log" + "time" + + "github.com/pvainio/scd30" + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/host/v3" +) + +func main() { + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + bus, err := i2creg.Open("") + if err != nil { + log.Fatal(err) + } + defer bus.Close() + + dev, err := scd30.Open(bus) + if err != nil { + log.Fatal(err) + } + + var interval uint16 = 5 + + dev.StartMeasurements(interval) + + for { + time.Sleep(time.Duration(interval) * time.Second) + if hasMeasurement, err := dev.HasMeasurement(); err != nil { + log.Fatalf("error %v", err) + } else if !hasMeasurement { + return + } + + m, err := dev.GetMeasurement() + if err != nil { + log.Fatalf("error %v", err) + } + + log.Printf("Got measure %f ppm %f%% %fC", m.CO2, m.Humidity, m.Temperature) + } +} +``` diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..2112383 --- /dev/null +++ b/example_test.go @@ -0,0 +1,47 @@ +package scd30_test + +import ( + "log" + "time" + + "github.com/pvainio/scd30" + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/host/v3" +) + +func Example() { + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + bus, err := i2creg.Open("") + if err != nil { + log.Fatal(err) + } + defer bus.Close() + + dev, err := scd30.Open(bus) + if err != nil { + log.Fatal(err) + } + + var interval uint16 = 5 + + dev.StartMeasurements(interval) + + for { + time.Sleep(time.Duration(interval) * time.Second) + if hasMeasurement, err := dev.HasMeasurement(); err != nil { + log.Fatalf("error %v", err) + } else if !hasMeasurement { + return + } + + m, err := dev.GetMeasurement() + if err != nil { + log.Fatalf("error %v", err) + } + + log.Printf("Got measure %f ppm %f%% %fC", m.CO2, m.Humidity, m.Temperature) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ac7ee59 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/pvainio/scd30 + +go 1.17 + +require ( + github.com/sigurn/crc8 v0.0.0-20160107002456-e55481d6f45c + periph.io/x/conn/v3 v3.6.9 + periph.io/x/host/v3 v3.7.1 +) + +require github.com/sigurn/utils v0.0.0-20190728110027-e1fefb11a144 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..28a60cf --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/sigurn/crc8 v0.0.0-20160107002456-e55481d6f45c h1:hk0Jigjfq59yDMgd6bzi22Das5tyxU0CtOkh7a9io84= +github.com/sigurn/crc8 v0.0.0-20160107002456-e55481d6f45c/go.mod h1:cyrWuItcOVIGX6fBZ/G00z4ykprWM7hH58fSavNkjRg= +github.com/sigurn/utils v0.0.0-20190728110027-e1fefb11a144 h1:ccb8W1+mYuZvlpn/mJUMAbsFHTMCpcJBS78AsBQxNcY= +github.com/sigurn/utils v0.0.0-20190728110027-e1fefb11a144/go.mod h1:VRI4lXkrUH5Cygl6mbG1BRUfMMoT2o8BkrtBDUAm+GU= +periph.io/x/conn/v3 v3.6.9 h1:cSAvXC6IRRYC9pTW/Fzhp0a7zq+aeAxV8+/JZ+oxwZI= +periph.io/x/conn/v3 v3.6.9/go.mod h1:UqWNaPMosWmNCwtufoTSTTYhB2wXWsMRAJyo1PlxO4Q= +periph.io/x/d2xx v0.0.4/go.mod h1:38Euaaj+s6l0faIRHh32a+PrjXvxFTFkPBEQI0TKg34= +periph.io/x/host/v3 v3.7.1 h1:SAe/7IWSOoFsqh2/74+SxbqehzOPny+jAPs25fd/NUI= +periph.io/x/host/v3 v3.7.1/go.mod h1:kqMB+cJHtIPQCCMqDoiIMwr0pu1p+qQObkrPha3mX6E= diff --git a/scd30.go b/scd30.go new file mode 100644 index 0000000..e4ab0da --- /dev/null +++ b/scd30.go @@ -0,0 +1,213 @@ +package scd30 + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" + "sync" + "time" + + "periph.io/x/conn/v3/i2c" + + "github.com/sigurn/crc8" +) + +type Measurement struct { + CO2 float32 + Temperature float32 + Humidity float32 +} + +type SCD30 struct { + dev *i2c.Dev +} + +var mutex sync.Mutex + +func Open(bus i2c.Bus) (*SCD30, error) { + mutex.Lock() + defer mutex.Unlock() + + dev := &i2c.Dev{Addr: 0x61, Bus: bus} + + return &SCD30{dev: dev}, nil +} + +// StartMeasurements starts continous measerements at given interval seconds +func (dev SCD30) StartMeasurements(interval uint16) error { + mutex.Lock() + defer mutex.Unlock() + + // Set measurement interval + if err := dev.sendCommandArg(0x4600, interval); err != nil { + return err + } + + // Start continuous measurements + if err := dev.sendCommandArg(0x0010, 0); err != nil { + return err + } + + return nil +} + +// StopMeasurements stops continuous measurements +func (dev SCD30) StopMeasurements() error { + mutex.Lock() + defer mutex.Unlock() + return dev.sendCommand(0x0104) +} + +// GetTemperatureOffset gets temperature offset to compensate internal +// heating. Value is 1/100C +func (dev SCD30) GetTemperatureOffset() (uint16, error) { + mutex.Lock() + defer mutex.Unlock() + + if err := dev.sendCommand(0x5403); err != nil { + return 0, err + } + + data, err := dev.readData(3) + + if err != nil { + return 0, err + } + data, err = readValid16(bytes.NewBuffer(data)) + if err != nil { + return 0, err + } else { + return binary.BigEndian.Uint16(data), nil + } +} + +// SetTemperatureOffset sets temperature offset to compensate internal +// heating. Value is 1/100C +func (dev SCD30) SetTemperatureOffset(offset uint16) error { + mutex.Lock() + defer mutex.Unlock() + return dev.sendCommandArg(0x5403, offset) +} + +// GetMeasurement returns ready measurement. HasMeasurement should be +// used first to check if there is one. +func (dev SCD30) GetMeasurement() (*Measurement, error) { + mutex.Lock() + defer mutex.Unlock() + + if err := dev.sendCommand(0x0300); err != nil { + return nil, err + } + + if data, err := dev.readData(18); err != nil { + return nil, err + } else { + buf := bytes.NewBuffer(data) + co2, err := readValidFloat32(buf) + if err != nil { + return nil, err + } + temp, err := readValidFloat32(buf) + if err != nil { + return nil, err + } + hum, err := readValidFloat32(buf) + if err != nil { + return nil, err + } + return &Measurement{CO2: co2, Temperature: temp, Humidity: hum}, nil + } +} + +// HasMeasurement checks if there is ready measurement +func (dev SCD30) HasMeasurement() (bool, error) { + mutex.Lock() + defer mutex.Unlock() + + if err := dev.sendCommand(0x0202); err != nil { + return false, err + } + + if data, err := dev.readData(3); err != nil { + return false, err + } else { + if data[2] != crc(data[:2]) { + return false, fmt.Errorf("crc error, expected %x got %x", crc(data[:2]), data[2]) + } + return data[1] == 1, nil + } +} + +func (dev SCD30) readData(len int) ([]byte, error) { + + data := make([]byte, len) + + if err := dev.dev.Tx(nil, data); err != nil { + return nil, err + } else { + return data, nil + } +} + +func (dev SCD30) sendCommand(cmd uint16) error { + cmdData := make([]byte, 2) + binary.BigEndian.PutUint16(cmdData, cmd) + return dev.writeAndWait(cmdData) +} + +func (dev SCD30) sendCommandArg(cmd uint16, arg uint16) error { + cmdData := make([]byte, 2) + argData := make([]byte, 2) + binary.BigEndian.PutUint16(cmdData, cmd) + binary.BigEndian.PutUint16(argData, arg) + write := []byte{cmdData[0], cmdData[1], argData[0], argData[1], crc(argData)} + return dev.writeAndWait(write) +} + +func (dev SCD30) writeAndWait(data []byte) error { + + if err := dev.dev.Tx(data, nil); err != nil { + return err + } + + time.Sleep(4 * time.Millisecond) + + return nil +} + +func crc(data []byte) byte { + return crc8.Checksum(data, crcTable) +} + +func readValid16(buf *bytes.Buffer) ([]byte, error) { + data := buf.Next(2) + crc8, err := buf.ReadByte() + if err != nil { + return nil, err + } + if crc(data) != crc8 { + return nil, fmt.Errorf("crc error, expected %x got %x", crc(data), crc8) + } + return data, nil +} + +func readValidFloat32(buf *bytes.Buffer) (float32, error) { + var out bytes.Buffer + + for i := 0; i < 2; i++ { + if data, err := readValid16(buf); err != nil { + return float32(math.NaN()), err + } else { + out.Write(data) + } + } + uint := binary.BigEndian.Uint32(out.Bytes()) + return math.Float32frombits(uint), nil +} + +var crcTable *crc8.Table + +func init() { + crcTable = crc8.MakeTable(crc8.Params{Poly: 0x31, Init: 0xff, RefIn: false, RefOut: false, XorOut: 0x00, Check: 0xff, Name: "Sensirion"}) +} diff --git a/scd30_test.go b/scd30_test.go new file mode 100644 index 0000000..ec3d704 --- /dev/null +++ b/scd30_test.go @@ -0,0 +1,122 @@ +package scd30 + +import ( + "encoding/binary" + "fmt" + "math" + "testing" + + "periph.io/x/conn/v3/i2c/i2ctest" +) + +const addr = 0x61 + +func TestOpen(t *testing.T) { + scd30, _ := Open(&i2ctest.Record{}) + + if scd30.dev.Addr != 0x61 { + t.Fatalf("Invalid addr %v", scd30.dev.Addr) + } +} + +func TestGetTemperatureOffset(t *testing.T) { + + bus := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: addr, W: []byte{0x54, 0x03}, R: nil}, + {Addr: addr, W: nil, R: []byte{0x01, 0x23, 0xa0}}, + }, + } + + scd30, _ := Open(bus) + o, err := scd30.GetTemperatureOffset() + assertNoError(t, err) + if o != 0x123 { + t.Fatalf("Got incorrect offset %v should be %v", o, 0x123) + } +} + +func TestSetTemperatureOffset(t *testing.T) { + + bus := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: addr, W: []byte{0x54, 0x03, 0x1, 0x23, 0xa0}, R: nil}, + }, + } + + scd30, _ := Open(bus) + err := scd30.SetTemperatureOffset(0x123) + assertNoError(t, err) +} + +func printFloat(f float32) { + bits := math.Float32bits(f) + data := make([]byte, 4) + binary.BigEndian.PutUint32(data, bits) + var out []byte + crcdata := []byte{data[0], data[1]} + out = append(out, crcdata...) + out = append(out, crc(crcdata)) + crcdata = []byte{data[2], data[3]} + out = append(out, crcdata...) + out = append(out, crc(crcdata)) + fmt.Printf("f %f out %#v", f, out) +} + +func TestHasMeasurement(t *testing.T) { + bus := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: addr, W: []byte{0x02, 0x02}, R: nil}, + {Addr: addr, W: nil, R: []byte{0x00, 0x01, 0xb0}}, + }, + } + + scd30, _ := Open(bus) + o, err := scd30.HasMeasurement() + assertNoError(t, err) + if !o { + t.Fatalf("expected true") + } +} + +func TestGetMeasurement(t *testing.T) { + + bus := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: addr, W: []byte{0x03, 0x00}, R: nil}, + {Addr: addr, W: nil, R: []byte{0x3f, 0x8c, 0xad, 0xcc, 0xcd, 0x94, 0x40, 0xc, 0x75, 0xcc, 0xcd, 0x94, 0x40, 0x53, 0x25, 0x33, 0x33, 0x88}}, + }, + } + + scd30, _ := Open(bus) + o, err := scd30.GetMeasurement() + assertNoError(t, err) + assertFloat(t, 1.1, o.CO2) + assertFloat(t, 2.2, o.Temperature) + assertFloat(t, 3.3, o.Humidity) +} + +func TestStartMeasurements(t *testing.T) { + bus := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: addr, W: []byte{0x46, 0x00, 0x01, 0x23, 0xa0}, R: nil}, + {Addr: addr, W: []byte{0x00, 0x10, 0x00, 0x00, 0x81}, R: nil}, + }, + } + + scd30, _ := Open(bus) + err := scd30.StartMeasurements(0x123) + assertNoError(t, err) +} + +func assertFloat(t *testing.T, expected float32, value float32) { + if expected != value { + t.Fatalf("Expected %f got %f", expected, value) + } +} + +func assertNoError(t *testing.T, err error) { + if err != nil { + t.Fatalf("Got error %v", err) + } +}