nicebadge/examples/co2-sensor/main.go

170 lines
4.1 KiB
Go
Raw Permalink Normal View History

2026-04-18 11:01:06 +00:00
package main
// CO2 sensor example using SCD4x (I2C).
// Connect the sensor to I2C1: SDA=P0_17, SCL=P0_20 (3.3V power).
//
// The display background changes color based on CO2 level:
// green → < 800 ppm (good air quality)
// yellow → < 1500 ppm (ventilate soon)
// red → ≥ 1500 ppm (ventilate now!)
//
// The WS2812 LEDs mirror the same color, and the buzzer sounds a
// warning tone when CO2 exceeds the danger threshold.
import (
"image/color"
"machine"
"strconv"
"time"
"tinygo.org/x/drivers/scd4x"
"tinygo.org/x/drivers/st7789"
"tinygo.org/x/drivers/ws2812"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freesans"
)
var (
display st7789.Device
leds ws2812.Device
bzrPin machine.Pin
co2dev *scd4x.Device
)
func main() {
time.Sleep(3 * time.Second)
machine.SPI0.Configure(machine.SPIConfig{
SCK: machine.P1_01,
SDO: machine.P1_02,
Frequency: 8000000,
Mode: 0,
})
machine.I2C1.Configure(machine.I2CConfig{
SDA: machine.P0_17,
SCL: machine.P0_20,
Frequency: 400000,
})
display = st7789.New(machine.SPI0,
machine.P1_15, // TFT_RESET
machine.P1_13, // TFT_DC
machine.P0_10, // TFT_CS
machine.P0_09) // TFT_LITE
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
Width: 135,
Height: 240,
RowOffset: 40,
ColumnOffset: 53,
})
neo := machine.P1_11
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
leds = ws2812.New(neo)
bzrPin = machine.P0_31
bzrPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
co2dev = scd4x.New(machine.I2C1)
co2dev.Configure()
display.FillScreen(color.RGBA{0, 0, 0, 255})
if err := co2dev.StartPeriodicMeasurement(); err != nil {
println(err)
}
var (
co2 int32
temp int32
hum int32
err error
bg color.RGBA
oldbg = color.RGBA{R: 0xff, G: 0xff, B: 0xff}
black = color.RGBA{0, 0, 0, 255}
)
for {
// SCD4x needs up to 5s between measurements; retry a few times
for i := 0; i < 5; i++ {
co2, err = co2dev.ReadCO2()
temp, _ = co2dev.ReadTemperature()
hum, _ = co2dev.ReadHumidity()
if err != nil {
println(err)
}
if co2 != 0 {
break
}
time.Sleep(200 * time.Millisecond)
}
switch {
case co2 < 800:
bg = color.RGBA{0, 220, 0, 255}
case co2 < 1500:
bg = color.RGBA{220, 220, 0, 255}
default:
bg = color.RGBA{220, 0, 0, 255}
}
if bg != oldbg {
display.FillScreen(bg)
oldbg = bg
}
// mirror CO2 status on the LEDs
leds.WriteColors([]color.RGBA{bg, bg})
// buzzer warning when CO2 is dangerously high
if co2 >= 1500 {
warn()
}
// display 240x135, text layout:
// y=40 CO2 level (most prominent)
// y=75 Temperature
// y=110 Humidity
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 40, "CO2: "+strconv.Itoa(int(co2))+" ppm", black)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 75, "Temp: "+formatMilliC(temp), black)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 110, "Hum: "+formatMilliPct(hum), black)
time.Sleep(time.Second)
// erase text by redrawing in background color (avoids full FillScreen flicker)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 40, "CO2: "+strconv.Itoa(int(co2))+" ppm", bg)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 75, "Temp: "+formatMilliC(temp), bg)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 110, "Hum: "+formatMilliPct(hum), bg)
}
}
// formatMilliC formats a millidegree Celsius value as "XX.X C".
func formatMilliC(mc int32) string {
if mc < 0 {
mc = -mc
return "-" + strconv.Itoa(int(mc/1000)) + "." + strconv.Itoa(int((mc%1000)/100)) + " C"
}
return strconv.Itoa(int(mc/1000)) + "." + strconv.Itoa(int((mc%1000)/100)) + " C"
}
// formatMilliPct formats a millipercent value as "XX.X %".
func formatMilliPct(mp int32) string {
return strconv.Itoa(int(mp/1000)) + "." + strconv.Itoa(int((mp%1000)/100)) + " %"
}
// warn sounds two short beeps on the buzzer.
func warn() {
for beep := 0; beep < 2; beep++ {
for i := 0; i < 100; i++ {
bzrPin.High()
time.Sleep(500 * time.Microsecond)
bzrPin.Low()
time.Sleep(500 * time.Microsecond)
}
time.Sleep(200 * time.Millisecond)
}
}