├── .formatter.exs
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
└── images
│ ├── modbux-logo.png
│ └── valiot-logo-blue.png
├── config
└── config.exs
├── lib
├── helpers
│ ├── float.ex
│ ├── helper.ex
│ ├── model.ex
│ ├── request.ex
│ ├── response.ex
│ └── shared.ex
├── rtu
│ ├── framer.ex
│ ├── master.ex
│ ├── rtu.ex
│ └── slave.ex
└── tcp
│ ├── client.ex
│ ├── server
│ ├── server.ex
│ ├── server_handler.ex
│ └── server_supervisor.ex
│ └── tcp.ex
├── mix.exs
├── mix.lock
├── pull_request_template.md
└── test
├── f01_test.exs
├── f02_test.exs
├── f03_test.exs
├── f04_test.exs
├── f05_test.exs
├── f06_test.exs
├── f15_test.exs
├── f16_test.exs
├── float_test.exs
├── helper_test.exs
├── model_test.exs
├── request_test.exs
├── response_test.exs
├── rtu
├── framer.exs
├── master.exs
├── rtu_test.exs
└── slave.exs
├── tcp
├── modbus_tcp_client_test.exs
├── modbus_tcp_server_test.exs
└── tcp_test.exs
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | line_length: 110,
4 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
5 | ]
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /_build
2 | /cover
3 | /deps
4 | /doc
5 | /priv
6 | erl_crash.dump
7 | *.ez
8 | .elixir_ls
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.3.13
4 |
5 | * Modbus TCP: the protocol identifier other than Modbus (0x00, 0x00) has been relaxed and logged.
6 |
7 | ## 0.3.12
8 |
9 | * Improve bitstring logging.
10 |
11 | ## 0.3.11
12 |
13 | * Elixir 1.15 (OTP 26) warnings fixed.
14 | * Dependencies updated.
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 VALIOT
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | ***
6 |
7 |
8 |

