-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
488 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.