Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions guide/docs/about/dependencies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,20 @@ slug: /dependencies
import useBaseUrl from '@docusaurus/useBaseUrl';
import sitedata from '../../sitedata.json'

Rust's package manager (`cargo`) makes it easy to work with external dependencies. This is wonderful for development, but means that Rust applications
Rust's package manager (`cargo`) makes it easy to work with external dependencies. While this benefits development, Rust applications
and libraries tend to have many dependencies.

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
our direct and indirect dependencies.
Our library directly depends on only a few third-party libraries, though those libraries have many transitive dependencies.

## Dependency Whitelisting

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
checks are performed:
We use a dependency whitelist to ensure that we never incorporate unapproved dependencies. During each CI build, we perform the following
checks:

* 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There appears to be a small grammatical error in this sentence. The word "add" should likely be "a".

* Check every dependency against the whitelist. Our CI packaging will fail if a dependency is added with a license that has not been pre-approved.

* Produce a license report called `third-party-licenses.txt` that consolidates all the dependency and license information. We include this document in all of
our binary distributions.
* 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.
* Ignore projects wholly owned by Step Function I/O, e.g. the library itself and dependencies shared with our other protocol libraries.

:::note
The license report file differs slightly for the Java library as it incorporates some additional components for the JNI functionality.
Expand All @@ -36,6 +35,6 @@ incorporation of strong copyleft licenses such as the GPL. You can see a complet

## Disclaimer

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.
We include this information because we take open source license compliance seriously. However, this information and the `third-party-licenses.txt` file are provided for reference and do not constitute legal advice. Treat this information as a starting point so you can perform your own due diligence.


2 changes: 1 addition & 1 deletion guide/docs/about/guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ In addition to this documentation, you can find example programs in the source r

