Subscribes to Eastron SDM630 power meter data via MQTT broker, decodes Modbus RTU frames, and writes readings to InfluxDB. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
mqtt "github.com/eclipse/paho.mqtt.golang"
|
|
)
|
|
|
|
// ── CRC-16 / Modbus ───────────────────────────────────────────────────────────
|
|
|
|
// crc16 computes the Modbus RTU CRC-16 (poly 0xA001, initial value 0xFFFF).
|
|
// The result must be appended as [low_byte, high_byte].
|
|
func crc16(data []byte) uint16 {
|
|
crc := uint16(0xFFFF)
|
|
for _, b := range data {
|
|
crc ^= uint16(b)
|
|
for range 8 {
|
|
if crc&1 != 0 {
|
|
crc = (crc >> 1) ^ 0xA001
|
|
} else {
|
|
crc >>= 1
|
|
}
|
|
}
|
|
}
|
|
return crc
|
|
}
|
|
|
|
// ── Frame building ────────────────────────────────────────────────────────────
|
|
|
|
// buildFC4Frame builds a Modbus RTU FC4 (Read Input Registers) request frame.
|
|
//
|
|
// [slave, 0x04, regAddr_hi, regAddr_lo, count_hi, count_lo, crc_lo, crc_hi]
|
|
//
|
|
// regAddr and count are in register (word) units.
|
|
func buildFC4Frame(slave byte, regAddr, count uint16) []byte {
|
|
frame := make([]byte, 6)
|
|
frame[0] = slave
|
|
frame[1] = 0x04
|
|
binary.BigEndian.PutUint16(frame[2:], regAddr)
|
|
binary.BigEndian.PutUint16(frame[4:], count)
|
|
crc := crc16(frame)
|
|
return append(frame, byte(crc), byte(crc>>8)) // little-endian CRC
|
|
}
|
|
|
|
// ── MQTT payload ──────────────────────────────────────────────────────────────
|
|
|
|
type requestPayload struct {
|
|
Data struct {
|
|
ModbusFrame struct {
|
|
Value []int `json:"value"`
|
|
Unit string `json:"unit"`
|
|
} `json:"modbus_frame"`
|
|
} `json:"data"`
|
|
Timestamp struct {
|
|
Incarnation int64 `json:"incarnation"`
|
|
SystemTime int64 `json:"system_time"`
|
|
} `json:"timestamp"`
|
|
}
|
|
|
|
// ── Requester ─────────────────────────────────────────────────────────────────
|
|
|
|
// Requester publishes Modbus FC4 read requests to the MQTT broker.
|
|
// Topic pattern: <topicPrefix>/<segment>/0x<slave_hex>
|
|
type Requester struct {
|
|
client mqtt.Client
|
|
topicPrefix string
|
|
incarnation atomic.Int64
|
|
startTime time.Time
|
|
}
|
|
|
|
func NewRequester(client mqtt.Client, topicPrefix string) *Requester {
|
|
return &Requester{
|
|
client: client,
|
|
topicPrefix: topicPrefix,
|
|
startTime: time.Now(),
|
|
}
|
|
}
|
|
|
|
// ReadInputRegisters sends an FC4 read request for `count` registers (words)
|
|
// starting at `regAddr` on the given slave within the named segment.
|
|
// count=2 reads one float32 value; use a multiple of 2 for more values.
|
|
func (r *Requester) ReadInputRegisters(segment string, slave byte, regAddr, count uint16) error {
|
|
frame := buildFC4Frame(slave, regAddr, count)
|
|
|
|
var p requestPayload
|
|
p.Data.ModbusFrame.Unit = "RAW"
|
|
p.Data.ModbusFrame.Value = bytesToInts(frame)
|
|
p.Timestamp.Incarnation = r.incarnation.Add(1)
|
|
p.Timestamp.SystemTime = time.Since(r.startTime).Nanoseconds()
|
|
|
|
payload, err := json.Marshal(p)
|
|
if err != nil {
|
|
return fmt.Errorf("marshalling request: %w", err)
|
|
}
|
|
|
|
topic := fmt.Sprintf("%s/%s/0x%02x", r.topicPrefix, segment, slave)
|
|
token := r.client.Publish(topic, 0, false, payload)
|
|
token.Wait()
|
|
return token.Error()
|
|
}
|
|
|
|
func bytesToInts(b []byte) []int {
|
|
ints := make([]int, len(b))
|
|
for i, v := range b {
|
|
ints[i] = int(v)
|
|
}
|
|
return ints
|
|
}
|