Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow RTU devices to provide further information during initialization #28

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions cmd/confighandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func createConnection(device string, rtu bool, baudrate int, comset string) (res
return res
}

// ConnectionManager returns connection manager for spec or creates new one
func (conf *DeviceConfigHandler) ConnectionManager(connSpec string, rtu bool, baudrate int, comset string) meters.Manager {
manager, ok := conf.Managers[connSpec]
if !ok {
Expand Down
26 changes: 3 additions & 23 deletions cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,6 @@ func addDesc(s *string, key string, val string) {
}
}

// validator checks if value is in range of reference values
type validator struct {
refs []float64
}

func (v validator) check(f float64) bool {
tolerance := 0.1 // 10%
for _, ref := range v.refs {
if f >= (1-tolerance)*ref && f <= (1+tolerance)*ref {
return true
}
}
return false
}

func scan(cmd *cobra.Command, args []string) {
if len(args) > 0 {
log.Fatalf("excess arguments, aborting: %v", args)
Expand Down Expand Up @@ -93,9 +78,6 @@ func scan(cmd *cobra.Command, args []string) {
deviceList := make(map[int]meters.Device)
log.Printf("starting bus scan on %s", adapter)

// validate against 110V and 230V to make detection reliable
v := validator{[]float64{110, 230}}

SCAN:
// loop over all valid slave adresses
for deviceID := 1; deviceID <= 247; deviceID++ {
Expand All @@ -111,13 +93,11 @@ SCAN:
log.Println(err) // log error but continue
}

mr, err := dev.Probe(client)
if err == nil && v.check(mr.Value) {
log.Printf("device %d: %s type device found, %s: %.2f\r\n",
match, err := dev.Probe(client)
if match && err == nil {
log.Printf("device %d: %s type device found\r\n",
deviceID,
dev.Descriptor().Manufacturer,
mr.Measurement,
mr.Value,
)

deviceList[deviceID] = dev
Expand Down
6 changes: 3 additions & 3 deletions meters/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ type DeviceDescriptor struct {

// Device is a modbus device that can be described, probed and queried
type Device interface {
// Initialize prepares the device for usage. Any setup or initilization should be done here.
// Initialize prepares the device for usage. Any setup or initialization should be done here.
// It requires that the client has the correct device id applied.
Initialize(client modbus.Client) error

// Descriptor returns the device descriptor. Since this method does not have
// bus access the descriptor should be preared during initilization.
// bus access the descriptor should be preared during initialization.
Descriptor() DeviceDescriptor

// Probe tests if a basic register, typically VoltageL1, can be read.
// It requires that the client has the correct device id applied.
Probe(client modbus.Client) (MeasurementResult, error)
Probe(client modbus.Client) (bool, error)

// Query retrieves all registers that the device supports.
// It requires that the client has the correct device id applied.
Expand Down
8 changes: 6 additions & 2 deletions meters/rs485/abb.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ func (p *ABBProducer) Type() string {

// Description implements Producer interface
func (p *ABBProducer) Description() string {
return "ABB A/B-Series meters"
return "ABB A/B-Series"
}

func (p *ABBProducer) Initialize(client modbusClient, descriptor *DeviceDescriptor) error {
return initializeMID(client, descriptor)
}

// wrapTransform validates if reading result is undefined and returns NaN in that case
Expand Down Expand Up @@ -95,7 +99,7 @@ func (p *ABBProducer) snip(iec Measurement, readlen uint16, sign signedness, tra
nanAwareTransform := wrapTransform(2*readlen, sign, transform)

snip := Operation{
FuncCode: ReadHoldingReg,
FuncCode: readHoldingReg,
OpCode: p.Opcodes[iec],
ReadLen: readlen,
Transform: nanAwareTransform,
Expand Down
4 changes: 2 additions & 2 deletions meters/rs485/dzg.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (p *DZGProducer) Type() string {

// Description implements Producer interface
func (p *DZGProducer) Description() string {
return "DZG Metering GmbH DVH4013 meters"
return "DZG Metering GmbH DVH4013"
}

func (p *DZGProducer) snip(iec Measurement, scaler ...float64) Operation {
Expand All @@ -69,7 +69,7 @@ func (p *DZGProducer) snip(iec Measurement, scaler ...float64) Operation {
}

snip := Operation{
FuncCode: ReadHoldingReg,
FuncCode: readHoldingReg,
OpCode: p.Opcode(iec),
ReadLen: 2,
IEC61850: iec,
Expand Down
32 changes: 30 additions & 2 deletions meters/rs485/inepro.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package rs485

import . "github.com/volkszaehler/mbmd/meters"
import (
"encoding/binary"
"fmt"

. "github.com/volkszaehler/mbmd/meters"
)

func init() {
Register(NewIneproProducer)
Expand Down Expand Up @@ -102,9 +107,32 @@ func (p *IneproProducer) Description() string {
return "Inepro Metering Pro 380"
}

// initialize implements initializer interface
func (p *IneproProducer) initialize(client modbusClient, descriptor *DeviceDescriptor) error {
// serial
if bytes, err := client.ReadHoldingRegisters(0x4000, 2); err == nil {
descriptor.Serial = fmt.Sprintf("%d", binary.BigEndian.Uint32(bytes))
}
// firmware
if bytes, err := client.ReadHoldingRegisters(0x4007, 2); err == nil {
descriptor.Version = fmt.Sprintf("%.2f", RTUIeee754ToFloat64(bytes))
}
// hardware
if bytes, err := client.ReadHoldingRegisters(0x4009, 2); err == nil {
descriptor.Options += fmt.Sprintf("HW %.2f", RTUIeee754ToFloat64(bytes))
}
// current rating
if bytes, err := client.ReadHoldingRegisters(0x400B, 1); err == nil {
descriptor.Options += fmt.Sprintf(" %dA", binary.BigEndian.Uint16(bytes))
}

// assume success
return nil
}

func (p *IneproProducer) snip(iec Measurement, scaler ...float64) Operation {
snip := Operation{
FuncCode: ReadHoldingReg,
FuncCode: readHoldingReg,
OpCode: p.Opcodes[iec],
ReadLen: 2,
IEC61850: iec,
Expand Down
12 changes: 9 additions & 3 deletions meters/rs485/janitza.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package rs485

import . "github.com/volkszaehler/mbmd/meters"
import (
. "github.com/volkszaehler/mbmd/meters"
)

func init() {
Register(NewJanitzaProducer)
Expand Down Expand Up @@ -51,12 +53,16 @@ func (p *JanitzaProducer) Type() string {

// Description implements Producer interface
func (p *JanitzaProducer) Description() string {
return "Janitza B-Series meters"
return "Janitza MID B-Series"
}

func (p *JanitzaProducer) Initialize(client modbusClient, descriptor *DeviceDescriptor) error {
return initializeMID(client, descriptor)
}

func (p *JanitzaProducer) snip(iec Measurement) Operation {
snip := Operation{
FuncCode: ReadHoldingReg,
FuncCode: readHoldingReg,
OpCode: p.Opcode(iec),
ReadLen: 2,
IEC61850: iec,
Expand Down
4 changes: 2 additions & 2 deletions meters/rs485/mpm3pm.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ func (p *MPM3MPProducer) Type() string {

// Description implements Producer interface
func (p *MPM3MPProducer) Description() string {
return "Bernecker Engineering MPM3PM meters"
return "Bernecker Engineering MPM3PM"
}

func (p *MPM3MPProducer) snip(iec Measurement, readlen uint16, transform RTUTransform, scaler ...float64) Operation {
snip := Operation{
FuncCode: ReadInputReg,
FuncCode: readInputReg,
OpCode: p.Opcodes[iec],
ReadLen: readlen,
Transform: transform,
Expand Down
126 changes: 99 additions & 27 deletions meters/rs485/rs485.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rs485

import (
"encoding/binary"
"fmt"
"time"

Expand All @@ -10,14 +11,55 @@ import (
)

const (
ReadHoldingReg = 3
ReadInputReg = 4
// modbus operation types
readHoldingReg = 3
readInputReg = 4
)

// modbusClient is the minimal interface that is usable by the initializer interface.
// It is used to keep the producers free of modbus implementation dependencies.
type modbusClient interface {
ReadHoldingRegisters(address, quantity uint16) (results []byte, err error)
ReadInputRegisters(address, quantity uint16) (results []byte, err error)
}

// identificator implements device recognition logic
type identificator interface {
identify(bytes []byte) bool
}

// initializer can be implemented by producers to perform bus operations for
// device initialization
type initializer interface {
// initialize prepares the device for usage. Any setup or initialization should
// be done here. It requires that the client has the correct device id applied.
initialize(client modbusClient, descriptor *meters.DeviceDescriptor) error
}

// MID meters initialization method used by Janitza and ABB
func initializeMID(client modbusClient, descriptor *meters.DeviceDescriptor) error {
// serial
if bytes, err := client.ReadHoldingRegisters(0x8900, 2); err == nil {
descriptor.Serial = fmt.Sprintf("%4x", binary.BigEndian.Uint32(bytes))
}
// firmware
if bytes, err := client.ReadHoldingRegisters(0x8908, 8); err == nil {
descriptor.Version = string(bytes)
}
// type
if bytes, err := client.ReadHoldingRegisters(0x8960, 6); err == nil {
descriptor.Model = string(bytes)
}

// assume success
return nil
}

type rs485 struct {
producer Producer
ops chan Operation
inflight Operation
producer Producer
descriptor meters.DeviceDescriptor
ops chan Operation
inflight Operation
}

// NewDevice creates a device who's type must exist in the producer registry
Expand All @@ -43,42 +85,56 @@ func NewDevice(typeid string) (meters.Device, error) {
return nil, fmt.Errorf("unknown meter type %s", typeid)
}

// Initialize prepares the device for usage. Any setup or initilization should be done here.
// Initialize prepares the device for usage. Any setup or initialization should be done here.
func (d *rs485) Initialize(client modbus.Client) error {
d.descriptor = meters.DeviceDescriptor{
Manufacturer: d.producer.Type(),
Model: d.producer.Description(),
}

// does device support initializing itself?
if p, ok := d.producer.(initializer); ok {
return p.initialize(client, &d.descriptor)
}

return nil
}

// Descriptor returns the device descriptor. Since this method doe not have bus access the descriptor should be preared
// during initilization.
// during initialization.
func (d *rs485) Descriptor() meters.DeviceDescriptor {
return meters.DeviceDescriptor{
Manufacturer: d.producer.Type(),
Model: d.producer.Description(),
}
return d.descriptor
}

func (d *rs485) query(client modbus.Client, op Operation) (res meters.MeasurementResult, err error) {
var bytes []byte

func (d *rs485) rawQuery(client modbus.Client, op Operation) (bytes []byte, err error) {
if op.ReadLen == 0 {
return res, fmt.Errorf("invalid meter operation %v", op)
}

if op.Transform == nil {
return res, fmt.Errorf("transformation not defined: %v", op)
return bytes, fmt.Errorf("invalid meter operation %v", op)
}

switch op.FuncCode {
case ReadHoldingReg:
case readHoldingReg:
bytes, err = client.ReadHoldingRegisters(op.OpCode, op.ReadLen)
case ReadInputReg:
case readInputReg:
bytes, err = client.ReadInputRegisters(op.OpCode, op.ReadLen)
default:
return res, fmt.Errorf("unknown function code %d", op.FuncCode)
return bytes, fmt.Errorf("unknown function code %d", op.FuncCode)
}

if err != nil {
return res, errors.Wrap(err, "read failed")
return bytes, errors.Wrap(err, "read failed")
}

return bytes, nil
}

func (d *rs485) query(client modbus.Client, op Operation) (res meters.MeasurementResult, err error) {
if op.Transform == nil {
return res, fmt.Errorf("transformation not defined: %v", op)
}

bytes, err := d.rawQuery(client, op)
if err != nil {
return res, err
}

res = meters.MeasurementResult{
Expand All @@ -91,15 +147,31 @@ func (d *rs485) query(client modbus.Client, op Operation) (res meters.Measuremen
}

// Probe is called by the handler after preparing the bus by setting the device id
func (d *rs485) Probe(client modbus.Client) (res meters.MeasurementResult, err error) {
func (d *rs485) Probe(client modbus.Client) (res bool, err error) {
op := d.producer.Probe()

res, err = d.query(client, op)
// use specific identificator for devices that are able to recognize
// themselves reliably
if idf, ok := d.producer.(identificator); ok {
bytes, err := d.rawQuery(client, op)
if err != nil {
return false, err
}

match := idf.identify(bytes)
return match, nil
}

// use default validator looking for 110/230V
measurement, err := d.query(client, op)
if err != nil {
return res, err
return false, err
}

return res, nil
v := validator{[]float64{110, 230}}
match := v.validate(measurement.Value)

return match, nil
}

// Query is called by the handler after preparing the bus by setting the device id and waiting for rate limit
Expand Down
Loading