Skip to content

Commit

Permalink
Merge pull request #1 from rolfl/addCLI
Browse files Browse the repository at this point in the history
Add CLI code
  • Loading branch information
rolfl authored Mar 14, 2021
2 parents b6de1cc + c1a54a1 commit f85fb62
Show file tree
Hide file tree
Showing 11 changed files with 494 additions and 6 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/rolfl/modbus

go 1.15

require github.com/jessevdk/go-flags v1.4.0 // indirect
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
94 changes: 94 additions & 0 deletions mbcli/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"fmt"
"strconv"
"strings"

"github.com/rolfl/modbus"
)

var busses = make(map[string]modbus.Modbus)

// 1200, 2400, 4800, 19200, 38400, 57600, and 115200.
var bauds = map[string]int{
"1200": 1200,
"2400": 2400,
"4800": 4800,
"9600": 9600,
"19200": 19200,
"38400": 38400,
"57600": 57600,
"115200": 115200,
}

var parities = map[string]int{
"N": modbus.ParityNone,
"E": modbus.ParityEven,
"O": modbus.ParityOdd,
}

var stopbits = map[string]int{
"1": 1,
"2": 2,
}

func client(access string) (modbus.Client, error) {
parts := strings.Split(access, ":")
if parts[0] == "tcp" {
if len(parts) != 4 {
return nil, fmt.Errorf("expect exactly 4 parts for TCP client access tcp:host:port:unit - not: %v", access)
}
host := strings.Join(parts[1:3], ":")
unit, err := strconv.Atoi(parts[3])
if err != nil {
return nil, err
}
if _, ok := busses[host]; !ok {
mb, err := modbus.NewTCP(host)
if err != nil {
return nil, err
}
busses[host] = mb
}
return busses[host].GetClient(unit), nil
}
if parts[0] == "rtu" {
if len(parts) < 6 || len(parts) > 7 {
return nil, fmt.Errorf("expect exactly 4 parts for TCP client access rtu:device:baud:parity:stop:(dtr:)unit - not: %v", access)
}
device := parts[1]
baud, ok := bauds[parts[2]]
if !ok {
return nil, fmt.Errorf("illegal baud %v", parts[2])
}
parity, ok := parities[parts[3]]
if !ok {
return nil, fmt.Errorf("illegal parity %v", parts[3])
}
stop, ok := stopbits[parts[4]]
if !ok {
return nil, fmt.Errorf("illegal stop bits %v", parts[4])
}
dtr := false
if len(parts) == 7 {
if parts[5] != "dtr" {
return nil, fmt.Errorf("DTR must be specified as 'dtr', not %v", parts[5])
}
}
unit, err := strconv.Atoi(parts[len(parts)-1])
if err != nil {
return nil, err
}
key := fmt.Sprintf("%v:%v:%v:%v:%v", device, baud, parity, stop, dtr)
if _, ok = busses[key]; !ok {
mb, err := modbus.NewRTU(device, baud, parity, stop, dtr)
if err != nil {
return nil, err
}
busses[key] = mb
}
return busses[key].GetClient(unit), nil
}
return nil, fmt.Errorf("unknown modbus connection type %v (expect tcp or rtu)", parts[0])
}
64 changes: 64 additions & 0 deletions mbcli/coils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"fmt"
"time"
)

type CoilGetCommands struct {
Units []string `short:"u" long:"unit" description:"Unit(s) to contact" required:"true"`
Timeout int `short:"t" long:"timeout" default:"5" description:"Timeout (in seconds)"`
Args struct {
Addresses []string `required:"1"`
} `positional-args:"yes" required:"yes"`
}

func (c *CoilGetCommands) Execute(args []string) error {
return genericClientReads("coil", c.Units, c.Args.Addresses, c.Timeout)
}

type CoilSetCommands struct {
Units []string `short:"u" long:"unit" description:"Unit(s) to contact" required:"true"`
Timeout int `short:"t" long:"timeout" default:"5" description:"Timeout (in seconds)"`
Args struct {
AddressValues []string `required:"1"`
} `positional-args:"yes" required:"yes"`
}

func (c *CoilSetCommands) Execute(args []string) error {
initializeConnections(c.Units)

timeout := time.Second * time.Duration(c.Timeout)
addresses, err := addressValues(c.Args.AddressValues, false)
if err != nil {
return err
}

// run the commands
for _, sys := range c.Units {
client, _ := client(sys)
for _, rng := range addresses {
flags := make([]bool, len(rng.values))
for i, v := range rng.values {
flags[i] = v == 1
}
_, err := client.WriteMultipleCoils(rng.address, flags, timeout)
if err != nil {
fmt.Printf("Write Holdings: Failed: %v\n", err)
continue
}
got, err := client.ReadCoils(rng.address, len(flags), timeout)
if err != nil {
fmt.Printf("Write Holdings verify: Failed: %v\n", err)
} else {
fmt.Printf("Write Holdings verify: %v\n", got)
}
}
}
return nil
}

type CoilCommands struct {
Get CoilGetCommands `command:"get" alias:"read" description:"Get or read Coil values"`
Set CoilSetCommands `command:"set" alias:"write" description:"Set or write Coil values"`
}
75 changes: 75 additions & 0 deletions mbcli/diag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"fmt"
"time"

"github.com/rolfl/modbus"
)

