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.

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.

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.

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:

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:

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/: 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 the basics, info.rs introspection, errors.rs failures, props.rs properties, events.rs events and concurrent.rs asynchronous requests.

Modules

  • dev — DevProxy, the client handle to a device: names, state/status, Init, commands, attributes and their configuration, properties, events and the black box.
  • database — 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 — 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 — 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 — 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) is part of it, built over the C library binding and reusing its CDR layer for marshalling.
  • value — Value and the FromValue/IntoValue conversions, what commands, attributes and events carry, DevEncoded included (decoded as an image with the image feature).
  • buf — Buf, the typed N-dimensional buffer a Value holds, sharing the octets a message was received into rather than copying them.
  • prop — parsing and serializing of property values, which Tango always represents as an array of strings.
  • name — case-insensitive names and Trl, a validated Tango Resource Locator (tango://host:port/...).
  • error — Error/ErrorKind/Result, the error type shared by every layer, including the full DevFailed reason catalogue from cppTango.
  • 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), 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/ 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/ run against a real Tango database given by TANGO_HOST, or against the one the server example serves itself with --dev.