├── .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 |

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

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 |
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 |
--------------------------------------------------------------------------------