## Modbus Standard

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,
While this guide covers many Modbus concepts, it does not replicate the Modbus standard. If you plan to develop a product that uses Modbus,
you can get a free copy from the [Modbus Organization website](https://modbus.org/).

<div style={{textAlign: 'center'}}>
Expand Down
13 changes: 6 additions & 7 deletions guide/docs/about/modbus.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ slug: /modbus

import useBaseUrl from '@docusaurus/useBaseUrl';

Modbus is a ubiquitous communication protocol used to communicate with various industrial devices, most notably PLCs. Its simplicity and versatility makes
it very popular in numerous applications. The standard itself is freely available from the [Modbus Organization](https://modbus.org/). It supports both TCP/IP
Modbus is a ubiquitous communication protocol used to communicate with various industrial devices, most notably PLCs. Its simplicity and versatility make
it popular in numerous applications. The standard itself is freely available from the [Modbus Organization](https://modbus.org/). It supports both TCP/IP
and serial communication, and more recently, has been defined for secure operation over TLS. Modbus is sometimes referred to as "Modicon Modbus" or just
"Modicon" for historical reasons.

Expand Down Expand Up @@ -55,7 +55,7 @@ remove the leading entity type and subtract 1.

### Point interpretation

Due to the limited point types available in Modbus, the data model is often extended to represent more complex values. You will need to read device
Because Modbus has limited point types, applications often extend the data model to represent more complex values. You will need to read device
documentation to interpret the data appropriately. For example, applications often use multiple registers to represent a single physical value.

Here are some examples of common point interpretations:
Expand Down Expand Up @@ -85,11 +85,10 @@ The following table lists the functions currently supported by our library:
| 0x15 | Write multiple coils | ✔️ |
| 0x16 | Write multiple registers | ✔️ |

These functions are all that is required for the vast majority of applications using Modbus. We would consider adding support for additional
functions codes if requested by a potential customer.
These functions are all that is required for the vast majority of applications using Modbus. Additional function codes can be added upon request.

Modbus also allows user-defined function codes in the range 65 to 72 and 100 to 110. This library does not support adding user defined functions at this time,
but could be added if desired.
Modbus also allows user-defined function codes in the range 65 to 72 and 100 to 110. This library does not currently support user-defined functions,
but support can be added upon request.

## Exceptions

Expand Down
10 changes: 4 additions & 6 deletions guide/docs/api/client/requests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ The library supports four read requests:
| `read_holding_registers` | 16-bit registers |
| `read_input_registers` | 16-bit registers |

Each read request takes an `AddressRange` parameter to specify what values are requested.
Each read request takes an `AddressRange` parameter specifying which values to retrieve.
It has a 16-bit `start` index and a 16-bit `count` value.

:::warning
The sum of the `start` and `count` fields **cannot exceed 65,536**. The library will automatically fail such invalid requests
returning an error.
:::

The asynchronous response will contain an iterator with the received values. If an error occurs, the iterator will be empty.
The asynchronous response contains an iterator over the retrieved values. If an error occurs, the iterator is empty.

The following example demonstrates reading the first 5 coils from a device:

Expand Down Expand Up @@ -127,12 +127,10 @@ The registers that are written using the `write_single_register` and `write_mult
are *holding registers*.
:::

The "single" requests, as their name suggests, writes a single point at a time.
Single requests write one point at a time.
The "multiple" requests write multiple **contiguous** points of the same type in a single message.

In order to write a set of discontinuous points, you must perform multiple requests. Modbus does
not have a transaction mechanism. Therefore, you must keep in mind that the device state
might change in between requests.
To write discontinuous points, perform multiple requests. Modbus lacks a transaction mechanism, so device state may change between requests.

The following example demonstrates how to write a coil:

Expand Down
5 changes: 2 additions & 3 deletions guide/docs/api/client/rtu_client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ slug: /api/client/rtu_client
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

You can create a RTU client channel using the `create_rtu_client` method. It will immediately try to open the serial port.
Create an RTU client channel using the `create_rtu_client` method. The client immediately attempts to open the serial port.

:::note
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.
Expand Down Expand Up @@ -88,8 +88,7 @@ Each channel sends one request at a time and has a fixed-length buffer of reques

## Retry Delay

A serial channel tries to open the serial port as soon as it is created. If the serial port cannot be opened, the library
automatically waits `retry_delay` before retrying to open the port.
The serial channel attempts to open the port immediately upon creation. If the port cannot be opened, the library waits `retry_delay` before retrying.

## Decode Level

Expand Down
8 changes: 3 additions & 5 deletions guide/docs/api/client/tcp_client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ slug: /api/client/tcp_client
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

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,
regardless of the underlying transport. You can create a TCP client channel using `create_tcp` method.
The `ClientChannel` class represents a communication channel for making requests to server devices. The channel presents the same interface regardless of underlying transport. Create a TCP client channel using the `create_tcp` method.

:::note
In Rust, you can use the `spawn_tcp_client_task` to create a channel and spawn the runner task in the current runtime.
Expand Down Expand Up @@ -77,10 +76,9 @@ The argument for the remote endpoint is a string in format the `<host>:<port>` w

## Retry Strategy

A TCP channel tries to establish and maintain a connection as soon as it is created. To avoid flooding, reconnection delays are applied.
A TCP channel establishes and maintains a connection immediately upon creation. The client applies reconnection delays to avoid flooding.

The `RetryStrategy` controls the rate at which the client retries failed connection attempts. The client uses exponential backoff when attempting to establish
a connection. The delay between attempts doubles from `min_delay` up to `max_delay`.
The `RetryStrategy` determines how quickly the client retries failed connections. The client uses exponential backoff: the delay between attempts doubles from `min_delay` up to `max_delay`.

## Decode Level

Expand Down
9 changes: 4 additions & 5 deletions guide/docs/api/logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import TabItem from '@theme/TabItem';
The library provides highly-contextual logging using the [tracing](https://crates.io/crates/tracing) crate. If you're using Rust, refer to the
tracing documentation for details.

In comparison, the bindings use a rigid logging interface with a single callback method to record a message. Configurable options include:
The bindings provide a single callback method for logging messages. Configurable options include:

* `LogLevel` that controls which messages are generated
* How and if to print the time as part of the message
* Line or JSON based output

:::note
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.
The LogLevel defaults to Info, recording Info, Warn, and Error messages. Use Debug and Trace levels only when debugging runtime issues.

Protocol decoding is always logged at the Info level and is configured separately on a per channel basis.
:::
Expand Down Expand Up @@ -87,7 +87,7 @@ use.

## Example Output

The logs provide a wealth of contextual metadata so you can:
The logs provide rich contextual metadata, allowing you to:

* Determine which communication session produced the message
* Understand what state the software was in when the event occurred
Expand Down Expand Up @@ -122,7 +122,6 @@ protocol stack, including:
Refer to the language-specific API documentation for the meaning of each enumeration value.

:::note
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,
try adjusting the application-layer decoding first to gain visibility into the messages being exchanged on one channel at a time.
Protocol decoding outputs at the Info log level and can generate excessive output in production systems. When debugging communication issues, adjust application-layer decoding first to gain visibility into messages on one channel at a time.
:::

18 changes: 8 additions & 10 deletions guide/docs/api/runtime.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ slug: /api/runtime
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

Rust's asynchronous programming model is a form of [cooperative multitasking](https://docs.rs/tokio/1.0.2/tokio/task/index.html). Once a task gets to a point where it would typically block, such as reading from a socket, execution is instead released back to an executor so that another task may run. This lets a pool of worker threads inside the executor efficiently execute thousands of asynchronous tasks concurrently without the overhead of per-task call stacks or thread context-switching.
Rust's asynchronous programming model is a form of [cooperative multitasking](https://docs.rs/tokio/1.0.2/tokio/task/index.html). When a task encounters blocking operations like socket reads, execution transfers to an executor, allowing worker threads to efficiently execute thousands of concurrent tasks without per-task call stacks or thread context-switching overhead.

Rust supports asynchronous programming using 'async' functions and 'async/await' syntax. The Rust compiler transforms synchronous-looking code into state machines that are just as efficient as what can be written by hand. Although Rust has this capability built into the compiler, it doesn't include a default runtime to execute the asynchronous programs. Instead, you are free to pick the runtime as an external library.

The rodbus library runs on top of the [Tokio](https://tokio.rs/) runtime, providing a state-of-the-art scheduler and platform-agnostic networking APIs. The OS-specific mechanisms vary by platform, for example, *epoll* on Linux and *IOCP* on Windows.

Tokio is a modern evolution of libraries like [libuv (C)](https://libuv.org/) and [ASIO (C++)](https://think-async.com/Asio/). It leverages Rust's thread and memory safety to deliver asynchronous programs that are not only incredibly fast, but also correct. This is extremely important since it is quite difficult to write correct asynchronous software in C/C++ due to the need to manually reason object lifetimes in callbacks.
Tokio is a modern evolution of libraries like [libuv (C)](https://libuv.org/) and [ASIO (C++)](https://think-async.com/Asio/). It leverages Rust's thread and memory safety to deliver asynchronous programs that are both fast and correct. Writing correct asynchronous C/C++ code requires careful manual management of object lifetimes in callbacks.

## Lifetime

You must create a `Runtime` before any communication can take place. It is a shared resource for multiple communication sessions that is typically created just after initializing logging. It is also the last component to shut down; see below for more details about runtime shutdown.
Create a `Runtime` before any communication can take place. The runtime is a shared resource for multiple communication sessions, typically created just after initializing logging. It should be the last component to shut down; see below for details.

:::note
Rust users can share the runtime with other libraries that also use Tokio. The bindings don't currently support sharing a runtime, but this will be possible in a future release.
Expand Down Expand Up @@ -81,12 +81,11 @@ Set the number of runtime threads to `0` to default to the number of system core

## Callbacks

The runtime's thread pool invokes callbacks from the library to user code. If you block during a callback, an entire thread is made unavailable for task execution. If all threads in a thread pool are blocked, no communication sessions will execute until
a thread becomes unblocked.
The runtime's thread pool invokes callbacks from the library to user code. Blocking during a callback removes an entire thread from task execution. If all threads in a thread pool are blocked, no communication sessions execute until a thread unblocks.

For example, when you receive a log message via a callback, a synchronous call to write the message to a file will block a thread. If this frequently occurs on all your pool threads, it can cause poor throughput or even task starvation.

For best results, avoid blocking whenever possible in your applications. Instead, you should defer blocking calls to dedicated worker threads, such as a user-managed thread that write log messages to file.
Avoid blocking in callbacks. Defer blocking calls to dedicated worker threads, such as a user-managed thread that writes log messages to file.

:::tip
If you have a case where some blocking is unavoidable, set the number of worker threads to a multiple of the number of system cores, such as 2x or 3x.
Expand All @@ -102,11 +101,10 @@ worker threads have been joined.
A blocked worker thread can cause shutdown to deadlock. For example, if a communication channel makes a callback to user code
that permanently blocks, `shutdown` will cause a deadlock.

If you cannot ensure a clean shutdown, you can use `Runtime.set_shutdown_timeout(..)` to put an upper
time limit on the eventual shut down. You would call this method immediately after creating the Runtime.
If you cannot ensure a clean shutdown, use `Runtime.set_shutdown_timeout(..)` to set an upper
time limit on shutdown. Call this method immediately after creating the Runtime.

**Shutting down the runtime using a timeout can leak memory as worker threads are be aborted if the timeout occurs. Only
use this method if you are exiting the process anyway.**
**A shutdown timeout can leak memory because the system aborts worker threads. Only use this method when exiting the process.**
:::

### Logging
Expand Down
10 changes: 4 additions & 6 deletions guide/docs/api/server/database.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ slug: /api/outstation/database
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

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
a thread-safe data structure in memory.
Use the `Database` class to manipulate the point values that the server exposes to clients. Although called a "database", this is a thread-safe in-memory data structure.

:::note
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.
Expand All @@ -25,8 +24,7 @@ A transaction can be started on a running server with the `update` method. Insid
They will be executed in sequence.

:::warning
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
inside a database transaction.
The transaction mechanism acquires a mutex lock, so keep transactions short. Never perform blocking operations inside a database transaction.
:::

## Database Initialization
Expand Down Expand Up @@ -125,14 +123,14 @@ values={[

## Getting Point Values

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.
You can also use the `Database` as a cache of the most recent values. Each type has a getter method to retrieve the most recently assigned value.

:::note
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.
:::

## Removing Points

Most applications don't need to remove points, but the option is there in case you want to remove points from a running server.
Few applications need to remove points, but this capability exists for dynamic scenarios.
There is a type-specific function for removing every point type given its index. The returned boolean indicates if the point
was defined prior to the call to remove it.
Loading