├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── derive ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── samples ├── all │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── plc.yml │ └── src │ │ ├── main.rs │ │ └── plc_types.rs ├── custom_io_rpi_gpio │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── plc.yml │ └── src │ │ └── main.rs └── quickstart │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── plc.yml │ └── src │ └── main.rs └── src ├── api.rs ├── builder ├── config.rs └── mod.rs ├── cli.rs ├── client └── mod.rs ├── comm ├── mod.rs ├── serial.rs └── tcp.rs ├── eapi.rs ├── interval.rs ├── io ├── eapi │ └── mod.rs ├── mod.rs ├── modbus │ ├── mod.rs │ ├── regs.rs │ └── types.rs └── opcua │ ├── cache.rs │ ├── mod.rs │ └── session.rs ├── lib.rs ├── server ├── mod.rs └── modbus.rs └── tasks.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | _build 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rplc" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Serhij S. "] 6 | license = "Apache-2.0" 7 | repository = "https://github.com/eva-ics/rplc" 8 | description = "PLC programming in Rust" 9 | readme = "README.md" 10 | keywords = ["plc", "automation", "modbus", "opcua"] 11 | 12 | [package.metadata.docs.rs] 13 | features = ["eva", "client", "openssl-vendored"] 14 | 15 | [package.metadata.playground] 16 | features = ["eva", "client", "openssl-vendored"] 17 | 18 | [lib] 19 | name = "rplc" 20 | path = "src/lib.rs" 21 | 22 | [[bin]] 23 | name = "rplc" 24 | path = "src/cli.rs" 25 | required-features = ["cli"] 26 | 27 | [dependencies] 28 | Inflector = "0.11.4" 29 | parking_lot = "0.12.1" 30 | serde = { version = "1.0.160", features = ["derive"] } 31 | serde_yaml = "0.9.17" 32 | rplc_derive = "0.3.1" 33 | indexmap = { version = "1.9.2", features = ["serde"] } 34 | eva-common = { version = "0.3.0", features = ["payload", "bus-rpc", "events"] } 35 | log = "0.4.17" 36 | once_cell = "1.17.1" 37 | bmart-derive = "0.1.3" 38 | env_logger = "0.10.0" 39 | codegen = "0.2.0" 40 | rmodbus = { version = "0.7.2", features = ["with_serde"], optional = true } 41 | threadpool = "1.8.1" 42 | triggered = "0.1.2" 43 | negative-impl = "0.1.3" 44 | syslog = "6.1.0" 45 | signal-hook = "0.3.15" 46 | libc = "0.2.142" 47 | core_affinity = "0.8.0" 48 | clap = { version = "4.2.5", optional = true, features = ["derive"] } 49 | prettytable-rs = { version = "0.10.0", optional = true } 50 | colored = { version = "2.0.0", optional = true } 51 | hostname = "0.3.1" 52 | ttl_cache = { version = "0.5.1", optional = true } 53 | busrt = { version = "0.4.4", features = ["ipc", "rpc"], optional = true } 54 | tokio = { version = "1.36.0", features = ["full"], optional = true } 55 | eva-sdk = { version = "0.3.0", features = ["controller"], optional = true } 56 | async-channel = { version = "1.8.0", optional = true } 57 | serial = { version = "0.4.0", optional = true } 58 | rplc_opcua = { version = "0.12.1", optional = true } 59 | bmart = { version = "0.2.4", optional = true } 60 | tera = "1.18.1" 61 | 62 | [features] 63 | cli = ["clap", "prettytable-rs", "colored", "client", "eva"] 64 | client = ["tokio", "bmart", "eva"] 65 | eva = ["busrt", "tokio", "eva-sdk", "async-channel"] 66 | modbus = ["rmodbus", "serial"] 67 | opcua = ["rplc_opcua", "ttl_cache"] 68 | openssl-vendored = ["eva-common/openssl-vendored"] 69 | 70 | [profile.release] 71 | strip = true 72 | lto = true 73 | codegen-units = 1 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell grep ^version Cargo.toml|cut -d\" -f2) 2 | 3 | all: @echo "Select target" 4 | 5 | tag: 6 | git tag -a v${VERSION} -m v${VERSION} 7 | git push origin --tags 8 | 9 | ver: 10 | sed -i 's/^version = ".*/version = "${VERSION}"/g' Cargo.toml 11 | sed -i 's/^const VERSION:.*/const VERSION: \&str = "${VERSION}";/g' src/main.rs 12 | 13 | release: tag pkg 14 | 15 | pkg: 16 | rm -rf _build 17 | mkdir -p _build 18 | cargo build --release --features cli,openssl-vendored 19 | cd target/release && cp rplc /opt/rplc/_build/rplc-${VERSION}-x86_64 20 | cross build --target aarch64-unknown-linux-gnu --release --features cli,openssl-vendored 21 | cd target/aarch64-unknown-linux-gnu/release && \ 22 | aarch64-linux-gnu-strip rplc && \ 23 | cp rplc /opt/rplc/_build/rplc-${VERSION}-aarch64 24 | cd _build && echo "" | gh release create v$(VERSION) -t "v$(VERSION)" \ 25 | rplc-${VERSION}-x86_64 \ 26 | rplc-${VERSION}-aarch64 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | rPLC - PLC programming for Linux in Rust 3 | crates.io page 4 | docs.rs page 5 |

