2 Commits

Author SHA1 Message Date
gballan e5d78e8423 [Issue #8] - adding MQTT reconnect if the connection drops
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2026-09-12 02:58:57 -04:00
gballan 4e022d4e76 [Issue #5] - Added a parameter to set which temperature sensor you're using in config.go
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-12 02:43:51 -04:00
3 changed files with 194 additions and 197 deletions
+3
View File
@@ -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
+67 -179
View File
@@ -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"
@@ -38,8 +36,8 @@ func (m *SimpleMQTT) Connect(clientID, username, password string) error {
flags |= 0x40
}
// Variable header: MQTT (proto name), Level 4 (MQTT 3.1.1), flags, KeepAlive 60s
varHeader := []byte{0x00, 0x04, 'M', 'Q', 'T', 'T', 0x04, flags, 0x00, 0x3C}
// Variable header: MQTT (proto name), Level 4 (MQTT 3.1.1), flags, KeepAlive 0s (disabled)
varHeader := []byte{0x00, 0x04, 'M', 'Q', 'T', 'T', 0x04, flags, 0x00, 0x00}
payload := encodeString(clientID)
if username != "" {
@@ -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 {
@@ -283,7 +145,7 @@ func registerHADiscovery(mqtt *SimpleMQTT) error {
}
// -------------------------------------------------------------
// Panic Handler & Entry Point
// Panic Handler & Helper Functions
// -------------------------------------------------------------
func panicErr(msg string, err error) {
if err != nil {
@@ -294,15 +156,31 @@ func panicErr(msg string, err error) {
}
}
func connectMQTT(netStack *NetworkStack, port uint16) (*SimpleMQTT, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
conn, err := netStack.DialTCP(ctx, MQTTHost, port)
if err != nil {
return nil, err
}
mqtt := NewSimpleMQTT(conn)
if err := mqtt.Connect(ClientID, MQTTUser, MQTTPass); err != nil {
return nil, err
}
return mqtt, nil
}
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,10 +188,8 @@ 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")
@@ -325,7 +201,6 @@ func main() {
})
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()
@@ -333,10 +208,12 @@ func main() {
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 {
@@ -344,45 +221,56 @@ func main() {
}
portNum, _ := strconv.Atoi(MQTTPort)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
conn, err := netStack.DialTCP(ctx, MQTTHost, uint16(portNum))
if err != nil {
panicErr("TCP dial", err)
}
mqtt := NewSimpleMQTT(conn)
println("Connecting MQTT session...")
if err := mqtt.Connect(ClientID, MQTTUser, MQTTPass); err != nil {
mqtt, err := connectMQTT(netStack, uint16(portNum))
if err != nil {
panicErr("MQTT connect", err)
}
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())
println("MQTT publish error:", err.Error(), "- attempting reconnect...")
newMqtt, recErr := connectMQTT(netStack, uint16(portNum))
if recErr != nil {
println("MQTT reconnect failed:", recErr.Error())
} else {
println("MQTT reconnected successfully.")
mqtt = newMqtt
_ = registerHADiscovery(mqtt, sensor)
_ = mqtt.Publish(StateTopic, []byte(stateJSON), false)
}
} 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)
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()
}
}
+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")
}
}