type DiagnosticCommands struct {
ServerID bool `short:"s" long:"serverid" description:"Return the ServerID value"`
DeviceID bool `short:"d" long:"deviceid" description:"Return the DeviceID values"`
Counts bool `short:"c" long:"counts" description:"Return the Diagnostic counter values"`
Events bool `short:"e" long:"events" description:"Return the Event counter value"`
Clear bool `short:"C" long:"clear" description:"Reset the Event counter value"`
Timeout int `short:"t" long:"timeout" default:"5" description:"Timeout (in seconds)"`
Units []string `short:"u" long:"unit" description:"Unit(s) to contatc" required:"true"`
}

func (c *DiagnosticCommands) Execute(args []string) error {
// initialize the connections
for _, sys := range c.Units {
_, err := client(sys)
if err != nil {
return err
}
}

timeout := time.Second * time.Duration(c.Timeout)

// run the commands
for _, sys := range c.Units {
client, _ := client(sys)
if c.ServerID {
if sid, err := client.ServerID(timeout); err != nil {
fmt.Printf("ServerID: Failed: %v\n", err)
} else {
fmt.Printf("ServerID: %v\n", sid)
}
}
if c.DeviceID {
if did, err := client.DeviceIdentification(timeout); err != nil {
fmt.Printf("DeviceID: Failed: %v\n", err)
} else {
fmt.Printf("DeviceID: %v\n", did)
}
}
if c.Counts {
counts := []modbus.Diagnostic{
modbus.BusCommErrors,
modbus.BusExceptionErrors,
modbus.BusCharacterOverruns,
modbus.ServerMessages,
modbus.ServerNoResponses,
modbus.ServerNAKs,
modbus.ServerBusies,
}
for _, count := range counts {
if cnt, err := client.DiagnosticCount(count, timeout); err != nil {
fmt.Printf("Count %v: Failed: %v\n", count, err)
} else {
fmt.Printf("Count: %v\n", cnt)
}
}
}
if c.Clear {
if err := client.DiagnosticClear(timeout); err != nil {
fmt.Printf("Diagnostic Reset: Failed: %v\n", err)
} else {
fmt.Printf("Diagnostic counters reset\n")
}
}
}
return nil
}
17 changes: 17 additions & 0 deletions mbcli/discretes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

type DiscreteGetCommands struct {
Units []string `short:"u" long:"unit" description:"Unit(s) to contact" required:"true"`
Timeout int `short:"t" long:"timeout" default:"5" description:"Timeout (in seconds)"`
Args struct {
Addresses []string `required:"1"`
} `positional-args:"yes" required:"yes"`
}

func (c *DiscreteGetCommands) Execute(args []string) error {
return genericClientReads("discrete", c.Units, c.Args.Addresses, c.Timeout)
}

type DiscreteCommands struct {
Get DiscreteGetCommands `command:"get" alias:"read" description:"Get or read Discrete values"`
}
127 changes: 127 additions & 0 deletions mbcli/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package main

import (
"fmt"
"strconv"
"strings"
"time"
)

type addressedRange struct {
address int
count int
}

func addressRanges(refs []string) ([]addressedRange, error) {
ret := []addressedRange{}
for _, ref := range refs {
parts := strings.Split(ref, ":")
add, err := strconv.Atoi(parts[0])
if err != nil {
return nil, err
}
cnt := 1
if len(parts) > 1 {
cnt, err = strconv.Atoi(parts[1])
if err != nil {
return nil, err
}
}
ret = append(ret, addressedRange{add, cnt})
}
return ret, nil
}

type addressedValues struct {
address int
values []int
}

func addressValues(refs []string, isbool bool) ([]addressedValues, error) {
ret := []addressedValues{}
for _, ref := range refs {
parts := strings.Split(ref, ":")
add, err := strconv.Atoi(parts[0])
if err != nil {
return nil, err
}
vals := []int{}
for _, piece := range parts[1:] {
valstrs := strings.Split(piece, ",")
for _, sval := range valstrs {
val, err := strconv.Atoi(sval)
if err != nil {
if isbool && (sval == "t" || sval == "true" || sval == "on") {
val = 1
} else if isbool && (sval == "f" || sval == "false" || sval == "off") {
val = 0
} else {
return nil, err
}
}
if val < 0 {
return nil, fmt.Errorf("illegal value %v", sval)
}
if isbool && val > 1 {
return nil, fmt.Errorf("illegal bit value %v", sval)
}
vals = append(vals, val)
}
}
ret = append(ret, addressedValues{add, vals})
}
return ret, nil
}

func initializeConnections(units []string) error {
for _, sys := range units {
_, err := client(sys)
if err != nil {
return err
}
}
return nil
}

func genericClientReads(toget string, units []string, addressRefs []string, timeoutSec int) error {
// initialize the connections
initializeConnections(units)

timeout := time.Second * time.Duration(timeoutSec)
addresses, err := addressRanges(addressRefs)
if err != nil {
return err
}

// run the commands
for _, sys := range units {
client, _ := client(sys)
var got interface{}
var name string

for _, rng := range addresses {
switch toget {
case "discrete":
got, err = client.ReadDiscretes(rng.address, rng.count, timeout)
name = "Get Discretes"
case "coil":
got, err = client.ReadCoils(rng.address, rng.count, timeout)
name = "Get Coils"
case "input":
got, err = client.ReadInputs(rng.address, rng.count, timeout)
name = "Get Inputs"
case "holding":
got, err = client.ReadHoldings(rng.address, rng.count, timeout)
name = "Get Holding Registers"
default:
return fmt.Errorf("unknown read type %v", toget)
}
if err != nil {
fmt.Printf("%v: Failed: %v\n", name, err)
} else {
fmt.Printf("%v: %v\n", name, got)
}
}
}
return nil
}
Loading

0 comments on commit f85fb62

Please sign in to comment.