rstango 0.3.0

Implementation of the Tango distributed control system
Documentation
# rstango

An implementation of the [Tango] distributed control system, written from scratch in
Rust, for writing clients and device servers.

The protocol actually used to reach a device is a detail of the crate rather than of the
code using it: a `dev::DevProxy` talks to whatever backend can reach its target, and a
`server::Server` serves its devices on every backend able to serve them.

Every call is blocking, up to the timeout of the proxy or the deadline of the request:
there is no runtime to install. Each one also has an asynchronous `*_async` form, which
runs on any executor, so that a single thread can wait on many devices at once; either
way, the backend multiplexes the concurrent requests over the connection it shares.

[Tango]: https://www.tango-controls.org/

## Example: client

A client asks a device for what it wants and says which type it expects, one name giving
one value and a tuple of names giving as many, in a single request.

```rust,no_run
use tango::dev::DevProxy;
use tango::error::Result;

fn main() -> Result<()> {

    // The device is looked up in the database given by `TANGO_HOST`, the connection
    // being made on the first request and kept for the following ones.
    let dev = DevProxy::new("sys/tg_test/1");
    println!("{} is {:?}", dev.name()?, dev.state()?);

    // A reading carries the value, the write value, the quality and the timestamp.
    let read = dev.read_attr::<f64, _>("double_scalar")?;
    println!("double_scalar = {:?} ({:?})", read.value, read.quality);

    dev.write_attr(("double_scalar", 3.14))?;

    // A command is typed the same way, on both of its sides, `()` being no data.
    let echo: String = dev.command("DevString", "hello")?;
    println!("DevString -> {echo}");

    Ok(())
}
```

## Example: server

A device server is a plain Rust struct, its model, and the functions of it that the
attributes and the commands of the device are defined from. Nothing is implemented on
the model and no macro is involved: a read function is given a shared access to it, a
write or an exclusive command an exclusive one, and the server takes care of the lock.

```rust,no_run
use tango::error::Result;
use tango::server::{AttrDef, ClassDef, CommandDef, Dev, ServerBuilder};

/// The model of one device, owned by the server serving it.
struct Counter {
    count: i32,
}

impl Counter {

    /// Build the model of a device, and give it its interface.
    fn init(dev: &Dev<Self>) -> Result<Self> {

        dev.add_attr(AttrDef::new("count")
            .doc("The current value of the counter.")
            .read(Counter::count))?;

        dev.add_command(CommandDef::new("Add")
            .in_doc("How much to add to the counter.")
            .out_doc("The value of the counter afterwards.")
            .exec_exclusive(Counter::add))?;

        Ok(Self { count: 0 })
    }

    /// Read the attribute, sharing the model with the other readers.
    fn count(&self, _dev: &Dev<Self>) -> Result<i32> {
        Ok(self.count)
    }

    /// Execute the command, alone on the model for the time it takes.
    fn add(&mut self, _dev: &Dev<Self>, value: i32) -> Result<i32> {
        self.count += value;
        Ok(self.count)
    }

}

/// Serve the devices the database lists for this server until it is stopped.
fn main() -> Result<()> {
    ServerBuilder::from_args()
        .class(ClassDef::new("Counter", Counter::init))?
        .run()
}
```

The server is named after its executable, as cppTango does, and its instance is taken
from the command line, so that a `Counter` executable is launched as the `Counter/test`
server with:

```sh
Counter test -v
```

`-v` logs more, once per level, and `--host` and `--port` say where to listen. With
`--dev`, the server serves a database of its own for testing, with those devices and the
`--prop` properties in it, so that it runs with no Tango database at all:

```sh
Counter test --dev test/counter/1 --dev test/counter/2
```

`from_args` comes with the default `cli` feature. A server with options of its own
flattens `ServerArgs` into its own clap parser and builds itself with
`ServerBuilder::from_raw_args`, and one without the feature names itself with
`ServerBuilder::new("Counter", "test")`.

More complete examples are in [`examples/`](examples):
[`server.rs`](examples/server.rs) is a `Motor` device server with properties, events and
a failing `init`, and every other example is a client of it showing one feature:
[`client.rs`](examples/client.rs) the basics, [`info.rs`](examples/info.rs) introspection,
[`errors.rs`](examples/errors.rs) failures, [`props.rs`](examples/props.rs) properties,
[`events.rs`](examples/events.rs) events and [`concurrent.rs`](examples/concurrent.rs)
asynchronous requests.

## Modules

- [`dev`]src/dev.rs — `DevProxy`, the client handle to a device: names, state/status,
  `Init`, commands, attributes and their configuration, properties, events and the black
  box.
