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: //0x 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 }