Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
pvainio committed Nov 1, 2021
1 parent e7e4bc6 commit 7116ab3
Show file tree
Hide file tree
Showing 6 changed files with 466 additions and 0 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
}
}
```
47 changes: 47 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
213 changes: 213 additions & 0 deletions scd30.go
Original file line number Diff line number Diff line change
@@ -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"})
}
Loading

0 comments on commit 7116ab3

Please sign in to comment.