├── .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 | [](https://godoc.org/github.com/thinkgos/gomodbus)
6 | [](https://pkg.go.dev/github.com/thinkgos/gomodbus/v2?tab=doc)
7 | [](https://www.travis-ci.com/thinkgos/gomodbus)
8 | [](https://codecov.io/gh/thinkgos/gomodbus)
9 | 
10 | [](https://goreportcard.com/report/github.com/thinkgos/gomodbus)
11 | [](https://raw.githubusercontent.com/thinkgos/gomodbus/master/LICENSE)
12 | [](https://github.com/thinkgos/gomodbus/tags)
13 | [](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 | 
203 |
204 | **WeChat Pay**
205 |
206 | 
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 |
--------------------------------------------------------------------------------