10 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
gballan 6acdc6c7f8 [Issue #7] - Added config.go setting to allow setting the hostname
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2026-09-12 02:01:02 -04:00
gballan a23507b2ca [Issue #6] - added config option to enable/disable icmp packets
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-12 01:58:24 -04:00
gballan 6e2882dc8d Updating repository information to point to Bit Goblin
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-07 01:26:45 -04:00
gballan af084ea33f Updating README with information about the hardware used
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-07 01:21:24 -04:00
gballan 6cc457c958 Adding a version badge via shields.io
ci/woodpecker/push/woodpecker Pipeline is running
2026-09-07 01:10:02 -04:00
gballan d4747ff559 [Issue #2] - adding code to verify a display is connected before attempting to use it
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2026-09-07 01:03:30 -04:00
gballan 20ba285263 [Issue #2] - adding initial support for SSD1306 displays
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-07 00:54:27 -04:00
gballan c60ef1c093 [Issue #1] - Adding UpdateInterval config value to set how long to wait in between each update loop, in seconds
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2026-09-07 00:34:29 -04:00
8 changed files with 268 additions and 182 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2026 gballan. Copyright (c) 2026 Bit Goblin.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+28 -3
View File
@@ -1,18 +1,43 @@
# Pico Weather # Pico Weather
![Woodpecker CI badge](https://builds.metaunix.net/api/badges/92/status.svg) ![Woodpecker CI badge](https://builds.metaunix.net/api/badges/92/status.svg)
![shields.io version badge](https://img.shields.io/gitea/v/release/BitGoblin/pico-weather?gitea_url=https://git.metaunix.net)
Raspberry Pi Pico app to read BME280 sensor data and send it to Home Assistant. Raspberry Pi Pico app to read BME280 sensor data and send it to Home Assistant.
Made in Go with [TinyGo](https://tinygo.org/). Made in Go with [TinyGo](https://tinygo.org/).
## Requirements ## Hardware
The following hardware is what I used for a deployment:
* Raspberry Pi Pico 2 W: https://www.amazon.com/dp/B0DRJXPPWL
* The **W** is VERY important, as without networking it can't reach Home Assistant!
* Also a Raspberry Pi Pico W *should* work too, but I haven't tested it yet
* Waveshare Bosch BME280 sensor: https://www.amazon.com/dp/B07Q47DFXS
* Elegoo SSD1306 LCD: https://www.amazon.com/dp/B0FSRQG23K
Most (or all?) of these components can be swapped for equivalent components. You just need to make sure the code is compatible with whatever board you choose (not likely), is a genuine BME280 sensor, and an SSD1306 display.
Support for other hardware types may come in the future, but if there's a specific sensor or board you're in submit an issue or pull request!
### Prototyping
To help with debugging/testing deployments, I also used these components:
* Elegoo 400-pin Breadboard: https://www.amazon.com/dp/B01EV640I6
* Freenove Raspberry Pi Pico Breakout Board: https://www.amazon.com/dp/B0BFB53Y2N
* Elegoo 20cm Jumper Cables 120 count: https://www.amazon.com/dp/B01EV70C78
## Software Deployment
### Requirements
* [TinyGo](https://tinygo.org/getting-started/install/) - TinyGo installation guide * [TinyGo](https://tinygo.org/getting-started/install/) - TinyGo installation guide
## Deployment ### Steps
1. Clone this git repository `git clone https://git.metaunix.net/gballan/pico-weather`. 1. Clone this git repository `git clone https://git.metaunix.net/BitGoblin/pico-weather`.
2. Copy `config.go.example` to `config.go` and modify the values to match your environment. 2. Copy `config.go.example` to `config.go` and modify the values to match your environment.
3. Hold down the **BOOTSEL** button on your Raspberry Pi Pico 2W. 3. Hold down the **BOOTSEL** button on your Raspberry Pi Pico 2W.
4. Plug in your Pico to a USB port on your PC. 4. Plug in your Pico to a USB port on your PC.
+13
View File
@@ -1,9 +1,20 @@
package main package main
import (
"time"
)
const ( const (
WifiSSID = "My Wifi Name" WifiSSID = "My Wifi Name"
WifiPass = "SecretWifiPassword" WifiPass = "SecretWifiPassword"
// Networking settings
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 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
@@ -12,4 +23,6 @@ const (
NodeName = "Studio Weather" NodeName = "Studio Weather"
NodeID = "studio_env" NodeID = "studio_env"
ClientID = "pico2w_studio_env" ClientID = "pico2w_studio_env"
UpdateInterval = 30 * time.Second
) )
+4 -1
View File
@@ -1,13 +1,16 @@
module git.metaunix.net/gballan/pico-weather module git.metaunix.net/BitGoblin/pico-weather
go 1.25.2 go 1.25.2
require ( require (
github.com/soypat/cyw43439 v0.1.1 github.com/soypat/cyw43439 v0.1.1
github.com/soypat/lneto v0.3.2 github.com/soypat/lneto v0.3.2
tinygo.org/x/drivers v0.36.0
tinygo.org/x/tinyfont v0.7.0
) )
require ( require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect
github.com/tinygo-org/pio v0.3.0 // indirect github.com/tinygo-org/pio v0.3.0 // indirect
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
+6
View File
@@ -1,3 +1,5 @@
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/soypat/cyw43439 v0.1.1 h1:vcaTiVzfuz3keK7lJpVxStZ6tV8HCw7Ugzsh1k4mneE= github.com/soypat/cyw43439 v0.1.1 h1:vcaTiVzfuz3keK7lJpVxStZ6tV8HCw7Ugzsh1k4mneE=
github.com/soypat/cyw43439 v0.1.1/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc= github.com/soypat/cyw43439 v0.1.1/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc=
github.com/soypat/lneto v0.3.2 h1:iUFeRSq2czT7Db6MMOsAnMCBlKCqvIr941zsNf9dcu0= github.com/soypat/lneto v0.3.2 h1:iUFeRSq2czT7Db6MMOsAnMCBlKCqvIr941zsNf9dcu0=
@@ -8,3 +10,7 @@ github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI=
github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU= github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
tinygo.org/x/drivers v0.36.0 h1:F0x342A6GWqh6abtCa57uAxCyz/b9MbGzvIVvIf+gpE=
tinygo.org/x/drivers v0.36.0/go.mod h1:DQgKyHkB4G6IEOKVTAjApbKnWGwESN91EVJO+nMOE9Y=
tinygo.org/x/tinyfont v0.7.0 h1:Ju901eGRThlHLoti9K7myMYyv4phd3I0DrJwc14NTTU=
tinygo.org/x/tinyfont v0.7.0/go.mod h1:onflMSkpWl7r7j4MIqhPEVV39pn7yL4N3MOePl3G+G8=
+105 -176
View File
@@ -1,14 +1,17 @@
package main package main
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"fmt" "fmt"
"image/color"
"io" "io"
"machine" "machine"
"strconv" "strconv"
"time" "time"
"tinygo.org/x/drivers/ssd1306"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/proggy"
) )
var StateTopic = fmt.Sprintf("homeassistant/sensor/%s/state", NodeID) var StateTopic = fmt.Sprintf("homeassistant/sensor/%s/state", NodeID)
@@ -33,8 +36,8 @@ func (m *SimpleMQTT) Connect(clientID, username, password string) error {
flags |= 0x40 flags |= 0x40
} }
// Variable header: MQTT (proto name), Level 4 (MQTT 3.1.1), flags, KeepAlive 60s // 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, 0x3C} varHeader := []byte{0x00, 0x04, 'M', 'Q', 'T', 'T', 0x04, flags, 0x00, 0x00}
payload := encodeString(clientID) payload := encodeString(clientID)
if username != "" { if username != "" {
@@ -98,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
// ------------------------------------------------------------- // -------------------------------------------------------------
@@ -254,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 {
@@ -278,7 +145,7 @@ func registerHADiscovery(mqtt *SimpleMQTT) error {
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
// Panic Handler & Entry Point // Panic Handler & Helper Functions
// ------------------------------------------------------------- // -------------------------------------------------------------
func panicErr(msg string, err error) { func panicErr(msg string, err error) {
if err != nil { if err != nil {
@@ -289,63 +156,125 @@ 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() { func main() {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Configure I2C (Pin 16 SDA, Pin 17 SCL -> I2C0) // Configure I2C0 bus
i2c := machine.I2C0 i2c := machine.I2C0
err := i2c.Configure(machine.I2CConfig{ err := i2c.Configure(machine.I2CConfig{
Frequency: 100 * 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)
} }
bme, err := NewBME280(i2c, BME280Addr) // Instantiate nil oled variable
if err != nil { var oled *ssd1306.Device
panicErr("BME280 init", err) white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
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
oled.ClearBuffer()
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 16, "Booting...", white)
oled.Display()
} else {
println("SSD1306 not found — running headless")
} }
// Initialize the configured sensor
sensor, err := InitSensor(i2c, SensorType)
if err != nil {
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 {
panicErr("network init", err) panicErr("network init", err)
} }
portNum, _ := strconv.Atoi(MQTTPort) 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...") 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) panicErr("MQTT connect", err)
} }
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 {
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 { 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 { } else {
println(fmt.Sprintf("[%s] Published: %s", NodeName, stateJSON)) println(fmt.Sprintf("[%s] Published: %s", NodeName, stateJSON))
} }
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()
}
} }
time.Sleep(30 * time.Second) time.Sleep(UpdateInterval)
} }
} }
+5 -1
View File
@@ -115,16 +115,20 @@ func InitNetwork(ssid, pass string) (*NetworkStack, error) {
stack := &xnet.StackAsync{} stack := &xnet.StackAsync{}
err = stack.Reset(xnet.StackConfig{ err = stack.Reset(xnet.StackConfig{
Hostname: "pico2w-bme280", Hostname: Hostname,
RandSeed: time.Now().UnixNano(), RandSeed: time.Now().UnixNano(),
MaxActiveTCPPorts: 2, MaxActiveTCPPorts: 2,
MTU: uint16(framelen - ethernet.MaxOverheadSize), MTU: uint16(framelen - ethernet.MaxOverheadSize),
HardwareAddress: hwaddr, HardwareAddress: hwaddr,
ICMPQueueLimit: 4,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("stack config reset: %w", err) return nil, fmt.Errorf("stack config reset: %w", err)
} }
// Enable network pings if configured
_ = stack.EnableICMP(EnableICMP)
// Start background frame pump // Start background frame pump
ctx := context.Background() ctx := context.Background()
go stackLoop(ctx, stack, adapter) go stackLoop(ctx, stack, adapter)
+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")
}
}