Initial implementation: MQTT-based energy meter subscriber
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>
This commit is contained in:
112
requester.go
Normal file
112
requester.go
Normal file
@@ -0,0 +1,112 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user