169 lines
4.1 KiB
Go
169 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|