├── .github └── workflows │ └── go.yml ├── .gitignore ├── .golangci.yaml ├── .revive.toml ├── .travis.yml ├── LICENSE ├── README.md ├── _examples ├── client_rtu_ascii │ └── main.go ├── client_tcp │ └── main.go ├── server_tcp │ └── main.go └── server_tcp_special │ └── main.go ├── api.go ├── asciiclient.go ├── asciiclient_test.go ├── buffer.go ├── buffer_test.go ├── client.go ├── client_option.go ├── client_test.go ├── crc.go ├── crc_test.go ├── error.go ├── function.go ├── function_test.go ├── go.mod ├── go.sum ├── log.go ├── lrc.go ├── lrc_test.go ├── modbus.go ├── register.go ├── register_test.go ├── revive.sh ├── rtuclient.go ├── rtuclient_test.go ├── serial.go ├── serial_test.go ├── tcp_test.go ├── tcpclient.go ├── tcpclient_test.go ├── tcpserver.go ├── tcpserver_session.go └── tcpserver_special.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build on GO ${{matrix.go-version}} 6 | runs-on: ${{matrix.os}} 7 | strategy: 8 | matrix: 9 | go-version: ["1.15.x", "1.16.x"] 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | 12 | steps: 13 | - name: Set up Go ${{matrix.go-version}} 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: ${{matrix.go-version}} 17 | id: go 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v2 21 | 22 | - name: Get dependencies 23 | run: | 24 | go get -v -t -d ./... 25 | 26 | - name: Go test osx linux 27 | if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' 28 | run: | 29 | go test -v -benchmem -test.bench=".*" -coverprofile=coverage.txt -covermode=atomic ./... 30 | 31 | - name: Go test windows 32 | if: matrix.os == 'windows-latest' 33 | run: | 34 | go test -v -covermode=atomic ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # golangci.com configuration 2 | # https://github.com/golangci/golangci/wiki/Configuration 3 | linters-settings: 4 | depguard: 5 | list-type: blacklist 6 | packages: 7 | # logging is allowed only by logutils.Log, logrus 8 | # is allowed to use only in logutils package 9 | - github.com/sirupsen/logrus 10 | packages-with-error-message: 11 | - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 12 | exhaustive: 13 | default-signifies-exhaustive: false 14 | gci: 15 | local-prefixes: github.com/golangci/golangci-lint 16 | goconst: 17 | min-len: 2 18 | min-occurrences: 2 19 | gocritic: 20 | enabled-tags: 21 | - diagnostic 22 | - experimental 23 | - opinionated 24 | - performance 25 | - style 26 | disabled-checks: 27 | - dupImport # https://github.com/go-critic/go-critic/issues/845 28 | - ifElseChain 29 | - octalLiteral 30 | - whyNoLint 31 | - wrapperFunc 32 | - unnamedResult 33 | - sloppyReassign 34 | goimports: 35 | local-prefixes: github.com/golangci/golangci-lint 36 | golint: 37 | min-confidence: 0 38 | govet: 39 | check-shadowing: false 40 | settings: 41 | printf: 42 | funcs: 43 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 44 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 45 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 46 | # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 47 | 48 | lll: 49 | line-length: 120 50 | maligned: 51 | suggest-new: true 52 | misspell: 53 | locale: US 54 | nolintlint: 55 | allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) 56 | allow-unused: false # report any unused nolint directives 57 | require-explanation: false # don't require an explanation for nolint directives 58 | require-specific: false # don't require nolint directives to be specific about which linter is being skipped 59 | # gomnd: 60 | # settings: 61 | # mnd: 62 | # checks: argument,case,condition,return # don't include the "operation" and "assign" 63 | # dupl: 64 | # threshold: 100 65 | # funlen: 66 | # lines: 100 67 | # statements: 50 68 | # gocyclo: 69 | # min-complexity: 20 70 | 71 | linters: 72 | disable-all: true 73 | enable: 74 | - bodyclose 75 | - deadcode 76 | - depguard 77 | - dogsled 78 | - errcheck 79 | - exhaustive 80 | - goconst 81 | - gocritic 82 | - gofmt 83 | - goimports 84 | - golint 85 | - goprintffuncname 86 | - gosimple 87 | - govet 88 | - ineffassign 89 | - lll 90 | - misspell 91 | - nakedret 92 | - noctx 93 | - nolintlint 94 | - rowserrcheck 95 | - scopelint 96 | - staticcheck 97 | - structcheck 98 | - stylecheck 99 | - typecheck 100 | - unconvert 101 | - unparam 102 | - unused 103 | - varcheck 104 | - whitespace 105 | - unparam 106 | # - interfacer 107 | # - gosec 108 | # - gomnd 109 | # - gochecknoinits 110 | # - dupl 111 | # - funlen 112 | # - gocyclo 113 | # don't enable: 114 | # - asciicheck 115 | # - gochecknoglobals 116 | # - gocognit 117 | # - godot 118 | # - godox 119 | # - goerr113 120 | # - maligned 121 | # - nestif 122 | # - prealloc 123 | # - testpackage 124 | # - wsl 125 | 126 | issues: 127 | # Excluding configuration per-path, per-linter, per-text and per-source 128 | exclude-rules: 129 | - path: _test\.go 130 | linters: 131 | - gomnd 132 | - goconst 133 | - scopelint 134 | - lll 135 | # https://github.com/go-critic/go-critic/issues/926 136 | - linters: 137 | - gocritic 138 | text: "unnecessaryDefer:" 139 | 140 | run: 141 | skip-dirs: 142 | - .github 143 | 144 | service: 145 | golangci-lint-version: 1.30.x # use the fixed version to not introduce new linters unexpectedly 146 | prepare: 147 | - echo "here I can run custom commands, but no preparation needed for this repo" -------------------------------------------------------------------------------- /.revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "error" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.time-naming] 22 | [rule.unexported-return] 23 | [rule.indent-error-flow] 24 | [rule.errorf] 25 | [rule.empty-block] 26 | [rule.superfluous-else] 27 | [rule.unused-parameter] 28 | [rule.unreachable-code] 29 | [rule.redefines-builtin-id] 30 | 31 | # Currently this makes too much noise, but should add it in 32 | # and perhaps ignore it in a few files 33 | #[rule.confusing-naming] 34 | # severity = "warning" 35 | #[rule.confusing-results] 36 | # severity = "warning" 37 | #[rule.unused-parameter] 38 | # severity = "warning" 39 | #[rule.deep-exit] 40 | # severity = "warning" 41 | #[rule.flag-parameter] 42 | # severity = "warning" 43 | 44 | # 接收者名字的检测 45 | [rule.receiver-naming] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | os: 4 | - osx 5 | - windows 6 | - linux 7 | 8 | go: 9 | - 1.15.x 10 | - 1.16.x 11 | 12 | before_install: 13 | - if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; 14 | export PATH="$GOPATH/bin:$PATH"; fi 15 | - mkdir -p ~/bin/ && export PATH="~/bin/:$PATH" 16 | 17 | install: 18 | - go get -u golang.org/x/lint/golint 19 | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin 20 | - curl -sfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s -- -b $(go env GOPATH)/bin 21 | 22 | script: 23 | - go get -v ./... 24 | - |- 25 | case $TRAVIS_OS_NAME in 26 | linux|osx) 27 | golint ./... | reviewdog -f=golint -reporter=github-check 28 | golangci-lint run --out-format=line-number -E goimports -E misspell | reviewdog -f=golangci-lint -reporter=github-check 29 | golint ./... | reviewdog -f=golint -reporter=github-pr-review 30 | golangci-lint run --out-format=line-number -E goimports -E misspell | reviewdog -f=golangci-lint -reporter=github-pr-review 31 | ;; 32 | esac 33 | - go test -v -benchmem -test.bench=".*" -coverprofile=coverage.txt -covermode=atomic ./... 34 | 35 | after_success: 36 | - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then curl -s https://codecov.io/bash > 37 | .codecov && chmod +x .codecov && ./.codecov; else bash <(curl -s https://codecov.io/bash); 38 | fi 39 | 40 | env: 41 | global: 42 | - GO111MODULE=on -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go modbus 2 | 3 | modbus write in pure go, support rtu,ascii,tcp master library,also support tcp slave. 4 | 5 | [![GoDoc](https://godoc.org/github.com/thinkgos/gomodbus?status.svg)](https://godoc.org/github.com/thinkgos/gomodbus) 6 | [![Go.Dev reference](https://img.shields.io/badge/go.dev-reference-blue?logo=go&logoColor=white)](https://pkg.go.dev/github.com/thinkgos/gomodbus/v2?tab=doc) 7 | [![Build Status](https://www.travis-ci.com/thinkgos/gomodbus.svg?branch=master)](https://www.travis-ci.com/thinkgos/gomodbus) 8 | [![codecov](https://codecov.io/gh/thinkgos/gomodbus/branch/master/graph/badge.svg)](https://codecov.io/gh/thinkgos/gomodbus) 9 | ![Action Status](https://github.com/thinkgos/gomodbus/workflows/Go/badge.svg) 10 | [![Go Report Card](https://goreportcard.com/badge/github.com/thinkgos/gomodbus)](https://goreportcard.com/report/github.com/thinkgos/gomodbus) 11 | [![Licence](https://img.shields.io/github/license/thinkgos/gomodbus)](https://raw.githubusercontent.com/thinkgos/gomodbus/master/LICENSE) 12 | [![Tag](https://img.shields.io/github/v/tag/thinkgos/gomodbus)](https://github.com/thinkgos/gomodbus/tags) 13 | [![Sourcegraph](https://sourcegraph.com/github.com/thinkgos/gomodbus/-/badge.svg)](https://sourcegraph.com/github.com/thinkgos/gomodbus?badge) 14 | 15 | 16 | ### Supported formats 17 | 18 | - modbus Serial(RTU,ASCII) Client 19 | - modbus TCP Client 20 | - modbus TCP Server 21 | 22 | ### Features 23 | 24 | - object pool design,reduce memory allocation 25 | - fast encode and decode 26 | - interface design 27 | - simple API and support raw data api 28 | 29 | ### Installation 30 | 31 | Use go get. 32 | ```bash 33 | go get github.com/thinkgos/gomodbus/v2 34 | ``` 35 | 36 | Then import the modbus package into your own code. 37 | ```bash 38 | import modbus "github.com/thinkgos/gomodbus/v2" 39 | ``` 40 | 41 | ### Supported functions 42 | 43 | --- 44 | 45 | Bit access: 46 | * Read Discrete Inputs 47 | * Read Coils 48 | * Write Single Coil 49 | * Write Multiple Coils 50 | 51 | 16-bit access: 52 | * Read Input Registers 53 | * Read Holding Registers 54 | * Write Single Register 55 | * Write Multiple Registers 56 | * Read/Write Multiple Registers 57 | * Mask Write Register 58 | * Read FIFO Queue 59 | 60 | ### Example 61 | 62 | --- 63 | 64 | 65 | modbus RTU/ASCII client see [example](_examples/client_rtu_ascii) 66 | 67 | [embedmd]:# (_examples/client_rtu_ascii/main.go go) 68 | ```go 69 | package main 70 | 71 | import ( 72 | "fmt" 73 | "time" 74 | 75 | "github.com/goburrow/serial" 76 | modbus "github.com/thinkgos/gomodbus/v2" 77 | ) 78 | 79 | func main() { 80 | p := modbus.NewRTUClientProvider(modbus.WithEnableLogger(), 81 | modbus.WithSerialConfig(serial.Config{ 82 | Address: "/dev/ttyUSB0", 83 | BaudRate: 115200, 84 | DataBits: 8, 85 | StopBits: 1, 86 | Parity: "N", 87 | Timeout: modbus.SerialDefaultTimeout, 88 | })) 89 | 90 | client := modbus.NewClient(p) 91 | err := client.Connect() 92 | if err != nil { 93 | fmt.Println("connect failed, ", err) 94 | return 95 | } 96 | defer client.Close() 97 | 98 | fmt.Println("starting") 99 | for { 100 | _, err := client.ReadCoils(3, 0, 10) 101 | if err != nil { 102 | fmt.Println(err.Error()) 103 | } 104 | 105 | // fmt.Printf("ReadDiscreteInputs %#v\r\n", results) 106 | 107 | time.Sleep(time.Second * 2) 108 | } 109 | } 110 | ``` 111 | 112 | 113 | modbus TCP client see [example](_examples/client_tcp) 114 | 115 | [embedmd]:# (_examples/client_tcp/main.go go) 116 | ```go 117 | package main 118 | 119 | import ( 120 | "fmt" 121 | "time" 122 | 123 | modbus "github.com/thinkgos/gomodbus/v2" 124 | ) 125 | 126 | func main() { 127 | p := modbus.NewTCPClientProvider("192.168.199.188:502", modbus.WithEnableLogger()) 128 | client := modbus.NewClient(p) 129 | err := client.Connect() 130 | if err != nil { 131 | fmt.Println("connect failed, ", err) 132 | return 133 | } 134 | defer client.Close() 135 | 136 | fmt.Println("starting") 137 | for { 138 | _, err := client.ReadCoils(1, 0, 10) 139 | if err != nil { 140 | fmt.Println(err.Error()) 141 | } 142 | 143 | // fmt.Printf("ReadDiscreteInputs %#v\r\n", results) 144 | 145 | time.Sleep(time.Second * 2) 146 | } 147 | } 148 | ``` 149 | 150 | modbus TCP server see [example](_examples/server_tcp) 151 | 152 | [embedmd]:# (_examples/server_tcp/main.go go) 153 | ```go 154 | package main 155 | 156 | import ( 157 | modbus "github.com/thinkgos/gomodbus/v2" 158 | ) 159 | 160 | func main() { 161 | srv := modbus.NewTCPServer() 162 | srv.LogMode(true) 163 | srv.AddNodes( 164 | modbus.NewNodeRegister( 165 | 1, 166 | 0, 10, 0, 10, 167 | 0, 10, 0, 10), 168 | modbus.NewNodeRegister( 169 | 2, 170 | 0, 10, 0, 10, 171 | 0, 10, 0, 10), 172 | modbus.NewNodeRegister( 173 | 3, 174 | 0, 10, 0, 10, 175 | 0, 10, 0, 10)) 176 | 177 | err := srv.ListenAndServe(":502") 178 | if err != nil { 179 | panic(err) 180 | } 181 | } 182 | ``` 183 | 184 | ### References 185 | 186 | --- 187 | 188 | - [Modbus Specifications and Implementation Guides](http://www.modbus.org/specs.php) 189 | - [goburrow](https://github.com/goburrow/modbus) 190 | 191 | ### JetBrains OS licenses 192 | gomodbus had been being developed with GoLand under the free JetBrains Open Source license(s) granted by JetBrains s.r.o., hence I would like to express my thanks here. 193 | 194 | 195 | 196 | ### Donation 197 | 198 | if package help you a lot,you can support us by: 199 | 200 | **Alipay** 201 | 202 | ![alipay](https://github.com/thinkgos/thinkgos/blob/master/asserts/alipay.jpg) 203 | 204 | **WeChat Pay** 205 | 206 | ![wxpay](https://github.com/thinkgos/thinkgos/blob/master/asserts/wxpay.jpg) 207 | -------------------------------------------------------------------------------- /_examples/client_rtu_ascii/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/goburrow/serial" 8 | modbus "github.com/thinkgos/gomodbus/v2" 9 | ) 10 | 11 | func main() { 12 | p := modbus.NewRTUClientProvider(modbus.WithEnableLogger(), 13 | modbus.WithSerialConfig(serial.Config{ 14 | Address: "/dev/ttyUSB0", 15 | BaudRate: 115200, 16 | DataBits: 8, 17 | StopBits: 1, 18 | Parity: "N", 19 | Timeout: modbus.SerialDefaultTimeout, 20 | })) 21 | 22 | client := modbus.NewClient(p) 23 | err := client.Connect() 24 | if err != nil { 25 | fmt.Println("connect failed, ", err) 26 | return 27 | } 28 | defer client.Close() 29 | 30 | fmt.Println("starting") 31 | for { 32 | _, err := client.ReadCoils(3, 0, 10) 33 | if err != nil { 34 | fmt.Println(err.Error()) 35 | } 36 | 37 | // fmt.Printf("ReadDiscreteInputs %#v\r\n", results) 38 | 39 | time.Sleep(time.Second * 2) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /_examples/client_tcp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | modbus "github.com/thinkgos/gomodbus/v2" 8 | ) 9 | 10 | func main() { 11 | p := modbus.NewTCPClientProvider("192.168.199.188:502", modbus.WithEnableLogger()) 12 | client := modbus.NewClient(p) 13 | err := client.Connect() 14 | if err != nil { 15 | fmt.Println("connect failed, ", err) 16 | return 17 | } 18 | defer client.Close() 19 | 20 | fmt.Println("starting") 21 | for { 22 | _, err := client.ReadCoils(1, 0, 10) 23 | if err != nil { 24 | fmt.Println(err.Error()) 25 | } 26 | 27 | // fmt.Printf("ReadDiscreteInputs %#v\r\n", results) 28 | 29 | time.Sleep(time.Second * 2) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /_examples/server_tcp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | modbus "github.com/thinkgos/gomodbus/v2" 5 | ) 6 | 7 | func main() { 8 | srv := modbus.NewTCPServer() 9 | srv.LogMode(true) 10 | srv.AddNodes( 11 | modbus.NewNodeRegister( 12 | 1, 13 | 0, 10, 0, 10, 14 | 0, 10, 0, 10), 15 | modbus.NewNodeRegister( 16 | 2, 17 | 0, 10, 0, 10, 18 | 0, 10, 0, 10), 19 | modbus.NewNodeRegister( 20 | 3, 21 | 0, 10, 0, 10, 22 | 0, 10, 0, 10)) 23 | 24 | err := srv.ListenAndServe(":502") 25 | if err != nil { 26 | panic(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /_examples/server_tcp_special/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | _ "net/http/pprof" 7 | "time" 8 | 9 | modbus "github.com/thinkgos/gomodbus/v2" 10 | ) 11 | 12 | func main() { 13 | srv := modbus.NewTCPServerSpecial(). 14 | SetOnConnectHandler(func(c *modbus.TCPServerSpecial) error { 15 | _, err := c.UnderlyingConn().Write([]byte("hello world")) 16 | return err 17 | }). 18 | SetConnectionLostHandler(func(c *modbus.TCPServerSpecial) { 19 | log.Println("connect lost") 20 | }). 21 | SetKeepAlive(true, time.Second*20, func(c *modbus.TCPServerSpecial) { 22 | _, _ = c.UnderlyingConn().Write([]byte("keep alive")) 23 | }) 24 | if err := srv.AddRemoteServer("127.0.0.1:3001"); err != nil { 25 | panic(err) 26 | } 27 | srv.LogMode(true) 28 | srv.AddNodes( 29 | modbus.NewNodeRegister( 30 | 1, 31 | 0, 10, 0, 10, 32 | 0, 10, 0, 10), 33 | modbus.NewNodeRegister( 34 | 2, 35 | 0, 10, 0, 10, 36 | 0, 10, 0, 10), 37 | modbus.NewNodeRegister( 38 | 3, 39 | 0, 10, 0, 10, 40 | 0, 10, 0, 10), 41 | ) 42 | 43 | if err := srv.Start(); err != nil { 44 | panic(err) 45 | } 46 | 47 | if err := http.ListenAndServe(":6060", nil); err != nil { 48 | panic(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | // Client interface. 4 | type Client interface { 5 | ClientProvider 6 | // Bits 7 | 8 | // ReadCoils reads from 1 to 2000 contiguous status of coils in a 9 | // remote device and returns coil status. 10 | ReadCoils(slaveID byte, address, quantity uint16) (results []byte, err error) 11 | // ReadDiscreteInputs reads from 1 to 2000 contiguous status of 12 | // discrete inputs in a remote device and returns input status. 13 | ReadDiscreteInputs(slaveID byte, address, quantity uint16) (results []byte, err error) 14 | 15 | // WriteSingleCoil write a single output to either ON or OFF in a 16 | // remote device and returns success or failed. 17 | WriteSingleCoil(slaveID byte, address uint16, isOn bool) error 18 | // WriteMultipleCoils forces each coil in a sequence of coils to either 19 | // ON or OFF in a remote device and returns success or failed. 20 | WriteMultipleCoils(slaveID byte, address, quantity uint16, value []byte) error 21 | 22 | // 16-bits 23 | 24 | // ReadInputRegistersBytes reads from 1 to 125 contiguous input registers in 25 | // a remote device and returns input registers. 26 | ReadInputRegistersBytes(slaveID byte, address, quantity uint16) (results []byte, err error) 27 | // ReadInputRegisters reads from 1 to 125 contiguous input registers in 28 | // a remote device and returns input registers. 29 | ReadInputRegisters(slaveID byte, address, quantity uint16) (results []uint16, err error) 30 | 31 | // ReadHoldingRegistersBytes reads the contents of a contiguous block of 32 | // holding registers in a remote device and returns register value. 33 | ReadHoldingRegistersBytes(slaveID byte, address, quantity uint16) (results []byte, err error) 34 | // ReadHoldingRegisters reads the contents of a contiguous block of 35 | // holding registers in a remote device and returns register value. 36 | ReadHoldingRegisters(slaveID byte, address, quantity uint16) (results []uint16, err error) 37 | 38 | // WriteSingleRegister writes a single holding register in a remote 39 | // device and returns success or failed. 40 | WriteSingleRegister(slaveID byte, address, value uint16) error 41 | // WriteMultipleRegistersBytes writes a block of contiguous registers 42 | // (1 to 123 registers) in a remote device and returns success or failed. 43 | WriteMultipleRegistersBytes(slaveID byte, address, quantity uint16, value []byte) error 44 | // WriteMultipleRegisters writes a block of contiguous registers 45 | // (1 to 123 registers) in a remote device and returns success or failed. 46 | WriteMultipleRegisters(slaveID byte, address, quantity uint16, value []uint16) error 47 | 48 | // ReadWriteMultipleRegistersBytes performs a combination of one read 49 | // operation and one write operation. It returns read registers value. 50 | ReadWriteMultipleRegistersBytes(slaveID byte, readAddress, readQuantity, 51 | writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) 52 | // ReadWriteMultipleRegisters performs a combination of one read 53 | // operation and one write operation. It returns read registers value. 54 | ReadWriteMultipleRegisters(slaveID byte, readAddress, readQuantity, 55 | writeAddress, writeQuantity uint16, value []byte) (results []uint16, err error) 56 | 57 | // MaskWriteRegister modify the contents of a specified holding 58 | // register using a combination of an AND mask, an OR mask, and the 59 | // register's current contents. The function returns success or failed. 60 | MaskWriteRegister(slaveID byte, address, andMask, orMask uint16) error 61 | // ReadFIFOQueue reads the contents of a First-In-First-Out (FIFO) queue 62 | // of register in a remote device and returns FIFO value register. 63 | ReadFIFOQueue(slaveID byte, address uint16) (results []byte, err error) 64 | } 65 | -------------------------------------------------------------------------------- /asciiclient.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | ) 7 | 8 | // protocol frame: asciiStart + ( slaveID + functionCode + data + lrc ) + CR + LF. 9 | const ( 10 | asciiStart = ":" 11 | asciiEnd = "\r\n" 12 | hexTable = "0123456789ABCDEF" 13 | ) 14 | 15 | // ASCIIClientProvider implements ClientProvider interface. 16 | type ASCIIClientProvider struct { 17 | serialPort 18 | logger 19 | *pool 20 | } 21 | 22 | // check ASCIIClientProvider implements the interface ClientProvider underlying method. 23 | var _ ClientProvider = (*ASCIIClientProvider)(nil) 24 | 25 | // request pool, all ASCII client use this pool. 26 | var asciiPool = newPool(asciiCharacterMaxSize) 27 | 28 | // NewASCIIClientProvider allocates and initializes a ASCIIClientProvider. 29 | // it will use default /dev/ttyS0 19200 8 1 N and timeout 1000. 30 | func NewASCIIClientProvider(opts ...ClientProviderOption) *ASCIIClientProvider { 31 | p := &ASCIIClientProvider{ 32 | logger: newLogger("modbusASCIIMaster => "), 33 | pool: asciiPool, 34 | } 35 | p.autoReconnect = SerialDefaultAutoReconnect 36 | for _, opt := range opts { 37 | opt(p) 38 | } 39 | return p 40 | } 41 | 42 | // encode slaveID & PDU to a ASCII frame,return adu 43 | // Start : 1 char 44 | // slaveID : 2 chars 45 | // ---- data Unit ---- 46 | // Function : 2 chars 47 | // Data : 0 up to 2x252 chars 48 | // ---- checksum ---- 49 | // LRC : 2 chars 50 | // End : 2 chars 51 | func (sf *protocolFrame) encodeASCIIFrame(slaveID byte, pdu ProtocolDataUnit) ([]byte, error) { 52 | length := len(pdu.Data) + 3 53 | if length > asciiAduMaxSize { 54 | return nil, fmt.Errorf("modbus: length of data '%v' must not be bigger than '%v'", length, asciiAduMaxSize) 55 | } 56 | 57 | // Exclude the beginning colon and terminating CRLF pair characters 58 | lrcVal := new(LRC). 59 | Reset(). 60 | Push(slaveID).Push(pdu.FuncCode).Push(pdu.Data...). 61 | Value() 62 | 63 | // real ascii frame to send, 64 | // including asciiStart + ( slaveID + functionCode + data + lrc ) + CRLF 65 | frame := sf.adu[: 0 : length*2+3] 66 | frame = append(frame, []byte(asciiStart)...) // the beginning colon characters 67 | // the real adu 68 | frame = append(frame, 69 | hexTable[slaveID>>4], hexTable[slaveID&0x0f], // slave ID 70 | hexTable[pdu.FuncCode>>4], hexTable[pdu.FuncCode&0x0f]) // pdu funcCode 71 | for _, v := range pdu.Data { 72 | frame = append(frame, hexTable[v>>4], hexTable[v&0x0f]) // pdu data 73 | } 74 | frame = append(frame, hexTable[lrcVal>>4], hexTable[lrcVal&0x0f]) // lrc value 75 | // terminating CRLF characters 76 | return append(frame, []byte(asciiEnd)...), nil 77 | } 78 | 79 | // decode extracts slaveID & PDU from ASCII frame and verify LRC. 80 | func decodeASCIIFrame(adu []byte) (uint8, []byte, error) { 81 | if len(adu) < asciiAduMinSize+6 { // Minimum size (including address, function and LRC) 82 | return 0, nil, fmt.Errorf("modbus: response length '%v' does not meet minimum '%v'", len(adu), 9) 83 | } 84 | switch { 85 | case len(adu)%2 != 1: // Length excluding colon must be an even number 86 | return 0, nil, fmt.Errorf("modbus: response length '%v' is not an even number", len(adu)-1) 87 | case string(adu[0:len(asciiStart)]) != asciiStart: // First char must be a colons 88 | return 0, nil, fmt.Errorf("modbus: response frame '%x'... is not started with '%x'", 89 | string(adu[0:len(asciiStart)]), asciiStart) 90 | case string(adu[len(adu)-len(asciiEnd):]) != asciiEnd: // 2 last chars must be \r\n 91 | return 0, nil, fmt.Errorf("modbus: response frame ...'%x' is not ended with '%x'", 92 | string(adu[len(adu)-len(asciiEnd):]), asciiEnd) 93 | } 94 | 95 | // real adu pass Start and CRLF 96 | dat := adu[1 : len(adu)-2] 97 | buf := make([]byte, hex.DecodedLen(len(dat))) 98 | length, err := hex.Decode(buf, dat) 99 | if err != nil { 100 | return 0, nil, err 101 | } 102 | // Calculate checksum 103 | lrcVal := new(LRC).Reset().Push(buf[:length-1]...).Value() 104 | if buf[length-1] != lrcVal { // LRC 105 | return 0, nil, fmt.Errorf("modbus: response lrc '%x' does not match expected '%x'", buf[length-1], lrcVal) 106 | } 107 | return buf[0], buf[1 : length-1], nil 108 | } 109 | 110 | // Send request to the remote server,it implements on SendRawFrame. 111 | func (sf *ASCIIClientProvider) Send(slaveID byte, request ProtocolDataUnit) (ProtocolDataUnit, error) { 112 | var response ProtocolDataUnit 113 | 114 | frame := sf.pool.get() 115 | defer sf.pool.put(frame) 116 | 117 | aduRequest, err := frame.encodeASCIIFrame(slaveID, request) 118 | if err != nil { 119 | return response, err 120 | } 121 | aduResponse, err := sf.SendRawFrame(aduRequest) 122 | if err != nil { 123 | return response, err 124 | } 125 | rspSlaveID, pdu, err := decodeASCIIFrame(aduResponse) 126 | if err != nil { 127 | return response, err 128 | } 129 | 130 | response = ProtocolDataUnit{pdu[0], pdu[1:]} 131 | if err = verify(slaveID, rspSlaveID, request, response); err != nil { 132 | return response, err 133 | } 134 | return response, nil 135 | } 136 | 137 | // SendPdu send pdu request to the remote server. 138 | func (sf *ASCIIClientProvider) SendPdu(slaveID byte, pduRequest []byte) ([]byte, error) { 139 | if len(pduRequest) < pduMinSize || len(pduRequest) > pduMaxSize { 140 | return nil, fmt.Errorf("modbus: pdu size '%v' must not be between '%v' and '%v'", 141 | len(pduRequest), pduMinSize, pduMaxSize) 142 | } 143 | 144 | frame := sf.pool.get() 145 | defer sf.pool.put(frame) 146 | 147 | request := ProtocolDataUnit{pduRequest[0], pduRequest[1:]} 148 | aduRequest, err := frame.encodeASCIIFrame(slaveID, request) 149 | if err != nil { 150 | return nil, err 151 | } 152 | aduResponse, err := sf.SendRawFrame(aduRequest) 153 | if err != nil { 154 | return nil, err 155 | } 156 | rspSlaveID, pdu, err := decodeASCIIFrame(aduResponse) 157 | if err != nil { 158 | return nil, err 159 | } 160 | response := ProtocolDataUnit{pdu[0], pdu[1:]} 161 | if err = verify(slaveID, rspSlaveID, request, response); err != nil { 162 | return nil, err 163 | } 164 | return pdu, nil 165 | } 166 | 167 | // SendRawFrame send Adu frame. 168 | func (sf *ASCIIClientProvider) SendRawFrame(aduRequest []byte) (aduResponse []byte, err error) { 169 | sf.mu.Lock() 170 | defer sf.mu.Unlock() 171 | 172 | // check port is connected 173 | if !sf.isConnected() { 174 | return nil, ErrClosedConnection 175 | } 176 | 177 | // Send the request 178 | sf.Debug("sending [% x]", aduRequest) 179 | var tryCnt byte 180 | for { 181 | _, err = sf.port.Write(aduRequest) 182 | if err == nil { // success 183 | break 184 | } 185 | if sf.autoReconnect == 0 { 186 | return 187 | } 188 | for { 189 | err = sf.connect() 190 | if err == nil { 191 | break 192 | } 193 | tryCnt++ 194 | if tryCnt >= sf.autoReconnect { 195 | return 196 | } 197 | } 198 | } 199 | 200 | // Get the response 201 | var n int 202 | var data [asciiCharacterMaxSize]byte 203 | length := 0 204 | for { 205 | if n, err = sf.port.Read(data[length:]); err != nil { 206 | return 207 | } 208 | length += n 209 | if length >= asciiCharacterMaxSize || n == 0 { 210 | break 211 | } 212 | // Expect end of frame in the data received 213 | if length > asciiAduMinSize { 214 | if string(data[length-len(asciiEnd):length]) == asciiEnd { 215 | break 216 | } 217 | } 218 | } 219 | aduResponse = data[:length] 220 | sf.Debug("received [% x]", aduResponse) 221 | return aduResponse, nil 222 | } 223 | -------------------------------------------------------------------------------- /asciiclient_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestASCIIClientProvider_encodeASCIIFrame(t *testing.T) { 9 | type args struct { 10 | slaveID byte 11 | pdu ProtocolDataUnit 12 | } 13 | tests := []struct { 14 | name string 15 | ascii *protocolFrame 16 | args args 17 | want []byte 18 | wantErr bool 19 | }{ 20 | { 21 | "ASCII encode right 1", 22 | &protocolFrame{adu: make([]byte, 0, asciiCharacterMaxSize)}, 23 | args{8, ProtocolDataUnit{1, []byte{2, 66, 1, 5}}}, 24 | []byte(":080102420105AD\r\n"), 25 | false, 26 | }, 27 | { 28 | "ASCII encode right 2", 29 | &protocolFrame{adu: make([]byte, 0, asciiCharacterMaxSize)}, 30 | args{1, ProtocolDataUnit{3, []byte{8, 100, 10, 13}}}, 31 | []byte(":010308640A0D79\r\n"), 32 | false, 33 | }, 34 | { 35 | "ASCII encode error", 36 | &protocolFrame{adu: make([]byte, 0, asciiCharacterMaxSize)}, 37 | args{1, ProtocolDataUnit{3, make([]byte, 254)}}, 38 | nil, 39 | true, 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | got, err := tt.ascii.encodeASCIIFrame(tt.args.slaveID, tt.args.pdu) 45 | if (err != nil) != tt.wantErr { 46 | t.Errorf("ASCIIClientProvider.encode() error = %v, wantErr %v", err, tt.wantErr) 47 | return 48 | } 49 | if !reflect.DeepEqual(got, tt.want) { 50 | t.Errorf("ASCIIClientProvider.encode() = %s, want %s", got, tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestASCIIClientProvider_decodeASCIIFrame(t *testing.T) { 57 | type args struct { 58 | adu []byte 59 | } 60 | tests := []struct { 61 | name string 62 | args args 63 | slaveID uint8 64 | pdu []byte 65 | wantErr bool 66 | }{ 67 | { 68 | "ASCII decode 1", 69 | args{[]byte(":080102420105AD\r\n")}, 70 | 8, 71 | []byte{1, 2, 66, 1, 5}, 72 | false, 73 | }, 74 | { 75 | "ASCII decode 2", 76 | args{[]byte(":010308640A0D79\r\n")}, 77 | 1, 78 | []byte{3, 8, 100, 10, 13}, 79 | false, 80 | }, 81 | } 82 | for _, tt := range tests { 83 | t.Run(tt.name, func(t *testing.T) { 84 | gotslaveID, gotpdu, err := decodeASCIIFrame(tt.args.adu) 85 | if (err != nil) != tt.wantErr { 86 | t.Errorf("ASCIIClientProvider.decode() error = %v, wantErr %v", err, tt.wantErr) 87 | return 88 | } 89 | if gotslaveID != tt.slaveID { 90 | t.Errorf("ASCIIClientProvider.decode() gotslaveID = %v, want %v", gotslaveID, tt.slaveID) 91 | } 92 | if !reflect.DeepEqual(gotpdu, tt.pdu) { 93 | t.Errorf("ASCIIClientProvider.decode() gotpdu = %v, want %v", gotpdu, tt.pdu) 94 | } 95 | }) 96 | } 97 | } 98 | 99 | func BenchmarkASCIIClientProvider_encodeASCIIFrame(b *testing.B) { 100 | p := protocolFrame{adu: make([]byte, 0, asciiCharacterMaxSize)} 101 | pdu := ProtocolDataUnit{ 102 | 1, 103 | []byte{2, 3, 4, 5, 6, 7, 8, 9}, 104 | } 105 | for i := 0; i < b.N; i++ { 106 | _, err := p.encodeASCIIFrame(10, pdu) 107 | if err != nil { 108 | b.Fatal(err) 109 | } 110 | } 111 | } 112 | 113 | func BenchmarkASCIIClientProvider_decodeASCIIFrame(b *testing.B) { 114 | adu := []byte(":010308640A0D79\r\n") 115 | for i := 0; i < b.N; i++ { 116 | _, _, err := decodeASCIIFrame(adu) 117 | if err != nil { 118 | b.Fatal(err) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /buffer.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type pool struct { 8 | pl *sync.Pool 9 | } 10 | 11 | func newPool(size int) *pool { 12 | return &pool{ 13 | &sync.Pool{ 14 | New: func() interface{} { return &protocolFrame{make([]byte, 0, size)} }, 15 | }, 16 | } 17 | } 18 | 19 | func (sf *pool) get() *protocolFrame { 20 | return sf.pl.Get().(*protocolFrame) 21 | } 22 | 23 | func (sf *pool) put(buffer *protocolFrame) { 24 | buffer.adu = buffer.adu[:0] 25 | sf.pl.Put(buffer) 26 | } 27 | -------------------------------------------------------------------------------- /buffer_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_pool(t *testing.T) { 8 | p := newPool(tcpAduMaxSize) 9 | frame := p.get() 10 | if len(frame.adu) != 0 { 11 | t.Errorf("pool.get() got len = %v, want %v", len(frame.adu), 0) 12 | } 13 | if cap(frame.adu) != tcpAduMaxSize { 14 | t.Errorf("pool.get() got cap = %v, want %v", cap(frame.adu), tcpAduMaxSize) 15 | } 16 | 17 | p = newPool(asciiCharacterMaxSize) 18 | frame = p.get() 19 | if len(frame.adu) != 0 { 20 | t.Errorf("pool.get() got len = %v, want %v", len(frame.adu), 0) 21 | } 22 | if cap(frame.adu) != asciiCharacterMaxSize { 23 | t.Errorf("pool.get() got cap = %v, want %v", cap(frame.adu), asciiCharacterMaxSize) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | // check implements Client interface. 9 | var _ Client = (*client)(nil) 10 | 11 | // client implements Client interface. 12 | type client struct { 13 | ClientProvider 14 | } 15 | 16 | // NewClient creates a new modbus client with given backend handler. 17 | func NewClient(p ClientProvider) Client { 18 | return &client{p} 19 | } 20 | 21 | // Request: 22 | // Slave Id : 1 byte 23 | // Function code : 1 byte (0x01) 24 | // Starting address : 2 bytes 25 | // Quantity of coils : 2 bytes 26 | // Response: 27 | // Function code : 1 byte (0x01) 28 | // Byte count : 1 byte 29 | // Coil status : N* bytes (=N or N+1) 30 | // return coils status 31 | func (sf *client) ReadCoils(slaveID byte, address, quantity uint16) ([]byte, error) { 32 | if slaveID < AddressMin || slaveID > AddressMax { 33 | return nil, fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 34 | slaveID, AddressMin, AddressMax) 35 | } 36 | if quantity < ReadBitsQuantityMin || quantity > ReadBitsQuantityMax { 37 | return nil, fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v'", 38 | quantity, ReadBitsQuantityMin, ReadBitsQuantityMax) 39 | } 40 | 41 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 42 | FuncCodeReadCoils, 43 | uint162Bytes(address, quantity), 44 | }) 45 | 46 | switch { 47 | case err != nil: 48 | return nil, err 49 | case len(response.Data)-1 != int(response.Data[0]): 50 | return nil, fmt.Errorf("modbus: response byte size '%v' does not match count '%v'", 51 | len(response.Data)-1, int(response.Data[0])) 52 | case uint16(response.Data[0]) != (quantity+7)/8: 53 | return nil, fmt.Errorf("modbus: response byte size '%v' does not match quantity to bytes '%v'", 54 | response.Data[0], (quantity+7)/8) 55 | } 56 | return response.Data[1:], nil 57 | } 58 | 59 | // Request: 60 | // Slave Id : 1 byte 61 | // Function code : 1 byte (0x02) 62 | // Starting address : 2 bytes 63 | // Quantity of inputs : 2 bytes 64 | // Response: 65 | // Function code : 1 byte (0x02) 66 | // Byte count : 1 byte 67 | // Input status : N* bytes (=N or N+1) 68 | // return result data 69 | func (sf *client) ReadDiscreteInputs(slaveID byte, address, quantity uint16) ([]byte, error) { 70 | if slaveID < AddressMin || slaveID > AddressMax { 71 | return nil, fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 72 | slaveID, AddressMin, AddressMax) 73 | } 74 | if quantity < ReadBitsQuantityMin || quantity > ReadBitsQuantityMax { 75 | return nil, fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v'", 76 | quantity, ReadBitsQuantityMin, ReadBitsQuantityMax) 77 | } 78 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 79 | FuncCode: FuncCodeReadDiscreteInputs, 80 | Data: uint162Bytes(address, quantity), 81 | }) 82 | 83 | switch { 84 | case err != nil: 85 | return nil, err 86 | case len(response.Data)-1 != int(response.Data[0]): 87 | return nil, fmt.Errorf("modbus: response byte size '%v' does not match count '%v'", 88 | len(response.Data)-1, response.Data[0]) 89 | case uint16(response.Data[0]) != (quantity+7)/8: 90 | return nil, fmt.Errorf("modbus: response byte size '%v' does not match quantity to bytes '%v'", 91 | response.Data[0], (quantity+7)/8) 92 | } 93 | return response.Data[1:], nil 94 | } 95 | 96 | // Request: 97 | // Slave Id : 1 byte 98 | // Function code : 1 byte (0x05) 99 | // Output address : 2 bytes 100 | // Output value : 2 bytes 101 | // Response: 102 | // Function code : 1 byte (0x05) 103 | // Output address : 2 bytes 104 | // Output value : 2 bytes 105 | func (sf *client) WriteSingleCoil(slaveID byte, address uint16, isOn bool) error { 106 | if slaveID > AddressMax { 107 | return fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 108 | slaveID, AddressBroadCast, AddressMax) 109 | } 110 | var value uint16 111 | if isOn { // The requested ON/OFF state can only be 0xFF00 and 0x0000 112 | value = 0xFF00 113 | } 114 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 115 | FuncCode: FuncCodeWriteSingleCoil, 116 | Data: uint162Bytes(address, value), 117 | }) 118 | 119 | switch { 120 | case err != nil: 121 | return err 122 | case len(response.Data) != 4: 123 | // Fixed response length 124 | return fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", 125 | len(response.Data), 4) 126 | case binary.BigEndian.Uint16(response.Data) != address: 127 | // check address 128 | return fmt.Errorf("modbus: response address '%v' does not match request '%v'", 129 | binary.BigEndian.Uint16(response.Data), address) 130 | case binary.BigEndian.Uint16(response.Data[2:]) != value: 131 | // check value 132 | return fmt.Errorf("modbus: response value '%v' does not match request '%v'", 133 | binary.BigEndian.Uint16(response.Data[2:]), value) 134 | } 135 | return nil 136 | } 137 | 138 | // Request: 139 | // Slave Id : 1 byte 140 | // Function code : 1 byte (0x0F) 141 | // Starting address : 2 bytes 142 | // Quantity of outputs : 2 bytes 143 | // Byte count : 1 byte 144 | // Outputs value : N* bytes 145 | // Response: 146 | // Function code : 1 byte (0x0F) 147 | // Starting address : 2 bytes 148 | // Quantity of outputs : 2 bytes 149 | func (sf *client) WriteMultipleCoils(slaveID byte, address, quantity uint16, value []byte) error { 150 | if slaveID > AddressMax { 151 | return fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 152 | slaveID, AddressBroadCast, AddressMax) 153 | } 154 | if quantity < WriteBitsQuantityMin || quantity > WriteBitsQuantityMax { 155 | return fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v'", 156 | quantity, WriteBitsQuantityMin, WriteBitsQuantityMax) 157 | } 158 | 159 | if len(value)*8 < int(quantity) { 160 | return fmt.Errorf("modbus: value bits size '%v' does not greater or equal to quantity '%v'", len(value)*8, quantity) 161 | } 162 | 163 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 164 | FuncCode: FuncCodeWriteMultipleCoils, 165 | Data: pduDataBlockSuffix(value, address, quantity), 166 | }) 167 | 168 | switch { 169 | case err != nil: 170 | return err 171 | case len(response.Data) != 4: 172 | // Fixed response length 173 | return fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", 174 | len(response.Data), 4) 175 | case binary.BigEndian.Uint16(response.Data) != address: 176 | return fmt.Errorf("modbus: response address '%v' does not match request '%v'", 177 | binary.BigEndian.Uint16(response.Data), address) 178 | case binary.BigEndian.Uint16(response.Data[2:]) != quantity: 179 | return fmt.Errorf("modbus: response quantity '%v' does not match request '%v'", 180 | binary.BigEndian.Uint16(response.Data[2:]), quantity) 181 | } 182 | return nil 183 | } 184 | 185 | /*********************************16-bits**************************************/ 186 | 187 | // Request: 188 | // Slave Id : 1 byte 189 | // Function code : 1 byte (0x04) 190 | // Starting address : 2 bytes 191 | // Quantity of registers : 2 bytes 192 | // Response: 193 | // Function code : 1 byte (0x04) 194 | // Byte count : 1 byte 195 | // Input registers : Nx2 bytes 196 | func (sf *client) ReadInputRegistersBytes(slaveID byte, address, quantity uint16) ([]byte, error) { 197 | if slaveID < AddressMin || slaveID > AddressMax { 198 | return nil, fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 199 | slaveID, AddressMin, AddressMax) 200 | } 201 | if quantity < ReadRegQuantityMin || quantity > ReadRegQuantityMax { 202 | return nil, fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v'", 203 | quantity, ReadRegQuantityMin, ReadRegQuantityMax) 204 | } 205 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 206 | FuncCode: FuncCodeReadInputRegisters, 207 | Data: uint162Bytes(address, quantity), 208 | }) 209 | 210 | switch { 211 | case err != nil: 212 | return nil, err 213 | case len(response.Data)-1 != int(response.Data[0]): 214 | return nil, fmt.Errorf("modbus: response data size '%v' does not match count '%v'", 215 | len(response.Data)-1, response.Data[0]) 216 | case uint16(response.Data[0]) != quantity*2: 217 | return nil, fmt.Errorf("modbus: response data size '%v' does not match quantity to bytes '%v'", 218 | response.Data[0], quantity*2) 219 | } 220 | 221 | return response.Data[1:], nil 222 | } 223 | 224 | // Request: 225 | // Slave Id : 1 byte 226 | // Function code : 1 byte (0x04) 227 | // Starting address : 2 bytes 228 | // Quantity of registers : 2 bytes 229 | // Response: 230 | // Function code : 1 byte (0x04) 231 | // Byte count : 1 byte 232 | // Input registers : N 2-bytes 233 | func (sf *client) ReadInputRegisters(slaveID byte, address, quantity uint16) ([]uint16, error) { 234 | b, err := sf.ReadInputRegistersBytes(slaveID, address, quantity) 235 | if err != nil { 236 | return nil, err 237 | } 238 | return bytes2Uint16(b), nil 239 | } 240 | 241 | // Request: 242 | // Slave Id : 1 byte 243 | // Function code : 1 byte (0x03) 244 | // Starting address : 2 bytes 245 | // Quantity of registers : 2 bytes 246 | // Response: 247 | // Function code : 1 byte (0x03) 248 | // Byte count : 1 byte 249 | // Register value : Nx2 bytes 250 | func (sf *client) ReadHoldingRegistersBytes(slaveID byte, address, quantity uint16) ([]byte, error) { 251 | if slaveID < AddressMin || slaveID > AddressMax { 252 | return nil, fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 253 | slaveID, AddressMin, AddressMax) 254 | } 255 | if quantity < ReadRegQuantityMin || quantity > ReadRegQuantityMax { 256 | return nil, fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v'", 257 | quantity, ReadRegQuantityMin, ReadRegQuantityMax) 258 | } 259 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 260 | FuncCode: FuncCodeReadHoldingRegisters, 261 | Data: uint162Bytes(address, quantity), 262 | }) 263 | 264 | switch { 265 | case err != nil: 266 | return nil, err 267 | case len(response.Data)-1 != int(response.Data[0]): 268 | return nil, fmt.Errorf("modbus: response data size '%v' does not match count '%v'", 269 | len(response.Data)-1, response.Data[0]) 270 | case uint16(response.Data[0]) != quantity*2: 271 | return nil, fmt.Errorf("modbus: response data size '%v' does not match quantity to bytes '%v'", 272 | response.Data[0], quantity*2) 273 | } 274 | return response.Data[1:], nil 275 | } 276 | 277 | // Request: 278 | // Slave Id : 1 byte 279 | // Function code : 1 byte (0x03) 280 | // Starting address : 2 bytes 281 | // Quantity of registers : 2 bytes 282 | // Response: 283 | // Function code : 1 byte (0x03) 284 | // Byte count : 1 byte 285 | // Register value : N 2-bytes 286 | func (sf *client) ReadHoldingRegisters(slaveID byte, address, quantity uint16) ([]uint16, error) { 287 | b, err := sf.ReadHoldingRegistersBytes(slaveID, address, quantity) 288 | if err != nil { 289 | return nil, err 290 | } 291 | return bytes2Uint16(b), nil 292 | } 293 | 294 | // Request: 295 | // Slave Id : 1 byte 296 | // Function code : 1 byte (0x06) 297 | // Register address : 2 bytes 298 | // Register value : 2 bytes 299 | // Response: 300 | // Function code : 1 byte (0x06) 301 | // Register address : 2 bytes 302 | // Register value : 2 bytes 303 | func (sf *client) WriteSingleRegister(slaveID byte, address, value uint16) error { 304 | if slaveID > AddressMax { 305 | return fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 306 | slaveID, AddressBroadCast, AddressMax) 307 | } 308 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 309 | FuncCode: FuncCodeWriteSingleRegister, 310 | Data: uint162Bytes(address, value), 311 | }) 312 | 313 | switch { 314 | case err != nil: 315 | return err 316 | case len(response.Data) != 4: 317 | // Fixed response length 318 | return fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", 319 | len(response.Data), 4) 320 | case binary.BigEndian.Uint16(response.Data) != address: 321 | return fmt.Errorf("modbus: response address '%v' does not match request '%v'", 322 | binary.BigEndian.Uint16(response.Data), address) 323 | case binary.BigEndian.Uint16(response.Data[2:]) != value: 324 | return fmt.Errorf("modbus: response value '%v' does not match request '%v'", 325 | binary.BigEndian.Uint16(response.Data[2:]), value) 326 | } 327 | return nil 328 | } 329 | 330 | // Request: 331 | // Slave Id : 1 byte 332 | // Function code : 1 byte (0x10) 333 | // Starting address : 2 bytes 334 | // Quantity of outputs : 2 bytes 335 | // Byte count : 1 byte 336 | // Registers value : N* bytes 337 | // Response: 338 | // Function code : 1 byte (0x10) 339 | // Starting address : 2 bytes 340 | // Quantity of registers : 2 bytes 341 | func (sf *client) WriteMultipleRegistersBytes(slaveID byte, address, quantity uint16, value []byte) error { 342 | if slaveID > AddressMax { 343 | return fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 344 | slaveID, AddressBroadCast, AddressMax) 345 | } 346 | if quantity < WriteRegQuantityMin || quantity > WriteRegQuantityMax { 347 | return fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v'", 348 | quantity, WriteRegQuantityMin, WriteRegQuantityMax) 349 | } 350 | 351 | if len(value) != int(quantity*2) { 352 | return fmt.Errorf("modbus: value length '%v' does not twice as quantity '%v'", len(value), quantity) 353 | } 354 | 355 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 356 | FuncCode: FuncCodeWriteMultipleRegisters, 357 | Data: pduDataBlockSuffix(value, address, quantity), 358 | }) 359 | 360 | switch { 361 | case err != nil: 362 | return err 363 | case len(response.Data) != 4: 364 | // Fixed response length 365 | return fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", 366 | len(response.Data), 4) 367 | case binary.BigEndian.Uint16(response.Data) != address: 368 | return fmt.Errorf("modbus: response address '%v' does not match request '%v'", 369 | binary.BigEndian.Uint16(response.Data), address) 370 | case binary.BigEndian.Uint16(response.Data[2:]) != quantity: 371 | return fmt.Errorf("modbus: response quantity '%v' does not match request '%v'", 372 | binary.BigEndian.Uint16(response.Data[2:]), quantity) 373 | } 374 | return nil 375 | } 376 | 377 | // Request: 378 | // Slave Id : 1 byte 379 | // Function code : 1 byte (0x10) 380 | // Starting address : 2 bytes 381 | // Quantity of outputs : 2 bytes 382 | // Byte count : 1 byte 383 | // Registers value : N* bytes 384 | // Response: 385 | // Function code : 1 byte (0x10) 386 | // Starting address : 2 bytes 387 | // Quantity of registers : 2 bytes 388 | func (sf *client) WriteMultipleRegisters(slaveID byte, address, quantity uint16, value []uint16) error { 389 | return sf.WriteMultipleRegistersBytes(slaveID, address, quantity, uint162Bytes(value...)) 390 | } 391 | 392 | // Request: 393 | // Slave Id : 1 byte 394 | // Function code : 1 byte (0x16) 395 | // Reference address : 2 bytes 396 | // AND-mask : 2 bytes 397 | // OR-mask : 2 bytes 398 | // Response: 399 | // Function code : 1 byte (0x16) 400 | // Reference address : 2 bytes 401 | // AND-mask : 2 bytes 402 | // OR-mask : 2 bytes 403 | func (sf *client) MaskWriteRegister(slaveID byte, address, andMask, orMask uint16) error { 404 | if slaveID > AddressMax { 405 | return fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 406 | slaveID, AddressBroadCast, AddressMax) 407 | } 408 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 409 | FuncCode: FuncCodeMaskWriteRegister, 410 | Data: uint162Bytes(address, andMask, orMask), 411 | }) 412 | 413 | switch { 414 | case err != nil: 415 | return err 416 | case len(response.Data) != 6: 417 | // Fixed response length 418 | return fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", 419 | len(response.Data), 6) 420 | case binary.BigEndian.Uint16(response.Data) != address: 421 | return fmt.Errorf("modbus: response address '%v' does not match request '%v'", 422 | binary.BigEndian.Uint16(response.Data), address) 423 | case binary.BigEndian.Uint16(response.Data[2:]) != andMask: 424 | return fmt.Errorf("modbus: response AND-mask '%v' does not match request '%v'", 425 | binary.BigEndian.Uint16(response.Data[2:]), andMask) 426 | case binary.BigEndian.Uint16(response.Data[4:]) != orMask: 427 | return fmt.Errorf("modbus: response OR-mask '%v' does not match request '%v'", 428 | binary.BigEndian.Uint16(response.Data[4:]), orMask) 429 | } 430 | return nil 431 | } 432 | 433 | // Request: 434 | // Slave Id : 1 byte 435 | // Function code : 1 byte (0x17) 436 | // Read starting address : 2 bytes 437 | // Quantity to read : 2 bytes 438 | // Write starting address: 2 bytes 439 | // Quantity to write : 2 bytes 440 | // Write byte count : 1 byte 441 | // Write registers value : N* bytes 442 | // Response: 443 | // Function code : 1 byte (0x17) 444 | // Byte count : 1 byte 445 | // Read registers value : Nx2 bytes 446 | func (sf *client) ReadWriteMultipleRegistersBytes(slaveID byte, readAddress, readQuantity, 447 | writeAddress, writeQuantity uint16, value []byte) ([]byte, error) { 448 | if slaveID < AddressMin || slaveID > AddressMax { 449 | return nil, fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 450 | slaveID, AddressMin, AddressMax) 451 | } 452 | if readQuantity < ReadWriteOnReadRegQuantityMin || readQuantity > ReadWriteOnReadRegQuantityMax { 453 | return nil, fmt.Errorf("modbus: quantity to read '%v' must be between '%v' and '%v'", 454 | readQuantity, ReadWriteOnReadRegQuantityMin, ReadWriteOnReadRegQuantityMax) 455 | } 456 | if writeQuantity < ReadWriteOnWriteRegQuantityMin || writeQuantity > ReadWriteOnWriteRegQuantityMax { 457 | return nil, fmt.Errorf("modbus: quantity to write '%v' must be between '%v' and '%v'", 458 | writeQuantity, ReadWriteOnWriteRegQuantityMin, ReadWriteOnWriteRegQuantityMax) 459 | } 460 | 461 | if len(value) != int(writeQuantity*2) { 462 | return nil, fmt.Errorf("modbus: value length '%v' does not twice as write quantity '%v'", 463 | len(value), writeQuantity) 464 | } 465 | 466 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 467 | FuncCode: FuncCodeReadWriteMultipleRegisters, 468 | Data: pduDataBlockSuffix(value, readAddress, readQuantity, writeAddress, writeQuantity), 469 | }) 470 | if err != nil { 471 | return nil, err 472 | } 473 | if int(response.Data[0]) != (len(response.Data) - 1) { 474 | return nil, fmt.Errorf("modbus: response data size '%v' does not match count '%v'", 475 | len(response.Data)-1, response.Data[0]) 476 | } 477 | return response.Data[1:], nil 478 | } 479 | 480 | // Request: 481 | // Slave Id : 1 byte 482 | // Function code : 1 byte (0x17) 483 | // Read starting address quantity: 2 bytes 484 | // Quantity to read : 2 bytes 485 | // Write starting address: 2 bytes 486 | // Quantity to write : 2 bytes 487 | // Write byte count : 1 byte 488 | // Write registers value : N* bytes 489 | // Response: 490 | // Function code : 1 byte (0x17) 491 | // Byte count : 1 byte 492 | // Read registers value : N 2-bytes 493 | func (sf *client) ReadWriteMultipleRegisters(slaveID byte, readAddress, readQuantity, 494 | writeAddress, writeQuantity uint16, value []byte) ([]uint16, error) { 495 | b, err := sf.ReadWriteMultipleRegistersBytes(slaveID, readAddress, readQuantity, 496 | writeAddress, writeQuantity, value) 497 | if err != nil { 498 | return nil, err 499 | } 500 | return bytes2Uint16(b), nil 501 | } 502 | 503 | // Request: 504 | // Slave Id : 1 byte 505 | // Function code : 1 byte (0x18) 506 | // FIFO pointer address : 2 bytes 507 | // Response: 508 | // Function code : 1 byte (0x18) 509 | // Byte count : 2 bytes only include follow 510 | // FIFO count : 2 bytes (<=31) 511 | // FIFO value register : Nx2 bytes 512 | func (sf *client) ReadFIFOQueue(slaveID byte, address uint16) ([]byte, error) { 513 | if slaveID < AddressMin || slaveID > AddressMax { 514 | return nil, fmt.Errorf("modbus: slaveID '%v' must be between '%v' and '%v'", 515 | slaveID, AddressMin, AddressMax) 516 | } 517 | response, err := sf.Send(slaveID, ProtocolDataUnit{ 518 | FuncCode: FuncCodeReadFIFOQueue, 519 | Data: uint162Bytes(address), 520 | }) 521 | switch { 522 | case err != nil: 523 | return nil, err 524 | case len(response.Data) < 4: 525 | return nil, fmt.Errorf("modbus: response data size '%v' is less than expected '%v'", 526 | len(response.Data), 4) 527 | case len(response.Data)-2 != int(binary.BigEndian.Uint16(response.Data)): 528 | return nil, fmt.Errorf("modbus: response data size '%v' does not match count '%v'", 529 | len(response.Data)-2, binary.BigEndian.Uint16(response.Data)) 530 | case int(binary.BigEndian.Uint16(response.Data[2:])) > 31: 531 | return nil, fmt.Errorf("modbus: fifo count '%v' is greater than expected '%v'", 532 | binary.BigEndian.Uint16(response.Data[2:]), 31) 533 | } 534 | return response.Data[4:], nil 535 | } 536 | 537 | // uint162Bytes creates a sequence of uint16 data. 538 | func uint162Bytes(value ...uint16) []byte { 539 | data := make([]byte, 2*len(value)) 540 | for i, v := range value { 541 | binary.BigEndian.PutUint16(data[i*2:], v) 542 | } 543 | return data 544 | } 545 | 546 | // bytes2Uint16 bytes convert to uint16 for register. 547 | func bytes2Uint16(buf []byte) []uint16 { 548 | data := make([]uint16, 0, len(buf)/2) 549 | for i := 0; i < len(buf)/2; i++ { 550 | data = append(data, binary.BigEndian.Uint16(buf[i*2:])) 551 | } 552 | return data 553 | } 554 | 555 | // pduDataBlockSuffix creates a sequence of uint16 data and append the suffix plus its length. 556 | func pduDataBlockSuffix(suffix []byte, value ...uint16) []byte { 557 | length := 2 * len(value) 558 | data := make([]byte, length+1+len(suffix)) 559 | for i, v := range value { 560 | binary.BigEndian.PutUint16(data[i*2:], v) 561 | } 562 | data[length] = uint8(len(suffix)) 563 | copy(data[length+1:], suffix) 564 | return data 565 | } 566 | 567 | // responseError response error. 568 | func responseError(response ProtocolDataUnit) error { 569 | mbError := &ExceptionError{} 570 | if response.Data != nil && len(response.Data) > 0 { 571 | mbError.ExceptionCode = response.Data[0] 572 | } 573 | return mbError 574 | } 575 | -------------------------------------------------------------------------------- /client_option.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/goburrow/serial" 7 | ) 8 | 9 | // ClientProviderOption client provider option for user. 10 | type ClientProviderOption func(ClientProvider) 11 | 12 | // WithLogProvider set logger provider. 13 | func WithLogProvider(provider LogProvider) ClientProviderOption { 14 | return func(p ClientProvider) { 15 | p.setLogProvider(provider) 16 | } 17 | } 18 | 19 | // WithEnableLogger enable log output when you has set logger. 20 | func WithEnableLogger() ClientProviderOption { 21 | return func(p ClientProvider) { 22 | p.LogMode(true) 23 | } 24 | } 25 | 26 | // WithAutoReconnect set auto reconnect count. 27 | // if cnt == 0, disable auto reconnect 28 | // if cnt > 0 ,enable auto reconnect,but max 6. 29 | func WithAutoReconnect(cnt byte) ClientProviderOption { 30 | return func(p ClientProvider) { 31 | p.SetAutoReconnect(cnt) 32 | } 33 | } 34 | 35 | // WithSerialConfig set serial config, only valid on serial. 36 | func WithSerialConfig(config serial.Config) ClientProviderOption { 37 | return func(p ClientProvider) { 38 | p.setSerialConfig(config) 39 | } 40 | } 41 | 42 | // WithTCPTimeout set tcp Connect & Read timeout, only valid on TCP. 43 | func WithTCPTimeout(t time.Duration) ClientProviderOption { 44 | return func(p ClientProvider) { 45 | p.setTCPTimeout(t) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/goburrow/serial" 10 | ) 11 | 12 | // check implements ClientProvider interface 13 | var _ ClientProvider = (*provider)(nil) 14 | 15 | type provider struct { 16 | data []byte 17 | err error 18 | } 19 | 20 | func (*provider) Connect() error { return nil } 21 | func (*provider) IsConnected() bool { return true } 22 | func (*provider) SetAutoReconnect(byte) {} 23 | func (*provider) LogMode(bool) {} 24 | func (*provider) Close() error { return nil } 25 | func (r *provider) Send(_ byte, _ ProtocolDataUnit) (ProtocolDataUnit, error) { 26 | return ProtocolDataUnit{Data: r.data}, r.err 27 | } 28 | func (*provider) SendPdu(byte, []byte) (pduResponse []byte, err error) { 29 | return nil, nil 30 | } 31 | func (*provider) SendRawFrame([]byte) (aduResponse []byte, err error) { 32 | return nil, nil 33 | } 34 | func (*provider) setLogProvider(LogProvider) {} 35 | func (*provider) setSerialConfig(serial.Config) {} 36 | func (*provider) setTCPTimeout(time.Duration) {} 37 | 38 | func Test_client_ReadCoils(t *testing.T) { 39 | type args struct { 40 | slaveID byte 41 | address uint16 42 | quantity uint16 43 | } 44 | tests := []struct { 45 | name string 46 | provide ClientProvider 47 | args args 48 | want []byte 49 | wantErr bool 50 | }{ 51 | {"slaveid不在范围1-247", &provider{}, 52 | args{slaveID: 248}, nil, true}, 53 | {"Quantity不在范围1-2000", &provider{}, 54 | args{slaveID: 1, quantity: 20001}, nil, true}, 55 | {"返回error", &provider{err: errors.New("error")}, 56 | args{slaveID: 1, quantity: 10}, nil, true}, 57 | {"返回数据长度不符", &provider{data: []byte{0x02, 0x00, 0x00, 0x00}}, 58 | args{slaveID: 1, quantity: 10}, nil, true}, 59 | {"返回字节与请求数量不符", &provider{data: []byte{0x01, 0x00}}, 60 | args{slaveID: 1, quantity: 10}, nil, true}, 61 | {"正确", &provider{data: []byte{0x02, 0x12, 0x34}}, 62 | args{slaveID: 1, quantity: 10}, []byte{0x12, 0x34}, false}, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | this := NewClient(tt.provide) 67 | got, err := this.ReadCoils(tt.args.slaveID, tt.args.address, tt.args.quantity) 68 | if (err != nil) != tt.wantErr { 69 | t.Errorf("client.ReadCoils() error = %v, wantErr %v", err, tt.wantErr) 70 | return 71 | } 72 | if !reflect.DeepEqual(got, tt.want) { 73 | t.Errorf("client.ReadCoils() = %v, want %v", got, tt.want) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func Test_client_ReadDiscreteInputs(t *testing.T) { 80 | type args struct { 81 | slaveID byte 82 | address uint16 83 | quantity uint16 84 | } 85 | tests := []struct { 86 | name string 87 | provide ClientProvider 88 | args args 89 | want []byte 90 | wantErr bool 91 | }{ 92 | {"slaveid不在范围1-247", &provider{}, 93 | args{slaveID: 248}, nil, true}, 94 | {"Quantity不在范围1-2000", &provider{}, 95 | args{slaveID: 1, quantity: 20001}, nil, true}, 96 | {"返回error", &provider{err: errors.New("error")}, 97 | args{slaveID: 1, quantity: 10}, nil, true}, 98 | {"返回数据长度不符", &provider{data: []byte{0x01, 0x00, 0x00}}, 99 | args{slaveID: 1, quantity: 10}, nil, true}, 100 | {"返回字节与请求数量不符", &provider{data: []byte{0x01, 0x00}}, 101 | args{slaveID: 1, quantity: 10}, nil, true}, 102 | {"正确", &provider{data: []byte{0x02, 0x12, 0x34}}, 103 | args{slaveID: 1, quantity: 10}, []byte{0x12, 0x34}, false}, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | this := &client{ 108 | ClientProvider: tt.provide, 109 | } 110 | got, err := this.ReadDiscreteInputs(tt.args.slaveID, tt.args.address, tt.args.quantity) 111 | if (err != nil) != tt.wantErr { 112 | t.Errorf("client.ReadDiscreteInputs() error = %v, wantErr %v", err, tt.wantErr) 113 | return 114 | } 115 | if !reflect.DeepEqual(got, tt.want) { 116 | t.Errorf("client.ReadDiscreteInputs() = %v, want %v", got, tt.want) 117 | } 118 | }) 119 | } 120 | } 121 | 122 | func Test_client_WriteSingleCoil(t *testing.T) { 123 | type args struct { 124 | slaveID byte 125 | address uint16 126 | isOn bool 127 | } 128 | tests := []struct { 129 | name string 130 | provide ClientProvider 131 | args args 132 | wantErr bool 133 | }{ 134 | {"slaveid不在范围0-247", &provider{}, 135 | args{slaveID: 248}, true}, 136 | {"返回error", &provider{err: errors.New("error")}, 137 | args{slaveID: 1}, true}, 138 | {"返回数据长度不符", &provider{data: []byte{0x01, 0x00, 0x00}}, 139 | args{slaveID: 1}, true}, 140 | {"返回字节不符合", &provider{data: []byte{0x01, 0x00}}, 141 | args{slaveID: 1}, true}, 142 | {"返回地址不符合", &provider{data: []byte{0x00, 0x01, 0xff, 0x00}}, 143 | args{slaveID: 1}, true}, 144 | {"返回值不符合", &provider{data: []byte{0x00, 0x00, 0xff, 0x00}}, 145 | args{slaveID: 1}, true}, 146 | {"正确", &provider{data: []byte{0x00, 0x00, 0xff, 0x00}}, 147 | args{slaveID: 1, isOn: true}, false}, 148 | } 149 | for _, tt := range tests { 150 | t.Run(tt.name, func(t *testing.T) { 151 | this := &client{ 152 | ClientProvider: tt.provide, 153 | } 154 | if err := this.WriteSingleCoil(tt.args.slaveID, tt.args.address, tt.args.isOn); (err != nil) != tt.wantErr { 155 | t.Errorf("client.WriteSingleCoil() error = %v, wantErr %v", err, tt.wantErr) 156 | } 157 | }) 158 | } 159 | } 160 | 161 | func Test_client_WriteMultipleCoils(t *testing.T) { 162 | type args struct { 163 | slaveID byte 164 | address uint16 165 | quantity uint16 166 | value []byte 167 | } 168 | tests := []struct { 169 | name string 170 | provide ClientProvider 171 | args args 172 | wantErr bool 173 | }{ 174 | { 175 | "slaveid不在范围0-247", 176 | &provider{}, 177 | args{slaveID: 248}, 178 | true, 179 | }, 180 | { 181 | "quantity不在范围1-1968", 182 | &provider{}, 183 | args{quantity: 1969}, 184 | true, 185 | }, 186 | { 187 | "返回error", 188 | &provider{err: errors.New("error")}, 189 | args{quantity: 1, value: []byte{1}}, 190 | true, 191 | }, 192 | { 193 | "value * 8 位数小于数量", 194 | &provider{data: []byte{0x00, 0x00, 0x00}}, 195 | args{quantity: 9, value: []byte{1}}, 196 | true, 197 | }, 198 | { 199 | "返回数据长度不符", 200 | &provider{data: []byte{0x00, 0x00, 0x00}}, 201 | args{quantity: 1, value: []byte{1}}, 202 | true, 203 | }, 204 | { 205 | "返回地址与请求一致", 206 | &provider{data: []byte{0x00, 0x01, 0x00, 0x01}}, 207 | args{quantity: 1, value: []byte{1}}, true}, 208 | { 209 | "返回数量与请求不一致", 210 | &provider{data: []byte{0x00, 0x00, 0x00, 0x02}}, 211 | args{quantity: 1, value: []byte{1}}, 212 | true, 213 | }, 214 | { 215 | "正确", 216 | &provider{data: []byte{0x00, 0x00, 0x00, 0x01}}, 217 | args{quantity: 1, value: []byte{1}}, 218 | false, 219 | }, 220 | } 221 | for _, tt := range tests { 222 | t.Run(tt.name, func(t *testing.T) { 223 | this := &client{ 224 | ClientProvider: tt.provide, 225 | } 226 | if err := this.WriteMultipleCoils(tt.args.slaveID, tt.args.address, tt.args.quantity, tt.args.value); (err != nil) != tt.wantErr { 227 | t.Errorf("client.WriteMultipleCoils() error = %v, wantErr %v", err, tt.wantErr) 228 | } 229 | }) 230 | } 231 | } 232 | 233 | func Test_client_ReadHoldingRegistersBytes(t *testing.T) { 234 | type args struct { 235 | slaveID byte 236 | address uint16 237 | quantity uint16 238 | } 239 | tests := []struct { 240 | name string 241 | provide ClientProvider 242 | args args 243 | want []byte 244 | wantErr bool 245 | }{ 246 | {"slaveid不在范围1-247", &provider{}, 247 | args{slaveID: 248}, nil, true}, 248 | {"Quantity不在范围1-125", &provider{}, 249 | args{slaveID: 1, quantity: 126}, nil, true}, 250 | {"返回error", &provider{err: errors.New("error")}, 251 | args{slaveID: 1, quantity: 10}, nil, true}, 252 | {"返回数据长度不符", &provider{data: []byte{0x01, 0x00, 0x00}}, 253 | args{slaveID: 1, quantity: 10}, nil, true}, 254 | {"返回字节与请求数量不符", &provider{data: []byte{0x01, 0x00}}, 255 | args{slaveID: 1, quantity: 10}, nil, true}, 256 | {"正确", &provider{data: []byte{0x04, 0x12, 0x34, 0x56, 0x78}}, 257 | args{slaveID: 1, quantity: 2}, []byte{0x12, 0x34, 0x56, 0x78}, false}, 258 | } 259 | for _, tt := range tests { 260 | t.Run(tt.name, func(t *testing.T) { 261 | this := &client{ 262 | ClientProvider: tt.provide, 263 | } 264 | got, err := this.ReadHoldingRegistersBytes(tt.args.slaveID, tt.args.address, tt.args.quantity) 265 | if (err != nil) != tt.wantErr { 266 | t.Errorf("client.ReadHoldingRegistersBytes() error = %v, wantErr %v", err, tt.wantErr) 267 | return 268 | } 269 | if !reflect.DeepEqual(got, tt.want) { 270 | t.Errorf("client.ReadHoldingRegistersBytes() = %v, want %v", got, tt.want) 271 | } 272 | }) 273 | } 274 | } 275 | 276 | func Test_client_ReadHoldingRegisters(t *testing.T) { 277 | type args struct { 278 | slaveID byte 279 | address uint16 280 | quantity uint16 281 | } 282 | tests := []struct { 283 | name string 284 | provide ClientProvider 285 | args args 286 | want []uint16 287 | wantErr bool 288 | }{ 289 | {"slaveid不在范围1-247", &provider{}, 290 | args{slaveID: 248}, nil, true}, 291 | {"Quantity不在范围1-125", &provider{}, 292 | args{slaveID: 1, quantity: 126}, nil, true}, 293 | {"返回error", &provider{err: errors.New("error")}, 294 | args{slaveID: 1, quantity: 10}, nil, true}, 295 | {"返回数据长度不符", &provider{data: []byte{0x01, 0x00, 0x00}}, 296 | args{slaveID: 1, quantity: 10}, nil, true}, 297 | {"返回字节与请求数量不符", &provider{data: []byte{0x01, 0x00}}, 298 | args{slaveID: 1, quantity: 10}, nil, true}, 299 | {"正确", &provider{data: []byte{0x04, 0x12, 0x34, 0x56, 0x78}}, 300 | args{slaveID: 1, quantity: 2}, []uint16{0x1234, 0x5678}, false}, 301 | } 302 | for _, tt := range tests { 303 | t.Run(tt.name, func(t *testing.T) { 304 | this := &client{ 305 | ClientProvider: tt.provide, 306 | } 307 | got, err := this.ReadHoldingRegisters(tt.args.slaveID, tt.args.address, tt.args.quantity) 308 | if (err != nil) != tt.wantErr { 309 | t.Errorf("client.ReadHoldingRegisters() error = %v, wantErr %v", err, tt.wantErr) 310 | return 311 | } 312 | if !reflect.DeepEqual(got, tt.want) { 313 | t.Errorf("client.ReadHoldingRegisters() = %v, want %v", got, tt.want) 314 | } 315 | }) 316 | } 317 | } 318 | 319 | func Test_client_ReadInputRegistersBytes(t *testing.T) { 320 | type args struct { 321 | slaveID byte 322 | address uint16 323 | quantity uint16 324 | } 325 | tests := []struct { 326 | name string 327 | provide ClientProvider 328 | args args 329 | want []byte 330 | wantErr bool 331 | }{ 332 | {"slaveid不在范围1-247", &provider{}, 333 | args{slaveID: 248}, nil, true}, 334 | {"Quantity不在范围1-125", &provider{}, 335 | args{slaveID: 1, quantity: 126}, nil, true}, 336 | {"返回error", &provider{err: errors.New("error")}, 337 | args{slaveID: 1, quantity: 10}, nil, true}, 338 | {"返回数据长度不符", &provider{data: []byte{0x01, 0x00, 0x00}}, 339 | args{slaveID: 1, quantity: 10}, nil, true}, 340 | {"返回字节与请求数量不符", &provider{data: []byte{0x01, 0x00}}, 341 | args{slaveID: 1, quantity: 10}, nil, true}, 342 | {"正确", &provider{data: []byte{0x04, 0x12, 0x34, 0x56, 0x78}}, 343 | args{slaveID: 1, quantity: 2}, []byte{0x12, 0x34, 0x56, 0x78}, false}, 344 | } 345 | for _, tt := range tests { 346 | t.Run(tt.name, func(t *testing.T) { 347 | this := &client{ 348 | ClientProvider: tt.provide, 349 | } 350 | got, err := this.ReadInputRegistersBytes(tt.args.slaveID, tt.args.address, tt.args.quantity) 351 | if (err != nil) != tt.wantErr { 352 | t.Errorf("client.ReadInputRegistersBytes() error = %v, wantErr %v", err, tt.wantErr) 353 | return 354 | } 355 | if !reflect.DeepEqual(got, tt.want) { 356 | t.Errorf("client.ReadInputRegistersBytes() = %v, want %v", got, tt.want) 357 | } 358 | }) 359 | } 360 | } 361 | 362 | func Test_client_ReadInputRegisters(t *testing.T) { 363 | type args struct { 364 | slaveID byte 365 | address uint16 366 | quantity uint16 367 | } 368 | tests := []struct { 369 | name string 370 | provide ClientProvider 371 | args args 372 | want []uint16 373 | wantErr bool 374 | }{ 375 | {"slaveid不在范围1-247", &provider{}, 376 | args{slaveID: 248}, nil, true}, 377 | {"Quantity不在范围1-125", &provider{}, 378 | args{slaveID: 1, quantity: 126}, nil, true}, 379 | {"返回error", &provider{err: errors.New("error")}, 380 | args{slaveID: 1, quantity: 10}, nil, true}, 381 | {"返回数据长度不符", &provider{data: []byte{0x01, 0x00, 0x00}}, 382 | args{slaveID: 1, quantity: 10}, nil, true}, 383 | {"返回字节与请求数量不符", &provider{data: []byte{0x01, 0x00}}, 384 | args{slaveID: 1, quantity: 10}, nil, true}, 385 | {"正确", &provider{data: []byte{0x04, 0x12, 0x34, 0x56, 0x78}}, 386 | args{slaveID: 1, quantity: 2}, []uint16{0x1234, 0x5678}, false}, 387 | } 388 | for _, tt := range tests { 389 | t.Run(tt.name, func(t *testing.T) { 390 | this := &client{ 391 | ClientProvider: tt.provide, 392 | } 393 | got, err := this.ReadInputRegisters(tt.args.slaveID, tt.args.address, tt.args.quantity) 394 | if (err != nil) != tt.wantErr { 395 | t.Errorf("client.ReadInputRegisters() error = %v, wantErr %v", err, tt.wantErr) 396 | return 397 | } 398 | if !reflect.DeepEqual(got, tt.want) { 399 | t.Errorf("client.ReadInputRegisters() = %v, want %v", got, tt.want) 400 | } 401 | }) 402 | } 403 | } 404 | 405 | func Test_client_WriteSingleRegister(t *testing.T) { 406 | type args struct { 407 | slaveID byte 408 | address uint16 409 | value uint16 410 | } 411 | tests := []struct { 412 | name string 413 | provide ClientProvider 414 | args args 415 | wantErr bool 416 | }{ 417 | {"slaveid不在范围0-247", &provider{}, 418 | args{slaveID: 248}, true}, 419 | {"返回error", &provider{err: errors.New("error")}, 420 | args{slaveID: 1}, true}, 421 | {"返回数据长度不符", &provider{data: []byte{0x01, 0x00, 0x00}}, 422 | args{slaveID: 1}, true}, 423 | {"返回字节不符合", &provider{data: []byte{0x01, 0x00}}, 424 | args{slaveID: 1}, true}, 425 | {"返回地址不符合", &provider{data: []byte{0x00, 0x01, 0xff, 0x00}}, 426 | args{slaveID: 1}, true}, 427 | {"返回值不符合", &provider{data: []byte{0x00, 0x00, 0xff, 0x00}}, 428 | args{slaveID: 1}, true}, 429 | {"正确", &provider{data: []byte{0x00, 0x00, 0xff, 0x00}}, 430 | args{slaveID: 1, value: 0xff00}, false}, 431 | } 432 | for _, tt := range tests { 433 | t.Run(tt.name, func(t *testing.T) { 434 | this := &client{ 435 | ClientProvider: tt.provide, 436 | } 437 | if err := this.WriteSingleRegister(tt.args.slaveID, tt.args.address, tt.args.value); (err != nil) != tt.wantErr { 438 | t.Errorf("client.WriteSingleRegister() error = %v, wantErr %v", err, tt.wantErr) 439 | } 440 | }) 441 | } 442 | } 443 | 444 | func Test_client_WriteMultipleRegistersBytes(t *testing.T) { 445 | type args struct { 446 | slaveID byte 447 | address uint16 448 | quantity uint16 449 | value []byte 450 | } 451 | tests := []struct { 452 | name string 453 | provide ClientProvider 454 | args args 455 | wantErr bool 456 | }{ 457 | { 458 | "slaveid不在范围0-247", 459 | &provider{}, 460 | args{slaveID: 248}, 461 | true, 462 | }, 463 | { 464 | "quantity不在范围1-123", 465 | &provider{}, 466 | args{quantity: 124}, 467 | true, 468 | }, 469 | { 470 | "返回error", 471 | &provider{err: errors.New("error")}, 472 | args{quantity: 1, value: []byte{0x01, 0x02}}, 473 | true, 474 | }, 475 | { 476 | "value数据长度*2与数量不一致", 477 | &provider{data: []byte{0x00, 0x00, 0x00}}, 478 | args{quantity: 2, value: []byte{0x01, 0x02}}, 479 | true, 480 | }, 481 | { 482 | "返回数据长度不符", 483 | &provider{data: []byte{0x00, 0x00, 0x00}}, 484 | args{quantity: 1, value: []byte{0x01, 0x02}}, 485 | true, 486 | }, 487 | { 488 | "返回地址与请求一致", 489 | &provider{data: []byte{0x00, 0x01, 0x00, 0x01}}, 490 | args{quantity: 1, 491 | value: []byte{0x01, 0x02}}, 492 | true, 493 | }, 494 | { 495 | "返回数量与请求不一致", 496 | &provider{data: []byte{0x00, 0x00, 0x00, 0x02}}, 497 | args{quantity: 1, value: []byte{0x01, 0x02}}, 498 | true, 499 | }, 500 | { 501 | "正确", 502 | &provider{data: []byte{0x00, 0x00, 0x00, 0x01}}, 503 | args{quantity: 1, value: []byte{0x01, 0x02}}, 504 | false, 505 | }, 506 | } 507 | for _, tt := range tests { 508 | t.Run(tt.name, func(t *testing.T) { 509 | this := &client{ 510 | ClientProvider: tt.provide, 511 | } 512 | if err := this.WriteMultipleRegistersBytes(tt.args.slaveID, tt.args.address, tt.args.quantity, tt.args.value); (err != nil) != tt.wantErr { 513 | t.Errorf("client.WriteMultipleRegisters() error = %v, wantErr %v", err, tt.wantErr) 514 | } 515 | }) 516 | } 517 | } 518 | 519 | func Test_client_WriteMultipleRegisters(t *testing.T) { 520 | type args struct { 521 | slaveID byte 522 | address uint16 523 | quantity uint16 524 | value []uint16 525 | } 526 | tests := []struct { 527 | name string 528 | provide ClientProvider 529 | args args 530 | wantErr bool 531 | }{ 532 | { 533 | "正确", 534 | &provider{data: []byte{0x00, 0x00, 0x00, 0x01}}, 535 | args{quantity: 1, value: []uint16{0x0102}}, 536 | false, 537 | }, 538 | } 539 | for _, tt := range tests { 540 | t.Run(tt.name, func(t *testing.T) { 541 | sf := &client{ 542 | ClientProvider: tt.provide, 543 | } 544 | if err := sf.WriteMultipleRegisters(tt.args.slaveID, tt.args.address, tt.args.quantity, tt.args.value); (err != nil) != tt.wantErr { 545 | t.Errorf("WriteMultipleRegisters() error = %v, wantErr %v", err, tt.wantErr) 546 | } 547 | }) 548 | } 549 | } 550 | 551 | func Test_client_MaskWriteRegister(t *testing.T) { 552 | type args struct { 553 | slaveID byte 554 | address uint16 555 | andMask uint16 556 | orMask uint16 557 | } 558 | tests := []struct { 559 | name string 560 | provide ClientProvider 561 | args args 562 | wantErr bool 563 | }{ 564 | {"slaveid不在范围0-247", &provider{}, 565 | args{slaveID: 248}, true}, 566 | {"返回error", &provider{err: errors.New("error")}, 567 | args{}, true}, 568 | {"返回数据长度不符", &provider{data: []byte{0x00, 0x00, 0x00}}, 569 | args{}, true}, 570 | {"返回地址与请求一致", &provider{data: []byte{0x00, 0x01, 0x00, 0x02, 0x00, 0x03}}, 571 | args{address: 0, andMask: 0x0002, orMask: 0x0003}, true}, 572 | {"返回andMask与请求不一致", &provider{data: []byte{0x00, 0x01, 0x00, 0x02, 0x00, 0x03}}, 573 | args{address: 1, andMask: 0x0003, orMask: 0x0003}, true}, 574 | {"返回orMask与请求不一致", &provider{data: []byte{0x00, 0x01, 0x00, 0x02, 0x00, 0x03}}, 575 | args{address: 1, andMask: 0x0002, orMask: 0x0004}, true}, 576 | {"正确", &provider{data: []byte{0x00, 0x01, 0x00, 0x02, 0x00, 0x03}}, 577 | args{address: 1, andMask: 0x0002, orMask: 0x0003}, false}, 578 | } 579 | for _, tt := range tests { 580 | t.Run(tt.name, func(t *testing.T) { 581 | this := &client{ 582 | ClientProvider: tt.provide, 583 | } 584 | if err := this.MaskWriteRegister(tt.args.slaveID, tt.args.address, tt.args.andMask, tt.args.orMask); (err != nil) != tt.wantErr { 585 | t.Errorf("client.MaskWriteRegister() error = %v, wantErr %v", err, tt.wantErr) 586 | } 587 | }) 588 | } 589 | } 590 | 591 | func Test_client_ReadWriteMultipleRegistersBytes(t *testing.T) { 592 | type args struct { 593 | slaveID byte 594 | readAddress uint16 595 | readQuantity uint16 596 | writeAddress uint16 597 | writeQuantity uint16 598 | value []byte 599 | } 600 | tests := []struct { 601 | name string 602 | provide ClientProvider 603 | args args 604 | want []byte 605 | wantErr bool 606 | }{ 607 | { 608 | "slaveid不在范围1-247", 609 | &provider{}, 610 | args{slaveID: 248}, 611 | nil, 612 | true, 613 | }, 614 | { 615 | "读数量不在范围1-125", 616 | &provider{}, 617 | args{slaveID: 1, readQuantity: 126}, 618 | nil, 619 | true, 620 | }, 621 | { 622 | "读数量不在范围1-123", 623 | &provider{}, 624 | args{slaveID: 1, readQuantity: 1, writeQuantity: 124}, 625 | nil, 626 | true, 627 | }, 628 | { 629 | "返回error", 630 | &provider{err: errors.New("error")}, 631 | args{slaveID: 1, readQuantity: 1, writeQuantity: 1, value: []byte{0x01, 0x02}}, 632 | nil, 633 | true, 634 | }, 635 | { 636 | "value长度*2不等于数量的", 637 | &provider{err: errors.New("error")}, 638 | args{slaveID: 1, readQuantity: 1, writeQuantity: 2, value: []byte{0x01, 0x02}}, 639 | nil, 640 | true, 641 | }, 642 | { 643 | "返回数据长度不符", 644 | &provider{data: []byte{0x01, 0x00, 0x00}}, 645 | args{slaveID: 1, readQuantity: 1, writeQuantity: 1, value: []byte{0x01, 0x02}}, 646 | nil, 647 | true, 648 | }, 649 | { 650 | "正确", 651 | &provider{data: []byte{0x02, 0x00, 0x03}}, 652 | args{slaveID: 1, readQuantity: 1, writeQuantity: 1, value: []byte{0x01, 0x02}}, 653 | []byte{0x00, 0x03}, 654 | false, 655 | }, 656 | } 657 | for _, tt := range tests { 658 | t.Run(tt.name, func(t *testing.T) { 659 | this := &client{ 660 | ClientProvider: tt.provide, 661 | } 662 | got, err := this.ReadWriteMultipleRegistersBytes(tt.args.slaveID, tt.args.readAddress, tt.args.readQuantity, tt.args.writeAddress, tt.args.writeQuantity, tt.args.value) 663 | if (err != nil) != tt.wantErr { 664 | t.Errorf("client.ReadWriteMultipleRegistersBytes() error = %v, wantErr %v", err, tt.wantErr) 665 | return 666 | } 667 | if !reflect.DeepEqual(got, tt.want) { 668 | t.Errorf("client.ReadWriteMultipleRegistersBytes() = %v, want %v", got, tt.want) 669 | } 670 | }) 671 | } 672 | } 673 | 674 | func Test_client_ReadWriteMultipleRegisters(t *testing.T) { 675 | type args struct { 676 | slaveID byte 677 | readAddress uint16 678 | readQuantity uint16 679 | writeAddress uint16 680 | writeQuantity uint16 681 | value []byte 682 | } 683 | tests := []struct { 684 | name string 685 | provide ClientProvider 686 | args args 687 | want []uint16 688 | wantErr bool 689 | }{ 690 | { 691 | "slaveid不在范围1-247", 692 | &provider{}, 693 | args{slaveID: 248}, 694 | nil, 695 | true, 696 | }, 697 | { 698 | "读数量不在范围1-125", 699 | &provider{}, 700 | args{slaveID: 1, readQuantity: 126}, 701 | nil, 702 | true, 703 | }, 704 | { 705 | "读数量不在范围1-123", 706 | &provider{}, 707 | args{slaveID: 1, readQuantity: 1, writeQuantity: 124}, 708 | nil, 709 | true, 710 | }, 711 | { 712 | "返回error", 713 | &provider{err: errors.New("error")}, 714 | args{slaveID: 1, readQuantity: 1, writeQuantity: 1, value: []byte{0x01, 0x02}}, 715 | nil, 716 | true, 717 | }, 718 | { 719 | "value长度*2不等于数量的", 720 | &provider{err: errors.New("error")}, 721 | args{slaveID: 1, readQuantity: 1, writeQuantity: 2, value: []byte{0x01, 0x02}}, 722 | nil, 723 | true, 724 | }, 725 | { 726 | "返回数据长度不符", 727 | &provider{data: []byte{0x01, 0x00, 0x00}}, 728 | args{slaveID: 1, readQuantity: 1, writeQuantity: 1, value: []byte{0x01, 0x02}}, 729 | nil, 730 | true, 731 | }, 732 | { 733 | "正确", 734 | &provider{data: []byte{0x02, 0x00, 0x03}}, 735 | args{slaveID: 1, readQuantity: 1, writeQuantity: 1, value: []byte{0x01, 0x02}}, 736 | []uint16{0x0003}, 737 | false, 738 | }} 739 | for _, tt := range tests { 740 | t.Run(tt.name, func(t *testing.T) { 741 | this := &client{ 742 | ClientProvider: tt.provide, 743 | } 744 | got, err := this.ReadWriteMultipleRegisters(tt.args.slaveID, tt.args.readAddress, tt.args.readQuantity, tt.args.writeAddress, tt.args.writeQuantity, tt.args.value) 745 | if (err != nil) != tt.wantErr { 746 | t.Errorf("client.ReadWriteMultipleRegisters() error = %v, wantErr %v", err, tt.wantErr) 747 | return 748 | } 749 | if !reflect.DeepEqual(got, tt.want) { 750 | t.Errorf("client.ReadWriteMultipleRegisters() = %v, want %v", got, tt.want) 751 | } 752 | }) 753 | } 754 | } 755 | 756 | func Test_client_ReadFIFOQueue(t *testing.T) { 757 | type args struct { 758 | slaveID byte 759 | address uint16 760 | } 761 | tests := []struct { 762 | name string 763 | provide ClientProvider 764 | args args 765 | want []byte 766 | wantErr bool 767 | }{ 768 | {"slaveid不在范围1-247", &provider{}, 769 | args{slaveID: 248}, nil, true}, 770 | {"返回error", &provider{err: errors.New("error")}, 771 | args{slaveID: 1}, nil, true}, 772 | {"返回数据长度不符,需大于4", &provider{data: []byte{0x01, 0x00, 0x00}}, 773 | args{slaveID: 1}, nil, true}, 774 | {"byte长度不正确", &provider{data: []byte{0x00, 0x02, 0x00, 0x01, 0x02}}, 775 | args{slaveID: 1}, nil, true}, 776 | {"fifo长度超范围0-31", &provider{data: []byte{0x00, 0x02, 0x00, 0x20}}, 777 | args{slaveID: 1}, nil, true}, 778 | {"正确", &provider{data: []byte{0x00, 0x04, 0x00, 0x01, 0x01, 0x02}}, 779 | args{slaveID: 1}, []byte{0x01, 0x02}, false}, 780 | } 781 | for _, tt := range tests { 782 | t.Run(tt.name, func(t *testing.T) { 783 | this := &client{ 784 | ClientProvider: tt.provide, 785 | } 786 | got, err := this.ReadFIFOQueue(tt.args.slaveID, tt.args.address) 787 | if (err != nil) != tt.wantErr { 788 | t.Errorf("client.ReadFIFOQueue() error = %v, wantErr %v", err, tt.wantErr) 789 | return 790 | } 791 | if !reflect.DeepEqual(got, tt.want) { 792 | t.Errorf("client.ReadFIFOQueue() = %v, want %v", got, tt.want) 793 | } 794 | }) 795 | } 796 | } 797 | 798 | func Test_uint162Bytes(t *testing.T) { 799 | type args struct { 800 | value []uint16 801 | } 802 | tests := []struct { 803 | name string 804 | args args 805 | want []byte 806 | }{ 807 | {"", args{[]uint16{0x1234, 0x5678}}, []byte{0x12, 0x34, 0x56, 0x78}}, 808 | } 809 | for _, tt := range tests { 810 | t.Run(tt.name, func(t *testing.T) { 811 | if got := uint162Bytes(tt.args.value...); !reflect.DeepEqual(got, tt.want) { 812 | t.Errorf("uint162Bytes() = %v, want %v", got, tt.want) 813 | } 814 | }) 815 | } 816 | } 817 | 818 | func Test_pduDataBlockSuffix(t *testing.T) { 819 | type args struct { 820 | suffix []byte 821 | value []uint16 822 | } 823 | tests := []struct { 824 | name string 825 | args args 826 | want []byte 827 | }{ 828 | {"", args{[]byte{0x12, 0x34}, []uint16{0x4567}}, []byte{0x45, 0x67, 0x02, 0x12, 0x34}}, 829 | } 830 | for _, tt := range tests { 831 | t.Run(tt.name, func(t *testing.T) { 832 | if got := pduDataBlockSuffix(tt.args.suffix, tt.args.value...); !reflect.DeepEqual(got, tt.want) { 833 | t.Errorf("pduDataBlockSuffix() = %#v, want %#v", got, tt.want) 834 | } 835 | }) 836 | } 837 | } 838 | 839 | func Test_bytes2Uint16(t *testing.T) { 840 | type args struct { 841 | buf []byte 842 | } 843 | tests := []struct { 844 | name string 845 | args args 846 | want []uint16 847 | }{ 848 | {"byte to uint16", args{[]byte{0x12, 0x34}}, []uint16{0x1234}}, 849 | } 850 | for _, tt := range tests { 851 | t.Run(tt.name, func(t *testing.T) { 852 | if got := bytes2Uint16(tt.args.buf); !reflect.DeepEqual(got, tt.want) { 853 | t.Errorf("bytes2Uint16() = %#v, want %#v", got, tt.want) 854 | } 855 | }) 856 | } 857 | } 858 | 859 | func Benchmark_dataBlock(b *testing.B) { 860 | data := []uint16{0x01, 0x10, 0x8A, 0x00, 0x00, 0x03, 0xAA, 0x10} 861 | for i := 0; i < b.N; i++ { 862 | uint162Bytes(data...) 863 | } 864 | } 865 | 866 | func Benchmark_dataBlockSuffix(b *testing.B) { 867 | suffix := []byte{0x01, 0x10, 0x8A, 0x00, 0x00, 0x03, 0xAA, 0x10} 868 | data := []uint16{0x01, 0x10, 0x8A, 0x00, 0x00, 0x03, 0xAA, 0x10} 869 | for i := 0; i < b.N; i++ { 870 | pduDataBlockSuffix(suffix, data...) 871 | } 872 | } 873 | -------------------------------------------------------------------------------- /crc.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Cyclical Redundancy Checking. 8 | var ( 9 | once sync.Once 10 | crtTable []uint16 11 | ) 12 | 13 | // CRC16 Calculate Cyclical Redundancy Checking. 14 | func CRC16(bs []byte) uint16 { 15 | once.Do(initCrcTable) 16 | 17 | val := uint16(0xFFFF) 18 | for _, v := range bs { 19 | val = (val >> 8) ^ crtTable[(val^uint16(v))&0x00FF] 20 | } 21 | return val 22 | } 23 | 24 | // initCrcTable 初始化表. 25 | func initCrcTable() { 26 | crcPoly16 := uint16(0xa001) 27 | crtTable = make([]uint16, 256) 28 | 29 | for i := uint16(0); i < 256; i++ { 30 | crc := uint16(0) 31 | b := i 32 | 33 | for j := uint16(0); j < 8; j++ { 34 | if ((crc ^ b) & 0x0001) > 0 { 35 | crc = (crc >> 1) ^ crcPoly16 36 | } else { 37 | crc >>= 1 38 | } 39 | b >>= 1 40 | } 41 | crtTable[i] = crc 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crc_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_CRC16(t *testing.T) { 8 | type args struct { 9 | bs []byte 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want uint16 15 | }{ 16 | {"CRC16 ", args{[]byte{0x01, 0x02, 0x03, 0x04, 0x05}}, 0xbb2a}, 17 | } 18 | for _, tt := range tests { 19 | t.Run(tt.name, func(t *testing.T) { 20 | if got := CRC16(tt.args.bs); got != tt.want { 21 | t.Errorf("CRC16() = %v, want %v", got, tt.want) 22 | } 23 | }) 24 | } 25 | } 26 | 27 | func Benchmark_CRC16(b *testing.B) { 28 | for i := 0; i < b.N; i++ { 29 | _ = CRC16([]byte{0x01, 0x02, 0x03, 0x04, 0x05}) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // ErrClosedConnection connection has closed. 8 | var ErrClosedConnection = errors.New("use of closed connection") 9 | -------------------------------------------------------------------------------- /function.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "sync" 7 | ) 8 | 9 | // handle pdu data filed limit size. 10 | const ( 11 | FuncReadMinSize = 4 // 读操作 最小数据域个数 12 | FuncWriteMinSize = 4 // 写操作 最小数据域个数 13 | FuncWriteMultiMinSize = 5 // 写多个操作 最小数据域个数 14 | FuncReadWriteMinSize = 9 // 读写操作 最小数据域个数 15 | FuncMaskWriteMinSize = 6 // 屏蔽写操作 最小数据域个数 16 | ) 17 | 18 | // FunctionHandler 功能码对应的函数回调. 19 | // data 仅pdu数据域 不含功能码, return pdu 数据域,不含功能码. 20 | type FunctionHandler func(reg *NodeRegister, data []byte) ([]byte, error) 21 | 22 | type serverCommon struct { 23 | node sync.Map 24 | function map[uint8]FunctionHandler 25 | } 26 | 27 | func newServerCommon() *serverCommon { 28 | return &serverCommon{ 29 | function: map[uint8]FunctionHandler{ 30 | FuncCodeReadDiscreteInputs: funcReadDiscreteInputs, 31 | FuncCodeReadCoils: funcReadCoils, 32 | FuncCodeWriteSingleCoil: funcWriteSingleCoil, 33 | FuncCodeWriteMultipleCoils: funcWriteMultiCoils, 34 | FuncCodeReadInputRegisters: funcReadInputRegisters, 35 | FuncCodeReadHoldingRegisters: funcReadHoldingRegisters, 36 | FuncCodeWriteSingleRegister: funcWriteSingleRegister, 37 | FuncCodeWriteMultipleRegisters: funcWriteMultiHoldingRegisters, 38 | FuncCodeReadWriteMultipleRegisters: funcReadWriteMultiHoldingRegisters, 39 | FuncCodeMaskWriteRegister: funcMaskWriteRegisters, 40 | }, 41 | } 42 | } 43 | 44 | // AddNodes 增加节点. 45 | func (sf *serverCommon) AddNodes(nodes ...*NodeRegister) { 46 | for _, v := range nodes { 47 | sf.node.Store(v.slaveID, v) 48 | } 49 | } 50 | 51 | // DeleteNode 删除一个节点. 52 | func (sf *serverCommon) DeleteNode(slaveID byte) { 53 | sf.node.Delete(slaveID) 54 | } 55 | 56 | // DeleteAllNode 删除所有节点. 57 | func (sf *serverCommon) DeleteAllNode() { 58 | sf.node.Range(func(k, v interface{}) bool { 59 | sf.node.Delete(k) 60 | return true 61 | }) 62 | } 63 | 64 | // GetNode 获取一个节点. 65 | func (sf *serverCommon) GetNode(slaveID byte) (*NodeRegister, error) { 66 | v, ok := sf.node.Load(slaveID) 67 | if !ok { 68 | return nil, errors.New("slaveID not exist") 69 | } 70 | return v.(*NodeRegister), nil 71 | } 72 | 73 | // GetNodeList 获取节点列表. 74 | func (sf *serverCommon) GetNodeList() []*NodeRegister { 75 | list := make([]*NodeRegister, 0) 76 | sf.node.Range(func(k, v interface{}) bool { 77 | list = append(list, v.(*NodeRegister)) 78 | return true 79 | }) 80 | return list 81 | } 82 | 83 | // Range 扫描节点 same as sync map range. 84 | func (sf *serverCommon) Range(f func(slaveID byte, node *NodeRegister) bool) { 85 | sf.node.Range(func(k, v interface{}) bool { 86 | return f(k.(byte), v.(*NodeRegister)) 87 | }) 88 | } 89 | 90 | // RegisterFunctionHandler 注册回调函数. 91 | func (sf *serverCommon) RegisterFunctionHandler(funcCode uint8, function FunctionHandler) { 92 | if function != nil { 93 | sf.function[funcCode] = function 94 | } 95 | } 96 | 97 | // readBits 读位寄存器. 98 | func readBits(reg *NodeRegister, data []byte, isCoil bool) ([]byte, error) { 99 | var value []byte 100 | var err error 101 | 102 | if len(data) != FuncReadMinSize { 103 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 104 | } 105 | 106 | address := binary.BigEndian.Uint16(data) 107 | quality := binary.BigEndian.Uint16(data[2:]) 108 | if quality < ReadBitsQuantityMin || quality > ReadBitsQuantityMax { 109 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 110 | } 111 | if isCoil { 112 | value, err = reg.ReadCoils(address, quality) 113 | } else { 114 | value, err = reg.ReadDiscretes(address, quality) 115 | } 116 | if err != nil { 117 | return nil, err 118 | } 119 | result := make([]byte, 0, len(value)+1) 120 | result = append(result, byte(len(value))) 121 | return append(result, value...), nil 122 | } 123 | 124 | // funcReadDiscreteInputs 读离散量输入,返回仅含PDU数据域. 125 | // data: 126 | // Starting address : 2 byte 127 | // Quantity : 2 byte 128 | // return: 129 | // Byte count : 1 bytes 130 | // Coils status : n bytes n = Quantity/8 or n = Quantity/8 + 1 131 | func funcReadDiscreteInputs(reg *NodeRegister, data []byte) ([]byte, error) { 132 | return readBits(reg, data, false) 133 | } 134 | 135 | // funcReadCoils read multi coils. 136 | // data: 137 | // Starting address : 2 byte 138 | // Quantity : 2 byte 139 | // return: 140 | // Byte count : 1 bytes 141 | // Coils status : n bytes n = Quantity/8 or n = Quantity/8 + 1 142 | func funcReadCoils(reg *NodeRegister, data []byte) ([]byte, error) { 143 | return readBits(reg, data, true) 144 | } 145 | 146 | // funcWriteSingleCoil write single coil. 147 | // data: 148 | // Address : 2 byte 149 | // Value : 2 byte 0xff00 or 0x0000s 150 | // return: 151 | // Address : 2 byte 152 | // Value : 2 byte 0xff00 or 0x0000 153 | func funcWriteSingleCoil(reg *NodeRegister, data []byte) ([]byte, error) { 154 | if len(data) != FuncWriteMinSize { 155 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 156 | } 157 | 158 | address := binary.BigEndian.Uint16(data) 159 | newValue := binary.BigEndian.Uint16(data[2:]) 160 | if !(newValue == 0xFF00 || newValue == 0x0000) { 161 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 162 | } 163 | b := byte(0) 164 | if newValue == 0xFF00 { 165 | b = 1 166 | } 167 | err := reg.WriteCoils(address, 1, []byte{b}) 168 | return data, err 169 | } 170 | 171 | // funcWriteMultiCoils write multi coils. 172 | // data: 173 | // Starting address : 2 byte 174 | // Quantity : 2 byte 175 | // Byte count : 1 byte 176 | // Value : n byte 177 | // return: 178 | // Starting address : 2 byte 179 | // Quantity : 2 byte 180 | func funcWriteMultiCoils(reg *NodeRegister, data []byte) ([]byte, error) { 181 | if len(data) < FuncWriteMultiMinSize { 182 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 183 | } 184 | 185 | address := binary.BigEndian.Uint16(data) 186 | quality := binary.BigEndian.Uint16(data[2:]) 187 | byteCnt := data[4] 188 | if quality < WriteBitsQuantityMin || quality > WriteBitsQuantityMax || 189 | byteCnt != byte((quality+7)/8) { 190 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 191 | } 192 | err := reg.WriteCoils(address, quality, data[5:]) 193 | return data[:4], err 194 | } 195 | 196 | // readRegisters read multi registers. 197 | func readRegisters(reg *NodeRegister, data []byte, isHolding bool) ([]byte, error) { 198 | var err error 199 | var value []byte 200 | 201 | if len(data) != FuncReadMinSize { 202 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 203 | } 204 | 205 | address := binary.BigEndian.Uint16(data) 206 | quality := binary.BigEndian.Uint16(data[2:]) 207 | if quality > ReadRegQuantityMax || quality < ReadRegQuantityMin { 208 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 209 | } 210 | 211 | if isHolding { 212 | value, err = reg.ReadHoldingsBytes(address, quality) 213 | } else { 214 | value, err = reg.ReadInputsBytes(address, quality) 215 | } 216 | if err != nil { 217 | return nil, err 218 | } 219 | result := make([]byte, 0, len(value)+1) 220 | result = append(result, byte(quality*2)) 221 | result = append(result, value...) 222 | return result, nil 223 | } 224 | 225 | // funcReadInputRegisters 读输入寄存器 226 | // data: 227 | // Starting address : 2 byte 228 | // Quantity : 2 byte 229 | // return: 230 | // Byte count : 2 byte Quantity*2 231 | // Value : (Quantity)*2 byte 232 | func funcReadInputRegisters(reg *NodeRegister, data []byte) ([]byte, error) { 233 | return readRegisters(reg, data, false) 234 | } 235 | 236 | // funcReadHoldingRegisters 读保持寄存器 237 | // data: 238 | // Starting address : 2 byte 239 | // Quantity : 2 byte 240 | // return: 241 | // Byte count : 2 byte Quantity*2 242 | // Value : (Quantity)*2 byte 243 | func funcReadHoldingRegisters(reg *NodeRegister, data []byte) ([]byte, error) { 244 | return readRegisters(reg, data, true) 245 | } 246 | 247 | // funcWriteSingleRegister 写单个保持寄存器 248 | // data: 249 | // Address : 2 byte 250 | // Value : 2 byte 251 | // return: 252 | // Address : 2 byte 253 | // Value : 2 byte 254 | func funcWriteSingleRegister(reg *NodeRegister, data []byte) ([]byte, error) { 255 | if len(data) != FuncWriteMinSize { 256 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 257 | } 258 | 259 | address := binary.BigEndian.Uint16(data) 260 | err := reg.WriteHoldingsBytes(address, 1, data[2:]) 261 | return data, err 262 | } 263 | 264 | // funcWriteMultiHoldingRegisters 写多个保持寄存器 265 | // data: 266 | // Starting address : 2 byte 267 | // Quantity : 2 byte 268 | // Byte count : 1 byte Quantity*2 269 | // Value : Quantity*2 byte 270 | // return: 271 | // Starting address : 2 byte 272 | // Quantity : 2 byte 273 | func funcWriteMultiHoldingRegisters(reg *NodeRegister, data []byte) ([]byte, error) { 274 | if len(data) < FuncWriteMultiMinSize { 275 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 276 | } 277 | 278 | address := binary.BigEndian.Uint16(data) 279 | count := binary.BigEndian.Uint16(data[2:]) 280 | byteCnt := data[4] 281 | if count < WriteRegQuantityMin || count > WriteRegQuantityMax || 282 | uint16(byteCnt) != count*2 { 283 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 284 | } 285 | 286 | err := reg.WriteHoldingsBytes(address, count, data[5:]) 287 | if err != nil { 288 | return nil, err 289 | } 290 | binary.BigEndian.PutUint16(data[2:], count) 291 | return data[:4], nil 292 | } 293 | 294 | // funcReadWriteMultiHoldingRegisters 读写多个保持寄存器 295 | // data: 296 | // Read Starting address : 2 byte 297 | // Quantity read : 2 byte 298 | // Write Starting address : 2 byte 299 | // Quantity Write : 2 byte 300 | // Byte count Write : 1 byte (Quantity Write)*2 301 | // Value Write : (Quantity Write)*2 byte 302 | // return: 303 | // Byte count : 2 byte (Quantity read)*2 304 | // Value : (Quantity read)*2 byte 305 | func funcReadWriteMultiHoldingRegisters(reg *NodeRegister, data []byte) ([]byte, error) { 306 | if len(data) < FuncReadWriteMinSize { 307 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 308 | } 309 | 310 | readAddress := binary.BigEndian.Uint16(data) 311 | readCount := binary.BigEndian.Uint16(data[2:]) 312 | writeAddress := binary.BigEndian.Uint16(data[4:]) 313 | WriteCount := binary.BigEndian.Uint16(data[6:]) 314 | writeByteCnt := data[8] 315 | if readCount < ReadWriteOnReadRegQuantityMin || readCount > ReadWriteOnReadRegQuantityMax || 316 | WriteCount < ReadWriteOnWriteRegQuantityMin || WriteCount > ReadWriteOnWriteRegQuantityMax || 317 | uint16(writeByteCnt) != WriteCount*2 { 318 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 319 | } 320 | 321 | if err := reg.WriteHoldingsBytes(writeAddress, WriteCount, data[9:]); err != nil { 322 | return nil, err 323 | } 324 | value, err := reg.ReadHoldingsBytes(readAddress, readCount) 325 | if err != nil { 326 | return nil, err 327 | } 328 | result := make([]byte, 0, len(value)+1) 329 | result = append(result, byte(readCount*2)) 330 | result = append(result, value...) 331 | return result, nil 332 | } 333 | 334 | // funcMaskWriteRegisters 屏蔽写寄存器 335 | // data: 336 | // address : 2 byte 337 | // And_mask : 2 byte 338 | // Or_mask : 2 byte 339 | // return: 340 | // address : 2 byte 341 | // And_mask : 2 byte 342 | // Or_mask : 2 byte 343 | func funcMaskWriteRegisters(reg *NodeRegister, data []byte) ([]byte, error) { 344 | if len(data) != FuncMaskWriteMinSize { 345 | return nil, &ExceptionError{ExceptionCodeIllegalDataValue} 346 | } 347 | 348 | referAddress := binary.BigEndian.Uint16(data) 349 | andMask := binary.BigEndian.Uint16(data[2:]) 350 | orMask := binary.BigEndian.Uint16(data[4:]) 351 | err := reg.MaskWriteHolding(referAddress, andMask, orMask) 352 | return data, err 353 | } 354 | 355 | // TODO funcReadFIFOQueue 356 | // func (this *ExtraOption)funcReadFIFOQueue(*NodeRegister, []byte) ([]byte, error) { 357 | // return nil, nil 358 | // } 359 | -------------------------------------------------------------------------------- /function_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_newServerHandler(t *testing.T) { 9 | sh := newServerCommon() 10 | tests := []struct { 11 | name string 12 | got *serverCommon 13 | want *serverCommon 14 | }{ 15 | {"just cover", sh, sh}, 16 | } 17 | for _, tt := range tests { 18 | t.Run(tt.name, func(t *testing.T) { 19 | if !reflect.DeepEqual(tt.got, tt.want) { 20 | t.Errorf("newServerCommon() = %v, want %v", tt.want, tt.want) 21 | } 22 | }) 23 | } 24 | } 25 | 26 | func Test_funcReadDiscreteInputs(t *testing.T) { 27 | type args struct { 28 | reg *NodeRegister 29 | data []byte 30 | } 31 | tests := []struct { 32 | name string 33 | args args 34 | want []byte 35 | wantErr bool 36 | }{ 37 | {"数据长度data小于FuncReadMinSize[4]", args{readReg, []byte{0, 0, 0}}, nil, true}, 38 | {"数量小于1或大于2000", args{readReg, []byte{0x00, 0x00, 0x07, 0xd1}}, nil, true}, 39 | {"正常读0起始,8位", args{readReg, []byte{0x00, 0x00, 0x00, 0x08}}, []byte{0x01, 0xaa}, false}, 40 | {"正常读4起始,9位", args{readReg, []byte{0x00, 0x04, 0x00, 0x09}}, []byte{0x02, 0x5a, 0x01}, false}, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | got, err := funcReadDiscreteInputs(tt.args.reg, tt.args.data) 45 | if (err != nil) != tt.wantErr { 46 | t.Errorf("funcReadDiscreteInputs() error = %v, wantErr %v", err, tt.wantErr) 47 | return 48 | } 49 | if !reflect.DeepEqual(got, tt.want) { 50 | t.Errorf("funcReadDiscreteInputs() = %#v, want %#v", got, tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func Test_funcReadCoils(t *testing.T) { 57 | type args struct { 58 | reg *NodeRegister 59 | data []byte 60 | } 61 | tests := []struct { 62 | name string 63 | args args 64 | want []byte 65 | wantErr bool 66 | }{ 67 | {"数据长度data小于FuncReadMinSize[4]", args{readReg, []byte{0, 0, 0}}, nil, true}, 68 | {"数量小于1或大于2000", args{readReg, []byte{0x00, 0x00, 0x07, 0xd1}}, nil, true}, 69 | {"正常读0起始,8位", args{readReg, []byte{0x00, 0x00, 0x00, 0x08}}, []byte{0x01, 0x55}, false}, 70 | {"正常读4起始,9位", args{readReg, []byte{0x00, 0x04, 0x00, 0x09}}, []byte{0x02, 0xa5, 0x00}, false}, 71 | } 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | got, err := funcReadCoils(tt.args.reg, tt.args.data) 75 | if (err != nil) != tt.wantErr { 76 | t.Errorf("funcReadCoils() error = %v, wantErr %v", err, tt.wantErr) 77 | return 78 | } 79 | if !reflect.DeepEqual(got, tt.want) { 80 | t.Errorf("funcReadCoils() = %#v, want %#v", got, tt.want) 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func Test_funcWriteSingleCoil(t *testing.T) { 87 | type args struct { 88 | reg *NodeRegister 89 | data []byte 90 | } 91 | tests := []struct { 92 | name string 93 | args args 94 | want []byte 95 | wantErr bool 96 | }{ 97 | {"数据长度data小于FuncReadMinSize[4]", args{newNodeReg(), []byte{0, 0, 0}}, nil, true}, 98 | {"线圈值不是0x000或0xff00", args{newNodeReg(), []byte{0x00, 0x00, 0x01, 0x00}}, nil, true}, 99 | {"写0xff00", args{newNodeReg(), []byte{0x00, 0x00, 0xff, 0x00}}, []byte{0x00, 0x00, 0xff, 0x00}, false}, 100 | {"写0xff00", args{newNodeReg(), []byte{0x00, 0x00, 0x00, 0x00}}, []byte{0x00, 0x00, 0x00, 0x00}, false}, 101 | } 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | got, err := funcWriteSingleCoil(tt.args.reg, tt.args.data) 105 | if (err != nil) != tt.wantErr { 106 | t.Errorf("funcWriteSingleCoil() error = %v, wantErr %v", err, tt.wantErr) 107 | return 108 | } 109 | if !reflect.DeepEqual(got, tt.want) { 110 | t.Errorf("funcWriteSingleCoil() = %#v, want %#v", got, tt.want) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func Test_funcWriteMultiCoils(t *testing.T) { 117 | type args struct { 118 | reg *NodeRegister 119 | data []byte 120 | } 121 | tests := []struct { 122 | name string 123 | args args 124 | want []byte 125 | wantErr bool 126 | }{ 127 | {"数据长度data小于FuncWriteMultiMinSize[5]", args{newNodeReg(), []byte{0, 0, 0}}, nil, true}, 128 | {"数量小于1或大于2000", args{newNodeReg(), []byte{0x00, 0x00, 0x07, 0xd1, 0x00}}, nil, true}, 129 | {"写值", args{newNodeReg(), []byte{0x00, 0x00, 0x00, 0x01, 0x01, 0x77}}, []byte{0x00, 0x00, 0x00, 0x01}, false}, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | got, err := funcWriteMultiCoils(tt.args.reg, tt.args.data) 134 | if (err != nil) != tt.wantErr { 135 | t.Errorf("funcWriteMultiCoils() error = %v, wantErr %v", err, tt.wantErr) 136 | return 137 | } 138 | if !reflect.DeepEqual(got, tt.want) { 139 | t.Errorf("funcWriteMultiCoils() = %#v, want %#v", got, tt.want) 140 | } 141 | }) 142 | } 143 | } 144 | 145 | func Test_funcReadInputRegisters(t *testing.T) { 146 | type args struct { 147 | reg *NodeRegister 148 | data []byte 149 | } 150 | tests := []struct { 151 | name string 152 | args args 153 | want []byte 154 | wantErr bool 155 | }{ 156 | {"数据长度data小于FuncReadMinSize[4]", args{readReg, []byte{0x00, 0x00, 0x00}}, nil, true}, 157 | {"数量小于1或大于125", args{readReg, []byte{0x00, 0x00, 0x00, 0x7e}}, nil, true}, 158 | {"读值", args{readReg, []byte{0x00, 0x00, 0x00, 0x1}}, []byte{0x02, 0x90, 0x12}, false}, 159 | } 160 | for _, tt := range tests { 161 | t.Run(tt.name, func(t *testing.T) { 162 | got, err := funcReadInputRegisters(tt.args.reg, tt.args.data) 163 | if (err != nil) != tt.wantErr { 164 | t.Errorf("funcReadInputRegisters() error = %v, wantErr %v", err, tt.wantErr) 165 | return 166 | } 167 | if !reflect.DeepEqual(got, tt.want) { 168 | t.Errorf("funcReadInputRegisters() = %#v, want %#v", got, tt.want) 169 | } 170 | }) 171 | } 172 | } 173 | 174 | func Test_funcReadHoldingRegisters(t *testing.T) { 175 | type args struct { 176 | reg *NodeRegister 177 | data []byte 178 | } 179 | tests := []struct { 180 | name string 181 | args args 182 | want []byte 183 | wantErr bool 184 | }{ 185 | {"数据长度data小于FuncReadMinSize[4]", args{readReg, []byte{0x00, 0x00, 0x00}}, nil, true}, 186 | {"数量小于1或大于125", args{readReg, []byte{0x00, 0x00, 0x00, 0x7e}}, nil, true}, 187 | {"读值", args{readReg, []byte{0x00, 0x00, 0x00, 0x1}}, []byte{0x02, 0x12, 0x34}, false}, 188 | } 189 | for _, tt := range tests { 190 | t.Run(tt.name, func(t *testing.T) { 191 | got, err := funcReadHoldingRegisters(tt.args.reg, tt.args.data) 192 | if (err != nil) != tt.wantErr { 193 | t.Errorf("funcReadHoldingRegisters() error = %v, wantErr %v", err, tt.wantErr) 194 | return 195 | } 196 | if !reflect.DeepEqual(got, tt.want) { 197 | t.Errorf("funcReadHoldingRegisters() = %v, want %v", got, tt.want) 198 | } 199 | }) 200 | } 201 | } 202 | 203 | func Test_funcWriteSingleRegister(t *testing.T) { 204 | type args struct { 205 | reg *NodeRegister 206 | data []byte 207 | } 208 | tests := []struct { 209 | name string 210 | args args 211 | want []byte 212 | wantErr bool 213 | }{ 214 | {"数据长度data小于FuncReadMinSize[4]", args{newNodeReg(), []byte{0x00, 0x00, 0x00}}, nil, true}, 215 | {"写值", args{newNodeReg(), []byte{0x00, 0x00, 0x00, 0x1}}, []byte{0x00, 0x00, 0x00, 0x1}, false}, 216 | } 217 | for _, tt := range tests { 218 | t.Run(tt.name, func(t *testing.T) { 219 | got, err := funcWriteSingleRegister(tt.args.reg, tt.args.data) 220 | if (err != nil) != tt.wantErr { 221 | t.Errorf("funcWriteSingleRegister() error = %v, wantErr %v", err, tt.wantErr) 222 | return 223 | } 224 | if !reflect.DeepEqual(got, tt.want) { 225 | t.Errorf("funcWriteSingleRegister() = %v, want %v", got, tt.want) 226 | } 227 | }) 228 | } 229 | } 230 | 231 | func Test_funcWriteMultiHoldingRegisters(t *testing.T) { 232 | type args struct { 233 | reg *NodeRegister 234 | data []byte 235 | } 236 | tests := []struct { 237 | name string 238 | args args 239 | want []byte 240 | wantErr bool 241 | }{ 242 | {"数据长度data小于FuncWriteMultiMinSize[5]", args{newNodeReg(), []byte{0x00, 0x00, 0x00}}, nil, true}, 243 | {"数量小于1或大于123", args{newNodeReg(), []byte{0x00, 0x00, 0x00, 0x7c, 0x01}}, nil, true}, 244 | {"写值", args{newNodeReg(), []byte{0x00, 0x00, 0x00, 0x01, 0x02, 0x12, 0x34}}, []byte{0x00, 0x00, 0x00, 0x01}, false}, 245 | } 246 | for _, tt := range tests { 247 | t.Run(tt.name, func(t *testing.T) { 248 | got, err := funcWriteMultiHoldingRegisters(tt.args.reg, tt.args.data) 249 | if (err != nil) != tt.wantErr { 250 | t.Errorf("funcWriteMultiHoldingRegisters() error = %v, wantErr %v", err, tt.wantErr) 251 | return 252 | } 253 | if !reflect.DeepEqual(got, tt.want) { 254 | t.Errorf("funcWriteMultiHoldingRegisters() = %v, want %v", got, tt.want) 255 | } 256 | }) 257 | } 258 | } 259 | 260 | func Test_funcReadWriteMultiHoldingRegisters(t *testing.T) { 261 | type args struct { 262 | reg *NodeRegister 263 | data []byte 264 | } 265 | tests := []struct { 266 | name string 267 | args args 268 | want []byte 269 | wantErr bool 270 | }{ 271 | { 272 | "数据长度data小于FuncReadWriteMinSize[9]", 273 | args{newNodeReg(), 274 | []byte{0x00, 0x00, 0x00}}, 275 | nil, 276 | true, 277 | }, 278 | { 279 | "读数量小于1或大于125 或者 写数量小于1或大于121", 280 | args{newNodeReg(), 281 | []byte{0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x7a, 0x00}}, 282 | nil, 283 | true, 284 | }, 285 | { 286 | "读写值", 287 | args{newNodeReg(), 288 | []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x12, 0x34}}, 289 | []byte{0x02, 0x12, 0x34}, 290 | false, 291 | }, 292 | } 293 | for _, tt := range tests { 294 | t.Run(tt.name, func(t *testing.T) { 295 | got, err := funcReadWriteMultiHoldingRegisters(tt.args.reg, tt.args.data) 296 | if (err != nil) != tt.wantErr { 297 | t.Errorf("funcReadWriteMultiHoldingRegisters() error = %v, wantErr %v", err, tt.wantErr) 298 | return 299 | } 300 | if !reflect.DeepEqual(got, tt.want) { 301 | t.Errorf("funcReadWriteMultiHoldingRegisters() = %v, want %v", got, tt.want) 302 | } 303 | }) 304 | } 305 | } 306 | 307 | func Test_funcMaskWriteRegisters(t *testing.T) { 308 | nodeReg := &NodeRegister{ 309 | holdingAddrStart: 0, 310 | holding: []uint16{0x0000, 0x0012, 0x0000}, 311 | } 312 | type args struct { 313 | reg *NodeRegister 314 | data []byte 315 | } 316 | tests := []struct { 317 | name string 318 | args args 319 | want []byte 320 | wantErr bool 321 | }{ 322 | {"数据长度data小于FuncMaskWriteMinSize[6]", args{newNodeReg(), []byte{0x00, 0x00, 0x00}}, nil, true}, 323 | {"写值", args{nodeReg, []byte{0x00, 0x01, 0x00, 0xf2, 0x00, 0x25}}, []byte{0x00, 0x01, 0x00, 0xf2, 0x00, 0x25}, false}, 324 | } 325 | for _, tt := range tests { 326 | t.Run(tt.name, func(t *testing.T) { 327 | got, err := funcMaskWriteRegisters(tt.args.reg, tt.args.data) 328 | if (err != nil) != tt.wantErr { 329 | t.Errorf("funcMaskWriteRegisters() error = %v, wantErr %v", err, tt.wantErr) 330 | return 331 | } 332 | if !reflect.DeepEqual(got, tt.want) { 333 | t.Errorf("funcMaskWriteRegisters() = %v, want %v", got, tt.want) 334 | } 335 | }) 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thinkgos/gomodbus/v2 2 | 3 | go 1.14 4 | 5 | require github.com/goburrow/serial v0.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= 2 | github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA= 3 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "os" 5 | "sync/atomic" 6 | 7 | "log" 8 | ) 9 | 10 | // 内部调试实现. 11 | type logger struct { 12 | provider LogProvider 13 | // has log output enabled, 14 | // 1: enable 15 | // 0: disable 16 | has uint32 17 | } 18 | 19 | // newLogger new logger with prefix. 20 | func newLogger(prefix string) logger { 21 | return logger{ 22 | provider: defaultLogger{log.New(os.Stdout, prefix, log.LstdFlags)}, 23 | has: 0, 24 | } 25 | } 26 | 27 | // LogMode set enable or disable log output when you has set logger. 28 | func (sf *logger) LogMode(enable bool) { 29 | if enable { 30 | atomic.StoreUint32(&sf.has, 1) 31 | } else { 32 | atomic.StoreUint32(&sf.has, 0) 33 | } 34 | } 35 | 36 | // setLogProvider overwrite log provider. 37 | func (sf *logger) setLogProvider(p LogProvider) { 38 | if p != nil { 39 | sf.provider = p 40 | } 41 | } 42 | 43 | // Error Log ERROR level message. 44 | func (sf logger) Error(format string, v ...interface{}) { 45 | if atomic.LoadUint32(&sf.has) == 1 { 46 | sf.provider.Error(format, v...) 47 | } 48 | } 49 | 50 | // Debug Log DEBUG level message. 51 | func (sf logger) Debug(format string, v ...interface{}) { 52 | if atomic.LoadUint32(&sf.has) == 1 { 53 | sf.provider.Debug(format, v...) 54 | } 55 | } 56 | 57 | // default log. 58 | type defaultLogger struct { 59 | *log.Logger 60 | } 61 | 62 | // check implement LogProvider interface. 63 | var _ LogProvider = (*defaultLogger)(nil) 64 | 65 | // Error Log ERROR level message. 66 | func (sf defaultLogger) Error(format string, v ...interface{}) { 67 | sf.Printf("[E]: "+format, v...) 68 | } 69 | 70 | // Debug Log DEBUG level message. 71 | func (sf defaultLogger) Debug(format string, v ...interface{}) { 72 | sf.Printf("[D]: "+format, v...) 73 | } 74 | -------------------------------------------------------------------------------- /lrc.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | // LRC lrc sum. 4 | type LRC struct { 5 | sum uint8 6 | } 7 | 8 | // Reset rest lrc sum. 9 | func (sf *LRC) Reset() *LRC { 10 | sf.sum = 0 11 | return sf 12 | } 13 | 14 | // Push push data in sum. 15 | func (sf *LRC) Push(data ...byte) *LRC { 16 | for _, b := range data { 17 | sf.sum += b 18 | } 19 | return sf 20 | } 21 | 22 | // Value got lrc value. 23 | func (sf *LRC) Value() byte { 24 | return uint8(-int8(sf.sum)) 25 | } 26 | -------------------------------------------------------------------------------- /lrc_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_LRC(t *testing.T) { 8 | var lrc LRC 9 | type args struct { 10 | bs []byte 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want byte 16 | }{ 17 | {"lrc校验", args{[]byte{0x01, 0x03, 0x01, 0x0a}}, 0xf1}, 18 | } 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | if got := lrc.Reset().Push(tt.args.bs...).Value(); got != tt.want { 22 | t.Errorf("lrc() = %v, want %v", got, tt.want) 23 | } 24 | }) 25 | } 26 | } 27 | 28 | func Benchmark_LRC(b *testing.B) { 29 | var lrc LRC 30 | for i := 0; i < b.N; i++ { 31 | lrc.Reset().Push([]byte{0x02, 0x07, 0x01, 0x03, 0x01, 0x0a}...).Value() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modbus.go: -------------------------------------------------------------------------------- 1 | /*! 2 | * Constants which defines the format of a modbus frame. The example is 3 | * shown for a Modbus RTU/ASCII frame. Note that the Modbus PDU is not 4 | * dependent on the underlying transport. 5 | * 6 | * 7 | * <------------------------ MODBUS SERIAL LINE ADU (1) -------------------> 8 | * <----------- MODBUS PDU (1') ----------------> 9 | * +-----------+---------------+----------------------------+-------------+ 10 | * | Address | Function Code | Data | CRC/LRC | 11 | * +-----------+---------------+----------------------------+-------------+ 12 | * | | | | 13 | * (2) (3/2') (3') (4) 14 | * 15 | * (1) ... SerADUMaxSize = 256 16 | * (2) ... SerAddressOffset = 0 17 | * (3) ... SerPDUOffset = 1 18 | * (4) ... SerCrcSize = 2 19 | * ... SerLrcSize = 1 20 | * 21 | * (1') ... SerPDUMaxSize = 253 22 | * (2') ... SerPDUFuncCodeOffset = 0 23 | * (3') ... SerPDUDataOffset = 1 24 | * 25 | */ 26 | 27 | /*! 28 | * <------------------------ MODBUS TCP/IP ADU(1) -------------------------> 29 | * <----------- MODBUS PDU (1') --------------> 30 | * +-----------+---------------+------------------------------------------+ 31 | * | TID | PID | Length | UID | Function Code | Data | 32 | * +-----------+---------------+------------------------------------------+ 33 | * | | | | | 34 | * (2) (3) (4) (5) (6) 35 | * 36 | * (2) ... TCPTidOffset = 0 (Transaction Identifier - 2 Byte) 37 | * (3) ... TCPPidOffset = 2 (Protocol Identifier - 2 Byte) 38 | * (4) ... TCPLengthOffset = 4 (Number of bytes - 2 Byte)( UID + PDU length ) 39 | * (5) ... TCPUidOffset = 6 (Unit Identifier - 1 Byte) 40 | * (6) ... TCPPDUOffset = 7 (Modbus PDU ) 41 | * 42 | * (1) ... TCPADUMaxSize = 260 Modbus TCP/IP Application Data Unit 43 | * (1') ... SerPDUMaxSize = 253 Modbus Protocol Data Unit 44 | */ 45 | 46 | /* 47 | Package modbus provides a client for modbus TCP and RTU/ASCII.contain modbus TCP server 48 | */ 49 | package modbus 50 | 51 | import ( 52 | "fmt" 53 | "time" 54 | 55 | "github.com/goburrow/serial" 56 | ) 57 | 58 | // proto address limit. 59 | const ( 60 | AddressBroadCast = 0 61 | AddressMin = 1 62 | addressMax = 247 63 | ) 64 | 65 | // AddressMax proto address max limit 66 | // you can change with SetSpecialAddressMax, 67 | // when your device have address upon addressMax 68 | var AddressMax byte = addressMax 69 | 70 | const ( 71 | pduMinSize = 1 // funcCode(1) 72 | pduMaxSize = 253 // funcCode(1) + data(252) 73 | 74 | rtuAduMinSize = 4 // address(1) + funcCode(1) + crc(2) 75 | rtuAduMaxSize = 256 // address(1) + PDU(253) + crc(2) 76 | 77 | asciiAduMinSize = 3 78 | asciiAduMaxSize = 256 79 | asciiCharacterMaxSize = 513 80 | 81 | tcpProtocolIdentifier = 0x0000 82 | // Modbus Application Protocol 83 | tcpHeaderMbapSize = 7 // MBAP header 84 | tcpAduMinSize = 8 // MBAP + funcCode 85 | tcpAduMaxSize = 260 86 | ) 87 | 88 | // proto register limit 89 | const ( 90 | // Bits 91 | ReadBitsQuantityMin = 1 // 0x0001 92 | ReadBitsQuantityMax = 2000 // 0x07d0 93 | WriteBitsQuantityMin = 1 // 1 94 | WriteBitsQuantityMax = 1968 // 0x07b0 95 | // 16 Bits 96 | ReadRegQuantityMin = 1 // 1 97 | ReadRegQuantityMax = 125 // 0x007d 98 | WriteRegQuantityMin = 1 // 1 99 | WriteRegQuantityMax = 123 // 0x007b 100 | ReadWriteOnReadRegQuantityMin = 1 // 1 101 | ReadWriteOnReadRegQuantityMax = 125 // 0x007d 102 | ReadWriteOnWriteRegQuantityMin = 1 // 1 103 | ReadWriteOnWriteRegQuantityMax = 121 // 0x0079 104 | ) 105 | 106 | // Function Code 107 | const ( 108 | // Bit access 109 | FuncCodeReadDiscreteInputs = 2 110 | FuncCodeReadCoils = 1 111 | FuncCodeWriteSingleCoil = 5 112 | FuncCodeWriteMultipleCoils = 15 113 | 114 | // 16-bit access 115 | FuncCodeReadInputRegisters = 4 116 | FuncCodeReadHoldingRegisters = 3 117 | FuncCodeWriteSingleRegister = 6 118 | FuncCodeWriteMultipleRegisters = 16 119 | FuncCodeReadWriteMultipleRegisters = 23 120 | FuncCodeMaskWriteRegister = 22 121 | FuncCodeReadFIFOQueue = 24 122 | FuncCodeOtherReportSlaveID = 17 123 | // FuncCodeDiagReadException = 7 124 | // FuncCodeDiagDiagnostic = 8 125 | // FuncCodeDiagGetComEventCnt = 11 126 | // FuncCodeDiagGetComEventLog = 12 127 | ) 128 | 129 | // Exception Code 130 | const ( 131 | ExceptionCodeIllegalFunction = 1 132 | ExceptionCodeIllegalDataAddress = 2 133 | ExceptionCodeIllegalDataValue = 3 134 | ExceptionCodeServerDeviceFailure = 4 135 | ExceptionCodeAcknowledge = 5 136 | ExceptionCodeServerDeviceBusy = 6 137 | ExceptionCodeNegativeAcknowledge = 7 138 | ExceptionCodeMemoryParityError = 8 139 | ExceptionCodeGatewayPathUnavailable = 10 140 | ExceptionCodeGatewayTargetDeviceFailedToRespond = 11 141 | ) 142 | 143 | // ExceptionError implements error interface. 144 | type ExceptionError struct { 145 | ExceptionCode byte 146 | } 147 | 148 | // Error converts known modbus exception code to error message. 149 | func (e *ExceptionError) Error() string { 150 | var name string 151 | switch e.ExceptionCode { 152 | case ExceptionCodeIllegalFunction: 153 | name = "illegal function" 154 | case ExceptionCodeIllegalDataAddress: 155 | name = "illegal data address" 156 | case ExceptionCodeIllegalDataValue: 157 | name = "illegal data value" 158 | case ExceptionCodeServerDeviceFailure: 159 | name = "server device failure" 160 | case ExceptionCodeAcknowledge: 161 | name = "acknowledge" 162 | case ExceptionCodeServerDeviceBusy: 163 | name = "server device busy" 164 | case ExceptionCodeNegativeAcknowledge: 165 | name = "Negative Acknowledge" 166 | case ExceptionCodeMemoryParityError: 167 | name = "memory parity error" 168 | case ExceptionCodeGatewayPathUnavailable: 169 | name = "gateway path unavailable" 170 | case ExceptionCodeGatewayTargetDeviceFailedToRespond: 171 | name = "gateway target device failed to respond" 172 | default: 173 | name = "unknown" 174 | } 175 | return fmt.Sprintf("modbus: exception '%v' (%s)", e.ExceptionCode, name) 176 | } 177 | 178 | // protocolTCPHeader independent of underlying communication layers. 179 | type protocolTCPHeader struct { 180 | transactionID uint16 181 | protocolID uint16 182 | length uint16 183 | slaveID uint8 // only modbus RTU and ascii 184 | } 185 | 186 | // ProtocolDataUnit (PDU) is independent of underlying communication layers. 187 | type ProtocolDataUnit struct { 188 | FuncCode byte 189 | Data []byte 190 | } 191 | 192 | // protocolFrame protocol frame in pool 193 | type protocolFrame struct { 194 | adu []byte 195 | } 196 | 197 | // ClientProvider is the interface implements underlying methods. 198 | type ClientProvider interface { 199 | // Connect try to connect the remote server 200 | Connect() error 201 | // IsConnected returns a bool signifying whether 202 | // the client is connected or not. 203 | IsConnected() bool 204 | // SetAutoReconnect set auto reconnect count 205 | // if cnt == 0, disable auto reconnect 206 | // if cnt > 0 ,enable auto reconnect,but max 6 207 | SetAutoReconnect(cnt byte) 208 | // LogMode set enable or diable log output when you has set logger 209 | LogMode(enable bool) 210 | // Close disconnect the remote server 211 | Close() error 212 | // Send request to the remote server,it implements on SendRawFrame 213 | Send(slaveID byte, request ProtocolDataUnit) (ProtocolDataUnit, error) 214 | // SendPdu send pdu request to the remote server 215 | SendPdu(slaveID byte, pduRequest []byte) (pduResponse []byte, err error) 216 | // SendRawFrame send raw frame to the remote server 217 | SendRawFrame(aduRequest []byte) (aduResponse []byte, err error) 218 | 219 | // private interface 220 | // setLogProvider set logger provider 221 | setLogProvider(p LogProvider) 222 | // setSerialConfig set serial config 223 | setSerialConfig(config serial.Config) 224 | // setTCPTimeout set tcp connect & read timeout 225 | setTCPTimeout(t time.Duration) 226 | } 227 | 228 | // LogProvider RFC5424 log message levels only Debug and Error 229 | type LogProvider interface { 230 | Error(format string, v ...interface{}) 231 | Debug(format string, v ...interface{}) 232 | } 233 | 234 | // SetSpecialAddressMax set special slaveID limit 235 | func SetSpecialAddressMax(slaveID byte) { 236 | AddressMax = slaveID 237 | } 238 | -------------------------------------------------------------------------------- /register.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | // 本文件提供了寄存器的底层封装,并且是线程安全的,丰富的api满足基本需求 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "sync" 9 | ) 10 | 11 | // NodeRegister 节点寄存器 12 | type NodeRegister struct { 13 | rw sync.RWMutex // 读写锁 14 | slaveID byte 15 | coilsAddrStart, coilsQuantity uint16 16 | coils []uint8 17 | discreteAddrStart, discreteQuantity uint16 18 | discrete []uint8 19 | inputAddrStart uint16 20 | input []uint16 21 | holdingAddrStart uint16 22 | holding []uint16 23 | } 24 | 25 | // NewNodeRegister 创建一个modbus子节点寄存器列表 26 | func NewNodeRegister(slaveID byte, 27 | coilsAddrStart, coilsQuantity, 28 | discreteAddrStart, discreteQuantity, 29 | inputAddrStart, inputQuantity, 30 | holdingAddrStart, holdingQuantity uint16) *NodeRegister { 31 | coilsBytes := (int(coilsQuantity) + 7) / 8 32 | discreteBytes := (int(discreteQuantity) + 7) / 8 33 | 34 | b := make([]byte, coilsBytes+discreteBytes) 35 | w := make([]uint16, int(inputQuantity)+int(holdingQuantity)) 36 | return &NodeRegister{ 37 | slaveID: slaveID, 38 | coilsAddrStart: coilsAddrStart, 39 | coilsQuantity: coilsQuantity, 40 | coils: b[:coilsBytes], 41 | discreteAddrStart: discreteAddrStart, 42 | discreteQuantity: discreteQuantity, 43 | discrete: b[coilsBytes:], 44 | inputAddrStart: inputAddrStart, 45 | input: w[:inputQuantity], 46 | holdingAddrStart: holdingAddrStart, 47 | holding: w[inputQuantity:], 48 | } 49 | } 50 | 51 | // SlaveID 获取从站地址 52 | func (sf *NodeRegister) SlaveID() byte { 53 | sf.rw.RLock() 54 | id := sf.slaveID 55 | sf.rw.RUnlock() 56 | return id 57 | } 58 | 59 | // SetSlaveID 更改从站地址 60 | func (sf *NodeRegister) SetSlaveID(id byte) *NodeRegister { 61 | sf.rw.Lock() 62 | sf.slaveID = id 63 | sf.rw.Unlock() 64 | return sf 65 | } 66 | 67 | // CoilsAddrParam 读coil起始地址与数量 68 | func (sf *NodeRegister) CoilsAddrParam() (start, quantity uint16) { 69 | return sf.coilsAddrStart, sf.coilsQuantity 70 | } 71 | 72 | // DiscreteParam 读discrete起始地址与数量 73 | func (sf *NodeRegister) DiscreteParam() (start, quantity uint16) { 74 | return sf.discreteAddrStart, sf.discreteQuantity 75 | } 76 | 77 | // InputAddrParam 读input起始地址与数量 78 | func (sf *NodeRegister) InputAddrParam() (start, quantity uint16) { 79 | return sf.inputAddrStart, uint16(len(sf.input)) 80 | } 81 | 82 | // HoldingAddrParam 读holding起始地址与数量 83 | func (sf *NodeRegister) HoldingAddrParam() (start, quantity uint16) { 84 | return sf.holdingAddrStart, uint16(len(sf.holding)) 85 | } 86 | 87 | // getBits 读取切片的位的值, nBits <= 8, nBits + start <= len(buf)*8 88 | func getBits(buf []byte, start, nBits uint16) uint8 { 89 | byteOffset := start / 8 // 计算字节偏移量 90 | preBits := start - byteOffset*8 // 有多少个位需要设置 91 | 92 | mask := (uint16(1) << nBits) - 1 // 准备一个掩码来设置新的位 93 | word := uint16(buf[byteOffset]) // 复制到临时存储 94 | if preBits+nBits > 8 { 95 | word |= uint16(buf[byteOffset+1]) << 8 96 | } 97 | word >>= preBits // 抛弃不用的位 98 | word &= mask 99 | return uint8(word) 100 | } 101 | 102 | // setBits 设置切片的位的值, nBits <= 8, nBits + start <= len(buf)*8 103 | func setBits(buf []byte, start, nBits uint16, value byte) { 104 | byteOffset := start / 8 // 计算字节偏移量 105 | preBits := start - byteOffset*8 // 有多少个位需要设置 106 | newValue := uint16(value) << preBits // 移到要设置的位的位置 107 | mask := uint16((1 << nBits) - 1) // 准备一个掩码来设置新的位 108 | mask <<= preBits 109 | newValue &= mask 110 | word := uint16(buf[byteOffset]) // 复制到临时存储 111 | if (preBits + nBits) > 8 { 112 | word |= uint16(buf[byteOffset+1]) << 8 113 | } 114 | 115 | word = (word & (^mask)) | newValue // 要写的位置清零 116 | buf[byteOffset] = uint8(word & 0xFF) // 写回到存储中 117 | if (preBits + nBits) > 8 { 118 | buf[byteOffset+1] = uint8(word >> 8) 119 | } 120 | } 121 | 122 | // WriteCoils 写线圈 123 | func (sf *NodeRegister) WriteCoils(address, quality uint16, valBuf []byte) error { 124 | sf.rw.Lock() 125 | if len(valBuf)*8 >= int(quality) && (address >= sf.coilsAddrStart) && 126 | ((address + quality) <= (sf.coilsAddrStart + sf.coilsQuantity)) { 127 | start := address - sf.coilsAddrStart 128 | nCoils := int16(quality) 129 | for idx := 0; nCoils > 0; idx++ { 130 | num := nCoils 131 | if nCoils > 8 { 132 | num = 8 133 | } 134 | setBits(sf.coils, start, uint16(num), valBuf[idx]) 135 | start += 8 136 | nCoils -= 8 137 | } 138 | sf.rw.Unlock() 139 | return nil 140 | } 141 | sf.rw.Unlock() 142 | return &ExceptionError{ExceptionCodeIllegalDataAddress} 143 | } 144 | 145 | // WriteSingleCoil 写单个线圈 146 | func (sf *NodeRegister) WriteSingleCoil(address uint16, val bool) error { 147 | newVal := byte(0) 148 | if val { 149 | newVal = 1 150 | } 151 | return sf.WriteCoils(address, 1, []byte{newVal}) 152 | } 153 | 154 | // ReadCoils 读线圈,返回值 155 | func (sf *NodeRegister) ReadCoils(address, quality uint16) ([]byte, error) { 156 | sf.rw.RLock() 157 | if (address >= sf.coilsAddrStart) && 158 | ((address + quality) <= (sf.coilsAddrStart + sf.coilsQuantity)) { 159 | start := address - sf.coilsAddrStart 160 | nCoils := int16(quality) 161 | result := make([]byte, 0, (quality+7)/8) 162 | for ; nCoils > 0; nCoils -= 8 { 163 | num := nCoils 164 | if nCoils > 8 { 165 | num = 8 166 | } 167 | result = append(result, getBits(sf.coils, start, uint16(num))) 168 | start += 8 169 | } 170 | sf.rw.RUnlock() 171 | return result, nil 172 | } 173 | sf.rw.RUnlock() 174 | return nil, &ExceptionError{ExceptionCodeIllegalDataAddress} 175 | } 176 | 177 | // ReadSingleCoil 读单个线圈 178 | func (sf *NodeRegister) ReadSingleCoil(address uint16) (bool, error) { 179 | v, err := sf.ReadCoils(address, 1) 180 | if err != nil { 181 | return false, err 182 | } 183 | return v[0] > 0, nil 184 | } 185 | 186 | // WriteDiscretes 写离散量 187 | func (sf *NodeRegister) WriteDiscretes(address, quality uint16, valBuf []byte) error { 188 | sf.rw.Lock() 189 | if len(valBuf)*8 >= int(quality) && (address >= sf.discreteAddrStart) && 190 | ((address + quality) <= (sf.discreteAddrStart + sf.discreteQuantity)) { 191 | start := address - sf.discreteAddrStart 192 | nCoils := int16(quality) 193 | for idx := 0; nCoils > 0; idx++ { 194 | num := nCoils 195 | if nCoils > 8 { 196 | num = 8 197 | } 198 | setBits(sf.discrete, start, uint16(num), valBuf[idx]) 199 | start += 8 200 | nCoils -= 8 201 | } 202 | sf.rw.Unlock() 203 | return nil 204 | } 205 | sf.rw.Unlock() 206 | return &ExceptionError{ExceptionCodeIllegalDataAddress} 207 | } 208 | 209 | // WriteSingleDiscrete 写单个离散量 210 | func (sf *NodeRegister) WriteSingleDiscrete(address uint16, val bool) error { 211 | newVal := byte(0) 212 | if val { 213 | newVal = 1 214 | } 215 | return sf.WriteDiscretes(address, 1, []byte{newVal}) 216 | } 217 | 218 | // ReadDiscretes 读离散量 219 | func (sf *NodeRegister) ReadDiscretes(address, quality uint16) ([]byte, error) { 220 | sf.rw.RLock() 221 | if (address >= sf.discreteAddrStart) && 222 | ((address + quality) <= (sf.discreteAddrStart + sf.discreteQuantity)) { 223 | start := address - sf.discreteAddrStart 224 | nCoils := int16(quality) 225 | result := make([]byte, 0, (quality+7)/8) 226 | for ; nCoils > 0; nCoils -= 8 { 227 | num := nCoils 228 | if nCoils > 8 { 229 | num = 8 230 | } 231 | result = append(result, getBits(sf.discrete, start, uint16(num))) 232 | start += 8 233 | } 234 | sf.rw.RUnlock() 235 | return result, nil 236 | } 237 | sf.rw.RUnlock() 238 | return nil, &ExceptionError{ExceptionCodeIllegalDataAddress} 239 | } 240 | 241 | // ReadSingleDiscrete 读单个离散量 242 | func (sf *NodeRegister) ReadSingleDiscrete(address uint16) (bool, error) { 243 | v, err := sf.ReadDiscretes(address, 1) 244 | if err != nil { 245 | return false, err 246 | } 247 | return v[0] > 0, nil 248 | } 249 | 250 | // WriteHoldingsBytes 写保持寄存器 251 | func (sf *NodeRegister) WriteHoldingsBytes(address, quality uint16, valBuf []byte) error { 252 | sf.rw.Lock() 253 | if len(valBuf) == int(quality*2) && 254 | (address >= sf.holdingAddrStart) && 255 | ((address + quality) <= (sf.holdingAddrStart + uint16(len(sf.holding)))) { 256 | start := address - sf.holdingAddrStart 257 | end := start + quality 258 | buf := bytes.NewBuffer(valBuf) 259 | err := binary.Read(buf, binary.BigEndian, sf.holding[start:end]) 260 | sf.rw.Unlock() 261 | if err != nil { 262 | return &ExceptionError{ExceptionCodeServerDeviceFailure} 263 | } 264 | return nil 265 | } 266 | sf.rw.Unlock() 267 | return &ExceptionError{ExceptionCodeIllegalDataAddress} 268 | } 269 | 270 | // WriteHoldings 写保持寄存器 271 | func (sf *NodeRegister) WriteHoldings(address uint16, valBuf []uint16) error { 272 | quality := uint16(len(valBuf)) 273 | sf.rw.Lock() 274 | if (address >= sf.holdingAddrStart) && 275 | ((address + quality) <= (sf.holdingAddrStart + uint16(len(sf.holding)))) { 276 | start := address - sf.holdingAddrStart 277 | end := start + quality 278 | copy(sf.holding[start:end], valBuf) 279 | sf.rw.Unlock() 280 | return nil 281 | } 282 | sf.rw.Unlock() 283 | return &ExceptionError{ExceptionCodeIllegalDataAddress} 284 | } 285 | 286 | // ReadHoldingsBytes 读保持寄存器,仅返回寄存器值 287 | func (sf *NodeRegister) ReadHoldingsBytes(address, quality uint16) ([]byte, error) { 288 | sf.rw.RLock() 289 | if (address >= sf.holdingAddrStart) && 290 | ((address + quality) <= (sf.holdingAddrStart + uint16(len(sf.holding)))) { 291 | start := address - sf.holdingAddrStart 292 | end := start + quality 293 | buf := new(bytes.Buffer) 294 | err := binary.Write(buf, binary.BigEndian, sf.holding[start:end]) 295 | sf.rw.RUnlock() 296 | if err != nil { 297 | return nil, &ExceptionError{ExceptionCodeServerDeviceFailure} 298 | } 299 | return buf.Bytes(), nil 300 | } 301 | sf.rw.RUnlock() 302 | return nil, &ExceptionError{ExceptionCodeIllegalDataAddress} 303 | } 304 | 305 | // ReadHoldings 读保持寄存器,仅返回寄存器值 306 | func (sf *NodeRegister) ReadHoldings(address, quality uint16) ([]uint16, error) { 307 | sf.rw.RLock() 308 | if (address >= sf.holdingAddrStart) && 309 | ((address + quality) <= (sf.holdingAddrStart + uint16(len(sf.holding)))) { 310 | start := address - sf.holdingAddrStart 311 | end := start + quality 312 | result := make([]uint16, quality) 313 | copy(result, sf.holding[start:end]) 314 | sf.rw.RUnlock() 315 | return result, nil 316 | } 317 | sf.rw.RUnlock() 318 | return nil, &ExceptionError{ExceptionCodeIllegalDataAddress} 319 | } 320 | 321 | // WriteInputsBytes 写输入寄存器 322 | func (sf *NodeRegister) WriteInputsBytes(address, quality uint16, regBuf []byte) error { 323 | sf.rw.Lock() 324 | if len(regBuf) == int(quality*2) && 325 | (address >= sf.inputAddrStart) && 326 | ((address + quality) <= (sf.inputAddrStart + uint16(len(sf.input)))) { 327 | start := address - sf.inputAddrStart 328 | end := start + quality 329 | buf := bytes.NewBuffer(regBuf) 330 | err := binary.Read(buf, binary.BigEndian, sf.input[start:end]) 331 | sf.rw.Unlock() 332 | if err != nil { 333 | return &ExceptionError{ExceptionCodeServerDeviceFailure} 334 | } 335 | return nil 336 | } 337 | sf.rw.Unlock() 338 | return &ExceptionError{ExceptionCodeIllegalDataAddress} 339 | } 340 | 341 | // WriteInputs 写输入寄存器 342 | func (sf *NodeRegister) WriteInputs(address uint16, valBuf []uint16) error { 343 | quality := uint16(len(valBuf)) 344 | sf.rw.Lock() 345 | if (address >= sf.inputAddrStart) && 346 | ((address + quality) <= (sf.inputAddrStart + uint16(len(sf.input)))) { 347 | start := address - sf.inputAddrStart 348 | end := start + quality 349 | copy(sf.input[start:end], valBuf) 350 | sf.rw.Unlock() 351 | return nil 352 | } 353 | sf.rw.Unlock() 354 | return &ExceptionError{ExceptionCodeIllegalDataAddress} 355 | } 356 | 357 | // ReadInputsBytes 读输入寄存器 358 | func (sf *NodeRegister) ReadInputsBytes(address, quality uint16) ([]byte, error) { 359 | sf.rw.RLock() 360 | if (address >= sf.inputAddrStart) && 361 | ((address + quality) <= (sf.inputAddrStart + uint16(len(sf.input)))) { 362 | start := address - sf.inputAddrStart 363 | end := start + quality 364 | buf := new(bytes.Buffer) 365 | err := binary.Write(buf, binary.BigEndian, sf.input[start:end]) 366 | sf.rw.RUnlock() 367 | if err != nil { 368 | return nil, &ExceptionError{ExceptionCodeServerDeviceFailure} 369 | } 370 | return buf.Bytes(), nil 371 | } 372 | sf.rw.RUnlock() 373 | return nil, &ExceptionError{ExceptionCodeIllegalDataAddress} 374 | } 375 | 376 | // ReadInputs 读输入寄存器 377 | func (sf *NodeRegister) ReadInputs(address, quality uint16) ([]uint16, error) { 378 | sf.rw.RLock() 379 | if (address >= sf.inputAddrStart) && 380 | ((address + quality) <= (sf.inputAddrStart + uint16(len(sf.input)))) { 381 | start := address - sf.inputAddrStart 382 | end := start + quality 383 | result := make([]uint16, quality) 384 | copy(result, sf.input[start:end]) 385 | sf.rw.RUnlock() 386 | return result, nil 387 | } 388 | sf.rw.RUnlock() 389 | return nil, &ExceptionError{ExceptionCodeIllegalDataAddress} 390 | } 391 | 392 | // MaskWriteHolding 屏蔽写保持寄存器 (val & andMask) | (orMask & ^andMask) 393 | func (sf *NodeRegister) MaskWriteHolding(address, andMask, orMask uint16) error { 394 | sf.rw.Lock() 395 | if (address >= sf.holdingAddrStart) && 396 | ((address + 1) <= (sf.holdingAddrStart + uint16(len(sf.holding)))) { 397 | sf.holding[address] &= andMask 398 | sf.holding[address] |= orMask & ^andMask 399 | sf.rw.Unlock() 400 | return nil 401 | } 402 | sf.rw.Unlock() 403 | return &ExceptionError{ExceptionCodeIllegalDataAddress} 404 | } 405 | -------------------------------------------------------------------------------- /register_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | bitQuantity = 16 11 | wordQuantity = 3 12 | ) 13 | 14 | func newNodeReg() *NodeRegister { 15 | return &NodeRegister{ 16 | slaveID: 0x01, 17 | coilsAddrStart: 0, 18 | coilsQuantity: bitQuantity, 19 | coils: []byte{0x55, 0xaa}, 20 | discreteAddrStart: 0, 21 | discreteQuantity: bitQuantity, 22 | discrete: []byte{0xaa, 0x55}, 23 | inputAddrStart: 0, 24 | input: []uint16{0x9012, 0x1234, 0x5678}, 25 | holdingAddrStart: 0, 26 | holding: []uint16{0x1234, 0x5678, 0x9012}, 27 | } 28 | } 29 | 30 | var readReg = newNodeReg() 31 | 32 | func TestNewNodeRegister(t *testing.T) { 33 | type args struct { 34 | slaveID byte 35 | coilsAddrStart uint16 36 | coilsQuantity uint16 37 | discreteAddrStart uint16 38 | discreteQuantity uint16 39 | inputAddrStart uint16 40 | inputQuantity uint16 41 | holdingAddrStart uint16 42 | holdingQuantity uint16 43 | } 44 | tests := []struct { 45 | name string 46 | args args 47 | want *NodeRegister 48 | }{ 49 | {"new node register", args{ 50 | slaveID: 0x01, 51 | coilsAddrStart: 0, 52 | coilsQuantity: 10, 53 | discreteAddrStart: 0, 54 | discreteQuantity: 10, 55 | inputAddrStart: 0, 56 | inputQuantity: 10, 57 | holdingAddrStart: 0, 58 | holdingQuantity: 10, 59 | }, &NodeRegister{ 60 | slaveID: 0x01, 61 | coilsAddrStart: 0, 62 | coilsQuantity: 10, 63 | coils: make([]byte, 2), 64 | discreteAddrStart: 0, 65 | discreteQuantity: 10, 66 | discrete: make([]byte, 2), 67 | inputAddrStart: 0, 68 | input: make([]uint16, 10), 69 | holdingAddrStart: 0, 70 | holding: make([]uint16, 10), 71 | }}, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | got := NewNodeRegister(tt.args.slaveID, 76 | tt.args.coilsAddrStart, tt.args.coilsQuantity, 77 | tt.args.discreteAddrStart, tt.args.discreteQuantity, 78 | tt.args.inputAddrStart, tt.args.inputQuantity, 79 | tt.args.holdingAddrStart, tt.args.holdingQuantity) 80 | if !reflect.DeepEqual(got, tt.want) { 81 | t.Errorf("NewNodeRegister() = %v, want %v", got, tt.want) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func TestNodeRegister_SlaveID(t *testing.T) { 88 | tests := []struct { 89 | name string 90 | this *NodeRegister 91 | want uint8 92 | }{ 93 | { 94 | "slave ID same", 95 | &NodeRegister{slaveID: 0x01}, 96 | 0x01, 97 | }, 98 | } 99 | for _, tt := range tests { 100 | t.Run(tt.name, func(t *testing.T) { 101 | if got := tt.this.SlaveID(); got != tt.want { 102 | t.Errorf("NodeRegister.SlaveID() = %#v, want %#v", got, tt.want) 103 | } 104 | }) 105 | } 106 | } 107 | 108 | func TestNodeRegister_SetSlaveID(t *testing.T) { 109 | tests := []struct { 110 | name string 111 | this *NodeRegister 112 | want byte 113 | }{ 114 | {"", &NodeRegister{}, 0x02}, 115 | } 116 | for _, tt := range tests { 117 | t.Run(tt.name, func(t *testing.T) { 118 | tt.this.SetSlaveID(tt.want) 119 | if tt.this.slaveID != tt.want { 120 | t.Errorf("NodeRegister.SetSlaveID() = got %#v, want %#v", tt.this.slaveID, tt.want) 121 | } 122 | }) 123 | } 124 | } 125 | func Test_getBits(t *testing.T) { 126 | type args struct { 127 | buf []byte 128 | start uint16 129 | nBits uint16 130 | } 131 | tests := []struct { 132 | name string 133 | args args 134 | want uint8 135 | }{ 136 | {"获取0-8位,共8个", args{[]byte{0xaa, 0x5}, 0, 8}, 0xaa}, 137 | {"获取0-4位,共4个", args{[]byte{0xaa, 0x55}, 0, 4}, 0x0a}, 138 | {"获取4-8位,共4个", args{[]byte{0xaa, 0x55}, 4, 4}, 0x0a}, 139 | {"获取4-12位,共4个", args{[]byte{0xaa, 0x55}, 4, 8}, 0x5a}, 140 | {"获取7-9位,共3个", args{[]byte{0xaa, 0x55}, 7, 3}, 0x03}, 141 | {"获取9-16位,共7个", args{[]byte{0xaa, 0x55}, 9, 7}, 0x2a}, 142 | } 143 | for _, tt := range tests { 144 | t.Run(tt.name, func(t *testing.T) { 145 | if got := getBits(tt.args.buf, tt.args.start, tt.args.nBits); got != tt.want { 146 | t.Errorf("getBits() = %#v, want %#v", got, tt.want) 147 | } 148 | }) 149 | } 150 | } 151 | 152 | func Test_setBits(t *testing.T) { 153 | type args struct { 154 | buf []byte 155 | start uint16 156 | nBits uint16 157 | value byte 158 | } 159 | tests := []struct { 160 | name string 161 | args args 162 | want []byte 163 | }{ 164 | {"设置0-8位,共8个", args{[]byte{0x00, 0x00}, 0, 8, 0xaa}, []byte{0xaa, 0x00}}, 165 | {"设置0-4位,共4个", args{[]byte{0x00, 0x00}, 0, 4, 0x0a}, []byte{0x0a, 0x00}}, 166 | {"设置4-12位,共8个", args{[]byte{0x00, 0x00}, 4, 8, 0xaa}, []byte{0xa0, 0x0a}}, 167 | {"设置1位,共1个", args{[]byte{0x00, 0x00}, 1, 1, 0xff}, []byte{0x02, 0x00}}, 168 | {"设置9-16位,共7个", args{[]byte{0x00, 0x00}, 9, 7, 0xff}, []byte{0x00, 0xfe}}, 169 | {"设置7-9位,共3个", args{[]byte{0x00, 0x00}, 7, 3, 0xff}, []byte{0x80, 0x03}}, 170 | } 171 | for _, tt := range tests { 172 | t.Run(tt.name, func(t *testing.T) { 173 | setBits(tt.args.buf, tt.args.start, tt.args.nBits, tt.args.value) 174 | if !bytes.Equal(tt.args.buf, tt.want) { 175 | t.Errorf("setBits() = %#v, want %#v", tt.args.buf, tt.want) 176 | } 177 | }) 178 | } 179 | } 180 | 181 | func TestNodeRegister_WriteCoils(t *testing.T) { 182 | type args struct { 183 | address uint16 184 | quality uint16 185 | valBuf []byte 186 | } 187 | tests := []struct { 188 | name string 189 | this *NodeRegister 190 | args args 191 | want []byte 192 | wantErr bool 193 | }{ 194 | {"超始地址超范围", newNodeReg(), args{address: bitQuantity + 1}, nil, true}, 195 | {"数量超范围", newNodeReg(), args{quality: bitQuantity + 1}, nil, true}, 196 | {"可读地址超范围", newNodeReg(), args{address: 1, quality: bitQuantity}, nil, true}, 197 | {"写8位", newNodeReg(), 198 | args{address: 4, quality: 8, valBuf: []byte{0xff}}, []byte{0xf5, 0xaf}, false}, 199 | {"写10位", newNodeReg(), 200 | args{address: 4, quality: 10, valBuf: []byte{0xff, 0xff}}, []byte{0xf5, 0xbf}, false}, 201 | } 202 | for _, tt := range tests { 203 | t.Run(tt.name, func(t *testing.T) { 204 | if err := tt.this.WriteCoils(tt.args.address, tt.args.quality, tt.args.valBuf); (err != nil) != tt.wantErr { 205 | t.Errorf("NodeRegister.WriteCoils() error = %v, wantErr %v", err, tt.wantErr) 206 | } 207 | if !tt.wantErr && !reflect.DeepEqual(tt.this.coils, tt.want) { 208 | t.Errorf("NodeRegister.WriteCoils() got = %#v, want %#v", tt.this.coils, tt.want) 209 | } 210 | }) 211 | } 212 | } 213 | 214 | func TestNodeRegister_WriteSingleCoil(t *testing.T) { 215 | type args struct { 216 | address uint16 217 | val bool 218 | } 219 | tests := []struct { 220 | name string 221 | this *NodeRegister 222 | args args 223 | want []byte 224 | wantErr bool 225 | }{ 226 | {"写false", newNodeReg(), args{2, false}, []byte{0x51, 0xaa}, false}, 227 | {"写true", newNodeReg(), args{1, true}, []byte{0x57, 0xaa}, false}, 228 | } 229 | for _, tt := range tests { 230 | t.Run(tt.name, func(t *testing.T) { 231 | if err := tt.this.WriteSingleCoil(tt.args.address, tt.args.val); (err != nil) != tt.wantErr { 232 | t.Errorf("NodeRegister.WriteSingleCoil() error = %v, wantErr %v", err, tt.wantErr) 233 | } 234 | if !reflect.DeepEqual(tt.this.coils, tt.want) { 235 | t.Errorf("NodeRegister.WriteSingleCoil() got = %#v, want %#v", tt.this.coils, tt.want) 236 | } 237 | }) 238 | } 239 | } 240 | 241 | func TestNodeRegister_ReadCoils(t *testing.T) { 242 | type args struct { 243 | address uint16 244 | quality uint16 245 | } 246 | tests := []struct { 247 | name string 248 | this *NodeRegister 249 | args args 250 | want []byte 251 | wantErr bool 252 | }{ 253 | {"超始地址超范围", readReg, args{address: bitQuantity + 1}, nil, true}, 254 | {"数量超范围", readReg, args{quality: bitQuantity + 1}, nil, true}, 255 | {"可读地址超范围", readReg, args{address: 1, quality: bitQuantity}, nil, true}, 256 | {"读8位", readReg, args{address: 4, quality: 8}, []byte{0xa5}, false}, 257 | {"读10位", readReg, args{address: 4, quality: 10}, []byte{0xa5, 0x02}, false}, 258 | } 259 | for _, tt := range tests { 260 | t.Run(tt.name, func(t *testing.T) { 261 | got, err := tt.this.ReadCoils(tt.args.address, tt.args.quality) 262 | if (err != nil) != tt.wantErr { 263 | t.Errorf("NodeRegister.ReadCoils() error = %v, wantErr %v", err, tt.wantErr) 264 | return 265 | } 266 | if !reflect.DeepEqual(got, tt.want) { 267 | t.Errorf("NodeRegister.ReadCoils() = %#v, want %#v", got, tt.want) 268 | } 269 | }) 270 | } 271 | } 272 | 273 | func TestNodeRegister_ReadSingleCoil(t *testing.T) { 274 | type args struct { 275 | address uint16 276 | } 277 | tests := []struct { 278 | name string 279 | this *NodeRegister 280 | args args 281 | want bool 282 | wantErr bool 283 | }{ 284 | {"读false", readReg, args{5}, false, false}, 285 | {"读true", readReg, args{6}, true, false}, 286 | {"超地址", readReg, args{bitQuantity}, false, true}, 287 | } 288 | for _, tt := range tests { 289 | t.Run(tt.name, func(t *testing.T) { 290 | got, err := tt.this.ReadSingleCoil(tt.args.address) 291 | if (err != nil) != tt.wantErr { 292 | t.Errorf("NodeRegister.ReadSingleCoil() error = %v, wantErr %v", err, tt.wantErr) 293 | return 294 | } 295 | if got != tt.want { 296 | t.Errorf("NodeRegister.ReadSingleCoil() = %v, want %v", got, tt.want) 297 | } 298 | }) 299 | } 300 | } 301 | 302 | func TestNodeRegister_WriteDiscretes(t *testing.T) { 303 | type args struct { 304 | address uint16 305 | quality uint16 306 | valBuf []byte 307 | } 308 | tests := []struct { 309 | name string 310 | this *NodeRegister 311 | args args 312 | want []byte 313 | wantErr bool 314 | }{ 315 | {"超始地址超范围", newNodeReg(), args{address: bitQuantity + 1}, nil, true}, 316 | {"数量超范围", newNodeReg(), args{quality: bitQuantity + 1}, nil, true}, 317 | {"可读地址超范围", newNodeReg(), args{address: 1, quality: bitQuantity}, nil, true}, 318 | {"写8位", newNodeReg(), 319 | args{address: 4, quality: 8, valBuf: []byte{0xff}}, []byte{0xfa, 0x5f}, false}, 320 | {"写10位", newNodeReg(), 321 | args{address: 4, quality: 10, valBuf: []byte{0xff, 0xff}}, []byte{0xfa, 0x7f}, false}, 322 | } 323 | for _, tt := range tests { 324 | t.Run(tt.name, func(t *testing.T) { 325 | if err := tt.this.WriteDiscretes(tt.args.address, tt.args.quality, tt.args.valBuf); (err != nil) != tt.wantErr { 326 | t.Errorf("NodeRegister.WriteDiscretes() error = %v, wantErr %v", err, tt.wantErr) 327 | } 328 | if !tt.wantErr && !reflect.DeepEqual(tt.this.discrete, tt.want) { 329 | t.Errorf("NodeRegister.WriteDiscretes() got = %#v, want %#v", tt.this.discrete, tt.want) 330 | } 331 | }) 332 | } 333 | } 334 | 335 | func TestNodeRegister_WriteSingleDiscrete(t *testing.T) { 336 | type args struct { 337 | address uint16 338 | val bool 339 | } 340 | tests := []struct { 341 | name string 342 | this *NodeRegister 343 | args args 344 | want []byte 345 | wantErr bool 346 | }{ 347 | {"写false", newNodeReg(), args{1, false}, []byte{0xa8, 0x55}, false}, 348 | {"写true", newNodeReg(), args{2, true}, []byte{0xae, 0x55}, false}, 349 | } 350 | for _, tt := range tests { 351 | t.Run(tt.name, func(t *testing.T) { 352 | if err := tt.this.WriteSingleDiscrete(tt.args.address, tt.args.val); (err != nil) != tt.wantErr { 353 | t.Errorf("NodeRegister.WriteSingleDiscrete() error = %v, wantErr %v", err, tt.wantErr) 354 | } 355 | if !reflect.DeepEqual(tt.this.discrete, tt.want) { 356 | t.Errorf("NodeRegister.WriteSingleCoil() got = %#v, want %#v", tt.this.discrete, tt.want) 357 | } 358 | }) 359 | } 360 | } 361 | 362 | func TestNodeRegister_ReadDiscretes(t *testing.T) { 363 | type args struct { 364 | address uint16 365 | quality uint16 366 | } 367 | tests := []struct { 368 | name string 369 | this *NodeRegister 370 | args args 371 | want []byte 372 | wantErr bool 373 | }{ 374 | {"超始地址超范围", readReg, args{address: bitQuantity + 1}, nil, true}, 375 | {"数量超范围", readReg, args{quality: bitQuantity + 1}, nil, true}, 376 | {"可读地址超范围", readReg, args{address: 1, quality: bitQuantity}, nil, true}, 377 | {"读8位", readReg, args{address: 4, quality: 8}, []byte{0x5a}, false}, 378 | {"读10位", readReg, args{address: 4, quality: 10}, []byte{0x5a, 0x01}, false}, 379 | } 380 | for _, tt := range tests { 381 | t.Run(tt.name, func(t *testing.T) { 382 | got, err := tt.this.ReadDiscretes(tt.args.address, tt.args.quality) 383 | if (err != nil) != tt.wantErr { 384 | t.Errorf("NodeRegister.ReadDiscretes() error = %v, wantErr %v", err, tt.wantErr) 385 | return 386 | } 387 | if !reflect.DeepEqual(got, tt.want) { 388 | t.Errorf("NodeRegister.ReadDiscretes() = %#v, want %#v", got, tt.want) 389 | } 390 | }) 391 | } 392 | } 393 | 394 | func TestNodeRegister_ReadSingleDiscrete(t *testing.T) { 395 | type args struct { 396 | address uint16 397 | } 398 | tests := []struct { 399 | name string 400 | this *NodeRegister 401 | args args 402 | want bool 403 | wantErr bool 404 | }{ 405 | {"读false", readReg, args{5}, true, false}, 406 | {"读true", readReg, args{6}, false, false}, 407 | {"超地址", readReg, args{bitQuantity}, false, true}, 408 | } 409 | for _, tt := range tests { 410 | t.Run(tt.name, func(t *testing.T) { 411 | got, err := tt.this.ReadSingleDiscrete(tt.args.address) 412 | if (err != nil) != tt.wantErr { 413 | t.Errorf("NodeRegister.ReadSingleDiscrete() error = %v, wantErr %v", err, tt.wantErr) 414 | return 415 | } 416 | if got != tt.want { 417 | t.Errorf("NodeRegister.ReadSingleDiscrete() = %v, want %v", got, tt.want) 418 | } 419 | }) 420 | } 421 | } 422 | func TestNodeRegister_WriteHoldingsBytes(t *testing.T) { 423 | type args struct { 424 | address uint16 425 | quality uint16 426 | valBuf []byte 427 | } 428 | tests := []struct { 429 | name string 430 | this *NodeRegister 431 | args args 432 | want []uint16 433 | wantErr bool 434 | }{ 435 | {"超始地址超范围", newNodeReg(), args{address: wordQuantity + 1}, nil, true}, 436 | {"数量超范围", newNodeReg(), args{quality: wordQuantity + 1}, nil, true}, 437 | {"可读地址超范围", newNodeReg(), args{address: 1, quality: wordQuantity}, nil, true}, 438 | {"读2个寄存器", newNodeReg(), args{address: 1, quality: 2, valBuf: []byte{0x11, 0x11, 0x22, 0x22}}, []uint16{0x1234, 0x1111, 0x2222}, false}, 439 | } 440 | for _, tt := range tests { 441 | t.Run(tt.name, func(t *testing.T) { 442 | if err := tt.this.WriteHoldingsBytes(tt.args.address, tt.args.quality, tt.args.valBuf); (err != nil) != tt.wantErr { 443 | t.Errorf("NodeRegister.WriteHoldingsBytes() error = %v, wantErr %v", err, tt.wantErr) 444 | } 445 | if !tt.wantErr && !reflect.DeepEqual(tt.this.holding, tt.want) { 446 | t.Errorf("NodeRegister.WriteHoldingsBytes() got = %#v, want %#v", tt.this.holding, tt.want) 447 | } 448 | }) 449 | } 450 | } 451 | 452 | func TestNodeRegister_WriteHoldings(t *testing.T) { 453 | type args struct { 454 | address uint16 455 | valBuf []uint16 456 | } 457 | tests := []struct { 458 | name string 459 | this *NodeRegister 460 | args args 461 | want []uint16 462 | wantErr bool 463 | }{ 464 | {"超始地址超范围", newNodeReg(), args{address: wordQuantity + 1}, nil, true}, 465 | {"数量超范围", newNodeReg(), args{valBuf: make([]uint16, wordQuantity+1)}, nil, true}, 466 | {"可读地址超范围", newNodeReg(), args{address: 1, valBuf: make([]uint16, wordQuantity+1)}, nil, true}, 467 | {"写2个寄存器", newNodeReg(), args{address: 1, valBuf: []uint16{0x1111, 0x2222}}, []uint16{0x1234, 0x1111, 0x2222}, false}, 468 | } 469 | for _, tt := range tests { 470 | t.Run(tt.name, func(t *testing.T) { 471 | if err := tt.this.WriteHoldings(tt.args.address, tt.args.valBuf); (err != nil) != tt.wantErr { 472 | t.Errorf("NodeRegister.WriteHoldings() error = %v, wantErr %v", err, tt.wantErr) 473 | } 474 | if !tt.wantErr && !reflect.DeepEqual(tt.this.holding, tt.want) { 475 | t.Errorf("NodeRegister.WriteHoldings() got = %#v, want %#v", tt.this.holding, tt.want) 476 | } 477 | }) 478 | } 479 | } 480 | 481 | func TestNodeRegister_ReadHoldingsBytes(t *testing.T) { 482 | type args struct { 483 | address uint16 484 | quality uint16 485 | } 486 | tests := []struct { 487 | name string 488 | this *NodeRegister 489 | args args 490 | want []byte 491 | wantErr bool 492 | }{ 493 | {"超始地址超范围", readReg, args{address: wordQuantity + 1}, nil, true}, 494 | {"数量超范围", readReg, args{quality: wordQuantity + 1}, nil, true}, 495 | {"可读地址超范围", readReg, args{address: 1, quality: wordQuantity + 1}, nil, true}, 496 | {"读2个寄存器", readReg, args{address: 1, quality: 2}, []byte{0x56, 0x78, 0x90, 0x12}, false}, 497 | } 498 | for _, tt := range tests { 499 | t.Run(tt.name, func(t *testing.T) { 500 | got, err := tt.this.ReadHoldingsBytes(tt.args.address, tt.args.quality) 501 | if (err != nil) != tt.wantErr { 502 | t.Errorf("NodeRegister.ReadHoldingsBytes() error = %v, wantErr %v", err, tt.wantErr) 503 | return 504 | } 505 | if !reflect.DeepEqual(got, tt.want) { 506 | t.Errorf("NodeRegister.ReadHoldingsBytes() = %#v, want %#v", got, tt.want) 507 | } 508 | }) 509 | } 510 | } 511 | 512 | func TestNodeRegister_ReadHoldings(t *testing.T) { 513 | type args struct { 514 | address uint16 515 | quality uint16 516 | } 517 | tests := []struct { 518 | name string 519 | this *NodeRegister 520 | args args 521 | want []uint16 522 | wantErr bool 523 | }{ 524 | {"超始地址超范围", readReg, args{address: wordQuantity + 1}, nil, true}, 525 | {"数量超范围", readReg, args{quality: wordQuantity + 1}, nil, true}, 526 | {"可读地址超范围", readReg, args{address: 1, quality: wordQuantity + 1}, nil, true}, 527 | {"读2个寄存器", readReg, args{address: 1, quality: 2}, []uint16{0x5678, 0x9012}, false}, 528 | } 529 | for _, tt := range tests { 530 | t.Run(tt.name, func(t *testing.T) { 531 | got, err := tt.this.ReadHoldings(tt.args.address, tt.args.quality) 532 | if (err != nil) != tt.wantErr { 533 | t.Errorf("NodeRegister.ReadHoldings() error = %v, wantErr %v", err, tt.wantErr) 534 | return 535 | } 536 | if !reflect.DeepEqual(got, tt.want) { 537 | t.Errorf("NodeRegister.ReadHoldings() = %#v, want %#v", got, tt.want) 538 | } 539 | }) 540 | } 541 | } 542 | 543 | func TestNodeRegister_WriteInputsBytes(t *testing.T) { 544 | type args struct { 545 | address uint16 546 | quality uint16 547 | regBuf []byte 548 | } 549 | tests := []struct { 550 | name string 551 | this *NodeRegister 552 | args args 553 | want []uint16 554 | wantErr bool 555 | }{ 556 | {"超始地址超范围", newNodeReg(), args{address: wordQuantity + 1}, nil, true}, 557 | {"数量超范围", newNodeReg(), args{quality: wordQuantity + 1}, nil, true}, 558 | {"可读地址超范围", newNodeReg(), args{address: 1, quality: wordQuantity}, nil, true}, 559 | { 560 | "读2个寄存器", newNodeReg(), 561 | args{address: 1, quality: 2, regBuf: []byte{0x11, 0x11, 0x22, 0x22}}, 562 | []uint16{0x9012, 0x1111, 0x2222}, 563 | false, 564 | }, 565 | } 566 | for _, tt := range tests { 567 | t.Run(tt.name, func(t *testing.T) { 568 | if err := tt.this.WriteInputsBytes(tt.args.address, tt.args.quality, tt.args.regBuf); (err != nil) != tt.wantErr { 569 | t.Errorf("NodeRegister.WriteInputsBytes() error = %v, wantErr %v", err, tt.wantErr) 570 | } 571 | if !tt.wantErr && !reflect.DeepEqual(tt.this.input, tt.want) { 572 | t.Errorf("NodeRegister.WriteInputsBytes() got = %#v, want %#v", tt.this.input, tt.want) 573 | } 574 | }) 575 | } 576 | } 577 | 578 | func TestNodeRegister_WriteInputs(t *testing.T) { 579 | type args struct { 580 | address uint16 581 | valBuf []uint16 582 | } 583 | tests := []struct { 584 | name string 585 | this *NodeRegister 586 | args args 587 | want []uint16 588 | wantErr bool 589 | }{ 590 | {"超始地址超范围", newNodeReg(), args{address: wordQuantity + 1}, nil, true}, 591 | {"数量超范围", newNodeReg(), args{valBuf: make([]uint16, wordQuantity+1)}, nil, true}, 592 | {"可读地址超范围", newNodeReg(), args{address: 1, valBuf: make([]uint16, wordQuantity+1)}, nil, true}, 593 | {"写2个寄存器", newNodeReg(), args{address: 1, valBuf: []uint16{0x1111, 0x2222}}, []uint16{0x9012, 0x1111, 0x2222}, false}, 594 | } 595 | for _, tt := range tests { 596 | t.Run(tt.name, func(t *testing.T) { 597 | if err := tt.this.WriteInputs(tt.args.address, tt.args.valBuf); (err != nil) != tt.wantErr { 598 | t.Errorf("NodeRegister.WriteInputs() error = %v, wantErr %v", err, tt.wantErr) 599 | } 600 | if !tt.wantErr && !reflect.DeepEqual(tt.this.input, tt.want) { 601 | t.Errorf("NodeRegister.WriteInputs() got = %#v, want %#v", tt.this.input, tt.want) 602 | } 603 | }) 604 | } 605 | } 606 | 607 | func TestNodeRegister_ReadInputsBytes(t *testing.T) { 608 | type args struct { 609 | address uint16 610 | quality uint16 611 | } 612 | tests := []struct { 613 | name string 614 | this *NodeRegister 615 | args args 616 | want []byte 617 | wantErr bool 618 | }{ 619 | {"超始地址超范围", readReg, args{address: wordQuantity + 1}, nil, true}, 620 | {"数量超范围", readReg, args{quality: wordQuantity + 1}, nil, true}, 621 | {"可读地址超范围", readReg, args{address: 1, quality: wordQuantity + 1}, nil, true}, 622 | {"读2个寄存器", readReg, args{address: 1, quality: 2}, []byte{0x12, 0x34, 0x56, 0x78}, false}, 623 | } 624 | for _, tt := range tests { 625 | t.Run(tt.name, func(t *testing.T) { 626 | got, err := tt.this.ReadInputsBytes(tt.args.address, tt.args.quality) 627 | if (err != nil) != tt.wantErr { 628 | t.Errorf("NodeRegister.ReadInputsBytes() error = %v, wantErr %v", err, tt.wantErr) 629 | return 630 | } 631 | if !reflect.DeepEqual(got, tt.want) { 632 | t.Errorf("NodeRegister.ReadInputsBytes() = %#v, want %#v", got, tt.want) 633 | } 634 | }) 635 | } 636 | } 637 | 638 | func TestNodeRegister_ReadInputs(t *testing.T) { 639 | type args struct { 640 | address uint16 641 | quality uint16 642 | } 643 | tests := []struct { 644 | name string 645 | this *NodeRegister 646 | args args 647 | want []uint16 648 | wantErr bool 649 | }{ 650 | {"超始地址超范围", readReg, args{address: wordQuantity + 1}, nil, true}, 651 | {"数量超范围", readReg, args{quality: wordQuantity + 1}, nil, true}, 652 | {"可读地址超范围", readReg, args{address: 1, quality: wordQuantity + 1}, nil, true}, 653 | {"读2个寄存器", readReg, args{address: 1, quality: 2}, []uint16{0x1234, 0x5678}, false}, 654 | } 655 | for _, tt := range tests { 656 | t.Run(tt.name, func(t *testing.T) { 657 | got, err := tt.this.ReadInputs(tt.args.address, tt.args.quality) 658 | if (err != nil) != tt.wantErr { 659 | t.Errorf("NodeRegister.ReadInputs() error = %v, wantErr %v", err, tt.wantErr) 660 | return 661 | } 662 | if !reflect.DeepEqual(got, tt.want) { 663 | t.Errorf("NodeRegister.ReadInputs() = %#v, want %#v", got, tt.want) 664 | } 665 | }) 666 | } 667 | } 668 | 669 | func TestNodeRegister_MaskWriteHolding(t *testing.T) { 670 | nodeReg := &NodeRegister{ 671 | holdingAddrStart: 0, 672 | holding: []uint16{0x0000, 0x0012, 0x0000}, 673 | } 674 | type args struct { 675 | address uint16 676 | andMask uint16 677 | orMask uint16 678 | } 679 | tests := []struct { 680 | name string 681 | this *NodeRegister 682 | args args 683 | want uint16 684 | wantErr bool 685 | }{ 686 | {"掩码", nodeReg, args{1, 0xf2, 0x25}, 0x0017, false}, 687 | {"超始始地址", nodeReg, args{address: wordQuantity + 1}, 0x0012, true}, 688 | {"超地址范围", nodeReg, args{address: wordQuantity}, 0x0012, true}, 689 | } 690 | for _, tt := range tests { 691 | t.Run(tt.name, func(t *testing.T) { 692 | if err := tt.this.MaskWriteHolding(tt.args.address, tt.args.andMask, tt.args.orMask); (err != nil) != tt.wantErr { 693 | t.Errorf("NodeRegister.MaskWriteHolding() error = %v, wantErr %v", err, tt.wantErr) 694 | } 695 | if !tt.wantErr && tt.this.holding[int(tt.args.address)] != tt.want { 696 | t.Errorf("NodeRegister.MaskWriteHolding() got = %#v, want %#v", tt.this.holding[tt.args.address], tt.want) 697 | } 698 | }) 699 | } 700 | } 701 | 702 | func Benchmark_getBits(b *testing.B) { 703 | val := []byte{0x00, 0x02, 0x03, 0x04, 0x05} 704 | for i := 0; i < b.N; i++ { 705 | getBits(val, 1, 30) 706 | } 707 | } 708 | 709 | func Benchmark_setBits(b *testing.B) { 710 | val := []byte{0x00, 0x02, 0x03, 0x04, 0x05} 711 | for i := 0; i < b.N; i++ { 712 | setBits(val, 12, 8, 0xaa) 713 | } 714 | } 715 | -------------------------------------------------------------------------------- /revive.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | revive -config .revive.toml -formatter friendly ./... 3 | golint ./... 4 | golangci-lint run --out-format=line-number -E goimports -------------------------------------------------------------------------------- /rtuclient.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "time" 8 | ) 9 | 10 | const ( 11 | rtuExceptionSize = 5 12 | ) 13 | 14 | // RTUClientProvider implements ClientProvider interface. 15 | type RTUClientProvider struct { 16 | serialPort 17 | logger 18 | *pool 19 | } 20 | 21 | // check RTUClientProvider implements the interface ClientProvider underlying method 22 | var _ ClientProvider = (*RTUClientProvider)(nil) 23 | 24 | // request pool, all RTU client use this pool 25 | var rtuPool = newPool(rtuAduMaxSize) 26 | 27 | // NewRTUClientProvider allocates and initializes a RTUClientProvider. 28 | // it will use default /dev/ttyS0 19200 8 1 N and timeout 1000 29 | func NewRTUClientProvider(opts ...ClientProviderOption) *RTUClientProvider { 30 | p := &RTUClientProvider{ 31 | logger: newLogger("modbusRTUMaster => "), 32 | pool: rtuPool, 33 | } 34 | p.autoReconnect = SerialDefaultAutoReconnect 35 | for _, opt := range opts { 36 | opt(p) 37 | } 38 | return p 39 | } 40 | 41 | func (sf *protocolFrame) encodeRTUFrame(slaveID byte, pdu ProtocolDataUnit) ([]byte, error) { 42 | length := len(pdu.Data) + 4 43 | if length > rtuAduMaxSize { 44 | return nil, fmt.Errorf("modbus: length of data '%v' must not be bigger than '%v'", length, rtuAduMaxSize) 45 | } 46 | requestAdu := sf.adu[:0:length] 47 | requestAdu = append(requestAdu, slaveID, pdu.FuncCode) 48 | requestAdu = append(requestAdu, pdu.Data...) 49 | checksum := CRC16(requestAdu) 50 | requestAdu = append(requestAdu, byte(checksum), byte(checksum>>8)) 51 | return requestAdu, nil 52 | } 53 | 54 | // decode extracts slaveID and PDU from RTU frame and verify CRC. 55 | func decodeRTUFrame(adu []byte) (uint8, []byte, error) { 56 | if len(adu) < rtuAduMinSize { // Minimum size (including address, funcCode and CRC) 57 | return 0, nil, fmt.Errorf("modbus: response length '%v' does not meet minimum '%v'", len(adu), rtuAduMinSize) 58 | } 59 | // Calculate checksum 60 | crc, expect := CRC16(adu[:len(adu)-2]), binary.LittleEndian.Uint16(adu[len(adu)-2:]) 61 | if crc != expect { 62 | return 0, nil, fmt.Errorf("modbus: response crc '%x' does not match expected '%x'", expect, crc) 63 | } 64 | // slaveID & PDU but pass crc 65 | return adu[0], adu[1 : len(adu)-2], nil 66 | } 67 | 68 | // Send request to the remote server, it implements on SendRawFrame 69 | func (sf *RTUClientProvider) Send(slaveID byte, request ProtocolDataUnit) (ProtocolDataUnit, error) { 70 | var response ProtocolDataUnit 71 | 72 | frame := sf.pool.get() 73 | defer sf.pool.put(frame) 74 | 75 | aduRequest, err := frame.encodeRTUFrame(slaveID, request) 76 | if err != nil { 77 | return response, err 78 | } 79 | aduResponse, err := sf.SendRawFrame(aduRequest) 80 | if err != nil { 81 | return response, err 82 | } 83 | rspSlaveID, pdu, err := decodeRTUFrame(aduResponse) 84 | if err != nil { 85 | return response, err 86 | } 87 | response = ProtocolDataUnit{pdu[0], pdu[1:]} 88 | err = verify(slaveID, rspSlaveID, request, response) 89 | return response, err 90 | } 91 | 92 | // SendPdu send pdu request to the remote server 93 | func (sf *RTUClientProvider) SendPdu(slaveID byte, pduRequest []byte) ([]byte, error) { 94 | if len(pduRequest) < pduMinSize || len(pduRequest) > pduMaxSize { 95 | return nil, fmt.Errorf("modbus: pdu size '%v' must not be between '%v' and '%v'", 96 | len(pduRequest), pduMinSize, pduMaxSize) 97 | } 98 | 99 | frame := sf.pool.get() 100 | defer sf.pool.put(frame) 101 | 102 | request := ProtocolDataUnit{pduRequest[0], pduRequest[1:]} 103 | requestAdu, err := frame.encodeRTUFrame(slaveID, request) 104 | if err != nil { 105 | return nil, err 106 | } 107 | aduResponse, err := sf.SendRawFrame(requestAdu) 108 | if err != nil { 109 | return nil, err 110 | } 111 | rspSlaveID, pdu, err := decodeRTUFrame(aduResponse) 112 | if err != nil { 113 | return nil, err 114 | } 115 | response := ProtocolDataUnit{pdu[0], pdu[1:]} 116 | if err = verify(slaveID, rspSlaveID, request, response); err != nil { 117 | return nil, err 118 | } 119 | // PDU pass slaveID & crc 120 | return pdu, nil 121 | } 122 | 123 | // SendRawFrame send Adu frame 124 | func (sf *RTUClientProvider) SendRawFrame(aduRequest []byte) (aduResponse []byte, err error) { 125 | sf.mu.Lock() 126 | defer sf.mu.Unlock() 127 | 128 | // check port is connected 129 | if !sf.isConnected() { 130 | return nil, ErrClosedConnection 131 | } 132 | 133 | // Send the request 134 | sf.Debug("sending [% x]", aduRequest) 135 | var tryCnt byte 136 | for { 137 | _, err = sf.port.Write(aduRequest) 138 | if err == nil { // success 139 | break 140 | } 141 | if sf.autoReconnect == 0 { 142 | return 143 | } 144 | for { 145 | err = sf.connect() 146 | if err == nil { 147 | break 148 | } 149 | tryCnt++ 150 | if tryCnt >= sf.autoReconnect { 151 | return 152 | } 153 | } 154 | } 155 | 156 | function, functionFail := aduRequest[1], aduRequest[1]|0x80 157 | bytesToRead := calculateResponseLength(aduRequest) 158 | time.Sleep(sf.calculateDelay(len(aduRequest) + bytesToRead)) 159 | 160 | var n int 161 | var n1 int 162 | var data [rtuAduMaxSize]byte 163 | // We first read the minimum length and then read either the full package 164 | // or the error package, depending on the error status (byte 2 of the response) 165 | n, err = io.ReadAtLeast(sf.port, data[:], rtuAduMinSize) 166 | if err != nil { 167 | return 168 | } 169 | 170 | switch { 171 | case data[1] == function: 172 | // if the function is correct 173 | // we read the rest of the bytes 174 | if n < bytesToRead { 175 | if bytesToRead > rtuAduMinSize && bytesToRead <= rtuAduMaxSize { 176 | if bytesToRead > n { 177 | n1, err = io.ReadFull(sf.port, data[n:bytesToRead]) 178 | n += n1 179 | } 180 | } 181 | } 182 | case data[1] == functionFail: 183 | // for error we need to read 5 bytes 184 | if n < rtuExceptionSize { 185 | n1, err = io.ReadFull(sf.port, data[n:rtuExceptionSize]) 186 | } 187 | n += n1 188 | default: 189 | err = fmt.Errorf("modbus: unknown function code % x", data[1]) 190 | } 191 | if err != nil { 192 | return 193 | } 194 | aduResponse = data[:n] 195 | sf.Debug("received [% x]", aduResponse) 196 | return aduResponse, nil 197 | } 198 | 199 | // calculateDelay roughly calculates time needed for the next frame. 200 | // See MODBUS over Serial Line - Specification and Implementation Guide (page 13). 201 | func (sf *RTUClientProvider) calculateDelay(chars int) time.Duration { 202 | var characterDelay, frameDelay int // us 203 | 204 | if sf.BaudRate <= 0 || sf.BaudRate > 19200 { 205 | characterDelay = 750 206 | frameDelay = 1750 207 | } else { 208 | characterDelay = 15000000 / sf.BaudRate 209 | frameDelay = 35000000 / sf.BaudRate 210 | } 211 | return time.Duration(characterDelay*chars+frameDelay) * time.Microsecond 212 | } 213 | 214 | func calculateResponseLength(adu []byte) int { 215 | length := rtuAduMinSize 216 | switch adu[1] { 217 | case FuncCodeReadDiscreteInputs, 218 | FuncCodeReadCoils: 219 | count := int(binary.BigEndian.Uint16(adu[4:])) 220 | length += 1 + count/8 221 | if count%8 != 0 { 222 | length++ 223 | } 224 | case FuncCodeReadInputRegisters, 225 | FuncCodeReadHoldingRegisters, 226 | FuncCodeReadWriteMultipleRegisters: 227 | count := int(binary.BigEndian.Uint16(adu[4:])) 228 | length += 1 + count*2 229 | case FuncCodeWriteSingleCoil, 230 | FuncCodeWriteMultipleCoils, 231 | FuncCodeWriteSingleRegister, 232 | FuncCodeWriteMultipleRegisters: 233 | length += 4 234 | case FuncCodeMaskWriteRegister: 235 | length += 6 236 | case FuncCodeReadFIFOQueue: 237 | // undetermined 238 | default: 239 | } 240 | return length 241 | } 242 | 243 | // helper 244 | 245 | // verify confirms valid data(including slaveID,funcCode,response data) 246 | func verify(reqSlaveID, rspSlaveID uint8, reqPDU, rspPDU ProtocolDataUnit) error { 247 | switch { 248 | case reqSlaveID != rspSlaveID: // Check slaveID same 249 | return fmt.Errorf("modbus: response slave id '%v' does not match request '%v'", rspSlaveID, reqSlaveID) 250 | 251 | case rspPDU.FuncCode != reqPDU.FuncCode: // Check correct function code returned (exception) 252 | return responseError(rspPDU) 253 | 254 | case rspPDU.Data == nil || len(rspPDU.Data) == 0: // check Empty response 255 | return fmt.Errorf("modbus: response data is empty") 256 | } 257 | return nil 258 | } 259 | -------------------------------------------------------------------------------- /rtuclient_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestRTUClientProvider_encodeRTUFrame(t *testing.T) { 9 | type args struct { 10 | slaveID byte 11 | pdu ProtocolDataUnit 12 | } 13 | tests := []struct { 14 | name string 15 | rtu *protocolFrame 16 | args args 17 | want []byte 18 | wantErr bool 19 | }{ 20 | { 21 | "RTU encode", 22 | &protocolFrame{make([]byte, 0, rtuAduMaxSize)}, 23 | args{0x01, ProtocolDataUnit{0x03, []byte{0x01, 0x02, 0x03, 0x04, 0x05}}}, 24 | []byte{0x01, 0x03, 0x01, 0x02, 0x03, 0x04, 0x05, 0x05, 0x48}, 25 | false, 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | got, err := tt.rtu.encodeRTUFrame(tt.args.slaveID, tt.args.pdu) 31 | if (err != nil) != tt.wantErr { 32 | t.Errorf("RTUClientProvider.encode() error = %v, wantErr %v", err, tt.wantErr) 33 | return 34 | } 35 | if !reflect.DeepEqual(got, tt.want) { 36 | t.Errorf("RTUClientProvider.encode() = %v, want %v", got, tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestRTUClientProvider_decodeRTUFrame(t *testing.T) { 43 | type args struct { 44 | adu []byte 45 | } 46 | tests := []struct { 47 | name string 48 | args args 49 | slaveID uint8 50 | pdu []byte 51 | wantErr bool 52 | }{ 53 | { 54 | "RTU decode", 55 | args{[]byte{0x01, 0x03, 0x01, 0x02, 0x03, 0x04, 0x05, 0x05, 0x48}}, 56 | 0x01, 57 | []byte{0x03, 0x01, 0x02, 0x03, 0x04, 0x05}, 58 | false, 59 | }, 60 | } 61 | for _, tt := range tests { 62 | t.Run(tt.name, func(t *testing.T) { 63 | gotslaveID, gotpdu, err := decodeRTUFrame(tt.args.adu) 64 | if (err != nil) != tt.wantErr { 65 | t.Errorf("RTUClientProvider.decode() error = %v, wantErr %v", err, tt.wantErr) 66 | return 67 | } 68 | if gotslaveID != tt.slaveID { 69 | t.Errorf("RTUClientProvider.decode() gotslaveID = %v, want %v", gotslaveID, tt.slaveID) 70 | } 71 | if !reflect.DeepEqual(gotpdu, tt.pdu) { 72 | t.Errorf("RTUClientProvider.decode() gotpdu = %v, want %v", gotpdu, tt.pdu) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func Test_verify(t *testing.T) { 79 | type args struct { 80 | reqSlaveID uint8 81 | rspSlaveID uint8 82 | reqPDU ProtocolDataUnit 83 | rspPDU ProtocolDataUnit 84 | } 85 | tests := []struct { 86 | name string 87 | args args 88 | wantErr bool 89 | }{ 90 | { 91 | "serial verify same", 92 | args{ 93 | 5, 94 | 5, 95 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 96 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 97 | }, 98 | false, 99 | }, 100 | { 101 | "serial verify slaveID different", 102 | args{ 103 | 4, 104 | 5, 105 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 106 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 107 | }, 108 | true, 109 | }, 110 | { 111 | "serial verify functionCode different", 112 | args{ 113 | 5, 114 | 5, 115 | ProtocolDataUnit{11, []byte{1, 2, 3, 4}}, 116 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 117 | }, 118 | true, 119 | }, 120 | { 121 | "serial verify pdu data zero length", 122 | args{ 123 | 5, 124 | 5, 125 | ProtocolDataUnit{10, []byte{}}, 126 | ProtocolDataUnit{10, []byte{}}, 127 | }, 128 | true, 129 | }, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | err := verify(tt.args.reqSlaveID, tt.args.rspSlaveID, tt.args.reqPDU, tt.args.rspPDU) 134 | if (err != nil) != tt.wantErr { 135 | t.Errorf("verify() error = %v, wantErr %v", err, tt.wantErr) 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func Test_calculateResponseLength(t *testing.T) { 142 | type args struct { 143 | adu []byte 144 | } 145 | tests := []struct { 146 | name string 147 | args args 148 | want int 149 | }{ 150 | {"1", args{[]byte{4, 1, 0, 0xA, 0, 0xD, 0xDD, 0x98}}, 7}, 151 | {"2", args{[]byte{4, 2, 0, 0xA, 0, 0xD, 0x99, 0x98}}, 7}, 152 | {"3", args{[]byte{1, 3, 0, 0, 0, 2, 0xC4, 0xB}}, 9}, 153 | {"4", args{[]byte{0x11, 5, 0, 0xAC, 0xFF, 0, 0x4E, 0x8B}}, 8}, 154 | {"5", args{[]byte{0x11, 6, 0, 1, 0, 3, 0x9A, 0x9B}}, 8}, 155 | {"6", args{[]byte{0x11, 0xF, 0, 0x13, 0, 0xA, 2, 0xCD, 1, 0xBF, 0xB}}, 8}, 156 | {"7", args{[]byte{0x11, 0x10, 0, 1, 0, 2, 4, 0, 0xA, 1, 2, 0xC6, 0xF0}}, 8}, 157 | } 158 | for _, tt := range tests { 159 | t.Run(tt.name, func(t *testing.T) { 160 | if got := calculateResponseLength(tt.args.adu); got != tt.want { 161 | t.Errorf("calculateResponseLength() = %v, want %v", got, tt.want) 162 | } 163 | }) 164 | } 165 | } 166 | 167 | func BenchmarkRTUClientProvider_encodeRTUFrame(b *testing.B) { 168 | p := &protocolFrame{make([]byte, 0, rtuAduMaxSize)} 169 | pdu := ProtocolDataUnit{ 170 | 1, 171 | []byte{2, 3, 4, 5, 6, 7, 8, 9}, 172 | } 173 | for i := 0; i < b.N; i++ { 174 | _, err := p.encodeRTUFrame(10, pdu) 175 | if err != nil { 176 | b.Fatal(err) 177 | } 178 | } 179 | } 180 | 181 | func BenchmarkRTUClientProvider_decodeRTUFrame(b *testing.B) { 182 | adu := []byte{0x01, 0x10, 0x8A, 0x00, 0x00, 0x03, 0xAA, 0x10} 183 | for i := 0; i < b.N; i++ { 184 | _, _, err := decodeRTUFrame(adu) 185 | if err != nil { 186 | b.Fatal(err) 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /serial.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | "time" 7 | 8 | "github.com/goburrow/serial" 9 | ) 10 | 11 | const ( 12 | // SerialDefaultTimeout Serial Default timeout 13 | SerialDefaultTimeout = 1 * time.Second 14 | // SerialDefaultAutoReconnect Serial Default auto reconnect count, zero means not active. 15 | SerialDefaultAutoReconnect = 0 16 | ) 17 | 18 | // serialPort has configuration and I/O controller. 19 | type serialPort struct { 20 | // Serial port configuration. 21 | serial.Config 22 | mu sync.Mutex 23 | port io.ReadWriteCloser 24 | // if == 0 auto reconnect not active 25 | // if > 0, when disconnect,it will try to reconnect the remote 26 | // but if we active close self,it will not to reconnect 27 | autoReconnect byte 28 | } 29 | 30 | // Connect try to connect the remote server 31 | func (sf *serialPort) Connect() (err error) { 32 | sf.mu.Lock() 33 | err = sf.connect() 34 | sf.mu.Unlock() 35 | return 36 | } 37 | 38 | // Caller must hold the mutex before calling this method. 39 | func (sf *serialPort) connect() error { 40 | port, err := serial.Open(&sf.Config) 41 | if err != nil { 42 | return err 43 | } 44 | sf.port = port 45 | return nil 46 | } 47 | 48 | // IsConnected returns a bool signifying whether the client is connected or not. 49 | func (sf *serialPort) IsConnected() (b bool) { 50 | sf.mu.Lock() 51 | b = sf.isConnected() 52 | sf.mu.Unlock() 53 | return b 54 | } 55 | 56 | // Caller must hold the mutex before calling this method. 57 | func (sf *serialPort) isConnected() bool { 58 | return sf.port != nil 59 | } 60 | 61 | // SetAutoReconnect set auto reconnect count 62 | // if cnt == 0, disable auto reconnect 63 | // if cnt > 0 ,enable auto reconnect,but max 6 64 | func (sf *serialPort) SetAutoReconnect(cnt byte) { 65 | sf.mu.Lock() 66 | sf.autoReconnect = cnt 67 | if sf.autoReconnect > 6 { 68 | sf.autoReconnect = 6 69 | } 70 | sf.mu.Unlock() 71 | } 72 | 73 | // setSerialConfig set serial config 74 | func (sf *serialPort) setSerialConfig(config serial.Config) { 75 | sf.Config = config 76 | } 77 | 78 | func (sf *serialPort) setTCPTimeout(time.Duration) {} 79 | 80 | // Close close current connection. 81 | func (sf *serialPort) Close() (err error) { 82 | sf.mu.Lock() 83 | if sf.port != nil { 84 | err = sf.port.Close() 85 | sf.port = nil 86 | } 87 | sf.mu.Unlock() 88 | return 89 | } 90 | -------------------------------------------------------------------------------- /serial_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | -------------------------------------------------------------------------------- /tcp_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const ( 10 | testslaveID1 = 0x01 11 | testslaveID2 = 0x02 12 | ) 13 | 14 | func Test_TCPClientWithServer(t *testing.T) { 15 | t.Run("", func(t *testing.T) { 16 | mbSrv := NewTCPServer() 17 | mbSrv.AddNodes(NewNodeRegister(testslaveID1, 18 | 0, 10, 0, 10, 19 | 0, 10, 0, 10), 20 | NewNodeRegister(testslaveID2, 21 | 0, 10, 0, 10, 22 | 0, 10, 0, 10)) 23 | 24 | _, err := mbSrv.GetNode(testslaveID2) 25 | if err != nil { 26 | t.Errorf("GetNode(%#v) error = %v, wantErr %v", testslaveID2, err, nil) 27 | return 28 | } 29 | 30 | list := mbSrv.GetNodeList() 31 | if list == nil { 32 | t.Errorf("GetNodeList() should not nil") 33 | return 34 | } 35 | 36 | mbSrv.DeleteNode(testslaveID2) 37 | _, err = mbSrv.GetNode(testslaveID2) 38 | if err == nil { 39 | t.Errorf("GetNode(%#v) error = %v, wantErr %v", testslaveID2, err, "slaveID not exist") 40 | return 41 | } 42 | 43 | go func() { 44 | _ = mbSrv.ListenAndServe("localhost:48091") 45 | }() 46 | time.Sleep(time.Second) // wait for server start 47 | mbPro := NewTCPClientProvider("localhost:48091") 48 | mbCli := NewClient(mbPro) 49 | err = mbCli.Connect() 50 | if err != nil { 51 | t.Errorf("Connect error = %v, wantErr %v", err, nil) 52 | return 53 | } 54 | 55 | result, err := mbCli.ReadCoils(testslaveID1, 0, 10) 56 | if err != nil { 57 | t.Errorf("ReadCoils error = %v, wantErr %v", err, nil) 58 | return 59 | } 60 | 61 | if !reflect.DeepEqual(result, []byte{0x00, 0x00}) { 62 | t.Errorf("ReadCoils result = %#v, want %#v", result, []byte{0x00, 0x00}) 63 | } 64 | 65 | if !mbCli.IsConnected() { 66 | t.Errorf("client IsConnected() = %v, want %v", false, true) 67 | return 68 | } 69 | 70 | err = mbCli.Close() 71 | if err != nil { 72 | t.Errorf("client Close() error = %v, wantErr %v", err, nil) 73 | return 74 | } 75 | 76 | if mbCli.IsConnected() { 77 | t.Errorf("client IsConnected() = %v, want %v", true, false) 78 | return 79 | } 80 | 81 | err = mbSrv.Close() 82 | if err != nil { 83 | t.Errorf("server Close() error = %v, wantErr %v", err, nil) 84 | return 85 | } 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /tcpclient.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "net" 8 | "strings" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/goburrow/serial" 14 | ) 15 | 16 | const ( 17 | // TCPDefaultTimeout TCP Default timeout 18 | TCPDefaultTimeout = 1 * time.Second 19 | // TCPDefaultAutoReconnect TCP Default auto reconnect count 20 | TCPDefaultAutoReconnect = 1 21 | ) 22 | 23 | // TCPClientProvider implements ClientProvider interface. 24 | type TCPClientProvider struct { 25 | logger 26 | address string 27 | mu sync.Mutex 28 | // TCP connection 29 | conn net.Conn 30 | // Connect & Read timeout 31 | timeout time.Duration 32 | // if > 0, when disconnect,it will try to reconnect the remote 33 | // but if we active close self,it will not to reconnect 34 | // if == 0 auto reconnect not active 35 | autoReconnect byte 36 | // For synchronization between messages of server & client 37 | transactionID uint32 38 | // request 39 | *pool 40 | } 41 | 42 | // check TCPClientProvider implements the interface ClientProvider underlying method 43 | var _ ClientProvider = (*TCPClientProvider)(nil) 44 | 45 | // request pool, all TCP client use this pool 46 | var tcpPool = newPool(tcpAduMaxSize) 47 | 48 | // NewTCPClientProvider allocates a new TCPClientProvider. 49 | func NewTCPClientProvider(address string, opts ...ClientProviderOption) *TCPClientProvider { 50 | p := &TCPClientProvider{ 51 | address: address, 52 | timeout: TCPDefaultTimeout, 53 | autoReconnect: TCPDefaultAutoReconnect, 54 | pool: tcpPool, 55 | logger: newLogger("modbusTCPMaster =>"), 56 | } 57 | for _, opt := range opts { 58 | opt(p) 59 | } 60 | return p 61 | } 62 | 63 | // encode modbus application protocol header & pdu to TCP frame,return adu. 64 | // ---- MBAP header ---- 65 | // Transaction identifier: 2 bytes 66 | // Protocol identifier: 2 bytes 67 | // Length: 2 bytes 68 | // Unit identifier: 1 byte 69 | // ---- data Unit ---- 70 | // Function code: 1 byte 71 | // Data: n bytes 72 | func (sf *protocolFrame) encodeTCPFrame(tid uint16, slaveID byte, 73 | pdu ProtocolDataUnit) (protocolTCPHeader, []byte, error) { 74 | length := tcpHeaderMbapSize + 1 + len(pdu.Data) 75 | if length > tcpAduMaxSize { 76 | return protocolTCPHeader{}, nil, fmt.Errorf("modbus: length of data '%v' must not be bigger than '%v'", 77 | length, tcpAduMaxSize) 78 | } 79 | 80 | head := protocolTCPHeader{ 81 | tid, 82 | tcpProtocolIdentifier, 83 | uint16(2 + len(pdu.Data)), // sizeof(SlaveId) + sizeof(FuncCode) + Data 84 | slaveID, 85 | } 86 | 87 | // fill adu buffer 88 | adu := sf.adu[0:length] 89 | binary.BigEndian.PutUint16(adu, head.transactionID) // MBAP Transaction identifier 90 | binary.BigEndian.PutUint16(adu[2:], head.protocolID) // MBAP Protocol identifier 91 | binary.BigEndian.PutUint16(adu[4:], head.length) // MBAP Length 92 | adu[6] = head.slaveID // MBAP Unit identifier 93 | adu[tcpHeaderMbapSize] = pdu.FuncCode // PDU funcCode 94 | copy(adu[tcpHeaderMbapSize+1:], pdu.Data) // PDU data 95 | return head, adu, nil 96 | } 97 | 98 | // decode extracts tcpHeader & PDU from TCP frame: 99 | // ---- MBAP header ---- 100 | // Transaction identifier: 2 bytes 101 | // Protocol identifier: 2 bytes 102 | // Length: 2 bytes 103 | // Unit identifier: 1 byte 104 | // ---- data Unit ---- 105 | // Function : 1 byte 106 | // Data : 0 up to 252 bytes 107 | func decodeTCPFrame(adu []byte) (protocolTCPHeader, []byte, error) { 108 | if len(adu) < tcpAduMinSize { // Minimum size (including MBAP, funcCode) 109 | return protocolTCPHeader{}, nil, fmt.Errorf("modbus: response length '%v' does not meet minimum '%v'", 110 | len(adu), tcpAduMinSize) 111 | } 112 | // Read length value in the header 113 | head := protocolTCPHeader{ 114 | transactionID: binary.BigEndian.Uint16(adu), 115 | protocolID: binary.BigEndian.Uint16(adu[2:]), 116 | length: binary.BigEndian.Uint16(adu[4:]), 117 | slaveID: adu[6], 118 | } 119 | 120 | pduLength := len(adu) - tcpHeaderMbapSize 121 | if pduLength != int(head.length-1) { 122 | return head, nil, fmt.Errorf("modbus: length in response '%v' does not match pdu data length '%v'", 123 | head.length-1, pduLength) 124 | } 125 | // The first byte after header is function code 126 | return head, adu[tcpHeaderMbapSize:], nil 127 | } 128 | 129 | // verify confirms valid data 130 | func verifyTCPFrame(reqHead, rspHead protocolTCPHeader, reqPDU, rspPDU ProtocolDataUnit) error { 131 | switch { 132 | case rspHead.transactionID != reqHead.transactionID: // Check transaction ID 133 | return fmt.Errorf("modbus: response transaction id '%v' does not match request '%v'", 134 | rspHead.transactionID, reqHead.transactionID) 135 | 136 | case rspHead.protocolID != reqHead.protocolID: // Check protocol ID 137 | return fmt.Errorf("modbus: response protocol id '%v' does not match request '%v'", 138 | rspHead.protocolID, reqHead.protocolID) 139 | 140 | case rspHead.slaveID != reqHead.slaveID: // Check slaveID same 141 | return fmt.Errorf("modbus: response unit id '%v' does not match request '%v'", 142 | rspHead.slaveID, reqHead.slaveID) 143 | 144 | case rspPDU.FuncCode != reqPDU.FuncCode: // Check correct function code returned (exception) 145 | return responseError(rspPDU) 146 | 147 | case rspPDU.Data == nil || len(rspPDU.Data) == 0: // check Empty response 148 | return fmt.Errorf("modbus: response data is empty") 149 | } 150 | return nil 151 | } 152 | 153 | // Send the request to tcp and get the response 154 | func (sf *TCPClientProvider) Send(slaveID byte, request ProtocolDataUnit) (ProtocolDataUnit, error) { 155 | var response ProtocolDataUnit 156 | 157 | frame := sf.pool.get() 158 | defer sf.pool.put(frame) 159 | // add transaction id 160 | tid := uint16(atomic.AddUint32(&sf.transactionID, 1)) 161 | 162 | head, aduRequest, err := frame.encodeTCPFrame(tid, slaveID, request) 163 | if err != nil { 164 | return response, err 165 | } 166 | aduResponse, err := sf.SendRawFrame(aduRequest) 167 | if err != nil { 168 | return response, err 169 | } 170 | rspHead, pdu, err := decodeTCPFrame(aduResponse) 171 | if err != nil { 172 | return response, err 173 | } 174 | response = ProtocolDataUnit{pdu[0], pdu[1:]} 175 | err = verifyTCPFrame(head, rspHead, request, response) 176 | return response, err 177 | } 178 | 179 | // SendPdu send pdu request to the remote server 180 | func (sf *TCPClientProvider) SendPdu(slaveID byte, pduRequest []byte) ([]byte, error) { 181 | if len(pduRequest) < pduMinSize || len(pduRequest) > pduMaxSize { 182 | return nil, fmt.Errorf("modbus: rspPdu size '%v' must not be between '%v' and '%v'", 183 | len(pduRequest), pduMinSize, pduMaxSize) 184 | } 185 | 186 | frame := sf.pool.get() 187 | defer sf.pool.put(frame) 188 | // add transaction id 189 | tid := uint16(atomic.AddUint32(&sf.transactionID, 1)) 190 | 191 | request := ProtocolDataUnit{pduRequest[0], pduRequest[1:]} 192 | head, aduRequest, err := frame.encodeTCPFrame(tid, slaveID, request) 193 | if err != nil { 194 | return nil, err 195 | } 196 | aduResponse, err := sf.SendRawFrame(aduRequest) 197 | if err != nil { 198 | return nil, err 199 | } 200 | rspHead, rspPdu, err := decodeTCPFrame(aduResponse) 201 | if err != nil { 202 | return nil, err 203 | } 204 | response := ProtocolDataUnit{rspPdu[0], rspPdu[1:]} 205 | if err = verifyTCPFrame(head, rspHead, request, response); err != nil { 206 | return nil, err 207 | } 208 | // rspPdu pass tcpMBAP head 209 | return rspPdu, nil 210 | } 211 | 212 | // SendRawFrame send raw adu request frame 213 | func (sf *TCPClientProvider) SendRawFrame(aduRequest []byte) (aduResponse []byte, err error) { 214 | sf.mu.Lock() 215 | defer sf.mu.Unlock() 216 | 217 | if !sf.isConnected() { 218 | return nil, ErrClosedConnection 219 | } 220 | // Send data 221 | sf.Debug("sending [% x]", aduRequest) 222 | // Set write and read timeout 223 | var timeout time.Time 224 | var tryCnt byte 225 | for { 226 | if sf.timeout > 0 { 227 | timeout = time.Now().Add(sf.timeout) 228 | } 229 | if err = sf.conn.SetDeadline(timeout); err != nil { 230 | return nil, err 231 | } 232 | 233 | if _, err = sf.conn.Write(aduRequest); err == nil { // success 234 | break 235 | } 236 | 237 | if sf.autoReconnect == 0 { 238 | return 239 | } 240 | 241 | for { 242 | err = sf.connect() 243 | if err == nil { 244 | break 245 | } 246 | tryCnt++ 247 | if tryCnt >= sf.autoReconnect { 248 | return 249 | } 250 | } 251 | } 252 | 253 | // Read header first 254 | var data [tcpAduMaxSize]byte 255 | var cnt int 256 | var mErr error 257 | for { 258 | if sf.timeout > 0 { 259 | timeout = time.Now().Add(sf.timeout) 260 | } 261 | if err = sf.conn.SetDeadline(timeout); err != nil { 262 | return nil, err 263 | } 264 | 265 | if cnt, err = io.ReadFull(sf.conn, data[:tcpHeaderMbapSize]); err == nil { 266 | break 267 | } 268 | if sf.autoReconnect == 0 { 269 | return 270 | } 271 | mErr = err 272 | if e, ok := err.(net.Error); ok && !e.Temporary() || 273 | err != io.EOF && err != io.ErrClosedPipe || 274 | strings.Contains(err.Error(), "use of closed network connection") || 275 | cnt == 0 && err == io.EOF { 276 | for { 277 | err = sf.connect() 278 | if err == nil { 279 | break 280 | } 281 | tryCnt++ 282 | if tryCnt >= sf.autoReconnect { 283 | return 284 | } 285 | } 286 | } 287 | tryCnt++ 288 | if tryCnt >= sf.autoReconnect { 289 | err = mErr 290 | return 291 | } 292 | } 293 | // Read length, ignore transaction & protocol id (4 bytes) 294 | length := int(binary.BigEndian.Uint16(data[4:])) 295 | switch { 296 | case length <= 0: 297 | _ = sf.flush(data[:]) 298 | err = fmt.Errorf("modbus: length in response header '%v' must not be zero", length) 299 | return 300 | case length > (tcpAduMaxSize - (tcpHeaderMbapSize - 1)): 301 | _ = sf.flush(data[:]) 302 | err = fmt.Errorf("modbus: length in response header '%v' must not greater than '%v'", 303 | length, tcpAduMaxSize-tcpHeaderMbapSize+1) 304 | return 305 | } 306 | 307 | if sf.timeout > 0 { 308 | timeout = time.Now().Add(sf.timeout) 309 | } 310 | if err = sf.conn.SetDeadline(timeout); err != nil { 311 | return nil, err 312 | } 313 | 314 | // Skip unit id 315 | length += tcpHeaderMbapSize - 1 316 | if _, err = io.ReadFull(sf.conn, data[tcpHeaderMbapSize:length]); err != nil { 317 | return 318 | } 319 | aduResponse = data[:length] 320 | sf.Debug("received [% x]", aduResponse) 321 | return aduResponse, nil 322 | } 323 | 324 | // Connect establishes a new connection to the address in Address. 325 | // Connect and Close are exported so that multiple requests can be done with one session 326 | func (sf *TCPClientProvider) Connect() error { 327 | sf.mu.Lock() 328 | err := sf.connect() 329 | sf.mu.Unlock() 330 | return err 331 | } 332 | 333 | // Caller must hold the mutex before calling this method. 334 | func (sf *TCPClientProvider) connect() error { 335 | dialer := &net.Dialer{Timeout: sf.timeout} 336 | conn, err := dialer.Dial("tcp", sf.address) 337 | if err != nil { 338 | return err 339 | } 340 | sf.conn = conn 341 | return nil 342 | } 343 | 344 | // IsConnected returns a bool signifying whether 345 | // the client is connected or not. 346 | func (sf *TCPClientProvider) IsConnected() bool { 347 | sf.mu.Lock() 348 | b := sf.isConnected() 349 | sf.mu.Unlock() 350 | return b 351 | } 352 | 353 | // Caller must hold the mutex before calling this method. 354 | func (sf *TCPClientProvider) isConnected() bool { 355 | return sf.conn != nil 356 | } 357 | 358 | // SetAutoReconnect set auto reconnect retry count 359 | func (sf *TCPClientProvider) SetAutoReconnect(cnt byte) { 360 | sf.mu.Lock() 361 | sf.autoReconnect = cnt 362 | if sf.autoReconnect > 6 { 363 | sf.autoReconnect = 6 364 | } 365 | sf.mu.Unlock() 366 | } 367 | 368 | // Close closes current connection. 369 | func (sf *TCPClientProvider) Close() (err error) { 370 | sf.mu.Lock() 371 | if sf.conn != nil { 372 | err = sf.conn.Close() 373 | sf.conn = nil 374 | } 375 | sf.mu.Unlock() 376 | return 377 | } 378 | 379 | func (sf *TCPClientProvider) setSerialConfig(serial.Config) {} 380 | 381 | func (sf *TCPClientProvider) setTCPTimeout(t time.Duration) { 382 | sf.timeout = t 383 | } 384 | 385 | // flush flushes pending data in the connection, 386 | // returns io.EOF if connection is closed. 387 | func (sf *TCPClientProvider) flush(b []byte) (err error) { 388 | if err = sf.conn.SetReadDeadline(time.Now()); err != nil { 389 | return 390 | } 391 | // timeout setting will be reset when reading 392 | if _, err = sf.conn.Read(b); err != nil { 393 | // Ignore timeout error 394 | if netError, ok := err.(net.Error); ok && netError.Timeout() { 395 | err = nil 396 | } 397 | } 398 | return 399 | } 400 | -------------------------------------------------------------------------------- /tcpclient_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_protocolFrame_encodeTCPFrame(t *testing.T) { 9 | newBuffer := func() *protocolFrame { 10 | return &protocolFrame{make([]byte, 0, tcpAduMaxSize)} 11 | } 12 | 13 | type args struct { 14 | tid uint16 15 | slaveID byte 16 | pdu ProtocolDataUnit 17 | } 18 | tests := []struct { 19 | name string 20 | this *protocolFrame 21 | args args 22 | want protocolTCPHeader 23 | want1 []byte 24 | wantErr bool 25 | }{ 26 | { 27 | "TCP encode", 28 | newBuffer(), 29 | args{ 30 | 0, 31 | 0, 32 | ProtocolDataUnit{1, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}}}, 33 | protocolTCPHeader{0, 0, 11, 0}, 34 | []byte{0, 0, 0, 0, 0, 11, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 35 | false, 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | got, got1, err := tt.this.encodeTCPFrame(tt.args.tid, tt.args.slaveID, tt.args.pdu) 41 | if (err != nil) != tt.wantErr { 42 | t.Errorf("protocolFrame.encodeTCPFrame() error = %v, wantErr %v", err, tt.wantErr) 43 | return 44 | } 45 | if !reflect.DeepEqual(got, tt.want) { 46 | t.Errorf("protocolFrame.encodeTCPFrame() got = %v, want %v", got, tt.want) 47 | } 48 | if !reflect.DeepEqual(got1, tt.want1) { 49 | t.Errorf("protocolFrame.encodeTCPFrame() got1 = %v, want %v", got1, tt.want1) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | func TestTCPClientProvider_decodeTCPFrame(t *testing.T) { 56 | type args struct { 57 | adu []byte 58 | } 59 | tests := []struct { 60 | name string 61 | args args 62 | head protocolTCPHeader 63 | pdu []byte 64 | wantErr bool 65 | }{ 66 | { 67 | "TCP decode", 68 | args{[]byte{0, 0, 0, 0, 0, 11, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9}}, 69 | protocolTCPHeader{0, 0, 11, 0}, 70 | []byte{1, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 71 | false, 72 | }, 73 | } 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | gothead, gotpdu, err := decodeTCPFrame(tt.args.adu) 77 | if (err != nil) != tt.wantErr { 78 | t.Errorf("TCPClientProvider.decode() error = %v, wantErr %v", err, tt.wantErr) 79 | return 80 | } 81 | if !reflect.DeepEqual(gothead, tt.head) { 82 | t.Errorf("TCPClientProvider.decode() gothead = %v, want %v", gothead, tt.head) 83 | } 84 | if !reflect.DeepEqual(gotpdu, tt.pdu) { 85 | t.Errorf("TCPClientProvider.decode() gotpdu = %v, want %v", gotpdu, tt.pdu) 86 | } 87 | }) 88 | } 89 | } 90 | 91 | func Test_verifyTCPFrame(t *testing.T) { 92 | type args struct { 93 | reqHead protocolTCPHeader 94 | rspHead protocolTCPHeader 95 | reqPDU ProtocolDataUnit 96 | rspPDU ProtocolDataUnit 97 | } 98 | tests := []struct { 99 | name string 100 | args args 101 | wantErr bool 102 | }{ 103 | { 104 | "TCP verify same", 105 | args{ 106 | protocolTCPHeader{1, 2, 6, 4}, 107 | protocolTCPHeader{1, 2, 6, 4}, 108 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 109 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 110 | }, 111 | false, 112 | }, 113 | { 114 | "TCP verify transactionID different", 115 | args{ 116 | protocolTCPHeader{1, 2, 6, 4}, 117 | protocolTCPHeader{5, 2, 6, 4}, 118 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 119 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 120 | }, 121 | true, 122 | }, 123 | { 124 | "TCP verify protocolID different", 125 | args{ 126 | protocolTCPHeader{1, 2, 6, 4}, 127 | protocolTCPHeader{1, 5, 6, 4}, 128 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 129 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 130 | }, 131 | true, 132 | }, 133 | { 134 | "serial verify slaveID different", 135 | args{ 136 | protocolTCPHeader{1, 2, 6, 11}, 137 | protocolTCPHeader{1, 2, 6, 4}, 138 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 139 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 140 | }, 141 | true, 142 | }, 143 | { 144 | "serial verify functionCode different", 145 | args{ 146 | protocolTCPHeader{1, 2, 6, 4}, 147 | protocolTCPHeader{1, 2, 6, 4}, 148 | ProtocolDataUnit{10, []byte{1, 2, 3, 4}}, 149 | ProtocolDataUnit{11, []byte{1, 2, 3, 4}}, 150 | }, 151 | true, 152 | }, 153 | { 154 | "serial verify pdu data zero length", 155 | args{ 156 | protocolTCPHeader{1, 2, 6, 4}, 157 | protocolTCPHeader{1, 2, 6, 4}, 158 | ProtocolDataUnit{10, []byte{}}, 159 | ProtocolDataUnit{10, []byte{}}, 160 | }, 161 | true, 162 | }, 163 | } 164 | for _, tt := range tests { 165 | t.Run(tt.name, func(t *testing.T) { 166 | err := verifyTCPFrame(tt.args.reqHead, tt.args.rspHead, tt.args.reqPDU, tt.args.rspPDU) 167 | if (err != nil) != tt.wantErr { 168 | t.Errorf("verifyTCPFrame() error = %v, wantErr %v", err, tt.wantErr) 169 | } 170 | }) 171 | } 172 | } 173 | 174 | func BenchmarkTCPClientProvider_encodeTCPFrame(b *testing.B) { 175 | tcp := &protocolFrame{make([]byte, 0, tcpAduMaxSize)} 176 | pdu := ProtocolDataUnit{ 177 | 1, 178 | []byte{2, 3, 4, 5, 6, 7, 8, 9, 10}, 179 | } 180 | 181 | for i := 0; i < b.N; i++ { 182 | _, _, err := tcp.encodeTCPFrame(0, 0, pdu) 183 | if err != nil { 184 | b.Fatal(err) 185 | } 186 | } 187 | } 188 | 189 | func BenchmarkTCPClientProvider_decodeTCPFrame(b *testing.B) { 190 | adu := []byte{0, 1, 0, 0, 0, 9, 20, 1, 2, 3, 4, 5, 6, 7, 8} 191 | for i := 0; i < b.N; i++ { 192 | _, _, err := decodeTCPFrame(adu) 193 | if err != nil { 194 | b.Fatal(err) 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tcpserver.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // TCP Default read & write timeout 11 | const ( 12 | TCPDefaultReadTimeout = 60 * time.Second 13 | TCPDefaultWriteTimeout = 1 * time.Second 14 | ) 15 | 16 | // TCPServer modbus tcp server 17 | type TCPServer struct { 18 | mu sync.Mutex 19 | listen net.Listener 20 | wg sync.WaitGroup 21 | cancel context.CancelFunc 22 | readTimeout time.Duration 23 | writeTimeout time.Duration 24 | *serverCommon 25 | logger 26 | } 27 | 28 | // NewTCPServer the modbus server listening on "address:port". 29 | func NewTCPServer() *TCPServer { 30 | return &TCPServer{ 31 | readTimeout: TCPDefaultReadTimeout, 32 | writeTimeout: TCPDefaultWriteTimeout, 33 | serverCommon: newServerCommon(), 34 | logger: newLogger("modbusTCPServer => "), 35 | } 36 | } 37 | 38 | // SetReadTimeout set read timeout 39 | func (sf *TCPServer) SetReadTimeout(t time.Duration) *TCPServer { 40 | sf.readTimeout = t 41 | return sf 42 | } 43 | 44 | // SetWriteTimeout set write timeout 45 | func (sf *TCPServer) SetWriteTimeout(t time.Duration) *TCPServer { 46 | sf.writeTimeout = t 47 | return sf 48 | } 49 | 50 | // Close close the server until all server close then return 51 | func (sf *TCPServer) Close() error { 52 | sf.mu.Lock() 53 | if sf.listen != nil { 54 | sf.listen.Close() 55 | sf.cancel() 56 | sf.listen = nil 57 | } 58 | sf.mu.Unlock() 59 | sf.wg.Wait() 60 | return nil 61 | } 62 | 63 | const minTempDelay = 5 * time.Millisecond 64 | 65 | // ListenAndServe listen and server 66 | func (sf *TCPServer) ListenAndServe(addr string) error { 67 | listen, err := net.Listen("tcp", addr) 68 | if err != nil { 69 | return err 70 | } 71 | ctx, cancel := context.WithCancel(context.Background()) 72 | sf.mu.Lock() 73 | sf.listen = listen 74 | sf.cancel = cancel 75 | sf.mu.Unlock() 76 | 77 | sf.Debug("server started,and listen address: %s", addr) 78 | defer func() { 79 | sf.Close() 80 | sf.Debug("server stopped") 81 | }() 82 | var tempDelay = minTempDelay // how long to sleep on accept failure 83 | 84 | for { 85 | conn, err := listen.Accept() 86 | if err != nil { 87 | if ne, ok := err.(net.Error); ok && ne.Temporary() { 88 | tempDelay <<= 1 89 | if max := 1 * time.Second; tempDelay > max { 90 | tempDelay = max 91 | } 92 | time.Sleep(tempDelay) 93 | continue 94 | } 95 | return err 96 | } 97 | tempDelay = minTempDelay 98 | sf.wg.Add(1) 99 | go func() { 100 | sess := &ServerSession{ 101 | conn, 102 | sf.readTimeout, 103 | sf.writeTimeout, 104 | sf.serverCommon, 105 | sf.logger, 106 | } 107 | sess.running(ctx) 108 | sf.wg.Done() 109 | }() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tcpserver_session.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // ServerSession tcp server session 15 | type ServerSession struct { 16 | conn net.Conn 17 | readTimeout time.Duration 18 | writeTimeout time.Duration 19 | *serverCommon 20 | logger 21 | } 22 | 23 | // handler net conn 24 | func (sf *ServerSession) running(ctx context.Context) { 25 | var err error 26 | var bytesRead int 27 | 28 | sf.Debug("client(%v) -> server(%v) connected", sf.conn.RemoteAddr(), sf.conn.LocalAddr()) 29 | defer func() { 30 | sf.conn.Close() 31 | sf.Debug("client(%v) -> server(%v) disconnected,cause by %v", sf.conn.RemoteAddr(), sf.conn.LocalAddr(), err) 32 | }() 33 | 34 | raw := make([]byte, tcpAduMaxSize) 35 | for { 36 | select { 37 | case <-ctx.Done(): 38 | err = errors.New("server active close") 39 | return 40 | default: 41 | } 42 | 43 | adu := raw 44 | for rdCnt, length := 0, tcpHeaderMbapSize; rdCnt < length; { 45 | err = sf.conn.SetReadDeadline(time.Now().Add(sf.readTimeout)) 46 | if err != nil { 47 | return 48 | } 49 | bytesRead, err = io.ReadFull(sf.conn, adu[rdCnt:length]) 50 | if err != nil { 51 | if err != io.EOF && err != io.ErrClosedPipe || strings.Contains(err.Error(), "use of closed network connection") { 52 | return 53 | } 54 | 55 | if e, ok := err.(net.Error); ok && !e.Temporary() { 56 | return 57 | } 58 | 59 | if bytesRead == 0 && err == io.EOF { 60 | err = fmt.Errorf("remote client closed, %v", err) 61 | return 62 | } 63 | // cnt >0 do nothing 64 | // cnt == 0 && err != io.EOF continue do it next 65 | } 66 | rdCnt += bytesRead 67 | if rdCnt >= length { 68 | // check head ProtocolIdentifier 69 | if binary.BigEndian.Uint16(adu[2:]) != tcpProtocolIdentifier { 70 | rdCnt, length = 0, tcpHeaderMbapSize 71 | continue 72 | } 73 | length = int(binary.BigEndian.Uint16(adu[4:])) + tcpHeaderMbapSize - 1 74 | if rdCnt == length { 75 | if err = sf.frameHandler(adu[:length]); err != nil { 76 | return 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | // modbus 包处理 85 | func (sf *ServerSession) frameHandler(requestAdu []byte) error { 86 | defer func() { 87 | if err := recover(); err != nil { 88 | sf.Error("painc happen,%v", err) 89 | } 90 | }() 91 | 92 | sf.Debug("RX Raw[% x]", requestAdu) 93 | // got head from request adu 94 | tcpHeader := protocolTCPHeader{ 95 | binary.BigEndian.Uint16(requestAdu[0:]), 96 | binary.BigEndian.Uint16(requestAdu[2:]), 97 | binary.BigEndian.Uint16(requestAdu[4:]), 98 | requestAdu[6], 99 | } 100 | funcCode := requestAdu[7] 101 | pduData := requestAdu[8:] 102 | 103 | node, err := sf.GetNode(tcpHeader.slaveID) 104 | if err != nil { // slave id not exit, ignore it 105 | return nil 106 | } 107 | var rspPduData []byte 108 | if handle, ok := sf.function[funcCode]; ok { 109 | rspPduData, err = handle(node, pduData) 110 | } else { 111 | err = &ExceptionError{ExceptionCodeIllegalFunction} 112 | } 113 | if err != nil { 114 | funcCode |= 0x80 115 | rspPduData = []byte{err.(*ExceptionError).ExceptionCode} 116 | } 117 | 118 | // prepare responseAdu data,fill it 119 | responseAdu := requestAdu[:tcpHeaderMbapSize] 120 | binary.BigEndian.PutUint16(responseAdu[0:], tcpHeader.transactionID) 121 | binary.BigEndian.PutUint16(responseAdu[2:], tcpHeader.protocolID) 122 | binary.BigEndian.PutUint16(responseAdu[4:], uint16(2+len(rspPduData))) 123 | responseAdu[6] = tcpHeader.slaveID 124 | responseAdu = append(responseAdu, funcCode) 125 | responseAdu = append(responseAdu, rspPduData...) 126 | 127 | sf.Debug("TX Raw[% x]", responseAdu) 128 | // write response 129 | return func(b []byte) error { 130 | for wrCnt := 0; len(b) > wrCnt; { 131 | err = sf.conn.SetWriteDeadline(time.Now().Add(sf.writeTimeout)) 132 | if err != nil { 133 | return fmt.Errorf("set read deadline %v", err) 134 | } 135 | byteCount, err := sf.conn.Write(b[wrCnt:]) 136 | if err != nil { 137 | // See: https://github.com/golang/go/issues/4373 138 | if err != io.EOF && err != io.ErrClosedPipe || 139 | strings.Contains(err.Error(), "use of closed network connection") { 140 | return err 141 | } 142 | if e, ok := err.(net.Error); !ok || !e.Temporary() { 143 | return err 144 | } 145 | // temporary error may be recoverable 146 | } 147 | wrCnt += byteCount 148 | } 149 | return nil 150 | }(responseAdu) 151 | } 152 | -------------------------------------------------------------------------------- /tcpserver_special.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "math/rand" 8 | "net" 9 | "net/url" 10 | "strings" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | ) 15 | 16 | // defined default value 17 | const ( 18 | DefaultConnectTimeout = 15 * time.Second 19 | DefaultReconnectInterval = 1 * time.Minute 20 | DefaultKeepAliveInterval = 30 * time.Second 21 | ) 22 | const ( 23 | initial uint32 = iota 24 | disconnected 25 | connected 26 | ) 27 | 28 | // OnConnectHandler when connected it will be call 29 | type OnConnectHandler func(c *TCPServerSpecial) error 30 | 31 | // OnConnectionLostHandler when Connection lost it will be call 32 | type OnConnectionLostHandler func(c *TCPServerSpecial) 33 | 34 | // OnKeepAliveHandler keep alive function 35 | type OnKeepAliveHandler func(c *TCPServerSpecial) 36 | 37 | // TCPServerSpecial modbus tcp server special 38 | type TCPServerSpecial struct { 39 | ServerSession 40 | server *url.URL // 连接的服务器端 41 | TLSConfig *tls.Config 42 | rwMux sync.RWMutex 43 | status uint32 // 状态 44 | 45 | autoReconnect bool // 是否启动重连 46 | enableKeepAlive bool // 是否使能心跳包 47 | connectTimeout time.Duration // 连接超时时间 48 | reconnectInterval time.Duration // 重连间隔时间 49 | keepAliveInterval time.Duration // 心跳包间隔 50 | onConnect OnConnectHandler // 连接回调 51 | onConnectionLost OnConnectionLostHandler // 失连回调 52 | onKeepAlive OnKeepAliveHandler // 保活函数 53 | cancel context.CancelFunc // cancel 54 | } 55 | 56 | // NewTCPServerSpecial new tcp server special, default enable auto reconnect 57 | func NewTCPServerSpecial() *TCPServerSpecial { 58 | return &TCPServerSpecial{ 59 | ServerSession: ServerSession{ 60 | readTimeout: TCPDefaultReadTimeout, 61 | writeTimeout: TCPDefaultWriteTimeout, 62 | serverCommon: newServerCommon(), 63 | logger: newLogger("modbusTCPServerSpec => "), 64 | }, 65 | autoReconnect: true, 66 | enableKeepAlive: false, 67 | connectTimeout: DefaultConnectTimeout, 68 | reconnectInterval: DefaultReconnectInterval, 69 | keepAliveInterval: DefaultKeepAliveInterval, 70 | onKeepAlive: func(*TCPServerSpecial) {}, 71 | onConnect: func(*TCPServerSpecial) error { return nil }, 72 | onConnectionLost: func(*TCPServerSpecial) {}, 73 | } 74 | } 75 | 76 | // UnderlyingConn got underlying tcp conn 77 | func (sf *TCPServerSpecial) UnderlyingConn() net.Conn { 78 | return sf.conn 79 | } 80 | 81 | // SetConnectTimeout set tcp connect the host timeout 82 | func (sf *TCPServerSpecial) SetConnectTimeout(t time.Duration) *TCPServerSpecial { 83 | sf.connectTimeout = t 84 | return sf 85 | } 86 | 87 | // SetReconnectInterval set tcp reconnect the host interval when connect failed after try 88 | func (sf *TCPServerSpecial) SetReconnectInterval(t time.Duration) *TCPServerSpecial { 89 | sf.reconnectInterval = t 90 | return sf 91 | } 92 | 93 | // EnableAutoReconnect enable auto reconnect 94 | func (sf *TCPServerSpecial) EnableAutoReconnect(b bool) *TCPServerSpecial { 95 | sf.autoReconnect = b 96 | return sf 97 | } 98 | 99 | // SetTLSConfig set tls config 100 | func (sf *TCPServerSpecial) SetTLSConfig(t *tls.Config) *TCPServerSpecial { 101 | sf.TLSConfig = t 102 | return sf 103 | } 104 | 105 | // SetReadTimeout set read timeout 106 | func (sf *TCPServerSpecial) SetReadTimeout(t time.Duration) *TCPServerSpecial { 107 | sf.readTimeout = t 108 | return sf 109 | } 110 | 111 | // SetWriteTimeout set write timeout 112 | func (sf *TCPServerSpecial) SetWriteTimeout(t time.Duration) *TCPServerSpecial { 113 | sf.writeTimeout = t 114 | return sf 115 | } 116 | 117 | // SetOnConnectHandler set on connect handler 118 | func (sf *TCPServerSpecial) SetOnConnectHandler(f OnConnectHandler) *TCPServerSpecial { 119 | if f != nil { 120 | sf.onConnect = f 121 | } 122 | return sf 123 | } 124 | 125 | // SetConnectionLostHandler set connection lost handler 126 | func (sf *TCPServerSpecial) SetConnectionLostHandler(f OnConnectionLostHandler) *TCPServerSpecial { 127 | if f != nil { 128 | sf.onConnectionLost = f 129 | } 130 | return sf 131 | } 132 | 133 | // SetKeepAlive set keep alive enable, alive time and handler 134 | func (sf *TCPServerSpecial) SetKeepAlive(enable bool, t time.Duration, f OnKeepAliveHandler) *TCPServerSpecial { 135 | sf.enableKeepAlive = enable 136 | if t > 0 { 137 | sf.keepAliveInterval = t 138 | } 139 | if f != nil { 140 | sf.onKeepAlive = f 141 | } 142 | return sf 143 | } 144 | 145 | // AddRemoteServer adds a broker URI to the list of brokers to be used. 146 | // The format should be scheme://host:port 147 | // Default values for hostname is "127.0.0.1", for schema is "tcp://". 148 | // An example broker URI would look like: tcp://foobar.com:502 149 | func (sf *TCPServerSpecial) AddRemoteServer(server string) error { 150 | if len(server) > 0 && server[0] == ':' { 151 | server = "127.0.0.1" + server 152 | } 153 | if !strings.Contains(server, "://") { 154 | server = "tcp://" + server 155 | } 156 | remoteURL, err := url.Parse(server) 157 | if err != nil { 158 | return err 159 | } 160 | sf.server = remoteURL 161 | return nil 162 | } 163 | 164 | // Start start the server,and return quickly,if it nil,the server will connecting background,other failed 165 | func (sf *TCPServerSpecial) Start() error { 166 | if sf.server == nil { 167 | return errors.New("empty remote server address,add it first") 168 | } 169 | go sf.run() 170 | return nil 171 | } 172 | 173 | // 增加间隔 174 | func (sf *TCPServerSpecial) run() { 175 | var ctx context.Context 176 | 177 | sf.rwMux.Lock() 178 | if !atomic.CompareAndSwapUint32(&sf.status, initial, disconnected) { 179 | sf.rwMux.Unlock() 180 | return 181 | } 182 | ctx, sf.cancel = context.WithCancel(context.Background()) 183 | sf.rwMux.Unlock() 184 | defer func() { 185 | sf.setConnectStatus(initial) 186 | sf.Debug("tcp server special stop!") 187 | }() 188 | sf.Debug("tcp server special start!") 189 | 190 | for { 191 | select { 192 | case <-ctx.Done(): 193 | return 194 | default: 195 | } 196 | 197 | sf.Debug("connecting server %+v", sf.server) 198 | conn, err := openConnection(sf.server, sf.TLSConfig, sf.connectTimeout) 199 | if err != nil { 200 | sf.Error("connect failed, %v", err) 201 | if !sf.autoReconnect { 202 | return 203 | } 204 | time.Sleep(sf.reconnectInterval) 205 | continue 206 | } 207 | sf.Debug("connect success") 208 | sf.conn = conn 209 | if err := sf.onConnect(sf); err != nil { 210 | time.Sleep(sf.reconnectInterval) 211 | continue 212 | } 213 | 214 | stopKeepAlive := make(chan struct{}) 215 | if sf.enableKeepAlive { 216 | go func() { 217 | tick := time.NewTicker(sf.keepAliveInterval) 218 | defer tick.Stop() 219 | for { 220 | select { 221 | case <-ctx.Done(): 222 | return 223 | case <-stopKeepAlive: 224 | return 225 | case <-tick.C: 226 | sf.onKeepAlive(sf) 227 | } 228 | } 229 | }() 230 | } 231 | sf.setConnectStatus(connected) 232 | sf.running(ctx) 233 | sf.setConnectStatus(disconnected) 234 | sf.onConnectionLost(sf) 235 | close(stopKeepAlive) 236 | select { 237 | case <-ctx.Done(): 238 | return 239 | default: 240 | // 随机500ms-1s的重试,避免快速重试造成服务器许多无效连接 241 | time.Sleep(time.Millisecond * time.Duration(500+rand.Intn(500))) 242 | } 243 | } 244 | } 245 | 246 | // IsConnected check connect is online 247 | func (sf *TCPServerSpecial) IsConnected() bool { 248 | return sf.connectStatus() == connected 249 | } 250 | 251 | // IsClosed check server is closed 252 | func (sf *TCPServerSpecial) IsClosed() bool { 253 | return sf.connectStatus() == initial 254 | } 255 | 256 | // Close close the server 257 | func (sf *TCPServerSpecial) Close() error { 258 | sf.rwMux.Lock() 259 | if sf.cancel != nil { 260 | sf.cancel() 261 | } 262 | sf.rwMux.Unlock() 263 | return nil 264 | } 265 | 266 | func (sf *TCPServerSpecial) setConnectStatus(status uint32) { 267 | sf.rwMux.Lock() 268 | atomic.StoreUint32(&sf.status, status) 269 | sf.rwMux.Unlock() 270 | } 271 | 272 | func (sf *TCPServerSpecial) connectStatus() uint32 { 273 | sf.rwMux.RLock() 274 | status := atomic.LoadUint32(&sf.status) 275 | sf.rwMux.RUnlock() 276 | return status 277 | } 278 | 279 | func openConnection(uri *url.URL, tlsc *tls.Config, timeout time.Duration) (net.Conn, error) { 280 | switch uri.Scheme { 281 | case "tcp": 282 | return net.DialTimeout("tcp", uri.Host, timeout) 283 | case "ssl", "tls", "tcps": 284 | return tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", uri.Host, tlsc) 285 | } 286 | return nil, errors.New("unknown protocol") 287 | } 288 | --------------------------------------------------------------------------------