diff --git a/go.mod b/go.mod index 7760195..a67d62f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/rolfl/modbus go 1.15 + +require github.com/jessevdk/go-flags v1.4.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1b3c118 --- /dev/null +++ b/go.sum @@ -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= diff --git a/mbcli/client.go b/mbcli/client.go new file mode 100644 index 0000000..7085d05 --- /dev/null +++ b/mbcli/client.go @@ -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]) +} diff --git a/mbcli/coils.go b/mbcli/coils.go new file mode 100644 index 0000000..d78936f --- /dev/null +++ b/mbcli/coils.go @@ -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"` +} diff --git a/mbcli/diag.go b/mbcli/diag.go new file mode 100644 index 0000000..513f6de --- /dev/null +++ b/mbcli/diag.go @@ -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 +} diff --git a/mbcli/discretes.go b/mbcli/discretes.go new file mode 100644 index 0000000..1d92fbc --- /dev/null +++ b/mbcli/discretes.go @@ -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"` +} diff --git a/mbcli/helpers.go b/mbcli/helpers.go new file mode 100644 index 0000000..64c8098 --- /dev/null +++ b/mbcli/helpers.go @@ -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 +} diff --git a/mbcli/holdings.go b/mbcli/holdings.go new file mode 100644 index 0000000..0ec0ede --- /dev/null +++ b/mbcli/holdings.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "time" +) + +type HoldingGetCommands 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 *HoldingGetCommands) Execute(args []string) error { + return genericClientReads("holding", c.Units, c.Args.Addresses, c.Timeout) +} + +type HoldingSetCommands 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 *HoldingSetCommands) 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 { + _, err := client.WriteMultipleHoldings(rng.address, rng.values, timeout) + if err != nil { + fmt.Printf("Write Holdings: Failed: %v\n", err) + continue + } + got, err := client.ReadHoldings(rng.address, len(rng.values), 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 HoldingCommands struct { + Get HoldingGetCommands `command:"get" alias:"read" description:"Get or read Holding values"` + Set HoldingSetCommands `command:"set" alias:"write" description:"Set or write Holding values"` +} diff --git a/mbcli/inputs.go b/mbcli/inputs.go new file mode 100644 index 0000000..2e6c5c5 --- /dev/null +++ b/mbcli/inputs.go @@ -0,0 +1,17 @@ +package main + +type InputGetCommands 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 *InputGetCommands) Execute(args []string) error { + return genericClientReads("input", c.Units, c.Args.Addresses, c.Timeout) +} + +type InputCommands struct { + Get InputGetCommands `command:"get" alias:"read" description:"Get or read Input values"` +} diff --git a/mbcli/mbcli.go b/mbcli/mbcli.go new file mode 100644 index 0000000..be2ce74 --- /dev/null +++ b/mbcli/mbcli.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "os" + + "github.com/jessevdk/go-flags" +) + +type CLICommand struct { + Verbose bool `long:"verbose" description:"Print API requests and responses"` + Diagnostic DiagnosticCommands `command:"diag" alias:"diagnostics" description:"Diagnostic functions"` + Discrete DiscreteCommands `command:"discrete" alias:"discretes" description:"Discrete functions"` + Coil CoilCommands `command:"coil" alias:"coils" description:"Coil functions"` + Input InputCommands `command:"input" alias:"inputs" description:"Input functions"` + Holding HoldingCommands `command:"holding" alias:"holdings" description:"Holding functions"` +} + +func main() { + clicmd := CLICommand{} + + parser := flags.NewParser(&clicmd, flags.HelpFlag|flags.PassDoubleDash) + + _, err := parser.Parse() + + if err != nil { + fmt.Println(err) + os.Exit(1) + } +}