├── .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 | modbux Logo 3 |
4 | 5 | *** 6 |
7 |
8 | Valiot Logo 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 | --------------------------------------------------------------------------------