- [`database`]src/database.rs — `DatabaseProxy`, a `DevProxy` to the Tango database
  device, plus what is specific to it: free/class/device/attribute properties, lookups,
  and the registration of servers and devices.
- [`server`]src/server — `ServerBuilder`/`Server`, `ClassDef`/`AttrDef`/`CommandDef` and
  `Dev<M>`: the server-side API for defining classes and serving devices, the `DServer`
  admin device every server exposes and, with the `cli` feature, `ServerArgs`.
- [`backend`]src/backend.rs — the `Backend`/`BackendDev`/`BackendServer` traits every
  protocol implements, and the process-global default backend devices connect through
  when none is given explicitly. A device served in the same process is reached
  directly, without going through any protocol.
- [`corba`]src/corba — the CORBA/GIOP backend: a from-scratch implementation of the
  subset of GIOP/IIOP that Tango needs, so no omniORB is linked in. The ZMQ event system
  (see [`ZMQ.md`]../ZMQ.md) is part of it, built over the C library binding and reusing
  its CDR layer for marshalling.
- [`value`]src/value.rs — `Value` and the `FromValue`/`IntoValue` conversions, what
  commands, attributes and events carry, `DevEncoded` included (decoded as an image with
  the `image` feature).
- [`buf`]src/buf.rs — `Buf`, the typed N-dimensional buffer a `Value` holds, sharing
  the octets a message was received into rather than copying them.
- [`prop`]src/prop.rs — parsing and serializing of property values, which Tango always
  represents as an array of strings.
- [`name`]src/name.rs — case-insensitive names and `Trl`, a validated Tango Resource
  Locator (`tango://host:port/...`).
- [`error`]src/error.rs — `Error`/`ErrorKind`/`Result`, the error type shared by every
  layer, including the full `DevFailed` reason catalogue from cppTango.
- [`testing`]src/testing — `TestDatabase` and `TestContext`, for running servers and
  clients in a test with no Tango database.

## Feature status

This is an experimental, from-scratch implementation; it covers what a well-behaved
client and a straightforward device server need, but it is not a complete cppTango
replacement yet.

Available:

- Device and database proxies: names, state/status, `Init`, ping, black box, admin
  device, with lazy reconnection of a device that went away.
- Typed command execution and attribute read/write, including the multi-name and
  all-names (`All`) forms.
- Command and attribute info queries, attribute configuration changes, and
  free/class/device/attribute property get/set/delete.
- Event subscriptions on `DevProxy`: attribute values (change, periodic, archive, user,
  alarm), data ready and attribute configuration.
- Device servers: class/attribute/command definitions, synchronous or asynchronous
  handlers with shared or exclusive access to the model, dynamic devices, attributes
  and commands, memorized attributes, alarm and warning levels, and properties read from
  the database.
- Events pushed by a device (change, archive, user, alarm, data ready, and the state and
  status), and attribute configuration events sent on every change of it.
- Servers exporting their devices to the database, or serving a test database of their
  own with `--dev`, so that they run with no Tango database at all.
- The `DServer` admin commands of the server itself (`DevRestart`, `RestartServer`,
  `Kill`, `QueryClass`, `QueryDevice`) and of the event system.
- The CORBA backend (GIOP/IIOP subset), with no omniORB dependency.
- The ZMQ event system (protocol per [`ZMQ.md`]../ZMQ.md), as client and as server,
  including the negotiation/subscription/heartbeat/confirmation phases; not implemented
  from cppTango: multicast (`epgm://`) endpoints, multi-`TANGO_HOST` topic variants, and
  active reconnection (a stopped heartbeat is reported, but resubscription only happens
  if the server comes back on the same endpoints).

Not yet implemented:

- Polling: neither the server-side polling engine (a `DevSource::Cache` read on the
  server always errors, and no periodic event is ever sent) nor the client-side polling
  fallback strategy. The polling commands of the admin device are declared with their
  cppTango signature but answer that they are not implemented.
- The criteria of change and archive events: everything a device pushes is sent.
- Command and attribute history, which the server does not marshal yet, and property
  history (`DbGetPropertyHistory` and friends).
- `DevVarEncodedArray` data, and Tango pipes.
- `Group`-style fan-out proxies, forwarded attributes, the logging service (its admin
  commands are declared only), and the OpenTelemetry trace-context extensions from Tango
  IDL v6.

## Testing

`cargo test` needs no Tango database: besides the unit tests of the CDR/GIOP wire layer
and of the server-side model, the integration tests in [`tests/`](tests) run servers and
their clients in the test itself, each of them twice — once reaching the device directly
in the process, and once through the CORBA and ZMQ backends over the loopback.

The [`examples/`](examples) run against a real Tango database given by `TANGO_HOST`, or
against the one the `server` example serves itself with `--dev`.