393 lines
11 KiB
Go
393 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"machine"
|
|
"strconv"
|
|
"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)
|
|
|
|
// -------------------------------------------------------------
|
|
// Minimal MQTT 3.1.1 Client
|
|
// -------------------------------------------------------------
|
|
type SimpleMQTT struct {
|
|
rw io.ReadWriter
|
|
}
|
|
|
|
func NewSimpleMQTT(rw io.ReadWriter) *SimpleMQTT {
|
|
return &SimpleMQTT{rw: rw}
|
|
}
|
|
|
|
func (m *SimpleMQTT) Connect(clientID, username, password string) error {
|
|
flags := byte(0x02) // Clean session
|
|
if username != "" {
|
|
flags |= 0x80
|
|
}
|
|
if password != "" {
|
|
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}
|
|
|
|
payload := encodeString(clientID)
|
|
if username != "" {
|
|
payload = append(payload, encodeString(username)...)
|
|
}
|
|
if password != "" {
|
|
payload = append(payload, encodeString(password)...)
|
|
}
|
|
|
|
body := append(varHeader, payload...)
|
|
packet := append([]byte{0x10}, encodeLength(len(body))...)
|
|
packet = append(packet, body...)
|
|
|
|
if _, err := m.rw.Write(packet); err != nil {
|
|
return err
|
|
}
|
|
|
|
resp := make([]byte, 4)
|
|
if _, err := io.ReadFull(m.rw, resp); err != nil {
|
|
return err
|
|
}
|
|
if resp[0] != 0x20 || resp[3] != 0x00 {
|
|
return fmt.Errorf("MQTT connection refused: code %d", resp[3])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *SimpleMQTT) Publish(topic string, payload []byte, retain bool) error {
|
|
cmd := byte(0x30)
|
|
if retain {
|
|
cmd |= 0x01
|
|
}
|
|
|
|
body := append(encodeString(topic), payload...)
|
|
packet := append([]byte{cmd}, encodeLength(len(body))...)
|
|
packet = append(packet, body...)
|
|
|
|
_, err := m.rw.Write(packet)
|
|
return err
|
|
}
|
|
|
|
func encodeString(s string) []byte {
|
|
b := []byte(s)
|
|
length := uint16(len(b))
|
|
return append([]byte{byte(length >> 8), byte(length & 0xFF)}, b...)
|
|
}
|
|
|
|
func encodeLength(length int) []byte {
|
|
var encoded []byte
|
|
for {
|
|
digit := byte(length % 128)
|
|
length /= 128
|
|
if length > 0 {
|
|
digit |= 0x80
|
|
}
|
|
encoded = append(encoded, digit)
|
|
if length == 0 {
|
|
break
|
|
}
|
|
}
|
|
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
|
|
// -------------------------------------------------------------
|
|
type haSensorDef struct {
|
|
id string
|
|
name string
|
|
unit string
|
|
class string
|
|
valTpl string
|
|
}
|
|
|
|
func registerHADiscovery(mqtt *SimpleMQTT) error {
|
|
deviceJSON := fmt.Sprintf(`"device":{"identifiers":["%s"],"name":"%s","model":"Pico 2 W","manufacturer":"Raspberry Pi"}`,
|
|
NodeID, NodeName)
|
|
|
|
sensors := []haSensorDef{
|
|
{"temperature", "Temperature", "°C", "temperature", "{{ value_json.temperature }}"},
|
|
{"humidity", "Humidity", "%", "humidity", "{{ value_json.humidity }}"},
|
|
{"pressure", "Pressure", "hPa", "atmospheric_pressure", "{{ value_json.pressure }}"},
|
|
}
|
|
|
|
for _, s := range sensors {
|
|
topic := fmt.Sprintf("homeassistant/sensor/%s/%s/config", NodeID, s.id)
|
|
payload := fmt.Sprintf(`{"name":"%s","has_entity_name":true,"unique_id":"%s_%s","device_class":"%s","state_class":"measurement","unit_of_measurement":"%s","state_topic":"%s","value_template":"%s",%s}`,
|
|
s.name, NodeID, s.id, s.class, s.unit, StateTopic, s.valTpl, deviceJSON)
|
|
|
|
if err := mqtt.Publish(topic, []byte(payload), true); err != nil {
|
|
return err
|
|
}
|
|
println("Registered HA discovery:", s.id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// Panic Handler & Entry Point
|
|
// -------------------------------------------------------------
|
|
func panicErr(msg string, err error) {
|
|
if err != nil {
|
|
println("FATAL:", msg, "-", err.Error())
|
|
}
|
|
for {
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Bump frequency to 400kHz for snappy display flushes
|
|
i2c := machine.I2C0
|
|
err := i2c.Configure(machine.I2CConfig{
|
|
Frequency: 400 * machine.KHz,
|
|
SDA: machine.GPIO16,
|
|
SCL: machine.GPIO17,
|
|
})
|
|
if err != nil {
|
|
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)
|
|
if err != nil {
|
|
panicErr("BME280 init", err)
|
|
}
|
|
|
|
netStack, err := InitNetwork(WifiSSID, WifiPass)
|
|
if err != nil {
|
|
panicErr("network init", err)
|
|
}
|
|
|
|
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 {
|
|
panicErr("MQTT connect", err)
|
|
}
|
|
println("MQTT connected.")
|
|
|
|
if err := registerHADiscovery(mqtt); err != nil {
|
|
println("Warning: HA discovery failed:", err.Error())
|
|
}
|
|
|
|
for {
|
|
t, p, h, err := bme.ReadValues()
|
|
if err != nil {
|
|
println("BME280 read error:", err.Error())
|
|
} else {
|
|
// Publish MQTT
|
|
stateJSON := fmt.Sprintf(`{"temperature":%.2f,"humidity":%.2f,"pressure":%.2f}`, t, h, p)
|
|
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()
|
|
}
|
|
}
|
|
|
|
time.Sleep(UpdateInterval)
|
|
}
|
|
}
|