├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.txt ├── ethercat-derive ├── Cargo.toml └── src │ └── lib.rs ├── ethercat-plc ├── Cargo.toml └── src │ ├── beckhoff.rs │ ├── image.rs │ ├── lib.rs │ ├── mlz_spec.rs │ ├── plc.rs │ └── server.rs ├── ethercat-sys ├── Cargo.toml ├── build.rs └── src │ └── lib.rs ├── ethercat ├── Cargo.toml └── src │ ├── lib.rs │ ├── master.rs │ └── types.rs └── src └── demo.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "ethercat-testplc" 5 | version = "0.1.0" 6 | authors = ["Georg Brandl "] 7 | edition = "2018" 8 | 9 | [[bin]] 10 | name = "demo" 11 | path = "src/demo.rs" 12 | 13 | [dependencies] 14 | byteorder = "1.3" 15 | time = "0.1.42" 16 | ethercat = { path = "ethercat" } 17 | ethercat-plc = { path = "ethercat-plc" } 18 | ethercat-derive = { path = "ethercat-derive" } 19 | -------------------------------------------------------------------------------- /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.txt: -------------------------------------------------------------------------------- 1 | EtherCAT/Rust toolbox 2 | ===================== 3 | 4 | An experimental Rust automation toolbox using the IgH (Etherlab) EtherCAT master. 5 | -------------------------------------------------------------------------------- /ethercat-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethercat-derive" 3 | description = "Automatic deriving of traits in the ethercat-plc crate" 4 | version = "0.1.1" 5 | authors = ["Georg Brandl "] 6 | license = "MIT/Apache-2.0" 7 | edition = "2018" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | quote = "1.0" 14 | syn = "1.0" 15 | proc-macro2 = "1.0" 16 | -------------------------------------------------------------------------------- /ethercat-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | //! Support for deriving ethercat-plc traits for a struct. 5 | 6 | extern crate proc_macro; // needed even in 2018 7 | 8 | use self::proc_macro::TokenStream; 9 | use syn::parse_macro_input; 10 | use quote::quote; 11 | use quote::ToTokens; 12 | 13 | 14 | #[proc_macro_derive(SlaveProcessImage, attributes(slave_id, pdos, entry))] 15 | pub fn derive_single_process_image(input: TokenStream) -> TokenStream { 16 | let input = parse_macro_input!(input as syn::DeriveInput); 17 | let ident = input.ident; 18 | 19 | let id_str = ident.to_string(); 20 | let slave_id = if id_str.starts_with("EK") { 21 | let nr = id_str[2..6].parse::().unwrap(); 22 | quote!(ethercat::SlaveId { vendor_id: 2, product_code: (#nr << 16) | 0x2c52 }) 23 | } else if id_str.starts_with("EL") { 24 | let nr = id_str[2..6].parse::().unwrap(); 25 | quote!(ethercat::SlaveId { vendor_id: 2, product_code: (#nr << 16) | 0x3052 }) 26 | } else { 27 | panic!("cannot interpret struct name '{}' into a slave ID", id_str); 28 | }; 29 | 30 | let mut sync_infos = vec![]; 31 | let mut pdo_regs = vec![]; 32 | let mut running_size = 0usize; 33 | let mut pdo_mapping = std::collections::HashMap::new(); 34 | 35 | if let syn::Data::Struct(syn::DataStruct { 36 | fields: syn::Fields::Named(flds), .. 37 | }) = input.data { 38 | for field in flds.named { 39 | let ty = field.ty.into_token_stream().to_string(); 40 | let bitlen = match &*ty { 41 | "u8" | "i8" => 8, 42 | "u16" | "i16" => 16, 43 | "u32" | "i32" | "f32" => 32, 44 | "u64" | "i64" | "f64" => 64, 45 | _ => panic!("cannot handle type '{}' in image", ty) 46 | }; 47 | for attr in &field.attrs { 48 | if attr.path.is_ident("entry") { 49 | if let syn::Meta::List(syn::MetaList { nested, .. }) = 50 | attr.parse_meta().unwrap() 51 | { 52 | let (pdo_str, ix, subix) = if nested.len() == 2 { 53 | ("".into(), &nested[0], &nested[1]) 54 | } else { 55 | let pdo = &nested[0]; 56 | (quote!(#pdo).to_string(), &nested[1], &nested[2]) 57 | }; 58 | pdo_regs.push(quote! { 59 | (ethercat::PdoEntryIndex { index: #ix, 60 | subindex: #subix }, 61 | ethercat::Offset { byte: #running_size, bit: 0 }) 62 | }); 63 | pdo_mapping.entry(pdo_str).or_insert_with(Vec::new).push(quote! { 64 | ethercat::PdoEntryInfo { 65 | index: PdoEntryIndex { index: #ix, subindex: #subix }, 66 | bit_length: #bitlen as u8, 67 | } 68 | }); 69 | } 70 | } 71 | } 72 | running_size += bitlen / 8; 73 | } 74 | } else { 75 | panic!("SlaveProcessImage must be a struct with named fields"); 76 | } 77 | 78 | for attr in &input.attrs { 79 | if attr.path.is_ident("pdos") { 80 | if let syn::Meta::List(syn::MetaList { nested, .. }) = 81 | attr.parse_meta().unwrap() 82 | { 83 | let sm = &nested[0]; 84 | let sd = &nested[1]; 85 | let mut pdos = vec![]; 86 | for pdo_index in nested.iter().skip(2) { 87 | let pdo_str = quote!(#pdo_index).to_string(); 88 | let entries = &pdo_mapping.get(&pdo_str).map_or(&[][..], |v| &*v); 89 | pdos.push(quote! { 90 | ethercat::PdoInfo { 91 | index: #pdo_index, 92 | entries: { 93 | const ENTRIES: &[ethercat::PdoEntryInfo] = 94 | &[#( #entries ),*]; ENTRIES 95 | } 96 | } 97 | }) 98 | } 99 | sync_infos.push(quote! { 100 | ethercat::SyncInfo { 101 | index: #sm, 102 | direction: ethercat::SyncDirection::#sd, 103 | watchdog_mode: ethercat::WatchdogMode::Default, 104 | pdos: { 105 | const INFOS: &[ethercat::PdoInfo<'static>] = 106 | &[#( #pdos ),*]; INFOS 107 | } 108 | } 109 | }); 110 | } 111 | } 112 | } 113 | 114 | let sync_infos = if sync_infos.is_empty() { 115 | quote!(None) 116 | } else { 117 | quote!(Some(vec![#( #sync_infos ),*])) 118 | }; 119 | 120 | let generated = quote! { 121 | #[automatically_derived] 122 | impl ProcessImage for #ident { 123 | const SLAVE_COUNT: usize = 1; 124 | fn get_slave_ids() -> Vec { vec![#slave_id] } 125 | fn get_slave_pdos() -> Vec>>> { 126 | vec![#sync_infos] 127 | } 128 | fn get_slave_regs() -> Vec> { 129 | vec![vec![ #( #pdo_regs ),* ]] 130 | } 131 | } 132 | }; 133 | 134 | // println!("{}", generated); 135 | generated.into() 136 | } 137 | 138 | 139 | #[proc_macro_derive(ProcessImage, attributes(sdo))] 140 | pub fn derive_process_image(input: TokenStream) -> TokenStream { 141 | let input = parse_macro_input!(input as syn::DeriveInput); 142 | let ident = input.ident; 143 | 144 | let mut slave_sdos = vec![]; 145 | let mut slave_tys = vec![]; 146 | 147 | if let syn::Data::Struct(syn::DataStruct { 148 | fields: syn::Fields::Named(flds), .. 149 | }) = input.data { 150 | for field in flds.named { 151 | let mut sdos = vec![]; 152 | for attr in &field.attrs { 153 | if attr.path.is_ident("sdo") { 154 | if let syn::Meta::List(syn::MetaList { nested, .. }) = 155 | attr.parse_meta().unwrap() 156 | { 157 | let ix = &nested[0]; 158 | let subix = &nested[1]; 159 | let data_expr = &nested[2]; 160 | let data_str = if let syn::NestedMeta::Lit(syn::Lit::Str(s)) = data_expr { 161 | syn::parse_str::(&s.value()).unwrap() 162 | } else { 163 | panic!("invalid SDO value, must be stringified") 164 | }; 165 | sdos.push(quote! { 166 | (ethercat::SdoIndex { index: #ix, subindex: #subix }, 167 | Box::new(#data_str)) 168 | }); 169 | } 170 | } 171 | } 172 | let ty = field.ty; 173 | if sdos.is_empty() { 174 | slave_sdos.push(quote!( res.extend(#ty::get_slave_sdos()); )); 175 | } else { 176 | slave_sdos.push(quote!( res.push(vec![#( #sdos ),*]); )); 177 | } 178 | slave_tys.push(ty); 179 | } 180 | } else { 181 | return compile_error("only structs with named fields can be a process image"); 182 | } 183 | 184 | let generated = quote! { 185 | #[automatically_derived] 186 | impl ProcessImage for #ident { 187 | const SLAVE_COUNT: usize = #( #slave_tys::SLAVE_COUNT )+*; 188 | fn get_slave_ids() -> Vec { 189 | let mut res = vec![]; #( res.extend(#slave_tys::get_slave_ids()); )* res 190 | } 191 | fn get_slave_pdos() -> Vec>>> { 192 | let mut res = vec![]; #( res.extend(#slave_tys::get_slave_pdos()); )* res 193 | } 194 | fn get_slave_regs() -> Vec> { 195 | let mut res = vec![]; #( res.extend(#slave_tys::get_slave_regs()); )* res 196 | } 197 | fn get_slave_sdos() -> Vec)>> { 198 | let mut res = vec![]; #(#slave_sdos)* res 199 | } 200 | } 201 | }; 202 | 203 | // println!("{}", generated); 204 | generated.into() 205 | } 206 | 207 | #[proc_macro_derive(ExternImage, attributes(plc))] 208 | pub fn derive_extern_image(input: TokenStream) -> TokenStream { 209 | let input = parse_macro_input!(input as syn::DeriveInput); 210 | let ident = input.ident; 211 | 212 | // currently a no-op, later: auto-generate Default from #[plc] attributes 213 | let generated = quote! { 214 | impl ExternImage for #ident {} 215 | }; 216 | generated.into() 217 | } 218 | 219 | fn compile_error(message: impl Into) -> TokenStream { 220 | let message = message.into(); 221 | quote!(compile_error! { #message }).into() 222 | } 223 | -------------------------------------------------------------------------------- /ethercat-plc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethercat-plc" 3 | description = "Tools to build a PLC like program using the ethercat crate" 4 | keywords = ["ethercat", "plc", "automation"] 5 | version = "0.1.2" 6 | authors = ["Georg Brandl "] 7 | license = "MIT/Apache-2.0" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | log = "0.4.6" 12 | libc = "0.2.49" 13 | time = "0.1.42" 14 | mlzlog = "0.5.1" 15 | byteorder = "1.3.1" 16 | crossbeam-channel = "0.3.8" 17 | ethercat = { path = "../ethercat", version = "0.1.1" } 18 | ethercat-derive = { path = "../ethercat-derive", version = "0.1.0" } 19 | -------------------------------------------------------------------------------- /ethercat-plc/src/beckhoff.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | use ethercat::*; 5 | use ethercat_derive::SlaveProcessImage; 6 | use crate::image::ProcessImage; 7 | 8 | #[repr(C, packed)] 9 | #[derive(SlaveProcessImage)] 10 | pub struct EK1100 {} 11 | 12 | #[repr(C, packed)] 13 | #[derive(SlaveProcessImage)] 14 | pub struct EL1008 { 15 | #[entry(0x6000, 1)] pub input: u8, 16 | } 17 | 18 | #[repr(C, packed)] 19 | #[derive(SlaveProcessImage)] 20 | #[pdos(3, Input, 0x1A00, 0x1A01)] 21 | #[pdos(2, Output, 0x1600, 0x1601)] 22 | pub struct EL1502 { 23 | #[entry(0x1A00, 0x6000, 1)] pub status_ch1: u16, 24 | #[entry(0x1A00, 0x6000, 17)] pub value_ch1: u32, 25 | #[entry(0x1A01, 0x6010, 1)] pub status_ch2: u16, 26 | #[entry(0x1A01, 0x6010, 17)] pub value_ch2: u32, 27 | 28 | #[entry(0x1600, 0x7000, 1)] pub control_ch1: u16, 29 | #[entry(0x1600, 0x7000, 17)] pub setvalue_ch1: u32, 30 | #[entry(0x1601, 0x7010, 1)] pub control_ch2: u16, 31 | #[entry(0x1601, 0x7010, 17)] pub setvalue_ch2: u32, 32 | } 33 | 34 | #[repr(C, packed)] 35 | #[derive(SlaveProcessImage)] 36 | pub struct EL1859 { 37 | #[entry(0x6000, 1)] pub input: u8, 38 | #[entry(0x7080, 1)] pub output: u8, 39 | } 40 | 41 | #[repr(C, packed)] 42 | #[derive(SlaveProcessImage)] 43 | pub struct EL2008 { 44 | #[entry(0x7000, 1)] pub output: u8, 45 | } 46 | 47 | #[repr(C, packed)] 48 | #[derive(SlaveProcessImage)] 49 | pub struct EL3104 { 50 | #[entry(0x6000, 1)] pub ch1_status: u16, 51 | #[entry(0x6000, 17)] pub ch1: i16, 52 | #[entry(0x6010, 1)] pub ch2_status: u16, 53 | #[entry(0x6010, 17)] pub ch2: i16, 54 | #[entry(0x6020, 1)] pub ch3_status: u16, 55 | #[entry(0x6020, 17)] pub ch3: i16, 56 | #[entry(0x6030, 1)] pub ch4_status: u16, 57 | #[entry(0x6030, 17)] pub ch4: i16, 58 | } 59 | 60 | #[repr(C, packed)] 61 | #[derive(SlaveProcessImage)] 62 | pub struct EL4132 { 63 | #[entry(0x3001, 1)] pub ch1: i16, 64 | #[entry(0x3002, 1)] pub ch2: i16, 65 | } 66 | 67 | #[repr(C, packed)] 68 | #[derive(SlaveProcessImage)] 69 | #[pdos(3, Input, 0x1A01, 0x1A03, 0x1A04, 0x1A08)] 70 | #[pdos(2, Output, 0x1601, 0x1602, 0x1604)] 71 | pub struct EL7047_Velocity { 72 | #[entry(0x1A01, 0x6000, 1)] pub enc_status: u16, 73 | #[entry(0x1A01, 0x6000, 11)] pub enc_counter: u32, 74 | #[entry(0x1A01, 0x6000, 12)] pub enc_latch: u32, 75 | #[entry(0x1A03, 0x6010, 1)] pub mot_status: u16, 76 | #[entry(0x1A04, 0x6010, 11)] pub info_data1: u16, 77 | #[entry(0x1A04, 0x6010, 12)] pub info_data2: u16, 78 | #[entry(0x1A08, 0x6010, 14)] pub mot_position: i32, 79 | 80 | #[entry(0x1601, 0x7000, 1)] pub enc_control: u16, 81 | #[entry(0x1601, 0x7000, 11)] pub enc_set_counter: u32, 82 | #[entry(0x1602, 0x7010, 1)] pub mot_control: u16, 83 | #[entry(0x1604, 0x7010, 21)] pub mot_velocity: i16, 84 | } 85 | 86 | #[repr(C, packed)] 87 | #[derive(SlaveProcessImage)] 88 | #[pdos(3, Input, 0x1A01, 0x1A03, 0x1A04, 0x1A08)] 89 | #[pdos(2, Output, 0x1601, 0x1602, 0x1603)] 90 | pub struct EL7047_Position { 91 | #[entry(0x1A01, 0x6000, 1)] pub enc_status: u16, 92 | #[entry(0x1A01, 0x6000, 11)] pub enc_counter: u32, 93 | #[entry(0x1A01, 0x6000, 12)] pub enc_latch: u32, 94 | #[entry(0x1A03, 0x6010, 1)] pub mot_status: u16, 95 | #[entry(0x1A04, 0x6010, 11)] pub info_data1: u16, 96 | #[entry(0x1A04, 0x6010, 12)] pub info_data2: u16, 97 | #[entry(0x1A08, 0x6010, 14)] pub mot_position: i32, 98 | 99 | #[entry(0x1601, 0x7000, 1)] pub enc_control: u16, 100 | #[entry(0x1601, 0x7000, 11)] pub enc_set_counter: u32, 101 | #[entry(0x1602, 0x7010, 1)] pub mot_control: u16, 102 | #[entry(0x1603, 0x7010, 11)] pub mot_target: i32, 103 | } 104 | 105 | #[repr(C, packed)] 106 | #[derive(SlaveProcessImage)] 107 | #[pdos(3, Input, 0x1A01, 0x1A03, 0x1A07)] 108 | #[pdos(2, Output, 0x1601, 0x1602, 0x1606)] 109 | pub struct EL7047_Positioning { 110 | #[entry(0x1A01, 0x6000, 1)] pub enc_status: u16, 111 | #[entry(0x1A01, 0x6000, 11)] pub enc_counter: u32, 112 | #[entry(0x1A01, 0x6000, 12)] pub enc_latch: u32, 113 | #[entry(0x1A03, 0x6010, 1)] pub mot_status: u16, 114 | #[entry(0x1A07, 0x6020, 1)] pub pos_status: u16, 115 | #[entry(0x1A07, 0x6020, 11)] pub act_pos: i32, 116 | #[entry(0x1A07, 0x6020, 21)] pub act_velo: u16, 117 | #[entry(0x1A07, 0x6020, 22)] pub drv_time: u32, 118 | 119 | #[entry(0x1601, 0x7000, 1)] pub enc_control: u16, 120 | #[entry(0x1601, 0x7000, 11)] pub enc_set_counter: u32, 121 | #[entry(0x1602, 0x7010, 1)] pub mot_control: u16, 122 | #[entry(0x1606, 0x7020, 1)] pub pos_control: u16, 123 | #[entry(0x1606, 0x7020, 11)] pub target_pos: u32, 124 | #[entry(0x1606, 0x7020, 21)] pub target_velo: u16, 125 | #[entry(0x1606, 0x7020, 22)] pub start_type: u16, 126 | #[entry(0x1606, 0x7020, 23)] pub accel: u16, 127 | #[entry(0x1606, 0x7020, 24)] pub decel: u16, 128 | } 129 | -------------------------------------------------------------------------------- /ethercat-plc/src/image.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | //! Tools to create a typesafe process image matching with possible slave PDOs. 5 | 6 | use ethercat::*; 7 | 8 | pub trait ProcessImage { 9 | // configuration APIs 10 | const SLAVE_COUNT: usize; 11 | fn get_slave_ids() -> Vec; 12 | fn get_slave_pdos() -> Vec>>> { vec![None] } 13 | fn get_slave_regs() -> Vec> { vec![vec![]] } 14 | fn get_slave_sdos() -> Vec)>> { vec![vec![]] } 15 | 16 | fn size() -> usize where Self: Sized { 17 | std::mem::size_of::() 18 | } 19 | 20 | fn cast(data: &mut [u8]) -> &mut Self where Self: Sized { 21 | unsafe { std::mem::transmute(data.as_mut_ptr()) } 22 | } 23 | } 24 | 25 | pub trait ExternImage : Default { 26 | fn size() -> usize where Self: Sized { 27 | std::mem::size_of::() 28 | } 29 | 30 | fn cast(&mut self) -> &mut [u8] where Self: Sized { 31 | unsafe { 32 | std::slice::from_raw_parts_mut(self as *mut _ as *mut u8, Self::size()) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ethercat-plc/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | mod plc; 5 | mod image; 6 | mod server; 7 | 8 | pub mod beckhoff; 9 | pub mod mlz_spec; 10 | 11 | pub use self::plc::{Plc, PlcBuilder}; 12 | pub use self::image::{ExternImage, ProcessImage}; 13 | pub use ethercat_derive::{ExternImage, ProcessImage, SlaveProcessImage}; 14 | -------------------------------------------------------------------------------- /ethercat-plc/src/mlz_spec.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | use byteorder::{ByteOrder, NativeEndian as NE}; 5 | 6 | pub const MAGIC: f32 = 2015.02; 7 | 8 | pub const RESET: u16 = 0x0000; 9 | pub const IDLE: u16 = 0x1000; 10 | pub const WARN: u16 = 0x3000; 11 | pub const START: u16 = 0x5000; 12 | pub const BUSY: u16 = 0x6000; 13 | pub const STOP: u16 = 0x7000; 14 | pub const ERROR: u16 = 0x8000; 15 | 16 | #[repr(C)] 17 | #[derive(Default)] 18 | pub struct DiscreteOutput { 19 | pub value: i16, 20 | pub target: i16, 21 | pub status: u16, 22 | } 23 | 24 | #[repr(C)] 25 | #[derive(Default)] 26 | pub struct FlatOutput1 { 27 | pub value: f32, 28 | pub target: f32, 29 | pub status: u16, 30 | pub aux: u16, 31 | pub param1: f32, 32 | } 33 | 34 | pub fn copy_string(dst: &mut [u16], src: &str) { 35 | let mut nbytes = src.len().min(dst.len() * 2); 36 | let mut src_vec; 37 | let src = if nbytes % 2 == 1 { 38 | src_vec = src.to_string(); 39 | src_vec.push('\0'); 40 | nbytes += 1; 41 | &src_vec 42 | } else { 43 | src 44 | }; 45 | NE::read_u16_into(&src[..nbytes].as_bytes(), &mut dst[..nbytes/2]) 46 | } 47 | 48 | pub fn copy_float(dst: &mut [u16], f: f32) { 49 | let mut buf = [0u8; 4]; 50 | NE::write_f32(&mut buf, f); 51 | NE::read_u16_into(&buf, &mut dst[..2]); 52 | } 53 | -------------------------------------------------------------------------------- /ethercat-plc/src/plc.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | //! Wrap an EtherCAT master and slave configuration and provide a PLC-like 5 | //! environment for cyclic task execution. 6 | 7 | use std::{thread, time::Duration, marker::PhantomData}; 8 | use time::precise_time_ns; 9 | use byteorder::{ByteOrder, NativeEndian as NE}; 10 | use crossbeam_channel::{Sender, Receiver}; 11 | use mlzlog; 12 | use log::*; 13 | 14 | use ethercat::*; 15 | 16 | use crate::image::{ProcessImage, ExternImage}; 17 | use crate::server::{Server, Request, Response}; 18 | 19 | #[derive(Default)] 20 | pub struct PlcBuilder { 21 | name: String, 22 | master_id: Option, 23 | cycle_freq: Option, 24 | server_addr: Option, 25 | logfile_base: Option, 26 | debug_logging: bool, 27 | } 28 | 29 | impl PlcBuilder { 30 | pub fn new(name: impl Into) -> Self { 31 | Self { 32 | name: name.into(), 33 | .. Self::default() 34 | } 35 | } 36 | 37 | pub fn master_id(mut self, id: u32) -> Self { 38 | self.master_id = Some(id); 39 | self 40 | } 41 | 42 | pub fn cycle_freq(mut self, freq: u32) -> Self { 43 | self.cycle_freq = Some(freq); 44 | self 45 | } 46 | 47 | pub fn with_server(mut self, addr: impl Into) -> Self { 48 | self.server_addr = Some(addr.into()); 49 | self 50 | } 51 | 52 | pub fn logging_cfg(mut self, logfile_base: Option, debug_logging: bool) -> Self { 53 | self.logfile_base = logfile_base; 54 | self.debug_logging = debug_logging; 55 | self 56 | } 57 | 58 | pub fn build(self) -> Result> { 59 | mlzlog::init(self.logfile_base, &self.name, false, self.debug_logging, true)?; 60 | 61 | let channels = if let Some(addr) = self.server_addr { 62 | let (srv, r, w) = Server::new(); 63 | srv.start(&addr)?; 64 | Some((r, w)) 65 | } else { 66 | None 67 | }; 68 | 69 | let mut master = Master::reserve(self.master_id.unwrap_or(0))?; 70 | let domain = master.create_domain()?; 71 | 72 | debug!("PLC: EtherCAT master opened"); 73 | 74 | // XXX 75 | // master.sdo_download(1, SdoIndex::new(0x1011, 1), &0x64616F6Cu32)?; 76 | // master.sdo_download(2, SdoIndex::new(0x1011, 1), &0x64616F6Cu32)?; 77 | 78 | let slave_ids = P::get_slave_ids(); 79 | let slave_pdos = P::get_slave_pdos(); 80 | let slave_regs = P::get_slave_regs(); 81 | let slave_sdos = P::get_slave_sdos(); 82 | for (i, (((id, pdos), regs), sdos)) in slave_ids.into_iter() 83 | .zip(slave_pdos) 84 | .zip(slave_regs) 85 | .zip(slave_sdos) 86 | .enumerate() 87 | { 88 | let mut config = master.configure_slave(SlaveAddr::ByPos(i as u16), id)?; 89 | if let Some(pdos) = pdos { 90 | config.config_pdos(&pdos)?; 91 | } 92 | let mut first_byte = 0; 93 | for (j, (entry, mut expected_position)) in regs.into_iter().enumerate() { 94 | let pos = config.register_pdo_entry(entry, domain)?; 95 | if j == 0 { 96 | if pos.bit != 0 { 97 | panic!("first PDO of slave {} not byte-aligned", i); 98 | } 99 | first_byte = pos.byte; 100 | } else { 101 | expected_position.byte += first_byte; 102 | if pos != expected_position { 103 | panic!("slave {} pdo {}: {:?} != {:?}", i, j, pos, expected_position); 104 | } 105 | } 106 | } 107 | 108 | for (sdo_index, data) in sdos { 109 | config.add_sdo(sdo_index, &*data)?; 110 | } 111 | 112 | let cfg_index = config.index(); 113 | drop(config); 114 | 115 | // ensure that the slave is actually present 116 | if master.get_config_info(cfg_index)?.slave_position.is_none() { 117 | panic!("slave {} does not match config", i); 118 | } 119 | } 120 | 121 | info!("PLC: EtherCAT slaves configured"); 122 | 123 | let domain_size = master.domain(domain).size()?; 124 | if domain_size != P::size() { 125 | panic!("size: {} != {}", domain_size, P::size()); 126 | } 127 | 128 | master.activate()?; 129 | info!("PLC: EtherCAT master activated"); 130 | 131 | Ok(Plc { 132 | master: master, 133 | domain: domain, 134 | server: channels, 135 | sleep: 1000_000_000 / self.cycle_freq.unwrap_or(1000) as u64, 136 | _types: (PhantomData, PhantomData), 137 | }) 138 | } 139 | } 140 | 141 | 142 | pub struct Plc { 143 | master: Master, 144 | domain: DomainHandle, 145 | sleep: u64, 146 | server: Option<(Receiver, Sender)>, 147 | _types: (PhantomData

