Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6acdc6c7f8 | |||
| a23507b2ca | |||
| 6e2882dc8d | |||
| af084ea33f | |||
| 6cc457c958 | |||
| d4747ff559 | |||
| 20ba285263 | |||
| c60ef1c093 |
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,43 @@
|
|||||||
# Pico Weather
|
# Pico Weather
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
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
|
||||||
|
|
||||||
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 +20,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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import (
|
|||||||
"machine"
|
"machine"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"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)
|
||||||
@@ -292,10 +297,10 @@ func panicErr(msg string, err error) {
|
|||||||
func main() {
|
func main() {
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
// Configure I2C (Pin 16 SDA, Pin 17 SCL -> I2C0)
|
// Bump frequency to 400kHz for snappy display flushes
|
||||||
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.GPIO16,
|
||||||
SCL: machine.GPIO17,
|
SCL: machine.GPIO17,
|
||||||
})
|
})
|
||||||
@@ -303,6 +308,31 @@ func main() {
|
|||||||
panicErr("I2C configuration", err)
|
panicErr("I2C configuration", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Run some initial clearing and add booting text to the screen
|
||||||
|
oled.ClearBuffer()
|
||||||
|
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 16, "Booting...", white)
|
||||||
|
oled.Display()
|
||||||
|
} else {
|
||||||
|
println("SSD1306 not found — running headless")
|
||||||
|
}
|
||||||
|
|
||||||
bme, err := NewBME280(i2c, BME280Addr)
|
bme, err := NewBME280(i2c, BME280Addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panicErr("BME280 init", err)
|
panicErr("BME280 init", err)
|
||||||
@@ -338,14 +368,25 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
println("BME280 read error:", err.Error())
|
println("BME280 read error:", err.Error())
|
||||||
} else {
|
} else {
|
||||||
|
// Publish MQTT
|
||||||
stateJSON := fmt.Sprintf(`{"temperature":%.2f,"humidity":%.2f,"pressure":%.2f}`, t, h, p)
|
stateJSON := fmt.Sprintf(`{"temperature":%.2f,"humidity":%.2f,"pressure":%.2f}`, t, h, p)
|
||||||
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 {
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(30 * time.Second)
|
time.Sleep(UpdateInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user