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) } }