9 |
10 |
11 |
12 | Modbux is a library for network and serial Modbus communications.
13 |
14 | This library currently supports behaviors for TCP (Client & Server) and RTU (Master & Slave) protocols.
15 |
16 | ## Index
17 |
18 | * [Features](#features)
19 |
20 | * [Installation](#installation)
21 |
22 | * [Usage](#usage)
23 | * [Modbus RTU](#Slave)
24 | * [Modbus TCP](#Server)
25 | * [Helpers](#Helpers)
26 |
27 | * [Documentation](#documentation)
28 |
29 | * [Contributing](#contributing)
30 |
31 | * [License](#License)
32 |
33 | * [TODO](#todo)
34 |
35 | ## Features
36 |
37 | The following list is the current supported protocols/behaviors and helpers:
38 |
39 | - Modbus RTU:
40 | - Master
41 | - Slave
42 | - Framer
43 |
44 | - Modbus TCP:
45 | - Client
46 | - Server
47 |
48 | - Helpers:
49 | - IEEE754 Float support
50 | - Endianess
51 |
52 |
53 | ## Installation
54 |
55 | The package can be installed by adding `modbux` to your list of dependencies in `mix.exs`:
56 |
57 | ```elixir
58 | def deps do
59 | [
60 | {:modbux, "~> 0.1.0"}
61 | ]
62 | end
63 | ```
64 |
65 | ## Usage
66 | ***
67 | ### Modbus RTU
68 |
69 | Modbus RTU is an open serial protocol derived from the Master/Slave architecture originally developed by Modicon. This protocol primarily uses an RS-232 or RS-485 serial interfaces for communications.
70 |
71 | #### Slave
72 |
73 | To start a Modbus RTU Slave process use `start_link/1`.
74 |
75 | The following options are available:
76 | - `tty` - defines the serial port to spawn the Slave.
77 | - `gen_opts` - defines extra options for the Genserver OTP configuration.
78 | - `uart_opts` - defines extra options for the UART configuration.
79 | - `model` - defines the DB initial state.
80 | - `active` - (`true` or `false`) enable/disable DB updates notifications (mailbox).
81 |
82 | The messages (when active mode is `true`) have the following form:
83 | ```elixir
84 | {:modbus_rtu, {:slave_request, payload}}
85 | ```
86 | or
87 |
88 | ```elixir
89 | {:modbus_rtu, {:slave_error, payload, reason}}
90 | ```
91 |
92 | The following are some reasons:
93 |
94 | * `:ecrc` - corrupted message (invalid crc).
95 | * `:einval` - invalid function.
96 | * `:eaddr` - invalid memory address requested.
97 |
98 | #### Model (DB)
99 |
100 | The model or data base (DB) defines the slave/server memory map, the DB is defined by the following syntax:
101 | ```elixir
102 | %{slave_id => %{{memory_type, address_number} => value}}
103 | ```
104 | where:
105 | * `slave_id` - specifies a unique unit address from 1 to 247.
106 | * `memory_type` - specifies the memory between:
107 | * `:c` - Discrete Output Coils.
108 | * `:i` - Discrete Input Contacts.
109 | * `:ir` - Analog Input Registers.
110 | * `:hr` - Analog Output Registers.
111 | * `address_number` - specifies the memory address.
112 | * `value` - the current value from that memory.
113 |
114 | ### Example
115 | ```elixir
116 | # DB inital state
117 | model = %{
118 | 80 => %{
119 | {:c, 1} => 1,
120 | {:c, 2} => 0,
121 | {:i, 1} => 1,
122 | {:i, 2} => 1,
123 | {:ir, 1} => 0,
124 | {:ir, 2} => 1,
125 | {:hr, 1} => 102,
126 | {:hr, 2} => 103
127 | }
128 | }
129 | # Starts the Slave at "ttyUSB0"
130 | {:ok, s_pid} = Modbux.Rtu.Slave.start_link(tty: "ttyUSB0", model: model, active: true)
131 | ```
132 | if needed, the Slave DB can be modified in runtime with elixir code by using `request/2`,
133 | a `cmd` must be used to update the DB, the `cmd` is a 4 elements tuple, as follows:
134 | - `{:rc, slave, address, count}` read `count` coils.
135 | - `{:ri, slave, address, count}` read `count` inputs.
136 | - `{:rhr, slave, address, count}` read `count` holding registers.
137 | - `{:rir, slave, address, count}` read `count` input registers.
138 | - `{:fc, slave, address, value}` force single coil.
139 | - `{:phr, slave, address, value}` preset single holding register.
140 | - `{:fc, slave, address, values}` force multiple coils.
141 | - `{:phr, slave, address, values}` preset multiple holding registers.
142 |
143 | #### Master
144 |
145 | To start a Modbus RTU Master process use `start_link/1`.
146 |
147 | The following options are available:
148 |
149 | * `tty` - defines the serial port to spawn the Master.
150 | * `timeout` - defines slave timeout.
151 | * `active` - (`true` or `false`) specifies whether data is received as
152 | messages (mailbox) or by calling `request/2`.
153 | * `gen_opts` - defines extra options for the Genserver OTP configuration.
154 | * `uart_opts` - defines extra options for the UART configuration (defaults:
155 | [speed: 115200, rx_framing_timeout: 1000]).
156 |
157 | The messages (when active mode is true) have the following form:
158 | ```elixir
159 | {:modbus_rtu, {:slave_response, cmd, values}}
160 | ```
161 | or
162 | ```elixir
163 | {:modbus_rtu, {:slave_error, payload, reason}}
164 | ```
165 | The following are some reasons:
166 |
167 | * `:ecrc` - corrupted message (invalid crc).
168 | * `:einval` - invalid function.
169 | * `:eaddr` - invalid memory address requested.
170 |
171 | use `request/2` to send a `cmd` (command) to a Modbus RTU Slave.
172 |
173 | ### Example
174 |
175 | ```elixir
176 | # Starts the Master at "ttyUSB1" (in the example is connected to ttyUSB1)
177 | {:ok, m_pid} = Modbux.Rtu.Master.start_link(tty: "ttyUSB1")
178 | # Read 2 holding registers at 1 (memory address) from the slave 80
179 | resp = Modbux.Rtu.Master.request(m_pid, {:rhr, 80, 1, 2})
180 | # resp == {:ok, [102, 103]}
181 | ```
182 |
183 | ### Modbux TCP
184 |
185 | Modbus TCP (also Modbus TCP/IP) is simply the Modbus RTU protocol with a TCP interface that runs on a network.
186 |
187 | #### Server
188 |
189 | To start a Modbus TCP Server process use `start_link/1`.
190 |
191 | The following options are available:
192 |
193 | * `port` - is the Modbux TCP Server tcp port number.
194 | * `timeout` - is the connection timeout.
195 | * `model` - defines the DB initial state.
196 | * `sup_otps` - server supervisor OTP options.
197 | * `active` - (`true` or `false`) enable/disable DB updates notifications (mailbox).
198 |
199 | The messages (when active mode is true) have the following form:
200 | ```elixir
201 | {:modbus_tcp, {:slave_request, payload}}
202 | ```
203 |
204 | ### Example
205 |
206 | ```elixir
207 | # DB initial state
208 | model = %{80 => %{{:c, 20818} => 0, {:hr, 20818} => 0}}
209 | # Starts the Server at tcp port: 2000
210 | Modbux.Tcp.Server.start_link(model: model, port: 2000)
211 | ```
212 |
213 | #### Client
214 |
215 | To start a Modbus TCP Client process use `start_link/1`.
216 |
217 | The following options are available:
218 |
219 | * `ip` - is the internet address of the desired Modbux TCP Server.
220 | * `tcp_port` - is the desired Modbux TCP Server tcp port number.
221 | * `timeout` - is the connection timeout.
222 | * `active` - (`true` or `false`) specifies whether data is received as
223 | messages (mailbox) or by calling `confirmation/1` each time `request/2` is called.
224 |
225 | The messages (when active mode is true) have the following form:
226 |
227 | ```elixir
228 | {:modbus_tcp, cmd, values}
229 | ```
230 |
231 | to connect to a Modbus TCP Server `connect/1`.
232 |
233 | Use `request/2` to send a `cmd` (command) to a Modbus TCP Server and `confirmation/1` to parse the server response.
234 |
235 | ### Example
236 |
237 | ```elixir
238 | # Starts the Client that will connect to a Server with tcp port: 2000
239 | {:ok, cpid} = Modbux.Tcp.Client.start_link(ip: {127,0,0,1}, tcp_port: 2000, timeout: 2000)
240 | # Connect to the Server
241 | Modbux.Tcp.Client.connect(cpid)
242 | # Read 1 coil at 20818 from the device 80
243 | Modbux.Tcp.Client.request(cpid, {:rc, 0x50, 20818, 1})
244 | # Parse the Server response
245 | resp = Modbux.Tcp.Client.confirmation(cpid)
246 | # resp == {:ok, [0]}
247 | ```
248 |
249 |
250 | ### Helpers
251 |
252 | #### IEEE754 Float
253 |
254 | Several modbus register use IEEE754 float format, therefore this library also provides functions to encode and decode data.
255 |
256 | ### Example
257 |
258 | ```elixir
259 | # Encode
260 | +5.0 = Modbux.IEEE754.from_2_regs(0x40a0, 0x0000, :be)
261 | [-5.0, +5.0] = Modbux.IEEE754.from_2n_regs([0xc0a0, 0x0000, 0x40a0, 0x0000], :be)
262 | # Decode
263 | [0xc0a0, 0x0000] = Modbux.IEEE754.to_2_regs(-5.0)
264 | [0xc0a0, 0x0000, 0x40a0, 0x0000] = Modbux.IEEE754.to_2n_regs([-5.0, +5.0])
265 | ```
266 |
267 | Based on https://www.h-schmidt.net/FloatConverter/IEEE754.html.
268 |
269 | #### Endianess
270 |
271 | Depending on the device / server the data can be encoded with different types of endianess, therefore this library also provides functions to encode data.
272 |
273 | ### Example
274 |
275 | ```elixir
276 | # Encode
277 | 2.3183081793789774e-41 = Modbux.IEEE754.from_2_regs(0x40a0, 0x0000, :le)
278 | [6.910082987278538e-41, 2.3183081793789774e-41] = Modbux.IEEE754.from_2n_regs([0xc0a0, 0x0000, 0x40a0, 0x0000], :le)
279 | ```
280 |
281 |
282 | Good to know:
283 | - [Erlang default endianess is BIG](http://erlang.org/doc/programming_examples/bit_syntax.html#Defaults)
284 | - [MODBUS default endianess is BIG (p.34)](http://modbus.org/docs/PI_MBUS_300.pdf)
285 | - [MODBUS CRC endianess is LITTLE (p.16)](http://modbus.org/docs/PI_MBUS_300.pdf)
286 |
287 |
288 | ## Documentation
289 | The docs can be found at [https://hexdocs.pm/modbux](https://hexdocs.pm/modbux).
290 |
291 | Based on:
292 |
293 | - http://modbus.org/docs/PI_MBUS_300.pdf
294 | - http://modbus.org/docs/Modbux_Messaging_Implementation_Guide_V1_0b.pdf
295 | - http://modbus.org/docs/Modbux_over_serial_line_V1_02.pdf
296 | - http://www.simplymodbus.ca/index.html
297 |
298 | ## Contributing
299 | * Fork our repository on github.
300 | * Fix or add what is needed.
301 | * Commit to your repository.
302 | * Issue a github pull request (fill the PR template).
303 |
304 | ## License
305 | See [LICENSE](https://github.com/valiot/modbux/blob/master/LICENSE).
306 |
307 | ## TODO
308 | * Add Modbux ASCII.
309 | * Add Modbux UDP.
310 | * Add more examples.
311 | * Improve error handling.
312 |
313 |
314 |
315 |
316 |
317 |
--------------------------------------------------------------------------------
/assets/images/modbux-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/valiot/modbux/0c568f1100b8364bf6af408099afbcc238816cf9/assets/images/modbux-logo.png
--------------------------------------------------------------------------------
/assets/images/valiot-logo-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/valiot/modbux/0c568f1100b8364bf6af408099afbcc238816cf9/assets/images/valiot-logo-blue.png
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # Add the RingLogger backend. This removes the
4 | # default :console backend.
5 | config :logger, backends: [RingLogger]
6 |
7 | # Set the number of messages to hold in the circular buffer
8 | config :logger, RingLogger, max_size: 100
9 |
--------------------------------------------------------------------------------
/lib/helpers/float.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.IEEE754 do
2 | @moduledoc """
3 | IEEE754 float helper
4 |
5 | Based on https://www.h-schmidt.net/FloatConverter/IEEE754.html.
6 | """
7 |
8 | @doc """
9 | Converts a couple of 16-bit registers to IEEE754 float.
10 |
11 | ## Example
12 |
13 | ```elixir
14 | +5.0 = IEEE754.from_2_regs(0x40a0, 0x0000, :be)
15 | ```
16 | """
17 | def from_2_regs(w0, w1, :be) do
18 | <> = <>
19 | value
20 | end
21 |
22 | def from_2_regs(w0, w1, :le) do
23 | <> = <>
24 | value
25 | end
26 |
27 | @doc """
28 | Converts a couple of 16-bit registers to IEEE754 float.
29 |
30 | ## Example
31 |
32 | ```elixir
33 | +5.0 = IEEE754.from_2_regs(0x40a0, 0x0000, :be)
34 | ```
35 | """
36 | def from_2_regs([w0, w1], :be) do
37 | <> = <>
38 | f
39 | end
40 |
41 | def from_2_regs([w0, w1], :le) do
42 | <> = <>
43 | f
44 | end
45 |
46 | @doc """
47 | Converts a list of 2n 16-bit registers to a IEEE754 floats list.
48 |
49 | ## Example
50 |
51 | ```elixir
52 | [-5.0, +5.0] = IEEE754.from_2n_regs([0xc0a0, 0x0000, 0x40a0, 0x0000], :be)
53 | ```
54 | """
55 | def from_2n_regs([], _), do: []
56 |
57 | def from_2n_regs([w0, w1 | tail], endianness) do
58 | [from_2_regs(w0, w1, endianness) | from_2n_regs(tail, endianness)]
59 | end
60 |
61 | @doc """
62 | Converts a IEEE754 float to a couple of 16-bit registers.
63 |
64 | ## Example
65 |
66 | ```elixir
67 | [0xc0a0, 0x0000] = IEEE754.to_2_regs(-5.0)
68 | ```
69 | """
70 | def to_2_regs(f, :be) do
71 | <> = <>
72 | [w0, w1]
73 | end
74 |
75 | def to_2_regs(f, :le) do
76 | <> = <>
77 | [w1, w0]
78 | end
79 |
80 | @doc """
81 | Converts a list of IEEE754 floats to a list of 2n 16-bit registers.
82 |
83 | ## Example
84 |
85 | ```elixir
86 | [0xc0a0, 0x0000, 0x40a0, 0x0000] = IEEE754.to_2n_regs([-5.0, +5.0])
87 | ```
88 | """
89 | def to_2n_regs([], _), do: []
90 |
91 | def to_2n_regs([f | tail], endianness) do
92 | [w0, w1] = to_2_regs(f, endianness)
93 | [w0, w1 | to_2n_regs(tail, endianness)]
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/lib/helpers/helper.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Helper do
2 | @moduledoc """
3 | Binary helper module
4 | """
5 |
6 | @otp_vesion System.otp_release() |> Integer.parse() |> elem(0)
7 |
8 | @hi [
9 | 0x00,
10 | 0xC1,
11 | 0x81,
12 | 0x40,
13 | 0x01,
14 | 0xC0,
15 | 0x80,
16 | 0x41,
17 | 0x01,
18 | 0xC0,
19 | 0x80,
20 | 0x41,
21 | 0x00,
22 | 0xC1,
23 | 0x81,
24 | 0x40,
25 | 0x01,
26 | 0xC0,
27 | 0x80,
28 | 0x41,
29 | 0x00,
30 | 0xC1,
31 | 0x81,
32 | 0x40,
33 | 0x00,
34 | 0xC1,
35 | 0x81,
36 | 0x40,
37 | 0x01,
38 | 0xC0,
39 | 0x80,
40 | 0x41,
41 | 0x01,
42 | 0xC0,
43 | 0x80,
44 | 0x41,
45 | 0x00,
46 | 0xC1,
47 | 0x81,
48 | 0x40,
49 | 0x00,
50 | 0xC1,
51 | 0x81,
52 | 0x40,
53 | 0x01,
54 | 0xC0,
55 | 0x80,
56 | 0x41,
57 | 0x00,
58 | 0xC1,
59 | 0x81,
60 | 0x40,
61 | 0x01,
62 | 0xC0,
63 | 0x80,
64 | 0x41,
65 | 0x01,
66 | 0xC0,
67 | 0x80,
68 | 0x41,
69 | 0x00,
70 | 0xC1,
71 | 0x81,
72 | 0x40,
73 | 0x01,
74 | 0xC0,
75 | 0x80,
76 | 0x41,
77 | 0x00,
78 | 0xC1,
79 | 0x81,
80 | 0x40,
81 | 0x00,
82 | 0xC1,
83 | 0x81,
84 | 0x40,
85 | 0x01,
86 | 0xC0,
87 | 0x80,
88 | 0x41,
89 | 0x00,
90 | 0xC1,
91 | 0x81,
92 | 0x40,
93 | 0x01,
94 | 0xC0,
95 | 0x80,
96 | 0x41,
97 | 0x01,
98 | 0xC0,
99 | 0x80,
100 | 0x41,
101 | 0x00,
102 | 0xC1,
103 | 0x81,
104 | 0x40,
105 | 0x00,
106 | 0xC1,
107 | 0x81,
108 | 0x40,
109 | 0x01,
110 | 0xC0,
111 | 0x80,
112 | 0x41,
113 | 0x01,
114 | 0xC0,
115 | 0x80,
116 | 0x41,
117 | 0x00,
118 | 0xC1,
119 | 0x81,
120 | 0x40,
121 | 0x01,
122 | 0xC0,
123 | 0x80,
124 | 0x41,
125 | 0x00,
126 | 0xC1,
127 | 0x81,
128 | 0x40,
129 | 0x00,
130 | 0xC1,
131 | 0x81,
132 | 0x40,
133 | 0x01,
134 | 0xC0,
135 | 0x80,
136 | 0x41,
137 | 0x01,
138 | 0xC0,
139 | 0x80,
140 | 0x41,
141 | 0x00,
142 | 0xC1,
143 | 0x81,
144 | 0x40,
145 | 0x00,
146 | 0xC1,
147 | 0x81,
148 | 0x40,
149 | 0x01,
150 | 0xC0,
151 | 0x80,
152 | 0x41,
153 | 0x00,
154 | 0xC1,
155 | 0x81,
156 | 0x40,
157 | 0x01,
158 | 0xC0,
159 | 0x80,
160 | 0x41,
161 | 0x01,
162 | 0xC0,
163 | 0x80,
164 | 0x41,
165 | 0x00,
166 | 0xC1,
167 | 0x81,
168 | 0x40,
169 | 0x00,
170 | 0xC1,
171 | 0x81,
172 | 0x40,
173 | 0x01,
174 | 0xC0,
175 | 0x80,
176 | 0x41,
177 | 0x01,
178 | 0xC0,
179 | 0x80,
180 | 0x41,
181 | 0x00,
182 | 0xC1,
183 | 0x81,
184 | 0x40,
185 | 0x01,
186 | 0xC0,
187 | 0x80,
188 | 0x41,
189 | 0x00,
190 | 0xC1,
191 | 0x81,
192 | 0x40,
193 | 0x00,
194 | 0xC1,
195 | 0x81,
196 | 0x40,
197 | 0x01,
198 | 0xC0,
199 | 0x80,
200 | 0x41,
201 | 0x00,
202 | 0xC1,
203 | 0x81,
204 | 0x40,
205 | 0x01,
206 | 0xC0,
207 | 0x80,
208 | 0x41,
209 | 0x01,
210 | 0xC0,
211 | 0x80,
212 | 0x41,
213 | 0x00,
214 | 0xC1,
215 | 0x81,
216 | 0x40,
217 | 0x01,
218 | 0xC0,
219 | 0x80,
220 | 0x41,
221 | 0x00,
222 | 0xC1,
223 | 0x81,
224 | 0x40,
225 | 0x00,
226 | 0xC1,
227 | 0x81,
228 | 0x40,
229 | 0x01,
230 | 0xC0,
231 | 0x80,
232 | 0x41,
233 | 0x01,
234 | 0xC0,
235 | 0x80,
236 | 0x41,
237 | 0x00,
238 | 0xC1,
239 | 0x81,
240 | 0x40,
241 | 0x00,
242 | 0xC1,
243 | 0x81,
244 | 0x40,
245 | 0x01,
246 | 0xC0,
247 | 0x80,
248 | 0x41,
249 | 0x00,
250 | 0xC1,
251 | 0x81,
252 | 0x40,
253 | 0x01,
254 | 0xC0,
255 | 0x80,
256 | 0x41,
257 | 0x01,
258 | 0xC0,
259 | 0x80,
260 | 0x41,
261 | 0x00,
262 | 0xC1,
263 | 0x81,
264 | 0x40
265 | ]
266 |
267 | @lo [
268 | 0x00,
269 | 0xC0,
270 | 0xC1,
271 | 0x01,
272 | 0xC3,
273 | 0x03,
274 | 0x02,
275 | 0xC2,
276 | 0xC6,
277 | 0x06,
278 | 0x07,
279 | 0xC7,
280 | 0x05,
281 | 0xC5,
282 | 0xC4,
283 | 0x04,
284 | 0xCC,
285 | 0x0C,
286 | 0x0D,
287 | 0xCD,
288 | 0x0F,
289 | 0xCF,
290 | 0xCE,
291 | 0x0E,
292 | 0x0A,
293 | 0xCA,
294 | 0xCB,
295 | 0x0B,
296 | 0xC9,
297 | 0x09,
298 | 0x08,
299 | 0xC8,
300 | 0xD8,
301 | 0x18,
302 | 0x19,
303 | 0xD9,
304 | 0x1B,
305 | 0xDB,
306 | 0xDA,
307 | 0x1A,
308 | 0x1E,
309 | 0xDE,
310 | 0xDF,
311 | 0x1F,
312 | 0xDD,
313 | 0x1D,
314 | 0x1C,
315 | 0xDC,
316 | 0x14,
317 | 0xD4,
318 | 0xD5,
319 | 0x15,
320 | 0xD7,
321 | 0x17,
322 | 0x16,
323 | 0xD6,
324 | 0xD2,
325 | 0x12,
326 | 0x13,
327 | 0xD3,
328 | 0x11,
329 | 0xD1,
330 | 0xD0,
331 | 0x10,
332 | 0xF0,
333 | 0x30,
334 | 0x31,
335 | 0xF1,
336 | 0x33,
337 | 0xF3,
338 | 0xF2,
339 | 0x32,
340 | 0x36,
341 | 0xF6,
342 | 0xF7,
343 | 0x37,
344 | 0xF5,
345 | 0x35,
346 | 0x34,
347 | 0xF4,
348 | 0x3C,
349 | 0xFC,
350 | 0xFD,
351 | 0x3D,
352 | 0xFF,
353 | 0x3F,
354 | 0x3E,
355 | 0xFE,
356 | 0xFA,
357 | 0x3A,
358 | 0x3B,
359 | 0xFB,
360 | 0x39,
361 | 0xF9,
362 | 0xF8,
363 | 0x38,
364 | 0x28,
365 | 0xE8,
366 | 0xE9,
367 | 0x29,
368 | 0xEB,
369 | 0x2B,
370 | 0x2A,
371 | 0xEA,
372 | 0xEE,
373 | 0x2E,
374 | 0x2F,
375 | 0xEF,
376 | 0x2D,
377 | 0xED,
378 | 0xEC,
379 | 0x2C,
380 | 0xE4,
381 | 0x24,
382 | 0x25,
383 | 0xE5,
384 | 0x27,
385 | 0xE7,
386 | 0xE6,
387 | 0x26,
388 | 0x22,
389 | 0xE2,
390 | 0xE3,
391 | 0x23,
392 | 0xE1,
393 | 0x21,
394 | 0x20,
395 | 0xE0,
396 | 0xA0,
397 | 0x60,
398 | 0x61,
399 | 0xA1,
400 | 0x63,
401 | 0xA3,
402 | 0xA2,
403 | 0x62,
404 | 0x66,
405 | 0xA6,
406 | 0xA7,
407 | 0x67,
408 | 0xA5,
409 | 0x65,
410 | 0x64,
411 | 0xA4,
412 | 0x6C,
413 | 0xAC,
414 | 0xAD,
415 | 0x6D,
416 | 0xAF,
417 | 0x6F,
418 | 0x6E,
419 | 0xAE,
420 | 0xAA,
421 | 0x6A,
422 | 0x6B,
423 | 0xAB,
424 | 0x69,
425 | 0xA9,
426 | 0xA8,
427 | 0x68,
428 | 0x78,
429 | 0xB8,
430 | 0xB9,
431 | 0x79,
432 | 0xBB,
433 | 0x7B,
434 | 0x7A,
435 | 0xBA,
436 | 0xBE,
437 | 0x7E,
438 | 0x7F,
439 | 0xBF,
440 | 0x7D,
441 | 0xBD,
442 | 0xBC,
443 | 0x7C,
444 | 0xB4,
445 | 0x74,
446 | 0x75,
447 | 0xB5,
448 | 0x77,
449 | 0xB7,
450 | 0xB6,
451 | 0x76,
452 | 0x72,
453 | 0xB2,
454 | 0xB3,
455 | 0x73,
456 | 0xB1,
457 | 0x71,
458 | 0x70,
459 | 0xB0,
460 | 0x50,
461 | 0x90,
462 | 0x91,
463 | 0x51,
464 | 0x93,
465 | 0x53,
466 | 0x52,
467 | 0x92,
468 | 0x96,
469 | 0x56,
470 | 0x57,
471 | 0x97,
472 | 0x55,
473 | 0x95,
474 | 0x94,
475 | 0x54,
476 | 0x9C,
477 | 0x5C,
478 | 0x5D,
479 | 0x9D,
480 | 0x5F,
481 | 0x9F,
482 | 0x9E,
483 | 0x5E,
484 | 0x5A,
485 | 0x9A,
486 | 0x9B,
487 | 0x5B,
488 | 0x99,
489 | 0x59,
490 | 0x58,
491 | 0x98,
492 | 0x88,
493 | 0x48,
494 | 0x49,
495 | 0x89,
496 | 0x4B,
497 | 0x8B,
498 | 0x8A,
499 | 0x4A,
500 | 0x4E,
501 | 0x8E,
502 | 0x8F,
503 | 0x4F,
504 | 0x8D,
505 | 0x4D,
506 | 0x4C,
507 | 0x8C,
508 | 0x44,
509 | 0x84,
510 | 0x85,
511 | 0x45,
512 | 0x87,
513 | 0x47,
514 | 0x46,
515 | 0x86,
516 | 0x82,
517 | 0x42,
518 | 0x43,
519 | 0x83,
520 | 0x41,
521 | 0x81,
522 | 0x80,
523 | 0x40
524 | ]
525 |
526 | @spec crc(binary) :: <<_::16>>
527 | def crc(data) do
528 | crc(data, 0xFF, 0xFF)
529 | end
530 |
531 | defp crc(<<>>, hi, lo), do: <>
532 |
533 | defp crc(data, hi, lo) do
534 | <> = data
535 | index = xor(lo, first)
536 | lo = xor(hi, Enum.at(@hi, index))
537 | hi = Enum.at(@lo, index)
538 | crc(tail, hi, lo)
539 | end
540 |
541 | @spec byte_count(integer) :: integer
542 | def byte_count(count) do
543 | div(count - 1, 8) + 1
544 | end
545 |
546 | @spec bool_to_byte(0 | 1) :: 0 | 255
547 | def bool_to_byte(value) do
548 | # enforce 0 or 1 only
549 | case value do
550 | 0 -> 0x00
551 | 1 -> 0xFF
552 | end
553 | end
554 |
555 | @spec bin_to_bitlist(integer, <<_::8, _::_*8>>) :: [any]
556 | def bin_to_bitlist(count, <>) when count <= 8 do
557 | Enum.take([b0, b1, b2, b3, b4, b5, b6, b7], count)
558 | end
559 |
560 | def bin_to_bitlist(count, <>) do
561 | [b0, b1, b2, b3, b4, b5, b6, b7] ++ bin_to_bitlist(count - 8, tail)
562 | end
563 |
564 | @spec bin_to_reglist(pos_integer, <<_::16, _::_*8>>) :: [char, ...]
565 | def bin_to_reglist(1, <>) do
566 | [register]
567 | end
568 |
569 | def bin_to_reglist(count, <>) do
570 | [register | bin_to_reglist(count - 1, tail)]
571 | end
572 |
573 | @spec bitlist_to_bin(any) :: any
574 | def bitlist_to_bin(values) do
575 | lists = Enum.chunk_every(values, 8, 8, [0, 0, 0, 0, 0, 0, 0, 0])
576 |
577 | list =
578 | for list8 <- lists do
579 | [v0, v1, v2, v3, v4, v5, v6, v7] =
580 | for b <- list8 do
581 | # enforce 0 or 1 only
582 | bool_to_byte(b)
583 | end
584 |
585 | <>
586 | end
587 |
588 | :erlang.iolist_to_binary(list)
589 | end
590 |
591 | @spec reglist_to_bin(any) :: any
592 | def reglist_to_bin(values) do
593 | list =
594 | for value <- values do
595 | <>
596 | end
597 |
598 | :erlang.iolist_to_binary(list)
599 | end
600 |
601 | if @otp_vesion >= 24 do
602 | defp xor(a, b), do: Bitwise.bxor(a, b)
603 | else
604 | defp xor(a, b), do: Bitwise.^^^(a, b)
605 | end
606 | end
607 |
--------------------------------------------------------------------------------
/lib/helpers/model.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Model do
2 | @moduledoc """
3 | Model helper, functions to write and read the Slave/Server DB.
4 | """
5 | require Logger
6 |
7 | def apply(state, {:rc, slave, address, count}) do
8 | reads(state, {slave, :c, address, count})
9 | end
10 |
11 | def apply(state, {:ri, slave, address, count}) do
12 | reads(state, {slave, :i, address, count})
13 | end
14 |
15 | def apply(state, {:rhr, slave, address, count}) do
16 | reads(state, {slave, :hr, address, count})
17 | end
18 |
19 | def apply(state, {:rir, slave, address, count}) do
20 | reads(state, {slave, :ir, address, count})
21 | end
22 |
23 | def apply(state, {:fc, slave, address, value}) when is_integer(value) do
24 | write(state, {slave, :c, address, value})
25 | end
26 |
27 | def apply(state, {:fc, slave, address, values}) when is_list(values) do
28 | writes(state, {slave, :c, address, values})
29 | end
30 |
31 | def apply(state, {:phr, slave, address, value}) when is_integer(value) do
32 | write(state, {slave, :hr, address, value})
33 | end
34 |
35 | def apply(state, {:phr, slave, address, values}) when is_list(values) do
36 | writes(state, {slave, :hr, address, values})
37 | end
38 |
39 | # Error and exception clause
40 | def apply(state, _cmd) do
41 | # do nothing
42 | {nil, state}
43 | end
44 |
45 | @spec reads(map, {any, any, any, any}) :: {nil | {:error, :eaddr} | {:ok, [any]}, map}
46 | def reads(state, {slave, type, address, count}) do
47 | # checks the slave_ids
48 | if Map.has_key?(state, slave) do
49 | try do
50 | map = Map.fetch!(state, slave)
51 |
52 | list =
53 | for point <- address..(address + count - 1) do
54 | Map.fetch!(map, {type, point})
55 | end
56 |
57 | {{:ok, list}, state}
58 | rescue
59 | _error ->
60 | {{:error, :eaddr}, state}
61 | end
62 | else
63 | # is a different slave id, do nothing.
64 | {nil, state}
65 | end
66 | end
67 |
68 | @spec write(map, {any, any, any, any}) :: {nil | {:error, :eaddr} | {:ok, nil}, map}
69 | def write(state, {slave, type, address, value}) do
70 | # checks the slave_ids
71 | if Map.has_key?(state, slave) do
72 | try do
73 | cmap = Map.fetch!(state, slave)
74 | nmap = Map.replace!(cmap, {type, address}, value)
75 | {{:ok, nil}, Map.put(state, slave, nmap)}
76 | rescue
77 | _error ->
78 | {{:error, :eaddr}, state}
79 | end
80 | else
81 | # is a different slave id, do nothing.
82 | {nil, state}
83 | end
84 | end
85 |
86 | def writes(state, {slave, type, address, values}) do
87 | # checks the slave_ids
88 | if Map.has_key?(state, slave) do
89 | try do
90 | cmap = Map.fetch!(state, slave)
91 | final = address + Enum.count(values)
92 |
93 | {^final, nmap} =
94 | Enum.reduce(values, {address, cmap}, fn value, {i, map} ->
95 | {i + 1, Map.replace!(map, {type, i}, value)}
96 | end)
97 |
98 | {{:ok, nil}, Map.put(state, slave, nmap)}
99 | rescue
100 | _error ->
101 | {{:error, :eaddr}, state}
102 | end
103 | else
104 | # is a different slave id, do nothing.
105 | {nil, state}
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/lib/helpers/request.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Request do
2 | @moduledoc """
3 | Request helper, functions that handles Client & Master request messages.
4 | """
5 | alias Modbux.Helper
6 |
7 | @spec pack({:fc | :phr | :rc | :rhr | :ri | :rir, integer, integer, maybe_improper_list | integer}) ::
8 | <<_::48, _::_*8>>
9 | def pack({:rc, slave, address, count}) do
10 | reads(:d, slave, 1, address, count)
11 | end
12 |
13 | def pack({:ri, slave, address, count}) do
14 | reads(:d, slave, 2, address, count)
15 | end
16 |
17 | def pack({:rhr, slave, address, count}) do
18 | reads(:a, slave, 3, address, count)
19 | end
20 |
21 | def pack({:rir, slave, address, count}) do
22 | reads(:a, slave, 4, address, count)
23 | end
24 |
25 | def pack({:fc, slave, address, value}) when is_integer(value) do
26 | write(:d, slave, 5, address, value)
27 | end
28 |
29 | def pack({:phr, slave, address, value}) when is_integer(value) do
30 | write(:a, slave, 6, address, value)
31 | end
32 |
33 | def pack({:fc, slave, address, values}) when is_list(values) do
34 | writes(:d, slave, 15, address, values)
35 | end
36 |
37 | def pack({:phr, slave, address, values}) when is_list(values) do
38 | writes(:a, slave, 16, address, values)
39 | end
40 |
41 | @spec parse(<<_::24, _::_*8>>) ::
42 | {:einval | :error | :fc | :phr | :rc | :rhr | :ri | :rir, byte, char, [any] | char}
43 | def parse(<>) do
44 | {:rc, slave, address, count}
45 | end
46 |
47 | def parse(<>) do
48 | {:ri, slave, address, count}
49 | end
50 |
51 | def parse(<>) do
52 | {:rhr, slave, address, count}
53 | end
54 |
55 | def parse(<>) do
56 | {:rir, slave, address, count}
57 | end
58 |
59 | def parse(<>) do
60 | {:fc, slave, address, 0}
61 | end
62 |
63 | def parse(<>) do
64 | {:fc, slave, address, 1}
65 | end
66 |
67 | def parse(<>) do
68 | {:phr, slave, address, value}
69 | end
70 |
71 | # Another slave response an error.
72 | def parse(<>) do
73 | ^bytes = Helper.byte_count(count)
74 | values = Helper.bin_to_bitlist(count, data)
75 | {:fc, slave, address, values}
76 | end
77 |
78 | def parse(<>) do
79 | ^bytes = 2 * count
80 | values = Helper.bin_to_reglist(count, data)
81 | {:phr, slave, address, values}
82 | end
83 |
84 | # Exceptions clauses
85 | def parse(<>) when fc in 129..144 do
86 | {:error, slave, fc, error_code}
87 | end
88 |
89 | def parse(<>) do
90 | {:einval, slave, fc, error_code}
91 | end
92 |
93 | @spec length({:fc | :phr | :rc | :rhr | :ri | :rir, any, any, any}) :: integer
94 | def length({:rc, _slave, _address, _count}) do
95 | 6
96 | end
97 |
98 | def length({:ri, _slave, _address, _count}) do
99 | 6
100 | end
101 |
102 | def length({:rhr, _slave, _address, _count}) do
103 | 6
104 | end
105 |
106 | def length({:rir, _slave, _address, _count}) do
107 | 6
108 | end
109 |
110 | def length({:fc, _slave, _address, value}) when is_integer(value) do
111 | 6
112 | end
113 |
114 | def length({:phr, _slave, _address, value}) when is_integer(value) do
115 | 6
116 | end
117 |
118 | def length({:fc, _slave, _address, values}) when is_list(values) do
119 | 7 + Helper.byte_count(Enum.count(values))
120 | end
121 |
122 | def length({:phr, _slave, _address, values}) when is_list(values) do
123 | 7 + 2 * Enum.count(values)
124 | end
125 |
126 | defp reads(_type, slave, function, address, count) do
127 | <>
128 | end
129 |
130 | defp write(:d, slave, function, address, value) do
131 | <>
132 | end
133 |
134 | defp write(:a, slave, function, address, value) do
135 | <>
136 | end
137 |
138 | defp writes(:d, slave, function, address, values) do
139 | count = Enum.count(values)
140 | bytes = Helper.byte_count(count)
141 | data = Helper.bitlist_to_bin(values)
142 | <>
143 | end
144 |
145 | defp writes(:a, slave, function, address, values) do
146 | count = Enum.count(values)
147 | bytes = 2 * count
148 | data = Helper.reglist_to_bin(values)
149 | <>
150 | end
151 | end
152 |
--------------------------------------------------------------------------------
/lib/helpers/response.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Response do
2 | @moduledoc """
3 | Response helper, functions that handles Server & Slave response messages.
4 | """
5 | alias Modbux.Helper
6 |
7 | @exceptions %{
8 | 1 => :efun,
9 | 2 => :eaddr,
10 | 3 => :einval,
11 | 4 => :edevice,
12 | 5 => :ack,
13 | 6 => :sbusy,
14 | 7 => :nack,
15 | 8 => :ememp,
16 | 9 => :error,
17 | 10 => :egpath,
18 | 11 => :egtarg
19 | }
20 |
21 | @spec pack({:fc | :phr | :rc | :rhr | :ri | :rir, integer, any, maybe_improper_list | integer}, any) ::
22 | <<_::24, _::_*8>>
23 | def pack({:rc, slave, _address, count}, values) do
24 | ^count = Enum.count(values)
25 | data = Helper.bitlist_to_bin(values)
26 | reads(slave, 1, data)
27 | end
28 |
29 | def pack({:ri, slave, _address, count}, values) do
30 | ^count = Enum.count(values)
31 | data = Helper.bitlist_to_bin(values)
32 | reads(slave, 2, data)
33 | end
34 |
35 | def pack({:rhr, slave, _address, count}, values) do
36 | ^count = Enum.count(values)
37 | data = Helper.reglist_to_bin(values)
38 | reads(slave, 3, data)
39 | end
40 |
41 | def pack({:rir, slave, _address, count}, values) do
42 | ^count = Enum.count(values)
43 | data = Helper.reglist_to_bin(values)
44 | reads(slave, 4, data)
45 | end
46 |
47 | def pack({:fc, slave, address, value}, nil) when is_integer(value) do
48 | write(:d, slave, 5, address, value)
49 | end
50 |
51 | def pack({:phr, slave, address, value}, nil) when is_integer(value) do
52 | write(:a, slave, 6, address, value)
53 | end
54 |
55 | def pack({:fc, slave, address, values}, nil) when is_list(values) do
56 | writes(:d, slave, 15, address, values)
57 | end
58 |
59 | def pack({:phr, slave, address, values}, nil) when is_list(values) do
60 | writes(:a, slave, 16, address, values)
61 | end
62 |
63 | @spec parse(any, <<_::24, _::_*8>>) :: nil | [any] | {:error, any} | {:error, byte, <<_::104>>}
64 | def parse({:rc, slave, _address, count}, <>) do
65 | ^bytes = Helper.byte_count(count)
66 | Helper.bin_to_bitlist(count, data)
67 | end
68 |
69 | def parse({:ri, slave, _address, count}, <>) do
70 | ^bytes = Helper.byte_count(count)
71 | Helper.bin_to_bitlist(count, data)
72 | end
73 |
74 | def parse({:rhr, slave, _address, count}, <>) do
75 | ^bytes = 2 * count
76 | Helper.bin_to_reglist(count, data)
77 | end
78 |
79 | def parse({:rir, slave, _address, count}, <>) do
80 | ^bytes = 2 * count
81 | Helper.bin_to_reglist(count, data)
82 | end
83 |
84 | def parse({:fc, slave, address, 0}, <>) do
85 | nil
86 | end
87 |
88 | def parse({:fc, slave, address, 1}, <>) do
89 | nil
90 | end
91 |
92 | def parse({:phr, slave, address, value}, <>) do
93 | nil
94 | end
95 |
96 | def parse({:fc, slave, address, values}, <>) do
97 | ^count = Enum.count(values)
98 | nil
99 | end
100 |
101 | def parse({:phr, slave, address, values}, <>) do
102 | ^count = Enum.count(values)
103 | nil
104 | end
105 |
106 | # Error messages.
107 | def parse(_cmd, <<_slave, _fc, error_code>>) when error_code in 1..11 do
108 | {:error, @exceptions[error_code]}
109 | end
110 |
111 | def parse(_cmd, <>) do
112 | {:error, slave, "Unknown error"}
113 | end
114 |
115 | @spec length({:fc | :phr | :rc | :rhr | :ri | :rir, any, any, any}) :: number
116 | def length({:rc, _slave, _address, count}) do
117 | 3 + Helper.byte_count(count)
118 | end
119 |
120 | def length({:ri, _slave, _address, count}) do
121 | 3 + Helper.byte_count(count)
122 | end
123 |
124 | def length({:rhr, _slave, _address, count}) do
125 | 3 + 2 * count
126 | end
127 |
128 | def length({:rir, _slave, _address, count}) do
129 | 3 + 2 * count
130 | end
131 |
132 | def length({:fc, _slave, _address, _}) do
133 | 6
134 | end
135 |
136 | def length({:phr, _slave, _address, _}) do
137 | 6
138 | end
139 |
140 | defp reads(slave, function, data) do
141 | bytes = :erlang.byte_size(data)
142 | <>
143 | end
144 |
145 | defp write(:d, slave, function, address, value) do
146 | <>
147 | end
148 |
149 | defp write(:a, slave, function, address, value) do
150 | <>
151 | end
152 |
153 | defp writes(_type, slave, function, address, values) do
154 | count = Enum.count(values)
155 | <>
156 | end
157 | end
158 |
--------------------------------------------------------------------------------
/lib/helpers/shared.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Model.Shared do
2 | @moduledoc """
3 | An agent that holds the current state of the Server/Slave DB.
4 | """
5 | alias Modbux.Model
6 |
7 | @spec start_link(any, [
8 | {:debug, [any]}
9 | | {:hibernate_after, :infinity | non_neg_integer}
10 | | {:name, atom | {any, any} | {any, any, any}}
11 | | {:spawn_opt, :link | :monitor | {any, any}}
12 | | {:timeout, :infinity | non_neg_integer}
13 | ]) :: {:error, any} | {:ok, pid}
14 | def start_link(params, opts \\ []) do
15 | Agent.start_link(fn -> init(params) end, opts)
16 | end
17 |
18 | @spec stop(atom | pid | {atom, any} | {:via, atom, any}) :: :ok
19 | def stop(pid) do
20 | Agent.stop(pid)
21 | end
22 |
23 | @spec state(atom | pid | {atom, any} | {:via, atom, any}) :: any
24 | def state(pid) do
25 | Agent.get(pid, fn model -> model end)
26 | end
27 |
28 | @spec apply(atom | pid | {atom, any} | {:via, atom, any}, any) :: any
29 | def apply(pid, cmd) do
30 | Agent.get_and_update(pid, fn model -> Model.apply(model, cmd) end)
31 | end
32 |
33 | defp init(params) do
34 | Keyword.fetch!(params, :model)
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/rtu/framer.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Rtu.Framer do
2 | @moduledoc """
3 | A framer for Modbus RTU frames. This framer doesn't do anything for the transmit
4 | direction, but for receives, it will collect bytes that follows the Modbus RTU protocol,
5 | it also execute the CRC validation and returns the frame or the error.
6 | """
7 |
8 | @behaviour Circuits.UART.Framing
9 |
10 | require Logger
11 | alias Modbux.Helper
12 |
13 | defmodule State do
14 | @moduledoc false
15 | defstruct behavior: nil,
16 | max_len: nil,
17 | expected_length: nil,
18 | index: 0,
19 | processed: <<>>,
20 | in_process: <<>>,
21 | fc: nil,
22 | error: nil,
23 | error_message: nil,
24 | lines: []
25 | end
26 |
27 | def init(args) do
28 | # modbus standard max len
29 | max_len = Keyword.get(args, :max_len, 255)
30 | behavior = Keyword.get(args, :behavior, :slave)
31 | state = %State{max_len: max_len, behavior: behavior}
32 | {:ok, state}
33 | end
34 |
35 | # do nothing, we assume this is already in the right form
36 | def add_framing(data, state) do
37 | {:ok, data, state}
38 | end
39 |
40 | def remove_framing(data, state) do
41 | # Logger.debug("new data #{inspect(state)}")
42 | new_state = process_data(state.in_process <> data, state.expected_length, state.processed, state)
43 | rc = if buffer_empty?(new_state), do: :ok, else: :in_frame
44 | # Logger.debug("new data processed #{inspect(new_state)}, #{inspect(rc)}")
45 |
46 | if has_error?(new_state),
47 | do: dispatch({:ok, new_state.error_message, new_state}),
48 | else: dispatch({rc, new_state.lines, new_state})
49 | end
50 |
51 | def frame_timeout(state) do
52 | partial_line = {:partial, state.processed <> state.in_process}
53 | new_state = %State{max_len: state.max_len, behavior: state.behavior}
54 | {:ok, [partial_line], new_state}
55 | end
56 |
57 | def flush(direction, state) when direction == :receive or direction == :both do
58 | %State{max_len: state.max_len, behavior: state.behavior}
59 | end
60 |
61 | def flush(_direction, state) do
62 | state
63 | end
64 |
65 | # helper functions
66 |
67 | # handle empty byte
68 | defp process_data(<<>>, _len, _in_process, state) do
69 | # Logger.debug("End #{inspect(state)}")
70 | state
71 | end
72 |
73 | # get first byte (index 0)
74 | defp process_data(<>, nil, processed, %{index: 0} = state) do
75 | # Logger.debug("line 0")
76 | new_state = %{state | index: 1, processed: processed <> <>, in_process: <<>>}
77 | process_data(b_tail, nil, new_state.processed, new_state)
78 | end
79 |
80 | # get second byte (function code) (index 1)
81 | defp process_data(<>, nil, processed, %{index: 1} = state) do
82 | # Logger.debug("line 1")
83 | new_state = %{state | fc: fc, index: 2, processed: processed <> <>}
84 | process_data(b_tail, nil, new_state.processed, new_state)
85 | end
86 |
87 | # Clause for functions code => 5, 6
88 | defp process_data(<>, nil, processed, %{index: 2, fc: fc} = state)
89 | when fc in [5, 6] do
90 | # Logger.debug("fc 5 or 6")
91 | new_state = %{state | expected_length: 8, index: 3, processed: processed <> <>}
92 | process_data(b_tail, new_state.expected_length, new_state.processed, new_state)
93 | end
94 |
95 | # Clause for functions code => 15 and 16
96 | defp process_data(<>, nil, processed, %{index: 2, fc: fc} = state)
97 | when fc in [15, 16] do
98 | # Logger.debug("fc 15 or 16")
99 | new_state =
100 | if state.behavior == :slave do
101 | %{state | expected_length: 7, index: 3, processed: processed <> <>}
102 | else
103 | %{state | expected_length: 8, index: 3, processed: processed <> <>}
104 | end
105 |
106 | process_data(b_tail, new_state.expected_length, new_state.processed, new_state)
107 | end
108 |
109 | defp process_data(<>, _len, processed, %{index: 6, fc: fc} = state)
110 | when fc in [15, 16] do
111 | # Logger.debug("fc 15 or 16, len")
112 |
113 | new_state =
114 | if state.behavior == :slave do
115 | %{state | expected_length: len + 9, index: 7, processed: processed <> <>}
116 | else
117 | %{state | expected_length: 8, index: 7, processed: processed <> <>}
118 | end
119 |
120 | process_data(b_tail, new_state.expected_length, new_state.processed, new_state)
121 | end
122 |
123 | # Clause for functions code => 1, 2, 3 and 4
124 | defp process_data(<>, nil, processed, %{index: 2, fc: fc} = state)
125 | when fc in 1..4 do
126 | # Logger.debug("fc 1, 2, 3 or 4")
127 |
128 | new_state =
129 | if state.behavior == :slave do
130 | %{state | expected_length: 8, index: 3, processed: processed <> <>}
131 | else
132 | %{state | expected_length: len + 5, index: 3, processed: processed <> <>}
133 | end
134 |
135 | process_data(b_tail, new_state.expected_length, new_state.processed, new_state)
136 | end
137 |
138 | # Clause for exceptions.
139 | defp process_data(<>, nil, processed, %{index: 2, fc: fc} = state)
140 | when fc in 129..144 do
141 | # Logger.debug("exceptions")
142 |
143 | new_state = %{state | expected_length: 5, index: 3, processed: processed <> <>}
144 |
145 | process_data(b_tail, new_state.expected_length, new_state.processed, new_state)
146 | end
147 |
148 | # Catch all fc (error)
149 | defp process_data(_data, nil, processed, %{index: 2, fc: _fc} = state) do
150 | %{state | error: true, error_message: [{:error, :einval, processed}]}
151 | end
152 |
153 | defp process_data(<>, len, processed, state) when is_binary(processed) do
154 | current_data = processed <> <>
155 |
156 | # Logger.info(
157 | # "(#{__MODULE__}) data_len: #{byte_size(current_data)}, len: #{inspect(len)}, state: #{inspect(state)}"
158 | # )
159 |
160 | if len == byte_size(current_data) do
161 | new_state = %{
162 | state
163 | | expected_length: nil,
164 | lines: [current_data],
165 | in_process: b_tail,
166 | index: 0,
167 | processed: <<>>
168 | }
169 |
170 | # we got the whole thing in 1 pass, so we're done
171 | check_crc(new_state)
172 | else
173 | new_state = %{state | index: state.index + 1, processed: current_data}
174 | # need to keep reading
175 | process_data(b_tail, len, current_data, new_state)
176 | end
177 | end
178 |
179 | def buffer_empty?(state) do
180 | state.processed == <<>> and state.in_process == <<>>
181 | end
182 |
183 | defp has_error?(state) do
184 | state.error != nil
185 | end
186 |
187 | defp dispatch({:in_frame, _lines, _state} = msg), do: msg
188 |
189 | defp dispatch({rc, msg, state}) do
190 | {rc, msg, %State{max_len: state.max_len, behavior: state.behavior}}
191 | end
192 |
193 | # once we have the full packet, verify it's CRC16
194 | defp check_crc(state) do
195 | [packet] = state.lines
196 | packet_without_crc = Kernel.binary_part(packet, 0, byte_size(packet) - 2)
197 | expected_crc = Kernel.binary_part(packet, byte_size(packet), -2)
198 | <> = Helper.crc(packet_without_crc)
199 | real_crc = <>
200 | # Logger.info("(#{__MODULE__}) #{inspect(expected_crc)} == #{inspect(real_crc)}")
201 |
202 | if real_crc == expected_crc,
203 | do: state,
204 | else: %{state | error: true, error_message: [{:error, :ecrc, "CRC Error"}]}
205 | end
206 | end
207 |
--------------------------------------------------------------------------------
/lib/rtu/master.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Rtu.Master do
2 | @moduledoc """
3 | API for a Modbus RTU Master device.
4 | """
5 | use GenServer, restart: :transient
6 |
7 | alias Modbux.Rtu.{Master, Framer}
8 | alias Modbux.Rtu
9 | alias Circuits.UART
10 | require Logger
11 |
12 | @timeout 1000
13 | @speed 115_200
14 |
15 | defstruct tty: nil,
16 | timeout: nil,
17 | cmd: nil,
18 | active: false,
19 | uart_opts: nil,
20 | uart_pid: nil,
21 | parent_pid: nil
22 |
23 | @doc """
24 | Starts a Modbus RTU Master process.
25 |
26 | The following options are available:
27 |
28 | * `tty` - defines the serial port to spawn the Master.
29 | * `timeout` - defines slave timeout.
30 | * `active` - (`true` or `false`) specifies whether data is received as
31 | messages (mailbox) or by calling `request/2`.
32 | * `gen_opts` - defines extra options for the Genserver OTP configuration.
33 | * `uart_opts` - defines extra options for the UART configuration (defaults:
34 | [speed: 115200, rx_framing_timeout: 1000]).
35 |
36 | The messages (when active mode is true) have the following form:
37 |
38 | `{:modbus_rtu, {:slave_response, cmd, values}}`
39 | or
40 |
41 | `{:modbus_rtu, {:slave_error, payload, reason}}`
42 |
43 | The following are some reasons:
44 |
45 | * `:ecrc` - corrupted message (invalid crc).
46 | * `:einval` - invalid function.
47 | * `:eaddr` - invalid memory address requested.
48 |
49 | ## Example
50 |
51 | ```elixir
52 | Modbux.Rtu.Master.start_link(tty: "tnt0", active: true, uart_opts: [speed: 9600])
53 | ```
54 | """
55 | @spec start_link(keyword) :: :ignore | {:error, any} | {:ok, pid}
56 | def start_link(params) do
57 | gen_opts = Keyword.get(params, :gen_opts, [])
58 | GenServer.start_link(__MODULE__, {params, self()}, gen_opts)
59 | end
60 |
61 | @spec stop(atom | pid | {atom, any} | {:via, atom, any}) :: :ok
62 | def stop(pid) do
63 | GenServer.stop(pid)
64 | end
65 |
66 | @doc """
67 | Gets the Master state.
68 | """
69 | def state(pid) do
70 | GenServer.call(pid, :state)
71 | end
72 |
73 | @doc """
74 | Configure the Master serial port.
75 |
76 | The following options are available:
77 |
78 | * `tty` - defines the serial port to spawn the Master.
79 | * `timeout` - defines slave timeout.
80 | * `active` - (`true` or `false`) specifies whether data is received as
81 | messages (mailbox) or by calling `request/2`.
82 | * `gen_opts` - defines extra options for the Genserver OTP configuration.
83 | * `uart_opts` - defines extra options for the UART configuration.
84 |
85 | """
86 | def configure(pid, params) do
87 | GenServer.call(pid, {:configure, {params, self()}})
88 | end
89 |
90 | @doc """
91 | Open the Master serial port.
92 | """
93 | def open(pid) do
94 | GenServer.call(pid, :open)
95 | end
96 |
97 | @doc """
98 | Close the Master serial port.
99 | """
100 | def close(pid) do
101 | GenServer.call(pid, :close)
102 | end
103 |
104 | @doc """
105 | Send a request to Modbus RTU Slave.
106 |
107 | `cmd` is one of:
108 | - `{:rc, slave, address, count}` read `count` coils.
109 | - `{:ri, slave, address, count}` read `count` inputs.
110 | - `{:rhr, slave, address, count}` read `count` holding registers.
111 | - `{:rir, slave, address, count}` read `count` input registers.
112 | - `{:fc, slave, address, value}` force single coil.
113 | - `{:phr, slave, address, value}` preset single holding register.
114 | - `{:fc, slave, address, values}` force multiple coils.
115 | - `{:phr, slave, address, values}` preset multiple holding registers.
116 | """
117 | @spec request(atom | pid | {atom, any} | {:via, atom, any}, tuple()) ::
118 | :ok | {:ok, list()} | {:error, String.t()}
119 | def request(pid, cmd) do
120 | GenServer.call(pid, {:request, cmd})
121 | end
122 |
123 | @doc """
124 | Read and parse the last request (if the last request timeouts).
125 | """
126 | @spec read(atom | pid | {atom, any} | {:via, atom, any}) :: any
127 | def read(pid) do
128 | GenServer.call(pid, :read)
129 | end
130 |
131 | def terminate(:normal, _state), do: nil
132 |
133 | def terminate(reason, state) do
134 | Logger.error("(#{__MODULE__}) Error: #{inspect(reason)}, state: #{inspect(state)}")
135 | end
136 |
137 | # Callbacks
138 | def init({params, parent_pid}) do
139 | active = Keyword.get(params, :active, false)
140 | parent_pid = if active, do: parent_pid
141 | timeout = Keyword.get(params, :timeout, @timeout)
142 | tty = Keyword.fetch!(params, :tty)
143 | Logger.debug("(#{__MODULE__}) Starting Modbux Master at \"#{tty}\"")
144 | uart_opts = Keyword.get(params, :uart_opts, speed: @speed, rx_framing_timeout: @timeout)
145 | {:ok, u_pid} = UART.start_link()
146 | UART.open(u_pid, tty, [framing: {Framer, behavior: :master}, active: false] ++ uart_opts)
147 | Logger.debug("(#{__MODULE__}) Reported UART configuration: \"#{inspect(UART.configuration(u_pid))}\"")
148 |
149 | state = %Master{
150 | parent_pid: parent_pid,
151 | tty: tty,
152 | active: active,
153 | uart_pid: u_pid,
154 | timeout: timeout,
155 | uart_opts: uart_opts
156 | }
157 |
158 | {:ok, state}
159 | end
160 |
161 | def handle_call(:state, _from, state), do: {:reply, state, state}
162 |
163 | def handle_call(:read, _from, state) do
164 | res = unless is_nil(state.cmd), do: uart_read(state, state.cmd)
165 | {:reply, res, state}
166 | end
167 |
168 | def handle_call(:open, _from, %{uart_pid: u_pid, tty: tty, uart_opts: uart_opts} = state) do
169 | UART.open(u_pid, tty, [framing: {Framer, behavior: :master}, active: false] ++ uart_opts)
170 | {:reply, :ok, state}
171 | end
172 |
173 | def handle_call(:close, _from, state) do
174 | UART.close(state.uart_pid)
175 | {:reply, :ok, state}
176 | end
177 |
178 | def handle_call({:request, cmd}, _from, state) do
179 | uart_frame = Rtu.pack_req(cmd)
180 | Logger.debug("(#{__MODULE__}) Frame: #{inspect(uart_frame, base: :hex)}")
181 | UART.flush(state.uart_pid)
182 | UART.write(state.uart_pid, uart_frame)
183 |
184 | res =
185 | if state.active do
186 | Task.start_link(__MODULE__, :async_uart_read, [state, cmd])
187 | :ok
188 | else
189 | uart_read(state, cmd)
190 | end
191 |
192 | {:reply, res, %{state | cmd: cmd}}
193 | end
194 |
195 | def handle_call({:configure, {params, parent_pid}}, _from, state) do
196 | active = Keyword.get(params, :active, false)
197 | parent_pid = if active, do: parent_pid
198 | timeout = Keyword.get(params, :timeout, state.timeout)
199 | tty = Keyword.get(params, :tty, state.tty)
200 | uart_opts = Keyword.get(params, :uart_opts, state.uart_opts)
201 | Logger.debug("(#{__MODULE__}) Starting Modbux Master at \"#{tty}\"")
202 |
203 | UART.close(state.uart_pid)
204 | UART.stop(state.uart_pid)
205 |
206 | {:ok, u_pid} = UART.start_link()
207 | UART.open(u_pid, tty, [framing: {Framer, behavior: :master}, active: false] ++ uart_opts)
208 |
209 | new_state = %Master{
210 | parent_pid: parent_pid,
211 | tty: tty,
212 | active: active,
213 | uart_pid: u_pid,
214 | timeout: timeout,
215 | uart_opts: uart_opts
216 | }
217 |
218 | {:reply, :ok, new_state}
219 | end
220 |
221 | # Catch all clause
222 | def handle_info(msg, state) do
223 | Logger.warning("(#{__MODULE__}) Unknown msg: #{inspect(msg)}")
224 | {:noreply, state}
225 | end
226 |
227 | def async_uart_read(state, cmd) do
228 | uart_read(state, cmd) |> notify(state, cmd)
229 | end
230 |
231 | defp uart_read(state, cmd) do
232 | case UART.read(state.uart_pid, state.timeout) do
233 | {:ok, ""} ->
234 | Logger.warning("(#{__MODULE__}) Timeout")
235 | {:error, :timeout}
236 |
237 | {:ok, {:error, reason, msg}} ->
238 | Logger.warning("(#{__MODULE__}) Error in frame: #{inspect(msg)}, reason: #{inspect(reason)}")
239 | {:error, reason}
240 |
241 | {:ok, slave_response} ->
242 | Rtu.parse_res(cmd, slave_response) |> pack_res()
243 |
244 | {:error, reason} ->
245 | Logger.warning("(#{__MODULE__}) Error: #{inspect(reason)}")
246 | {:error, reason}
247 | end
248 | end
249 |
250 | defp notify({:error, reason}, state, cmd),
251 | do: send(state.parent_pid, {:modbus_rtu, {:slave_error, cmd, reason}})
252 |
253 | defp notify({:ok, slave_response}, state, cmd),
254 | do: send(state.parent_pid, {:modbus_rtu, {:slave_response, cmd, slave_response}})
255 |
256 | defp notify(:ok, state, cmd), do: send(state.parent_pid, {:modbus_rtu, {:slave_response, cmd, :ok}})
257 |
258 | defp pack_res(nil), do: :ok
259 | defp pack_res(value) when is_tuple(value), do: value
260 | defp pack_res(value), do: {:ok, value}
261 | end
262 |
--------------------------------------------------------------------------------
/lib/rtu/rtu.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Rtu do
2 | @moduledoc """
3 | RTU message helper, functions that handles RTU responses/requests messages.
4 | """
5 | alias Modbux.Helper
6 | alias Modbux.Request
7 | alias Modbux.Response
8 |
9 | @spec pack_req({:fc | :phr | :rc | :rhr | :ri | :rir, integer, integer, maybe_improper_list | integer}) ::
10 | <<_::16, _::_*8>>
11 | def pack_req(cmd) do
12 | cmd |> Request.pack() |> wrap
13 | end
14 |
15 | @spec parse_req(<<_::16, _::_*8>>) ::
16 | {:einval | :error | :fc | :phr | :rc | :rhr | :ri | :rir, byte, char, [any] | char}
17 | def parse_req(wraped) do
18 | wraped |> unwrap |> Request.parse()
19 | end
20 |
21 | # invalid function
22 | @spec pack_res(
23 | <<_::16, _::_*8>>
24 | | {:fc | :phr | :rc | :rhr | :ri | :rir, integer, any, maybe_improper_list | integer},
25 | any
26 | ) :: <<_::16, _::_*8>>
27 | def pack_res(<>, :einval) do
28 | <> |> wrap
29 | end
30 |
31 | # invalid address
32 | def pack_res(<>, :eaddr) do
33 | <> |> wrap
34 | end
35 |
36 | def pack_res(cmd, values) do
37 | cmd |> Response.pack(values) |> wrap
38 | end
39 |
40 | @spec parse_res(any, <<_::16, _::_*8>>) :: nil | [any] | {:error, any} | {:error, byte, <<_::104>>}
41 | def parse_res(cmd, wraped) do
42 | Response.parse(cmd, wraped |> unwrap)
43 | end
44 |
45 | # exceptions
46 | @spec pack_res({any, integer, integer, integer}) :: <<_::16, _::_*8>>
47 | def pack_res({_reason, slave_id, fc, error_code}) do
48 | <> |> wrap
49 | end
50 |
51 | @spec res_len({:fc | :phr | :rc | :rhr | :ri | :rir, any, any, any}) :: number
52 | def res_len(cmd) do
53 | Response.length(cmd) + 2
54 | end
55 |
56 | @spec req_len({:fc | :phr | :rc | :rhr | :ri | :rir, any, any, any}) :: integer
57 | def req_len(cmd) do
58 | Request.length(cmd) + 2
59 | end
60 |
61 | # CRC is little endian
62 | # http://modbus.org/docs/Modbux_over_serial_line_V1_02.pdf page 13
63 | @spec wrap(binary) :: <<_::16, _::_*8>>
64 | def wrap(payload) do
65 | <> = Helper.crc(payload)
66 | <>
67 | end
68 |
69 | # CRC is little endian
70 | # http://modbus.org/docs/Modbux_over_serial_line_V1_02.pdf page 13
71 | @spec unwrap(<<_::16, _::_*8>>) :: binary
72 | def unwrap(data) do
73 | size = :erlang.byte_size(data) - 2
74 | <> = data
75 | <<^crc_hi, ^crc_lo>> = Helper.crc(payload)
76 | payload
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/rtu/slave.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Rtu.Slave do
2 | @moduledoc """
3 | API for a Modbus RTU Slave device.
4 | """
5 | use GenServer, restart: :transient
6 |
7 | alias Modbux.Model.Shared
8 | alias Modbux.Rtu.{Slave, Framer}
9 | alias Modbux.Rtu
10 | alias Circuits.UART
11 | require Logger
12 |
13 | @timeout 1000
14 | @speed 115_200
15 |
16 | defstruct model_pid: nil,
17 | uart_pid: nil,
18 | tty: nil,
19 | uart_opts: nil,
20 | parent_pid: nil
21 |
22 | @doc """
23 | Starts a Modbus RTU Slave process.
24 |
25 | The following options are available:
26 |
27 | * `tty` - defines the serial port to spawn the Slave.
28 | * `gen_opts` - defines extra options for the Genserver OTP configuration.
29 | * `uart_opts` - defines extra options for the UART configuration (defaults:
30 | [speed: 115200, rx_framing_timeout: 1000]).
31 | * `model` - defines the DB initial state.
32 | * `active` - (`true` or `false`) enable/disable DB updates notifications (mailbox).
33 |
34 | The messages (when active mode is true) have the following form:
35 |
36 | `{:modbus_rtu, {:slave_request, payload}}`
37 |
38 | or
39 |
40 | `{:modbus_rtu, {:slave_error, payload, reason}}`
41 |
42 | The following are some reasons:
43 |
44 | * `:ecrc` - corrupted message (invalid crc).
45 | * `:einval` - invalid function.
46 | * `:eaddr` - invalid memory address requested.
47 |
48 | ## Model (DB)
49 |
50 | The model or data base (DB) defines the slave memory map, the DB is defined by the following syntax:
51 | ```elixir
52 | %{slave_id => %{{memory_type, address_number} => value}}
53 | ```
54 | where:
55 | * `slave_id` - specifies a unique unit address from 1 to 247.
56 | * `memory_type` - specifies the memory between:
57 | * `:c` - Discrete Output Coils.
58 | * `:i` - Discrete Input Contacts.
59 | * `:ir` - Analog Input Registers.
60 | * `:hr` - Analog Output Registers.
61 | * `address_number` - specifies the memory address.
62 | * `value` - the current value from that memory.
63 |
64 | ## Example
65 |
66 | ```elixir
67 | model = %{80 => %{{:c, 20818} => 0, {:hr, 20818} => 0}}
68 | Modbux.Tcp.Server.start_link(model: model, port: 2000)
69 | ```
70 | """
71 | @spec start_link(keyword) :: :ignore | {:error, any} | {:ok, pid}
72 | def start_link(params) do
73 | gen_opts = Keyword.get(params, :gen_opts, [])
74 | GenServer.start_link(__MODULE__, {params, self()}, gen_opts)
75 | end
76 |
77 | @spec stop(atom | pid | {atom, any} | {:via, atom, any}) :: :ok
78 | def stop(pid) do
79 | GenServer.stop(pid)
80 | end
81 |
82 | @doc """
83 | Gets the Slave state.
84 | """
85 | @spec state(atom | pid | {atom, any} | {:via, atom, any}) :: any
86 | def state(pid) do
87 | GenServer.call(pid, :state)
88 | end
89 |
90 | @doc """
91 | Updates the state of the Slave DB.
92 |
93 | `cmd` is a 4 elements tuple, as follows:
94 | - `{:rc, slave, address, count}` read `count` coils.
95 | - `{:ri, slave, address, count}` read `count` inputs.
96 | - `{:rhr, slave, address, count}` read `count` holding registers.
97 | - `{:rir, slave, address, count}` read `count` input registers.
98 | - `{:fc, slave, address, value}` force single coil.
99 | - `{:phr, slave, address, value}` preset single holding register.
100 | - `{:fc, slave, address, values}` force multiple coils.
101 | - `{:phr, slave, address, values}` preset multiple holding registers.
102 | """
103 | @spec request(atom | pid | {atom, any} | {:via, atom, any}, any) :: any
104 | def request(pid, cmd) do
105 | GenServer.call(pid, {:request, cmd})
106 | end
107 |
108 | @doc """
109 | Gets the current state of the Slave DB.
110 | """
111 | @spec get_db(atom | pid | {atom, any} | {:via, atom, any}) :: any
112 | def get_db(pid) do
113 | GenServer.call(pid, :get_db)
114 | end
115 |
116 | @doc """
117 | Send a raw frame through the serial port.
118 | """
119 | @spec raw_write(atom | pid | {atom, any} | {:via, atom, any}, any) :: any
120 | def raw_write(pid, data) do
121 | GenServer.call(pid, {:raw_write, data})
122 | end
123 |
124 | def init({params, parent_pid}) do
125 | parent_pid = if Keyword.get(params, :active, false), do: parent_pid
126 | tty = Keyword.fetch!(params, :tty)
127 | model = Keyword.fetch!(params, :model)
128 | Logger.debug("(#{__MODULE__}) Starting Modbux Slave at \"#{tty}\"")
129 | uart_opts = Keyword.get(params, :uart_opts, speed: @speed, rx_framing_timeout: @timeout)
130 | {:ok, model_pid} = Shared.start_link(model: model)
131 | {:ok, u_pid} = UART.start_link()
132 | UART.open(u_pid, tty, [framing: {Framer, behavior: :slave}] ++ uart_opts)
133 |
134 | state = %Slave{
135 | model_pid: model_pid,
136 | parent_pid: parent_pid,
137 | tty: tty,
138 | uart_pid: u_pid,
139 | uart_opts: uart_opts
140 | }
141 |
142 | {:ok, state}
143 | end
144 |
145 | def terminate(:normal, _state), do: nil
146 |
147 | def terminate(reason, state) do
148 | Logger.error("(#{__MODULE__}) Error: #{inspect(reason)}, state: #{inspect(state)}")
149 | end
150 |
151 | def handle_call(:state, _from, state), do: {:reply, state, state}
152 |
153 | def handle_call({:request, cmd}, _from, state) do
154 | res =
155 | case Shared.apply(state.model_pid, cmd) do
156 | {:ok, values} ->
157 | Logger.debug("(#{__MODULE__}) DB request: #{inspect(cmd)}, #{inspect(values)}")
158 | values
159 |
160 | nil ->
161 | Logger.debug("(#{__MODULE__}) DB update: #{inspect(cmd)}")
162 |
163 | error ->
164 | Logger.debug("(#{__MODULE__}) An error has occur #{inspect(error)}")
165 | error
166 | end
167 |
168 | {:reply, res, state}
169 | end
170 |
171 | def handle_call(:get_db, _from, state) do
172 | {:reply, Shared.state(state.model_pid), state}
173 | end
174 |
175 | def handle_call({:raw_write, data}, _from, state) do
176 | UART.write(state.uart_pid, data)
177 | {:reply, :ok, state}
178 | end
179 |
180 | def handle_info({:circuits_uart, device, {:error, reason, bad_frame}}, state) do
181 | Logger.warning("(#{__MODULE__}) Error with \"#{device}\" received: #{inspect(bad_frame, base: :hex)}, reason: #{reason}")
182 |
183 | case reason do
184 | :einval ->
185 | if valid_slave_id?(state, bad_frame) do
186 | response = Rtu.pack_res(bad_frame, :einval)
187 | Logger.debug("(#{__MODULE__}) Sending error code: #{inspect(response)}, reason: #{reason}")
188 | UART.write(state.uart_pid, response)
189 | end
190 |
191 | _ ->
192 | nil
193 | end
194 |
195 | if !is_nil(state.parent_pid), do: notify(state.parent_pid, reason, bad_frame)
196 | {:noreply, state}
197 | end
198 |
199 | def handle_info({:circuits_uart, _device, {:partial, data}}, state) do
200 | Logger.warning("(#{__MODULE__}) Timeout: #{inspect(data)}")
201 | {:noreply, state}
202 | end
203 |
204 | def handle_info({:circuits_uart, device, modbus_frame}, state) do
205 | Logger.debug("(#{__MODULE__}) Recieved from UART (#{device}): #{inspect(modbus_frame)}")
206 | cmd = Rtu.parse_req(modbus_frame)
207 | Logger.debug("(#{__MODULE__}) Received Modbux request: #{inspect(cmd)}")
208 |
209 | case Shared.apply(state.model_pid, cmd) do
210 | {:ok, values} ->
211 | response = Rtu.pack_res(cmd, values)
212 | if !is_nil(state.parent_pid), do: notify(state.parent_pid, nil, cmd)
213 | UART.write(state.uart_pid, response)
214 |
215 | {:error, reason} ->
216 | response = Rtu.pack_res(modbus_frame, reason)
217 | if !is_nil(state.parent_pid), do: notify(state.parent_pid, reason, cmd)
218 | UART.write(state.uart_pid, response)
219 |
220 | Logger.debug(
221 | "(#{__MODULE__}) An error has occur for cmd: #{inspect(cmd)}, response #{inspect(response)}"
222 | )
223 |
224 | nil ->
225 | nil
226 | end
227 |
228 | {:noreply, state}
229 | end
230 |
231 | # Catch all clause
232 | def handle_info(msg, state) do
233 | Logger.warning("(#{__MODULE__}) Unknown msg: #{inspect(msg)}")
234 | {:noreply, state}
235 | end
236 |
237 | defp valid_slave_id?(state, <>) do
238 | state.model_pid
239 | |> Shared.state()
240 | |> Map.has_key?(slave_id)
241 | end
242 |
243 | defp notify(pid, nil, cmd) do
244 | send(pid, {:modbus_rtu, {:slave_request, cmd}})
245 | end
246 |
247 | defp notify(pid, reason, cmd) do
248 | send(pid, {:modbus_rtu, {:slave_error, cmd, reason}})
249 | end
250 | end
251 |
--------------------------------------------------------------------------------
/lib/tcp/client.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Tcp.Client do
2 | @moduledoc """
3 | API for Modbus TCP Client.
4 | """
5 | alias Modbux.Tcp.Client
6 | alias Modbux.Tcp
7 | use GenServer, restart: :permanent, shutdown: 500
8 | require Logger
9 |
10 | @timeout 2000
11 | @port 502
12 | @ip {0, 0, 0, 0}
13 | @active false
14 | @to 2000
15 |
16 | defstruct ip: nil,
17 | tcp_port: nil,
18 | socket: nil,
19 | timeout: @to,
20 | active: false,
21 | transid: 0,
22 | status: nil,
23 | d_pid: nil,
24 | msg_len: 0,
25 | pending_msg: %{},
26 | cmd: nil
27 |
28 | @type client_option ::
29 | {:ip, {byte(), byte(), byte(), byte()}}
30 | | {:active, boolean}
31 | | {:tcp_port, non_neg_integer}
32 | | {:timeout, non_neg_integer}
33 |
34 | @doc """
35 | Starts a Modbus TCP Client process.
36 |
37 | The following options are available:
38 |
39 | * `ip` - is the internet address of the desired Modbux TCP Server.
40 | * `tcp_port` - is the desired Modbux TCP Server tcp port number.
41 | * `timeout` - is the connection timeout.
42 | * `active` - (`true` or `false`) specifies whether data is received as
43 | messages (mailbox) or by calling `confirmation/1` each time `request/2` is called.
44 |
45 | The messages (when active mode is true) have the following form:
46 |
47 | `{:modbus_tcp, cmd, values}`
48 |
49 | ## Example
50 |
51 | ```elixir
52 | Modbux.Tcp.Client.start_link(ip: {10,77,0,2}, port: 502, timeout: 2000, active: true)
53 | ```
54 | """
55 | def start_link(params, opts \\ []) do
56 | GenServer.start_link(__MODULE__, params, opts)
57 | end
58 |
59 | @doc """
60 | Stops the Client.
61 | """
62 | def stop(pid) do
63 | GenServer.stop(pid)
64 | end
65 |
66 | @doc """
67 | Gets the state of the Client.
68 | """
69 | def state(pid) do
70 | GenServer.call(pid, :state)
71 | end
72 |
73 | @doc """
74 | Configure the Client (`status` must be `:closed`).
75 |
76 | The following options are available:
77 |
78 | * `ip` - is the internet address of the desired Modbux TCP Server.
79 | * `tcp_port` - is the Modbux TCP Server tcp port number .
80 | * `timeout` - is the connection timeout.
81 | * `active` - (`true` or `false`) specifies whether data is received as
82 | messages (mailbox) or by calling `confirmation/1` each time `request/2` is called.
83 | """
84 | def configure(pid, params) do
85 | GenServer.call(pid, {:configure, params})
86 | end
87 |
88 | @doc """
89 | Connect the Client to a Server.
90 | """
91 | def connect(pid) do
92 | GenServer.call(pid, :connect)
93 | end
94 |
95 | @doc """
96 | Close the tcp port of the Client.
97 | """
98 | def close(pid) do
99 | GenServer.call(pid, :close)
100 | end
101 |
102 | @doc """
103 | Send a request to Modbux TCP Server.
104 |
105 | `cmd` is a 4 elements tuple, as follows:
106 | - `{:rc, slave, address, count}` read `count` coils.
107 | - `{:ri, slave, address, count}` read `count` inputs.
108 | - `{:rhr, slave, address, count}` read `count` holding registers.
109 | - `{:rir, slave, address, count}` read `count` input registers.
110 | - `{:fc, slave, address, value}` force single coil.
111 | - `{:phr, slave, address, value}` preset single holding register.
112 | - `{:fc, slave, address, values}` force multiple coils.
113 | - `{:phr, slave, address, values}` preset multiple holding registers.
114 | """
115 | def request(pid, cmd) do
116 | GenServer.call(pid, {:request, cmd})
117 | end
118 |
119 | @doc """
120 | In passive mode (active: false), reads the confirmation of the connected Modbux Server.
121 | """
122 | def confirmation(pid) do
123 | GenServer.call(pid, :confirmation)
124 | end
125 |
126 | @doc """
127 | In passive mode (active: false), flushed the pending messages.
128 | """
129 | def flush(pid) do
130 | GenServer.call(pid, :flush)
131 | end
132 |
133 | # callbacks
134 | def init(args) do
135 | port = args[:tcp_port] || @port
136 | ip = args[:ip] || @ip
137 | timeout = args[:timeout] || @timeout
138 | status = :closed
139 |
140 | active =
141 | if args[:active] == nil do
142 | @active
143 | else
144 | args[:active]
145 | end
146 |
147 | state = %Client{ip: ip, tcp_port: port, timeout: timeout, status: status, active: active}
148 | {:ok, state}
149 | end
150 |
151 | def handle_call(:state, from, state) do
152 | Logger.debug("(#{__MODULE__}, :state) from: #{inspect(from)}")
153 | {:reply, state, state}
154 | end
155 |
156 | def handle_call({:configure, args}, _from, state) do
157 | case state.status do
158 | :closed ->
159 | port = args[:tcp_port] || state.tcp_port
160 | ip = args[:ip] || state.ip
161 | timeout = args[:timeout] || state.timeout
162 | d_pid = args[:d_pid] || state.d_pid
163 |
164 | active =
165 | if args[:active] == nil do
166 | state.active
167 | else
168 | args[:active]
169 | end
170 |
171 | new_state = %Client{state | ip: ip, tcp_port: port, timeout: timeout, active: active, d_pid: d_pid}
172 | {:reply, :ok, new_state}
173 |
174 | _ ->
175 | {:reply, :error, state}
176 | end
177 | end
178 |
179 | def handle_call(:connect, {from, _ref}, state) do
180 | Logger.debug("(#{__MODULE__}, :connect) state: #{inspect(state)}")
181 | Logger.debug("(#{__MODULE__}, :connect) from: #{inspect(from)}")
182 |
183 | case :gen_tcp.connect(
184 | state.ip,
185 | state.tcp_port,
186 | [:binary, packet: :raw, active: state.active],
187 | state.timeout
188 | ) do
189 | {:ok, socket} ->
190 | ctrl_pid =
191 | if state.d_pid == nil do
192 | from
193 | else
194 | state.d_pid
195 | end
196 |
197 | # state
198 | new_state = %Client{state | socket: socket, status: :connected, d_pid: ctrl_pid}
199 | {:reply, :ok, new_state}
200 |
201 | {:error, reason} ->
202 | Logger.error("(#{__MODULE__}, :connect) reason #{inspect(reason)}")
203 | # state
204 | {:reply, {:error, reason}, state}
205 | end
206 | end
207 |
208 | def handle_call(:close, _from, state) do
209 | Logger.debug("(#{__MODULE__}, :close) state: #{inspect(state)}")
210 |
211 | if state.socket != nil do
212 | new_state = close_socket(state)
213 | {:reply, :ok, new_state}
214 | else
215 | Logger.error("(#{__MODULE__}, :close) No port to close")
216 | # state
217 | {:reply, {:error, :closed}, state}
218 | end
219 | end
220 |
221 | def handle_call({:request, cmd}, _from, state) do
222 | Logger.debug("(#{__MODULE__}, :request) state: #{inspect(state)}")
223 |
224 | case state.status do
225 | :connected ->
226 | request = Tcp.pack_req(cmd, state.transid)
227 | length = Tcp.res_len(cmd)
228 |
229 | case :gen_tcp.send(state.socket, request) do
230 | :ok ->
231 | new_state =
232 | if state.active do
233 | new_msg = Map.put(state.pending_msg, state.transid, cmd)
234 |
235 | n_msg =
236 | if state.transid + 1 > 0xFFFF do
237 | 0
238 | else
239 | state.transid + 1
240 | end
241 |
242 | %Client{state | msg_len: length, cmd: cmd, pending_msg: new_msg, transid: n_msg}
243 | else
244 | %Client{state | msg_len: length, cmd: cmd}
245 | end
246 |
247 | {:reply, :ok, new_state}
248 |
249 | {:error, :closed} ->
250 | new_state = close_socket(state)
251 | {:reply, {:error, :closed}, new_state}
252 |
253 | {:error, reason} ->
254 | {:reply, {:error, reason}, state}
255 | end
256 |
257 | :closed ->
258 | {:reply, {:error, :closed}, state}
259 | end
260 | end
261 |
262 | # only in passive mode
263 | def handle_call(:confirmation, _from, state) do
264 | Logger.debug("(#{__MODULE__}, :confirmation) state: #{inspect(state)}")
265 |
266 | if state.active do
267 | {:reply, :error, state}
268 | else
269 | case state.status do
270 | :connected ->
271 | case :gen_tcp.recv(state.socket, state.msg_len, state.timeout) do
272 | {:ok, response} ->
273 | values = Tcp.parse_res(state.cmd, response, state.transid)
274 | Logger.debug("(#{__MODULE__}, :confirmation) response: #{inspect(response)}")
275 |
276 | n_msg =
277 | if state.transid + 1 > 0xFFFF do
278 | 0
279 | else
280 | state.transid + 1
281 | end
282 |
283 | new_state = %Client{state | transid: n_msg, cmd: nil, msg_len: 0}
284 |
285 | case values do
286 | # escribió algo
287 | nil ->
288 | {:reply, :ok, new_state}
289 |
290 | # leemos algo
291 | _ ->
292 | {:reply, {:ok, values}, new_state}
293 | end
294 |
295 | {:error, reason} ->
296 | Logger.error("(#{__MODULE__}, :confirmation) reason: #{inspect(reason)}")
297 | # cerrar?
298 | new_state = close_socket(state)
299 | new_state = %Client{new_state | cmd: nil, msg_len: 0}
300 | {:reply, {:error, reason}, new_state}
301 | end
302 |
303 | :closed ->
304 | {:reply, {:error, :closed}, state}
305 | end
306 | end
307 | end
308 |
309 | def handle_call(:flush, _from, state) do
310 | new_state = %Client{state | pending_msg: %{}}
311 | {:reply, {:ok, state.pending_msg}, new_state}
312 | end
313 |
314 | # only for active mode (active: true)
315 | def handle_info({:tcp, _port, response}, state) do
316 | Logger.debug("(#{__MODULE__}, :message_active) response: #{inspect(response)}")
317 | Logger.debug("(#{__MODULE__}, :message_active) state: #{inspect(state)}")
318 |
319 | h = :binary.at(response, 0)
320 | l = :binary.at(response, 1)
321 | transid = h * 256 + l
322 | Logger.debug("(#{__MODULE__}, :message_active) transid: #{inspect(transid)}")
323 |
324 | case Map.fetch(state.pending_msg, transid) do
325 | :error ->
326 | Logger.error("(#{__MODULE__}, :message_active) unknown transaction id")
327 | {:noreply, state}
328 |
329 | {:ok, cmd} ->
330 | values = Tcp.parse_res(cmd, response, transid)
331 | msg = {:modbus_tcp, cmd, values}
332 | send(state.d_pid, msg)
333 | new_pending_msg = Map.delete(state.pending_msg, transid)
334 | new_state = %Client{state | cmd: nil, msg_len: 0, pending_msg: new_pending_msg}
335 | {:noreply, new_state}
336 | end
337 | end
338 |
339 | def handle_info({:tcp_closed, _port}, state) do
340 | Logger.info("(#{__MODULE__}, :tcp_close) Server close the port")
341 | new_state = close_socket(state)
342 | {:noreply, new_state}
343 | end
344 |
345 | def handle_info(msg, state) do
346 | Logger.error("(#{__MODULE__}, :random_msg) msg: #{inspect(msg)}")
347 | {:noreply, state}
348 | end
349 |
350 | defp close_socket(state) do
351 | :ok = :gen_tcp.close(state.socket)
352 | new_state = %Client{state | socket: nil, status: :closed}
353 | new_state
354 | end
355 | end
356 |
--------------------------------------------------------------------------------
/lib/tcp/server/server.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Tcp.Server do
2 | @moduledoc """
3 | API for Modbus TCP Server.
4 | """
5 | alias Modbux.Tcp.Server
6 | alias Modbux.Model.Shared
7 | use GenServer, restart: :transient
8 | require Logger
9 |
10 | @port 502
11 | @to :infinity
12 |
13 | defstruct ip: nil,
14 | model_pid: nil,
15 | tcp_port: nil,
16 | timeout: nil,
17 | listener: nil,
18 | parent_pid: nil,
19 | sup_pid: nil,
20 | acceptor_pid: nil
21 |
22 | @doc """
23 | Starts a Modbus TCP Server process.
24 |
25 | The following options are available:
26 |
27 | * `port` - is the Modbux TCP Server tcp port number.
28 | * `timeout` - is the connection timeout.
29 | * `model` - defines the DB initial state.
30 | * `sup_otps` - server supervisor OTP options.
31 | * `active` - (`true` or `false`) enable/disable DB updates notifications (mailbox).
32 |
33 | The messages (when active mode is true) have the following form:
34 |
35 | `{:modbus_tcp, {:slave_request, payload}}`
36 |
37 | ## Model (DB)
38 |
39 | The model or data base (DB) defines the server memory map, the DB is defined by the following syntax:
40 | ```elixir
41 | %{slave_id => %{{memory_type, address_number} => value}}
42 | ```
43 | where:
44 | * `slave_id` - specifies a unique unit address from 1 to 247.
45 | * `memory_type` - specifies the memory between:
46 | * `:c` - Discrete Output Coils.
47 | * `:i` - Discrete Input Contacts.
48 | * `:ir` - Analog Input Registers.
49 | * `:hr` - Analog Output Registers.
50 | * `address_number` - specifies the memory address.
51 | * `value` - the current value from that memory.
52 |
53 | ## Example
54 |
55 | ```elixir
56 | model = %{80 => %{{:c, 20818} => 0, {:hr, 20818} => 0}}
57 | Modbux.Tcp.Server.start_link(model: model, port: 2000)
58 | ```
59 | """
60 | @spec start_link(any, [
61 | {:debug, [:log | :statistics | :trace | {any, any}]}
62 | | {:hibernate_after, :infinity | non_neg_integer}
63 | | {:name, atom | {:global, any} | {:via, atom, any}}
64 | | {:spawn_opt,
65 | :link
66 | | :monitor
67 | | {:fullsweep_after, non_neg_integer}
68 | | {:min_bin_vheap_size, non_neg_integer}
69 | | {:min_heap_size, non_neg_integer}
70 | | {:priority, :high | :low | :normal}}
71 | | {:timeout, :infinity | non_neg_integer}
72 | ]) :: :ignore | {:error, any} | {:ok, pid}
73 | def start_link(params, opts \\ []) do
74 | GenServer.start_link(__MODULE__, {params, self()}, opts)
75 | end
76 |
77 | @spec stop(atom | pid | {atom, any} | {:via, atom, any}) :: :ok
78 | def stop(pid) do
79 | GenServer.stop(pid)
80 | end
81 |
82 | @doc """
83 | Updates the state of the Server DB.
84 |
85 | `cmd` is a 4 elements tuple, as follows:
86 | - `{:rc, slave, address, count}` read `count` coils.
87 | - `{:ri, slave, address, count}` read `count` inputs.
88 | - `{:rhr, slave, address, count}` read `count` holding registers.
89 | - `{:rir, slave, address, count}` read `count` input registers.
90 | - `{:fc, slave, address, value}` force single coil.
91 | - `{:phr, slave, address, value}` preset single holding register.
92 | - `{:fc, slave, address, values}` force multiple coils.
93 | - `{:phr, slave, address, values}` preset multiple holding registers.
94 | """
95 | @spec update(atom | pid | {atom, any} | {:via, atom, any}, any) :: any
96 | def update(pid, cmd) do
97 | GenServer.call(pid, {:update, cmd})
98 | end
99 |
100 | @doc """
101 | Gets the current state of the Server DB.
102 | """
103 | @spec get_db(atom | pid | {atom, any} | {:via, atom, any}) :: any
104 | def get_db(pid) do
105 | GenServer.call(pid, :get_db)
106 | end
107 |
108 | def init({params, parent_pid}) do
109 | port = Keyword.get(params, :port, @port)
110 | timeout = Keyword.get(params, :timeout, @to)
111 | parent_pid = if Keyword.get(params, :active, false), do: parent_pid
112 | model = Keyword.fetch!(params, :model)
113 | {:ok, model_pid} = Shared.start_link(model: model)
114 | sup_opts = Keyword.get(params, :sup_opts, [])
115 | {:ok, sup_pid} = Server.Supervisor.start_link(sup_opts)
116 |
117 | state = %Server{
118 | tcp_port: port,
119 | model_pid: model_pid,
120 | timeout: timeout,
121 | parent_pid: parent_pid,
122 | sup_pid: sup_pid
123 | }
124 |
125 | {:ok, state, {:continue, :setup}}
126 | end
127 |
128 | def terminate(:normal, _state), do: nil
129 |
130 | def terminate(reason, state) do
131 | Logger.error("(#{__MODULE__}) Error: #{inspect(reason)}")
132 | :gen_tcp.close(state.listener)
133 | end
134 |
135 | def handle_call({:update, request}, _from, state) do
136 | res =
137 | case Shared.apply(state.model_pid, request) do
138 | {:ok, values} ->
139 | Logger.debug("(#{__MODULE__}) DB request: #{inspect(request)}, #{inspect(values)}")
140 | values
141 |
142 | nil ->
143 | Logger.debug("(#{__MODULE__}) DB update: #{inspect(request)}")
144 |
145 | error ->
146 | Logger.debug("(#{__MODULE__}) An error has occur")
147 | error
148 | end
149 |
150 | {:reply, res, state}
151 | end
152 |
153 | def handle_call(:get_db, _from, state) do
154 | {:reply, Shared.state(state.model_pid), state}
155 | end
156 |
157 | def handle_continue(:setup, state) do
158 | new_state = listener_setup(state)
159 | {:noreply, new_state}
160 | end
161 |
162 | defp listener_setup(state) do
163 | case :gen_tcp.listen(state.tcp_port, [:binary, packet: :raw, active: true, reuseaddr: true]) do
164 | {:ok, listener} ->
165 | {:ok, {ip, _port}} = :inet.sockname(listener)
166 | accept = Task.async(fn -> accept(state, listener) end)
167 | %Server{state | ip: ip, acceptor_pid: accept, listener: listener}
168 |
169 | {:error, :eaddrinuse} ->
170 | Logger.error("(#{__MODULE__}) Error: A listener is still alive")
171 | close_alive_sockets(state.tcp_port)
172 | Process.sleep(100)
173 | listener_setup(state)
174 |
175 | {:error, reason} ->
176 | Logger.error("(#{__MODULE__}) Error in Listen: #{reason}")
177 | Process.sleep(1000)
178 | listener_setup(state)
179 | end
180 | end
181 |
182 | def close_alive_sockets(port) do
183 | Port.list()
184 | |> Enum.filter(fn x -> Port.info(x)[:name] == ~c"tcp_inet" end)
185 | |> Enum.filter(fn x ->
186 | {:ok, {{0, 0, 0, 0}, port}} == :inet.sockname(x) || {:ok, {{127, 0, 0, 1}, port}} == :inet.sockname(x)
187 | end)
188 | |> Enum.each(fn x -> :gen_tcp.close(x) end)
189 | end
190 |
191 | defp accept(state, listener) do
192 | case :gen_tcp.accept(listener) do
193 | {:ok, socket} ->
194 | {:ok, pid} =
195 | Server.Supervisor.start_child(state.sup_pid, Server.Handler, [
196 | socket,
197 | state.model_pid,
198 | state.parent_pid
199 | ])
200 |
201 | Logger.debug("(#{__MODULE__}) New Client socket: #{inspect(socket)}, pid: #{inspect(pid)}")
202 |
203 | case :gen_tcp.controlling_process(socket, pid) do
204 | :ok ->
205 | nil
206 |
207 | error ->
208 | Logger.error("(#{__MODULE__}) Error in controlling process: #{inspect(error)}")
209 | end
210 |
211 | accept(state, listener)
212 |
213 | {:error, reason} ->
214 | Logger.error("(#{__MODULE__}) Error Accept: #{inspect(reason)}")
215 | exit(reason)
216 | end
217 | end
218 | end
219 |
--------------------------------------------------------------------------------
/lib/tcp/server/server_handler.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Tcp.Server.Handler do
2 | @moduledoc """
3 | A worker for each Modbus Client, handles Client requests.
4 | """
5 | alias Modbux.Tcp.Server.Handler
6 | alias Modbux.Model.Shared
7 | alias Modbux.Tcp
8 | use GenServer, restart: :temporary
9 | require Logger
10 |
11 | defstruct model_pid: nil,
12 | parent_pid: nil,
13 | socket: nil
14 |
15 | @spec start_link([...]) :: :ignore | {:error, any} | {:ok, pid}
16 | def start_link([socket, model_pid, parent_pid]) do
17 | GenServer.start_link(__MODULE__, [socket, model_pid, parent_pid])
18 | end
19 |
20 | def init([socket, model_pid, parent_pid]) do
21 | {:ok, %Handler{model_pid: model_pid, socket: socket, parent_pid: parent_pid}}
22 | end
23 |
24 | def handle_info({:tcp, socket, data}, state) do
25 | Logger.debug("(#{__MODULE__}) Received: #{inspect(data, base: :hex)} ")
26 | {cmd, transid} = Tcp.parse_req(data)
27 | Logger.debug("(#{__MODULE__}) Received Modbux request: #{inspect({cmd, transid})}")
28 |
29 | case Shared.apply(state.model_pid, cmd) do
30 | {:ok, values} ->
31 | Logger.debug("(#{__MODULE__}) msg send")
32 | resp = Tcp.pack_res(cmd, values, transid)
33 | if !is_nil(state.parent_pid), do: notify(state.parent_pid, cmd)
34 | :ok = :gen_tcp.send(socket, resp)
35 |
36 | {:error, reason} ->
37 | Logger.debug("(#{__MODULE__}) An error has occur: #{reason}")
38 |
39 | nil ->
40 | Logger.debug("(#{__MODULE__}) Message for another slave")
41 | end
42 |
43 | {:noreply, state}
44 | end
45 |
46 | def handle_info({:tcp_closed, _socket}, state) do
47 | Logger.debug("(#{__MODULE__}) Socket closed")
48 | {:stop, :normal, state}
49 | end
50 |
51 | def handle_info({:tcp_error, socket, reason}, state) do
52 | Logger.error("(#{__MODULE__}) TCP error: #{reason}")
53 | :gen_tcp.close(socket)
54 | {:stop, :normal, state}
55 | end
56 |
57 | def handle_info(:timeout, state) do
58 | Logger.debug("(#{__MODULE__}) timeout")
59 | :gen_tcp.close(state.socket)
60 | {:stop, :normal, state}
61 | end
62 |
63 | def terminate(:normal, _state), do: nil
64 |
65 | def terminate(reason, state) do
66 | Logger.error("(#{__MODULE__}) Error: #{inspect(reason)}")
67 | :gen_tcp.close(state.socket)
68 | end
69 |
70 | defp notify(pid, cmd) do
71 | send(pid, {:modbus_tcp, {:server_request, cmd}})
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/tcp/server/server_supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Tcp.Server.Supervisor do
2 | @moduledoc """
3 | Spawns and supervises each Modbus Client handler.
4 | """
5 | use DynamicSupervisor
6 |
7 | @spec start_link([
8 | {:debug, [:log | :statistics | :trace | {any, any}]}
9 | | {:hibernate_after, :infinity | non_neg_integer}
10 | | {:name, atom | {:global, any} | {:via, atom, any}}
11 | | {:spawn_opt,
12 | :link
13 | | :monitor
14 | | {:fullsweep_after, non_neg_integer}
15 | | {:min_bin_vheap_size, non_neg_integer}
16 | | {:min_heap_size, non_neg_integer}
17 | | {:priority, :high | :low | :normal}}
18 | | {:timeout, :infinity | non_neg_integer}
19 | ]) :: :ignore | {:error, any} | {:ok, pid}
20 | def start_link(opts) do
21 | DynamicSupervisor.start_link(__MODULE__, [], opts)
22 | end
23 |
24 | @spec start_child(atom | pid | {atom, any} | {:via, atom, any}, atom, any) ::
25 | :ignore | {:error, any} | {:ok, pid} | {:ok, pid, any}
26 | def start_child(sup_pid, module, args) do
27 | DynamicSupervisor.start_child(sup_pid, {module, args})
28 | end
29 |
30 | def init([]) do
31 | DynamicSupervisor.init(strategy: :one_for_one)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/tcp/tcp.ex:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Tcp do
2 | @moduledoc """
3 | Tcp message helper, functions that handles TCP responses/requests messages.
4 | """
5 | alias Modbux.Request
6 | alias Modbux.Response
7 | require Logger
8 |
9 | @spec pack_req(
10 | {:fc | :phr | :rc | :rhr | :ri | :rir, integer, integer, maybe_improper_list | integer},
11 | integer
12 | ) :: <<_::48, _::_*8>>
13 | def pack_req(cmd, transid) do
14 | cmd |> Request.pack() |> wrap(transid)
15 | end
16 |
17 | @spec parse_req(<<_::48, _::_*8>>) ::
18 | {{:einval | :error | :fc | :phr | :rc | :rhr | :ri | :rir, byte, char, [any] | char}, char}
19 | def parse_req(wraped) do
20 | {pack, transid} = wraped |> unwrap
21 | {pack |> Request.parse(), transid}
22 | end
23 |
24 | @spec pack_res(
25 | {:fc | :phr | :rc | :rhr | :ri | :rir, integer, any, maybe_improper_list | integer},
26 | any,
27 | integer
28 | ) :: <<_::48, _::_*8>>
29 | def pack_res(cmd, values, transid) do
30 | cmd |> Response.pack(values) |> wrap(transid)
31 | end
32 |
33 | @spec parse_res(any, <<_::48, _::_*8>>, char) :: nil | [any] | {:error, any} | {:error, byte, <<_::104>>}
34 | def parse_res(cmd, wraped, transid) do
35 | Response.parse(cmd, wraped |> unwrap(transid))
36 | end
37 |
38 | @spec res_len({:fc | :phr | :rc | :rhr | :ri | :rir, any, any, any}) :: number
39 | def res_len(cmd) do
40 | Response.length(cmd) + 6
41 | end
42 |
43 | @spec req_len({:fc | :phr | :rc | :rhr | :ri | :rir, any, any, any}) :: integer
44 | def req_len(cmd) do
45 | Request.length(cmd) + 6
46 | end
47 |
48 | @spec wrap(binary, integer) :: <<_::48, _::_*8>>
49 | def wrap(payload, transid) do
50 | size = :erlang.byte_size(payload)
51 | <>
52 | end
53 |
54 | @spec unwrap(<<_::48, _::_*8>>, char) :: nil | binary
55 | def unwrap(<> = msg, transid) do
56 | r_size = :erlang.byte_size(payload)
57 |
58 | check_protocol_identifier(protocol_id_h, protocol_id_l)
59 |
60 | data =
61 | if size == r_size do
62 | payload
63 | else
64 | Logger.error(
65 | "(#{__MODULE__}) size = #{size}, payload_size = #{r_size}, msg = #{inspect(msg, base: :hex)}"
66 | )
67 |
68 | nil
69 | end
70 |
71 | data
72 | end
73 |
74 | @spec unwrap(<<_::48, _::_*8>>) :: {binary(), char()}
75 | def unwrap(<>) do
76 | ^size = :erlang.byte_size(payload)
77 | check_protocol_identifier(protocol_id_h, protocol_id_l)
78 | {payload, transid}
79 | end
80 |
81 | def unwrap(inv_data) do
82 | Logger.error("(#{__MODULE__}) invalid data: #{inspect(inv_data, base: :hex)}")
83 | raise("(#{__MODULE__}) invalid data: #{inspect(inv_data)}")
84 | end
85 |
86 | # Protocol identifier -> MODBUS protocol = 0x00, 0x00
87 | defp check_protocol_identifier(0, 0), do: :ok
88 | defp check_protocol_identifier(protocol_id_h, protocol_id_l),
89 | do: Logger.warning("(#{__MODULE__}) Protocol Identifier: #{inspect(<>, base: :hex)}")
90 | end
91 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Modbux.Mixfile do
2 | use Mix.Project
3 |
4 | @version "0.3.14"
5 | @source_url "https://github.com/valiot/modbux"
6 |
7 |
8 | def project do
9 | [
10 | app: :modbux,
11 | version: @version,
12 | elixir: "~> 1.8",
13 | name: "Modbux",
14 | docs: docs(),
15 | description: description(),
16 | package: package(),
17 | source_url: @source_url,
18 | start_permanent: Mix.env() == :prod,
19 | deps: deps(),
20 | aliases: aliases(),
21 | ]
22 | end
23 |
24 | def application do
25 | [
26 | extra_applications: [:logger]
27 | ]
28 | end
29 |
30 | defp deps do
31 | [
32 | {:circuits_uart, "~> 1.3"},
33 | {:ex_doc, "~> 0.19", only: :dev},
34 | {:ring_logger, "~> 0.4"}
35 | ]
36 | end
37 |
38 | defp extras(), do: ["README.md"]
39 |
40 | defp docs do
41 | [
42 | main: "readme",
43 | source_ref: "v#{@version}",
44 | canonical: "http://hexdocs.pm/modbux",
45 | logo: "assets/images/valiot-logo-blue.png",
46 | source_url: @source_url,
47 | extras: extras(),
48 | groups_for_modules: [
49 | "Modbus RTU": [
50 | Modbux.Rtu.Master,
51 | Modbux.Rtu.Slave,
52 | ],
53 | "Modbus TCP": [
54 | Modbux.Tcp.Client,
55 | Modbux.Tcp.Server,
56 | ],
57 | ]
58 | ]
59 | end
60 |
61 | defp description do
62 | "Modbus for network and serial communication, this library implements TCP (Client & Server) and RTU (Master & Slave) protocols."
63 | end
64 |
65 | defp aliases do
66 | [docs: ["docs", ©_images/1]]
67 | end
68 |
69 | defp copy_images(_) do
70 | File.ls!("assets/images")
71 | |> Enum.each(fn x ->
72 | File.cp!("assets/images/#{x}", "doc/assets/#{x}")
73 | end)
74 | end
75 |
76 | defp package do
77 | [
78 | files: [
79 | "lib",
80 | "test",
81 | "mix.exs",
82 | "README.md",
83 | "LICENSE"
84 | ],
85 | maintainers: ["valiot"],
86 | licenses: ["MIT"],
87 | links: %{"GitHub" => "https://github.com/valiot/modbux"}
88 | ]
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "circuits_uart": {:hex, :circuits_uart, "1.5.3", "fb8e9cb8dcdcb987497b889d9bb51ee2932486afdf9961da2c3c699598da02d3", [:mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9bd14660cc9f2c29500012f1772130e00b7edca27e2052d20360750185fc5d0f"},
3 | "circular_buffer": {:hex, :circular_buffer, "0.4.1", "477f370fd8cfe1787b0a1bade6208bbd274b34f1610e41f1180ba756a7679839", [:mix], [], "hexpm", "633ef2e059dde0d7b89bbab13b1da9d04c6685e80e68fbdf41282d4fae746b72"},
4 | "earmark": {:hex, :earmark, "1.3.5", "0db71c8290b5bc81cb0101a2a507a76dca659513984d683119ee722828b424f6", [:mix], [], "hexpm", "762b999fd414fb41e297944228aa1de2cd4a3876a07f968c8b11d1e9a2190d07"},
5 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
6 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
7 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
8 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
9 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
10 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
11 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
12 | "ring_logger": {:hex, :ring_logger, "0.11.3", "08a423e5d088d5bf41bf1bdf23b804f96279624ef09ca6b5b5012c32014b38a8", [:mix], [{:circular_buffer, "~> 0.4.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}], "hexpm", "b870a23b8f8329aeadcbee3429e5e43942e4134d810a71e9a5a2f0567b9ce78d"},
13 | }
14 |
--------------------------------------------------------------------------------
/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 | Please include a summary of the change and/or which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
4 |
5 | Fixes # (issue)
6 |
7 | ## Type of change
8 |
9 | Please delete options that are not relevant.
10 |
11 | - [ ] Bug fix (non-breaking change which fixes an issue)
12 | - [ ] New feature (non-breaking change which adds functionality)
13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
14 | - [ ] This change requires a documentation update
15 |
16 | # How Has This Been Tested?
17 |
18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
19 |
20 | - [ ] Test A
21 | - [ ] Test B
22 |
23 | **Test Configuration**:
24 | * Firmware version:
25 | * Hardware:
26 | * Toolchain:
27 | * SDK:
28 |
29 | # Checklist:
30 |
31 | - [ ] My code follows the style guidelines of this project
32 | - [ ] I have performed a self-review of my own code
33 | - [ ] I have commented my code, particularly in hard-to-understand areas
34 | - [ ] I have made corresponding changes to the documentation
35 | - [ ] My changes generate no new warnings
36 | - [ ] I have added tests that prove my fix is effective or that my feature works
37 | - [ ] New and existing unit tests pass locally with my changes
38 | - [ ] Any dependent changes have been merged and published in downstream modules
39 |
--------------------------------------------------------------------------------
/test/f01_test.exs:
--------------------------------------------------------------------------------
1 | defmodule F01Test do
2 | use ExUnit.Case
3 | import TestHelper
4 |
5 | test "Read 0 from Single Coil" do
6 | model0 = %{0x50 => %{{:c, 0x5152} => 0}}
7 | cmd0 = {:rc, 0x50, 0x5152, 1}
8 | req0 = <<0x50, 1, 0x51, 0x52, 0, 1>>
9 | res0 = <<0x50, 1, 1, 0x00>>
10 | val0 = [0]
11 | pp1(cmd0, req0, res0, val0, model0)
12 | end
13 |
14 | test "Read 1 from Single Coil" do
15 | model0 = %{0x50 => %{{:c, 0x5152} => 1}}
16 | cmd0 = {:rc, 0x50, 0x5152, 1}
17 | req0 = <<0x50, 1, 0x51, 0x52, 0, 1>>
18 | res0 = <<0x50, 1, 1, 0x01>>
19 | val0 = [1]
20 | pp1(cmd0, req0, res0, val0, model0)
21 | end
22 |
23 | test "Read 011 from Multiple Coils" do
24 | model0 = %{
25 | 0x50 => %{
26 | {:c, 0x5152} => 0,
27 | {:c, 0x5153} => 1,
28 | {:c, 0x5154} => 1
29 | }
30 | }
31 |
32 | cmd0 = {:rc, 0x50, 0x5152, 3}
33 | req0 = <<0x50, 1, 0x51, 0x52, 0, 3>>
34 | res0 = <<0x50, 1, 1, 0x06>>
35 | val0 = [0, 1, 1]
36 | pp1(cmd0, req0, res0, val0, model0)
37 | end
38 |
39 | test "Read 0011 1100 0101 from Multiple Coils" do
40 | model0 = %{
41 | 0x50 => %{
42 | {:c, 0x5152} => 0,
43 | {:c, 0x5153} => 0,
44 | {:c, 0x5154} => 1,
45 | {:c, 0x5155} => 1,
46 | {:c, 0x5156} => 1,
47 | {:c, 0x5157} => 1,
48 | {:c, 0x5158} => 0,
49 | {:c, 0x5159} => 0,
50 | {:c, 0x515A} => 0,
51 | {:c, 0x515B} => 1,
52 | {:c, 0x515C} => 0,
53 | {:c, 0x515D} => 1
54 | }
55 | }
56 |
57 | cmd0 = {:rc, 0x50, 0x5152, 12}
58 | req0 = <<0x50, 1, 0x51, 0x52, 0, 12>>
59 | res0 = <<0x50, 1, 2, 0x3C, 0x0A>>
60 | val0 = [0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1]
61 | pp1(cmd0, req0, res0, val0, model0)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/test/f02_test.exs:
--------------------------------------------------------------------------------
1 | defmodule F02Test do
2 | use ExUnit.Case
3 | import TestHelper
4 |
5 | test "Read 0 from Single Input" do
6 | model0 = %{0x50 => %{{:i, 0x5152} => 0}}
7 | cmd0 = {:ri, 0x50, 0x5152, 1}
8 | req0 = <<0x50, 2, 0x51, 0x52, 0, 1>>
9 | res0 = <<0x50, 2, 1, 0x00>>
10 | val0 = [0]
11 | pp1(cmd0, req0, res0, val0, model0)
12 | end
13 |
14 | test "Read 1 from Single Input" do
15 | model0 = %{0x50 => %{{:i, 0x5152} => 1}}
16 | cmd0 = {:ri, 0x50, 0x5152, 1}
17 | req0 = <<0x50, 2, 0x51, 0x52, 0, 1>>
18 | res0 = <<0x50, 2, 1, 0x01>>
19 | val0 = [1]
20 | pp1(cmd0, req0, res0, val0, model0)
21 | end
22 |
23 | test "Read 011 from Multiple Inputs" do
24 | model0 = %{
25 | 0x50 => %{
26 | {:i, 0x5152} => 0,
27 | {:i, 0x5153} => 1,
28 | {:i, 0x5154} => 1
29 | }
30 | }
31 |
32 | cmd0 = {:ri, 0x50, 0x5152, 3}
33 | req0 = <<0x50, 2, 0x51, 0x52, 0, 3>>
34 | res0 = <<0x50, 2, 1, 0x06>>
35 | val0 = [0, 1, 1]
36 | pp1(cmd0, req0, res0, val0, model0)
37 | end
38 |
39 | test "Read 0011 1100 0101 from Multiple Inputs" do
40 | model0 = %{
41 | 0x50 => %{
42 | {:i, 0x5152} => 0,
43 | {:i, 0x5153} => 0,
44 | {:i, 0x5154} => 1,
45 | {:i, 0x5155} => 1,
46 | {:i, 0x5156} => 1,
47 | {:i, 0x5157} => 1,
48 | {:i, 0x5158} => 0,
49 | {:i, 0x5159} => 0,
50 | {:i, 0x515A} => 0,
51 | {:i, 0x515B} => 1,
52 | {:i, 0x515C} => 0,
53 | {:i, 0x515D} => 1
54 | }
55 | }
56 |
57 | cmd0 = {:ri, 0x50, 0x5152, 12}
58 | req0 = <<0x50, 2, 0x51, 0x52, 0, 12>>
59 | res0 = <<0x50, 2, 2, 0x3C, 0x0A>>
60 | val0 = [0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1]
61 | pp1(cmd0, req0, res0, val0, model0)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/test/f03_test.exs:
--------------------------------------------------------------------------------
1 | defmodule F03Test do
2 | use ExUnit.Case
3 | import TestHelper
4 |
5 | test "Read 0x6162 from Single Holding Register" do
6 | model0 = %{0x50 => %{{:hr, 0x5152} => 0x6162}}
7 | cmd0 = {:rhr, 0x50, 0x5152, 1}
8 | req0 = <<0x50, 3, 0x51, 0x52, 0, 1>>
9 | res0 = <<0x50, 3, 2, 0x61, 0x62>>
10 | val0 = [0x6162]
11 | pp1(cmd0, req0, res0, val0, model0)
12 | end
13 |
14 | test "Read 0x616263646566 from Multiple Holding Registers" do
15 | model0 = %{
16 | 0x50 => %{
17 | {:hr, 0x5152} => 0x6162,
18 | {:hr, 0x5153} => 0x6364,
19 | {:hr, 0x5154} => 0x6566
20 | }
21 | }
22 |
23 | cmd0 = {:rhr, 0x50, 0x5152, 3}
24 | req0 = <<0x50, 3, 0x51, 0x52, 0, 3>>
25 | res0 = <<0x50, 3, 6, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66>>
26 | val0 = [0x6162, 0x6364, 0x6566]
27 | pp1(cmd0, req0, res0, val0, model0)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/f04_test.exs:
--------------------------------------------------------------------------------
1 | defmodule F04Test do
2 | use ExUnit.Case
3 | import TestHelper
4 |
5 | test "Read 0x6162 from Single Input Register" do
6 | model0 = %{0x50 => %{{:ir, 0x5152} => 0x6162}}
7 | cmd0 = {:rir, 0x50, 0x5152, 1}
8 | req0 = <<0x50, 4, 0x51, 0x52, 0, 1>>
9 | res0 = <<0x50, 4, 2, 0x61, 0x62>>
10 | val0 = [0x6162]
11 | pp1(cmd0, req0, res0, val0, model0)
12 | end
13 |
14 | test "Read 0x616263646566 from Multiple Input Registers" do
15 | model0 = %{
16 | 0x50 => %{
17 | {:ir, 0x5152} => 0x6162,
18 | {:ir, 0x5153} => 0x6364,
19 | {:ir, 0x5154} => 0x6566
20 | }
21 | }
22 |
23 | cmd0 = {:rir, 0x50, 0x5152, 3}
24 | req0 = <<0x50, 4, 0x51, 0x52, 0, 3>>
25 | res0 = <<0x50, 4, 6, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66>>
26 | val0 = [0x6162, 0x6364, 0x6566]
27 | pp1(cmd0, req0, res0, val0, model0)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/f05_test.exs:
--------------------------------------------------------------------------------
1 | defmodule F05Test do
2 | use ExUnit.Case
3 | import TestHelper
4 |
5 | test "Write 0 to Single Coil" do
6 | model0 = %{0x50 => %{{:c, 0x5152} => 1}}
7 | model1 = %{0x50 => %{{:c, 0x5152} => 0}}
8 | val0 = 0
9 | cmd0 = {:fc, 0x50, 0x5152, val0}
10 | req0 = <<0x50, 5, 0x51, 0x52, 0, 0>>
11 | res0 = <<0x50, 5, 0x51, 0x52, 0, 0>>
12 | pp2(cmd0, req0, res0, model0, model1)
13 | end
14 |
15 | test "Write 1 to Single Coil" do
16 | model0 = %{0x50 => %{{:c, 0x5152} => 0}}
17 | model1 = %{0x50 => %{{:c, 0x5152} => 1}}
18 | val0 = 1
19 | cmd0 = {:fc, 0x50, 0x5152, val0}
20 | req0 = <<0x50, 5, 0x51, 0x52, 0xFF, 0>>
21 | res0 = <<0x50, 5, 0x51, 0x52, 0xFF, 0>>
22 | pp2(cmd0, req0, res0, model0, model1)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/f06_test.exs:
--------------------------------------------------------------------------------
1 | defmodule F06Test do
2 | use ExUnit.Case
3 | import TestHelper
4 |
5 | test "Write 0x6162 to Single Holding Register" do
6 | model0 = %{0x50 => %{{:hr, 0x5152} => 0}}
7 | model1 = %{0x50 => %{{:hr, 0x5152} => 0x6162}}
8 | val0 = 0x6162
9 | cmd0 = {:phr, 0x50, 0x5152, val0}
10 | req0 = <<0x50, 6, 0x51, 0x52, 0x61, 0x62>>
11 | res0 = <<0x50, 6, 0x51, 0x52, 0x61, 0x62>>
12 | pp2(cmd0, req0, res0, model0, model1)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/f15_test.exs:
--------------------------------------------------------------------------------
1 | defmodule F15Test do
2 | use ExUnit.Case
3 | import TestHelper
4 |
5 | test "Write 011 to Multiple Coils" do
6 | model0 = %{
7 | 0x50 => %{
8 | {:c, 0x5152} => 1,
9 | {:c, 0x5153} => 0,
10 | {:c, 0x5154} => 0
11 | }
12 | }
13 |
14 | model1 = %{
15 | 0x50 => %{
16 | {:c, 0x5152} => 0,
17 | {:c, 0x5153} => 1,
18 | {:c, 0x5154} => 1
19 | }
20 | }
21 |
22 | val0 = [0, 1, 1]
23 | cmd0 = {:fc, 0x50, 0x5152, val0}
24 | req0 = <<0x50, 15, 0x51, 0x52, 0, 3, 1, 0x06>>
25 | res0 = <<0x50, 15, 0x51, 0x52, 0, 3>>
26 | pp2(cmd0, req0, res0, model0, model1)
27 | end
28 |
29 | test "Write 0011 1100 0101 to Multiple Coils" do
30 | model0 = %{
31 | 0x50 => %{
32 | {:c, 0x5152} => 1,
33 | {:c, 0x5153} => 1,
34 | {:c, 0x5154} => 0,
35 | {:c, 0x5155} => 0,
36 | {:c, 0x5156} => 0,
37 | {:c, 0x5157} => 0,
38 | {:c, 0x5158} => 1,
39 | {:c, 0x5159} => 1,
40 | {:c, 0x515A} => 1,
41 | {:c, 0x515B} => 0,
42 | {:c, 0x515C} => 1,
43 | {:c, 0x515D} => 0
44 | }
45 | }
46 |
47 | model1 = %{
48 | 0x50 => %{
49 | {:c, 0x5152} => 0,
50 | {:c, 0x5153} => 0,
51 | {:c, 0x5154} => 1,
52 | {:c, 0x5155} => 1,
53 | {:c, 0x5156} => 1,
54 | {:c, 0x5157} => 1,
55 | {:c, 0x5158} => 0,
56 | {:c, 0x5159} => 0,
57 | {:c, 0x515A} => 0,
58 | {:c, 0x515B} => 1,
59 | {:c, 0x515C} => 0,
60 | {:c, 0x515D} => 1
61 | }
62 | }
63 |
64 | val0 = [0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1]
65 | cmd0 = {:fc, 0x50, 0x5152, val0}
66 | req0 = <<0x50, 15, 0x51, 0x52, 0, 12, 2, 0x3C, 0x0A>>
67 | res0 = <<0x50, 15, 0x51, 0x52, 0, 12>>
68 | pp2(cmd0, req0, res0, model0, model1)
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/test/f16_test.exs:
--------------------------------------------------------------------------------
1 | defmodule F16Test do
2 | use ExUnit.Case
3 | import TestHelper
4 |
5 | test "Write 0x616263646566 to Multiple Holding Registers" do
6 | model0 = %{
7 | 0x50 => %{
8 | {:hr, 0x5152} => 0,
9 | {:hr, 0x5153} => 0,
10 | {:hr, 0x5154} => 0
11 | }
12 | }
13 |
14 | model1 = %{
15 | 0x50 => %{
16 | {:hr, 0x5152} => 0x6162,
17 | {:hr, 0x5153} => 0x6364,
18 | {:hr, 0x5154} => 0x6566
19 | }
20 | }
21 |
22 | val0 = [0x6162, 0x6364, 0x6566]
23 | cmd0 = {:phr, 0x50, 0x5152, val0}
24 | req0 = <<0x50, 16, 0x51, 0x52, 0, 3, 6, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66>>
25 | res0 = <<0x50, 16, 0x51, 0x52, 0, 3>>
26 | pp2(cmd0, req0, res0, model0, model1)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/float_test.exs:
--------------------------------------------------------------------------------
1 | defmodule FloatTest do
2 | use ExUnit.Case
3 | alias Modbux.IEEE754
4 |
5 | # https://www.h-schmidt.net/FloatConverter/IEEE754.html
6 | # endianess tested agains opto22 analog modules
7 | test "float convertion test" do
8 | assert [0xC0A0, 0x0000] == IEEE754.to_2_regs(-5.0, :be)
9 | assert [0x0000, 0xC0A0] == IEEE754.to_2_regs(-5.0, :le)
10 | assert [0x40A0, 0x0000] == IEEE754.to_2_regs(+5.0, :be)
11 | assert [0x0000, 0x40A0] == IEEE754.to_2_regs(+5.0, :le)
12 | assert [0xC0A0, 0x0000, 0x40A0, 0x0000] == IEEE754.to_2n_regs([-5.0, +5.0], :be)
13 | assert [0x0000, 0xC0A0, 0x0000, 0x40A0] == IEEE754.to_2n_regs([-5.0, +5.0], :le)
14 | assert -5.0 == IEEE754.from_2_regs(0xC0A0, 0x0000, :be)
15 | assert -5.0 == IEEE754.from_2_regs(0x0000, 0xC0A0, :le)
16 | assert -5.0 == IEEE754.from_2_regs([0xC0A0, 0x0000], :be)
17 | assert -5.0 == IEEE754.from_2_regs([0x0000, 0xC0A0], :le)
18 | assert +5.0 == IEEE754.from_2_regs(0x40A0, 0x0000, :be)
19 | assert +5.0 == IEEE754.from_2_regs(0x0000, 0x40A0, :le)
20 | assert +5.0 == IEEE754.from_2_regs([0x40A0, 0x0000], :be)
21 | assert +5.0 == IEEE754.from_2_regs([0x0000, 0x40A0], :le)
22 | assert [-5.0, +5.0] == IEEE754.from_2n_regs([0xC0A0, 0x0000, 0x40A0, 0x0000], :be)
23 | assert [-5.0, +5.0] == IEEE754.from_2n_regs([0x0000, 0xC0A0, 0x0000, 0x40A0], :le)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/test/helper_test.exs:
--------------------------------------------------------------------------------
1 | defmodule HelperTest do
2 | use ExUnit.Case
3 | alias Modbux.Helper
4 |
5 | test "bool_to_byte test" do
6 | assert 0x00 == Helper.bool_to_byte(0)
7 | assert 0xFF == Helper.bool_to_byte(1)
8 | end
9 |
10 | test "byte_count test" do
11 | assert 1 == Helper.byte_count(1)
12 | assert 1 == Helper.byte_count(2)
13 | assert 1 == Helper.byte_count(7)
14 | assert 1 == Helper.byte_count(8)
15 | assert 2 == Helper.byte_count(9)
16 | assert 2 == Helper.byte_count(15)
17 | assert 2 == Helper.byte_count(16)
18 | assert 3 == Helper.byte_count(17)
19 | end
20 |
21 | test "bin_to_bitlist test" do
22 | assert [1] == Helper.bin_to_bitlist(1, <<0x13>>)
23 | assert [1, 1] == Helper.bin_to_bitlist(2, <<0x13>>)
24 | assert [1, 1, 0] == Helper.bin_to_bitlist(3, <<0x13>>)
25 | assert [1, 1, 0, 0] == Helper.bin_to_bitlist(4, <<0x13>>)
26 | assert [1, 1, 0, 0, 1] == Helper.bin_to_bitlist(5, <<0x13>>)
27 | assert [1, 1, 0, 0, 1, 0] == Helper.bin_to_bitlist(6, <<0x13>>)
28 | assert [1, 1, 0, 0, 1, 0, 0] == Helper.bin_to_bitlist(7, <<0x13>>)
29 | assert [1, 1, 0, 0, 1, 0, 0, 0] == Helper.bin_to_bitlist(8, <<0x13>>)
30 | assert [1, 1, 0, 0, 1, 0, 0, 0, 1] == Helper.bin_to_bitlist(9, <<0x13, 0x01>>)
31 | end
32 |
33 | test "bin_to_reglist test" do
34 | assert [0x0102] == Helper.bin_to_reglist(1, <<0x01, 0x02>>)
35 | assert [0x0102, 0x0304] == Helper.bin_to_reglist(2, <<0x01, 0x02, 0x03, 0x04>>)
36 | end
37 |
38 | test "crc test" do
39 | p(<<0xCB, 0x4F>>, <<0x01, 0x05, 0x0B, 0xB8, 0x00, 0x00>>)
40 | p(<<0x3B, 0x0E>>, <<0x01, 0x05, 0x0B, 0xB8, 0xFF, 0x00>>)
41 | p(<<0xCB, 0x7F>>, <<0x01, 0x01, 0x0B, 0xB8, 0x00, 0x01>>)
42 | p(<<0x88, 0x51>>, <<0x01, 0x01, 0x01, 0x00>>)
43 | end
44 |
45 | defp p(crc, data) do
46 | assert crc == data |> Helper.crc()
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/test/model_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ModelTest do
2 | use ExUnit.Case
3 | alias Modbux.Model
4 | @state %{0x50 => %{{:c, 0x5152} => 0, {:c, 0x5153} => 0, {:c, 0x5155} => 0}}
5 |
6 | test "invalid slave" do
7 | assert {nil, @state} == Model.reads(@state, {81, :c, 20818, 1})
8 | assert {nil, @state} == Model.write(@state, {81, :c, 20818, 1})
9 | assert {nil, @state} == Model.writes(@state, {81, :c, 20818, [1, 1]})
10 | end
11 |
12 | test "invalid address" do
13 | assert {{:error, :eaddr}, @state} == Model.reads(@state, {80, :c, 20830, 1})
14 | assert {{:error, :eaddr}, @state} == Model.write(@state, {80, :c, 20830, 1})
15 | assert {{:error, :eaddr}, @state} == Model.writes(@state, {80, :c, 20830, [1, 1]})
16 | end
17 |
18 | test "invalid address (# bytes to read)" do
19 | assert {{:error, :eaddr}, @state} == Model.reads(@state, {80, :c, 20818, 3})
20 | assert {{:error, :eaddr}, @state} == Model.writes(@state, {80, :c, 20818, [1, 1, 1]})
21 | end
22 |
23 | test "illegal function" do
24 | assert {{:error, :eaddr}, @state} == Model.reads(@state, {80, :fc, 20818, 1})
25 | assert {{:error, :eaddr}, @state} == Model.write(@state, {80, :fc, 20818, 1})
26 | assert {{:error, :eaddr}, @state} == Model.writes(@state, {80, :fc, 20818, [1, 1]})
27 | end
28 |
29 | test "valid slave, function, address and number of bytes" do
30 | assert {{:ok, [0]}, @state} == Model.reads(@state, {80, :c, 20818, 1})
31 | d_state = %{0x50 => %{{:c, 0x5152} => 1, {:c, 0x5153} => 0, {:c, 0x5155} => 0}}
32 | assert {{:ok, nil}, d_state} == Model.write(@state, {80, :c, 20818, 1})
33 | d_state = %{0x50 => %{{:c, 0x5152} => 1, {:c, 0x5153} => 1, {:c, 0x5155} => 0}}
34 | assert {{:ok, nil}, d_state} == Model.writes(@state, {80, :c, 20818, [1, 1]})
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/test/request_test.exs:
--------------------------------------------------------------------------------
1 | defmodule RequestTest do
2 | use ExUnit.Case
3 | alias Modbux.Request
4 |
5 | test "Request pack and parse test" do
6 | pp(<<0x22, 0x01, 0x23, 0x24, 0x25, 0x26>>, {:rc, 0x22, 0x2324, 0x2526})
7 | pp(<<0x22, 0x02, 0x23, 0x24, 0x25, 0x26>>, {:ri, 0x22, 0x2324, 0x2526})
8 | pp(<<0x22, 0x03, 0x23, 0x24, 0x25, 0x26>>, {:rhr, 0x22, 0x2324, 0x2526})
9 | pp(<<0x22, 0x04, 0x23, 0x24, 0x25, 0x26>>, {:rir, 0x22, 0x2324, 0x2526})
10 | pp(<<0x22, 0x05, 0x23, 0x24, 0x00, 0x00>>, {:fc, 0x22, 0x2324, 0})
11 | pp(<<0x22, 0x05, 0x23, 0x24, 0xFF, 0x00>>, {:fc, 0x22, 0x2324, 1})
12 | pp(<<0x22, 0x06, 0x23, 0x24, 0x25, 0x26>>, {:phr, 0x22, 0x2324, 0x2526})
13 | pp(<<0x22, 0x0F, 0x23, 0x24, 0x00, 0x01, 0x01, 0x00>>, {:fc, 0x22, 0x2324, [0]})
14 | pp(<<0x22, 0x0F, 0x23, 0x24, 0x00, 0x01, 0x01, 0x01>>, {:fc, 0x22, 0x2324, [1]})
15 | pp(<<0x22, 0x10, 0x23, 0x24, 0x00, 0x01, 0x02, 0x25, 0x26>>, {:phr, 0x22, 0x2324, [0x2526]})
16 | # corner cases
17 | pp(<<0x22, 0x0F, 0x23, 0x24, 0x00, 0x08, 0x01, 0x96>>, {:fc, 0x22, 0x2324, [0, 1, 1, 0, 1, 0, 0, 1]})
18 |
19 | pp(
20 | <<0x22, 0x0F, 0x23, 0x24, 0x00, 0x09, 0x02, 0x96, 0x01>>,
21 | {:fc, 0x22, 0x2324, [0, 1, 1, 0, 1, 0, 0, 1, 1]}
22 | )
23 |
24 | pp(
25 | <<0x22, 0x0F, 0x23, 0x24, 0x00, 0x10, 0x02, 0x96, 0xC3>>,
26 | {:fc, 0x22, 0x2324, [0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1]}
27 | )
28 |
29 | pp(
30 | <<0x22, 0x0F, 0x23, 0x24, 0x00, 0x11, 0x03, 0x96, 0xC3, 0x01>>,
31 | {:fc, 0x22, 0x2324, [0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1]}
32 | )
33 |
34 | pp(<<0x22, 0x0F, 0x23, 0x24, 0x07, 0xF8, 0xFF>> <> l2b1(bls(2040)), {:fc, 0x22, 0x2324, bls(2040)})
35 | pp(<<0x22, 0x10, 0x23, 0x24, 0x00, 0x7F, 0xFE>> <> l2b16(rls(127)), {:phr, 0x22, 0x2324, rls(127)})
36 | # invalid cases
37 | assert <<0x22, 0x0F, 0x23, 0x24, 0x07, 0xF9, 0x00>> <> l2b1(bls(2041)) ==
38 | Request.pack({:fc, 0x22, 0x2324, bls(2041)})
39 |
40 | assert <<0x22, 0x10, 0x23, 0x24, 0x00, 0x80, 0x00>> <> l2b16(rls(128)) ==
41 | Request.pack({:phr, 0x22, 0x2324, rls(128)})
42 | end
43 |
44 | defp pp(packet, cmd) do
45 | assert packet == Request.pack(cmd)
46 | assert cmd == Request.parse(packet)
47 | end
48 |
49 | defp bls(size) do
50 | for i <- 1..size do
51 | rem(i, 2)
52 | end
53 | end
54 |
55 | defp rls(size) do
56 | for i <- 1..size do
57 | i
58 | end
59 | end
60 |
61 | defp l2b1(list) do
62 | lists = Enum.chunk_every(list, 8, 8, [0, 0, 0, 0, 0, 0, 0, 0])
63 |
64 | list =
65 | for [v0, v1, v2, v3, v4, v5, v6, v7] <- lists do
66 | <>
67 | end
68 |
69 | :erlang.iolist_to_binary(list)
70 | end
71 |
72 | defp l2b16(list) do
73 | list2 =
74 | for i <- list do
75 | <>
76 | end
77 |
78 | :erlang.iolist_to_binary(list2)
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/test/response_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ResponseTest do
2 | use ExUnit.Case
3 | alias Modbux.Response
4 |
5 | test "Response pack and parse test" do
6 | pp(<<0x22, 0x01, 0x01, 0x00>>, {:rc, 0x22, 0x2324, 1}, [0])
7 | pp(<<0x22, 0x01, 0x01, 0x01>>, {:rc, 0x22, 0x2324, 1}, [1])
8 | pp(<<0x22, 0x02, 0x01, 0x00>>, {:ri, 0x22, 0x2324, 1}, [0])
9 | pp(<<0x22, 0x02, 0x01, 0x01>>, {:ri, 0x22, 0x2324, 1}, [1])
10 | pp(<<0x22, 0x03, 0x02, 0x25, 0x26>>, {:rhr, 0x22, 0x2324, 1}, [0x2526])
11 | pp(<<0x22, 0x04, 0x02, 0x25, 0x26>>, {:rir, 0x22, 0x2324, 1}, [0x2526])
12 | pp(<<0x22, 0x05, 0x23, 0x24, 0x00, 0x00>>, {:fc, 0x22, 0x2324, 0}, nil)
13 | pp(<<0x22, 0x05, 0x23, 0x24, 0xFF, 0x00>>, {:fc, 0x22, 0x2324, 1}, nil)
14 | pp(<<0x22, 0x06, 0x23, 0x24, 0x25, 0x26>>, {:phr, 0x22, 0x2324, 0x2526}, nil)
15 | pp(<<0x22, 0x0F, 0x23, 0x24, 0x00, 0x01>>, {:fc, 0x22, 0x2324, [0]}, nil)
16 | pp(<<0x22, 0x10, 0x23, 0x24, 0x00, 0x01>>, {:phr, 0x22, 0x2324, [0x2526]}, nil)
17 | # corner cases
18 | pp(<<0x22, 0x01, 0x01, 0x96>>, {:rc, 0x22, 0x2324, 8}, [0, 1, 1, 0, 1, 0, 0, 1])
19 | pp(<<0x22, 0x01, 0x02, 0x96, 0x01>>, {:rc, 0x22, 0x2324, 9}, [0, 1, 1, 0, 1, 0, 0, 1, 1])
20 |
21 | pp(<<0x22, 0x01, 0x02, 0x96, 0xC3>>, {:rc, 0x22, 0x2324, 16}, [
22 | 0,
23 | 1,
24 | 1,
25 | 0,
26 | 1,
27 | 0,
28 | 0,
29 | 1,
30 | 1,
31 | 1,
32 | 0,
33 | 0,
34 | 0,
35 | 0,
36 | 1,
37 | 1
38 | ])
39 |
40 | pp(<<0x22, 0x01, 0x03, 0x96, 0xC3, 0x01>>, {:rc, 0x22, 0x2324, 17}, [
41 | 0,
42 | 1,
43 | 1,
44 | 0,
45 | 1,
46 | 0,
47 | 0,
48 | 1,
49 | 1,
50 | 1,
51 | 0,
52 | 0,
53 | 0,
54 | 0,
55 | 1,
56 | 1,
57 | 1
58 | ])
59 |
60 | pp(<<0x22, 0x01, 0xFF>> <> l2b1(bls(2040)), {:rc, 0x22, 0x2324, 2040}, bls(2040))
61 | pp(<<0x22, 0x02, 0x01, 0x96>>, {:ri, 0x22, 0x2324, 8}, [0, 1, 1, 0, 1, 0, 0, 1])
62 | pp(<<0x22, 0x02, 0x02, 0x96, 0x01>>, {:ri, 0x22, 0x2324, 9}, [0, 1, 1, 0, 1, 0, 0, 1, 1])
63 |
64 | pp(<<0x22, 0x02, 0x02, 0x96, 0xC3>>, {:ri, 0x22, 0x2324, 16}, [
65 | 0,
66 | 1,
67 | 1,
68 | 0,
69 | 1,
70 | 0,
71 | 0,
72 | 1,
73 | 1,
74 | 1,
75 | 0,
76 | 0,
77 | 0,
78 | 0,
79 | 1,
80 | 1
81 | ])
82 |
83 | pp(<<0x22, 0x02, 0x03, 0x96, 0xC3, 0x01>>, {:ri, 0x22, 0x2324, 17}, [
84 | 0,
85 | 1,
86 | 1,
87 | 0,
88 | 1,
89 | 0,
90 | 0,
91 | 1,
92 | 1,
93 | 1,
94 | 0,
95 | 0,
96 | 0,
97 | 0,
98 | 1,
99 | 1,
100 | 1
101 | ])
102 |
103 | pp(<<0x22, 0x02, 0xFF>> <> l2b1(bls(2040)), {:ri, 0x22, 0x2324, 2040}, bls(2040))
104 | pp(<<0x22, 0x03, 0xFE>> <> l2b16(rls(127)), {:rhr, 0x22, 0x2324, 127}, rls(127))
105 | pp(<<0x22, 0x04, 0xFE>> <> l2b16(rls(127)), {:rir, 0x22, 0x2324, 127}, rls(127))
106 | # invalid cases
107 | assert <<0x22, 0x01, 0x00>> <> l2b1(bls(2041)) == Response.pack({:rc, 0x22, 0x2324, 2041}, bls(2041))
108 | assert <<0x22, 0x02, 0x00>> <> l2b1(bls(2041)) == Response.pack({:ri, 0x22, 0x2324, 2041}, bls(2041))
109 | assert <<0x22, 0x03, 0x00>> <> l2b16(rls(128)) == Response.pack({:rhr, 0x22, 0x2324, 128}, rls(128))
110 | assert <<0x22, 0x04, 0x00>> <> l2b16(rls(128)) == Response.pack({:rir, 0x22, 0x2324, 128}, rls(128))
111 | end
112 |
113 | defp pp(packet, cmd, vals) do
114 | assert packet == Response.pack(cmd, vals)
115 | assert Response.length(cmd) == byte_size(packet)
116 | assert vals == Response.parse(cmd, packet)
117 | end
118 |
119 | defp bls(size) do
120 | for i <- 1..size do
121 | rem(i, 2)
122 | end
123 | end
124 |
125 | defp rls(size) do
126 | for i <- 1..size do
127 | i
128 | end
129 | end
130 |
131 | defp l2b1(list) do
132 | lists = Enum.chunk_every(list, 8, 8, [0, 0, 0, 0, 0, 0, 0, 0])
133 |
134 | list =
135 | for [v0, v1, v2, v3, v4, v5, v6, v7] <- lists do
136 | <>
137 | end
138 |
139 | :erlang.iolist_to_binary(list)
140 | end
141 |
142 | defp l2b16(list) do
143 | list2 =
144 | for i <- list do
145 | <>
146 | end
147 |
148 | :erlang.iolist_to_binary(list2)
149 | end
150 | end
151 |
--------------------------------------------------------------------------------
/test/rtu/framer.exs:
--------------------------------------------------------------------------------
1 | defmodule RtuFramer do
2 | use ExUnit.Case
3 | alias Circuits.UART
4 |
5 | @moduledoc """
6 | These tests only runs if 'tty0tty' is installed in the host computer.
7 | """
8 | test "Framer test for fc => [1, 2, 3, 4, 5, 6, 15, 16]" do
9 | # Raw initialization.
10 | # RingLogger.attach()
11 | {:ok, m_pid} = UART.start_link()
12 | {:ok, s_pid} = UART.start_link()
13 |
14 | UART.open(m_pid, "tnt0", speed: 115_200, framing: {Modbux.Rtu.Framer, behavior: :master})
15 | UART.open(s_pid, "tnt1", speed: 115_200, framing: {Modbux.Rtu.Framer, behavior: :slave})
16 |
17 | # Master Requests.
18 | # Read Coil Status (FC=01)
19 | fc = <<0x11, 0x01, 0x00, 0x13, 0x00, 0x25, 0x0E, 0x84>>
20 | UART.write(m_pid, fc)
21 | assert_receive {circuits_uart, "tnt1", fc}
22 |
23 | # Read Input Status (FC=02)
24 | fc = <<0x11, 0x02, 0x00, 0xC4, 0x00, 0x16, 0xBA, 0xA9>>
25 | UART.write(m_pid, fc)
26 | assert_receive {circuits_uart, "tnt1", fc}
27 |
28 | # Read Holding Registers (FC=03)
29 | fc = <<0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x76, 0x87>>
30 | UART.write(m_pid, fc)
31 | assert_receive {circuits_uart, "tnt1", fc}
32 |
33 | # Read Input Registers (FC=04)
34 | fc = <<0x11, 0x04, 0x00, 0x08, 0x00, 0x01, 0xB2, 0x98>>
35 | UART.write(m_pid, fc)
36 | assert_receive {circuits_uart, "tnt1", fc}
37 |
38 | # Force Single Coil (FC=05)
39 | fc = <<0x11, 0x05, 0x00, 0xAC, 0xFF, 0x00, 0x4E, 0x8B>>
40 | UART.write(m_pid, fc)
41 | assert_receive {circuits_uart, "tnt1", fc}
42 |
43 | # Preset Single Register (FC=06)
44 | fc = <<0x11, 0x06, 0x00, 0x01, 0x00, 0x03, 0x9A, 0x9B>>
45 | UART.write(m_pid, fc)
46 | assert_receive {circuits_uart, "tnt1", fc}
47 |
48 | # Force Multiple Coils (FC=15)
49 | fc = <<0x11, 0x0F, 0x00, 0x13, 0x00, 0x0A, 0x02, 0xCD, 0x01, 0xBF, 0x0B>>
50 | UART.write(m_pid, fc)
51 | assert_receive {circuits_uart, "tnt1", fc}
52 |
53 | # Preset Multiple Registers (FC=16)
54 | fc = <<0x11, 0x10, 0x00, 0x01, 0x00, 0x02, 0x04, 0x00, 0x0A, 0x01, 0x02, 0xC6, 0xF0>>
55 | UART.write(m_pid, fc)
56 | assert_receive {circuits_uart, "tnt1", fc}
57 |
58 | # Exception
59 | excep = <<0x0A, 0x81, 0x02, 0xB0, 0x53>>
60 | UART.write(m_pid, excep)
61 | assert_receive {circuits_uart, "tnt1", excep}
62 |
63 | excep = <<0x11, 0x10, 0x00, 0x01, 0x00, 0x02, 0x04, 0x00, 0x0A, 0x01, 0x02, 0xC6, 0x1>>
64 | UART.write(m_pid, excep)
65 | assert_receive {:circuits_uart, "tnt1", {:error, :ecrc, "CRC Error"}}
66 |
67 | # Server Response.
68 | # Read Coil Status (FC=01)
69 | resp = <<0x11, 0x01, 0x05, 0xCD, 0x6B, 0xB2, 0x0E, 0x1B, 0x45, 0xE6>>
70 | UART.write(s_pid, resp)
71 | assert_receive {circuits_uart, "tnt0", resp}
72 |
73 | # Read Input Status (FC=02)
74 | resp = <<0x11, 0x02, 0x03, 0xAC, 0xDB, 0x35, 0x20, 0x18>>
75 | UART.write(s_pid, resp)
76 | assert_receive {circuits_uart, "tnt0", resp}
77 |
78 | # Read Holding Registers (FC=03)
79 | resp = <<0x11, 0x03, 0x06, 0xAE, 0x41, 0x56, 0x52, 0x43, 0x40, 0x49, 0xAD>>
80 | UART.write(s_pid, resp)
81 | assert_receive {circuits_uart, "tnt0", resp}
82 |
83 | # Read Input Registers (FC=04)
84 | resp = <<0x11, 0x04, 0x02, 0x00, 0x0A, 0xF8, 0xF4>>
85 | UART.write(s_pid, resp)
86 | assert_receive {circuits_uart, "tnt0", resp}
87 |
88 | # Force Single Coil (FC=05)
89 | resp = <<0x11, 0x05, 0x00, 0xAC, 0xFF, 0x00, 0x4E, 0x8B>>
90 | UART.write(s_pid, resp)
91 | assert_receive {circuits_uart, "tnt0", resp}
92 |
93 | # Preset Single Register (FC=06)
94 | resp = <<0x11, 0x06, 0x00, 0x01, 0x00, 0x03, 0x9A, 0x9B>>
95 | UART.write(s_pid, resp)
96 | assert_receive {circuits_uart, "tnt0", resp}
97 |
98 | # Force Multiple Coils (FC=15)
99 | resp = <<0x11, 0x0F, 0x00, 0x13, 0x00, 0x0A, 0x26, 0x99>>
100 | UART.write(s_pid, resp)
101 | assert_receive {circuits_uart, "tnt0", resp}
102 |
103 | # Preset Multiple Registers (FC=16)
104 | resp = <<0x11, 0x10, 0x00, 0x01, 0x00, 0x02, 0x12, 0x98>>
105 | UART.write(s_pid, resp)
106 | assert_receive {circuits_uart, "tnt0", resp}
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/test/rtu/master.exs:
--------------------------------------------------------------------------------
1 | defmodule RtuMaster do
2 | use ExUnit.Case
3 |
4 | @moduledoc """
5 | These tests only runs if 'tty0tty' is installed in the host computer.
6 | """
7 | test "Master test for fc => [1, 2, 3, 4, 5, 6, 15, 16]" do
8 | RingLogger.attach()
9 | {:ok, m_pid} = Modbux.Rtu.Master.start_link(tty: "tnt0")
10 |
11 | model = %{
12 | 80 => %{
13 | {:c, 1} => 1,
14 | {:c, 2} => 0,
15 | {:i, 1} => 1,
16 | {:i, 2} => 1,
17 | {:ir, 1} => 0,
18 | {:ir, 2} => 1,
19 | {:hr, 1} => 102,
20 | {:hr, 2} => 103
21 | }
22 | }
23 |
24 | {:ok, s_pid} = Modbux.Rtu.Slave.start_link(tty: "tnt1", model: model, active: true)
25 |
26 | assert model == Modbux.Rtu.Slave.get_db(s_pid)
27 | # Master Requests.
28 | # Read Coil Status (FC=01)
29 | resp = Modbux.Rtu.Master.request(m_pid, {:rc, 80, 1, 1})
30 | # slave
31 | assert_receive {:modbus_rtu, {:slave_request, {:rc, 80, 1, 1}}}
32 | # master
33 | assert resp == {:ok, [1]}
34 |
35 | # Read Input Status (FC=02)
36 | resp = Modbux.Rtu.Master.request(m_pid, {:ri, 80, 1, 1})
37 | # slave
38 | assert_receive {:modbus_rtu, {:slave_request, {:ri, 80, 1, 1}}}
39 | # master
40 | assert resp == {:ok, [1]}
41 |
42 | # Read Holding Registers (FC=03)
43 | resp = Modbux.Rtu.Master.request(m_pid, {:rhr, 80, 1, 2})
44 | # slave
45 | assert_receive {:modbus_rtu, {:slave_request, {:rhr, 80, 1, 2}}}
46 | # master
47 | assert resp == {:ok, [102, 103]}
48 |
49 | # # Read Input Registers (FC=04)
50 | resp = Modbux.Rtu.Master.request(m_pid, {:rir, 80, 1, 1})
51 | # slave
52 | assert_receive {:modbus_rtu, {:slave_request, {:rir, 80, 1, 1}}}
53 | # master
54 | assert resp == {:ok, [0]}
55 |
56 | # Force Single Coil (FC=05)
57 | resp = Modbux.Rtu.Master.request(m_pid, {:fc, 80, 1, 0})
58 | # slave
59 | assert_receive {:modbus_rtu, {:slave_request, {:fc, 80, 1, 0}}}
60 | # master
61 | assert resp == :ok
62 |
63 | model = Modbux.Rtu.Slave.get_db(s_pid)
64 |
65 | assert model[80][{:c, 1}] == 0
66 |
67 | # backward
68 | resp = Modbux.Rtu.Master.request(m_pid, {:fc, 80, 1, 1})
69 | # slave
70 | assert_receive {:modbus_rtu, {:slave_request, {:fc, 80, 1, 1}}}
71 | # master
72 | assert resp == :ok
73 |
74 | model = Modbux.Rtu.Slave.get_db(s_pid)
75 |
76 | assert model[80][{:c, 1}] == 1
77 |
78 | # Preset Single Register (FC=06)
79 | resp = Modbux.Rtu.Master.request(m_pid, {:phr, 80, 1, 10})
80 | # slave
81 | assert_receive {:modbus_rtu, {:slave_request, {:phr, 80, 1, 10}}}
82 | # master
83 | assert resp == :ok
84 |
85 | model = Modbux.Rtu.Slave.get_db(s_pid)
86 |
87 | assert model[80][{:hr, 1}] == 10
88 |
89 | # backward
90 | resp = Modbux.Rtu.Master.request(m_pid, {:phr, 80, 1, 102})
91 | # slave
92 | assert_receive {:modbus_rtu, {:slave_request, {:phr, 80, 1, 102}}}
93 | # master
94 | assert resp == :ok
95 |
96 | model = Modbux.Rtu.Slave.get_db(s_pid)
97 |
98 | assert model[80][{:hr, 1}] == 102
99 |
100 | # Force Multiple Coils (FC=15)
101 | resp = Modbux.Rtu.Master.request(m_pid, {:fc, 80, 1, [0, 1]})
102 | # slave
103 | assert_receive {:modbus_rtu, {:slave_request, {:fc, 80, 1, [0, 1]}}}
104 | # master
105 | assert resp == :ok
106 |
107 | model = Modbux.Rtu.Slave.get_db(s_pid)
108 |
109 | assert model[80][{:c, 1}] == 0
110 | assert model[80][{:c, 2}] == 1
111 |
112 | # backward
113 | resp = Modbux.Rtu.Master.request(m_pid, {:fc, 80, 1, [1, 0]})
114 | # slave
115 | assert_receive {:modbus_rtu, {:slave_request, {:fc, 80, 1, [1, 0]}}}
116 | # master
117 | assert resp == :ok
118 |
119 | model = Modbux.Rtu.Slave.get_db(s_pid)
120 |
121 | assert model[80][{:c, 1}] == 1
122 | assert model[80][{:c, 2}] == 0
123 |
124 | # Preset Multiple Registers (FC=16)
125 | resp = Modbux.Rtu.Master.request(m_pid, {:phr, 80, 1, [0, 1]})
126 | # slave
127 | assert_receive {:modbus_rtu, {:slave_request, {:phr, 80, 1, [0, 1]}}}
128 | # master
129 | assert resp == :ok
130 |
131 | model = Modbux.Rtu.Slave.get_db(s_pid)
132 |
133 | assert model[80][{:hr, 1}] == 0
134 | assert model[80][{:hr, 2}] == 1
135 |
136 | # backward
137 | resp = Modbux.Rtu.Master.request(m_pid, {:phr, 80, 1, [102, 103]})
138 | # slave
139 | assert_receive {:modbus_rtu, {:slave_request, {:phr, 80, 1, [102, 103]}}}
140 | # master
141 | assert resp == :ok
142 |
143 | model = Modbux.Rtu.Slave.get_db(s_pid)
144 |
145 | assert model[80][{:hr, 1}] == 102
146 | assert model[80][{:hr, 2}] == 103
147 |
148 | # Return exception for invalid address
149 | resp = Modbux.Rtu.Master.request(m_pid, {:phr, 80, 2, [102, 103]})
150 | # slave
151 | assert_receive {:modbus_rtu, {:slave_error, {:phr, 80, 2, 'fg'}, :eaddr}}
152 | # master
153 | assert resp == {:error, :eaddr}
154 |
155 | model = Modbux.Rtu.Slave.get_db(s_pid)
156 |
157 | assert model[80][{:hr, 1}] == 102
158 | assert model[80][{:hr, 2}] == 103
159 |
160 | # Return CRC error
161 | # bad crc
162 | assert Modbux.Rtu.Slave.raw_write(s_pid, <<80, 4, 2, 0, 1, 133, 61>>)
163 | resp = Modbux.Rtu.Master.read(m_pid)
164 |
165 | assert resp == {:error, :ecrc}
166 |
167 | Modbux.Rtu.Master.configure(m_pid, active: true)
168 |
169 | # Active mode.
170 | # Read Coil Status (FC=01)
171 | resp = Modbux.Rtu.Master.request(m_pid, {:rc, 80, 1, 1})
172 | # slave
173 | assert_receive {:modbus_rtu, {:slave_request, {:rc, 80, 1, 1}}}
174 | # master
175 | assert resp == :ok
176 | assert_receive {:modbus_rtu, {:slave_response, {:rc, 80, 1, 1}, [1]}}
177 | end
178 | end
179 |
--------------------------------------------------------------------------------
/test/rtu/rtu_test.exs:
--------------------------------------------------------------------------------
1 | defmodule RtuTest do
2 | use ExUnit.Case
3 | alias Modbux.Rtu
4 |
5 | test "wrap test" do
6 | p(<<0xCB, 0x4F>>, <<0x01, 0x05, 0x0B, 0xB8, 0x00, 0x00>>)
7 | p(<<0x3B, 0x0E>>, <<0x01, 0x05, 0x0B, 0xB8, 0xFF, 0x00>>)
8 | p(<<0xCB, 0x7F>>, <<0x01, 0x01, 0x0B, 0xB8, 0x00, 0x01>>)
9 | p(<<0x88, 0x51>>, <<0x01, 0x01, 0x01, 0x00>>)
10 | end
11 |
12 | defp p(<>, payload) do
13 | assert <> == payload |> Rtu.wrap()
14 | assert payload == <> |> Rtu.unwrap()
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/rtu/slave.exs:
--------------------------------------------------------------------------------
1 | defmodule RtuSlave do
2 | use ExUnit.Case
3 | alias Circuits.UART
4 |
5 | @moduledoc """
6 | These tests only runs if 'tty0tty' is installed in the host computer.
7 | """
8 | test "Slave test for fc => [1, 2, 3, 4, 5, 6, 15, 16]" do
9 | # Raw initialization.
10 | # RingLogger.attach()
11 | {:ok, m_pid} = UART.start_link()
12 |
13 | UART.open(m_pid, "tnt0", speed: 115_200, framing: {Modbux.Rtu.Framer, behavior: :master})
14 |
15 | model = %{
16 | 80 => %{
17 | {:c, 1} => 1,
18 | {:c, 2} => 0,
19 | {:i, 1} => 1,
20 | {:i, 2} => 1,
21 | {:ir, 1} => 1,
22 | {:ir, 2} => 0,
23 | {:hr, 1} => 0,
24 | {:hr, 2} => 1
25 | }
26 | }
27 |
28 | {:ok, s_pid} = Modbux.Rtu.Slave.start_link(tty: "tnt1", model: model, active: true)
29 |
30 | # Master Requests.
31 | # Read Coil Status (FC=01)
32 | fc = <<80, 1, 0, 1, 0, 1, 161, 139>>
33 | UART.write(m_pid, fc)
34 | # slave
35 | assert_receive {:modbus_rtu, {:slave_request, {:rc, 80, 1, 1}}}
36 | # master
37 | assert_receive {circuits_uart, "tnt0", <<80, 1, 1, 1, 128, 180>>}
38 |
39 | # Read Input Status (FC=02)
40 | fc = <<80, 2, 0, 1, 0, 1, 229, 139>>
41 | UART.write(m_pid, fc)
42 | # slave
43 | assert_receive {:modbus_rtu, {:slave_request, {:ri, 80, 1, 1}}}
44 | # master
45 | assert_receive {:circuits_uart, "tnt0", <<80, 2, 1, 1, 112, 180>>}
46 |
47 | # Read Holding Registers (FC=03)
48 | fc = <<80, 3, 0, 1, 0, 1, 216, 75>>
49 | UART.write(m_pid, fc)
50 | # slave
51 | assert_receive {:modbus_rtu, {:slave_request, {:rhr, 80, 1, 1}}}
52 | # master
53 | assert_receive {:circuits_uart, "tnt0", <<80, 3, 2, 0, 0, 69, 136>>}
54 |
55 | # Read Input Registers (FC=04)
56 | fc = <<80, 4, 0, 1, 0, 1, 109, 139>>
57 | UART.write(m_pid, fc)
58 | # slave
59 | assert_receive {:modbus_rtu, {:slave_request, {:rir, 80, 1, 1}}}
60 | # master
61 | assert_receive {:circuits_uart, "tnt0", <<80, 4, 2, 0, 1, 133, 60>>}
62 |
63 | # Force Single Coil (FC=05)
64 | fc = <<80, 5, 0, 1, 0, 0, 145, 139>>
65 | UART.write(m_pid, fc)
66 | # slave
67 | assert_receive {:modbus_rtu, {:slave_request, {:fc, 80, 1, 0}}}
68 | # master
69 | assert_receive {:circuits_uart, "tnt0", <<80, 5, 0, 1, 0, 0, 145, 139>>}
70 | assert Modbux.Rtu.Slave.get_db(s_pid)[80][{:c, 1}] == 0
71 |
72 | # Preset Single Register (FC=06)
73 | fc = <<80, 6, 0, 1, 0, 0, 213, 139>>
74 | UART.write(m_pid, fc)
75 | # slave
76 | assert_receive {:modbus_rtu, {:slave_request, {:phr, 80, 1, 0}}}
77 | # master
78 | assert_receive {:circuits_uart, "tnt0", <<80, 6, 0, 1, 0, 0, 213, 139>>}
79 | assert Modbux.Rtu.Slave.get_db(s_pid)[80][{:hr, 1}] == 0
80 |
81 | # # Force Multiple Coils (FC=15)
82 | fc = <<80, 15, 0, 1, 0, 2, 1, 2, 166, 102>>
83 | UART.write(m_pid, fc)
84 | # slave
85 | assert_receive {:modbus_rtu, {:slave_request, {:fc, 80, 1, [0, 1]}}}
86 | # master
87 | assert_receive {:circuits_uart, "tnt0", <<80, 15, 0, 1, 0, 2, 136, 75>>}
88 | assert Modbux.Rtu.Slave.get_db(s_pid)[80][{:c, 1}] == 0
89 | assert Modbux.Rtu.Slave.get_db(s_pid)[80][{:c, 2}] == 1
90 |
91 | # Preset Multiple Registers (FC=16)
92 | fc = <<80, 16, 0, 1, 0, 2, 4, 0, 0, 0, 1, 246, 94>>
93 | UART.write(m_pid, fc)
94 | # slave
95 | assert_receive {:modbus_rtu, {:slave_request, {:phr, 80, 1, [0, 1]}}}
96 | # master
97 | assert_receive {:circuits_uart, "tnt0", <<80, 16, 0, 1, 0, 2, 29, 137>>}
98 | assert Modbux.Rtu.Slave.get_db(s_pid)[80][{:hr, 1}] == 0
99 | assert Modbux.Rtu.Slave.get_db(s_pid)[80][{:hr, 2}] == 1
100 |
101 | # Exception in bus from other slaves
102 | excep = <<0x0A, 0x81, 0x02, 0xB0, 0x53>>
103 | UART.write(m_pid, excep)
104 | # slave
105 | refute_receive {:modbus_rtu, {:slave_request, {:error, 10, 129, 2}}}
106 |
107 | # Return exception for invalid address
108 | fc = <<80, 1, 7, 210, 0, 1, 81, 6>>
109 | UART.write(m_pid, fc)
110 | # slave
111 | assert_receive {:modbus_rtu, {:slave_error, {:rc, 80, 2002, 1}, :eaddr}}
112 | # master
113 | assert_receive {:circuits_uart, "tnt0", <<80, 129, 2, 144, 64>>}
114 |
115 | # Returns exception for invalid function code
116 | fc = <<80, 11, 7>>
117 | UART.write(m_pid, fc)
118 | # slave
119 | assert_receive {:modbus_rtu, {:slave_error, "P\v", :einval}}
120 | # master
121 | assert_receive {:circuits_uart, "tnt0", <<80, 139, 1, 214, 225>>}
122 |
123 | # Crc
124 | fc = <<80, 1, 7, 210, 0, 1, 81, 7>>
125 | UART.write(m_pid, fc)
126 | # slave
127 | assert_receive {:modbus_rtu, {:slave_error, "CRC Error", :ecrc}}
128 | # master
129 | refute_receive {:circuits_uart, "tnt0", <<80, 129, 2, 144, 64>>}
130 |
131 | # Slave request function
132 | # sets
133 | assert Modbux.Rtu.Slave.request(s_pid, {:phr, 80, 1, [1, 0]}) == nil
134 | # reads
135 | assert Modbux.Rtu.Slave.request(s_pid, {:rhr, 80, 1, 1}) == [1]
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/test/tcp/modbus_tcp_client_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ModbuxTcpClientTest do
2 | use ExUnit.Case
3 | alias Modbux.Tcp.{Client, Server}
4 |
5 | test "test Client (connection, stop, configuration)" do
6 | model = %{80 => %{{:c, 20818} => 0, {:c, 20819} => 1, {:hr, 20817} => 0}}
7 | {:ok, _spid} = Server.start_link(model: model, port: 2000)
8 | {:ok, cpid} = Client.start_link(ip: {127, 0, 0, 1}, tcp_port: 3000)
9 | state_cpid = Client.state(cpid)
10 | assert state_cpid != Client.configure(cpid, tcp_port: 2000)
11 | assert :ok == Client.connect(cpid)
12 | assert :error == Client.configure(cpid, active: false)
13 | assert :ok == Client.close(cpid)
14 | assert :ok == Client.stop(cpid)
15 | end
16 |
17 | test "test Client errors" do
18 | {:ok, cpid} = Client.start_link(ip: {127, 0, 0, 1}, tcp_port: 5000)
19 | assert {:error, :econnrefused} == Client.connect(cpid)
20 | assert {:error, :closed} == Client.close(cpid)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/tcp/modbus_tcp_server_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ModbuxTcpServerTest do
2 | use ExUnit.Case
3 | alias Modbux.Tcp.Server
4 | alias Modbux.Tcp.Client
5 |
6 | setup do
7 | RingLogger.attach()
8 | end
9 |
10 | test "Server (connection, stop, configuration)" do
11 | model = %{80 => %{{:c, 20818} => 0, {:c, 20819} => 1, {:hr, 20817} => 0, {:hr, 20818} => 0}}
12 | {:ok, _spid} = Server.start_link(model: model, port: 2000)
13 | {:ok, cpid} = Client.start_link(ip: {127, 0, 0, 1}, tcp_port: 3000)
14 | state_cpid = Client.state(cpid)
15 | assert state_cpid != Client.configure(cpid, tcp_port: 2000)
16 | assert :ok == Client.connect(cpid)
17 | Modbux.Tcp.Client.request(cpid, {:rc, 0x50, 20818, 2})
18 | assert Modbux.Tcp.Client.confirmation(cpid) == {:ok, [0, 1]}
19 | Modbux.Tcp.Client.request(cpid, {:rhr, 0x50, 20817, 1})
20 | assert Modbux.Tcp.Client.confirmation(cpid) == {:ok, [0]}
21 | Modbux.Tcp.Client.request(cpid, {:phr, 0x50, 20817, [1, 1]})
22 | assert Modbux.Tcp.Client.confirmation(cpid) == :ok
23 | Modbux.Tcp.Client.request(cpid, {:rhr, 0x50, 20817, 2})
24 | assert Modbux.Tcp.Client.confirmation(cpid) == {:ok, [1, 1]}
25 | Modbux.Tcp.Client.request(cpid, {:rhr, 0x50, 20818, 1})
26 | assert Modbux.Tcp.Client.confirmation(cpid) == {:ok, [1]}
27 | assert :error == Client.configure(cpid, active: false)
28 | assert :ok == Client.close(cpid)
29 | assert :ok == Client.stop(cpid)
30 | refute_received {:modbus_tcp, {:server_request, {:rc, 0x50, 20818, 2}}}
31 | end
32 |
33 | test "Server close active ports" do
34 | :gen_tcp.listen(2000, [:binary, packet: :raw, active: true, reuseaddr: true])
35 | model = %{0x50 => %{{:c, 0x5152} => 0}}
36 | {:ok, s_pid} = Server.start_link(model: model, port: 2000)
37 | assert Process.alive?(s_pid)
38 | end
39 |
40 | test "Server notifies an DB update" do
41 | model = %{0x50 => %{{:c, 0x5152} => 0}}
42 | {:ok, _spid} = Server.start_link(model: model, port: 2001, active: true)
43 | {:ok, c_pid} = Client.start_link(ip: {127, 0, 0, 1}, tcp_port: 2001)
44 | Client.connect(c_pid)
45 | Modbux.Tcp.Client.request(c_pid, {:rc, 0x50, 0x5152, 1})
46 | assert Modbux.Tcp.Client.confirmation(c_pid) == {:ok, [0]}
47 | assert_received {:modbus_tcp, {:server_request, {:rc, 80, 20818, 1}}}
48 | end
49 |
50 | test "DB updates from Elixir" do
51 | model = %{0x50 => %{{:c, 0x5152} => 0}}
52 | {:ok, s_pid} = Server.start_link(model: model, port: 2002, active: true)
53 | assert Server.update(s_pid, {:fc, 0x50, 0x5152, 1}) == nil
54 | assert Server.get_db(s_pid) == %{80 => %{{:c, 20818} => 1}}
55 | refute_received {:modbus_tcp, {:server_request, {:rc, 80, 20818, 1}}}
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/test/tcp/tcp_test.exs:
--------------------------------------------------------------------------------
1 | defmodule TcpTest do
2 | use ExUnit.Case
3 | alias Modbux.Tcp
4 |
5 | # http://www.tahapaksu.com/crc/
6 | # https://www.lammertbies.nl/comm/info/crc-calculation.html
7 | test "wrap test" do
8 | p(0, <<>>, <<0, 0, 0, 0, 0, 0>>)
9 | p(1, <<0>>, <<0, 1, 0, 0, 0, 1, 0>>)
10 | p(2, <<0, 1>>, <<0, 2, 0, 0, 0, 2, 0, 1>>)
11 | p(3, <<0, 1, 2>>, <<0, 3, 0, 0, 0, 3, 0, 1, 2>>)
12 | p(4, <<0, 1, 2, 3>>, <<0, 4, 0, 0, 0, 4, 0, 1, 2, 3>>)
13 | end
14 |
15 | defp p(transid, payload, packet) do
16 | assert packet == payload |> Tcp.wrap(transid)
17 | assert {payload, transid} == packet |> Tcp.unwrap()
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
3 | defmodule TestHelper do
4 | use ExUnit.Case
5 | alias Modbux.Request
6 | alias Modbux.Response
7 | alias Modbux.Model
8 | alias Modbux.Rtu
9 | alias Modbux.Tcp
10 | alias Modbux.Tcp.Client
11 | alias Modbux.Tcp.Server
12 |
13 | def pp1(cmd, req, res, val, model) do
14 | assert req == Request.pack(cmd)
15 | assert cmd == Request.parse(req)
16 | assert {{:ok, val}, model} == Model.apply(model, cmd)
17 | assert res == Response.pack(cmd, val)
18 | assert val == Response.parse(cmd, res)
19 | # length predition
20 | assert byte_size(res) == Response.length(cmd)
21 | assert byte_size(req) == Request.length(cmd)
22 | # rtu
23 | rtu_req = Rtu.pack_req(cmd)
24 | assert cmd == Rtu.parse_req(rtu_req)
25 | rtu_res = Rtu.pack_res(cmd, val)
26 | assert val == Rtu.parse_res(cmd, rtu_res)
27 | assert byte_size(rtu_res) == Rtu.res_len(cmd)
28 | assert byte_size(rtu_req) == Rtu.req_len(cmd)
29 | # tcp
30 | tcp_req = Tcp.pack_req(cmd, 1)
31 | assert {cmd, 1} == Tcp.parse_req(tcp_req)
32 | tcp_res = Tcp.pack_res(cmd, val, 1)
33 | assert val == Tcp.parse_res(cmd, tcp_res, 1)
34 | assert byte_size(tcp_res) == Tcp.res_len(cmd)
35 | assert byte_size(tcp_req) == Tcp.req_len(cmd)
36 | # master
37 | Server.start_link(model: model, port: 2002)
38 | {:ok, master_pid} = Client.start_link(tcp_port: 2002, ip: {127, 0, 0, 1})
39 | Client.connect(master_pid)
40 |
41 | for _ <- 0..10 do
42 | Client.request(master_pid, cmd)
43 | assert {:ok, val} == Client.confirmation(master_pid)
44 | end
45 | end
46 |
47 | def pp2(cmd, req, res, model0, model1) do
48 | assert req == Request.pack(cmd)
49 | assert cmd == Request.parse(req)
50 | assert {{:ok, nil}, model1} == Model.apply(model0, cmd)
51 | assert res == Response.pack(cmd, nil)
52 | assert nil == Response.parse(cmd, res)
53 | # length predition
54 | assert byte_size(res) == Response.length(cmd)
55 | # rtu
56 | rtu_req = Rtu.pack_req(cmd)
57 | assert cmd == Rtu.parse_req(rtu_req)
58 | rtu_res = Rtu.pack_res(cmd, nil)
59 | assert nil == Rtu.parse_res(cmd, rtu_res)
60 | assert byte_size(rtu_res) == Rtu.res_len(cmd)
61 | # tcp
62 | tcp_req = Tcp.pack_req(cmd, 1)
63 | assert {cmd, 1} == Tcp.parse_req(tcp_req)
64 | tcp_res = Tcp.pack_res(cmd, nil, 1)
65 | assert nil == Tcp.parse_res(cmd, tcp_res, 1)
66 | assert byte_size(tcp_res) == Tcp.res_len(cmd)
67 | # master
68 | Server.start_link(model: model0, port: 2001)
69 | {:ok, master_pid} = Client.start_link(tcp_port: 2001, ip: {127, 0, 0, 1})
70 | Client.connect(master_pid)
71 |
72 | for _ <- 0..10 do
73 | Client.request(master_pid, cmd)
74 | assert :ok == Client.confirmation(master_pid)
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------