Files
modbus/cmd/modbus-cli.go
2025-11-07 13:53:18 +08:00

1289 lines
34 KiB
Go

package main
import (
"crypto/tls"
"encoding/hex"
"errors"
"fmt"
"flag"
"os"
"strings"
"strconv"
"time"
"github.com/simonvetter/modbus"
)
func main() {
var err error
var help bool
var client *modbus.ModbusClient
var config *modbus.ClientConfiguration
var target string
var caPath string // path to TLS CA/server certificate
var certPath string // path to TLS client certificate
var keyPath string // path to TLS client key
var clientKeyPair tls.Certificate
var speed uint
var dataBits uint
var parity string
var stopBits uint
var endianness string
var wordOrder string
var timeout string
var cEndianess modbus.Endianness
var cWordOrder modbus.WordOrder
var unitId uint
var runList []operation
flag.StringVar(&target, "target", "", "target device to connect to (e.g. tcp://somehost:502) [required]")
flag.UintVar(&speed, "speed", 19200, "serial bus speed in bps (rtu)")
flag.UintVar(&dataBits, "data-bits", 8, "number of bits per character on the serial bus (rtu)")
flag.StringVar(&parity, "parity", "none", "parity bit <none|even|odd> on the serial bus (rtu)")
flag.UintVar(&stopBits, "stop-bits", 2, "number of stop bits <0|1|2>) on the serial bus (rtu)")
flag.StringVar(&timeout, "timeout", "3s", "timeout value")
flag.StringVar(&endianness, "endianness", "big", "register endianness <little|big>")
flag.StringVar(&wordOrder, "word-order", "highfirst", "word ordering for 32-bit registers <highfirst|hf|lowfirst|lf>")
flag.UintVar(&unitId, "unit-id", 1, "unit/slave id to use")
flag.StringVar(&certPath, "cert", "", "path to TLS client certificate")
flag.StringVar(&keyPath, "key", "", "path to TLS client key")
flag.StringVar(&caPath, "ca", "", "path to TLS CA/server certificate")
flag.BoolVar(&help, "help", false, "show a wall-of-text help message")
flag.Parse()
if help {
displayHelp()
os.Exit(0)
}
if target == "" {
fmt.Printf("no target specified, please use --target\n")
os.Exit(1)
}
// create and populate the client configuration object
config = &modbus.ClientConfiguration{
URL: target,
Speed: speed,
DataBits: dataBits,
StopBits: stopBits,
}
switch parity {
case "none": config.Parity = modbus.PARITY_NONE
case "odd": config.Parity = modbus.PARITY_ODD
case "even": config.Parity = modbus.PARITY_EVEN
default:
fmt.Printf("unknown parity setting '%s' (should be one of none, odd or even)\n",
parity)
os.Exit(1)
}
config.Timeout, err = time.ParseDuration(timeout)
if err != nil {
fmt.Printf("failed to parse timeout setting '%s': %v\n", timeout, err)
os.Exit(1)
}
// parse encoding (endianness and word order) settings
switch endianness {
case "big": cEndianess = modbus.BIG_ENDIAN
case "little": cEndianess = modbus.LITTLE_ENDIAN
default:
fmt.Printf("unknown endianness setting '%s' (should either be big or little)\n",
endianness)
os.Exit(1)
}
switch wordOrder {
case "highfirst", "hf": cWordOrder = modbus.HIGH_WORD_FIRST
case "lowfirst", "lf": cWordOrder = modbus.LOW_WORD_FIRST
default:
fmt.Printf("unknown word order setting '%s' (should be one of highfirst, hf, littlefirst, lf)\n",
wordOrder)
os.Exit(1)
}
// handle TLS options
if strings.HasPrefix(target, "tcp+tls://") {
if certPath == "" {
fmt.Print("TLS requested but no client certificate given, please use --cert\n")
os.Exit(1)
}
if keyPath == "" {
fmt.Print("TLS requested but no client key given, please use --key\n")
os.Exit(1)
}
if caPath == "" {
fmt.Print("TLS requested but no CA/server cert given, please use --ca\n")
os.Exit(1)
}
clientKeyPair, err = tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
fmt.Printf("failed to load client tls key pair: %v\n", err)
os.Exit(1)
}
config.TLSClientCert = &clientKeyPair
config.TLSRootCAs, err = modbus.LoadCertPool(caPath)
if err != nil {
fmt.Printf("failed to load tls CA/server certificate: %v\n", err)
os.Exit(1)
}
}
if len(flag.Args()) == 0 {
fmt.Printf("nothing to do.\n")
os.Exit(0)
}
// parse arguments and build a list of objects
for _, arg := range flag.Args() {
var splitArgs []string
var o operation
splitArgs = strings.Split(arg, ":")
if len(splitArgs) < 2 && splitArgs[0] != "repeat" && splitArgs[0] != "date" {
fmt.Printf("illegal command format (should be command:arg1:arg2..., e.g. rh:uint32:0x1000+5)\n")
os.Exit(2)
}
switch splitArgs[0] {
case "rc", "readCoil", "readCoils",
"rdi", "readDiscreteInput", "readDiscreteInputs":
if len(splitArgs) != 2 {
fmt.Printf("need exactly 1 argument after rc/rdi, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
if splitArgs[0] == "rc" || splitArgs[0] == "readCoil" || splitArgs[0] == "readCoils" {
o.isCoil = true
}
o.op = readBools
o.addr, o.quantity, err = parseAddressAndQuantity(splitArgs[1])
if err != nil {
fmt.Printf("failed to parse address ('%v'): %v\n", splitArgs[1], err)
os.Exit(2)
}
case "rh", "readHoldingRegister", "readHoldingRegisters",
"ri", "readInputRegister", "readInputRegisters":
if len(splitArgs) != 3 {
fmt.Printf("need exactly 2 arguments after rh/ri, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
if splitArgs[0] == "rh" || splitArgs[0] == "readHoldingRegister" ||
splitArgs[0] == "readHoldingRegisters" {
o.isHoldingReg = true
}
switch splitArgs[1] {
case "uint16": o.op = readUint16
case "int16": o.op = readInt16
case "uint32": o.op = readUint32
case "int32": o.op = readInt32
case "float32": o.op = readFloat32
case "uint64": o.op = readUint64
case "int64": o.op = readInt64
case "float64": o.op = readFloat64
case "bytes": o.op = readBytes
default:
fmt.Printf("unknown register type '%v' (should be one of " +
"[u]unt16, [u]int32, [u]int64, float32, float64, bytes)\n",
splitArgs[1])
os.Exit(2)
}
o.addr, o.quantity, err = parseAddressAndQuantity(splitArgs[2])
if err != nil {
fmt.Printf("failed to parse address ('%v'): %v\n", splitArgs[2], err)
os.Exit(2)
}
case "wc", "writeCoil":
if len(splitArgs) != 3 {
fmt.Printf("need exactly 2 arguments after writeCoil, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
o.op = writeCoil
o.addr, err = parseUint16(splitArgs[1])
if err != nil {
fmt.Printf("failed to parse address ('%v'): %v\n", splitArgs[1], err)
os.Exit(2)
}
switch splitArgs[2] {
case "true": o.coil = true
case "false": o.coil = false
default:
fmt.Printf("failed to parse coil value '%v' (should either be true or false)\n",
splitArgs[2])
os.Exit(2)
}
case "wr", "writeRegister":
if len(splitArgs) != 4 {
fmt.Printf("need exactly 3 arguments after writeRegister, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
o.addr, err = parseUint16(splitArgs[2])
if err != nil {
fmt.Printf("failed to parse address ('%v'): %v\n", splitArgs[2], err)
os.Exit(2)
}
switch splitArgs[1] {
case "uint16":
o.op = writeUint16
o.u16, err = parseUint16(splitArgs[3])
case "int16":
o.op = writeInt16
o.u16, err = parseInt16(splitArgs[3])
case "uint32":
o.op = writeUint32
o.u32, err = parseUint32(splitArgs[3])
case "int32":
o.op = writeInt32
o.u32, err = parseInt32(splitArgs[3])
case "float32":
o.op = writeFloat32
o.f32, err = parseFloat32(splitArgs[3])
case "uint64":
o.op = writeUint64
o.u64, err = parseUint64(splitArgs[3])
case "int64":
o.op = writeInt64
o.u64, err = parseInt64(splitArgs[3])
case "float64":
o.op = writeFloat64
o.f64, err = parseFloat64(splitArgs[3])
case "bytes":
o.op = writeBytes
o.bytes, err = parseHexBytes(splitArgs[3])
case "string":
o.op = writeBytes
o.bytes = []byte(splitArgs[3])
err = nil
default:
fmt.Printf("unknown register type '%v' (should be one of " +
"[u]unt16, [u]int32, [u]int64, float32, float64, bytes, string)\n",
splitArgs[1])
os.Exit(2)
}
if err != nil {
fmt.Printf("failed to parse '%s' as %s: %v\n", splitArgs[3], splitArgs[1], err)
os.Exit(2)
}
case "sleep":
if len(splitArgs) != 2 {
fmt.Printf("need exactly 1 argument after sleep, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
o.op = sleep
o.duration, err = time.ParseDuration(splitArgs[1])
if err != nil {
fmt.Printf("failed to parse '%s' as duration: %v\n", splitArgs[1], err)
os.Exit(2)
}
case "suid", "setUnitId", "sid":
if len(splitArgs) != 2 {
fmt.Printf("need exactly 1 argument after setUnitId, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
o.op = setUnitId
o.unitId, err = parseUnitId(splitArgs[1])
if err != nil {
fmt.Printf("failed to parse '%s' as unit id: %v\n", splitArgs[1], err)
os.Exit(2)
}
case "repeat":
if len(splitArgs) != 1 {
fmt.Printf("repeat takes no arguments, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
o.op = repeat
case "date":
if len(splitArgs) != 1 {
fmt.Printf("date takes no arguments, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
o.op = date
case "scan":
if len(splitArgs) != 2 {
fmt.Printf("need exactly 1 argument after scan, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
switch splitArgs[1] {
case "c", "coils":
o.op = scanBools
o.isCoil = true
case "di", "discreteInputs":
o.op = scanBools
o.isCoil = false
case "h", "hr", "holding", "holdingRegisters":
o.op = scanRegisters
o.isHoldingReg = true
case "i", "ir", "input", "inputRegisters":
o.op = scanRegisters
o.isHoldingReg = false
case "s", "sid":
o.op = scanUnitId
default:
fmt.Printf("unknown scan/register type '%s' (valid options <coils|di|hr|ir|s>\n",
splitArgs[1])
os.Exit(2)
}
case "ping":
if len(splitArgs) < 2 || len(splitArgs) > 3 {
fmt.Printf("need 1 or 2 arguments after ping, got %v\n",
len(splitArgs) - 1)
os.Exit(2)
}
o.op = ping
o.quantity, err = parseUint16(splitArgs[1])
if err != nil {
fmt.Printf("failed to parse ping count ('%v'): %v\n", splitArgs[1], err)
os.Exit(2)
}
if o.quantity == 0 {
fmt.Printf("illegal ping count value (must be >= 1)\n")
os.Exit(2)
}
if len(splitArgs) == 3 {
o.duration, err = time.ParseDuration(splitArgs[2])
if err != nil {
fmt.Printf("failed to parse '%s' as duration: %v\n", splitArgs[2], err)
os.Exit(2)
}
}
default:
fmt.Printf("unsupported command '%v'\n", splitArgs[0])
os.Exit(2)
}
runList = append(runList, o)
}
// create the modbus client
client, err = modbus.NewClient(config)
if err != nil {
fmt.Printf("failed to create client: %v\n", err)
os.Exit(1)
}
err = client.SetEncoding(cEndianess, cWordOrder)
if err != nil {
fmt.Printf("failed to set encoding: %v\n", err)
os.Exit(1)
}
// set the initial unit id (note: this can be changed later at runtime through
// the setUnitId command)
if unitId > 0xff {
fmt.Printf("set unit id: value '%v' out of range\n", unitId)
os.Exit(1)
}
client.SetUnitId(uint8(unitId))
// connect to the remote host/open the serial port
err = client.Open()
if err != nil {
fmt.Printf("failed to open client: %v\n", err)
os.Exit(2)
}
for opIdx := 0; opIdx < len(runList); opIdx++ {
var o *operation = &runList[opIdx]
switch o.op {
case readBools:
var res []bool
if o.isCoil {
res, err = client.ReadCoils(o.addr, o.quantity + 1)
} else {
res, err = client.ReadDiscreteInputs(o.addr, o.quantity + 1)
}
if err != nil {
fmt.Printf("failed to read coils/discrete inputs: %v\n", err)
} else {
for idx := range res {
fmt.Printf("0x%04x\t%-5v : %v\n", o.addr + uint16(idx),
o.addr + uint16(idx),
res[idx])
}
}
case readUint16, readInt16:
var res []uint16
if o.isHoldingReg {
res, err = client.ReadRegisters(o.addr, o.quantity + 1, modbus.HOLDING_REGISTER)
} else {
res, err = client.ReadRegisters(o.addr, o.quantity + 1, modbus.INPUT_REGISTER)
}
if err != nil {
fmt.Printf("failed to read holding/input registers: %v\n", err)
} else {
for idx := range res {
if o.op == readUint16 {
fmt.Printf("0x%04x\t%-5v : 0x%04x\t%v\n",
o.addr + uint16(idx),
o.addr + uint16(idx),
res[idx], res[idx])
} else {
fmt.Printf("0x%04x\t%-5v : 0x%04x\t%v\n",
o.addr + uint16(idx),
o.addr + uint16(idx),
res[idx], int16(res[idx]))
}
}
}
case readUint32, readInt32:
var res []uint32
if o.isHoldingReg {
res, err = client.ReadUint32s(o.addr, o.quantity + 1, modbus.HOLDING_REGISTER)
} else {
res, err = client.ReadUint32s(o.addr, o.quantity + 1, modbus.INPUT_REGISTER)
}
if err != nil {
fmt.Printf("failed to read holding/input registers: %v\n", err)
} else {
for idx := range res {
if o.op == readUint32 {
fmt.Printf("0x%04x\t%-5v : 0x%08x\t%v\n",
o.addr + (uint16(idx) * 2),
o.addr + (uint16(idx) * 2),
res[idx], res[idx])
} else {
fmt.Printf("0x%04x\t%-5v : 0x%08x\t%v\n",
o.addr + (uint16(idx) * 2),
o.addr + (uint16(idx) * 2),
res[idx], int32(res[idx]))
}
}
}
case readFloat32:
var res []float32
if o.isHoldingReg {
res, err = client.ReadFloat32s(o.addr, o.quantity + 1, modbus.HOLDING_REGISTER)
} else {
res, err = client.ReadFloat32s(o.addr, o.quantity + 1, modbus.INPUT_REGISTER)
}
if err != nil {
fmt.Printf("failed to read holding/input registers: %v\n", err)
} else {
for idx := range res {
fmt.Printf("0x%04x\t%-5v : %f\n",
o.addr + (uint16(idx) * 2),
o.addr + (uint16(idx) * 2),
res[idx])
}
}
case readUint64, readInt64:
var res []uint64
if o.isHoldingReg {
res, err = client.ReadUint64s(o.addr, o.quantity + 1, modbus.HOLDING_REGISTER)
} else {
res, err = client.ReadUint64s(o.addr, o.quantity + 1, modbus.INPUT_REGISTER)
}
if err != nil {
fmt.Printf("failed to read holding/input registers: %v\n", err)
} else {
for idx := range res {
if o.op == readUint64 {
fmt.Printf("0x%04x\t%-5v : 0x%016x\t%v\n",
o.addr + (uint16(idx) * 4),
o.addr + (uint16(idx) * 4),
res[idx], res[idx])
} else {
fmt.Printf("0x%04x\t%-5v : 0x%016x\t%v\n",
o.addr + (uint16(idx) * 4),
o.addr + (uint16(idx) * 4),
res[idx], int64(res[idx]))
}
}
}
case readFloat64:
var res []float64
if o.isHoldingReg {
res, err = client.ReadFloat64s(o.addr, o.quantity + 1, modbus.HOLDING_REGISTER)
} else {
res, err = client.ReadFloat64s(o.addr, o.quantity + 1, modbus.INPUT_REGISTER)
}
if err != nil {
fmt.Printf("failed to read holding/input registers: %v\n", err)
} else {
for idx := range res {
fmt.Printf("0x%04x\t%-5v : %f\n",
o.addr + (uint16(idx) * 4),
o.addr + (uint16(idx) * 4),
res[idx])
}
}
case readBytes:
var res []byte
if o.isHoldingReg {
res, err = client.ReadBytes(o.addr, o.quantity + 1, modbus.HOLDING_REGISTER)
} else {
res, err = client.ReadBytes(o.addr, o.quantity + 1, modbus.INPUT_REGISTER)
}
if err != nil {
fmt.Printf("failed to read holding/input registers: %v\n", err)
} else {
for idx := range res {
if (idx % 16) == 0 {
fmt.Printf("0x%04x\t%-5v : ",
o.addr + (uint16(idx/2)), o.addr + (uint16(idx/2)))
}
fmt.Printf("%02x", res[idx])
if (idx % 16) == 15 || idx == len(res) - 1 {
fmt.Printf(" <%s>\n",
decodeString(res[(idx/16*16):(idx/16*16)+(idx%16)+1]))
} else if (idx % 16) == 7 {
fmt.Printf(" ")
}
}
}
case writeCoil:
err = client.WriteCoil(o.addr, o.coil)
if err != nil {
fmt.Printf("failed to write %v at coil address 0x%04x: %v\n",
o.coil, o.addr, err)
} else {
fmt.Printf("wrote %v at coil address 0x%04x\n",
o.coil, o.addr)
}
case writeUint16:
err = client.WriteRegister(o.addr, o.u16)
if err != nil {
fmt.Printf("failed to write %v at register address 0x%04x: %v\n",
o.u16, o.addr, err)
} else {
fmt.Printf("wrote %v at register address 0x%04x\n",
o.u16, o.addr)
}
case writeInt16:
err = client.WriteRegister(o.addr, o.u16)
if err != nil {
fmt.Printf("failed to write %v at register address 0x%04x: %v\n",
int16(o.u16), o.addr, err)
} else {
fmt.Printf("wrote %v at register address 0x%04x\n",
int16(o.u16), o.addr)
}
case writeUint32:
err = client.WriteUint32(o.addr, o.u32)
if err != nil {
fmt.Printf("failed to write %v at address 0x%04x: %v\n",
o.u32, o.addr, err)
} else {
fmt.Printf("wrote %v at address 0x%04x\n",
o.u32, o.addr)
}
case writeInt32:
err = client.WriteUint32(o.addr, o.u32)
if err != nil {
fmt.Printf("failed to write %v at address 0x%04x: %v\n",
int32(o.u32), o.addr, err)
} else {
fmt.Printf("wrote %v at address 0x%04x\n",
int32(o.u32), o.addr)
}
case writeFloat32:
err = client.WriteFloat32(o.addr, o.f32)
if err != nil {
fmt.Printf("failed to write %f at address 0x%04x: %v\n",
o.f32, o.addr, err)
} else {
fmt.Printf("wrote %f at address 0x%04x\n",
o.f32, o.addr)
}
case writeUint64:
err = client.WriteUint64(o.addr, o.u64)
if err != nil {
fmt.Printf("failed to write %v at address 0x%04x: %v\n",
o.u64, o.addr, err)
} else {
fmt.Printf("wrote %v at address 0x%04x\n",
o.u64, o.addr)
}
case writeInt64:
err = client.WriteUint64(o.addr, o.u64)
if err != nil {
fmt.Printf("failed to write %v at address 0x%04x: %v\n",
int64(o.u64), o.addr, err)
} else {
fmt.Printf("wrote %v at address 0x%04x\n",
int64(o.u64), o.addr)
}
case writeFloat64:
err = client.WriteFloat64(o.addr, o.f64)
if err != nil {
fmt.Printf("failed to write %f at address 0x%04x: %v\n",
o.f64, o.addr, err)
} else {
fmt.Printf("wrote %f at address 0x%04x\n",
o.f64, o.addr)
}
case writeBytes:
err = client.WriteBytes(o.addr, o.bytes)
if err != nil {
fmt.Printf("failed to write %v at address 0x%04x: %v\n",
o.bytes, o.addr, err)
} else {
fmt.Printf("wrote %v bytes at address 0x%04x\n",
len(o.bytes), o.addr)
}
case sleep:
time.Sleep(o.duration)
case setUnitId:
client.SetUnitId(o.unitId)
case repeat:
// start over
opIdx = -1
case date:
fmt.Printf("%s\n", time.Now().Format(time.RFC3339))
case scanBools:
performBoolScan(client, o.isCoil)
case scanRegisters:
performRegisterScan(client, o.isHoldingReg)
case scanUnitId:
performUnitIdScan(client)
case ping:
performPing(client, o.quantity, o.duration);
default:
fmt.Printf("unknown operation %v\n", o)
os.Exit(100)
}
}
return
}
const (
readBools uint = iota + 1
readUint16
readInt16
readUint32
readInt32
readFloat32
readUint64
readInt64
readFloat64
readBytes
writeCoil
writeCoils
writeUint16
writeInt16
writeInt32
writeUint32
writeFloat32
writeInt64
writeUint64
writeFloat64
writeBytes
setUnitId
sleep
repeat
date
scanBools
scanRegisters
scanUnitId
ping
)
type operation struct {
op uint
addr uint16
isCoil bool
isHoldingReg bool
quantity uint16
coil bool
u16 uint16
u32 uint32
f32 float32
u64 uint64
f64 float64
bytes []byte
duration time.Duration
unitId uint8
}
func parseUint16(in string) (u16 uint16, err error) {
var val uint64
val, err = strconv.ParseUint(in, 0, 16)
if err == nil {
u16 = uint16(val)
return
}
return
}
func parseInt16(in string) (u16 uint16, err error) {
var val int64
val, err = strconv.ParseInt(in, 0, 16)
if err == nil {
u16 = uint16(int16(val))
}
return
}
func parseUint32(in string) (u32 uint32, err error) {
var val uint64
val, err = strconv.ParseUint(in, 0, 32)
if err == nil {
u32 = uint32(val)
return
}
return
}
func parseInt32(in string) (u32 uint32, err error) {
var val int64
val, err = strconv.ParseInt(in, 0, 32)
if err == nil {
u32 = uint32(int32(val))
}
return
}
func parseFloat32(in string) (f32 float32, err error) {
var val float64
val, err = strconv.ParseFloat(in, 32)
if err == nil {
f32 = float32(val)
}
return
}
func parseUint64(in string) (u64 uint64, err error) {
var val uint64
val, err = strconv.ParseUint(in, 0, 64)
if err == nil {
u64 = val
}
return
}
func parseInt64(in string) (u64 uint64, err error) {
var val int64
val, err = strconv.ParseInt(in, 0, 64)
if err == nil {
u64 = uint64(val)
}
return
}
func parseFloat64(in string) (f64 float64, err error) {
var val float64
val, err = strconv.ParseFloat(in, 64)
if err == nil {
f64 = val
}
return
}
func parseAddressAndQuantity(in string) (addr uint16, quantity uint16, err error) {
var split = strings.Split(in, "+")
switch {
case len(split) == 1:
addr, err = parseUint16(in)
case len(split) == 2:
addr, err = parseUint16(split[0])
if err != nil {
return
}
quantity, err = parseUint16(split[1])
default:
err = errors.New("illegal format")
}
return
}
func parseUnitId(in string) (addr uint8, err error) {
var val uint64
val, err = strconv.ParseUint(in, 0, 8)
if err == nil {
addr = uint8(val)
}
return
}
func parseHexBytes(in string) (out []byte, err error) {
out, err = hex.DecodeString(in)
return
}
func performBoolScan(client *modbus.ModbusClient, isCoil bool) {
var err error
var addr uint32
var val bool
var count uint
var regType string
if isCoil {
regType = "coil"
} else {
regType = "discrete input"
}
fmt.Printf("starting %s scan\n", regType)
for addr = 0; addr <= 0xffff; addr++ {
if isCoil {
val, err = client.ReadCoil(uint16(addr))
} else {
val, err = client.ReadDiscreteInput(uint16(addr))
}
if err == modbus.ErrIllegalDataAddress || err == modbus.ErrIllegalFunction {
// the register does not exist
continue
} else if err != nil {
fmt.Printf("failed to read %s at address 0x%04x: %v\n",
regType, addr, err)
} else {
// we found a coil: display its address and value
fmt.Printf("0x%04x\t%-5v : %v\n", addr, addr, val)
count++
}
}
fmt.Printf("found %v %ss\n", count, regType)
return
}
func performRegisterScan(client *modbus.ModbusClient, isHoldingReg bool) {
var err error
var addr uint32
var val uint16
var count uint
var regType string
if isHoldingReg {
regType = "holding register"
} else {
regType = "input register"
}
fmt.Printf("starting %s scan\n", regType)
for addr = 0; addr <= 0xffff; addr++ {
if isHoldingReg {
val, err = client.ReadRegister(uint16(addr), modbus.HOLDING_REGISTER)
} else {
val, err = client.ReadRegister(uint16(addr), modbus.INPUT_REGISTER)
}
if err == modbus.ErrIllegalDataAddress || err == modbus.ErrIllegalFunction {
// the register does not exist
continue
} else if err != nil {
fmt.Printf("failed to read %s at address 0x%04x: %v\n",
regType, addr, err)
} else {
// we found a register: display its address and value
fmt.Printf("0x%04x\t%-5v : 0x%04x\t%v\n",
addr, addr, val, val)
count++
}
}
fmt.Printf("found %v %ss\n", count, regType)
return
}
func performUnitIdScan(client *modbus.ModbusClient) {
var err error
var countOk uint
var countErr uint
var countTimeout uint
var countGWTimeout uint
fmt.Println("starting unit id scan")
for unitId := uint(0); unitId <= 0xff; unitId++ {
client.SetUnitId(uint8(unitId))
_, err = client.ReadRegister(0, modbus.INPUT_REGISTER)
switch err {
case nil,
modbus.ErrIllegalDataAddress,
modbus.ErrIllegalFunction,
modbus.ErrIllegalDataValue:
fmt.Printf("0x%02x (%3v): ok\n", unitId, unitId)
countOk++
case modbus.ErrRequestTimedOut:
countTimeout++
case modbus.ErrGWTargetFailedToRespond:
countGWTimeout++
default:
fmt.Printf("0x%02x (%3v): %v\n", unitId, unitId, err)
countErr++
}
}
fmt.Printf("found %v devices (%v errors, %v timeouts, %v gateway timeouts)\n",
countOk, countErr, countTimeout, countGWTimeout)
return
}
func performPing(client *modbus.ModbusClient, count uint16, interval time.Duration) {
var err error
var okCount uint
var timeoutCount uint
var otherErrCount uint
var startTs time.Time
var ts time.Time
var rtt time.Duration
var minRTT time.Duration
var maxRTT time.Duration
var avgRTT time.Duration
fmt.Printf("ping: sending %v requests...\n", count)
startTs = time.Now()
for run := uint16(0); run < count; run++ {
ts = time.Now()
_, err = client.ReadRegister(0x0000, modbus.HOLDING_REGISTER)
rtt = time.Since(ts)
avgRTT += rtt
if run == 0 || rtt < minRTT {
minRTT = rtt
}
if rtt > maxRTT {
maxRTT = rtt
}
switch err {
// mask illegal data address and illegal function errors since we
// only care about getting a response from the target device
// (on which holding reg #0 may or may not exist)
case nil, modbus.ErrIllegalDataAddress, modbus.ErrIllegalFunction:
okCount++
fmt.Printf("ok: seq = %v, time: %v\n",
run + 1, rtt.Round(time.Microsecond))
case modbus.ErrRequestTimedOut, modbus.ErrGWTargetFailedToRespond:
timeoutCount++
fmt.Printf("timeout (%v): seq = %v, time: %v\n",
err, run + 1, rtt.Round(time.Microsecond))
default:
otherErrCount++
fmt.Printf("error (%v): seq = %v, time: %v\n",
err, run + 1, rtt.Round(time.Microsecond))
}
if interval > 0 {
time.Sleep(interval)
}
}
fmt.Printf("--- ping statistics ---\n" +
"%v queries, %v target replies, %v transmission errors, %v timeouts, time: %v\n",
count, okCount, otherErrCount, timeoutCount,
time.Since(startTs).Round(time.Millisecond))
fmt.Printf("rtt min/avg/max = %v/%v/%v\n",
minRTT.Round(time.Microsecond),
(avgRTT / time.Duration(count)).Round(time.Microsecond),
maxRTT.Round(time.Microsecond))
return
}
func decodeString(in []byte) (out string) {
var dec []byte
var b byte
for idx := range in {
if in[idx] >= 0x20 && in[idx] <= 0x7e {
b = in[idx]
} else {
b = '.'
}
dec = append(dec, b)
}
out = string(dec)
return
}
func displayHelp() {
flag.CommandLine.SetOutput(os.Stdout)
fmt.Println(
`This tool is a modbus command line interface client meant to allow quick and easy
interaction with modbus devices (e.g. for probing or troubleshooting).
Available options:`)
flag.PrintDefaults()
fmt.Printf(
`
Commands must be given as trailing arguments after any options.
Example: modbus-cli --target=tcp://somehost:502 --timeout=3s rh:uint16:0x100+5 wc:12:true
Read 6 holding registers at address 0x100 then set the coil at address 12 to true
on modbus/tcp device somehost port 502, with a timeout of 3s.
Available commands:
* <rc|readCoils>:<addr>[+additional quantity]
Read coil at address <addr>, plus any additional coils if specified.
rc:0x100+199 reads 200 coils starting at address 0x100 (hex)
rc:300 reads 1 coil at address 300 (decimal)
* <rdi|readDiscreteInputs>:<addr>[+additional quantity]
Read discrete input at address <addr>, plus any additional discrete inputs if specified.
rdi:0x100+199 reads 200 discrete inputs starting at address 0x100 (hex)
rdi:300 reads 1 discrete input at address 300 (decimal)
* <rh|readHoldingRegisters>:<type>:<addr>[+additional quantity]
Read holding registers at address <addr>, plus any additional registers if specified,
decoded as <type> which should be one of:
- uint16: unsigned 16-bit integer,
- int16: signed 16-bit integer,
- uint32: unsigned 32-bit integer (2 contiguous modbus registers),
- int32: signed 32-bit integer (2 contiguous modbus registers),
- float32: 32-bit floating point number (2 contiguous modbus registers),
- uint64: unsigned 64-bit integer (4 contiguous modbus registers),
- int64: signed 64-bit integer (4 contiguous modbus registers),
- float64: 64-bit floating point number (4 contiguous modbus registers),
- bytes: string of bytes (2 bytes per modbus register).
rh:int16:0x300+1 reads 2 consecutive 16-bit signed integers at addresses 0x300 and 0x301
rh:uint32:20 reads a 32-bit unsigned integer at addresses 20-21 (2 modbus registers)
rh:float32:500+10 reads 11 32-bit floating point numbers at addresses 500-521
(11 * 32bit make for 22 16-bit contiguous modbus registers)
* <ri:readInputRegisters>:<type>:<addr>[+additional quanitity]
Read input registers at address <addr>, plus any additional registers if specified, decoded
in the same way as explained above.
ri:uint16:0x300+1 reads 2 consecutive 16-bit unsigned integers at addresses 0x300 and 0x301
ri:int32:20 reads a 32-bit signed integer at addresses 20-21 (2 modbus registers)
* <wc|writeCoil>:<addr>:<value>
Set the coil at address <addr> to either true or false, depending on <value>.
wc:1:true writes true to the coil at address 1
wc:2:false writes false to the coil at address 2
* <wr:writeRegister>:<type>:<addr>:<value>
Write <value> to register(s) at address <addr>, using the encoding given by <type>.
wr:int16:0xf100:-10 writes -10 as a 16-bit signed integer at address 0xf100
(1 modbus register)
wr:int32:0xff00:0xff writes 0xff as a 32-bit signed integer at addresses 0xff00-0xff01
(2 consecutive modbus registers)
wr:float64:100:-3.2 writes -3.2 as a 64-bit float at addresses 100-103
(4 consecutive modbus registers)
wr:bytes:5:fafbfcfd writes 0xfafbfcfd as a 4-byte string at addresses 5-6
(2 consecutive modbus registers)
* sleep:<duration>
Pause for <duration>, specified as a golang duration string.
sleep:300s sleeps for 300 seconds
sleep:3m sleeps for 3 minutes
sleep:3ms sleeps for 3 milliseconds
* <setUnitId|suid|sid>:<unit id>
Switch to unit id (slave id) <unit id> for subsequent requests.
sid:10 selects unit id #10
* repeat
Restart execution of the given commands.
rh:uint32:100 sleep:1s repeat reads a 32-bit unsigned integer at addresses 100-101 and
pauses for one second, forever in a loop.
* date
Print the current date and time (can be useful for long-running scripts).
* scan:<type>
Perform a modbus "scan" of the modbus type <type>, which can be one of:
- "c", "coils",
- "di", "discreteInputs",
- "hr", "holdingRegisters",
- "ir", "inputRegisters",
- "s", "sid".
scan:hr scans the device for holding registers.
scan:di scans the device for discrete inputs.
Read requests are made over the entire address space (65535 addresses).
Adresses for which a non-error response is received are listed, along with the value received.
Errors other than Illegal Data Address and Illegal Function are also shown, as they should
not happen in sane implementations.
scan:sid scans the target for devices.
Scans all unit IDs (0 to 255) using a single read input register request. Addresses responding
positively or with non-timeout errors are shown, while timeouts and gateway timeouts are ignored.
This command can be used to find active nodes on RS485 buses, behind gateways or in composite
devices.
* ping:<count>[:interval]
Executes <count> modbus reads (1 holding register at address 0x0000), either back to back or
separated by [interval] if specified, then prints timing and outcome statistics.
This command can be used to troubleshoot network or serial connections.
Register endianness and word order:
The endianness of holding/input registers can be specified with --endianness <big|little> and
defaults to big endian (as per the modbus spec).
For constructs spanning multiple consecutive registers (namely [u]int32, float32, [u]int64 and
float64), the word order can be set with --word-order <highfirst|lowfirst> and arbitrarily
defaults to highfirst (i.e. most significant word first).
Supported transports and associated target schemes:
- Modbus RTU using a local serial device: rtu:///path/to/device
- Modbus RTU over TCP (RTU framing over a TCP socket): rtuovertcp://host:port
- Modbus RTU over UDP (RTU framing over an UDP socket): rtuoverudp://host:port
- Modbus TCP (MBAP): tcp://host:port
- Modbus TCP over TLS (MBAPS or Modbus Security): tcp+tls://host:port
- Modbus TCP over UDP (MBAP over UDP): udp://host:port
Note that UDP transports are not part of the Modbus protocol specification.
Examples:
$ modbus-cli --target tcp://10.100.0.10:502 rh:uint32:0x100+5 rc:0+10 wc:3:true
Connect to 10.100.0.10 port 502, read 6 consecutive 32-bit unsigned integers at addresses
0x100-0x10b (12 modbus registers) and 11 coils at addresses 0-10, then set the coil at
address 3 to true.
$ modbus-cli --target rtu:///dev/ttyUSB0 --speed 19200 suid:2 rh:uint16:0+7 \
wr:uint16:0x2:0x0605 suid:3 ri:int16:0+1 sleep:1s repeat
Open serial port /dev/ttyUSB0 at a speed of 19200 bps and repeat forever:
select unit id (slave id) 2, read holding registers at addresses 0-7 as 16 bit unsigned
integers, write 0x605 as a 16-bit unsigned integer at address 2,
change for unit id 3, read input registers 0-1 as 16-bit signed integers,
pause for 1s.
$ modbus-cli --target tcp://somehost:502 scan:hr scan:ir scan:di scan:coils
Connect to somehost port 502 and perform a scan of all modbus types (namely
holding registers, input registers, discrete inputs and coils).
$ modbus-cli --target tcp+tls://securehost:802 --cert client.cert.pem --key client.key.pem \
--ca ca.cert.pem rh:uint32:0x3000
Connect to securehost port 802 using modbus/TCP over TLS, using client.cert.pem and
client.key.pem to authenticate to the server (client auth) and ca.cert.pem to authenticate
the server, then read holding registers 0x3000-0x3001 as a 32-bit unsigned integer.
Note that ca.cert.pem can either be a CA (Certificate Authority) or the server (leaf)
certificate.
`)
return
}