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