, PhantomData), 148 | } 149 | 150 | const BASE: usize = 0x3000; 151 | 152 | impl Plc { 153 | pub fn run(&mut self, mut cycle_fn: F) 154 | where F: FnMut(&mut P, &mut E) 155 | { 156 | let mut ext = E::default(); 157 | let mut cycle_start = precise_time_ns(); 158 | 159 | loop { 160 | // process data exchange + logic 161 | if let Err(e) = self.single_cycle(&mut cycle_fn, &mut ext) { 162 | // XXX: logging unconditionally here is bad, could repeat endlessly 163 | warn!("error in cycle: {}", e); 164 | } 165 | 166 | // external data exchange via modbus 167 | if let Some((r, w)) = self.server.as_mut() { 168 | while let Ok(mut req) = r.try_recv() { 169 | debug!("PLC got request: {:?}", req); 170 | let data = ext.cast(); 171 | let resp = if req.addr < BASE || req.addr + req.count > BASE + E::size()/2 { 172 | Response::Error(req, 2) 173 | } else { 174 | let from = 2 * (req.addr - BASE); 175 | let to = from + 2 * req.count; 176 | if let Some(ref mut values) = req.write { 177 | // write request 178 | NE::write_u16_into(values, &mut data[from..to]); 179 | let values = req.write.take().unwrap(); 180 | Response::Ok(req, values) 181 | } else { 182 | // read request 183 | let mut values = vec![0; req.count]; 184 | NE::read_u16_into(&data[from..to], &mut values); 185 | Response::Ok(req, values) 186 | } 187 | }; 188 | debug!("PLC response: {:?}", resp); 189 | if let Err(e) = w.send(resp) { 190 | warn!("could not send back response: {}", e); 191 | } 192 | } 193 | } 194 | 195 | // wait until next cycle 196 | let now = precise_time_ns(); 197 | cycle_start += self.sleep; 198 | if cycle_start > now { 199 | thread::sleep(Duration::from_nanos(cycle_start - now)); 200 | } 201 | } 202 | } 203 | 204 | fn single_cycle(&mut self, mut cycle_fn: F, ext: &mut E) -> Result<()> 205 | where F: FnMut(&mut P, &mut E) 206 | { 207 | self.master.receive()?; 208 | self.master.domain(self.domain).process()?; 209 | 210 | // XXX: check working counters periodically, etc. 211 | // println!("master state: {:?}", self.master.state()); 212 | // println!("domain state: {:?}", self.master.domain(self.domain).state()); 213 | 214 | let data = P::cast(self.master.domain_data(self.domain)); 215 | cycle_fn(data, ext); 216 | 217 | self.master.domain(self.domain).queue()?; 218 | self.master.send()?; 219 | Ok(()) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /ethercat-plc/src/server.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | //! Modbus server allowing access to the PLC "memory" variables. 5 | 6 | use std::collections::BTreeMap; 7 | use std::io::{Result, Read, Write, ErrorKind}; 8 | use std::net::{TcpListener, TcpStream}; 9 | use std::thread; 10 | use log::*; 11 | use byteorder::{ByteOrder, BE}; 12 | use crossbeam_channel::{unbounded, Sender, Receiver}; 13 | 14 | 15 | #[derive(Debug)] 16 | pub(crate) struct Request { 17 | pub hid: usize, 18 | pub tid: u16, 19 | pub fc: u8, 20 | pub addr: usize, 21 | pub count: usize, 22 | pub write: Option>, 23 | } 24 | 25 | #[derive(Debug)] 26 | pub(crate) enum Response { 27 | Ok(Request, Vec), 28 | Error(Request, u8), 29 | } 30 | 31 | enum HandlerEvent { 32 | Request(Request), 33 | New((usize, Sender)), 34 | Finished(usize), 35 | } 36 | 37 | struct Handler { 38 | hid: usize, 39 | client: TcpStream, 40 | requests: Sender, 41 | } 42 | 43 | pub struct Server { 44 | to_plc: Sender, 45 | from_plc: Receiver, 46 | } 47 | 48 | impl Handler { 49 | pub fn new(client: TcpStream, hid: usize, requests: Sender, 50 | replies: Receiver) -> Self 51 | { 52 | let send_client = client.try_clone().expect("could not clone socket"); 53 | thread::spawn(move || Handler::sender(send_client, replies)); 54 | Handler { client, hid, requests } 55 | } 56 | 57 | fn sender(mut client: TcpStream, replies: Receiver) { 58 | let mut buf = [0u8; 256]; 59 | mlzlog::set_thread_prefix(format!("{} sender: ", client.peer_addr().unwrap())); 60 | 61 | for response in replies { 62 | debug!("sending response: {:?}", response); 63 | let count = match response { 64 | Response::Ok(req, values) => { 65 | BE::write_u16(&mut buf, req.tid); 66 | buf[7] = req.fc; 67 | match req.fc { 68 | 3 | 4 => { 69 | let nbytes = 2 * values.len(); 70 | buf[8] = nbytes as u8; 71 | BE::write_u16_into(&values, &mut buf[9..9+nbytes]); 72 | 9 + nbytes 73 | } 74 | 6 => { 75 | BE::write_u16(&mut buf[8..], req.addr as u16); 76 | BE::write_u16(&mut buf[10..], values[0]); 77 | 12 78 | } 79 | 16 => { 80 | BE::write_u16(&mut buf[8..], req.addr as u16); 81 | BE::write_u16(&mut buf[10..], values.len() as u16); 82 | 12 83 | } 84 | x => panic!("impossible function code {}", x) 85 | } 86 | } 87 | Response::Error(req, ec) => { 88 | BE::write_u16(&mut buf, req.tid); 89 | buf[7] = req.fc | 0x80; 90 | buf[8] = ec; 91 | 9 92 | } 93 | }; 94 | BE::write_u16(&mut buf[4..], (count - 6) as u16); 95 | if let Err(err) = client.write_all(&buf[..count]) { 96 | warn!("write error: {}", err); 97 | break; 98 | } 99 | } 100 | } 101 | 102 | fn handle(mut self) { 103 | let mut headbuf = [0u8; 8]; 104 | let mut bodybuf = [0u8; 250]; // max frame size is 255 105 | let mut errbuf = [0, 0, 0, 0, 0, 9, 0, 0, 0]; 106 | 107 | mlzlog::set_thread_prefix(format!("{}: ", self.client.peer_addr().unwrap())); 108 | info!("connection accepted"); 109 | 110 | 'outer: loop { 111 | if let Err(err) = self.client.read_exact(&mut headbuf) { 112 | if err.kind() != ErrorKind::UnexpectedEof { 113 | warn!("error reading request head: {}", err); 114 | } 115 | break; 116 | } 117 | if &headbuf[2..4] != &[0, 0] { 118 | warn!("protocol ID mismatch: {:?}", headbuf); 119 | break; 120 | } 121 | let tid = BE::read_u16(&headbuf); 122 | let data_len = BE::read_u16(&headbuf[4..6]) as usize; 123 | if let Err(err) = self.client.read_exact(&mut bodybuf[..data_len - 2]) { 124 | warn!("error reading request body: {}", err); 125 | break; 126 | } 127 | if headbuf[6] != 0 { 128 | warn!("invalid slave {}", headbuf[6]); 129 | continue; 130 | } 131 | let fc = headbuf[7]; 132 | let req = match fc { 133 | 3 | 4 => { 134 | if data_len != 6 { 135 | warn!("invalid data length for fc {}", fc); 136 | continue; 137 | } 138 | let addr = BE::read_u16(&bodybuf[..2]) as usize; 139 | let count = BE::read_u16(&bodybuf[2..4]) as usize; 140 | Request { hid: self.hid, tid, fc, addr, count, write: None } 141 | } 142 | 6 => { 143 | if data_len != 6 { 144 | warn!("invalid data length for fc {}", fc); 145 | continue; 146 | } 147 | let addr = BE::read_u16(&bodybuf[..2]) as usize; 148 | let value = BE::read_u16(&bodybuf[2..4]); 149 | Request { hid: self.hid, tid, fc, addr, count: 1, write: Some(vec![value]) } 150 | } 151 | 16 => { 152 | if data_len < 7 { 153 | warn!("insufficient data length for fc {}", fc); 154 | continue; 155 | } 156 | let addr = BE::read_u16(&bodybuf[..2]) as usize; 157 | let bytecount = bodybuf[4] as usize; 158 | if data_len != 7 + bytecount { 159 | warn!("invalid data length for fc {}", fc); 160 | continue; 161 | } 162 | let mut values = vec![0; bytecount / 2]; 163 | BE::read_u16_into(&bodybuf[5..5+bytecount], &mut values); 164 | Request { hid: self.hid, tid, fc, addr, count: values.len(), write: Some(values) } 165 | } 166 | _ => { 167 | warn!("unknown function code {}", fc); 168 | BE::write_u16(&mut errbuf, tid); 169 | errbuf[7] = fc | 0x80; 170 | errbuf[8] = 1; 171 | if let Err(err) = self.client.write_all(&errbuf) { 172 | warn!("error writing error response: {}", err); 173 | break; 174 | } 175 | continue; 176 | } 177 | }; 178 | debug!("got request: {:?}", req); 179 | if let Err(e) = self.requests.send(HandlerEvent::Request(req)) { 180 | warn!("couldn't send request to server: {}", e); 181 | } 182 | } 183 | info!("connection closed"); 184 | if let Err(e) = self.requests.send(HandlerEvent::Finished(self.hid)) { 185 | warn!("couldn't send finish event to server: {}", e); 186 | } 187 | } 188 | } 189 | 190 | impl Server { 191 | pub(crate) fn new() -> (Self, Receiver, Sender) { 192 | let (w_to_plc, r_to_plc) = unbounded(); 193 | let (w_from_plc, r_from_plc) = unbounded(); 194 | (Server { to_plc: w_to_plc, from_plc: r_from_plc }, r_to_plc, w_from_plc) 195 | } 196 | 197 | /// Listen for connections on the TCP socket and spawn handlers for it. 198 | fn tcp_listener(tcp_sock: TcpListener, handler_sender: Sender) { 199 | mlzlog::set_thread_prefix("Modbus: "); 200 | 201 | info!("listening on {}", tcp_sock.local_addr().unwrap()); 202 | let mut handler_id = 0; 203 | 204 | while let Ok((stream, _)) = tcp_sock.accept() { 205 | let (w_rep, r_rep) = unbounded(); 206 | let w_req = handler_sender.clone(); 207 | handler_id += 1; 208 | if let Err(e) = w_req.send(HandlerEvent::New((handler_id, w_rep))) { 209 | warn!("couldn't send new handler event: {}", e); 210 | } else { 211 | thread::spawn(move || Handler::new(stream, handler_id, 212 | w_req, r_rep).handle()); 213 | } 214 | } 215 | } 216 | 217 | fn dispatcher(self, r_clients: Receiver) { 218 | mlzlog::set_thread_prefix("Dispatcher: "); 219 | 220 | let mut handlers = BTreeMap::new(); 221 | 222 | for event in r_clients { 223 | match event { 224 | HandlerEvent::New((id, chan)) => { 225 | handlers.insert(id, chan); 226 | } 227 | HandlerEvent::Finished(id) => { 228 | handlers.remove(&id); 229 | } 230 | HandlerEvent::Request(req) => { 231 | let hid = req.hid; 232 | if let Err(e) = self.to_plc.send(req) { 233 | warn!("couldn't send request to PLC: {}", e); 234 | } else { 235 | let resp = self.from_plc.recv().unwrap(); 236 | if let Err(e) = handlers[&hid].send(resp) { 237 | warn!("couldn't send reply to handler: {}", e); 238 | } 239 | } 240 | } 241 | } 242 | } 243 | } 244 | 245 | pub fn start(self, addr: &str) -> Result<()> { 246 | let (w_clients, r_clients) = unbounded(); 247 | let tcp_sock = TcpListener::bind(addr)?; 248 | 249 | thread::spawn(move || Server::tcp_listener(tcp_sock, w_clients)); 250 | thread::spawn(move || Server::dispatcher(self, r_clients)); 251 | 252 | Ok(()) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /ethercat-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethercat-sys" 3 | description = "Binding to the Etherlab open-source EtherCAT master" 4 | keywords = ["ethercat", "master", "etherlab", "binding"] 5 | version = "0.1.1" 6 | authors = ["Georg Brandl "] 7 | license = "MIT/Apache-2.0" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | ioctl-sys = "0.5.2" 12 | 13 | [build-dependencies] 14 | bindgen = "0.49.0" 15 | -------------------------------------------------------------------------------- /ethercat-sys/build.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | use std::env; 5 | use std::path::PathBuf; 6 | 7 | fn main() { 8 | let path = env::var("ETHERCAT_PATH") 9 | .expect("Please set the ETHERCAT_PATH env var to the location of \ 10 | a checkout of the Ethercat master after running configure"); 11 | 12 | let bindings = bindgen::Builder::default() 13 | .header(format!("{}/lib/ioctl.h", path)) 14 | .clang_arg(format!("-I{}", path)) 15 | .derive_default(true) 16 | .derive_debug(false) 17 | .prepend_enum_name(false) 18 | .ignore_functions() 19 | .whitelist_type("ec_ioctl_.*") 20 | .whitelist_type("ec_master_state_t") 21 | .whitelist_var("EC_IOCTL_.*") 22 | .whitelist_var("EC_MAX_.*") 23 | .layout_tests(false) 24 | .generate() 25 | .expect("Unable to generate bindings"); 26 | 27 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 28 | bindings 29 | .write_to_file(out_path.join("bindings.rs")) 30 | .expect("Couldn't write bindings!"); 31 | } 32 | -------------------------------------------------------------------------------- /ethercat-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | #![allow(non_upper_case_globals)] 5 | #![allow(non_camel_case_types)] 6 | #![allow(non_snake_case)] 7 | 8 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 9 | 10 | use ioctl_sys::{ioctl, io, ioc, ior, iow, iorw}; 11 | 12 | pub mod ioctl { 13 | use super::*; 14 | use super::EC_IOCTL_TYPE as EC; 15 | 16 | ioctl!(read MODULE with EC, 0x00; ec_ioctl_module_t); 17 | ioctl!(read MASTER with EC, 0x01; ec_ioctl_master_t); 18 | ioctl!(readwrite SLAVE with EC, 0x02; ec_ioctl_slave_t); 19 | ioctl!(readwrite SLAVE_SYNC with EC, 0x03; ec_ioctl_slave_sync_t); 20 | ioctl!(readwrite SLAVE_SYNC_PDO with EC, 0x04; ec_ioctl_slave_sync_pdo_t); 21 | ioctl!(readwrite SLAVE_SYNC_PDO_ENTRY with EC, 0x05; ec_ioctl_slave_sync_pdo_entry_t); 22 | ioctl!(readwrite DOMAIN with EC, 0x06; ec_ioctl_domain_t); 23 | ioctl!(readwrite DOMAIN_FMMU with EC, 0x07; ec_ioctl_domain_fmmu_t); 24 | ioctl!(readwrite DOMAIN_DATA with EC, 0x08; ec_ioctl_domain_data_t); 25 | ioctl!(none MASTER_DEBUG with EC, 0x09); 26 | ioctl!(none MASTER_RESCAN with EC, 0x0a); 27 | ioctl!(write SLAVE_STATE with EC, 0x0b; ec_ioctl_slave_state_t); 28 | ioctl!(readwrite SLAVE_SDO with EC, 0x0c; ec_ioctl_slave_sdo_t); 29 | ioctl!(readwrite SLAVE_SDO_ENTRY with EC, 0x0d; ec_ioctl_slave_sdo_entry_t); 30 | ioctl!(readwrite SLAVE_SDO_UPLOAD with EC, 0x0e; ec_ioctl_slave_sdo_upload_t); 31 | ioctl!(readwrite SLAVE_SDO_DOWNLOAD with EC, 0x0f; ec_ioctl_slave_sdo_download_t); 32 | ioctl!(readwrite SLAVE_SII_READ with EC, 0x10; ec_ioctl_slave_sii_t); 33 | ioctl!(write SLAVE_SII_WRITE with EC, 0x11; ec_ioctl_slave_sii_t); 34 | ioctl!(readwrite SLAVE_REG_READ with EC, 0x12; ec_ioctl_slave_reg_t); 35 | ioctl!(write SLAVE_REG_WRITE with EC, 0x13; ec_ioctl_slave_reg_t); 36 | ioctl!(readwrite SLAVE_FOE_READ with EC, 0x14; ec_ioctl_slave_foe_t); 37 | ioctl!(write SLAVE_FOE_WRITE with EC, 0x15; ec_ioctl_slave_foe_t); 38 | ioctl!(readwrite SLAVE_SOE_READ with EC, 0x16; ec_ioctl_slave_soe_read_t); 39 | ioctl!(readwrite SLAVE_SOE_WRITE with EC, 0x17; ec_ioctl_slave_soe_write_t); 40 | ioctl!(write SLAVE_EOE_IP_PARAM with EC, 0x18; ec_ioctl_slave_eoe_ip_t); 41 | ioctl!(readwrite CONFIG with EC, 0x19; ec_ioctl_config_t); 42 | ioctl!(readwrite CONFIG_PDO with EC, 0x1a; ec_ioctl_config_pdo_t); 43 | ioctl!(readwrite CONFIG_PDO_ENTRY with EC, 0x1b; ec_ioctl_config_pdo_entry_t); 44 | ioctl!(readwrite CONFIG_SDO with EC, 0x1c; ec_ioctl_config_sdo_t); 45 | ioctl!(readwrite CONFIG_IDN with EC, 0x1d; ec_ioctl_config_idn_t); 46 | ioctl!(readwrite EOE_HANDLER with EC, 0x1e; ec_ioctl_eoe_handler_t); 47 | ioctl!(none REQUEST with EC, 0x1f); 48 | ioctl!(none CREATE_DOMAIN with EC, 0x20); 49 | ioctl!(readwrite CREATE_SLAVE_CONFIG with EC, 0x21; ec_ioctl_config_t); 50 | ioctl!(write SELECT_REF_CLOCK with EC, 0x22; u32); 51 | ioctl!(read ACTIVATE with EC, 0x23; ec_ioctl_master_activate_t); 52 | ioctl!(none DEACTIVATE with EC, 0x24); 53 | ioctl!(arg SEND with EC, 0x25); 54 | ioctl!(none RECEIVE with EC, 0x26); 55 | ioctl!(read MASTER_STATE with EC, 0x27; ec_master_state_t); 56 | ioctl!(readwrite MASTER_LINK_STATE with EC, 0x28; ec_ioctl_link_state_t); 57 | ioctl!(write APP_TIME with EC, 0x29; ec_ioctl_app_time_t); 58 | ioctl!(none SYNC_REF with EC, 0x2a); 59 | ioctl!(none SYNC_SLAVES with EC, 0x2b); 60 | ioctl!(read REF_CLOCK_TIME with EC, 0x2c; u32); 61 | ioctl!(none SYNC_MON_QUEUE with EC, 0x2d); 62 | ioctl!(read SYNC_MON_PROCESS with EC, 0x2e; u32); 63 | ioctl!(none RESET with EC, 0x2f); 64 | ioctl!(write SC_SYNC with EC, 0x30; ec_ioctl_config_t); 65 | ioctl!(write SC_WATCHDOG with EC, 0x31; ec_ioctl_config_t); 66 | ioctl!(write SC_ADD_PDO with EC, 0x32; ec_ioctl_config_pdo_t); 67 | ioctl!(write SC_CLEAR_PDOS with EC, 0x33; ec_ioctl_config_pdo_t); 68 | ioctl!(write SC_ADD_ENTRY with EC, 0x34; ec_ioctl_add_pdo_entry_t); 69 | ioctl!(write SC_CLEAR_ENTRIES with EC, 0x35; ec_ioctl_config_pdo_t); 70 | ioctl!(readwrite SC_REG_PDO_ENTRY with EC, 0x36; ec_ioctl_reg_pdo_entry_t); 71 | ioctl!(readwrite SC_REG_PDO_POS with EC, 0x37; ec_ioctl_reg_pdo_pos_t); 72 | ioctl!(write SC_DC with EC, 0x38; ec_ioctl_config_t); 73 | ioctl!(write SC_SDO with EC, 0x39; ec_ioctl_sc_sdo_t); 74 | ioctl!(write SC_EMERG_SIZE with EC, 0x3a; ec_ioctl_sc_emerg_t); 75 | ioctl!(readwrite SC_EMERG_POP with EC, 0x3b; ec_ioctl_sc_emerg_t); 76 | ioctl!(write SC_EMERG_CLEAR with EC, 0x3c; ec_ioctl_sc_emerg_t); 77 | ioctl!(readwrite SC_EMERG_OVERRUNS with EC, 0x3d; ec_ioctl_sc_emerg_t); 78 | ioctl!(readwrite SC_SDO_REQUEST with EC, 0x3e; ec_ioctl_sdo_request_t); 79 | ioctl!(readwrite SC_REG_REQUEST with EC, 0x3f; ec_ioctl_reg_request_t); 80 | ioctl!(readwrite SC_VOE with EC, 0x40; ec_ioctl_voe_t); 81 | ioctl!(readwrite SC_STATE with EC, 0x41; ec_ioctl_sc_state_t); 82 | ioctl!(write SC_IDN with EC, 0x42; ec_ioctl_sc_idn_t); 83 | ioctl!(arg DOMAIN_SIZE with EC, 0x43); 84 | ioctl!(arg DOMAIN_OFFSET with EC, 0x44); 85 | ioctl!(arg DOMAIN_PROCESS with EC, 0x45); 86 | ioctl!(arg DOMAIN_QUEUE with EC, 0x46); 87 | ioctl!(readwrite DOMAIN_STATE with EC, 0x47; ec_ioctl_domain_state_t); 88 | ioctl!(readwrite SDO_REQUEST_INDEX with EC, 0x48; ec_ioctl_sdo_request_t); 89 | ioctl!(readwrite SDO_REQUEST_TIMEOUT with EC, 0x49; ec_ioctl_sdo_request_t); 90 | ioctl!(readwrite SDO_REQUEST_STATE with EC, 0x4a; ec_ioctl_sdo_request_t); 91 | ioctl!(readwrite SDO_REQUEST_READ with EC, 0x4b; ec_ioctl_sdo_request_t); 92 | ioctl!(readwrite SDO_REQUEST_WRITE with EC, 0x4c; ec_ioctl_sdo_request_t); 93 | ioctl!(readwrite SDO_REQUEST_DATA with EC, 0x4d; ec_ioctl_sdo_request_t); 94 | ioctl!(readwrite REG_REQUEST_DATA with EC, 0x4e; ec_ioctl_reg_request_t); 95 | ioctl!(readwrite REG_REQUEST_STATE with EC, 0x4f; ec_ioctl_reg_request_t); 96 | ioctl!(readwrite REG_REQUEST_WRITE with EC, 0x50; ec_ioctl_reg_request_t); 97 | ioctl!(readwrite REG_REQUEST_READ with EC, 0x51; ec_ioctl_reg_request_t); 98 | ioctl!(write VOE_SEND_HEADER with EC, 0x52; ec_ioctl_voe_t); 99 | ioctl!(readwrite VOE_REC_HEADER with EC, 0x53; ec_ioctl_voe_t); 100 | ioctl!(write VOE_READ with EC, 0x54; ec_ioctl_voe_t); 101 | ioctl!(write VOE_READ_NOSYNC with EC, 0x55; ec_ioctl_voe_t); 102 | ioctl!(readwrite VOE_WRITE with EC, 0x56; ec_ioctl_voe_t); 103 | ioctl!(readwrite VOE_EXEC with EC, 0x57; ec_ioctl_voe_t); 104 | ioctl!(readwrite VOE_DATA with EC, 0x58; ec_ioctl_voe_t); 105 | ioctl!(write SET_SEND_INTERVAL with EC, 0x59; usize); 106 | ioctl!(write SC_OVERLAPPING_IO with EC, 0x5a; ec_ioctl_config_t); 107 | } 108 | -------------------------------------------------------------------------------- /ethercat/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethercat" 3 | description = "Binding to the Etherlab open-source EtherCAT master" 4 | keywords = ["ethercat", "master", "etherlab"] 5 | version = "0.1.1" 6 | authors = ["Georg Brandl "] 7 | license = "MIT/Apache-2.0" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | libc = "0.2.49" 12 | memmap = "0.7.0" 13 | derive-new = "0.5.6" 14 | ethercat-sys = { path = "../ethercat-sys", version = "0.1.0" } 15 | -------------------------------------------------------------------------------- /ethercat/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | use ethercat_sys as ec; 5 | 6 | mod master; 7 | mod types; 8 | 9 | pub use self::types::*; 10 | pub use self::master::{Master, Domain, SlaveConfig}; 11 | -------------------------------------------------------------------------------- /ethercat/src/master.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | use std::fs::{File, OpenOptions}; 5 | use std::ffi::CStr; 6 | use std::io::{Error, ErrorKind}; 7 | use std::os::unix::io::AsRawFd; 8 | use std::os::raw::c_ulong; 9 | use crate::ec; 10 | use crate::Result; 11 | use crate::types::*; 12 | 13 | macro_rules! ioctl { 14 | ($m:expr, $f:expr) => { ioctl!($m, $f,) }; 15 | ($m:expr, $f:expr, $($arg:tt)*) => {{ 16 | let res = unsafe { $f($m.file.as_raw_fd(), $($arg)*) }; 17 | if res < 0 { Err(Error::last_os_error()) } else { Ok(res) } 18 | }} 19 | } 20 | 21 | /// An EtherCAT master. 22 | pub struct Master { 23 | file: File, 24 | map: Option, 25 | domains: Vec<(DomainIndex, usize, usize)>, 26 | } 27 | 28 | pub struct Domain<'m> { 29 | master: &'m Master, 30 | index: DomainIndex, 31 | } 32 | 33 | impl Master { 34 | pub fn reserve(index: MasterIndex) -> Result { 35 | let devpath = format!("/dev/EtherCAT{}", index); 36 | let file = OpenOptions::new().read(true).write(true).open(&devpath)?; 37 | let mut module_info = ec::ec_ioctl_module_t { 38 | ioctl_version_magic: 0, 39 | master_count: 0, 40 | }; 41 | let master = Master { file, map: None, domains: vec![] }; 42 | ioctl!(master, ec::ioctl::MODULE, &mut module_info)?; 43 | if module_info.ioctl_version_magic != ec::EC_IOCTL_VERSION_MAGIC { 44 | Err(Error::new(ErrorKind::Other, 45 | format!("module version mismatch: expected {}, found {}", 46 | ec::EC_IOCTL_VERSION_MAGIC, 47 | module_info.ioctl_version_magic))) 48 | } else { 49 | ioctl!(master, ec::ioctl::REQUEST)?; 50 | Ok(master) 51 | } 52 | } 53 | 54 | pub fn create_domain(&mut self) -> Result { 55 | let index = ioctl!(self, ec::ioctl::CREATE_DOMAIN).map(|v| v as DomainIndex)?; 56 | self.domains.push((index, 0, 0)); 57 | Ok(DomainHandle(self.domains.len() - 1)) 58 | } 59 | 60 | pub fn domain(&self, index: DomainHandle) -> Domain { 61 | Domain { master: self, index: self.domains[index.0].0 } 62 | } 63 | 64 | pub fn domain_data(&mut self, index: DomainHandle) -> &mut [u8] { 65 | let (ix, mut offset, mut size) = self.domains[index.0]; 66 | if size == 0 { 67 | size = ioctl!(self, ec::ioctl::DOMAIN_SIZE, ix as c_ulong).unwrap() as usize; 68 | offset = ioctl!(self, ec::ioctl::DOMAIN_OFFSET, ix as c_ulong).unwrap() as usize; 69 | self.domains[index.0] = (ix, offset, size); 70 | } 71 | &mut self.map.as_mut().expect("master is not activated")[offset..offset+size] 72 | } 73 | 74 | pub fn activate(&mut self) -> Result<()> { 75 | let mut data = ec::ec_ioctl_master_activate_t::default(); 76 | ioctl!(self, ec::ioctl::ACTIVATE, &mut data)?; 77 | 78 | self.map = unsafe { 79 | memmap::MmapOptions::new() 80 | .len(data.process_data_size) 81 | .map_mut(&self.file) 82 | .map(Some)? 83 | }; 84 | self.map.as_mut().unwrap()[0] = 0; 85 | Ok(()) 86 | } 87 | 88 | pub fn deactivate(&mut self) -> Result<()> { 89 | ioctl!(self, ec::ioctl::DEACTIVATE)?; 90 | self.domains.clear(); 91 | self.map = None; 92 | Ok(()) 93 | } 94 | 95 | pub fn set_send_interval(&mut self, mut interval_us: usize) -> Result<()> { 96 | ioctl!(self, ec::ioctl::SET_SEND_INTERVAL, &mut interval_us).map(|_| ()) 97 | } 98 | 99 | pub fn send(&mut self) -> Result { 100 | let mut sent = 0; 101 | ioctl!(self, ec::ioctl::SEND, &mut sent as *mut _ as c_ulong)?; 102 | Ok(sent) 103 | } 104 | 105 | pub fn receive(&mut self) -> Result<()> { 106 | ioctl!(self, ec::ioctl::RECEIVE).map(|_| ()) 107 | } 108 | 109 | pub fn reset(&mut self) -> Result<()> { 110 | ioctl!(self, ec::ioctl::RESET).map(|_| ()) 111 | } 112 | 113 | pub fn state(&self) -> Result { 114 | let mut data = ec::ec_master_state_t::default(); 115 | ioctl!(self, ec::ioctl::MASTER_STATE, &mut data)?; 116 | Ok(MasterState { 117 | slaves_responding: data.slaves_responding, 118 | al_states: data.al_states() as u8, 119 | link_up: data.link_up() != 0, 120 | }) 121 | } 122 | 123 | pub fn link_state(&self, dev_idx: u32) -> Result { 124 | let mut state = ec::ec_master_link_state_t::default(); 125 | let mut data = ec::ec_ioctl_link_state_t { 126 | dev_idx, 127 | state: &mut state, 128 | }; 129 | ioctl!(self, ec::ioctl::MASTER_LINK_STATE, &mut data)?; 130 | Ok(MasterState { 131 | slaves_responding: state.slaves_responding, 132 | al_states: state.al_states() as u8, 133 | link_up: state.link_up() != 0, 134 | }) 135 | } 136 | 137 | pub fn get_info(&self) -> Result { 138 | let mut data = ec::ec_ioctl_master_t::default(); 139 | ioctl!(self, ec::ioctl::MASTER, &mut data)?; 140 | Ok(MasterInfo { 141 | slave_count: data.slave_count, 142 | link_up: data.devices[0].link_state != 0, 143 | scan_busy: data.scan_busy != 0, 144 | app_time: data.app_time, 145 | }) 146 | } 147 | 148 | pub fn get_slave_info(&self, position: SlavePosition) -> Result { 149 | let mut data = ec::ec_ioctl_slave_t::default(); 150 | data.position = position; 151 | ioctl!(self, ec::ioctl::SLAVE, &mut data)?; 152 | let mut ports = [SlavePortInfo::default(); ec::EC_MAX_PORTS as usize]; 153 | for i in 0..ec::EC_MAX_PORTS as usize { 154 | ports[i].desc = match data.ports[i].desc { 155 | ec::EC_PORT_NOT_IMPLEMENTED => SlavePortType::NotImplemented, 156 | ec::EC_PORT_NOT_CONFIGURED => SlavePortType::NotConfigured, 157 | ec::EC_PORT_EBUS => SlavePortType::EBus, 158 | ec::EC_PORT_MII => SlavePortType::MII, 159 | x => panic!("invalid port type {}", x), 160 | }; 161 | ports[i].link = SlavePortLink { 162 | link_up: data.ports[i].link.link_up != 0, 163 | loop_closed: data.ports[i].link.loop_closed != 0, 164 | signal_detected: data.ports[i].link.signal_detected != 0, 165 | }; 166 | ports[i].receive_time = data.ports[i].receive_time; 167 | ports[i].next_slave = data.ports[i].next_slave; 168 | ports[i].delay_to_next_dc = data.ports[i].delay_to_next_dc; 169 | } 170 | Ok(SlaveInfo { 171 | name: unsafe { CStr::from_ptr(data.name.as_ptr()).to_string_lossy().into_owned() }, 172 | ring_pos: data.position, 173 | id: SlaveId { vendor_id: data.vendor_id, product_code: data.product_code }, 174 | rev: SlaveRev { revision_number: data.revision_number, 175 | serial_number: data.serial_number }, 176 | alias: data.alias, 177 | current_on_ebus: data.current_on_ebus, 178 | al_state: AlState::from(data.al_state as u32), 179 | error_flag: data.error_flag, 180 | sync_count: data.sync_count, 181 | sdo_count: data.sdo_count, 182 | ports 183 | }) 184 | } 185 | 186 | pub fn get_config_info(&self, index: SlaveConfigIndex) -> Result { 187 | let mut data = ec::ec_ioctl_config_t::default(); 188 | data.config_index = index; 189 | ioctl!(self, ec::ioctl::CONFIG, &mut data)?; 190 | Ok(ConfigInfo { 191 | alias: data.alias, 192 | position: data.position, 193 | id: SlaveId { vendor_id: data.vendor_id, product_code: data.product_code }, 194 | slave_position: if data.slave_position == -1 { None } else { 195 | Some(data.slave_position as u32) }, 196 | sdo_count: data.sdo_count, 197 | idn_count: data.idn_count, 198 | }) 199 | } 200 | 201 | pub fn configure_slave(&mut self, addr: SlaveAddr, 202 | expected: SlaveId) -> Result { 203 | let mut data = ec::ec_ioctl_config_t::default(); 204 | let (alias, pos) = addr.as_pair(); 205 | data.alias = alias; 206 | data.position = pos; 207 | data.vendor_id = expected.vendor_id; 208 | data.product_code = expected.product_code; 209 | ioctl!(self, ec::ioctl::CREATE_SLAVE_CONFIG, &mut data)?; 210 | Ok(SlaveConfig { master: self, index: data.config_index }) 211 | } 212 | 213 | pub fn sdo_download(&mut self, position: SlavePosition, sdo_index: SdoIndex, 214 | data: &T) -> Result<()> 215 | where T: SdoData + ?Sized 216 | { 217 | let mut data = ec::ec_ioctl_slave_sdo_download_t { 218 | slave_position: position, 219 | sdo_index: sdo_index.index, 220 | sdo_entry_subindex: sdo_index.subindex, 221 | complete_access: 0, 222 | data_size: data.data_size(), 223 | data: data.data_ptr(), 224 | abort_code: 0, 225 | }; 226 | ioctl!(self, ec::ioctl::SLAVE_SDO_DOWNLOAD, &mut data).map(|_| ()) 227 | } 228 | 229 | pub fn sdo_download_complete(&mut self, position: SlavePosition, sdo_index: SdoIndex, 230 | data: &[u8]) -> Result<()> { 231 | let mut data = ec::ec_ioctl_slave_sdo_download_t { 232 | slave_position: position, 233 | sdo_index: sdo_index.index, 234 | sdo_entry_subindex: sdo_index.subindex, 235 | complete_access: 1, 236 | data_size: data.len(), 237 | data: data.as_ptr(), 238 | abort_code: 0, 239 | }; 240 | ioctl!(self, ec::ioctl::SLAVE_SDO_DOWNLOAD, &mut data).map(|_| ()) 241 | } 242 | 243 | pub fn sdo_upload<'t>(&self, position: SlavePosition, sdo_index: SdoIndex, 244 | target: &'t mut [u8]) -> Result<&'t mut [u8]> { 245 | let mut data = ec::ec_ioctl_slave_sdo_upload_t { 246 | slave_position: position, 247 | sdo_index: sdo_index.index, 248 | sdo_entry_subindex: sdo_index.subindex, 249 | target_size: target.len(), 250 | target: target.as_mut_ptr(), 251 | data_size: 0, 252 | abort_code: 0, 253 | }; 254 | ioctl!(self, ec::ioctl::SLAVE_SDO_UPLOAD, &mut data)?; 255 | Ok(&mut target[..data.data_size]) 256 | } 257 | 258 | // XXX missing: get_sync_manager, get_pdo, get_pdo_entry, write_idn, read_idn, 259 | // application_time, sync_reference_clock, sync_slave_clocks, 260 | // reference_clock_time, sync_monitor_queue, sync_monitor_process 261 | } 262 | 263 | pub struct SlaveConfig<'m> { 264 | master: &'m Master, 265 | index: SlaveConfigIndex, 266 | } 267 | 268 | impl<'m> SlaveConfig<'m> { 269 | pub fn index(&self) -> SlaveConfigIndex { 270 | self.index 271 | } 272 | 273 | pub fn state(&self) -> Result { 274 | let mut state = ec::ec_slave_config_state_t::default(); 275 | let mut data = ec::ec_ioctl_sc_state_t { config_index: self.index, state: &mut state }; 276 | ioctl!(self.master, ec::ioctl::SC_STATE, &mut data)?; 277 | Ok(SlaveConfigState { 278 | online: state.online() != 0, 279 | operational: state.operational() != 0, 280 | al_state: AlState::from(state.al_state()), 281 | }) 282 | } 283 | 284 | pub fn config_pdos(&mut self, info: &[SyncInfo]) -> Result<()> { 285 | for sm_info in info { 286 | self.config_sync_manager(sm_info)?; 287 | 288 | self.clear_pdo_assignments(sm_info.index)?; 289 | for pdo_info in sm_info.pdos { 290 | self.add_pdo_assignment(sm_info.index, pdo_info)?; 291 | 292 | if !pdo_info.entries.is_empty() { 293 | self.clear_pdo_mapping(pdo_info.index)?; 294 | for entry in pdo_info.entries { 295 | self.add_pdo_mapping(pdo_info.index, entry)?; 296 | } 297 | } 298 | } 299 | } 300 | Ok(()) 301 | } 302 | 303 | pub fn config_watchdog(&mut self, divider: u16, intervals: u16) -> Result<()> { 304 | let mut data = ec::ec_ioctl_config_t::default(); 305 | data.config_index = self.index; 306 | data.watchdog_divider = divider; 307 | data.watchdog_intervals = intervals; 308 | ioctl!(self.master, ec::ioctl::SC_WATCHDOG, &mut data).map(|_| ()) 309 | } 310 | 311 | pub fn config_overlapping_pdos(&mut self, allow: bool) -> Result<()> { 312 | let mut data = ec::ec_ioctl_config_t::default(); 313 | data.config_index = self.index; 314 | data.allow_overlapping_pdos = allow as u8; 315 | ioctl!(self.master, ec::ioctl::SC_OVERLAPPING_IO, &mut data).map(|_| ()) 316 | } 317 | 318 | pub fn config_sync_manager(&mut self, info: &SyncInfo) -> Result<()> { 319 | if info.index >= ec::EC_MAX_SYNC_MANAGERS as u8 { 320 | return Err(Error::new(ErrorKind::Other, "sync manager index too large")); 321 | } 322 | 323 | let mut data = ec::ec_ioctl_config_t::default(); 324 | data.config_index = self.index; 325 | let ix = info.index as usize; 326 | data.syncs[ix].dir = info.direction as u32; 327 | data.syncs[ix].watchdog_mode = info.watchdog_mode as u32; 328 | data.syncs[ix].config_this = 1; 329 | ioctl!(self.master, ec::ioctl::SC_SYNC, &mut data).map(|_| ()) 330 | } 331 | 332 | pub fn clear_pdo_assignments(&mut self, sync_index: SmIndex) -> Result<()> { 333 | let mut data = ec::ec_ioctl_config_pdo_t::default(); 334 | data.config_index = self.index; 335 | data.sync_index = sync_index; 336 | ioctl!(self.master, ec::ioctl::SC_CLEAR_PDOS, &mut data).map(|_| ()) 337 | } 338 | 339 | pub fn add_pdo_assignment(&mut self, sync_index: SmIndex, pdo: &PdoInfo) -> Result<()> { 340 | let mut data = ec::ec_ioctl_config_pdo_t::default(); 341 | data.config_index = self.index; 342 | data.sync_index = sync_index; 343 | data.index = pdo.index; 344 | ioctl!(self.master, ec::ioctl::SC_ADD_PDO, &mut data).map(|_| ()) 345 | } 346 | 347 | pub fn clear_pdo_mapping(&mut self, pdo_index: PdoIndex) -> Result<()> { 348 | let mut data = ec::ec_ioctl_config_pdo_t::default(); 349 | data.config_index = self.index; 350 | data.index = pdo_index; 351 | ioctl!(self.master, ec::ioctl::SC_CLEAR_ENTRIES, &mut data).map(|_| ()) 352 | } 353 | 354 | pub fn add_pdo_mapping(&mut self, pdo_index: PdoIndex, entry: &PdoEntryInfo) -> Result<()> { 355 | let mut data = ec::ec_ioctl_add_pdo_entry_t { 356 | config_index: self.index, 357 | pdo_index, 358 | entry_index: entry.index.index, 359 | entry_subindex: entry.index.subindex, 360 | entry_bit_length: entry.bit_length, 361 | }; 362 | ioctl!(self.master, ec::ioctl::SC_ADD_ENTRY, &mut data).map(|_| ()) 363 | } 364 | 365 | pub fn register_pdo_entry(&mut self, index: PdoEntryIndex, domain: DomainHandle) -> Result { 366 | let mut data = ec::ec_ioctl_reg_pdo_entry_t { 367 | config_index: self.index, 368 | entry_index: index.index, 369 | entry_subindex: index.subindex, 370 | domain_index: self.master.domains[domain.0].0, 371 | bit_position: 0, 372 | }; 373 | let byte = ioctl!(self.master, ec::ioctl::SC_REG_PDO_ENTRY, &mut data)?; 374 | Ok(Offset { byte: byte as usize, bit: data.bit_position }) 375 | } 376 | 377 | pub fn register_pdo_entry_by_position(&mut self, sync_index: SmIndex, pdo_pos: u32, entry_pos: u32, 378 | domain: DomainHandle) -> Result { 379 | let mut data = ec::ec_ioctl_reg_pdo_pos_t { 380 | config_index: self.index, 381 | sync_index: sync_index as u32, 382 | pdo_pos, 383 | entry_pos, 384 | domain_index: self.master.domains[domain.0].0, 385 | bit_position: 0, 386 | }; 387 | let byte = ioctl!(self.master, ec::ioctl::SC_REG_PDO_POS, &mut data)?; 388 | Ok(Offset { byte: byte as usize, bit: data.bit_position }) 389 | } 390 | 391 | pub fn config_dc(&mut self, assign_activate: u16, sync0_cycle_time: u32, sync0_shift_time: i32, 392 | sync1_cycle_time: u32, sync1_shift_time: i32) -> Result<()> { 393 | let mut data = ec::ec_ioctl_config_t::default(); 394 | data.config_index = self.index; 395 | data.dc_assign_activate = assign_activate; 396 | data.dc_sync[0].cycle_time = sync0_cycle_time; 397 | data.dc_sync[0].shift_time = sync0_shift_time; 398 | data.dc_sync[1].cycle_time = sync1_cycle_time; 399 | data.dc_sync[1].shift_time = sync1_shift_time; 400 | ioctl!(self.master, ec::ioctl::SC_DC, &mut data).map(|_| ()) 401 | } 402 | 403 | pub fn add_sdo(&mut self, index: SdoIndex, data: &T) -> Result<()> 404 | where T: SdoData + ?Sized 405 | { 406 | let mut data = ec::ec_ioctl_sc_sdo_t { 407 | config_index: self.index, 408 | index: index.index, 409 | subindex: index.subindex, 410 | data: data.data_ptr(), 411 | size: data.data_size(), 412 | complete_access: 0, 413 | }; 414 | ioctl!(self.master, ec::ioctl::SC_SDO, &mut data).map(|_| ()) 415 | } 416 | 417 | pub fn add_complete_sdo(&mut self, index: SdoIndex, data: &[u8]) -> Result<()> { 418 | let mut data = ec::ec_ioctl_sc_sdo_t { 419 | config_index: self.index, 420 | index: index.index, 421 | subindex: index.subindex, 422 | data: data.as_ptr(), 423 | size: data.len(), 424 | complete_access: 1, 425 | }; 426 | ioctl!(self.master, ec::ioctl::SC_SDO, &mut data).map(|_| ()) 427 | } 428 | 429 | pub fn config_idn(&mut self, drive_no: u8, idn: u16, al_state: AlState, data: &[u8]) -> Result<()> { 430 | let mut data = ec::ec_ioctl_sc_idn_t { 431 | config_index: self.index, 432 | drive_no, 433 | idn, 434 | al_state: al_state as u32, 435 | data: data.as_ptr(), 436 | size: data.len(), 437 | }; 438 | ioctl!(self.master, ec::ioctl::SC_IDN, &mut data).map(|_| ()) 439 | } 440 | 441 | pub fn set_emerg_size(&mut self, elements: usize) -> Result<()> { 442 | let mut data = ec::ec_ioctl_sc_emerg_t::default(); 443 | data.config_index = self.index; 444 | data.size = elements; 445 | ioctl!(self.master, ec::ioctl::SC_EMERG_SIZE, &mut data).map(|_| ()) 446 | } 447 | 448 | pub fn pop_emerg(&mut self, target: &mut [u8]) -> Result<()> { 449 | let mut data = ec::ec_ioctl_sc_emerg_t::default(); 450 | data.config_index = self.index; 451 | data.target = target.as_mut_ptr(); 452 | ioctl!(self.master, ec::ioctl::SC_EMERG_POP, &mut data).map(|_| ()) 453 | } 454 | 455 | pub fn clear_emerg(&mut self) -> Result<()> { 456 | let mut data = ec::ec_ioctl_sc_emerg_t::default(); 457 | data.config_index = self.index; 458 | ioctl!(self.master, ec::ioctl::SC_EMERG_CLEAR, &mut data).map(|_| ()) 459 | } 460 | 461 | pub fn emerg_overruns(&mut self) -> Result { 462 | let mut data = ec::ec_ioctl_sc_emerg_t::default(); 463 | data.config_index = self.index; 464 | ioctl!(self.master, ec::ioctl::SC_EMERG_OVERRUNS, &mut data)?; 465 | Ok(data.overruns) 466 | } 467 | 468 | // XXX missing: create_sdo_request, create_reg_request, create_voe_handler 469 | } 470 | 471 | impl<'m> Domain<'m> { 472 | pub fn size(&self) -> Result { 473 | ioctl!(self.master, ec::ioctl::DOMAIN_SIZE, self.index as c_ulong).map(|v| v as usize) 474 | } 475 | 476 | pub fn state(&self) -> Result { 477 | let mut state = ec::ec_domain_state_t::default(); 478 | let mut data = ec::ec_ioctl_domain_state_t { 479 | domain_index: self.index, 480 | state: &mut state, 481 | }; 482 | ioctl!(self.master, ec::ioctl::DOMAIN_STATE, &mut data)?; 483 | Ok(DomainState { 484 | working_counter: state.working_counter, 485 | redundancy_active: state.redundancy_active != 0, 486 | wc_state: WcState::from(state.wc_state), 487 | }) 488 | } 489 | 490 | pub fn process(&mut self) -> Result<()> { 491 | ioctl!(self.master, ec::ioctl::DOMAIN_PROCESS, self.index as c_ulong).map(|_| ()) 492 | } 493 | 494 | pub fn queue(&mut self) -> Result<()> { 495 | ioctl!(self.master, ec::ioctl::DOMAIN_QUEUE, self.index as c_ulong).map(|_| ()) 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /ethercat/src/types.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | use std::io; 5 | use derive_new::new; 6 | use crate::ec; 7 | 8 | pub type Error = io::Error; 9 | pub type Result = io::Result; 10 | 11 | pub type MasterIndex = u32; 12 | pub type DomainIndex = u32; 13 | pub type SlaveConfigIndex = u32; 14 | pub type SlavePosition = u16; 15 | 16 | #[derive(Debug, Clone, Copy)] 17 | pub struct DomainHandle(pub(crate) usize); 18 | 19 | 20 | /// An EtherCAT slave identification, consisting of vendor ID and product code. 21 | #[derive(Debug, Clone, Copy, new)] 22 | pub struct SlaveId { 23 | pub vendor_id: u32, 24 | pub product_code: u32, 25 | } 26 | 27 | 28 | /// An EtherCAT slave revision identification. 29 | #[derive(Debug, Clone, Copy, new)] 30 | pub struct SlaveRev { 31 | pub revision_number: u32, 32 | pub serial_number: u32, 33 | } 34 | 35 | 36 | /// An EtherCAT slave, which is specified either by absolute position in the 37 | /// ring or by offset from a given alias. 38 | #[derive(Clone, Copy)] 39 | pub enum SlaveAddr { 40 | ByPos(u16), 41 | ByAlias(u16, u16) 42 | } 43 | 44 | impl SlaveAddr { 45 | pub(crate) fn as_pair(&self) -> (u16, u16) { 46 | match *self { 47 | SlaveAddr::ByPos(x) => (0, x), 48 | SlaveAddr::ByAlias(x, y) => (x, y), 49 | } 50 | } 51 | } 52 | 53 | /// Offset of a PDO entry in the domain image. 54 | #[derive(Debug, Default, PartialEq, Eq, new)] 55 | pub struct Offset { 56 | pub byte: usize, 57 | pub bit: u32, 58 | } 59 | 60 | #[derive(Debug)] 61 | pub struct MasterInfo { 62 | pub slave_count: u32, 63 | pub link_up: bool, 64 | pub scan_busy: bool, 65 | pub app_time: u64, 66 | } 67 | 68 | #[derive(Debug)] 69 | pub struct MasterState { 70 | pub slaves_responding: u32, 71 | pub al_states: u8, 72 | pub link_up: bool, 73 | } 74 | 75 | #[derive(Debug)] 76 | pub struct ConfigInfo { 77 | pub alias: u16, 78 | pub position: u16, 79 | pub id: SlaveId, 80 | pub slave_position: Option, 81 | pub sdo_count: u32, 82 | pub idn_count: u32, 83 | // TODO: more attributes are returned: 84 | // syncs[*], watchdog_*, dc_* 85 | } 86 | 87 | #[derive(Debug)] 88 | pub struct SlaveInfo { 89 | pub name: String, 90 | pub ring_pos: u16, 91 | pub id: SlaveId, 92 | pub rev: SlaveRev, 93 | pub alias: u16, 94 | pub current_on_ebus: i16, 95 | pub al_state: AlState, 96 | pub error_flag: u8, 97 | pub sync_count: u8, 98 | pub sdo_count: u16, 99 | pub ports: [SlavePortInfo; ec::EC_MAX_PORTS as usize], 100 | } 101 | 102 | #[derive(Debug, Clone, Copy)] 103 | pub enum SlavePortType { 104 | NotImplemented, 105 | NotConfigured, 106 | EBus, 107 | MII, 108 | } 109 | 110 | impl Default for SlavePortType { 111 | fn default() -> Self { 112 | SlavePortType::NotImplemented 113 | } 114 | } 115 | 116 | #[derive(Debug, Default, Clone, Copy)] 117 | pub struct SlavePortLink { 118 | pub link_up: bool, 119 | pub loop_closed: bool, 120 | pub signal_detected: bool, 121 | } 122 | 123 | #[derive(Debug, Default, Clone, Copy)] 124 | pub struct SlavePortInfo { 125 | pub desc: SlavePortType, 126 | pub link: SlavePortLink, 127 | pub receive_time: u32, 128 | pub next_slave: u16, 129 | pub delay_to_next_dc: u32, 130 | } 131 | 132 | #[derive(Debug)] 133 | pub struct SlaveConfigState { 134 | pub online: bool, 135 | pub operational: bool, 136 | pub al_state: AlState, 137 | } 138 | 139 | #[derive(Debug, Clone, Copy)] 140 | pub enum SyncDirection { 141 | Invalid, 142 | Output, 143 | Input, 144 | } 145 | 146 | #[derive(Debug, Clone, Copy)] 147 | pub enum WatchdogMode { 148 | Default, 149 | Enable, 150 | Disable, 151 | } 152 | 153 | pub type SmIndex = u8; 154 | pub type PdoIndex = u16; 155 | 156 | #[derive(Debug, Clone, Copy, new)] 157 | pub struct PdoEntryIndex { 158 | pub index: u16, 159 | pub subindex: u8, 160 | } 161 | 162 | #[derive(Clone, Copy, new)] 163 | pub struct SdoIndex { 164 | pub index: u16, 165 | pub subindex: u8, 166 | } 167 | 168 | #[derive(Debug)] 169 | pub struct SyncInfo<'a> { 170 | pub index: SmIndex, 171 | pub direction: SyncDirection, 172 | pub watchdog_mode: WatchdogMode, 173 | pub pdos: &'a [PdoInfo<'a>], 174 | } 175 | 176 | impl SyncInfo<'static> { 177 | pub const fn input(index: SmIndex, pdos: &'static [PdoInfo<'static>]) -> Self { 178 | SyncInfo { index, direction: SyncDirection::Input, watchdog_mode: WatchdogMode::Default, pdos } 179 | } 180 | 181 | pub const fn output(index: SmIndex, pdos: &'static [PdoInfo<'static>]) -> Self { 182 | SyncInfo { index, direction: SyncDirection::Output, watchdog_mode: WatchdogMode::Default, pdos } 183 | } 184 | } 185 | 186 | #[derive(Debug)] 187 | pub struct PdoInfo<'a> { 188 | pub index: PdoIndex, 189 | pub entries: &'a [PdoEntryInfo], 190 | } 191 | 192 | const NO_ENTRIES: &[PdoEntryInfo] = &[]; 193 | 194 | impl PdoInfo<'static> { 195 | pub const fn default(index: PdoIndex) -> PdoInfo<'static> { 196 | PdoInfo { index, entries: NO_ENTRIES } 197 | } 198 | } 199 | 200 | #[derive(Debug)] 201 | pub struct PdoEntryInfo { 202 | pub index: PdoEntryIndex, 203 | pub bit_length: u8, 204 | } 205 | 206 | #[derive(Debug)] 207 | pub enum AlState { 208 | Init = 1, 209 | Preop = 2, 210 | Safeop = 4, 211 | Op = 8, 212 | } 213 | 214 | impl From for AlState { 215 | fn from(st: u32) -> Self { 216 | match st { 217 | 1 => AlState::Init, 218 | 2 => AlState::Preop, 219 | 4 => AlState::Safeop, 220 | 8 => AlState::Op, 221 | x => panic!("invalid state {}", x), 222 | } 223 | } 224 | } 225 | 226 | pub trait SdoData { 227 | fn data_ptr(&self) -> *const u8 { self as *const _ as _ } 228 | fn data_size(&self) -> usize { std::mem::size_of_val(self) } 229 | } 230 | 231 | impl SdoData for u8 { } 232 | impl SdoData for u16 { } 233 | impl SdoData for u32 { } 234 | impl SdoData for u64 { } 235 | impl SdoData for i8 { } 236 | impl SdoData for i16 { } 237 | impl SdoData for i32 { } 238 | impl SdoData for i64 { } 239 | 240 | impl SdoData for &'_ [u8] { 241 | fn data_ptr(&self) -> *const u8 { self.as_ptr() } 242 | fn data_size(&self) -> usize { self.len() } 243 | } 244 | 245 | #[derive(Debug)] 246 | pub struct DomainState { 247 | pub working_counter: u32, 248 | pub wc_state: WcState, 249 | pub redundancy_active: bool, 250 | } 251 | 252 | #[derive(Debug, Clone, Copy)] 253 | pub enum WcState { 254 | Zero = 0, 255 | Incomplete, 256 | Complete, 257 | } 258 | 259 | impl From for WcState { 260 | fn from(st: u32) -> Self { 261 | match st { 262 | 0 => WcState::Zero, 263 | 1 => WcState::Incomplete, 264 | 2 => WcState::Complete, 265 | x => panic!("invalid state {}", x), 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/demo.rs: -------------------------------------------------------------------------------- 1 | // Part of ethercat-rs. Copyright 2018-2019 by the authors. 2 | // This work is dual-licensed under Apache 2.0 and MIT terms. 3 | 4 | use ethercat_plc::{PlcBuilder, ProcessImage, ExternImage}; 5 | use ethercat_plc::beckhoff::*; 6 | use ethercat_plc::mlz_spec::*; 7 | 8 | const PLC_NAME: &str = "testplc"; 9 | const PLC_VERSION: &str = "v0.0.5beta"; 10 | const PLC_AUTHOR_1: &str = "some very long strings that I think"; 11 | const PLC_AUTHOR_2: &str = "won't really fit into the indexer"; 12 | 13 | const INDEXER_SIZE: u16 = std::mem::size_of::() as u16; 14 | const INDEXER_OFFS: u16 = 6; 15 | 16 | 17 | #[repr(C, packed)] 18 | #[derive(ProcessImage)] 19 | struct Image { 20 | coupler: EK1100, 21 | #[sdo(0x8010, 1, "750u16")] // normal current 750 mA 22 | #[sdo(0x8010, 2, "250u16")] // reduced current 250 mA 23 | #[sdo(0x8010, 3, "2400u16")] // supply is 24 V 24 | #[sdo(0x8010, 4, "1000u16")] // resistance is 10 Ohm 25 | #[sdo(0x8012, 8, "1u8")] // feedback internal 26 | #[sdo(0x8012, 0x11, "7u8")] // info data 1: velocity 27 | #[sdo(0x8012, 0x19, "13u8")] // info data 2: motor current 28 | motor: EL7047_Position, 29 | dig_in: EL1008, 30 | dig_out: EL2008, 31 | ana_in: EL3104, 32 | ana_out: EL4132, 33 | } 34 | 35 | #[repr(C)] 36 | #[derive(Default)] 37 | struct Indexer { 38 | request: u16, 39 | data: [u16; 17], 40 | } 41 | 42 | #[repr(C)] 43 | #[derive(Default, ExternImage)] 44 | struct Extern { 45 | magic: f32, 46 | offset: u16, 47 | indexer: Indexer, 48 | if_blink: DiscreteOutput, 49 | if_magnet: FlatOutput1, 50 | } 51 | 52 | #[derive(Default)] 53 | struct MagnetVars { 54 | target: f32, 55 | start: f32, 56 | step: f32, 57 | current: f32, 58 | cycles: u32, 59 | } 60 | 61 | #[derive(Default)] 62 | struct Globals { 63 | cycle: u16, 64 | indexer_is_init: bool, 65 | devices: Vec, 66 | v_magnet: MagnetVars, 67 | } 68 | 69 | #[derive(Default)] 70 | struct DeviceInfo { 71 | typcode: u16, 72 | size: u16, 73 | offset: u16, 74 | unit: u16, 75 | flags: u8, 76 | all_flags: u32, 77 | params: [u16; 16], 78 | name: &'static str, 79 | aux: &'static [&'static str], 80 | absmax: f32, 81 | absmin: f32, 82 | } 83 | 84 | fn indexer(ext: &mut Extern, globals: &mut Globals) { 85 | if !globals.indexer_is_init { 86 | let mut calc_offset = INDEXER_OFFS + INDEXER_SIZE; 87 | for dev in &mut globals.devices { 88 | dev.all_flags = (dev.flags as u32) << 24; 89 | for j in 0..8 { 90 | if dev.aux.len() > j && !dev.aux[j].is_empty() { 91 | dev.all_flags |= 1 << j; 92 | } 93 | } 94 | if dev.size < (dev.typcode & 0xff) << 1 { 95 | dev.size = (dev.typcode & 0xff) << 1; 96 | } 97 | if dev.offset == 0 { 98 | dev.offset = calc_offset; 99 | } else { 100 | calc_offset = dev.offset; 101 | } 102 | calc_offset += dev.size; 103 | } 104 | globals.indexer_is_init = true; 105 | } 106 | 107 | ext.magic = MAGIC; 108 | ext.offset = INDEXER_OFFS; 109 | 110 | let devnum = ext.indexer.request as usize & 0xff; 111 | let infotype = (ext.indexer.request as usize >> 8) & 0x7f; 112 | 113 | let data = &mut ext.indexer.data; 114 | data.copy_from_slice(&[0; 17]); 115 | 116 | match devnum { 117 | 0 => match infotype { 118 | 0 => { 119 | data[..10].copy_from_slice( 120 | &[0, INDEXER_SIZE, ext.offset, 0, 0, 0x8000, 0, 0, 0, 0]); 121 | copy_string(&mut data[10..], PLC_NAME); 122 | } 123 | 1 => data[0] = INDEXER_SIZE, 124 | 4 => copy_string(data, PLC_NAME), 125 | 5 => copy_string(data, PLC_VERSION), 126 | 6 => copy_string(data, PLC_AUTHOR_1), 127 | 7 => copy_string(data, PLC_AUTHOR_2), 128 | _ => {} 129 | }, 130 | n if n <= globals.devices.len() => { 131 | let dev = &globals.devices[n-1]; 132 | match infotype { 133 | 0 => { 134 | data[..6].copy_from_slice(&[ 135 | dev.typcode, dev.size, dev.offset, dev.unit, 136 | dev.all_flags as u16, (dev.all_flags >> 16) as u16, 137 | ]); 138 | copy_float(&mut data[6..], dev.absmin); 139 | copy_float(&mut data[8..], dev.absmax); 140 | copy_string(&mut data[10..], dev.name); 141 | } 142 | 1 => data[0] = dev.size, 143 | 2 => data[0] = dev.offset, 144 | 3 => data[0] = dev.unit, 145 | 4 => copy_string(data, dev.name), 146 | 15 => data[..16].copy_from_slice(&dev.params), 147 | 0x10 ..= 0x17 => copy_string(data, dev.aux.get(infotype-0x10).unwrap_or(&"")), 148 | _ => {} 149 | } 150 | }, 151 | _ => {} 152 | } 153 | 154 | if infotype == 127 { 155 | data[0] = globals.cycle; 156 | } 157 | 158 | ext.indexer.request |= 0x8000; 159 | globals.cycle = globals.cycle.wrapping_add(1); 160 | } 161 | 162 | fn fb_blink(data_in: &mut EL1008, data_out: &mut EL2008, iface: &mut DiscreteOutput) { 163 | match iface.status & 0xf000 { 164 | RESET => { 165 | data_out.output = 0; 166 | iface.target = 0; 167 | iface.status = IDLE; 168 | } 169 | IDLE | WARN => { 170 | iface.status = if iface.target == iface.value { IDLE } else { WARN }; 171 | } 172 | START => { 173 | data_out.output = iface.target as u8; 174 | iface.status = IDLE; 175 | } 176 | _ => iface.status = ERROR, 177 | } 178 | 179 | iface.value = data_in.input as i16; 180 | } 181 | 182 | fn fb_magnet(inp: &mut EL3104, outp: &mut EL4132, 183 | iface: &mut FlatOutput1, vars: &mut MagnetVars) { 184 | iface.target = iface.target.max(-15.0).min(15.0); 185 | iface.param1 = iface.param1.max(-10.0).min(10.0); 186 | 187 | const SLOPE: f32 = 2000.; 188 | 189 | match iface.status & 0xf000 { 190 | RESET => { 191 | iface.status = IDLE; 192 | iface.target = 0.; 193 | } 194 | IDLE | WARN => {} 195 | START => { 196 | vars.target = iface.target; 197 | vars.start = iface.value; 198 | vars.current = iface.value; 199 | vars.cycles = 0; 200 | vars.step = iface.param1 / 100.; 201 | if vars.target < vars.start { 202 | vars.step = -vars.step; 203 | } 204 | if (vars.current - vars.target).abs() <= vars.step.abs() { 205 | vars.current = vars.target; 206 | iface.status = IDLE; 207 | } else { 208 | iface.status = BUSY; 209 | } 210 | } 211 | BUSY => { 212 | vars.current = vars.start + (vars.cycles as f32) * vars.step; 213 | if (vars.current - vars.target).abs() <= vars.step.abs() { 214 | vars.current = vars.target; 215 | iface.status = IDLE; 216 | } else { 217 | vars.cycles += 1; 218 | } 219 | } 220 | STOP => { 221 | iface.status = IDLE; 222 | } 223 | ERROR => { 224 | iface.value = 0.; 225 | } 226 | _ => iface.status = ERROR, 227 | } 228 | 229 | outp.ch1 = (vars.current * SLOPE) as i16; 230 | iface.value = inp.ch1 as f32 / SLOPE; 231 | } 232 | 233 | fn main() { 234 | let mut plc = PlcBuilder::new("plc") 235 | .cycle_freq(100) 236 | .with_server("0.0.0.0:5020") 237 | .logging_cfg(None, false) 238 | .build::().unwrap(); 239 | 240 | let mut globals = Globals::default(); 241 | globals.devices = vec![ 242 | DeviceInfo { typcode: 0x1E03, name: "Blink", offset: 42, .. Default::default() }, 243 | DeviceInfo { typcode: 0x3008, name: "Magnet", unit: 0x0007, 244 | params: [0x3c, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 245 | aux: &["output disabled", "emergency shutdown"], 246 | absmin: -15.0, absmax: 15.0, .. Default::default() }, 247 | ]; 248 | 249 | plc.run(|data, ext| { 250 | indexer(ext, &mut globals); 251 | fb_blink(&mut data.dig_in, &mut data.dig_out, &mut ext.if_blink); 252 | fb_magnet(&mut data.ana_in, &mut data.ana_out, &mut ext.if_magnet, 253 | &mut globals.v_magnet); 254 | 255 | if data.motor.mot_status & 1 != 0 { 256 | data.motor.mot_control = 0x1; 257 | } 258 | if data.motor.mot_status & 2 != 0 { 259 | data.motor.mot_target = (globals.v_magnet.current * 10000.) as _; 260 | } 261 | // let info1 = data.motor.info_data1; 262 | // let info2 = data.motor.info_data2; 263 | // println!("st = {:#x}, id = {:#x}, {:#x}", data.motor.mot_status & 0xfff, 264 | // info1, info2); 265 | println!("pos = {}", data.motor.mot_position & !0); 266 | }); 267 | } 268 | --------------------------------------------------------------------------------