├── .github └── workflows │ └── main.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── adstool.rs ├── notify.rs ├── timing.rs ├── timing_server.rs └── values_via_handle.rs ├── rust-toolchain └── src ├── client.rs ├── errors.rs ├── file.rs ├── index.rs ├── lib.rs ├── netid.rs ├── notif.rs ├── ports.rs ├── strings.rs ├── symbol.rs ├── test ├── mod.rs ├── test_client.rs ├── test_netid.rs └── test_udp.rs └── udp.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | toolchain: 15 | - 1.63.0 16 | - stable 17 | - nightly 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions-rust-lang/setup-rust-toolchain@v1 21 | with: 22 | components: clippy, rustfmt 23 | toolchain: ${{ matrix.toolchain }} 24 | - run: cargo fmt --check 25 | - run: cargo clippy --all-targets 26 | - run: cargo test --all 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | tarpaulin-report.html 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | chain_width = 80 2 | fn_call_width = 80 3 | fn_args_layout = "Compressed" 4 | max_width = 110 5 | single_line_if_else_max_width = 80 6 | struct_lit_width = 80 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.5.0 -- unreleased 4 | 5 | - Rust 1.63 is now required. 6 | - Fixed-length string types in `ads::strings` are now using const generics 7 | instead of macro-created types. 8 | - Array bounds can be negative, change type in symbol info (#13). 9 | 10 | ## 0.4.2 -- Sep 2022 11 | 12 | - Fix a problem with long comments in symbol info (#8). 13 | 14 | ## 0.4.1 -- Aug 2022 15 | 16 | - Add `Handle::raw` to get the u32 handle ID. 17 | 18 | ## 0.4.0 -- Jun 2022 19 | 20 | - Support asking the AMS router for an open port. This is required when 21 | connecting to a PLC on 127.0.0.1. 22 | - Support for directly reading Rust types from `Device`s and `symbol::Handle`s 23 | using `read_value`/`write_value`. 24 | - Support the "sum-up" requests from `Device`, which can run multiple 25 | read/write/read-write/notif requests in a single ADS request-reply cycle. 26 | - Add the `strings` module with the possibility to create fixed-length string 27 | types corresponding to the PLC's `STRING` and `WSTRING`. 28 | - Add more known ADS states. 29 | - Document the `symbol::Symbol` and `symbol::Type` flags. 30 | - Add an adstool command to list AMS routes on the target. 31 | - Add an adstool subcommand to list type inventory. 32 | 33 | ## 0.3.1 -- May 2022 34 | 35 | - Fix Rust 1.48 compatibility. 36 | 37 | ## 0.3.0 -- May 2022 38 | 39 | - Add `symbol::get_symbol_info()` and related structs. 40 | - Add an adstool option to automatically set a route. 41 | - Add an adstool subcommand to query module licenses. 42 | - Add an adstool command to query the target description. 43 | 44 | ## 0.1.1 -- Nov 2021 45 | 46 | - Add `Client::source()`. 47 | - Add `Client::write_read_exact()`. 48 | - Add `symbol::get_size()` and `symbol::get_location()`. 49 | - Add more known index groups. 50 | - Support system info from TC/RTOS. 51 | - Display ADS errors in hex. 52 | - Many improvements to the adstool example. 53 | 54 | ## 0.1.0 -- Nov 2021 55 | 56 | - Initial release. 57 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ads" 3 | version = "0.4.4" 4 | edition = "2018" 5 | authors = ["Georg Brandl "] 6 | license = "MIT/Apache-2.0" 7 | description = "Client for the Beckhoff Automation Device Specification protocol for PLCs" 8 | repository = "https://github.com/birkenfeld/ads-rs" 9 | keywords = ["Beckhoff", "ADS", "automation", "device", "PLC"] 10 | rust-version = "1.63" 11 | 12 | [dependencies] 13 | byteorder = "1.5.0" 14 | crossbeam-channel = "0.5.13" 15 | itertools = "0.13.0" 16 | thiserror = "2.0" 17 | zerocopy = { version = "0.8.9", features = ["derive"] } 18 | 19 | [dev-dependencies] 20 | once_cell = "~1.20.2" 21 | parse_int = "0.6.0" 22 | quick-xml = "0.37.0" 23 | regex = "<1.10.0" 24 | chrono = "0.4.38" 25 | clap = { version = "3.2.25", features = ["derive"] } 26 | strum = { version = "0.26.3", features = ["derive"] } 27 | 28 | # MSRV 29 | textwrap = "=0.16.0" 30 | unicode-width = "=0.1.13" 31 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 The Rust Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `ads` 2 | 3 | [![Build status](https://github.com/birkenfeld/ads-rs/actions/workflows/main.yml/badge.svg)](https://github.com/birkenfeld/ads-rs) 4 | [![crates.io](https://img.shields.io/crates/v/ads.svg)](https://crates.io/crates/ads) 5 | [![docs.rs](https://img.shields.io/docsrs/ads)](https://docs.rs/ads) 6 | 7 | This crate allows to connect to [Beckhoff](https://beckhoff.com) TwinCAT devices 8 | and other servers speaking the ADS (Automation Device Specification) protocol. 9 | 10 | ## Installation 11 | 12 | Use with Cargo as usual, no system dependencies are required. 13 | 14 | ```toml 15 | [dependencies] 16 | ads = "0.4" 17 | ``` 18 | 19 | ### Rust version 20 | 21 | Minimum supported Rust version is 1.63.0. 22 | 23 | ## Usage 24 | 25 | A simple example: 26 | 27 | ```rust 28 | fn main() -> ads::Result<()> { 29 | // Open a connection to an ADS device identified by hostname/IP and port. 30 | // For TwinCAT devices, a route must be set to allow the client to connect. 31 | // The source AMS address is automatically generated from the local IP, 32 | // but can be explicitly specified as the third argument. 33 | let client = ads::Client::new(("plchost", ads::PORT), ads::Timeouts::none(), 34 | ads::Source::Auto)?; 35 | // On Windows, when connecting to a TwinCAT instance running on the same 36 | // machine, use the following to connect: 37 | let client = ads::Client::new(("127.0.0.1", ads::PORT), ads::Timeouts::none(), 38 | ads::Source::Request)?; 39 | 40 | // Specify the target ADS device to talk to, by NetID and AMS port. 41 | // Port 851 usually refers to the first PLC instance. 42 | let device = client.device(ads::AmsAddr::new([5, 32, 116, 5, 1, 1].into(), 851)); 43 | 44 | // Ensure that the PLC instance is running. 45 | assert!(device.get_state()?.0 == ads::AdsState::Run); 46 | 47 | // Request a handle to a named symbol in the PLC instance. 48 | let handle = Handle::new(device, "MY_SYMBOL")?; 49 | 50 | // Read data in form of an u32 from the handle. 51 | let value: u32 = handle.read_value()?; 52 | println!("MY_SYMBOL value is {}", value); 53 | 54 | // Connection will be closed when the client is dropped. 55 | Ok(()) 56 | } 57 | ``` 58 | 59 | ## Features 60 | 61 | All ADS requests are implemented. 62 | 63 | Further features include support for receiving notifications from a channel, 64 | file access via ADS, and communication via UDP to identify an ADS system and set 65 | routes automatically. 66 | 67 | ## Examples 68 | 69 | A utility called `adstool` is found under `examples/`, very similar to the one 70 | provided by [the C++ library](https://github.com/Beckhoff/ADS). 71 | -------------------------------------------------------------------------------- /examples/adstool.rs: -------------------------------------------------------------------------------- 1 | //! Reproduces the functionality of "adstool" from the Beckhoff ADS C++ library. 2 | 3 | use std::convert::TryInto; 4 | use std::io::{stdin, stdout, Read, Write}; 5 | use std::str::FromStr; 6 | 7 | use byteorder::{ByteOrder, WriteBytesExt, BE, LE}; 8 | use chrono::{DateTime, Utc}; 9 | use clap::{AppSettings, ArgGroup, Parser, Subcommand}; 10 | use itertools::Itertools; 11 | use parse_int::parse; 12 | use quick_xml::{events::Event, name::QName}; 13 | use strum::EnumString; 14 | 15 | #[derive(Parser, Debug)] 16 | #[clap(disable_help_subcommand = true)] 17 | #[clap(global_setting = AppSettings::DeriveDisplayOrder)] 18 | /// A utility for managing ADS servers. 19 | struct Args { 20 | #[clap(subcommand)] 21 | cmd: Cmd, 22 | /// If true, automatically try to set a route with the default password 23 | /// before connecting. 24 | #[clap(short, long)] 25 | autoroute: bool, 26 | /// Target for the command. 27 | /// 28 | /// This can be `hostname[:port]` or include an AMS address using 29 | /// `hostname[:port]/netid[:amsport]`, for example: 30 | /// 31 | /// localhost/5.23.91.23.1.1:851 32 | /// 33 | /// The IP port defaults to 0xBF02 (TCP) and 0xBF03 (UDP). 34 | /// 35 | /// An AMS address is required for all subcommands except `addroute` and 36 | /// `info`. If it's not present, it is queried via UDP from the given 37 | /// hostname, but only the connected router (normally `.1.1`) can be reached 38 | /// in that way. 39 | /// 40 | /// The default AMS port depends on the command: `file` and `state` default 41 | /// to the system service, `license` to the license service, while `raw and 42 | /// `var` default to the first PLC instance (port 851). 43 | target: Target, 44 | } 45 | 46 | #[derive(Subcommand, Debug)] 47 | enum Cmd { 48 | /// Query basic information about the system over UDP. 49 | Info, 50 | /// Query extended information about the system over ADS. 51 | TargetDesc, 52 | #[clap(subcommand)] 53 | Route(RouteAction), 54 | #[clap(subcommand)] 55 | File(FileAction), 56 | #[clap(subcommand)] 57 | License(LicenseAction), 58 | State(StateArgs), 59 | #[clap(subcommand)] 60 | Raw(RawAction), 61 | #[clap(subcommand)] 62 | Var(VarAction), 63 | Exec(ExecArgs), 64 | } 65 | 66 | #[derive(Subcommand, Debug)] 67 | /// Manipulate ADS routes. 68 | enum RouteAction { 69 | /// Add an ADS route to the remote TwinCAT system. 70 | Add(AddRouteArgs), 71 | /// Query and display the list of ADS routes on the system. 72 | List, 73 | } 74 | 75 | #[derive(Parser, Debug)] 76 | struct AddRouteArgs { 77 | /// hostname or IP address of the route's destionation 78 | addr: String, 79 | 80 | /// AMS NetId of the route's destination 81 | netid: ads::AmsNetId, 82 | 83 | /// name of the new route (defaults to `addr`) 84 | #[clap(long)] 85 | routename: Option, 86 | 87 | /// password for logging into the system (defaults to `1`) 88 | #[clap(long, default_value = "1")] 89 | password: String, 90 | 91 | /// username for logging into the system (defaults to `Administrator`) 92 | #[clap(long, default_value = "Administrator")] 93 | username: String, 94 | 95 | /// mark route as temporary? 96 | #[clap(long)] 97 | temporary: bool, 98 | } 99 | 100 | #[derive(Subcommand, Debug)] 101 | /// Execute operations on files on the TwinCAT system. 102 | enum FileAction { 103 | /// List remote files in the given directory. 104 | List { 105 | /// the directory path 106 | path: String, 107 | }, 108 | /// Read a remote file and write its contents to stdout. 109 | Read { 110 | /// the file path 111 | path: String, 112 | }, 113 | /// Write a remote file with content from stdin. 114 | Write { 115 | /// the file path 116 | path: String, 117 | /// whether to append to the file when writing 118 | #[clap(long)] 119 | append: bool, 120 | }, 121 | /// Delete a remote file. 122 | Delete { 123 | /// the file path 124 | path: String, 125 | }, 126 | } 127 | 128 | #[derive(Subcommand, Debug)] 129 | /// Query different license ids. 130 | enum LicenseAction { 131 | /// Get the platform ID 132 | Platformid, 133 | /// Get the system ID 134 | Systemid, 135 | /// Get the volume number 136 | Volumeno, 137 | /// Get the individual module license GUIDs and their activation status 138 | Modules, 139 | } 140 | 141 | #[derive(Parser, Debug)] 142 | /// Read or write the ADS state of the device. 143 | struct StateArgs { 144 | /// if given, the target state 145 | /// 146 | /// Note that state transitions are not always straightforward; 147 | /// for example, you need to set `Reset` to go from `Config` to `Run`, 148 | /// and `Reconfig` to go from `Run` to `Config`. 149 | target_state: Option, 150 | } 151 | 152 | #[derive(Subcommand, Debug)] 153 | /// Raw read or write access for an indexgroup. 154 | enum RawAction { 155 | /// Read some data from an index. Specify either --length (to print raw 156 | /// bytes) or --type (to convert to a data type and print that). 157 | #[clap(group = ArgGroup::with_name("spec").required(true))] 158 | Read { 159 | /// the index group, can be 0xABCD 160 | #[clap(parse(try_from_str = parse))] 161 | index_group: u32, 162 | /// the index offset, can be 0xABCD 163 | #[clap(parse(try_from_str = parse))] 164 | index_offset: u32, 165 | /// the length, can be 0xABCD 166 | #[clap(long, parse(try_from_str = parse), group = "spec")] 167 | length: Option, 168 | /// the data type 169 | #[clap(long, group = "spec")] 170 | r#type: Option, 171 | /// whether to print integers as hex, or raw data as hexdump 172 | #[clap(long)] 173 | hex: bool, 174 | }, 175 | /// Write some data to an index. Data is read from stdin. 176 | Write { 177 | /// the index group, can be 0xABCD 178 | #[clap(parse(try_from_str = parse))] 179 | index_group: u32, 180 | /// the index offset, can be 0xABCD 181 | #[clap(parse(try_from_str = parse))] 182 | index_offset: u32, 183 | }, 184 | /// Write some data (read from stdin), then read data from an index. 185 | #[clap(group = ArgGroup::with_name("spec").required(true))] 186 | WriteRead { 187 | /// the index group, can be 0xABCD 188 | #[clap(parse(try_from_str = parse))] 189 | index_group: u32, 190 | /// the index offset, can be 0xABCD 191 | #[clap(parse(try_from_str = parse))] 192 | index_offset: u32, 193 | /// the length to read, can be 0xABCD 194 | #[clap(long, parse(try_from_str = parse), group = "spec")] 195 | length: Option, 196 | /// the data type to interpret the read data as 197 | #[clap(long, group = "spec")] 198 | r#type: Option, 199 | /// whether to print integers as hex, or raw data as hexdump 200 | #[clap(long)] 201 | hex: bool, 202 | }, 203 | } 204 | 205 | #[derive(Subcommand, Debug)] 206 | /// Variable read or write access. 207 | enum VarAction { 208 | /// List variables together with their types, sizes and offsets. 209 | List { 210 | /// a filter for the returned symbol names 211 | filter: Option, 212 | }, 213 | /// List type definitions. 214 | ListTypes { 215 | /// a filter for the returned symbol names 216 | filter: Option, 217 | }, 218 | /// Read a variable by name. 219 | #[clap(group = ArgGroup::with_name("spec"))] 220 | Read { 221 | /// the variable name 222 | name: String, 223 | /// the variable type 224 | #[clap(long, group = "spec")] 225 | r#type: Option, 226 | /// the length to read, can be 0xABCD 227 | #[clap(long, parse(try_from_str = parse), group = "spec")] 228 | length: Option, 229 | /// whether to print integers as hex 230 | #[clap(long)] 231 | hex: bool, 232 | }, 233 | /// Write a variable by name. If --type is given, the new value 234 | /// is converted from the command line argument. If not, the new 235 | /// value is read as raw data from stdin. 236 | Write { 237 | /// the variable name 238 | name: String, 239 | /// the new value, if given, to write 240 | #[clap(requires = "type")] 241 | value: Option, 242 | /// the variable type 243 | #[clap(long)] 244 | r#type: Option, 245 | }, 246 | } 247 | 248 | #[derive(Parser, Debug)] 249 | /// Execute a system command on the target. 250 | struct ExecArgs { 251 | /// the executable with path 252 | program: String, 253 | /// the working directory (defaults to the executable's) 254 | #[clap(long)] 255 | workingdir: Option, 256 | /// arguments for the executable 257 | args: Vec, 258 | } 259 | 260 | #[derive(Clone, Copy, Debug, EnumString)] 261 | #[strum(serialize_all = "UPPERCASE")] 262 | // TODO put the type mapping stuff into the lib? 263 | enum VarType { 264 | Bool, 265 | Byte, 266 | Sint, 267 | Word, 268 | Int, 269 | Dword, 270 | Dint, 271 | Lword, 272 | Lint, 273 | String, 274 | Real, 275 | Lreal, 276 | } 277 | 278 | impl VarType { 279 | fn size(&self) -> usize { 280 | match self { 281 | VarType::Bool | VarType::Byte | VarType::Sint => 1, 282 | VarType::Word | VarType::Int => 2, 283 | VarType::Real | VarType::Dword | VarType::Dint => 4, 284 | VarType::Lreal | VarType::Lword | VarType::Lint => 8, 285 | VarType::String => 255, 286 | } 287 | } 288 | } 289 | 290 | /// Target spec: IP plus optional AMS adress. 291 | #[derive(Debug)] 292 | struct Target { 293 | host: String, 294 | port: Option, 295 | netid: Option, 296 | amsport: Option, 297 | } 298 | 299 | const RX: &str = "^(?P[^:/]+)(:(?P\\d+))?(/(?P[0-9.]+)?(:(?P\\d+))?)?$"; 300 | 301 | impl FromStr for Target { 302 | type Err = &'static str; 303 | 304 | fn from_str(s: &str) -> Result { 305 | let rx = regex::Regex::new(RX).expect("valid regex"); 306 | match rx.captures(s) { 307 | None => Err("target format is host[:port][/netid[:amsport]]"), 308 | Some(cap) => Ok(Target { 309 | host: cap["host"].into(), 310 | port: cap.name("port").map(|p| p.as_str().parse().expect("from rx")), 311 | netid: cap.name("netid").map(|p| p.as_str().parse()).transpose()?, 312 | amsport: cap.name("amsport").map(|p| p.as_str().parse().expect("from rx")), 313 | }), 314 | } 315 | } 316 | } 317 | 318 | fn main() { 319 | let args = Args::from_args(); 320 | 321 | if let Err(e) = main_inner(args) { 322 | eprintln!("Error: {}", e); 323 | std::process::exit(1); 324 | } 325 | } 326 | 327 | #[derive(thiserror::Error, Debug)] 328 | enum Error { 329 | #[error(transparent)] 330 | Lib(#[from] ads::Error), 331 | #[error(transparent)] 332 | Io(#[from] std::io::Error), 333 | #[error("{0}")] 334 | Str(String), 335 | } 336 | 337 | fn connect( 338 | target: Target, autoroute: bool, defport: ads::AmsPort, 339 | ) -> ads::Result<(ads::Client, ads::AmsAddr)> { 340 | let target_netid = match target.netid { 341 | Some(netid) => netid, 342 | None => ads::udp::get_netid((target.host.as_str(), ads::UDP_PORT))?, 343 | }; 344 | let tcp_addr = (target.host.as_str(), target.port.unwrap_or(ads::PORT)); 345 | let amsport = target.amsport.unwrap_or(defport); 346 | let amsaddr = ads::AmsAddr::new(target_netid, amsport); 347 | let source = if matches!(target.host.as_str(), "127.0.0.1" | "localhost") { 348 | ads::Source::Request 349 | } else { 350 | ads::Source::Auto 351 | }; 352 | let client = ads::Client::new(tcp_addr, ads::Timeouts::none(), source)?; 353 | if autoroute { 354 | if let Err(ads::Error::Io(..)) = client.device(amsaddr).get_info() { 355 | println!("Device info failed, trying to set a route..."); 356 | let ip = client.source().netid().0; 357 | let ip = format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]); 358 | ads::udp::add_route( 359 | (target.host.as_str(), ads::UDP_PORT), 360 | client.source().netid(), 361 | &ip, 362 | None, 363 | None, 364 | None, 365 | true, 366 | )?; 367 | return connect(target, false, defport); 368 | } 369 | } 370 | Ok((client, amsaddr)) 371 | } 372 | 373 | fn main_inner(args: Args) -> Result<(), Error> { 374 | let udp_addr = (args.target.host.as_str(), args.target.port.unwrap_or(ads::UDP_PORT)); 375 | match args.cmd { 376 | Cmd::Route(RouteAction::Add(subargs)) => { 377 | ads::udp::add_route( 378 | udp_addr, 379 | subargs.netid, 380 | &subargs.addr, 381 | subargs.routename.as_deref(), 382 | Some(&subargs.username), 383 | Some(&subargs.password), 384 | subargs.temporary, 385 | )?; 386 | println!("Success."); 387 | } 388 | Cmd::Route(RouteAction::List) => { 389 | let (client, amsaddr) = connect(args.target, args.autoroute, ads::ports::SYSTEM_SERVICE)?; 390 | let dev = client.device(amsaddr); 391 | let mut routeinfo = [0; 2048]; 392 | println!("{:-20} {:-22} {:-18} Flags", "Name", "NetID", "Host/IP"); 393 | for subindex in 0.. { 394 | match dev.read(ads::index::ROUTE_LIST, subindex, &mut routeinfo) { 395 | Err(ads::Error::Ads(_, _, 0x716)) => break, 396 | Err(other) => return Err(Error::Lib(other)), 397 | Ok(n) if n >= 48 => { 398 | let netid = ads::AmsNetId::from_slice(&routeinfo[..6]).unwrap(); 399 | let flags = LE::read_u32(&routeinfo[8..]); 400 | let _timeout = LE::read_u32(&routeinfo[12..]); 401 | let _max_frag = LE::read_u32(&routeinfo[16..]); 402 | let hostlen = LE::read_u32(&routeinfo[32..]) as usize; 403 | let namelen = LE::read_u32(&routeinfo[36..]) as usize; 404 | let host = String::from_utf8_lossy(&routeinfo[44..][..hostlen - 1]); 405 | let name = String::from_utf8_lossy(&routeinfo[44 + hostlen..][..namelen - 1]); 406 | print!("{:-20} {:-22} {:-18}", name, netid, host); 407 | if flags & 0x01 != 0 { 408 | print!(" temporary"); 409 | } 410 | if flags & 0x80 != 0 { 411 | print!(" unidirectional"); 412 | } 413 | if flags & 0x100 != 0 { 414 | print!(" virtual/nat"); 415 | } 416 | println!(); 417 | } 418 | _ => println!("Route entry {} too short", subindex), 419 | } 420 | } 421 | } 422 | Cmd::Info => { 423 | let info = ads::udp::get_info(udp_addr)?; 424 | println!("NetID: {}", info.netid); 425 | println!("Hostname: {}", info.hostname); 426 | println!( 427 | "TwinCAT version: {}.{}.{}", 428 | info.twincat_version.0, info.twincat_version.1, info.twincat_version.2 429 | ); 430 | println!( 431 | "OS version: {} {}.{}.{} {}", 432 | info.os_version.0, info.os_version.1, info.os_version.2, info.os_version.3, info.os_version.4 433 | ); 434 | if !info.fingerprint.is_empty() { 435 | println!("Fingerprint: {}", info.fingerprint); 436 | } 437 | } 438 | Cmd::TargetDesc => { 439 | let (client, amsaddr) = connect(args.target, args.autoroute, ads::ports::SYSTEM_SERVICE)?; 440 | let dev = client.device(amsaddr); 441 | let mut xml = [0; 2048]; 442 | dev.read(ads::index::TARGET_DESC, 1, &mut xml)?; 443 | let mut rdr = quick_xml::Reader::from_reader(&xml[..]); 444 | rdr.config_mut().trim_text(true); 445 | let mut stack = Vec::new(); 446 | loop { 447 | match rdr.read_event() { 448 | Ok(Event::Start(el)) => { 449 | if el.name() != QName(b"TcTargetDesc") { 450 | stack.push(String::from_utf8_lossy(el.name().0).to_string()); 451 | } 452 | } 453 | Ok(Event::End(_)) => { 454 | let _ = stack.pop(); 455 | } 456 | Ok(Event::Text(t)) => { 457 | if !stack.is_empty() { 458 | println!("{}: {}", stack.iter().format("."), String::from_utf8_lossy(&t)); 459 | } 460 | } 461 | Ok(Event::Eof) => break, 462 | Err(e) => return Err(Error::Str(format!("error parsing target desc XML: {}", e))), 463 | _ => (), 464 | } 465 | } 466 | println!(); 467 | let n = dev.read(ads::index::TARGET_DESC, 4, &mut xml)?; 468 | println!("Platform: {}", String::from_utf8_lossy(&xml[..n - 1])); 469 | let n = dev.read(ads::index::TARGET_DESC, 7, &mut xml)?; 470 | println!("Project name: {}", String::from_utf8_lossy(&xml[..n - 1])); 471 | } 472 | Cmd::File(subargs) => { 473 | use ads::file; 474 | let (client, amsaddr) = connect(args.target, args.autoroute, ads::ports::SYSTEM_SERVICE)?; 475 | let dev = client.device(amsaddr); 476 | match subargs { 477 | FileAction::List { path } => { 478 | let entries = file::listdir(dev, path)?; 479 | for (name, attr, size) in entries { 480 | println!( 481 | "{} {:8} {}", 482 | if attr & file::DIRECTORY != 0 { "D" } else { " " }, 483 | size, 484 | String::from_utf8_lossy(&name) 485 | ); 486 | } 487 | } 488 | FileAction::Read { path } => { 489 | let mut file = 490 | file::File::open(dev, &path, file::READ | file::BINARY | file::ENSURE_DIR)?; 491 | std::io::copy(&mut file, &mut stdout())?; 492 | } 493 | FileAction::Write { path, append } => { 494 | let flag = if append { ads::file::APPEND } else { ads::file::WRITE }; 495 | let mut file = 496 | file::File::open(dev, &path, flag | file::BINARY | file::PLUS | file::ENSURE_DIR)?; 497 | std::io::copy(&mut stdin(), &mut file)?; 498 | } 499 | FileAction::Delete { path } => { 500 | file::File::delete(dev, path, file::ENABLE_DIR)?; 501 | } 502 | } 503 | } 504 | Cmd::State(subargs) => { 505 | let (client, amsaddr) = connect(args.target, args.autoroute, ads::ports::SYSTEM_SERVICE)?; 506 | let dev = client.device(amsaddr); 507 | let info = dev.get_info()?; 508 | println!("Device: {} {}.{}.{}", info.name, info.major, info.minor, info.version); 509 | let (state, dev_state) = dev.get_state()?; 510 | println!("Current state: {:?}", state); 511 | if let Some(newstate) = subargs.target_state { 512 | println!("Set new state: {:?}", newstate); 513 | dev.write_control(newstate, dev_state)?; 514 | } 515 | } 516 | Cmd::License(object) => { 517 | // Connect to the selected target, defaulting to the license server. 518 | let (client, amsaddr) = connect(args.target, args.autoroute, ads::ports::LICENSE_SERVER)?; 519 | let dev = client.device(amsaddr); 520 | match object { 521 | LicenseAction::Platformid => { 522 | let mut id = [0; 2]; 523 | dev.read_exact(ads::index::LICENSE, 2, &mut id)?; 524 | println!("{}", u16::from_le_bytes(id)); 525 | } 526 | LicenseAction::Systemid => { 527 | let mut id = [0; 16]; 528 | dev.read_exact(ads::index::LICENSE, 1, &mut id)?; 529 | println!("{}", format_guid(&id)); 530 | } 531 | LicenseAction::Volumeno => { 532 | let mut no = [0; 4]; 533 | dev.read_exact(ads::index::LICENSE, 5, &mut no)?; 534 | println!("{}", u32::from_le_bytes(no)); 535 | } 536 | LicenseAction::Modules => { 537 | // Read the number of modules. 538 | let mut count = [0; 4]; 539 | dev.read_exact(ads::index::LICENSE_MODULES, 0, &mut count)?; 540 | let nmodules = u32::from_le_bytes(count) as usize; 541 | 542 | // Read the data (0x30 bytes per module). 543 | let mut data = vec![0; 0x30 * nmodules]; 544 | dev.read_exact(ads::index::LICENSE_MODULES, 0, &mut data)?; 545 | 546 | // Print the data. 547 | for i in 0..nmodules { 548 | let guid = &data[0x30 * i..][..0x10]; 549 | let expires = LE::read_i64(&data[0x30 * i + 0x10..]); 550 | let exp_time = convert_filetime(expires); 551 | let inst_total = LE::read_u32(&data[0x30 * i + 0x18..]); 552 | let inst_used = LE::read_u32(&data[0x30 * i + 0x1c..]); 553 | 554 | println!("ID: {}", format_guid(guid)); 555 | if let Some(exp) = exp_time { 556 | println!(" Expires: {}", exp); 557 | } 558 | if inst_total != 0 { 559 | println!(" Instances used: {}/{}", inst_used, inst_total); 560 | } 561 | } 562 | } 563 | } 564 | } 565 | Cmd::Raw(subargs) => { 566 | // Connect to the selected target, defaulting to the first PLC instance. 567 | let (client, amsaddr) = connect(args.target, args.autoroute, ads::ports::TC3_PLC_SYSTEM1)?; 568 | let dev = client.device(amsaddr); 569 | 570 | match subargs { 571 | RawAction::Read { index_group, index_offset, length, r#type, hex } => { 572 | if let Some(length) = length { 573 | let mut read_data = vec![0; length]; 574 | let nread = dev.read(index_group, index_offset, &mut read_data)?; 575 | if hex { 576 | hexdump(&read_data[..nread]); 577 | } else { 578 | stdout().write_all(&read_data[..nread])?; 579 | } 580 | } else if let Some(typ) = r#type { 581 | let mut read_data = vec![0; typ.size()]; 582 | dev.read_exact(index_group, index_offset, &mut read_data)?; 583 | print_read_value(typ, &read_data, hex); 584 | } 585 | } 586 | RawAction::Write { index_group, index_offset } => { 587 | let mut write_data = Vec::new(); 588 | stdin().read_to_end(&mut write_data)?; 589 | dev.write(index_group, index_offset, &write_data)?; 590 | } 591 | RawAction::WriteRead { index_group, index_offset, length, r#type, hex } => { 592 | let mut write_data = Vec::new(); 593 | stdin().read_to_end(&mut write_data)?; 594 | if let Some(length) = length { 595 | let mut read_data = vec![0; length]; 596 | let nread = dev.write_read(index_group, index_offset, &write_data, &mut read_data)?; 597 | if hex { 598 | hexdump(&read_data[..nread]); 599 | } else { 600 | stdout().write_all(&read_data[..nread])?; 601 | } 602 | } else if let Some(typ) = r#type { 603 | let mut read_data = vec![0; typ.size()]; 604 | dev.write_read_exact(index_group, index_offset, &write_data, &mut read_data)?; 605 | print_read_value(typ, &read_data, hex); 606 | } 607 | } 608 | } 609 | } 610 | Cmd::Var(subargs) => { 611 | // Connect to the selected target, defaulting to the first PLC instance. 612 | let (client, amsaddr) = connect(args.target, args.autoroute, ads::ports::TC3_PLC_SYSTEM1)?; 613 | let dev = client.device(amsaddr); 614 | 615 | fn print_fields(type_map: &ads::symbol::TypeMap, base_offset: u32, typ: &str, level: usize) { 616 | for field in &type_map[typ].fields { 617 | if let Some(offset) = field.offset { 618 | let indent = (0..2 * level).map(|_| ' ').collect::(); 619 | println!( 620 | " {:6x} ({:6x}) {}.{:5$} {}", 621 | base_offset + offset, 622 | field.size, 623 | indent, 624 | field.name, 625 | field.typ, 626 | 39 - 2 * level 627 | ); 628 | print_fields(type_map, base_offset + offset, &field.typ, level + 1); 629 | } 630 | } 631 | } 632 | 633 | match subargs { 634 | VarAction::List { filter } => { 635 | let (symbols, type_map) = ads::symbol::get_symbol_info(dev)?; 636 | let filter = filter.unwrap_or_default().to_lowercase(); 637 | for sym in symbols { 638 | if sym.name.to_lowercase().contains(&filter) { 639 | println!( 640 | "{:4x}:{:6x} ({:6x}) {:40} {}", 641 | sym.ix_group, sym.ix_offset, sym.size, sym.name, sym.typ 642 | ); 643 | print_fields(&type_map, sym.ix_offset, &sym.typ, 1); 644 | } 645 | } 646 | } 647 | VarAction::ListTypes { filter } => { 648 | let (_symbols, type_map) = ads::symbol::get_symbol_info(dev)?; 649 | let filter = filter.unwrap_or_default().to_lowercase(); 650 | for (name, ty) in &type_map { 651 | if name.to_lowercase().contains(&filter) { 652 | println!("** ({:6x}) {:40}", ty.size, name); 653 | print_fields(&type_map, 0, name, 1); 654 | } 655 | } 656 | } 657 | VarAction::Read { name, r#type, length, hex } => { 658 | let handle = ads::symbol::Handle::new(dev, &name)?; 659 | if let Some(typ) = r#type { 660 | let mut read_data = vec![0; typ.size()]; 661 | handle.read(&mut read_data)?; 662 | print_read_value(typ, &read_data, hex); 663 | } else { 664 | let length = match length { 665 | Some(l) => l, 666 | None => ads::symbol::get_size(dev, &name)?, 667 | }; 668 | let mut read_data = vec![0; length]; 669 | handle.read(&mut read_data)?; 670 | if hex { 671 | hexdump(&read_data); 672 | } else { 673 | stdout().write_all(&read_data)?; 674 | } 675 | } 676 | } 677 | VarAction::Write { name, value, r#type } => { 678 | let handle = ads::symbol::Handle::new(dev, &name)?; 679 | if let Some(typ) = r#type { 680 | let write_data = get_write_value(typ, value.unwrap())?; 681 | handle.write(&write_data)?; 682 | } else { 683 | let mut write_data = Vec::new(); 684 | stdin().read_to_end(&mut write_data)?; 685 | handle.write(&write_data)?; 686 | } 687 | } 688 | } 689 | } 690 | Cmd::Exec(subargs) => { 691 | let (client, amsaddr) = connect(args.target, args.autoroute, ads::ports::SYSTEM_SERVICE)?; 692 | let dev = client.device(amsaddr); 693 | 694 | let workingdir = subargs.workingdir.as_deref().unwrap_or(""); 695 | let args = subargs.args.into_iter().join(" "); 696 | 697 | let mut data = Vec::new(); 698 | data.write_u32::(subargs.program.len() as u32).unwrap(); 699 | data.write_u32::(workingdir.len() as u32).unwrap(); 700 | data.write_u32::(args.len() as u32).unwrap(); 701 | data.write_all(subargs.program.as_bytes()).unwrap(); 702 | data.write_all(&[0]).unwrap(); 703 | data.write_all(workingdir.as_bytes()).unwrap(); 704 | data.write_all(&[0]).unwrap(); 705 | data.write_all(args.as_bytes()).unwrap(); 706 | data.write_all(&[0]).unwrap(); 707 | 708 | dev.write(ads::index::EXECUTE, 0, &data)?; 709 | } 710 | } 711 | Ok(()) 712 | } 713 | 714 | fn get_write_value(typ: VarType, value: String) -> Result, Error> { 715 | let err = |_| Error::Str("expected integer".into()); 716 | let float_err = |_| Error::Str("expected floating point number".into()); 717 | Ok(match typ { 718 | VarType::String => value.into_bytes(), 719 | VarType::Bool => { 720 | if value == "TRUE" { 721 | vec![1] 722 | } else if value == "FALSE" { 723 | vec![0] 724 | } else { 725 | return Err(Error::Str("invalid BOOL value".into())); 726 | } 727 | } 728 | VarType::Byte => parse::(&value).map_err(err)?.to_le_bytes().into(), 729 | VarType::Sint => parse::(&value).map_err(err)?.to_le_bytes().into(), 730 | VarType::Word => parse::(&value).map_err(err)?.to_le_bytes().into(), 731 | VarType::Int => parse::(&value).map_err(err)?.to_le_bytes().into(), 732 | VarType::Dword => parse::(&value).map_err(err)?.to_le_bytes().into(), 733 | VarType::Dint => parse::(&value).map_err(err)?.to_le_bytes().into(), 734 | VarType::Lword => parse::(&value).map_err(err)?.to_le_bytes().into(), 735 | VarType::Lint => parse::(&value).map_err(err)?.to_le_bytes().into(), 736 | VarType::Real => value.parse::().map_err(float_err)?.to_le_bytes().into(), 737 | VarType::Lreal => value.parse::().map_err(float_err)?.to_le_bytes().into(), 738 | }) 739 | } 740 | 741 | fn print_read_value(typ: VarType, buf: &[u8], hex: bool) { 742 | let value = match typ { 743 | VarType::String => { 744 | println!("{}", String::from_utf8_lossy(buf).split('\0').next().expect("item")); 745 | return; 746 | } 747 | VarType::Bool => { 748 | match buf[0] { 749 | 0 => println!("FALSE"), 750 | 1 => println!("TRUE"), 751 | n => println!("non-bool ({})", n), 752 | } 753 | return; 754 | } 755 | VarType::Real => { 756 | let v = f32::from_le_bytes(buf[..4].try_into().expect("size")); 757 | println!("{}", v); 758 | return; 759 | } 760 | VarType::Lreal => { 761 | let v = f64::from_le_bytes(buf[..8].try_into().expect("size")); 762 | println!("{}", v); 763 | return; 764 | } 765 | VarType::Byte => buf[0] as i128, 766 | VarType::Sint => buf[0] as i8 as i128, 767 | VarType::Word => u16::from_le_bytes(buf[..2].try_into().expect("size")) as i128, 768 | VarType::Int => i16::from_le_bytes(buf[..2].try_into().expect("size")) as i128, 769 | VarType::Dword => u32::from_le_bytes(buf[..4].try_into().expect("size")) as i128, 770 | VarType::Dint => i32::from_le_bytes(buf[..4].try_into().expect("size")) as i128, 771 | VarType::Lword => u64::from_le_bytes(buf[..8].try_into().expect("size")) as i128, 772 | VarType::Lint => i64::from_le_bytes(buf[..8].try_into().expect("size")) as i128, 773 | }; 774 | // Only reaches here for integer types 775 | if hex { 776 | println!("{:#x}", value); 777 | } else { 778 | println!("{}", value); 779 | } 780 | } 781 | 782 | /// If the char is not printable, replace it by a dot. 783 | fn printable(ch: &u8) -> char { 784 | if *ch >= 32 && *ch <= 127 { 785 | *ch as char 786 | } else { 787 | '.' 788 | } 789 | } 790 | 791 | /// Print a hexdump of a byte slice in the usual format. 792 | fn hexdump(mut data: &[u8]) { 793 | let mut addr = 0; 794 | while !data.is_empty() { 795 | let (line, rest) = data.split_at(data.len().min(16)); 796 | println!( 797 | "{:#08x}: {:02x}{} | {}", 798 | addr, 799 | line.iter().format(" "), 800 | (0..16 - line.len()).map(|_| " ").format(""), 801 | line.iter().map(printable).format("") 802 | ); 803 | addr += 16; 804 | data = rest; 805 | } 806 | println!(); 807 | } 808 | 809 | /// Difference between FILETIME and Unix offsets. 810 | const EPOCH_OFFSET: i64 = 11644473600; 811 | 812 | /// Convert Windows FILETIME to DateTime 813 | fn convert_filetime(ft: i64) -> Option> { 814 | if ft == 0 { 815 | return None; 816 | } 817 | let unix_ts = ft / 10_000_000 - EPOCH_OFFSET; 818 | DateTime::from_timestamp(unix_ts, 0) 819 | } 820 | 821 | /// Format a GUID. 822 | fn format_guid(guid: &[u8]) -> String { 823 | format!( 824 | "{:08X}-{:04X}-{:04X}-{:04X}-{:012X}", 825 | LE::read_u32(guid), 826 | LE::read_u16(&guid[4..]), 827 | LE::read_u16(&guid[6..]), 828 | BE::read_u16(&guid[8..]), 829 | BE::read_u48(&guid[10..]) 830 | ) 831 | } 832 | -------------------------------------------------------------------------------- /examples/notify.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use ads::notif::{Attributes, TransmissionMode}; 4 | use ads::{Client, Source, Timeouts}; 5 | 6 | fn main() { 7 | let client = Client::new(("127.0.0.1", ads::PORT), Timeouts::none(), Source::Request).unwrap(); 8 | let recv_notify = client.get_notification_channel(); 9 | std::thread::spawn(move || { 10 | for msg in recv_notify.iter() { 11 | for sample in msg.samples() { 12 | println!("notify: {:?}", sample); 13 | } 14 | } 15 | }); 16 | let dev = client.device(ads::AmsAddr::new([5, 62, 215, 36, 1, 1].into(), 851)); 17 | let h1 = dev 18 | .add_notification( 19 | 0x4020, 20 | 4, 21 | &Attributes::new( 22 | 4, 23 | TransmissionMode::ServerCycle, 24 | Duration::from_secs(1), 25 | Duration::from_secs(1), 26 | ), 27 | ) 28 | .unwrap(); 29 | let dev2 = client.device(ads::AmsAddr::new([5, 62, 215, 36, 1, 1].into(), 852)); 30 | let h2 = dev2 31 | .add_notification( 32 | 0x4020, 33 | 0, 34 | &Attributes::new( 35 | 4, 36 | TransmissionMode::ServerOnChange, 37 | Duration::from_secs(1), 38 | Duration::from_secs(1), 39 | ), 40 | ) 41 | .unwrap(); 42 | println!("{} {}", h1, h2); 43 | loop { 44 | std::thread::sleep(std::time::Duration::from_secs(1)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/timing.rs: -------------------------------------------------------------------------------- 1 | fn dumb() { 2 | let mut sock = std::net::TcpStream::connect("127.0.0.1:48999").unwrap(); 3 | use std::io::{Read, Write}; 4 | let now = std::time::Instant::now(); 5 | for _ in 0..5000 { 6 | let mut buf = [0; 50]; 7 | sock.write_all( 8 | b"\ 9 | \0\0(\0\0\0\x7f\0\0\x01\x01\x01\0\0\x01\x02\x03\x04\x05\x06S\x03\x02\0\ 10 | \x05\0\x0c\0\0\0\0\0\0\0\x01\0\0\0\0\0\0\0\x04\0\0\0\xa4\xe0\xfbD", 11 | ) 12 | .unwrap(); 13 | sock.read_exact(&mut buf).unwrap(); 14 | } 15 | println!("dumb: {:?}", now.elapsed()); 16 | } 17 | 18 | fn with_client() { 19 | let timeout = ads::Timeouts::new(std::time::Duration::from_secs(1)); 20 | let client = ads::Client::new("127.0.0.1:48999", timeout, ads::Source::Auto).unwrap(); 21 | let dev = client.device(ads::AmsAddr::new([1, 2, 3, 4, 5, 6].into(), 851)); 22 | let mut data = [0; 4]; 23 | let now = std::time::Instant::now(); 24 | for _ in 0..5000 { 25 | dev.read(0x4020, 0, &mut data).unwrap(); 26 | } 27 | println!("client: {:?}", now.elapsed()); 28 | } 29 | 30 | fn main() { 31 | dumb(); 32 | with_client(); 33 | } 34 | -------------------------------------------------------------------------------- /examples/timing_server.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::net::TcpListener; 3 | 4 | fn main() { 5 | let srv = TcpListener::bind("127.0.0.1:48999").unwrap(); 6 | let resp = b"\ 7 | \0\0\x2c\0\0\0\x7f\0\0\x01\x01\x01\0\0\x01\x02\x03\x04\x05\x06S\x03\x02\0\x05\0\x0c\ 8 | \0\0\0\0\0\0\0\x01\0\0\0\0\0\0\0\x04\0\0\0\xa4\xe0\xfbD"; 9 | loop { 10 | if let Ok((mut clt, _)) = srv.accept() { 11 | clt.set_nodelay(true).unwrap(); 12 | std::thread::spawn(move || { 13 | let mut resp = resp.to_vec(); 14 | let mut buf = [0; 50]; 15 | loop { 16 | clt.read_exact(&mut buf).unwrap(); 17 | resp[34..38].copy_from_slice(&buf[34..38]); 18 | clt.write_all(&resp).unwrap(); 19 | } 20 | }); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/values_via_handle.rs: -------------------------------------------------------------------------------- 1 | //! Shows how to read a whole structure from the PLC by handle, 2 | //! by redefining it as a Rust struct with zerocopy traits. 3 | 4 | use ads::{symbol::Handle, Client, Source, Timeouts}; 5 | use zerocopy::{FromBytes, IntoBytes}; 6 | 7 | #[derive(Default, FromBytes, IntoBytes)] 8 | #[repr(packed)] 9 | struct Motor { 10 | position: f32, 11 | speed: f32, 12 | moving: u8, 13 | } 14 | 15 | fn main() { 16 | let client = Client::new(("127.0.0.1", ads::PORT), Timeouts::none(), Source::Request).unwrap(); 17 | let dev = client.device(ads::AmsAddr::new([5, 62, 215, 36, 1, 1].into(), 851)); 18 | let handle = Handle::new(dev, "MY_SYMBOL").unwrap(); 19 | let motor = handle.read_value::().unwrap(); 20 | let pos = motor.position; 21 | let spd = motor.speed; 22 | println!("Motor params: pos={} spd={} moving={}", pos, spd, motor.moving != 0); 23 | } 24 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.63.0 2 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | //! Contains the TCP client to connect to an ADS server. 2 | 3 | use std::cell::{Cell, RefCell}; 4 | use std::collections::BTreeSet; 5 | use std::convert::{TryFrom, TryInto}; 6 | use std::io::{self, Read, Write}; 7 | use std::mem::size_of; 8 | use std::net::{IpAddr, Shutdown, TcpStream, ToSocketAddrs}; 9 | use std::str::FromStr; 10 | use std::time::Duration; 11 | 12 | use byteorder::{ByteOrder, ReadBytesExt, LE}; 13 | use crossbeam_channel::{bounded, unbounded, Receiver, Sender}; 14 | use itertools::Itertools; 15 | 16 | use crate::errors::{ads_error, ErrContext}; 17 | use crate::notif; 18 | use crate::{AmsAddr, AmsNetId, Error, Result}; 19 | 20 | use zerocopy::byteorder::little_endian::{U16, U32}; 21 | use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes}; 22 | 23 | /// An ADS protocol command. 24 | // https://infosys.beckhoff.com/content/1033/tc3_ads_intro/115847307.html?id=7738940192708835096 25 | #[repr(u16)] 26 | #[derive(Clone, Copy, Debug)] 27 | pub enum Command { 28 | /// Return device info 29 | DevInfo = 1, 30 | /// Read some data 31 | Read = 2, 32 | /// Write some data 33 | Write = 3, 34 | /// Write some data, then read back some data 35 | /// (used as a poor-man's function call) 36 | ReadWrite = 9, 37 | /// Read the ADS and device state 38 | ReadState = 4, 39 | /// Set the ADS and device state 40 | WriteControl = 5, 41 | /// Add a notification for a given index 42 | AddNotification = 6, 43 | /// Add a notification for a given index 44 | DeleteNotification = 7, 45 | /// Change occurred in a given notification, 46 | /// can be sent by the PLC only 47 | Notification = 8, 48 | } 49 | 50 | impl Command { 51 | fn action(self) -> &'static str { 52 | match self { 53 | Command::DevInfo => "get device info", 54 | Command::Read => "read data", 55 | Command::Write => "write data", 56 | Command::ReadWrite => "write and read data", 57 | Command::ReadState => "read state", 58 | Command::WriteControl => "write control", 59 | Command::AddNotification => "add notification", 60 | Command::DeleteNotification => "delete notification", 61 | Command::Notification => "notification", 62 | } 63 | } 64 | } 65 | 66 | /// Size of the AMS/TCP + AMS headers 67 | // https://infosys.beckhoff.com/content/1033/tc3_ads_intro/115845259.html?id=6032227753916597086 68 | pub(crate) const TCP_HEADER_SIZE: usize = 6; 69 | pub(crate) const AMS_HEADER_SIZE: usize = 38; // including AMS/TCP header 70 | pub(crate) const DEFAULT_BUFFER_SIZE: usize = 100; 71 | 72 | /// Holds the different timeouts that will be used by the Client. 73 | /// None means no timeout in every case. 74 | #[derive(Clone, Copy, Debug)] 75 | pub struct Timeouts { 76 | /// Connect timeout 77 | pub connect: Option, 78 | /// Reply read timeout 79 | pub read: Option, 80 | /// Socket write timoeut 81 | pub write: Option, 82 | } 83 | 84 | impl Timeouts { 85 | /// Create a new `Timeouts` where all values are identical. 86 | pub fn new(duration: Duration) -> Self { 87 | Self { connect: Some(duration), read: Some(duration), write: Some(duration) } 88 | } 89 | 90 | /// Create a new `Timeouts` without any timeouts specified. 91 | pub fn none() -> Self { 92 | Self { connect: None, read: None, write: None } 93 | } 94 | } 95 | 96 | /// Specifies the source AMS address to use. 97 | #[derive(Clone, Copy, Debug)] 98 | pub enum Source { 99 | /// Auto-generate a source address from the local address and a random port. 100 | Auto, 101 | /// Use a specified source address. 102 | Addr(AmsAddr), 103 | /// Request to open a port in the connected router and get the address from 104 | /// it. This is necessary when connecting to a local PLC on `127.0.0.1`. 105 | Request, 106 | } 107 | 108 | /// Represents a connection to a ADS server. 109 | /// 110 | /// The Client's communication methods use `&self`, so that it can be freely 111 | /// shared within one thread, or sent, between threads. Wrappers such as 112 | /// `Device` or `symbol::Handle` use a `&Client` as well. 113 | pub struct Client { 114 | /// TCP connection (duplicated with the reader) 115 | socket: TcpStream, 116 | /// Current invoke ID (identifies the request/reply pair), incremented 117 | /// after each request 118 | invoke_id: Cell, 119 | /// Read timeout (actually receive timeout for the channel) 120 | read_timeout: Option, 121 | /// The AMS address of the client 122 | source: AmsAddr, 123 | /// Sender for used Vec buffers to the reader thread 124 | buf_send: Sender>, 125 | /// Receiver for synchronous replies: used in `communicate` 126 | reply_recv: Receiver>>, 127 | /// Receiver for notifications: cloned and given out to interested parties 128 | notif_recv: Receiver, 129 | /// Active notification handles: these will be closed on Drop 130 | notif_handles: RefCell>, 131 | /// If we opened our local port with the router 132 | source_port_opened: bool, 133 | } 134 | 135 | impl Drop for Client { 136 | fn drop(&mut self) { 137 | // Close all open notification handles. 138 | let handles = std::mem::take(&mut *self.notif_handles.borrow_mut()); 139 | for (addr, handle) in handles { 140 | let _ = self.device(addr).delete_notification(handle); 141 | } 142 | 143 | // Remove our port from the router, if necessary. 144 | if self.source_port_opened { 145 | let mut close_port_msg = [1, 0, 2, 0, 0, 0, 0, 0]; 146 | LE::write_u16(&mut close_port_msg[6..], self.source.port()); 147 | let _ = self.socket.write_all(&close_port_msg); 148 | } 149 | 150 | // Need to shutdown the connection since the socket is duplicated in the 151 | // reader thread. This will cause the read() in the thread to return 152 | // with no data. 153 | let _ = self.socket.shutdown(Shutdown::Both); 154 | } 155 | } 156 | 157 | impl Client { 158 | /// Open a new connection to an ADS server. 159 | /// 160 | /// If connecting to a server that has an AMS router, it needs to have a 161 | /// route set for the source IP and NetID, otherwise the connection will be 162 | /// closed immediately. The route can be added from TwinCAT, or this 163 | /// crate's `udp::add_route` helper can be used to add a route via UDP 164 | /// message. 165 | /// 166 | /// `source` is the AMS address to to use as the source; the NetID needs to 167 | /// match the route entry in the server. If `Source::Auto`, the NetID is 168 | /// constructed from the local IP address with .1.1 appended; if there is no 169 | /// IPv4 address, `127.0.0.1.1.1` is used. 170 | /// 171 | /// The AMS port of `source` is not important, as long as it is not a 172 | /// well-known service port; an ephemeral port number > 49152 is 173 | /// recommended. If Auto, the port is set to 58913. 174 | /// 175 | /// If you are connecting to the local PLC, you need to set `source` to 176 | /// `Source::Request`. This will ask the local AMS router for a new 177 | /// port and use it as the source port. 178 | /// 179 | /// Since all communications is supposed to be handled by an ADS router, 180 | /// only one TCP/ADS connection can exist between two hosts. Non-TwinCAT 181 | /// clients should make sure to replicate this behavior, as opening a second 182 | /// connection will close the first. 183 | pub fn new(addr: impl ToSocketAddrs, timeouts: Timeouts, source: Source) -> Result { 184 | // Connect, taking the timeout into account. Unfortunately 185 | // connect_timeout wants a single SocketAddr. 186 | let addr = addr 187 | .to_socket_addrs() 188 | .ctx("converting address to SocketAddr")? 189 | .next() 190 | .expect("at least one SocketAddr"); 191 | let mut socket = if let Some(timeout) = timeouts.connect { 192 | TcpStream::connect_timeout(&addr, timeout).ctx("connecting TCP socket with timeout")? 193 | } else { 194 | TcpStream::connect(addr).ctx("connecting TCP socket")? 195 | }; 196 | 197 | // Disable Nagle to ensure small requests are sent promptly; we're 198 | // playing ping-pong with request reply, so no pipelining. 199 | socket.set_nodelay(true).ctx("setting NODELAY")?; 200 | socket.set_write_timeout(timeouts.write).ctx("setting write timeout")?; 201 | 202 | // Determine our source AMS address. If it's not specified, try to use 203 | // the socket's local IPv4 address, if it's IPv6 (not sure if Beckhoff 204 | // devices support that) use `127.0.0.1` as the last resort. 205 | // 206 | // If source is Request, send an AMS port open message to the connected 207 | // router to get our source address. This is required when connecting 208 | // via localhost, apparently. 209 | let mut source_port_opened = false; 210 | let source = match source { 211 | Source::Addr(id) => id, 212 | Source::Auto => { 213 | let my_addr = socket.local_addr().ctx("getting local socket address")?.ip(); 214 | if let IpAddr::V4(ip) = my_addr { 215 | let [a, b, c, d] = ip.octets(); 216 | // use some random ephemeral port 217 | AmsAddr::new(AmsNetId::new(a, b, c, d, 1, 1), 58913) 218 | } else { 219 | AmsAddr::new(AmsNetId::new(127, 0, 0, 1, 1, 1), 58913) 220 | } 221 | } 222 | Source::Request => { 223 | let request_port_msg = [0, 16, 2, 0, 0, 0, 0, 0]; 224 | let mut reply = [0; 14]; 225 | socket.write_all(&request_port_msg).ctx("requesting port from router")?; 226 | socket.read_exact(&mut reply).ctx("requesting port from router")?; 227 | if reply[..6] != [0, 16, 8, 0, 0, 0] { 228 | return Err(Error::Reply("requesting port", "unexpected reply header", 0)); 229 | } 230 | source_port_opened = true; 231 | AmsAddr::new(AmsNetId::from_slice(&reply[6..12]).expect("size"), LE::read_u16(&reply[12..14])) 232 | } 233 | }; 234 | 235 | // Clone the socket for the reader thread and create our channels for 236 | // bidirectional communication. 237 | let socket_clone = socket.try_clone().ctx("cloning TCP socket")?; 238 | let (buf_send, buf_recv) = bounded(10); 239 | let (reply_send, reply_recv) = bounded(1); 240 | let (notif_send, notif_recv) = unbounded(); 241 | let mut source_bytes = [0; 8]; 242 | source.write_to(&mut &mut source_bytes[..]).expect("size"); 243 | 244 | // Start the reader thread. 245 | let reader = Reader { socket: socket_clone, source: source_bytes, buf_recv, reply_send, notif_send }; 246 | std::thread::spawn(|| reader.run()); 247 | 248 | Ok(Client { 249 | socket, 250 | source, 251 | buf_send, 252 | reply_recv, 253 | notif_recv, 254 | invoke_id: Cell::new(0), 255 | read_timeout: timeouts.read, 256 | notif_handles: RefCell::default(), 257 | source_port_opened, 258 | }) 259 | } 260 | 261 | /// Return the source address the client is using. 262 | pub fn source(&self) -> AmsAddr { 263 | self.source 264 | } 265 | 266 | /// Get a receiver for notifications. 267 | pub fn get_notification_channel(&self) -> Receiver { 268 | self.notif_recv.clone() 269 | } 270 | 271 | /// Return a wrapper that executes operations for a target device (known by 272 | /// NetID and port). 273 | /// 274 | /// The local NetID `127.0.0.1.1.1` is mapped to the client's source NetID, 275 | /// so that you can connect to a local PLC using: 276 | /// 277 | /// ```ignore 278 | /// let client = Client::new("127.0.0.1", ..., Source::Request); 279 | /// let device = client.device(AmsAddr::new(AmsNetId::local(), 851)); 280 | /// ``` 281 | /// 282 | /// without knowing its NetID. 283 | pub fn device(&self, mut addr: AmsAddr) -> Device<'_> { 284 | if addr.netid() == AmsNetId::local() { 285 | addr = AmsAddr::new(self.source().netid(), addr.port()); 286 | } 287 | Device { client: self, addr } 288 | } 289 | 290 | /// Low-level function to execute an ADS command. 291 | /// 292 | /// Writes a data from a number of input buffers, and returns data in a 293 | /// number of output buffers. The latter might not be filled completely; 294 | /// the return value specifies the number of total valid bytes. It is up to 295 | /// the caller to determine what this means in terms of the passed buffers. 296 | pub fn communicate( 297 | &self, cmd: Command, target: AmsAddr, data_in: &[&[u8]], data_out: &mut [&mut [u8]], 298 | ) -> Result { 299 | // Increase the invoke ID. We could also generate a random u32, but 300 | // this way the sequence of packets can be tracked. 301 | self.invoke_id.set(self.invoke_id.get().wrapping_add(1)); 302 | 303 | // The data we send is the sum of all data_in buffers. 304 | let data_in_len = data_in.iter().map(|v| v.len()).sum::(); 305 | 306 | // Create outgoing header. 307 | let ads_data_len = AMS_HEADER_SIZE - TCP_HEADER_SIZE + data_in_len; 308 | let header = AdsHeader { 309 | ams_cmd: 0, // send command 310 | length: U32::new(ads_data_len.try_into()?), 311 | dest_netid: target.netid(), 312 | dest_port: U16::new(target.port()), 313 | src_netid: self.source.netid(), 314 | src_port: U16::new(self.source.port()), 315 | command: U16::new(cmd as u16), 316 | state_flags: U16::new(4), // state flags (4 = send command) 317 | data_length: U32::new(data_in_len as u32), // overflow checked above 318 | error_code: U32::new(0), 319 | invoke_id: U32::new(self.invoke_id.get()), 320 | }; 321 | 322 | // Collect the outgoing data. Note, allocating a Vec and calling 323 | // `socket.write_all` only once is faster than writing in multiple 324 | // steps, even with TCP_NODELAY. 325 | let mut request = Vec::with_capacity(ads_data_len); 326 | request.extend_from_slice(header.as_bytes()); 327 | for buf in data_in { 328 | request.extend_from_slice(buf); 329 | } 330 | // &T impls Write for T: Write, so no &mut self required. 331 | (&self.socket).write_all(&request).ctx("sending request")?; 332 | 333 | // Get a reply from the reader thread, with timeout or not. 334 | let reply = if let Some(tmo) = self.read_timeout { 335 | self.reply_recv 336 | .recv_timeout(tmo) 337 | .map_err(|_| io::ErrorKind::TimedOut.into()) 338 | .ctx("receiving reply (route set?)")? 339 | } else { 340 | self.reply_recv 341 | .recv() 342 | .map_err(|_| io::ErrorKind::UnexpectedEof.into()) 343 | .ctx("receiving reply (route set?)")? 344 | }?; 345 | 346 | // Validate the incoming reply. The reader thread already made sure that 347 | // it is consistent and addressed to us. 348 | 349 | // The source netid/port must match what we sent. 350 | if reply[14..22] != request[6..14] { 351 | return Err(Error::Reply(cmd.action(), "unexpected source address", 0)); 352 | } 353 | // Read the other fields we need. 354 | assert!(reply.len() >= AMS_HEADER_SIZE); 355 | let mut ptr = &reply[22..]; 356 | let ret_cmd = ptr.read_u16::().expect("size"); 357 | let state_flags = ptr.read_u16::().expect("size"); 358 | let data_len = ptr.read_u32::().expect("size"); 359 | let error_code = ptr.read_u32::().expect("size"); 360 | let invoke_id = ptr.read_u32::().expect("size"); 361 | let result = if reply.len() >= AMS_HEADER_SIZE + 4 { 362 | ptr.read_u32::().expect("size") 363 | } else { 364 | 0 // this must be because an error code is already set 365 | }; 366 | 367 | // Command must match. 368 | if ret_cmd != cmd as u16 { 369 | return Err(Error::Reply(cmd.action(), "unexpected command", ret_cmd.into())); 370 | } 371 | // State flags must be "4 | 1". 372 | if state_flags != 5 { 373 | return Err(Error::Reply(cmd.action(), "unexpected state flags", state_flags.into())); 374 | } 375 | // Invoke ID must match what we sent. 376 | if invoke_id != self.invoke_id.get() { 377 | return Err(Error::Reply(cmd.action(), "unexpected invoke ID", invoke_id)); 378 | } 379 | // Check error code in AMS header. 380 | if error_code != 0 { 381 | return ads_error(cmd.action(), error_code); 382 | } 383 | // Check result field in payload, only relevant if error_code == 0. 384 | if result != 0 { 385 | return ads_error(cmd.action(), result); 386 | } 387 | 388 | // If we don't want return data, we're done. 389 | if data_out.is_empty() { 390 | let _ = self.buf_send.send(reply); 391 | return Ok(0); 392 | } 393 | 394 | // Check returned length, it needs to fill at least the first data_out 395 | // buffer. This also ensures that we had a result field. 396 | if (data_len as usize) < data_out[0].len() + 4 { 397 | return Err(Error::Reply(cmd.action(), "got less data than expected", data_len)); 398 | } 399 | 400 | // The pure user data length, without the result field. 401 | let data_len = data_len as usize - 4; 402 | 403 | // Distribute the data into the user output buffers, up to the returned 404 | // data length. 405 | let mut offset = AMS_HEADER_SIZE + 4; 406 | let mut rest_len = data_len; 407 | for buf in data_out { 408 | let n = buf.len().min(rest_len); 409 | buf[..n].copy_from_slice(&reply[offset..][..n]); 410 | offset += n; 411 | rest_len -= n; 412 | if rest_len == 0 { 413 | break; 414 | } 415 | } 416 | 417 | // Send back the Vec buffer to the reader thread. 418 | let _ = self.buf_send.send(reply); 419 | 420 | // Return either the error or the length of data. 421 | Ok(data_len) 422 | } 423 | } 424 | 425 | // Implementation detail: reader thread that takes replies and notifications 426 | // and distributes them accordingly. 427 | struct Reader { 428 | socket: TcpStream, 429 | source: [u8; 8], 430 | buf_recv: Receiver>, 431 | reply_send: Sender>>, 432 | notif_send: Sender, 433 | } 434 | 435 | impl Reader { 436 | fn run(mut self) { 437 | self.run_inner(); 438 | // We can't do much here. But try to shut down the socket so that 439 | // the main client can't be used anymore either. 440 | let _ = self.socket.shutdown(Shutdown::Both); 441 | } 442 | 443 | fn run_inner(&mut self) { 444 | loop { 445 | // Get a buffer from the free-channel or create a new one. 446 | let mut buf = self 447 | .buf_recv 448 | .try_recv() 449 | .unwrap_or_else(|_| Vec::with_capacity(DEFAULT_BUFFER_SIZE)); 450 | 451 | // Read a header from the socket. 452 | buf.resize(TCP_HEADER_SIZE, 0); 453 | if self.socket.read_exact(&mut buf).ctx("reading AMS packet header").is_err() { 454 | // Not sending an error back; we don't know if something was 455 | // requested or the socket was just closed from either side. 456 | return; 457 | } 458 | 459 | // Read the rest of the packet. 460 | let packet_length = LE::read_u32(&buf[2..6]) as usize; 461 | buf.resize(TCP_HEADER_SIZE + packet_length, 0); 462 | if let Err(e) = self.socket.read_exact(&mut buf[6..]).ctx("reading rest of packet") { 463 | let _ = self.reply_send.send(Err(e)); 464 | return; 465 | } 466 | 467 | // Is it something other than an ADS command packet? 468 | let ams_cmd = LE::read_u16(&buf); 469 | if ams_cmd != 0 { 470 | // if it's a known packet type, continue 471 | if matches!(ams_cmd, 1 | 4096 | 4097 | 4098) { 472 | continue; 473 | } 474 | let _ = self.reply_send.send(Err(Error::Reply( 475 | "reading packet", 476 | "invalid packet or unknown AMS command", 477 | ams_cmd as _, 478 | ))); 479 | return; 480 | } 481 | 482 | // If the header length fields aren't self-consistent, abort the connection. 483 | let rest_length = LE::read_u32(&buf[26..30]) as usize; 484 | if rest_length != packet_length + TCP_HEADER_SIZE - AMS_HEADER_SIZE { 485 | let _ = self 486 | .reply_send 487 | .send(Err(Error::Reply("reading packet", "inconsistent packet", 0))); 488 | return; 489 | } 490 | 491 | // Check that the packet is meant for us. 492 | if buf[6..14] != self.source { 493 | continue; 494 | } 495 | 496 | // If it looks like a reply, send it back to the requesting thread, 497 | // it will handle further validation. 498 | if LE::read_u16(&buf[22..24]) != Command::Notification as u16 { 499 | if self.reply_send.send(Ok(buf)).is_err() { 500 | // Client must have been shut down. 501 | return; 502 | } 503 | continue; 504 | } 505 | 506 | // Validate notification message fields. 507 | let state_flags = LE::read_u16(&buf[24..26]); 508 | let error_code = LE::read_u32(&buf[30..34]); 509 | let length = LE::read_u32(&buf[38..42]) as usize; 510 | if state_flags != 4 || error_code != 0 || length != rest_length - 4 || length < 4 { 511 | continue; 512 | } 513 | 514 | // Send the notification to whoever wants to receive it. 515 | if let Ok(notif) = notif::Notification::new(buf) { 516 | self.notif_send.send(notif).expect("never disconnects"); 517 | } 518 | } 519 | } 520 | } 521 | 522 | /// A `Client` wrapper that talks to a specific ADS device. 523 | #[derive(Clone, Copy)] 524 | pub struct Device<'c> { 525 | /// The underlying `Client`. 526 | pub client: &'c Client, 527 | addr: AmsAddr, 528 | } 529 | 530 | impl Device<'_> { 531 | /// Read the device's name + version. 532 | pub fn get_info(&self) -> Result { 533 | let mut data = DeviceInfoRaw::new_zeroed(); 534 | self.client 535 | .communicate(Command::DevInfo, self.addr, &[], &mut [data.as_mut_bytes()])?; 536 | 537 | // Decode the name string, which is null-terminated. Technically it's 538 | // Windows-1252, but in practice no non-ASCII occurs. 539 | let name = data 540 | .name 541 | .iter() 542 | .take_while(|&&ch| ch > 0) 543 | .map(|&ch| ch as char) 544 | .collect::(); 545 | Ok(DeviceInfo { major: data.major, minor: data.minor, version: data.version.get(), name }) 546 | } 547 | 548 | /// Read some data at a given index group/offset. Returned data can be shorter than 549 | /// the buffer, the length is the return value. 550 | pub fn read(&self, index_group: u32, index_offset: u32, data: &mut [u8]) -> Result { 551 | let header = IndexLength { 552 | index_group: U32::new(index_group), 553 | index_offset: U32::new(index_offset), 554 | length: U32::new(data.len().try_into()?), 555 | }; 556 | let mut read_len = U32::new(0); 557 | 558 | self.client.communicate( 559 | Command::Read, 560 | self.addr, 561 | &[header.as_bytes()], 562 | &mut [read_len.as_mut_bytes(), data], 563 | )?; 564 | 565 | Ok(read_len.get() as usize) 566 | } 567 | 568 | /// Read some data at a given index group/offset, ensuring that the returned data has 569 | /// exactly the size of the passed buffer. 570 | pub fn read_exact(&self, index_group: u32, index_offset: u32, data: &mut [u8]) -> Result<()> { 571 | let len = self.read(index_group, index_offset, data)?; 572 | if len != data.len() { 573 | return Err(Error::Reply("read data", "got less data than expected", len as u32)); 574 | } 575 | Ok(()) 576 | } 577 | 578 | /// Read data of given type. 579 | /// 580 | /// Any type that supports `zerocopy::FromBytes` can be read. You can also 581 | /// derive that trait on your own structures and read structured data 582 | /// directly from the symbol. 583 | /// 584 | /// Note: to be independent of the host's byte order, use the integer types 585 | /// defined in `zerocopy::byteorder`. 586 | pub fn read_value( 587 | &self, index_group: u32, index_offset: u32, 588 | ) -> Result { 589 | let mut buf = T::default(); 590 | self.read_exact(index_group, index_offset, buf.as_mut_bytes())?; 591 | Ok(buf) 592 | } 593 | 594 | /// Read multiple index groups/offsets with one ADS request (a "sum-up" request). 595 | /// 596 | /// This function only returns Err on errors that cause the whole sum-up 597 | /// request to fail (e.g. if the device doesn't support such requests). If 598 | /// the request as a whole succeeds, each single read can have returned its 599 | /// own error. 600 | /// 601 | /// The returned data can be shorter than the buffer in each request. The 602 | /// [`ReadRequest::data`] method will return either the properly truncated 603 | /// returned data or the error for each read. 604 | /// 605 | /// Example: 606 | /// ```no_run 607 | /// # fn main() -> ads::Result<()> { 608 | /// # use ads::client::*; 609 | /// # let client = Client::new(("", ads::PORT), ads::Timeouts::none(), ads::Source::Auto)?; 610 | /// # let device = client.device(ads::AmsAddr::new(Default::default(), 0)); 611 | /// # let (ix1, ix2, off1, off2) = (0, 0, 0, 0); 612 | /// // create buffers 613 | /// let mut buf_1 = [0; 128]; // request reading 128 bytes each, 614 | /// let mut buf_2 = [0; 128]; // from two indices 615 | /// // create the request structures 616 | /// let req_1 = ReadRequest::new(ix1, off1, &mut buf_1); 617 | /// let req_2 = ReadRequest::new(ix2, off2, &mut buf_2); 618 | /// let mut requests = [req_1, req_2]; 619 | /// // execute the multi-request on the remote end 620 | /// device.read_multi(&mut requests)?; 621 | /// // extract the resulting data, checking individual reads for 622 | /// // errors and getting the returned data otherwise 623 | /// let res_1 = requests[0].data()?; 624 | /// let res_2 = requests[1].data()?; 625 | /// # Ok(()) 626 | /// # } 627 | /// ``` 628 | pub fn read_multi(&self, requests: &mut [ReadRequest]) -> Result<()> { 629 | let nreq = requests.len(); 630 | let rlen = requests.iter().map(|r| size_of::() + r.rbuf.len()).sum::(); 631 | let wlen = size_of::() * nreq; 632 | let header = IndexLengthRW { 633 | // using SUMUP_READ_EX_2 since would return the actual returned 634 | // number of bytes, and no empty bytes if the read is short, 635 | // but then we'd have to reshuffle the buffers 636 | index_group: U32::new(crate::index::SUMUP_READ_EX), 637 | index_offset: U32::new(nreq as u32), 638 | read_length: U32::new(rlen.try_into()?), 639 | write_length: U32::new(wlen.try_into()?), 640 | }; 641 | let mut read_len = U32::new(0); 642 | let mut w_buffers = vec![header.as_bytes()]; 643 | let mut r_buffers = (0..2 * nreq + 1).map(|_| &mut [][..]).collect_vec(); 644 | r_buffers[0] = read_len.as_mut_bytes(); 645 | for (i, req) in requests.iter_mut().enumerate() { 646 | w_buffers.push(req.req.as_bytes()); 647 | r_buffers[1 + i] = req.res.as_mut_bytes(); 648 | r_buffers[1 + nreq + i] = req.rbuf; 649 | } 650 | self.client 651 | .communicate(Command::ReadWrite, self.addr, &w_buffers, &mut r_buffers)?; 652 | Ok(()) 653 | } 654 | 655 | /// Write some data to a given index group/offset. 656 | pub fn write(&self, index_group: u32, index_offset: u32, data: &[u8]) -> Result<()> { 657 | let header = IndexLength { 658 | index_group: U32::new(index_group), 659 | index_offset: U32::new(index_offset), 660 | length: U32::new(data.len().try_into()?), 661 | }; 662 | self.client 663 | .communicate(Command::Write, self.addr, &[header.as_bytes(), data], &mut [])?; 664 | Ok(()) 665 | } 666 | 667 | /// Write data of given type. 668 | /// 669 | /// See `read_value` for details. 670 | pub fn write_value( 671 | &self, index_group: u32, index_offset: u32, value: &T, 672 | ) -> Result<()> { 673 | self.write(index_group, index_offset, value.as_bytes()) 674 | } 675 | 676 | /// Write multiple index groups/offsets with one ADS request (a "sum-up" request). 677 | /// 678 | /// This function only returns Err on errors that cause the whole sum-up 679 | /// request to fail (e.g. if the device doesn't support such requests). If 680 | /// the request as a whole succeeds, each single write can have returned its 681 | /// own error. To retrieve and handle them, the [`WriteRequest::ensure`] 682 | /// method should be called on each request. 683 | /// 684 | /// Example: 685 | /// ```no_run 686 | /// # fn main() -> ads::Result<()> { 687 | /// # use ads::client::*; 688 | /// # let client = Client::new(("", ads::PORT), ads::Timeouts::none(), ads::Source::Auto)?; 689 | /// # let device = client.device(ads::AmsAddr::new(Default::default(), 0)); 690 | /// # let (ix1, ix2, off1, off2) = (0, 0, 0, 0); 691 | /// // create buffers 692 | /// let buf_1 = [1, 5, 7, 10]; // request writing 4 bytes each, 693 | /// let buf_2 = [0, 8, 9, 11]; // to two indices 694 | /// // create the request structures 695 | /// let req_1 = WriteRequest::new(ix1, off1, &buf_1); 696 | /// let req_2 = WriteRequest::new(ix2, off2, &buf_2); 697 | /// let mut requests = [req_1, req_2]; 698 | /// // execute the multi-request on the remote end 699 | /// device.write_multi(&mut requests)?; 700 | /// // check the individual writes for errors 701 | /// requests[0].ensure()?; 702 | /// requests[1].ensure()?; 703 | /// # Ok(()) 704 | /// # } 705 | /// ``` 706 | pub fn write_multi(&self, requests: &mut [WriteRequest]) -> Result<()> { 707 | let nreq = requests.len(); 708 | let rlen = size_of::() * nreq; 709 | let wlen = requests.iter().map(|r| size_of::() + r.wbuf.len()).sum::(); 710 | let header = IndexLengthRW { 711 | index_group: U32::new(crate::index::SUMUP_WRITE), 712 | index_offset: U32::new(nreq as u32), 713 | read_length: U32::new(rlen.try_into()?), 714 | write_length: U32::new(wlen.try_into()?), 715 | }; 716 | let mut read_len = U32::new(0); 717 | let mut w_buffers = vec![&[][..]; 2 * nreq + 1]; 718 | let mut r_buffers = vec![read_len.as_mut_bytes()]; 719 | w_buffers[0] = header.as_bytes(); 720 | for (i, req) in requests.iter_mut().enumerate() { 721 | w_buffers[1 + i] = req.req.as_bytes(); 722 | w_buffers[1 + nreq + i] = req.wbuf; 723 | r_buffers.push(req.res.as_mut_bytes()); 724 | } 725 | self.client 726 | .communicate(Command::ReadWrite, self.addr, &w_buffers, &mut r_buffers)?; 727 | Ok(()) 728 | } 729 | 730 | /// Write some data to a given index group/offset and then read back some 731 | /// reply from there. This is not the same as a write() followed by read(); 732 | /// it is used as a kind of RPC call. 733 | pub fn write_read( 734 | &self, index_group: u32, index_offset: u32, write_data: &[u8], read_data: &mut [u8], 735 | ) -> Result { 736 | let header = IndexLengthRW { 737 | index_group: U32::new(index_group), 738 | index_offset: U32::new(index_offset), 739 | read_length: U32::new(read_data.len().try_into()?), 740 | write_length: U32::new(write_data.len().try_into()?), 741 | }; 742 | let mut read_len = U32::new(0); 743 | self.client.communicate( 744 | Command::ReadWrite, 745 | self.addr, 746 | &[header.as_bytes(), write_data], 747 | &mut [read_len.as_mut_bytes(), read_data], 748 | )?; 749 | Ok(read_len.get() as usize) 750 | } 751 | 752 | /// Like `write_read`, but ensure the returned data length matches the output buffer. 753 | pub fn write_read_exact( 754 | &self, index_group: u32, index_offset: u32, write_data: &[u8], read_data: &mut [u8], 755 | ) -> Result<()> { 756 | let len = self.write_read(index_group, index_offset, write_data, read_data)?; 757 | if len != read_data.len() { 758 | return Err(Error::Reply("write/read data", "got less data than expected", len as u32)); 759 | } 760 | Ok(()) 761 | } 762 | 763 | /// Write multiple index groups/offsets with one ADS request (a "sum-up" request). 764 | /// 765 | /// This function only returns Err on errors that cause the whole sum-up 766 | /// request to fail (e.g. if the device doesn't support such requests). If 767 | /// the request as a whole succeeds, each single write/read can have 768 | /// returned its own error. The [`WriteReadRequest::data`] method will 769 | /// return either the returned data or the error for each write/read. 770 | /// 771 | /// See [`Device::read_multi`] or [`Device::write_multi`] for analogous usage examples. 772 | pub fn write_read_multi(&self, requests: &mut [WriteReadRequest]) -> Result<()> { 773 | let nreq = requests.len(); 774 | let rlen = requests.iter().map(|r| size_of::() + r.rbuf.len()).sum::(); 775 | let wlen = requests 776 | .iter() 777 | .map(|r| size_of::() + r.wbuf.len()) 778 | .sum::(); 779 | let header = IndexLengthRW { 780 | index_group: U32::new(crate::index::SUMUP_READWRITE), 781 | index_offset: U32::new(nreq as u32), 782 | read_length: U32::new(rlen.try_into()?), 783 | write_length: U32::new(wlen.try_into()?), 784 | }; 785 | let mut read_len = U32::new(0); 786 | let mut w_buffers = vec![&[][..]; 2 * nreq + 1]; 787 | let mut r_buffers = (0..2 * nreq + 1).map(|_| &mut [][..]).collect_vec(); 788 | w_buffers[0] = header.as_bytes(); 789 | r_buffers[0] = read_len.as_mut_bytes(); 790 | for (i, req) in requests.iter_mut().enumerate() { 791 | w_buffers[1 + i] = req.req.as_bytes(); 792 | w_buffers[1 + nreq + i] = req.wbuf; 793 | r_buffers[1 + i] = req.res.as_mut_bytes(); 794 | r_buffers[1 + nreq + i] = req.rbuf; 795 | } 796 | self.client 797 | .communicate(Command::ReadWrite, self.addr, &w_buffers, &mut r_buffers)?; 798 | // unfortunately SUMUP_READWRITE returns only the actual read bytes for each 799 | // request, so if there are short reads the buffers got filled wrongly 800 | fixup_write_read_return_buffers(requests); 801 | Ok(()) 802 | } 803 | 804 | /// Return the ADS and device state of the device. 805 | pub fn get_state(&self) -> Result<(AdsState, u16)> { 806 | let mut state = ReadState::new_zeroed(); 807 | self.client 808 | .communicate(Command::ReadState, self.addr, &[], &mut [state.as_mut_bytes()])?; 809 | 810 | // Convert ADS state to the enum type 811 | let ads_state = AdsState::try_from(state.ads_state.get()) 812 | .map_err(|e| Error::Reply("read state", e, state.ads_state.get().into()))?; 813 | 814 | Ok((ads_state, state.dev_state.get())) 815 | } 816 | 817 | /// (Try to) set the ADS and device state of the device. 818 | pub fn write_control(&self, ads_state: AdsState, dev_state: u16) -> Result<()> { 819 | let data = WriteControl { 820 | ads_state: U16::new(ads_state as _), 821 | dev_state: U16::new(dev_state), 822 | data_length: U32::new(0), 823 | }; 824 | self.client 825 | .communicate(Command::WriteControl, self.addr, &[data.as_bytes()], &mut [])?; 826 | Ok(()) 827 | } 828 | 829 | /// Add a notification handle for some index group/offset. 830 | /// 831 | /// Notifications are delivered via a MPMC channel whose reading end can be 832 | /// obtained from `get_notification_channel` on the `Client` object. 833 | /// The returned `Handle` can be used to check which notification has fired. 834 | /// 835 | /// If the notification is not deleted explictly using `delete_notification` 836 | /// and the `Handle`, it is deleted when the `Client` object is dropped. 837 | pub fn add_notification( 838 | &self, index_group: u32, index_offset: u32, attributes: ¬if::Attributes, 839 | ) -> Result { 840 | let data = AddNotif { 841 | index_group: U32::new(index_group), 842 | index_offset: U32::new(index_offset), 843 | length: U32::new(attributes.length.try_into()?), 844 | trans_mode: U32::new(attributes.trans_mode as u32), 845 | max_delay: U32::new(attributes.max_delay.as_millis().try_into()?), 846 | cycle_time: U32::new(attributes.cycle_time.as_millis().try_into()?), 847 | reserved: [0; 16], 848 | }; 849 | let mut handle = U32::new(0); 850 | self.client.communicate( 851 | Command::AddNotification, 852 | self.addr, 853 | &[data.as_bytes()], 854 | &mut [handle.as_mut_bytes()], 855 | )?; 856 | self.client.notif_handles.borrow_mut().insert((self.addr, handle.get())); 857 | Ok(handle.get()) 858 | } 859 | 860 | /// Add multiple notification handles. 861 | /// 862 | /// This function only returns Err on errors that cause the whole sum-up 863 | /// request to fail (e.g. if the device doesn't support such requests). If 864 | /// the request as a whole succeeds, each single read can have returned its 865 | /// own error. The [`AddNotifRequest::handle`] method will return either 866 | /// the returned handle or the error for each read. 867 | pub fn add_notification_multi(&self, requests: &mut [AddNotifRequest]) -> Result<()> { 868 | let nreq = requests.len(); 869 | let rlen = size_of::() * nreq; 870 | let wlen = size_of::() * nreq; 871 | let header = IndexLengthRW { 872 | index_group: U32::new(crate::index::SUMUP_ADDDEVNOTE), 873 | index_offset: U32::new(nreq as u32), 874 | read_length: U32::new(rlen.try_into()?), 875 | write_length: U32::new(wlen.try_into()?), 876 | }; 877 | let mut read_len = U32::new(0); 878 | let mut w_buffers = vec![header.as_bytes()]; 879 | let mut r_buffers = vec![read_len.as_mut_bytes()]; 880 | for req in requests.iter_mut() { 881 | w_buffers.push(req.req.as_bytes()); 882 | r_buffers.push(req.res.as_mut_bytes()); 883 | } 884 | self.client 885 | .communicate(Command::ReadWrite, self.addr, &w_buffers, &mut r_buffers)?; 886 | for req in requests { 887 | if let Ok(handle) = req.handle() { 888 | self.client.notif_handles.borrow_mut().insert((self.addr, handle)); 889 | } 890 | } 891 | Ok(()) 892 | } 893 | 894 | /// Delete a notification with given handle. 895 | pub fn delete_notification(&self, handle: notif::Handle) -> Result<()> { 896 | self.client.communicate( 897 | Command::DeleteNotification, 898 | self.addr, 899 | &[U32::new(handle).as_bytes()], 900 | &mut [], 901 | )?; 902 | self.client.notif_handles.borrow_mut().remove(&(self.addr, handle)); 903 | Ok(()) 904 | } 905 | 906 | /// Delete multiple notification handles. 907 | /// 908 | /// This function only returns Err on errors that cause the whole sum-up 909 | /// request to fail (e.g. if the device doesn't support such requests). If 910 | /// the request as a whole succeeds, each single read can have returned its 911 | /// own error. The [`DelNotifRequest::ensure`] method will return either the 912 | /// returned data or the error for each read. 913 | pub fn delete_notification_multi(&self, requests: &mut [DelNotifRequest]) -> Result<()> { 914 | let nreq = requests.len(); 915 | let rlen = size_of::() * nreq; 916 | let wlen = size_of::() * nreq; 917 | let header = IndexLengthRW { 918 | index_group: U32::new(crate::index::SUMUP_DELDEVNOTE), 919 | index_offset: U32::new(nreq as u32), 920 | read_length: U32::new(rlen.try_into()?), 921 | write_length: U32::new(wlen.try_into()?), 922 | }; 923 | let mut read_len = U32::new(0); 924 | let mut w_buffers = vec![header.as_bytes()]; 925 | let mut r_buffers = vec![read_len.as_mut_bytes()]; 926 | for req in requests.iter_mut() { 927 | w_buffers.push(req.req.as_bytes()); 928 | r_buffers.push(req.res.as_mut_bytes()); 929 | } 930 | self.client 931 | .communicate(Command::ReadWrite, self.addr, &w_buffers, &mut r_buffers)?; 932 | for req in requests { 933 | if req.ensure().is_ok() { 934 | self.client.notif_handles.borrow_mut().remove(&(self.addr, req.req.get())); 935 | } 936 | } 937 | Ok(()) 938 | } 939 | } 940 | 941 | /// Device info returned from an ADS server. 942 | #[derive(Debug)] 943 | pub struct DeviceInfo { 944 | /// Name of the ADS device/service. 945 | pub name: String, 946 | /// Major version. 947 | pub major: u8, 948 | /// Minor version. 949 | pub minor: u8, 950 | /// Build version. 951 | pub version: u16, 952 | } 953 | 954 | /// The ADS state of a device. 955 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 956 | #[allow(missing_docs)] 957 | #[repr(u16)] 958 | pub enum AdsState { 959 | Invalid = 0, 960 | Idle = 1, 961 | Reset = 2, 962 | Init = 3, 963 | Start = 4, 964 | Run = 5, 965 | Stop = 6, 966 | SaveCfg = 7, 967 | LoadCfg = 8, 968 | PowerFail = 9, 969 | PowerGood = 10, 970 | Error = 11, 971 | Shutdown = 12, 972 | Suspend = 13, 973 | Resume = 14, 974 | Config = 15, 975 | Reconfig = 16, 976 | Stopping = 17, 977 | Incompatible = 18, 978 | Exception = 19, 979 | } 980 | 981 | impl TryFrom for AdsState { 982 | type Error = &'static str; 983 | 984 | fn try_from(value: u16) -> std::result::Result { 985 | Ok(match value { 986 | 0 => Self::Invalid, 987 | 1 => Self::Idle, 988 | 2 => Self::Reset, 989 | 3 => Self::Init, 990 | 4 => Self::Start, 991 | 5 => Self::Run, 992 | 6 => Self::Stop, 993 | 7 => Self::SaveCfg, 994 | 8 => Self::LoadCfg, 995 | 9 => Self::PowerFail, 996 | 10 => Self::PowerGood, 997 | 11 => Self::Error, 998 | 12 => Self::Shutdown, 999 | 13 => Self::Suspend, 1000 | 14 => Self::Resume, 1001 | 15 => Self::Config, 1002 | 16 => Self::Reconfig, 1003 | 17 => Self::Stopping, 1004 | 18 => Self::Incompatible, 1005 | 19 => Self::Exception, 1006 | _ => return Err("invalid state constant"), 1007 | }) 1008 | } 1009 | } 1010 | 1011 | impl FromStr for AdsState { 1012 | type Err = &'static str; 1013 | 1014 | fn from_str(s: &str) -> std::result::Result { 1015 | Ok(match &*s.to_ascii_lowercase() { 1016 | "invalid" => Self::Invalid, 1017 | "idle" => Self::Idle, 1018 | "reset" => Self::Reset, 1019 | "init" => Self::Init, 1020 | "start" => Self::Start, 1021 | "run" => Self::Run, 1022 | "stop" => Self::Stop, 1023 | "savecfg" => Self::SaveCfg, 1024 | "loadcfg" => Self::LoadCfg, 1025 | "powerfail" => Self::PowerFail, 1026 | "powergood" => Self::PowerGood, 1027 | "error" => Self::Error, 1028 | "shutdown" => Self::Shutdown, 1029 | "suspend" => Self::Suspend, 1030 | "resume" => Self::Resume, 1031 | "config" => Self::Config, 1032 | "reconfig" => Self::Reconfig, 1033 | "stopping" => Self::Stopping, 1034 | "incompatible" => Self::Incompatible, 1035 | "exception" => Self::Exception, 1036 | _ => return Err("invalid state name"), 1037 | }) 1038 | } 1039 | } 1040 | 1041 | // Structures used in communication, not exposed to user, 1042 | // but pub(crate) for the test suite. 1043 | 1044 | #[derive(FromBytes, IntoBytes, Immutable, Debug)] 1045 | #[repr(C)] 1046 | pub(crate) struct AdsHeader { 1047 | /// 0x0 - ADS command 1048 | /// 0x1 - close port 1049 | /// 0x1000 - open port 1050 | /// 0x1001 - note from router (router state changed) 1051 | /// 0x1002 - get local netid 1052 | pub ams_cmd: u16, 1053 | pub length: U32, 1054 | pub dest_netid: AmsNetId, 1055 | pub dest_port: U16, 1056 | pub src_netid: AmsNetId, 1057 | pub src_port: U16, 1058 | pub command: U16, 1059 | /// 0x01 - response 1060 | /// 0x02 - no return 1061 | /// 0x04 - ADS command 1062 | /// 0x08 - system command 1063 | /// 0x10 - high priority 1064 | /// 0x20 - with time stamp (8 bytes added) 1065 | /// 0x40 - UDP 1066 | /// 0x80 - command during init phase 1067 | /// 0x8000 - broadcast 1068 | pub state_flags: U16, 1069 | pub data_length: U32, 1070 | pub error_code: U32, 1071 | pub invoke_id: U32, 1072 | } 1073 | 1074 | #[derive(FromBytes, IntoBytes, Immutable)] 1075 | #[repr(C)] 1076 | pub(crate) struct DeviceInfoRaw { 1077 | pub major: u8, 1078 | pub minor: u8, 1079 | pub version: U16, 1080 | pub name: [u8; 16], 1081 | } 1082 | 1083 | #[derive(FromBytes, IntoBytes, Immutable)] 1084 | #[repr(C)] 1085 | pub(crate) struct IndexLength { 1086 | pub index_group: U32, 1087 | pub index_offset: U32, 1088 | pub length: U32, 1089 | } 1090 | 1091 | #[derive(FromBytes, IntoBytes, Immutable)] 1092 | #[repr(C)] 1093 | pub(crate) struct ResultLength { 1094 | pub result: U32, 1095 | pub length: U32, 1096 | } 1097 | 1098 | #[derive(FromBytes, IntoBytes, Immutable)] 1099 | #[repr(C)] 1100 | pub(crate) struct IndexLengthRW { 1101 | pub index_group: U32, 1102 | pub index_offset: U32, 1103 | pub read_length: U32, 1104 | pub write_length: U32, 1105 | } 1106 | 1107 | #[derive(FromBytes, IntoBytes, Immutable)] 1108 | #[repr(C)] 1109 | pub(crate) struct ReadState { 1110 | pub ads_state: U16, 1111 | pub dev_state: U16, 1112 | } 1113 | 1114 | #[derive(FromBytes, IntoBytes, Immutable)] 1115 | #[repr(C)] 1116 | pub(crate) struct WriteControl { 1117 | pub ads_state: U16, 1118 | pub dev_state: U16, 1119 | pub data_length: U32, 1120 | } 1121 | 1122 | #[derive(FromBytes, IntoBytes, Immutable)] 1123 | #[repr(C)] 1124 | pub(crate) struct AddNotif { 1125 | pub index_group: U32, 1126 | pub index_offset: U32, 1127 | pub length: U32, 1128 | pub trans_mode: U32, 1129 | pub max_delay: U32, 1130 | pub cycle_time: U32, 1131 | pub reserved: [u8; 16], 1132 | } 1133 | 1134 | /// A single request for a [`Device::read_multi`] request. 1135 | pub struct ReadRequest<'buf> { 1136 | req: IndexLength, 1137 | res: ResultLength, 1138 | rbuf: &'buf mut [u8], 1139 | } 1140 | 1141 | impl<'buf> ReadRequest<'buf> { 1142 | /// Create the request with given index group, index offset and result buffer. 1143 | pub fn new(index_group: u32, index_offset: u32, buffer: &'buf mut [u8]) -> Self { 1144 | Self { 1145 | req: IndexLength { 1146 | index_group: U32::new(index_group), 1147 | index_offset: U32::new(index_offset), 1148 | length: U32::new(buffer.len() as u32), 1149 | }, 1150 | res: ResultLength::new_zeroed(), 1151 | rbuf: buffer, 1152 | } 1153 | } 1154 | 1155 | /// Get the actual returned data. 1156 | /// 1157 | /// If the request returned an error, returns Err. 1158 | pub fn data(&self) -> Result<&[u8]> { 1159 | if self.res.result.get() != 0 { 1160 | ads_error("multi-read data", self.res.result.get()) 1161 | } else { 1162 | Ok(&self.rbuf[..self.res.length.get() as usize]) 1163 | } 1164 | } 1165 | } 1166 | 1167 | /// A single request for a [`Device::write_multi`] request. 1168 | pub struct WriteRequest<'buf> { 1169 | req: IndexLength, 1170 | res: U32, 1171 | wbuf: &'buf [u8], 1172 | } 1173 | 1174 | impl<'buf> WriteRequest<'buf> { 1175 | /// Create the request with given index group, index offset and input buffer. 1176 | pub fn new(index_group: u32, index_offset: u32, buffer: &'buf [u8]) -> Self { 1177 | Self { 1178 | req: IndexLength { 1179 | index_group: U32::new(index_group), 1180 | index_offset: U32::new(index_offset), 1181 | length: U32::new(buffer.len() as u32), 1182 | }, 1183 | res: U32::default(), 1184 | wbuf: buffer, 1185 | } 1186 | } 1187 | 1188 | /// Verify that the data was successfully written. 1189 | /// 1190 | /// If the request returned an error, returns Err. 1191 | pub fn ensure(&self) -> Result<()> { 1192 | if self.res.get() != 0 { 1193 | ads_error("multi-write data", self.res.get()) 1194 | } else { 1195 | Ok(()) 1196 | } 1197 | } 1198 | } 1199 | 1200 | /// A single request for a [`Device::write_read_multi`] request. 1201 | pub struct WriteReadRequest<'buf> { 1202 | req: IndexLengthRW, 1203 | res: ResultLength, 1204 | wbuf: &'buf [u8], 1205 | rbuf: &'buf mut [u8], 1206 | } 1207 | 1208 | impl<'buf> WriteReadRequest<'buf> { 1209 | /// Create the request with given index group, index offset and input and 1210 | /// result buffers. 1211 | pub fn new( 1212 | index_group: u32, index_offset: u32, write_buffer: &'buf [u8], read_buffer: &'buf mut [u8], 1213 | ) -> Self { 1214 | Self { 1215 | req: IndexLengthRW { 1216 | index_group: U32::new(index_group), 1217 | index_offset: U32::new(index_offset), 1218 | read_length: U32::new(read_buffer.len() as u32), 1219 | write_length: U32::new(write_buffer.len() as u32), 1220 | }, 1221 | res: ResultLength::new_zeroed(), 1222 | wbuf: write_buffer, 1223 | rbuf: read_buffer, 1224 | } 1225 | } 1226 | 1227 | /// Get the actual returned data. 1228 | /// 1229 | /// If the request returned an error, returns Err. 1230 | pub fn data(&self) -> Result<&[u8]> { 1231 | if self.res.result.get() != 0 { 1232 | ads_error("multi-read/write data", self.res.result.get()) 1233 | } else { 1234 | Ok(&self.rbuf[..self.res.length.get() as usize]) 1235 | } 1236 | } 1237 | } 1238 | 1239 | /// A single request for a [`Device::add_notification_multi`] request. 1240 | pub struct AddNotifRequest { 1241 | req: AddNotif, 1242 | res: ResultLength, // length is the handle 1243 | } 1244 | 1245 | impl AddNotifRequest { 1246 | /// Create the request with given index group, index offset and notification 1247 | /// attributes. 1248 | pub fn new(index_group: u32, index_offset: u32, attributes: ¬if::Attributes) -> Self { 1249 | Self { 1250 | req: AddNotif { 1251 | index_group: U32::new(index_group), 1252 | index_offset: U32::new(index_offset), 1253 | length: U32::new(attributes.length as u32), 1254 | trans_mode: U32::new(attributes.trans_mode as u32), 1255 | max_delay: U32::new(attributes.max_delay.as_millis() as u32), 1256 | cycle_time: U32::new(attributes.cycle_time.as_millis() as u32), 1257 | reserved: [0; 16], 1258 | }, 1259 | res: ResultLength::new_zeroed(), 1260 | } 1261 | } 1262 | 1263 | /// Get the returned notification handle. 1264 | /// 1265 | /// If the request returned an error, returns Err. 1266 | pub fn handle(&self) -> Result { 1267 | if self.res.result.get() != 0 { 1268 | ads_error("multi-read/write data", self.res.result.get()) 1269 | } else { 1270 | Ok(self.res.length.get()) 1271 | } 1272 | } 1273 | } 1274 | 1275 | /// A single request for a [`Device::delete_notification_multi`] request. 1276 | pub struct DelNotifRequest { 1277 | req: U32, 1278 | res: U32, 1279 | } 1280 | 1281 | impl DelNotifRequest { 1282 | /// Create the request with given index group, index offset and notification 1283 | /// attributes. 1284 | pub fn new(handle: notif::Handle) -> Self { 1285 | Self { req: U32::new(handle), res: U32::default() } 1286 | } 1287 | 1288 | /// Verify that the handle was successfully deleted. 1289 | /// 1290 | /// If the request returned an error, returns Err. 1291 | pub fn ensure(&self) -> Result<()> { 1292 | if self.res.get() != 0 { 1293 | ads_error("multi-read/write data", self.res.get()) 1294 | } else { 1295 | Ok(()) 1296 | } 1297 | } 1298 | } 1299 | 1300 | fn fixup_write_read_return_buffers(requests: &mut [WriteReadRequest]) { 1301 | // Calculate the initial (using buffer sizes) and actual (using result 1302 | // sizes) offsets of each request. 1303 | let offsets = requests 1304 | .iter() 1305 | .scan((0, 0), |(init_cum, act_cum), req| { 1306 | let (init, act) = (req.rbuf.len(), req.res.length.get() as usize); 1307 | let current = Some((*init_cum, *act_cum, init, act)); 1308 | assert!(init >= act); 1309 | *init_cum += init; 1310 | *act_cum += act; 1311 | current 1312 | }) 1313 | .collect_vec(); 1314 | 1315 | // Go through the buffers in reverse order. 1316 | for i in (0..requests.len()).rev() { 1317 | let (my_initial, my_actual, _, mut size) = offsets[i]; 1318 | if size == 0 { 1319 | continue; 1320 | } 1321 | if my_initial == my_actual { 1322 | // Offsets match, no further action required since all 1323 | // previous buffers must be of full length too. 1324 | break; 1325 | } 1326 | 1327 | // Check in which buffer our last byte is. 1328 | let mut j = offsets[..i + 1] 1329 | .iter() 1330 | .rposition(|r| r.0 < my_actual + size) 1331 | .expect("index must be somewhere"); 1332 | let mut j_end = my_actual + size - offsets[j].0; 1333 | 1334 | // Copy the required number of bytes from every buffer from j up to i. 1335 | loop { 1336 | let n = j_end.min(size); 1337 | size -= n; 1338 | if i == j { 1339 | requests[i].rbuf.copy_within(j_end - n..j_end, size); 1340 | } else { 1341 | let (first, second) = requests.split_at_mut(i); 1342 | second[0].rbuf[size..][..n].copy_from_slice(&first[j].rbuf[j_end - n..j_end]); 1343 | } 1344 | if size == 0 { 1345 | break; 1346 | } 1347 | j -= 1; 1348 | j_end = offsets[j].2; 1349 | } 1350 | } 1351 | } 1352 | 1353 | #[test] 1354 | fn test_fixup_buffers() { 1355 | let mut buf0 = *b"12345678AB"; 1356 | let mut buf1 = *b"CDEFabc"; 1357 | let mut buf2 = *b"dxyUVW"; 1358 | let mut buf3 = *b"XYZY"; 1359 | let mut buf4 = *b"XW----"; 1360 | let mut buf5 = *b"-------------"; 1361 | let reqs = &mut [ 1362 | WriteReadRequest::new(0, 0, &[], &mut buf0), 1363 | WriteReadRequest::new(0, 0, &[], &mut buf1), 1364 | WriteReadRequest::new(0, 0, &[], &mut buf2), 1365 | WriteReadRequest::new(0, 0, &[], &mut buf3), 1366 | WriteReadRequest::new(0, 0, &[], &mut buf4), 1367 | WriteReadRequest::new(0, 0, &[], &mut buf5), 1368 | ]; 1369 | reqs[0].res.length.set(8); 1370 | reqs[1].res.length.set(6); 1371 | reqs[2].res.length.set(0); 1372 | reqs[3].res.length.set(4); 1373 | reqs[4].res.length.set(2); 1374 | reqs[5].res.length.set(9); 1375 | 1376 | fixup_write_read_return_buffers(reqs); 1377 | 1378 | assert!(&reqs[5].rbuf[..9] == b"UVWXYZYXW"); 1379 | assert!(&reqs[4].rbuf[..2] == b"xy"); 1380 | assert!(&reqs[3].rbuf[..4] == b"abcd"); 1381 | assert!(&reqs[1].rbuf[..6] == b"ABCDEF"); 1382 | assert!(&reqs[0].rbuf[..8] == b"12345678"); 1383 | } 1384 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Defines ADS error types. 2 | 3 | /// Result alias for `ads::Error`. 4 | pub type Result = std::result::Result; 5 | 6 | /// A collection of different errors that can happen with ADS requests. 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum Error { 9 | /// An IO error occurred. 10 | #[error("{0}: {1}")] 11 | Io(&'static str, std::io::Error), 12 | 13 | /// The ADS server responded with an error code. 14 | #[error("{0}: {1} ({2:#x})")] 15 | Ads(&'static str, &'static str, u32), 16 | 17 | /// An unexpected or inconsistent reply was received. 18 | #[error("{0}: {1} ({2})")] 19 | Reply(&'static str, &'static str, u32), 20 | 21 | /// A value exceeds the allowed 32 bits for ADS. 22 | #[error("data length or duration exceeds 32 bits")] 23 | Overflow(#[from] std::num::TryFromIntError), 24 | } 25 | 26 | pub(crate) trait ErrContext { 27 | type Success; 28 | fn ctx(self, context: &'static str) -> Result; 29 | } 30 | 31 | impl ErrContext for std::result::Result { 32 | type Success = T; 33 | fn ctx(self, context: &'static str) -> Result { 34 | self.map_err(|e| Error::Io(context, e)) 35 | } 36 | } 37 | 38 | /// The list of known ADS error codes from 39 | /// [here](https://infosys.beckhoff.com/content/1033/tc3_ads_intro_howto/374277003.html?id=2736996179007627436). 40 | pub const ADS_ERRORS: &[(u32, &str)] = &[ 41 | (0x001, "Internal error"), 42 | (0x002, "No real-time"), 43 | (0x003, "Allocation locked - memory error"), 44 | (0x004, "Mailbox full - ADS message could not be sent"), 45 | (0x005, "Wrong receive HMSG"), 46 | (0x006, "Target port not found, possibly ADS server not started"), 47 | (0x007, "Target machine not found, possibly missing ADS routes"), 48 | (0x008, "Unknown command ID"), 49 | (0x009, "Invalid task ID"), 50 | (0x00A, "No IO"), 51 | (0x00B, "Unknown AMS command"), 52 | (0x00C, "Win32 error"), 53 | (0x00D, "Port not connected"), 54 | (0x00E, "Invalid AMS length"), 55 | (0x00F, "Invalid AMS NetID"), 56 | (0x010, "Low installation level"), 57 | (0x011, "No debugging available"), 58 | (0x012, "Port disabled - system service not started"), 59 | (0x013, "Port already connected"), 60 | (0x014, "AMS Sync Win32 error"), 61 | (0x015, "AMS Sync timeout"), 62 | (0x016, "AMS Sync error"), 63 | (0x017, "AMS Sync no index map"), 64 | (0x018, "Invalid AMS port"), 65 | (0x019, "No memory"), 66 | (0x01A, "TCP send error"), 67 | (0x01B, "Host unreachable"), 68 | (0x01C, "Invalid AMS fragment"), 69 | (0x01D, "TLS send error - secure ADS connection failed"), 70 | (0x01E, "Access denied - secure ADS access denied"), 71 | (0x500, "Router: no locked memory"), 72 | (0x501, "Router: memory size could not be changed"), 73 | (0x502, "Router: mailbox full"), 74 | (0x503, "Router: debug mailbox full"), 75 | (0x504, "Router: port type is unknown"), 76 | (0x505, "Router is not initialized"), 77 | (0x506, "Router: desired port number is already assigned"), 78 | (0x507, "Router: port not registered"), 79 | (0x508, "Router: maximum number of ports reached"), 80 | (0x509, "Router: port is invalid"), 81 | (0x50A, "Router is not active"), 82 | (0x50B, "Router: mailbox full for fragmented messages"), 83 | (0x50C, "Router: fragment timeout occurred"), 84 | (0x50D, "Router: port removed"), 85 | (0x700, "General device error"), 86 | (0x701, "Service is not supported by server"), 87 | (0x702, "Invalid index group"), 88 | (0x703, "Invalid index offset"), 89 | (0x704, "Reading/writing not permitted"), 90 | (0x705, "Parameter size not correct"), 91 | (0x706, "Invalid parameter value(s)"), 92 | (0x707, "Device is not in a ready state"), 93 | (0x708, "Device is busy"), 94 | (0x709, "Invalid OS context -> use multi-task data access"), 95 | (0x70A, "Out of memory"), 96 | (0x70B, "Invalid parameter value(s)"), 97 | (0x70C, "Not found (files, ...)"), 98 | (0x70D, "Syntax error in command or file"), 99 | (0x70E, "Objects do not match"), 100 | (0x70F, "Object already exists"), 101 | (0x710, "Symbol not found"), 102 | (0x711, "Symbol version invalid -> create a new handle"), 103 | (0x712, "Server is in an invalid state"), 104 | (0x713, "AdsTransMode not supported"), 105 | (0x714, "Notification handle is invalid"), 106 | (0x715, "Notification client not registered"), 107 | (0x716, "No more notification handles"), 108 | (0x717, "Notification size too large"), 109 | (0x718, "Device not initialized"), 110 | (0x719, "Device has a timeout"), 111 | (0x71A, "Query interface failed"), 112 | (0x71B, "Wrong interface required"), 113 | (0x71C, "Class ID is invalid"), 114 | (0x71D, "Object ID is invalid"), 115 | (0x71E, "Request is pending"), 116 | (0x71F, "Request is aborted"), 117 | (0x720, "Signal warning"), 118 | (0x721, "Invalid array index"), 119 | (0x722, "Symbol not active -> release handle and try again"), 120 | (0x723, "Access denied"), 121 | (0x724, "No license found -> activate license"), 122 | (0x725, "License expired"), 123 | (0x726, "License exceeded"), 124 | (0x727, "License invalid"), 125 | (0x728, "Invalid system ID in license"), 126 | (0x729, "License not time limited"), 127 | (0x72A, "License issue time in the future"), 128 | (0x72B, "License time period too long"), 129 | (0x72C, "Exception in device specific code -> check each device"), 130 | (0x72D, "License file read twice"), 131 | (0x72E, "Invalid signature"), 132 | (0x72F, "Invalid public key certificate"), 133 | (0x730, "Public key not known from OEM"), 134 | (0x731, "License not valid for this system ID"), 135 | (0x732, "Demo license prohibited"), 136 | (0x733, "Invalid function ID"), 137 | (0x734, "Outside the valid range"), 138 | (0x735, "Invalid alignment"), 139 | (0x736, "Invalid platform level"), 140 | (0x737, "Context - forward to passive level"), 141 | (0x738, "Content - forward to dispatch level"), 142 | (0x739, "Context - forward to real-time"), 143 | (0x740, "General client error"), 144 | (0x741, "Invalid parameter at service"), 145 | (0x742, "Polling list is empty"), 146 | (0x743, "Var connection already in use"), 147 | (0x744, "Invoke ID in use"), 148 | (0x745, "Timeout elapsed -> check route setting"), 149 | (0x746, "Error in Win32 subsystem"), 150 | (0x747, "Invalid client timeout value"), 151 | (0x748, "ADS port not opened"), 152 | (0x749, "No AMS address"), 153 | (0x750, "Internal error in ADS sync"), 154 | (0x751, "Hash table overflow"), 155 | (0x752, "Key not found in hash"), 156 | (0x753, "No more symbols in cache"), 157 | (0x754, "Invalid response received"), 158 | (0x755, "Sync port is locked"), 159 | (0x1000, "Internal error in real-time system"), 160 | (0x1001, "Timer value not valid"), 161 | (0x1002, "Task pointer has invalid value 0"), 162 | (0x1003, "Stack pointer has invalid value 0"), 163 | (0x1004, "Requested task priority already assigned"), 164 | (0x1005, "No free Task Control Block"), 165 | (0x1006, "No free semaphores"), 166 | (0x1007, "No free space in the queue"), 167 | (0x100D, "External sync interrupt already applied"), 168 | (0x100E, "No external sync interrupt applied"), 169 | (0x100F, "External sync interrupt application failed"), 170 | (0x1010, "Call of service function in wrong context"), 171 | (0x1017, "Intel VT-x not supported"), 172 | (0x1018, "Intel VT-x not enabled in BIOS"), 173 | (0x1019, "Missing function in Intel VT-x"), 174 | (0x101A, "Activation of Intel VT-x failed"), 175 | ]; 176 | 177 | /// Return an `Error` corresponding to the given ADS result code. 178 | pub fn ads_error(action: &'static str, err: u32) -> Result { 179 | match ADS_ERRORS.binary_search_by_key(&err, |e| e.0) { 180 | Ok(idx) => Err(Error::Ads(action, ADS_ERRORS[idx].1, err)), 181 | Err(_) => Err(Error::Ads(action, "Unknown error code", err)), 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/file.rs: -------------------------------------------------------------------------------- 1 | //! File access over ADS. 2 | 3 | use std::io; 4 | 5 | use crate::index; 6 | use crate::{Device, Error, Result}; 7 | use byteorder::{ByteOrder, LE}; 8 | 9 | /// A file opened within the PLC. Files implement `Read` and `Write`, so they 10 | /// can be used like normal files in Rust APIs. 11 | /// 12 | /// The file is closed automatically on drop. 13 | pub struct File<'c> { 14 | device: Device<'c>, 15 | handle: u32, 16 | } 17 | 18 | impl<'c> File<'c> { 19 | /// Open a file. `flags` must be combined from the constants in this module. 20 | pub fn open(device: Device<'c>, filename: impl AsRef<[u8]>, flags: u32) -> Result { 21 | let mut hdl = [0; 4]; 22 | device.write_read_exact(index::FILE_OPEN, flags, filename.as_ref(), &mut hdl)?; 23 | Ok(File { device, handle: u32::from_le_bytes(hdl) }) 24 | } 25 | 26 | /// Delete a file. `flags` must be combined from the constants in this module. 27 | pub fn delete(device: Device, filename: impl AsRef<[u8]>, flags: u32) -> Result<()> { 28 | device 29 | .write_read(index::FILE_DELETE, flags, filename.as_ref(), &mut []) 30 | .map(drop) 31 | } 32 | } 33 | 34 | /// Return a list of files in the named directory. 35 | /// 36 | /// Returned tuples are (name, attributes, size). Returned filenames are not String 37 | /// since they are likely encoded in Windows-1252. 38 | pub fn listdir(device: Device, dirname: impl AsRef<[u8]>) -> Result, u32, u64)>> { 39 | let mut files = Vec::new(); 40 | let mut buf = [0; 324]; 41 | // Initial offset. Offset 4 would start at the TwinCAT Boot directory instead. 42 | let mut offset = 1; 43 | // Initial argument, should be empty in later calls. 44 | let mut argument = dirname.as_ref().to_vec(); 45 | argument.extend(b"\\*.*"); 46 | 47 | loop { 48 | match device.write_read_exact(index::FILE_BROWSE, offset, &argument, &mut buf) { 49 | Ok(_) => { 50 | let attrs = LE::read_u32(&buf[4..8]); 51 | let sizeh = LE::read_u32(&buf[32..36]); 52 | let sizel = LE::read_u32(&buf[36..40]); 53 | let size = (sizeh as u64) << 32 | sizel as u64; 54 | let name = buf[48..].iter().copied().take_while(|&b| b != 0).collect(); 55 | files.push((name, attrs, size)); 56 | } 57 | // Error "not found" means the end of the list. 58 | Err(Error::Ads(_, _, 0x70c)) => return Ok(files), 59 | Err(e) => return Err(e), 60 | } 61 | offset = LE::read_u32(&buf[..4]); 62 | argument.clear(); 63 | } 64 | } 65 | 66 | impl io::Write for File<'_> { 67 | fn write(&mut self, data: &[u8]) -> io::Result { 68 | self.device 69 | .write_read(index::FILE_WRITE, self.handle, data, &mut []) 70 | // need to convert errors back to io::Error 71 | .map_err(map_error) 72 | // no info about written length is returned 73 | .map(|_| data.len()) 74 | } 75 | 76 | fn flush(&mut self) -> std::io::Result<()> { 77 | Ok(()) 78 | } 79 | } 80 | 81 | impl std::io::Read for File<'_> { 82 | fn read(&mut self, data: &mut [u8]) -> io::Result { 83 | self.device 84 | .write_read(index::FILE_READ, self.handle, &[], data) 85 | .map_err(map_error) 86 | } 87 | } 88 | 89 | impl Drop for File<'_> { 90 | fn drop(&mut self) { 91 | let _ = self.device.write_read(index::FILE_CLOSE, self.handle, &[], &mut []); 92 | } 93 | } 94 | 95 | // Map an ads::Error to an io::Error, trying to keep semantics where possible 96 | fn map_error(e: Error) -> io::Error { 97 | match e { 98 | Error::Io(_, io_error) => io_error, 99 | Error::Ads(_, _, 0x704) => io::ErrorKind::InvalidInput.into(), 100 | Error::Ads(_, _, 0x70C) => io::ErrorKind::NotFound.into(), 101 | _ => io::Error::new(io::ErrorKind::Other, e.to_string()), 102 | } 103 | } 104 | 105 | /// File read mode. 106 | pub const READ: u32 = 1 << 0; 107 | /// File write mode. 108 | pub const WRITE: u32 = 1 << 1; 109 | /// File append mode. 110 | pub const APPEND: u32 = 1 << 2; 111 | /// Unknown. 112 | pub const PLUS: u32 = 1 << 3; 113 | /// Binary file mode. 114 | pub const BINARY: u32 = 1 << 4; 115 | /// Text file mode. 116 | pub const TEXT: u32 = 1 << 5; 117 | /// Unknown. 118 | pub const ENSURE_DIR: u32 = 1 << 6; 119 | /// Unknown. 120 | pub const ENABLE_DIR: u32 = 1 << 7; 121 | /// Unknown. 122 | pub const OVERWRITE: u32 = 1 << 8; 123 | /// Unknown. 124 | pub const OVERWRITE_RENAME: u32 = 1 << 9; 125 | 126 | /// File attribute bit for directories. 127 | pub const DIRECTORY: u32 = 0x10; 128 | -------------------------------------------------------------------------------- /src/index.rs: -------------------------------------------------------------------------------- 1 | //! Well-known ADS index groups. 2 | //! 3 | //! These are defined 4 | //! [here](https://infosys.beckhoff.com/content/1033/tc3_ads_intro/117241867.html?id=1944752650545554679) 5 | //! and [here](https://github.com/Beckhoff/ADS/blob/master/AdsLib/standalone/AdsDef.h). 6 | 7 | // Unfortunately, not all those constants are documented. 8 | #![allow(missing_docs)] 9 | 10 | /// PLC: Read/write PLC memory (%M fields). 11 | pub const PLC_RW_M: u32 = 0x4020; 12 | /// PLC: Read/write PLC memory as bits (%MX fields). Offset is (byte*8 + bit) address. 13 | pub const PLC_RW_MX: u32 = 0x4021; 14 | /// PLC: Read byte length of %M area (only offset 0). 15 | pub const PLC_SIZE_M: u32 = 0x4025; 16 | /// PLC: Read/write retain data area. 17 | pub const PLC_RW_RB: u32 = 0x4030; 18 | /// PLC: Read byte length of the retain data area (only offset 0). 19 | pub const PLC_SIZE_RB: u32 = 0x4035; 20 | /// PLC: Read/write data area. 21 | pub const PLC_RW_DB: u32 = 0x4040; 22 | /// PLC: Read byte length of data area (only offset 0). 23 | pub const PLC_SIZE_DB: u32 = 0x4045; 24 | 25 | /// Get u32 handle to the name in the write data. Index offset is 0. 26 | /// Use with a `write_read` transaction. 27 | pub const GET_SYMHANDLE_BYNAME: u32 = 0xF003; 28 | /// Read/write data for a symbol by handle. 29 | /// Use the handle as the index offset. 30 | pub const RW_SYMVAL_BYHANDLE: u32 = 0xF005; 31 | /// Release a symbol handle. Index offset is 0. 32 | pub const RELEASE_SYMHANDLE: u32 = 0xF006; 33 | 34 | // undocumented; from AdsDef.h 35 | pub const SYMTAB: u32 = 0xF000; 36 | pub const SYMNAME: u32 = 0xF001; 37 | pub const SYMVAL: u32 = 0xF002; 38 | pub const GET_SYMVAL_BYNAME: u32 = 0xF004; 39 | pub const GET_SYMINFO_BYNAME: u32 = 0xF007; 40 | pub const GET_SYMVERSION: u32 = 0xF008; 41 | pub const GET_SYMINFO_BYNAME_EX: u32 = 0xF009; 42 | pub const SYM_DOWNLOAD: u32 = 0xF00A; 43 | pub const SYM_UPLOAD: u32 = 0xF00B; 44 | pub const SYM_UPLOAD_INFO: u32 = 0xF00C; 45 | pub const SYM_DOWNLOAD2: u32 = 0xF00D; 46 | pub const SYM_DT_UPLOAD: u32 = 0xF00E; 47 | pub const SYM_UPLOAD_INFO2: u32 = 0xF00F; 48 | pub const SYM_NOTE: u32 = 0xF010; 49 | 50 | /// Read/write process image of physical inputs (%I fields). 51 | pub const IO_RW_I: u32 = 0xF020; 52 | /// Read/write process image of physical inputs as bits (%IX fields). 53 | pub const IO_RW_IX: u32 = 0xF021; 54 | /// Read byte length of the physical inputs (only offset 0). 55 | pub const IO_SIZE_I: u32 = 0xF025; 56 | 57 | /// Read/write process image of physical outputs (%Q fields). 58 | pub const IO_RW_Q: u32 = 0xF030; 59 | /// Read/write process image of physical outputs as bits (%QX fields). 60 | pub const IO_RW_QX: u32 = 0xF031; 61 | /// Read byte length of the physical outputs (only offset 0). 62 | pub const IO_SIZE_Q: u32 = 0xF035; 63 | 64 | pub const IO_CLEAR_I: u32 = 0xF040; 65 | pub const IO_CLEAR_O: u32 = 0xF050; 66 | pub const IO_RW_IOB: u32 = 0xF060; 67 | 68 | /// Combine multiple index group/offset reads. 69 | /// See Beckhoff docs for the format of the data. 70 | pub const SUMUP_READ: u32 = 0xF080; 71 | /// Combine multiple index group/offset writes. 72 | /// See Beckhoff docs for the format of the data. 73 | pub const SUMUP_WRITE: u32 = 0xF081; 74 | /// Combine multiple index group/offset write+reads. 75 | /// See Beckhoff docs for the format of the data. 76 | pub const SUMUP_READWRITE: u32 = 0xF082; 77 | /// Combine multiple index group/offset reads. 78 | /// See Beckhoff docs for the format of the data. 79 | pub const SUMUP_READ_EX: u32 = 0xF083; 80 | /// Combine multiple index group/offset reads. 81 | /// See Beckhoff docs for the format of the data. 82 | pub const SUMUP_READ_EX_2: u32 = 0xF084; 83 | /// Combine multiple device notification adds. 84 | /// See Beckhoff docs for the format of the data. 85 | pub const SUMUP_ADDDEVNOTE: u32 = 0xF085; 86 | /// Combine multiple device notification deletes. 87 | /// See Beckhoff docs for the format of the data. 88 | pub const SUMUP_DELDEVNOTE: u32 = 0xF086; 89 | 90 | pub const DEVICE_DATA: u32 = 0xF100; 91 | 92 | /// File service: open a file. 93 | pub const FILE_OPEN: u32 = 120; 94 | /// File service: close an open file. 95 | pub const FILE_CLOSE: u32 = 121; 96 | /// File service: read from an open file. 97 | pub const FILE_READ: u32 = 122; 98 | /// File service: write to an open file. 99 | pub const FILE_WRITE: u32 = 123; 100 | /// File service: delete a file. 101 | pub const FILE_DELETE: u32 = 131; 102 | /// File service: browse files. 103 | pub const FILE_BROWSE: u32 = 133; 104 | 105 | /// Index group for target desc query. 106 | pub const TARGET_DESC: u32 = 0x2bc; 107 | 108 | /// Index group for license queries. 109 | pub const LICENSE: u32 = 0x0101_0004; 110 | pub const LICENSE_MODULES: u32 = 0x0101_0006; 111 | 112 | // Diverse officially undocumented ports, used with the system service. 113 | pub const WIN_REGISTRY: u32 = 200; 114 | pub const EXECUTE: u32 = 500; 115 | pub const TC_TARGET_XML: u32 = 700; 116 | pub const ROUTE_ADD: u32 = 801; 117 | pub const ROUTE_REMOVE: u32 = 802; 118 | pub const ROUTE_LIST: u32 = 803; 119 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Work with PLCs using the ADS protocol 2 | //! 3 | //! # Introduction 4 | //! 5 | //! ADS is the native protocol used by programmable logic controllers (PLCs) and 6 | //! the TwinCAT automation system produced by [Beckhoff GmbH](https://www.beckhoff.com/). 7 | //! 8 | //! The [specification](https://infosys.beckhoff.de/content/1031/tc3_adscommon/html/tcadscommon_introads.htm) 9 | //! can be found on their Information System pages. 10 | //! 11 | //! # Example 12 | //! 13 | //! ```no_run 14 | //! # fn main() -> ads::Result<()> { 15 | //! // Open a connection to an ADS device identified by hostname/IP and port. 16 | //! // For TwinCAT devices, a route must be set to allow the client to connect. 17 | //! // The source AMS address is automatically generated from the local IP, 18 | //! // but can be explicitly specified as the third argument. 19 | //! let client = ads::Client::new(("plchost", ads::PORT), ads::Timeouts::none(), 20 | //! ads::Source::Auto)?; 21 | //! // On Windows, when connecting to a TwinCAT instance running on the same 22 | //! // machine, use the following to connect: 23 | //! let client = ads::Client::new(("127.0.0.1", ads::PORT), ads::Timeouts::none(), 24 | //! ads::Source::Request)?; 25 | //! 26 | //! // Specify the target ADS device to talk to, by NetID and AMS port. 27 | //! // Port 851 usually refers to the first PLC instance. 28 | //! let device = client.device(ads::AmsAddr::new([5, 32, 116, 5, 1, 1].into(), 851)); 29 | //! 30 | //! // Ensure that the PLC instance is running. 31 | //! assert!(device.get_state()?.0 == ads::AdsState::Run); 32 | //! 33 | //! // Request a handle to a named symbol in the PLC instance. 34 | //! let handle = ads::Handle::new(device, "MY_SYMBOL")?; 35 | //! 36 | //! // Read data in form of an u32 from the handle. 37 | //! let value: u32 = handle.read_value()?; 38 | //! println!("MY_SYMBOL value is {}", value); 39 | //! # Ok(()) 40 | //! # } 41 | //! ``` 42 | 43 | #![deny(missing_docs)] 44 | #![cfg_attr(not(test), deny(clippy::unwrap_used))] 45 | 46 | pub mod client; 47 | pub mod errors; 48 | pub mod file; 49 | pub mod index; 50 | pub mod netid; 51 | pub mod notif; 52 | pub mod ports; 53 | pub mod strings; 54 | pub mod symbol; 55 | #[cfg(test)] 56 | mod test; 57 | pub mod udp; 58 | 59 | pub use client::{AdsState, Client, Device, Source, Timeouts}; 60 | pub use errors::{Error, Result}; 61 | pub use file::File; 62 | pub use netid::{AmsAddr, AmsNetId, AmsPort}; 63 | pub use symbol::Handle; 64 | 65 | /// The default port for TCP communication. 66 | pub const PORT: u16 = 0xBF02; 67 | /// The default port for UDP communication. 68 | pub const UDP_PORT: u16 = 0xBF03; 69 | -------------------------------------------------------------------------------- /src/netid.rs: -------------------------------------------------------------------------------- 1 | //! Contains the AMS NetId and related types. 2 | 3 | use std::convert::TryInto; 4 | use std::fmt::{self, Display}; 5 | use std::io::{Read, Write}; 6 | use std::net::Ipv4Addr; 7 | use std::str::FromStr; 8 | 9 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 10 | use itertools::Itertools; 11 | use zerocopy::{FromBytes, Immutable, IntoBytes}; 12 | 13 | /// Represents an AMS NetID. 14 | /// 15 | /// The NetID consists of 6 bytes commonly written like an IPv4 address, i.e. 16 | /// `1.2.3.4.5.6`. Together with an AMS port (16-bit integer), it uniquely 17 | /// identifies an endpoint of an ADS system that can be communicated with. 18 | /// 19 | /// Although often the first 4 bytes of a NetID look like an IP address, and 20 | /// sometimes even are identical to the device's IP address, there is no 21 | /// requirement for this, and one should never rely on it. 22 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Debug, FromBytes, IntoBytes, Immutable)] 23 | #[repr(C)] 24 | pub struct AmsNetId(pub [u8; 6]); 25 | 26 | /// An AMS port is, similar to an IP port, a 16-bit integer. 27 | pub type AmsPort = u16; 28 | 29 | impl AmsNetId { 30 | /// Create a NetID from six bytes. 31 | pub const fn new(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8) -> Self { 32 | AmsNetId([a, b, c, d, e, f]) 33 | } 34 | 35 | /// Return the "local NetID", `127.0.0.1.1.1`. 36 | pub const fn local() -> Self { 37 | AmsNetId([127, 0, 0, 1, 1, 1]) 38 | } 39 | 40 | /// Create a NetID from a slice (which must have length 6). 41 | pub fn from_slice(slice: &[u8]) -> Option { 42 | Some(AmsNetId(slice.try_into().ok()?)) 43 | } 44 | 45 | /// Create a NetID from an IPv4 address and two additional octets. 46 | pub fn from_ip(ip: Ipv4Addr, e: u8, f: u8) -> Self { 47 | let [a, b, c, d] = ip.octets(); 48 | Self::new(a, b, c, d, e, f) 49 | } 50 | 51 | /// Check if the NetID is all-zero. 52 | pub fn is_zero(&self) -> bool { 53 | self.0 == [0, 0, 0, 0, 0, 0] 54 | } 55 | } 56 | 57 | impl FromStr for AmsNetId { 58 | type Err = &'static str; 59 | 60 | /// Parse a NetID from a string (`a.b.c.d.e.f`). 61 | /// 62 | /// Bytes can be missing in the end; missing bytes are substituted by 1. 63 | fn from_str(s: &str) -> Result { 64 | let mut arr = [1; 6]; 65 | for (i, part) in s.split('.').enumerate() { 66 | match (arr.get_mut(i), part.parse()) { 67 | (Some(loc), Ok(byte)) => *loc = byte, 68 | _ => return Err("invalid NetID string"), 69 | } 70 | } 71 | Ok(AmsNetId(arr)) 72 | } 73 | } 74 | 75 | impl From<[u8; 6]> for AmsNetId { 76 | fn from(array: [u8; 6]) -> Self { 77 | Self(array) 78 | } 79 | } 80 | 81 | impl Display for AmsNetId { 82 | /// Format a NetID in the usual format. 83 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 84 | write!(f, "{}", self.0.iter().format(".")) 85 | } 86 | } 87 | 88 | /// Combination of an AMS NetID and a port. 89 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] 90 | pub struct AmsAddr(AmsNetId, AmsPort); 91 | 92 | impl AmsAddr { 93 | /// Create a new address from NetID and port. 94 | pub const fn new(netid: AmsNetId, port: AmsPort) -> Self { 95 | Self(netid, port) 96 | } 97 | 98 | /// Return the NetID of this address. 99 | pub const fn netid(&self) -> AmsNetId { 100 | self.0 101 | } 102 | 103 | /// Return the port of this address. 104 | pub const fn port(&self) -> AmsPort { 105 | self.1 106 | } 107 | 108 | /// Write the NetID to a stream. 109 | pub fn write_to(&self, w: &mut W) -> std::io::Result<()> { 110 | w.write_all(&(self.0).0)?; 111 | w.write_u16::(self.1) 112 | } 113 | 114 | /// Read the NetID from a stream. 115 | pub fn read_from(r: &mut R) -> std::io::Result { 116 | let mut netid = [0; 6]; 117 | r.read_exact(&mut netid)?; 118 | let port = r.read_u16::()?; 119 | Ok(Self(AmsNetId(netid), port)) 120 | } 121 | } 122 | 123 | impl FromStr for AmsAddr { 124 | type Err = &'static str; 125 | 126 | /// Parse an AMS address from a string (netid:port). 127 | fn from_str(s: &str) -> Result { 128 | let (addr, port) = s.split(':').collect_tuple().ok_or("invalid AMS addr string")?; 129 | Ok(Self(addr.parse()?, port.parse().map_err(|_| "invalid port number")?)) 130 | } 131 | } 132 | 133 | impl Display for AmsAddr { 134 | /// Format an AMS address in the usual format. 135 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 136 | write!(f, "{}:{}", self.0, self.1) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/notif.rs: -------------------------------------------------------------------------------- 1 | //! Everything to do with ADS notifications. 2 | 3 | use std::io; 4 | use std::time::Duration; 5 | 6 | use byteorder::{ReadBytesExt, LE}; 7 | 8 | use crate::client::AMS_HEADER_SIZE; 9 | use crate::errors::ErrContext; 10 | use crate::{Error, Result}; 11 | 12 | /// A handle to the notification; this can be used to delete the notification later. 13 | pub type Handle = u32; 14 | 15 | /// Attributes for creating a notification. 16 | pub struct Attributes { 17 | /// Length of data the notification is interested in. 18 | pub length: usize, 19 | /// When notification messages should be transmitted. 20 | pub trans_mode: TransmissionMode, 21 | /// The maximum delay between change and transmission. 22 | pub max_delay: Duration, 23 | /// The cycle time for checking for changes. 24 | pub cycle_time: Duration, 25 | } 26 | 27 | impl Attributes { 28 | /// Return new notification attributes. 29 | pub fn new( 30 | length: usize, trans_mode: TransmissionMode, max_delay: Duration, cycle_time: Duration, 31 | ) -> Self { 32 | Self { length, trans_mode, max_delay, cycle_time } 33 | } 34 | } 35 | 36 | /// When notifications should be generated. 37 | #[repr(u32)] 38 | #[derive(Clone, Copy, Debug)] 39 | pub enum TransmissionMode { 40 | /// No transmission. 41 | NoTrans = 0, 42 | /// Notify each server cycle. 43 | ServerCycle = 3, 44 | /// Notify when the content changes. 45 | ServerOnChange = 4, 46 | // Other constants from the C++ library: 47 | // ClientCycle = 1, 48 | // ClientOnChange = 2, 49 | // ServerCycle2 = 5, 50 | // ServerOnChange2 = 6, 51 | // Client1Req = 10, 52 | } 53 | 54 | /// A notification message from the ADS server. 55 | pub struct Notification { 56 | data: Vec, 57 | nstamps: u32, 58 | } 59 | 60 | impl std::fmt::Debug for Notification { 61 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 62 | writeln!(f, "Notification [")?; 63 | for sample in self.samples() { 64 | writeln!(f, " {:?}", sample)?; 65 | } 66 | write!(f, "]") 67 | } 68 | } 69 | 70 | impl Notification { 71 | /// Parse a notification message from an ADS message. 72 | pub fn new(data: impl Into>) -> Result { 73 | // Relevant data starts at byte 42 with the number of stamps. 74 | let data = data.into(); 75 | if data.len() < AMS_HEADER_SIZE + 8 { 76 | // header + length + #stamps 77 | return Err(Error::Io("parsing notification", io::ErrorKind::UnexpectedEof.into())); 78 | } 79 | let mut ptr = &data[AMS_HEADER_SIZE + 4..]; 80 | let nstamps = ptr.read_u32::().ctx("parsing notification")?; 81 | for _ in 0..nstamps { 82 | let _timestamp = ptr.read_u64::().ctx("parsing notification")?; 83 | let nsamples = ptr.read_u32::().ctx("parsing notification")?; 84 | 85 | for _ in 0..nsamples { 86 | let _handle = ptr.read_u32::().ctx("parsing notification")?; 87 | let length = ptr.read_u32::().ctx("parsing notification")? as usize; 88 | if ptr.len() >= length { 89 | ptr = &ptr[length..]; 90 | } else { 91 | return Err(Error::Io("parsing notification", io::ErrorKind::UnexpectedEof.into())); 92 | } 93 | } 94 | } 95 | if ptr.is_empty() { 96 | Ok(Self { data, nstamps }) 97 | } else { 98 | Err(Error::Io("parsing notification", io::ErrorKind::UnexpectedEof.into())) 99 | } 100 | } 101 | 102 | /// Return an iterator over all data samples in this notification. 103 | pub fn samples(&self) -> SampleIter<'_> { 104 | SampleIter { 105 | data: &self.data[46..], 106 | cur_timestamp: 0, 107 | stamps_left: self.nstamps, 108 | samples_left: 0, 109 | } 110 | } 111 | } 112 | 113 | /// A single sample in a notification message. 114 | #[derive(Clone, Debug, PartialEq, Eq)] 115 | pub struct Sample<'a> { 116 | /// The notification handle associated with the data. 117 | pub handle: Handle, 118 | /// Timestamp of generation (nanoseconds since 01/01/1601). 119 | pub timestamp: u64, // TODO: better dtype? 120 | /// Data of the handle at the specified time. 121 | pub data: &'a [u8], 122 | } 123 | 124 | /// An iterator over all samples within a notification message. 125 | pub struct SampleIter<'a> { 126 | data: &'a [u8], 127 | cur_timestamp: u64, 128 | stamps_left: u32, 129 | samples_left: u32, 130 | } 131 | 132 | impl<'a> Iterator for SampleIter<'a> { 133 | type Item = Sample<'a>; 134 | 135 | fn next(&mut self) -> Option { 136 | if self.samples_left > 0 { 137 | // Read more samples from the current stamp. 138 | let handle = self.data.read_u32::().expect("size"); 139 | let length = self.data.read_u32::().expect("size") as usize; 140 | let (data, rest) = self.data.split_at(length); 141 | self.data = rest; 142 | self.samples_left -= 1; 143 | Some(Sample { handle, data, timestamp: self.cur_timestamp }) 144 | } else if self.stamps_left > 0 { 145 | // Go to next stamp. 146 | self.cur_timestamp = self.data.read_u64::().expect("size"); 147 | self.samples_left = self.data.read_u32::().expect("size"); 148 | self.stamps_left -= 1; 149 | self.next() 150 | } else { 151 | // Nothing else here. 152 | None 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/ports.rs: -------------------------------------------------------------------------------- 1 | //! Well known ADS ports as defined 2 | //! [here](https://infosys.beckhoff.com/content/1033/tc3_ads_intro/116159883.html?id=6824734840428332798). 3 | 4 | #![allow(missing_docs)] 5 | 6 | use crate::AmsPort; 7 | 8 | pub const AMS_ROUTER: AmsPort = 1; 9 | pub const AMS_DEBUGGER: AmsPort = 2; 10 | pub const TCOM_SERVER: AmsPort = 10; 11 | pub const TCOM_SERVER_TASK: AmsPort = 11; 12 | pub const TCOM_SERVER_PASSIVE: AmsPort = 12; 13 | pub const TCAT_DEBUGGER: AmsPort = 20; 14 | pub const TCAT_DEBUGGER_TASK: AmsPort = 21; 15 | pub const LICENSE_SERVER: AmsPort = 30; 16 | pub const LOGGER: AmsPort = 100; 17 | pub const EVENT_LOGGER: AmsPort = 110; 18 | pub const APPLICATION: AmsPort = 120; 19 | pub const EVENT_LOGGER_USER: AmsPort = 130; 20 | pub const EVENT_LOGGER_REALTIME: AmsPort = 131; 21 | pub const EVENT_LOGGER_PUBLISHER: AmsPort = 132; 22 | pub const RING0_REALTIME: AmsPort = 200; 23 | pub const RING0_TRACE: AmsPort = 290; 24 | pub const RING0_IO: AmsPort = 300; 25 | pub const RING0_PLC: AmsPort = 400; // legacy 26 | pub const RING0_NC: AmsPort = 500; 27 | pub const RING0_NC_SAF: AmsPort = 501; 28 | pub const RING0_NC_SVB: AmsPort = 511; 29 | pub const NC_INSTANCE: AmsPort = 520; 30 | pub const RING_ISG: AmsPort = 550; 31 | pub const RING0_CNC: AmsPort = 600; 32 | pub const RING0_LINE: AmsPort = 700; 33 | pub const RING0_TC2_PLC: AmsPort = 800; 34 | pub const TC2_PLC_SYSTEM1: AmsPort = 801; 35 | pub const TC2_PLC_SYSTEM2: AmsPort = 811; 36 | pub const TC2_PLC_SYSTEM3: AmsPort = 821; 37 | pub const TC2_PLC_SYSTEM4: AmsPort = 831; 38 | pub const RING0_TC3_PLC: AmsPort = 850; 39 | pub const TC3_PLC_SYSTEM1: AmsPort = 851; 40 | pub const TC3_PLC_SYSTEM2: AmsPort = 852; 41 | pub const TC3_PLC_SYSTEM3: AmsPort = 853; 42 | pub const TC3_PLC_SYSTEM4: AmsPort = 854; // and following 43 | pub const CAMSHAFT_CONTROLLER: AmsPort = 900; 44 | pub const CAM_TOOL: AmsPort = 950; 45 | pub const RING0_IO_PORTS: AmsPort = 1000; // to 1199 46 | pub const RING0_USER: AmsPort = 2000; 47 | pub const CRESTRON_SERVER: AmsPort = 2500; 48 | pub const SYSTEM_SERVICE: AmsPort = 10000; 49 | pub const TCPIP_SERVER: AmsPort = 10201; 50 | pub const SYSTEM_MANAGER: AmsPort = 10300; 51 | pub const SMS_SERVER: AmsPort = 10400; 52 | pub const MODBUS_SERVER: AmsPort = 10500; 53 | pub const AMS_LOGGER: AmsPort = 10502; 54 | pub const XML_DATA_SERVER: AmsPort = 10600; 55 | pub const AUTO_CONFIG: AmsPort = 10700; 56 | pub const PLC_CONTROL: AmsPort = 10800; 57 | pub const FTP_CLIENT: AmsPort = 10900; 58 | pub const NC_CONTROL: AmsPort = 11000; 59 | pub const NC_INTERPRETER: AmsPort = 11500; 60 | pub const GST_INTERPRETER: AmsPort = 11600; 61 | pub const STRECKE_CONTROL: AmsPort = 12000; 62 | pub const CAM_CONTROL: AmsPort = 13000; 63 | pub const SCOPE_SERVER: AmsPort = 14000; 64 | pub const COND_MONITORING: AmsPort = 14100; 65 | pub const SINE_CH1: AmsPort = 15000; 66 | pub const CONTROL_NET: AmsPort = 16000; 67 | pub const OPC_SERVER: AmsPort = 17000; 68 | pub const OPC_CLIENT: AmsPort = 17500; 69 | pub const MAIL_SERVER: AmsPort = 18000; 70 | pub const VIRTUAL_COM: AmsPort = 19000; 71 | pub const MGMT_SERVER: AmsPort = 19100; 72 | pub const MIELE_HOME_SERVER: AmsPort = 19200; 73 | pub const CP_LINK3: AmsPort = 19300; 74 | pub const VISION_SERVICE: AmsPort = 19500; 75 | pub const MULTIUSER: AmsPort = 19600; 76 | pub const DATABASE_SERVER: AmsPort = 21372; 77 | pub const FIAS_SERVER: AmsPort = 25013; 78 | pub const BANG_OLUFSEN_SERVER: AmsPort = 25015; 79 | -------------------------------------------------------------------------------- /src/strings.rs: -------------------------------------------------------------------------------- 1 | //! Const-generic string types for representing fixed-length strings. 2 | 3 | use zerocopy::{FromBytes, Immutable, IntoBytes}; 4 | 5 | /// Represents a fixed-length byte string. 6 | /// 7 | /// This type can be created from a `&str` or `&[u8]` if their byte 8 | /// length does not exceed the fixed length. 9 | /// 10 | /// It can be freely converted from and to a `[u8; N]` array, and 11 | /// to a `Vec` where it will be cut at the first null byte. 12 | /// 13 | /// It can be converted to a Rust `String` if it is UTF8 encoded. 14 | #[repr(C)] 15 | #[derive(Clone, Copy, FromBytes, IntoBytes, Immutable)] 16 | pub struct String([u8; LEN], u8); // one extra NULL byte 17 | 18 | impl String { 19 | /// Create a new empty string. 20 | pub fn new() -> Self { 21 | Self([0; LEN], 0) 22 | } 23 | 24 | /// Return the number of bytes up to the first null byte. 25 | pub fn len(&self) -> usize { 26 | self.0.iter().position(|&b| b == 0).unwrap_or(self.0.len()) 27 | } 28 | 29 | /// Returns true if the string is empty. 30 | pub fn is_empty(&self) -> bool { 31 | self.len() == 0 32 | } 33 | 34 | /// Get the slice up to the first null byte. 35 | pub fn as_bytes(&self) -> &[u8] { 36 | &self.0[..self.len()] 37 | } 38 | 39 | /// Get a reference to the full array of bytes. 40 | pub fn backing_array(&mut self) -> &mut [u8; LEN] { 41 | &mut self.0 42 | } 43 | } 44 | 45 | // standard traits 46 | 47 | impl std::default::Default for String { 48 | fn default() -> Self { 49 | Self::new() 50 | } 51 | } 52 | 53 | impl std::fmt::Debug for String { 54 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 55 | std::fmt::Debug::fmt(&std::string::String::from_utf8_lossy(self.as_bytes()), fmt) 56 | } 57 | } 58 | 59 | impl std::cmp::PartialEq for String { 60 | fn eq(&self, other: &Self) -> bool { 61 | self.as_bytes() == other.as_bytes() 62 | } 63 | } 64 | 65 | impl std::cmp::PartialEq<&[u8]> for String { 66 | fn eq(&self, other: &&[u8]) -> bool { 67 | self.as_bytes() == *other 68 | } 69 | } 70 | 71 | impl std::cmp::PartialEq<&str> for String { 72 | fn eq(&self, other: &&str) -> bool { 73 | self.as_bytes() == other.as_bytes() 74 | } 75 | } 76 | 77 | // conversion with [u8; N] 78 | 79 | impl std::convert::From<[u8; LEN]> for String { 80 | fn from(arr: [u8; LEN]) -> Self { 81 | Self(arr, 0) 82 | } 83 | } 84 | 85 | impl std::convert::From> for [u8; LEN] { 86 | fn from(bstr: String) -> Self { 87 | bstr.0 88 | } 89 | } 90 | 91 | // conversion with bytes 92 | 93 | impl std::convert::TryFrom<&'_ [u8]> for String { 94 | type Error = (); 95 | fn try_from(arr: &[u8]) -> std::result::Result { 96 | if arr.len() > LEN { 97 | return Err(()); 98 | } 99 | let mut bstr = Self::new(); 100 | bstr.0[..arr.len()].copy_from_slice(arr); 101 | Ok(bstr) 102 | } 103 | } 104 | 105 | impl std::convert::From> for std::vec::Vec { 106 | fn from(bstr: String) -> Self { 107 | bstr.as_bytes().to_vec() 108 | } 109 | } 110 | 111 | // conversion with strings 112 | 113 | impl std::convert::TryFrom<&'_ str> for String { 114 | type Error = (); 115 | fn try_from(s: &str) -> std::result::Result { 116 | Self::try_from(s.as_bytes()) 117 | } 118 | } 119 | 120 | impl std::convert::TryFrom> for std::string::String { 121 | type Error = std::str::Utf8Error; 122 | fn try_from(bstr: String) -> std::result::Result { 123 | std::str::from_utf8(bstr.as_bytes()).map(Into::into) 124 | } 125 | } 126 | 127 | /// Represents a fixed-length wide string. 128 | /// 129 | /// This type can be created from a `&[u16]` if its length does not 130 | /// exceed the fixed length. It can be created from a `&str` if its 131 | /// length, encoded in UTF16, does not exceed the fixed length. 132 | /// 133 | /// It can be freely converted from and to a `[u16; N]` array, and 134 | /// to a `Vec` where it will be cut at the first null. 135 | /// 136 | /// It can be converted to a Rust `String` if it is properly UTF16 137 | /// encoded. 138 | #[repr(C)] 139 | // NOTE: can't derive IntoBytes automatically until zerocopy #10 is fixed. 140 | #[derive(Clone, Copy, FromBytes, Immutable)] 141 | pub struct WString([u16; LEN], u16); // one extra NULL byte 142 | 143 | impl WString { 144 | /// Create a new empty string. 145 | pub fn new() -> Self { 146 | Self([0; LEN], 0) 147 | } 148 | 149 | /// Return the number of code units up to the first null. 150 | pub fn len(&self) -> usize { 151 | let v = &self.0; 152 | v.iter().position(|&b| b == 0).unwrap_or(self.0.len()) 153 | } 154 | 155 | /// Returns true if the string is empty. 156 | pub fn is_empty(&self) -> bool { 157 | self.len() == 0 158 | } 159 | 160 | /// Get the slice up to the first null code unit. 161 | pub fn as_slice(&self) -> &[u16] { 162 | &self.0[..self.len()] 163 | } 164 | 165 | /// Get a reference to the full array of code units. 166 | pub fn backing_array(&mut self) -> &mut [u16; LEN] { 167 | &mut self.0 168 | } 169 | } 170 | 171 | impl std::default::Default for WString { 172 | fn default() -> Self { 173 | Self::new() 174 | } 175 | } 176 | 177 | impl std::fmt::Debug for WString { 178 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 179 | let fmted: std::string::String = 180 | std::char::decode_utf16(self.0.iter().cloned().take_while(|&b| b != 0)) 181 | .map(|ch| ch.unwrap_or(std::char::REPLACEMENT_CHARACTER)) 182 | .collect(); 183 | std::fmt::Debug::fmt(&fmted, fmt) 184 | } 185 | } 186 | 187 | impl std::cmp::PartialEq for WString { 188 | fn eq(&self, other: &Self) -> bool { 189 | self.as_slice() == other.as_slice() 190 | } 191 | } 192 | 193 | impl std::cmp::PartialEq<&[u16]> for WString { 194 | fn eq(&self, other: &&[u16]) -> bool { 195 | self.as_slice() == *other 196 | } 197 | } 198 | 199 | impl std::cmp::PartialEq<&str> for WString { 200 | fn eq(&self, other: &&str) -> bool { 201 | self.as_slice().iter().cloned().eq(other.encode_utf16()) 202 | } 203 | } 204 | 205 | // conversion with [u16; N] 206 | 207 | impl std::convert::From<[u16; LEN]> for WString { 208 | fn from(arr: [u16; LEN]) -> Self { 209 | Self(arr, 0) 210 | } 211 | } 212 | 213 | impl std::convert::From> for [u16; LEN] { 214 | fn from(wstr: WString) -> Self { 215 | wstr.0 216 | } 217 | } 218 | 219 | // conversion with [u16] 220 | 221 | impl std::convert::TryFrom<&'_ [u16]> for WString { 222 | type Error = (); 223 | fn try_from(arr: &[u16]) -> std::result::Result { 224 | if arr.len() > LEN { 225 | return Err(()); 226 | } 227 | let mut wstr = Self::new(); 228 | wstr.0[..arr.len()].copy_from_slice(arr); 229 | Ok(wstr) 230 | } 231 | } 232 | 233 | impl std::convert::From> for std::vec::Vec { 234 | fn from(wstr: WString) -> Self { 235 | wstr.as_slice().to_vec() 236 | } 237 | } 238 | 239 | // conversion with strings 240 | 241 | impl std::convert::TryFrom<&'_ str> for WString { 242 | type Error = (); 243 | fn try_from(s: &str) -> std::result::Result { 244 | let mut wstr = Self::new(); 245 | for (i, unit) in s.encode_utf16().enumerate() { 246 | if i >= wstr.0.len() { 247 | return Err(()); 248 | } 249 | wstr.0[i] = unit; 250 | } 251 | Ok(wstr) 252 | } 253 | } 254 | 255 | impl std::convert::TryFrom> for std::string::String { 256 | type Error = std::char::DecodeUtf16Error; 257 | fn try_from(wstr: WString) -> std::result::Result { 258 | std::char::decode_utf16(wstr.0.iter().cloned().take_while(|&b| b != 0)).collect() 259 | } 260 | } 261 | 262 | // manual zerocopy implementation 263 | 264 | // SAFETY: the layout of WString consists of only u16 elements; no padding is inserted 265 | // between them. 266 | unsafe impl zerocopy::IntoBytes for WString { 267 | fn only_derive_is_allowed_to_implement_this_trait() {} 268 | } 269 | 270 | // compatibility aliases 271 | 272 | /// Alias for `String<80>`. 273 | pub type String80 = String<80>; 274 | 275 | /// Alias for `WString<80>`. 276 | pub type WString80 = WString<80>; 277 | -------------------------------------------------------------------------------- /src/symbol.rs: -------------------------------------------------------------------------------- 1 | //! Wrappers for symbol operations and symbol handles. 2 | 3 | use std::collections::HashMap; 4 | use std::convert::TryInto; 5 | use std::io::Read; 6 | 7 | use byteorder::{ByteOrder, ReadBytesExt, LE}; 8 | use zerocopy::{FromBytes, Immutable, IntoBytes}; 9 | 10 | use crate::errors::{ErrContext, Error}; 11 | use crate::index; 12 | use crate::{Device, Result}; 13 | 14 | /// A handle to a variable within the ADS device. 15 | /// 16 | /// The handle is released automatically on drop. 17 | pub struct Handle<'c> { 18 | device: Device<'c>, 19 | handle: u32, 20 | } 21 | 22 | impl<'c> Handle<'c> { 23 | /// Create a new handle to a single symbol. 24 | pub fn new(device: Device<'c>, symbol: &str) -> Result { 25 | let mut handle_bytes = [0; 4]; 26 | device.write_read_exact(index::GET_SYMHANDLE_BYNAME, 0, symbol.as_bytes(), &mut handle_bytes)?; 27 | Ok(Self { device, handle: u32::from_le_bytes(handle_bytes) }) 28 | } 29 | 30 | /// Return the raw handle. 31 | pub fn raw(&self) -> u32 { 32 | self.handle 33 | } 34 | 35 | /// Read data from the variable (returned data must match size of buffer). 36 | pub fn read(&self, buf: &mut [u8]) -> Result<()> { 37 | self.device.read_exact(index::RW_SYMVAL_BYHANDLE, self.handle, buf) 38 | } 39 | 40 | /// Write data to the variable. 41 | pub fn write(&self, buf: &[u8]) -> Result<()> { 42 | self.device.write(index::RW_SYMVAL_BYHANDLE, self.handle, buf) 43 | } 44 | 45 | /// Read data of given type. 46 | /// 47 | /// Any type that supports `zerocopy::FromBytes` can be read. You can also 48 | /// derive that trait on your own structures and read structured data 49 | /// directly from the symbol. 50 | /// 51 | /// Note: to be independent of the host's byte order, use the integer types 52 | /// defined in `zerocopy::byteorder`. 53 | pub fn read_value(&self) -> Result { 54 | self.device.read_value(index::RW_SYMVAL_BYHANDLE, self.handle) 55 | } 56 | 57 | /// Write data of given type. 58 | /// 59 | /// See `read_value` for details. 60 | pub fn write_value(&self, value: &T) -> Result<()> { 61 | self.device.write_value(index::RW_SYMVAL_BYHANDLE, self.handle, value) 62 | } 63 | } 64 | 65 | impl Drop for Handle<'_> { 66 | fn drop(&mut self) { 67 | let _ = self.device.write(index::RELEASE_SYMHANDLE, 0, &self.handle.to_le_bytes()); 68 | } 69 | } 70 | 71 | /// Get symbol size by name. 72 | pub fn get_size(device: Device<'_>, symbol: &str) -> Result { 73 | let mut buf = [0; 12]; 74 | device.write_read_exact(index::GET_SYMINFO_BYNAME, 0, symbol.as_bytes(), &mut buf)?; 75 | Ok(u32::from_le_bytes(buf[8..].try_into().expect("size")) as usize) 76 | } 77 | 78 | /// Get symbol location (index group and index offset) by name. 79 | pub fn get_location(device: Device<'_>, symbol: &str) -> Result<(u32, u32)> { 80 | let mut buf = [0; 12]; 81 | device.write_read_exact(index::GET_SYMINFO_BYNAME, 0, symbol.as_bytes(), &mut buf)?; 82 | Ok(( 83 | u32::from_le_bytes(buf[0..4].try_into().expect("size")), 84 | u32::from_le_bytes(buf[4..8].try_into().expect("size")), 85 | )) 86 | } 87 | 88 | /// Represents a symbol in the PLC memory. 89 | pub struct Symbol { 90 | /// Hierarchical name of the symbol. 91 | pub name: String, 92 | /// Index group of the symbol location. 93 | pub ix_group: u32, 94 | /// Index offset of the symbol location. 95 | pub ix_offset: u32, 96 | /// Type name of the symbol. 97 | pub typ: String, 98 | /// Total size of the symbol, in bytes. 99 | pub size: usize, 100 | /// Base type: 101 | /// - 0 - void 102 | /// - 2 - INT (i16) 103 | /// - 3 - DINT (i32) 104 | /// - 4 - REAL (f32) 105 | /// - 5 - LREAL (f64) 106 | /// - 16 - SINT (i8) 107 | /// - 17 - USINT/BYTE (u8) 108 | /// - 18 - UINT/WORD (u16) 109 | /// - 19 - UDINT/DWORD (u32) 110 | /// - 20 - LINT (i64) 111 | /// - 21 - ULINT/LWORD (u64) 112 | /// - 30 - STRING 113 | /// - 31 - WSTRING 114 | /// - 32 - REAL80 (f80) 115 | /// - 33 - BOOL (u1) 116 | /// - 65 - Other/Compound type 117 | pub base_type: u32, 118 | /// Symbol flags: 119 | /// - 0x01 - Persistent 120 | /// - 0x02 - Bit value 121 | /// - 0x04 - Reference to 122 | /// - 0x08 - Type GUID present 123 | /// - 0x10 - TComInterfacePtr 124 | /// - 0x20 - Read only 125 | /// - 0x40 - ITF method access 126 | /// - 0x80 - Method deref 127 | /// - 0x0F00 - Context mask 128 | /// - 0x1000 - Attributes present 129 | /// - 0x2000 - Static 130 | /// - 0x4000 - Init on reset 131 | /// - 0x8000 - Extended flags present 132 | pub flags: u32, 133 | } 134 | 135 | /// Represents a type in the PLC's type inventory. 136 | pub struct Type { 137 | /// Name of the type. 138 | pub name: String, 139 | /// Total size of the type, in bytes. 140 | pub size: usize, 141 | /// If the type is an array, (lower, upper) index bounds for all dimensions. 142 | pub array: Vec<(i32, i32)>, 143 | /// If the type is a struct, all fields it contains. 144 | pub fields: Vec, 145 | /// Base type (see [`Symbol::base_type`]). 146 | pub base_type: u32, 147 | /// Type flags: 148 | /// - 0x01 - Data type 149 | /// - 0x02 - Data item 150 | /// - 0x04 - Reference to 151 | /// - 0x08 - Method deref 152 | /// - 0x10 - Oversample 153 | /// - 0x20 - Bit values 154 | /// - 0x40 - Prop item 155 | /// - 0x80 - Type GUID present 156 | /// - 0x0100 - Persistent 157 | /// - 0x0200 - Copy mask 158 | /// - 0x0400 - TComInterfacePtr 159 | /// - 0x0800 - Method infos present 160 | /// - 0x1000 - Attributes present 161 | /// - 0x2000 - Enum infos present 162 | /// - 0x010000 - Aligned 163 | /// - 0x020000 - Static 164 | /// - 0x040000 - Contains/Has Sp levels present 165 | /// - 0x080000 - Ignore persistent data 166 | /// - 0x100000 - Any size array 167 | /// - 0x200000 - Persistent datatype 168 | /// - 0x400000 - Init on reset 169 | /// - 0x800000 - Is/Contains PLC pointer type 170 | /// - 0x01000000 - Refactor infos present 171 | pub flags: u32, 172 | } 173 | 174 | /// Represents a field of a structure type. 175 | pub struct Field { 176 | /// Name of the field. 177 | pub name: String, 178 | /// Type name of the field. 179 | pub typ: String, 180 | /// Offset of the field in the structure. If `None`, the field is defined 181 | /// in some other memory block and not inline to the structure. 182 | pub offset: Option, 183 | /// Size of the field, in bytes. 184 | pub size: usize, 185 | /// If the field is an array, (lower, upper) index bounds for all dimensions. 186 | pub array: Vec<(i32, i32)>, 187 | /// Base type (see [`Symbol::base_type`]). 188 | pub base_type: u32, 189 | /// Type flags (see [`Type::flags`]). 190 | pub flags: u32, 191 | } 192 | 193 | /// A mapping from type name to type. 194 | pub type TypeMap = HashMap; 195 | 196 | /// Get and decode symbol and type information from the PLC. 197 | pub fn get_symbol_info(device: Device<'_>) -> Result<(Vec, TypeMap)> { 198 | // Query the sizes of symbol and type info. 199 | let mut read_data = [0; 64]; 200 | device.read_exact(index::SYM_UPLOAD_INFO2, 0, &mut read_data)?; 201 | let symbol_len = LE::read_u32(&read_data[4..]) as usize; 202 | let types_len = LE::read_u32(&read_data[12..]) as usize; 203 | 204 | // Query the type info. 205 | let mut type_data = vec![0; types_len]; 206 | device.read_exact(index::SYM_DT_UPLOAD, 0, &mut type_data)?; 207 | 208 | // Query the symbol info. 209 | let mut symbol_data = vec![0; symbol_len]; 210 | device.read_exact(index::SYM_UPLOAD, 0, &mut symbol_data)?; 211 | 212 | decode_symbol_info(symbol_data, type_data) 213 | } 214 | 215 | /// Decode symbol and type information from the PLC. 216 | /// 217 | /// The data must come from the `SYM_UPLOAD` and `SYM_DT_UPLOAD` queries, 218 | /// respectively. 219 | /// 220 | /// Returns a list of symbols, and a map of type names to types. 221 | pub fn decode_symbol_info(symbol_data: Vec, type_data: Vec) -> Result<(Vec, TypeMap)> { 222 | // Decode the type info. 223 | let mut buf = [0; 1024]; 224 | let mut data_ptr = type_data.as_slice(); 225 | let mut type_map = HashMap::new(); 226 | 227 | fn decode_type_info(mut ptr: &[u8], parent: Option<&mut Type>) -> Result> { 228 | let ctx = "decoding type info"; 229 | 230 | let mut buf = [0; 1024]; 231 | let version = ptr.read_u32::().ctx(ctx)?; 232 | if version != 1 { 233 | return Err(Error::Reply(ctx, "unknown type info version", version)); 234 | } 235 | let _subitem_index = ptr.read_u16::().ctx(ctx)?; 236 | let _plc_interface_id = ptr.read_u16::().ctx(ctx)?; 237 | let _reserved = ptr.read_u32::().ctx(ctx)?; 238 | let size = ptr.read_u32::().ctx(ctx)? as usize; 239 | let offset = ptr.read_u32::().ctx(ctx)?; 240 | let base_type = ptr.read_u32::().ctx(ctx)?; 241 | let flags = ptr.read_u32::().ctx(ctx)?; 242 | let len_name = ptr.read_u16::().ctx(ctx)? as usize; 243 | let len_type = ptr.read_u16::().ctx(ctx)? as usize; 244 | let len_comment = ptr.read_u16::().ctx(ctx)? as usize; 245 | let array_dim = ptr.read_u16::().ctx(ctx)?; 246 | let sub_items = ptr.read_u16::().ctx(ctx)?; 247 | ptr.read_exact(&mut buf[..len_name + 1]).ctx(ctx)?; 248 | let name = String::from_utf8_lossy(&buf[..len_name]).into_owned(); 249 | ptr.read_exact(&mut buf[..len_type + 1]).ctx(ctx)?; 250 | let typ = String::from_utf8_lossy(&buf[..len_type]).into_owned(); 251 | ptr = &ptr[len_comment + 1..]; 252 | 253 | let mut array = vec![]; 254 | for _ in 0..array_dim { 255 | let lower = ptr.read_i32::().ctx(ctx)?; 256 | let total = ptr.read_i32::().ctx(ctx)?; 257 | array.push((lower, lower + total - 1)); 258 | } 259 | 260 | if let Some(parent) = parent { 261 | assert_eq!(sub_items, 0); 262 | // Offset -1 marks that the field is placed somewhere else in memory 263 | // (e.g. AT %Mxx). 264 | let offset = if offset == 0xFFFF_FFFF { None } else { Some(offset) }; 265 | parent.fields.push(Field { name, typ, offset, size, array, base_type, flags }); 266 | Ok(None) 267 | } else { 268 | assert_eq!(offset, 0); 269 | let mut typinfo = Type { name, size, array, base_type, flags, fields: Vec::new() }; 270 | 271 | for _ in 0..sub_items { 272 | let sub_size = ptr.read_u32::().ctx(ctx)? as usize; 273 | let (sub_ptr, rest) = ptr.split_at(sub_size - 4); 274 | decode_type_info(sub_ptr, Some(&mut typinfo))?; 275 | ptr = rest; 276 | } 277 | 278 | // following fields (variable length), which we jump over: 279 | // - type GUID if flags has Type GUID 280 | // - copy mask of *size* bytes 281 | // - # of methods and method entries if flags has Method infos 282 | // - # of attributes and attributes if flags has Attributes 283 | // - # of enum infos and enum infos if flags has Enum infos 284 | // - refactor infos if flags has Refactor infos 285 | // - splevels if flags has SP levels 286 | 287 | Ok(Some(typinfo)) 288 | } 289 | } 290 | 291 | while !data_ptr.is_empty() { 292 | let entry_size = data_ptr.read_u32::().ctx("decoding type info")? as usize; 293 | let (entry_ptr, rest) = data_ptr.split_at(entry_size - 4); 294 | let typ = decode_type_info(entry_ptr, None)?.expect("base type"); 295 | type_map.insert(typ.name.clone(), typ); 296 | data_ptr = rest; 297 | } 298 | 299 | // Decode the symbol info. 300 | let mut symbols = Vec::new(); 301 | let mut data_ptr = symbol_data.as_slice(); 302 | let ctx = "decoding symbol info"; 303 | while !data_ptr.is_empty() { 304 | let entry_size = data_ptr.read_u32::().ctx(ctx)? as usize; 305 | let (mut entry_ptr, rest) = data_ptr.split_at(entry_size - 4); 306 | let ix_group = entry_ptr.read_u32::().ctx(ctx)?; 307 | let ix_offset = entry_ptr.read_u32::().ctx(ctx)?; 308 | let size = entry_ptr.read_u32::().ctx(ctx)? as usize; 309 | let base_type = entry_ptr.read_u32::().ctx(ctx)?; 310 | let flags = entry_ptr.read_u16::().ctx(ctx)? as u32; 311 | let _legacy_array_dim = entry_ptr.read_u16::().ctx(ctx)?; 312 | let len_name = entry_ptr.read_u16::().ctx(ctx)? as usize; 313 | let len_type = entry_ptr.read_u16::().ctx(ctx)? as usize; 314 | let _len_comment = entry_ptr.read_u16::().ctx(ctx)? as usize; 315 | entry_ptr.read_exact(&mut buf[..len_name + 1]).ctx(ctx)?; 316 | let name = String::from_utf8_lossy(&buf[..len_name]).into_owned(); 317 | entry_ptr.read_exact(&mut buf[..len_type + 1]).ctx(ctx)?; 318 | let typ = String::from_utf8_lossy(&buf[..len_type]).into_owned(); 319 | // following fields (variable length), which we jump over: 320 | // - comment with \0 321 | // - type GUID if flags has Type GUID 322 | // - # of attributes and attribute entries if flags has Attributes 323 | // - flags2 if flags has Extended flags 324 | // - if flags2 has Old names 325 | 326 | symbols.push(Symbol { name, ix_group, ix_offset, typ, size, base_type, flags }); 327 | 328 | data_ptr = rest; 329 | } 330 | 331 | Ok((symbols, type_map)) 332 | } 333 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | // Code used in the crate test suite. 2 | 3 | use std::convert::{TryFrom, TryInto}; 4 | use std::io::{Read, Write}; 5 | use std::mem::size_of; 6 | use std::net::{TcpListener, TcpStream}; 7 | use std::sync::{Arc, Mutex}; 8 | use std::thread; 9 | use std::time::Duration; 10 | 11 | use byteorder::{ByteOrder, ReadBytesExt, WriteBytesExt, LE}; 12 | use once_cell::sync::Lazy; 13 | use zerocopy::{ 14 | byteorder::little_endian::{U32, U64}, 15 | FromBytes, FromZeros, Immutable, IntoBytes, 16 | }; 17 | 18 | use crate::client::{AddNotif, AdsHeader, IndexLength, IndexLengthRW}; 19 | use crate::{file, index}; 20 | 21 | // Test modules. 22 | mod test_client; 23 | mod test_netid; 24 | mod test_udp; 25 | 26 | // Since Cargo tests run multi-threaded, start one server per thread and 27 | // handle clients from the test functions in that thread. 28 | thread_local! { 29 | pub static SERVER: Lazy<(u16, Arc>)> = Lazy::new(|| { 30 | let opts = Arc::new(Mutex::new(ServerOpts::default())); 31 | 32 | let socket = TcpListener::bind("127.0.0.1:0").unwrap(); 33 | let port = socket.local_addr().unwrap().port(); 34 | let opts_server = opts.clone(); 35 | thread::spawn(move || { 36 | let mut server = Server { 37 | opts: opts_server, 38 | state: (crate::AdsState::Run, 0), 39 | data: vec![0; 1024], 40 | file_ptr: None, 41 | notif: None, 42 | }; 43 | for client in socket.incoming().flatten() { 44 | // We only need to handle one client concurrently. 45 | server.handle_client(client); 46 | } 47 | }); 48 | 49 | (port, opts) 50 | }); 51 | } 52 | 53 | // Configures different ways the server should behave. 54 | #[derive(Default)] 55 | pub struct ServerOpts { 56 | pub timeout: Option, 57 | pub no_reply: bool, 58 | pub garbage_header: bool, 59 | pub bad_notif: bool, 60 | pub ignore_invokeid: bool, 61 | } 62 | 63 | pub fn config_test_server(opts: ServerOpts) -> u16 { 64 | SERVER.with(|obj| { 65 | let (port, server_opts) = &**obj; 66 | *server_opts.lock().unwrap() = opts; 67 | *port 68 | }) 69 | } 70 | 71 | struct Server { 72 | opts: Arc>, 73 | data: Vec, 74 | // If the test file is opened for writing, and the read/write position. 75 | file_ptr: Option<(bool, usize)>, 76 | // If a notification has been added, the (offset, size) to send. 77 | notif: Option<(usize, usize)>, 78 | // The simulated device state. 79 | state: (crate::AdsState, u16), 80 | } 81 | 82 | impl Server { 83 | fn handle_client(&mut self, mut socket: TcpStream) { 84 | let opts = self.opts.clone(); 85 | loop { 86 | let opts = opts.lock().unwrap(); 87 | let mut header = AdsHeader::new_zeroed(); 88 | if let Err(e) = socket.read_exact(header.as_mut_bytes()) { 89 | if e.kind() == std::io::ErrorKind::UnexpectedEof { 90 | // connection was closed 91 | return; 92 | } 93 | panic!("unexpected receive error: {}", e); 94 | } 95 | println!(">>> {:?}", header); 96 | let mut data = vec![0; header.data_length.get() as usize]; 97 | socket.read_exact(&mut data).unwrap(); 98 | 99 | if opts.no_reply { 100 | return; 101 | } 102 | 103 | let (reply_data, error) = match header.command.get() { 104 | 1 => self.do_devinfo(&data), 105 | 2 => self.do_read(&data), 106 | 3 => self.do_write(&data), 107 | 4 => self.do_read_state(&data), 108 | 5 => self.do_write_control(&data), 109 | 6 => self.do_add_notif(&data), 110 | 7 => self.do_del_notif(&data), 111 | 9 => self.do_read_write(&data), 112 | _ => (vec![], 0x701), 113 | }; 114 | 115 | // Generate a notification if they are enabled. 116 | if let Some((off, len)) = &self.notif { 117 | self.send_notification(*off, *len, &header, opts.bad_notif, &mut socket); 118 | } 119 | 120 | let mut reply_header = AdsHeader::new_zeroed(); 121 | if opts.garbage_header { 122 | reply_header.ams_cmd = 234; 123 | } 124 | reply_header.length.set(32 + reply_data.len() as u32); 125 | reply_header.dest_netid = header.src_netid; 126 | reply_header.dest_port = header.src_port; 127 | reply_header.src_netid = header.dest_netid; 128 | reply_header.src_port = header.dest_port; 129 | reply_header.command = header.command; 130 | reply_header.state_flags.set(header.state_flags.get() | 1); 131 | reply_header.data_length.set(reply_data.len() as u32); 132 | reply_header.error_code.set(error); 133 | if !opts.ignore_invokeid { 134 | reply_header.invoke_id = header.invoke_id; 135 | } 136 | println!("<<< {:?}", reply_header); 137 | 138 | socket.write_all(reply_header.as_bytes()).unwrap(); 139 | socket.write_all(&reply_data).unwrap(); 140 | } 141 | } 142 | 143 | fn send_notification( 144 | &self, off: usize, len: usize, header: &AdsHeader, bad: bool, socket: &mut TcpStream, 145 | ) { 146 | let data_len = std::mem::size_of::() + len; 147 | 148 | let mut notif_header = SingleNotification::default(); 149 | notif_header.len.set(data_len as u32 - 4); 150 | notif_header.stamps.set(if bad { u32::MAX } else { 1 }); 151 | notif_header.stamp.set(0x9988776655443322); 152 | notif_header.samples.set(1); 153 | notif_header.handle.set(132); 154 | notif_header.size.set(len as u32); 155 | 156 | let mut ads_header = AdsHeader::new_zeroed(); 157 | ads_header.length.set(32 + data_len as u32); 158 | ads_header.dest_netid = header.src_netid; 159 | ads_header.dest_port = header.src_port; 160 | ads_header.src_netid = header.dest_netid; 161 | ads_header.src_port = header.dest_port; 162 | ads_header.command.set(crate::client::Command::Notification as u16); 163 | ads_header.state_flags.set(4); 164 | ads_header.data_length.set(data_len as u32); 165 | println!("not: {:?}", ads_header); 166 | 167 | socket.write_all(ads_header.as_bytes()).unwrap(); 168 | socket.write_all(notif_header.as_bytes()).unwrap(); 169 | socket.write_all(&self.data[off..][..len]).unwrap(); 170 | } 171 | 172 | fn do_devinfo(&self, data: &[u8]) -> (Vec, u32) { 173 | if !data.is_empty() { 174 | return (vec![], 0x706); 175 | } 176 | // no error, major 7, minor 1 177 | let mut out = 0u32.to_le_bytes().to_vec(); 178 | out.write_u8(7).unwrap(); 179 | out.write_u8(1).unwrap(); 180 | out.write_u16::(4024).unwrap(); 181 | out.extend(b"Nice device\0\0\0\0\0"); 182 | (out, 0) 183 | } 184 | 185 | fn do_read_state(&self, data: &[u8]) -> (Vec, u32) { 186 | if !data.is_empty() { 187 | return (vec![], 0x706); 188 | } 189 | let mut out = 0u32.to_le_bytes().to_vec(); 190 | out.write_u16::(self.state.0 as u16).unwrap(); 191 | out.write_u16::(self.state.1).unwrap(); 192 | (out, 0) 193 | } 194 | 195 | fn do_write_control(&mut self, mut data: &[u8]) -> (Vec, u32) { 196 | if data.len() != 8 { 197 | return (vec![], 0x706); 198 | } 199 | let adsstate = data.read_u16::().unwrap(); 200 | let devstate = data.read_u16::().unwrap(); 201 | let mut out = vec![]; 202 | match crate::AdsState::try_from(adsstate) { 203 | Err(_) | Ok(crate::AdsState::Invalid) => { 204 | out.write_u32::(0x70B).unwrap(); 205 | } 206 | Ok(adsstate) => { 207 | self.state = (adsstate, devstate); 208 | out.write_u32::(0).unwrap(); 209 | } 210 | } 211 | (out, 0) 212 | } 213 | 214 | fn do_read(&self, data: &[u8]) -> (Vec, u32) { 215 | if data.len() != size_of::() { 216 | return (vec![], 0x706); 217 | } 218 | let request = IndexLength::read_from_bytes(data).unwrap(); 219 | let grp = request.index_group.get(); 220 | let mut off = request.index_offset.get() as usize; 221 | let len = request.length.get() as usize; 222 | let mut out = Vec::new(); 223 | out.write_u32::(0).unwrap(); 224 | // Simulate symbol access. 225 | if grp == index::RW_SYMVAL_BYHANDLE { 226 | if off != 77 { 227 | return (vec![], 0x710); 228 | } 229 | off = 1020; // symbol lives at the end of self.data 230 | } else if grp != index::PLC_RW_M { 231 | return (vec![], 0x702); 232 | } 233 | if off + len > self.data.len() { 234 | return (vec![], 0x703); 235 | } 236 | out.write_u32::(request.length.get()).unwrap(); 237 | out.extend(&self.data[off..][..len]); 238 | (out, 0) 239 | } 240 | 241 | fn do_write(&mut self, data: &[u8]) -> (Vec, u32) { 242 | if data.len() < size_of::() { 243 | return (vec![], 0x706); 244 | } 245 | let request = IndexLength::read_from_bytes(&data[..12]).unwrap(); 246 | let grp = request.index_group.get(); 247 | let mut off = request.index_offset.get() as usize; 248 | let len = request.length.get() as usize; 249 | 250 | if grp == index::RW_SYMVAL_BYHANDLE { 251 | if off != 77 { 252 | return (vec![], 0x710); 253 | } 254 | off = 1020; 255 | } else if grp == index::RELEASE_SYMHANDLE { 256 | if off != 77 { 257 | return (vec![], 0x710); 258 | } 259 | return (0u32.to_le_bytes().into(), 0); 260 | } else if grp != index::PLC_RW_M { 261 | return (vec![], 0x702); 262 | } 263 | if off + len > self.data.len() { 264 | return (vec![], 0x703); 265 | } 266 | if data.len() != size_of::() + len { 267 | return (vec![], 0x706); 268 | } 269 | self.data[off..][..len].copy_from_slice(&data[12..]); 270 | (0u32.to_le_bytes().into(), 0) 271 | } 272 | 273 | fn do_read_write(&mut self, data: &[u8]) -> (Vec, u32) { 274 | if data.len() < size_of::() { 275 | return (vec![], 0x706); 276 | } 277 | let request = IndexLengthRW::read_from_bytes(&data[..16]).unwrap(); 278 | let off = request.index_offset.get(); 279 | let rlen = request.read_length.get() as usize; 280 | let wlen = request.write_length.get() as usize; 281 | let mut out = 0u32.to_le_bytes().to_vec(); 282 | 283 | // Simulate file and symbol access. 284 | match request.index_group.get() { 285 | index::SUMUP_READ_EX => { 286 | let mut mdata: Vec = vec![]; 287 | let mut rdata: Vec = vec![]; 288 | for i in 0..off as usize { 289 | let rlen = LE::read_u32(&data[16 + i * 12 + 8..]) as usize; 290 | let (mut d, e) = self.do_read(&data[16 + i * 12..][..12]); 291 | mdata.write_u32::(e).unwrap(); 292 | d.resize(rlen + 8, 0); 293 | mdata.write_u32::(d.len() as u32 - 8).unwrap(); 294 | rdata.extend(&d[8..]); 295 | } 296 | out.write_u32::((mdata.len() + rdata.len()) as u32).unwrap(); 297 | out.extend(mdata); 298 | out.extend(rdata); 299 | } 300 | index::SUMUP_WRITE => { 301 | let mut woff = 16 + off as usize * 12; 302 | out.write_u32::(4 * off).unwrap(); 303 | for i in 0..off as usize { 304 | let wlen = LE::read_u32(&data[16 + i * 12 + 8..]) as usize; 305 | let mut subdata = data[16 + i * 12..][..12].to_vec(); 306 | subdata.extend(&data[woff..][..wlen]); 307 | woff += wlen; 308 | let (_, e) = self.do_write(&subdata); 309 | out.write_u32::(e).unwrap(); 310 | } 311 | } 312 | index::SUMUP_READWRITE => { 313 | let mut mdata: Vec = vec![]; 314 | let mut rdata: Vec = vec![]; 315 | let mut woff = 16 + off as usize * 16; 316 | for i in 0..off as usize { 317 | let wlen = LE::read_u32(&data[16 + i * 16 + 12..]) as usize; 318 | let mut subdata = data[16 + i * 16..][..16].to_vec(); 319 | subdata.extend(&data[woff..][..wlen]); 320 | woff += wlen; 321 | let (d, e) = self.do_read_write(&subdata); 322 | mdata.write_u32::(e).unwrap(); 323 | if d.len() > 8 { 324 | mdata.write_u32::(d.len() as u32 - 8).unwrap(); 325 | rdata.extend(&d[8..]); 326 | } else { 327 | mdata.write_u32::(0).unwrap(); 328 | } 329 | } 330 | out.write_u32::((mdata.len() + rdata.len()) as u32).unwrap(); 331 | out.extend(mdata); 332 | out.extend(rdata); 333 | } 334 | index::SUMUP_ADDDEVNOTE => { 335 | out.write_u32::(8 * off).unwrap(); 336 | for i in 0..off as usize { 337 | let (d, e) = self.do_add_notif(&data[16 + i * 40..][..40]); 338 | out.write_u32::(e).unwrap(); 339 | if d.len() > 4 { 340 | out.extend(&d[4..]); 341 | } else { 342 | out.write_u32::(0).unwrap(); 343 | } 344 | } 345 | } 346 | index::SUMUP_DELDEVNOTE => { 347 | out.write_u32::(4 * off).unwrap(); 348 | for i in 0..off as usize { 349 | let (_, e) = self.do_del_notif(&data[16 + i * 4..][..4]); 350 | out.write_u32::(e).unwrap(); 351 | } 352 | } 353 | index::FILE_OPEN => { 354 | if &data[16..] != b"/etc/passwd" { 355 | return (vec![], 0x70C); 356 | } 357 | if self.file_ptr.is_some() { 358 | return (vec![], 0x708); 359 | } 360 | let write = off & (file::WRITE | file::APPEND) != 0; 361 | out.write_u32::(4).unwrap(); 362 | out.write_u32::(42).unwrap(); 363 | self.file_ptr = Some((write, 0)); 364 | } 365 | index::FILE_CLOSE => { 366 | if !data[16..].is_empty() { 367 | return (vec![], 0x70B); 368 | } 369 | if off != 42 { 370 | return (vec![], 0x70C); 371 | } 372 | out.write_u32::(0).unwrap(); 373 | self.file_ptr = None; 374 | } 375 | index::FILE_WRITE => { 376 | if let Some((true, ptr)) = &mut self.file_ptr { 377 | out.write_u32::(0).unwrap(); 378 | *ptr += wlen; 379 | } else { 380 | return (vec![], 0x704); 381 | } 382 | } 383 | index::FILE_READ => { 384 | if let Some((false, ptr)) = &mut self.file_ptr { 385 | let cur = *ptr; 386 | *ptr = (*ptr + rlen).min(888); 387 | let amount = *ptr - cur; 388 | out.write_u32::(amount as u32).unwrap(); 389 | out.resize(out.len() + amount, 0); 390 | } else { 391 | return (vec![], 0x704); 392 | } 393 | } 394 | index::FILE_DELETE => { 395 | if &data[16..] != b"/etc/passwd" { 396 | return (vec![], 0x70C); 397 | } 398 | if self.file_ptr.is_some() { 399 | // send an unknown error number 400 | return (vec![], 0xFFFF); 401 | } 402 | out.write_u32::(0).unwrap(); 403 | } 404 | index::GET_SYMHANDLE_BYNAME => { 405 | if &data[16..] != b"SYMBOL" { 406 | return (vec![], 0x710); 407 | } 408 | out.write_u32::(4).unwrap(); 409 | out.write_u32::(77).unwrap(); 410 | } 411 | _ => return (vec![], 0x702), 412 | } 413 | (out, 0) 414 | } 415 | 416 | fn do_add_notif(&mut self, data: &[u8]) -> (Vec, u32) { 417 | if data.len() != size_of::() { 418 | return (vec![], 0x706); 419 | } 420 | let request = AddNotif::read_from_bytes(data).unwrap(); 421 | let off = request.index_offset.get() as usize; 422 | let len = request.length.get() as usize; 423 | 424 | if request.index_group.get() != index::PLC_RW_M { 425 | return (vec![], 0x702); 426 | } 427 | if off + len > self.data.len() { 428 | return (vec![], 0x703); 429 | } 430 | self.notif = Some((off, len)); 431 | let mut out = 0u32.to_le_bytes().to_vec(); 432 | out.write_u32::(132).unwrap(); // handle 433 | (out, 0) 434 | } 435 | 436 | fn do_del_notif(&mut self, data: &[u8]) -> (Vec, u32) { 437 | if data.len() != 4 { 438 | return (vec![], 0x706); 439 | } 440 | let handle = u32::from_le_bytes(data.try_into().unwrap()); 441 | if handle != 132 { 442 | return (vec![], 0x714); 443 | } 444 | if self.notif.is_none() { 445 | return (vec![], 0x714); 446 | } 447 | self.notif = None; 448 | (0u32.to_le_bytes().into(), 0) 449 | } 450 | } 451 | 452 | #[derive(FromBytes, IntoBytes, Immutable, Debug, Default)] 453 | #[repr(C)] 454 | struct SingleNotification { 455 | len: U32, 456 | stamps: U32, 457 | stamp: U64, 458 | samples: U32, 459 | handle: U32, 460 | size: U32, 461 | } 462 | -------------------------------------------------------------------------------- /src/test/test_client.rs: -------------------------------------------------------------------------------- 1 | //! Test for the TCP client. 2 | 3 | use std::convert::TryFrom; 4 | use std::io::{self, Read, Write}; 5 | use std::time::Duration; 6 | 7 | use crate::test::{config_test_server, ServerOpts}; 8 | use crate::{AmsAddr, AmsNetId, Client, Device, Error, Source, Timeouts}; 9 | 10 | fn run_test(opts: ServerOpts, f: impl Fn(Device)) { 11 | let timeouts = if let Some(tmo) = opts.timeout { Timeouts::new(tmo) } else { Timeouts::none() }; 12 | let port = config_test_server(opts); 13 | let client = Client::new(("127.0.0.1", port), timeouts, Source::Auto).unwrap(); 14 | f(client.device(AmsAddr::new(AmsNetId::new(1, 2, 3, 4, 5, 6), 851))); 15 | } 16 | 17 | #[test] 18 | fn test_garbage_packet() { 19 | run_test(ServerOpts { garbage_header: true, ..Default::default() }, |device| { 20 | let err = device.get_info().unwrap_err(); 21 | assert!(matches!(err, Error::Reply(_, "invalid packet or unknown AMS command", _))); 22 | }) 23 | } 24 | 25 | #[test] 26 | fn test_timeout() { 27 | run_test( 28 | ServerOpts { timeout: Some(Duration::from_millis(1)), no_reply: true, ..Default::default() }, 29 | |device| { 30 | let err = device.get_info().unwrap_err(); 31 | match err { 32 | Error::Io(_, ioe) if ioe.kind() == io::ErrorKind::TimedOut => (), 33 | _ => panic!("unexpected error from timeout: {}", err), 34 | } 35 | }, 36 | ) 37 | } 38 | 39 | #[test] 40 | fn test_wrong_invokeid() { 41 | run_test(ServerOpts { ignore_invokeid: true, ..Default::default() }, |device| { 42 | assert!(matches!( 43 | device.get_info().unwrap_err(), 44 | Error::Reply(_, "unexpected invoke ID", 0) 45 | )); 46 | }) 47 | } 48 | 49 | #[test] 50 | fn test_devinfo() { 51 | run_test(ServerOpts::default(), |device| { 52 | let info = device.get_info().unwrap(); 53 | assert_eq!(info.version, 4024); 54 | assert_eq!(info.name, "Nice device"); 55 | }) 56 | } 57 | 58 | #[test] 59 | fn test_state() { 60 | run_test(ServerOpts::default(), |device| { 61 | device.write_control(crate::AdsState::Config, 42).unwrap(); 62 | assert_eq!(device.get_state().unwrap(), (crate::AdsState::Config, 42)); 63 | assert!(device.write_control(crate::AdsState::Invalid, 42).is_err()); 64 | }) 65 | } 66 | 67 | #[test] 68 | fn test_readwrite() { 69 | run_test(ServerOpts::default(), |device| { 70 | let data = [1, 6, 8, 9]; 71 | let mut buf = [0; 4]; 72 | device.write(0x4020, 7, &data).unwrap(); 73 | device.read_exact(0x04020, 7, &mut buf).unwrap(); 74 | assert_eq!(data, buf); 75 | 76 | assert!(matches!(device.read_exact(0x4021, 0, &mut buf), Err(Error::Ads(_, _, 0x702)))); 77 | assert!(matches!( 78 | device.read_exact(0x4020, 98765, &mut buf), 79 | Err(Error::Ads(_, _, 0x703)) 80 | )); 81 | 82 | device.write_value(0x4020, 7, &0xdeadbeef_u32).unwrap(); 83 | assert!(device.read_value::(0x4020, 7).unwrap() == 0xdeadbeef); 84 | }) 85 | } 86 | 87 | #[test] 88 | fn test_multi_requests() { 89 | use crate::client::*; 90 | use crate::index::*; 91 | run_test(ServerOpts::default(), |device| { 92 | let mut buf1 = *b"ABCDEFGHIJ"; 93 | let mut buf2 = *b"0123456789"; 94 | let mut buf3 = *b"-----"; 95 | let mut reqs = vec![ 96 | WriteRequest::new(0x4020, 7, &buf1), 97 | WriteRequest::new(0x4020, 9, &buf2), 98 | WriteRequest::new(0x6789, 5, &buf3), 99 | ]; 100 | device.write_multi(&mut reqs).unwrap(); 101 | assert!(reqs[0].ensure().is_ok()); 102 | assert!(reqs[1].ensure().is_ok()); 103 | assert!(reqs[2].ensure().is_err()); 104 | 105 | let mut reqs = vec![ 106 | ReadRequest::new(0x4020, 7, &mut buf1), 107 | ReadRequest::new(0x4020, 9, &mut buf2), 108 | ReadRequest::new(0x7689, 5, &mut buf3), 109 | ]; 110 | device.read_multi(&mut reqs).unwrap(); 111 | assert!(reqs[0].data().unwrap() == b"AB01234567"); 112 | assert!(reqs[1].data().unwrap() == b"0123456789"); 113 | 114 | let mut reqs = vec![ 115 | WriteReadRequest::new(FILE_OPEN, 0, b"/etc/passwd", &mut buf1), 116 | WriteReadRequest::new(FILE_READ, 0, b"", &mut buf2), 117 | WriteReadRequest::new(FILE_CLOSE, 42, b"", &mut []), 118 | WriteReadRequest::new(0x7689, 5, b"blub", &mut buf3), 119 | ]; 120 | device.write_read_multi(&mut reqs).unwrap(); 121 | assert!(reqs[0].data().unwrap() == 42u32.to_le_bytes()); 122 | assert!(reqs[1].data().unwrap() == b"\0\0\0\0\0\0\0\0\0\0"); 123 | assert!(reqs[2].data().unwrap() == b""); 124 | assert!(reqs[3].data().is_err()); 125 | }); 126 | } 127 | 128 | #[test] 129 | fn test_fileaccess() { 130 | use crate::file::*; 131 | run_test(ServerOpts::default(), |device| { 132 | assert!(File::open(device, "blub", 0).is_err()); 133 | let mut file = File::open(device, "/etc/passwd", WRITE).unwrap(); 134 | assert!(File::open(device, "/etc/passwd", 0).is_err()); 135 | assert!(file.read(&mut [0; 4]).is_err()); 136 | file.write_all(b"asdf").unwrap(); 137 | file.flush().unwrap(); 138 | drop(file); 139 | 140 | let mut file = File::open(device, "/etc/passwd", READ).unwrap(); 141 | assert!(file.write(&[0; 4]).is_err()); 142 | let mut vec = vec![]; 143 | file.read_to_end(&mut vec).unwrap(); 144 | assert!(vec.len() == 888); 145 | assert!(File::delete(device, "/etc/passwd", 0).is_err()); 146 | drop(file); 147 | 148 | File::delete(device, "/etc/passwd", 0).unwrap(); 149 | }) 150 | } 151 | 152 | #[test] 153 | fn test_symbolaccess() { 154 | use crate::symbol::*; 155 | run_test(ServerOpts::default(), |device| { 156 | assert!(Handle::new(device, "blub").is_err()); 157 | let handle = Handle::new(device, "SYMBOL").unwrap(); 158 | assert!(handle.write(&[1, 2, 3, 4, 5]).is_err()); 159 | assert!(handle.read(&mut [0; 5]).is_err()); 160 | 161 | assert!(handle.raw() == 77); 162 | 163 | handle.write(&[4, 3, 2, 1]).unwrap(); 164 | let mut buf = [0; 4]; 165 | handle.read(&mut buf).unwrap(); 166 | assert!(buf == [4, 3, 2, 1]); 167 | 168 | handle.write_value(&0xdeadbeef_u32).unwrap(); 169 | assert!(handle.read_value::().unwrap() == 0xdeadbeef); 170 | }) 171 | } 172 | 173 | #[test] 174 | fn test_notification() { 175 | use crate::notif::*; 176 | use std::time::Duration; 177 | run_test(ServerOpts::default(), |device| { 178 | let chan = device.client.get_notification_channel(); 179 | 180 | let attrib = Attributes::new( 181 | 4, 182 | TransmissionMode::ServerOnChange, 183 | Duration::from_secs(1), 184 | Duration::from_secs(1), 185 | ); 186 | device.write(0x4020, 0, &[4, 4, 1, 1]).unwrap(); 187 | let handle = device.add_notification(0x4020, 0, &attrib).unwrap(); 188 | device.write(0x4020, 0, &[8, 8, 1, 1]).unwrap(); 189 | device.delete_notification(handle).unwrap(); 190 | 191 | // Including the add_notification, each request generates a notification 192 | // from the test server. 193 | let first = chan.try_recv().unwrap(); 194 | let second = chan.try_recv().unwrap(); 195 | 196 | println!("{:?}", first); 197 | 198 | let mut samples = first.samples(); 199 | assert_eq!( 200 | samples.next().unwrap(), 201 | Sample { handle, timestamp: 0x9988776655443322, data: &[4, 4, 1, 1] } 202 | ); 203 | assert_eq!(samples.next(), None); 204 | assert_eq!( 205 | second.samples().next().unwrap(), 206 | Sample { handle, timestamp: 0x9988776655443322, data: &[8, 8, 1, 1] } 207 | ); 208 | }) 209 | } 210 | 211 | #[test] 212 | fn test_multi_notification() { 213 | use crate::client::*; 214 | use crate::notif::*; 215 | use std::time::Duration; 216 | run_test(ServerOpts::default(), |device| { 217 | let attrib = Attributes::new( 218 | 4, 219 | TransmissionMode::ServerOnChange, 220 | Duration::from_secs(1), 221 | Duration::from_secs(1), 222 | ); 223 | let mut reqs = [ 224 | AddNotifRequest::new(0x4020, 7, &attrib), 225 | AddNotifRequest::new(0x6789, 0, &attrib), 226 | ]; 227 | device.add_notification_multi(&mut reqs).unwrap(); 228 | let handle = reqs[0].handle().unwrap(); 229 | assert!(reqs[1].handle().is_err()); 230 | 231 | let mut reqs = [DelNotifRequest::new(handle), DelNotifRequest::new(42)]; 232 | device.delete_notification_multi(&mut reqs).unwrap(); 233 | assert!(reqs[0].ensure().is_ok()); 234 | assert!(reqs[1].ensure().is_err()); 235 | }); 236 | } 237 | 238 | #[test] 239 | fn test_bad_notification() { 240 | use crate::notif::*; 241 | use std::time::Duration; 242 | run_test(ServerOpts { bad_notif: true, ..Default::default() }, |device| { 243 | let chan = device.client.get_notification_channel(); 244 | 245 | let attrib = Attributes::new( 246 | 4, 247 | TransmissionMode::ServerOnChange, 248 | Duration::from_secs(1), 249 | Duration::from_secs(1), 250 | ); 251 | let _ = device.add_notification(0x4020, 0, &attrib).unwrap(); 252 | device.write(0x4020, 0, &[8, 8, 1, 1]).unwrap(); 253 | 254 | // No notification should have come through. 255 | assert!(chan.try_recv().is_err()); 256 | 257 | // Notification is automatically deleted at end of scope. 258 | }) 259 | } 260 | 261 | #[test] 262 | fn test_string_type() { 263 | type String5 = crate::strings::String<5>; 264 | 265 | run_test(ServerOpts::default(), |device| { 266 | let mut bstr = String5::try_from("abc").unwrap(); 267 | assert!(<[u8; 5]>::from(bstr) == [b'a', b'b', b'c', 0, 0]); 268 | assert!(bstr == &b"abc"[..]); 269 | assert!(bstr == "abc"); 270 | assert!(bstr.len() == 3); 271 | assert!(bstr.backing_array().len() == 5); 272 | 273 | assert!(String5::try_from("abcdef").is_err()); 274 | 275 | let bstr2 = String5::try_from(&b"abc"[..]).unwrap(); 276 | assert!(<[u8; 5]>::from(bstr2) == [b'a', b'b', b'c', 0, 0]); 277 | assert!(bstr == bstr2); 278 | 279 | assert!(format!("{:?}", bstr) == "\"abc\""); 280 | 281 | device.write_value(0x4020, 7, &bstr).unwrap(); 282 | 283 | let ret = device.read_value::(0x4020, 7).unwrap(); 284 | assert!(<[u8; 5]>::from(ret) == [b'a', b'b', b'c', 0, 0]); 285 | assert!(String::try_from(ret).unwrap() == "abc"); 286 | assert!(>::from(ret) == [b'a', b'b', b'c']); 287 | }) 288 | } 289 | 290 | #[test] 291 | fn test_wstring_type() { 292 | type WString5 = crate::strings::WString<5>; 293 | 294 | run_test(ServerOpts::default(), |device| { 295 | let mut wstr = WString5::try_from("abc").unwrap(); 296 | assert!(<[u16; 5]>::from(wstr) == [b'a' as u16, b'b' as u16, b'c' as u16, 0, 0]); 297 | assert!(wstr == &[b'a' as u16, b'b' as u16, b'c' as u16][..]); 298 | assert!(wstr == "abc"); 299 | assert!(wstr.len() == 3); 300 | assert!(wstr.backing_array().len() == 5); 301 | 302 | assert!(WString5::try_from(&[1, 2, 3, 4, 5, 6][..]).is_err()); 303 | assert!(WString5::try_from("abcdef").is_err()); 304 | 305 | assert!(format!("{:?}", wstr) == "\"abc\""); 306 | 307 | let wstr2 = WString5::try_from(&[b'a' as u16, b'b' as u16, b'c' as u16][..]).unwrap(); 308 | assert!(<[u16; 5]>::from(wstr2) == [b'a' as u16, b'b' as u16, b'c' as u16, 0, 0]); 309 | assert!(wstr == wstr2); 310 | 311 | device.write_value(0x4020, 7, &wstr).unwrap(); 312 | 313 | let ret = device.read_value::(0x4020, 7).unwrap(); 314 | assert!(<[u16; 5]>::from(ret) == [b'a' as u16, b'b' as u16, b'c' as u16, 0, 0]); 315 | assert!(String::try_from(ret).unwrap() == "abc"); 316 | assert!(>::from(ret) == [b'a' as u16, b'b' as u16, b'c' as u16]); 317 | }) 318 | } 319 | -------------------------------------------------------------------------------- /src/test/test_netid.rs: -------------------------------------------------------------------------------- 1 | // Tests for the AmsNetId/AmsAddr types. 2 | 3 | use std::net::Ipv4Addr; 4 | use std::str::FromStr; 5 | 6 | use crate::netid::*; 7 | 8 | #[test] 9 | fn test_netid() { 10 | let netid = AmsNetId::new(5, 123, 8, 9, 1, 1); 11 | assert!(!netid.is_zero()); 12 | assert_eq!(netid.to_string(), "5.123.8.9.1.1"); 13 | 14 | assert_eq!(netid, AmsNetId::from_ip(Ipv4Addr::new(5, 123, 8, 9), 1, 1)); 15 | 16 | assert_eq!(netid, AmsNetId::from_slice(&[5, 123, 8, 9, 1, 1]).unwrap()); 17 | assert_eq!(netid, AmsNetId::from([5, 123, 8, 9, 1, 1])); 18 | assert!(AmsNetId::from_slice(&[0; 8]).is_none()); 19 | 20 | assert_eq!(netid, AmsNetId::from_str("5.123.8.9.1.1").unwrap()); 21 | assert_eq!(netid, AmsNetId::from_str("5.123.8.9").unwrap()); 22 | assert!(AmsNetId::from_str("256.123.8.9.1.1").is_err()); 23 | assert!(AmsNetId::from_str("blah").is_err()); 24 | } 25 | 26 | #[test] 27 | fn test_addr() { 28 | let netid = AmsNetId::new(5, 123, 8, 9, 1, 1); 29 | let addr = AmsAddr::new(netid, 851); 30 | assert_eq!(addr.netid(), netid); 31 | assert_eq!(addr.port(), 851); 32 | assert_eq!(addr.to_string(), "5.123.8.9.1.1:851"); 33 | 34 | let mut buf = vec![]; 35 | addr.write_to(&mut buf).unwrap(); 36 | assert_eq!(buf, [5, 123, 8, 9, 1, 1, 0x53, 3]); 37 | 38 | assert_eq!(AmsAddr::read_from(&mut buf.as_slice()).unwrap(), addr); 39 | 40 | assert_eq!(addr, AmsAddr::from_str("5.123.8.9:851").unwrap()); 41 | assert!(AmsAddr::from_str("5.123.8.9.1.1:88851").is_err()); 42 | assert!(AmsAddr::from_str("256.123.8.9.1.1:851").is_err()); 43 | assert!(AmsAddr::from_str("5.123.8.9.1.1").is_err()); 44 | assert!(AmsAddr::from_str(":88851").is_err()); 45 | assert!(AmsAddr::from_str("blah").is_err()); 46 | assert!(AmsAddr::from_str("").is_err()); 47 | } 48 | -------------------------------------------------------------------------------- /src/test/test_udp.rs: -------------------------------------------------------------------------------- 1 | //! Test for the UDP client. 2 | 3 | // use std::io::{self, Read, Write}; 4 | // use std::time::Duration; 5 | use std::net::UdpSocket; 6 | 7 | use crate::udp; 8 | 9 | const OS_VERSION: &[u8] = b"\0\0\0\0\x05\0\0\0\x08\0\0\0\x09\0\0\0\x02\0\0\0T\0e\0s\0t\0\0\0"; 10 | 11 | fn udp_replier(sock: UdpSocket) { 12 | let mut buf = [0; 2048]; 13 | let my_addr = crate::AmsAddr::new(crate::AmsNetId::from([1, 2, 3, 4, 5, 6]), 10000); 14 | loop { 15 | let (n, sender) = sock.recv_from(&mut buf).unwrap(); 16 | let mut reply = udp::Message::new(udp::ServiceId::Identify, my_addr); 17 | if udp::Message::parse(&buf[..n], udp::ServiceId::Identify, false).is_ok() { 18 | reply.set_service(udp::ServiceId::Identify, true); 19 | reply.add_str(udp::Tag::ComputerName, "box"); 20 | reply.add_bytes(udp::Tag::OSVersion, OS_VERSION); 21 | reply.add_bytes(udp::Tag::TCVersion, b"\x04\x01\x07\x00"); 22 | } else if let Ok(msg) = udp::Message::parse(&buf[..n], udp::ServiceId::AddRoute, false) { 23 | reply.set_service(udp::ServiceId::AddRoute, true); 24 | let status = msg.get_str(udp::Tag::RouteName) != Some("route"); 25 | reply.add_u32(udp::Tag::Status, status as u32); 26 | } else { 27 | panic!("received invalid UDP packet"); 28 | }; 29 | sock.send_to(reply.as_bytes(), sender).unwrap(); 30 | } 31 | } 32 | 33 | #[test] 34 | fn test_udp() { 35 | let serversock = UdpSocket::bind("127.0.0.1:0").unwrap(); 36 | let port = serversock.local_addr().unwrap().port(); 37 | let tgt_netid = crate::AmsNetId::from([1, 2, 3, 4, 5, 6]); 38 | 39 | std::thread::spawn(move || udp_replier(serversock)); 40 | 41 | let netid = udp::get_netid(("127.0.0.1", port)).unwrap(); 42 | assert_eq!(netid, tgt_netid); 43 | 44 | let info = udp::get_info(("127.0.0.1", port)).unwrap(); 45 | assert_eq!(info.hostname, "box"); 46 | assert_eq!(info.netid, tgt_netid); 47 | assert_eq!(info.twincat_version, (4, 1, 7)); 48 | assert_eq!(info.os_version, ("Windows NT", 5, 8, 9, "Test".into())); 49 | 50 | udp::add_route(("127.0.0.1", port), tgt_netid, "a", Some("route"), None, None, false).unwrap(); 51 | assert!(udp::add_route(("127.0.0.1", port), tgt_netid, "a", None, None, None, false).is_err()); 52 | } 53 | -------------------------------------------------------------------------------- /src/udp.rs: -------------------------------------------------------------------------------- 1 | //! Implements the Beckhoff UDP message protocol for basic operations. 2 | 3 | use std::convert::TryInto; 4 | use std::io::Write; 5 | use std::net::{ToSocketAddrs, UdpSocket}; 6 | use std::{char, iter, str}; 7 | 8 | use byteorder::{ByteOrder, ReadBytesExt, WriteBytesExt, LE}; 9 | use zerocopy::byteorder::little_endian::{U16, U32}; 10 | use zerocopy::{FromBytes, Immutable, IntoBytes}; 11 | 12 | use crate::errors::ErrContext; 13 | use crate::{AmsAddr, AmsNetId, Error, Result}; 14 | 15 | /// Magic number for the first four bytes of each UDP packet. 16 | pub const BECKHOFF_UDP_MAGIC: u32 = 0x_71_14_66_03; 17 | 18 | /// Represents a message in the UDP protocol. 19 | pub struct Message { 20 | items: Vec<(u16, usize, usize)>, 21 | data: Vec, 22 | } 23 | 24 | /// The operation that the PLC should execute. 25 | #[derive(Debug, Clone, Copy)] 26 | #[repr(u32)] 27 | pub enum ServiceId { 28 | /// Identify the PLC, TwinCAT and OS versions. 29 | Identify = 1, 30 | /// Add a routing entry to the router. 31 | AddRoute = 6, 32 | } 33 | 34 | /// Identifies a piece of information in the UDP message. 35 | #[derive(Debug, Clone, Copy)] 36 | #[allow(missing_docs)] 37 | #[repr(u16)] 38 | pub enum Tag { 39 | Status = 1, 40 | Password = 2, 41 | TCVersion = 3, 42 | OSVersion = 4, 43 | ComputerName = 5, 44 | NetID = 7, 45 | Options = 9, 46 | RouteName = 12, 47 | UserName = 13, 48 | Fingerprint = 18, 49 | } 50 | 51 | impl Message { 52 | /// Create a new UDP message backed by a byte vector. 53 | pub fn new(service: ServiceId, source: AmsAddr) -> Self { 54 | let header = UdpHeader { 55 | magic: U32::new(BECKHOFF_UDP_MAGIC), 56 | invoke_id: U32::new(0), 57 | service: U32::new(service as u32), 58 | src_netid: source.netid(), 59 | src_port: U16::new(source.port()), 60 | num_items: U32::new(0), // will be adapted later 61 | }; 62 | let data = header.as_bytes().to_vec(); 63 | Self { items: Vec::with_capacity(8), data } 64 | } 65 | 66 | #[cfg(test)] 67 | pub(crate) fn set_service(&mut self, service: ServiceId, reply: bool) { 68 | let service = service as u32 | (if reply { 0x8000_0000 } else { 0 }); 69 | LE::write_u32(&mut self.data[8..12], service); 70 | } 71 | 72 | /// Parse a UDP message from a byte slice. 73 | pub fn parse(data: &[u8], exp_service: ServiceId, reply: bool) -> Result { 74 | let exp_service = exp_service as u32 | (if reply { 0x8000_0000 } else { 0 }); 75 | Self::parse_internal(data, exp_service) 76 | } 77 | 78 | fn parse_internal(data: &[u8], exp_service: u32) -> Result { 79 | let mut data_ptr = data; 80 | let magic = data_ptr.read_u32::().ctx("parsing UDP packet")?; 81 | let invoke_id = data_ptr.read_u32::().ctx("parsing UDP packet")?; 82 | let rep_service = data_ptr.read_u32::().ctx("parsing UDP packet")?; 83 | if magic != BECKHOFF_UDP_MAGIC { 84 | return Err(Error::Reply("parsing UDP packet", "invalid magic", magic)); 85 | } 86 | if invoke_id != 0 { 87 | // we're only generating 0 88 | return Err(Error::Reply("parsing UDP packet", "invalid invoke ID", invoke_id)); 89 | } 90 | if rep_service != exp_service { 91 | return Err(Error::Reply("parsing UDP packet", "invalid service ID", rep_service)); 92 | } 93 | let _src = AmsAddr::read_from(&mut data_ptr).ctx("parsing UDP packet")?; 94 | let nitems = data_ptr.read_u32::().ctx("parsing UDP packet")?; 95 | 96 | let mut items = Vec::with_capacity(nitems as usize); 97 | { 98 | let mut pos = 28; 99 | while let Ok(tag) = data_ptr.read_u16::() { 100 | let len = data_ptr.read_u16::().ctx("parsing UDP packet")? as usize; 101 | items.push((tag, pos, pos + len)); 102 | pos += len + 4; 103 | data_ptr = &data_ptr[len..]; 104 | } 105 | } 106 | Ok(Self { data: data.to_vec(), items }) 107 | } 108 | 109 | /// Add a tag containing arbitrary bytes. 110 | pub fn add_bytes(&mut self, tag: Tag, data: &[u8]) { 111 | self.data.write_u16::(tag as u16).expect("vec"); 112 | let start = self.data.len(); 113 | self.data.write_u16::(data.len() as u16).expect("vec"); 114 | self.data.write_all(data).expect("vec"); 115 | self.items.push((tag as u16, start, self.data.len())); 116 | LE::write_u32(&mut self.data[20..], self.items.len() as u32); 117 | } 118 | 119 | /// Add a tag containing a string with null terminator. 120 | pub fn add_str(&mut self, tag: Tag, data: &str) { 121 | self.data.write_u16::(tag as u16).expect("vec"); 122 | let start = self.data.len(); 123 | // add the null terminator 124 | self.data.write_u16::(data.len() as u16 + 1).expect("vec"); 125 | self.data.write_all(data.as_bytes()).expect("vec"); 126 | self.data.write_u8(0).expect("vec"); 127 | self.items.push((tag as u16, start, self.data.len())); 128 | LE::write_u32(&mut self.data[20..], self.items.len() as u32); 129 | } 130 | 131 | /// Add a tag containing an u32. 132 | pub fn add_u32(&mut self, tag: Tag, data: u32) { 133 | self.data.write_u16::(tag as u16).expect("vec"); 134 | let start = self.data.len(); 135 | self.data.write_u16::(4).expect("vec"); 136 | self.data.write_u32::(data).expect("vec"); 137 | self.items.push((tag as u16, start, self.data.len())); 138 | LE::write_u32(&mut self.data[20..], self.items.len() as u32); 139 | } 140 | 141 | fn map_tag<'a, O, F>(&'a self, tag: Tag, map: F) -> Option 142 | where 143 | F: Fn(&'a [u8]) -> Option, 144 | { 145 | self.items 146 | .iter() 147 | .find(|item| item.0 == tag as u16) 148 | .and_then(|&(_, i, j)| map(&self.data[i..j])) 149 | } 150 | 151 | /// Get the data for given tag as bytes. 152 | pub fn get_bytes(&self, tag: Tag) -> Option<&[u8]> { 153 | self.map_tag(tag, Some) 154 | } 155 | 156 | /// Get the data for given tag as null-terminated string. 157 | pub fn get_str(&self, tag: Tag) -> Option<&str> { 158 | // exclude the null terminator 159 | self.map_tag(tag, |b| str::from_utf8(&b[..b.len() - 1]).ok()) 160 | } 161 | 162 | /// Get the data for given tag as a u32. 163 | pub fn get_u32(&self, tag: Tag) -> Option { 164 | self.map_tag(tag, |mut b| b.read_u32::().ok()) 165 | } 166 | 167 | /// Get the AMS address originating the message. 168 | pub fn get_source(&self) -> AmsAddr { 169 | AmsAddr::read_from(&mut &self.data[12..20]).expect("size") 170 | } 171 | 172 | /// Create a complete UDP packet from the message and its header. 173 | pub fn as_bytes(&self) -> &[u8] { 174 | &self.data 175 | } 176 | 177 | /// Send the packet and receive a reply from the server. 178 | pub fn send_receive(&self, to: impl ToSocketAddrs) -> Result { 179 | // Send self as a request. 180 | let sock = UdpSocket::bind("0.0.0.0:0").ctx("binding UDP socket")?; 181 | sock.send_to(self.as_bytes(), to).ctx("sending UDP request")?; 182 | 183 | // Receive the reply. 184 | let mut reply = [0; 576]; 185 | sock.set_read_timeout(Some(std::time::Duration::from_secs(3))) 186 | .ctx("setting UDP timeout")?; 187 | let (n, _) = sock.recv_from(&mut reply).ctx("receiving UDP reply")?; 188 | 189 | // Parse the reply. 190 | Self::parse_internal(&reply[..n], LE::read_u32(&self.data[8..]) | 0x8000_0000) 191 | } 192 | } 193 | 194 | /// Send a UDP message for setting a route. 195 | /// 196 | /// - `target`: (host, port) of the AMS router to add the route to 197 | /// (the port should normally be `ads::UDP_PORT`) 198 | /// - `netid`: the NetID of the route's target 199 | /// - `host`: the IP address or hostname of the route's target (when using 200 | /// hostnames instead of IP addresses, beware of Windows hostname resolution) 201 | /// - `routename`: name of the route, default is `host` 202 | /// - `username`: system username for the router, default is `Administrator` 203 | /// - `password`: system password for the given user, default is `1` 204 | /// - `temporary`: marks the route as "temporary" 205 | pub fn add_route( 206 | target: (&str, u16), netid: AmsNetId, host: &str, routename: Option<&str>, username: Option<&str>, 207 | password: Option<&str>, temporary: bool, 208 | ) -> Result<()> { 209 | let mut packet = Message::new(ServiceId::AddRoute, AmsAddr::new(netid, 0)); 210 | packet.add_bytes(Tag::NetID, &netid.0); 211 | packet.add_str(Tag::ComputerName, host); 212 | packet.add_str(Tag::UserName, username.unwrap_or("Administrator")); 213 | packet.add_str(Tag::Password, password.unwrap_or("1")); 214 | packet.add_str(Tag::RouteName, routename.unwrap_or(host)); 215 | if temporary { 216 | packet.add_u32(Tag::Options, 1); 217 | } 218 | 219 | let reply = packet.send_receive(target)?; 220 | 221 | match reply.get_u32(Tag::Status) { 222 | None => Err(Error::Reply("setting route", "no status in reply", 0)), 223 | Some(0) => Ok(()), 224 | Some(n) => crate::errors::ads_error("setting route", n), 225 | } 226 | } 227 | 228 | /// Send a UDP message for querying remote system NetID. 229 | pub fn get_netid(target: (&str, u16)) -> Result { 230 | let packet = Message::new(ServiceId::Identify, AmsAddr::default()); 231 | let reply = packet.send_receive(target)?; 232 | Ok(reply.get_source().netid()) 233 | } 234 | 235 | /// Information about the system running TwinCAT. 236 | pub struct SysInfo { 237 | /// AMS NetID of the system. 238 | pub netid: AmsNetId, 239 | /// Hostname of the system. 240 | pub hostname: String, 241 | /// The TwinCAT (major, minor, build) version. 242 | pub twincat_version: (u8, u8, u16), 243 | /// The OS (name, major, minor, build, service_pack) version. 244 | pub os_version: (&'static str, u32, u32, u32, String), 245 | /// The system's fingerprint. 246 | pub fingerprint: String, 247 | } 248 | 249 | /// Send a UDP message for querying remote system information. 250 | pub fn get_info(target: (&str, u16)) -> Result { 251 | let request = Message::new(ServiceId::Identify, AmsAddr::default()); 252 | let reply = request.send_receive(target)?; 253 | 254 | // Parse TwinCAT version. 255 | let tcver = reply.get_bytes(Tag::TCVersion).unwrap_or(&[]); 256 | let twincat_version = if tcver.len() >= 4 { 257 | let tcbuild = u16::from_le_bytes(tcver[2..4].try_into().expect("size")); 258 | (tcver[0], tcver[1], tcbuild) 259 | } else { 260 | (0, 0, 0) 261 | }; 262 | 263 | // Parse OS version. If Windows OSVERSIONINFO structure, it will 264 | // consists of major/minor/build versions, the platform, and a "service 265 | // pack" string, coded as UTF-16. 266 | // If TwinCAT/BSD currently it will give major minor and build that is displayed 267 | let os_version = if let Some(mut bytes) = reply.get_bytes(Tag::OSVersion) { 268 | if bytes.len() >= 22 { 269 | // Size of the structure (redundant). 270 | let _ = bytes.read_u32::().expect("size"); 271 | let major = bytes.read_u32::().expect("size"); 272 | let minor = bytes.read_u32::().expect("size"); 273 | let build = bytes.read_u32::().expect("size"); 274 | let platform = match bytes.read_u32::().expect("size") { 275 | 0 => "TwinCAT/BSD", 276 | 1 => "TC/RTOS", 277 | 2 => "Windows NT", 278 | 3 => "Windows CE", 279 | _ => "Unknown platform", 280 | }; 281 | let string = if platform == "TC/RTOS" { 282 | bytes.iter().take_while(|&&b| b != 0).map(|&b| b as char).collect() 283 | } else if platform == "TwinCAT/BSD" { 284 | // The following data is TwinCAT/BSD in bytes. But we know the platform from 0. 285 | "".into() 286 | } else { 287 | iter::from_fn(|| bytes.read_u16::().ok()) 288 | .take_while(|&ch| ch != 0) 289 | .filter_map(|ch| char::from_u32(ch as u32)) 290 | .collect() 291 | }; 292 | (platform, major, minor, build, string) 293 | } else { 294 | ("Unknown OS info format", 0, 0, 0, "".into()) 295 | } 296 | } else { 297 | ("No OS info", 0, 0, 0, "".into()) 298 | }; 299 | Ok(SysInfo { 300 | netid: reply.get_source().netid(), 301 | hostname: reply.get_str(Tag::ComputerName).unwrap_or("unknown").into(), 302 | twincat_version, 303 | os_version, 304 | fingerprint: reply.get_str(Tag::Fingerprint).unwrap_or("").into(), 305 | }) 306 | } 307 | 308 | #[derive(FromBytes, IntoBytes, Immutable, Default)] 309 | #[repr(C)] 310 | pub(crate) struct UdpHeader { 311 | magic: U32, 312 | invoke_id: U32, 313 | service: U32, 314 | src_netid: AmsNetId, 315 | src_port: U16, 316 | num_items: U32, 317 | } 318 | --------------------------------------------------------------------------------