├── .formatter.exs ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── assets └── images │ ├── compatibility.png │ ├── snapex7-logo.png │ └── valiot-logo-blue.png ├── config └── config.exs ├── lib ├── snap7 │ └── client.ex └── snapex7.ex ├── mix.exs ├── mix.lock ├── pull_request_template.md ├── src ├── Makefile ├── erlcmd.c ├── erlcmd.h ├── s7_client.c ├── s7_partner.c ├── s7_server.c ├── util.c └── util.h └── test ├── c_backend_tests ├── c_driver_test.exs ├── cli_admin_test.exs ├── cli_block_oriented_test.exs ├── cli_data_io_test.exs ├── cli_date_time_test.exs ├── cli_directory_test.exs ├── cli_low_level_test.exs ├── cli_misc_test.exs ├── cli_plc_control_test.exs ├── cli_security_test.exs └── cli_sys_info_test.exs ├── s7_client_test ├── admin_fun_test.exs ├── block_oriented_fun_test.exs ├── data_io_fun_test.exs ├── date_time_fun_test.exs ├── directory_fun_test.exs ├── low_level_fun_test.exs ├── miscellaneous_fun_test.exs ├── plc_control_fun_test.exs ├── security_fun_test.exs └── system_info_fun_test.exs ├── snapex7_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | .elixir_ls/ 3 | .vscode/ 4 | /_build/ 5 | 6 | # If you run "mix test --cover", coverage assets end up here. 7 | /cover/ 8 | 9 | # The directory Mix downloads your dependencies sources to. 10 | /deps/ 11 | 12 | # Where 3rd-party dependencies like ExDoc output generated docs. 13 | /doc/ 14 | 15 | # Ignore .fetch files in case you like to edit your project deps locally. 16 | /.fetch 17 | 18 | # If the VM crashes, it generates a dump, let's ignore it too. 19 | erl_crash.dump 20 | 21 | # Also ignore archive artifacts (built via "mix archive.build"). 22 | *.ez 23 | 24 | # Ignore package tarball (built via "mix hex.build"). 25 | snapex7-*.tar 26 | 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "scr/snap7"] 2 | path = scr/snap7 3 | url = git@github.com:valiot/snap7.git 4 | [submodule "src/snap7"] 5 | path = src/snap7 6 | url = git@github.com:valiot/snap7.git 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Snapex7 Logo 3 |
4 | 5 | *** 6 |
7 |
8 | Valiot Logo 9 |
10 |
11 | Snapex7 is an Elixir wrapper for the Snap7 library. Snap7 i san open source, 32/64 bit, multi-platform Ethernet communication suite for interfacing natively with Siemens S7 PLCs. 12 | 13 | * Snapex7 is developer for snap7 1.4.2 and Elixir 1.8.1. It is tested on: 14 | * Ubuntu 15 | * MacOS 16 | * Nerves 17 | 18 | * **Note** Currently only the **Client implementation as synchronous** of Snap7 is available. Future implementations can be found in our [TODO](#todo) section. 19 | 20 | ## Content 21 | 22 | * [Installation](#installation) 23 | * [Target Compatibility](#target-compatibility) 24 | * [Using Snapex7](#using-snapex7) 25 | * [Connection functions](#connection-functions) 26 | * [Data IO functions](#data-io-functions) 27 | * [Error format](#error-format) 28 | * [Types](#types) 29 | * [Further Documentation and Examples](#further-documentation-and-examples) 30 | * [Contributing to this Repo](#contributing-to-this-repo) 31 | * [TODO](#todo) 32 | 33 | ## Installation 34 | 35 | The package can be installed by adding `snapex7` to your list of dependencies in `mix.exs`: 36 | 37 | ```elixir 38 | def deps do 39 | [ 40 | {:snapex7, "~> 0.1.2"} 41 | ] 42 | end 43 | ``` 44 | Fetch the dependency by running `mix deps.get` in your terminal. 45 | 46 | ## Target Compatibility 47 | Many hardware components equipped with an Ethernet port can communicate via the S7 protocol. 48 | Check compatibility with the component that you are using: 49 | * **Snap7** full compatibility documentation can be found at pg.33 [Snap7-refman.pdf](https://github.com/valiot/snap7/blob/a1845454f5f16f3b127b987807f1cbc59205db70/doc/Snap7-refman.pdf) 50 | 51 | Here is a summary compatibility table from the documentation above: 52 | 53 | Compatibility 54 | 55 | ## Using Snapex7 56 | 57 | Start a `Snapex7.Client` and connect it to the PLC's IP, where: 58 | * **port**: C port process 59 | * **controlling_process**: where events get sent 60 | * **queued_messages**: queued messages when in passive mode **(WIP)**. 61 | * **ip**: the address of the server 62 | * **rack**: the rack of the server. 63 | * **slot**: the slot of the server. 64 | * **is_active**: active or passive mode **(WIP)**. 65 | 66 | ```elixir 67 | iex> {:ok, pid} = Snapex7.Client.start_link() 68 | iex> Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 69 | ``` 70 | ### Connection functions 71 | * **Connect to a S7 server**. 72 | The following options are available: 73 | * `:active` - (`true` or `false`) specifies whether data is received as 74 | messages or by calling "Data I/O functions". 75 | * `:ip` - (string) PLC/Equipment IPV4 Address (e.g., "192.168.0.1") 76 | * `:rack` - (int) PLC Rack number (0..7). 77 | * `:slot` - (int) PLC Slot number (1..31). 78 | 79 | ```elixir 80 | iex> :ok = Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 81 | ``` 82 | 83 | * **Disconnects** gracefully the Client from the PLC. 84 | ```elixir 85 | iex> :ok = Snapex7.Client.disconnect(pid) 86 | ``` 87 | 88 | * **Get connected** returns the connection status. 89 | ```elixir 90 | iex> {:ok, boolean} = Snapex7.Client.get_connected(pid) 91 | ``` 92 | 93 | ### Data IO functions 94 | 95 | * **Reads a data area** from a PLC. 96 | The following options are available: 97 | * `:area` - (atom) Area Identifier. See [@area_types](#types). 98 | * `:db_number` - (int) DataBase number, if `area: :DB` otherwise is ignored. 99 | * `:start` - (int) An offset to start. 100 | * `:amount` - (int) Amount of words to read/write. 101 | * `:word_len` - (atom) Word size See [@word_types](#types). 102 | 103 | ```elixir 104 | iex> {:ok, resp_binary} = 105 | Snapex7.Client.read_area(pid, 106 | area: :DB, 107 | word_len: :byte, 108 | start: 2, 109 | amount: 4, 110 | db_number: 1 111 | ) 112 | ``` 113 | 114 | * **Write a data area** from a PLC.. 115 | The same options of `Snapex.Client.read_area` are available here plus: 116 | * `:data` - (atom) buffer to write. 117 | 118 | ```elixir 119 | iex> :ok = 120 | Snapex7.Client.write_area(pid, 121 | area: :DB, 122 | word_len: :byte, 123 | start: 2, 124 | amount: 4, 125 | db_number: 1, 126 | data: <<0x42, 0xCB, 0x00, 0x00>> 127 | ) 128 | ``` 129 | * Lean functions of `Snapex.Client.read_area` and `Snapex.Client.write_area` for different types of ... are implemented: 130 | * `db_read` & `db_write`: for PLC Database. 131 | * `ab_read` & `ab_write`: for PLC process outputs. 132 | * `eb_read` & `eb_write`: for PLC process inputs. 133 | * `mb_read` & `mb_write`: for PLC merkers. 134 | * `tm_read` & `tm_write`: for PLC timers. 135 | * `ct_read` & `ct_write`: for PLC counters. 136 | 137 | * **Write and Read different kinds of variables** from a PLC in a single call. It can do so with it can read DB, inputs, outputs, Merkers, Timers and Counters. 138 | ```elixir 139 | iex> data1 = %{ 140 | area: :DB, 141 | word_len: :byte, 142 | db_number: 1, 143 | start: 2, 144 | amount: 4, 145 | data: <<0x42, 0xCB, 0x20, 0x10>> 146 | } 147 | 148 | iex> data2 = %{ 149 | area: :PA, 150 | word_len: :byte, 151 | db_number: 1, 152 | start: 0, 153 | amount: 1, 154 | data: <<0x0F>> 155 | } 156 | iex> r_data1 = %{ 157 | area: :DB, 158 | word_len: :byte, 159 | db_number: 1, 160 | start: 2, 161 | amount: 4 162 | } 163 | 164 | iex> r_data2 = %{ 165 | area: :PA, 166 | word_len: :byte, 167 | db_number: 1, 168 | start: 0, 169 | amount: 1 170 | } 171 | 172 | iex> :ok = Snapex7.Client.write_multi_vars(pid, data: [data1, data2]) 173 | iex> {:ok, [binary]} = Snapex7.Client.read_multi_vars(pid, data: [r_data1, r_data2]) 174 | ``` 175 | 176 | ### Error format 177 | When a response returns an error, it will have the following format: 178 | ```elixir 179 | iex> {:error, %{eiso: nil, es7: nil, etcp: 113}} 180 | ``` 181 | * **eiso**: returns an atom with the ISO TCP error. 182 | * **es7**: returns an atom with the S7 protocol error. 183 | * **etcp**: returns an int TCP error or nil (Depends on your OS). Example: see [TCP_error_info](https://gist.github.com/gabrielfalcao/4216897) for Ubuntu OS. 184 | 185 | See [Snap7-refman.pdf](https://github.com/valiot/snap7/blob/a1845454f5f16f3b127b987807f1cbc59205db70/doc/Snap7-refman.pdf) pg.252 for further error info. 186 | 187 | ### Types 188 | Arguments definition for each particular memory block: 189 | 190 | ```elixir 191 | @block_types [ 192 | OB: 0x38, # Organization block 193 | DB: 0x41, # Data block 194 | SDB: 0x42, # System data block 195 | FC: 0x43, # Function 196 | SFC: 0x44, # System function 197 | FB: 0x45, # Functional block 198 | SFB: 0x46 # System functional block 199 | ] 200 | 201 | @connection_types [ 202 | PG: 0x01, # Programming console 203 | OP: 0x02, # Siemens HMI panel 204 | S7_basic: 0x03 # Generic data transfer connection 205 | ] 206 | 207 | @area_types [ 208 | PE: 0x81, # Process Inputs 209 | PA: 0x82, # Process Outputs 210 | MK: 0x83, # Merkers 211 | DB: 0x84, # DB 212 | CT: 0x1C, # Counters 213 | TM: 0x1D # Timers 214 | ] 215 | 216 | @word_types [ 217 | bit: 0x01, 218 | byte: 0x02, 219 | word: 0x04, 220 | d_word: 0x06, 221 | real: 0x08, 222 | counter: 0x1C, 223 | timer: 0x1D 224 | ] 225 | ``` 226 | 227 | ## Further documentation and examples 228 | Snapex7 has further client functions implementation which can be found at [Snapex7 Hexdocs](https://hexdocs.pm/snapex7). 229 | * Client function types in [Hexdocs](https://hexdocs.pm/snapex7): 230 | * Administrative 231 | * Data IO 232 | * Directory 233 | * Block oriented 234 | * Date/Time 235 | * System Info 236 | * PLC Control 237 | * Security 238 | * Low level 239 | * Miscellaneous 240 | 241 | * **Additional examples of usage** can be found in the testing section of the project 242 | [/test/s7_client_text/](https://github.com/valiot/snapex7/tree/master/test/s7_client_test) 243 | * **Note**: Tests where writen with a local preconfigured PLC. 244 | 245 | * **Snap7** source code and documentation can be found at [Snap7-refman.pdf](https://github.com/valiot/snap7/blob/a1845454f5f16f3b127b987807f1cbc59205db70/doc/Snap7-refman.pdf) 246 | 247 | ## Contributing to this Repo 248 | * Fork our repository on github. 249 | * Fix or add what is needed. 250 | * Commit to your repository 251 | * Issue a github pullrequest. 252 | 253 | If you wish to clone this Repo use: 254 | ``` 255 | git clone --recursive git@github.com:valiot/snap7.git 256 | ``` 257 | 258 | ## License 259 | 260 | See [LICENSE](./LICENSE). 261 | 262 | ## TODO 263 | * **Better handling c code** 264 | * **Server implementation** 265 | * **Partner implementation** 266 | * **Asynchronous Client implementation** 267 | 268 | -------------------------------------------------------------------------------- /assets/images/compatibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valiot/snapex7/856fa12fc667742a3b56053dd76b1347377ba31b/assets/images/compatibility.png -------------------------------------------------------------------------------- /assets/images/snapex7-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valiot/snapex7/856fa12fc667742a3b56053dd76b1347377ba31b/assets/images/snapex7-logo.png -------------------------------------------------------------------------------- /assets/images/valiot-logo-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valiot/snapex7/856fa12fc667742a3b56053dd76b1347377ba31b/assets/images/valiot-logo-blue.png -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :snapex7, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:snapex7, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /lib/snap7/client.ex: -------------------------------------------------------------------------------- 1 | defmodule Snapex7.Client do 2 | use GenServer 3 | require Logger 4 | 5 | @c_timeout 5000 6 | 7 | @block_types [ 8 | OB: 0x38, 9 | DB: 0x41, 10 | SDB: 0x42, 11 | FC: 0x43, 12 | SFC: 0x44, 13 | FB: 0x45, 14 | SFB: 0x46 15 | ] 16 | 17 | @connection_types [ 18 | PG: 0x01, 19 | OP: 0x02, 20 | S7_basic: 0x03 21 | ] 22 | 23 | @area_types [ 24 | PE: 0x81, 25 | PA: 0x82, 26 | MK: 0x83, 27 | DB: 0x84, 28 | CT: 0x1C, 29 | TM: 0x1D 30 | ] 31 | 32 | @word_types [ 33 | bit: 0x01, 34 | byte: 0x02, 35 | word: 0x04, 36 | d_word: 0x06, 37 | real: 0x08, 38 | counter: 0x1C, 39 | timer: 0x1D 40 | ] 41 | 42 | defmodule State do 43 | @moduledoc false 44 | 45 | # port: C port process 46 | # controlling_process: where events get sent 47 | # queued_messages: queued messages when in passive mode 48 | # ip: the address of the server 49 | # rack: the rack of the server. 50 | # slot: the slot of the server. 51 | # is_active: active or passive mode 52 | defstruct port: nil, 53 | controlling_process: nil, 54 | queued_messages: [], 55 | ip: nil, 56 | rack: nil, 57 | slot: nil, 58 | state: nil, 59 | is_active: false 60 | end 61 | 62 | @doc """ 63 | Start up a Snap7 Client GenServer. 64 | """ 65 | @spec start_link([term]) :: {:ok, pid} | {:error, term} | {:error, :einval} 66 | def start_link(opts \\ []) do 67 | GenServer.start_link(__MODULE__, [], opts) 68 | end 69 | 70 | @doc """ 71 | Stop the Snap7 Client GenServer. 72 | """ 73 | @spec stop(GenServer.server()) :: :ok 74 | def stop(pid) do 75 | GenServer.stop(pid) 76 | end 77 | 78 | # Administrative functions. 79 | 80 | @type connect_opt :: 81 | {:ip, bitstring} 82 | | {:rack, 0..7} 83 | | {:slot, 1..31} 84 | | {:local_tsap, integer} 85 | | {:remote_tsap, integer} 86 | 87 | @doc """ 88 | Connect to a S7 server. 89 | The following options are available: 90 | 91 | * `:active` - (`true` or `false`) specifies whether data is received as 92 | messages or by calling "Data I/O functions". 93 | 94 | * `:ip` - (string) PLC/Equipment IPV4 Address (e.g., "192.168.0.1") 95 | 96 | * `:rack` - (int) PLC Rack number (0..7). 97 | 98 | * `:slot` - (int) PLC Slot number (1..31). 99 | 100 | For more info see pg. 96 form Snap7 docs. 101 | """ 102 | @spec connect_to(GenServer.server(), [connect_opt]) :: :ok | {:error, map()} | {:error, :einval} 103 | def connect_to(pid, opts \\ []) do 104 | GenServer.call(pid, {:connect_to, opts}) 105 | end 106 | 107 | @doc """ 108 | Sets the connection resource type, i.e the way in which the Clients connects to a PLC. 109 | """ 110 | @spec set_connection_type(GenServer.server(), atom()) :: 111 | :ok | {:error, map()} | {:error, :einval} 112 | def set_connection_type(pid, connection_type) do 113 | GenServer.call(pid, {:set_connection_type, connection_type}) 114 | end 115 | 116 | @doc """ 117 | Sets internally (IP, LocalTSAP, RemoteTSAP) Coordinates 118 | The following options are available: 119 | 120 | * `:ip` - (string) PLC/Equipment IPV4 Address (e.g., "192.168.0.1") 121 | 122 | * `:local_tsap` - (int) Local TSAP (PC TSAP) // 0. 123 | 124 | * `:remote_tsap` - (int) Remote TSAP (PLC TSAP) // 0. 125 | """ 126 | @spec set_connection_params(GenServer.server(), [connect_opt]) :: 127 | :ok | {:error, map()} | {:error, :einval} 128 | def set_connection_params(pid, opts \\ []) do 129 | GenServer.call(pid, {:set_connection_params, opts}) 130 | end 131 | 132 | @doc """ 133 | Connects the client to the PLC with the parameters specified in the previous call of 134 | `connect_to/2` or `set_connection_params/2`. 135 | """ 136 | @spec connect(GenServer.server()) :: :ok | {:error, map()} | {:error, :einval} 137 | def connect(pid) do 138 | GenServer.call(pid, :connect) 139 | end 140 | 141 | @doc """ 142 | Disconnects “gracefully” the Client from the PLC. 143 | """ 144 | @spec disconnect(GenServer.server()) :: :ok | {:error, map()} | {:error, :einval} 145 | def disconnect(pid) do 146 | GenServer.call(pid, :disconnect) 147 | end 148 | 149 | @doc """ 150 | Reads an internal Client object parameter. 151 | For more info see pg. 89 form Snap7 docs. 152 | """ 153 | @spec get_params(GenServer.server(), integer()) :: :ok | {:error, map()} | {:error, :einval} 154 | def get_params(pid, param_number) do 155 | GenServer.call(pid, {:get_params, param_number}) 156 | end 157 | 158 | @doc """ 159 | Sets an internal Client object parameter. 160 | """ 161 | @spec set_params(GenServer.server(), integer(), integer()) :: 162 | :ok | {:error, map()} | {:error, :einval} 163 | def set_params(pid, param_number, value) do 164 | GenServer.call(pid, {:set_params, param_number, value}) 165 | end 166 | 167 | @type data_io_opt :: 168 | {:area, atom} 169 | | {:db_number, integer} 170 | | {:start, integer} 171 | | {:amount, integer} 172 | | {:word_len, atom} 173 | | {:data, bitstring} 174 | # Data I/O functions 175 | 176 | @doc """ 177 | Reads a data area from a PLC. 178 | The following options are available: 179 | 180 | * `:area` - (atom) Area Identifier (see @area_types). 181 | 182 | * `:db_number` - (int) DB number, if `area: :DB` otherwise is ignored. 183 | 184 | * `:start` - (int) An offset to start. 185 | 186 | * `:amount` - (int) Amount of words to read/write. 187 | 188 | * `:word_len` - (atom) Word size (see @word_types). 189 | 190 | For more info see pg. 104 form Snap7 docs. 191 | """ 192 | @spec read_area(GenServer.server(), [data_io_opt]) :: 193 | {:ok, bitstring} | {:error, map()} | {:error, :einval} 194 | def read_area(pid, opts) do 195 | GenServer.call(pid, {:read_area, opts}) 196 | end 197 | 198 | @doc """ 199 | Write a data area from a PLC. 200 | The following options are available: 201 | 202 | * `:area` - (atom) Area Identifier (see @area_types). 203 | 204 | * `:db_number` - (int) DB number, if `area: :DB` otherwise is ignored. 205 | 206 | * `:start` - (int) An offset to start. 207 | 208 | * `:amount` - (int) Amount of words to read/write. 209 | 210 | * `:word_len` - (atom) Word size (see @word_types). 211 | 212 | * `:data` - (atom) buffer to write. 213 | 214 | For more info see pg. 104 form Snap7 docs. 215 | """ 216 | @spec write_area(GenServer.server(), [data_io_opt]) :: :ok | {:error, map()} | {:error, :einval} 217 | def write_area(pid, opts) do 218 | GenServer.call(pid, {:write_area, opts}) 219 | end 220 | 221 | @doc """ 222 | This is a lean function of read_area/2 to read PLC DB. 223 | It simply internally calls read_area/2 with 224 | * `area: :DB` 225 | * `word_len: :byte` 226 | 227 | The following options are available: 228 | 229 | * `:db_number` - (int) DB number (0..0xFFFF). 230 | 231 | * `:start` - (int) An offset to start. 232 | 233 | * `:amount` - (int) Amount of words (bytes) to read/write. 234 | 235 | For more info see pg. 104 form Snap7 docs. 236 | """ 237 | @spec db_read(GenServer.server(), [data_io_opt]) :: 238 | {:ok, bitstring} | {:error, map()} | {:error, :einval} 239 | def db_read(pid, opts) do 240 | GenServer.call(pid, {:db_read, opts}) 241 | end 242 | 243 | @doc """ 244 | This is a lean function of write_area/2 to write PLC DB. 245 | It simply internally calls read_area/2 with 246 | * `area: :DB` 247 | * `word_len: :byte` 248 | 249 | The following options are available: 250 | 251 | * `:db_number` - (int) DB number (0..0xFFFF). 252 | 253 | * `:start` - (int) An offset to start. 254 | 255 | * `:amount` - (int) Amount of words (bytes) to read/write. 256 | 257 | * `:data` - (bitstring) buffer to write. 258 | 259 | For more info see pg. 104 form Snap7 docs. 260 | """ 261 | @spec db_write(GenServer.server(), [data_io_opt]) :: :ok | {:error, map()} | {:error, :einval} 262 | def db_write(pid, opts) do 263 | GenServer.call(pid, {:db_write, opts}) 264 | end 265 | 266 | @doc """ 267 | This is a lean function of read_area/2 to read PLC process outputs. 268 | It simply internally calls read_area/2 with 269 | * `area: :PA` 270 | * `word_len: :byte` 271 | 272 | The following options are available: 273 | 274 | * `:start` - (int) An offset to start. 275 | 276 | * `:amount` - (int) Amount of words (bytes) to read/write . 277 | 278 | For more info see pg. 104 form Snap7 docs. 279 | """ 280 | @spec ab_read(GenServer.server(), [data_io_opt]) :: 281 | {:ok, bitstring} | {:error, map()} | {:error, :einval} 282 | def ab_read(pid, opts) do 283 | GenServer.call(pid, {:ab_read, opts}) 284 | end 285 | 286 | @doc """ 287 | This is a lean function of write_area/2 to write PLC process outputs. 288 | It simply internally calls read_area/2 with 289 | * `area: :PA` 290 | * `word_len: :byte` 291 | 292 | The following options are available: 293 | 294 | * `:start` - (int) An offset to start. 295 | 296 | * `:amount` - (int) Amount of words (bytes) to read/write. 297 | 298 | * `:data` - (bitstring) buffer to write. 299 | 300 | For more info see pg. 104 form Snap7 docs. 301 | """ 302 | @spec ab_write(GenServer.server(), [data_io_opt]) :: :ok | {:error, map()} | {:error, :einval} 303 | def ab_write(pid, opts) do 304 | GenServer.call(pid, {:ab_write, opts}) 305 | end 306 | 307 | @doc """ 308 | This is a lean function of read_area/2 to read PLC process inputs. 309 | It simply internally calls read_area/2 with 310 | * `area: :PE` 311 | * `word_len: :byte` 312 | 313 | The following options are available: 314 | 315 | * `:start` - (int) An offset to start. 316 | 317 | * `:amount` - (int) Amount of words (bytes) to read/write . 318 | 319 | For more info see pg. 104 form Snap7 docs. 320 | """ 321 | @spec eb_read(GenServer.server(), [data_io_opt]) :: 322 | {:ok, bitstring} | {:error, map()} | {:error, :einval} 323 | def eb_read(pid, opts) do 324 | GenServer.call(pid, {:eb_read, opts}) 325 | end 326 | 327 | @doc """ 328 | This is a lean function of write_area/2 to write PLC process inputs. 329 | It simply internally calls read_area/2 with 330 | * `area: :PE` 331 | * `word_len: :byte` 332 | 333 | The following options are available: 334 | 335 | * `:start` - (int) An offset to start. 336 | 337 | * `:amount` - (int) Amount of words (bytes) to read/write. 338 | 339 | * `:data` - (bitstring) buffer to write. 340 | 341 | For more info see pg. 104 form Snap7 docs. 342 | """ 343 | @spec eb_write(GenServer.server(), [data_io_opt]) :: :ok | {:error, map()} | {:error, :einval} 344 | def eb_write(pid, opts) do 345 | GenServer.call(pid, {:eb_write, opts}) 346 | end 347 | 348 | @doc """ 349 | This is a lean function of read_area/2 to read PLC merkers. 350 | It simply internally calls read_area/2 with 351 | * `area: :MK` 352 | * `word_len: :byte` 353 | 354 | The following options are available: 355 | 356 | * `:start` - (int) An offset to start. 357 | 358 | * `:amount` - (int) Amount of words (bytes) to read/write . 359 | 360 | For more info see pg. 104 form Snap7 docs. 361 | """ 362 | @spec mb_read(GenServer.server(), [data_io_opt]) :: 363 | {:ok, bitstring} | {:error, map()} | {:error, :einval} 364 | def mb_read(pid, opts) do 365 | GenServer.call(pid, {:mb_read, opts}) 366 | end 367 | 368 | @doc """ 369 | This is a lean function of write_area/2 to write PLC merkers. 370 | It simply internally calls read_area/2 with 371 | * `area: :MK` 372 | * `word_len: :byte` 373 | 374 | The following options are available: 375 | 376 | * `:start` - (int) An offset to start. 377 | 378 | * `:amount` - (int) Amount of words (bytes) to read/write. 379 | 380 | * `:data` - (bitstring) buffer to write. 381 | 382 | For more info see pg. 104 form Snap7 docs. 383 | """ 384 | @spec mb_write(GenServer.server(), [data_io_opt]) :: :ok | {:error, map()} | {:error, :einval} 385 | def mb_write(pid, opts) do 386 | GenServer.call(pid, {:mb_write, opts}) 387 | end 388 | 389 | @doc """ 390 | This is a lean function of read_area/2 to read PLC Timers. 391 | It simply internally calls read_area/2 with 392 | * `area: :TM` 393 | * `word_len: :timer` 394 | 395 | The following options are available: 396 | 397 | * `:start` - (int) An offset to start. 398 | 399 | * `:amount` - (int) Amount of words (bytes) to read/write . 400 | 401 | For more info see pg. 104 form Snap7 docs. 402 | """ 403 | @spec tm_read(GenServer.server(), [data_io_opt]) :: 404 | {:ok, bitstring} | {:error, map()} | {:error, :einval} 405 | def tm_read(pid, opts) do 406 | GenServer.call(pid, {:tm_read, opts}) 407 | end 408 | 409 | @doc """ 410 | This is a lean function of write_area/2 to write PLC Timers. 411 | It simply internally calls read_area/2 with 412 | * `area: :TM` 413 | * `word_len: :timer` 414 | 415 | The following options are available: 416 | 417 | * `:start` - (int) An offset to start. 418 | 419 | * `:amount` - (int) Amount of words (bytes) to read/write. 420 | 421 | * `:data` - (bitstring) buffer to write. 422 | 423 | For more info see pg. 104 form Snap7 docs. 424 | """ 425 | @spec tm_write(GenServer.server(), [data_io_opt]) :: :ok | {:error, map()} | {:error, :einval} 426 | def tm_write(pid, opts) do 427 | GenServer.call(pid, {:tm_write, opts}) 428 | end 429 | 430 | @doc """ 431 | This is a lean function of read_area/2 to read PLC Counters. 432 | It simply internally calls read_area/2 with 433 | * `area: :CT` 434 | * `word_len: :timer` 435 | 436 | The following options are available: 437 | 438 | * `:start` - (int) An offset to start. 439 | 440 | * `:amount` - (int) Amount of words (bytes) to read/write . 441 | 442 | For more info see pg. 104 form Snap7 docs. 443 | """ 444 | @spec ct_read(GenServer.server(), [data_io_opt]) :: 445 | {:ok, bitstring} | {:error, map()} | {:error, :einval} 446 | def ct_read(pid, opts) do 447 | GenServer.call(pid, {:ct_read, opts}) 448 | end 449 | 450 | @doc """ 451 | This is a lean function of write_area/2 to write PLC Counters. 452 | It simply internally calls read_area/2 with 453 | * `area: :CT` 454 | * `word_len: :timer` 455 | 456 | The following options are available: 457 | 458 | * `:start` - (int) An offset to start. 459 | 460 | * `:amount` - (int) Amount of words (bytes) to read/write. 461 | 462 | * `:data` - (bitstring) buffer to write. 463 | 464 | For more info see pg. 104 form Snap7 docs. 465 | """ 466 | @spec ct_write(GenServer.server(), [data_io_opt]) :: :ok | {:error, map()} | {:error, :einval} 467 | def ct_write(pid, opts) do 468 | GenServer.call(pid, {:ct_write, opts}) 469 | end 470 | 471 | @doc """ 472 | This function allows to read different kind of variables from a PLC in a single call. 473 | With it can read DB, inputs, outputs, Merkers, Timers and Counters. 474 | 475 | The following options are available: 476 | 477 | * `:data` - (list of maps) a list of requests (maps with @data_io_opt options as keys) to read from PLC. 478 | 479 | For more info see pg. 119 form Snap7 docs. 480 | """ 481 | @spec read_multi_vars(GenServer.server(), list) :: 482 | {:ok, bitstring} | {:error, map()} | {:error, :einval} 483 | def read_multi_vars(pid, opt) do 484 | GenServer.call(pid, {:read_multi_vars, opt}) 485 | end 486 | 487 | @doc """ 488 | This function allows to write different kind of variables from a PLC in a single call. 489 | With it can read DB, inputs, outputs, Merkers, Timers and Counters. 490 | 491 | The following options are available: 492 | 493 | * `:data` - (list of maps) a list of requests (maps with @data_io_opt options as keys) to read from PLC. 494 | 495 | For more info see pg. 119 form Snap7 docs. 496 | """ 497 | @spec write_multi_vars(GenServer.server(), [data_io_opt]) :: 498 | :ok | {:error, map()} | {:error, :einval} 499 | def write_multi_vars(pid, opts) do 500 | GenServer.call(pid, {:write_multi_vars, opts}) 501 | end 502 | 503 | # Directory functions 504 | 505 | @doc """ 506 | This function returns the AG blocks amount divided by type. 507 | """ 508 | @spec list_blocks(GenServer.server()) :: {:ok, list} | {:error, map()} | {:error, :einval} 509 | def list_blocks(pid) do 510 | GenServer.call(pid, :list_blocks) 511 | end 512 | 513 | @doc """ 514 | This function returns the AG list of a specified block type. 515 | """ 516 | @spec list_blocks_of_type(GenServer.server(), atom(), integer()) :: 517 | {:ok, list} | {:error, map} | {:error, :einval} 518 | def list_blocks_of_type(pid, block_type, n_items) do 519 | GenServer.call(pid, {:list_blocks_of_type, block_type, n_items}) 520 | end 521 | 522 | @doc """ 523 | Return detail information about an AG given block. 524 | This function is very useful if you nead to read or write data in a DB 525 | which you do not know the size in advance (see pg 127). 526 | """ 527 | @spec get_ag_block_info(GenServer.server(), atom(), integer()) :: 528 | {:ok, list} | {:error, map} | {:error, :einval} 529 | def get_ag_block_info(pid, block_type, block_num) do 530 | GenServer.call(pid, {:get_ag_block_info, block_type, block_num}) 531 | end 532 | 533 | @doc """ 534 | Return detailed information about a block present in a user buffer. 535 | This function is usually used in conjunction with full_upload/2. 536 | An uploaded a block saved to disk, could be loaded in a user buffer 537 | and checked with this function. 538 | """ 539 | @spec get_pg_block_info(GenServer.server(), bitstring()) :: 540 | {:ok, list} | {:error, map} | {:error, :einval} 541 | def get_pg_block_info(pid, buffer) do 542 | GenServer.call(pid, {:get_pg_block_info, buffer}) 543 | end 544 | 545 | # Block Oriented functions 546 | 547 | @doc """ 548 | Uploads a block from AG. (gets a block from PLC) 549 | The whole block (including header and footer) is copied into the user buffer (as bytes). 550 | """ 551 | @spec full_upload(GenServer.server(), atom(), integer(), integer()) :: 552 | {:ok, bitstring} | {:error, map} | {:error, :einval} 553 | def full_upload(pid, block_type, block_num, bytes2read) do 554 | GenServer.call(pid, {:full_upload, block_type, block_num, bytes2read}) 555 | end 556 | 557 | @doc """ 558 | Uploads a block from AG. (gets a block from PLC) 559 | Only the block body (but header and footer) is copied into the user buffer (as bytes). 560 | """ 561 | @spec upload(GenServer.server(), atom(), integer(), integer()) :: 562 | {:ok, bitstring} | {:error, map} | {:error, :einval} 563 | def upload(pid, block_type, block_num, bytes2read) do 564 | GenServer.call(pid, {:upload, block_type, block_num, bytes2read}) 565 | end 566 | 567 | @doc """ 568 | Downloads a block from AG. (gets a block from PLC) 569 | The whole block (including header and footer) must be available into the user buffer. 570 | """ 571 | @spec download(GenServer.server(), integer(), bitstring()) :: 572 | :ok | {:error, map} | {:error, :einval} 573 | def download(pid, block_num, buffer) do 574 | GenServer.call(pid, {:download, block_num, buffer}) 575 | end 576 | 577 | @doc """ 578 | Deletes a block from AG. 579 | (There is an undo function available). 580 | """ 581 | @spec delete(GenServer.server(), atom(), integer()) :: :ok | {:error, map} | {:error, :einval} 582 | def delete(pid, block_type, block_num) do 583 | GenServer.call(pid, {:delete, block_type, block_num}) 584 | end 585 | 586 | @doc """ 587 | Uploads a DB from AG. 588 | This function is equivalent to upload/4 with block_type = :DB but it uses a 589 | different approach so it's not subject to the security level set. 590 | Only data is uploaded. 591 | """ 592 | @spec db_get(GenServer.server(), integer(), integer()) :: 593 | {:ok, list} | {:error, map} | {:error, :einval} 594 | def db_get(pid, db_number, size \\ 65536) do 595 | GenServer.call(pid, {:db_get, db_number, size}) 596 | end 597 | 598 | @doc """ 599 | Fills a DB in AG qirh a given byte without the need of specifying its size. 600 | """ 601 | @spec db_fill(GenServer.server(), integer(), integer()) :: 602 | {:ok, list} | {:error, map} | {:error, :einval} 603 | def db_fill(pid, db_number, fill_char) do 604 | GenServer.call(pid, {:db_fill, db_number, fill_char}) 605 | end 606 | 607 | # Date/Time functions 608 | 609 | @doc """ 610 | Reads PLC date and time, if successful, returns `{:ok, date, time}` 611 | """ 612 | @spec get_plc_date_time(GenServer.server()) :: 613 | {:ok, term, term} | {:error, map} | {:error, :einval} 614 | def get_plc_date_time(pid) do 615 | GenServer.call(pid, :get_plc_date_time) 616 | end 617 | 618 | @type plc_time_opt :: 619 | {:sec, 0..59} 620 | | {:min, 0..7} 621 | | {:hour, 0..23} 622 | | {:mday, 1..31} 623 | | {:mon, 1..12} 624 | | {:year, integer} 625 | | {:wday, 0..6} 626 | | {:yday, 0..365} 627 | | {:isdst, integer} 628 | @doc """ 629 | Sets PLC date and time. 630 | The following options are available: 631 | 632 | * `:sec` - (int) seconds afer the minute (0..59). 633 | 634 | * `:min` - (int) minutes after the hour (0..59). 635 | 636 | * `:hour` - (int) hour since midnight (0..23). 637 | 638 | * `:mday` - (int) day of the month (1..31). 639 | 640 | * `:mon` - (int) month since January (1..12). 641 | 642 | * `:year` - (int) year (1900...). 643 | 644 | * `:wday` - (int) days since Sunday (0..6). 645 | 646 | * `:yday` - (int) days since January 1 (0..365). 647 | 648 | * `:isdst` - (int) Daylight Saving Time flag. 649 | 650 | The default is of all functions are the minimum value. 651 | """ 652 | @spec set_plc_date_time(GenServer.server(), [plc_time_opt]) :: 653 | :ok | {:error, map} | {:error, :einval} 654 | def set_plc_date_time(pid, opts \\ []) do 655 | GenServer.call(pid, {:set_plc_date_time, opts}) 656 | end 657 | 658 | @doc """ 659 | Sets the PLC date and time in accord to the PC system Date/Time. 660 | """ 661 | @spec set_plc_system_date_time(GenServer.server()) :: :ok | {:error, map} | {:error, :einval} 662 | def set_plc_system_date_time(pid) do 663 | GenServer.call(pid, :set_plc_system_date_time) 664 | end 665 | 666 | # System info functions 667 | 668 | @doc """ 669 | Reads a partial list of given ID and INDEX 670 | See System Software for S7-300/400 System and Standard Functions 671 | Volume 1 and Volume 2 for ID and INDEX info (chapter 13.3), look for 672 | TIA Portal Information Systems for DR data type. 673 | """ 674 | @spec read_szl(GenServer.server(), integer, integer) :: 675 | {:ok, bitstring} | {:error, map} | {:error, :einval} 676 | def read_szl(pid, id, index) do 677 | GenServer.call(pid, {:read_szl, id, index}) 678 | end 679 | 680 | @doc """ 681 | Reads the directory of the partial list 682 | """ 683 | @spec read_szl_list(GenServer.server()) :: {:ok, list} | {:error, map} | {:error, :einval} 684 | def read_szl_list(pid) do 685 | GenServer.call(pid, :read_szl_list) 686 | end 687 | 688 | @doc """ 689 | Gets CPU order code and version info. 690 | """ 691 | @spec get_order_code(GenServer.server()) :: {:ok, list} | {:error, map} | {:error, :einval} 692 | def get_order_code(pid) do 693 | GenServer.call(pid, :get_order_code) 694 | end 695 | 696 | @doc """ 697 | Gets CPU module name, serial number and other info. 698 | """ 699 | @spec get_cpu_info(GenServer.server()) :: {:ok, list} | {:error, map} | {:error, :einval} 700 | def get_cpu_info(pid) do 701 | GenServer.call(pid, :get_cpu_info) 702 | end 703 | 704 | @doc """ 705 | Gets CP (communication processor) info. 706 | """ 707 | @spec get_cp_info(GenServer.server()) :: {:ok, list} | {:error, map} | {:error, :einval} 708 | def get_cp_info(pid) do 709 | GenServer.call(pid, :get_cp_info) 710 | end 711 | 712 | # PLC control functions 713 | 714 | @doc """ 715 | Puts the CPU in RUN mode performing an HOT START. 716 | """ 717 | @spec plc_hot_start(GenServer.server()) :: :ok | {:error, map} | {:error, :einval} 718 | def plc_hot_start(pid) do 719 | GenServer.call(pid, :plc_hot_start) 720 | end 721 | 722 | @doc """ 723 | Puts the CPU in RUN mode performing an COLD START. 724 | """ 725 | @spec plc_cold_start(GenServer.server()) :: :ok | {:error, map} | {:error, :einval} 726 | def plc_cold_start(pid) do 727 | GenServer.call(pid, :plc_cold_start) 728 | end 729 | 730 | @doc """ 731 | Puts the CPU in STOP mode. 732 | """ 733 | @spec plc_stop(GenServer.server()) :: :ok | {:error, map} | {:error, :einval} 734 | def plc_stop(pid) do 735 | GenServer.call(pid, :plc_stop) 736 | end 737 | 738 | @doc """ 739 | Performs the copy ram to rom action. (CPU must be in STOP mode) 740 | """ 741 | @spec copy_ram_to_rom(GenServer.server(), integer) :: :ok | {:error, map} | {:error, :einval} 742 | def copy_ram_to_rom(pid, timeout \\ 1000) do 743 | GenServer.call(pid, {:copy_ram_to_rom, timeout}) 744 | end 745 | 746 | @doc """ 747 | Performas the Memory compress action (not all CPU's supports this function and the CPU must be in STOP mode). 748 | """ 749 | @spec compress(GenServer.server(), integer) :: :ok | {:error, map} | {:error, :einval} 750 | def compress(pid, timeout \\ 1000) do 751 | GenServer.call(pid, {:compress, timeout}) 752 | end 753 | 754 | @doc """ 755 | Returns the CPU status (running/stoppped). 756 | """ 757 | @spec get_plc_status(GenServer.server()) :: :ok | {:error, map} | {:error, :einval} 758 | def get_plc_status(pid) do 759 | GenServer.call(pid, :get_plc_status) 760 | end 761 | 762 | # Security functions 763 | 764 | @doc """ 765 | Send the password (an 8 chars string) to the PLC to meet its security level. 766 | """ 767 | @spec set_session_password(GenServer.server(), bitstring()) :: 768 | :ok | {:error, map} | {:error, :einval} 769 | def set_session_password(pid, password) do 770 | GenServer.call(pid, {:set_session_password, password}) 771 | end 772 | 773 | @doc """ 774 | Clears the password set for the current session (logout). 775 | """ 776 | @spec clear_session_password(GenServer.server()) :: :ok | {:error, map} | {:error, :einval} 777 | def clear_session_password(pid) do 778 | GenServer.call(pid, :clear_session_password) 779 | end 780 | 781 | @doc """ 782 | Gets the CPU protection level info. 783 | """ 784 | @spec get_protection(GenServer.server()) :: :ok | {:error, map} | {:error, :einval} 785 | def get_protection(pid) do 786 | GenServer.call(pid, :get_protection) 787 | end 788 | 789 | # Low level functions 790 | 791 | @doc """ 792 | Exchanges a given S7 PDU (protocol data unit) with the CPU. 793 | """ 794 | @spec iso_exchange_buffer(GenServer.server(), bitstring) :: 795 | :ok | {:error, map} | {:error, :einval} 796 | def iso_exchange_buffer(pid, buffer) do 797 | GenServer.call(pid, {:iso_exchange_buffer, buffer}) 798 | end 799 | 800 | # Miscellaneous functions 801 | 802 | @doc """ 803 | Returns the last job execution time in miliseconds. 804 | """ 805 | @spec get_exec_time(GenServer.server()) :: {:ok, integer} | {:error, map} | {:error, :einval} 806 | def get_exec_time(pid) do 807 | GenServer.call(pid, :get_exec_time) 808 | end 809 | 810 | @doc """ 811 | Returns the last job result. 812 | """ 813 | @spec get_last_error(GenServer.server()) :: {:ok, map} | {:error, map} | {:error, :einval} 814 | def get_last_error(pid) do 815 | GenServer.call(pid, :get_last_error) 816 | end 817 | 818 | @doc """ 819 | Returns info about the PDU length. 820 | """ 821 | @spec get_pdu_length(GenServer.server()) :: {:ok, list} | {:error, map} | {:error, :einval} 822 | def get_pdu_length(pid) do 823 | GenServer.call(pid, :get_pdu_length) 824 | end 825 | 826 | @doc """ 827 | Returns the connection status. 828 | """ 829 | @spec get_connected(GenServer.server()) :: {:ok, boolean} | {:error, map} | {:error, :einval} 830 | def get_connected(pid) do 831 | GenServer.call(pid, :get_connected) 832 | end 833 | 834 | @doc """ 835 | This function can execute any desired function as a request. 836 | The `request` can be a tuple (the first element is an atom according to the desired function to be executed, 837 | and the following elements are the args of the desired function) or an atom (when the desired function 838 | has no arguments), for example: 839 | request = {:connect_to , [ip: "192.168.1.100", rack: 0, slot: 0]}, 840 | request = :get_connected 841 | """ 842 | @spec command(GenServer.server(), term) :: :ok | {:ok, term} | {:error, map} | {:error, :einval} 843 | def command(pid, request) do 844 | GenServer.call(pid, request) 845 | end 846 | 847 | @spec init([]) :: {:ok, Snapex7.Client.State.t()} 848 | def init([]) do 849 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 850 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 851 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 852 | 853 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 854 | 855 | port = 856 | Port.open({:spawn_executable, executable}, [ 857 | {:args, []}, 858 | {:packet, 2}, 859 | :use_stdio, 860 | :binary, 861 | :exit_status 862 | ]) 863 | 864 | state = %State{port: port} 865 | {:ok, state} 866 | end 867 | 868 | # Administrative funtions 869 | 870 | def handle_call({:connect_to, opts}, {from_pid, _}, state) do 871 | ip = Keyword.fetch!(opts, :ip) 872 | rack = Keyword.get(opts, :rack, 0) 873 | slot = Keyword.get(opts, :slot, 0) 874 | active = Keyword.get(opts, :active, false) 875 | 876 | response = call_port(state, :connect_to, {ip, rack, slot}) 877 | 878 | new_state = 879 | case response do 880 | :ok -> 881 | %State{ 882 | state 883 | | state: :connected, 884 | ip: ip, 885 | rack: rack, 886 | slot: slot, 887 | is_active: active, 888 | controlling_process: from_pid 889 | } 890 | 891 | {:error, _x} -> 892 | %State{state | state: :idle} 893 | end 894 | 895 | {:reply, response, new_state} 896 | end 897 | 898 | def handle_call({:set_connection_type, connection_type}, _from, state) do 899 | connection_type = Keyword.fetch!(@connection_types, connection_type) 900 | response = call_port(state, :set_connection_type, connection_type) 901 | {:reply, response, state} 902 | end 903 | 904 | def handle_call({:set_connection_params, opts}, _from, state) do 905 | ip = Keyword.fetch!(opts, :ip) 906 | local_tsap = Keyword.get(opts, :local_tsap, 0) 907 | remote_tsap = Keyword.get(opts, :remote_tsap, 0) 908 | response = call_port(state, :set_connection_params, {ip, local_tsap, remote_tsap}) 909 | {:reply, response, state} 910 | end 911 | 912 | def handle_call(:connect, _from, state) do 913 | response = call_port(state, :connect, nil) 914 | 915 | new_state = 916 | case response do 917 | :ok -> 918 | %{state | state: :connected} 919 | 920 | {:error, _x} -> 921 | %State{state | state: :idle} 922 | end 923 | 924 | {:reply, response, new_state} 925 | end 926 | 927 | def handle_call(:disconnect, {_from, _}, state) do 928 | response = call_port(state, :disconnect, nil) 929 | new_state = %State{state | state: :idle} 930 | {:reply, response, new_state} 931 | end 932 | 933 | def handle_call({:get_params, param_number}, {_from, _}, state) do 934 | response = call_port(state, :get_params, param_number) 935 | {:reply, response, state} 936 | end 937 | 938 | def handle_call({:set_params, param_number, value}, {_from, _}, state) do 939 | response = call_port(state, :set_params, {param_number, value}) 940 | {:reply, response, state} 941 | end 942 | 943 | # Data I/O functions 944 | 945 | def handle_call({:read_area, opts}, _from, state) do 946 | area_key = Keyword.fetch!(opts, :area) 947 | word_len_key = Keyword.get(opts, :word_len, :byte) 948 | db_number = Keyword.get(opts, :db_number, 0) 949 | start = Keyword.get(opts, :start, 0) 950 | amount = Keyword.get(opts, :amount, 0) 951 | area_type = Keyword.fetch!(@area_types, area_key) 952 | word_type = Keyword.fetch!(@word_types, word_len_key) 953 | response = call_port(state, :read_area, {area_type, db_number, start, amount, word_type}) 954 | {:reply, response, state} 955 | end 956 | 957 | def handle_call({:write_area, opts}, _from, state) do 958 | area_key = Keyword.fetch!(opts, :area) 959 | word_len_key = Keyword.get(opts, :word_len, :byte) 960 | db_number = Keyword.get(opts, :db_number, 0) 961 | start = Keyword.get(opts, :start, 0) 962 | data = Keyword.fetch!(opts, :data) 963 | amount = Keyword.get(opts, :amount, byte_size(data)) 964 | area_type = Keyword.fetch!(@area_types, area_key) 965 | word_type = Keyword.fetch!(@word_types, word_len_key) 966 | 967 | response = 968 | call_port(state, :write_area, {area_type, db_number, start, amount, word_type, data}) 969 | 970 | {:reply, response, state} 971 | end 972 | 973 | def handle_call({:db_read, opts}, _from, state) do 974 | db_number = Keyword.get(opts, :db_number, 0) 975 | start = Keyword.get(opts, :start, 0) 976 | amount = Keyword.get(opts, :amount, 0) 977 | response = call_port(state, :db_read, {db_number, start, amount}) 978 | {:reply, response, state} 979 | end 980 | 981 | def handle_call({:db_write, opts}, _from, state) do 982 | db_number = Keyword.get(opts, :db_number, 0) 983 | start = Keyword.get(opts, :start, 0) 984 | data = Keyword.fetch!(opts, :data) 985 | amount = Keyword.get(opts, :amount, byte_size(data)) 986 | response = call_port(state, :db_write, {db_number, start, amount, data}) 987 | {:reply, response, state} 988 | end 989 | 990 | def handle_call({:ab_read, opts}, _from, state) do 991 | start = Keyword.get(opts, :start, 0) 992 | amount = Keyword.get(opts, :amount, 0) 993 | response = call_port(state, :ab_read, {start, amount}) 994 | {:reply, response, state} 995 | end 996 | 997 | def handle_call({:ab_write, opts}, _from, state) do 998 | start = Keyword.get(opts, :start, 0) 999 | data = Keyword.fetch!(opts, :data) 1000 | amount = Keyword.get(opts, :amount, byte_size(data)) 1001 | response = call_port(state, :ab_write, {start, amount, data}) 1002 | {:reply, response, state} 1003 | end 1004 | 1005 | def handle_call({:eb_read, opts}, _from, state) do 1006 | start = Keyword.get(opts, :start, 0) 1007 | amount = Keyword.get(opts, :amount, 0) 1008 | response = call_port(state, :eb_read, {start, amount}) 1009 | {:reply, response, state} 1010 | end 1011 | 1012 | def handle_call({:eb_write, opts}, _from, state) do 1013 | start = Keyword.get(opts, :start, 0) 1014 | data = Keyword.fetch!(opts, :data) 1015 | amount = Keyword.get(opts, :amount, byte_size(data)) 1016 | response = call_port(state, :eb_write, {start, amount, data}) 1017 | {:reply, response, state} 1018 | end 1019 | 1020 | def handle_call({:mb_read, opts}, _from, state) do 1021 | start = Keyword.get(opts, :start, 0) 1022 | amount = Keyword.get(opts, :amount, 0) 1023 | response = call_port(state, :mb_read, {start, amount}) 1024 | {:reply, response, state} 1025 | end 1026 | 1027 | def handle_call({:mb_write, opts}, _from, state) do 1028 | start = Keyword.get(opts, :start, 0) 1029 | data = Keyword.fetch!(opts, :data) 1030 | amount = Keyword.get(opts, :amount, byte_size(data)) 1031 | response = call_port(state, :mb_write, {start, amount, data}) 1032 | {:reply, response, state} 1033 | end 1034 | 1035 | def handle_call({:tm_read, opts}, _from, state) do 1036 | start = Keyword.get(opts, :start, 0) 1037 | amount = Keyword.get(opts, :amount, 0) 1038 | response = call_port(state, :tm_read, {start, amount}) 1039 | {:reply, response, state} 1040 | end 1041 | 1042 | def handle_call({:tm_write, opts}, _from, state) do 1043 | start = Keyword.get(opts, :start, 0) 1044 | data = Keyword.fetch!(opts, :data) 1045 | amount = Keyword.get(opts, :amount, byte_size(data)) 1046 | response = call_port(state, :tm_write, {start, amount, data}) 1047 | {:reply, response, state} 1048 | end 1049 | 1050 | def handle_call({:ct_read, opts}, _from, state) do 1051 | start = Keyword.get(opts, :start, 0) 1052 | amount = Keyword.get(opts, :amount, 0) 1053 | response = call_port(state, :ct_read, {start, amount}) 1054 | {:reply, response, state} 1055 | end 1056 | 1057 | def handle_call({:ct_write, opts}, _from, state) do 1058 | start = Keyword.get(opts, :start, 0) 1059 | data = Keyword.fetch!(opts, :data) 1060 | amount = Keyword.get(opts, :amount, byte_size(data)) 1061 | response = call_port(state, :ct_write, {start, amount, data}) 1062 | {:reply, response, state} 1063 | end 1064 | 1065 | def handle_call({:read_multi_vars, opts}, _from, state) do 1066 | data = Keyword.fetch!(opts, :data) |> Enum.map(&key2value/1) 1067 | size = length(data) 1068 | response = call_port(state, :read_multi_vars, {size, data}) 1069 | {:reply, response, state} 1070 | end 1071 | 1072 | def handle_call({:write_multi_vars, opts}, _from, state) do 1073 | data = Keyword.fetch!(opts, :data) |> Enum.map(&key2value/1) 1074 | size = length(data) 1075 | response = call_port(state, :write_multi_vars, {size, data}) 1076 | {:reply, response, state} 1077 | end 1078 | 1079 | # Directory functions 1080 | 1081 | def handle_call(:list_blocks, _from, state) do 1082 | response = call_port(state, :list_blocks, nil) 1083 | {:reply, response, state} 1084 | end 1085 | 1086 | def handle_call({:list_blocks_of_type, block_type, n_items}, _from, state) do 1087 | block_value = Keyword.fetch!(@block_types, block_type) 1088 | response = call_port(state, :list_blocks_of_type, {block_value, n_items}) 1089 | {:reply, response, state} 1090 | end 1091 | 1092 | def handle_call({:get_ag_block_info, block_type, block_num}, _from, state) do 1093 | block_value = Keyword.fetch!(@block_types, block_type) 1094 | response = call_port(state, :get_ag_block_info, {block_value, block_num}) 1095 | {:reply, response, state} 1096 | end 1097 | 1098 | def handle_call({:get_pg_block_info, buffer}, _from, state) do 1099 | b_size = byte_size(buffer) 1100 | response = call_port(state, :get_pg_block_info, {b_size, buffer}) 1101 | {:reply, response, state} 1102 | end 1103 | 1104 | # Block Oriented functions 1105 | 1106 | def handle_call({:full_upload, block_type, block_num, bytes2read}, _from, state) do 1107 | block_value = Keyword.fetch!(@block_types, block_type) 1108 | response = call_port(state, :full_upload, {block_value, block_num, bytes2read}) 1109 | {:reply, response, state} 1110 | end 1111 | 1112 | def handle_call({:upload, block_type, block_num, bytes2read}, _from, state) do 1113 | block_value = Keyword.fetch!(@block_types, block_type) 1114 | response = call_port(state, :upload, {block_value, block_num, bytes2read}) 1115 | {:reply, response, state} 1116 | end 1117 | 1118 | def handle_call({:download, block_num, buffer}, _from, state) do 1119 | b_size = byte_size(buffer) 1120 | response = call_port(state, :download, {block_num, b_size, buffer}) 1121 | {:reply, response, state} 1122 | end 1123 | 1124 | def handle_call({:delete, block_type, block_num}, _from, state) do 1125 | block_value = Keyword.fetch!(@block_types, block_type) 1126 | response = call_port(state, :delete, {block_value, block_num}) 1127 | {:reply, response, state} 1128 | end 1129 | 1130 | def handle_call({:db_get, db_number, size}, _from, state) do 1131 | response = call_port(state, :db_get, {db_number, size}) 1132 | {:reply, response, state} 1133 | end 1134 | 1135 | def handle_call({:db_fill, db_number, fill_char}, _from, state) do 1136 | response = call_port(state, :db_fill, {db_number, fill_char}) 1137 | {:reply, response, state} 1138 | end 1139 | 1140 | # Date/Time functions 1141 | 1142 | def handle_call(:get_plc_date_time, _from, state) do 1143 | response = 1144 | case call_port(state, :get_plc_date_time, nil) do 1145 | {:ok, tm} -> 1146 | {:ok, time} = Time.new(tm.tm_hour, tm.tm_min, tm.tm_sec) 1147 | {:ok, date} = Date.new(tm.tm_year, tm.tm_mon, tm.tm_mday) 1148 | {:ok, date, time} 1149 | 1150 | x -> 1151 | x 1152 | end 1153 | 1154 | {:reply, response, state} 1155 | end 1156 | 1157 | def handle_call({:set_plc_date_time, opt}, _from, state) do 1158 | sec = Keyword.get(opt, :sec, 0) 1159 | min = Keyword.get(opt, :min, 0) 1160 | hour = Keyword.get(opt, :hour, 1) 1161 | mday = Keyword.get(opt, :mday, 1) 1162 | mon = Keyword.get(opt, :mon, 1) 1163 | year = Keyword.get(opt, :year, 1900) 1164 | wday = Keyword.get(opt, :wday, 0) 1165 | yday = Keyword.get(opt, :yday, 0) 1166 | isdst = Keyword.get(opt, :isdst, 1) 1167 | 1168 | response = 1169 | call_port(state, :set_plc_date_time, {sec, min, hour, mday, mon, year, wday, yday, isdst}) 1170 | 1171 | {:reply, response, state} 1172 | end 1173 | 1174 | def handle_call(:set_plc_system_date_time, _from, state) do 1175 | response = call_port(state, :set_plc_system_date_time, nil) 1176 | {:reply, response, state} 1177 | end 1178 | 1179 | # System info functions 1180 | 1181 | def handle_call({:read_szl, id, index}, _from, state) do 1182 | response = call_port(state, :read_szl, {id, index}) 1183 | {:reply, response, state} 1184 | end 1185 | 1186 | def handle_call(:read_szl_list, _from, state) do 1187 | response = call_port(state, :read_szl_list, nil) 1188 | {:reply, response, state} 1189 | end 1190 | 1191 | def handle_call(:get_order_code, _from, state) do 1192 | response = call_port(state, :get_order_code, nil) 1193 | {:reply, response, state} 1194 | end 1195 | 1196 | def handle_call(:get_cpu_info, _from, state) do 1197 | response = call_port(state, :get_cpu_info, nil) 1198 | {:reply, response, state} 1199 | end 1200 | 1201 | def handle_call(:get_cp_info, _from, state) do 1202 | response = call_port(state, :get_cp_info, nil) 1203 | {:reply, response, state} 1204 | end 1205 | 1206 | # PLC control functions 1207 | 1208 | def handle_call(:plc_hot_start, _from, state) do 1209 | response = call_port(state, :plc_hot_start, nil) 1210 | {:reply, response, state} 1211 | end 1212 | 1213 | def handle_call(:plc_cold_start, _from, state) do 1214 | response = call_port(state, :plc_cold_start, nil) 1215 | {:reply, response, state} 1216 | end 1217 | 1218 | def handle_call(:plc_stop, _from, state) do 1219 | response = call_port(state, :plc_stop, nil) 1220 | {:reply, response, state} 1221 | end 1222 | 1223 | def handle_call({:copy_ram_to_rom, timeout}, _from, state) do 1224 | response = call_port(state, :copy_ram_to_rom, timeout) 1225 | {:reply, response, state} 1226 | end 1227 | 1228 | def handle_call({:compress, timeout}, _from, state) do 1229 | response = call_port(state, :compress, timeout) 1230 | {:reply, response, state} 1231 | end 1232 | 1233 | def handle_call(:get_plc_status, _from, state) do 1234 | response = call_port(state, :get_plc_status, nil) 1235 | {:reply, response, state} 1236 | end 1237 | 1238 | # Security functions 1239 | 1240 | def handle_call({:set_session_password, password}, _from, state) do 1241 | response = call_port(state, :set_session_password, password) 1242 | {:reply, response, state} 1243 | end 1244 | 1245 | def handle_call(:clear_session_password, _from, state) do 1246 | response = call_port(state, :clear_session_password, nil) 1247 | {:reply, response, state} 1248 | end 1249 | 1250 | def handle_call(:get_protection, _from, state) do 1251 | response = call_port(state, :get_protection, nil) 1252 | {:reply, response, state} 1253 | end 1254 | 1255 | # Low Level functions 1256 | 1257 | def handle_call({:iso_exchange_buffer, buffer}, _from, state) do 1258 | b_size = byte_size(buffer) 1259 | response = call_port(state, :iso_exchange_buffer, {b_size, buffer}) 1260 | {:reply, response, state} 1261 | end 1262 | 1263 | # Miscellaneous functions 1264 | 1265 | def handle_call(:get_exec_time, _from, state) do 1266 | response = call_port(state, :get_exec_time, nil) 1267 | {:reply, response, state} 1268 | end 1269 | 1270 | def handle_call(:get_last_error, _from, state) do 1271 | response = call_port(state, :get_last_error, nil) 1272 | {:reply, response, state} 1273 | end 1274 | 1275 | def handle_call(:get_pdu_length, _from, state) do 1276 | response = call_port(state, :get_pdu_length, nil) 1277 | {:reply, response, state} 1278 | end 1279 | 1280 | def handle_call(:get_connected, _from, state) do 1281 | response = call_port(state, :get_connected, nil) 1282 | {:reply, response, state} 1283 | end 1284 | 1285 | def handle_call(request, _from, state) do 1286 | Logger.error("(#{__MODULE__}) Invalid request: #{inspect(request)}") 1287 | response = {:error, :einval} 1288 | {:reply, response, state} 1289 | end 1290 | 1291 | defp call_port(state, command, arguments, timeout \\ @c_timeout) do 1292 | msg = {command, arguments} 1293 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 1294 | # Block until the response comes back since the C side 1295 | # doesn't want to handle any queuing of requests. REVISIT 1296 | receive do 1297 | {_, {:data, <>}} -> 1298 | :erlang.binary_to_term(response) 1299 | after 1300 | timeout -> 1301 | # Not sure how this can be recovered 1302 | exit(:port_timed_out) 1303 | end 1304 | end 1305 | 1306 | defp key2value(map) do 1307 | area_key = Map.fetch!(map, :area) 1308 | area_value = Keyword.fetch!(@area_types, area_key) 1309 | map = Map.put(map, :area, area_value) 1310 | word_len_key = Map.get(map, :word_len, :byte) 1311 | word_len_value = Keyword.get(@word_types, word_len_key) 1312 | map = Map.put(map, :word_len, word_len_value) 1313 | map 1314 | end 1315 | end 1316 | -------------------------------------------------------------------------------- /lib/snapex7.ex: -------------------------------------------------------------------------------- 1 | defmodule Snapex7 do 2 | @moduledoc """ 3 | Documentation for Snapex7. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> Snapex7.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Snapex7.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :snapex7, 7 | version: "0.1.4", 8 | elixir: "~> 1.8", 9 | name: "Snapex7", 10 | description: description(), 11 | package: package(), 12 | source_url: "https://github.com/valiot/snapex7", 13 | compilers: [:elixir_make] ++ Mix.compilers(), 14 | make_targets: ["all"], 15 | make_clean: ["clean"], 16 | make_executable: make_executable(), 17 | make_makefile: "src/Makefile", 18 | make_error_message: make_error_message(), 19 | make_env: &make_env/0, 20 | docs: [extras: ["README.md"], main: "readme"], 21 | start_permanent: Mix.env() == :prod, 22 | build_embedded: true, 23 | 24 | deps: deps() 25 | ] 26 | end 27 | 28 | defp make_env() do 29 | base = 30 | Mix.Project.compile_path() 31 | |> Path.join("..") 32 | |> Path.expand() 33 | 34 | %{ 35 | "MIX_ENV" => to_string(Mix.env()), 36 | "PREFIX" => Path.join(base, "priv"), 37 | "BUILD" => Path.join(base, "obj") 38 | } 39 | |> Map.merge(ei_env()) 40 | end 41 | 42 | defp ei_env() do 43 | case System.get_env("ERL_EI_INCLUDE_DIR") do 44 | nil -> 45 | %{ 46 | "ERL_EI_INCLUDE_DIR" => "#{:code.root_dir()}/usr/include", 47 | "ERL_EI_LIBDIR" => "#{:code.root_dir()}/usr/lib" 48 | } 49 | 50 | _ -> 51 | %{} 52 | end 53 | end 54 | 55 | defp make_executable do 56 | case :os.type() do 57 | {:win32, _} -> 58 | "mingw32-make" 59 | 60 | _ -> 61 | :default 62 | end 63 | end 64 | 65 | @windows_mingw_error_msg """ 66 | You may need to install mingw-w64 and make sure that it is in your PATH. Test this by 67 | running `gcc --version` on the command line. 68 | If you do not have mingw-w64, one method to install it is by using 69 | Chocolatey. See http://chocolatey.org to install Chocolatey and run the 70 | following from and command prompt with administrative privileges: 71 | `choco install mingw` 72 | """ 73 | 74 | defp make_error_message do 75 | case :os.type() do 76 | {:win32, _} -> @windows_mingw_error_msg 77 | _ -> :default 78 | end 79 | end 80 | 81 | # Run "mix help compile.app" to learn about applications. 82 | def application do 83 | [ 84 | extra_applications: [:logger] 85 | ] 86 | end 87 | 88 | defp description() do 89 | "Elixir wrapper for Snap7, for communication with Siemens PLC's." 90 | end 91 | 92 | defp package() do 93 | [ 94 | files: [ 95 | "lib", 96 | "src/*.[ch]", 97 | "src/Makefile", 98 | "src/snap7/src", 99 | "src/snap7/build", 100 | "src/snap7/examples/plain-c/*.h", 101 | "test", 102 | "mix.exs", 103 | "README.md", 104 | "LICENSE" 105 | ], 106 | maintainers: ["valiot"], 107 | licenses: ["MIT"], 108 | links: %{"GitHub" => "https://github.com/valiot/snapex7"} 109 | ] 110 | end 111 | 112 | # Run "mix help deps" to learn about dependencies. 113 | defp deps do 114 | [ 115 | {:elixir_make, "~> 0.5", runtime: false}, 116 | {:ex_doc, "~> 0.24", only: :dev, runtime: false}, 117 | ] 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 4 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 5 | "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"}, 6 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 7 | "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"}, 8 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 9 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Makefile targets: 3 | # 4 | # all/install build and install the NIF 5 | # clean clean build products and intermediates 6 | # 7 | # Variables to override: 8 | # 9 | # BUILD where to store intermediate files (defaults to src directory) 10 | # PREFIX path to the installation direction (defaults to ./priv) 11 | # 12 | # CC C compiler 13 | # CROSSCOMPILE crosscompiler prefix, if any 14 | # CFLAGS compiler flags for compiling all C files 15 | # ERL_CFLAGS additional compiler flags for files using Erlang header files 16 | # ERL_EI_INCLUDE_DIR include path to ei.h (Required for crosscompile) 17 | # ERL_EI_LIBDIR path to libei.a (Required for crosscompile) 18 | # LDFLAGS linker flags for linking all binaries 19 | # ERL_LDFLAGS additional linker flags for projects referencing Erlang libraries 20 | 21 | ifeq ($(MIX_COMPILE_PATH),) 22 | $(error MIX_COMPILE_PATH should be set by elixir_make!) 23 | endif 24 | 25 | PREFIX = $(MIX_COMPILE_PATH)/../priv 26 | BUILD = $(MIX_COMPILE_PATH)/../obj 27 | 28 | ## 29 | ## Snap7 OS and processor selector 30 | ## 31 | LibrarySwitch :=-l 32 | ifeq ($(CC), $(filter $(CC), cc gcc)) 33 | ifeq ($(shell uname -s),Linux) 34 | OS :=linux 35 | Libs := $(LibrarySwitch)pthread $(LibrarySwitch)rt 36 | LibExt :=so 37 | ifeq ($(shell uname -m), $(filter $(shell uname -m),x86_64 i386)) 38 | TargetCPU :=$(shell uname -m) 39 | else 40 | $(error processor not supported $(shell uname -m || echo)) 41 | endif 42 | 43 | else ifeq ($(shell uname -s ), Darwin) 44 | OS :=osx 45 | Libs := $(LibrarySwitch)pthread 46 | LibExt :=dylib 47 | ifeq ($(shell uname -m), $(filter $(shell uname -m),x86_64 i386)) 48 | TargetCPU :=$(shell uname -m) 49 | else 50 | $(error processor not supported $(shell uname -m || echo)) 51 | endif 52 | 53 | else 54 | $(error OS not supported $(shell uname -s || echo)) 55 | endif 56 | 57 | else 58 | TargetCPU :=arm_v6 59 | OS :=linux 60 | Libs := $(LibrarySwitch)pthread $(LibrarySwitch)rt 61 | LibExt :=so 62 | endif 63 | 64 | 65 | ## 66 | ## Snap7 config options and dirs 67 | ## 68 | CXXFLAGS := -O3 -fPIC -pedantic 69 | Platform :=$(TargetCPU)-$(OS) 70 | ConfigurationName :=Release 71 | IntermediateDirectory :=$(BUILD) 72 | OutDir :=$(IntermediateDirectory) 73 | SharedObjectLinkerName :=-shared -fPIC 74 | DebugSwitch :=-gstab 75 | IncludeSwitch :=-I 76 | OutputSwitch :=-o 77 | LibraryPathSwitch :=-L 78 | PreprocessorSwitch :=-D 79 | SourceSwitch :=-c 80 | OutputFile :=$(PREFIX)/libsnap.$(LibExt) 81 | PreprocessOnlySwitch :=-E 82 | ObjectsFileList :="filelist.txt" 83 | MakeDirCommand :=mkdir -p 84 | LinkOptions := -O3 85 | IncludePath := $(IncludeSwitch)src/snap7/build/unix $(IncludeSwitch)src/snap7/src/sys $(IncludeSwitch)src/snap7/src/core $(IncludeSwitch)src/snap7/src/lib 86 | LibPath := $(LibraryPathSwitch)src/snap7/build/unix 87 | LibInstall :=$(PREFIX) 88 | 89 | ## 90 | ## User defined environment variables 91 | ## 92 | Objects = $(IntermediateDirectory)/sys_snap_msgsock.o $(IntermediateDirectory)/sys_snap_sysutils.o $(IntermediateDirectory)/sys_snap_tcpsrvr.o $(IntermediateDirectory)/sys_snap_threads.o $(IntermediateDirectory)/core_s7_client.o $(IntermediateDirectory)/core_s7_isotcp.o $(IntermediateDirectory)/core_s7_partner.o $(IntermediateDirectory)/core_s7_peer.o $(IntermediateDirectory)/core_s7_server.o $(IntermediateDirectory)/core_s7_text.o \ 93 | $(IntermediateDirectory)/core_s7_micro_client.o $(IntermediateDirectory)/lib_snap7_libmain.o 94 | 95 | SNAPEX7_OUTPUT = $(LibInstall)/s7_client.o $(LibInstall)/s7_server.o $(LibInstall)/s7_partner.o 96 | 97 | SRC_PATH = src 98 | 99 | OBJ_SNAP7 = $(wildcard $(Objects): $(Occp)) 100 | 101 | OBJ_SNAPEX7 = $(wildcard $(LibInstall)/*.o: $(SRC_PATH)/*.c) 102 | 103 | SNAP7_PATH = src/snap7 104 | S7_H_PATH = /examples/plain-c 105 | 106 | # Set Erlang-specific compile and linker flags 107 | ERL_CFLAGS ?= -I$(ERL_EI_INCLUDE_DIR) 108 | ERL_LDFLAGS ?= -L$(ERL_EI_LIBDIR) -lei 109 | LDFLAGS += 110 | CFLAGS += -std=gnu99 111 | CFLAGS += -DDEBUG 112 | 113 | 114 | .PHONY: all clean 115 | all: $(OutputFile) $(OBJ_SNAPEX7) $(SNAPEX7_OUTPUT) 116 | 117 | ## 118 | ## SNAP7 OUTPUTS 119 | ## 120 | $(OutputFile): $(Objects) 121 | @$(MakeDirCommand) $(LibInstall) 122 | @$(MakeDirCommand) $(@D) 123 | @ 124 | @echo $(Objects) > $(ObjectsFileList) 125 | $(CXX) $(SharedObjectLinkerName) $(OutputSwitch)$(OutputFile) @$(ObjectsFileList) $(LibPath) $(Libs) $(LinkOptions) 126 | $(RM) $(ObjectsFileList) 127 | 128 | ## 129 | ## SNAP7 Objects 130 | ## 131 | $(IntermediateDirectory)/sys_snap_msgsock.o: 132 | @test -d $(IntermediateDirectory) || $(MakeDirCommand) $(IntermediateDirectory) 133 | $(CXX) $(SourceSwitch) "src/snap7/src/sys/snap_msgsock.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 134 | 135 | $(IntermediateDirectory)/sys_snap_sysutils.o: 136 | $(CXX) $(SourceSwitch) "src/snap7/src/sys/snap_sysutils.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 137 | 138 | $(IntermediateDirectory)/sys_snap_tcpsrvr.o: 139 | $(CXX) $(SourceSwitch) "src/snap7/src/sys/snap_tcpsrvr.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 140 | 141 | $(IntermediateDirectory)/sys_snap_threads.o: 142 | $(CXX) $(SourceSwitch) "src/snap7/src/sys/snap_threads.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 143 | 144 | $(IntermediateDirectory)/core_s7_client.o: 145 | $(CXX) $(SourceSwitch) "src/snap7/src/core/s7_client.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 146 | 147 | $(IntermediateDirectory)/core_s7_isotcp.o: 148 | $(CXX) $(SourceSwitch) "src/snap7/src/core/s7_isotcp.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 149 | 150 | $(IntermediateDirectory)/core_s7_partner.o: 151 | $(CXX) $(SourceSwitch) "src/snap7/src/core/s7_partner.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 152 | 153 | $(IntermediateDirectory)/core_s7_peer.o: 154 | $(CXX) $(SourceSwitch) "src/snap7/src/core/s7_peer.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 155 | 156 | $(IntermediateDirectory)/core_s7_server.o: 157 | $(CXX) $(SourceSwitch) "src/snap7/src/core/s7_server.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 158 | 159 | $(IntermediateDirectory)/core_s7_text.o: 160 | $(CXX) $(SourceSwitch) "src/snap7/src/core/s7_text.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 161 | 162 | $(IntermediateDirectory)/core_s7_micro_client.o: 163 | $(CXX) $(SourceSwitch) "src/snap7/src/core/s7_micro_client.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 164 | 165 | $(IntermediateDirectory)/lib_snap7_libmain.o: 166 | $(CXX) $(SourceSwitch) "src/snap7/src/lib/snap7_libmain.cpp" $(CXXFLAGS) -o $@ $(IncludePath) 167 | 168 | ## 169 | ## SNAPEX7 OBJECTS 170 | ## 171 | $(PREFIX)/%.o: $(BUILD)/erlcmd.o $(BUILD)/%.o 172 | @echo debug 173 | $(CC) -O3 $^ -L$(LibInstall) -I$(LibInstall) -lsnap $(ERL_LDFLAGS) $(LDFLAGS) -o $@ 174 | 175 | $(BUILD)/%.o: $(SRC_PATH)/%.c 176 | @echo debug s7: $@, $^ 177 | $(CC) -c $(ERL_CFLAGS) -I$(SNAP7_PATH)$(S7_H_PATH) -L$(LibInstall) -I$(LibInstall) $(CFLAGS) -o $@ $< 178 | 179 | ## 180 | ## Clean 181 | ## 182 | clean: 183 | $(RM) $(IntermediateDirectory)/*.o 184 | $(RM) $(OutputFile) 185 | $(RM) -f $(LibInstall)/*.o $(LibInstall)/*.so $(SRC_PATH)/*.o 186 | $(RM) -rf $(LibInstall) $(IntermediateDirectory) 187 | 188 | 189 | -------------------------------------------------------------------------------- /src/erlcmd.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Frank Hunleth 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Common Erlang->C port communications code 17 | */ 18 | 19 | #include "erlcmd.h" 20 | #include "util.h" 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #ifdef __WIN32__ 28 | // Assume that all windows platforms are little endian 29 | #define TO_BIGENDIAN16(X) _byteswap_ushort(X) 30 | #define FROM_BIGENDIAN16(X) _byteswap_ushort(X) 31 | #else 32 | // Other platforms have htons and ntohs without pulling in another library 33 | #define TO_BIGENDIAN16(X) htons(X) 34 | #define FROM_BIGENDIAN16(X) ntohs(X) 35 | #endif 36 | 37 | #ifdef __WIN32__ 38 | /* 39 | * stdin on Windows 40 | * 41 | * Caveat: I'm convinced that I don't understand stdin on Windows, so take 42 | * with a grain of salt. 43 | * 44 | * Here's what I know: 45 | * 46 | * 1. When running from a command prompt, stdin is a console. Windows 47 | * consoles collect lines at a time. See SetConsoleMode. 48 | * 2. The console is opened for synchronous use. I.e., overlapped I/O 49 | * doesn't work. 50 | * 3. Opening stdin using CONIN$ works for console use, but not 51 | * redirected use. (Seems obvious, but lots of search hits on 52 | * Google do this, so it seemed worthwhile to try.) 53 | * 5. When stdin is redirected to another program, it behaves like 54 | * a pipe. It may really be a pipe, but the docs that I've read 55 | * don't call it one. 56 | * 6. The pipe is opened for synchronous use. 57 | * 7. Calling ReOpenFile to change it to support overlapped I/O 58 | * didn't work. 59 | * 8. Despite pipes not being listed as things you can wait on with 60 | * WaitForMultipleObjects, it seems to work. Hopefully just a 61 | * documentation omission... 62 | * 63 | * Since there seems to be no way of making stdin be overlapped I/O 64 | * capable, I created a thread and a new pipe. The thread synchronously 65 | * reads from stdin and writes to the new pipe. The read end of the new 66 | * pipe supports overlapped I/O so I can use it in the main WFMO loop. 67 | * 68 | * If performance gets to be an issue, the pipe could be replaced with 69 | * a shared memory/event notification setup. Until this, the pipe version 70 | * is pretty simple albeit with a lot of data copies and process switches. 71 | */ 72 | 73 | static DWORD WINAPI pipe_copy_thread(LPVOID lpParam) 74 | { 75 | struct erlcmd *handler = (struct erlcmd *) lpParam; 76 | 77 | // NEED to get overlapped version of stdin reader 78 | HANDLE real_stdin = GetStdHandle(STD_INPUT_HANDLE); 79 | 80 | // Turn off ENABLE_LINE_INPUT (and everything else) 81 | // This has to be done on the STD_INPUT_HANDLE rather than 82 | // the one we use for events. Don't know why. 83 | SetConsoleMode(real_stdin, 0); 84 | 85 | while (handler->running) { 86 | char buffer[1024]; 87 | DWORD bytes_read; 88 | if (!ReadFile(real_stdin, buffer, sizeof(buffer), &bytes_read, NULL)) { 89 | debug("ReadFile on real_stdin failed (port closed)! %d", (int) GetLastError()); 90 | break; 91 | } 92 | 93 | if (!WriteFile(handler->stdin_write_pipe, buffer, bytes_read, NULL, NULL)) { 94 | debug("WriteFile on stdin_write_pipe failed! %d", (int) GetLastError()); 95 | break; 96 | } 97 | } 98 | 99 | handler->running = FALSE; 100 | CloseHandle(handler->stdin_write_pipe); 101 | handler->stdin_write_pipe = NULL; 102 | 103 | ExitThread(0); 104 | return 0; 105 | } 106 | 107 | static void start_async_read(struct erlcmd *handler) 108 | { 109 | ReadFile(handler->h, 110 | handler->buffer + handler->index, 111 | sizeof(handler->buffer) - handler->index, 112 | NULL, 113 | &handler->overlapped); 114 | } 115 | 116 | #endif 117 | 118 | /** 119 | * Initialize an Erlang command handler. 120 | * 121 | * @param handler the structure to initialize 122 | * @param request_handler callback for each message received 123 | * @param cookie optional data to pass back to the handler 124 | */ 125 | void erlcmd_init(struct erlcmd *handler, 126 | void (*request_handler)(const char *req, void *cookie), 127 | void *cookie) 128 | { 129 | memset(handler, 0, sizeof(*handler)); 130 | 131 | handler->request_handler = request_handler; 132 | handler->cookie = cookie; 133 | 134 | #ifdef __WIN32__ 135 | handler->running = TRUE; 136 | 137 | char pipe_name[64]; 138 | sprintf(pipe_name, 139 | "\\\\.\\Pipe\\nerves-uart.%08x", 140 | (unsigned int) GetCurrentProcessId()); 141 | 142 | handler->stdin_read_pipe = CreateNamedPipeA( 143 | pipe_name, 144 | PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, 145 | PIPE_TYPE_BYTE | PIPE_WAIT, 146 | 1, // Number of pipes 147 | 4096, // Out buffer size 148 | 4096, // In buffer size 149 | 120 * 1000, // Timeout in ms 150 | NULL 151 | ); 152 | if (!handler->stdin_read_pipe) 153 | errx(EXIT_FAILURE, "Can't create overlapped i/o stdin pipe"); 154 | 155 | handler->stdin_write_pipe = CreateFileA( 156 | pipe_name, 157 | GENERIC_WRITE, 158 | 0, // No sharing 159 | NULL, 160 | OPEN_EXISTING, 161 | FILE_ATTRIBUTE_NORMAL, 162 | NULL // Template file 163 | ); 164 | if (!handler->stdin_write_pipe) 165 | errx(EXIT_FAILURE, "Can't create write side of stdin pipe"); 166 | 167 | handler->stdin_reader_thread = CreateThread( 168 | NULL, // default security attributes 169 | 0, // use default stack size 170 | pipe_copy_thread, // thread function name 171 | handler, // argument to thread function 172 | 0, // use default creation flags 173 | NULL); 174 | if (handler->stdin_reader_thread == NULL) { 175 | errx(EXIT_FAILURE, "CreateThread failed: %d", (int) GetLastError()); 176 | return; 177 | } 178 | handler->h = handler->stdin_read_pipe; 179 | if (handler->h == INVALID_HANDLE_VALUE) 180 | errx(EXIT_FAILURE, "Can't open stdin %d", (int) GetLastError()); 181 | 182 | handler->overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); 183 | handler->overlapped.Offset = 0; 184 | handler->overlapped.OffsetHigh = 0; 185 | 186 | start_async_read(handler); 187 | #endif 188 | } 189 | 190 | /** 191 | * @brief Synchronously send a response back to Erlang 192 | * 193 | * @param response what to send back 194 | */ 195 | void erlcmd_send(char *response, size_t len) 196 | { 197 | uint16_t be_len = TO_BIGENDIAN16(len - sizeof(uint16_t)); 198 | memcpy(response, &be_len, sizeof(be_len)); 199 | 200 | #ifdef __WIN32__ 201 | BOOL rc = WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), response, len, NULL, NULL); 202 | if (!rc) 203 | errx(EXIT_FAILURE, "WriteFile to stdout failed (Erlang exit?)"); 204 | #else 205 | size_t wrote = 0; 206 | do { 207 | ssize_t amount_written = write(STDOUT_FILENO, response + wrote, len - wrote); 208 | if (amount_written < 0) { 209 | if (errno == EINTR) 210 | continue; 211 | 212 | err(EXIT_FAILURE, "write"); 213 | } 214 | wrote += amount_written; 215 | } while (wrote < len); 216 | #endif 217 | } 218 | 219 | /** 220 | * @brief Dispatch commands in the buffer 221 | * @return the number of bytes processed 222 | */ 223 | static size_t erlcmd_try_dispatch(struct erlcmd *handler) 224 | { 225 | /* Check for length field */ 226 | if (handler->index < sizeof(uint16_t)) 227 | return 0; 228 | 229 | uint16_t be_len; 230 | memcpy(&be_len, handler->buffer, sizeof(uint16_t)); 231 | size_t msglen = FROM_BIGENDIAN16(be_len); 232 | if (msglen + sizeof(uint16_t) > sizeof(handler->buffer)) 233 | errx(EXIT_FAILURE, "Message too long: %d bytes. Max is %d bytes", 234 | (int) (msglen + sizeof(uint16_t)), (int) sizeof(handler->buffer)); 235 | 236 | /* Check whether we've received the entire message */ 237 | if (msglen + sizeof(uint16_t) > handler->index) 238 | return 0; 239 | 240 | handler->request_handler(handler->buffer, handler->cookie); 241 | 242 | return msglen + sizeof(uint16_t); 243 | } 244 | 245 | /** 246 | * @brief Call to process any new requests from Erlang 247 | * 248 | * @return 1 if the program should exit gracefully 249 | */ 250 | int erlcmd_process(struct erlcmd *handler) 251 | { 252 | #ifdef __WIN32__ 253 | DWORD amount_read; 254 | BOOL rc = GetOverlappedResult(handler->h, 255 | &handler->overlapped, 256 | &amount_read, FALSE); 257 | 258 | if (!rc) { 259 | DWORD last_error = GetLastError(); 260 | 261 | // Check if this was a spurious event. 262 | if (last_error == ERROR_IO_PENDING) 263 | return 0; 264 | 265 | // Error - most likely the Erlang port connected to us was closed. 266 | // Tell the caller to exit gracefully. 267 | return 1; 268 | } 269 | 270 | ResetEvent(handler->overlapped.hEvent); 271 | #else 272 | ssize_t amount_read = read(STDIN_FILENO, handler->buffer + handler->index, sizeof(handler->buffer) - handler->index); 273 | if (amount_read < 0) { 274 | /* EINTR is ok to get, since we were interrupted by a signal. */ 275 | if (errno == EINTR) 276 | return 0; 277 | 278 | /* Everything else is unexpected. */ 279 | err(EXIT_FAILURE, "read"); 280 | } else if (amount_read == 0) { 281 | /* EOF. Erlang process was terminated. This happens after a release or if there was an error. */ 282 | return 1; 283 | } 284 | #endif 285 | handler->index += amount_read; 286 | 287 | for (;;) { 288 | size_t bytes_processed = erlcmd_try_dispatch(handler); 289 | 290 | if (bytes_processed == 0) { 291 | /* Only have part of the command to process. */ 292 | break; 293 | } else if (handler->index > bytes_processed) { 294 | /* Processed the command and there's more data. */ 295 | memmove(handler->buffer, &handler->buffer[bytes_processed], handler->index - bytes_processed); 296 | handler->index -= bytes_processed; 297 | } else { 298 | /* Processed the whole buffer. */ 299 | handler->index = 0; 300 | break; 301 | } 302 | } 303 | 304 | #ifdef __WIN32__ 305 | start_async_read(handler); 306 | #endif 307 | 308 | return 0; 309 | } 310 | 311 | #ifdef __WIN32__ 312 | HANDLE erlcmd_wfmo_event(struct erlcmd *handler) 313 | { 314 | return handler->overlapped.hEvent; 315 | } 316 | 317 | #endif 318 | -------------------------------------------------------------------------------- /src/erlcmd.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Frank Hunleth 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef ERLCMD_H 18 | #define ERLCMD_H 19 | 20 | #include 21 | 22 | #ifdef __WIN32__ 23 | #include 24 | #endif 25 | 26 | /* 27 | * Erlang request/response processing 28 | */ 29 | #define ERLCMD_BUF_SIZE 16384 // Large size is to support large UART writes 30 | struct erlcmd 31 | { 32 | char buffer[ERLCMD_BUF_SIZE]; 33 | size_t index; 34 | 35 | void (*request_handler)(const char *emsg, void *cookie); 36 | void *cookie; 37 | 38 | #ifdef __WIN32__ 39 | HANDLE h; 40 | OVERLAPPED overlapped; 41 | 42 | HANDLE stdin_reader_thread; 43 | HANDLE stdin_read_pipe; 44 | HANDLE stdin_write_pipe; 45 | BOOL running; 46 | #endif 47 | }; 48 | 49 | void erlcmd_init(struct erlcmd *handler, 50 | void (*request_handler)(const char *req, void *cookie), 51 | void *cookie); 52 | void erlcmd_send(char *response, size_t len); 53 | int erlcmd_process(struct erlcmd *handler); 54 | 55 | #ifdef __WIN32__ 56 | HANDLE erlcmd_wfmo_event(struct erlcmd *handler); 57 | #endif 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /src/s7_partner.c: -------------------------------------------------------------------------------- 1 | #include "snap7.h" 2 | #include "erlcmd.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #define MAX_READ 1023 10 | 11 | byte MyDB32[256]; // byte is a portable type of snap7.h 12 | byte DB1[20]; // byte is a portable type of snap7.h 13 | byte DB2[20]; // byte is a portable type of snap7.h 14 | byte MyAB32[256]; // byte is a portable type of snap7.h 15 | byte MyEB32[256]; // byte is a portable type of snap7.h 16 | int MyTM32[256]; // byte is a portable type of snap7.h 17 | float f = 123.45; 18 | byte* bytes = (byte*)&f; 19 | S7Object Server; 20 | S7Object Client; 21 | 22 | void ReadmultiVars(void *array) 23 | { 24 | byte *DB = ((TS7DataItem *)array)[0].pdata; 25 | printf("r = %d\n", ((TS7DataItem *)array)[0].Result); 26 | printf("0x"); 27 | //printf("%02x", ); 28 | printf("%02x", DB[0]); 29 | printf("%02x", DB[1]); 30 | printf("%02x", DB[2]); 31 | printf("%02x", DB[3]); 32 | printf("\n"); 33 | //byte *DB = ((TS7DataItem *)array)[0].pdata; 34 | printf("0x"); 35 | printf("%02x", ((byte *)((TS7DataItem *)array)[1].pdata)[0]); 36 | printf("\n"); 37 | } 38 | 39 | void print_arrays(byte *data,int size) 40 | { 41 | for(int i = 0; i < size; i++) 42 | { 43 | printf("%02x ", data[i]); 44 | } 45 | printf("\n"); 46 | } 47 | 48 | int main() 49 | { 50 | char *str; 51 | char *str1 = "tutorialspoint"; 52 | char array[] = {'H','o','l','a'}; 53 | struct erlcmd handler; 54 | Client = Cli_Create(); 55 | uint32_t param; 56 | int result = Cli_ConnectTo(Client,"192.168.0.1",0,1); 57 | printf("r = %d\n", result); 58 | 59 | // Read/Write Area test 60 | printf("Read/write area test-----------------------------\n"); 61 | Cli_ReadArea(Client, S7AreaDB, 1, 2, 4, S7WLByte, &MyDB32); 62 | 63 | printf("0x"); 64 | printf("%02x", MyDB32[0]); 65 | printf("%02x", MyDB32[1]); 66 | printf("%02x", MyDB32[2]); 67 | printf("%02x\n", MyDB32[3]); 68 | MyDB32[1] = 0xcb; 69 | MyDB32[3] = 0x00; 70 | 71 | result = Cli_WriteArea(Client, S7AreaDB, 1, 2, 4, S7WLByte, &MyDB32); 72 | printf("r = %d\n", result); 73 | 74 | MyDB32[0] = 0x00; 75 | MyDB32[1] = 0x00; 76 | MyDB32[2] = 0x00; 77 | MyDB32[3] = 0x00; 78 | Cli_ReadArea(Client, S7AreaDB, 1, 2, 4, S7WLByte, &MyDB32); 79 | printf("0x"); 80 | printf("%02x", MyDB32[0]); 81 | printf("%02x", MyDB32[1]); 82 | printf("%02x", MyDB32[2]); 83 | printf("%02x\n", MyDB32[3]); 84 | 85 | MyDB32[1] = 0xca; 86 | result = Cli_WriteArea(Client, S7AreaDB, 1, 2, 1, S7WLWord, &MyDB32); 87 | printf("r = %d\n", result); 88 | 89 | printf("Read/writeVars test-----------------------------\n"); 90 | 91 | printf("S7WLWord test-----------------------------\n"); 92 | Cli_ReadArea(Client, S7AreaDB, 1, 2, 2, S7WLWord, &MyDB32); 93 | printf("0x"); 94 | printf("%02x", MyDB32[0]); 95 | printf("%02x", MyDB32[1]); 96 | printf("%02x", MyDB32[2]); 97 | printf("%02x\n", MyDB32[3]); 98 | 99 | printf("Read/writeVars test-----------------------------\n"); 100 | Cli_ABRead(Client, 0, 1, &MyAB32); 101 | printf("0x"); 102 | printf("%02x", MyAB32[0]); 103 | printf("\n"); 104 | 105 | MyAB32[0] = 0x01; 106 | result = Cli_ABWrite(Client, 0, 1, &MyAB32); 107 | printf("r = %d\n", result); 108 | 109 | Cli_ABRead(Client, 0, 1, &MyAB32); 110 | printf("0x"); 111 | printf("%02x", MyAB32[0]); 112 | printf("\n"); 113 | sleep(1); 114 | MyAB32[0] = 0x00; 115 | result = Cli_ABWrite(Client, 0, 1, &MyAB32); 116 | printf("r = %d\n", result); 117 | 118 | Cli_ABRead(Client, 0, 1, &MyAB32); 119 | printf("0x"); 120 | printf("%02x", MyAB32[0]); 121 | printf("\n"); 122 | 123 | // Read/Write Input test 124 | Cli_EBRead(Client, 0, 1, &MyEB32); 125 | printf("0x"); 126 | printf("%02x", MyEB32[0]); 127 | printf("\n"); 128 | 129 | printf("TM_Read test-----------------------------\n"); 130 | result = Cli_TMRead(Client, 0, 1, &MyTM32); 131 | printf("r = %d\n", result); 132 | printf("%02x", MyTM32[0]); 133 | printf("%02x", MyTM32[1]); 134 | printf("\n"); 135 | 136 | printf("CT_Read test-----------------------------\n"); 137 | result = Cli_CTRead(Client, 0, 1, &MyTM32); 138 | printf("r = %d\n", result); 139 | printf("%02x", MyTM32[0]); 140 | printf("%02x", MyTM32[1]); 141 | printf("\n"); 142 | 143 | //ReadmultiVars Test 144 | printf("ReadMultiVars test-----------------------------\n"); 145 | TS7DataItem Items[2]; 146 | 147 | Items[0].Area = S7AreaDB; 148 | Items[0].WordLen = S7WLByte; 149 | Items[0].DBNumber = 1; 150 | Items[0].Start= 2; 151 | Items[0].Amount= 4; 152 | Items[0].pdata = &DB1; 153 | 154 | Items[1].Area = S7AreaPE; 155 | Items[1].WordLen = S7WLByte; 156 | Items[1].DBNumber = 0; 157 | Items[1].Start= 0; 158 | Items[1].Amount= 1; 159 | Items[1].pdata = &DB2; 160 | 161 | result = Cli_ReadMultiVars(Client, &Items[0], 2); 162 | ReadmultiVars(&Items); 163 | 164 | printf("readszl test-----------------------------\n"); 165 | int ID = 0x0111; 166 | int Index = 0x0006; 167 | TS7SZL data; 168 | int size = sizeof(data); 169 | result = Cli_ReadSZL(Client, ID, Index, &data, &size); 170 | printf("r = %d\n", result); 171 | printf("size = %d\n", size); 172 | printf("LENTHDR = %d\n", data.Header.LENTHDR); 173 | printf("N_DR = %d\n", data.Header.N_DR); 174 | int lim = data.Header.LENTHDR*data.Header.N_DR; 175 | for(int index =0; index 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #define MAX_READ 1023 10 | 11 | int MyDB32[256]; // byte is a portable type of snap7.h 12 | ei_x_buff x; 13 | 14 | S7Object Client; 15 | 16 | int main() 17 | { 18 | struct erlcmd handler; 19 | Client = Cli_Create(); 20 | 21 | Cli_ConnectTo(Client,"192.168.0.1",0,1); 22 | 23 | Cli_DBRead(Client, 1, 2, 4, MyDB32); 24 | 25 | printf("0x"); 26 | printf("%04x", MyDB32[0]); 27 | printf("\n"); 28 | 29 | Cli_Destroy(&Client); 30 | } -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Frank Hunleth 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "util.h" 18 | #ifdef __APPLE__ 19 | #include 20 | #include 21 | #else 22 | #include 23 | #endif 24 | 25 | #ifdef DEBUG 26 | FILE *log_location; 27 | #endif 28 | 29 | /** 30 | * @return a monotonic timestamp in milliseconds 31 | */ 32 | uint64_t current_time() 33 | { 34 | #ifdef __APPLE__ 35 | clock_serv_t cclock; 36 | mach_timespec_t mts; 37 | 38 | host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock); 39 | clock_get_time(cclock, &mts); 40 | mach_port_deallocate(mach_task_self(), cclock); 41 | 42 | return ((uint64_t) mts.tv_sec) * 1000 + mts.tv_nsec / 1000000; 43 | #else 44 | // Linux and Windows support clock_gettime() 45 | struct timespec tp; 46 | int rc = clock_gettime(CLOCK_MONOTONIC, &tp); 47 | if (rc < 0) 48 | errx(EXIT_FAILURE, "clock_gettime failed?"); 49 | 50 | return ((uint64_t) tp.tv_sec) * 1000 + tp.tv_nsec / 1000000; 51 | #endif 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Frank Hunleth 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef UTIL_H 18 | #define UTIL_H 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | //#define DEBUG 25 | 26 | #ifdef DEBUG 27 | FILE *log_location; 28 | #define LOG_LOCATION log_location 29 | #define debug(...) do { fprintf(log_location, __VA_ARGS__); fprintf(log_location, "\r\n"); fflush(log_location); } while(0) 30 | #else 31 | #define LOG_LOCATION stderr 32 | #define debug(...) 33 | #endif 34 | 35 | #ifndef __WIN32__ 36 | #include 37 | #else 38 | // If err.h doesn't exist, define substitutes. 39 | #define err(STATUS, MSG, ...) do { fprintf(LOG_LOCATION, "nerves_uart: " MSG "\n", ## __VA_ARGS__); fflush(LOG_LOCATION); exit(STATUS); } while (0) 40 | #define errx(STATUS, MSG, ...) do { fprintf(LOG_LOCATION, "nerves_uart: " MSG "\n", ## __VA_ARGS__); fflush(LOG_LOCATION); exit(STATUS); } while (0) 41 | #define warn(MSG, ...) do { fprintf(LOG_LOCATION, "nerves_uart: " MSG "\n", ## __VA_ARGS__); fflush(LOG_LOCATION); } while (0) 42 | #define warnx(MSG, ...) do { fprintf(LOG_LOCATION, "nerves_uart: " MSG "\n", ## __VA_ARGS__); fflush(LOG_LOCATION); } while (0) 43 | #endif 44 | 45 | #define ONE_YEAR_MILLIS (1000ULL * 60 * 60 * 24 * 365) 46 | uint64_t current_time(); 47 | 48 | #endif // UTIL_H 49 | -------------------------------------------------------------------------------- /test/c_backend_tests/c_driver_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CDriverTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 7 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 8 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 9 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 10 | 11 | port = 12 | Port.open({:spawn_executable, executable}, [ 13 | {:args, []}, 14 | {:packet, 2}, 15 | :use_stdio, 16 | :binary, 17 | :exit_status 18 | ]) 19 | 20 | %{port: port} 21 | end 22 | 23 | test "Erlang - C driver test", state do 24 | msg = {:test, "x"} 25 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 26 | 27 | c_response = 28 | receive do 29 | {_, {:data, <>}} -> 30 | :erlang.binary_to_term(response) 31 | 32 | x -> 33 | IO.inspect(x) 34 | :error 35 | after 36 | 1000 -> 37 | # Not sure how this can be recovered 38 | exit(:port_timed_out) 39 | end 40 | 41 | assert c_response == :ok 42 | end 43 | 44 | # test "checking ei functions", state do 45 | # data1 = %{ #DB1 46 | # area: 132, 47 | # wordlen: 2, 48 | # dbnumber: 1, 49 | # start: 2, 50 | # amount: 4 51 | # } 52 | 53 | # data2 = %{ #PE0 54 | # area: 129, 55 | # wordlen: 2, 56 | # dbnumber: 1, 57 | # start: 0, 58 | # amount: 1 59 | # } 60 | # msg = {:test, {2, [data1, data2]}} 61 | # send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 62 | 63 | # c_response = 64 | # receive do 65 | # {_, {:data, <>}} -> 66 | # :erlang.binary_to_term(response) 67 | # x -> 68 | # IO.inspect(x) 69 | # :error 70 | # after 71 | # 1000 -> 72 | # # Not sure how this can be recovered 73 | # exit(:port_timed_out) 74 | # end 75 | 76 | # assert c_response == {:ok, 2} 77 | 78 | # c_response = 79 | # receive do 80 | # {_, {:data, <>}} -> 81 | # :erlang.binary_to_term(response) 82 | # x -> 83 | # IO.inspect(x) 84 | # :error 85 | # after 86 | # 1000 -> 87 | # # Not sure how this can be recovered 88 | # exit(:port_timed_out) 89 | # end 90 | # IO.puts("#{inspect(c_response)}") 91 | 92 | # c_response = 93 | # receive do 94 | # {_, {:data, <>}} -> 95 | # :erlang.binary_to_term(response) 96 | # x -> 97 | # IO.inspect(x) 98 | # :error 99 | # after 100 | # 1000 -> 101 | # # Not sure how this can be recovered 102 | # exit(:port_timed_out) 103 | # end 104 | # IO.puts("#{inspect(c_response)}") 105 | 106 | # c_response = 107 | # receive do 108 | # {_, {:data, <>}} -> 109 | # :erlang.binary_to_term(response) 110 | # x -> 111 | # IO.inspect(x) 112 | # :error 113 | # after 114 | # 1000 -> 115 | # # Not sure how this can be recovered 116 | # exit(:port_timed_out) 117 | # end 118 | # IO.puts("#{inspect(c_response)}") 119 | 120 | # c_response = 121 | # receive do 122 | # {_, {:data, <>}} -> 123 | # :erlang.binary_to_term(response) 124 | # x -> 125 | # IO.inspect(x) 126 | # :error 127 | # after 128 | # 1000 -> 129 | # # Not sure how this can be recovered 130 | # exit(:port_timed_out) 131 | # end 132 | # IO.puts("#{inspect(c_response)}") 133 | 134 | # c_response = 135 | # receive do 136 | # {_, {:data, <>}} -> 137 | # :erlang.binary_to_term(response) 138 | # x -> 139 | # IO.inspect(x) 140 | # :error 141 | # after 142 | # 1000 -> 143 | # # Not sure how this can be recovered 144 | # exit(:port_timed_out) 145 | # end 146 | # IO.puts("#{inspect(c_response)}") 147 | 148 | # c_response = 149 | # receive do 150 | # {_, {:data, <>}} -> 151 | # :erlang.binary_to_term(response) 152 | # x -> 153 | # IO.inspect(x) 154 | # :error 155 | # after 156 | # 1000 -> 157 | # # Not sure how this can be recovered 158 | # exit(:port_timed_out) 159 | # end 160 | # IO.puts("#{inspect(c_response)}") 161 | 162 | # c_response = 163 | # receive do 164 | # {_, {:data, <>}} -> 165 | # :erlang.binary_to_term(response) 166 | # x -> 167 | # IO.inspect(x) 168 | # :error 169 | # after 170 | # 1000 -> 171 | # # Not sure how this can be recovered 172 | # exit(:port_timed_out) 173 | # end 174 | # IO.puts("#{inspect(c_response)}") 175 | 176 | # c_response = 177 | # receive do 178 | # {_, {:data, <>}} -> 179 | # :erlang.binary_to_term(response) 180 | # x -> 181 | # IO.inspect(x) 182 | # :error 183 | # after 184 | # 1000 -> 185 | # # Not sure how this can be recovered 186 | # exit(:port_timed_out) 187 | # end 188 | # IO.puts("#{inspect(c_response)}") 189 | 190 | # c_response = 191 | # receive do 192 | # {_, {:data, <>}} -> 193 | # :erlang.binary_to_term(response) 194 | # x -> 195 | # IO.inspect(x) 196 | # :error 197 | # after 198 | # 1000 -> 199 | # # Not sure how this can be recovered 200 | # exit(:port_timed_out) 201 | # end 202 | # IO.puts("#{inspect(c_response)}") 203 | 204 | # c_response = 205 | # receive do 206 | # {_, {:data, <>}} -> 207 | # :erlang.binary_to_term(response) 208 | # x -> 209 | # IO.inspect(x) 210 | # :error 211 | # after 212 | # 1000 -> 213 | # # Not sure how this can be recovered 214 | # exit(:port_timed_out) 215 | # end 216 | # IO.puts("#{inspect(c_response)}") 217 | 218 | # end 219 | end 220 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_admin_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliAdminFuncTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | # checar como cambiar esto para que use :code.priv_dir 7 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 8 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 9 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 10 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 11 | 12 | port = 13 | Port.open({:spawn_executable, executable}, [ 14 | {:args, []}, 15 | {:packet, 2}, 16 | :use_stdio, 17 | :binary, 18 | :exit_status 19 | ]) 20 | 21 | %{port: port} 22 | end 23 | 24 | test "set_connection_type test", state do 25 | msg = {:set_connection_type, 1} 26 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 27 | 28 | c_response = 29 | receive do 30 | {_, {:data, <>}} -> 31 | :erlang.binary_to_term(response) 32 | 33 | x -> 34 | IO.inspect(x) 35 | :error 36 | after 37 | 1000 -> 38 | # Not sure how this can be recovered 39 | exit(:port_timed_out) 40 | end 41 | 42 | assert c_response == :ok 43 | end 44 | 45 | test "handle_set_connection_params test", state do 46 | msg = {:set_connection_params, {"192.168.1.100", 1, 2}} 47 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 48 | 49 | c_response = 50 | receive do 51 | {_, {:data, <>}} -> 52 | :erlang.binary_to_term(response) 53 | 54 | x -> 55 | IO.inspect(x) 56 | :error 57 | after 58 | 1000 -> 59 | # Not sure how this can be recovered 60 | exit(:port_timed_out) 61 | end 62 | 63 | assert c_response == :ok 64 | end 65 | 66 | test "handle_connect_to test", state do 67 | # 68 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 69 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 70 | 71 | c_response = 72 | receive do 73 | {_, {:data, <>}} -> 74 | :erlang.binary_to_term(response) 75 | 76 | x -> 77 | IO.inspect(x) 78 | :error 79 | after 80 | 5000 -> 81 | # Not sure how this can be recovered 82 | exit(:port_timed_out) 83 | end 84 | 85 | d_response = 86 | case c_response do 87 | {:error, x} -> 88 | IO.puts("connected_to response is #{inspect(x)}") 89 | :error 90 | 91 | :ok -> 92 | :ok 93 | end 94 | 95 | # no plc connected or connected 96 | assert d_response == :error || d_response == :ok 97 | end 98 | 99 | test "handle_connect test", state do 100 | msg = {:connect, nil} 101 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 102 | 103 | c_response = 104 | receive do 105 | {_, {:data, <>}} -> 106 | :erlang.binary_to_term(response) 107 | 108 | x -> 109 | IO.inspect(x) 110 | :error 111 | after 112 | 5000 -> 113 | # Not sure how this can be recovered 114 | exit(:port_timed_out) 115 | end 116 | 117 | d_response = 118 | case c_response do 119 | {:error, _x} -> 120 | :error 121 | 122 | :ok -> 123 | :ok 124 | end 125 | 126 | # no plc connected or connected 127 | assert d_response == :error || d_response == :ok 128 | end 129 | 130 | test "handler_disconnect test", state do 131 | msg = {:disconnect, 1} 132 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 133 | 134 | c_response = 135 | receive do 136 | {_, {:data, <>}} -> 137 | :erlang.binary_to_term(response) 138 | 139 | x -> 140 | IO.inspect(x) 141 | :error 142 | after 143 | 1000 -> 144 | # Not sure how this can be recovered 145 | exit(:port_timed_out) 146 | end 147 | 148 | assert c_response == :ok 149 | end 150 | 151 | test "handler_get_params/handler_set_params test", state do 152 | msg = {:set_params, {2, 103}} 153 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 154 | 155 | receive do 156 | {_, {:data, <>}} -> 157 | :erlang.binary_to_term(response) 158 | 159 | x -> 160 | IO.inspect(x) 161 | :error 162 | after 163 | 5000 -> 164 | exit(:port_timed_out) 165 | end 166 | 167 | msg = {:get_params, 2} 168 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 169 | 170 | c_response = 171 | receive do 172 | {_, {:data, <>}} -> 173 | :erlang.binary_to_term(response) 174 | 175 | x -> 176 | IO.inspect(x) 177 | :error 178 | after 179 | 1000 -> 180 | exit(:port_timed_out) 181 | end 182 | 183 | assert c_response == {:ok, 103} 184 | 185 | msg = {:set_params, {3, 800}} 186 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 187 | 188 | receive do 189 | {_, {:data, <>}} -> 190 | :erlang.binary_to_term(response) 191 | 192 | x -> 193 | IO.inspect(x) 194 | :error 195 | after 196 | 5000 -> 197 | exit(:port_timed_out) 198 | end 199 | 200 | msg = {:get_params, 3} 201 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 202 | 203 | c_response = 204 | receive do 205 | {_, {:data, <>}} -> 206 | :erlang.binary_to_term(response) 207 | 208 | x -> 209 | IO.inspect(x) 210 | :error 211 | after 212 | 1000 -> 213 | # Not sure how this can be recovered 214 | exit(:port_timed_out) 215 | end 216 | 217 | assert c_response == {:ok, 800} 218 | 219 | msg = {:set_params, {4, 20}} 220 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 221 | 222 | receive do 223 | {_, {:data, <>}} -> 224 | :erlang.binary_to_term(response) 225 | 226 | x -> 227 | IO.inspect(x) 228 | :error 229 | after 230 | 5000 -> 231 | exit(:port_timed_out) 232 | end 233 | 234 | msg = {:get_params, 4} 235 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 236 | 237 | c_response = 238 | receive do 239 | {_, {:data, <>}} -> 240 | :erlang.binary_to_term(response) 241 | 242 | x -> 243 | IO.inspect(x) 244 | :error 245 | after 246 | 1000 -> 247 | # Not sure how this can be recovered 248 | exit(:port_timed_out) 249 | end 250 | 251 | assert c_response == {:ok, 20} 252 | 253 | msg = {:set_params, {5, 3500}} 254 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 255 | 256 | receive do 257 | {_, {:data, <>}} -> 258 | :erlang.binary_to_term(response) 259 | 260 | x -> 261 | IO.inspect(x) 262 | :error 263 | after 264 | 5000 -> 265 | exit(:port_timed_out) 266 | end 267 | 268 | msg = {:get_params, 5} 269 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 270 | 271 | c_response = 272 | receive do 273 | {_, {:data, <>}} -> 274 | :erlang.binary_to_term(response) 275 | 276 | x -> 277 | IO.inspect(x) 278 | :error 279 | after 280 | 1000 -> 281 | # Not sure how this can be recovered 282 | exit(:port_timed_out) 283 | end 284 | 285 | assert c_response == {:ok, 3500} 286 | 287 | msg = {:set_params, {7, 512}} 288 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 289 | 290 | receive do 291 | {_, {:data, <>}} -> 292 | :erlang.binary_to_term(response) 293 | 294 | x -> 295 | IO.inspect(x) 296 | :error 297 | after 298 | 5000 -> 299 | exit(:port_timed_out) 300 | end 301 | 302 | msg = {:get_params, 7} 303 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 304 | 305 | c_response = 306 | receive do 307 | {_, {:data, <>}} -> 308 | :erlang.binary_to_term(response) 309 | 310 | x -> 311 | IO.inspect(x) 312 | :error 313 | after 314 | 1000 -> 315 | # Not sure how this can be recovered 316 | exit(:port_timed_out) 317 | end 318 | 319 | assert c_response == {:ok, 512} 320 | 321 | msg = {:set_params, {8, 1}} 322 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 323 | 324 | receive do 325 | {_, {:data, <>}} -> 326 | :erlang.binary_to_term(response) 327 | 328 | x -> 329 | IO.inspect(x) 330 | :error 331 | after 332 | 5000 -> 333 | exit(:port_timed_out) 334 | end 335 | 336 | msg = {:get_params, 8} 337 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 338 | 339 | c_response = 340 | receive do 341 | {_, {:data, <>}} -> 342 | :erlang.binary_to_term(response) 343 | 344 | x -> 345 | IO.inspect(x) 346 | :error 347 | after 348 | 1000 -> 349 | # Not sure how this can be recovered 350 | exit(:port_timed_out) 351 | end 352 | 353 | assert c_response == {:ok, 1} 354 | 355 | msg = {:set_params, {9, 127}} 356 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 357 | 358 | receive do 359 | {_, {:data, <>}} -> 360 | :erlang.binary_to_term(response) 361 | 362 | x -> 363 | IO.inspect(x) 364 | :error 365 | after 366 | 5000 -> 367 | exit(:port_timed_out) 368 | end 369 | 370 | msg = {:get_params, 9} 371 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 372 | 373 | c_response = 374 | receive do 375 | {_, {:data, <>}} -> 376 | :erlang.binary_to_term(response) 377 | 378 | x -> 379 | IO.inspect(x) 380 | :error 381 | after 382 | 1000 -> 383 | # Not sure how this can be recovered 384 | exit(:port_timed_out) 385 | end 386 | 387 | assert c_response == {:ok, 127} 388 | 389 | msg = {:set_params, {10, 500}} 390 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 391 | 392 | receive do 393 | {_, {:data, <>}} -> 394 | :erlang.binary_to_term(response) 395 | 396 | x -> 397 | IO.inspect(x) 398 | :error 399 | after 400 | 5000 -> 401 | exit(:port_timed_out) 402 | end 403 | 404 | msg = {:get_params, 10} 405 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 406 | 407 | c_response = 408 | receive do 409 | {_, {:data, <>}} -> 410 | :erlang.binary_to_term(response) 411 | 412 | x -> 413 | IO.inspect(x) 414 | :error 415 | after 416 | 1000 -> 417 | # Not sure how this can be recovered 418 | exit(:port_timed_out) 419 | end 420 | 421 | assert c_response == {:ok, 500} 422 | end 423 | end 424 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_block_oriented_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliBlockOrientedTest do 2 | use ExUnit.Case, async: false 3 | doctest Snapex7 4 | 5 | # We don't have the way to test this function 6 | # (we've a plc s7-1200 and snap7 server doesn't support these functions) 7 | # These tests only help us to track the input variables for c code. 8 | 9 | setup do 10 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 11 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 12 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 13 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 14 | 15 | port = 16 | Port.open({:spawn_executable, executable}, [ 17 | {:args, []}, 18 | {:packet, 2}, 19 | :use_stdio, 20 | :binary, 21 | :exit_status 22 | ]) 23 | 24 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 25 | send(port, {self(), {:command, :erlang.term_to_binary(msg)}}) 26 | 27 | status = 28 | receive do 29 | {_, {:data, <>}} -> 30 | :erlang.binary_to_term(response) 31 | after 32 | 10000 -> 33 | :error 34 | end 35 | 36 | %{port: port, status: status} 37 | end 38 | 39 | test "handle_full_update", state do 40 | case state.status do 41 | :ok -> 42 | # {Blocktype, BlockNum, size} 43 | msg = {:full_upload, {0x38, 0x41, 0x04}} 44 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 45 | 46 | c_response = 47 | receive do 48 | {_, {:data, <>}} -> 49 | :erlang.binary_to_term(response) 50 | 51 | x -> 52 | IO.inspect(x) 53 | :error 54 | after 55 | 1000 -> 56 | # Not sure how this can be recovered 57 | exit(:port_timed_out) 58 | end 59 | 60 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 61 | 62 | _ -> 63 | IO.puts("(#{__MODULE__}) Not connected") 64 | end 65 | end 66 | 67 | test "handle_update", state do 68 | case state.status do 69 | :ok -> 70 | # {Blocktype, BlockNum, size} 71 | msg = {:upload, {0x38, 0x41, 0x04}} 72 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 73 | 74 | c_response = 75 | receive do 76 | {_, {:data, <>}} -> 77 | :erlang.binary_to_term(response) 78 | 79 | x -> 80 | IO.inspect(x) 81 | :error 82 | after 83 | 1000 -> 84 | # Not sure how this can be recovered 85 | exit(:port_timed_out) 86 | end 87 | 88 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 89 | 90 | _ -> 91 | IO.puts("(#{__MODULE__}) Not connected") 92 | end 93 | end 94 | 95 | test "handle_upload", state do 96 | case state.status do 97 | :ok -> 98 | # {Blocktype, BlockNum, size} 99 | msg = {:upload, {0x38, 0x41, 0x04}} 100 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 101 | 102 | c_response = 103 | receive do 104 | {_, {:data, <>}} -> 105 | :erlang.binary_to_term(response) 106 | 107 | x -> 108 | IO.inspect(x) 109 | :error 110 | after 111 | 1000 -> 112 | # Not sure how this can be recovered 113 | exit(:port_timed_out) 114 | end 115 | 116 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 117 | 118 | _ -> 119 | IO.puts("(#{__MODULE__}) Not connected") 120 | end 121 | end 122 | 123 | test "handle_download", state do 124 | case state.status do 125 | :ok -> 126 | # {Blocknum, size, data (bitstring)} 127 | msg = {:download, {0x38, 0x03, <<0x02, 0x34, 0x35>>}} 128 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 129 | 130 | c_response = 131 | receive do 132 | {_, {:data, <>}} -> 133 | :erlang.binary_to_term(response) 134 | 135 | x -> 136 | IO.inspect(x) 137 | :error 138 | after 139 | 1000 -> 140 | # Not sure how this can be recovered 141 | exit(:port_timed_out) 142 | end 143 | 144 | assert c_response == {:error, %{eiso: nil, es7: :errCliInvalidBlockSize, etcp: nil}} 145 | 146 | _ -> 147 | IO.puts("(#{__MODULE__}) Not connected") 148 | end 149 | end 150 | 151 | test "handle_delete", state do 152 | case state.status do 153 | :ok -> 154 | # {Blocktype, blocknumber} 155 | msg = {:delete, {0x38, 0x03}} 156 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 157 | 158 | c_response = 159 | receive do 160 | {_, {:data, <>}} -> 161 | :erlang.binary_to_term(response) 162 | 163 | x -> 164 | IO.inspect(x) 165 | :error 166 | after 167 | 1000 -> 168 | # Not sure how this can be recovered 169 | exit(:port_timed_out) 170 | end 171 | 172 | assert c_response == {:error, %{eiso: nil, es7: :errCliDeleteRefused, etcp: nil}} 173 | 174 | _ -> 175 | IO.puts("(#{__MODULE__}) Not connected") 176 | end 177 | end 178 | 179 | test "handle_db_get", state do 180 | case state.status do 181 | :ok -> 182 | # {Blocktype, blocknumber} 183 | msg = {:db_get, {0x38, 0x03}} 184 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 185 | 186 | c_response = 187 | receive do 188 | {_, {:data, <>}} -> 189 | :erlang.binary_to_term(response) 190 | 191 | x -> 192 | IO.inspect(x) 193 | :error 194 | after 195 | 1000 -> 196 | # Not sure how this can be recovered 197 | exit(:port_timed_out) 198 | end 199 | 200 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 201 | 202 | _ -> 203 | IO.puts("(#{__MODULE__}) Not connected") 204 | end 205 | end 206 | 207 | test "handle_db_fill", state do 208 | case state.status do 209 | :ok -> 210 | # {DBNumber, fillchar} 211 | msg = {:db_fill, {0x38, 0x03}} 212 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 213 | 214 | c_response = 215 | receive do 216 | {_, {:data, <>}} -> 217 | :erlang.binary_to_term(response) 218 | 219 | x -> 220 | IO.inspect(x) 221 | :error 222 | after 223 | 1000 -> 224 | # Not sure how this can be recovered 225 | exit(:port_timed_out) 226 | end 227 | 228 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 229 | 230 | _ -> 231 | IO.puts("(#{__MODULE__}) Not connected") 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_data_io_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliDataIoTest do 2 | use ExUnit.Case, async: false 3 | doctest Snapex7 4 | 5 | # We need to implement S7 Server behavior in order to make a proper tests. 6 | # these tests are done connected to a real PLC 7 | 8 | setup do 9 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 10 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 11 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 12 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 13 | 14 | port = 15 | Port.open({:spawn_executable, executable}, [ 16 | {:args, []}, 17 | {:packet, 2}, 18 | :use_stdio, 19 | :binary, 20 | :exit_status 21 | ]) 22 | 23 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 24 | send(port, {self(), {:command, :erlang.term_to_binary(msg)}}) 25 | 26 | status = 27 | receive do 28 | {_, {:data, <>}} -> 29 | :erlang.binary_to_term(response) 30 | after 31 | 10000 -> 32 | :error 33 | end 34 | 35 | %{port: port, status: status} 36 | end 37 | 38 | test "handler_write_area/read_area (DB) test", state do 39 | case state.status do 40 | :ok -> 41 | msg = {:write_area, {0x84, 1, 2, 4, 2, <<0x42, 0xCB, 0x00, 0x00>>}} 42 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 43 | 44 | c_response = 45 | receive do 46 | {_, {:data, <>}} -> 47 | :erlang.binary_to_term(response) 48 | 49 | x -> 50 | IO.inspect(x) 51 | :error 52 | after 53 | 1000 -> 54 | # Not sure how this can be recovered 55 | exit(:port_timed_out) 56 | end 57 | 58 | assert c_response == :ok 59 | 60 | # {Area, db_number, start, amount, word_len} 61 | msg = {:read_area, {0x84, 1, 2, 4, 2}} 62 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 63 | 64 | c_response = 65 | receive do 66 | {_, {:data, <>}} -> 67 | :erlang.binary_to_term(response) 68 | 69 | x -> 70 | IO.inspect(x) 71 | :error 72 | after 73 | 1000 -> 74 | # Not sure how this can be recovered 75 | exit(:port_timed_out) 76 | end 77 | 78 | assert c_response == {:ok, <<0x42, 0xCB, 0x00, 0x00>>} 79 | 80 | msg = {:write_area, {0x84, 1, 2, 4, 2, <<0x42, 0xCA, 0x00, 0x00>>}} 81 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 82 | 83 | c_response = 84 | receive do 85 | {_, {:data, <>}} -> 86 | :erlang.binary_to_term(response) 87 | 88 | x -> 89 | IO.inspect(x) 90 | :error 91 | after 92 | 1000 -> 93 | # Not sure how this can be recovered 94 | exit(:port_timed_out) 95 | end 96 | 97 | assert c_response == :ok 98 | 99 | msg = {:read_area, {0x84, 1, 2, 4, 2}} 100 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 101 | 102 | c_response = 103 | receive do 104 | {_, {:data, <>}} -> 105 | :erlang.binary_to_term(response) 106 | 107 | x -> 108 | IO.inspect(x) 109 | :error 110 | after 111 | 1000 -> 112 | # Not sure how this can be recovered 113 | exit(:port_timed_out) 114 | end 115 | 116 | assert c_response == {:ok, <<0x42, 0xCA, 0x00, 0x00>>} 117 | 118 | _ -> 119 | IO.puts("(#{__MODULE__}) Not connected") 120 | end 121 | end 122 | 123 | test "handler_db_write/db_read test", state do 124 | case state.status do 125 | :ok -> 126 | msg = {:db_write, {1, 2, 4, <<0x42, 0xCB, 0x00, 0x00>>}} 127 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 128 | 129 | c_response = 130 | receive do 131 | {_, {:data, <>}} -> 132 | :erlang.binary_to_term(response) 133 | 134 | x -> 135 | IO.inspect(x) 136 | :error 137 | after 138 | 1000 -> 139 | # Not sure how this can be recovered 140 | exit(:port_timed_out) 141 | end 142 | 143 | assert c_response == :ok 144 | 145 | msg = {:db_read, {1, 2, 4}} 146 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 147 | 148 | c_response = 149 | receive do 150 | {_, {:data, <>}} -> 151 | :erlang.binary_to_term(response) 152 | 153 | x -> 154 | IO.inspect(x) 155 | :error 156 | after 157 | 1000 -> 158 | # Not sure how this can be recovered 159 | exit(:port_timed_out) 160 | end 161 | 162 | assert c_response == {:ok, <<0x42, 0xCB, 0x00, 0x00>>} 163 | 164 | msg = {:db_write, {1, 2, 4, <<0x42, 0xCA, 0x00, 0x00>>}} 165 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 166 | 167 | c_response = 168 | receive do 169 | {_, {:data, <>}} -> 170 | :erlang.binary_to_term(response) 171 | 172 | x -> 173 | IO.inspect(x) 174 | :error 175 | after 176 | 1000 -> 177 | # Not sure how this can be recovered 178 | exit(:port_timed_out) 179 | end 180 | 181 | assert c_response == :ok 182 | 183 | msg = {:db_read, {1, 2, 4}} 184 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 185 | 186 | c_response = 187 | receive do 188 | {_, {:data, <>}} -> 189 | :erlang.binary_to_term(response) 190 | 191 | x -> 192 | IO.inspect(x) 193 | :error 194 | after 195 | 1000 -> 196 | # Not sure how this can be recovered 197 | exit(:port_timed_out) 198 | end 199 | 200 | assert c_response == {:ok, <<0x42, 0xCA, 0x00, 0x00>>} 201 | 202 | _ -> 203 | IO.puts("(#{__MODULE__}) Not connected") 204 | end 205 | end 206 | 207 | test "handler_ab_write/ab_read test", state do 208 | case state.status do 209 | :ok -> 210 | msg = {:ab_write, {0, 1, <<0x0F>>}} 211 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 212 | 213 | c_response = 214 | receive do 215 | {_, {:data, <>}} -> 216 | :erlang.binary_to_term(response) 217 | 218 | x -> 219 | IO.inspect(x) 220 | :error 221 | after 222 | 1000 -> 223 | # Not sure how this can be recovered 224 | exit(:port_timed_out) 225 | end 226 | 227 | assert c_response == :ok 228 | 229 | msg = {:ab_read, {0, 1}} 230 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 231 | 232 | c_response = 233 | receive do 234 | {_, {:data, <>}} -> 235 | :erlang.binary_to_term(response) 236 | 237 | x -> 238 | IO.inspect(x) 239 | :error 240 | after 241 | 1000 -> 242 | # Not sure how this can be recovered 243 | exit(:port_timed_out) 244 | end 245 | 246 | assert c_response == {:ok, <<0x0F>>} 247 | Process.sleep(500) 248 | msg = {:ab_write, {0, 1, <<0x00>>}} 249 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 250 | 251 | c_response = 252 | receive do 253 | {_, {:data, <>}} -> 254 | :erlang.binary_to_term(response) 255 | 256 | x -> 257 | IO.inspect(x) 258 | :error 259 | after 260 | 1000 -> 261 | # Not sure how this can be recovered 262 | exit(:port_timed_out) 263 | end 264 | 265 | assert c_response == :ok 266 | 267 | msg = {:ab_read, {0, 1}} 268 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 269 | 270 | c_response = 271 | receive do 272 | {_, {:data, <>}} -> 273 | :erlang.binary_to_term(response) 274 | 275 | x -> 276 | IO.inspect(x) 277 | :error 278 | after 279 | 1000 -> 280 | # Not sure how this can be recovered 281 | exit(:port_timed_out) 282 | end 283 | 284 | assert c_response == {:ok, <<0x00>>} 285 | 286 | _ -> 287 | IO.puts("(#{__MODULE__}) Not connected") 288 | end 289 | end 290 | 291 | # eb_write doesn't make sense... 292 | test "handler_eb_read test", state do 293 | case state.status do 294 | :ok -> 295 | msg = {:eb_read, {0, 1}} 296 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 297 | 298 | c_response = 299 | receive do 300 | {_, {:data, <>}} -> 301 | :erlang.binary_to_term(response) 302 | 303 | x -> 304 | IO.inspect(x) 305 | :error 306 | after 307 | 1000 -> 308 | # Not sure how this can be recovered 309 | exit(:port_timed_out) 310 | end 311 | 312 | assert c_response == {:ok, <<0x08>>} 313 | 314 | _ -> 315 | IO.puts("(#{__MODULE__}) Not connected") 316 | end 317 | end 318 | 319 | test "handler_read/write_multi_vars test", state do 320 | case state.status do 321 | :ok -> 322 | # DB1 323 | data1 = %{ 324 | area: 132, 325 | word_len: 2, 326 | db_number: 1, 327 | start: 2, 328 | amount: 4, 329 | data: <<0x42, 0xCB, 0x20, 0x10>> 330 | } 331 | 332 | # P0 333 | data2 = %{ 334 | area: 130, 335 | word_len: 2, 336 | db_number: 1, 337 | start: 0, 338 | amount: 1, 339 | data: <<0x0F>> 340 | } 341 | 342 | # DB1 343 | data3 = %{ 344 | area: 132, 345 | word_len: 2, 346 | db_number: 1, 347 | start: 2, 348 | amount: 4, 349 | data: <<0x42, 0xCA, 0x00, 0x00>> 350 | } 351 | 352 | # P0 353 | data4 = %{ 354 | area: 130, 355 | word_len: 2, 356 | db_number: 1, 357 | start: 0, 358 | amount: 1, 359 | data: <<0x00>> 360 | } 361 | 362 | # DB1 363 | r_data1 = %{ 364 | area: 132, 365 | word_len: 2, 366 | db_number: 1, 367 | start: 2, 368 | amount: 4 369 | } 370 | 371 | # P0 372 | r_data2 = %{ 373 | area: 130, 374 | word_len: 2, 375 | db_number: 1, 376 | start: 0, 377 | amount: 1 378 | } 379 | 380 | # PE0 381 | r_data3 = %{ 382 | area: 129, 383 | word_len: 2, 384 | db_number: 1, 385 | start: 0, 386 | amount: 1 387 | } 388 | 389 | msg = {:write_multi_vars, {2, [data1, data2]}} 390 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 391 | 392 | c_response = 393 | receive do 394 | {_, {:data, <>}} -> 395 | :erlang.binary_to_term(response) 396 | 397 | x -> 398 | IO.inspect(x) 399 | :error 400 | after 401 | 1000 -> 402 | # Not sure how this can be recovered 403 | exit(:port_timed_out) 404 | end 405 | 406 | assert c_response == :ok 407 | 408 | msg = {:read_multi_vars, {2, [r_data1, r_data2]}} 409 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 410 | 411 | c_response = 412 | receive do 413 | {_, {:data, <>}} -> 414 | :erlang.binary_to_term(response) 415 | 416 | x -> 417 | IO.inspect(x) 418 | :error 419 | after 420 | 1000 -> 421 | # Not sure how this can be recovered 422 | exit(:port_timed_out) 423 | end 424 | 425 | assert c_response == {:ok, [<<0x42, 0xCB, 0x20, 0x10>>, <<0x0F>>]} 426 | 427 | msg = {:write_multi_vars, {2, [data3, data4]}} 428 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 429 | 430 | c_response = 431 | receive do 432 | {_, {:data, <>}} -> 433 | :erlang.binary_to_term(response) 434 | 435 | x -> 436 | IO.inspect(x) 437 | :error 438 | after 439 | 1000 -> 440 | # Not sure how this can be recovered 441 | exit(:port_timed_out) 442 | end 443 | 444 | assert c_response == :ok 445 | 446 | msg = {:read_multi_vars, {3, [r_data1, r_data2, r_data3]}} 447 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 448 | 449 | c_response = 450 | receive do 451 | {_, {:data, <>}} -> 452 | :erlang.binary_to_term(response) 453 | 454 | x -> 455 | IO.inspect(x) 456 | :error 457 | after 458 | 1000 -> 459 | # Not sure how this can be recovered 460 | exit(:port_timed_out) 461 | end 462 | 463 | assert c_response == {:ok, [<<0x42, 0xCA, 0x00, 0x00>>, <<0x00>>, <<0x08>>]} 464 | 465 | _ -> 466 | IO.puts("(#{__MODULE__}) Not connected") 467 | end 468 | end 469 | end 470 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_date_time_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliDateTimeTest do 2 | use ExUnit.Case, async: false 3 | doctest Snapex7 4 | 5 | # We don't have the way to test this function 6 | # (we've a plc s7-1200 and snap7 server doesn't support these functions) 7 | # These tests only help us to track the input variables. 8 | 9 | setup do 10 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 11 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 12 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 13 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 14 | 15 | port = 16 | Port.open({:spawn_executable, executable}, [ 17 | {:args, []}, 18 | {:packet, 2}, 19 | :use_stdio, 20 | :binary, 21 | :exit_status 22 | ]) 23 | 24 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 25 | send(port, {self(), {:command, :erlang.term_to_binary(msg)}}) 26 | 27 | status = 28 | receive do 29 | {_, {:data, <>}} -> 30 | :erlang.binary_to_term(response) 31 | after 32 | 10000 -> 33 | :error 34 | end 35 | 36 | %{port: port, status: status} 37 | end 38 | 39 | test "handle_get_plc_date_time", state do 40 | case state.status do 41 | :ok -> 42 | # nil 43 | msg = {:get_plc_date_time, nil} 44 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 45 | 46 | c_response = 47 | receive do 48 | {_, {:data, <>}} -> 49 | :erlang.binary_to_term(response) 50 | 51 | x -> 52 | IO.inspect(x) 53 | :error 54 | after 55 | 1000 -> 56 | # Not sure how this can be recovered 57 | exit(:port_timed_out) 58 | end 59 | 60 | assert c_response == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 61 | 62 | _ -> 63 | IO.puts("(#{__MODULE__}) Not connected") 64 | end 65 | end 66 | 67 | test "handle_set_plc_date_time", state do 68 | case state.status do 69 | # {sec, min, hour, mday, mon, year, wday, yday, isdst} 70 | :ok -> 71 | msg = {:set_plc_date_time, {1, 2, 3, 29, 12, 2018, 0, 355, 1}} 72 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 73 | 74 | c_response = 75 | receive do 76 | {_, {:data, <>}} -> 77 | :erlang.binary_to_term(response) 78 | 79 | x -> 80 | IO.inspect(x) 81 | :error 82 | after 83 | 1000 -> 84 | # Not sure how this can be recovered 85 | exit(:port_timed_out) 86 | end 87 | 88 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 89 | 90 | _ -> 91 | IO.puts("(#{__MODULE__}) Not connected") 92 | end 93 | end 94 | 95 | test "handle_set_plc_system_date_time", state do 96 | case state.status do 97 | :ok -> 98 | # NA 99 | msg = {:set_plc_system_date_time, nil} 100 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 101 | 102 | c_response = 103 | receive do 104 | {_, {:data, <>}} -> 105 | :erlang.binary_to_term(response) 106 | 107 | x -> 108 | IO.inspect(x) 109 | :error 110 | after 111 | 1000 -> 112 | # Not sure how this can be recovered 113 | exit(:port_timed_out) 114 | end 115 | 116 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 117 | 118 | _ -> 119 | IO.puts("(#{__MODULE__}) Not connected") 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_directory_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliDirectoryTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | # We need to implement S7 Server behavior in order to make a proper tests. 6 | # we have a PLC that doesn't supports these functions (S7-1200). 7 | 8 | setup do 9 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 10 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 11 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 12 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 13 | 14 | port = 15 | Port.open({:spawn_executable, executable}, [ 16 | {:args, []}, 17 | {:packet, 2}, 18 | :use_stdio, 19 | :binary, 20 | :exit_status 21 | ]) 22 | 23 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 24 | send(port, {self(), {:command, :erlang.term_to_binary(msg)}}) 25 | 26 | status = 27 | receive do 28 | {_, {:data, <>}} -> 29 | :erlang.binary_to_term(response) 30 | after 31 | 10000 -> 32 | :error 33 | end 34 | 35 | %{port: port, status: status} 36 | end 37 | 38 | test "handler_list_blocks", state do 39 | case state.status do 40 | :ok -> 41 | msg = {:list_blocks, nil} 42 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 43 | 44 | c_response = 45 | receive do 46 | {_, {:data, <>}} -> 47 | :erlang.binary_to_term(response) 48 | 49 | x -> 50 | IO.inspect(x) 51 | :error 52 | after 53 | 1000 -> 54 | # Not sure how this can be recovered 55 | exit(:port_timed_out) 56 | end 57 | 58 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 59 | 60 | _ -> 61 | IO.puts("(#{__MODULE__}) Not connected") 62 | end 63 | end 64 | 65 | test "handler_list_blocks_of_type", state do 66 | case state.status do 67 | :ok -> 68 | msg = {:list_blocks_of_type, {0x38, 2}} 69 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 70 | 71 | c_response = 72 | receive do 73 | {_, {:data, <>}} -> 74 | :erlang.binary_to_term(response) 75 | 76 | x -> 77 | IO.inspect(x) 78 | :error 79 | after 80 | 1000 -> 81 | # Not sure how this can be recovered 82 | exit(:port_timed_out) 83 | end 84 | 85 | assert c_response == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 86 | 87 | _ -> 88 | IO.puts("(#{__MODULE__}) Not connected") 89 | end 90 | end 91 | 92 | test "handler_get_ag_block_info test", state do 93 | case state.status do 94 | :ok -> 95 | msg = {:get_ag_block_info, {0x38, 2}} 96 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 97 | 98 | c_response = 99 | receive do 100 | {_, {:data, <>}} -> 101 | :erlang.binary_to_term(response) 102 | 103 | x -> 104 | IO.inspect(x) 105 | :error 106 | after 107 | 1000 -> 108 | # Not sure how this can be recovered 109 | exit(:port_timed_out) 110 | end 111 | 112 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 113 | 114 | _ -> 115 | IO.puts("(#{__MODULE__}) Not connected") 116 | end 117 | end 118 | 119 | test "handler_get_pg_block_info test", state do 120 | case state.status do 121 | :ok -> 122 | msg = {:get_pg_block_info, {3, <<0x01, 0x02, 0x03>>}} 123 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 124 | 125 | c_response = 126 | receive do 127 | {_, {:data, <>}} -> 128 | :erlang.binary_to_term(response) 129 | 130 | x -> 131 | IO.inspect(x) 132 | :error 133 | after 134 | 1000 -> 135 | # Not sure how this can be recovered 136 | exit(:port_timed_out) 137 | end 138 | 139 | assert c_response == {:error, %{eiso: nil, es7: :errCliInvalidBlockSize, etcp: nil}} 140 | 141 | _ -> 142 | IO.puts("(#{__MODULE__}) Not connected") 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_low_level_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliLowLevelTest do 2 | use ExUnit.Case, async: false 3 | doctest Snapex7 4 | 5 | @s7_msg <<0x32, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x04, 0x01, 0x12, 0x0A, 6 | 0x10, 0x02, 0x00, 0x04, 0x00, 0x01, 0x84, 0x00, 0x00, 0x10>> 7 | @s7_response <<0x32, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, 0x00, 0x04, 8 | 0x01, 0xFF, 0x04, 0x00, 0x20, 0x42, 0xCA, 0x00, 0x00, 0x00, 0x10>> 9 | 10 | setup do 11 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 12 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 13 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 14 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 15 | 16 | port = 17 | Port.open({:spawn_executable, executable}, [ 18 | {:args, []}, 19 | {:packet, 2}, 20 | :use_stdio, 21 | :binary, 22 | :exit_status 23 | ]) 24 | 25 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 26 | send(port, {self(), {:command, :erlang.term_to_binary(msg)}}) 27 | 28 | status = 29 | receive do 30 | {_, {:data, <>}} -> 31 | :erlang.binary_to_term(response) 32 | after 33 | 10000 -> 34 | :error 35 | end 36 | 37 | %{port: port, status: status} 38 | end 39 | 40 | test "handle_iso_exchange_buffer", state do 41 | case state.status do 42 | :ok -> 43 | # {size, S7 pdu} 44 | msg = {:iso_exchange_buffer, {String.length(@s7_msg), @s7_msg}} 45 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 46 | 47 | c_response = 48 | receive do 49 | {_, {:data, <>}} -> 50 | :erlang.binary_to_term(response) 51 | 52 | x -> 53 | IO.inspect(x) 54 | :error 55 | after 56 | 1000 -> 57 | # Not sure how this can be recovered 58 | exit(:port_timed_out) 59 | end 60 | 61 | assert {:ok, @s7_response} == c_response 62 | 63 | _ -> 64 | IO.puts("(#{__MODULE__}) Not connected") 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_misc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliMiscTest do 2 | use ExUnit.Case, async: false 3 | doctest Snapex7 4 | 5 | setup do 6 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 7 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 8 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 9 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 10 | 11 | port = 12 | Port.open({:spawn_executable, executable}, [ 13 | {:args, []}, 14 | {:packet, 2}, 15 | :use_stdio, 16 | :binary, 17 | :exit_status 18 | ]) 19 | 20 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 21 | send(port, {self(), {:command, :erlang.term_to_binary(msg)}}) 22 | 23 | status = 24 | receive do 25 | {_, {:data, <>}} -> 26 | :erlang.binary_to_term(response) 27 | after 28 | 10000 -> 29 | :error 30 | end 31 | 32 | %{port: port, status: status} 33 | end 34 | 35 | test "handle_get_exec_time", state do 36 | case state.status do 37 | :ok -> 38 | # NA 39 | msg = {:get_exec_time, nil} 40 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 41 | 42 | c_response = 43 | receive do 44 | {_, {:data, <>}} -> 45 | :erlang.binary_to_term(response) 46 | 47 | x -> 48 | IO.inspect(x) 49 | :error 50 | after 51 | 1000 -> 52 | # Not sure how this can be recovered 53 | exit(:port_timed_out) 54 | end 55 | 56 | {:ok, num} = c_response 57 | assert is_integer(num) 58 | 59 | _ -> 60 | IO.puts("(#{__MODULE__}) Not connected") 61 | end 62 | end 63 | 64 | test "handle_get_last_error", state do 65 | case state.status do 66 | :ok -> 67 | # no supported function (returns an error). 68 | # NA 69 | msg = {:plc_stop, nil} 70 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 71 | 72 | c_response = 73 | receive do 74 | {_, {:data, <>}} -> 75 | :erlang.binary_to_term(response) 76 | 77 | x -> 78 | IO.inspect(x) 79 | :error 80 | after 81 | 1000 -> 82 | # Not sure how this can be recovered 83 | exit(:port_timed_out) 84 | end 85 | 86 | {:error, snap7_error} = c_response 87 | 88 | # NA 89 | msg = {:get_last_error, nil} 90 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 91 | 92 | c_response = 93 | receive do 94 | {_, {:data, <>}} -> 95 | :erlang.binary_to_term(response) 96 | 97 | x -> 98 | IO.inspect(x) 99 | :error 100 | after 101 | 1000 -> 102 | # Not sure how this can be recovered 103 | exit(:port_timed_out) 104 | end 105 | 106 | {:ok, last_error} = c_response 107 | assert snap7_error == last_error 108 | 109 | _ -> 110 | IO.puts("(#{__MODULE__}) Not connected") 111 | end 112 | end 113 | 114 | test "handle_get_pdu_length", state do 115 | case state.status do 116 | :ok -> 117 | # NA 118 | msg = {:get_pdu_length, nil} 119 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 120 | 121 | c_response = 122 | receive do 123 | {_, {:data, <>}} -> 124 | :erlang.binary_to_term(response) 125 | 126 | x -> 127 | IO.inspect(x) 128 | :error 129 | after 130 | 1000 -> 131 | # Not sure how this can be recovered 132 | exit(:port_timed_out) 133 | end 134 | 135 | {:ok, pdu} = c_response 136 | assert pdu == [Requested: 480, Negotiated: 240] 137 | 138 | _ -> 139 | IO.puts("(#{__MODULE__}) Not connected") 140 | end 141 | end 142 | 143 | test "handle_get_connected", state do 144 | case state.status do 145 | :ok -> 146 | # NA 147 | msg = {:get_connected, nil} 148 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 149 | 150 | c_response = 151 | receive do 152 | {_, {:data, <>}} -> 153 | :erlang.binary_to_term(response) 154 | 155 | x -> 156 | IO.inspect(x) 157 | :error 158 | after 159 | 1000 -> 160 | # Not sure how this can be recovered 161 | exit(:port_timed_out) 162 | end 163 | 164 | assert c_response == {:ok, true} 165 | 166 | # disconnect 167 | msg = {:disconnect, nil} 168 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 169 | 170 | c_response = 171 | receive do 172 | {_, {:data, <>}} -> 173 | :erlang.binary_to_term(response) 174 | 175 | x -> 176 | IO.inspect(x) 177 | :error 178 | after 179 | 1000 -> 180 | # Not sure how this can be recovered 181 | exit(:port_timed_out) 182 | end 183 | 184 | assert c_response == :ok 185 | 186 | # NA 187 | msg = {:get_connected, nil} 188 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 189 | 190 | c_response = 191 | receive do 192 | {_, {:data, <>}} -> 193 | :erlang.binary_to_term(response) 194 | 195 | x -> 196 | IO.inspect(x) 197 | :error 198 | after 199 | 1000 -> 200 | # Not sure how this can be recovered 201 | exit(:port_timed_out) 202 | end 203 | 204 | assert c_response == {:ok, false} 205 | 206 | _ -> 207 | IO.puts("(#{__MODULE__}) Not connected") 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_plc_control_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliPlcControlTest do 2 | use ExUnit.Case, async: false 3 | doctest Snapex7 4 | # We don't have the way to test this function 5 | # (we've a plc s7-1200 and snap7 server doesn't support these functions) 6 | # These tests only help us to track the input variables. 7 | setup do 8 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 9 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 10 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 11 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 12 | 13 | port = 14 | Port.open({:spawn_executable, executable}, [ 15 | {:args, []}, 16 | {:packet, 2}, 17 | :use_stdio, 18 | :binary, 19 | :exit_status 20 | ]) 21 | 22 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 23 | send(port, {self(), {:command, :erlang.term_to_binary(msg)}}) 24 | 25 | status = 26 | receive do 27 | {_, {:data, <>}} -> 28 | :erlang.binary_to_term(response) 29 | after 30 | 10000 -> 31 | :error 32 | end 33 | 34 | %{port: port, status: status} 35 | end 36 | 37 | test "handle_plc_hot_start", state do 38 | case state.status do 39 | :ok -> 40 | # NA 41 | msg = {:plc_hot_start, nil} 42 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 43 | 44 | c_response = 45 | receive do 46 | {_, {:data, <>}} -> 47 | :erlang.binary_to_term(response) 48 | 49 | x -> 50 | IO.inspect(x) 51 | :error 52 | after 53 | 1000 -> 54 | # Not sure how this can be recovered 55 | exit(:port_timed_out) 56 | end 57 | 58 | # when PLC is running 59 | assert c_response == {:error, %{eiso: nil, es7: :errCliCannotStartPLC, etcp: nil}} 60 | 61 | _ -> 62 | IO.puts("(#{__MODULE__}) Not connected") 63 | end 64 | end 65 | 66 | test "handle_plc_cold_start", state do 67 | case state.status do 68 | :ok -> 69 | # NA 70 | msg = {:plc_cold_start, nil} 71 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 72 | 73 | c_response = 74 | receive do 75 | {_, {:data, <>}} -> 76 | :erlang.binary_to_term(response) 77 | 78 | x -> 79 | IO.inspect(x) 80 | :error 81 | after 82 | 1000 -> 83 | # Not sure how this can be recovered 84 | exit(:port_timed_out) 85 | end 86 | 87 | # when PLC is running 88 | assert c_response == {:error, %{eiso: nil, es7: :errCliCannotStartPLC, etcp: nil}} 89 | 90 | _ -> 91 | IO.puts("(#{__MODULE__}) Not connected") 92 | end 93 | end 94 | 95 | test "handle_plc_stop", state do 96 | case state.status do 97 | :ok -> 98 | # NA 99 | msg = {:plc_stop, nil} 100 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 101 | 102 | c_response = 103 | receive do 104 | {_, {:data, <>}} -> 105 | :erlang.binary_to_term(response) 106 | 107 | x -> 108 | IO.inspect(x) 109 | :error 110 | after 111 | 1000 -> 112 | # Not sure how this can be recovered 113 | exit(:port_timed_out) 114 | end 115 | 116 | # PLC s7-1200 cannot be stopped 117 | assert c_response == {:error, %{eiso: nil, es7: :errCliCannotStopPLC, etcp: nil}} 118 | 119 | _ -> 120 | IO.puts("(#{__MODULE__}) Not connected") 121 | end 122 | end 123 | 124 | test "handle_copy_ram_to_rom", state do 125 | case state.status do 126 | :ok -> 127 | # timeout 128 | msg = {:copy_ram_to_rom, 300} 129 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 130 | 131 | c_response = 132 | receive do 133 | {_, {:data, <>}} -> 134 | :erlang.binary_to_term(response) 135 | 136 | x -> 137 | IO.inspect(x) 138 | :error 139 | after 140 | 1000 -> 141 | # Not sure how this can be recovered 142 | exit(:port_timed_out) 143 | end 144 | 145 | # when PLC is running 146 | assert c_response == {:error, %{eiso: nil, es7: :errCliCannotCopyRamToRom, etcp: nil}} 147 | 148 | _ -> 149 | IO.puts("(#{__MODULE__}) Not connected") 150 | end 151 | end 152 | 153 | test "handle_compress", state do 154 | case state.status do 155 | :ok -> 156 | # timeout 157 | msg = {:compress, 300} 158 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 159 | 160 | c_response = 161 | receive do 162 | {_, {:data, <>}} -> 163 | :erlang.binary_to_term(response) 164 | 165 | x -> 166 | IO.inspect(x) 167 | :error 168 | after 169 | 1000 -> 170 | # Not sure how this can be recovered 171 | exit(:port_timed_out) 172 | end 173 | 174 | # when the PLC is runnig 175 | assert c_response == {:error, %{eiso: nil, es7: :errCliCannotCompress, etcp: nil}} 176 | 177 | _ -> 178 | IO.puts("(#{__MODULE__}) Not connected") 179 | end 180 | end 181 | 182 | test "handle_get_plc_status", state do 183 | case state.status do 184 | :ok -> 185 | # NA 186 | msg = {:get_plc_status, nil} 187 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 188 | 189 | c_response = 190 | receive do 191 | {_, {:data, <>}} -> 192 | :erlang.binary_to_term(response) 193 | 194 | x -> 195 | IO.inspect(x) 196 | :error 197 | after 198 | 1000 -> 199 | # Not sure how this can be recovered 200 | exit(:port_timed_out) 201 | end 202 | 203 | # when the PLC is running 204 | assert c_response == {:ok, :S7CpuStatusRun} 205 | 206 | _ -> 207 | IO.puts("(#{__MODULE__}) Not connected") 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_security_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliSecurityTest do 2 | use ExUnit.Case, async: false 3 | doctest Snapex7 4 | # We don't have the way to test this function 5 | # (we've a plc s7-1200 and snap7 server doesn't support these functions) 6 | # These tests only help us to track the input variables. 7 | setup do 8 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 9 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 10 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 11 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 12 | 13 | port = 14 | Port.open({:spawn_executable, executable}, [ 15 | {:args, []}, 16 | {:packet, 2}, 17 | :use_stdio, 18 | :binary, 19 | :exit_status 20 | ]) 21 | 22 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 23 | send(port, {self(), {:command, :erlang.term_to_binary(msg)}}) 24 | 25 | status = 26 | receive do 27 | {_, {:data, <>}} -> 28 | :erlang.binary_to_term(response) 29 | after 30 | 10000 -> 31 | :error 32 | end 33 | 34 | %{port: port, status: status} 35 | end 36 | 37 | test "handle_set_session_password", state do 38 | case state.status do 39 | :ok -> 40 | # password 41 | msg = {:set_session_password, "holahola"} 42 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 43 | 44 | c_response = 45 | receive do 46 | {_, {:data, <>}} -> 47 | :erlang.binary_to_term(response) 48 | 49 | x -> 50 | IO.inspect(x) 51 | :error 52 | after 53 | 1000 -> 54 | # Not sure how this can be recovered 55 | exit(:port_timed_out) 56 | end 57 | 58 | # PLC s7-1200 doesnt support this func. 59 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 60 | 61 | _ -> 62 | IO.puts("(#{__MODULE__}) Not connected") 63 | end 64 | end 65 | 66 | test "handle_clear_session_password", state do 67 | case state.status do 68 | :ok -> 69 | # NA 70 | msg = {:clear_session_password, nil} 71 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 72 | 73 | c_response = 74 | receive do 75 | {_, {:data, <>}} -> 76 | :erlang.binary_to_term(response) 77 | 78 | x -> 79 | IO.inspect(x) 80 | :error 81 | after 82 | 1000 -> 83 | # Not sure how this can be recovered 84 | exit(:port_timed_out) 85 | end 86 | 87 | # PLC s7-1200 doesnt support this func. 88 | assert c_response == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 89 | 90 | _ -> 91 | IO.puts("(#{__MODULE__}) Not connected") 92 | end 93 | end 94 | 95 | test "handle_get_protection", state do 96 | case state.status do 97 | :ok -> 98 | # NA 99 | msg = {:get_protection, nil} 100 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 101 | 102 | c_response = 103 | receive do 104 | {_, {:data, <>}} -> 105 | :erlang.binary_to_term(response) 106 | 107 | x -> 108 | IO.inspect(x) 109 | :error 110 | after 111 | 1000 -> 112 | # Not sure how this can be recovered 113 | exit(:port_timed_out) 114 | end 115 | 116 | # PLC s7-1200 doesnt support this func. 117 | assert c_response == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 118 | 119 | _ -> 120 | IO.puts("(#{__MODULE__}) Not connected") 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/c_backend_tests/cli_sys_info_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliSysInfoTest do 2 | use ExUnit.Case, async: false 3 | doctest Snapex7 4 | 5 | # We don't have the way to test this function 6 | # (we've a plc s7-1200 and snap7 server doesn't support these functions) 7 | # These tests only help us to track the input variables. 8 | 9 | setup do 10 | snap7_dir = :code.priv_dir(:snapex7) |> List.to_string() 11 | System.put_env("LD_LIBRARY_PATH", snap7_dir) 12 | System.put_env("DYLD_LIBRARY_PATH", snap7_dir) 13 | executable = :code.priv_dir(:snapex7) ++ ~c"/s7_client.o" 14 | 15 | port = 16 | Port.open({:spawn_executable, executable}, [ 17 | {:args, []}, 18 | {:packet, 2}, 19 | :use_stdio, 20 | :binary, 21 | :exit_status 22 | ]) 23 | 24 | msg = {:connect_to, {"192.168.0.1", 0, 1}} 25 | send(port, {self(), {:command, :erlang.term_to_binary(msg)}}) 26 | 27 | status = 28 | receive do 29 | {_, {:data, <>}} -> 30 | :erlang.binary_to_term(response) 31 | after 32 | 10000 -> 33 | :error 34 | end 35 | 36 | %{port: port, status: status} 37 | end 38 | 39 | test "handle_read_szl", state do 40 | case state.status do 41 | :ok -> 42 | # {ID, index} 43 | msg = {:read_szl, {0x0111, 0x0006}} 44 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 45 | 46 | c_response = 47 | receive do 48 | {_, {:data, <>}} -> 49 | :erlang.binary_to_term(response) 50 | 51 | x -> 52 | IO.inspect(x) 53 | :error 54 | after 55 | 1000 -> 56 | # Not sure how this can be recovered 57 | exit(:port_timed_out) 58 | end 59 | 60 | id = <<0, 6>> 61 | module_num = "6ES7 211-1AE40-0XB0" 62 | tail = <<32, 0, 0, 0, 7, 32, 32>> 63 | assert c_response == {:ok, id <> module_num <> tail} 64 | 65 | _ -> 66 | IO.puts("(#{__MODULE__}) Not connected") 67 | end 68 | end 69 | 70 | test "handle_read_szl_list", state do 71 | case state.status do 72 | :ok -> 73 | # NA 74 | msg = {:read_szl_list, nil} 75 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 76 | 77 | c_response = 78 | receive do 79 | {_, {:data, <>}} -> 80 | :erlang.binary_to_term(response) 81 | 82 | x -> 83 | IO.inspect(x) 84 | :error 85 | after 86 | 1000 -> 87 | # Not sure how this can be recovered 88 | exit(:port_timed_out) 89 | end 90 | 91 | assert c_response == {:ok, [0, 17, 273, 3857, 1060, 305]} 92 | 93 | _ -> 94 | IO.puts("(#{__MODULE__}) Not connected") 95 | end 96 | end 97 | 98 | test "handle_get_order_code", state do 99 | case state.status do 100 | :ok -> 101 | # NA 102 | msg = {:get_order_code, nil} 103 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 104 | 105 | c_response = 106 | receive do 107 | {_, {:data, <>}} -> 108 | :erlang.binary_to_term(response) 109 | 110 | x -> 111 | IO.inspect(x) 112 | :error 113 | after 114 | 1000 -> 115 | # Not sure how this can be recovered 116 | exit(:port_timed_out) 117 | end 118 | 119 | assert c_response == {:ok, [Code: "6ES7 211-1AE40-0XB0 ", Version: "4.2.1"]} 120 | 121 | _ -> 122 | IO.puts("(#{__MODULE__}) Not connected") 123 | end 124 | end 125 | 126 | test "handle_get_cpu_info", state do 127 | case state.status do 128 | :ok -> 129 | # NA 130 | msg = {:get_cpu_info, nil} 131 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 132 | 133 | c_response = 134 | receive do 135 | {_, {:data, <>}} -> 136 | :erlang.binary_to_term(response) 137 | 138 | x -> 139 | IO.inspect(x) 140 | :error 141 | after 142 | 1000 -> 143 | # Not sure how this can be recovered 144 | exit(:port_timed_out) 145 | end 146 | 147 | assert c_response == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 148 | 149 | _ -> 150 | IO.puts("(#{__MODULE__}) Not connected") 151 | end 152 | end 153 | 154 | test "handle_get_cp_info", state do 155 | case state.status do 156 | :ok -> 157 | # NA 158 | msg = {:get_cp_info, nil} 159 | send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}}) 160 | 161 | c_response = 162 | receive do 163 | {_, {:data, <>}} -> 164 | :erlang.binary_to_term(response) 165 | 166 | x -> 167 | IO.inspect(x) 168 | :error 169 | after 170 | 1000 -> 171 | # Not sure how this can be recovered 172 | exit(:port_timed_out) 173 | end 174 | 175 | assert c_response == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 176 | 177 | _ -> 178 | IO.puts("(#{__MODULE__}) Not connected") 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /test/s7_client_test/admin_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AdminFunTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | {:ok, pid} = Snapex7.Client.start_link() 7 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 8 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 9 | %{pid: pid, status: state} 10 | end 11 | 12 | test "set_connection_type function", state do 13 | case state.status do 14 | :connected -> 15 | resp = Snapex7.Client.set_connection_type(state.pid, :PG) 16 | assert resp == :ok 17 | 18 | _ -> 19 | IO.puts("(#{__MODULE__}) Not connected") 20 | end 21 | end 22 | 23 | test "set_connection_params function", state do 24 | case state.status do 25 | :connected -> 26 | resp = 27 | Snapex7.Client.set_connection_params(state.pid, 28 | ip: "192.168.1.100", 29 | local_tsap: 1, 30 | remote_tsap: 2 31 | ) 32 | 33 | assert resp == :ok 34 | 35 | _ -> 36 | IO.puts("(#{__MODULE__}) Not connected") 37 | end 38 | end 39 | 40 | test "connect_to function" do 41 | {:ok, pid} = Snapex7.Client.start_link() 42 | resp = Snapex7.Client.connect_to(pid, ip: "192.168.0.200", rack: 0, slot: 1) 43 | assert resp == {:error, %{eiso: nil, es7: nil, etcp: 113}} 44 | resp = Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 45 | assert resp == :ok 46 | end 47 | 48 | test "connect/disconnect function", state do 49 | case state.status do 50 | :connected -> 51 | resp = Snapex7.Client.disconnect(state.pid) 52 | assert resp == :ok 53 | 54 | resp = Snapex7.Client.connect(state.pid) 55 | assert resp == :ok 56 | 57 | _ -> 58 | IO.puts("(#{__MODULE__}) Not connected") 59 | end 60 | end 61 | 62 | test "get_params/set_params function" do 63 | {:ok, pid} = Snapex7.Client.start_link() 64 | resp = Snapex7.Client.set_params(pid, 3, 800) 65 | assert resp == :ok 66 | resp = Snapex7.Client.get_params(pid, 3) 67 | assert resp == {:ok, 800} 68 | 69 | resp = Snapex7.Client.set_params(pid, 2, 400) 70 | assert resp == :ok 71 | resp = Snapex7.Client.get_params(pid, 2) 72 | assert resp == {:ok, 400} 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/s7_client_test/block_oriented_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BlockOrientedFunTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | {:ok, pid} = Snapex7.Client.start_link() 7 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 8 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 9 | %{pid: pid, status: state} 10 | end 11 | 12 | # functions no supported by PLC s7-1200 13 | test "full_upload function", state do 14 | case state.status do 15 | :connected -> 16 | resp = Snapex7.Client.full_upload(state.pid, :OB, 0x41, 4) 17 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 18 | 19 | _ -> 20 | IO.puts("(#{__MODULE__}) Not connected") 21 | end 22 | end 23 | 24 | test "upload function", state do 25 | case state.status do 26 | :connected -> 27 | resp = Snapex7.Client.upload(state.pid, :OB, 0x41, 4) 28 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 29 | 30 | _ -> 31 | IO.puts("(#{__MODULE__}) Not connected") 32 | end 33 | end 34 | 35 | test "download function", state do 36 | case state.status do 37 | :connected -> 38 | resp = Snapex7.Client.download(state.pid, 0x38, <<0x02, 0x34, 0x35>>) 39 | assert resp == {:error, %{eiso: nil, es7: :errCliInvalidBlockSize, etcp: nil}} 40 | 41 | _ -> 42 | IO.puts("(#{__MODULE__}) Not connected") 43 | end 44 | end 45 | 46 | test "delete function", state do 47 | case state.status do 48 | :connected -> 49 | resp = Snapex7.Client.delete(state.pid, :OB, 0x03) 50 | assert resp == {:error, %{eiso: nil, es7: :errCliDeleteRefused, etcp: nil}} 51 | 52 | _ -> 53 | IO.puts("(#{__MODULE__}) Not connected") 54 | end 55 | end 56 | 57 | test "db_get function", state do 58 | case state.status do 59 | :connected -> 60 | resp = Snapex7.Client.db_get(state.pid, 0) 61 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 62 | 63 | _ -> 64 | IO.puts("(#{__MODULE__}) Not connected") 65 | end 66 | end 67 | 68 | test "db_fill function", state do 69 | case state.status do 70 | :connected -> 71 | resp = Snapex7.Client.db_fill(state.pid, 2, 0) 72 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 73 | 74 | _ -> 75 | IO.puts("(#{__MODULE__}) Not connected") 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/s7_client_test/data_io_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DataIoFunTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | {:ok, pid} = Snapex7.Client.start_link() 7 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 8 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 9 | %{pid: pid, status: state} 10 | end 11 | 12 | test "read/write_area function", state do 13 | case state.status do 14 | :connected -> 15 | resp = 16 | Snapex7.Client.write_area(state.pid, 17 | area: :DB, 18 | word_len: :byte, 19 | start: 2, 20 | amount: 4, 21 | db_number: 1, 22 | data: <<0x42, 0xCB, 0x00, 0x00>> 23 | ) 24 | 25 | assert resp == :ok 26 | 27 | {:ok, resp_bin} = 28 | Snapex7.Client.read_area(state.pid, 29 | area: :DB, 30 | word_len: :byte, 31 | start: 2, 32 | amount: 4, 33 | db_number: 1 34 | ) 35 | 36 | assert resp_bin == <<0x42, 0xCB, 0x00, 0x00>> 37 | 38 | resp = 39 | Snapex7.Client.write_area(state.pid, 40 | area: :DB, 41 | word_len: :byte, 42 | start: 2, 43 | amount: 4, 44 | db_number: 1, 45 | data: <<0x42, 0xCA, 0x00, 0x00>> 46 | ) 47 | 48 | assert resp == :ok 49 | 50 | {:ok, resp_bin} = 51 | Snapex7.Client.read_area(state.pid, 52 | area: :DB, 53 | word_len: :byte, 54 | start: 2, 55 | amount: 4, 56 | db_number: 1 57 | ) 58 | 59 | assert resp_bin == <<0x42, 0xCA, 0x00, 0x00>> 60 | 61 | _ -> 62 | IO.puts("(#{__MODULE__}) Not connected") 63 | end 64 | end 65 | 66 | test "db_read/write function", state do 67 | case state.status do 68 | :connected -> 69 | resp = 70 | Snapex7.Client.db_write(state.pid, 71 | start: 2, 72 | amount: 4, 73 | db_number: 1, 74 | data: <<0x42, 0xCB, 0x00, 0x00>> 75 | ) 76 | 77 | assert resp == :ok 78 | 79 | {:ok, resp_bin} = 80 | Snapex7.Client.db_read(state.pid, 81 | start: 2, 82 | amount: 4, 83 | db_number: 1 84 | ) 85 | 86 | assert resp_bin == <<0x42, 0xCB, 0x00, 0x00>> 87 | 88 | resp = 89 | Snapex7.Client.db_write(state.pid, 90 | start: 2, 91 | amount: 4, 92 | db_number: 1, 93 | data: <<0x42, 0xCA, 0x00, 0x00>> 94 | ) 95 | 96 | assert resp == :ok 97 | 98 | {:ok, resp_bin} = 99 | Snapex7.Client.db_read(state.pid, 100 | start: 2, 101 | amount: 4, 102 | db_number: 1 103 | ) 104 | 105 | assert resp_bin == <<0x42, 0xCA, 0x00, 0x00>> 106 | 107 | _ -> 108 | IO.puts("(#{__MODULE__}) Not connected") 109 | end 110 | end 111 | 112 | test "ab_read/write function", state do 113 | case state.status do 114 | :connected -> 115 | resp = 116 | Snapex7.Client.ab_write(state.pid, 117 | start: 0, 118 | amount: 1, 119 | data: <<0x0F>> 120 | ) 121 | 122 | assert resp == :ok 123 | 124 | {:ok, resp_bin} = 125 | Snapex7.Client.ab_read(state.pid, 126 | start: 0, 127 | amount: 1 128 | ) 129 | 130 | assert resp_bin == <<0x0F>> 131 | 132 | resp = 133 | Snapex7.Client.ab_write(state.pid, 134 | start: 0, 135 | amount: 1, 136 | data: <<0x00>> 137 | ) 138 | 139 | assert resp == :ok 140 | 141 | {:ok, resp_bin} = 142 | Snapex7.Client.ab_read(state.pid, 143 | start: 0, 144 | amount: 1 145 | ) 146 | 147 | assert resp_bin == <<0x00>> 148 | 149 | _ -> 150 | IO.puts("(#{__MODULE__}) Not connected") 151 | end 152 | end 153 | 154 | # eb_write doesn't make sense... 155 | test "eb_read/write function", state do 156 | case state.status do 157 | :connected -> 158 | resp = 159 | Snapex7.Client.eb_write(state.pid, 160 | start: 0, 161 | amount: 1, 162 | data: <<0x0F>> 163 | ) 164 | 165 | assert resp == :ok 166 | 167 | {:ok, resp_bin} = 168 | Snapex7.Client.eb_read(state.pid, 169 | start: 0, 170 | amount: 1 171 | ) 172 | 173 | assert resp_bin == <<0x08>> 174 | 175 | _ -> 176 | IO.puts("(#{__MODULE__}) Not connected") 177 | end 178 | end 179 | 180 | test "mb_read/write function", state do 181 | case state.status do 182 | :connected -> 183 | resp = 184 | Snapex7.Client.mb_write(state.pid, 185 | start: 0, 186 | amount: 1, 187 | data: <<0x0F>> 188 | ) 189 | 190 | assert resp == :ok 191 | 192 | {:ok, resp_bin} = 193 | Snapex7.Client.mb_read(state.pid, 194 | start: 0, 195 | amount: 1 196 | ) 197 | 198 | assert resp_bin == <<0x0F>> 199 | 200 | resp = 201 | Snapex7.Client.mb_write(state.pid, 202 | start: 0, 203 | amount: 1, 204 | data: <<0x00>> 205 | ) 206 | 207 | assert resp == :ok 208 | 209 | {:ok, resp_bin} = 210 | Snapex7.Client.mb_read(state.pid, 211 | start: 0, 212 | amount: 1 213 | ) 214 | 215 | assert resp_bin == <<0x00>> 216 | 217 | _ -> 218 | IO.puts("(#{__MODULE__}) Not connected") 219 | end 220 | end 221 | 222 | # PLC s7-1200 doesn't supports this functions. 223 | test "tm_read/write function", state do 224 | case state.status do 225 | :connected -> 226 | resp = 227 | Snapex7.Client.tm_write(state.pid, 228 | start: 0, 229 | amount: 1, 230 | data: <<0x0F, 0x00>> 231 | ) 232 | 233 | assert resp == {:error, %{eiso: nil, es7: :errCliAddressOutOfRange, etcp: nil}} 234 | 235 | resp = 236 | Snapex7.Client.tm_read(state.pid, 237 | start: 0, 238 | amount: 1 239 | ) 240 | 241 | assert resp == {:error, %{eiso: nil, es7: :errCliAddressOutOfRange, etcp: nil}} 242 | 243 | _ -> 244 | IO.puts("(#{__MODULE__}) Not connected") 245 | end 246 | end 247 | 248 | # PLC s7-1200 doesn't supports this functions. 249 | test "ct_read/write function", state do 250 | case state.status do 251 | :connected -> 252 | resp = 253 | Snapex7.Client.ct_write(state.pid, 254 | start: 0, 255 | amount: 1, 256 | data: <<0x0F, 0x00>> 257 | ) 258 | 259 | assert resp == {:error, %{eiso: nil, es7: :errCliAddressOutOfRange, etcp: nil}} 260 | 261 | resp = 262 | Snapex7.Client.ct_read(state.pid, 263 | start: 0, 264 | amount: 1 265 | ) 266 | 267 | assert resp == {:error, %{eiso: nil, es7: :errCliAddressOutOfRange, etcp: nil}} 268 | 269 | _ -> 270 | IO.puts("(#{__MODULE__}) Not connected") 271 | end 272 | end 273 | 274 | test "read/write_multi_vars function", state do 275 | data1 = %{ 276 | area: :DB, 277 | word_len: :byte, 278 | db_number: 1, 279 | start: 2, 280 | amount: 4, 281 | data: <<0x42, 0xCB, 0x20, 0x10>> 282 | } 283 | 284 | # P0 285 | data2 = %{ 286 | area: :PA, 287 | word_len: :byte, 288 | db_number: 1, 289 | start: 0, 290 | amount: 1, 291 | data: <<0x0F>> 292 | } 293 | 294 | # DB1 295 | data3 = %{ 296 | area: :DB, 297 | word_len: :byte, 298 | db_number: 1, 299 | start: 2, 300 | amount: 4, 301 | data: <<0x42, 0xCA, 0x00, 0x00>> 302 | } 303 | 304 | # P0 305 | data4 = %{ 306 | area: :PA, 307 | word_len: :byte, 308 | db_number: 1, 309 | start: 0, 310 | amount: 1, 311 | data: <<0x00>> 312 | } 313 | 314 | # DB1 315 | r_data1 = %{ 316 | area: :DB, 317 | word_len: :byte, 318 | db_number: 1, 319 | start: 2, 320 | amount: 4 321 | } 322 | 323 | # P0 324 | r_data2 = %{ 325 | area: :PA, 326 | word_len: :byte, 327 | db_number: 1, 328 | start: 0, 329 | amount: 1 330 | } 331 | 332 | # PE0 333 | r_data3 = %{ 334 | area: :PE, 335 | word_len: :byte, 336 | db_number: 1, 337 | start: 0, 338 | amount: 1 339 | } 340 | 341 | case state.status do 342 | :connected -> 343 | resp = Snapex7.Client.write_multi_vars(state.pid, data: [data1, data2]) 344 | assert resp == :ok 345 | 346 | resp = Snapex7.Client.read_multi_vars(state.pid, data: [r_data1, r_data2]) 347 | assert resp == {:ok, [<<0x42, 0xCB, 0x20, 0x10>>, <<0x0F>>]} 348 | 349 | resp = Snapex7.Client.write_multi_vars(state.pid, data: [data3, data4]) 350 | assert resp == :ok 351 | 352 | resp = Snapex7.Client.read_multi_vars(state.pid, data: [r_data1, r_data2, r_data3]) 353 | assert resp == {:ok, [<<0x42, 0xCA, 0x00, 0x00>>, <<0x00>>, <<0x08>>]} 354 | 355 | _ -> 356 | IO.puts("(#{__MODULE__}) Not connected") 357 | end 358 | end 359 | end 360 | -------------------------------------------------------------------------------- /test/s7_client_test/date_time_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DateTimeFunTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | {:ok, pid} = Snapex7.Client.start_link() 7 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 8 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 9 | %{pid: pid, status: state} 10 | end 11 | 12 | # functions no supported by PLC s7-1200 13 | test "get_plc_date_time function", state do 14 | case state.status do 15 | :connected -> 16 | resp = Snapex7.Client.get_plc_date_time(state.pid) 17 | assert resp == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 18 | 19 | _ -> 20 | IO.puts("(#{__MODULE__}) Not connected") 21 | end 22 | end 23 | 24 | test "set_plc_date_time function", state do 25 | case state.status do 26 | :connected -> 27 | resp = 28 | Snapex7.Client.set_plc_date_time(state.pid, 29 | sec: 20, 30 | min: 59, 31 | hour: 23, 32 | mday: 23, 33 | mon: 12, 34 | year: 1990, 35 | wday: 3, 36 | yday: 320, 37 | isdst: 1 38 | ) 39 | 40 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 41 | 42 | _ -> 43 | IO.puts("(#{__MODULE__}) Not connected") 44 | end 45 | end 46 | 47 | test "set_plc_system_date_time function", state do 48 | case state.status do 49 | :connected -> 50 | resp = Snapex7.Client.set_plc_system_date_time(state.pid) 51 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 52 | 53 | _ -> 54 | IO.puts("(#{__MODULE__}) Not connected") 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/s7_client_test/directory_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DirectoryFunTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | {:ok, pid} = Snapex7.Client.start_link() 7 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 8 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 9 | %{pid: pid, status: state} 10 | end 11 | 12 | # functions no supported by PLC s7-1200 13 | test "list_blocks function", state do 14 | case state.status do 15 | :connected -> 16 | resp = Snapex7.Client.list_blocks(state.pid) 17 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 18 | 19 | _ -> 20 | IO.puts("(#{__MODULE__}) Not connected") 21 | end 22 | end 23 | 24 | test "list_blocks_of_type function", state do 25 | case state.status do 26 | :connected -> 27 | resp = Snapex7.Client.list_blocks_of_type(state.pid, :OB, 2) 28 | assert resp == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 29 | 30 | _ -> 31 | IO.puts("(#{__MODULE__}) Not connected") 32 | end 33 | end 34 | 35 | test "get_ag_block_info function", state do 36 | case state.status do 37 | :connected -> 38 | resp = Snapex7.Client.get_ag_block_info(state.pid, :OB, 2) 39 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 40 | 41 | _ -> 42 | IO.puts("(#{__MODULE__}) Not connected") 43 | end 44 | end 45 | 46 | test "get_pg_block_info function", state do 47 | case state.status do 48 | :connected -> 49 | resp = Snapex7.Client.get_pg_block_info(state.pid, <<0x01, 0x02, 0x03>>) 50 | assert resp == {:error, %{eiso: nil, es7: :errCliInvalidBlockSize, etcp: nil}} 51 | 52 | _ -> 53 | IO.puts("(#{__MODULE__}) Not connected") 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/s7_client_test/low_level_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LowLevelFunTest do 2 | use ExUnit.Case, async: false 3 | doctest Snapex7 4 | 5 | @s7_msg <<0x32, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x04, 0x01, 0x12, 0x0A, 6 | 0x10, 0x02, 0x00, 0x04, 0x00, 0x01, 0x84, 0x00, 0x00, 0x10>> 7 | @s7_response <<0x32, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, 0x00, 0x04, 8 | 0x01, 0xFF, 0x04, 0x00, 0x20, 0x42, 0xCA, 0x00, 0x00, 0x00, 0x10>> 9 | 10 | setup do 11 | {:ok, pid} = Snapex7.Client.start_link() 12 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 13 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 14 | %{pid: pid, status: state} 15 | end 16 | 17 | test "iso_exchange_buffer function", state do 18 | case state.status do 19 | :connected -> 20 | resp = Snapex7.Client.iso_exchange_buffer(state.pid, @s7_msg) 21 | # when PLC is running 22 | assert resp == {:ok, @s7_response} 23 | 24 | _ -> 25 | IO.puts("(#{__MODULE__}) Not connected") 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/s7_client_test/miscellaneous_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MiscellaneousFunTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | {:ok, pid} = Snapex7.Client.start_link() 7 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 8 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 9 | %{pid: pid, status: state} 10 | end 11 | 12 | test "get_exec_time function", state do 13 | case state.status do 14 | :connected -> 15 | {:ok, resp_num} = Snapex7.Client.get_exec_time(state.pid) 16 | assert resp_num |> is_integer 17 | 18 | _ -> 19 | IO.puts("(#{__MODULE__}) Not connected") 20 | end 21 | end 22 | 23 | test "get_last_error function", state do 24 | case state.status do 25 | :connected -> 26 | {:error, error} = Snapex7.Client.plc_stop(state.pid) 27 | resp = Snapex7.Client.get_last_error(state.pid) 28 | assert resp == {:ok, error} 29 | 30 | _ -> 31 | IO.puts("(#{__MODULE__}) Not connected") 32 | end 33 | end 34 | 35 | test "get_pdu_length function", state do 36 | case state.status do 37 | :connected -> 38 | {:ok, pdu} = Snapex7.Client.get_pdu_length(state.pid) 39 | assert pdu == [Requested: 480, Negotiated: 240] 40 | 41 | _ -> 42 | IO.puts("(#{__MODULE__}) Not connected") 43 | end 44 | end 45 | 46 | test "get_connected function", state do 47 | case state.status do 48 | :connected -> 49 | resp = Snapex7.Client.get_connected(state.pid) 50 | assert resp == {:ok, true} 51 | 52 | _ -> 53 | IO.puts("(#{__MODULE__}) Not connected") 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/s7_client_test/plc_control_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlcControlFunTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | {:ok, pid} = Snapex7.Client.start_link() 7 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 8 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 9 | %{pid: pid, status: state} 10 | end 11 | 12 | # functions no supported by PLC s7-1200 13 | test "plc_hot_start function", state do 14 | case state.status do 15 | :connected -> 16 | resp = Snapex7.Client.plc_hot_start(state.pid) 17 | # when PLC is running 18 | assert resp == {:error, %{eiso: nil, es7: :errCliCannotStartPLC, etcp: nil}} 19 | 20 | _ -> 21 | IO.puts("(#{__MODULE__}) Not connected") 22 | end 23 | end 24 | 25 | test "plc_cold_start function", state do 26 | case state.status do 27 | :connected -> 28 | resp = Snapex7.Client.plc_cold_start(state.pid) 29 | # when PLC is running 30 | assert resp == {:error, %{eiso: nil, es7: :errCliCannotStartPLC, etcp: nil}} 31 | 32 | _ -> 33 | IO.puts("(#{__MODULE__}) Not connected") 34 | end 35 | end 36 | 37 | test "plc_stop function", state do 38 | case state.status do 39 | :connected -> 40 | resp = Snapex7.Client.plc_stop(state.pid) 41 | # when PLC is running 42 | assert resp == {:error, %{eiso: nil, es7: :errCliCannotStopPLC, etcp: nil}} 43 | 44 | _ -> 45 | IO.puts("(#{__MODULE__}) Not connected") 46 | end 47 | end 48 | 49 | test "copy_ram_to_rom function", state do 50 | case state.status do 51 | :connected -> 52 | resp = Snapex7.Client.copy_ram_to_rom(state.pid) 53 | # when PLC is running 54 | assert resp == {:error, %{eiso: nil, es7: :errCliCannotCopyRamToRom, etcp: nil}} 55 | 56 | _ -> 57 | IO.puts("(#{__MODULE__}) Not connected") 58 | end 59 | end 60 | 61 | test "compress function", state do 62 | case state.status do 63 | :connected -> 64 | resp = Snapex7.Client.compress(state.pid, 300) 65 | # when PLC is running 66 | assert resp == {:error, %{eiso: nil, es7: :errCliCannotCompress, etcp: nil}} 67 | 68 | _ -> 69 | IO.puts("(#{__MODULE__}) Not connected") 70 | end 71 | end 72 | 73 | test "get_plc_status function", state do 74 | case state.status do 75 | :connected -> 76 | resp = Snapex7.Client.get_plc_status(state.pid) 77 | # when the PLC is running 78 | assert resp == {:ok, :S7CpuStatusRun} 79 | 80 | _ -> 81 | IO.puts("(#{__MODULE__}) Not connected") 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/s7_client_test/security_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SecurityFunTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | {:ok, pid} = Snapex7.Client.start_link() 7 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 8 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 9 | %{pid: pid, status: state} 10 | end 11 | 12 | # functions no supported by PLC s7-1200 13 | test "set_session_password function", state do 14 | case state.status do 15 | :connected -> 16 | resp = Snapex7.Client.set_session_password(state.pid, "holahola") 17 | # when PLC is running 18 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 19 | 20 | _ -> 21 | IO.puts("(#{__MODULE__}) Not connected") 22 | end 23 | end 24 | 25 | # functions no supported by PLC s7-1200 26 | test "clear_session_password function", state do 27 | case state.status do 28 | :connected -> 29 | resp = Snapex7.Client.clear_session_password(state.pid) 30 | # when PLC is running 31 | assert resp == {:error, %{eiso: nil, es7: :errCliFunNotAvailable, etcp: nil}} 32 | 33 | _ -> 34 | IO.puts("(#{__MODULE__}) Not connected") 35 | end 36 | end 37 | 38 | test "get_protection function", state do 39 | case state.status do 40 | :connected -> 41 | resp = Snapex7.Client.get_protection(state.pid) 42 | # when PLC is running 43 | assert resp == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 44 | 45 | _ -> 46 | IO.puts("(#{__MODULE__}) Not connected") 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/s7_client_test/system_info_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SystemInfoFunTest do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | setup do 6 | {:ok, pid} = Snapex7.Client.start_link() 7 | Snapex7.Client.connect_to(pid, ip: "192.168.0.1", rack: 0, slot: 1) 8 | {:ok, state} = :sys.get_state(pid) |> Map.fetch(:state) 9 | %{pid: pid, status: state} 10 | end 11 | 12 | test "read_szl function", state do 13 | case state.status do 14 | :connected -> 15 | resp = Snapex7.Client.read_szl(state.pid, 0x0111, 0x0006) 16 | id = <<0, 6>> 17 | module_num = "6ES7 211-1AE40-0XB0" 18 | tail = <<32, 0, 0, 0, 7, 32, 32>> 19 | assert resp == {:ok, id <> module_num <> tail} 20 | 21 | _ -> 22 | IO.puts("(#{__MODULE__}) Not connected") 23 | end 24 | end 25 | 26 | test "read_szl_list function", state do 27 | case state.status do 28 | :connected -> 29 | resp = Snapex7.Client.read_szl_list(state.pid) 30 | assert resp == {:ok, [0, 17, 273, 3857, 1060, 305]} 31 | 32 | _ -> 33 | IO.puts("(#{__MODULE__}) Not connected") 34 | end 35 | end 36 | 37 | test "get_order_code function", state do 38 | case state.status do 39 | :connected -> 40 | resp = Snapex7.Client.get_order_code(state.pid) 41 | assert resp == {:ok, [Code: "6ES7 211-1AE40-0XB0 ", Version: "4.2.1"]} 42 | 43 | _ -> 44 | IO.puts("(#{__MODULE__}) Not connected") 45 | end 46 | end 47 | 48 | test "get_cpu_info function", state do 49 | case state.status do 50 | :connected -> 51 | resp = Snapex7.Client.get_cpu_info(state.pid) 52 | assert resp == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 53 | 54 | _ -> 55 | IO.puts("(#{__MODULE__}) Not connected") 56 | end 57 | end 58 | 59 | test "get_cp_info function", state do 60 | case state.status do 61 | :connected -> 62 | resp = Snapex7.Client.get_cp_info(state.pid) 63 | assert resp == {:error, %{eiso: nil, es7: :errCliItemNotAvailable, etcp: nil}} 64 | 65 | _ -> 66 | IO.puts("(#{__MODULE__}) Not connected") 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/snapex7_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Snapex7Test do 2 | use ExUnit.Case 3 | doctest Snapex7 4 | 5 | test "greets the world" do 6 | assert Snapex7.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------