├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE.txt ├── README.md ├── allowed.json ├── certs ├── ca_chain │ ├── ca_cert.pem │ ├── client_cert.pem │ ├── client_csr.pem │ ├── client_key.pem │ ├── server_cert.pem │ ├── server_csr.pem │ └── server_key.pem └── self_signed │ ├── entity1_cert.pem │ ├── entity1_key.pem │ ├── entity2_cert.pem │ └── entity2_key.pem ├── ffi ├── bindings │ ├── c │ │ ├── .clang-format │ │ ├── CMakeLists.txt │ │ ├── client_example.c │ │ ├── client_example.cpp │ │ ├── server_example.c │ │ └── server_example.cpp │ ├── dotnet │ │ ├── examples │ │ │ ├── client │ │ │ │ ├── Program.cs │ │ │ │ └── client.csproj │ │ │ └── server │ │ │ │ ├── Program.cs │ │ │ │ └── server.csproj │ │ ├── rodbus-tests │ │ │ ├── IntegrationTest.cs │ │ │ └── rodbus-tests.csproj │ │ └── rodbus.sln │ └── java │ │ ├── examples │ │ ├── pom.xml │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── io │ │ │ └── stepfunc │ │ │ └── rodbus │ │ │ └── examples │ │ │ ├── ClientExample.java │ │ │ └── ServerExample.java │ │ ├── pom.xml │ │ └── rodbus-tests │ │ ├── pom.xml │ │ └── src │ │ └── test │ │ └── java │ │ └── io │ │ └── stepfunc │ │ └── rodbus │ │ └── tests │ │ └── IntegrationTest.java ├── rodbus-bindings │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── rodbus-ffi-java │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── lib.rs ├── rodbus-ffi │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── client.rs │ │ ├── database.rs │ │ ├── error.rs │ │ ├── ffi.rs │ │ ├── helpers │ │ ├── conversions.rs │ │ └── ext.rs │ │ ├── iterator.rs │ │ ├── lib.rs │ │ ├── list.rs │ │ ├── runtime.rs │ │ ├── server.rs │ │ └── tracing.rs └── rodbus-schema │ ├── Cargo.toml │ └── src │ ├── client.rs │ ├── common.rs │ ├── decoding.rs │ ├── lib.rs │ └── server.rs ├── guide ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── .gitignore │ ├── about │ │ ├── dependencies.mdx │ │ ├── guide.mdx │ │ ├── license.mdx │ │ ├── modbus.mdx │ │ └── versioning.mdx │ ├── api │ │ ├── client │ │ │ ├── requests.mdx │ │ │ ├── rtu_client.mdx │ │ │ ├── tcp_client.mdx │ │ │ └── tls_client.mdx │ │ ├── logging.mdx │ │ ├── runtime.mdx │ │ ├── server │ │ │ ├── database.mdx │ │ │ ├── rtu_server.mdx │ │ │ ├── tcp_server.mdx │ │ │ ├── tls_server.mdx │ │ │ └── write_handler.mdx │ │ └── tls.mdx │ ├── examples │ │ └── summary.mdx │ └── languages │ │ ├── bindings.mdx │ │ ├── c_lang.mdx │ │ ├── cpp_lang.mdx │ │ ├── csharp.mdx │ │ └── java.mdx ├── docusaurus.config.js ├── package.json ├── plugins │ ├── changelog │ │ └── index.js │ ├── mermaid │ │ └── index.js │ └── sample │ │ └── index.js ├── sidebars.js ├── sitedata.json ├── src │ ├── css │ │ └── custom.css │ ├── pages │ │ └── styles.module.css │ └── theme │ │ ├── Footer │ │ ├── index.tsx │ │ └── styles.module.css │ │ └── Mermaid.js ├── static │ ├── .nojekyll │ ├── images │ │ └── brand │ │ │ ├── favicon.png │ │ │ ├── footer-logo.svg │ │ │ └── logo.svg │ ├── img │ │ ├── favicon.ico │ │ ├── modbus_logo.jpg │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg │ └── index.html └── yarn.lock ├── packaging.json ├── rodbus-client ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── rodbus ├── Cargo.toml ├── README.md ├── examples │ ├── client.rs │ ├── perf.rs │ └── server.rs ├── src │ ├── channel.rs │ ├── client │ │ ├── channel.rs │ │ ├── ffi_channel.rs │ │ ├── listener.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ ├── requests │ │ │ ├── mod.rs │ │ │ ├── read_bits.rs │ │ │ ├── read_registers.rs │ │ │ ├── write_multiple.rs │ │ │ └── write_single.rs │ │ └── task.rs │ ├── common │ │ ├── bits.rs │ │ ├── buffer.rs │ │ ├── frame.rs │ │ ├── function.rs │ │ ├── mod.rs │ │ ├── parse.rs │ │ ├── phys.rs │ │ ├── serialize.rs │ │ └── traits.rs │ ├── constants.rs │ ├── decode.rs │ ├── error.rs │ ├── exception.rs │ ├── lib.rs │ ├── maybe_async.rs │ ├── retry.rs │ ├── serial │ │ ├── client.rs │ │ ├── frame.rs │ │ ├── mod.rs │ │ └── server.rs │ ├── server │ │ ├── address_filter.rs │ │ ├── handler.rs │ │ ├── mod.rs │ │ ├── request.rs │ │ ├── response.rs │ │ ├── task.rs │ │ └── types.rs │ ├── tcp │ │ ├── client.rs │ │ ├── frame.rs │ │ ├── mod.rs │ │ ├── server.rs │ │ └── tls │ │ │ ├── client.rs │ │ │ ├── mod.rs │ │ │ └── server.rs │ └── types.rs └── tests │ └── integration_test.rs └── sfio_logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | dependencies.json 3 | dependencies.txt 4 | 5 | # IDES 6 | .vscode 7 | .idea 8 | .vs 9 | *.iml 10 | 11 | # this is where criterion puts the benchmark report 12 | rodbus/target 13 | 14 | # Cargo config 15 | /.cargo/config.toml 16 | 17 | # generated c lib 18 | /ffi/bindings/c/build 19 | /ffi/bindings/c/generated 20 | 21 | # generated .NET lib 22 | /ffi/bindings/dotnet/rodbus 23 | /ffi/bindings/dotnet/**/bin 24 | /ffi/bindings/dotnet/**/obj 25 | ffi/bindings/dotnet/nupkg 26 | 27 | # Generated Java lib 28 | /ffi/bindings/java/.idea 29 | /ffi/bindings/java/rodbus 30 | /ffi/bindings/java/rodbus-jni 31 | ffi/bindings/java/**/target 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.4.0 ### 2 | * :wrench: Avoid task spawning in client FFI methods. See [#136](https://github.com/stepfunc/rodbus/pull/136). 3 | * :wrench: Requests in the client API now fail immediately while the client is connecting. 4 | 5 | ### 1.3.1 ### 6 | * :bug: Fix issue with master channels not properly exiting and thrashing CPU. See [#120](https://github.com/stepfunc/rodbus/issues/120). 7 | 8 | ### 1.3.0 ### 9 | * :wrench: Update to rustls 0.21 which allows peer names with IP addresses in the SAN extension. 10 | * :wrench: Move common TLS configuration to its own crate shared with our Modbus library. 11 | * :star: PEM parser now supports extracting PKCS#1 private keys, i.e. PEM files with `BEGIN RSA PRIVATE KEY`. 12 | * :book: Documentation improvements in the bindings via [oo-bindgen 0.8.3](https://github.com/stepfunc/oo_bindgen/blob/main/CHANGELOG.md). 13 | 14 | ### 1.2.0 ### 15 | * :star: Add a mechanism to the bindings to shut down the Runtime with a timeout. See [#110](https://github.com/stepfunc/rodbus/pull/110). 16 | 17 | ### 1.1.0 ### 18 | * :star: Enable TCP_NODELAY for client and server sockets. See [#99](https://github.com/stepfunc/rodbus/pull/99). 19 | * :star: Enable full link-time optimization (LTO) in release builds. See [#103](https://github.com/stepfunc/rodbus/pull/103). 20 | * :star: Add support for 3 MUSL Linux targets to C/C++ and .NET. See [#104](https://github.com/stepfunc/rodbus/pull/104). 21 | * :star: Use only dependencies from crates.io allowing first release there. See [#106](https://github.com/stepfunc/rodbus/pull/106). 22 | * :star: Internal refactoring to promote code reuse with DNP3. See: [#100](https://github.com/stepfunc/rodbus/pull/100), [#101](https://github.com/stepfunc/rodbus/pull/101), [#102](https://github.com/stepfunc/rodbus/pull/102). 23 | 24 | ### 1.0.0 ### 25 | * :star: Add Modbus Security (TLS) support. See [#52](https://github.com/stepfunc/rodbus/pull/52). 26 | * :star: Add RTU support. See [#56](https://github.com/stepfunc/rodbus/pull/56). 27 | * :star: Dynamic protocol decoding. See [#61](https://github.com/stepfunc/rodbus/pull/66). 28 | * :star: Resolve host names on client. See [#68](https://github.com/stepfunc/rodbus/pull/68). 29 | * :star: Add communication channel state callbacks. See [#77](https://github.com/stepfunc/rodbus/issues/77). 30 | * :star: TCP/TLS server can now filter incoming connections based on IP. See [#87](https://github.com/stepfunc/rodbus/pull/87). 31 | * :bug: Properly reset TCP connection retry timeout on success. See [#82](https://github.com/stepfunc/rodbus/issues/82). 32 | 33 | ### 0.9.1 ### 34 | * Client callbacks are now not blocking. 35 | See [#53](https://github.com/stepfunc/rodbus/pull/53). 36 | * :bug: Fix leak of `tracing::Span` in bindings. 37 | See [#53](https://github.com/stepfunc/rodbus/pull/53). 38 | * :star: Add Linux AArch64 support in Java and .NET. 39 | See [#51](https://github.com/stepfunc/rodbus/pull/51). 40 | 41 | ### 0.9.0 ### 42 | * :tada: First official release 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "rodbus", 6 | "rodbus-client", 7 | "ffi/rodbus-bindings", 8 | "ffi/rodbus-ffi", 9 | "ffi/rodbus-ffi-java", 10 | "ffi/rodbus-schema", 11 | ] 12 | 13 | [workspace.dependencies] 14 | oo-bindgen = "0.8.7" 15 | sfio-tokio-ffi = "0.9.0" 16 | sfio-tracing-ffi = "0.9.0" 17 | tokio = "1.37.0" 18 | tracing = "0.1.40" 19 | tracing-subscriber = { version = "0.3.18" } 20 | 21 | [workspace.package] 22 | authors = ["Step Function I/O LLC >"] 23 | rust-version = "1.75" 24 | edition = "2021" 25 | license-file = "LICENSE.txt" 26 | homepage = "https://stepfunc.io/products/libraries/modbus/" 27 | repository = "https://github.com/stepfunc/rodbus" 28 | keywords = ["dnp3", "ics", "scada", "security"] 29 | categories = ["network-programming"] 30 | 31 | [workspace.lints.rust] 32 | unsafe_code = "forbid" 33 | non_ascii_idents = "deny" 34 | unreachable_pub = "deny" 35 | trivial_casts = "deny" 36 | missing_docs = "deny" 37 | unused = { level = "deny", priority = -1 } 38 | missing_copy_implementations = "deny" 39 | 40 | [profile.release] 41 | lto=true 42 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build.env] 2 | passthrough = [ 3 | "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS", 4 | "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS", 5 | "CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABIHF_RUSTFLAGS", 6 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Step Function I/O](./sfio_logo.png) 2 | 3 | Commercial Modbus library by [Step Function I/O](https://stepfunc.io/). 4 | 5 | Please see the [README](rodbus/README.md) for information on the core Rust library. 6 | 7 | A user guide and documentation may be found [here](https://stepfunc.io/products/libraries/modbus/). 8 | 9 | Refer to the [CHANGELOG](CHANGELOG.md) for a release history. -------------------------------------------------------------------------------- /certs/ca_chain/ca_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | yDMKk5ZZlloz1TdkQ4dOXhJwEAq6Vik= 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /certs/ca_chain/client_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOjCCAyKgAwIBAgIBATANBgkqhkiG9w0BAQsFADBRMQswCQYDVQQGEwJVUzEP 3 | MA0GA1UECAwGT3JlZ29uMQ0wCwYDVQQHDARCZW5kMQ0wCwYDVQQKDARUZXN0MRMw 4 | EQYDVQQDDApETyBOT1QgVVNFMB4XDTIyMDcyMTE5NTIxMloXDTMyMDcxODE5NTIx 5 | MlowUTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjENMAsGA1UEBwwEQmVu 6 | ZDENMAsGA1UECgwEVGVzdDETMBEGA1UEAwwKRE8gTk9UIFVTRTCCAiIwDQYJKoZI 7 | hvcNAQEBBQADggIPADCCAgoCggIBALuPKeB30BPc94Tj2b+HZgxuO09c6glzEcox 8 | cdzzdMgG09LCY7bCwqYnq+eHkQr4bM7Y97mT1W3Gc9nrltoqh+BLJOYXknbg234J 9 | O68U2tk6F5sH7OmCuu/T0TCgnufNcElh8E5ZbVvlBzHleyqKRUbBV2/hLQOQHMsV 10 | 4qlM//Ch3OuuMVJHDOOobswLPk2TNiGl8MvGZgBVdb0Sr0XQ5HOu5bELsZbMxxcu 11 | TtGzragrgOoXLuBvEQnVMou3tGnc70NK9S+xdoHWQ3cr0OonkkxYfEDEivKgvHLU 12 | SlXQ80WqfIz8p1rIEAfC+LgyrAGTClnrkqIBX9Q6EtMsCaKhbgVUjE3DedI8IGxY 13 | L+YREkTwRVfoG/4nuC9T6qnv364gWU4nKOr7YlfMYJEjP+Lz9BPNZ2RTvKPC5djk 14 | pSgSno0Ybn+n6f2UGXiD5swjAV5AO1A3uuNYQ2EIvw9/KSlNtWRawbC+D2xWpdhZ 15 | I5F4Af4eFgE9jh7DSknyrywvtkKdoLnKhqoA55TNZ0Q+DybJaXEbybYAR7JhP3pA 16 | AI/PG8Z6HyClP4UQlwf5OS0QKC32H8CwthblWf5ijHjbEB1O5TShfSg4B6dIVwnh 17 | Auhr4Lw1feXWDfTNC4tycY5fD1a9scsptw8b/BPs6+etUk2YYT0w8rc2iBlE6cyh 18 | qMDpBothAgMBAAGjHTAbMBkGCysGAQQBg4kMhiIBBAoMCG9wZXJhdG9yMA0GCSqG 19 | SIb3DQEBCwUAA4ICAQC6XXRYO9F3uQyiEL56UU1m6KquXYleFhql3mbcT17r/pio 20 | HxZF0BJwdHzS1g9/BQkoIwsES4R2HHfaof39UJsHc3TzRAHAOvtFXSCdy31uo4rw 21 | s2ebLGtIiBR/X7aqX2qew+GWd2IWQ1dY2qUSViC0RlIOLTzUQ9tKdisWLfknufK2 22 | pdVQVE0gT6ejtKGRaUXDcygumxw65Qlhl1qdrqWCCXSfIg+1fv4jM2ODhywG8T5v 23 | 54/hOEFNpgAGkQRExkKVOMAMf1y5IJRHhdl0k4IX8vNgpx0IUTiz+7RdNG0zyx77 24 | IjJSqAXqT3WTqHZhrm2RuRMcF+oJrG+ddnTC/8ujshcRbYiDLCQlwxwpDQ0SCNvq 25 | LQMILdi2bMu3aBr/P6IIpqW6IqM6D5Ndjlm1B/Y/U9laOZstMzJ1DZCh44xt29ts 26 | UzBEMmizjX4kxC+Dt4P5joEzml6jn4VUjGxtKcfazMVEhUP7B8ziIjfAYofIxLr4 27 | R2MR9wFljH71Gg2cYkqQ+4FzjdSvXhopovck//2aXZtOQK3i64AJxkT0HUQ0uckP 28 | i+qgv3EsCc8+81+ZKnap/EDOujhUmRmJUnFtR6PfI/aznMi4pmB3LiNHSTrqUTVs 29 | 1+QSrbho2zf6mhli1stSSJ5IH3HXHNLoYc69vgFYROd5Na7VoYi0bzhzyr1kKQ== 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/ca_chain/client_csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEwjCCAqoCAQAwUTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjENMAsG 3 | A1UEBwwEQmVuZDENMAsGA1UECgwEVGVzdDETMBEGA1UEAwwKRE8gTk9UIFVTRTCC 4 | AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALuPKeB30BPc94Tj2b+HZgxu 5 | O09c6glzEcoxcdzzdMgG09LCY7bCwqYnq+eHkQr4bM7Y97mT1W3Gc9nrltoqh+BL 6 | JOYXknbg234JO68U2tk6F5sH7OmCuu/T0TCgnufNcElh8E5ZbVvlBzHleyqKRUbB 7 | V2/hLQOQHMsV4qlM//Ch3OuuMVJHDOOobswLPk2TNiGl8MvGZgBVdb0Sr0XQ5HOu 8 | 5bELsZbMxxcuTtGzragrgOoXLuBvEQnVMou3tGnc70NK9S+xdoHWQ3cr0OonkkxY 9 | fEDEivKgvHLUSlXQ80WqfIz8p1rIEAfC+LgyrAGTClnrkqIBX9Q6EtMsCaKhbgVU 10 | jE3DedI8IGxYL+YREkTwRVfoG/4nuC9T6qnv364gWU4nKOr7YlfMYJEjP+Lz9BPN 11 | Z2RTvKPC5djkpSgSno0Ybn+n6f2UGXiD5swjAV5AO1A3uuNYQ2EIvw9/KSlNtWRa 12 | wbC+D2xWpdhZI5F4Af4eFgE9jh7DSknyrywvtkKdoLnKhqoA55TNZ0Q+DybJaXEb 13 | ybYAR7JhP3pAAI/PG8Z6HyClP4UQlwf5OS0QKC32H8CwthblWf5ijHjbEB1O5TSh 14 | fSg4B6dIVwnhAuhr4Lw1feXWDfTNC4tycY5fD1a9scsptw8b/BPs6+etUk2YYT0w 15 | 8rc2iBlE6cyhqMDpBothAgMBAAGgLDAqBgkqhkiG9w0BCQ4xHTAbMBkGCysGAQQB 16 | g4kMhiIBBAoMCG9wZXJhdG9yMA0GCSqGSIb3DQEBCwUAA4ICAQAVGUsqFiugJzT0 17 | Reil8nxkAUDo0hqdep3EqkyG634+7lcQ2HMK+vaOO0OAtTzAd0CJ2c1u6TCnAKht 18 | Pd8GFfd5wPR7AT1JJ+uzMpst+BdMCuzddhhC7AOJO9Lu2PWcuvAzsXvNKE58dsJu 19 | KCxeCsJwY44fq41N5SKeKTMcunilHRurxhP05q+oyVxWHmyVsvKltZFVUXwNCU+v 20 | oDHs1nu+5XGN/fk24VL5PyCHy5W2ouy8pX0Az7MEOAGrrDECpe3Oe/t5LSnKW35B 21 | nhr3ncicClKtj7jsaAPETnF7TE4QCPvzbz60iDb6iweKqvwUN6LDCfAJS3HFBB3r 22 | eACBdhdCj4Gq4NeUcc/Ucc7TTO1sYxFPI1Ryqhc+V7LQxGatAHRznrBglrjCxOxV 23 | gW/YE7Oeo2M7/hqGxQVAwxT27HmILOke8ZNH/34PD0WeCxYpeZ8Qsx0o8rE9g3/f 24 | 5J0+9QyWqm9ECIOWnuKjirOZuUkNkv+9PQ5G35Eu7xe9ryX51ldBBpm2PTOMnQZA 25 | y1yGQU9hZo3+4EFz/XePRjXRnYDpykq8wSjlQhMisy8qx3EKnTk5MMSjoBCyli1b 26 | MoiFUaHLuXHtC+9Z93ghNL8z0fmIO+ZO+DWSxQt2DqL+dqdz4FKzQRiJsUE+ZFME 27 | gToo3A3HbFN0IEh/64PcRjWQRNVu5w== 28 | -----END CERTIFICATE REQUEST----- 29 | -------------------------------------------------------------------------------- /certs/ca_chain/client_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | ian9x9HmfAfLxWXWFyufnVxlDlRopMSU 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /certs/ca_chain/server_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFNDCCAxygAwIBAgIBAjANBgkqhkiG9w0BAQsFADBRMQswCQYDVQQGEwJVUzEP 3 | MA0GA1UECAwGT3JlZ29uMQ0wCwYDVQQHDARCZW5kMQ0wCwYDVQQKDARUZXN0MRMw 4 | EQYDVQQDDApETyBOT1QgVVNFMB4XDTIyMDcyMTE5NTIzNVoXDTMyMDcxODE5NTIz 5 | NVowUTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjENMAsGA1UEBwwEQmVu 6 | ZDENMAsGA1UECgwEVGVzdDETMBEGA1UEAwwKRE8gTk9UIFVTRTCCAiIwDQYJKoZI 7 | hvcNAQEBBQADggIPADCCAgoCggIBALop6eC8g4Sr+PlaR53WvyFuMVgWBqqHXFhT 8 | O+LobVWY1c/O6ivQS8LEzOndWF3W9XtJJ7mc015bMsJCD/BHoMjPWCZp+a7VZpCo 9 | 4YJmDZjc+YrzGnoZ0I9JZ4RIN33av8wl5xFjVRIaMARebrTpl2d70wqUU84VUXiO 10 | VS+OeCOPvfO2HjwMMgHmRamp00t5oeckKGbCWuA9C5g6Sc6cODGvAG6/zqmoOtKm 11 | ODtYRo7mku/WQKTG/BI2uP1+Ophr9cpVWdKXbYNKw6x2x0XVj01aQ11RyoBNExVN 12 | mZAnfJnIhqf4esitfazaexk0IY0n8mpNiJm026sA0QPlEgHrO4c2pyYvHq57ITLh 13 | 96kM8k2RfXc+5jHI35sa+GenAyH8T4f5FQVMd0z6Nr1HEO1yiDV04rTRMtTtE88E 14 | jHYVACI71gWAW+XfGBKP0wZPPJOEU5SqK/bzntSYzrg4W69Xslf/VqjIjp98bK5c 15 | 5R7k9JGx+WO/wuHgUixsrO9qop4SizjACnxlHozfMT302BEq4heWJIwpMhigt7FP 16 | vvzN772FENEK/0eslO3mKKk2Ygk7PiuWiNgxTUIPEQwvG+PJfR2GgCWCPgskfWps 17 | x6l+/P7FiFfFwSz11C7vLLabfW0ive9aK9OpJd0gPHG3zpX+j9rRxLeHXEYsY3A7 18 | d9wSqIk9AgMBAAGjFzAVMBMGA1UdEQQMMAqCCHRlc3QuY29tMA0GCSqGSIb3DQEB 19 | CwUAA4ICAQARdzs9J6/BrP0WqbsG9MKN5hVqNgcd/3gbOqexEl8qRiM1S2EgI238 20 | a+m9W6m3rrOsl8Fjwfux2XBSCqNviktBg4M7cVuUwusR91WyrEXVg0s0O8phfz2q 21 | lrJVAJ/RpihHp4AK80KOmmN0VHitLCVAxder7Shci393yBHeazCLy5AAc1aAIyjx 22 | YWG2FWNMZNjGfn3uqkPJdEMfhBlJ5BTYn8Gc4ttKn/7LrH2J+ZFOdHfJ0XSKHqcZ 23 | BvrZW/vG2oUl7ygxkgAtgsnVxc6PQQPWGgpbWixaaOxzVH/FIaof0/bxYBlqxti/ 24 | PRio38nl1A0f21Rzo9kZavRRhADmhq7fbQTxK78ka3LK1QQRp9n4J+4GeKAYW/jA 25 | fDzytUc6My5VlAHLZdSdp5KrBCNawlNZpWZOZoWDpjmsTA3XkSLZ8wnXRCqlJp0w 26 | fOkQyKvcvAy4OrcIeQCVrvfm+D/AQMm6Ux5EN5G3s85RllIlLeYeYlpHgOTMWGop 27 | O8P6yr5iXr4keTB2ZO1BwfHwlXOwKlA5vMJNeW0gJHfJTy3ILZ7yX3MyMg9C4kNd 28 | q/XSEM1DBLA7NEDg2ivDg1t8z63aydEee2WGlFB+YKQoif+lTdU+/eV0pRqh04QL 29 | YSHR9tQgP4TddYNbqEfdfQZT2+xlxXfEVGM5xhtEVgzwv3zf5cSdpw== 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/ca_chain/server_csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEvDCCAqQCAQAwUTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjENMAsG 3 | A1UEBwwEQmVuZDENMAsGA1UECgwEVGVzdDETMBEGA1UEAwwKRE8gTk9UIFVTRTCC 4 | AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALop6eC8g4Sr+PlaR53WvyFu 5 | MVgWBqqHXFhTO+LobVWY1c/O6ivQS8LEzOndWF3W9XtJJ7mc015bMsJCD/BHoMjP 6 | WCZp+a7VZpCo4YJmDZjc+YrzGnoZ0I9JZ4RIN33av8wl5xFjVRIaMARebrTpl2d7 7 | 0wqUU84VUXiOVS+OeCOPvfO2HjwMMgHmRamp00t5oeckKGbCWuA9C5g6Sc6cODGv 8 | AG6/zqmoOtKmODtYRo7mku/WQKTG/BI2uP1+Ophr9cpVWdKXbYNKw6x2x0XVj01a 9 | Q11RyoBNExVNmZAnfJnIhqf4esitfazaexk0IY0n8mpNiJm026sA0QPlEgHrO4c2 10 | pyYvHq57ITLh96kM8k2RfXc+5jHI35sa+GenAyH8T4f5FQVMd0z6Nr1HEO1yiDV0 11 | 4rTRMtTtE88EjHYVACI71gWAW+XfGBKP0wZPPJOEU5SqK/bzntSYzrg4W69Xslf/ 12 | VqjIjp98bK5c5R7k9JGx+WO/wuHgUixsrO9qop4SizjACnxlHozfMT302BEq4heW 13 | JIwpMhigt7FPvvzN772FENEK/0eslO3mKKk2Ygk7PiuWiNgxTUIPEQwvG+PJfR2G 14 | gCWCPgskfWpsx6l+/P7FiFfFwSz11C7vLLabfW0ive9aK9OpJd0gPHG3zpX+j9rR 15 | xLeHXEYsY3A7d9wSqIk9AgMBAAGgJjAkBgkqhkiG9w0BCQ4xFzAVMBMGA1UdEQQM 16 | MAqCCHRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4ICAQBr53hQxFuXzO95YddrXMB8 17 | P4R++Bl5UI44ODd9ILpNPPue0SmXavD74MDFPRwaJLNtP/Ju7e+Wn/8C8dAf6SWh 18 | 3d9q8SbMUHTl5VUnnf52fnpr7OiZ1nhQlJ3pkkokbZO/GVvYa6MMXuzSlZi/IM/q 19 | 6pqRQriCSK7BUaiuNL5lqKxeR8zA2Qw586AhN5ljupTlq17JKV5eDdbaPBZG0AZa 20 | KJaUVE+Fo/4tHYSU6qovps9wAduTl3w8kP9dR1lmwz/E8L2ErLYZlJPFG5PlZXee 21 | Dh0HgglZOSWl2641QucjUie43o4Uvmp41js7VOTa4RBZsq7zsLmdg8ctdjk9Syfw 22 | cOUyE+MK8As2D+vNOmXUKEFTb6LcwdA7lpFRiHP2XBq0RfaNndxhPxfKiF0LvwhU 23 | p4lKFlMQx1MxjcrrLqO57nWCM+bhnWw+1cr9aqMaEKVwobM1BTfIXQFbkGuTxcI4 24 | yDjoeEgGRbVT8ffLikTZocvOh6eU3m+J9zAs80pANduB+FTRrZ7mk9Xn4bFBtFpc 25 | oXeeyL6zuNB9GV5amEW4lvAfDRWYcxUE1+nWyUabKOc54oPxFPSoXUfK1Jsz44Lj 26 | wr6FMzPbFqz/KaHJ2izya7bd/FjP6mrIO8ZhXigtko6gutiwFW5/jFTnAFe+zEyB 27 | 5pq6cVpZyYznOaJK7PJj6Q== 28 | -----END CERTIFICATE REQUEST----- 29 | -------------------------------------------------------------------------------- /certs/ca_chain/server_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC6KengvIOEq/j5 3 | Wked1r8hbjFYFgaqh1xYUzvi6G1VmNXPzuor0EvCxMzp3Vhd1vV7SSe5nNNeWzLC 4 | Qg/wR6DIz1gmafmu1WaQqOGCZg2Y3PmK8xp6GdCPSWeESDd92r/MJecRY1USGjAE 5 | Xm606Zdne9MKlFPOFVF4jlUvjngjj73zth48DDIB5kWpqdNLeaHnJChmwlrgPQuY 6 | OknOnDgxrwBuv86pqDrSpjg7WEaO5pLv1kCkxvwSNrj9fjqYa/XKVVnSl22DSsOs 7 | dsdF1Y9NWkNdUcqATRMVTZmQJ3yZyIan+HrIrX2s2nsZNCGNJ/JqTYiZtNurANED 8 | 5RIB6zuHNqcmLx6ueyEy4fepDPJNkX13PuYxyN+bGvhnpwMh/E+H+RUFTHdM+ja9 9 | RxDtcog1dOK00TLU7RPPBIx2FQAiO9YFgFvl3xgSj9MGTzyThFOUqiv2857UmM64 10 | OFuvV7JX/1aoyI6ffGyuXOUe5PSRsfljv8Lh4FIsbKzvaqKeEos4wAp8ZR6M3zE9 11 | 9NgRKuIXliSMKTIYoLexT778ze+9hRDRCv9HrJTt5iipNmIJOz4rlojYMU1CDxEM 12 | LxvjyX0dhoAlgj4LJH1qbMepfvz+xYhXxcEs9dQu7yy2m31tIr3vWivTqSXdIDxx 13 | t86V/o/a0cS3h1xGLGNwO3fcEqiJPQIDAQABAoICAQCPSbF9TDwCgwd4jbdv0pl8 14 | Vr9eKF3rJZK7XR1MElANQTzY3U86d/HIMQfzvW3FlK2Pvgf4gq/iAVn5UTXJYKht 15 | x8H2lz0aB6ERGRux7XDrxrG/9EvdzT7M+tfVZmxnz1YvAMFwjb8t1sA8rJ6TWvQx 16 | L4qJqw8yIRB7NTp2BzeLT5kKG5P77fsToTTfWRqMDkvCh/8c0N13mSTyf3LNt0o9 17 | W5B+rd2YkibsegnwC4sLdIeGWKea7JmaP0p1upHJPdeKa9VeFTWyh3pCpY3Nv4m0 18 | JRSSoGC4DhrxG7Bl1y69gy253bRL/9UqlWOIS6rDZgrdLV5FyIIHhfJbCBarKW6/ 19 | 2rR6iGW9uwZ6tdHWVpHP5R90erUOzR9llLKskIvpklNyFXAa8B7dVhdg41HFQp8U 20 | xiSfJkgSZ4lLcVII6KlfoclmfqUL1TmvhZmXWKDPgqGdqmDJAWFzzVsRmFtMPYVM 21 | Us6AW7JEH60RD0jiePC7gkOXtYsGhQEnIOqpEwk5I1xJn7ZZZrPJKjTfe/IcMw+p 22 | Kb0HGiMaqAkWGuKQEYufvo/SxkyigyEFRSPtfztdsvJRewEnXUbYgOlFpafQhaJ4 23 | za9k1qVtpn7MJ49nQ1W1iPqJYee9I2R7qIpMfINeqaJ5uaPvIIkLu9dDEM/Zw+W5 24 | piDaXxCxvQBR7MCKyZULlQKCAQEA9bREwDj7KIErADyrBA6HwknYGwGfo4XdY5Xj 25 | Gn8c+5zKVMhT1zuPlaaSlQa3E5wSQMcp9Vc+LGX+wXuwC5GJXgMB6ILTxPZ6tr/l 26 | dCb9uVWS+d4Js4iuAqlWywsxLLJHgLPdcUYklyjBgjoyR4a/daLBkJqxetYvFGgu 27 | Mjt5eLVdVm9VxnYnTAB5O/AESBR+daCsZzPRy8OgPNv0yQX1I34qKe2OOJCLE7jg 28 | WRV5MiJBimhzH/R9IsN3CYtX54EHkBYKbbv6L9ccrzkVNBKmd+TyxJWuq6XwZb3f 29 | 6vtlGPVJY+dlEw0JZbymmuNe2HWE455BbddAIyYgtcsnzt/K8wKCAQEAwfbwfc+4 30 | SEF5XPRf/GTsxpznv3cgCv/9lxs3ntGljN2SbvxBzleCEiqUdKYf6rZn9syVYosC 31 | UexEWvi6FjBxEoSovVmFwsBwHn7ciNj/p+sCXRtobPkPSG12KAfUnfZYtj50dyqZ 32 | Yvz7jgUK/B9+9Z9SyietGE9YB756wpRR7uk+aUa8uphMqBWQEIyeJdcIELSXEoH1 33 | rR7o0EyKooPRmLHsKwKq8eKs9RKhDhiinmgFuuRubyCUXcl8m/uwjXxDg+kYgcHK 34 | vOSclZp7Kt8s6c6Pm3SWvZlY+kgbF33EeWiSKzOf6H/b65HZ0e42O0X/nWTLon6W 35 | 5PUwjxtTuMUHDwKCAQEAxvybwWFkZsVVYffAOKTb5cmQLy79bp13UrONHWRcwJmW 36 | c1taKzGdc4FXrUGm+0amsbeaj4t8WtCvpVralEPlkDG3Veq+PwGvGFoJJix9x586 37 | TQo5qSDEbHtgQ0hpJt2rctw4NwirkY/8R8+B8NN8M3UAdnAR1H2p20kUQLJSCiQ3 38 | 10yLlxMReEfZ52TW0tlmITVi6qHZAsEIAMHSWQtuILSIyTQOvlixieNsLjlp0eRU 39 | WBeHRRXoUkMjIKOtKVwiF9sBzyUx1ZTNMkt0lJuWHftZcUQYFgNp0bwYoGHA2pms 40 | OjCodulT9wT1mPMTWRKz8PO14ZM16kFytEg3kR9FaQKCAQBXkbk509ILLSQxx1Ke 41 | jKIpeSVX19xrmu9OjUcrONNZKZ+248SB+BgNQ5QKpgW63edKEUtQVhoSCm1B9RFu 42 | eyhRh/r2obg2GNOFsRi33+BsWzmR95HJwRgZRvIVWJOxTUr33HKQancdrcGUeMpg 43 | 0YdsNSYXhporY4cG2ARsXLIw/rTiqsECLUhOio2kKxgrVU2a3S1nea0FH/NUN3pP 44 | rNuv6IQLVwT+d6xGaMa1qoGgqq4Llp4VJcxWG2VatuMavYNtxor96hQx+7SGBlyd 45 | Gm0Ykd1b/eMgbl8xf+RbUrilZ4S2ZHRJb5BGZm7EzJtHX27sDFmzXxuY+umVcmz1 46 | +l4ZAoIBAGP/dP6SKegfw+fgK3qW8qnKH3iLPcp/YgrWE84wgVEmbsUXRFZfq14G 47 | 8BGOGs7EMx7NwL11hBZm1eXbjHBeuUaoMJuzY8t/czxz1L2V6/Rma81TFmGMMcj3 48 | dx2SzVROwObWnBRoLWcanZq2D75pHWpB1XLC0O1RQimY+vZqVlTfry340rzfS3tv 49 | YQeqevOkVrAnaiuMi8aGkN1qKr9rs6rlVyaE58+J/gRP9Ezh5fFbddvCzh9xnMk+ 50 | kJ9EQd0DYrCyEH3N/unGNs7H+hh7ag83TlgvH2Mo1OyvdjigUm8ri3VLA+z8HP+v 51 | 7qJyEbYl8J0r9y7vbSvNX+a5feNtPKo= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /certs/self_signed/entity1_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | gYs= 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /certs/self_signed/entity1_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | iZw18Ty570w7auehxdKuB5Ru5sBRfMA= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /certs/self_signed/entity2_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /certs/self_signed/entity2_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | UJa+KRHz7VI5J3RJ2h3+GNg2uIvn 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /ffi/bindings/c/.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: false 7 | AlignConsecutiveAssignments: false 8 | AlignConsecutiveDeclarations: false 9 | AlignEscapedNewlines: Right 10 | AlignOperands: true 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: Never 16 | AllowShortCaseLabelsOnASingleLine: false 17 | AllowShortFunctionsOnASingleLine: All 18 | AllowShortLambdasOnASingleLine: All 19 | AllowShortIfStatementsOnASingleLine: Never 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: MultiLine 25 | BinPackArguments: true 26 | BinPackParameters: true 27 | BraceWrapping: 28 | AfterCaseLabel: false 29 | AfterClass: false 30 | AfterControlStatement: false 31 | AfterEnum: false 32 | AfterFunction: false 33 | AfterNamespace: false 34 | AfterObjCDeclaration: false 35 | AfterStruct: false 36 | AfterUnion: false 37 | AfterExternBlock: false 38 | BeforeCatch: false 39 | BeforeElse: false 40 | IndentBraces: false 41 | SplitEmptyFunction: true 42 | SplitEmptyRecord: true 43 | SplitEmptyNamespace: true 44 | BreakBeforeBinaryOperators: None 45 | BreakBeforeBraces: Stroustrup 46 | BreakBeforeInheritanceComma: false 47 | BreakInheritanceList: BeforeColon 48 | BreakBeforeTernaryOperators: true 49 | BreakConstructorInitializersBeforeComma: false 50 | BreakConstructorInitializers: BeforeColon 51 | BreakAfterJavaFieldAnnotations: false 52 | BreakStringLiterals: true 53 | ColumnLimit: 160 54 | CommentPragmas: '^ IWYU pragma:' 55 | CompactNamespaces: false 56 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 57 | ConstructorInitializerIndentWidth: 4 58 | ContinuationIndentWidth: 4 59 | Cpp11BracedListStyle: true 60 | DeriveLineEnding: true 61 | DerivePointerAlignment: false 62 | DisableFormat: false 63 | ExperimentalAutoDetectBinPacking: false 64 | FixNamespaceComments: true 65 | ForEachMacros: 66 | - foreach 67 | - Q_FOREACH 68 | - BOOST_FOREACH 69 | IncludeBlocks: Preserve 70 | IncludeCategories: 71 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 72 | Priority: 2 73 | SortPriority: 0 74 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 75 | Priority: 3 76 | SortPriority: 0 77 | - Regex: '.*' 78 | Priority: 1 79 | SortPriority: 0 80 | IncludeIsMainRegex: '(Test)?$' 81 | IncludeIsMainSourceRegex: '' 82 | IndentCaseLabels: false 83 | IndentGotoLabels: true 84 | IndentPPDirectives: None 85 | IndentWidth: 4 86 | IndentWrappedFunctionNames: false 87 | JavaScriptQuotes: Leave 88 | JavaScriptWrapImports: true 89 | KeepEmptyLinesAtTheStartOfBlocks: true 90 | MacroBlockBegin: '' 91 | MacroBlockEnd: '' 92 | MaxEmptyLinesToKeep: 1 93 | NamespaceIndentation: None 94 | ObjCBinPackProtocolList: Auto 95 | ObjCBlockIndentWidth: 2 96 | ObjCSpaceAfterProperty: false 97 | ObjCSpaceBeforeProtocolList: true 98 | PenaltyBreakAssignment: 2 99 | PenaltyBreakBeforeFirstCallParameter: 19 100 | PenaltyBreakComment: 300 101 | PenaltyBreakFirstLessLess: 120 102 | PenaltyBreakString: 1000 103 | PenaltyBreakTemplateDeclaration: 10 104 | PenaltyExcessCharacter: 1000000 105 | PenaltyReturnTypeOnItsOwnLine: 60 106 | PointerAlignment: Right 107 | ReflowComments: true 108 | SortIncludes: true 109 | SortUsingDeclarations: true 110 | SpaceAfterCStyleCast: false 111 | SpaceAfterLogicalNot: false 112 | SpaceAfterTemplateKeyword: true 113 | SpaceBeforeAssignmentOperators: true 114 | SpaceBeforeCpp11BracedList: false 115 | SpaceBeforeCtorInitializerColon: true 116 | SpaceBeforeInheritanceColon: true 117 | SpaceBeforeParens: ControlStatements 118 | SpaceBeforeRangeBasedForLoopColon: true 119 | SpaceInEmptyBlock: false 120 | SpaceInEmptyParentheses: false 121 | SpacesBeforeTrailingComments: 1 122 | SpacesInAngles: false 123 | SpacesInConditionalStatement: false 124 | SpacesInContainerLiterals: true 125 | SpacesInCStyleCastParentheses: false 126 | SpacesInParentheses: false 127 | SpacesInSquareBrackets: false 128 | SpaceBeforeSquareBrackets: false 129 | Standard: Latest 130 | StatementMacros: 131 | - Q_UNUSED 132 | - QT_REQUIRE_VERSION 133 | TabWidth: 8 134 | UseCRLF: false 135 | UseTab: Never 136 | ... 137 | 138 | -------------------------------------------------------------------------------- /ffi/bindings/c/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | project(rodbus_c LANGUAGES C CXX) 4 | 5 | set(RODBUS_BACKUP_VERSION "1.0.0") 6 | 7 | # Determine the architecture 8 | if(WIN32 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64" AND CMAKE_SIZEOF_VOID_P EQUAL 8) 9 | set(RODBUS_RUST_TARGET "x86_64-pc-windows-msvc") 10 | elseif(UNIX AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64") 11 | set(RODBUS_RUST_TARGET "x86_64-unknown-linux-gnu") 12 | elseif(UNIX AND CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") 13 | set(RODBUS_RUST_TARGET "aarch64-unknown-linux-gnu") 14 | else() 15 | message(FATAL_ERROR "target architecture not supported by this CMake file") 16 | endif() 17 | 18 | # Find the Rodbus package 19 | if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/generated) 20 | message("Rodbus package is local") 21 | # Use the locally built library 22 | find_package(rodbus REQUIRED HINTS ${CMAKE_CURRENT_LIST_DIR}/generated/cmake) 23 | else() 24 | message("No local rodbus, fetching remote library version ${RODBUS_BACKUP_VERSION}") 25 | 26 | # Download the library from GitHub 27 | include(FetchContent) 28 | FetchContent_Declare( 29 | rodbus 30 | URL https://github.com/stepfunc/rodbus/releases/download/${RODBUS_BACKUP_VERSION}/rodbus-${RODBUS_BACKUP_VERSION}.zip 31 | ) 32 | 33 | FetchContent_GetProperties(rodbus) 34 | if(NOT rodbus_POPULATED) 35 | FetchContent_Populate(rodbus) 36 | find_package(rodbus REQUIRED HINTS ${rodbus_SOURCE_DIR}) 37 | endif() 38 | endif() 39 | 40 | # -------------- C examples ----------------- 41 | 42 | # C Client example 43 | add_executable(client_example ./client_example.c) 44 | target_link_libraries(client_example PRIVATE rodbus) 45 | 46 | # C Server example 47 | add_executable(server_example server_example.c) 48 | target_link_libraries(server_example PRIVATE rodbus) 49 | 50 | # -------------- C++ examples ----------------- 51 | 52 | # C++ Master example 53 | add_executable(cpp_client_example client_example.cpp) 54 | target_link_libraries(cpp_client_example PRIVATE rodbus_cpp) 55 | 56 | # C++ Outstation example 57 | add_executable(cpp_server_example server_example.cpp ) 58 | target_link_libraries(cpp_server_example PRIVATE rodbus_cpp) 59 | 60 | # Copy the DLL after build 61 | add_custom_command(TARGET client_example POST_BUILD 62 | COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ 63 | ) 64 | add_custom_command(TARGET server_example POST_BUILD 65 | COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ 66 | ) 67 | add_custom_command(TARGET cpp_client_example POST_BUILD 68 | COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ 69 | ) 70 | add_custom_command(TARGET cpp_server_example POST_BUILD 71 | COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ 72 | ) 73 | -------------------------------------------------------------------------------- /ffi/bindings/dotnet/examples/client/client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | False 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ffi/bindings/dotnet/examples/server/server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | False 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ffi/bindings/dotnet/rodbus-tests/rodbus-tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | rodbus_tests 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ffi/bindings/dotnet/rodbus.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30128.74 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "rodbus", "rodbus\rodbus.csproj", "{D37ECA5D-556F-46FD-B384-BFB5880B41CA}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{837D82AF-4C61-433A-A941-EBD2F8DBD521}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "client", "examples\client\client.csproj", "{DFE79CD9-AF28-4ADE-A82A-FCD8D927B075}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "server", "examples\server\server.csproj", "{067A724B-6709-4281-9537-C8A9C05F3C2C}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "rodbus-tests", "rodbus-tests\rodbus-tests.csproj", "{B1950703-2DA4-412B-AC12-106539F21C25}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {D37ECA5D-556F-46FD-B384-BFB5880B41CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {D37ECA5D-556F-46FD-B384-BFB5880B41CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {D37ECA5D-556F-46FD-B384-BFB5880B41CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {D37ECA5D-556F-46FD-B384-BFB5880B41CA}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {DFE79CD9-AF28-4ADE-A82A-FCD8D927B075}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {DFE79CD9-AF28-4ADE-A82A-FCD8D927B075}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {DFE79CD9-AF28-4ADE-A82A-FCD8D927B075}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {DFE79CD9-AF28-4ADE-A82A-FCD8D927B075}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {067A724B-6709-4281-9537-C8A9C05F3C2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {067A724B-6709-4281-9537-C8A9C05F3C2C}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {067A724B-6709-4281-9537-C8A9C05F3C2C}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {067A724B-6709-4281-9537-C8A9C05F3C2C}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {B1950703-2DA4-412B-AC12-106539F21C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {B1950703-2DA4-412B-AC12-106539F21C25}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {B1950703-2DA4-412B-AC12-106539F21C25}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {B1950703-2DA4-412B-AC12-106539F21C25}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(NestedProjects) = preSolution 43 | {DFE79CD9-AF28-4ADE-A82A-FCD8D927B075} = {837D82AF-4C61-433A-A941-EBD2F8DBD521} 44 | {067A724B-6709-4281-9537-C8A9C05F3C2C} = {837D82AF-4C61-433A-A941-EBD2F8DBD521} 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {010FC04F-2F42-450E-98C9-C45A514B58FC} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /ffi/bindings/java/examples/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.stepfunc.dnp3 6 | examples 7 | 1.0.0 8 | 9 | 10 | 1.8 11 | 1.8 12 | UTF-8 13 | 14 | 15 | 16 | 17 | io.stepfunc 18 | rodbus 19 | 1.4.0 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ffi/bindings/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.stepfunc 6 | rodbus-parent 7 | 1.0.0 8 | pom 9 | 10 | 11 | 1.8 12 | UTF-8 13 | 14 | 15 | 16 | 17 | 18 | org.apache.maven.plugins 19 | maven-compiler-plugin 20 | 3.8.1 21 | 22 | 1.8 23 | 1.8 24 | 25 | 26 | 27 | 28 | 29 | 30 | rodbus 31 | examples 32 | rodbus-tests 33 | 34 | -------------------------------------------------------------------------------- /ffi/bindings/java/rodbus-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | rodbus-parent 8 | io.stepfunc 9 | 1.0.0 10 | 11 | 4.0.0 12 | 13 | rodbus-tests 14 | 15 | 16 | 17 | 18 | org.apache.maven.plugins 19 | maven-surefire-plugin 20 | 3.0.0-M5 21 | 22 | 23 | 24 | 25 | 26 | 27 | io.stepfunc 28 | rodbus 29 | 1.4.0 30 | 31 | 32 | org.junit.jupiter 33 | junit-jupiter 34 | 5.6.2 35 | test 36 | 37 | 38 | org.assertj 39 | assertj-core 40 | 3.16.1 41 | test 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /ffi/rodbus-bindings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rodbus-bindings" 3 | version = "1.4.0" 4 | description = "application to generate bindings for Rodbus" 5 | readme = "../README.md" 6 | 7 | # inherit from workspace 8 | authors.workspace = true 9 | rust-version.workspace = true 10 | edition.workspace = true 11 | license-file.workspace = true 12 | homepage.workspace = true 13 | repository.workspace = true 14 | keywords.workspace = true 15 | categories.workspace = true 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [dependencies] 21 | oo-bindgen = { workspace = true } 22 | rodbus-schema = { path = "../rodbus-schema" } 23 | tracing = { workspace = true } 24 | tracing-subscriber = { workspace = true } 25 | 26 | -------------------------------------------------------------------------------- /ffi/rodbus-bindings/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Entry point to generate FFI bindings for Rodbus 2 | 3 | use std::path::PathBuf; 4 | use std::rc::Rc; 5 | 6 | fn main() { 7 | tracing_subscriber::fmt() 8 | .with_max_level(tracing::Level::INFO) 9 | .with_target(false) 10 | .init(); 11 | 12 | let library = rodbus_schema::build_lib().unwrap(); 13 | 14 | let builder_settings = oo_bindgen::cli::BindingBuilderSettings { 15 | ffi_target_name: "rodbus-ffi", 16 | jni_target_name: "rodbus-ffi-java", 17 | ffi_name: "rodbus_ffi", 18 | ffi_path: PathBuf::from("ffi/rodbus-ffi"), 19 | java_group_id: "io.stepfunc", 20 | destination_path: PathBuf::from("ffi/bindings"), 21 | library: Rc::new(library), 22 | }; 23 | 24 | oo_bindgen::cli::run(builder_settings); 25 | } 26 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi-java/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rodbus-ffi-java" 3 | version = "1.4.0" 4 | authors = ["Step Function I/O LLC "] 5 | edition = "2021" 6 | build = "build.rs" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | jni = "0.19" 13 | rodbus-ffi = { path = "../rodbus-ffi", default-features = false } 14 | 15 | [features] 16 | default = ["serial", "tls"] 17 | serial = ["rodbus-ffi/serial"] 18 | tls = ["rodbus-ffi/tls"] 19 | 20 | [build-dependencies] 21 | rodbus-schema = { path = "../rodbus-schema" } 22 | oo-bindgen = { workspace = true } 23 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi-java/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | fn main() { 4 | println!("cargo:rerun-if-changed=build.rs"); 5 | println!("cargo:rerun-if-changed=lib.rs"); 6 | 7 | // normally you'd never want to write files here, but this crate isn't used as a dependency 8 | let out_path: PathBuf = Path::new(&std::env::var_os("OUT_DIR").unwrap()).join("jni.rs"); 9 | 10 | let config = oo_bindgen::backend::java::JniBindgenConfig { 11 | group_id: "io.stepfunc", 12 | ffi_name: "rodbus_ffi", 13 | }; 14 | 15 | match rodbus_schema::build_lib() { 16 | Err(err) => { 17 | eprintln!("{err}"); 18 | std::process::exit(-1); 19 | } 20 | Ok(lib) => { 21 | oo_bindgen::backend::java::generate_jni(&out_path, &lib, &config).unwrap(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi-java/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::unused_unit, 3 | clippy::useless_conversion, 4 | clippy::redundant_closure, 5 | clippy::needless_borrow, 6 | clippy::let_unit_value, 7 | clippy::needless_return, 8 | clippy::not_unsafe_ptr_arg_deref, 9 | clippy::uninlined_format_args, 10 | unused_variables, 11 | dead_code 12 | )] 13 | // ^ these lints don't matter in the generated code 14 | 15 | include!(concat!(env!("OUT_DIR"), "/jni.rs")); 16 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rodbus-ffi" 3 | version = "1.4.0" 4 | authors = ["Step Function I/O LLC "] 5 | edition = "2021" 6 | description = "FFI for Rodbus" 7 | keywords = ["ffi", "c", "modbus", "industrial", "plc"] 8 | categories = ["network-programming"] 9 | repository = "https://github.com/stepfunc/rodbus" 10 | readme = "../README.md" 11 | 12 | [lib] 13 | crate-type = ["rlib", "cdylib"] 14 | 15 | [dependencies] 16 | lazy_static = "1.0" 17 | tracing = "0.1" 18 | tracing-core = "0.1" 19 | tracing-subscriber = { workspace = true, features = ["json", "chrono"] } 20 | rodbus = { path = "../../rodbus", default-features = false, features = ["ffi"] } 21 | tokio = { workspace = true, features = ["rt-multi-thread"]} 22 | num_cpus = "1" 23 | sfio-promise = "0.2" 24 | 25 | [build-dependencies] 26 | rodbus-schema = { path = "../rodbus-schema" } 27 | oo-bindgen = { workspace = true } 28 | sfio-tokio-ffi = { workspace = true } 29 | sfio-tracing-ffi = { workspace = true } 30 | 31 | 32 | [features] 33 | default = ["serial", "tls"] 34 | serial = ["rodbus/serial"] 35 | tls = ["rodbus/tls"] 36 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io::Write; 3 | use std::path::Path; 4 | 5 | fn write_tracing_ffi() { 6 | let mut file = 7 | std::fs::File::create(Path::new(&env::var_os("OUT_DIR").unwrap()).join("tracing.rs")) 8 | .unwrap(); 9 | file.write_all(sfio_tracing_ffi::get_impl_file().as_bytes()) 10 | .unwrap(); 11 | } 12 | 13 | fn write_tokio_ffi() { 14 | let mut file = 15 | std::fs::File::create(Path::new(&env::var_os("OUT_DIR").unwrap()).join("runtime.rs")) 16 | .unwrap(); 17 | file.write_all(sfio_tokio_ffi::get_impl_file().as_bytes()) 18 | .unwrap(); 19 | } 20 | 21 | fn main() { 22 | println!("cargo:rerun-if-changed=build.rs"); 23 | 24 | write_tracing_ffi(); 25 | write_tokio_ffi(); 26 | 27 | match rodbus_schema::build_lib() { 28 | Ok(lib) => { 29 | oo_bindgen::backend::rust::generate_ffi(&lib).unwrap(); 30 | } 31 | Err(err) => { 32 | eprintln!("rodbus model error: {err}"); 33 | std::process::exit(-1); 34 | } 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/src/error.rs: -------------------------------------------------------------------------------- 1 | use rodbus::{InvalidRange, InvalidRequest}; 2 | use std::net::AddrParseError; 3 | 4 | use crate::ffi; 5 | 6 | impl From for ffi::ParamError { 7 | fn from(_: AddrParseError) -> Self { 8 | ffi::ParamError::InvalidIpAddress 9 | } 10 | } 11 | 12 | impl From for ffi::ParamError { 13 | fn from(_: InvalidRange) -> Self { 14 | ffi::ParamError::InvalidRange 15 | } 16 | } 17 | 18 | impl From for ffi::ParamError { 19 | fn from(_: InvalidRequest) -> Self { 20 | ffi::ParamError::InvalidRequest 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/src/ffi.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/ffi.rs")); 2 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/src/helpers/ext.rs: -------------------------------------------------------------------------------- 1 | use crate::ffi; 2 | use rodbus::{BitIterator, RegisterIterator, RequestError}; 3 | 4 | impl<'a> sfio_promise::FutureType, rodbus::RequestError>> 5 | for ffi::BitReadCallback 6 | { 7 | fn on_drop() -> Result, rodbus::RequestError> { 8 | Err(rodbus::RequestError::Shutdown) 9 | } 10 | 11 | fn complete(self, result: Result) { 12 | match result { 13 | Ok(x) => { 14 | let mut iter = crate::iterator::BitValueIterator::new(x); 15 | self.on_complete(&mut iter); 16 | } 17 | Err(err) => { 18 | self.on_failure(err.into()); 19 | } 20 | } 21 | } 22 | } 23 | 24 | impl<'a> sfio_promise::FutureType, rodbus::RequestError>> 25 | for ffi::RegisterReadCallback 26 | { 27 | fn on_drop() -> Result, rodbus::RequestError> { 28 | Err(rodbus::RequestError::Shutdown) 29 | } 30 | 31 | fn complete(self, result: Result) { 32 | match result { 33 | Ok(x) => { 34 | let mut iter = crate::iterator::RegisterValueIterator::new(x); 35 | self.on_complete(&mut iter); 36 | } 37 | Err(err) => { 38 | self.on_failure(err.into()); 39 | } 40 | } 41 | } 42 | } 43 | 44 | impl sfio_promise::FutureType> for ffi::WriteCallback { 45 | fn on_drop() -> Result { 46 | Err(rodbus::RequestError::Shutdown) 47 | } 48 | 49 | fn complete(self, result: Result) { 50 | match result { 51 | Ok(_) => { 52 | self.on_complete(ffi::Nothing::Nothing); 53 | } 54 | Err(err) => { 55 | self.on_failure(err.into()); 56 | } 57 | } 58 | } 59 | } 60 | 61 | impl ffi::WriteResult { 62 | pub(crate) fn convert_to_result(self) -> Result<(), rodbus::ExceptionCode> { 63 | if self.success() { 64 | return Ok(()); 65 | } 66 | let ex = match self.exception() { 67 | ffi::ModbusException::Acknowledge => rodbus::ExceptionCode::Acknowledge, 68 | ffi::ModbusException::GatewayPathUnavailable => { 69 | rodbus::ExceptionCode::GatewayPathUnavailable 70 | } 71 | ffi::ModbusException::GatewayTargetDeviceFailedToRespond => { 72 | rodbus::ExceptionCode::GatewayTargetDeviceFailedToRespond 73 | } 74 | ffi::ModbusException::IllegalDataAddress => rodbus::ExceptionCode::IllegalDataAddress, 75 | ffi::ModbusException::IllegalDataValue => rodbus::ExceptionCode::IllegalDataValue, 76 | ffi::ModbusException::IllegalFunction => rodbus::ExceptionCode::IllegalFunction, 77 | ffi::ModbusException::MemoryParityError => rodbus::ExceptionCode::MemoryParityError, 78 | ffi::ModbusException::ServerDeviceBusy => rodbus::ExceptionCode::ServerDeviceBusy, 79 | ffi::ModbusException::ServerDeviceFailure => rodbus::ExceptionCode::ServerDeviceFailure, 80 | ffi::ModbusException::Unknown => rodbus::ExceptionCode::Unknown(self.raw_exception()), 81 | }; 82 | 83 | Err(ex) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/src/iterator.rs: -------------------------------------------------------------------------------- 1 | pub struct BitValueIterator<'a> { 2 | inner: rodbus::BitIterator<'a>, 3 | current: crate::ffi::BitValue, 4 | } 5 | 6 | impl<'a> BitValueIterator<'a> { 7 | pub(crate) fn new(inner: rodbus::BitIterator<'a>) -> Self { 8 | Self { 9 | inner, 10 | current: crate::ffi::BitValue { 11 | index: 0, 12 | value: false, 13 | }, 14 | } 15 | } 16 | } 17 | 18 | pub struct RegisterValueIterator<'a> { 19 | inner: rodbus::RegisterIterator<'a>, 20 | current: crate::ffi::RegisterValue, 21 | } 22 | 23 | impl<'a> RegisterValueIterator<'a> { 24 | pub(crate) fn new(inner: rodbus::RegisterIterator<'a>) -> Self { 25 | Self { 26 | inner, 27 | current: crate::ffi::RegisterValue { index: 0, value: 0 }, 28 | } 29 | } 30 | } 31 | 32 | pub(crate) unsafe fn bit_value_iterator_next( 33 | it: *mut crate::BitValueIterator, 34 | ) -> Option<&crate::ffi::BitValue> { 35 | match it.as_mut() { 36 | Some(it) => match it.inner.next() { 37 | Some(x) => { 38 | it.current.index = x.index; 39 | it.current.value = x.value; 40 | Some(&it.current) 41 | } 42 | None => None, 43 | }, 44 | None => None, 45 | } 46 | } 47 | 48 | pub(crate) unsafe fn register_value_iterator_next( 49 | it: *mut crate::RegisterValueIterator, 50 | ) -> Option<&crate::ffi::RegisterValue> { 51 | match it.as_mut() { 52 | Some(it) => match it.inner.next() { 53 | Some(x) => { 54 | it.current.index = x.index; 55 | it.current.value = x.value; 56 | Some(&it.current) 57 | } 58 | None => None, 59 | }, 60 | None => None, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | #![allow(dead_code)] 3 | 4 | mod client; 5 | mod database; 6 | mod error; 7 | mod iterator; 8 | mod list; 9 | mod runtime; 10 | mod server; 11 | mod tracing; 12 | 13 | pub(crate) mod helpers { 14 | // From implementations for FFI types 15 | mod conversions; 16 | // Additional impl for FFI types 17 | mod ext; 18 | } 19 | 20 | pub(crate) use crate::tracing::*; 21 | pub use client::*; 22 | pub use database::*; 23 | pub use iterator::*; 24 | pub use list::*; 25 | pub use runtime::*; 26 | pub use server::*; 27 | use std::str::Utf8Error; 28 | 29 | pub mod ffi; 30 | 31 | lazy_static::lazy_static! { 32 | static ref VERSION: std::ffi::CString = std::ffi::CString::new(rodbus::VERSION).unwrap(); 33 | } 34 | 35 | fn version() -> &'static std::ffi::CStr { 36 | &VERSION 37 | } 38 | 39 | // the From<> impls below are needed to map tracing and tokio ffi stuff to the actual errors used in this crate 40 | 41 | impl From for std::os::raw::c_int { 42 | fn from(_: crate::TracingInitError) -> Self { 43 | crate::ffi::ParamError::LoggingAlreadyConfigured.into() 44 | } 45 | } 46 | 47 | impl From for crate::ffi::ParamError { 48 | fn from(err: crate::runtime::RuntimeError) -> Self { 49 | match err { 50 | crate::runtime::RuntimeError::RuntimeDestroyed => { 51 | crate::ffi::ParamError::RuntimeDestroyed 52 | } 53 | crate::runtime::RuntimeError::CannotBlockWithinAsync => { 54 | crate::ffi::ParamError::RuntimeCannotBlockWithinAsync 55 | } 56 | crate::runtime::RuntimeError::FailedToCreateRuntime => { 57 | crate::ffi::ParamError::RuntimeCreationFailure 58 | } 59 | } 60 | } 61 | } 62 | 63 | impl From for std::os::raw::c_int { 64 | fn from(err: crate::runtime::RuntimeError) -> Self { 65 | let err: crate::ffi::ParamError = err.into(); 66 | err.into() 67 | } 68 | } 69 | 70 | impl From for crate::ffi::ParamError { 71 | fn from(_: Utf8Error) -> Self { 72 | Self::InvalidUtf8 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/src/list.rs: -------------------------------------------------------------------------------- 1 | pub struct BitList { 2 | pub(crate) inner: Vec, 3 | } 4 | 5 | pub(crate) unsafe fn bit_list_create(size_hint: u32) -> *mut crate::BitList { 6 | Box::into_raw(Box::new(BitList { 7 | inner: Vec::with_capacity(size_hint as usize), 8 | })) 9 | } 10 | 11 | pub(crate) unsafe fn bit_list_destroy(list: *mut crate::BitList) { 12 | if !list.is_null() { 13 | drop(Box::from_raw(list)); 14 | }; 15 | } 16 | 17 | pub(crate) unsafe fn bit_list_add(list: *mut crate::BitList, item: bool) { 18 | if let Some(list) = list.as_mut() { 19 | list.inner.push(item) 20 | } 21 | } 22 | 23 | pub struct RegisterList { 24 | pub(crate) inner: Vec, 25 | } 26 | 27 | pub(crate) unsafe fn register_list_create(size_hint: u32) -> *mut crate::RegisterList { 28 | Box::into_raw(Box::new(RegisterList { 29 | inner: Vec::with_capacity(size_hint as usize), 30 | })) 31 | } 32 | 33 | pub(crate) unsafe fn register_list_destroy(list: *mut crate::RegisterList) { 34 | if !list.is_null() { 35 | drop(Box::from_raw(list)); 36 | }; 37 | } 38 | 39 | pub(crate) unsafe fn register_list_add(list: *mut crate::RegisterList, item: u16) { 40 | if let Some(list) = list.as_mut() { 41 | list.inner.push(item) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/src/runtime.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/runtime.rs")); 2 | -------------------------------------------------------------------------------- /ffi/rodbus-ffi/src/tracing.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/tracing.rs")); 2 | -------------------------------------------------------------------------------- /ffi/rodbus-schema/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rodbus-schema" 3 | # this is the version that all the FFI libraries get, since it's in their schema 4 | version = "1.4.0" 5 | description = "oobindgen schema for Rodbus" 6 | readme = "../README.md" 7 | 8 | # inherit from workspace 9 | authors.workspace = true 10 | rust-version.workspace = true 11 | edition.workspace = true 12 | license-file.workspace = true 13 | homepage.workspace = true 14 | repository.workspace = true 15 | keywords.workspace = true 16 | categories.workspace = true 17 | 18 | [lints] 19 | workspace = true 20 | 21 | [dependencies] 22 | oo-bindgen = { workspace = true } 23 | sfio-tokio-ffi = { workspace = true } 24 | sfio-tracing-ffi = { workspace = true } 25 | -------------------------------------------------------------------------------- /ffi/rodbus-schema/src/decoding.rs: -------------------------------------------------------------------------------- 1 | use oo_bindgen::model::*; 2 | 3 | const NOTHING: &str = "nothing"; 4 | 5 | pub(crate) fn define(lib: &mut LibraryBuilder) -> BackTraced { 6 | let app_decode_level_enum = lib 7 | .define_enum("app_decode_level")? 8 | .push(NOTHING, "Decode nothing")? 9 | .push("function_code", "Decode the function code only")? 10 | .push("data_headers", "Decode the function code and the general description of the data")? 11 | .push( 12 | "data_values", 13 | "Decode the function code, the general description of the data and the actual data values", 14 | )? 15 | .doc( 16 | doc("Controls how transmitted and received message at the application layer are decoded at the INFO log level") 17 | .details("Application-layer messages are referred to as Protocol Data Units (PDUs) in the specification.") 18 | )? 19 | .build()?; 20 | 21 | let frame_decode_level_enum = lib 22 | .define_enum("frame_decode_level")? 23 | .push(NOTHING, "Log nothing")? 24 | .push("header", " Decode the header")? 25 | .push("payload", "Decode the header and the raw payload as hexadecimal")? 26 | .doc( 27 | doc("Controls how the transmitted and received frames are decoded at the INFO log level") 28 | .details("Transport-specific framing wraps the application-layer traffic. You'll see these frames called ADUs in the Modbus specification.") 29 | .details("On TCP, this is the MBAP decoding. On serial, this controls the serial line PDU.") 30 | )? 31 | .build()?; 32 | 33 | let phys_decode_level_enum = lib 34 | .define_enum("phys_decode_level")? 35 | .push(NOTHING, "Log nothing")? 36 | .push( 37 | "length", 38 | "Log only the length of data that is sent and received", 39 | )? 40 | .push( 41 | "data", 42 | "Log the length and the actual data that is sent and received", 43 | )? 44 | .doc("Controls how data transmitted at the physical layer (TCP, serial, etc) is logged")? 45 | .build()?; 46 | 47 | let app_field = Name::create("app")?; 48 | let frame_field = Name::create("frame")?; 49 | let physical_field = Name::create("physical")?; 50 | 51 | let decode_level_struct = lib.declare_universal_struct("decode_level")?; 52 | let decode_level_struct = lib.define_universal_struct(decode_level_struct)? 53 | .add(&app_field, app_decode_level_enum, "Controls decoding of the application layer (PDU)")? 54 | .add(&frame_field, frame_decode_level_enum, "Controls decoding of frames (MBAP / Serial PDU)")? 55 | .add(&physical_field, phys_decode_level_enum, "Controls the logging of physical layer read/write")? 56 | .doc("Controls the decoding of transmitted and received data at the application, frame, and physical layer")? 57 | .end_fields()? 58 | .add_full_initializer("build")? 59 | .begin_initializer("nothing", InitializerType::Static, "Initialize log levels to defaults which is to decode nothing")? 60 | .default_variant(&app_field, NOTHING)? 61 | .default_variant(&frame_field, NOTHING)? 62 | .default_variant(&physical_field, NOTHING)? 63 | .end_initializer()? 64 | .build()?; 65 | 66 | Ok(decode_level_struct) 67 | } 68 | -------------------------------------------------------------------------------- /ffi/rodbus-schema/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! FFI oo-bindgen schema for Rodbus 2 | 3 | use std::path::PathBuf; 4 | 5 | use crate::common::CommonDefinitions; 6 | use oo_bindgen::model::*; 7 | 8 | mod client; 9 | mod common; 10 | mod decoding; 11 | mod server; 12 | 13 | // derived from Cargo.toml 14 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 15 | 16 | /// Build the library schema 17 | pub fn build_lib() -> BackTraced { 18 | let info = LibraryInfo { 19 | description: "Safe and fast Modbus library".to_string(), 20 | project_url: "https://stepfunc.io/products/libraries/modbus/".to_string(), 21 | repository: "stepfunc/rodbus".to_string(), 22 | license_name: "Custom license".to_string(), 23 | license_description: [ 24 | "This library is provided under the terms of a non-commercial license.", 25 | "", 26 | "Please refer to the source repository for details:", 27 | "", 28 | "https://github.com/stepfunc/rodbus/blob/master/LICENSE.txt", 29 | "", 30 | "Please contact Step Function I/O if you are interested in commercial license:", 31 | "", 32 | "info@stepfunc.io", 33 | ] 34 | .iter() 35 | .map(|s| s.to_string()) 36 | .collect(), 37 | license_path: PathBuf::from("LICENSE.txt"), 38 | developers: vec![ 39 | DeveloperInfo { 40 | name: "J. Adam Crain".to_string(), 41 | email: "adam@stepfunc.io".to_string(), 42 | organization: "Step Function I/O".to_string(), 43 | organization_url: "https://stepfunc.io/".to_string(), 44 | }, 45 | DeveloperInfo { 46 | name: "Émile Grégoire".to_string(), 47 | email: "emile@stepfunc.io".to_string(), 48 | organization: "Step Function I/O".to_string(), 49 | organization_url: "https://stepfunc.io/".to_string(), 50 | }, 51 | ], 52 | logo_png: include_bytes!("../../../sfio_logo.png"), 53 | }; 54 | 55 | let settings = LibrarySettings::create( 56 | "rodbus", 57 | "rodbus", 58 | ClassSettings::default(), 59 | IteratorSettings::default(), 60 | CollectionSettings::default(), 61 | FutureSettings::default(), 62 | InterfaceSettings::default(), 63 | )?; 64 | 65 | let mut builder = LibraryBuilder::new(Version::parse(VERSION).unwrap(), info, settings); 66 | 67 | let common = CommonDefinitions::build(&mut builder)?; 68 | 69 | sfio_tracing_ffi::define(&mut builder, common.error_type.clone())?; 70 | 71 | client::build(&mut builder, &common)?; 72 | server::build(&mut builder, &common)?; 73 | 74 | let library = builder.build()?; 75 | 76 | Ok(library) 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | #[test] 82 | fn builds_library_without_error() { 83 | crate::build_lib().unwrap(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /guide/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # editor 23 | .idea 24 | -------------------------------------------------------------------------------- /guide/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /guide/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /guide/docs/.gitignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | -------------------------------------------------------------------------------- /guide/docs/about/dependencies.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: dependencies 3 | title: Open Source Dependencies 4 | sidebar_label: Dependency Licenses 5 | slug: /dependencies 6 | --- 7 | 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import sitedata from '../../sitedata.json' 10 | 11 | Rust's package manager (`cargo`) makes it easy to work with external dependencies. This is wonderful for development, but means that Rust applications 12 | and libraries tend to have many dependencies. 13 | 14 | Our library only depends directly on a handful of third-party libraries; however, those libraries pull in dozens of their own dependencies. Here's how we manage 15 | our direct and indirect dependencies. 16 | 17 | ## Dependency Whitelisting 18 | 19 | We use a dependency whitelist to ensure that we never incorporate dependencies into our builds unless they are manually approved. During each CI build, the following 20 | checks are performed: 21 | 22 | * Check every dependency against the whitelist. Our CI packaging will fail if add a dependency is added with a license that has not been pre-approved. 23 | * Produce a license report called `third-party-licenses.txt` that consolidates all the dependency and license information. We include this document in all of 24 | our binary distributions. 25 | * Ignore projects that are 100% copyrighted by Step Function I/O, e.g. the library itself and some dependencies we share between with our other protocol libraries. 26 | 27 | :::note 28 | The license report file differs slightly for the Java library as it incorporates some additional components for the JNI functionality. 29 | ::: 30 | 31 | 32 | ## Proprietary Compatible 33 | 34 | All dependencies of the library have licenses that are both mutually compatible and compatible with commercial/proprietary products. We don't allow the 35 | incorporation of strong copyleft licenses such as the GPL. You can see a complete list of allowed dependencies and licenses in dep_config.json. 36 | 37 | ## Disclaimer 38 | 39 | We've included this information because we take open source license compliance seriously. That said, this information and the `third-party-licenses.txt` file are provided for your reference and do not constitute legal advice. Treat this information as a starting point so you can perform your own due diligence. 40 | 41 | 42 | -------------------------------------------------------------------------------- /guide/docs/about/guide.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: guide 3 | title: About This Guide 4 | sidebar_label: Guide 5 | slug: /guide 6 | --- 7 | 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import sitedata from '../../sitedata.json' 10 | 11 | This guide provides the primary documentation for Step Function I/O's Modbus [library](https://github.com/stepfunc/rodbus). Here you'll find: 12 | 13 | * Which parts of Modbus the library implements 14 | * How to use the library's Application Programming Interface (API) 15 | * How the API maps to Modbus concepts 16 | * How to get pre-built binary distributions of the library for supported languages 17 | * How to build the core Rust library and bindings from the source 18 | 19 | You can find exhaustive details for every field, method, and argument to the language-specific API documentation here: 20 | 21 | * Rust (rustdoc) 22 | * C (doxygen) 23 | * C++ (doxygen) 24 | * Java (javadoc) 25 | * C# (doxygen) 26 | 27 | In addition to this documentation, you can find example programs in the source repository that demonstrate the most common API features for each language. 28 | 29 | ## Modbus Standard 30 | 31 | Please note that while this guide covers many Modbus concepts, it cannot replicate the Modbus standard itself. If you plan to develop a product that uses Modbus, 32 | you can get a free copy from the [Modbus Organization website](https://modbus.org/). 33 | 34 |
35 | Modbus logo 36 |
37 | -------------------------------------------------------------------------------- /guide/docs/about/license.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: license 3 | title: Non-Commercial License 4 | sidebar_label: License 5 | slug: /license 6 | --- 7 | 8 | We have made our library publicly available under a non-commercial/non-production license for the purposes of evaluation, research and development, and education. 9 | While we follow many of the same practices (public source/development), our library is not "open source" per the strict definition defined by the 10 | [Open Source Initiative](https://opensource.org/osd). 11 | 12 | We license the library for commercial applications using two different licensing models: 13 | 14 | * per-product royalty-free licensing 15 | * per-unit licensing 16 | 17 | Please contact us via our [website](https://stepfunc.io/contact/) for pricing. 18 | 19 | ## License Text 20 | 21 | ``` 22 | {{#include ../LICENSE.txt}} 23 | ``` 24 | -------------------------------------------------------------------------------- /guide/docs/about/versioning.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: versioning 3 | title: Versioning 4 | sidebar_label: Versioning 5 | slug: /versioning 6 | --- 7 | 8 | Our library follows the [semantic versioning](https://semver.org/) specification. Refer to the specification for the meaning of the `..` 9 | fields of the version number. 10 | 11 | import sitedata from '../../sitedata.json' 12 | 13 | <>The latest release (version {sitedata.version}) is available here. 14 | 15 | ## Changelog 16 | 17 | import Changelog from '../CHANGELOG.md'; 18 | 19 | 20 | -------------------------------------------------------------------------------- /guide/docs/api/client/rtu_client.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: rtu_client 3 | title: Serial RTU Client 4 | sidebar_label: Serial RTU Client 5 | slug: /api/client/rtu_client 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | You can create a RTU client channel using the `create_rtu_client` method. It will immediately try to open the serial port. 12 | 13 | :::note 14 | In Rust, you can use the `spawn_rtu_client_task` to create a channel and spawn the async task in the context of the current runtime. 15 | Outside the Tokio runtime, you can use `create_rtu_handle_and_task` and manually spawn the returned future. 16 | ::: 17 | 18 | :::info 19 | The library does **not** support sending broadcast requests yet. Sending a request with a broadcast unit ID 20 | (`0x00`) will generate a timeout error, because no server will respond. 21 | ::: 22 | 23 | 33 | 34 | 35 | ```rust 36 | {{#include ../rodbus/examples/client.rs:create_rtu_channel}} 37 | ``` 38 | 39 | 40 | 41 | 42 | ```c 43 | {{#include ../ffi/bindings/c/client_example.c:create_rtu_channel}} 44 | ``` 45 | 46 | 47 | 48 | 49 | ```cpp 50 | {{#include ../ffi/bindings/c/client_example.cpp:create_rtu_channel}} 51 | ``` 52 | 53 | 54 | 55 | 56 | ```java 57 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ClientExample.java:create_rtu_channel}} 58 | ``` 59 | 60 | 61 | 62 | 63 | ```csharp 64 | {{#include ../ffi/bindings/dotnet/examples/client/Program.cs:create_rtu_channel}} 65 | ``` 66 | 67 | 68 | 69 | 70 | ## Path 71 | 72 | A path to the serial device must be supplied. On Windows, it's generally something like `COM3`. On Linux, it's 73 | generally something like `/dev/ttyS3`. You need to have the adequate permissions to access these devices. 74 | 75 | ## Serial Port settings 76 | 77 | The serial port settings are the following: 78 | 79 | - Baud rate in bit per second 80 | - Data bits. Note that Modbus should use 8 data bits. 81 | - Stop bits 82 | - Parity 83 | - Flow control 84 | 85 | ## Maximum Queued Requests 86 | 87 | Each channel sends one request at a time and has a fixed-length buffer of requests to send. 88 | 89 | ## Retry Delay 90 | 91 | A serial channel tries to open the serial port as soon as it is created. If the serial port cannot be opened, the library 92 | automatically waits `retry_delay` before retrying to open the port. 93 | 94 | ## Decode Level 95 | 96 | See [logging configuration page](../logging.mdx#protocol-decoding) for more details. 97 | -------------------------------------------------------------------------------- /guide/docs/api/client/tcp_client.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: tcp_client 3 | title: TCP Client 4 | sidebar_label: TCP Client 5 | slug: /api/client/tcp_client 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | The `ClientChannel` class represents a communication channel on which you can make requests to server device. `Channel` presents the same interface once you create it, 12 | regardless of the underlying transport. You can create a TCP client channel using `create_tcp` method. 13 | 14 | :::note 15 | In Rust, you can use the `spawn_tcp_client_task` to create a channel and spawn the runner task in the current runtime. 16 | Otherwise, you can use `create_tcp_handle_and_task` and manually spawn the returned future when ready. 17 | ::: 18 | 19 | 29 | 30 | 31 | ```rust 32 | {{#include ../rodbus/examples/client.rs:create_tcp_channel}} 33 | ``` 34 | 35 | 36 | 37 | 38 | ```c 39 | {{#include ../ffi/bindings/c/client_example.c:create_tcp_channel}} 40 | ``` 41 | 42 | 43 | 44 | 45 | ```cpp 46 | {{#include ../ffi/bindings/c/client_example.cpp:create_tcp_channel}} 47 | ``` 48 | 49 | 50 | 51 | 52 | ```java 53 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ClientExample.java:create_tcp_channel}} 54 | ``` 55 | 56 | 57 | 58 | 59 | ```csharp 60 | {{#include ../ffi/bindings/dotnet/examples/client/Program.cs:create_tcp_channel}} 61 | ``` 62 | 63 | 64 | 65 | 66 | ## Maximum Queued Requests 67 | 68 | Each channel sends one request at a time and has a fixed-length buffer of requests to send. 69 | 70 | ## Endpoint Configuration 71 | 72 | The argument for the remote endpoint is a string in format the `:` where "host" must be one of the following: 73 | 74 | * IPv4 address 75 | * IPv6 address 76 | * DNS name (library will perform DNS name resolution internally) 77 | 78 | ## Retry Strategy 79 | 80 | A TCP channel tries to establish and maintain a connection as soon as it is created. To avoid flooding, reconnection delays are applied. 81 | 82 | The `RetryStrategy` controls the rate at which the client retries failed connection attempts. The client uses exponential backoff when attempting to establish 83 | a connection. The delay between attempts doubles from `min_delay` up to `max_delay`. 84 | 85 | ## Decode Level 86 | 87 | See [logging configuration page](../logging.mdx#protocol-decoding) for more details. 88 | -------------------------------------------------------------------------------- /guide/docs/api/client/tls_client.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: tls_client 3 | title: TLS Client 4 | sidebar_label: TLS Client 5 | slug: /api/master/tls_client 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | Creating a TLS client is exactly the same process as a [TCP client](./tcp_client.mdx), 12 | except that an extra `TlsClientConfig` is required. For more details about TLS support and the configuration options, 13 | check the [TLS general information](../tls.mdx) page. 14 | 15 | ## Examples 16 | 17 | ### Certificate chain configuration 18 | 19 | 29 | 30 | 31 | ```rust 32 | {{#include ../rodbus/examples/client.rs:tls_ca_chain_config}} 33 | 34 | {{#include ../rodbus/examples/client.rs:create_tls_channel}} 35 | ``` 36 | 37 | 38 | 39 | 40 | ```c 41 | {{#include ../ffi/bindings/c/client_example.c:tls_ca_chain_config}} 42 | 43 | {{#include ../ffi/bindings/c/client_example.c:create_tls_channel}} 44 | // check error 45 | ``` 46 | 47 | 48 | 49 | 50 | ```cpp 51 | {{#include ../ffi/bindings/c/client_example.cpp:tls_ca_chain_config}} 52 | 53 | {{#include ../ffi/bindings/c/client_example.cpp:create_tls_channel}} 54 | ``` 55 | 56 | 57 | 58 | 59 | ```java 60 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ClientExample.java:tls_ca_chain_config}} 61 | 62 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ClientExample.java:create_tls_channel}} 63 | ``` 64 | 65 | 66 | 67 | 68 | ```csharp 69 | {{#include ../ffi/bindings/dotnet/examples/client/Program.cs:tls_ca_chain_config}} 70 | 71 | {{#include ../ffi/bindings/dotnet/examples/client/Program.cs:create_tls_channel}} 72 | ``` 73 | 74 | 75 | 76 | 77 | ### Self-signed certificate configuration 78 | 79 | 89 | 90 | 91 | ```rust 92 | {{#include ../rodbus/examples/client.rs:tls_self_signed_config}} 93 | 94 | {{#include ../rodbus/examples/client.rs:create_tls_channel}} 95 | ``` 96 | 97 | 98 | 99 | 100 | ```c 101 | {{#include ../ffi/bindings/c/client_example.c:tls_self_signed_config}} 102 | 103 | {{#include ../ffi/bindings/c/client_example.c:create_tls_channel}} 104 | // check error 105 | ``` 106 | 107 | 108 | 109 | 110 | ```cpp 111 | {{#include ../ffi/bindings/c/client_example.cpp:tls_self_signed_config}} 112 | 113 | {{#include ../ffi/bindings/c/client_example.cpp:create_tls_channel}} 114 | ``` 115 | 116 | 117 | 118 | 119 | ```java 120 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ClientExample.java:tls_self_signed_config}} 121 | 122 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ClientExample.java:create_tls_channel}} 123 | ``` 124 | 125 | 126 | 127 | 128 | ```csharp 129 | {{#include ../ffi/bindings/dotnet/examples/client/Program.cs:tls_self_signed_config}} 130 | 131 | {{#include ../ffi/bindings/dotnet/examples/client/Program.cs:create_tls_channel}} 132 | ``` 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /guide/docs/api/logging.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: logging 3 | title: Logging 4 | sidebar_label: Logging 5 | slug: /api/logging 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | The library provides highly-contextual logging using the [tracing](https://crates.io/crates/tracing) crate. If you're using Rust, refer to the 12 | tracing documentation for details. 13 | 14 | In comparison, the bindings use a rigid logging interface with a single callback method to record a message. Configurable options include: 15 | 16 | * `LogLevel` that controls which messages are generated 17 | * How and if to print the time as part of the message 18 | * Line or JSON based output 19 | 20 | :::note 21 | The LogLevel is set to Info by default. This will record Info, Warn, and Error messages. The Debug and Trace levels are generally only useful if debugging an issue with the underlying runtime. 22 | 23 | Protocol decoding is always logged at the Info level and is configured separately on a per channel basis. 24 | ::: 25 | 26 | ## Configuration 27 | 28 | 38 | 39 | 40 | ```rust 41 | {{#include ../rodbus/examples/client.rs:logging}} 42 | ``` 43 | 44 | 45 | 46 | 47 | ```c 48 | {{#include ../ffi/bindings/c/client_example.c:logging_callback}} 49 | 50 | {{#include ../ffi/bindings/c/client_example.c:logging_init}} 51 | ``` 52 | 53 | 54 | 55 | 56 | ```cpp 57 | {{#include ../ffi/bindings/c/client_example.cpp:logging_callback}} 58 | 59 | {{#include ../ffi/bindings/c/client_example.cpp:logging_init}} 60 | ``` 61 | 62 | 63 | 64 | 65 | ```java 66 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ClientExample.java:logging_interface}} 67 | 68 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ClientExample.java:logging_init}} 69 | ``` 70 | 71 | 72 | 73 | 74 | ```csharp 75 | {{#include ../ffi/bindings/dotnet/examples/client/Program.cs:logging_interface}} 76 | 77 | {{#include ../ffi/bindings/dotnet/examples/client/Program.cs:logging_init}} 78 | ``` 79 | 80 | 81 | 82 | 83 | :::note 84 | The bindings use the [tracing_subscriber](https://crates.io/crates/tracing-subscriber) crate internally. If you use Rust, you can pick which tracing backend to 85 | use. 86 | ::: 87 | 88 | ## Example Output 89 | 90 | The logs provide a wealth of contextual metadata so you can: 91 | 92 | * Determine which communication session produced the message 93 | * Understand what state the software was in when the event occurred 94 | 95 | ``` 96 | Jun 21 10:33:01.608 INFO Modbus-Server-TCP{listen=127.0.0.1:502}: accepted connection 0 from: 127.0.0.1:1143 97 | Jun 21 10:33:01.610 INFO Modbus-Server-TCP{listen=127.0.0.1:502}:Session{remote=127.0.0.1:1143}:Transaction{tx_id=0x00}: PDU RX - READ DISCRETE INPUTS (0x02) start: 0x0000 qty: 10 98 | Jun 21 10:33:01.611 INFO Modbus-Server-TCP{listen=127.0.0.1:502}:Session{remote=127.0.0.1:1143}:Transaction{tx_id=0x00}: PDU TX - READ DISCRETE INPUTS (0x02) start: 0x0000 qty: 10 99 | idx: 0x0000 value: 0 100 | idx: 0x0001 value: 0 101 | idx: 0x0002 value: 0 102 | idx: 0x0003 value: 0 103 | idx: 0x0004 value: 0 104 | idx: 0x0005 value: 0 105 | idx: 0x0006 value: 0 106 | idx: 0x0007 value: 0 107 | idx: 0x0008 value: 0 108 | idx: 0x0009 value: 0 109 | Jun 21 10:33:01.617 INFO shutdown session: 0 110 | ``` 111 | 112 | ## Protocol Decoding 113 | 114 | Protocol decoding is configurable on a per-communication channel basis, such as all of the traffic on a TCP socket or a serial port. You can specify the 115 | `DecodeLevel` when you create a client or a server. This struct controls the level of decoding (including none) that takes place for each layer of the 116 | protocol stack, including: 117 | 118 | * Protocol Data Unit (PDU) function code, data headers, and data values 119 | * Application Data Unit (ADU) transport-dependent logging. On TCP channels, this controls the MBAP decoding 120 | * Physical-layer length and data bytes 121 | 122 | Refer to the language-specific API documentation for the meaning of each enumeration value. 123 | 124 | :::note 125 | Protocol decoding is always output at the *Info* log level. If left enabled, it can be too verbose in a production system. When you're debugging a communication issue, 126 | try adjusting the application-layer decoding first to gain visibility into the messages being exchanged on one channel at a time. 127 | ::: 128 | 129 | 130 | -------------------------------------------------------------------------------- /guide/docs/api/server/database.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: database 3 | title: Database 4 | sidebar_label: Database 5 | slug: /api/outstation/database 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | You can use the `Database` class to manipulate the point values that the server exposes to the clients. Note that while it's called a "database", it's really just 12 | a thread-safe data structure in memory. 13 | 14 | :::note 15 | For maximum versatility, the Rust interface does not provide a database implementation. The generic `RequestHandler` trait is used to get the values to return to the client. 16 | However, in most cases, you would just store the values in a `Vec` or `HashMap`. 17 | ::: 18 | 19 | ## Transactions 20 | 21 | All modifications to the internal database are done inside a transaction. All the changes are applied atomically at the end of the 22 | transaction. This makes it impossible for clients to read inconsistent state in a single request. 23 | 24 | A transaction can be started on a running server with the `update` method. Inside the transaction, any operations can be performed. 25 | They will be executed in sequence. 26 | 27 | :::warning 28 | Because the transaction mechanism acquires a lock on a mutex, it is important to keep each transaction as short as possible. Never perform a blocking operation 29 | inside a database transaction. 30 | ::: 31 | 32 | ## Database Initialization 33 | 34 | When adding a device to the `DeviceMap`, an initialization transaction must be specified. This is usually used to add the points 35 | to the database so that when the server is actually created, it can immediately report valid values. 36 | 37 | 46 | 47 | 48 | ```c 49 | // initialize 10 of every point type 50 | {{#include ../ffi/bindings/c/server_example.c:configure_db}} 51 | 52 | {{#include ../ffi/bindings/c/server_example.c:device_map_init}} 53 | ``` 54 | 55 | 56 | 57 | 58 | ```cpp 59 | // initialize 10 of every point type 60 | {{#include ../ffi/bindings/c/server_example.cpp:device_map_init}} 61 | ``` 62 | 63 | 64 | 65 | 66 | ```java 67 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:device_map_init}} 68 | ``` 69 | 70 | 71 | 72 | 73 | ```csharp 74 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:device_map_init}} 75 | ``` 76 | 77 | 78 | 79 | 80 | ## Updating Points 81 | 82 | You can update a point value by calling one of the `update_xxx` method on the `Database` object inside a transaction. The returned boolean 83 | indicates if the update was successful (i.e. the point existed). 84 | 85 | 94 | 95 | 96 | ```c 97 | {{#include ../ffi/bindings/c/server_example.c:update_coil_callback}} 98 | 99 | {{#include ../ffi/bindings/c/server_example.c:update_coil}} 100 | ``` 101 | 102 | 103 | 104 | 105 | ```cpp 106 | {{#include ../ffi/bindings/c/server_example.cpp:update_coil}} 107 | ``` 108 | 109 | 110 | 111 | 112 | ```java 113 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:update_coil}} 114 | ``` 115 | 116 | 117 | 118 | 119 | ```csharp 120 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:update_coil}} 121 | ``` 122 | 123 | 124 | 125 | 126 | ## Getting Point Values 127 | 128 | You may also use the `Database` as a cache of the most recent value if desired. Each type has a getter method to retrieve the most recently assigned value. 129 | 130 | :::note 131 | Since the point may not be defined, the getters can fail. If you try to retrieve a point that doesn't exist using Java and C#, an exception will be thrown. 132 | ::: 133 | 134 | ## Removing Points 135 | 136 | Most applications don't need to remove points, but the option is there in case you want to remove points from a running server. 137 | There is a type-specific function for removing every point type given its index. The returned boolean indicates if the point 138 | was defined prior to the call to remove it. 139 | -------------------------------------------------------------------------------- /guide/docs/api/server/rtu_server.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: rtu_server 3 | title: Serial RTU Server 4 | sidebar_label: Serial RTU Server 5 | slug: /api/server/rtu_server 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | The library supports serial communication using the RTU transmission mode. RTU uses a similar Modbus addressing scheme but adds 12 | a CRC to each transmitted frame. 13 | 14 | Multi-drop communications may be implemented by having a single client communicating with multiple servers sharing the same serial channel. Broadcasts 15 | messages may be used to write requests simultaneously to all server on a channel. 16 | 17 | ## Special addresses 18 | 19 | ### Broadcast 20 | 21 | When a write request is received with the broadcast unit ID (`0x00`), it is automatically forwarded to all registered 22 | handlers. No response will be returned. 23 | 24 | ### Reserved addresses 25 | 26 | Addresses 248 (`0xF8`) to 255 (`0xFF`) (inclusive) are reserved in the specification and **should not be used**. The library does not 27 | enforce this requirement, but a warning message is generated as a reminder that it may not be interoperable. 28 | 29 | ## Creating a server 30 | 31 | To create a RTU server, first build a `DeviceMap` for each unit ID to which the server will respond. The is similar to how it's done in the [TCP server](./tcp_server). 32 | Then use the `Server.CreateRtu` factory method to create the background task. 33 | 34 | The `Server.CreateRtu` method takes the following arguments: 35 | 36 | - `runtime`: tokio runtime used to drive the async process. See [Runtime](../runtime.mdx) for more details. 37 | - `path`: path of the serial device. 38 | - On Windows, it's generally something like `COM3` 39 | - On Linux, it's generally something like `/dev/ttyS3`. You need to have the adequate permissions 40 | to access these devices. 41 | - `serial_port_settings`: structure with various serial port settings: 42 | - Baud rate in bit per second 43 | - Data bits. Note that Modbus should use 8 data bits. 44 | - Stop bits 45 | - Parity 46 | - Flow control 47 | - `port_retry_delay`: how long to wait before reopening after a failed open or port error. 48 | - `map`: Map of unit ids and their corresponding callback handlers. 49 | - `level`: Initial decoding level for the port which can be adjusted later via the returned Channel. 50 | 51 | :::tip 52 | The task handling the port is tolerant to the hardware device being added and removed from the system as might occur with USB to serial adapters. 53 | ::: 54 | 55 | 65 | 66 | 67 | ```rust 68 | {{#include ../rodbus/examples/server.rs:handler_map_create}} 69 | 70 | {{#include ../rodbus/examples/server.rs:rtu_server_create}} 71 | ``` 72 | 73 | 74 | 75 | 76 | ```c 77 | {{#include ../ffi/bindings/c/server_example.c:device_map_init}} 78 | 79 | {{#include ../ffi/bindings/c/server_example.c:rtu_server_create}} 80 | // check error 81 | ``` 82 | 83 | 84 | 85 | 86 | ```cpp 87 | {{#include ../ffi/bindings/c/server_example.cpp:device_map_init}} 88 | 89 | {{#include ../ffi/bindings/c/server_example.cpp:rtu_server_create}} 90 | ``` 91 | 92 | 93 | 94 | 95 | ```java 96 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:device_map_init}} 97 | 98 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:rtu_server_create}} 99 | ``` 100 | 101 | 102 | 103 | 104 | ```csharp 105 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:device_map_init}} 106 | 107 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:rtu_server_create}} 108 | ``` 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /guide/docs/api/server/tcp_server.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: tcp_server 3 | title: TCP Server 4 | sidebar_label: TCP Server 5 | slug: /api/server/tcp_server 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | Each TCP server instance is capable of processing requests for one or more unit IDs from multiple external clients. 12 | 13 | ```mermaid 14 | graph TD 15 | A[Client #1] --> D[Modbus TCP Server] 16 | B[Client #2] --> D 17 | C[Client #3] --> D 18 | subgraph Application 19 | D --> E[UNIT ID 0x01] 20 | D --> F[UNIT ID 0x02] 21 | end 22 | ``` 23 | 24 | The `DeviceMap` class is used to build the association between unit IDs and the custom read/write functionality you wish to provide to clients. 25 | 26 | ## Creating a server 27 | 28 | To create a server, first build a `DeviceMap` for each unit ID that the server will answer. Then use the `create_tcp_server` static method of the `Server` class. 29 | The created server will start listening on the port immediately. 30 | 31 | The `Server.CreateTcp` method takes the following arguments: 32 | 33 | - `runtime`: tokio runtime used to drive the async process. See [Runtime](../runtime.mdx) for more details. 34 | - `address`: IP address of the adapter on which to listen. It may be any specified as any valid IPv4 or IPv6 local endpoint, such as: 35 | - `127.0.0.1` for localhost only 36 | - `0.0.0.0` for all adapters 37 | - The IP address for a particular adapter 38 | - `port`: port on which to listen for connection 39 | - `filter`: `AddressFilter` which can be used to limit which external IPs may connect. 40 | - `max_sessions`: maximum concurrent sessions allowed by the server. When the maximum number of sessions is reached, a new connection will end the oldest session 41 | in order to limit resource usage. 42 | - `map`: Map of unit ids and their corresponding callback handlers. 43 | 44 | 54 | 55 | 56 | ```rust 57 | {{#include ../rodbus/examples/server.rs:handler_map_create}} 58 | 59 | {{#include ../rodbus/examples/server.rs:tcp_server_create}} 60 | ``` 61 | 62 | 63 | 64 | 65 | ```c 66 | {{#include ../ffi/bindings/c/server_example.c:device_map_init}} 67 | 68 | {{#include ../ffi/bindings/c/server_example.c:tcp_server_create}} 69 | // check error 70 | ``` 71 | 72 | 73 | 74 | 75 | ```cpp 76 | {{#include ../ffi/bindings/c/server_example.cpp:device_map_init}} 77 | 78 | {{#include ../ffi/bindings/c/server_example.cpp:tcp_server_create}} 79 | ``` 80 | 81 | 82 | 83 | 84 | ```java 85 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:device_map_init}} 86 | 87 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:tcp_server_create}} 88 | ``` 89 | 90 | 91 | 92 | 93 | ```csharp 94 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:device_map_init}} 95 | 96 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:tcp_server_create}} 97 | ``` 98 | 99 | 100 | 101 | 102 | :::tip 103 | In Rust, you can easily wrap your `RequestHandler` implementation in a `Arc` using the `wrap()` default implementation. 104 | ::: 105 | -------------------------------------------------------------------------------- /guide/docs/api/server/tls_server.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: tls_server 3 | title: TLS Server 4 | sidebar_label: TLS Server 5 | slug: /api/outstation/tls_server 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | Creating a TLS server for outstation instances is exactly the same process as a [TCP server](./tcp_server.mdx), 12 | except that an extra `TlsServerConfig` and an `AuthorizationHandler` is required. For more details about TLS 13 | support and the configuration options, check the [TLS general information](../tls.mdx) page. 14 | 15 | :::tip 16 | A server mode of operation is also supported which does not require the client certificate to contain the role extension. 17 | 18 | The example only demonstrate the `Server.CreateTlsWithAuthz` method, but there is a `Server.CreateTls` which does NOT take an `AuthorizationHandler` parameter 19 | and allows an authenticated client to perform any Modbus operation. 20 | ::: 21 | 22 | ## Examples 23 | 24 | ### Certificate chain configuration 25 | 26 | 36 | 37 | 38 | ```rust 39 | {{#include ../rodbus/examples/server.rs:handler_map_create}} 40 | 41 | {{#include ../rodbus/examples/server.rs:tls_ca_chain_config}} 42 | 43 | {{#include ../rodbus/examples/server.rs:tls_server_create}} 44 | ``` 45 | 46 | 47 | 48 | 49 | ```c 50 | {{#include ../ffi/bindings/c/server_example.c:auth_handler}} 51 | 52 | {{#include ../ffi/bindings/c/server_example.c:auth_handler_init}} 53 | 54 | {{#include ../ffi/bindings/c/server_example.c:device_map_init}} 55 | 56 | {{#include ../ffi/bindings/c/server_example.c:tls_ca_chain_config}} 57 | 58 | {{#include ../ffi/bindings/c/server_example.c:tls_server_create}} 59 | // check error 60 | ``` 61 | 62 | 63 | 64 | 65 | ```cpp 66 | {{#include ../ffi/bindings/c/server_example.cpp:auth_handler}} 67 | 68 | {{#include ../ffi/bindings/c/server_example.cpp:device_map_init}} 69 | 70 | {{#include ../ffi/bindings/c/server_example.cpp:tls_ca_chain_config}} 71 | 72 | {{#include ../ffi/bindings/c/server_example.cpp:tls_server_create}} 73 | ``` 74 | 75 | 76 | 77 | 78 | ```java 79 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:auth_handler}} 80 | 81 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:tls_ca_chain_config}} 82 | 83 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:tls_server_create}} 84 | ``` 85 | 86 | 87 | 88 | 89 | ```csharp 90 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:auth_handler}} 91 | 92 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:tls_ca_chain_config}} 93 | 94 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:tls_server_create}} 95 | ``` 96 | 97 | 98 | 99 | 100 | ### Self-signed certificate configuration 101 | 102 | 112 | 113 | 114 | ```rust 115 | {{#include ../rodbus/examples/server.rs:handler_map_create}} 116 | 117 | {{#include ../rodbus/examples/server.rs:tls_self_signed_config}} 118 | 119 | {{#include ../rodbus/examples/server.rs:tls_server_create}} 120 | ``` 121 | 122 | 123 | 124 | 125 | ```c 126 | {{#include ../ffi/bindings/c/server_example.c:auth_handler}} 127 | 128 | {{#include ../ffi/bindings/c/server_example.c:auth_handler_init}} 129 | 130 | {{#include ../ffi/bindings/c/server_example.c:device_map_init}} 131 | 132 | {{#include ../ffi/bindings/c/server_example.c:tls_self_signed_config}} 133 | 134 | {{#include ../ffi/bindings/c/server_example.c:tls_server_create}} 135 | // check error 136 | ``` 137 | 138 | 139 | 140 | 141 | ```cpp 142 | {{#include ../ffi/bindings/c/server_example.cpp:auth_handler}} 143 | 144 | {{#include ../ffi/bindings/c/server_example.cpp:device_map_init}} 145 | 146 | {{#include ../ffi/bindings/c/server_example.cpp:tls_self_signed_config}} 147 | 148 | {{#include ../ffi/bindings/c/server_example.cpp:tls_server_create}} 149 | ``` 150 | 151 | 152 | 153 | 154 | ```java 155 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:auth_handler}} 156 | 157 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:tls_self_signed_config}} 158 | 159 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:tls_server_create}} 160 | ``` 161 | 162 | 163 | 164 | 165 | ```csharp 166 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:auth_handler}} 167 | 168 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:tls_self_signed_config}} 169 | 170 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:tls_server_create}} 171 | ``` 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /guide/docs/api/server/write_handler.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: write_handler 3 | title: WriteHandler Interface 4 | sidebar_label: WriteHandler 5 | slug: /api/outstation/write_handler 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | The `WriteHandler` interface allows you to process write requests in your application code. Each callback receives a `Database` 12 | object on which changes can be applied. Generally, when a value is written, it should be reflected back in 13 | the internal database. 14 | 15 | :::note 16 | In Rust, the `WriteHandler` does not exist, it is merged with the `RequestHandler` trait. 17 | ::: 18 | 19 | :::warning 20 | The callback functions **should never block** as they are being made from a `Runtime` thread. 21 | ::: 22 | 23 | ## Return value 24 | 25 | The return value of each callback function determines what is returned by the server. Here are the most common use cases: 26 | 27 | - The received index and value are valid, the database is updated and a success value is returned. 28 | - If the requested index doesn't exist, then an `ILLEGAL_DATA_ADDRESS` Modbus exception should be returned. 29 | - If the requested value is not allowed for the point, then a `ILLEGAL_DATA_VALUE` Modbus exception should be returned. 30 | 31 | The library automatically responds to improperly formatted requests such as ones containing invalid start/count combinations. Such invalid 32 | requests are never forwarded to your application code. 33 | 34 | To return an exception, the `WriteResult` value is used. It has three static methods to help build results. The `success_init` 35 | methods is used to create a success result. The `exception_init` is used to write a standard Modbus exception. The `raw_exception_init` 36 | is used to create a non-standard Modbus exception. 37 | 38 | ## Example 39 | 40 | 50 | 51 | 52 | ```rust 53 | {{#include ../rodbus/examples/server.rs:request_handler}} 54 | ``` 55 | 56 | 57 | 58 | 59 | ```c 60 | {{#include ../ffi/bindings/c/server_example.c:write_handler}} 61 | ``` 62 | 63 | 64 | 65 | 66 | ```cpp 67 | {{#include ../ffi/bindings/c/server_example.cpp:write_handler}} 68 | ``` 69 | 70 | 71 | 72 | 73 | ```java 74 | {{#include ../ffi/bindings/java/examples/src/main/java/io/stepfunc/rodbus/examples/ServerExample.java:write_handler}} 75 | ``` 76 | 77 | 78 | 79 | 80 | ```csharp 81 | {{#include ../ffi/bindings/dotnet/examples/server/Program.cs:write_handler}} 82 | ``` 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /guide/docs/examples/summary.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: summary 3 | title: Example Programs 4 | sidebar_label: Links 5 | slug: /examples/summary 6 | --- 7 | 8 | import sitedata from '../../sitedata.json' 9 | 10 | The source repository contains example programs that you can compile and run interactively. Each program demonstrates the usage of various API features by reading console input from the user. 11 | 12 | As you read through the examples, you may notice blocks of code surrounded by `ANCHOR` tags. 13 | 14 | ```c 15 | // ANCHOR: logging 16 | 17 | code that configures the logging ... 18 | 19 | // ANCHOR_END: logging 20 | ``` 21 | 22 | We pull these code blocks from the example programs when we generate this guide. This ensures that all the snippets you encounter when reading the documentation are syntactically valid and up to date. 23 | 24 | ## Links 25 | 26 | The following links redirect to language-specific examples for the current version on Github: 27 | 28 | * Rust 29 | * C and C++ 30 | * C# 31 | * Java 32 | -------------------------------------------------------------------------------- /guide/docs/languages/cpp_lang.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: cpp_bindings 3 | title: C++ bindings 4 | sidebar_label: C++ 5 | slug: /cpp_bindings 6 | --- 7 | 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import sitedata from '../../sitedata.json' 10 | 11 | Each [C package distribution](./c_lang.mdx) also includes a C++ API that fully wraps the C API. The `rodbus.hpp` file includes the public API 12 | and a `src/rodbus.cpp` contains the companion code which maps the C++ API to the underlying C API. 13 | 14 | ```mermaid 15 | graph TD 16 | A[C++ Application] 17 | B[rodbus.hpp] 18 | C[rodbus.cpp] 19 | D[rodbus.h] 20 | E[librodbus_ffi.so] 21 | F[rodbus.o] 22 | A -- #include --> B 23 | C -- #include --> B 24 | C -- #include --> D 25 | A -- link --> E 26 | A -- link --> F 27 | C -- compile --> F 28 | ``` 29 | 30 | ## CMake Usage 31 | 32 | The CMake package script includes a `rodbus_cpp` target that automatically links with the C bindings and builds the C++ wrapper code. 33 | 34 | Make the find package script discoverable by adding it to the prefix path. Next, call `find_package`: 35 | 36 | ```cmake 37 | # Define CMake project with CXX language 38 | project(my_awesome_project LANGUAGES C CXX) 39 | 40 | # Import the rodbus package 41 | set(CMAKE_PREFIX_PATH ${DISTRIBUTION_PATH}/cmake) 42 | # Tell cmake which library we're linking 43 | set(RODBUS_RUST_TARGET x86_64-unknown-linux-gnu) 44 | find_package(rodbus REQUIRED) 45 | 46 | # Create and link the executable w/ the c++ library 47 | add_executable(my_awesome_project main.cpp) 48 | target_link_libraries(my_awesome_project PRIVATE rodbus_cpp) 49 | ``` 50 | 51 | :::note 52 | The `rodbus_cpp` CMake target is made available only if the `CXX` language is enabled. Languages can be enabled in the 53 | [project()](https://cmake.org/cmake/help/latest/command/project.html) command or with a separate 54 | [enable_language()](https://cmake.org/cmake/help/latest/command/enable_language.html) command. 55 | ::: 56 | 57 | :::tip 58 | Just like with the C library, it's easy compile and link a program from the command line without CMake: 59 | 60 | ``` 61 | > g++ main.cpp c-bindings/src/rodbus.cpp -I ./c-bindings/include/ -lrodbus_ffi 62 | ``` 63 | 64 | All you have to do is compile and link one additional file! 65 | ::: 66 | 67 | ## Mapping 68 | 69 | Most of the abstract concepts in the binding generator map directly to C++. 70 | 71 | ### Errors 72 | 73 | All C API errors are transformed into a C++ exceptions containing the error enum. All exceptions derive from `std::logic_error`. 74 | 75 | Other validations (e.g. checking that a moved class isn't used after the move) also throw `std::logic_error`. 76 | 77 | :::warning 78 | Uncaught exceptions thrown in callbacks will terminate the program. Always wrap your callback logic using `try/catch` syntax if there's a possibility the callback will throw. 79 | ::: 80 | 81 | ### Iterators 82 | 83 | Iterators are wrapped in a class for easier manipulation. Iterating on them should done like so: 84 | 85 | ```cpp 86 | while(iter.next()) { 87 | auto value = iter.get(); 88 | } 89 | ``` 90 | 91 | The `next()` advances the iterator and returns `true` if a value is available. The `get()` returns the current value pointed, or throws 92 | `std::logic_error` if the end was reached. 93 | 94 | :::warning 95 | The iterator wrapper does **not** copy and accumulate the values like in C# or Java. Therefore, an iterator should **never** be used outside of the callback. 96 | Frequently, the iterator points to memory on the stack and will result in undefined behavior if it is used after the callback is complete. 97 | ::: 98 | 99 | ### Collections 100 | 101 | Collections are taken through a constant reference to a `std::vector`. The elements will be copied internally. 102 | 103 | ### Classes 104 | 105 | Classes have a opaque pointer inside and therefore cannot be copied. They can be moved around with `std::move`. If a method 106 | is called on a moved class it throw a `std::logic_error`. 107 | 108 | The class destructor will call the underlying C destructor automatically. 109 | 110 | ### Interfaces 111 | 112 | Interfaces are abstract classes containing only pure virtual functions where every callback must be implemented. The destructor is virtual to allow proper cleanup. 113 | 114 | Owned interfaces that are invoked asynchronously by the library are passed into the API as a `std::unique_ptr`. Use `std::make_unique` to create these smart pointers. 115 | 116 | Non-owned (synchronous) interfaces are passed by reference. There are also functional wrappers that take a lambda 117 | function as an argument available in the `dnp3::functional` namespace. 118 | 119 | ### Async methods 120 | 121 | C++ doesn't have a robust model for asynchronous computations in the standard library (yet). You can only extract a value 122 | from a C++ `std::future` using the blocking `get` method and there is no way to chain asynchronously chain futures. 123 | 124 | Asynchronous methods are mapped to callback interfaces with two methods: 125 | 126 | * `on_complete` is called when the operation succeeds 127 | * `on_failure` is called if an error occurs 128 | 129 | If you already use an external futures library, it will be easy to use our callbacks to complete your futures. -------------------------------------------------------------------------------- /guide/docs/languages/csharp.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: c_sharp 3 | title: C# Bindings 4 | sidebar_label: C# 5 | slug: /c_sharp 6 | --- 7 | 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | 10 | We distribute the C# bindings as a [Nuget package](https://www.nuget.org/packages/rodbus). Internally, the C API is called using [P/Invoke](https://docs.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke) 11 | The bindings are .NET Standard 2.0 compatible and include platform-specific shared libraries for 64-bit Windows and Linux. They are automatically loaded by the .NET runtime. 12 | 13 | ## Mapping 14 | 15 | C# is an object-oriented language that supports all the abstract patterns modeled in the code generator. Here's what you need to know. 16 | 17 | ### Errors 18 | 19 | C API errors are transformed into exceptions that contain the error `enum`. 20 | 21 | :::warning 22 | Uncaught exceptions thrown in callbacks will terminate the program. Your code should always wrap callback logic with `try/catch` syntax if there's a chance that the callback will throw. 23 | ::: 24 | 25 | ### Iterators 26 | 27 | The code generator transforms iterators into `ICollection`. This means that the collections returned by callbacks may be used outside the callback. For example, you can send them to another thread for processing. 28 | 29 | ### Structs 30 | 31 | Native structs are mapped to C# classes. They have public member visibility, and the constructor ensures that all values are initialized. 32 | 33 | ### Classes 34 | 35 | Abstract classes are mapped to C# classes. They have a private pointer to the underlying native resource. There are two types of generated classes in C#: 36 | * Generated classes that only have a private finalizer: These are automatically garbage collected, while native resources are deallocated in the class's finalize method. These 37 | types of classes are typically builder objects such as `Commands`, `Request`, and `AddressFilter`. 38 | * Generated classes that also provide a public `Shutdown` method: These represent long-lived resources such as `Runtime`, `Master`, or `TCPServer`. They map to an asynchronous 39 | Rust task executing on the Tokio runtime. The `Shutdown` method lets you precisely control when the resource/task will stop. 40 | 41 | ### Asynchronous Methods 42 | 43 | Abstract asynchronous methods are transformed into methods that return `Task`. You can block on the task or `await` it in an async method. 44 | -------------------------------------------------------------------------------- /guide/docs/languages/java.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: java 3 | title: Java Bindings 4 | sidebar_label: Java 5 | slug: /java 6 | --- 7 | 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | 10 | The Java bindings are distributed as a JAR targeting Java 8. Native libraries for Windows x64, Linux x64 and Linux AArch64 are embedded in the JAR's `resources` directory. The 11 | correct native library will automatically load during static initialization. These native libraries wrap the underlying C API with a thin layer of 12 | [JNI](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/). 13 | 14 | ## Maven 15 | 16 | Release artifacts are published to Maven central. Add this dependency to incorporate them into your projects: 17 | 18 | ```xml 19 | 20 | io.stepfunc 21 | rodbus 22 | ${rodbus.version} 23 | 24 | ``` 25 | 26 | ## Dependencies 27 | 28 | In addition to the Rust dependencies, the Java bindings depend on this open source project: 29 | 30 | * [joou-java-6](https://github.com/jOOQ/jOOU) - Apache 2.0 - Java Object Oriented Unsigned (JOOU) integer classes 31 | 32 | This library is not distributed by Step Function I/O directly. It is only declared as a dependency for the package manager to retrieve. 33 | 34 | ## Unsigned Integers 35 | 36 | Java doesn't support unsigned integers as part of the core language. Instead, the Java code generator uses classes from the `JOOU` library. This ensures that numeric types crossing the Java/JNI boundary are pre-validated within the correct range. User code that creates unsigned integers will need to import symbols from the `JOOU` library. 37 | 38 | It is particularly helpful to statically import the factory methods on `Unsigned` class: 39 | 40 | ```java 41 | import static org.joou.Unsigned.*; 42 | import org.joou.UShort; 43 | ``` 44 | 45 | This lets you create instances of the unsigned classes: 46 | 47 | ```java 48 | UShort value = ushort(65535); 49 | ``` 50 | 51 | ## Mapping 52 | 53 | Java is an object-oriented language that supports all the abstract patterns modeled in the code generator. This section describes those mappings. 54 | 55 | ### Errors 56 | 57 | C API errors are transformed into exceptions containing the error enum. The exception class inherits from `RuntimeException`. 58 | 59 | :::warning 60 | Uncaught exceptions thrown in callbacks will terminate the program. Always wrap your callback logic using `try/catch` syntax if there's a possibility the callback will throw. 61 | ::: 62 | 63 | 64 | ### Iterators 65 | 66 | Iterators are transformed into `List` by the code generator. This means that the collections returned by callbacks may be 67 | used outside the callback. For example, you can send them to another thread for processing. 68 | 69 | ### Structs 70 | 71 | Native structs are mapped to Java classes. They have public member visibility, and the constructor ensures that all values are initialized. 72 | 73 | ### Classes 74 | 75 | Abstract classes are also mapped to Java classes. They have a private pointer to the underlying native resource. There are two types of generated classes in Java: 76 | * Generated classes that only have a `finalize` method: These are automatically garbage collected, while native resources are deallocated in the class's finalize method. These types of classes are typically builder objects such as `Commands`, `Request`, and `AddressFilter`. 77 | * Generated classes that also provide a public `Shutdown` method to proactively release native resources: These represent long-lived resources such as `Runtime`, `Master`, or `TCPServer`. They map to an asynchronous Rust task executing on the Tokio runtime. The `Shutdown` method lets you precisely control when the resource/task will stop. 78 | 79 | 80 | ### Asynchronous Methods 81 | 82 | Abstract asynchronous methods are transformed into methods that return `CompletionStage`. You can then chain and use the object in an asynchronous 83 | workflow, or transform it into a `CompletableFuture` and call `get()`. 84 | -------------------------------------------------------------------------------- /guide/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const samplePlugin = require('./plugins/sample'); 3 | const mermaidPlugin = require('./plugins/mermaid'); 4 | const sitedata = require('./sitedata.json'); 5 | 6 | module.exports = { 7 | title: `Rodbus ${sitedata.version}`, 8 | tagline: 'Pretty sure we don\'t need this page, just the docs', 9 | url: 'https://docs.stepfunc.io', 10 | baseUrl: `/rodbus/${sitedata.version}/guide/`, 11 | onBrokenLinks: 'throw', 12 | onBrokenMarkdownLinks: 'warn', 13 | favicon: 'images/brand/favicon.png', 14 | organizationName: 'stepfunc', // Usually your GitHub org/user name. 15 | projectName: 'rodbus', // Usually your repo name. 16 | themeConfig: { 17 | prism: { 18 | theme: require('prism-react-renderer/themes/vsLight'), 19 | additionalLanguages: ['rust', 'java', 'csharp', 'cmake'], 20 | }, 21 | colorMode: { 22 | defaultMode: 'light', 23 | disableSwitch: true, 24 | }, 25 | navbar: { 26 | title: `Rodbus ${sitedata.version}`, 27 | logo: { 28 | alt: 'Logo', 29 | src: 'images/brand/logo.svg', 30 | href: '/docs/guide' 31 | }, 32 | items: [], 33 | }, 34 | footer: { 35 | logo: { 36 | alt: 'Step Function', 37 | src: 'images/brand/footer-logo.svg', 38 | }, 39 | links: [ 40 | { 41 | title: 'Step Function I/O', 42 | items: [ 43 | { 44 | label: 'Products', 45 | href: 'https://stepfunc.io/products/', 46 | }, 47 | { 48 | label: 'Blog', 49 | to: 'https://stepfunc.io/blog/', 50 | }, 51 | ], 52 | }, 53 | { 54 | title: 'Library', 55 | items: [ 56 | { 57 | label: 'GitHub', 58 | href: sitedata.github_url, 59 | }, 60 | { 61 | label: 'Homepage', 62 | href: 'https://stepfunc.io/products/libraries/modbus/', 63 | }, 64 | ], 65 | }, 66 | { 67 | title: 'Modbus', 68 | items: [ 69 | { 70 | label: 'Modbus.org', 71 | to: 'https://modbus.org/', 72 | } 73 | ], 74 | }, 75 | ], 76 | copyright: `Copyright © ${new Date().getFullYear()} Step Function I/O LLC`, 77 | }, 78 | }, 79 | presets: [ 80 | [ 81 | '@docusaurus/preset-classic', 82 | { 83 | docs: { 84 | sidebarPath: require.resolve('./sidebars.js'), 85 | remarkPlugins: [ 86 | samplePlugin, 87 | mermaidPlugin, 88 | ], 89 | }, 90 | theme: { 91 | customCss: require.resolve('./src/css/custom.css'), 92 | }, 93 | }, 94 | ], 95 | ], 96 | plugins: [path.resolve(__dirname, './plugins/changelog')], 97 | }; 98 | -------------------------------------------------------------------------------- /guide/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rodbus-guide", 3 | "version": "0.9.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "serve": "docusaurus serve", 12 | "clear": "docusaurus clear" 13 | }, 14 | "dependencies": { 15 | "@algolia/client-search": "^4.8.3", 16 | "@docusaurus/core": "^2.1.0", 17 | "@docusaurus/preset-classic": "^2.1.0", 18 | "@mdx-js/react": "^1.6.21", 19 | "clsx": "^1.1.1", 20 | "mermaid": "^8.9.0", 21 | "react": "16", 22 | "react-dom": "16", 23 | "unist-util-visit": "^2.0.3" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.5%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | }, 37 | "devDependencies": { 38 | "@types/react": "16" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /guide/plugins/changelog/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs/promises'); 2 | const path = require('path'); 3 | 4 | module.exports = function (context, options) { 5 | return { 6 | name: 'changelog', 7 | async loadContent() { 8 | const filename = 'CHANGELOG.md'; 9 | await fs.copyFile(path.resolve(context.siteDir, `../${filename}`), path.resolve(context.siteDir, `docs/${filename}`)); 10 | }, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /guide/plugins/mermaid/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const visit = require('unist-util-visit'); 3 | 4 | module.exports = function mermaid(options = {}) { 5 | return (tree, file) => { 6 | let importAdded = false; 7 | visit(tree, 'code', (node, index, parent) => { 8 | if(node.lang === 'mermaid') { 9 | node.type = 'jsx'; 10 | node.value = ``; 11 | 12 | if(!importAdded) { 13 | const importPath = path.relative(file.dirname, path.resolve(__dirname, '../../src/theme/Mermaid')).replace(/\\/g, '/'); 14 | const importNode = { 15 | type: 'import', 16 | value: `import Mermaid from '${importPath}'`, 17 | } 18 | parent.children.splice(index, 0, importNode); 19 | importAdded = true; 20 | 21 | return index + 1 22 | } 23 | } 24 | }); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /guide/plugins/sample/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const visit = require('unist-util-visit'); 4 | 5 | function findSample(filename, anchor) { 6 | const file = fs.readFileSync(filename, 'utf8'); 7 | const start_regex = /ANCHOR:\s*([\w_-]+)/; 8 | const end_regex = /ANCHOR_END:\s*([\w_-]+)/; 9 | 10 | if(anchor) { 11 | let found = false; 12 | let lines = []; 13 | let min_num_whitespaces = null; 14 | for (const line of file.split(/\r?\n/)) { 15 | if(!found) { 16 | const match = line.match(start_regex) 17 | if(match && match[1] === anchor) { 18 | found = true; 19 | } 20 | } 21 | else { 22 | // Check if we found end anchor 23 | const match = line.match(end_regex) 24 | if(match && match[1] === anchor) { 25 | break; 26 | } 27 | else { 28 | // Check whitespaces 29 | const num_whitespaces = line.match(/([ ]*).*/)[1].length; 30 | if(!min_num_whitespaces || num_whitespaces < min_num_whitespaces) { 31 | min_num_whitespaces = num_whitespaces; 32 | } 33 | 34 | // Push the line 35 | lines.push(line); 36 | } 37 | } 38 | }; 39 | 40 | if(!found) { 41 | throw new Error(`Could not find '${anchor}' anchor in ${filename}.`); 42 | } 43 | 44 | let result = ''; 45 | lines.flatMap((line, index) => { 46 | result += line.substring(min_num_whitespaces); 47 | if(index + 1 != lines.length) { 48 | result += "\n"; 49 | } 50 | }); 51 | return result; 52 | } else { 53 | return file; 54 | } 55 | } 56 | 57 | const RE_PARENS = new RegExp('' 58 | + '\{\{\\s*' // link opening parens and whitespace 59 | + '\#([a-zA-Z0-9_]+)' // link type 60 | + '\\s+' // separating whitespace 61 | + '([a-zA-Z0-9\s_.\\-:/\\\+]+)' // link target path and space separated properties 62 | + '\\s*\}\}' // whitespace and link closing parens" 63 | , 'g'); 64 | 65 | module.exports = function codeSample(options = {}) { 66 | return (tree, file) => { 67 | const codes = []; 68 | 69 | visit(tree, 'code', (node, index, parent) => { 70 | codes.push([node, index, parent]); 71 | }); 72 | 73 | for (const [node] of codes) { 74 | const matches = node.value.matchAll(RE_PARENS); 75 | 76 | let result = ''; 77 | let current_idx = 0; 78 | for (const match of matches) { 79 | // Check if it's an include 80 | if (match[1] == 'include') { 81 | const [filepath, anchor] = match[2].split(':'); 82 | 83 | // Copy everything before the tag 84 | result += node.value.substr(current_idx, match.index - current_idx); 85 | 86 | // Copy the modified text 87 | result += findSample(path.resolve(__dirname, '../../', filepath), anchor); 88 | 89 | // Update the current index 90 | current_idx = match.index + match[0].length; 91 | } 92 | } 93 | 94 | result += node.value.substr(current_idx, node.value.length - current_idx); 95 | 96 | node.value = result; 97 | } 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /guide/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | someSidebar: { 3 | About: [ 4 | 'about/guide', 5 | 'about/modbus', 6 | 'about/versioning', 7 | 'about/license', 8 | 'about/dependencies', 9 | ], 10 | Languages: [ 11 | 'languages/bindings', 12 | { 13 | Bindings: [ 14 | 'languages/c_bindings', 15 | 'languages/cpp_bindings', 16 | 'languages/java', 17 | 'languages/c_sharp', 18 | ] 19 | } 20 | ], 21 | API: [ 22 | 'api/logging', 23 | 'api/runtime', 24 | 'api/tls', 25 | { 26 | Client: [ 27 | 'api/client/tcp_client', 28 | 'api/client/rtu_client', 29 | 'api/client/tls_client', 30 | 'api/client/requests', 31 | ] 32 | }, 33 | { 34 | Server: [ 35 | 'api/server/tcp_server', 36 | 'api/server/rtu_server', 37 | 'api/server/tls_server', 38 | 'api/server/database', 39 | 'api/server/write_handler', 40 | ] 41 | }, 42 | ], 43 | Examples: [ 44 | 'examples/summary' 45 | ], 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /guide/sitedata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.4.0", 3 | "github_url": "https://github.com/stepfunc/rodbus" 4 | } -------------------------------------------------------------------------------- /guide/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #004e98; 11 | --ifm-color-primary-dark: #004689; 12 | --ifm-color-primary-darker: #004281; 13 | --ifm-color-primary-darkest: #00376a; 14 | --ifm-color-primary-light: #0056a7; 15 | --ifm-color-primary-lighter: #005aaf; 16 | --ifm-color-primary-lightest: #0065c6; 17 | --ifm-code-font-size: 95%; 18 | 19 | /* this overrides the default color for the admonition "tips" */ 20 | --ifm-color-success: #5C7D5E; 21 | 22 | --ifm-navbar-background-color: var(--ifm-color-primary); 23 | --ifm-navbar-link-color: #fff; 24 | --ifm-navbar-link-hover-color: rgba(255, 255, 255, 0.9); 25 | 26 | --ifm-footer-background-color: #fff; 27 | } 28 | 29 | .navbar__link--active { 30 | font-weight: 700; 31 | } 32 | 33 | .docusaurus-highlight-code-line { 34 | background-color: rgb(72, 77, 91); 35 | display: block; 36 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 37 | padding: 0 var(--ifm-pre-padding); 38 | } 39 | 40 | .footer { 41 | border-top: 1px solid var(--ifm-toc-border-color); 42 | 43 | } 44 | .footer__logo { 45 | max-width: 5rem; 46 | } 47 | .footer__copyright { 48 | font-size: 14px; 49 | opacity: 0.5; 50 | } 51 | -------------------------------------------------------------------------------- /guide/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | 27 | .features { 28 | display: flex; 29 | align-items: center; 30 | padding: 2rem 0; 31 | width: 100%; 32 | } 33 | 34 | .featureImage { 35 | height: 200px; 36 | width: 200px; 37 | } 38 | -------------------------------------------------------------------------------- /guide/src/theme/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import React from 'react'; 9 | import clsx from 'clsx'; 10 | 11 | import Link from '@docusaurus/Link'; 12 | import {useThemeConfig} from '@docusaurus/theme-common'; 13 | import useBaseUrl from '@docusaurus/useBaseUrl'; 14 | import styles from './styles.module.css'; 15 | 16 | function FooterLink({to, href, label, prependBaseUrlToHref, ...props}: any) { 17 | const toUrl = useBaseUrl(to); 18 | const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true}); 19 | 20 | return ( 21 | 33 | {label} 34 | 35 | ); 36 | } 37 | 38 | const FooterLogo = ({url, alt}) => ( 39 | {alt} 40 | ); 41 | 42 | function Footer(): JSX.Element | null { 43 | const {footer} = useThemeConfig(); 44 | 45 | const {copyright, links = [], logo = {}} = footer || {}; 46 | const logoUrl = useBaseUrl(logo.src); 47 | 48 | if (!footer) { 49 | return null; 50 | } 51 | 52 | return ( 53 |
57 |
58 | {links && links.length > 0 && ( 59 |
60 | {links.map((linkItem, i) => ( 61 |
62 | {linkItem.title != null ? ( 63 |

{linkItem.title}

64 | ) : null} 65 | {linkItem.items != null && 66 | Array.isArray(linkItem.items) && 67 | linkItem.items.length > 0 ? ( 68 |
    69 | {linkItem.items.map((item, key) => 70 | item.html ? ( 71 |
  • 80 | ) : ( 81 |
  • 82 | 83 |
  • 84 | ), 85 | )} 86 |
87 | ) : null} 88 |
89 | ))} 90 |
91 | )} 92 | {(logo || copyright) && ( 93 |
94 | {logo && logo.src && ( 95 |
96 | {logo.href ? ( 97 | 102 | 103 | 104 | ) : ( 105 | 106 | )} 107 |
108 | )} 109 | {copyright ? ( 110 |
118 | ) : null} 119 |
120 | )} 121 |
122 |
123 | ); 124 | } 125 | 126 | export default Footer; 127 | -------------------------------------------------------------------------------- /guide/src/theme/Footer/styles.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | .footerLogoLink { 9 | opacity: 0.75; 10 | transition: opacity var(--ifm-transition-fast) var(--ifm-transition-timing-default); 11 | } 12 | 13 | .footerLogoLink:hover { 14 | opacity: 1; 15 | } 16 | -------------------------------------------------------------------------------- /guide/src/theme/Mermaid.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import mermaid from "mermaid"; 3 | 4 | mermaid.initialize({ 5 | startOnLoad: true, 6 | theme: 'neutral', 7 | }); 8 | 9 | const Mermaid = ({ chart }) => { 10 | useEffect(() => { 11 | mermaid.contentLoaded(); 12 | }, []); 13 | 14 | return
{chart}
; 15 | }; 16 | 17 | export default Mermaid; 18 | -------------------------------------------------------------------------------- /guide/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepfunc/rodbus/1833f4b32d30c8266a90a952dadb593f2075d29a/guide/static/.nojekyll -------------------------------------------------------------------------------- /guide/static/images/brand/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepfunc/rodbus/1833f4b32d30c8266a90a952dadb593f2075d29a/guide/static/images/brand/favicon.png -------------------------------------------------------------------------------- /guide/static/images/brand/footer-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /guide/static/images/brand/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /guide/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepfunc/rodbus/1833f4b32d30c8266a90a952dadb593f2075d29a/guide/static/img/favicon.ico -------------------------------------------------------------------------------- /guide/static/img/modbus_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepfunc/rodbus/1833f4b32d30c8266a90a952dadb593f2075d29a/guide/static/img/modbus_logo.jpg -------------------------------------------------------------------------------- /guide/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 12 | Your Site Title Here 13 | 14 | 15 | If you are not redirected automatically, follow this 16 | link. 17 | 18 | -------------------------------------------------------------------------------- /packaging.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "x86_64-pc-windows-msvc": { 4 | "cpp": true, 5 | "dotnet": true, 6 | "java": true 7 | }, 8 | "i686-pc-windows-msvc": { 9 | "cpp": true, 10 | "dotnet": true, 11 | "java": false 12 | }, 13 | "aarch64-apple-darwin": { 14 | "cpp": false, 15 | "dotnet": true, 16 | "java": true 17 | }, 18 | "x86_64-apple-darwin": { 19 | "cpp": false, 20 | "dotnet": true, 21 | "java": true 22 | }, 23 | "x86_64-unknown-linux-gnu": { 24 | "cpp": true, 25 | "dotnet": true, 26 | "java": true 27 | }, 28 | "aarch64-unknown-linux-gnu": { 29 | "cpp": true, 30 | "dotnet": true, 31 | "java": true 32 | }, 33 | "arm-unknown-linux-gnueabihf": { 34 | "cpp": true, 35 | "dotnet": true, 36 | "java": true 37 | }, 38 | "arm-unknown-linux-gnueabi": { 39 | "cpp": true, 40 | "dotnet": false, 41 | "java": false 42 | }, 43 | "armv7-unknown-linux-gnueabihf": { 44 | "cpp": true, 45 | "dotnet": false, 46 | "java": false 47 | }, 48 | "x86_64-unknown-linux-musl": { 49 | "cpp": true, 50 | "dotnet": true, 51 | "java": false 52 | }, 53 | "aarch64-unknown-linux-musl": { 54 | "cpp": true, 55 | "dotnet": true, 56 | "java": false 57 | }, 58 | "arm-unknown-linux-musleabihf": { 59 | "cpp": true, 60 | "dotnet": true, 61 | "java": false 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /rodbus-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rodbus-client" 3 | version = "1.4.0" 4 | description = "A command line program for making Modbus client requests using the Rodbus crate" 5 | readme = "README.md" 6 | 7 | # inherit from workspace 8 | authors.workspace = true 9 | rust-version.workspace = true 10 | edition.workspace = true 11 | license-file.workspace = true 12 | homepage.workspace = true 13 | repository.workspace = true 14 | keywords.workspace = true 15 | categories.workspace = true 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [[bin]] 21 | name = "rodbus-client" 22 | path = "src/main.rs" 23 | 24 | [dependencies] 25 | rodbus = { path = "../rodbus", default-features = false, features = ["serial"]} 26 | clap = { version = "4.0", features = ["derive"] } 27 | tokio = { workspace = true, features = ["macros", "time"] } 28 | tracing = { workspace = true } 29 | tracing-subscriber = { workspace = true } 30 | thiserror = { version = "2.0.12" } 31 | -------------------------------------------------------------------------------- /rodbus-client/README.md: -------------------------------------------------------------------------------- 1 | 2 | Rodbus-client is a command line application that uses the [Rodbus](https://crates.io/crates/rodbus) crate 3 | to send Modbus requests and print responses to the console. 4 | 5 | ``` 6 | > cargo install rodbus-client 7 | ``` 8 | 9 | Use the `-h` option to specify the host to connect to and the `-i` option to 10 | specify the Modbus unit ID. 11 | 12 | Each request can be sent using the following subcommands: 13 | 14 | - `rc`: read coils 15 | - `-s`: starting address 16 | - `-q`: quantity of coils 17 | - `rdi`: read discrete inputs 18 | - `-s`: starting address 19 | - `-q`: quantity of discrete inputs 20 | - `rhr`: read holding registers 21 | - `-s`: starting address 22 | - `-q`: quantity of holding registers 23 | - `rir`: read input registers 24 | - `-s`: starting address 25 | - `-q`: quantity of input registers 26 | - `wsc`: write single coil 27 | - `-i`: index of the coil 28 | - `-v`: value of the coil (`true` or `false`) 29 | - `wsr`: write single register 30 | - `-i`: index of the register 31 | - `-v`: value of the register 32 | - `wmc`: write multiple coils 33 | - `-s`: starting address 34 | - `-v`: values of the coils (e.g. 10100011) 35 | - `wmr`: write multiple registers 36 | - `-s`: starting address 37 | - `-v`: values of the registers as a comma delimited list (e.g. 1,4,7) 38 | 39 | Examples: 40 | 41 | - Read coils 10 to 19 on `localhost`, port 502, unit ID `0x02`: `cargo run -p rodbus-client -- -h 42 | 127.0.0.1:502 -i 2 rc -s 10 -q 10` 43 | - Read holding registers 10 to 19: `cargo run -p rodbus-client -- rhr -s 10 -q 10` 44 | - Write coil 10: `cargo run -p rodbus-client -- wsc -i 10 -v true` 45 | - Write multiple coils: `cargo run -p rodbus-client -- wmc -s 10 -v 101001` 46 | - Write register 10: `cargo run -p rodbus-client -- wsr -i 10 -v 76` 47 | - Write 42 to registers 10, 11 and 12: `cargo run -p rodbus-client -- wmr -s 10 48 | -v 42,42,42` 49 | 50 | It is also possible to send periodic requests with the `-p` argument. For example, 51 | to send a read coils request every 2 seconds, you would do this: 52 | `cargo run -p rodbus-client -- -p 2000 rc -s 10 -q 10` 53 | 54 | -------------------------------------------------------------------------------- /rodbus/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rodbus" 3 | version = "1.4.0" 4 | description = "A high-performance async implementation of the Modbus protocol using tokio" 5 | readme = "README.md" 6 | 7 | # inherit from workspace 8 | authors.workspace = true 9 | rust-version.workspace = true 10 | edition.workspace = true 11 | license-file.workspace = true 12 | homepage.workspace = true 13 | repository.workspace = true 14 | keywords.workspace = true 15 | categories.workspace = true 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [dependencies] 21 | crc = "3.0" 22 | scursor = "0.2.0" 23 | tokio = { workspace = true, features = ["net", "sync", "io-util", "io-std", "time", "rt", "rt-multi-thread", "macros"] } 24 | tracing = { workspace = true } 25 | 26 | # TLS dependencies 27 | rx509 = { version = "^0.2", optional = true } 28 | sfio-rustls-config = { version = "0.3.2", optional = true } 29 | tokio-rustls = { version = "0.26.0", features = ["tls12"], default-features = false, optional = true } 30 | 31 | # serial dependencies 32 | tokio-serial = { version = "5.4", default-features = false, optional = true } 33 | 34 | [dev-dependencies] 35 | clap = { version = "4.1.8", features = ["derive"] } 36 | tokio-stream = "0.1" 37 | tokio-util = { version = "0.7", features = ["codec"] } 38 | tokio-test = "0.4.2" 39 | sfio-tokio-mock-io = "0.2" 40 | tracing-subscriber = { workspace = true } 41 | 42 | [features] 43 | default = ["tls", "serial"] 44 | ffi = [] 45 | tls = ["rx509", "sfio-rustls-config", "tokio-rustls"] 46 | serial = ["tokio-serial"] 47 | -------------------------------------------------------------------------------- /rodbus/README.md: -------------------------------------------------------------------------------- 1 | Commercial library by [Step Function I/O](https://stepfunc.io/) 2 | 3 | A high-performance implementation of the [Modbus](http://modbus.org/) protocol using [Tokio](https://docs.rs/tokio) and Rust's `async/await` syntax. 4 | 5 | # Features 6 | 7 | * Panic-free parsing 8 | * Correctness and compliance to the specification 9 | * Built-in logging and protocol decoding 10 | * Automatic connection management with configurable reconnect strategy 11 | * Scalable performance using Tokio's multi-threaded executor 12 | * TLS is implemented using [rustls](https://github.com/rustls/rustls) not openssl 13 | * Model-generated bindings for C, C++, Java, and .NET Core 14 | * Runs on all platforms and operating systems supported by the [Tokio](https://tokio.rs/) runtime: 15 | - Official support for: Windows x64 and Linux x64, AArch64, ARMv7 and ARMv6 16 | - Unofficial support: MacOS, PowerPC, MIPS, FreeBSD, and others 17 | 18 | # Supported Modes 19 | 20 | * TCP, RTU (serial), and Modbus security (TLS) with and without X.509 extension containing the user role. 21 | * Client and server 22 | 23 | ## Function Codes 24 | 25 | The [`client`](https://github.com/stepfunc/rodbus/blob/main/rodbus/examples/client.rs) and [`server`](https://github.com/stepfunc/rodbus/blob/main/rodbus/examples/server.rs) examples demonstrate simple 26 | usage of the API. 27 | 28 | The following function codes are supported: 29 | - Read Coils (`0x01`) 30 | - Read Discrete Inputs (`0x02`) 31 | - Read Holding Registers (`0x03`) 32 | - Read Input Registers (`0x04`) 33 | - Write Single Coil (`0x05`) 34 | - Write Single Register (`0x06`) 35 | - Write Multiple Coils (`0x0F`) 36 | - Write Multiple Registers (`0x10`) 37 | 38 | ## License 39 | 40 | This library is publicly available under a non-commercial / non-production license. 41 | Refer to [`LICENSE.txt`](https://raw.githubusercontent.com/stepfunc/rodbus/main/LICENSE.txt) for the terms 42 | of this non-commercial license. 43 | 44 | This software is publicly available, but is not "open source". 45 | __You must purchase a commercial license to use this software for profit.__ 46 | 47 | Please inquire about commercial licensing on our website: 48 | 49 | [https://stepfunc.io/contact/](https://stepfunc.io/contact/) 50 | 51 | # Cargo Features 52 | 53 | Default features can be disabled at compile time: 54 | * `tls` - Build the library with support for TLS (secure Modbus) 55 | * `serial` - Build the library with support for Modbus RTU and serial ports 56 | 57 | ## Bindings 58 | 59 | Bindings in C, C++, java, and .NET Core are available for this library. See the 60 | [documentation](https://stepfunc.io/products/libraries/modbus/) for more details. 61 | 62 | -------------------------------------------------------------------------------- /rodbus/examples/perf.rs: -------------------------------------------------------------------------------- 1 | //! Coarse performance test for Rodbus 2 | 3 | use std::net::{IpAddr, SocketAddr}; 4 | use std::str::FromStr; 5 | use std::time::Duration; 6 | 7 | use rodbus::client::*; 8 | use rodbus::constants::limits::MAX_READ_REGISTERS_COUNT; 9 | use rodbus::server::*; 10 | use rodbus::RequestError; 11 | use rodbus::*; 12 | 13 | use clap::Parser; 14 | 15 | struct Handler; 16 | 17 | impl RequestHandler for Handler { 18 | fn read_holding_register(&self, address: u16) -> Result { 19 | // value is always the address 20 | Ok(address) 21 | } 22 | } 23 | 24 | #[derive(Parser)] 25 | #[clap(author, version, about, long_about = None)] 26 | struct Cli { 27 | #[clap(short, long, value_parser, default_value_t = 1)] 28 | sessions: usize, 29 | #[clap(short = 'c', long, value_parser, default_value_t = 5)] 30 | seconds: usize, 31 | #[clap(short, long, value_parser, default_value_t = false)] 32 | log: bool, 33 | #[clap(short, long, value_parser, default_value_t = 40000)] 34 | port: u16, 35 | } 36 | 37 | async fn join_and_sum(tasks: Vec>>) -> usize { 38 | let mut total = 0; 39 | for task in tasks { 40 | total += task.await.unwrap().unwrap(); 41 | } 42 | total 43 | } 44 | 45 | #[tokio::main(flavor = "multi_thread")] 46 | async fn main() -> Result<(), Box> { 47 | let args = Cli::parse(); 48 | 49 | if args.log { 50 | // Initialize logging 51 | tracing_subscriber::fmt() 52 | .with_max_level(tracing::Level::INFO) 53 | .with_target(false) 54 | .init(); 55 | } 56 | 57 | let duration = std::time::Duration::from_secs(args.seconds as u64); 58 | 59 | println!( 60 | "creating {} parallel connections and making requests for {:?}", 61 | args.sessions, duration 62 | ); 63 | 64 | let ip = IpAddr::from_str("127.0.0.1")?; 65 | let addr = SocketAddr::new(ip, args.port); 66 | 67 | let handler = Handler {}.wrap(); 68 | 69 | let _handle = spawn_tcp_server_task( 70 | args.sessions, 71 | addr, 72 | ServerHandlerMap::single(UnitId::new(1), handler), 73 | AddressFilter::Any, 74 | DecodeLevel::new( 75 | AppDecodeLevel::Nothing, 76 | FrameDecodeLevel::Nothing, 77 | PhysDecodeLevel::Nothing, 78 | ), 79 | ) 80 | .await?; 81 | 82 | // now spawn a bunch of clients 83 | let mut channels: Vec<(Channel, RequestParam)> = Vec::new(); 84 | for _ in 0..args.sessions { 85 | let channel = spawn_tcp_client_task( 86 | addr.into(), 87 | 10, 88 | default_retry_strategy(), 89 | DecodeLevel::new( 90 | AppDecodeLevel::Nothing, 91 | FrameDecodeLevel::Nothing, 92 | PhysDecodeLevel::Nothing, 93 | ), 94 | None, 95 | ); 96 | channel.enable().await.unwrap(); 97 | let params = RequestParam::new(UnitId::new(1), Duration::from_secs(1)); 98 | 99 | channels.push((channel, params)); 100 | } 101 | 102 | let mut query_tasks: Vec>> = Vec::new(); 103 | 104 | let start = std::time::Instant::now(); 105 | 106 | // spawn tasks that make a query 1000 times 107 | for (mut channel, params) in channels { 108 | let handle: tokio::task::JoinHandle> = 109 | tokio::spawn(async move { 110 | let mut iterations = 0; 111 | loop { 112 | if let Err(err) = channel 113 | .read_holding_registers( 114 | params, 115 | AddressRange::try_from(0, MAX_READ_REGISTERS_COUNT).unwrap(), 116 | ) 117 | .await 118 | { 119 | println!("failure: {err}"); 120 | return Err(err); 121 | } 122 | 123 | iterations += 1; 124 | let elapsed = start.elapsed(); 125 | if elapsed >= duration { 126 | return Ok(iterations); 127 | } 128 | } 129 | }); 130 | query_tasks.push(handle); 131 | } 132 | 133 | // join the tasks and calculate the total number of iterations that were run 134 | let iterations = join_and_sum(query_tasks).await; 135 | 136 | let elapsed = std::time::Instant::now() - start; 137 | 138 | let requests_per_sec: f64 = (iterations as f64) / elapsed.as_secs_f64(); 139 | let registers_per_sec = requests_per_sec * (MAX_READ_REGISTERS_COUNT as f64); 140 | 141 | println!("performed {iterations} requests in {elapsed:?}"); 142 | println!("requests/sec == {requests_per_sec:.1}"); 143 | println!("registers/sec == {registers_per_sec:.1}"); 144 | 145 | Ok(()) 146 | } 147 | -------------------------------------------------------------------------------- /rodbus/src/channel.rs: -------------------------------------------------------------------------------- 1 | use crate::Shutdown; 2 | 3 | /// wrap a Tokio receiver and only provide a recv() that returns a Result 4 | /// that makes it harder to misuse. 5 | pub(crate) struct Receiver(tokio::sync::mpsc::Receiver); 6 | 7 | impl From> for Receiver { 8 | fn from(value: tokio::sync::mpsc::Receiver) -> Self { 9 | Self(value) 10 | } 11 | } 12 | 13 | impl Receiver { 14 | pub(crate) async fn recv(&mut self) -> Result { 15 | self.0.recv().await.ok_or(Shutdown) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rodbus/src/client/listener.rs: -------------------------------------------------------------------------------- 1 | use crate::MaybeAsync; 2 | 3 | /// Generic listener type that can be invoked multiple times 4 | pub trait Listener: Send { 5 | /// Inform the listener that the value has changed 6 | fn update(&mut self, _value: T) -> MaybeAsync<()> { 7 | MaybeAsync::ready(()) 8 | } 9 | } 10 | 11 | /// Listener that does nothing 12 | #[derive(Copy, Clone)] 13 | pub(crate) struct NullListener; 14 | 15 | impl NullListener { 16 | /// Create a Box> that does nothing 17 | pub(crate) fn create() -> Box> { 18 | Box::new(NullListener) 19 | } 20 | } 21 | 22 | impl Listener for NullListener { 23 | fn update(&mut self, _value: T) -> MaybeAsync<()> { 24 | MaybeAsync::ready(()) 25 | } 26 | } 27 | 28 | /// State of TCP/TLS client connection 29 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 30 | pub enum ClientState { 31 | /// Client is disabled 32 | Disabled, 33 | /// Client attempting to establish a connection 34 | Connecting, 35 | /// Client is connected 36 | Connected, 37 | /// Client is waiting to retry after a failed attempt to connect 38 | WaitAfterFailedConnect(std::time::Duration), 39 | /// Client is waiting to retry after a disconnection 40 | WaitAfterDisconnect(std::time::Duration), 41 | /// Client has been shut down 42 | Shutdown, 43 | } 44 | 45 | /// State of the serial port 46 | #[cfg(feature = "serial")] 47 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 48 | pub enum PortState { 49 | /// Disabled and idle until enabled 50 | Disabled, 51 | /// Waiting to perform an open retry 52 | Wait(std::time::Duration), 53 | /// Port is open 54 | Open, 55 | /// Port has been shut down 56 | Shutdown, 57 | } 58 | -------------------------------------------------------------------------------- /rodbus/src/client/requests/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod read_bits; 2 | pub(crate) mod read_registers; 3 | pub(crate) mod write_multiple; 4 | pub(crate) mod write_single; 5 | -------------------------------------------------------------------------------- /rodbus/src/client/requests/read_bits.rs: -------------------------------------------------------------------------------- 1 | use crate::common::function::FunctionCode; 2 | use crate::common::traits::Serialize; 3 | use crate::decode::AppDecodeLevel; 4 | use crate::error::RequestError; 5 | use crate::types::{AddressRange, BitIterator, BitIteratorDisplay, ReadBitsRange}; 6 | use crate::Indexed; 7 | 8 | use scursor::{ReadCursor, WriteCursor}; 9 | 10 | pub(crate) trait BitsCallback: 11 | FnOnce(Result) + Send + Sync + 'static 12 | { 13 | } 14 | impl BitsCallback for T where T: FnOnce(Result) + Send + Sync + 'static 15 | {} 16 | 17 | pub(crate) struct Promise { 18 | callback: Option>, 19 | } 20 | 21 | impl Drop for Promise { 22 | fn drop(&mut self) { 23 | self.failure(RequestError::Shutdown); 24 | } 25 | } 26 | 27 | impl Promise { 28 | pub(crate) fn new(callback: T) -> Self 29 | where 30 | T: BitsCallback, 31 | { 32 | Self { 33 | callback: Some(Box::new(callback)), 34 | } 35 | } 36 | 37 | pub(crate) fn failure(&mut self, err: RequestError) { 38 | self.complete(Err(err)) 39 | } 40 | 41 | pub(crate) fn success(&mut self, iter: BitIterator) { 42 | self.complete(Ok(iter)) 43 | } 44 | 45 | fn complete(&mut self, result: Result) { 46 | if let Some(callback) = self.callback.take() { 47 | callback(result) 48 | } 49 | } 50 | } 51 | 52 | pub(crate) struct ReadBits { 53 | pub(crate) request: ReadBitsRange, 54 | promise: Promise, 55 | } 56 | 57 | impl ReadBits { 58 | pub(crate) fn new(request: ReadBitsRange, promise: Promise) -> Self { 59 | Self { request, promise } 60 | } 61 | 62 | pub(crate) fn channel( 63 | request: ReadBitsRange, 64 | tx: tokio::sync::oneshot::Sender>, RequestError>>, 65 | ) -> Self { 66 | Self::new( 67 | request, 68 | Promise::new(|x: Result| { 69 | let _ = tx.send(x.map(|x| x.collect())); 70 | }), 71 | ) 72 | } 73 | 74 | pub(crate) fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { 75 | self.request.get().serialize(cursor) 76 | } 77 | 78 | pub(crate) fn failure(&mut self, err: RequestError) { 79 | self.promise.failure(err) 80 | } 81 | 82 | pub(crate) fn handle_response( 83 | &mut self, 84 | mut cursor: ReadCursor, 85 | function: FunctionCode, 86 | decode: AppDecodeLevel, 87 | ) -> Result<(), RequestError> { 88 | let response = Self::parse_bits_response(self.request.get(), &mut cursor)?; 89 | 90 | if decode.enabled() { 91 | tracing::info!( 92 | "PDU RX - {} {}", 93 | function, 94 | BitIteratorDisplay::new(decode, response) 95 | ); 96 | } 97 | 98 | self.promise.success(response); 99 | Ok(()) 100 | } 101 | 102 | fn parse_bits_response<'a>( 103 | range: AddressRange, 104 | cursor: &'a mut ReadCursor, 105 | ) -> Result, RequestError> { 106 | // there's a byte-count here that we don't actually need 107 | cursor.read_u8()?; 108 | // the rest is a sequence of bits 109 | BitIterator::parse_all(range, cursor) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /rodbus/src/client/requests/read_registers.rs: -------------------------------------------------------------------------------- 1 | use crate::common::function::FunctionCode; 2 | use crate::common::traits::Serialize; 3 | use crate::decode::AppDecodeLevel; 4 | use crate::error::RequestError; 5 | use crate::types::{ 6 | AddressRange, Indexed, ReadRegistersRange, RegisterIterator, RegisterIteratorDisplay, 7 | }; 8 | 9 | use scursor::{ReadCursor, WriteCursor}; 10 | 11 | pub(crate) trait RegistersCallback: 12 | FnOnce(Result) + Send + Sync + 'static 13 | { 14 | } 15 | impl RegistersCallback for T where 16 | T: FnOnce(Result) + Send + Sync + 'static 17 | { 18 | } 19 | 20 | pub(crate) struct Promise { 21 | callback: Option>, 22 | } 23 | 24 | impl Drop for Promise { 25 | fn drop(&mut self) { 26 | self.failure(RequestError::Shutdown); 27 | } 28 | } 29 | 30 | impl Promise { 31 | pub(crate) fn new(callback: T) -> Self 32 | where 33 | T: RegistersCallback, 34 | { 35 | Self { 36 | callback: Some(Box::new(callback)), 37 | } 38 | } 39 | 40 | pub(crate) fn failure(&mut self, err: RequestError) { 41 | self.complete(Err(err)) 42 | } 43 | 44 | pub(crate) fn success(&mut self, iter: RegisterIterator) { 45 | self.complete(Ok(iter)) 46 | } 47 | 48 | fn complete(&mut self, x: Result) { 49 | if let Some(callback) = self.callback.take() { 50 | callback(x) 51 | } 52 | } 53 | } 54 | 55 | pub(crate) struct ReadRegisters { 56 | pub(crate) request: ReadRegistersRange, 57 | promise: Promise, 58 | } 59 | 60 | impl ReadRegisters { 61 | pub(crate) fn new(request: ReadRegistersRange, promise: Promise) -> Self { 62 | Self { request, promise } 63 | } 64 | 65 | pub(crate) fn channel( 66 | request: ReadRegistersRange, 67 | tx: tokio::sync::oneshot::Sender>, RequestError>>, 68 | ) -> Self { 69 | Self::new( 70 | request, 71 | Promise::new(|x: Result| { 72 | let _ = tx.send(x.map(|x| x.collect())); 73 | }), 74 | ) 75 | } 76 | 77 | pub(crate) fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { 78 | self.request.get().serialize(cursor) 79 | } 80 | 81 | pub(crate) fn failure(&mut self, err: RequestError) { 82 | self.promise.failure(err) 83 | } 84 | 85 | pub(crate) fn handle_response( 86 | &mut self, 87 | mut cursor: ReadCursor, 88 | function: FunctionCode, 89 | decode: AppDecodeLevel, 90 | ) -> Result<(), RequestError> { 91 | let response = Self::parse_registers_response(self.request.get(), &mut cursor)?; 92 | 93 | if decode.enabled() { 94 | tracing::info!( 95 | "PDU RX - {} {}", 96 | function, 97 | RegisterIteratorDisplay::new(decode, response) 98 | ); 99 | } 100 | 101 | self.promise.success(response); 102 | Ok(()) 103 | } 104 | 105 | fn parse_registers_response<'a>( 106 | range: AddressRange, 107 | cursor: &'a mut ReadCursor, 108 | ) -> Result, RequestError> { 109 | // there's a byte-count here that we don't actually need 110 | cursor.read_u8()?; 111 | // the reset is a sequence of bits 112 | RegisterIterator::parse_all(range, cursor) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /rodbus/src/client/requests/write_multiple.rs: -------------------------------------------------------------------------------- 1 | use crate::client::message::Promise; 2 | use crate::common::function::FunctionCode; 3 | use crate::common::traits::{Parse, Serialize}; 4 | use crate::decode::AppDecodeLevel; 5 | use crate::error::RequestError; 6 | use crate::error::{AduParseError, InvalidRequest}; 7 | use crate::types::{AddressRange, Indexed}; 8 | 9 | use scursor::{ReadCursor, WriteCursor}; 10 | use std::convert::TryFrom; 11 | 12 | /// Collection of values and starting address 13 | /// 14 | /// Used when making write multiple coil/register requests 15 | #[derive(Debug, Clone)] 16 | pub struct WriteMultiple { 17 | /// starting address 18 | pub(crate) range: AddressRange, 19 | /// vector of values 20 | pub(crate) values: Vec, 21 | } 22 | 23 | pub(crate) struct WriteMultipleIterator<'a, T> { 24 | range: AddressRange, 25 | pos: u16, 26 | iter: std::slice::Iter<'a, T>, 27 | } 28 | 29 | impl WriteMultiple { 30 | /// Create new collection of values 31 | pub fn from(start: u16, values: Vec) -> Result { 32 | let count = match u16::try_from(values.len()) { 33 | Ok(x) => x, 34 | Err(_) => return Err(InvalidRequest::CountTooBigForU16(values.len())), 35 | }; 36 | let range = AddressRange::try_from(start, count)?; 37 | Ok(Self { range, values }) 38 | } 39 | 40 | pub(crate) fn iter(&self) -> WriteMultipleIterator<'_, T> { 41 | WriteMultipleIterator::new(self.range, self.values.iter()) 42 | } 43 | } 44 | 45 | impl<'a, T> WriteMultipleIterator<'a, T> { 46 | fn new(range: AddressRange, iter: std::slice::Iter<'a, T>) -> Self { 47 | Self { 48 | range, 49 | pos: 0, 50 | iter, 51 | } 52 | } 53 | } 54 | 55 | impl Iterator for WriteMultipleIterator<'_, T> 56 | where 57 | T: Copy, 58 | { 59 | type Item = Indexed; 60 | 61 | fn next(&mut self) -> Option { 62 | let next = self.iter.next(); 63 | 64 | match next { 65 | Some(next) => { 66 | let result = Indexed::new(self.range.start + self.pos, *next); 67 | self.pos += 1; 68 | Some(result) 69 | } 70 | None => None, 71 | } 72 | } 73 | 74 | // implementing this allows collect to optimize the vector capacity 75 | fn size_hint(&self) -> (usize, Option) { 76 | let remaining = (self.range.count - self.pos) as usize; 77 | (remaining, Some(remaining)) 78 | } 79 | } 80 | 81 | pub(crate) struct MultipleWriteRequest 82 | where 83 | WriteMultiple: Serialize, 84 | { 85 | pub(crate) request: WriteMultiple, 86 | promise: Promise, 87 | } 88 | 89 | impl MultipleWriteRequest 90 | where 91 | WriteMultiple: Serialize, 92 | { 93 | pub(crate) fn new(request: WriteMultiple, promise: Promise) -> Self { 94 | Self { request, promise } 95 | } 96 | 97 | pub(crate) fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { 98 | self.request.serialize(cursor) 99 | } 100 | 101 | pub(crate) fn failure(&mut self, err: RequestError) { 102 | self.promise.failure(err) 103 | } 104 | 105 | pub(crate) fn handle_response( 106 | &mut self, 107 | cursor: ReadCursor, 108 | function: FunctionCode, 109 | decode: AppDecodeLevel, 110 | ) -> Result<(), RequestError> { 111 | let response = self.parse_all(cursor)?; 112 | 113 | if decode.data_headers() { 114 | tracing::info!("PDU RX - {} {}", function, response); 115 | } else if decode.header() { 116 | tracing::info!("PDU RX - {}", function); 117 | } 118 | 119 | self.promise.success(response); 120 | Ok(()) 121 | } 122 | 123 | fn parse_all(&self, mut cursor: ReadCursor) -> Result { 124 | let range = AddressRange::parse(&mut cursor)?; 125 | if range != self.request.range { 126 | return Err(RequestError::BadResponse(AduParseError::ReplyEchoMismatch)); 127 | } 128 | cursor.expect_empty()?; 129 | Ok(range) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /rodbus/src/client/requests/write_single.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::client::message::Promise; 4 | use crate::common::function::FunctionCode; 5 | use crate::decode::AppDecodeLevel; 6 | use crate::error::AduParseError; 7 | use crate::error::RequestError; 8 | use crate::types::{coil_from_u16, coil_to_u16, Indexed}; 9 | 10 | use scursor::{ReadCursor, WriteCursor}; 11 | 12 | pub(crate) trait SingleWriteOperation: Sized + PartialEq { 13 | fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError>; 14 | fn parse(cursor: &mut ReadCursor) -> Result; 15 | } 16 | 17 | pub(crate) struct SingleWrite 18 | where 19 | T: SingleWriteOperation + Display + Send + 'static, 20 | { 21 | pub(crate) request: T, 22 | promise: Promise, 23 | } 24 | 25 | impl SingleWrite 26 | where 27 | T: SingleWriteOperation + Display + Send + 'static, 28 | { 29 | pub(crate) fn new(request: T, promise: Promise) -> Self { 30 | Self { request, promise } 31 | } 32 | 33 | pub(crate) fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { 34 | self.request.serialize(cursor) 35 | } 36 | 37 | pub(crate) fn failure(&mut self, err: RequestError) { 38 | self.promise.failure(err) 39 | } 40 | 41 | pub(crate) fn handle_response( 42 | &mut self, 43 | cursor: ReadCursor, 44 | function: FunctionCode, 45 | decode: AppDecodeLevel, 46 | ) -> Result<(), RequestError> { 47 | let response = self.parse_all(cursor)?; 48 | 49 | if decode.data_headers() { 50 | tracing::info!("PDU RX - {} {}", function, response); 51 | } else if decode.header() { 52 | tracing::info!("PDU RX - {}", function); 53 | } 54 | 55 | self.promise.success(response); 56 | Ok(()) 57 | } 58 | 59 | fn parse_all(&self, mut cursor: ReadCursor) -> Result { 60 | let response = T::parse(&mut cursor)?; 61 | cursor.expect_empty()?; 62 | if self.request != response { 63 | return Err(AduParseError::ReplyEchoMismatch.into()); 64 | } 65 | Ok(response) 66 | } 67 | } 68 | 69 | impl SingleWriteOperation for Indexed { 70 | fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { 71 | cursor.write_u16_be(self.index)?; 72 | cursor.write_u16_be(coil_to_u16(self.value))?; 73 | Ok(()) 74 | } 75 | 76 | fn parse(cursor: &mut ReadCursor) -> Result { 77 | Ok(Indexed::new( 78 | cursor.read_u16_be()?, 79 | coil_from_u16(cursor.read_u16_be()?)?, 80 | )) 81 | } 82 | } 83 | 84 | impl SingleWriteOperation for Indexed { 85 | fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { 86 | cursor.write_u16_be(self.index)?; 87 | cursor.write_u16_be(self.value)?; 88 | Ok(()) 89 | } 90 | 91 | fn parse(cursor: &mut ReadCursor) -> Result { 92 | Ok(Indexed::new(cursor.read_u16_be()?, cursor.read_u16_be()?)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /rodbus/src/common/bits.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn num_bytes_for_bits(count: u16) -> usize { 2 | (count as usize).div_ceil(8) 3 | } 4 | 5 | #[cfg(test)] 6 | mod tests { 7 | use super::*; 8 | 9 | #[test] 10 | fn calculates_number_of_bytes_needed_for_count_of_packed_bits() { 11 | assert_eq!(num_bytes_for_bits(7), 1); 12 | assert_eq!(num_bytes_for_bits(8), 1); 13 | assert_eq!(num_bytes_for_bits(9), 2); 14 | assert_eq!(num_bytes_for_bits(15), 2); 15 | assert_eq!(num_bytes_for_bits(16), 2); 16 | assert_eq!(num_bytes_for_bits(17), 3); 17 | assert_eq!(num_bytes_for_bits(0xFFFF), 8192); // ensure that it's free from overflow 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rodbus/src/common/function.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | mod constants { 4 | pub(crate) const READ_COILS: u8 = 1; 5 | pub(crate) const READ_DISCRETE_INPUTS: u8 = 2; 6 | pub(crate) const READ_HOLDING_REGISTERS: u8 = 3; 7 | pub(crate) const READ_INPUT_REGISTERS: u8 = 4; 8 | pub(crate) const WRITE_SINGLE_COIL: u8 = 5; 9 | pub(crate) const WRITE_SINGLE_REGISTER: u8 = 6; 10 | pub(crate) const WRITE_MULTIPLE_COILS: u8 = 15; 11 | pub(crate) const WRITE_MULTIPLE_REGISTERS: u8 = 16; 12 | } 13 | 14 | #[derive(Debug, Copy, Clone, PartialEq)] 15 | #[repr(u8)] 16 | pub(crate) enum FunctionCode { 17 | ReadCoils = constants::READ_COILS, 18 | ReadDiscreteInputs = constants::READ_DISCRETE_INPUTS, 19 | ReadHoldingRegisters = constants::READ_HOLDING_REGISTERS, 20 | ReadInputRegisters = constants::READ_INPUT_REGISTERS, 21 | WriteSingleCoil = constants::WRITE_SINGLE_COIL, 22 | WriteSingleRegister = constants::WRITE_SINGLE_REGISTER, 23 | WriteMultipleCoils = constants::WRITE_MULTIPLE_COILS, 24 | WriteMultipleRegisters = constants::WRITE_MULTIPLE_REGISTERS, 25 | } 26 | 27 | impl Display for FunctionCode { 28 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 29 | match self { 30 | FunctionCode::ReadCoils => write!(f, "READ COILS ({:#04X})", self.get_value()), 31 | FunctionCode::ReadDiscreteInputs => { 32 | write!(f, "READ DISCRETE INPUTS ({:#04X})", self.get_value()) 33 | } 34 | FunctionCode::ReadHoldingRegisters => { 35 | write!(f, "READ HOLDING REGISTERS ({:#04X})", self.get_value()) 36 | } 37 | FunctionCode::ReadInputRegisters => { 38 | write!(f, "READ INPUT REGISTERS ({:#04X})", self.get_value()) 39 | } 40 | FunctionCode::WriteSingleCoil => { 41 | write!(f, "WRITE SINGLE COIL ({:#04X})", self.get_value()) 42 | } 43 | FunctionCode::WriteSingleRegister => { 44 | write!(f, "WRITE SINGLE REGISTER ({:#04X})", self.get_value()) 45 | } 46 | FunctionCode::WriteMultipleCoils => { 47 | write!(f, "WRITE MULTIPLE COILS ({:#04X})", self.get_value()) 48 | } 49 | FunctionCode::WriteMultipleRegisters => { 50 | write!(f, "WRITE MULTIPLE REGISTERS ({:#04X})", self.get_value()) 51 | } 52 | } 53 | } 54 | } 55 | 56 | impl FunctionCode { 57 | pub(crate) const fn get_value(self) -> u8 { 58 | self as u8 59 | } 60 | 61 | pub(crate) const fn as_error(self) -> u8 { 62 | self.get_value() | 0x80 63 | } 64 | 65 | pub(crate) fn get(value: u8) -> Option { 66 | match value { 67 | constants::READ_COILS => Some(FunctionCode::ReadCoils), 68 | constants::READ_DISCRETE_INPUTS => Some(FunctionCode::ReadDiscreteInputs), 69 | constants::READ_HOLDING_REGISTERS => Some(FunctionCode::ReadHoldingRegisters), 70 | constants::READ_INPUT_REGISTERS => Some(FunctionCode::ReadInputRegisters), 71 | constants::WRITE_SINGLE_COIL => Some(FunctionCode::WriteSingleCoil), 72 | constants::WRITE_SINGLE_REGISTER => Some(FunctionCode::WriteSingleRegister), 73 | constants::WRITE_MULTIPLE_COILS => Some(FunctionCode::WriteMultipleCoils), 74 | constants::WRITE_MULTIPLE_REGISTERS => Some(FunctionCode::WriteMultipleRegisters), 75 | _ => None, 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /rodbus/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod function; 2 | pub(crate) mod traits; 3 | 4 | pub(crate) mod bits; 5 | pub(crate) mod buffer; 6 | pub(crate) mod frame; 7 | mod parse; 8 | pub(crate) mod phys; 9 | mod serialize; 10 | -------------------------------------------------------------------------------- /rodbus/src/common/parse.rs: -------------------------------------------------------------------------------- 1 | use crate::common::traits::Parse; 2 | use crate::error::*; 3 | use crate::types::{coil_from_u16, AddressRange, Indexed}; 4 | 5 | use scursor::ReadCursor; 6 | 7 | impl Parse for AddressRange { 8 | fn parse(cursor: &mut ReadCursor) -> Result { 9 | Ok(AddressRange::try_from( 10 | cursor.read_u16_be()?, 11 | cursor.read_u16_be()?, 12 | )?) 13 | } 14 | } 15 | 16 | impl Parse for Indexed { 17 | fn parse(cursor: &mut ReadCursor) -> Result { 18 | Ok(Indexed::new( 19 | cursor.read_u16_be()?, 20 | coil_from_u16(cursor.read_u16_be()?)?, 21 | )) 22 | } 23 | } 24 | 25 | impl Parse for Indexed { 26 | fn parse(cursor: &mut ReadCursor) -> Result { 27 | Ok(Indexed::new(cursor.read_u16_be()?, cursor.read_u16_be()?)) 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod coils { 33 | use crate::common::traits::Parse; 34 | use crate::error::AduParseError; 35 | use crate::types::Indexed; 36 | 37 | use scursor::ReadCursor; 38 | 39 | #[test] 40 | fn parse_fails_for_unknown_coil_value() { 41 | let mut cursor = ReadCursor::new(&[0x00, 0x01, 0xAB, 0xCD]); 42 | let result = Indexed::::parse(&mut cursor); 43 | assert_eq!(result, Err(AduParseError::UnknownCoilState(0xABCD).into())) 44 | } 45 | 46 | #[test] 47 | fn parse_succeeds_for_valid_coil_value_false() { 48 | let mut cursor = ReadCursor::new(&[0x00, 0x01, 0x00, 0x00]); 49 | let result = Indexed::::parse(&mut cursor); 50 | assert_eq!(result, Ok(Indexed::new(1, false))); 51 | } 52 | 53 | #[test] 54 | fn parse_succeeds_for_valid_coil_value_true() { 55 | let mut cursor = ReadCursor::new(&[0x00, 0x01, 0xFF, 0x00]); 56 | let result = Indexed::::parse(&mut cursor); 57 | assert_eq!(result, Ok(Indexed::new(1, true))); 58 | } 59 | 60 | #[test] 61 | fn parse_succeeds_for_valid_indexed_register() { 62 | let mut cursor = ReadCursor::new(&[0x00, 0x01, 0xCA, 0xFE]); 63 | let result = Indexed::::parse(&mut cursor); 64 | assert_eq!(result, Ok(Indexed::new(1, 0xCAFE))); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rodbus/src/common/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::decode::AppDecodeLevel; 2 | use crate::error::*; 3 | use crate::ExceptionCode; 4 | 5 | use scursor::{ReadCursor, WriteCursor}; 6 | 7 | pub(crate) trait Serialize { 8 | fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError>; 9 | } 10 | 11 | pub(crate) trait Loggable { 12 | fn log( 13 | &self, 14 | bytes: &[u8], 15 | level: AppDecodeLevel, 16 | f: &mut std::fmt::Formatter, 17 | ) -> std::fmt::Result; 18 | } 19 | 20 | pub(crate) struct LoggableDisplay<'a, 'b> { 21 | loggable: &'a dyn Loggable, 22 | bytes: &'b [u8], 23 | level: AppDecodeLevel, 24 | } 25 | 26 | impl<'a, 'b> LoggableDisplay<'a, 'b> { 27 | pub(crate) fn new(loggable: &'a dyn Loggable, bytes: &'b [u8], level: AppDecodeLevel) -> Self { 28 | Self { 29 | loggable, 30 | bytes, 31 | level, 32 | } 33 | } 34 | } 35 | 36 | impl std::fmt::Display for LoggableDisplay<'_, '_> { 37 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 38 | self.loggable.log(self.bytes, self.level, f) 39 | } 40 | } 41 | 42 | pub(crate) trait Parse: Sized { 43 | fn parse(cursor: &mut ReadCursor) -> Result; 44 | } 45 | 46 | impl Loggable for ExceptionCode { 47 | fn log( 48 | &self, 49 | _bytes: &[u8], 50 | _level: AppDecodeLevel, 51 | f: &mut std::fmt::Formatter, 52 | ) -> std::fmt::Result { 53 | write!(f, "{self:?}") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rodbus/src/constants.rs: -------------------------------------------------------------------------------- 1 | /// u16 representation of coils when performing write single coil 2 | pub(crate) mod coil { 3 | /// u16 representation of COIL == ON when performing write single coil 4 | pub(crate) const ON: u16 = 0xFF00; 5 | /// u16 representation of COIL == OFF when performing write single coil 6 | pub(crate) const OFF: u16 = 0x0000; 7 | } 8 | 9 | /// Limits of request sizes 10 | pub mod limits { 11 | /// Maximum count allowed in a read coils/discrete inputs request 12 | pub const MAX_READ_COILS_COUNT: u16 = 0x07D0; 13 | /// Maximum count allowed in a read holding/input registers request 14 | pub const MAX_READ_REGISTERS_COUNT: u16 = 0x007D; 15 | /// Maximum count allowed in a `write multiple coils` request 16 | pub const MAX_WRITE_COILS_COUNT: u16 = 0x07B0; 17 | /// Maximum count allowed in a `write multiple registers` request 18 | pub const MAX_WRITE_REGISTERS_COUNT: u16 = 0x007B; 19 | } 20 | 21 | /// Modbus exception codes 22 | pub mod exceptions { 23 | /// Constant value corresponding to [crate::exception::ExceptionCode::IllegalFunction] 24 | pub const ILLEGAL_FUNCTION: u8 = 0x01; 25 | /// Data address received in the request is not valid for the server 26 | pub const ILLEGAL_DATA_ADDRESS: u8 = 0x02; 27 | /// A value contained in the request not allowed by the server (e.g. out of range) 28 | pub const ILLEGAL_DATA_VALUE: u8 = 0x03; 29 | /// An unrecoverable error occurred while the server was attempting to perform the requested action 30 | pub const SERVER_DEVICE_FAILURE: u8 = 0x04; 31 | /// Specialized use in conjunction with programming commands. The server accepted the request, but time is needed to fully process it. 32 | pub const ACKNOWLEDGE: u8 = 0x05; 33 | /// Specialized use in conjunction with programming commands. The server is engaged in processing a long–duration program command. 34 | pub const SERVER_DEVICE_BUSY: u8 = 0x06; 35 | /// Specialized use in conjunction with function codes 20 and 21 and reference type 6, to indicate that the extended file area failed to pass a consistency check. 36 | pub const MEMORY_PARITY_ERROR: u8 = 0x08; 37 | /// Specialized use in conjunction with gateways, indicates that the gateway was unable to allocate an internal communication path from the input port to the output port for processing the request. 38 | pub const GATEWAY_PATH_UNAVAILABLE: u8 = 0x0A; 39 | /// Specialized use in conjunction with gateways, indicates that no response was obtained from the target device. Usually means that the device is not present on the network. 40 | pub const GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND: u8 = 0x0B; 41 | } 42 | -------------------------------------------------------------------------------- /rodbus/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | //! # Example Client 3 | //! 4 | //! A simple client application that periodically polls for some Coils 5 | //! 6 | //! ```no_run 7 | //!use rodbus::*; 8 | //!use rodbus::client::*; 9 | //! 10 | //!use std::net::SocketAddr; 11 | //!use std::time::Duration; 12 | //!use std::str::FromStr; 13 | //! 14 | //! 15 | //!#[tokio::main(flavor = "multi_thread")] 16 | //!async fn main() -> Result<(), Box> { 17 | //! 18 | //! let mut channel = spawn_tcp_client_task( 19 | //! HostAddr::ip("127.0.0.1".parse()?, 502), 20 | //! 10, 21 | //! default_retry_strategy(), 22 | //! DecodeLevel::default(), 23 | //! None 24 | //! ); 25 | //! 26 | //! channel.enable().await?; 27 | //! 28 | //! let param = RequestParam::new( 29 | //! UnitId::new(0x02), 30 | //! Duration::from_secs(1), 31 | //! ); 32 | //! 33 | //! // try to poll for some coils every 3 seconds 34 | //! loop { 35 | //! match channel.read_coils(param, AddressRange::try_from(0, 5).unwrap()).await { 36 | //! Ok(values) => { 37 | //! for x in values { 38 | //! println!("index: {} value: {}", x.index, x.value) 39 | //! } 40 | //! } 41 | //! Err(err) => println!("Error: {:?}", err) 42 | //! } 43 | //! 44 | //! tokio::time::sleep(std::time::Duration::from_secs(3)).await 45 | //! } 46 | //!} 47 | //! ``` 48 | //! 49 | //! # Example Server 50 | //! 51 | //! ```no_run 52 | //! use rodbus::*; 53 | //! use rodbus::server::*; 54 | //! 55 | //! use std::net::SocketAddr; 56 | //! use std::str::FromStr; 57 | //! 58 | //! use tokio::net::TcpListener; 59 | //! 60 | //! struct CoilsOnlyHandler { 61 | //! pub coils: [bool; 10] 62 | //! } 63 | //! 64 | //! impl CoilsOnlyHandler { 65 | //! fn new() -> Self { 66 | //! Self { 67 | //! coils: [false; 10] 68 | //! } 69 | //! } 70 | //! } 71 | //! 72 | //! impl RequestHandler for CoilsOnlyHandler { 73 | //! fn read_coil(&self, address: u16) -> Result { 74 | //! self.coils.get(0).to_result() 75 | //! } 76 | //! } 77 | //! 78 | //! #[tokio::main(flavor = "multi_thread")] 79 | //! async fn main() -> Result<(), Box> { 80 | //! 81 | //! let handler = CoilsOnlyHandler::new().wrap(); 82 | //! 83 | //! // map unit ids to a handler for processing requests 84 | //! let map = ServerHandlerMap::single(UnitId::new(1), handler.clone()); 85 | //! 86 | //! // spawn a server to handle connections onto its own task 87 | //! // if the handle _server is dropped, the server shuts down 88 | //! let _server = rodbus::server::spawn_tcp_server_task( 89 | //! 1, 90 | //! SocketAddr::from_str("127.0.0.1:502")?, 91 | //! map, 92 | //! AddressFilter::Any, 93 | //! DecodeLevel::default(), 94 | //! ).await?; 95 | //! 96 | //! let mut next = tokio::time::Instant::now(); 97 | //! 98 | //! // toggle all coils every couple of seconds 99 | //! loop { 100 | //! next += tokio::time::Duration::from_secs(2); 101 | //! { 102 | //! let mut guard = handler.lock().unwrap(); 103 | //! for c in &mut guard.coils { 104 | //! *c = !*c; 105 | //! } 106 | //! } 107 | //! tokio::time::sleep_until(next).await; 108 | //! } 109 | //!} 110 | //!``` 111 | 112 | /// Current version of the library 113 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 114 | 115 | /// Client API 116 | pub mod client; 117 | /// Public constant values related to the Modbus specification 118 | pub mod constants; 119 | 120 | /// Server API 121 | pub mod server; 122 | 123 | // modules that are re-exported 124 | pub(crate) mod channel; 125 | pub(crate) mod decode; 126 | pub(crate) mod error; 127 | pub(crate) mod exception; 128 | pub(crate) mod maybe_async; 129 | pub(crate) mod retry; 130 | #[cfg(feature = "serial")] 131 | mod serial; 132 | pub(crate) mod types; 133 | 134 | // re-exports 135 | pub use crate::decode::*; 136 | pub use crate::error::*; 137 | pub use crate::exception::*; 138 | pub use crate::maybe_async::*; 139 | pub use crate::retry::*; 140 | #[cfg(feature = "serial")] 141 | pub use crate::serial::*; 142 | pub use crate::types::*; 143 | 144 | // internal modules 145 | mod common; 146 | mod tcp; 147 | -------------------------------------------------------------------------------- /rodbus/src/maybe_async.rs: -------------------------------------------------------------------------------- 1 | enum Value { 2 | Ready(T), 3 | Async(core::pin::Pin + Send + 'static>>), 4 | } 5 | 6 | /// Represents a result that may be computed synchronously or asynchronously by user code. 7 | /// 8 | /// Rust does not currently allow `async fn` in trait methods, so we need 9 | /// a workaround. There are crates such as `async_trait` which provide proc_macros 10 | /// that do this, but they don't provide an optimization to *avoid* the heap allocation 11 | /// if the underlying implementation is synchronous. 12 | /// 13 | /// This allows us to use `async` operations in Rust if desired, but just have synchronous callbacks 14 | /// in the FFI without paying always allocating. 15 | #[must_use] 16 | pub struct MaybeAsync { 17 | inner: Value, 18 | } 19 | 20 | impl MaybeAsync { 21 | /// Retrieve the value, which might be available immediately or require awaiting 22 | pub async fn get(self) -> T { 23 | match self.inner { 24 | Value::Ready(x) => x, 25 | Value::Async(x) => x.await, 26 | } 27 | } 28 | 29 | /// Construct a new `MaybeAsync` from an already available result 30 | pub fn ready(result: T) -> Self { 31 | MaybeAsync { 32 | inner: Value::Ready(result), 33 | } 34 | } 35 | 36 | /// Construct a new `MaybeAsync` from a future which yields the value eventually 37 | pub fn asynchronous(result: F) -> Self 38 | where 39 | F: core::future::Future + Send + 'static, 40 | { 41 | MaybeAsync { 42 | inner: Value::Async(Box::pin(result)), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rodbus/src/retry.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | /// Trait that controls how the channel retries failed connect (TCP/TLS) or open (serial) attempts 4 | pub trait RetryStrategy: Send { 5 | /// Reset internal state. Called when a connection is successful or a port is opened 6 | fn reset(&mut self); 7 | /// Return the next delay before making another connection/open attempt 8 | fn after_failed_connect(&mut self) -> Duration; 9 | /// Return the delay to wait after a disconnect before attempting to reconnect/open 10 | fn after_disconnect(&mut self) -> Duration; 11 | } 12 | 13 | /// Return the default [`RetryStrategy`] 14 | pub fn default_retry_strategy() -> Box { 15 | doubling_retry_strategy(Duration::from_millis(1000), Duration::from_secs(60000)) 16 | } 17 | 18 | /// Return a [`RetryStrategy`] that doubles on failure up to a maximum value 19 | pub fn doubling_retry_strategy(min: Duration, max: Duration) -> Box { 20 | Doubling::create(min, max) 21 | } 22 | 23 | struct Doubling { 24 | min: Duration, 25 | max: Duration, 26 | current: Duration, 27 | } 28 | 29 | impl Doubling { 30 | pub(crate) fn create(min: Duration, max: Duration) -> Box { 31 | Box::new(Doubling { 32 | min, 33 | max, 34 | current: min, 35 | }) 36 | } 37 | } 38 | 39 | impl RetryStrategy for Doubling { 40 | fn reset(&mut self) { 41 | self.current = self.min; 42 | } 43 | 44 | fn after_failed_connect(&mut self) -> Duration { 45 | let ret = self.current; 46 | self.current = std::cmp::min(2 * self.current, self.max); 47 | ret 48 | } 49 | 50 | fn after_disconnect(&mut self) -> Duration { 51 | self.min 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rodbus/src/serial/client.rs: -------------------------------------------------------------------------------- 1 | use crate::common::phys::PhysLayer; 2 | use crate::decode::DecodeLevel; 3 | use crate::serial::SerialSettings; 4 | 5 | use crate::client::message::Command; 6 | use crate::client::task::{ClientLoop, SessionError, StateChange}; 7 | use crate::client::{Listener, PortState, RetryStrategy}; 8 | use crate::common::frame::{FrameWriter, FramedReader}; 9 | use crate::error::Shutdown; 10 | 11 | pub(crate) struct SerialChannelTask { 12 | path: String, 13 | serial_settings: SerialSettings, 14 | retry: Box, 15 | client_loop: ClientLoop, 16 | listener: Box>, 17 | } 18 | 19 | impl SerialChannelTask { 20 | pub(crate) fn new( 21 | path: &str, 22 | serial_settings: SerialSettings, 23 | rx: crate::channel::Receiver, 24 | retry: Box, 25 | decode: DecodeLevel, 26 | listener: Box>, 27 | ) -> Self { 28 | Self { 29 | path: path.to_string(), 30 | serial_settings, 31 | retry, 32 | client_loop: ClientLoop::new( 33 | rx, 34 | FrameWriter::rtu(), 35 | FramedReader::rtu_response(), 36 | decode, 37 | ), 38 | listener, 39 | } 40 | } 41 | 42 | pub(crate) async fn run(&mut self) -> Shutdown { 43 | self.listener.update(PortState::Disabled).get().await; 44 | let ret = self.run_inner().await; 45 | self.listener.update(PortState::Shutdown).get().await; 46 | ret 47 | } 48 | 49 | async fn run_inner(&mut self) -> Shutdown { 50 | loop { 51 | // wait for the channel to be enabled 52 | if let Err(Shutdown) = self.client_loop.wait_for_enabled().await { 53 | return Shutdown; 54 | } 55 | 56 | if let Err(StateChange::Shutdown) = self.try_open_and_run().await { 57 | return Shutdown; 58 | } 59 | 60 | if !self.client_loop.is_enabled() { 61 | self.listener.update(PortState::Disabled).get().await; 62 | } 63 | } 64 | } 65 | 66 | pub(crate) async fn try_open_and_run(&mut self) -> Result<(), StateChange> { 67 | match crate::serial::open(self.path.as_str(), self.serial_settings) { 68 | Err(err) => { 69 | let delay = self.retry.after_failed_connect(); 70 | self.listener.update(PortState::Wait(delay)).get().await; 71 | tracing::warn!("{} - waiting {} ms to re-open port", err, delay.as_millis()); 72 | self.client_loop.fail_requests_for(delay).await 73 | } 74 | Ok(serial) => { 75 | self.retry.reset(); 76 | self.listener.update(PortState::Open).get().await; 77 | let mut phys = PhysLayer::new_serial(serial); 78 | tracing::info!("serial port open"); 79 | match self.client_loop.run(&mut phys).await { 80 | // the mpsc was closed, end the task 81 | SessionError::Shutdown => Err(StateChange::Shutdown), 82 | // don't wait, we're disabled 83 | SessionError::Disabled => Ok(()), 84 | // wait before retrying 85 | SessionError::IoError(_) | SessionError::BadFrame => { 86 | let delay = self.retry.after_disconnect(); 87 | self.listener.update(PortState::Wait(delay)).get().await; 88 | tracing::warn!("waiting {} ms to re-open port", delay.as_millis()); 89 | self.client_loop.fail_requests_for(delay).await 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /rodbus/src/serial/mod.rs: -------------------------------------------------------------------------------- 1 | use tokio_serial::SerialStream; 2 | pub use tokio_serial::{DataBits, FlowControl, Parity, StopBits}; 3 | 4 | pub(crate) mod client; 5 | pub(crate) mod frame; 6 | pub(crate) mod server; 7 | 8 | /// Serial port settings 9 | #[derive(Copy, Clone, Debug)] 10 | pub struct SerialSettings { 11 | /// Baud rate of the port 12 | pub baud_rate: u32, 13 | /// Number of data bits 14 | pub data_bits: DataBits, 15 | /// Types of flow control 16 | pub flow_control: FlowControl, 17 | /// Number of stop bits 18 | pub stop_bits: StopBits, 19 | /// Parity setting 20 | pub parity: Parity, 21 | } 22 | 23 | impl SerialSettings { 24 | pub(crate) fn apply( 25 | &self, 26 | builder: tokio_serial::SerialPortBuilder, 27 | ) -> tokio_serial::SerialPortBuilder { 28 | builder 29 | .baud_rate(self.baud_rate) 30 | .data_bits(self.data_bits) 31 | .flow_control(self.flow_control) 32 | .stop_bits(self.stop_bits) 33 | .parity(self.parity) 34 | } 35 | } 36 | 37 | impl Default for SerialSettings { 38 | fn default() -> Self { 39 | Self { 40 | baud_rate: 9600, 41 | data_bits: DataBits::Eight, 42 | flow_control: FlowControl::None, 43 | stop_bits: StopBits::One, 44 | parity: Parity::None, 45 | } 46 | } 47 | } 48 | 49 | pub(crate) fn open(path: &str, settings: SerialSettings) -> tokio_serial::Result { 50 | let builder = settings.apply(tokio_serial::new(path, settings.baud_rate)); 51 | SerialStream::open(&builder) 52 | } 53 | -------------------------------------------------------------------------------- /rodbus/src/serial/server.rs: -------------------------------------------------------------------------------- 1 | use crate::common::phys::PhysLayer; 2 | use crate::server::task::SessionTask; 3 | use crate::server::RequestHandler; 4 | use crate::{RequestError, RetryStrategy, SerialSettings, Shutdown}; 5 | 6 | pub(crate) struct RtuServerTask 7 | where 8 | T: RequestHandler, 9 | { 10 | pub(crate) port: String, 11 | pub(crate) retry: Box, 12 | pub(crate) settings: SerialSettings, 13 | pub(crate) session: SessionTask, 14 | } 15 | 16 | impl RtuServerTask 17 | where 18 | T: RequestHandler, 19 | { 20 | pub(crate) async fn run(&mut self) -> Shutdown { 21 | loop { 22 | match crate::serial::open(&self.port, self.settings) { 23 | Ok(serial) => { 24 | self.retry.reset(); 25 | tracing::info!("opened port"); 26 | // run an open port until shutdown or failure 27 | let mut phys = PhysLayer::new_serial(serial); 28 | if let RequestError::Shutdown = self.session.run(&mut phys).await { 29 | return Shutdown; 30 | } 31 | // we wait here to prevent any kind of rapid retry scenario if the port opens and immediately fails 32 | let delay = self.retry.after_disconnect(); 33 | tracing::warn!("waiting {:?} to reopen port", delay); 34 | if let Err(Shutdown) = self.session.sleep_for(delay).await { 35 | return Shutdown; 36 | } 37 | } 38 | Err(err) => { 39 | let delay = self.retry.after_failed_connect(); 40 | tracing::warn!( 41 | "unable to open serial port, retrying in {:?} - error: {}", 42 | delay, 43 | err 44 | ); 45 | if let Err(Shutdown) = self.session.sleep_for(delay).await { 46 | return Shutdown; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rodbus/src/server/address_filter.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | /// Represents IPv4 addresses which may contain "*" wildcards 4 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 5 | pub struct WildcardIPv4 { 6 | pub(crate) b3: Option, 7 | pub(crate) b2: Option, 8 | pub(crate) b1: Option, 9 | pub(crate) b0: Option, 10 | } 11 | 12 | /// Error returned when an IPv4 wildcard is not in the correct format 13 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 14 | pub struct BadIpv4Wildcard; 15 | 16 | fn get_byte(value: &str) -> Result, BadIpv4Wildcard> { 17 | match value { 18 | "*" => Ok(None), 19 | _ => match value.parse::() { 20 | Ok(x) => Ok(Some(x)), 21 | Err(_) => Err(BadIpv4Wildcard), 22 | }, 23 | } 24 | } 25 | 26 | impl FromStr for WildcardIPv4 { 27 | type Err = BadIpv4Wildcard; 28 | 29 | fn from_str(s: &str) -> Result { 30 | let mut iter = s.split('.'); 31 | let b3 = get_byte(iter.next().ok_or(BadIpv4Wildcard)?)?; 32 | let b2 = get_byte(iter.next().ok_or(BadIpv4Wildcard)?)?; 33 | let b1 = get_byte(iter.next().ok_or(BadIpv4Wildcard)?)?; 34 | let b0 = get_byte(iter.next().ok_or(BadIpv4Wildcard)?)?; 35 | 36 | if iter.next().is_some() { 37 | return Err(BadIpv4Wildcard); 38 | } 39 | 40 | Ok(WildcardIPv4 { b3, b2, b1, b0 }) 41 | } 42 | } 43 | 44 | impl WildcardIPv4 { 45 | pub(crate) fn matches(&self, addr: std::net::IpAddr) -> bool { 46 | fn bm(b: u8, other: Option) -> bool { 47 | match other { 48 | Some(x) => b == x, 49 | None => true, 50 | } 51 | } 52 | 53 | match addr { 54 | std::net::IpAddr::V4(x) => { 55 | let [b3, b2, b1, b0] = x.octets(); 56 | bm(b3, self.b3) && bm(b2, self.b2) && bm(b1, self.b1) && bm(b0, self.b0) 57 | } 58 | std::net::IpAddr::V6(_) => false, 59 | } 60 | } 61 | } 62 | 63 | /// Address filter used to control which master address(es) may connect to an outstation. 64 | /// 65 | /// Note: User code cannot exhaustively match against this enum as new variants may be added in the future. 66 | #[non_exhaustive] 67 | #[derive(Clone, Debug)] 68 | pub enum AddressFilter { 69 | /// Allow any address 70 | Any, 71 | /// Allow a specific address 72 | Exact(std::net::IpAddr), 73 | /// Allow any of set of addresses 74 | AnyOf(std::collections::HashSet), 75 | /// Matches against an IPv4 address with wildcards 76 | WildcardIpv4(WildcardIPv4), 77 | } 78 | 79 | impl AddressFilter { 80 | pub(crate) fn matches(&self, addr: std::net::IpAddr) -> bool { 81 | match self { 82 | AddressFilter::Any => true, 83 | AddressFilter::Exact(x) => *x == addr, 84 | AddressFilter::AnyOf(set) => set.contains(&addr), 85 | AddressFilter::WildcardIpv4(wc) => wc.matches(addr), 86 | } 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod test { 92 | use super::{BadIpv4Wildcard, WildcardIPv4}; 93 | use std::net::IpAddr; 94 | 95 | #[test] 96 | fn parses_address_with_subnet_wildcard() { 97 | let wc: WildcardIPv4 = "172.17.20.*".parse().unwrap(); 98 | assert_eq!( 99 | wc, 100 | WildcardIPv4 { 101 | b3: Some(172), 102 | b2: Some(17), 103 | b1: Some(20), 104 | b0: None 105 | } 106 | ) 107 | } 108 | 109 | #[test] 110 | fn parses_all_wildcards() { 111 | let wc: WildcardIPv4 = "*.*.*.*".parse().unwrap(); 112 | assert_eq!( 113 | wc, 114 | WildcardIPv4 { 115 | b3: None, 116 | b2: None, 117 | b1: None, 118 | b0: None 119 | } 120 | ) 121 | } 122 | 123 | #[test] 124 | fn rejects_bad_input() { 125 | let bad_input = [ 126 | "*.*.*.*.*", 127 | "*.*..*.*", 128 | "*.256.*.*", 129 | ".*.256.*.*", 130 | "1.1.1.1ab", 131 | ]; 132 | 133 | for x in bad_input { 134 | let res: Result = x.parse(); 135 | assert_eq!(res, Err(BadIpv4Wildcard)); 136 | } 137 | 138 | let res: Result = "*.*.*.*.*".parse(); 139 | assert_eq!(res, Err(BadIpv4Wildcard)); 140 | } 141 | 142 | #[test] 143 | fn wildcard_matching_works() { 144 | let wc: WildcardIPv4 = "192.168.0.*".parse().unwrap(); 145 | let ip1: IpAddr = "192.168.0.1".parse().unwrap(); 146 | let ip2: IpAddr = "192.168.1.1".parse().unwrap(); 147 | 148 | assert!(wc.matches(ip1)); 149 | assert!(!wc.matches(ip2)); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /rodbus/src/server/response.rs: -------------------------------------------------------------------------------- 1 | use crate::exception::ExceptionCode; 2 | use crate::types::{ReadBitsRange, ReadRegistersRange}; 3 | 4 | pub(crate) struct BitWriter 5 | where 6 | T: Fn(u16) -> Result, 7 | { 8 | pub(crate) range: ReadBitsRange, 9 | pub(crate) getter: T, 10 | } 11 | 12 | impl BitWriter 13 | where 14 | T: Fn(u16) -> Result, 15 | { 16 | pub(crate) fn new(range: ReadBitsRange, getter: T) -> Self { 17 | Self { range, getter } 18 | } 19 | } 20 | 21 | pub(crate) struct RegisterWriter 22 | where 23 | T: Fn(u16) -> Result, 24 | { 25 | pub(crate) range: ReadRegistersRange, 26 | pub(crate) getter: T, 27 | } 28 | 29 | impl RegisterWriter 30 | where 31 | T: Fn(u16) -> Result, 32 | { 33 | pub(crate) fn new(range: ReadRegistersRange, getter: T) -> Self { 34 | Self { range, getter } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rodbus/src/server/types.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{AddressRange, BitIterator, RegisterIterator}; 2 | 3 | /// Request to write coils received by the server 4 | #[derive(Debug, Copy, Clone)] 5 | pub struct WriteCoils<'a> { 6 | /// address range of the request 7 | pub range: AddressRange, 8 | /// lazy iterator over the coil values to write 9 | pub iterator: BitIterator<'a>, 10 | } 11 | 12 | impl<'a> WriteCoils<'a> { 13 | pub(crate) fn new(range: AddressRange, iterator: BitIterator<'a>) -> Self { 14 | Self { range, iterator } 15 | } 16 | } 17 | 18 | /// Request to write registers received by the server 19 | #[derive(Debug, Copy, Clone)] 20 | pub struct WriteRegisters<'a> { 21 | /// address range of the request 22 | pub range: AddressRange, 23 | /// lazy iterator over the register values to write 24 | pub iterator: RegisterIterator<'a>, 25 | } 26 | 27 | impl<'a> WriteRegisters<'a> { 28 | pub(crate) fn new(range: AddressRange, iterator: RegisterIterator<'a>) -> Self { 29 | Self { range, iterator } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rodbus/src/tcp/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod client; 2 | pub(crate) mod frame; 3 | pub(crate) mod server; 4 | 5 | #[cfg(feature = "tls")] 6 | pub(crate) mod tls; 7 | -------------------------------------------------------------------------------- /rodbus/src/tcp/tls/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod client; 2 | pub(crate) mod server; 3 | 4 | pub(crate) use client::*; 5 | pub(crate) use server::*; 6 | 7 | /// Determines how the certificate(s) presented by the peer are validated 8 | /// 9 | /// This validation always occurs **after** the handshake signature has been 10 | /// verified. 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 12 | pub enum CertificateMode { 13 | /// Validates the peer certificate against one or more configured trust anchors 14 | /// 15 | /// This mode uses the default certificate verifier in `rustls` to ensure that 16 | /// the chain of certificates presented by the peer is valid against one of 17 | /// the configured trust anchors. 18 | /// 19 | /// The name verification is relaxed to allow for certificates that do not contain 20 | /// the SAN extension. In these cases the name is verified using the Common Name instead. 21 | AuthorityBased, 22 | /// Validates that the peer presents a single certificate which is a byte-for-byte match 23 | /// against the configured peer certificate. 24 | /// 25 | /// The certificate is parsed only to ensure that the `NotBefore` and `NotAfter` 26 | /// are valid for the current system time. 27 | SelfSigned, 28 | } 29 | 30 | /// TLS-related errors 31 | #[derive(Debug)] 32 | pub enum TlsError { 33 | /// Invalid peer certificate 34 | InvalidPeerCertificate(std::io::Error), 35 | /// Invalid local certificate 36 | InvalidLocalCertificate(std::io::Error), 37 | /// Invalid private key 38 | InvalidPrivateKey(std::io::Error), 39 | /// DNS name is invalid 40 | InvalidDnsName, 41 | /// Error building TLS configuration 42 | BadConfig(String), 43 | } 44 | 45 | impl std::fmt::Display for TlsError { 46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | match self { 48 | Self::InvalidPeerCertificate(err) => { 49 | write!(f, "invalid peer certificate file: {err}") 50 | } 51 | Self::InvalidLocalCertificate(err) => { 52 | write!(f, "invalid local certificate file: {err}") 53 | } 54 | Self::InvalidPrivateKey(err) => write!(f, "invalid private key file: {err}"), 55 | Self::InvalidDnsName => write!(f, "invalid DNS name"), 56 | Self::BadConfig(err) => write!(f, "bad config: {err}"), 57 | } 58 | } 59 | } 60 | 61 | impl std::error::Error for TlsError {} 62 | 63 | impl From for TlsError { 64 | fn from(err: sfio_rustls_config::Error) -> Self { 65 | Self::BadConfig(err.to_string()) 66 | } 67 | } 68 | 69 | /// Minimum TLS version to allow 70 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 71 | pub enum MinTlsVersion { 72 | /// TLS 1.2 73 | V1_2, 74 | /// TLS 1.3 75 | V1_3, 76 | } 77 | 78 | /* 79 | impl From for sfio_rustls_config::MinProtocolVersion { 80 | fn from(value: MinTlsVersion) -> Self { 81 | match value { 82 | MinTlsVersion::V1_2 => sfio_rustls_config::MinProtocolVersion::V1_2, 83 | MinTlsVersion::V1_3 => sfio_rustls_config::MinProtocolVersion::V1_3, 84 | } 85 | } 86 | } 87 | 88 | impl From for TlsError { 89 | fn from(_: InvalidDnsNameError) -> Self { 90 | Self::InvalidDnsName 91 | } 92 | } 93 | */ 94 | -------------------------------------------------------------------------------- /rodbus/src/tcp/tls/server.rs: -------------------------------------------------------------------------------- 1 | use sfio_rustls_config::ClientNameVerification; 2 | use std::path::Path; 3 | use std::sync::Arc; 4 | 5 | use tokio::net::TcpStream; 6 | use tokio_rustls::rustls; 7 | 8 | use crate::common::phys::PhysLayer; 9 | use crate::server::task::AuthorizationType; 10 | use crate::server::AuthorizationHandler; 11 | use crate::tcp::tls::{CertificateMode, MinTlsVersion, TlsError}; 12 | 13 | /// TLS configuration 14 | #[derive(Clone)] 15 | pub struct TlsServerConfig { 16 | inner: Arc, 17 | } 18 | 19 | impl TlsServerConfig { 20 | /// Create a TLS server config 21 | pub fn new( 22 | peer_cert_path: &Path, 23 | local_cert_path: &Path, 24 | private_key_path: &Path, 25 | password: Option<&str>, 26 | min_tls_version: MinTlsVersion, 27 | certificate_mode: CertificateMode, 28 | ) -> Result { 29 | let config = match certificate_mode { 30 | CertificateMode::SelfSigned => sfio_rustls_config::server::self_signed( 31 | min_tls_version.into(), 32 | peer_cert_path, 33 | local_cert_path, 34 | private_key_path, 35 | password, 36 | )?, 37 | CertificateMode::AuthorityBased => sfio_rustls_config::server::authority( 38 | min_tls_version.into(), 39 | ClientNameVerification::None, 40 | peer_cert_path, 41 | local_cert_path, 42 | private_key_path, 43 | password, 44 | )?, 45 | }; 46 | 47 | Ok(TlsServerConfig { 48 | inner: Arc::new(config), 49 | }) 50 | } 51 | 52 | pub(crate) async fn handle_connection( 53 | &mut self, 54 | socket: TcpStream, 55 | auth_handler: Option>, 56 | ) -> Result<(PhysLayer, AuthorizationType), String> { 57 | let connector = tokio_rustls::TlsAcceptor::from(self.inner.clone()); 58 | match connector.accept(socket).await { 59 | Err(err) => Err(format!("failed to establish TLS session: {err}")), 60 | Ok(stream) => { 61 | let auth_type = match auth_handler { 62 | // bare TLS mode without authz 63 | None => AuthorizationType::None, 64 | // full secure modbus requires the client certificate contain a role 65 | Some(handler) => { 66 | // get the peer cert data 67 | let peer_cert = stream 68 | .get_ref() 69 | .1 70 | .peer_certificates() 71 | .and_then(|x| x.first()) 72 | .ok_or_else(|| "No peer certificate".to_string())?; 73 | 74 | let parsed = rx509::x509::Certificate::parse(peer_cert) 75 | .map_err(|err| format!("ASNError: {err}"))?; 76 | let role = extract_modbus_role(&parsed)?; 77 | 78 | tracing::info!("client role: {}", role); 79 | AuthorizationType::Handler(handler, role) 80 | } 81 | }; 82 | 83 | let layer = PhysLayer::new_tls(tokio_rustls::TlsStream::from(stream)); 84 | 85 | Ok((layer, auth_type)) 86 | } 87 | } 88 | } 89 | } 90 | 91 | fn extract_modbus_role(cert: &rx509::x509::Certificate) -> Result { 92 | // Parse the extensions 93 | let extensions = cert 94 | .tbs_certificate 95 | .value 96 | .extensions 97 | .as_ref() 98 | .ok_or_else(|| "certificate doesn't contain Modbus role extension".to_string())?; 99 | 100 | let extensions = extensions 101 | .parse() 102 | .map_err(|err| format!("unable to parse cert extensions with rasn: {err:?}"))?; 103 | 104 | // Extract the ModbusRole extensions 105 | let mut it = extensions.into_iter().filter_map(|ext| match ext.content { 106 | rx509::x509::ext::SpecificExtension::ModbusRole(role) => Some(role.role), 107 | _ => None, 108 | }); 109 | 110 | // Extract the first ModbusRole extension 111 | let role = it 112 | .next() 113 | .ok_or_else(|| "certificate doesn't have Modbus extension".to_string())?; 114 | 115 | // Check that there is only one role extension 116 | if it.next().is_some() { 117 | return Err("certificate has more than one Modbus extension".to_string()); 118 | } 119 | 120 | Ok(role.to_string()) 121 | } 122 | -------------------------------------------------------------------------------- /sfio_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepfunc/rodbus/1833f4b32d30c8266a90a952dadb593f2075d29a/sfio_logo.png --------------------------------------------------------------------------------