[Issue #5] - Added a parameter to set which temperature sensor you're using in config.go
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-09-12 02:43:51 -04:00
parent 6acdc6c7f8
commit 4e022d4e76
3 changed files with 163 additions and 184 deletions
+3
View File
@@ -12,6 +12,9 @@ const (
Hostname = "pico2w-bme280" Hostname = "pico2w-bme280"
EnableICMP = true EnableICMP = true
// Sensor configuration
SensorType = "bme280"
MQTTHost = "192.168.1.50" // Must be an IP address; DNS resolution is not supported currently MQTTHost = "192.168.1.50" // Must be an IP address; DNS resolution is not supported currently
MQTTPort = "1883" MQTTPort = "1883"
MQTTUser = "" // Leave empty if not required MQTTUser = "" // Leave empty if not required
+36 -166
View File
@@ -1,15 +1,13 @@
package main package main
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"fmt" "fmt"
"image/color"
"io" "io"
"machine" "machine"
"strconv" "strconv"
"time" "time"
"image/color"
"tinygo.org/x/drivers/ssd1306" "tinygo.org/x/drivers/ssd1306"
"tinygo.org/x/tinyfont" "tinygo.org/x/tinyfont"
@@ -103,151 +101,6 @@ func encodeLength(length int) []byte {
return encoded 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 // Home Assistant Auto-Discovery
// ------------------------------------------------------------- // -------------------------------------------------------------
@@ -259,14 +112,23 @@ type haSensorDef struct {
valTpl string valTpl string
} }
func registerHADiscovery(mqtt *SimpleMQTT) error { func registerHADiscovery(mqtt *SimpleMQTT, sensor ClimateSensor) error {
deviceJSON := fmt.Sprintf(`"device":{"identifiers":["%s"],"name":"%s","model":"Pico 2 W","manufacturer":"Raspberry Pi"}`, deviceJSON := fmt.Sprintf(`"device":{"identifiers":["%s"],"name":"%s","model":"Pico 2 W (%s)","manufacturer":"Raspberry Pi"}`,
NodeID, NodeName) NodeID, NodeName, sensor.Name())
sensors := []haSensorDef{ sensors := []haSensorDef{
{"temperature", "Temperature", "°C", "temperature", "{{ value_json.temperature }}"}, {"temperature", "Temperature", "°C", "temperature", "{{ value_json.temperature }}"},
{"humidity", "Humidity", "%", "humidity", "{{ value_json.humidity }}"}, {"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 { for _, s := range sensors {
@@ -297,12 +159,12 @@ func panicErr(msg string, err error) {
func main() { func main() {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Bump frequency to 400kHz for snappy display flushes // Configure I2C0 bus
i2c := machine.I2C0 i2c := machine.I2C0
err := i2c.Configure(machine.I2CConfig{ err := i2c.Configure(machine.I2CConfig{
Frequency: 400 * machine.KHz, Frequency: 400 * machine.KHz,
SDA: machine.GPIO16, SDA: machine.GPIO4,
SCL: machine.GPIO17, SCL: machine.GPIO5,
}) })
if err != nil { if err != nil {
panicErr("I2C configuration", err) panicErr("I2C configuration", err)
@@ -310,10 +172,8 @@ func main() {
// Instantiate nil oled variable // Instantiate nil oled variable
var oled *ssd1306.Device var oled *ssd1306.Device
// White color for display
white := color.RGBA{R: 255, G: 255, B: 255, A: 255} 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 var dummy [1]byte
if err := i2c.ReadRegister(0x3C, 0x00, dummy[:]); err == nil { if err := i2c.ReadRegister(0x3C, 0x00, dummy[:]); err == nil {
println("SSD1306 detected at 0x3C") println("SSD1306 detected at 0x3C")
@@ -325,7 +185,6 @@ func main() {
}) })
oled = dev oled = dev
// Run some initial clearing and add booting text to the screen
oled.ClearBuffer() oled.ClearBuffer()
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 16, "Booting...", white) tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 16, "Booting...", white)
oled.Display() oled.Display()
@@ -333,10 +192,12 @@ func main() {
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 { 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) netStack, err := InitNetwork(WifiSSID, WifiPass)
if err != nil { if err != nil {
@@ -359,30 +220,39 @@ func main() {
} }
println("MQTT connected.") println("MQTT connected.")
if err := registerHADiscovery(mqtt); err != nil { if err := registerHADiscovery(mqtt, sensor); err != nil {
println("Warning: HA discovery failed:", err.Error()) println("Warning: HA discovery failed:", err.Error())
} }
for { for {
t, p, h, err := bme.ReadValues() t, p, h, hasPres, err := sensor.ReadValues()
if err != nil { if err != nil {
println("BME280 read error:", err.Error()) println(fmt.Sprintf("%s read error: %s", sensor.Name(), err.Error()))
} else { } else {
// Publish MQTT var stateJSON string
stateJSON := fmt.Sprintf(`{"temperature":%.2f,"humidity":%.2f,"pressure":%.2f}`, t, h, p) 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 { if err := mqtt.Publish(StateTopic, []byte(stateJSON), false); err != nil {
println("MQTT publish error:", err.Error()) println("MQTT publish error:", err.Error())
} else { } else {
println(fmt.Sprintf("[%s] Published: %s", NodeName, stateJSON)) println(fmt.Sprintf("[%s] Published: %s", NodeName, stateJSON))
} }
// Render to OLED
if oled != nil { if oled != nil {
oled.ClearBuffer() oled.ClearBuffer()
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 14, fmt.Sprintf("Node: %s", NodeName), white) 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, 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, 46, fmt.Sprintf("Hum: %.1f %%", h), white)
if hasPres {
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 60, fmt.Sprintf("Pres: %.1f hPa", p), white) 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() oled.Display()
} }
} }
+106
View File
@@ -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")
}
}