From 4e022d4e762c1be09d9d01960099b23d280bc167 Mon Sep 17 00:00:00 2001 From: Gregory Ballantine Date: Sat, 12 Sep 2026 02:43:51 -0400 Subject: [PATCH] [Issue #5] - Added a parameter to set which temperature sensor you're using in config.go --- config.go.example | 3 + main.go | 238 +++++++++++----------------------------------- sensor.go | 106 +++++++++++++++++++++ 3 files changed, 163 insertions(+), 184 deletions(-) create mode 100644 sensor.go diff --git a/config.go.example b/config.go.example index 93190f3..d6756b3 100644 --- a/config.go.example +++ b/config.go.example @@ -12,6 +12,9 @@ const ( Hostname = "pico2w-bme280" EnableICMP = true + // Sensor configuration + SensorType = "bme280" + MQTTHost = "192.168.1.50" // Must be an IP address; DNS resolution is not supported currently MQTTPort = "1883" MQTTUser = "" // Leave empty if not required diff --git a/main.go b/main.go index c66bdb9..e5d2299 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,13 @@ package main import ( - "bytes" "context" - "encoding/binary" "fmt" + "image/color" "io" "machine" "strconv" "time" - "image/color" "tinygo.org/x/drivers/ssd1306" "tinygo.org/x/tinyfont" @@ -103,151 +101,6 @@ func encodeLength(length int) []byte { return encoded } -// ------------------------------------------------------------- -// BME280 Driver -// ------------------------------------------------------------- -const ( - BME280Addr = 0x77 - regCalib00 = 0x88 - regCalib26 = 0xE1 - regReset = 0xE0 - regCtrlHum = 0xF2 - regCtrlMeas = 0xF4 - regConfig = 0xF5 - regData = 0xF7 -) - -type BME280Calib struct { - digT1 uint16 - digT2 int16 - digT3 int16 - digP1 uint16 - digP2 int16 - digP3 int16 - digP4 int16 - digP5 int16 - digP6 int16 - digP7 int16 - digP8 int16 - digP9 int16 - digH1 uint8 - digH2 int16 - digH3 uint8 - digH4 int16 - digH5 int16 - digH6 int8 -} - -type BME280 struct { - bus *machine.I2C - addr uint8 - calib BME280Calib -} - -func NewBME280(bus *machine.I2C, addr uint8) (*BME280, error) { - b := &BME280{bus: bus, addr: addr} - if err := bus.WriteRegister(addr, regReset, []byte{0xB6}); err != nil { - return nil, err - } - time.Sleep(100 * time.Millisecond) - - if err := b.readCalibration(); err != nil { - return nil, err - } - if err := bus.WriteRegister(addr, regCtrlHum, []byte{0x01}); err != nil { - return nil, err - } - if err := bus.WriteRegister(addr, regCtrlMeas, []byte{0x27}); err != nil { - return nil, err - } - if err := bus.WriteRegister(addr, regConfig, []byte{0xA0}); err != nil { - return nil, err - } - return b, nil -} - -func (b *BME280) readCalibration() error { - var buf24 [24]byte - if err := b.bus.ReadRegister(b.addr, regCalib00, buf24[:]); err != nil { - return err - } - r := bytes.NewReader(buf24[:]) - binary.Read(r, binary.LittleEndian, &b.calib.digT1) - binary.Read(r, binary.LittleEndian, &b.calib.digT2) - binary.Read(r, binary.LittleEndian, &b.calib.digT3) - binary.Read(r, binary.LittleEndian, &b.calib.digP1) - binary.Read(r, binary.LittleEndian, &b.calib.digP2) - binary.Read(r, binary.LittleEndian, &b.calib.digP3) - binary.Read(r, binary.LittleEndian, &b.calib.digP4) - binary.Read(r, binary.LittleEndian, &b.calib.digP5) - binary.Read(r, binary.LittleEndian, &b.calib.digP6) - binary.Read(r, binary.LittleEndian, &b.calib.digP7) - binary.Read(r, binary.LittleEndian, &b.calib.digP8) - binary.Read(r, binary.LittleEndian, &b.calib.digP9) - - var h1 [1]byte - if err := b.bus.ReadRegister(b.addr, 0xA1, h1[:]); err != nil { - return err - } - b.calib.digH1 = h1[0] - - var hBuf [7]byte - if err := b.bus.ReadRegister(b.addr, regCalib26, hBuf[:]); err != nil { - return err - } - b.calib.digH2 = int16(binary.LittleEndian.Uint16(hBuf[0:2])) - b.calib.digH3 = hBuf[2] - b.calib.digH4 = (int16(hBuf[3]) << 4) | (int16(hBuf[4]) & 0x0F) - b.calib.digH5 = (int16(hBuf[5]) << 4) | (int16(hBuf[4]) >> 4) - b.calib.digH6 = int8(hBuf[6]) - return nil -} - -func (b *BME280) ReadValues() (float64, float64, float64, error) { - var raw [8]byte - if err := b.bus.ReadRegister(b.addr, regData, raw[:]); err != nil { - return 0, 0, 0, err - } - - rawP := (int32(raw[0]) << 12) | (int32(raw[1]) << 4) | (int32(raw[2]) >> 4) - rawT := (int32(raw[3]) << 12) | (int32(raw[4]) << 4) | (int32(raw[5]) >> 4) - rawH := (int32(raw[6]) << 8) | int32(raw[7]) - - var1 := (((rawT >> 3) - (int32(b.calib.digT1) << 1)) * int32(b.calib.digT2)) >> 11 - var2 := (((((rawT >> 4) - int32(b.calib.digT1)) * ((rawT >> 4) - int32(b.calib.digT1))) >> 12) * int32(b.calib.digT3)) >> 14 - tFine := var1 + var2 - temp := float64((tFine*5+128)>>8) / 100.0 - - pVar1 := int64(tFine) - 128000 - pVar2 := pVar1*pVar1*int64(b.calib.digP6) + ((pVar1 * int64(b.calib.digP5)) << 17) + (int64(b.calib.digP4) << 35) - pVar1 = ((pVar1 * pVar1 * int64(b.calib.digP3)) >> 8) + ((pVar1 * int64(b.calib.digP2)) << 12) - pVar1 = (((int64(1) << 47) + pVar1) * int64(b.calib.digP1)) >> 33 - - var pres float64 - if pVar1 != 0 { - p := int64(1048576 - rawP) - p = (((p << 31) - pVar2) * 3125) / pVar1 - pVar1 = (int64(b.calib.digP9) * (p >> 13) * (p >> 13)) >> 25 - pVar2 = (int64(b.calib.digP8) * p) >> 19 - p = ((p + pVar1 + pVar2) >> 8) + (int64(b.calib.digP7) << 4) - pres = (float64(p) / 256.0) / 100.0 - } - - hVar := tFine - 76800 - hVar = (((((rawH << 14) - (int32(b.calib.digH4) << 20) - (int32(b.calib.digH5) * hVar)) + 16384) >> 15) * - (((((((hVar * int32(b.calib.digH6)) >> 10) * (((hVar * int32(b.calib.digH3)) >> 11) + 32768)) >> 10) + 2097152)* - int32(b.calib.digH2) + 8192) >> 14)) - hVar = hVar - (((((hVar >> 15) * (hVar >> 15)) >> 7) * int32(b.calib.digH1)) >> 4) - if hVar < 0 { - hVar = 0 - } else if hVar > 419430400 { - hVar = 419430400 - } - hum := float64(hVar>>12) / 1024.0 - - return temp, pres, hum, nil -} - // ------------------------------------------------------------- // Home Assistant Auto-Discovery // ------------------------------------------------------------- @@ -259,14 +112,23 @@ type haSensorDef struct { valTpl string } -func registerHADiscovery(mqtt *SimpleMQTT) error { - deviceJSON := fmt.Sprintf(`"device":{"identifiers":["%s"],"name":"%s","model":"Pico 2 W","manufacturer":"Raspberry Pi"}`, - NodeID, NodeName) +func registerHADiscovery(mqtt *SimpleMQTT, sensor ClimateSensor) error { + deviceJSON := fmt.Sprintf(`"device":{"identifiers":["%s"],"name":"%s","model":"Pico 2 W (%s)","manufacturer":"Raspberry Pi"}`, + NodeID, NodeName, sensor.Name()) sensors := []haSensorDef{ {"temperature", "Temperature", "°C", "temperature", "{{ value_json.temperature }}"}, {"humidity", "Humidity", "%", "humidity", "{{ value_json.humidity }}"}, - {"pressure", "Pressure", "hPa", "atmospheric_pressure", "{{ value_json.pressure }}"}, + } + + if sensor.Name() == "BME280" { + sensors = append(sensors, haSensorDef{ + id: "pressure", + name: "Pressure", + unit: "hPa", + class: "atmospheric_pressure", + valTpl: "{{ value_json.pressure }}", + }) } for _, s := range sensors { @@ -297,12 +159,12 @@ func panicErr(msg string, err error) { func main() { time.Sleep(2 * time.Second) - // Bump frequency to 400kHz for snappy display flushes + // Configure I2C0 bus i2c := machine.I2C0 err := i2c.Configure(machine.I2CConfig{ Frequency: 400 * machine.KHz, - SDA: machine.GPIO16, - SCL: machine.GPIO17, + SDA: machine.GPIO4, + SCL: machine.GPIO5, }) if err != nil { panicErr("I2C configuration", err) @@ -310,33 +172,32 @@ func main() { // Instantiate nil oled variable var oled *ssd1306.Device - // White color for display white := color.RGBA{R: 255, G: 255, B: 255, A: 255} - // Probe address 0x3C with a zero-byte or 1-byte read var dummy [1]byte if err := i2c.ReadRegister(0x3C, 0x00, dummy[:]); err == nil { - println("SSD1306 detected at 0x3C") - dev := ssd1306.NewI2C(i2c) - dev.Configure(ssd1306.Config{ - Address: 0x3C, - Width: 128, - Height: 64, - }) - oled = dev + println("SSD1306 detected at 0x3C") + dev := ssd1306.NewI2C(i2c) + dev.Configure(ssd1306.Config{ + Address: 0x3C, + Width: 128, + Height: 64, + }) + oled = dev - // Run some initial clearing and add booting text to the screen - oled.ClearBuffer() - tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 16, "Booting...", white) - oled.Display() + oled.ClearBuffer() + tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 16, "Booting...", white) + oled.Display() } else { - println("SSD1306 not found — running headless") + println("SSD1306 not found — running headless") } - bme, err := NewBME280(i2c, BME280Addr) + // Initialize the configured sensor + sensor, err := InitSensor(i2c, SensorType) if err != nil { - panicErr("BME280 init", err) + panicErr("Sensor init", err) } + fmt.Printf("Started climate monitor using: %s\n", sensor.Name()) netStack, err := InitNetwork(WifiSSID, WifiPass) if err != nil { @@ -359,31 +220,40 @@ func main() { } println("MQTT connected.") - if err := registerHADiscovery(mqtt); err != nil { + if err := registerHADiscovery(mqtt, sensor); err != nil { println("Warning: HA discovery failed:", err.Error()) } for { - t, p, h, err := bme.ReadValues() + t, p, h, hasPres, err := sensor.ReadValues() if err != nil { - println("BME280 read error:", err.Error()) + println(fmt.Sprintf("%s read error: %s", sensor.Name(), err.Error())) } else { - // Publish MQTT - stateJSON := fmt.Sprintf(`{"temperature":%.2f,"humidity":%.2f,"pressure":%.2f}`, t, h, p) + var stateJSON string + if hasPres { + stateJSON = fmt.Sprintf(`{"temperature":%.2f,"humidity":%.2f,"pressure":%.2f}`, t, h, p) + } else { + stateJSON = fmt.Sprintf(`{"temperature":%.2f,"humidity":%.2f}`, t, h) + } + if err := mqtt.Publish(StateTopic, []byte(stateJSON), false); err != nil { println("MQTT publish error:", err.Error()) } else { println(fmt.Sprintf("[%s] Published: %s", NodeName, stateJSON)) } - // Render to OLED if oled != nil { - oled.ClearBuffer() - tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 14, fmt.Sprintf("Node: %s", NodeName), white) - tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 30, fmt.Sprintf("Temp: %.1f C", t), white) - tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 46, fmt.Sprintf("Hum: %.1f %%", h), white) - tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 60, fmt.Sprintf("Pres: %.1f hPa", p), white) - oled.Display() + oled.ClearBuffer() + tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 14, fmt.Sprintf("Node: %s", NodeName), white) + tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 30, fmt.Sprintf("Temp: %.1f C", t), white) + tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 46, fmt.Sprintf("Hum: %.1f %%", h), white) + + if hasPres { + tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 60, fmt.Sprintf("Pres: %.1f hPa", p), white) + } else { + tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 60, fmt.Sprintf("Sens: %s", sensor.Name()), white) + } + oled.Display() } } diff --git a/sensor.go b/sensor.go new file mode 100644 index 0000000..3be6ec6 --- /dev/null +++ b/sensor.go @@ -0,0 +1,106 @@ +package main + +import ( + "errors" + "machine" + "time" + + "tinygo.org/x/drivers/bme280" + "tinygo.org/x/drivers/sht4x" +) + +type ClimateSensor interface { + ReadValues() (temp float64, pres float64, hum float64, hasPres bool, err error) + Name() string +} + +// ------------------------------------------------------------- +// BME280 Adapter +// ------------------------------------------------------------- +type BME280Adapter struct { + dev bme280.Device +} + +func (b *BME280Adapter) ReadValues() (float64, float64, float64, bool, error) { + // ReadTemperature returns milli-°C (e.g. 25000 = 25.00°C) + tMilli, err := b.dev.ReadTemperature() + if err != nil { + return 0, 0, 0, false, err + } + + // ReadPressure returns milli-Pa (divide by 100,000 for hPa) + pMilli, err := b.dev.ReadPressure() + if err != nil { + return 0, 0, 0, false, err + } + + // ReadHumidity returns 100 * % (e.g. 4500 = 45.00%) + hPct, err := b.dev.ReadHumidity() + if err != nil { + return 0, 0, 0, false, err + } + + temp := float64(tMilli) / 1000.0 + pres := float64(pMilli) / 100000.0 + hum := float64(hPct) / 100.0 + + return temp, pres, hum, true, nil +} + +func (b *BME280Adapter) Name() string { return "BME280" } + +// ------------------------------------------------------------- +// SHT41 Adapter +// ------------------------------------------------------------- +type SHT41Adapter struct { + dev sht4x.Device +} + +func (s *SHT41Adapter) ReadValues() (float64, float64, float64, bool, error) { + // ReadTemperatureHumidity takes no parameters. + // Returns temp in milli-°C and humidity in milli-% (divide by 1000.0) + tMilli, hMilli, err := s.dev.ReadTemperatureHumidity() + if err != nil { + return 0, 0, 0, false, err + } + + temp := float64(tMilli) / 1000.0 + hum := float64(hMilli) / 1000.0 + + return temp, 0, hum, false, nil +} + +func (s *SHT41Adapter) Name() string { return "SHT41" } + +// ------------------------------------------------------------- +// Factory +// ------------------------------------------------------------- +func InitSensor(i2c *machine.I2C, sensorType string) (ClimateSensor, error) { + // Give the sensors a split-second to wake up before trying to register them + time.Sleep(100 * time.Millisecond) + + switch sensorType { + case "bme280": + dev := bme280.New(i2c) + dev.Configure() + if !dev.Connected() { + return nil, errors.New("BME280 not found on I2C bus") + } + return &BME280Adapter{dev: dev}, nil + + case "sht41": + dev := sht4x.New(i2c) + dev.Address = 0x45 // Default is 0x44 (sht4x.Address) + if _, _, err := dev.ReadTemperatureHumidity(); err != nil { + // Try fallback back to 0x44 + dev.Address = 0x44 + if _, _, err := dev.ReadTemperatureHumidity(); err != nil { + return nil, errors.New("SHT41 not found at 0x44 or 0x45") + } + } + return &SHT41Adapter{dev: dev}, nil + + default: + return nil, errors.New("unsupported sensor configured") + } +}