6 | 7 | THIS IS A LEGACY REPOSITORY. CONSIDER SWITCHING TO [RoboPLC](https://github.com/roboplc/roboplc) INSTEAD. 8 | 9 | rPLC project allows to write PLC programs for Linux systems in Rust using 10 | classical PLC programming approach. 11 | 12 | rPLC supports Modbus and OPC-UA input/output protocols out-of-the-box and can 13 | be easily extended with custom I/O as well. 14 | 15 | rPLC is a part of [EVA ICS](https://www.eva-ics.com) open-source industrial 16 | automation eco-system. 17 | 18 | ## A quick example 19 | 20 | ```rust,ignore 21 | use rplc::prelude::*; 22 | 23 | mod plc; 24 | 25 | #[plc_program(loop = "200ms")] 26 | fn tempmon() { 27 | let mut ctx = plc_context_mut!(); 28 | if ctx.temperature > 30.0 { 29 | ctx.fan = true; 30 | } else if ctx.temperature < 25.0 { 31 | ctx.fan = false; 32 | } 33 | } 34 | 35 | fn main() { 36 | init_plc!(); 37 | tempmon_spawn(); 38 | run_plc!(); 39 | } 40 | ``` 41 | 42 | ## Technical documentation 43 | 44 | Available at 45 | -------------------------------------------------------------------------------- /derive/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /derive/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "darling" 7 | version = "0.14.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "c0808e1bd8671fb44a113a14e13497557533369847788fa2ae912b6ebfce9fa8" 10 | dependencies = [ 11 | "darling_core", 12 | "darling_macro", 13 | ] 14 | 15 | [[package]] 16 | name = "darling_core" 17 | version = "0.14.3" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "001d80444f28e193f30c2f293455da62dcf9a6b29918a4253152ae2b1de592cb" 20 | dependencies = [ 21 | "fnv", 22 | "ident_case", 23 | "proc-macro2", 24 | "quote", 25 | "strsim", 26 | "syn", 27 | ] 28 | 29 | [[package]] 30 | name = "darling_macro" 31 | version = "0.14.3" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "b36230598a2d5de7ec1c6f51f72d8a99a9208daff41de2084d06e3fd3ea56685" 34 | dependencies = [ 35 | "darling_core", 36 | "quote", 37 | "syn", 38 | ] 39 | 40 | [[package]] 41 | name = "fnv" 42 | version = "1.0.7" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 45 | 46 | [[package]] 47 | name = "ident_case" 48 | version = "1.0.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 51 | 52 | [[package]] 53 | name = "proc-macro2" 54 | version = "1.0.51" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" 57 | dependencies = [ 58 | "unicode-ident", 59 | ] 60 | 61 | [[package]] 62 | name = "quote" 63 | version = "1.0.23" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 66 | dependencies = [ 67 | "proc-macro2", 68 | ] 69 | 70 | [[package]] 71 | name = "rplc_derive" 72 | version = "0.3.1" 73 | dependencies = [ 74 | "darling", 75 | "quote", 76 | "syn", 77 | ] 78 | 79 | [[package]] 80 | name = "strsim" 81 | version = "0.10.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 84 | 85 | [[package]] 86 | name = "syn" 87 | version = "1.0.107" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 90 | dependencies = [ 91 | "proc-macro2", 92 | "quote", 93 | "unicode-ident", 94 | ] 95 | 96 | [[package]] 97 | name = "unicode-ident" 98 | version = "1.0.6" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 101 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rplc_derive" 3 | version = "0.3.1" 4 | edition = "2021" 5 | authors = ["Serhij S. "] 6 | license = "Apache-2.0" 7 | repository = "https://github.com/eva-ics/rplc" 8 | description = "Derive macros for rPLC project" 9 | readme = "README.md" 10 | keywords = ["plc", "automation"] 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | darling = "0.14.3" 17 | quote = "1.0.23" 18 | syn = { version = "1.0.107", features = ["full"] } 19 | 20 | -------------------------------------------------------------------------------- /derive/README.md: -------------------------------------------------------------------------------- 1 | # Derive macros for rPLC project 2 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use darling::FromMeta; 2 | use proc_macro::TokenStream; 3 | use quote::{format_ident, quote, ToTokens}; 4 | use syn::{parse_macro_input, AttributeArgs}; 5 | 6 | #[derive(Debug, FromMeta)] 7 | struct PlcProgramArgs { 8 | #[darling(rename = "loop")] 9 | lp: String, 10 | #[darling()] 11 | shift: Option, 12 | } 13 | 14 | /// # Panics 15 | /// 16 | /// Will panic if function name is more than 14 symbols 17 | #[proc_macro_attribute] 18 | pub fn plc_program(args: TokenStream, input: TokenStream) -> TokenStream { 19 | let attr_args = parse_macro_input!(args as AttributeArgs); 20 | let args = match PlcProgramArgs::from_list(&attr_args) { 21 | Ok(v) => v, 22 | Err(e) => { 23 | return TokenStream::from(e.write_errors()); 24 | } 25 | }; 26 | let item: syn::Item = syn::parse(input).expect("Invalid input"); 27 | let int = parse_interval(&args.lp).unwrap(); 28 | let shift: u64 = args 29 | .shift 30 | .map(|v| parse_interval(&v).unwrap()) 31 | .unwrap_or_default(); 32 | if let syn::Item::Fn(fn_item) = item { 33 | let block = fn_item.block; 34 | let name = fn_item.sig.ident; 35 | assert!( 36 | name.to_string().len() < 15, 37 | "function name must be less than 15 symbols ({})", 38 | name 39 | ); 40 | assert!( 41 | fn_item.sig.inputs.is_empty(), 42 | "function must have no arguments ({})", 43 | name 44 | ); 45 | assert!( 46 | fn_item.sig.output == syn::ReturnType::Default, 47 | "function must not return a value ({})", 48 | name 49 | ); 50 | let spawner_name = format_ident!("{}_spawn", name); 51 | let prgname = name.to_string(); 52 | let f = quote! { 53 | fn #spawner_name() { 54 | ::rplc::tasks::spawn_program_loop(#prgname, 55 | #name, 56 | ::std::time::Duration::from_nanos(#int), 57 | ::std::time::Duration::from_nanos(#shift) 58 | ); 59 | } 60 | fn #name() { 61 | #block 62 | } 63 | }; 64 | f.into_token_stream().into() 65 | } else { 66 | panic!("expected fn") 67 | } 68 | } 69 | 70 | #[derive(Debug)] 71 | enum PError { 72 | Parse, 73 | } 74 | 75 | fn parse_interval(s: &str) -> Result { 76 | if let Some(v) = s.strip_suffix("ms") { 77 | Ok(v.parse::().map_err(|_| PError::Parse)? * 1_000_000) 78 | } else if let Some(v) = s.strip_suffix("us") { 79 | Ok(v.parse::().map_err(|_| PError::Parse)? * 1_000) 80 | } else if let Some(v) = s.strip_suffix("ns") { 81 | Ok(v.parse::().map_err(|_| PError::Parse)?) 82 | } else if let Some(v) = s.strip_suffix('s') { 83 | Ok(v.parse::().map_err(|_| PError::Parse)? * 1_000_000_000) 84 | } else { 85 | Err(PError::Parse) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /samples/all/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | src/plc 3 | plc.dat 4 | -------------------------------------------------------------------------------- /samples/all/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "all" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Test plc" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | rmp-serde = "1.1.1" 11 | rplc = { path = "../..", features = ["eva", "modbus", "opcua"] } 12 | snmp = "0.2.2" 13 | 14 | [build-dependencies] 15 | rplc = { path = "../..", features = ["eva", "modbus", "opcua"] } 16 | 17 | [profile.release] 18 | strip = true 19 | lto = true 20 | codegen-units = 1 21 | -------------------------------------------------------------------------------- /samples/all/build.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | fn main() -> Result<(), Box> { 4 | let mut builder = rplc::builder::Builder::new("plc.yml"); 5 | builder.insert("modbus_server_port", &9503); 6 | builder.generate()?; 7 | //rplc::builder::generate("plc.yml")?; 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /samples/all/plc.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | core: 3 | stop_timeout: 10 4 | server: 5 | - kind: modbus 6 | config: 7 | proto: tcp 8 | listen: 127.0.0.1:{{ modbus_server_port }} 9 | unit: 0x01 10 | timeout: 1 11 | maxconn: 5 12 | #- kind: modbus 13 | #config: 14 | #proto: rtu 15 | #listen: /dev/modbus_srv:9600:8:N:1 16 | #unit: 0x01 17 | #timeout: 3600 18 | context: 19 | serialize: true 20 | modbus: 21 | c: 1000 22 | d: 1000 23 | i: 1000 24 | h: 1000 25 | fields: 26 | router_if1: BOOL 27 | temperature: REAL 28 | humidity: REAL 29 | temps: REAL[4] 30 | fan: BOOL 31 | fan2: BOOL 32 | fan3: BOOL 33 | fan4: BOOL 34 | fan_speed: UINT 35 | fan4_speed: REAL 36 | timers: Timers 37 | timer2: Duration 38 | "connector[4]": 39 | state: BOOL 40 | voltage: REAL 41 | data: 42 | counter: ULINT 43 | flags: UINT[12] 44 | flags2: UDINT[3] 45 | bool_flags: BOOL[12] 46 | opc_temp: LREAL[2] 47 | subfield: 48 | a: ULINT 49 | b: ULINT 50 | temp_out: REAL 51 | eapi: 52 | action_pool_size: 4 53 | io: 54 | - id: scada1 55 | kind: eapi 56 | input: 57 | - action_map: 58 | - oid: unit:tests/fan3 59 | value: fan3 60 | - oid: unit:tests/fan4 61 | value: fan4 62 | - oid: unit:tests/fan4_speed 63 | value: fan4_speed 64 | output: 65 | - oid_map: 66 | - oid: sensor:tests/temp_out 67 | value: data.subfield.temp_out 68 | - oid: sensor:tests/temp3 69 | value: temps[3] 70 | - oid: sensor:tests/data_flags 71 | value: data.flags 72 | sync: 500ms 73 | cache: 5s 74 | - oid_map: 75 | - oid: unit:tests/fan 76 | value: fan 77 | - oid: unit:tests/fan_speed 78 | value: fan_speed 79 | - oid: unit:tests/fan2 80 | value: fan2 81 | - oid: unit:tests/fan3 82 | value: fan3 83 | - oid: unit:tests/fan4 84 | value: fan4 85 | - oid: unit:tests/fan4_speed 86 | value: fan4_speed 87 | sync: 300ms 88 | cache: 30s 89 | - id: opc1 90 | kind: opcua 91 | config: 92 | pki_dir: /tmp/plc1_pki 93 | trust_server_certs: false 94 | create_keys: true 95 | timeout: 5.0 96 | auth: 97 | user: sample1 98 | password: sample1pwd 99 | #cert_file: own/cert.der 100 | #key_file: private/private.pem 101 | url: opc.tcp://localhost:4855 102 | input: 103 | - nodes: 104 | - id: "ns=2;g=dcff8e02-4706-49ea-979c-fc1ec6cff8ef" 105 | map: data.opc_temp 106 | sync: 1s 107 | output: 108 | - nodes: 109 | - id: "ns=2;s=fan1" 110 | map: fan 111 | - id: "ns=2;s=fan2" 112 | map: fan2 113 | - id: "ns=2;s=fan3" 114 | map: fan3 115 | sync: 1s 116 | cache: 10s 117 | - id: mb_local 118 | kind: modbus 119 | config: 120 | proto: tcp 121 | path: 127.0.0.1:5504 122 | #proto: rtu 123 | #path: /dev/modbus:9600:8:N:1 124 | timeout: 3600 125 | input: 126 | - reg: h0-3 127 | unit: 0x01 128 | map: 129 | - offset: 0 130 | target: temperature 131 | sync: 500ms 132 | - reg: h10 133 | unit: 0x01 134 | number: 18 135 | map: 136 | - offset: =10 137 | target: data.flags 138 | - offset: 12 139 | target: data.flags2 140 | sync: 1s 141 | - reg: c2 142 | unit: 0x01 143 | number: 12 144 | map: 145 | - target: data.bool_flags 146 | sync: 500ms 147 | output: 148 | - reg: c0-2 149 | unit: 0x01 150 | map: 151 | - offset: 0 152 | source: fan 153 | - offset: =1 154 | source: fan2 155 | - offset: =2 156 | source: fan3 157 | sync: 500ms 158 | - reg: h200 159 | unit: 0x01 160 | number: 10 161 | map: 162 | #- offset: 0 163 | #source: data.subfield.a 164 | #- offset: 4 165 | #source: data.subfield.b 166 | - offset: =208 167 | source: data.subfield.temp_out 168 | sync: 500ms 169 | -------------------------------------------------------------------------------- /samples/all/src/main.rs: -------------------------------------------------------------------------------- 1 | use rplc::prelude::*; 2 | 3 | mod plc; 4 | mod plc_types; 5 | 6 | use std::time::Duration; 7 | use std::time::Instant; 8 | 9 | #[plc_program(loop = "500ms")] 10 | fn p1() { 11 | let mut ctx = plc_context_mut!(); 12 | //info!("p1"); 13 | if ctx.temperature > 25.0 { 14 | ctx.fan = true; 15 | ctx.fan2 = false; 16 | } else if ctx.temperature < 23.0 { 17 | ctx.fan = false; 18 | ctx.fan2 = true; 19 | } 20 | info!("fan1: {}, fan2: {}", ctx.fan, ctx.fan2); 21 | ctx.data.subfield.a += 1_000_000_000; 22 | ctx.data.subfield.b += 10; 23 | let temp = ctx.temperature; 24 | ctx.modbus.set_holding(20, (temp * 100.0) as u16).unwrap(); 25 | ctx.timers.t1 = Some(Instant::now()); 26 | if let Some(info) = rplc::tasks::controller_stats().lock().current_thread_info() { 27 | ctx.modbus.set_inputs_from_u32(100, info.iters).unwrap(); 28 | ctx.modbus.set_input(102, info.jitter_max).unwrap(); 29 | ctx.modbus.set_input(103, info.jitter_min).unwrap(); 30 | ctx.modbus.set_input(104, info.jitter_avg).unwrap(); 31 | ctx.modbus.set_input(105, info.jitter_last).unwrap(); 32 | } 33 | } 34 | 35 | #[plc_program(loop = "1s")] 36 | fn p2() { 37 | let mut ctx = plc_context_mut!(); 38 | ctx.data.counter += 1; 39 | //info!("p2"); 40 | info!( 41 | "temperature: {}, counter: {}, opc temps: {:?}", 42 | ctx.temperature, ctx.data.counter, ctx.data.opc_temp 43 | ); 44 | ctx.data.subfield.temp_out = ctx.temperature; 45 | if ctx.router_if1 { 46 | info!("router up"); 47 | } else { 48 | warn!("router down"); 49 | } 50 | } 51 | 52 | fn get_if_status( 53 | oid: &[u32], 54 | session: &mut snmp::SyncSession, 55 | ) -> Result> { 56 | let response = session 57 | .get(oid) 58 | .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "snmp"))?; 59 | if let Some((_oid, snmp::Value::Integer(state))) = response.varbinds.last() { 60 | Ok(state == 1) 61 | } else { 62 | Err(std::io::Error::new( 63 | std::io::ErrorKind::InvalidInput, 64 | "snmp value", 65 | ))? 66 | } 67 | } 68 | 69 | fn spawn_check_router() { 70 | let oid = &[1, 3, 6, 1, 2, 1, 2, 2, 1, 8, 1]; 71 | let agent_addr = "10.90.34.1:161"; 72 | let community = b"public"; 73 | let timeout = Duration::from_secs(2); 74 | let mut session = snmp::SyncSession::new(agent_addr, community, Some(timeout), 0).unwrap(); 75 | rplc::tasks::spawn_input_loop( 76 | "router1", 77 | Duration::from_secs(2), 78 | Duration::default(), 79 | move || match get_if_status(oid, &mut session) { 80 | Ok(v) => { 81 | plc_context_mut!().router_if1 = v; 82 | } 83 | Err(e) => { 84 | error!("{}", e); 85 | } 86 | }, 87 | ); 88 | } 89 | 90 | fn spawn_relays() { 91 | rplc::tasks::spawn_output_loop( 92 | "relays", 93 | Duration::from_secs(2), 94 | Duration::default(), 95 | move || { 96 | info!("relays set"); 97 | }, 98 | ); 99 | } 100 | 101 | fn shutdown() { 102 | warn!("shutting down"); 103 | let mut ctx = plc_context_mut!(); 104 | ctx.fan = false; 105 | ctx.fan2 = false; 106 | ctx.fan3 = false; 107 | ctx.fan4 = false; 108 | warn!("shutdown program completed"); 109 | } 110 | 111 | use std::fs; 112 | 113 | fn main() { 114 | init_plc!(); 115 | rplc::tasks::on_shutdown(shutdown); 116 | if let Ok(data) = fs::read("plc.dat") { 117 | info!("loading context"); 118 | *plc_context_mut!() = rmp_serde::from_slice(&data).unwrap(); 119 | } 120 | p1_spawn(); 121 | p2_spawn(); 122 | spawn_check_router(); 123 | spawn_relays(); 124 | rplc::tasks::spawn_stats_log(Duration::from_secs(5)); 125 | run_plc!(); 126 | fs::write( 127 | "plc.dat", 128 | rmp_serde::to_vec_named(&*plc_context!()).unwrap(), 129 | ) 130 | .unwrap(); 131 | } 132 | -------------------------------------------------------------------------------- /samples/all/src/plc_types.rs: -------------------------------------------------------------------------------- 1 | use ::rplc::export::serde::{self, Deserialize, Serialize}; 2 | use std::time::Instant; 3 | 4 | pub use std::time::Duration; 5 | 6 | #[derive(Default, Serialize, Deserialize)] 7 | #[serde(crate = "self::serde")] 8 | pub struct Timers { 9 | #[serde(skip)] 10 | pub t1: Option, 11 | pub enabled: bool, 12 | } 13 | -------------------------------------------------------------------------------- /samples/custom_io_rpi_gpio/.gitignore: -------------------------------------------------------------------------------- 1 | src/plc 2 | -------------------------------------------------------------------------------- /samples/custom_io_rpi_gpio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "custom_io_rpi_gpio" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | rplc = { path = "../.." } 10 | rppal = "0.14.1" 11 | 12 | [build-dependencies] 13 | rplc = { path = "../.." } 14 | -------------------------------------------------------------------------------- /samples/custom_io_rpi_gpio/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | rplc::builder::generate("plc.yml").unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /samples/custom_io_rpi_gpio/plc.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | context: 3 | fields: 4 | in1: BOOL 5 | in2: BOOL 6 | out1: BOOL 7 | out2: BOOL 8 | -------------------------------------------------------------------------------- /samples/custom_io_rpi_gpio/src/main.rs: -------------------------------------------------------------------------------- 1 | use rplc::prelude::*; 2 | use rppal::gpio::Gpio; 3 | use std::time::Duration; 4 | 5 | mod plc; 6 | 7 | const GPIO_IN1: u8 = 1; 8 | const GPIO_IN2: u8 = 2; 9 | 10 | const GPIO_OUT1: u8 = 3; 11 | const GPIO_OUT2: u8 = 4; 12 | 13 | #[plc_program(loop = "500ms")] 14 | fn pinroute() { 15 | let mut ctx = plc_context_mut!(); 16 | ctx.out1 = ctx.in1; 17 | ctx.out2 = ctx.in2; 18 | } 19 | 20 | fn gpio_input_spawn() { 21 | let pin_in1 = Gpio::new().unwrap().get(GPIO_IN1).unwrap().into_input(); 22 | let pin_in2 = Gpio::new().unwrap().get(GPIO_IN2).unwrap().into_input(); 23 | rplc::tasks::spawn_input_loop( 24 | "gpio", 25 | Duration::from_millis(500), 26 | Duration::default(), 27 | move || { 28 | let in1 = pin_in1.is_high(); 29 | let in2 = pin_in2.is_high(); 30 | let mut ctx = plc_context_mut!(); 31 | ctx.in1 = in1; 32 | ctx.in2 = in2; 33 | }, 34 | ); 35 | } 36 | 37 | fn gpio_output_spawn() { 38 | let mut pin_out1 = Gpio::new().unwrap().get(GPIO_OUT1).unwrap().into_output(); 39 | let mut pin_out2 = Gpio::new().unwrap().get(GPIO_OUT2).unwrap().into_output(); 40 | rplc::tasks::spawn_output_loop( 41 | "gpio", 42 | Duration::from_millis(500), 43 | Duration::default(), 44 | move || { 45 | let (out1, out2) = { 46 | let ctx = plc_context!(); 47 | (ctx.out1, ctx.out2) 48 | }; 49 | pin_out1.write(out1.into()); 50 | pin_out2.write(out2.into()); 51 | }, 52 | ); 53 | } 54 | 55 | fn main() { 56 | init_plc!(); 57 | gpio_input_spawn(); 58 | gpio_output_spawn(); 59 | pinroute_spawn(); 60 | run_plc!(); 61 | } 62 | -------------------------------------------------------------------------------- /samples/quickstart/.gitignore: -------------------------------------------------------------------------------- 1 | src/plc 2 | -------------------------------------------------------------------------------- /samples/quickstart/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quickstart" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | rplc = { path = "../..", features = ["modbus"] } 10 | 11 | [build-dependencies] 12 | rplc = { path = "../..", features = ["modbus"] } 13 | -------------------------------------------------------------------------------- /samples/quickstart/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | rplc::builder::generate("plc.yml").unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /samples/quickstart/plc.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | context: 3 | fields: 4 | temperature: REAL 5 | fan: BOOL 6 | io: 7 | - id: modbus1 8 | kind: modbus 9 | config: 10 | proto: tcp 11 | path: 127.0.0.1:5504 12 | input: 13 | - reg: h0-1 14 | unit: 0x01 15 | map: 16 | - target: temperature 17 | sync: 500ms 18 | output: 19 | - reg: c0 20 | unit: 0x01 21 | map: 22 | - source: fan 23 | sync: 500ms 24 | -------------------------------------------------------------------------------- /samples/quickstart/src/main.rs: -------------------------------------------------------------------------------- 1 | use rplc::prelude::*; 2 | 3 | mod plc; 4 | 5 | #[plc_program(loop = "200ms")] 6 | fn tempmon() { 7 | let mut ctx = plc_context_mut!(); 8 | if ctx.temperature > 30.0 { 9 | ctx.fan = true; 10 | } else if ctx.temperature < 25.0 { 11 | ctx.fan = false; 12 | } 13 | } 14 | 15 | fn main() { 16 | init_plc!(); 17 | tempmon_spawn(); 18 | run_plc!(); 19 | } 20 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use crate::tasks; 2 | use eva_common::payload::{pack, unpack}; 3 | use eva_common::value::{to_value, Value}; 4 | use eva_common::{EResult, Error}; 5 | use log::error; 6 | use serde::{Deserialize, Serialize}; 7 | use std::collections::BTreeMap; 8 | use std::fs; 9 | use std::io; 10 | use std::io::{Read, Write}; 11 | use std::os::unix; 12 | use std::path::PathBuf; 13 | 14 | const JSON_RPC: &str = "2.0"; 15 | const MAX_API_CONN: usize = 10; 16 | 17 | #[derive(Serialize, Deserialize)] 18 | pub struct Request { 19 | jsonrpc: String, 20 | method: String, 21 | params: Option, 22 | } 23 | 24 | impl Request { 25 | pub fn new(method: &str, params: Option) -> Self { 26 | Self { 27 | jsonrpc: JSON_RPC.to_owned(), 28 | method: method.to_owned(), 29 | params, 30 | } 31 | } 32 | fn check(&self) -> EResult<()> { 33 | if self.jsonrpc == JSON_RPC { 34 | Ok(()) 35 | } else { 36 | Err(Error::unsupported("unsupported json rpc version")) 37 | } 38 | } 39 | } 40 | 41 | #[derive(Serialize, Deserialize)] 42 | pub struct Response { 43 | jsonrpc: String, 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub result: Option, 46 | #[serde(skip_serializing_if = "Option::is_none")] 47 | error: Option, 48 | } 49 | 50 | impl Response { 51 | #[inline] 52 | fn err(e: Error) -> Self { 53 | Self { 54 | jsonrpc: JSON_RPC.to_owned(), 55 | result: None, 56 | error: Some(ResponseError { 57 | code: e.kind() as i16, 58 | message: e.message().map(ToOwned::to_owned), 59 | }), 60 | } 61 | } 62 | #[inline] 63 | fn result(val: Value) -> Self { 64 | Self { 65 | jsonrpc: JSON_RPC.to_owned(), 66 | result: Some(val), 67 | error: None, 68 | } 69 | } 70 | pub fn check(&self) -> EResult<()> { 71 | if self.jsonrpc != JSON_RPC { 72 | return Err(Error::unsupported("unsupported json rpc version")); 73 | } 74 | if let Some(ref err) = self.error { 75 | return Err(Error::newc( 76 | eva_common::ErrorKind::from(err.code), 77 | err.message.as_deref(), 78 | )); 79 | } 80 | Ok(()) 81 | } 82 | } 83 | 84 | #[derive(Serialize, Deserialize)] 85 | struct ResponseError { 86 | code: i16, 87 | message: Option, 88 | } 89 | 90 | impl From> for Response { 91 | fn from(r: EResult) -> Self { 92 | match r { 93 | Ok(v) => Response::result(v), 94 | Err(e) => Response::err(e), 95 | } 96 | } 97 | } 98 | 99 | pub(crate) fn spawn_api() -> PathBuf { 100 | let mut socket_path = crate::var_dir(); 101 | socket_path.push(format!("{}.plcsock", crate::name())); 102 | let _ = fs::remove_file(&socket_path); 103 | let listener = unix::net::UnixListener::bind(&socket_path).unwrap(); 104 | tasks::spawn_service("api", move || { 105 | let pool = threadpool::ThreadPool::new(MAX_API_CONN); 106 | for sr in listener.incoming() { 107 | match sr { 108 | Ok(stream) => { 109 | pool.execute(move || { 110 | if let Err(e) = handle_api_stream(stream) { 111 | error!("API {}", e); 112 | } 113 | }); 114 | } 115 | Err(e) => error!("API {}", e), 116 | } 117 | } 118 | }); 119 | socket_path 120 | } 121 | 122 | fn handle_api_stream(mut stream: unix::net::UnixStream) -> Result<(), Error> { 123 | stream.set_read_timeout(Some(crate::DEFAULT_TIMEOUT))?; 124 | stream.set_write_timeout(Some(crate::DEFAULT_TIMEOUT))?; 125 | loop { 126 | let mut buf: [u8; 5] = [0; 5]; 127 | match stream.read_exact(&mut buf) { 128 | Ok(()) => {} 129 | Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => { 130 | break; 131 | } 132 | Err(e) => { 133 | return Err(e.into()); 134 | } 135 | } 136 | if buf[0] != 0 { 137 | return Err(Error::invalid_data("invalid header")); 138 | } 139 | let mut buf = vec![0; usize::try_from(u32::from_le_bytes(buf[1..].try_into()?))?]; 140 | stream.read_exact(&mut buf)?; 141 | let req: Request = unpack(&buf)?; 142 | req.check()?; 143 | let response: Response = handle_api_call(&req.method, req.params).into(); 144 | let packed = pack(&response)?; 145 | let mut buf = Vec::with_capacity(packed.len() + 5); 146 | buf.push(0u8); 147 | buf.extend(u32::try_from(packed.len())?.to_le_bytes()); 148 | buf.extend(packed); 149 | stream.write_all(&buf)?; 150 | } 151 | Ok(()) 152 | } 153 | 154 | fn handle_api_call(method: &str, params: Option) -> Result { 155 | macro_rules! ok { 156 | () => { 157 | Ok(Value::Unit) 158 | }; 159 | } 160 | macro_rules! invalid_params { 161 | () => { 162 | Err(Error::invalid_params("invalid method parameters")) 163 | }; 164 | } 165 | match method { 166 | "test" => { 167 | if params.is_none() { 168 | ok!() 169 | } else { 170 | invalid_params!() 171 | } 172 | } 173 | "info" => { 174 | if params.is_none() { 175 | to_value(crate::plc_info()).map_err(Into::into) 176 | } else { 177 | invalid_params!() 178 | } 179 | } 180 | "thread_stats.get" => { 181 | if params.is_none() { 182 | let mut result = BTreeMap::new(); 183 | let thread_stats = &tasks::controller_stats().lock().thread_stats; 184 | for (name, st) in thread_stats { 185 | result.insert(name, st.info()); 186 | } 187 | to_value(result).map_err(Into::into) 188 | } else { 189 | invalid_params!() 190 | } 191 | } 192 | "thread_stats.reset" => { 193 | if params.is_none() { 194 | tasks::reset_thread_stats(); 195 | ok!() 196 | } else { 197 | invalid_params!() 198 | } 199 | } 200 | v => Err(Error::not_implemented(v)), 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/builder/config.rs: -------------------------------------------------------------------------------- 1 | use crate::io::Kind; 2 | use eva_common::value::Value; 3 | use indexmap::IndexMap; 4 | use inflector::Inflector; 5 | use serde::Deserialize; 6 | use std::error::Error; 7 | use std::fmt::Write as _; 8 | use std::fs; 9 | use std::path::Path; 10 | 11 | #[derive(Deserialize, Debug)] 12 | #[serde(deny_unknown_fields)] 13 | pub struct Config { 14 | version: u16, 15 | #[serde(default)] 16 | pub(crate) core: CoreConfig, 17 | #[serde(default)] 18 | context: ContextConfig, 19 | #[cfg(feature = "eva")] 20 | #[serde(default)] 21 | pub(crate) eapi: EapiConfig, 22 | #[serde(default)] 23 | io: Vec, 24 | #[serde(default)] 25 | server: Vec, 26 | } 27 | 28 | fn default_stop_timeout() -> f64 { 29 | crate::DEFAULT_STOP_TIMEOUT 30 | } 31 | 32 | #[derive(Deserialize, Debug)] 33 | #[serde(deny_unknown_fields)] 34 | pub(crate) struct CoreConfig { 35 | #[serde(default = "default_stop_timeout")] 36 | pub(crate) stop_timeout: f64, 37 | #[serde(default)] 38 | pub(crate) stack_size: Option, 39 | } 40 | 41 | impl Default for CoreConfig { 42 | fn default() -> Self { 43 | Self { 44 | stop_timeout: default_stop_timeout(), 45 | stack_size: None, 46 | } 47 | } 48 | } 49 | 50 | #[cfg(feature = "eva")] 51 | #[inline] 52 | fn default_eapi_action_pool_size() -> usize { 53 | 1 54 | } 55 | 56 | #[cfg(feature = "eva")] 57 | #[derive(Deserialize, Debug)] 58 | pub(crate) struct EapiConfig { 59 | #[serde(default = "default_eapi_action_pool_size")] 60 | pub(crate) action_pool_size: usize, 61 | } 62 | 63 | #[cfg(feature = "eva")] 64 | impl Default for EapiConfig { 65 | fn default() -> Self { 66 | Self { 67 | action_pool_size: default_eapi_action_pool_size(), 68 | } 69 | } 70 | } 71 | 72 | #[derive(Deserialize, Debug)] 73 | #[serde(deny_unknown_fields)] 74 | struct ServerConfig { 75 | kind: crate::server::Kind, 76 | #[allow(dead_code)] 77 | config: Value, 78 | } 79 | 80 | #[derive(Deserialize, Default, Debug)] 81 | #[serde(deny_unknown_fields)] 82 | struct ContextConfig { 83 | #[serde(default)] 84 | serialize: bool, 85 | #[cfg(feature = "modbus")] 86 | #[serde(default)] 87 | modbus: Option, 88 | #[serde(default)] 89 | fields: IndexMap, 90 | } 91 | 92 | #[cfg(feature = "modbus")] 93 | #[derive(Deserialize, Default, Debug)] 94 | #[serde(deny_unknown_fields)] 95 | pub(crate) struct ModbusConfig { 96 | #[serde(default)] 97 | pub(crate) c: usize, 98 | #[serde(default)] 99 | pub(crate) d: usize, 100 | #[serde(default)] 101 | pub(crate) i: usize, 102 | #[serde(default)] 103 | pub(crate) h: usize, 104 | } 105 | 106 | #[cfg(feature = "modbus")] 107 | impl ModbusConfig { 108 | pub(crate) fn as_const_generics(&self) -> String { 109 | format!("{}, {}, {}, {}", self.c, self.d, self.i, self.h) 110 | } 111 | } 112 | 113 | #[derive(Deserialize, Debug)] 114 | #[serde(deny_unknown_fields)] 115 | struct Io { 116 | id: String, 117 | kind: Kind, 118 | #[allow(dead_code)] 119 | #[serde(default)] 120 | config: Value, 121 | #[allow(dead_code)] 122 | #[serde(default)] 123 | input: Vec, 124 | #[allow(dead_code)] 125 | #[serde(default)] 126 | output: Vec, 127 | } 128 | 129 | #[derive(Deserialize, Debug)] 130 | #[serde(untagged)] 131 | enum ContextField { 132 | Map(IndexMap), 133 | Type(String), 134 | } 135 | 136 | impl Config { 137 | pub fn load>(path: P, context: &tera::Context) -> Result> { 138 | let config_tpl = fs::read_to_string(path)?; 139 | let config: Config = 140 | serde_yaml::from_str(&tera::Tera::default().render_str(&config_tpl, context)?)?; 141 | if config.version != 1 { 142 | unimplemented!("config version {} is not supported", config.version); 143 | } 144 | Ok(config) 145 | } 146 | #[allow(unreachable_code)] 147 | pub fn generate_io>(&self, path: P) -> Result<(), Box> { 148 | let mut m = codegen::Scope::new(); 149 | m.raw(crate::builder::AUTO_GENERATED); 150 | m.raw("#[allow(unused_imports)]"); 151 | m.raw("use crate::plc::context::{Context, CONTEXT};"); 152 | #[allow(unused_mut)] 153 | let mut funcs: Vec = Vec::new(); 154 | //let mut output_required: bool = false; 155 | for i in &self.io { 156 | match i.kind { 157 | #[cfg(feature = "modbus")] 158 | Kind::Modbus => { 159 | m.raw( 160 | crate::io::modbus::generate_io(&i.id, &i.config, &i.input, &i.output)? 161 | .to_string(), 162 | ); 163 | } 164 | #[cfg(feature = "opcua")] 165 | Kind::OpcUa => { 166 | m.raw( 167 | crate::io::opcua::generate_io(&i.id, &i.config, &i.input, &i.output)? 168 | .to_string(), 169 | ); 170 | } 171 | #[cfg(feature = "eva")] 172 | Kind::Eapi => { 173 | m.raw( 174 | crate::io::eapi::generate_io(&i.id, &i.config, &i.input, &i.output)? 175 | .to_string(), 176 | ); 177 | } 178 | } 179 | funcs.push(format!("launch_datasync_{}", i.id.to_lowercase())); 180 | } 181 | let f_launch_datasync = m.new_fn("launch_datasync").vis("pub"); 182 | for function in funcs { 183 | f_launch_datasync.line(format!("{}();", function)); 184 | } 185 | #[allow(unused_variables)] 186 | for (i, serv) in self.server.iter().enumerate() { 187 | match serv.kind { 188 | #[cfg(feature = "modbus")] 189 | crate::server::Kind::Modbus => { 190 | f_launch_datasync.push_block(crate::server::modbus::generate_server_launcher( 191 | i + 1, 192 | &serv.config, 193 | self.context 194 | .modbus 195 | .as_ref() 196 | .expect("modbus not specified in PLC context"), 197 | )?); 198 | } 199 | } 200 | } 201 | let f_stop_datasync = m.new_fn("stop_datasync").vis("pub"); 202 | f_stop_datasync.line("::rplc::tasks::stop_if_no_output_or_sfn();"); 203 | super::write(path, m.to_string())?; 204 | Ok(()) 205 | } 206 | pub fn generate_context>(&self, path: P) -> Result<(), Box> { 207 | let mut b = path.as_ref().to_path_buf(); 208 | b.pop(); 209 | b.pop(); 210 | let mut bm = b.clone(); 211 | b.push("plc_types.rs"); 212 | bm.push("plc_types"); 213 | bm.push("mod.rs"); 214 | let mut m = codegen::Scope::new(); 215 | m.raw(crate::builder::AUTO_GENERATED); 216 | if b.exists() || bm.exists() { 217 | m.raw("#[allow(clippy::wildcard_imports)]"); 218 | m.raw("use crate::plc_types::*;"); 219 | } 220 | m.import("::rplc::export::parking_lot", "RwLock"); 221 | m.import("::rplc::export::once_cell::sync", "Lazy"); 222 | if self.context.serialize { 223 | m.import("::rplc::export::serde", "Serialize"); 224 | m.import("::rplc::export::serde", "Deserialize"); 225 | m.import("::rplc::export::serde", "self"); 226 | } 227 | m.raw("#[allow(dead_code)] pub(crate) static CONTEXT: Lazy> = Lazy::new(<_>::default);"); 228 | generate_structs( 229 | "Context", 230 | &self.context.fields, 231 | &mut m, 232 | #[cfg(feature = "modbus")] 233 | self.context.modbus.as_ref(), 234 | self.context.serialize, 235 | )?; 236 | super::write(path, m.to_string())?; 237 | Ok(()) 238 | } 239 | } 240 | 241 | fn parse_iec_type(tp: &str) -> &str { 242 | match tp { 243 | "BOOL" => "bool", 244 | "BYTE" | "USINT" => "u8", 245 | "WORD" | "UINT" => "u16", 246 | "DWORD" | "UDINT" => "u32", 247 | "LWORD" | "ULINT" => "u64", 248 | "SINT" => "i8", 249 | "INT" => "i16", 250 | "DINT" => "i32", 251 | "LINT" => "i64", 252 | "REAL" => "f32", 253 | "LREAL" => "f64", 254 | _ => tp, 255 | } 256 | } 257 | 258 | fn base_val(tp: &str) -> Option<&str> { 259 | match tp { 260 | "bool" => Some("false"), 261 | "u8" | "u16" | "u32" | "u64" | "usize" | "i8" | "i16" | "i32" | "i64" | "isize" => { 262 | Some("0") 263 | } 264 | "f32" | "f64" => Some("0.0"), 265 | _ => None, 266 | } 267 | } 268 | 269 | fn parse_type(t: &str) -> String { 270 | let tp = t.trim(); 271 | if tp.ends_with(']') && !tp.starts_with('[') { 272 | let mut sp = tp.split('['); 273 | let mut result = String::new(); 274 | let base_tp = parse_iec_type(sp.next().unwrap()); 275 | for d in sp { 276 | let mut size_s = d[0..d.len() - 1].trim().replace('_', ""); 277 | let boxed = if size_s.ends_with('!') { 278 | size_s = size_s[..size_s.len() - 1].to_owned(); 279 | true 280 | } else { 281 | false 282 | }; 283 | let size = size_s 284 | .parse::() 285 | .unwrap_or_else(|e| panic!("{e}: {size_s}")); 286 | if result.is_empty() { 287 | if boxed { 288 | write!(result, "Box<[{}; {}]>", base_tp, size).unwrap(); 289 | } else { 290 | write!(result, "[{}; {}]", base_tp, size).unwrap(); 291 | } 292 | } else if boxed { 293 | result = format!("Box<[{}; {}]>", result, size); 294 | } else { 295 | result = format!("[{}; {}]", result, size); 296 | } 297 | } 298 | result 299 | } else { 300 | parse_iec_type(tp).to_owned() 301 | } 302 | } 303 | 304 | fn generate_default(t: &str) -> String { 305 | let tp = t.trim(); 306 | if tp.ends_with(']') && !tp.starts_with('[') { 307 | let mut sp = tp.split('['); 308 | let mut result = String::new(); 309 | let base_tp = parse_iec_type(sp.next().unwrap()); 310 | for d in sp { 311 | let mut size_s = d[0..d.len() - 1].trim().replace('_', ""); 312 | let boxed = if size_s.ends_with('!') { 313 | size_s = size_s[..size_s.len() - 1].to_owned(); 314 | true 315 | } else { 316 | false 317 | }; 318 | let size = size_s 319 | .parse::() 320 | .unwrap_or_else(|e| panic!("{e}: {size_s}")); 321 | if result.is_empty() { 322 | if boxed { 323 | write!(result, "Box::new(").unwrap(); 324 | } 325 | if let Some(val) = base_val(base_tp) { 326 | write!(result, "[{};{}]", val, size).unwrap(); 327 | } else { 328 | write!(result, "::std::array::from_fn(|_| <_>::default())").unwrap(); 329 | } 330 | if boxed { 331 | write!(result, ")").unwrap(); 332 | } 333 | } else { 334 | let mut r = if boxed { 335 | "Box::new([".to_owned() 336 | } else { 337 | "[".to_owned() 338 | }; 339 | for _ in 0..size { 340 | write!(r, "{},", result).unwrap(); 341 | } 342 | write!(r, "]").unwrap(); 343 | if boxed { 344 | write!(r, ")").unwrap(); 345 | } 346 | result = r; 347 | } 348 | } 349 | result 350 | } else { 351 | "<_>::default()".to_owned() 352 | } 353 | } 354 | 355 | #[allow(clippy::too_many_lines)] 356 | fn generate_structs( 357 | name: &str, 358 | fields: &IndexMap, 359 | scope: &mut codegen::Scope, 360 | #[cfg(feature = "modbus")] modbus_config: Option<&ModbusConfig>, 361 | serialize: bool, 362 | ) -> Result<(), Box> { 363 | let mut st: codegen::Struct = codegen::Struct::new(name); 364 | let mut st_impl: codegen::Impl = codegen::Impl::new(name); 365 | st_impl.impl_trait("Default"); 366 | let default = st_impl.new_fn("default"); 367 | default.ret("Self"); 368 | default.line("Self {"); 369 | st.allow("dead_code") 370 | .allow("clippy::module_name_repetitions") 371 | .repr("C") 372 | .vis("pub"); 373 | if serialize { 374 | st.derive("Serialize").derive("Deserialize"); 375 | st.attr("serde(crate = \"self::serde\")"); 376 | } 377 | for (k, v) in fields { 378 | match v { 379 | ContextField::Type(t) => { 380 | let mut field = codegen::Field::new(k, parse_type(t)); 381 | field.vis("pub"); 382 | if serialize { 383 | field.annotation.push("#[serde(default)]".to_owned()); 384 | } 385 | default.line(format!("{}: {},", field.name, generate_default(t))); 386 | st.push_field(field); 387 | } 388 | ContextField::Map(m) => { 389 | let (mut field, sub_name) = if k.ends_with(']') { 390 | let (number, field_name, boxed) = if let Some(pos) = k.rfind('[') { 391 | let mut size_s = k[pos + 1..k.len() - 1].trim().replace('_', ""); 392 | let boxed = if size_s.ends_with('!') { 393 | size_s = size_s[..size_s.len() - 1].to_owned(); 394 | true 395 | } else { 396 | false 397 | }; 398 | let field = size_s.parse::().map_err(|e| { 399 | eva_common::Error::invalid_params(format!( 400 | "invalid struct name: {} ({})", 401 | k, e 402 | )) 403 | })?; 404 | (field, &k[..pos], boxed) 405 | } else { 406 | return Err(eva_common::Error::invalid_params(format!( 407 | "invalid struct name: {}", 408 | k 409 | )) 410 | .into()); 411 | }; 412 | let sub_name = format!("{}{}", name, field_name.to_title_case()); 413 | let field_value = if boxed { 414 | format!("Box<[{}; {}]>", sub_name, number) 415 | } else { 416 | format!("[{}; {}]", sub_name, number) 417 | }; 418 | (codegen::Field::new(field_name, field_value), sub_name) 419 | } else { 420 | let sub_name = format!("{}{}", name, k.to_title_case()); 421 | (codegen::Field::new(k, &sub_name), sub_name) 422 | }; 423 | field.vis("pub"); 424 | if serialize { 425 | field.annotation.push("#[serde(default)]".to_owned()); 426 | } 427 | default.line(format!("{}: {},", field.name, generate_default(k))); 428 | st.push_field(field); 429 | generate_structs( 430 | &sub_name, 431 | m, 432 | scope, 433 | #[cfg(feature = "modbus")] 434 | None, 435 | serialize, 436 | )?; 437 | } 438 | } 439 | } 440 | #[cfg(feature = "modbus")] 441 | if let Some(c) = modbus_config { 442 | let mut field = codegen::Field::new( 443 | "modbus", 444 | format!( 445 | "::rplc::export::rmodbus::server::context::ModbusContext<{}>", 446 | c.as_const_generics() 447 | ), 448 | ); 449 | field.vis("pub"); 450 | if serialize { 451 | field.annotation.push("#[serde(default)]".to_owned()); 452 | } 453 | st.push_field(field); 454 | default.line("modbus: <_>::default(),"); 455 | } 456 | default.line("}"); 457 | scope.push_struct(st); 458 | scope.raw("#[allow(clippy::derivable_impls)]"); 459 | scope.push_impl(st_impl); 460 | #[cfg(feature = "modbus")] 461 | if let Some(c) = modbus_config { 462 | let im = scope.new_impl(&format!( 463 | "::rplc::server::modbus::SlaveContext<{}> for Context", 464 | c.as_const_generics() 465 | )); 466 | { 467 | let fn_ctx = im 468 | .new_fn("modbus_context") 469 | .arg_ref_self() 470 | .ret(format!( 471 | "&::rplc::export::rmodbus::server::context::ModbusContext<{}>", 472 | c.as_const_generics() 473 | )) 474 | .attr("inline"); 475 | fn_ctx.line("&self.modbus"); 476 | } 477 | { 478 | let fn_ctx_mut = im 479 | .new_fn("modbus_context_mut") 480 | .arg_mut_self() 481 | .ret(format!( 482 | "&mut ::rplc::export::rmodbus::server::context::ModbusContext<{}>", 483 | c.as_const_generics() 484 | )) 485 | .attr("inline"); 486 | fn_ctx_mut.line("&mut self.modbus"); 487 | } 488 | } 489 | Ok(()) 490 | } 491 | -------------------------------------------------------------------------------- /src/builder/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use std::env; 3 | use std::error::Error; 4 | use std::fmt; 5 | use std::fs; 6 | use std::io::Write; 7 | use std::path::Path; 8 | use std::process::{Command, Stdio}; 9 | 10 | pub const AUTO_GENERATED: &str = "// AUTO-GENERATED BY RPLC"; 11 | 12 | pub mod config; 13 | 14 | use config::Config; 15 | 16 | pub fn generate(config_file: &str) -> Result<(), Box> { 17 | Builder::new(config_file).generate() 18 | } 19 | 20 | pub struct Builder<'a> { 21 | config_file: &'a str, 22 | context: tera::Context, 23 | } 24 | 25 | impl<'a> Builder<'a> { 26 | pub fn new(config_file: &'a str) -> Self { 27 | Self { 28 | config_file, 29 | context: tera::Context::new(), 30 | } 31 | } 32 | pub fn insert>(&mut self, variable: S, value: &T) { 33 | self.context.insert(variable, value); 34 | } 35 | pub fn generate(&self) -> Result<(), Box> { 36 | let config = Config::load(self.config_file, &self.context)?; 37 | prepare(&config)?; 38 | fs::create_dir_all("src/plc")?; 39 | config.generate_io("src/plc/io.rs")?; 40 | config.generate_context("src/plc/context.rs")?; 41 | Ok(()) 42 | } 43 | } 44 | 45 | fn prepare(config: &Config) -> Result<(), Box> { 46 | fs::create_dir_all("src/plc")?; 47 | let mut plc_mod = codegen::Scope::new(); 48 | plc_mod.raw(AUTO_GENERATED); 49 | plc_mod.raw("use ::std::time::Duration;"); 50 | plc_mod.raw("pub mod context;"); 51 | plc_mod.raw("pub mod io;"); 52 | for c in &["VERSION", "NAME", "DESCRIPTION"] { 53 | plc_mod.raw(format!( 54 | "pub const {c}: &str = \"{}\";", 55 | env::var(format!("CARGO_PKG_{c}"))? 56 | )); 57 | } 58 | plc_mod.raw(format!( 59 | "pub const STACK_SIZE: Option = {};", 60 | if let Some(ss) = config.core.stack_size { 61 | format!("Some({})", ss * 1000) 62 | } else { 63 | "None".to_owned() 64 | } 65 | )); 66 | #[allow(clippy::cast_possible_truncation)] 67 | #[allow(clippy::cast_sign_loss)] 68 | plc_mod.raw(format!( 69 | "#[allow(clippy::unreadable_literal)] pub const STOP_TIMEOUT: Duration = Duration::from_nanos({});", 70 | (config.core.stop_timeout.trunc() as u64) * 1_000_000_000 71 | )); 72 | #[cfg(feature = "eva")] 73 | plc_mod.raw(format!( 74 | "pub const EAPI_ACTION_POOL_SIZE: usize = {};", 75 | config.eapi.action_pool_size 76 | )); 77 | #[cfg(not(feature = "eva"))] 78 | plc_mod.raw("pub const EAPI_ACTION_POOL_SIZE: usize = 0;"); 79 | write("src/plc/mod.rs", plc_mod.to_string())?; 80 | Ok(()) 81 | } 82 | 83 | fn format_code(code: String) -> Result> { 84 | let mut child = Command::new("rustfmt") 85 | .arg("--edition=2021") 86 | .stdin(Stdio::piped()) 87 | .stdout(Stdio::piped()) 88 | .spawn()?; 89 | let mut stdin = child 90 | .stdin 91 | .take() 92 | .ok_or_else(|| eva_common::Error::io("unable to take stdin"))?; 93 | std::thread::spawn(move || { 94 | stdin.write_all(code.as_bytes()).unwrap(); 95 | stdin.flush().unwrap(); 96 | }); 97 | let output = child.wait_with_output()?; 98 | if !output.status.success() { 99 | return Err(eva_common::Error::failed(format!( 100 | "process exit code: {:?}", 101 | output.status.code() 102 | )) 103 | .into()); 104 | } 105 | //let mut formatted_code = String::new(); 106 | //for line in stdout { 107 | //writeln!(formatted_code, "{}", line.unwrap()).unwrap(); 108 | //} 109 | let formatted_code = std::str::from_utf8(&output.stdout)?; 110 | Ok(formatted_code.to_owned()) 111 | } 112 | 113 | fn write(target: T, data: D) -> Result<(), Box> 114 | where 115 | T: AsRef, 116 | D: fmt::Display, 117 | { 118 | let mut code = data.to_string(); 119 | match format_code(code.clone()) { 120 | Ok(c) => code = c, 121 | Err(e) => println!("cargo:warning=unable to format code with rustfmt: {}", e), 122 | } 123 | if let Ok(x) = fs::read_to_string(&target) { 124 | if code == x { 125 | return Ok(()); 126 | } 127 | } 128 | fs::write(target, code)?; 129 | Ok(()) 130 | } 131 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use colored::Colorize; 3 | use eva_common::EResult; 4 | use prettytable::Row; 5 | use rplc::tasks::{Affinity, Status}; 6 | use rplc::{client, eapi}; 7 | use std::collections::BTreeMap; 8 | use std::path::Path; 9 | 10 | #[macro_use] 11 | extern crate prettytable; 12 | 13 | #[derive(Parser)] 14 | struct Args { 15 | #[clap(long = "color")] 16 | color: Option, 17 | #[clap(subcommand)] 18 | command: Command, 19 | } 20 | 21 | #[derive(clap::ValueEnum, Copy, Clone)] 22 | enum Color { 23 | Always, 24 | Never, 25 | } 26 | 27 | impl Color { 28 | fn ovrride(self) { 29 | colored::control::set_override(match self { 30 | Color::Always => true, 31 | Color::Never => false, 32 | }); 33 | } 34 | } 35 | 36 | #[derive(Parser)] 37 | enum Command { 38 | #[clap(about = "list PLCs")] 39 | List(ListParams), 40 | #[clap(about = "Test PLC")] 41 | Test(PlcParams), 42 | #[clap(about = "PLC info")] 43 | Info(PlcParams), 44 | #[clap(about = "PLC task (thread) stats")] 45 | Stat(PlcParams), 46 | #[clap(about = "reset PLC task (thread) stats")] 47 | Reset(PlcParams), 48 | #[clap(about = "register PLC binary in systemd")] 49 | Register(PlcRegisterParams), 50 | #[clap(about = "unregister PLC binary from systemd (stop if running)")] 51 | Unregister(PlcParams), 52 | #[clap(about = "start PLC with systemd")] 53 | Start(PlcParams), 54 | #[clap(about = "stop PLC with systemd")] 55 | Stop(PlcParams), 56 | #[clap(about = "restart PLC with systemd")] 57 | Restart(PlcParams), 58 | #[clap(about = "PLC systemd status")] 59 | Status(PlcParams), 60 | } 61 | 62 | #[derive(Parser)] 63 | struct PlcRegisterParams { 64 | plc_file_path: String, 65 | #[clap(short = 'a', help = "thread affinity: NAME=CPU,PRIORITY")] 66 | thread_affinity: Vec, 67 | #[clap( 68 | short = 'e', 69 | long = "eapi", 70 | help = "EVA ICS bus connection: path[,timeout=Xs][,buf_size=X][,queue_size=X][,buf_ttl=Xms]" 71 | )] 72 | eapi: Option, 73 | #[clap(long = "var", help = "Custom environment variable: name=value")] 74 | vars: Vec, 75 | #[clap(long = "force")] 76 | force: bool, 77 | #[clap(short = 's', long = "start", help = "start PLC after registration")] 78 | start: bool, 79 | } 80 | 81 | trait StatusColored { 82 | fn as_colored_string(&self) -> colored::ColoredString; 83 | } 84 | 85 | impl StatusColored for Status { 86 | fn as_colored_string(&self) -> colored::ColoredString { 87 | if *self == Status::Active { 88 | self.to_string().green() 89 | } else if *self <= Status::Stopping { 90 | self.to_string().yellow() 91 | } else { 92 | self.to_string().normal() 93 | } 94 | } 95 | } 96 | 97 | #[derive(Parser)] 98 | struct ListParams { 99 | #[clap(short = 'y', long = "full")] 100 | full: bool, 101 | } 102 | 103 | #[derive(Parser)] 104 | struct PlcParams { 105 | name: String, 106 | } 107 | 108 | fn ctable(titles: &[&str]) -> prettytable::Table { 109 | let mut table = prettytable::Table::new(); 110 | let format = prettytable::format::FormatBuilder::new() 111 | .column_separator(' ') 112 | .borders(' ') 113 | .separators( 114 | &[prettytable::format::LinePosition::Title], 115 | prettytable::format::LineSeparator::new('-', '-', '-', '-'), 116 | ) 117 | .padding(0, 1) 118 | .build(); 119 | table.set_format(format); 120 | let mut titlevec: Vec = Vec::new(); 121 | for t in titles { 122 | titlevec.push(cell!(t.blue())); 123 | } 124 | table.set_titles(prettytable::Row::new(titlevec)); 125 | table 126 | } 127 | 128 | async fn handle_list(p: ListParams, var_dir: &Path) -> EResult<()> { 129 | if p.full { 130 | let mut table = ctable(&[ 131 | "name", 132 | "description", 133 | "version", 134 | "systemd", 135 | "status", 136 | "pid", 137 | "uptime", 138 | ]); 139 | for r in client::list_extended(var_dir).await? { 140 | let systemd = r 141 | .sds 142 | .and_then(|v| v.status) 143 | .map_or_else(String::new, |v| v.to_string()); 144 | let plc_info = r.plc_info; 145 | let status = if plc_info.status == -1000 { 146 | "API_ERROR".red() 147 | } else { 148 | Status::from(plc_info.status).as_colored_string() 149 | }; 150 | table.add_row(row![ 151 | plc_info.name, 152 | plc_info.description, 153 | plc_info.version, 154 | systemd, 155 | status, 156 | plc_info.pid, 157 | plc_info.uptime.trunc() 158 | ]); 159 | } 160 | table.printstd(); 161 | } else { 162 | for name in client::list(var_dir).await? { 163 | println!("{}", name); 164 | } 165 | } 166 | Ok(()) 167 | } 168 | 169 | async fn handle_stat(p: PlcParams, var_dir: &Path) -> EResult<()> { 170 | let tasks = client::stat_extended(&p.name, var_dir).await?; 171 | let mut table = ctable(&[ 172 | "task", "spid", "cpu", "rt", "iters", "jmin", "jmax", "jlast", "javg", 173 | ]); 174 | for task in tasks { 175 | let mut cols = vec![ 176 | cell!(task.name), 177 | cell!(task.spid.to_string().green()), 178 | cell!(task.cpu_id.to_string().bold()), 179 | cell!(if task.rt_priority > 0 { 180 | task.rt_priority.to_string().cyan() 181 | } else { 182 | String::new().normal() 183 | }), 184 | ]; 185 | if let Some(t) = task.thread_info { 186 | let cols_t = vec![ 187 | cell!(t.iters), 188 | cell!(t.jitter_min), 189 | cell!(if t.jitter_max < 150 { 190 | t.jitter_max.to_string().normal() 191 | } else if t.jitter_max < 250 { 192 | t.jitter_max.to_string().yellow() 193 | } else { 194 | t.jitter_max.to_string().red() 195 | }), 196 | cell!(t.jitter_last), 197 | cell!(t.jitter_avg), 198 | ]; 199 | cols.extend(cols_t); 200 | } 201 | table.add_row(Row::new(cols)); 202 | } 203 | table.printstd(); 204 | Ok(()) 205 | } 206 | 207 | async fn handle_info(p: PlcParams, var_dir: &Path) -> EResult<()> { 208 | let result = client::info(&p.name, var_dir).await?; 209 | let mut table = ctable(&["variable", "value"]); 210 | table.add_row(row!["name", result.name]); 211 | table.add_row(row!["description", result.description]); 212 | table.add_row(row!["version", result.version]); 213 | table.add_row(row![ 214 | "status", 215 | Status::from(result.status).as_colored_string() 216 | ]); 217 | table.add_row(row!["pid", result.pid]); 218 | table.add_row(row!["system_name", result.system_name]); 219 | table.add_row(row!["uptime", result.uptime.trunc()]); 220 | table.printstd(); 221 | Ok(()) 222 | } 223 | 224 | async fn handle_start(name: &str) -> EResult<()> { 225 | client::start(name).await?; 226 | println!("{} has been started", name); 227 | Ok(()) 228 | } 229 | 230 | #[tokio::main(flavor = "current_thread")] 231 | async fn main() -> EResult<()> { 232 | eva_common::self_test(); 233 | let args = Args::parse(); 234 | let var_dir = rplc::var_dir(); 235 | if let Some(color) = args.color { 236 | color.ovrride(); 237 | } 238 | match args.command { 239 | Command::List(p) => { 240 | handle_list(p, &var_dir).await?; 241 | } 242 | Command::Test(p) => { 243 | client::test(&p.name, &var_dir).await?; 244 | println!("OK"); 245 | } 246 | Command::Info(p) => { 247 | handle_info(p, &var_dir).await?; 248 | } 249 | Command::Stat(p) => { 250 | handle_stat(p, &var_dir).await?; 251 | } 252 | Command::Reset(p) => { 253 | client::reset_stat(&p.name, &var_dir).await?; 254 | println!("{} stats have been reset", p.name); 255 | } 256 | Command::Register(p) => { 257 | let aff: BTreeMap = p 258 | .thread_affinity 259 | .into_iter() 260 | .map(|a| { 261 | let mut sp = a.split('='); 262 | let name = sp.next().unwrap().to_owned(); 263 | let a: Affinity = sp 264 | .next() 265 | .ok_or_else(|| panic!("no affinity specified")) 266 | .unwrap() 267 | .parse() 268 | .unwrap(); 269 | (name, a) 270 | }) 271 | .collect(); 272 | let eapi_params: Option = p.eapi.map(|s| s.parse().unwrap()); 273 | let (name, svc_name) = client::register( 274 | Path::new(&p.plc_file_path), 275 | &var_dir, 276 | p.force, 277 | &aff, 278 | eapi_params.as_ref(), 279 | &p.vars, 280 | ) 281 | .await?; 282 | println!( 283 | "{} has been registered in systemd as: {} ({})", 284 | p.plc_file_path, name, svc_name 285 | ); 286 | if p.force { 287 | handle_start(&name).await?; 288 | } 289 | } 290 | Command::Unregister(p) => { 291 | client::unregister(&p.name).await?; 292 | println!("{} has been unregistered from systemd", p.name); 293 | } 294 | Command::Start(p) => { 295 | handle_start(&p.name).await?; 296 | } 297 | Command::Stop(p) => { 298 | client::stop(&p.name).await?; 299 | println!("{} has been stopped", p.name); 300 | } 301 | Command::Restart(p) => { 302 | client::restart(&p.name).await?; 303 | println!("{} has been restarted", p.name); 304 | } 305 | Command::Status(p) => { 306 | println!("{}", client::status(&p.name).await?); 307 | } 308 | } 309 | Ok(()) 310 | } 311 | -------------------------------------------------------------------------------- /src/client/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::tasks::{Affinity, ThreadInfo}; 2 | use crate::{api, eapi, PlcInfo}; 3 | use bmart_derive::{EnumStr, Sorting}; 4 | use eva_common::payload::{pack, unpack}; 5 | use eva_common::prelude::Value; 6 | use eva_common::{EResult, Error}; 7 | use serde::de::DeserializeOwned; 8 | use serde::{Deserialize, Serialize}; 9 | use std::collections::{BTreeMap, BTreeSet}; 10 | use std::path::{Path, PathBuf}; 11 | use std::time::Duration; 12 | use tokio::fs; 13 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 14 | use tokio::net::UnixStream; 15 | 16 | const TIMEOUT: Duration = Duration::from_secs(2); 17 | const SYSTEMCTL_TIMEOUT: Duration = Duration::from_secs(2); 18 | const START_STOP_TIMEOUT: Duration = Duration::from_secs(60); 19 | const PLC_SYSTEMD_PREFIX: &str = "rplc."; 20 | const SYSTEMCTL: &str = "/usr/bin/systemctl"; 21 | const SYSTEMD_DIR: &str = "/etc/systemd/system"; 22 | 23 | #[derive(Default, Debug, Sorting)] 24 | #[sorting(id = "spid")] 25 | struct ProcInfo { 26 | name: String, 27 | spid: i32, 28 | cpu_id: u8, 29 | rt_priority: i8, 30 | } 31 | 32 | async fn proc_info(procfs_path: &Path) -> Result> { 33 | let mut stat_path = procfs_path.to_owned(); 34 | stat_path.push("stat"); 35 | let stat = fs::read_to_string(stat_path).await?; 36 | let vals: Vec<&str> = stat.split(' ').collect(); 37 | let name = vals.get(1).ok_or_else(|| Error::invalid_data("COL2"))?; 38 | if name.len() < 2 { 39 | return Err(Error::invalid_data("COL2").into()); 40 | } 41 | Ok(ProcInfo { 42 | name: name[1..name.len() - 1].to_owned(), 43 | spid: procfs_path 44 | .file_name() 45 | .unwrap_or_default() 46 | .to_string_lossy() 47 | .parse()?, 48 | cpu_id: vals 49 | .get(38) 50 | .ok_or_else(|| Error::invalid_data("COL39"))? 51 | .parse()?, 52 | rt_priority: vals 53 | .get(39) 54 | .ok_or_else(|| Error::invalid_data("COL40"))? 55 | .parse()?, 56 | }) 57 | } 58 | 59 | #[inline] 60 | async fn get_proc_info(procfs_path: &Path) -> ProcInfo { 61 | proc_info(procfs_path).await.unwrap_or_else(|e| ProcInfo { 62 | name: format!("PROCFS ERR: {}", e), 63 | ..ProcInfo::default() 64 | }) 65 | } 66 | 67 | #[derive(Serialize, Deserialize, Sorting)] 68 | #[sorting(id = "name")] 69 | pub struct ThreadInfoExtended { 70 | pub name: String, 71 | pub spid: i32, 72 | pub cpu_id: u8, 73 | pub rt_priority: i8, 74 | pub thread_info: Option, 75 | } 76 | 77 | pub fn plc_socket_path(var_dir: &Path, name: &str) -> EResult { 78 | let mut path = var_dir.to_owned(); 79 | path.push(format!("{}.plcsock", name)); 80 | if path.exists() { 81 | Ok(path) 82 | } else { 83 | Err(Error::not_found("no API socket, is PLC process running?")) 84 | } 85 | } 86 | 87 | pub async fn stat_extended(name: &str, var_dir: &Path) -> EResult> { 88 | let socket_path = plc_socket_path(var_dir, name)?; 89 | let info: PlcInfo = api_call(&socket_path, "info", None).await?; 90 | let mut thread_info_map: BTreeMap> = 91 | api_call(&socket_path, "thread_stats.get", None).await?; 92 | let mut paths = fs::read_dir(format!("/proc/{}/task", info.pid)).await?; 93 | let mut tasks = Vec::new(); 94 | while let Some(path) = paths.next_entry().await? { 95 | if path.file_type().await?.is_dir() { 96 | let p = get_proc_info(&path.path()).await; 97 | let thread_info = thread_info_map.remove(&p.name); 98 | tasks.push(ThreadInfoExtended { 99 | name: p.name, 100 | spid: p.spid, 101 | cpu_id: p.cpu_id, 102 | rt_priority: p.rt_priority, 103 | thread_info: thread_info.flatten(), 104 | }); 105 | } 106 | } 107 | tasks.sort(); 108 | Ok(tasks) 109 | } 110 | 111 | pub async fn info(name: &str, var_dir: &Path) -> EResult { 112 | let socket_path = plc_socket_path(var_dir, name)?; 113 | let result: PlcInfo = api_call(&socket_path, "info", None).await?; 114 | Ok(result) 115 | } 116 | 117 | pub async fn reset_stat(name: &str, var_dir: &Path) -> EResult<()> { 118 | let socket_path = plc_socket_path(var_dir, name)?; 119 | api_call::<()>(&socket_path, "thread_stats.reset", None).await?; 120 | Ok(()) 121 | } 122 | 123 | pub async fn test(name: &str, var_dir: &Path) -> EResult<()> { 124 | let socket_path = plc_socket_path(var_dir, name)?; 125 | api_call::<()>(&socket_path, "test", None).await?; 126 | Ok(()) 127 | } 128 | 129 | pub async fn list(var_dir: &Path) -> EResult> { 130 | let mut plcs = BTreeSet::::new(); 131 | let mut paths = fs::read_dir(var_dir).await?; 132 | while let Some(path) = paths.next_entry().await? { 133 | let p = path.path(); 134 | if let Some(ext) = p.extension() { 135 | if ext == "plcsock" { 136 | if let Some(name) = p.file_stem() { 137 | plcs.insert(name.to_string_lossy().to_string()); 138 | } 139 | } 140 | } 141 | } 142 | for (name, _) in systemd_units().await.map_err(Error::failed)? { 143 | plcs.insert(name); 144 | } 145 | let mut result: Vec = plcs.into_iter().collect(); 146 | result.sort(); 147 | Ok(result) 148 | } 149 | 150 | #[derive(Serialize, Deserialize)] 151 | pub struct PlcInfoExtended { 152 | pub plc_info: PlcInfo, 153 | pub sds: Option, 154 | } 155 | 156 | /// # Panics 157 | /// 158 | /// should not panic 159 | pub async fn list_extended(var_dir: &Path) -> EResult> { 160 | let mut infos = BTreeMap::new(); 161 | let mut paths = fs::read_dir(var_dir).await?; 162 | while let Some(path) = paths.next_entry().await? { 163 | let p = path.path(); 164 | if let Some(ext) = p.extension() { 165 | if ext == "plcsock" { 166 | if let Some(name) = p.file_stem() { 167 | let name = name.to_string_lossy().to_string(); 168 | let plc_info = info(&name, var_dir).await.unwrap_or_else(|_| PlcInfo { 169 | name: name.clone(), 170 | status: -1000, 171 | ..PlcInfo::default() 172 | }); 173 | infos.insert( 174 | name, 175 | PlcInfoExtended { 176 | plc_info, 177 | sds: None, 178 | }, 179 | ); 180 | } 181 | } 182 | } 183 | } 184 | for (name, sds) in systemd_units().await.map_err(Error::failed)? { 185 | if let Some(info) = infos.get_mut(&name) { 186 | info.sds = Some(sds); 187 | } else { 188 | infos.insert( 189 | name.clone(), 190 | PlcInfoExtended { 191 | plc_info: PlcInfo { 192 | name, 193 | ..PlcInfo::default() 194 | }, 195 | sds: Some(sds), 196 | }, 197 | ); 198 | } 199 | } 200 | let mut result: Vec = infos.into_values().collect(); 201 | result.sort_by(|a, b| a.plc_info.name.partial_cmp(&b.plc_info.name).unwrap()); 202 | Ok(result) 203 | } 204 | 205 | async fn api_call(socket_path: &Path, method: &str, params: Option) -> EResult 206 | where 207 | R: DeserializeOwned, 208 | { 209 | tokio::time::timeout(TIMEOUT, _api_call(socket_path, method, params)) 210 | .await 211 | .map_err(|_| Error::timeout())? 212 | } 213 | 214 | async fn _api_call(socket_path: &Path, method: &str, params: Option) -> EResult 215 | where 216 | R: DeserializeOwned, 217 | { 218 | let req = api::Request::new(method, params); 219 | let mut socket = UnixStream::connect(socket_path).await?; 220 | let packed = pack(&req)?; 221 | let mut buf = Vec::with_capacity(packed.len() + 5); 222 | buf.push(0); 223 | buf.extend(u32::try_from(packed.len())?.to_le_bytes()); 224 | buf.extend(packed); 225 | socket.write_all(&buf).await?; 226 | let mut buf: [u8; 5] = [0; 5]; 227 | socket.read_exact(&mut buf).await?; 228 | if buf[0] != 0 { 229 | return Err(Error::invalid_data("invalid header")); 230 | } 231 | let mut buf = vec![0; usize::try_from(u32::from_le_bytes(buf[1..].try_into()?))?]; 232 | socket.read_exact(&mut buf).await?; 233 | let response: api::Response = unpack(&buf)?; 234 | response.check()?; 235 | Ok(R::deserialize(response.result.unwrap_or_default())?) 236 | } 237 | 238 | fn systemd_plc_and_service_name_from_path(plc: &Path) -> EResult<(String, String)> { 239 | let name = plc 240 | .file_name() 241 | .ok_or_else(|| Error::invalid_params("the file has no name"))? 242 | .to_string_lossy(); 243 | Ok((name.to_string(), systemd_service_name(&name))) 244 | } 245 | 246 | #[inline] 247 | fn systemd_service_name(name: &str) -> String { 248 | format!("{}{}.service", PLC_SYSTEMD_PREFIX, name) 249 | } 250 | 251 | const SYSTEMD_UNIT_CONFIG: &str = r#"[Unit] 252 | Description= 253 | After=network.target 254 | StartLimitIntervalSec=0 255 | 256 | [Service] 257 | Type=simple 258 | Restart=always 259 | RestartSec=1 260 | {% for entry in env -%} 261 | Environment="{{ entry }}" 262 | {% endfor -%} 263 | ExecStart={{ bin_path }} 264 | 265 | [Install] 266 | WantedBy=multi-user.target 267 | "#; 268 | 269 | // TODO thread affinity 270 | // TODO eapi path and settings 271 | pub async fn register( 272 | plc: &Path, 273 | var_dir: &Path, 274 | force: bool, 275 | affinities: &BTreeMap, 276 | eapi_params: Option<&eapi::Params>, 277 | vars: &[String], 278 | ) -> EResult<(String, String)> { 279 | if !plc.is_file() { 280 | return Err(Error::invalid_params(format!( 281 | "not a file: {}", 282 | plc.to_string_lossy() 283 | ))); 284 | } 285 | let mut su_path = Path::new(SYSTEMD_DIR).to_owned(); 286 | let (plc_name, su_name) = systemd_plc_and_service_name_from_path(plc)?; 287 | su_path.push(&su_name); 288 | if su_path.exists() { 289 | if force { 290 | let _ = stop(&plc_name).await; 291 | let _ = unregister(&plc_name).await; 292 | } else { 293 | return Err(Error::busy("PLC is already registered")); 294 | } 295 | } 296 | let mut ctx = tera::Context::new(); 297 | let mut env = vec![ 298 | "SYSLOG=1".to_owned(), 299 | format!("PLC_VAR_DIR={}", var_dir.to_string_lossy()), 300 | ]; 301 | if let Some(params) = eapi_params { 302 | env.push(format!("PLC_EAPI={}", params)); 303 | } 304 | for var in vars { 305 | env.push(var.clone()); 306 | } 307 | for (task_name, aff) in affinities { 308 | env.push(format!( 309 | "PLC_THREAD_AFFINITY_{}={},{}", 310 | task_name.replace('.', "__"), 311 | aff.cpu_id, 312 | aff.sched_priority 313 | )); 314 | } 315 | ctx.insert("env", &env); 316 | ctx.insert("bin_path", &plc.canonicalize()?); 317 | let mut tera = tera::Tera::default(); 318 | let unit_config = tera 319 | .render_str(SYSTEMD_UNIT_CONFIG, &ctx) 320 | .map_err(Error::failed)?; 321 | fs::write(&su_path, unit_config).await?; 322 | if let Err(e) = systemd_svc_action(&su_name, SystemdAction::Enable).await { 323 | let _ = fs::remove_file(su_path).await; 324 | Err(e) 325 | } else { 326 | Ok((plc_name, su_name)) 327 | } 328 | } 329 | 330 | fn systemd_service_checked(name: &str) -> EResult<(PathBuf, String)> { 331 | let su_name = systemd_service_name(name); 332 | let mut su_path = Path::new(SYSTEMD_DIR).to_owned(); 333 | su_path.push(&su_name); 334 | if !su_path.exists() { 335 | return Err(Error::not_found("PLC is not registered")); 336 | } 337 | Ok((su_path, su_name)) 338 | } 339 | 340 | pub async fn unregister(name: &str) -> EResult<()> { 341 | let (su_path, su_name) = systemd_service_checked(name)?; 342 | let _ = stop(name).await; 343 | systemd_svc_action(&su_name, SystemdAction::Disable).await?; 344 | fs::remove_file(su_path).await?; 345 | Ok(()) 346 | } 347 | 348 | pub async fn start(name: &str) -> EResult<()> { 349 | let (_, su_name) = systemd_service_checked(name)?; 350 | systemd_svc_action(&su_name, SystemdAction::Start).await?; 351 | Ok(()) 352 | } 353 | 354 | pub async fn stop(name: &str) -> EResult<()> { 355 | let (_, su_name) = systemd_service_checked(name)?; 356 | systemd_svc_action(&su_name, SystemdAction::Stop).await?; 357 | Ok(()) 358 | } 359 | 360 | pub async fn restart(name: &str) -> EResult<()> { 361 | let (_, su_name) = systemd_service_checked(name)?; 362 | systemd_svc_action(&su_name, SystemdAction::Restart).await?; 363 | Ok(()) 364 | } 365 | 366 | /// # Panics 367 | /// 368 | /// Should not panic 369 | pub async fn status(name: &str) -> EResult { 370 | let (_, su_name) = systemd_service_checked(name)?; 371 | let status_str = systemd_svc_action(&su_name, SystemdAction::Status) 372 | .await? 373 | .unwrap(); 374 | Ok(status_str) 375 | } 376 | 377 | #[derive(EnumStr, Copy, Clone, Eq, PartialEq)] 378 | #[enumstr(rename_all = "lowercase")] 379 | enum SystemdAction { 380 | Enable, 381 | Disable, 382 | Start, 383 | Stop, 384 | Restart, 385 | Status, 386 | } 387 | 388 | impl SystemdAction { 389 | fn timeout(self) -> Duration { 390 | match self { 391 | SystemdAction::Enable | SystemdAction::Disable | SystemdAction::Status => { 392 | SYSTEMCTL_TIMEOUT 393 | } 394 | SystemdAction::Start | SystemdAction::Stop => START_STOP_TIMEOUT, 395 | SystemdAction::Restart => START_STOP_TIMEOUT * 2, 396 | } 397 | } 398 | } 399 | 400 | async fn systemd_svc_action(su_name: &str, action: SystemdAction) -> EResult> { 401 | let result = bmart::process::command( 402 | SYSTEMCTL, 403 | &[action.to_string(), su_name.to_owned()], 404 | action.timeout(), 405 | bmart::process::Options::default(), 406 | ) 407 | .await?; 408 | let mut code = result.code.unwrap_or(-1); 409 | if action == SystemdAction::Status && code == 3 { 410 | code = 0; 411 | } 412 | if code != 0 { 413 | return Err(Error::failed("systemctl exited with an error")); 414 | } 415 | if action == SystemdAction::Status { 416 | Ok(Some(result.out.join("\n"))) 417 | } else { 418 | Ok(None) 419 | } 420 | } 421 | 422 | #[derive(Debug, Serialize, Deserialize)] 423 | pub struct SystemdUnitStats { 424 | pub status: Option, 425 | } 426 | 427 | #[derive(EnumStr, Debug, Serialize, Deserialize)] 428 | #[serde(rename_all = "lowercase")] 429 | #[enumstr(rename_all = "lowercase")] 430 | pub enum SystemdStatus { 431 | Active, 432 | Activating, 433 | Deactivating, 434 | Inactive, 435 | } 436 | 437 | async fn systemd_units() -> EResult> { 438 | let mut units = BTreeMap::new(); 439 | let result = bmart::process::command( 440 | SYSTEMCTL, 441 | &["-a"], 442 | SYSTEMCTL_TIMEOUT, 443 | bmart::process::Options::default(), 444 | ) 445 | .await?; 446 | if result.code.unwrap_or(-1) != 0 { 447 | return Err(Error::failed("systemctl exited with an error")); 448 | } 449 | for line in result.out.into_iter().skip(1) { 450 | if line.is_empty() { 451 | break; 452 | } 453 | let l = if line.starts_with('●') { 454 | &line[4..] 455 | } else { 456 | &line 457 | }; 458 | let vals: Vec<&str> = l.split_ascii_whitespace().take(4).collect(); 459 | if vals.len() == 4 { 460 | if let Some(plc_name) = vals[0].strip_prefix(PLC_SYSTEMD_PREFIX) { 461 | if let Some(plc_name) = plc_name.strip_suffix(".service") { 462 | units.insert( 463 | plc_name.to_owned(), 464 | SystemdUnitStats { 465 | status: vals[2].parse().ok(), 466 | }, 467 | ); 468 | } 469 | } 470 | } 471 | } 472 | Ok(units) 473 | } 474 | -------------------------------------------------------------------------------- /src/comm/mod.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::MutexGuard; 2 | use std::sync::Arc; 3 | 4 | #[cfg(feature = "serial")] 5 | pub mod serial; 6 | pub mod tcp; 7 | 8 | pub type Communicator = Arc; 9 | 10 | pub trait Comm { 11 | fn lock(&self) -> MutexGuard<()>; 12 | fn reconnect(&self); 13 | fn write(&self, buf: &[u8]) -> Result<(), std::io::Error>; 14 | fn read_exact(&self, buf: &mut [u8]) -> Result<(), std::io::Error>; 15 | } 16 | -------------------------------------------------------------------------------- /src/comm/serial.rs: -------------------------------------------------------------------------------- 1 | use super::Comm; 2 | use parking_lot::{Mutex, MutexGuard}; 3 | use serial::prelude::*; 4 | use serial::SystemPort; 5 | use std::error::Error; 6 | use std::io::{Read, Write}; 7 | use std::sync::Arc; 8 | use std::time::{Duration, Instant}; 9 | 10 | fn parse_path( 11 | path: &str, 12 | ) -> ( 13 | &str, 14 | serial::BaudRate, 15 | serial::CharSize, 16 | serial::Parity, 17 | serial::StopBits, 18 | ) { 19 | let mut sp = path.split(':'); 20 | let port_dev = sp.next().unwrap(); 21 | let s_baud_rate = sp 22 | .next() 23 | .unwrap_or_else(|| panic!("serial baud rate not specified: {}", path)); 24 | let s_char_size = sp 25 | .next() 26 | .unwrap_or_else(|| panic!("serial char size not specified: {}", path)); 27 | let s_parity = sp 28 | .next() 29 | .unwrap_or_else(|| panic!("serial parity not specified: {}", path)); 30 | let s_stop_bits = sp 31 | .next() 32 | .unwrap_or_else(|| panic!("serial stopbits not specified: {}", path)); 33 | let baud_rate = match s_baud_rate { 34 | "110" => serial::Baud110, 35 | "300" => serial::Baud300, 36 | "600" => serial::Baud600, 37 | "1200" => serial::Baud1200, 38 | "2400" => serial::Baud2400, 39 | "4800" => serial::Baud4800, 40 | "9600" => serial::Baud9600, 41 | "19200" => serial::Baud19200, 42 | "38400" => serial::Baud38400, 43 | "57600" => serial::Baud57600, 44 | "115200" => serial::Baud115200, 45 | v => panic!("specified serial baud rate not supported: {}", v), 46 | }; 47 | let char_size = match s_char_size { 48 | "5" => serial::Bits5, 49 | "6" => serial::Bits6, 50 | "7" => serial::Bits7, 51 | "8" => serial::Bits8, 52 | v => panic!("specified serial char size not supported: {}", v), 53 | }; 54 | let parity = match s_parity { 55 | "N" => serial::ParityNone, 56 | "E" => serial::ParityEven, 57 | "O" => serial::ParityOdd, 58 | v => panic!("specified serial parity not supported: {}", v), 59 | }; 60 | let stop_bits = match s_stop_bits { 61 | "1" => serial::Stop1, 62 | "2" => serial::Stop2, 63 | v => unimplemented!("specified serial stop bits not supported: {}", v), 64 | }; 65 | (port_dev, baud_rate, char_size, parity, stop_bits) 66 | } 67 | 68 | /// # Panics 69 | /// 70 | /// Will panic on misconfigured listen string 71 | pub fn check_path(path: &str) { 72 | let _ = parse_path(path); 73 | } 74 | 75 | /// # Panics 76 | /// 77 | /// Will panic on misconfigured listen string 78 | pub fn open(listen: &str, timeout: Duration) -> Result { 79 | let (port_dev, baud_rate, char_size, parity, stop_bits) = parse_path(listen); 80 | let mut port = serial::open(&port_dev)?; 81 | port.reconfigure(&|settings| { 82 | (settings.set_baud_rate(baud_rate).unwrap()); 83 | settings.set_char_size(char_size); 84 | settings.set_parity(parity); 85 | settings.set_stop_bits(stop_bits); 86 | settings.set_flow_control(serial::FlowNone); 87 | Ok(()) 88 | })?; 89 | port.set_timeout(timeout)?; 90 | Ok(port) 91 | } 92 | 93 | #[allow(clippy::module_name_repetitions)] 94 | pub struct SerialComm { 95 | path: String, 96 | port: Mutex, 97 | timeout: Duration, 98 | frame_delay: Duration, 99 | busy: Mutex<()>, 100 | } 101 | 102 | #[derive(Default)] 103 | struct SPort { 104 | system_port: Option, 105 | last_frame: Option, 106 | } 107 | 108 | #[allow(clippy::module_name_repetitions)] 109 | pub type SerialCommunicator = Arc; 110 | 111 | impl Comm for SerialComm { 112 | fn lock(&self) -> MutexGuard<()> { 113 | self.busy.lock() 114 | } 115 | fn reconnect(&self) { 116 | let mut port = self.port.lock(); 117 | port.system_port.take(); 118 | port.last_frame.take(); 119 | } 120 | fn write(&self, buf: &[u8]) -> Result<(), std::io::Error> { 121 | let mut port = self.get_port()?; 122 | if let Some(last_frame) = port.last_frame { 123 | let el = last_frame.elapsed(); 124 | if el < self.frame_delay { 125 | std::thread::sleep(self.frame_delay - el); 126 | } 127 | } 128 | let result = port 129 | .system_port 130 | .as_mut() 131 | .unwrap() 132 | .write_all(buf) 133 | .map_err(|e| { 134 | self.reconnect(); 135 | e 136 | }); 137 | if result.is_ok() { 138 | port.last_frame.replace(Instant::now()); 139 | } 140 | result 141 | } 142 | fn read_exact(&self, buf: &mut [u8]) -> Result<(), std::io::Error> { 143 | let mut port = self.get_port()?; 144 | port.system_port 145 | .as_mut() 146 | .unwrap() 147 | .read_exact(buf) 148 | .map_err(|e| { 149 | self.reconnect(); 150 | e 151 | }) 152 | } 153 | } 154 | 155 | impl SerialComm { 156 | /// # Panics 157 | /// 158 | /// Will panic on misconfigured path string 159 | pub fn create( 160 | path: &str, 161 | timeout: Duration, 162 | frame_delay: Duration, 163 | ) -> Result> { 164 | check_path(path); 165 | Ok(Self { 166 | path: path.to_owned(), 167 | port: <_>::default(), 168 | timeout, 169 | frame_delay, 170 | busy: <_>::default(), 171 | }) 172 | } 173 | fn get_port(&self) -> Result, std::io::Error> { 174 | let mut lock = self.port.lock(); 175 | if lock.system_port.as_mut().is_none() { 176 | let port = open(&self.path, self.timeout)?; 177 | lock.system_port.replace(port); 178 | lock.last_frame.take(); 179 | } 180 | Ok(lock) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/comm/tcp.rs: -------------------------------------------------------------------------------- 1 | use super::Comm; 2 | use parking_lot::{Mutex, MutexGuard}; 3 | use std::error::Error; 4 | use std::io::{Read, Write}; 5 | use std::net::SocketAddr; 6 | use std::net::TcpStream; 7 | use std::sync::Arc; 8 | use std::time::Duration; 9 | 10 | #[allow(clippy::module_name_repetitions)] 11 | pub struct TcpComm { 12 | addr: SocketAddr, 13 | stream: Mutex>, 14 | timeout: Duration, 15 | busy: Mutex<()>, 16 | } 17 | 18 | #[allow(clippy::module_name_repetitions)] 19 | pub type TcpCommunicator = Arc; 20 | 21 | macro_rules! handle_tcp_stream_error { 22 | ($stream: expr, $err: expr, $any: expr) => {{ 23 | if $any || $err.kind() == std::io::ErrorKind::TimedOut { 24 | $stream.take(); 25 | } 26 | $err 27 | }}; 28 | } 29 | 30 | impl Comm for TcpComm { 31 | fn lock(&self) -> MutexGuard<()> { 32 | self.busy.lock() 33 | } 34 | fn reconnect(&self) { 35 | self.stream.lock().take(); 36 | } 37 | fn write(&self, buf: &[u8]) -> Result<(), std::io::Error> { 38 | let mut stream = self.get_stream()?; 39 | stream 40 | .as_mut() 41 | .unwrap() 42 | .write_all(buf) 43 | .map_err(|e| handle_tcp_stream_error!(stream, e, true)) 44 | } 45 | fn read_exact(&self, buf: &mut [u8]) -> Result<(), std::io::Error> { 46 | let mut stream = self.get_stream()?; 47 | stream 48 | .as_mut() 49 | .unwrap() 50 | .read_exact(buf) 51 | .map_err(|e| handle_tcp_stream_error!(stream, e, false)) 52 | } 53 | } 54 | 55 | impl TcpComm { 56 | pub fn create(path: &str, timeout: Duration) -> Result> { 57 | Ok(Self { 58 | addr: path.parse()?, 59 | stream: <_>::default(), 60 | busy: <_>::default(), 61 | timeout, 62 | }) 63 | } 64 | fn get_stream(&self) -> Result>, std::io::Error> { 65 | let mut lock = self.stream.lock(); 66 | if lock.as_mut().is_none() { 67 | let stream = TcpStream::connect_timeout(&self.addr, self.timeout)?; 68 | stream.set_read_timeout(Some(self.timeout))?; 69 | stream.set_write_timeout(Some(self.timeout))?; 70 | stream.set_nodelay(true)?; 71 | lock.replace(stream); 72 | } 73 | Ok(lock) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/eapi.rs: -------------------------------------------------------------------------------- 1 | use crate::tasks; 2 | use busrt::async_trait; 3 | use busrt::ipc::{Client, Config}; 4 | use busrt::rpc::{Rpc, RpcClient, RpcError, RpcEvent, RpcHandlers, RpcResult}; 5 | use busrt::{Frame, QoS}; 6 | use eva_common::events::RAW_STATE_TOPIC; 7 | use eva_common::payload::{pack, unpack}; 8 | use eva_common::value::Value; 9 | use eva_common::{EResult, Error, OID}; 10 | use eva_sdk::controller::{format_action_topic, Action, RawStateEventPreparedOwned}; 11 | use log::{error, info, warn}; 12 | use once_cell::sync::Lazy; 13 | use parking_lot::Mutex; 14 | use std::collections::{BTreeMap, HashMap}; 15 | use std::env; 16 | use std::fmt; 17 | use std::str::FromStr; 18 | use std::sync::atomic; 19 | use std::sync::Arc; 20 | use std::time::Duration; 21 | 22 | eva_common::err_logger!(); 23 | 24 | #[derive(Debug)] 25 | pub struct Params { 26 | pub path: String, 27 | pub timeout: Option, 28 | pub buf_size: Option, 29 | pub queue_size: Option, 30 | pub buf_ttl: Option, 31 | } 32 | 33 | impl FromStr for Params { 34 | type Err = Error; 35 | fn from_str(s: &str) -> Result { 36 | let mut sp = s.split(','); 37 | let path = sp.next().unwrap().to_owned(); 38 | let mut timeout = None; 39 | let mut buf_size = None; 40 | let mut queue_size = None; 41 | let mut buf_ttl = None; 42 | for param in sp { 43 | let mut sp_param = param.split('='); 44 | let key = sp_param.next().unwrap(); 45 | let value = sp_param 46 | .next() 47 | .ok_or_else(|| Error::invalid_params("no value"))?; 48 | match key { 49 | "timeout" => timeout = Some(Duration::from_secs_f64(value.parse()?)), 50 | "buf_size" => buf_size = Some(value.parse()?), 51 | "queue_size" => queue_size = Some(value.parse()?), 52 | "buf_ttl" => buf_ttl = Some(Duration::from_micros(value.parse()?)), 53 | v => return Err(Error::invalid_params(format!("unsupported option: {v}"))), 54 | } 55 | } 56 | Ok(Self { 57 | path, 58 | timeout, 59 | buf_size, 60 | queue_size, 61 | buf_ttl, 62 | }) 63 | } 64 | } 65 | 66 | impl fmt::Display for Params { 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | write!(f, "{}", self.path)?; 69 | if let Some(timeout) = self.timeout { 70 | write!(f, ",timeout={}", timeout.as_secs_f64())?; 71 | } 72 | if let Some(buf_size) = self.buf_size { 73 | write!(f, ",buf_size={}", buf_size)?; 74 | } 75 | if let Some(queue_size) = self.queue_size { 76 | write!(f, ",queue_size={}", queue_size)?; 77 | } 78 | if let Some(buf_ttl) = self.buf_ttl { 79 | write!(f, ",buf_ttl={}", buf_ttl.as_micros())?; 80 | } 81 | Ok(()) 82 | } 83 | } 84 | 85 | enum PublishPayload { 86 | Single(String, Vec), 87 | Bulk(Vec<(String, Vec)>), 88 | } 89 | 90 | const RECONNECT_DELAY: Duration = Duration::from_secs(5); 91 | const EVENT_CHANNEL_SIZE: usize = 1_000; 92 | 93 | const ERR_NOT_REGISTERED: &str = "BUS/RT EAPI not registered"; 94 | 95 | pub type ActionHandlerFn = Box EResult<()> + Send + Sync + 'static>; 96 | 97 | static PUBLISHER_TX: Lazy>>> = 98 | Lazy::new(<_>::default); 99 | static ACTION_HANDLERS: Lazy>> = Lazy::new(<_>::default); 100 | static CONNECTED: atomic::AtomicBool = atomic::AtomicBool::new(false); 101 | 102 | /// # Panics 103 | /// 104 | /// Will panic if action handler is already registered 105 | pub fn append_action_handler(oid: OID, f: F) 106 | where 107 | F: Fn(&mut Action) -> EResult<()> + Send + Sync + 'static, 108 | { 109 | let oid_c = oid.clone(); 110 | assert!( 111 | ACTION_HANDLERS.lock().insert(oid, Box::new(f)).is_none(), 112 | "Action handler for {} is already registered", 113 | oid_c 114 | ); 115 | } 116 | 117 | /// # Panics 118 | /// 119 | /// Will panic if action handler is already registered 120 | pub fn append_action_handlers_bulk(oids: &[OID], f: Vec) { 121 | let mut action_handlers = ACTION_HANDLERS.lock(); 122 | for (oid, f) in oids.iter().zip(f) { 123 | assert!( 124 | action_handlers.insert(oid.clone(), f).is_none(), 125 | "Action handler for {} is already registered", 126 | oid 127 | ); 128 | } 129 | } 130 | 131 | struct Handlers {} 132 | 133 | #[async_trait] 134 | impl RpcHandlers for Handlers { 135 | async fn handle_call(&self, event: RpcEvent) -> RpcResult { 136 | // keep all methods minimalistic 137 | let payload = event.payload(); 138 | match event.parse_method()? { 139 | "test" => { 140 | if payload.is_empty() { 141 | Ok(None) 142 | } else { 143 | Err(RpcError::params(None)) 144 | } 145 | } 146 | "info" => { 147 | if payload.is_empty() { 148 | Ok(Some(pack(&crate::plc_info())?)) 149 | } else { 150 | Err(RpcError::params(None)) 151 | } 152 | } 153 | "thread_stats.get" => { 154 | if payload.is_empty() { 155 | let mut result = BTreeMap::new(); 156 | let thread_stats = &tasks::controller_stats().lock().thread_stats; 157 | for (name, st) in thread_stats { 158 | result.insert(name, st.info()); 159 | } 160 | Ok(Some(pack(&result)?)) 161 | } else { 162 | Err(RpcError::params(None)) 163 | } 164 | } 165 | "thread_stats.reset" => { 166 | if payload.is_empty() { 167 | tasks::reset_thread_stats(); 168 | Ok(None) 169 | } else { 170 | Err(RpcError::params(None)) 171 | } 172 | } 173 | "action" => { 174 | if payload.is_empty() { 175 | return Err(RpcError::params(None)); 176 | } 177 | let mut action: Action = unpack(payload)?; 178 | tokio::task::spawn_blocking(move || { 179 | let topic = format_action_topic(action.oid()); 180 | let payload = if let Err(e) = handle_action(&mut action, &topic) { 181 | action.event_failed(1, None, Some(Value::String(e.to_string()))) 182 | } else { 183 | action.event_completed(None) 184 | }; 185 | match pack(&payload) { 186 | Ok(packed) => { 187 | if let Some(tx) = PUBLISHER_TX.lock().as_ref() { 188 | tx.send_blocking(PublishPayload::Single(topic, packed)) 189 | .log_ef(); 190 | } else { 191 | warn!("action response orphaned, BUS/RT EAPI not registered"); 192 | } 193 | } 194 | Err(e) => error!("action payload pack failed: {}", e), 195 | } 196 | }) 197 | .await 198 | .map_err(Error::failed)?; 199 | Ok(None) 200 | } 201 | _ => Err(RpcError::method(None)), 202 | } 203 | } 204 | async fn handle_notification(&self, _event: RpcEvent) {} 205 | async fn handle_frame(&self, _frame: Frame) {} 206 | } 207 | 208 | fn handle_action(action: &mut Action, action_topic: &str) -> EResult<()> { 209 | let oid = action.oid().clone(); 210 | if let Some(handler) = ACTION_HANDLERS.lock().get(&oid) { 211 | if let Some(tx) = PUBLISHER_TX.lock().as_ref() { 212 | let packed = pack(&action.event_running())?; 213 | tx.send_blocking(PublishPayload::Single(action_topic.to_owned(), packed)) 214 | .map_err(Error::failed)?; 215 | } else { 216 | return Err(Error::failed(ERR_NOT_REGISTERED)); 217 | } 218 | handler(action) 219 | } else { 220 | Err(Error::not_found(format!( 221 | "BUS/RT EAPI action handler for {} not registered", 222 | oid 223 | ))) 224 | } 225 | } 226 | 227 | pub(crate) fn launch(action_pool_size: usize) { 228 | info!("preparing BUS/RT EAPI connection"); 229 | if let Ok(eapi_s) = env::var("PLC_EAPI") { 230 | match eapi_s.parse::() { 231 | Ok(eapi_params) => { 232 | let timeout = eapi_params.timeout.unwrap_or(busrt::DEFAULT_TIMEOUT); 233 | info!("eapi.path = {}", eapi_params.path); 234 | info!("eapi.timeout = {:?}", eapi_params.timeout); 235 | let rt = tokio::runtime::Builder::new_current_thread() 236 | .enable_all() 237 | .max_blocking_threads(action_pool_size) 238 | .thread_name("Sbusrt.scada") 239 | .build() 240 | .unwrap(); 241 | tasks::spawn_service("busrt.scada", move || { 242 | rt.block_on(bus(&eapi_params)); 243 | }); 244 | let op = eva_common::op::Op::new(timeout); 245 | loop { 246 | tasks::step_sleep(); 247 | if CONNECTED.load(atomic::Ordering::Relaxed) || op.is_timed_out() { 248 | break; 249 | } 250 | } 251 | } 252 | Err(e) => error!("unable to parse PLC_EAPI: {e}"), 253 | }; 254 | } else { 255 | warn!("no PLC_EAPI specified, aborting EAPI connection"); 256 | } 257 | } 258 | 259 | async fn bus(params: &Params) { 260 | let name = format!( 261 | "fieldbus.{}.plc.{}", 262 | crate::HOSTNAME.get().unwrap(), 263 | crate::NAME.get().unwrap() 264 | ); 265 | let mut config = Config::new(¶ms.path, &name); 266 | if let Some(timeout) = params.timeout { 267 | config = config.timeout(timeout); 268 | } 269 | if let Some(buf_size) = params.buf_size { 270 | info!("eapi.buf_size = {buf_size}"); 271 | config = config.buf_size(buf_size); 272 | } 273 | if let Some(queue_size) = params.queue_size { 274 | info!("eapi.queue_size = {queue_size}"); 275 | config = config.queue_size(queue_size); 276 | } 277 | if let Some(buf_ttl) = params.buf_ttl { 278 | info!("eapi.buf_ttl = {:?}", buf_ttl); 279 | config = config.buf_ttl(buf_ttl); 280 | } 281 | loop { 282 | if let Err(e) = run(&config).await { 283 | error!("BUS/RT EAPI error: {}", e); 284 | tokio::time::sleep(RECONNECT_DELAY).await; 285 | } 286 | } 287 | } 288 | 289 | async fn run(config: &Config) -> EResult<()> { 290 | let client = Client::connect(config).await?; 291 | let rpc = Arc::new(RpcClient::new(client, Handlers {})); 292 | info!("BUS/RT EAPI connected"); 293 | let (tx, rx) = async_channel::bounded::(EVENT_CHANNEL_SIZE); 294 | let rpc_c = rpc.clone(); 295 | let publisher_worker = tokio::spawn(async move { 296 | while let Ok(payload) = rx.recv().await { 297 | let cl = rpc_c.client(); 298 | let mut client = cl.lock().await; 299 | match payload { 300 | PublishPayload::Single(topic, value) => { 301 | client.publish(&topic, value.into(), QoS::No).await.log_ef(); 302 | } 303 | PublishPayload::Bulk(values) => { 304 | for (topic, value) in values { 305 | if client 306 | .publish(&topic, value.into(), QoS::No) 307 | .await 308 | .log_err() 309 | .is_err() 310 | { 311 | break; 312 | } 313 | } 314 | } 315 | } 316 | } 317 | }); 318 | PUBLISHER_TX.lock().replace(tx.clone()); 319 | CONNECTED.store(true, atomic::Ordering::Relaxed); 320 | while rpc.client().lock().await.is_connected() { 321 | tokio::time::sleep(crate::tasks::SLEEP_STEP).await; 322 | } 323 | publisher_worker.abort(); 324 | PUBLISHER_TX.lock().take(); 325 | warn!("BUS/RT EAPI disconnected"); 326 | tokio::time::sleep(RECONNECT_DELAY).await; 327 | Ok(()) 328 | } 329 | 330 | pub fn notify( 331 | map: HashMap<&OID, RawStateEventPreparedOwned, S>, 332 | ) -> EResult<()> { 333 | if map.is_empty() { 334 | return Ok(()); 335 | } 336 | let mut data = Vec::with_capacity(map.len()); 337 | for (oid, event) in map { 338 | data.push(( 339 | format!("{}{}", RAW_STATE_TOPIC, oid.as_path()), 340 | pack(event.state())?, 341 | )); 342 | } 343 | if let Some(tx) = PUBLISHER_TX.lock().as_ref() { 344 | tx.send_blocking(PublishPayload::Bulk(data)) 345 | .map_err(Error::failed)?; 346 | Ok(()) 347 | } else { 348 | Err(Error::failed(ERR_NOT_REGISTERED)) 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/interval.rs: -------------------------------------------------------------------------------- 1 | use crate::tasks::{self, ConvX}; 2 | use eva_common::EResult; 3 | use log::warn; 4 | use serde::{Deserialize, Deserializer}; 5 | use std::cmp::Ordering; 6 | use std::time::Duration; 7 | use std::time::Instant; 8 | 9 | pub struct Loop { 10 | next_iter: Instant, 11 | interval: Duration, 12 | int_micros: i64, 13 | t: Instant, 14 | report: bool, 15 | // only Input or Program, used to mark ready 16 | task_kind: Option, 17 | marked: bool, 18 | } 19 | 20 | #[negative_impl::negative_impl] 21 | impl !Send for Loop {} 22 | 23 | impl Loop { 24 | pub fn prepare0(interval: Duration) -> Self { 25 | Self::prepare(interval, Duration::default(), false) 26 | } 27 | pub fn prepare_reported(interval: Duration, shift: Duration) -> Self { 28 | Self::prepare(interval, shift, true) 29 | } 30 | /// For input, program and output threads waits until the thread can pass 31 | /// # Panics 32 | /// 33 | /// will panic if interval in us > i64::MAX 34 | pub fn prepare(interval: Duration, shift: Duration, report: bool) -> Self { 35 | let task_kind: Option = if let Some(ch) = tasks::thread_name().chars().next() { 36 | match ch { 37 | 'I' => { 38 | tasks::wait_can_run_input(); 39 | Some(tasks::Kind::Input) 40 | } 41 | 'P' => { 42 | tasks::wait_can_run_program(); 43 | Some(tasks::Kind::Program) 44 | } 45 | 'O' => { 46 | tasks::wait_can_run_output(); 47 | None 48 | } 49 | _ => None, 50 | } 51 | } else { 52 | None 53 | }; 54 | tasks::sleep(shift); 55 | let now = Instant::now(); 56 | Loop { 57 | next_iter: now + interval, 58 | interval, 59 | int_micros: i64::try_from(interval.as_micros()).unwrap(), 60 | t: now, 61 | report, 62 | task_kind, 63 | marked: task_kind.is_none(), 64 | } 65 | } 66 | 67 | pub fn tick(&mut self) -> bool { 68 | if !self.marked { 69 | if let Some(kind) = self.task_kind { 70 | tasks::mark_thread_ready(kind); 71 | } 72 | self.marked = true; 73 | } 74 | let t = Instant::now(); 75 | let result = match t.cmp(&self.next_iter) { 76 | Ordering::Greater => false, 77 | Ordering::Equal => true, 78 | Ordering::Less => { 79 | tasks::sleep(self.next_iter - t); 80 | true 81 | } 82 | }; 83 | if result { 84 | self.next_iter += self.interval; 85 | } else { 86 | self.next_iter = Instant::now() + self.interval; 87 | warn!( 88 | "{} loop timeout ({:?} + {:?})", 89 | tasks::thread_name(), 90 | self.interval, 91 | self.next_iter.elapsed() 92 | ); 93 | } 94 | if self.report { 95 | let t = Instant::now(); 96 | #[allow(clippy::cast_possible_truncation)] 97 | let jitter = (self.int_micros - (t.duration_since(self.t)).as_micros() as i64) 98 | .unsigned_abs() 99 | .as_u16_max(); 100 | tasks::report_jitter(jitter); 101 | self.t = t; 102 | }; 103 | result 104 | } 105 | } 106 | 107 | pub(crate) fn parse_interval(s: &str) -> EResult { 108 | if let Some(v) = s.strip_suffix("ms") { 109 | Ok(v.parse::()? * 1_000_000) 110 | } else if let Some(v) = s.strip_suffix("us") { 111 | Ok(v.parse::()? * 1_000) 112 | } else if let Some(v) = s.strip_suffix("ns") { 113 | Ok(v.parse::()?) 114 | } else if let Some(v) = s.strip_suffix('s') { 115 | Ok(v.parse::()? * 1_000_000_000) 116 | } else { 117 | Ok(s.parse::()? * 1_000_000_000) 118 | } 119 | } 120 | 121 | #[allow(dead_code)] 122 | #[inline] 123 | pub(crate) fn deserialize_interval_as_nanos<'de, D>(deserializer: D) -> Result 124 | where 125 | D: Deserializer<'de>, 126 | { 127 | let buf = String::deserialize(deserializer)?; 128 | parse_interval(&buf).map_err(serde::de::Error::custom) 129 | } 130 | 131 | #[allow(dead_code)] 132 | #[inline] 133 | pub(crate) fn deserialize_opt_interval_as_nanos<'de, D>( 134 | deserializer: D, 135 | ) -> Result, D::Error> 136 | where 137 | D: Deserializer<'de>, 138 | { 139 | let buf: Option = Option::deserialize(deserializer)?; 140 | if let Some(b) = buf { 141 | Ok(Some(parse_interval(&b).map_err(serde::de::Error::custom)?)) 142 | } else { 143 | Ok(None) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/io/eapi/mod.rs: -------------------------------------------------------------------------------- 1 | use eva_common::value::Value; 2 | use eva_common::OID; 3 | use serde::Deserialize; 4 | use std::error::Error; 5 | 6 | fn default_cache() -> u64 { 7 | 0 8 | } 9 | 10 | #[derive(Deserialize)] 11 | #[serde(deny_unknown_fields)] 12 | struct OutputConfig { 13 | oid_map: Vec, 14 | #[serde(deserialize_with = "crate::interval::deserialize_interval_as_nanos")] 15 | sync: u64, 16 | #[serde( 17 | default, 18 | deserialize_with = "crate::interval::deserialize_opt_interval_as_nanos" 19 | )] 20 | shift: Option, 21 | #[serde( 22 | default = "default_cache", 23 | deserialize_with = "crate::interval::deserialize_interval_as_nanos" 24 | )] 25 | cache: u64, 26 | } 27 | 28 | #[derive(Deserialize)] 29 | #[serde(deny_unknown_fields)] 30 | struct InputConfig { 31 | action_map: Vec, 32 | } 33 | 34 | #[derive(Deserialize)] 35 | #[serde(deny_unknown_fields)] 36 | struct OidMap { 37 | oid: OID, 38 | value: String, 39 | } 40 | 41 | fn generate_output(id: &str, i: usize, scope: &mut codegen::Scope, output_config: OutputConfig) { 42 | let mut output_fn = codegen::Function::new(&format!("output_{}_{}", id, i + 1)); 43 | output_fn 44 | .arg( 45 | "oids", 46 | "&[::std::sync::Arc<::rplc::export::eva_common::OID>]", 47 | ) 48 | .arg( 49 | "cache", 50 | "&mut ::rplc::export::eva_sdk::controller::RawStateCache", 51 | ); 52 | let mut block = codegen::Block::new(&format!( 53 | "if let Err(e) = output_{}_{}_worker(oids, cache)", 54 | id, 55 | i + 1 56 | )); 57 | block.line("::rplc::export::log::error!(\"{}: {}\", ::rplc::tasks::thread_name(), e);"); 58 | output_fn.push_block(block); 59 | let mut worker_fn = codegen::Function::new(&format!("output_{}_{}_worker", id, i + 1)); 60 | worker_fn 61 | .arg( 62 | "oids", 63 | "&[::std::sync::Arc<::rplc::export::eva_common::OID>]", 64 | ) 65 | .arg( 66 | "cache", 67 | "&mut ::rplc::export::eva_sdk::controller::RawStateCache", 68 | ); 69 | worker_fn.ret("Result<(), Box>"); 70 | worker_fn.line("use ::rplc::export::eva_common::events::RawStateEventOwned;"); 71 | worker_fn.line("use ::rplc::export::eva_common::OID;"); 72 | worker_fn.line("use ::rplc::export::eva_common::value::{to_value, ValueOptionOwned};"); 73 | worker_fn.line("use ::rplc::export::eva_sdk::controller::RawStateEventPreparedOwned;"); 74 | worker_fn.line("let mut result: ::std::collections::HashMap<&OID, RawStateEventPreparedOwned> = <_>::default();"); 75 | let mut out_block = codegen::Block::new(""); 76 | out_block.line("let ctx = CONTEXT.read();"); 77 | for (entry_id, entry) in output_config.oid_map.into_iter().enumerate() { 78 | out_block.line(format!("// {}", entry.oid)); 79 | out_block.line(format!( 80 | "let value = ValueOptionOwned::Value(to_value(ctx.{})?);", 81 | entry.value 82 | )); 83 | out_block.line(format!("result.insert(&oids[{}],", entry_id)); 84 | out_block.line("RawStateEventPreparedOwned::from_rse_owned("); 85 | out_block.line("RawStateEventOwned { status: 1, value, force: false, },"); 86 | out_block.line("None));"); 87 | } 88 | worker_fn.push_block(out_block); 89 | let mut block = codegen::Block::new("if ::rplc::tasks::is_active()"); 90 | block.line("cache.retain_map_modified(&mut result);"); 91 | worker_fn.push_block(block); 92 | worker_fn.line("::rplc::eapi::notify(result)?;"); 93 | worker_fn.line("Ok(())"); 94 | scope.push_fn(output_fn); 95 | scope.push_fn(worker_fn); 96 | } 97 | 98 | fn generate_input(id: &str, i: usize, scope: &mut codegen::Scope, input_config: InputConfig) { 99 | for (entry_id, entry) in input_config.action_map.into_iter().enumerate() { 100 | let mut handler_fn = codegen::Function::new(format!( 101 | "handle_eapi_action_{}_{}_{}", 102 | id, 103 | i + 1, 104 | entry_id + 1 105 | )); 106 | handler_fn.arg("action", "&mut ::rplc::export::eva_sdk::controller::Action"); 107 | handler_fn.ret("::rplc::export::eva_common::EResult<()>"); 108 | handler_fn.line("let params = action.take_unit_params()?;"); 109 | handler_fn.line(format!( 110 | "CONTEXT.write().{} = params.value.try_into()?;", 111 | entry.value 112 | )); 113 | handler_fn.line("Ok(())"); 114 | scope.push_fn(handler_fn); 115 | } 116 | } 117 | 118 | pub(crate) fn generate_io( 119 | id: &str, 120 | cfg: &Value, 121 | inputs: &[Value], 122 | outputs: &[Value], 123 | ) -> Result> { 124 | let id = id.to_lowercase(); 125 | let mut scope = codegen::Scope::new(); 126 | assert_eq!(cfg, &Value::Unit, "EVA ICS I/O must have no config"); 127 | let mut launch_fn = codegen::Function::new(&format!("launch_datasync_{id}")); 128 | launch_fn.allow("clippy::redundant_clone, clippy::unreadable_literal"); 129 | launch_fn.line("use ::rplc::export::eva_common::OID;"); 130 | for (i, input) in inputs.iter().enumerate() { 131 | let input_config = InputConfig::deserialize(input.clone())?; 132 | launch_fn.line("let oids: Vec = vec!["); 133 | for entry in &input_config.action_map { 134 | launch_fn.line(format!("\"{}\".parse::().unwrap(),", entry.oid)); 135 | } 136 | launch_fn.line("];"); 137 | launch_fn.line("let handlers: Vec<::rplc::eapi::ActionHandlerFn> = vec!["); 138 | for x in 0..input_config.action_map.len() { 139 | launch_fn.line(format!( 140 | "Box::new(handle_eapi_action_{}_{}_{}),", 141 | id, 142 | i + 1, 143 | x + 1 144 | )); 145 | } 146 | launch_fn.line("];"); 147 | launch_fn.line("::rplc::eapi::append_action_handlers_bulk(&oids, handlers);"); 148 | generate_input(&id, i, &mut scope, input_config); 149 | } 150 | for (i, output) in outputs.iter().enumerate() { 151 | let output_config = OutputConfig::deserialize(output.clone())?; 152 | launch_fn.line( 153 | format!("let mut cache = ::rplc::export::eva_sdk::controller::RawStateCache::new(Some(::std::time::Duration::from_nanos({})));", output_config.cache) 154 | ); 155 | launch_fn.line("let oids: Vec<::std::sync::Arc> = vec!["); 156 | for entry in &output_config.oid_map { 157 | launch_fn.line(format!("\"{}\".parse::().unwrap().into(),", entry.oid)); 158 | } 159 | launch_fn.line("];"); 160 | let mut block = codegen::Block::new(&format!( 161 | r#"::rplc::tasks::spawn_output_loop("{}_{}", 162 | ::std::time::Duration::from_nanos({}), 163 | ::std::time::Duration::from_nanos({}), 164 | move ||"#, 165 | id, 166 | i + 1, 167 | output_config.sync, 168 | output_config.shift.unwrap_or_default() 169 | )); 170 | block.line(format!("output_{}_{}(&oids, &mut cache);", id, i + 1)); 171 | block.after(");"); 172 | launch_fn.push_block(block); 173 | generate_output(&id, i, &mut scope, output_config); 174 | } 175 | scope.push_fn(launch_fn); 176 | Ok(scope) 177 | } 178 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[cfg(feature = "eva")] 4 | pub mod eapi; 5 | #[cfg(feature = "modbus")] 6 | pub mod modbus; 7 | #[cfg(feature = "opcua")] 8 | pub mod opcua; 9 | 10 | #[derive(Deserialize, Serialize, Debug, Copy, Clone)] 11 | #[serde(rename_all = "lowercase")] 12 | pub enum Kind { 13 | #[cfg(feature = "modbus")] 14 | Modbus, 15 | #[cfg(feature = "opcua")] 16 | OpcUa, 17 | #[cfg(feature = "eva")] 18 | Eapi, 19 | } 20 | -------------------------------------------------------------------------------- /src/io/modbus/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::tasks; 2 | use eva_common::value::Value; 3 | use serde::Deserialize; 4 | use std::error::Error; 5 | use std::fmt::Write as _; 6 | use std::net::SocketAddr; 7 | pub use types::{Coils, Registers, SwapModbusEndianess}; 8 | 9 | mod regs; 10 | mod types; 11 | 12 | const DEFAULT_FRAME_DELAY: f64 = 0.1; 13 | 14 | #[derive(Deserialize)] 15 | #[serde(deny_unknown_fields)] 16 | struct InputConfig { 17 | #[serde(flatten)] 18 | reg: regs::Reg, 19 | unit: u8, 20 | #[serde(default)] 21 | map: Vec, 22 | #[serde(deserialize_with = "crate::interval::deserialize_interval_as_nanos")] 23 | sync: u64, 24 | #[serde( 25 | default, 26 | deserialize_with = "crate::interval::deserialize_opt_interval_as_nanos" 27 | )] 28 | shift: Option, 29 | } 30 | 31 | #[derive(Deserialize)] 32 | #[serde(deny_unknown_fields)] 33 | struct OutputConfig { 34 | #[serde(flatten)] 35 | reg: regs::Reg, 36 | unit: u8, 37 | #[serde(default)] 38 | map: Vec, 39 | #[serde(deserialize_with = "crate::interval::deserialize_interval_as_nanos")] 40 | sync: u64, 41 | #[serde( 42 | default, 43 | deserialize_with = "crate::interval::deserialize_opt_interval_as_nanos" 44 | )] 45 | shift: Option, 46 | } 47 | 48 | #[derive(Deserialize)] 49 | #[serde(deny_unknown_fields)] 50 | struct RegMapInput { 51 | #[serde(default, flatten)] 52 | offset: regs::MapOffset, 53 | target: String, 54 | } 55 | 56 | #[derive(Deserialize)] 57 | #[serde(deny_unknown_fields)] 58 | struct RegMapOutput { 59 | #[serde(default, flatten)] 60 | offset: regs::MapOffset, 61 | source: String, 62 | } 63 | 64 | fn default_timeout() -> f64 { 65 | 1.0 66 | } 67 | 68 | fn default_frame_delay() -> f64 { 69 | DEFAULT_FRAME_DELAY 70 | } 71 | 72 | #[derive(Deserialize, Copy, Clone, Debug)] 73 | #[serde(rename_all = "lowercase")] 74 | enum Proto { 75 | Tcp, 76 | Udp, 77 | Rtu, 78 | Ascii, 79 | } 80 | 81 | impl Proto { 82 | fn as_rmodbus_proto_str(self) -> &'static str { 83 | match self { 84 | Proto::Ascii => "Ascii", 85 | Proto::Tcp | Proto::Udp => "TcpUdp", 86 | Proto::Rtu => "Rtu", 87 | } 88 | } 89 | fn generate_datasync( 90 | self, 91 | path: &str, 92 | timeout: f64, 93 | frame_delay: f64, 94 | ) -> Result> { 95 | let comm = match self { 96 | Proto::Tcp => { 97 | path.parse::()?; 98 | format!( 99 | r#"::rplc::comm::tcp::TcpComm::create("{}", ::std::time::Duration::from_secs_f64({:.6})).unwrap()"#, 100 | path, timeout 101 | ) 102 | } 103 | Proto::Rtu => { 104 | crate::comm::serial::check_path(path); 105 | let mut res = format!(r#"::rplc::comm::serial::SerialComm::create("{}", "#, path); 106 | write!(res, "::std::time::Duration::from_secs_f64({:.6}),", timeout)?; 107 | write!( 108 | res, 109 | "::std::time::Duration::from_secs_f64({:.6}),", 110 | frame_delay 111 | )?; 112 | write!(res, ").unwrap()")?; 113 | res 114 | } 115 | _ => unimplemented!(), 116 | }; 117 | Ok(comm) 118 | } 119 | } 120 | 121 | fn push_launcher( 122 | kind: tasks::Kind, 123 | sync: u64, 124 | shift: u64, 125 | num: usize, 126 | id: &str, 127 | f: &mut codegen::Function, 128 | ) { 129 | f.line("let comm_c = comm.clone();"); 130 | let mut spawn_block = codegen::Block::new(&format!( 131 | r#"::rplc::tasks::spawn_{}_loop("{}_{}", 132 | ::std::time::Duration::from_nanos({}), 133 | ::std::time::Duration::from_nanos({}), 134 | move ||"#, 135 | kind, id, num, sync, shift 136 | )); 137 | spawn_block.line(&format!("{kind}_{id}_{num}(&comm_c);")); 138 | spawn_block.after(");"); 139 | f.push_block(spawn_block); 140 | } 141 | 142 | #[derive(Deserialize)] 143 | struct Config { 144 | path: String, 145 | proto: Proto, 146 | #[serde(default = "default_timeout")] 147 | timeout: f64, 148 | #[serde(default = "default_frame_delay")] 149 | frame_delay: f64, 150 | } 151 | 152 | fn push_input_worker( 153 | num: usize, 154 | id: &str, 155 | config: InputConfig, 156 | proto: Proto, 157 | scope: &mut codegen::Scope, 158 | ) { 159 | let f_input = scope.new_fn(&format!("input_{id}_{num}")); 160 | f_input.arg("comm", "&::rplc::comm::Communicator"); 161 | let mut loop_block = 162 | codegen::Block::new(&format!("if let Err(e) = input_{id}_{num}_worker(comm)")); 163 | loop_block.line("::rplc::export::log::error!(\"{}: {}\", ::rplc::tasks::thread_name(), e);"); 164 | f_input.push_block(loop_block); 165 | let f_input_worker = scope.new_fn(&format!("input_{id}_{num}_worker")); 166 | f_input_worker.arg("comm", "&::rplc::comm::Communicator"); 167 | f_input_worker.ret("Result<(), Box>"); 168 | f_input_worker 169 | .line("use ::rplc::export::rmodbus::{self, client::ModbusRequest, guess_response_frame_len, ModbusProto};"); 170 | f_input_worker.line(format!( 171 | "use ::rplc::io::modbus::{};", 172 | config.reg.kind().as_helper_type_str() 173 | )); 174 | let mut req_block = codegen::Block::new("let (mreq, response) ="); 175 | req_block.after(";"); 176 | req_block.line(format!( 177 | "let mut mreq = ModbusRequest::new({}, ModbusProto::{});", 178 | config.unit, 179 | proto.as_rmodbus_proto_str() 180 | )); 181 | req_block.line("let mut request = Vec::new();"); 182 | req_block.line(format!( 183 | "mreq.generate_get_{}s({}, {}, &mut request)?;", 184 | config.reg.kind(), 185 | config.reg.offset(), 186 | config.reg.number() 187 | )); 188 | req_block.line("let _lock = comm.lock();"); 189 | req_block.line("comm.write(&request)?;"); 190 | req_block.line("let mut buf = [0u8; 6];"); 191 | req_block.line("comm.read_exact(&mut buf)?;"); 192 | req_block.line("let mut response = buf.to_vec();"); 193 | req_block.line(format!( 194 | "let len = guess_response_frame_len(&buf, ModbusProto::{})?;", 195 | proto.as_rmodbus_proto_str() 196 | )); 197 | let mut lf_block = codegen::Block::new("if len > 6"); 198 | lf_block.line("let mut rest = vec![0u8; (len - 6) as usize];"); 199 | lf_block.line("comm.read_exact(&mut rest)?;"); 200 | lf_block.line("response.extend(rest);"); 201 | req_block.push_block(lf_block); 202 | req_block.line("(mreq, response)"); 203 | f_input_worker.push_block(req_block); 204 | f_input_worker.line("let mut data = Vec::new();"); 205 | match config.reg.kind() { 206 | regs::Kind::Coil | regs::Kind::Discrete => { 207 | let mut r_block = 208 | codegen::Block::new("if let Err(e) = mreq.parse_bool(&response, &mut data)"); 209 | let mut r_unknown = codegen::Block::new("if e == rmodbus::ErrorKind::UnknownError"); 210 | r_unknown.line("comm.reconnect();"); 211 | r_block.push_block(r_unknown); 212 | r_block.line("return Err(e.into());"); 213 | f_input_worker.push_block(r_block); 214 | f_input_worker.line("let regs = Coils(data);"); 215 | } 216 | regs::Kind::Input | regs::Kind::Holding => { 217 | let mut r_block = 218 | codegen::Block::new("if let Err(e) = mreq.parse_u16(&response, &mut data)"); 219 | let mut r_unknown = codegen::Block::new("if e == rmodbus::ErrorKind::UnknownError"); 220 | r_unknown.line("comm.reconnect();"); 221 | r_block.push_block(r_unknown); 222 | r_block.line("return Err(e.into());"); 223 | f_input_worker.push_block(r_block); 224 | f_input_worker.line("let regs = Registers(data);"); 225 | } 226 | } 227 | if !config.map.is_empty() { 228 | let mut cp_block = codegen::Block::new(""); 229 | cp_block.line("let mut ctx = CONTEXT.write();"); 230 | for i in config.map { 231 | cp_block.line(format!("// {}", i.target)); 232 | let mut cp_block_try_into = codegen::Block::new("match slice.try_into()"); 233 | cp_block_try_into.line(format!("Ok(v) => ctx.{} = v,", i.target)); 234 | cp_block_try_into.line(format!( 235 | "Err(e) => ::rplc::export::log::error!(\"modbus ctx.{} set err: {{}}\", e)", 236 | i.target 237 | )); 238 | let mut cp_block_match_slice_at = 239 | codegen::Block::new(&format!("match regs.slice_at({})", i.offset.offset())); 240 | cp_block_match_slice_at.line("Ok(slice) => "); 241 | cp_block_match_slice_at.push_block(cp_block_try_into); 242 | cp_block_match_slice_at.line(format!( 243 | "Err(e) => ::rplc::export::log::error!(\"modbus slice err ctx.{}: {{}}\", e)", 244 | i.target 245 | )); 246 | cp_block.push_block(cp_block_match_slice_at); 247 | } 248 | f_input_worker.push_block(cp_block); 249 | } 250 | f_input_worker.line("Ok(())"); 251 | } 252 | 253 | fn push_output_worker( 254 | num: usize, 255 | id: &str, 256 | config: OutputConfig, 257 | proto: Proto, 258 | scope: &mut codegen::Scope, 259 | ) { 260 | let f_output = scope.new_fn(&format!("output_{id}_{num}")); 261 | f_output.arg("comm", "&::rplc::comm::Communicator"); 262 | let mut loop_block = 263 | codegen::Block::new(&format!("if let Err(e) = output_{id}_{num}_worker(comm)")); 264 | loop_block.line("::rplc::export::log::error!(\"{}: {}\", ::rplc::tasks::thread_name(), e);"); 265 | f_output.push_block(loop_block); 266 | let f_output_worker = scope.new_fn(&format!("output_{id}_{num}_worker")); 267 | f_output_worker.arg("comm", "&::rplc::comm::Communicator"); 268 | f_output_worker.ret("Result<(), Box>"); 269 | f_output_worker 270 | .line("use ::rplc::export::rmodbus::{self, client::ModbusRequest, guess_response_frame_len, ModbusProto};"); 271 | f_output_worker.line(format!( 272 | "use ::rplc::io::modbus::{};", 273 | config.reg.kind().as_helper_type_str() 274 | )); 275 | f_output_worker.line(format!( 276 | "let mut data: Vec<{}> = vec![{}; {}];", 277 | config.reg.kind().as_type_str(), 278 | config.reg.kind().as_type_default_value_str(), 279 | config.reg.number() 280 | )); 281 | let mut cp_block = codegen::Block::new(""); 282 | cp_block.line("let ctx = CONTEXT.read();"); 283 | for m in config.map { 284 | cp_block.line(format!("// {}", m.source)); 285 | cp_block.line(format!("let offset = {};", m.offset.offset())); 286 | cp_block.line(format!( 287 | "let payload = {}::from(&ctx.{});", 288 | config.reg.kind().as_helper_type_str(), 289 | m.source 290 | )); 291 | let mut iter_block = codegen::Block::new("for (i, p) in payload.0.into_iter().enumerate()"); 292 | iter_block 293 | .line("*data.get_mut(i+offset).ok_or(::rplc::export::rmodbus::ErrorKind::OOB)? = p;"); 294 | cp_block.push_block(iter_block); 295 | } 296 | f_output_worker.push_block(cp_block); 297 | f_output_worker.line("let mut request = Vec::new();"); 298 | f_output_worker.line(format!( 299 | "let mut mreq = ModbusRequest::new({}, ModbusProto::{});", 300 | config.unit, 301 | proto.as_rmodbus_proto_str() 302 | )); 303 | f_output_worker.line(format!( 304 | "mreq.generate_set_{}s_bulk({}, &data, &mut request)?;", 305 | config.reg.kind(), 306 | config.reg.offset(), 307 | )); 308 | let mut resp_block = codegen::Block::new("let response ="); 309 | resp_block.line("let _lock = comm.lock();"); 310 | resp_block.line("comm.write(&request)?;"); 311 | resp_block.line("let mut buf = [0u8; 6];"); 312 | resp_block.line("comm.read_exact(&mut buf)?;"); 313 | resp_block.line("let mut response = buf.to_vec();"); 314 | resp_block.line(format!( 315 | "let len = guess_response_frame_len(&buf, ModbusProto::{})?;", 316 | proto.as_rmodbus_proto_str() 317 | )); 318 | let mut lf_block = codegen::Block::new("if len > 6"); 319 | lf_block.line("let mut rest = vec![0u8; (len - 6) as usize];"); 320 | lf_block.line("comm.read_exact(&mut rest)?;"); 321 | lf_block.line("response.extend(rest);"); 322 | resp_block.push_block(lf_block); 323 | resp_block.line("response"); 324 | resp_block.after(";"); 325 | f_output_worker.push_block(resp_block); 326 | let mut r_block = codegen::Block::new("if let Err(e) = mreq.parse_ok(&response)"); 327 | let mut r_unknown = codegen::Block::new("if e == rmodbus::ErrorKind::UnknownError"); 328 | r_unknown.line("comm.reconnect();"); 329 | r_block.push_block(r_unknown); 330 | r_block.line("return Err(e.into());"); 331 | f_output_worker.push_block(r_block); 332 | f_output_worker.line("Ok(())"); 333 | } 334 | 335 | pub(crate) fn generate_io( 336 | id: &str, 337 | cfg: &Value, 338 | inputs: &[Value], 339 | outputs: &[Value], 340 | ) -> Result> { 341 | let id = id.to_lowercase(); 342 | let mut scope = codegen::Scope::new(); 343 | let config = Config::deserialize(cfg.clone())?; 344 | let mut launch_fn = codegen::Function::new(&format!("launch_datasync_{id}")); 345 | launch_fn.allow("clippy::redundant_clone, clippy::unreadable_literal"); 346 | launch_fn.line(format!( 347 | "let comm_obj = {};", 348 | config 349 | .proto 350 | .generate_datasync(&config.path, config.timeout, config.frame_delay)? 351 | )); 352 | if !inputs.is_empty() || !outputs.is_empty() { 353 | launch_fn.line("let comm: ::rplc::comm::Communicator = ::std::sync::Arc::new(comm_obj);"); 354 | } 355 | for (i, input) in inputs.iter().enumerate() { 356 | let mut input_config = InputConfig::deserialize(input.clone())?; 357 | for m in &mut input_config.map { 358 | m.offset.normalize(input_config.reg.offset())?; 359 | } 360 | push_launcher( 361 | tasks::Kind::Input, 362 | input_config.sync, 363 | input_config.shift.unwrap_or_default(), 364 | i + 1, 365 | &id, 366 | &mut launch_fn, 367 | ); 368 | push_input_worker(i + 1, &id, input_config, config.proto, &mut scope); 369 | } 370 | for (i, output) in outputs.iter().enumerate() { 371 | let mut output_config = OutputConfig::deserialize(output.clone())?; 372 | for m in &mut output_config.map { 373 | m.offset.normalize(output_config.reg.offset())?; 374 | } 375 | push_launcher( 376 | tasks::Kind::Output, 377 | output_config.sync, 378 | output_config.shift.unwrap_or_default(), 379 | i + 1, 380 | &id, 381 | &mut launch_fn, 382 | ); 383 | push_output_worker(i + 1, &id, output_config, config.proto, &mut scope); 384 | } 385 | scope.push_fn(launch_fn); 386 | Ok(scope) 387 | } 388 | -------------------------------------------------------------------------------- /src/io/modbus/regs.rs: -------------------------------------------------------------------------------- 1 | use bmart_derive::EnumStr; 2 | use eva_common::{EResult, Error}; 3 | use serde::{Deserialize, Deserializer}; 4 | use std::str::FromStr; 5 | 6 | #[derive(Eq, PartialEq, Copy, Clone, Debug, EnumStr)] 7 | #[enumstr(rename_all = "lowercase")] 8 | pub(crate) enum Kind { 9 | Coil, 10 | Discrete, 11 | Input, 12 | Holding, 13 | } 14 | 15 | impl Kind { 16 | pub fn as_helper_type_str(self) -> &'static str { 17 | match self { 18 | Kind::Coil | Kind::Discrete => "Coils", 19 | Kind::Holding | Kind::Input => "Registers", 20 | } 21 | } 22 | pub fn as_type_str(self) -> &'static str { 23 | match self { 24 | Kind::Coil | Kind::Discrete => "bool", 25 | Kind::Holding | Kind::Input => "u16", 26 | } 27 | } 28 | pub fn as_type_default_value_str(self) -> &'static str { 29 | match self { 30 | Kind::Coil | Kind::Discrete => "false", 31 | Kind::Holding | Kind::Input => "0", 32 | } 33 | } 34 | } 35 | 36 | #[derive(Debug, Clone, Deserialize)] 37 | pub(crate) struct Reg { 38 | #[serde(deserialize_with = "deserialize_reg_base")] 39 | reg: RegBase, 40 | number: Option, 41 | } 42 | 43 | #[inline] 44 | fn deserialize_reg_base<'de, D>(deserializer: D) -> Result 45 | where 46 | D: Deserializer<'de>, 47 | { 48 | let buf = String::deserialize(deserializer)?; 49 | buf.parse().map_err(serde::de::Error::custom) 50 | } 51 | 52 | impl Reg { 53 | #[inline] 54 | pub fn kind(&self) -> Kind { 55 | self.reg.kind 56 | } 57 | #[inline] 58 | pub fn number(&self) -> u16 { 59 | if let Some(number) = self.number { 60 | number 61 | } else { 62 | self.reg.number 63 | } 64 | } 65 | #[inline] 66 | pub fn offset(&self) -> u16 { 67 | self.reg.offset 68 | } 69 | #[allow(dead_code)] 70 | pub fn update(&mut self) { 71 | if self.number.is_none() { 72 | self.number.replace(self.reg.number); 73 | } 74 | } 75 | } 76 | 77 | #[derive(Debug, Clone)] 78 | struct RegBase { 79 | kind: Kind, 80 | offset: u16, 81 | number: u16, 82 | } 83 | 84 | fn parse_kind_offset(r: &str) -> EResult<(Kind, u16)> { 85 | if let Some(v) = r.strip_prefix('c') { 86 | Ok((Kind::Coil, v.parse()?)) 87 | } else if let Some(v) = r.strip_prefix('d') { 88 | Ok((Kind::Discrete, v.parse()?)) 89 | } else if let Some(v) = r.strip_prefix('i') { 90 | Ok((Kind::Input, v.parse()?)) 91 | } else if let Some(v) = r.strip_prefix('h') { 92 | Ok((Kind::Holding, v.parse()?)) 93 | } else { 94 | Err(Error::invalid_params(format!( 95 | "invalid register kind: {}", 96 | r 97 | ))) 98 | } 99 | } 100 | 101 | impl FromStr for RegBase { 102 | type Err = Error; 103 | fn from_str(s: &str) -> Result { 104 | let mut sp = s.split('-'); 105 | let reg_str = sp.next().unwrap(); 106 | let (kind, offset) = parse_kind_offset(reg_str)?; 107 | let next_offset = if let Some(next_reg) = sp.next() { 108 | if let Ok(v) = next_reg.parse::() { 109 | Some(v) 110 | } else { 111 | let (kind2, offset2) = parse_kind_offset(next_reg)?; 112 | if kind != kind2 { 113 | return Err(Error::invalid_params(format!( 114 | "invalid register range: {}", 115 | s 116 | ))); 117 | } 118 | Some(offset2) 119 | } 120 | } else { 121 | None 122 | }; 123 | let number = if let Some(no) = next_offset { 124 | if no < offset { 125 | return Err(Error::invalid_params(format!( 126 | "invalid register range: {}", 127 | s 128 | ))); 129 | } 130 | no - offset + 1 131 | } else { 132 | 1 133 | }; 134 | Ok(RegBase { 135 | kind, 136 | offset, 137 | number, 138 | }) 139 | } 140 | } 141 | 142 | #[derive(Debug, Deserialize, Clone)] 143 | #[serde(untagged)] 144 | enum Offset { 145 | Str(String), 146 | Num(u16), 147 | } 148 | 149 | impl Default for Offset { 150 | fn default() -> Self { 151 | Self::Num(0) 152 | } 153 | } 154 | 155 | #[derive(Debug, Deserialize, Clone, Default)] 156 | pub struct MapOffset { 157 | #[serde(default)] 158 | offset: Offset, 159 | } 160 | 161 | fn calc_offset(s: &str) -> EResult { 162 | let mut o = 0; 163 | for offset in s.split('+') { 164 | o += offset.parse::()?; 165 | } 166 | Ok(o) 167 | } 168 | 169 | fn calc_offset_base(s: &str, base_offset: u16) -> EResult { 170 | if let Some(v) = s.strip_prefix('=') { 171 | let o = calc_offset(v)?; 172 | if o < base_offset { 173 | Err(Error::invalid_params(format!( 174 | "invalid offset {}, base: {}", 175 | s, base_offset 176 | ))) 177 | } else { 178 | Ok(o - base_offset) 179 | } 180 | } else { 181 | calc_offset(s) 182 | } 183 | } 184 | 185 | impl MapOffset { 186 | pub fn normalize(&mut self, base_offset: u16) -> EResult<()> { 187 | if let Offset::Str(ref s) = self.offset { 188 | self.offset = Offset::Num(calc_offset_base(s, base_offset)?); 189 | } 190 | Ok(()) 191 | } 192 | pub fn offset(&self) -> u16 { 193 | if let Offset::Num(v) = self.offset { 194 | v 195 | } else { 196 | panic!("offset has been not normailized"); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/io/opcua/cache.rs: -------------------------------------------------------------------------------- 1 | use opcua::client::prelude::{NodeId, Variant}; 2 | use parking_lot::Mutex; 3 | use std::collections::HashMap; 4 | use std::time::Duration; 5 | use ttl_cache::TtlCache; 6 | 7 | const OPC_CACHE_MAX_CAPACITY: usize = 100_000; 8 | 9 | #[allow(clippy::module_name_repetitions)] 10 | pub struct OpcCache { 11 | cache: Mutex>, 12 | ttl: Option, 13 | } 14 | 15 | impl OpcCache { 16 | pub fn new(ttl: Option) -> Self { 17 | Self { 18 | cache: Mutex::new(TtlCache::new(OPC_CACHE_MAX_CAPACITY)), 19 | ttl, 20 | } 21 | } 22 | pub fn retain_map_modified(&self, states: &mut HashMap<&NodeId, Variant>) { 23 | if let Some(ttl) = self.ttl { 24 | let mut cache = self.cache.lock(); 25 | states.retain(|node_id, raw| { 26 | if let Some(cached) = cache.get(node_id) { 27 | cached != raw 28 | } else { 29 | true 30 | } 31 | }); 32 | // cache kept ones 33 | for (oid, raw) in states { 34 | cache.insert((*oid).clone(), raw.clone(), ttl); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/io/opcua/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::tasks; 2 | pub use cache::OpcCache; 3 | use eva_common::value::Value; 4 | use serde::Deserialize; 5 | pub use session::{OpcSafeSess, OpcSafeSession}; 6 | use std::error::Error; 7 | use std::time::Duration; 8 | 9 | mod cache; 10 | mod session; 11 | 12 | #[derive(Deserialize)] 13 | #[serde(deny_unknown_fields)] 14 | struct InputConfig { 15 | #[serde(default)] 16 | nodes: Vec, 17 | #[serde(deserialize_with = "crate::interval::deserialize_interval_as_nanos")] 18 | sync: u64, 19 | #[serde( 20 | default, 21 | deserialize_with = "crate::interval::deserialize_opt_interval_as_nanos" 22 | )] 23 | shift: Option, 24 | } 25 | 26 | #[derive(Deserialize)] 27 | #[serde(deny_unknown_fields)] 28 | struct OutputConfig { 29 | #[serde(default)] 30 | nodes: Vec, 31 | #[serde(deserialize_with = "crate::interval::deserialize_interval_as_nanos")] 32 | sync: u64, 33 | #[serde( 34 | default, 35 | deserialize_with = "crate::interval::deserialize_opt_interval_as_nanos" 36 | )] 37 | shift: Option, 38 | #[serde( 39 | default = "default_cache", 40 | deserialize_with = "crate::interval::deserialize_interval_as_nanos" 41 | )] 42 | cache: u64, 43 | } 44 | 45 | #[derive(Deserialize)] 46 | #[serde(deny_unknown_fields)] 47 | struct NodeMap { 48 | id: String, 49 | map: String, 50 | } 51 | 52 | fn default_timeout() -> f64 { 53 | 1.0 54 | } 55 | 56 | fn default_cache() -> u64 { 57 | 0 58 | } 59 | 60 | #[derive(Deserialize, Clone, Debug, Default)] 61 | #[serde(untagged)] 62 | enum OpcAuth { 63 | #[default] 64 | Anonymous, 65 | User(UserAuth), 66 | X509(X509Auth), 67 | } 68 | 69 | #[derive(Deserialize, Clone, Debug)] 70 | #[serde(deny_unknown_fields)] 71 | struct UserAuth { 72 | user: String, 73 | password: String, 74 | } 75 | 76 | #[derive(Deserialize, Clone, Debug)] 77 | #[serde(deny_unknown_fields)] 78 | struct X509Auth { 79 | cert_file: String, 80 | key_file: String, 81 | } 82 | 83 | #[derive(Deserialize)] 84 | struct Config { 85 | pki_dir: Option, 86 | #[serde(default)] 87 | trust_server_certs: bool, 88 | #[serde(default)] 89 | create_keys: bool, 90 | #[serde(default = "default_timeout")] 91 | timeout: f64, 92 | #[serde(default)] 93 | auth: OpcAuth, 94 | url: String, 95 | } 96 | 97 | fn push_launcher( 98 | kind: tasks::Kind, 99 | map: &[NodeMap], 100 | sync: u64, 101 | shift: u64, 102 | num: usize, 103 | id: &str, 104 | f: &mut codegen::Function, 105 | cache: Option, 106 | ) { 107 | f.line("let sess = session.clone();"); 108 | f.line("let node_ids: Vec = vec!["); 109 | for m in map { 110 | f.line(format!("\"{}\".parse().unwrap(),", m.id)); 111 | } 112 | f.line("];"); 113 | if kind == tasks::Kind::Output { 114 | f.line( 115 | format!("let mut cache = ::rplc::io::opcua::OpcCache::new(Some(::std::time::Duration::from_nanos({})));", cache.unwrap()) 116 | ); 117 | } 118 | let mut spawn_block = codegen::Block::new(&format!( 119 | r#"::rplc::tasks::spawn_{}_loop("{}_{}", 120 | ::std::time::Duration::from_nanos({}), 121 | ::std::time::Duration::from_nanos({}), 122 | move ||"#, 123 | kind, id, num, sync, shift 124 | )); 125 | if kind == tasks::Kind::Output { 126 | spawn_block.line(&format!("{kind}_{id}_{num}(&sess, &node_ids, &mut cache);")); 127 | } else { 128 | spawn_block.line(&format!("{kind}_{id}_{num}(&sess, &node_ids);")); 129 | } 130 | spawn_block.after(");"); 131 | f.push_block(spawn_block); 132 | } 133 | 134 | fn push_input_worker(num: usize, id: &str, config: InputConfig, scope: &mut codegen::Scope) { 135 | let f_input = scope.new_fn(&format!("input_{id}_{num}")); 136 | f_input.arg("session", "&::rplc::io::opcua::OpcSafeSession"); 137 | f_input.arg( 138 | "node_ids", 139 | "&[::rplc::export::opcua::types::node_id::NodeId]", 140 | ); 141 | let mut loop_block = codegen::Block::new(&format!( 142 | "if let Err(e) = input_{id}_{num}_worker(session, node_ids)" 143 | )); 144 | loop_block.line("session.reconnect();"); 145 | loop_block.line("::rplc::export::log::error!(\"{}: {}\", ::rplc::tasks::thread_name(), e);"); 146 | f_input.push_block(loop_block); 147 | let f_input_worker = scope.new_fn(&format!("input_{id}_{num}_worker")); 148 | f_input_worker.arg("session", "&::rplc::io::opcua::OpcSafeSession"); 149 | f_input_worker.arg( 150 | "node_ids", 151 | "&[::rplc::export::opcua::types::node_id::NodeId]", 152 | ); 153 | f_input_worker.ret("Result<(), Box>"); 154 | f_input_worker.line("use ::rplc::export::opcua::client::prelude::*;"); 155 | f_input_worker.line("let to_read = vec!["); 156 | for i in 0..config.nodes.len() { 157 | f_input_worker.line(format!( 158 | r#"ReadValueId {{ 159 | node_id: node_ids[{i}].clone(), 160 | attribute_id: AttributeId::Value as u32, 161 | index_range: UAString::null(), 162 | data_encoding: QualifiedName::null(), 163 | }}"# 164 | )); 165 | } 166 | f_input_worker.line("];"); 167 | f_input_worker.line("let result = session.read(&to_read, TimestampsToReturn::Neither, 0.0)??;"); 168 | f_input_worker.line("let mut ctx = CONTEXT.write();"); 169 | let mut for_block = codegen::Block::new("for (i, res) in result.into_iter().enumerate()"); 170 | let mut match_idx_block = codegen::Block::new("match i"); 171 | for (i, node) in config.nodes.into_iter().enumerate() { 172 | let mut idx_block = codegen::Block::new(&format!("{i} =>")); 173 | let mut val_block = codegen::Block::new("if let Some(value) = res.value"); 174 | let mut val_into_block = codegen::Block::new("if let Ok(v) = value.try_into()"); 175 | val_into_block.line(format!("ctx.{} = v;", node.map)); 176 | val_into_block.after(&format!( 177 | " else {{ ::rplc::export::log::error!(\"OPC error set OPC {{}} to ctx.{{}}\", node_ids[{i}], \"{}\"); }}", 178 | node.map 179 | )); 180 | val_block.push_block(val_into_block); 181 | val_block.after(&format!( 182 | " else {{ ::rplc::export::log::error!(\"OPC read error {{}}\", node_ids[{i}]); }}" 183 | )); 184 | idx_block.push_block(val_block); 185 | match_idx_block.push_block(idx_block); 186 | } 187 | match_idx_block.push_block(codegen::Block::new("_ =>")); 188 | for_block.push_block(match_idx_block); 189 | f_input_worker.push_block(for_block); 190 | f_input_worker.line("Ok(())"); 191 | } 192 | 193 | fn push_output_worker(num: usize, id: &str, config: OutputConfig, scope: &mut codegen::Scope) { 194 | let f_output = scope.new_fn(&format!("output_{id}_{num}")); 195 | f_output.arg("session", "&::rplc::io::opcua::OpcSafeSession"); 196 | f_output.arg( 197 | "node_ids", 198 | "&[::rplc::export::opcua::types::node_id::NodeId]", 199 | ); 200 | f_output.arg("cache", "&mut ::rplc::io::opcua::OpcCache"); 201 | let mut loop_block = codegen::Block::new(&format!( 202 | "if let Err(e) = output_{id}_{num}_worker(session, node_ids, cache)" 203 | )); 204 | loop_block.line("::rplc::export::log::error!(\"{}: {}\", ::rplc::tasks::thread_name(), e);"); 205 | f_output.push_block(loop_block); 206 | let f_output_worker = scope.new_fn(&format!("output_{id}_{num}_worker")); 207 | f_output_worker.arg("session", "&::rplc::io::opcua::OpcSafeSession"); 208 | f_output_worker.arg( 209 | "node_ids", 210 | "&[::rplc::export::opcua::types::node_id::NodeId]", 211 | ); 212 | f_output_worker.arg("cache", "&mut ::rplc::io::opcua::OpcCache"); 213 | f_output_worker.ret("Result<(), Box>"); 214 | f_output_worker.line("use ::rplc::export::opcua::client::prelude::*;"); 215 | f_output_worker.line("let now = DateTime::now();"); 216 | let mut block_values = codegen::Block::new("let mut values ="); 217 | block_values.line("let ctx = CONTEXT.read();"); 218 | block_values.line(format!( 219 | "let mut values = ::std::collections::HashMap::with_capacity({});", 220 | config.nodes.len() 221 | )); 222 | for (i, m) in config.nodes.iter().enumerate() { 223 | block_values.line(format!( 224 | "values.insert(&node_ids[{i}], Variant::from(ctx.{}));", 225 | m.map 226 | )); 227 | } 228 | block_values.line("values"); 229 | block_values.after(";"); 230 | f_output_worker.push_block(block_values); 231 | let mut block = codegen::Block::new("if ::rplc::tasks::is_active()"); 232 | block.line("cache.retain_map_modified(&mut values);"); 233 | f_output_worker.push_block(block); 234 | let mut write_block = codegen::Block::new("if !values.is_empty()"); 235 | write_block.line( 236 | r#"let to_write: Vec = values 237 | .into_iter() 238 | .map(|(n, v)| WriteValue { 239 | node_id: n.clone(), 240 | attribute_id: AttributeId::Value as u32, 241 | index_range: UAString::null(), 242 | value: DataValue { 243 | value: Some(v), 244 | status: Some(StatusCode::Good), 245 | source_timestamp: Some(now), 246 | ..DataValue::default() 247 | }, 248 | }) 249 | .collect(); 250 | "#, 251 | ); 252 | write_block.line("let statuses = session.write(&to_write)??;"); 253 | let mut status_block = 254 | codegen::Block::new("for (i, status) in statuses.into_iter().enumerate()"); 255 | status_block.line("if i == node_ids.len() { break; }"); 256 | status_block.line("if status != StatusCode::Good { ::rplc::export::log::error!(\"OPC write error {}: {}\", node_ids[i], status); }"); 257 | write_block.push_block(status_block); 258 | f_output_worker.push_block(write_block); 259 | f_output_worker.line("Ok(())"); 260 | } 261 | 262 | #[allow(clippy::too_many_lines)] 263 | pub(crate) fn generate_io( 264 | id: &str, 265 | cfg: &Value, 266 | inputs: &[Value], 267 | outputs: &[Value], 268 | ) -> Result> { 269 | let mut scope = codegen::Scope::new(); 270 | if inputs.is_empty() && outputs.is_empty() { 271 | return Ok(scope); 272 | } 273 | let id = id.to_lowercase(); 274 | let config = Config::deserialize(cfg.clone())?; 275 | let mut launch_fn = codegen::Function::new(format!("launch_datasync_{id}")); 276 | launch_fn.allow("clippy::redundant_clone, clippy::unreadable_literal"); 277 | launch_fn.line("use ::rplc::export::opcua::client::prelude::*;"); 278 | launch_fn.line("use ::std::path::Path;"); 279 | if let Some(pki_dir) = config.pki_dir { 280 | launch_fn.line(format!( 281 | "let pki_dir = Path::new(\"{}\").to_owned();", 282 | pki_dir 283 | )); 284 | } else { 285 | launch_fn.line("let mut pki_dir = ::rplc::var_dir();"); 286 | launch_fn.line("pki_dir.push(format!(\"{}_pki\", crate::plc::NAME));"); 287 | } 288 | match config.auth { 289 | OpcAuth::Anonymous => { 290 | launch_fn.line("let token = IdentityToken::Anonymous;"); 291 | } 292 | OpcAuth::User(u) => { 293 | launch_fn.line(format!( 294 | "let token = IdentityToken::UserName(\"{}\".to_string(), \"{}\".to_string());", 295 | u.user, u.password 296 | )); 297 | } 298 | OpcAuth::X509(x) => { 299 | if x.cert_file.starts_with('/') { 300 | launch_fn.line(format!( 301 | "let cert_file = Path::new(\"{}\").to_owned();", 302 | x.cert_file 303 | )); 304 | } else { 305 | launch_fn.line("let mut cert_file = pki_dir.clone();"); 306 | launch_fn.line(format!("cert_file.push(\"{}\");", x.cert_file)); 307 | } 308 | if x.key_file.starts_with('/') { 309 | launch_fn.line(format!( 310 | "let key_file = Path::new(\"{}\").to_owned();", 311 | x.key_file 312 | )); 313 | } else { 314 | launch_fn.line("let mut key_file = pki_dir.clone();"); 315 | launch_fn.line(format!("key_file.push(\"{}\");", x.cert_file)); 316 | } 317 | launch_fn.line("let token = IdentityToken::X509(cert_file, key_file);"); 318 | } 319 | } 320 | launch_fn.line(format!( 321 | r#"let client = ClientBuilder::new() 322 | .application_name(format!("rplc.{{}}", crate::plc::NAME)) 323 | .application_uri("urn:") 324 | .product_uri("urn:") 325 | .ignore_clock_skew() 326 | .trust_server_certs({}) 327 | .create_sample_keypair({}) 328 | .pki_dir(pki_dir) 329 | .session_retry_limit(0) 330 | .session_name(format!("{{}}.{{}}", ::rplc::hostname(), crate::plc::NAME)) 331 | .session_timeout({}) 332 | .client() 333 | .unwrap(); 334 | "#, 335 | config.trust_server_certs, 336 | config.create_keys, 337 | Duration::from_secs_f64(config.timeout).as_millis() 338 | )); 339 | launch_fn.line(format!( 340 | r#"let session = ::std::sync::Arc::new(::rplc::io::opcua::OpcSafeSess::new( 341 | client, 342 | ( 343 | "{}", 344 | "None", 345 | MessageSecurityMode::None, 346 | UserTokenPolicy::anonymous(), 347 | ), 348 | token 349 | )); 350 | "#, 351 | config.url 352 | )); 353 | for (i, input) in inputs.iter().enumerate() { 354 | let input_config = InputConfig::deserialize(input.clone())?; 355 | if !input_config.nodes.is_empty() { 356 | push_launcher( 357 | tasks::Kind::Input, 358 | &input_config.nodes, 359 | input_config.sync, 360 | input_config.shift.unwrap_or_default(), 361 | i + 1, 362 | &id, 363 | &mut launch_fn, 364 | None, 365 | ); 366 | push_input_worker(i + 1, &id, input_config, &mut scope); 367 | } 368 | } 369 | for (i, output) in outputs.iter().enumerate() { 370 | let output_config = OutputConfig::deserialize(output.clone())?; 371 | if !output_config.nodes.is_empty() { 372 | push_launcher( 373 | tasks::Kind::Output, 374 | &output_config.nodes, 375 | output_config.sync, 376 | output_config.shift.unwrap_or_default(), 377 | i + 1, 378 | &id, 379 | &mut launch_fn, 380 | Some(output_config.cache), 381 | ); 382 | push_output_worker(i + 1, &id, output_config, &mut scope); 383 | } 384 | } 385 | scope.push_fn(launch_fn); 386 | Ok(scope) 387 | } 388 | -------------------------------------------------------------------------------- /src/io/opcua/session.rs: -------------------------------------------------------------------------------- 1 | use opcua::client::prelude::*; 2 | use parking_lot::{Mutex, MutexGuard, RwLock}; 3 | use std::error::Error; 4 | use std::sync::Arc; 5 | 6 | #[allow(clippy::module_name_repetitions)] 7 | pub type OpcSafeSession = Arc; 8 | 9 | type OpcSession = Arc>; 10 | 11 | pub struct OpcSafeSess { 12 | client: Mutex, 13 | endpoint_description: EndpointDescription, 14 | user_identity_token: IdentityToken, 15 | session: Mutex>, 16 | } 17 | 18 | impl OpcSafeSess { 19 | pub fn new(client: Client, endpoint: T, user_identity_token: IdentityToken) -> Self 20 | where 21 | T: Into, 22 | { 23 | Self { 24 | client: Mutex::new(client), 25 | endpoint_description: endpoint.into(), 26 | user_identity_token, 27 | session: <_>::default(), 28 | } 29 | } 30 | pub fn reconnect(&self) { 31 | self.session.lock().take(); 32 | } 33 | fn get_session(&self) -> Result>, std::io::Error> { 34 | let mut lock = self.session.lock(); 35 | if lock.as_mut().is_none() { 36 | let session = self.client.lock().connect_to_endpoint( 37 | self.endpoint_description.clone(), 38 | self.user_identity_token.clone(), 39 | )?; 40 | lock.replace(session); 41 | } 42 | Ok(lock) 43 | } 44 | /// # Panics 45 | /// 46 | /// Should not panic 47 | pub fn read( 48 | &self, 49 | nodes_to_read: &[ReadValueId], 50 | timestamps_to_return: TimestampsToReturn, 51 | max_age: f64, 52 | ) -> Result, StatusCode>, Box> { 53 | let mut session = self.get_session()?; 54 | let result = 55 | session 56 | .as_mut() 57 | .unwrap() 58 | .read() 59 | .read(nodes_to_read, timestamps_to_return, max_age); 60 | Ok(result) 61 | } 62 | /// # Panics 63 | /// 64 | /// Should not panic 65 | pub fn write( 66 | &self, 67 | nodes_to_write: &[WriteValue], 68 | ) -> Result, StatusCode>, Box> { 69 | let mut session = self.get_session()?; 70 | let result = session.as_mut().unwrap().read().write(nodes_to_write); 71 | Ok(result) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![ doc = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/", "README.md" ) ) ] 2 | use log::{debug, info}; 3 | use once_cell::sync::{Lazy, OnceCell}; 4 | use parking_lot::RwLock; 5 | use serde::{Deserialize, Serialize}; 6 | use std::env; 7 | use std::fmt::Write as _; 8 | use std::fs; 9 | use std::panic; 10 | use std::path::{Path, PathBuf}; 11 | use std::process; 12 | use std::sync::atomic; 13 | use std::sync::Arc; 14 | use std::time::{Duration, Instant}; 15 | 16 | pub mod api; 17 | pub mod builder; 18 | #[cfg(feature = "client")] 19 | pub mod client; 20 | pub mod comm; 21 | #[cfg(feature = "eva")] 22 | pub mod eapi; 23 | pub mod interval; 24 | pub mod io; 25 | pub mod server; 26 | pub mod tasks; 27 | 28 | pub mod prelude { 29 | pub use super::{init_plc, plc_context, plc_context_mut, run_plc}; 30 | pub use log::{debug, error, info, trace, warn}; 31 | pub use rplc_derive::plc_program; 32 | } 33 | 34 | pub mod export { 35 | pub use eva_common; 36 | #[cfg(feature = "eva")] 37 | pub use eva_sdk; 38 | pub use log; 39 | pub use once_cell; 40 | #[cfg(feature = "opcua")] 41 | pub use opcua; 42 | pub use parking_lot; 43 | #[cfg(feature = "modbus")] 44 | pub use rmodbus; 45 | pub use serde; 46 | } 47 | 48 | pub type LockedContext = RwLock; 49 | 50 | pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(1); 51 | pub const DEFAULT_STOP_TIMEOUT: f64 = 30.0; 52 | pub static NAME: OnceCell = OnceCell::new(); 53 | pub static DESCRIPTION: OnceCell = OnceCell::new(); 54 | pub static VERSION: OnceCell = OnceCell::new(); 55 | pub static CPUS: OnceCell = OnceCell::new(); 56 | pub static STACK_SIZE: OnceCell = OnceCell::new(); 57 | 58 | static HOSTNAME: OnceCell = OnceCell::new(); 59 | static STARTUP_TIME: OnceCell = OnceCell::new(); 60 | 61 | static TERM_FLAG: Lazy> = 62 | Lazy::new(|| Arc::new(atomic::AtomicBool::new(false))); 63 | 64 | fn sigterm_received() -> bool { 65 | TERM_FLAG.load(atomic::Ordering::SeqCst) 66 | } 67 | 68 | #[derive(Serialize, Deserialize, Default)] 69 | pub struct PlcInfo { 70 | pub system_name: String, 71 | pub name: String, 72 | pub description: String, 73 | pub version: String, 74 | pub status: i16, 75 | pub pid: u32, 76 | pub uptime: f64, 77 | } 78 | 79 | pub(crate) fn plc_info() -> PlcInfo { 80 | PlcInfo { 81 | system_name: HOSTNAME.get().unwrap().clone(), 82 | name: NAME.get().unwrap().clone(), 83 | description: DESCRIPTION.get().unwrap().clone(), 84 | version: VERSION.get().unwrap().clone(), 85 | status: tasks::status() as i16, 86 | pid: process::id(), 87 | uptime: uptime().as_secs_f64(), 88 | } 89 | } 90 | 91 | /// # Panics 92 | /// 93 | /// Will panic if PLC is not initialized 94 | #[inline] 95 | pub fn hostname() -> &'static str { 96 | HOSTNAME.get().unwrap() 97 | } 98 | 99 | /// # Panics 100 | /// 101 | /// Will panic if PLC is not initialized 102 | #[inline] 103 | pub fn uptime() -> Duration { 104 | STARTUP_TIME.get().unwrap().elapsed() 105 | } 106 | 107 | /// use init_plc!() macro to init the PLC 108 | /// 109 | /// # Panics 110 | /// 111 | /// Will panic if syslog is selected but can not be connected 112 | pub fn init(name: &str, description: &str, version: &str, stack_size: Option) { 113 | eva_common::self_test(); 114 | panic::set_hook(Box::new(|s| { 115 | eprintln!("PANIC: {}", s); 116 | std::process::exit(1); 117 | })); 118 | HOSTNAME 119 | .set(hostname::get().unwrap().to_string_lossy().to_string()) 120 | .unwrap(); 121 | STARTUP_TIME.set(Instant::now()).unwrap(); 122 | NAME.set(name.to_owned()).unwrap(); 123 | DESCRIPTION.set(description.to_owned()).unwrap(); 124 | VERSION.set(version.to_owned()).unwrap(); 125 | if let Some(ss) = stack_size { 126 | STACK_SIZE.set(ss).unwrap(); 127 | } 128 | let verbose: bool = env::var("VERBOSE").ok().map_or(false, |v| v == "1"); 129 | let syslog: bool = env::var("SYSLOG").ok().map_or(false, |v| v == "1"); 130 | if syslog { 131 | let formatter = syslog::Formatter3164 { 132 | facility: syslog::Facility::LOG_USER, 133 | hostname: None, 134 | process: name.to_owned(), 135 | pid: std::process::id(), 136 | }; 137 | log::set_boxed_logger(Box::new(syslog::BasicLogger::new( 138 | syslog::unix(formatter).unwrap(), 139 | ))) 140 | .unwrap(); 141 | log::set_max_level(if verbose { 142 | log::LevelFilter::Trace 143 | } else { 144 | log::LevelFilter::Info 145 | }); 146 | } else { 147 | env_logger::Builder::new() 148 | .target(env_logger::Target::Stdout) 149 | .filter_level(if verbose { 150 | log::LevelFilter::Trace 151 | } else { 152 | log::LevelFilter::Info 153 | }) 154 | .init(); 155 | } 156 | debug!("log initialization completed"); 157 | tasks::init(); 158 | } 159 | 160 | #[allow(clippy::crate_in_macro_def)] 161 | #[macro_export] 162 | macro_rules! init_plc { 163 | () => { 164 | ::rplc::init( 165 | crate::plc::NAME, 166 | crate::plc::DESCRIPTION, 167 | crate::plc::VERSION, 168 | crate::plc::STACK_SIZE, 169 | ); 170 | }; 171 | } 172 | 173 | #[allow(clippy::crate_in_macro_def)] 174 | #[macro_export] 175 | macro_rules! plc_context { 176 | () => { 177 | crate::plc::context::CONTEXT.read() 178 | }; 179 | } 180 | 181 | #[allow(clippy::crate_in_macro_def)] 182 | #[macro_export] 183 | macro_rules! plc_context_mut { 184 | () => { 185 | crate::plc::context::CONTEXT.write() 186 | }; 187 | } 188 | 189 | /// # Panics 190 | /// 191 | /// Will panic if unable to register SIGTERM/SIGINT handler 192 | fn register_signals() { 193 | signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&TERM_FLAG)).unwrap(); 194 | signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&TERM_FLAG)).unwrap(); 195 | } 196 | 197 | pub fn var_dir() -> PathBuf { 198 | env::var("PLC_VAR_DIR").map_or_else(|_| env::temp_dir(), |p| Path::new(&p).to_owned()) 199 | } 200 | 201 | pub(crate) fn name() -> &'static str { 202 | NAME.get().map(String::as_str).unwrap() 203 | } 204 | 205 | /// use run_plc!() macro to run the PLC 206 | /// 207 | /// # Panics 208 | /// 209 | /// Will panic if unable to write/remove the pid file/api socket or if PLC is not intialized 210 | pub fn run( 211 | launch_datasync: &F, 212 | stop_datasync: &F2, 213 | stop_timeout: Duration, 214 | _eapi_action_pool_size: usize, 215 | ) { 216 | tasks::set_starting(); 217 | let name = NAME.get().expect("PLC not initialized"); 218 | let description = DESCRIPTION.get().unwrap(); 219 | let version = VERSION.get().unwrap(); 220 | let mut msg = format!("{} {}", name, version); 221 | if !description.is_empty() { 222 | let _ = write!(msg, " ({})", description); 223 | } 224 | info!("system: {}, cpus: {}", HOSTNAME.get().unwrap(), cpus()); 225 | info!("{}", msg); 226 | register_signals(); 227 | #[cfg(feature = "eva")] 228 | eapi::launch(_eapi_action_pool_size); 229 | launch_datasync(); 230 | tasks::set_syncing(); 231 | tasks::set_preparing_if_no_inputs(); 232 | tasks::set_active_if_no_inputs_and_programs(); 233 | let pid = process::id(); 234 | let mut pid_file = var_dir(); 235 | pid_file.push(format!("{}.pid", name)); 236 | fs::write(&pid_file, pid.to_string()).unwrap(); 237 | let socket_path = api::spawn_api(); 238 | while !sigterm_received() { 239 | tasks::step_sleep(); 240 | check_health(); 241 | } 242 | tasks::spawn0(move || { 243 | tasks::sleep(stop_timeout); 244 | panic!("timeout has been reached, FORCE STOP"); 245 | }); 246 | if tasks::status() == tasks::Status::Active { 247 | tasks::shutdown(); 248 | if tasks::status() != tasks::Status::Stopped { 249 | stop_datasync(); 250 | } 251 | while tasks::status() != tasks::Status::Stopped { 252 | tasks::step_sleep(); 253 | check_health(); 254 | } 255 | } else { 256 | tasks::set_stopped(); 257 | } 258 | fs::remove_file(pid_file).unwrap(); 259 | fs::remove_file(socket_path).unwrap(); 260 | } 261 | 262 | fn check_health() { 263 | //tasks::check_health(); 264 | } 265 | 266 | #[allow(clippy::crate_in_macro_def)] 267 | #[macro_export] 268 | macro_rules! run_plc { 269 | () => { 270 | ::rplc::run( 271 | &crate::plc::io::launch_datasync, 272 | &crate::plc::io::stop_datasync, 273 | crate::plc::STOP_TIMEOUT, 274 | crate::plc::EAPI_ACTION_POOL_SIZE, 275 | ); 276 | }; 277 | } 278 | 279 | pub fn cpus() -> usize { 280 | if let Some(cpus) = CPUS.get() { 281 | *cpus 282 | } else { 283 | let cpus = if let Ok(s) = std::fs::read_to_string("/proc/cpuinfo") { 284 | let mut c = 0; 285 | for line in s.split('\n') { 286 | if line.starts_with("processor\t") { 287 | c += 1; 288 | } 289 | } 290 | c 291 | } else { 292 | 0 293 | }; 294 | let _ = CPUS.set(cpus); 295 | cpus 296 | } 297 | } 298 | 299 | // TODO manually impl default traits for context structs 300 | // TODO custom action handlers, long action examples, kill and terminate support 301 | // TODO allows programs which are called via BUS/RT as lmacros 302 | 303 | // TODO hostmaster daemon (BUS/RT) 304 | // TODO hostmaster daemon (web ui, plc logs) 305 | // TODO BUS/RT fieldbus (direct context exchange between PLCs) 306 | 307 | // TODO vs code plugins for context and mappings 308 | // TODO freertos support 309 | // TODO sync context with EthernetIP structures 310 | // TODO sync context with CANOpen registers 311 | // TODO sync context with TwinCAT registers 312 | // TODO chains: inputs, programs, outputs 313 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[cfg(feature = "modbus")] 4 | pub mod modbus; 5 | 6 | #[derive(Deserialize, Debug)] 7 | #[serde(rename_all = "lowercase")] 8 | pub enum Kind { 9 | #[cfg(feature = "modbus")] 10 | Modbus, 11 | } 12 | -------------------------------------------------------------------------------- /src/server/modbus.rs: -------------------------------------------------------------------------------- 1 | use eva_common::value::Value; 2 | use eva_common::{EResult, Error}; 3 | use log::{error, info, warn}; 4 | use serde::Deserialize; 5 | use std::fmt::Write as _; 6 | use std::io::{Read, Write}; 7 | use std::net::SocketAddr; 8 | use std::net::TcpListener; 9 | use std::time::Duration; 10 | 11 | #[derive(Deserialize, Debug, bmart_derive::EnumStr)] 12 | #[enumstr(rename_all = "lowercase")] 13 | #[serde(rename_all = "lowercase")] 14 | enum Proto { 15 | Tcp, 16 | Rtu, 17 | } 18 | 19 | use rmodbus::{ 20 | server::{context::ModbusContext, ModbusFrame}, 21 | ModbusFrameBuf, ModbusProto, 22 | }; 23 | 24 | pub trait SlaveContext { 25 | fn modbus_context(&self) -> &ModbusContext; 26 | fn modbus_context_mut(&mut self) -> &mut ModbusContext; 27 | } 28 | 29 | fn default_maxconn() -> usize { 30 | 5 31 | } 32 | 33 | #[derive(Deserialize, Debug)] 34 | #[serde(deny_unknown_fields)] 35 | struct ServerConfig { 36 | proto: Proto, 37 | listen: String, 38 | unit: u8, 39 | timeout: f64, 40 | #[serde(default = "default_maxconn")] 41 | maxconn: usize, 42 | } 43 | 44 | pub(crate) fn generate_server_launcher( 45 | id: usize, 46 | config: &Value, 47 | modbus_context_config: &crate::builder::config::ModbusConfig, 48 | ) -> EResult { 49 | let config = ServerConfig::deserialize(config.clone())?; 50 | let name = format!("srv{}_modbus", id); 51 | let mut launch_block = 52 | codegen::Block::new(&format!("::rplc::tasks::spawn_service(\"{name}\", move ||")); 53 | launch_block.line("#[allow(clippy::unreadable_literal)]"); 54 | config.listen.parse::().map_err(|e| { 55 | Error::invalid_params(format!("invalid modbus server listen address: {}", e)) 56 | })?; 57 | let mut launch_loop = codegen::Block::new("loop"); 58 | let mut launch_str = format!( 59 | "::rplc::server::modbus::{}_server::(", 60 | config.proto, 61 | modbus_context_config.as_const_generics() 62 | ); 63 | write!( 64 | launch_str, 65 | "{}, \"{}\", &CONTEXT, ::std::time::Duration::from_secs_f64({:.6}), {})", 66 | config.unit, config.listen, config.timeout, config.maxconn 67 | )?; 68 | let mut launch_if = codegen::Block::new(&format!("if let Err(e) = {}", launch_str)); 69 | launch_if.line(format!( 70 | "::rplc::export::log::error!(\"modbus server {} {} error: {{e}}\");", 71 | config.proto, config.listen 72 | )); 73 | launch_loop.push_block(launch_if); 74 | launch_loop.line("::rplc::tasks::step_sleep_err();"); 75 | launch_block.push_block(launch_loop); 76 | launch_block.after(");"); 77 | Ok(launch_block) 78 | } 79 | 80 | pub fn handle_tcp_stream( 81 | stream: Result, 82 | ctx: &'static crate::LockedContext, 83 | unit: u8, 84 | timeout: Duration, 85 | ) -> Result<(), Box> 86 | where 87 | X: SlaveContext, 88 | { 89 | let mut stream = stream?; 90 | stream.set_nodelay(true)?; 91 | stream.set_read_timeout(Some(timeout))?; 92 | stream.set_write_timeout(Some(timeout))?; 93 | loop { 94 | let mut buf: ModbusFrameBuf = [0; 256]; 95 | let mut response = Vec::new(); // for nostd use FixedVec with alloc [u8;256] 96 | if stream.read(&mut buf).unwrap_or(0) == 0 { 97 | break; 98 | } 99 | let mut frame = ModbusFrame::new(unit, &buf, ModbusProto::TcpUdp, &mut response); 100 | frame.parse()?; 101 | if frame.processing_required { 102 | if frame.readonly { 103 | frame.process_read(ctx.read().modbus_context())?; 104 | } else { 105 | frame.process_write(ctx.write().modbus_context_mut())?; 106 | }; 107 | } 108 | if frame.response_required { 109 | frame.finalize_response()?; 110 | if stream.write(response.as_slice()).is_err() { 111 | break; 112 | } 113 | } 114 | } 115 | Ok(()) 116 | } 117 | 118 | pub fn tcp_server( 119 | unit: u8, 120 | listen: &str, 121 | ctx: &'static crate::LockedContext, 122 | timeout: Duration, 123 | maxconn: usize, 124 | ) -> Result<(), Box> 125 | where 126 | X: SlaveContext + Send + Sync + 'static, 127 | { 128 | let listener = TcpListener::bind(listen)?; 129 | let pool = threadpool::ThreadPool::new(maxconn); 130 | info!("modbus listener started at: {listen}"); 131 | for stream in listener.incoming() { 132 | pool.execute(move || { 133 | if let Err(e) = handle_tcp_stream(stream, ctx, unit, timeout) { 134 | error!("modbus server error: {}", e); 135 | } 136 | }); 137 | } 138 | Ok(()) 139 | } 140 | 141 | /// # Panics 142 | /// 143 | /// Will panic on misconfigured listen string 144 | pub fn rtu_server( 145 | unit: u8, 146 | listen: &str, 147 | ctx: &'static crate::LockedContext, 148 | timeout: Duration, 149 | _maxconn: usize, 150 | ) -> Result<(), Box> 151 | where 152 | X: SlaveContext + Send + Sync + 'static, 153 | { 154 | let mut port = crate::comm::serial::open(listen, timeout)?; 155 | info!("modbus listener started at: {listen}"); 156 | loop { 157 | let mut buf: ModbusFrameBuf = [0; 256]; 158 | if port.read(&mut buf)? > 0 { 159 | let mut response = Vec::new(); 160 | let mut frame = ModbusFrame::new(unit, &buf, ModbusProto::Rtu, &mut response); 161 | if frame.parse().is_err() { 162 | warn!("broken frame received on {}", listen); 163 | continue; 164 | } 165 | if frame.processing_required { 166 | let result = if frame.readonly { 167 | frame.process_read(ctx.read().modbus_context()) 168 | } else { 169 | frame.process_write(ctx.write().modbus_context_mut()) 170 | }; 171 | match result { 172 | Ok(()) => {} 173 | Err(e) => { 174 | warn!("frame processing error on {}: {}", listen, e); 175 | continue; 176 | } 177 | } 178 | } 179 | if frame.response_required { 180 | frame.finalize_response()?; 181 | println!("{:x?}", response); 182 | port.write_all(&response)?; 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/tasks.rs: -------------------------------------------------------------------------------- 1 | use crate::cpus; 2 | use crate::interval::Loop; 3 | use bmart_derive::EnumStr; 4 | use eva_common::{EResult, Error}; 5 | use log::{debug, error, info, warn}; 6 | use once_cell::sync::{Lazy, OnceCell}; 7 | use parking_lot::{Condvar, Mutex}; 8 | use serde::{Deserialize, Serialize}; 9 | use std::collections::{btree_map, BTreeMap}; 10 | use std::env; 11 | use std::str::FromStr; 12 | use std::sync::atomic; 13 | use std::sync::mpsc; 14 | use std::thread; 15 | use std::time::Duration; 16 | 17 | static CONTROLLER_STATS: Lazy> = Lazy::new(<_>::default); 18 | static WAIT_HANDLES: Lazy>>>> = Lazy::new(<_>::default); 19 | static STATS_TX: OnceCell>> = OnceCell::new(); 20 | static SHUTDOWN_FN: OnceCell> = OnceCell::new(); 21 | static STATUS_CHANGED: Condvar = Condvar::new(); 22 | static STATUS_MUTEX: Mutex<()> = Mutex::new(()); 23 | 24 | static STATUS: atomic::AtomicI16 = atomic::AtomicI16::new(Status::Inactive as i16); 25 | 26 | pub fn controller_stats() -> &'static Mutex { 27 | &CONTROLLER_STATS 28 | } 29 | 30 | pub const WAIT_STEP: Duration = Duration::from_secs(1); 31 | pub const SLEEP_STEP: Duration = Duration::from_millis(500); 32 | pub const SLEEP_STEP_ERR: Duration = Duration::from_secs(2); 33 | 34 | const STATS_CHANNEL_SIZE: usize = 100_000; 35 | 36 | pub(crate) fn init() { 37 | WAIT_HANDLES.lock().replace(<_>::default()); 38 | let (tx, rx) = mpsc::sync_channel::<(String, u16)>(STATS_CHANNEL_SIZE); 39 | STATS_TX.set(Mutex::new(tx)).unwrap(); 40 | spawn_service("stats", move || { 41 | while let Ok((name, jitter)) = rx.recv() { 42 | if let Some(entry) = CONTROLLER_STATS.lock().thread_stats.get_mut(&name) { 43 | entry.report_jitter(jitter); 44 | } 45 | } 46 | }); 47 | } 48 | 49 | /// # Panics 50 | /// 51 | /// Will panic if set twice 52 | pub fn on_shutdown(f: F) 53 | where 54 | F: Fn() + Send + Sync + 'static, 55 | { 56 | assert!( 57 | SHUTDOWN_FN.set(Box::new(f)).is_ok(), 58 | "Unable to set shutdown function. Has it been already set?" 59 | ); 60 | } 61 | 62 | pub(crate) fn shutdown() { 63 | set_status(Status::Stopping); 64 | if let Some(wait_handles) = WAIT_HANDLES.lock().take() { 65 | if let Some(f) = SHUTDOWN_FN.get() { 66 | for handle in wait_handles { 67 | let _ = handle.join(); 68 | } 69 | f(); 70 | } 71 | } else { 72 | warn!("no wait handles, is shutdown called twice?"); 73 | } 74 | set_status(Status::StopSyncing); 75 | } 76 | 77 | pub(crate) trait ConvX { 78 | fn as_u16_max(&self) -> u16; 79 | } 80 | 81 | macro_rules! impl_convx { 82 | ($t: ty) => { 83 | impl ConvX for $t { 84 | fn as_u16_max(&self) -> u16 { 85 | let val = *self; 86 | if val > <$t>::from(u16::MAX) { 87 | u16::MAX 88 | } else { 89 | val as u16 90 | } 91 | } 92 | } 93 | }; 94 | } 95 | 96 | impl_convx!(u32); 97 | impl_convx!(u64); 98 | 99 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, EnumStr)] 100 | #[repr(i16)] 101 | #[enumstr(rename_all = "UPPERCASE")] 102 | pub enum Status { 103 | Inactive = 0, // plc is launched 104 | Starting = 1, // plc is starting 105 | Syncing = 2, // inputs can run 106 | Preparing = 3, // programs can run 107 | Active = 100, // outputs can run 108 | Stopping = -1, // plc started shutdown, inputs and programs must quit 109 | StopSyncing = -2, // final data sync 110 | Stopped = -100, // outputs completed, PLC is stopped 111 | Unknown = -200, 112 | } 113 | 114 | #[inline] 115 | pub fn step_sleep() { 116 | sleep(SLEEP_STEP); 117 | } 118 | 119 | #[inline] 120 | pub fn sleep(duration: Duration) { 121 | thread::sleep(duration); 122 | } 123 | 124 | #[inline] 125 | pub fn step_sleep_err() { 126 | sleep(SLEEP_STEP_ERR); 127 | } 128 | 129 | pub fn thread_name() -> String { 130 | let th = thread::current(); 131 | if let Some(name) = th.name() { 132 | name.to_owned() 133 | } else { 134 | format!("{:?}", th.id()) 135 | } 136 | } 137 | 138 | #[derive(Eq, PartialEq, Copy, Clone, EnumStr)] 139 | #[enumstr(rename_all = "lowercase")] 140 | pub enum Kind { 141 | Input, 142 | Output, 143 | Program, 144 | Service, 145 | } 146 | 147 | impl Kind { 148 | fn thread_prefix(self) -> &'static str { 149 | match self { 150 | Kind::Input => "I", 151 | Kind::Output => "O", 152 | Kind::Program => "P", 153 | Kind::Service => "S", 154 | } 155 | } 156 | } 157 | 158 | pub enum Period { 159 | Interval(Duration), 160 | Trigger(triggered::Listener), 161 | } 162 | 163 | pub(crate) fn set_preparing_if_no_inputs() { 164 | if CONTROLLER_STATS.lock().input_threads_ready.is_empty() { 165 | set_status(Status::Preparing); 166 | } 167 | } 168 | 169 | pub(crate) fn set_active_if_no_inputs_and_programs() { 170 | let cs = CONTROLLER_STATS.lock(); 171 | if cs.program_threads_ready.is_empty() && cs.input_threads_ready.is_empty() { 172 | set_status(Status::Active); 173 | } 174 | } 175 | 176 | pub fn stop_if_no_output_or_sfn() { 177 | if CONTROLLER_STATS.lock().output_threads_stopped.is_empty() || SHUTDOWN_FN.get().is_none() { 178 | set_status(Status::Stopped); 179 | } 180 | } 181 | 182 | #[allow(clippy::struct_excessive_bools)] 183 | pub struct ControllerStats { 184 | input_threads_ready: BTreeMap, 185 | program_threads_ready: BTreeMap, 186 | output_threads_stopped: BTreeMap, 187 | inputs_ready: bool, 188 | programs_ready: bool, 189 | outputs_stopped: bool, 190 | pub(crate) thread_stats: BTreeMap, 191 | } 192 | 193 | #[derive(Default, Debug)] 194 | pub(crate) struct ThreadStats { 195 | iters: u32, 196 | jitter: Option, 197 | } 198 | 199 | impl ThreadStats { 200 | #[allow(clippy::cast_possible_truncation)] 201 | pub(crate) fn info(&self) -> Option { 202 | self.jitter.as_ref().map(|jitter| ThreadInfo { 203 | iters: self.iters, 204 | jitter_min: jitter.min, 205 | jitter_max: jitter.max, 206 | jitter_last: jitter.last, 207 | jitter_avg: (jitter.total / self.iters).as_u16_max(), 208 | }) 209 | } 210 | } 211 | 212 | #[derive(Serialize, Deserialize, Debug, Default)] 213 | pub struct ThreadInfo { 214 | pub iters: u32, 215 | pub jitter_min: u16, 216 | pub jitter_max: u16, 217 | pub jitter_last: u16, 218 | pub jitter_avg: u16, 219 | } 220 | 221 | #[derive(Default, Debug, Serialize)] 222 | struct JitterStats { 223 | min: u16, 224 | max: u16, 225 | last: u16, 226 | total: u32, 227 | } 228 | 229 | impl JitterStats { 230 | #[inline] 231 | fn new(jitter: u16) -> Self { 232 | Self { 233 | min: jitter, 234 | max: jitter, 235 | last: jitter, 236 | total: u32::from(jitter), 237 | } 238 | } 239 | } 240 | 241 | impl ThreadStats { 242 | #[inline] 243 | fn report_jitter(&mut self, jitter: u16) { 244 | let was_reset = if self.iters == u32::MAX { 245 | self.iters = 1; 246 | true 247 | } else { 248 | self.iters += 1; 249 | false 250 | }; 251 | if let Some(ref mut j_stats) = self.jitter { 252 | if j_stats.min > jitter { 253 | j_stats.min = jitter; 254 | } 255 | if j_stats.max < jitter { 256 | j_stats.max = jitter; 257 | } 258 | j_stats.last = jitter; 259 | let j32 = u32::from(jitter); 260 | if was_reset { 261 | j_stats.total = j32; 262 | } else if j_stats.total > u32::MAX - j32 { 263 | self.iters = 1; 264 | j_stats.total = j32; 265 | } else { 266 | j_stats.total += j32; 267 | } 268 | } else { 269 | self.jitter.replace(JitterStats::new(jitter)); 270 | } 271 | } 272 | pub(crate) fn reset(&mut self) { 273 | self.iters = 0; 274 | self.jitter.take(); 275 | } 276 | } 277 | 278 | #[inline] 279 | pub(crate) fn report_jitter(jitter: u16) { 280 | if STATS_TX 281 | .get() 282 | .unwrap() 283 | .lock() 284 | .try_send((thread_name(), jitter)) 285 | .is_err() 286 | { 287 | error!("CRITICAL: stats channel full"); 288 | } 289 | } 290 | 291 | impl Default for ControllerStats { 292 | fn default() -> Self { 293 | Self { 294 | input_threads_ready: <_>::default(), 295 | program_threads_ready: <_>::default(), 296 | output_threads_stopped: <_>::default(), 297 | inputs_ready: true, 298 | programs_ready: true, 299 | outputs_stopped: true, 300 | thread_stats: <_>::default(), 301 | } 302 | } 303 | } 304 | 305 | impl ControllerStats { 306 | fn register_thread_stats(&mut self, name: &str) -> EResult<()> { 307 | if let btree_map::Entry::Vacant(v) = self.thread_stats.entry(name.to_owned()) { 308 | v.insert(ThreadStats::default()); 309 | Ok(()) 310 | } else { 311 | Err(Error::busy(format!( 312 | "thread {} is already registered", 313 | name 314 | ))) 315 | } 316 | } 317 | fn register_input_thread(&mut self, name: &str) -> EResult<()> { 318 | self.register_thread_stats(name)?; 319 | self.input_threads_ready.insert(name.to_owned(), false); 320 | self.inputs_ready = false; 321 | Ok(()) 322 | } 323 | fn register_output_thread(&mut self, name: &str) -> EResult<()> { 324 | self.output_threads_stopped.insert(name.to_owned(), false); 325 | self.outputs_stopped = false; 326 | self.register_thread_stats(name) 327 | } 328 | fn register_program_thread(&mut self, name: &str) -> EResult<()> { 329 | self.register_thread_stats(name)?; 330 | self.program_threads_ready.insert(name.to_owned(), false); 331 | self.programs_ready = false; 332 | Ok(()) 333 | } 334 | fn register_service_thread(&mut self, name: &str) -> EResult<()> { 335 | self.register_thread_stats(name) 336 | } 337 | fn mark_input_thread_ready(&mut self) { 338 | if let Some(name) = thread::current().name() { 339 | if !self.inputs_ready && status() >= Status::Syncing { 340 | if self 341 | .input_threads_ready 342 | .insert(name.to_owned(), true) 343 | .is_none() 344 | { 345 | warn!("input thread {name} not registered"); 346 | } 347 | for v in self.input_threads_ready.values() { 348 | if !v { 349 | return; 350 | } 351 | } 352 | self.inputs_ready = true; 353 | set_status(Status::Preparing); 354 | if self.program_threads_ready.is_empty() { 355 | set_status(Status::Active); 356 | } 357 | } 358 | } 359 | } 360 | fn mark_program_thread_ready(&mut self) { 361 | if let Some(name) = thread::current().name() { 362 | if !self.programs_ready && status() >= Status::Preparing { 363 | if self 364 | .program_threads_ready 365 | .insert(name.to_owned(), true) 366 | .is_none() 367 | { 368 | warn!("program thread {name} not registered"); 369 | } 370 | for v in self.program_threads_ready.values() { 371 | if !v { 372 | return; 373 | } 374 | } 375 | self.programs_ready = true; 376 | set_status(Status::Active); 377 | } 378 | } 379 | } 380 | fn mark_output_thread_stopped(&mut self) { 381 | if !self.outputs_stopped { 382 | let name = thread_name(); 383 | if self 384 | .output_threads_stopped 385 | .insert(name.clone(), true) 386 | .is_none() 387 | { 388 | warn!("output thread {name} not registered"); 389 | } else { 390 | debug!("output thread {} stopped", name); 391 | } 392 | for v in self.output_threads_stopped.values() { 393 | if !v { 394 | return; 395 | } 396 | } 397 | self.outputs_stopped = true; 398 | set_status(Status::Stopped); 399 | } 400 | } 401 | pub fn current_thread_info(&self) -> Option { 402 | if let Some(name) = thread::current().name() { 403 | self.thread_info(name) 404 | } else { 405 | None 406 | } 407 | } 408 | pub fn thread_info(&self, name: &str) -> Option { 409 | if let Some(thread_stats) = self.thread_stats.get(name) { 410 | thread_stats.info() 411 | } else { 412 | None 413 | } 414 | } 415 | } 416 | 417 | #[inline] 418 | fn set_status(status: Status) { 419 | let _lock = STATUS_MUTEX.lock(); 420 | STATUS.store(status as i16, atomic::Ordering::Relaxed); 421 | info!("controller status: {}", status); 422 | STATUS_CHANGED.notify_all(); 423 | } 424 | 425 | #[inline] 426 | pub(crate) fn set_starting() { 427 | if status() != Status::Stopping { 428 | set_status(Status::Starting); 429 | } 430 | } 431 | 432 | #[inline] 433 | pub(crate) fn set_syncing() { 434 | if status() != Status::Stopping { 435 | set_status(Status::Syncing); 436 | } 437 | } 438 | 439 | #[inline] 440 | pub fn set_stopped() { 441 | set_status(Status::Stopped); 442 | } 443 | 444 | impl From for Status { 445 | fn from(s: i16) -> Status { 446 | match s { 447 | x if x == Status::Inactive as i16 => Status::Inactive, 448 | x if x == Status::Starting as i16 => Status::Starting, 449 | x if x == Status::Syncing as i16 => Status::Syncing, 450 | x if x == Status::Preparing as i16 => Status::Preparing, 451 | x if x == Status::Active as i16 => Status::Active, 452 | x if x == Status::Stopping as i16 => Status::Stopping, 453 | x if x == Status::StopSyncing as i16 => Status::StopSyncing, 454 | x if x == Status::Stopped as i16 => Status::Stopped, 455 | _ => Status::Unknown, 456 | } 457 | } 458 | } 459 | 460 | #[inline] 461 | pub fn status() -> Status { 462 | STATUS.load(atomic::Ordering::Relaxed).into() 463 | } 464 | 465 | #[inline] 466 | pub fn is_active() -> bool { 467 | status() == Status::Active 468 | } 469 | 470 | #[inline] 471 | fn mark_input_thread_ready() { 472 | CONTROLLER_STATS.lock().mark_input_thread_ready(); 473 | } 474 | 475 | #[inline] 476 | fn mark_program_thread_ready() { 477 | CONTROLLER_STATS.lock().mark_program_thread_ready(); 478 | } 479 | 480 | #[inline] 481 | fn mark_output_thread_stopped() { 482 | CONTROLLER_STATS.lock().mark_output_thread_stopped(); 483 | } 484 | 485 | #[inline] 486 | pub(crate) fn mark_thread_ready(kind: Kind) { 487 | match kind { 488 | Kind::Input => mark_input_thread_ready(), 489 | Kind::Program => mark_program_thread_ready(), 490 | _ => {} 491 | } 492 | } 493 | 494 | #[inline] 495 | fn can_run_inputs() -> bool { 496 | status() >= Status::Syncing 497 | } 498 | 499 | #[inline] 500 | fn can_run_programs() -> bool { 501 | status() >= Status::Preparing 502 | } 503 | 504 | #[inline] 505 | fn can_run_outputs() -> bool { 506 | let status = status(); 507 | status >= Status::Preparing || status <= Status::Stopping 508 | } 509 | 510 | pub(crate) fn wait_can_run_input() { 511 | while !can_run_inputs() { 512 | let mut lock = STATUS_MUTEX.lock(); 513 | let _ = STATUS_CHANGED.wait_for(&mut lock, WAIT_STEP); 514 | } 515 | } 516 | 517 | pub(crate) fn wait_can_run_output() { 518 | while !can_run_outputs() { 519 | let mut lock = STATUS_MUTEX.lock(); 520 | let _ = STATUS_CHANGED.wait_for(&mut lock, WAIT_STEP); 521 | } 522 | } 523 | 524 | pub(crate) fn wait_can_run_program() { 525 | while !can_run_programs() { 526 | let mut lock = STATUS_MUTEX.lock(); 527 | let _ = STATUS_CHANGED.wait_for(&mut lock, WAIT_STEP); 528 | } 529 | } 530 | 531 | /// On Linux alias for std::thread::spawn 532 | #[inline] 533 | pub fn spawn0(f: F) -> thread::JoinHandle 534 | where 535 | F: FnOnce() -> T + Send + 'static, 536 | T: Send + 'static, 537 | { 538 | thread::spawn(f) 539 | } 540 | 541 | /// Spawns a new thread/task 542 | /// 543 | /// If the PLC is already started, all tasks except service ones are ignored 544 | /// 545 | /// # Panics 546 | /// 547 | /// The function will panic if 548 | /// 549 | /// - the thread with such name is already registered 550 | /// 551 | /// - the thread name is more than 14 characters 552 | /// 553 | /// - the OS is unable to spawn the thread 554 | /// 555 | /// - the thread has invalid CPU id or priority if specified 556 | pub fn spawn(name: &str, kind: Kind, f: F) 557 | where 558 | F: FnOnce() + Send + 'static, 559 | { 560 | let status = status(); 561 | if status != Status::Inactive && status != Status::Starting && kind != Kind::Service { 562 | error!("can not spawn {}, the PLC is already running", name); 563 | return; 564 | } 565 | if let Some(wait_handles) = WAIT_HANDLES.lock().as_mut() { 566 | assert!( 567 | name.len() < 15, 568 | "task name MUST be LESS than 15 characters ({})", 569 | name 570 | ); 571 | let name = format!("{}{}", kind.thread_prefix(), name); 572 | match kind { 573 | Kind::Input => CONTROLLER_STATS 574 | .lock() 575 | .register_input_thread(&name) 576 | .unwrap(), 577 | Kind::Program => CONTROLLER_STATS 578 | .lock() 579 | .register_program_thread(&name) 580 | .unwrap(), 581 | Kind::Output => CONTROLLER_STATS 582 | .lock() 583 | .register_output_thread(&name) 584 | .unwrap(), 585 | Kind::Service => CONTROLLER_STATS 586 | .lock() 587 | .register_service_thread(&name) 588 | .unwrap(), 589 | } 590 | let var = format!("PLC_THREAD_AFFINITY_{}", name.replace('.', "__")); 591 | let affinity = env::var(var) 592 | .map(|aff| { 593 | aff.parse::() 594 | .unwrap_or_else(|e| panic!("UNABLE TO SET THREAD {} AFFINITY: {}", name, e)) 595 | }) 596 | .ok(); 597 | let mut builder = thread::Builder::new(); 598 | if let Some(ss) = crate::STACK_SIZE.get() { 599 | builder = builder.stack_size(*ss); 600 | } 601 | let handle = builder 602 | .name(name) 603 | .spawn(move || { 604 | if let Some(affinity) = affinity { 605 | let name = thread_name(); 606 | info!( 607 | "setting {} affinity to CPU {}, priority: {}", 608 | name, affinity.cpu_id, affinity.sched_priority 609 | ); 610 | core_affinity::set_for_current(core_affinity::CoreId { 611 | id: affinity.cpu_id, 612 | }); 613 | let res = unsafe { 614 | libc::sched_setscheduler( 615 | 0, 616 | libc::SCHED_RR, 617 | &libc::sched_param { 618 | sched_priority: affinity.sched_priority, 619 | }, 620 | ) 621 | }; 622 | assert!( 623 | res == 0, 624 | "UNABLE TO SET THREAD {} AFFINITY, error code: {}", 625 | name, 626 | res 627 | ); 628 | } 629 | f(); 630 | }) 631 | .unwrap(); 632 | if kind == Kind::Input || kind == Kind::Program { 633 | wait_handles.push(handle); 634 | } 635 | } 636 | } 637 | 638 | pub struct Affinity { 639 | pub cpu_id: usize, 640 | pub sched_priority: libc::c_int, 641 | } 642 | 643 | impl FromStr for Affinity { 644 | type Err = Error; 645 | fn from_str(s: &str) -> Result { 646 | let mut sp = s.split(','); 647 | let cpu_id: usize = sp 648 | .next() 649 | .unwrap() 650 | .parse() 651 | .map_err(|e| Error::invalid_params(format!("invalid task cpu id: {e}")))?; 652 | let sched_priority: libc::c_int = sp 653 | .next() 654 | .ok_or_else(|| Error::invalid_params("no priority specified"))? 655 | .parse() 656 | .map_err(|e| Error::invalid_params(format!("invalid task priority: {e}")))?; 657 | if let Some(s) = sp.next() { 658 | return Err(Error::invalid_params(format!( 659 | "extra affinity params not supported: {}", 660 | s 661 | ))); 662 | } 663 | if cpu_id >= cpus() { 664 | return Err(Error::invalid_params(format!("CPU not found: {}", cpu_id))); 665 | } 666 | if !(1..=99).contains(&sched_priority) { 667 | return Err(Error::invalid_params(format!( 668 | "invalid scheduler priority: {}", 669 | sched_priority 670 | ))); 671 | } 672 | Ok(Self { 673 | cpu_id, 674 | sched_priority, 675 | }) 676 | } 677 | } 678 | 679 | #[inline] 680 | pub fn spawn_input_loop(name: &str, interval: Duration, shift: Duration, f: F) 681 | where 682 | F: FnMut() + Send + 'static, 683 | { 684 | spawn_loop(name, interval, shift, Kind::Input, f); 685 | } 686 | 687 | #[inline] 688 | pub fn spawn_output_loop(name: &str, interval: Duration, shift: Duration, f: F) 689 | where 690 | F: FnMut() + Send + 'static, 691 | { 692 | spawn_loop(name, interval, shift, Kind::Output, f); 693 | } 694 | 695 | pub fn spawn_loop(name: &str, interval: Duration, shift: Duration, kind: Kind, mut f: F) 696 | where 697 | F: FnMut() + Send + 'static, 698 | { 699 | if kind == Kind::Output { 700 | spawn(name, Kind::Output, move || { 701 | let mut int = Loop::prepare_reported(interval, shift); 702 | loop { 703 | let last_sync = output_last_sync(); 704 | f(); 705 | if last_sync { 706 | break; 707 | } 708 | int.tick(); 709 | } 710 | mark_output_thread_stopped(); 711 | log_finished(); 712 | }); 713 | } else { 714 | spawn(name, kind, move || { 715 | let mut int = Loop::prepare_reported(interval, shift); 716 | loop { 717 | log_running(); 718 | f(); 719 | if need_stop(kind) { 720 | break; 721 | } 722 | int.tick(); 723 | } 724 | log_finished(); 725 | }); 726 | } 727 | } 728 | 729 | #[inline] 730 | fn log_running() { 731 | debug!("loop {} running", thread_name()); 732 | } 733 | 734 | #[inline] 735 | fn log_finished() { 736 | debug!("loop {} finished", thread_name()); 737 | } 738 | 739 | #[inline] 740 | fn need_stop(kind: Kind) -> bool { 741 | match kind { 742 | Kind::Input | Kind::Program => status() <= Status::Stopping, 743 | Kind::Output => status() <= Status::StopSyncing, 744 | Kind::Service => false, 745 | } 746 | } 747 | 748 | #[inline] 749 | fn output_last_sync() -> bool { 750 | status() == Status::StopSyncing 751 | } 752 | 753 | /// # Panics 754 | /// 755 | /// The function will panic if 756 | /// 757 | /// - the thread with such name is already registered 758 | /// 759 | /// - the thread name is more than 14 characters 760 | /// 761 | /// - the OS is unable to spawn the thread 762 | pub fn spawn_service(name: &str, f: F) 763 | where 764 | F: FnOnce() + Send + 'static, 765 | { 766 | spawn(name, Kind::Service, f); 767 | } 768 | 769 | /// # Panics 770 | /// 771 | /// The function will panic if 772 | /// 773 | /// - the thread with such name is already registered 774 | /// 775 | /// - the thread name is more than 14 characters 776 | /// 777 | /// - the OS is unable to spawn the thread 778 | pub fn spawn_program_loop(name: &str, prog: F, interval: Duration, shift: Duration) 779 | where 780 | F: Fn() + Send + 'static, 781 | { 782 | spawn(name, Kind::Program, move || { 783 | let mut int = Loop::prepare_reported(interval, shift); 784 | loop { 785 | log_running(); 786 | { 787 | if status() >= Status::Preparing { 788 | prog(); 789 | } 790 | } 791 | if need_stop(Kind::Program) { 792 | break; 793 | } 794 | int.tick(); 795 | } 796 | log_finished(); 797 | }); 798 | } 799 | 800 | pub fn spawn_stats_log(int: Duration) { 801 | spawn_service("stlog", move || { 802 | let mut stats_interval = Loop::prepare0(int); 803 | loop { 804 | stats_interval.tick(); 805 | let stats = CONTROLLER_STATS.lock(); 806 | for (name, t_stats) in &stats.thread_stats { 807 | log_thread_stats(name, t_stats); 808 | } 809 | } 810 | }); 811 | } 812 | 813 | fn log_thread_stats(name: &str, t_stats: &ThreadStats) { 814 | if let Some(info) = t_stats.info() { 815 | info!( 816 | "thread {} iters {}, jitter min: {}, max: {}, last: {}, avg: {}", 817 | name, info.iters, info.jitter_min, info.jitter_max, info.jitter_last, info.jitter_avg 818 | ); 819 | } 820 | } 821 | 822 | pub(crate) fn reset_thread_stats() { 823 | CONTROLLER_STATS 824 | .lock() 825 | .thread_stats 826 | .values_mut() 827 | .for_each(ThreadStats::reset); 828 | } 829 | --------------------------------------------------------------------------------