rstango 0.3.0

Implementation of the Tango distributed control system
Documentation
//! A device server written against the user API of this crate, which is what a device
//! server *is* here: 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, so the reads of a device run in parallel; a write or an
//! exclusive command is given it mutably, and the server takes the lock for it.
//!
//! Run it against the database `TANGO_HOST` names, having registered the server under the
//! name below and given it a device of the `Motor` class:
//!
//! ```sh
//! cargo run --example server --features cli -- test -v
//! ```
//!
//! Or with no database at all, the server serving one for testing with its devices in it,
//! which the clients then reach at the `TANGO_HOST` it logs:
//!
//! ```sh
//! cargo run --example server --features cli -- test --dev test/motor/1 --dev test/motor/2 \
//!     --prop "test/motor/2->Limit=90" --db-port 10000
//! ```
//!
//! The other examples are clients of it, each showing one feature of a client.

use std::borrow::Cow;

use tango::server::{AttrDef, ClassDef, CommandDef, Dev, ServerBuilder};
use tango::error::{Error, ErrorKind, Result};
use tango::dev::DevState;


/// The model of one motor, owned by the server serving it, which is the whole state of
/// the device: what a client reads of it is derived from this and nothing else.
struct Motor {
    /// Where the axis is, which the `position` attribute reads.
    position: f64,
    /// Where it was last asked to go, which is what tells a moving axis from an idle one.
    target: f64,
    /// How far the axis may go either way, read from the properties of the device at
    /// initialization: what makes two devices of this class different from each other.
    limit: f64,
}

impl Motor {

    /// The definition of the class, which is the interface every device of it has.
    fn class() -> ClassDef<Self> {

        ClassDef::new("Motor", Motor::init)
            .doc("One rotation axis, which is moved by asking it to go somewhere and watched by reading where it is.")
            .doc_url("https://www.tango-controls.org/")
            .state(Motor::state)
            .status(Motor::status)
            .attr(AttrDef::new("position")
                .doc("Where the axis is, and where it is being asked to go.")
                .unit("deg")
                .display_format("%7.3f")
                .change_event(0.001, None)
                .push_change_event(true)
                .read(Motor::position)
                .write_exclusive(Motor::set_position))
            .command(CommandDef::new("MoveTo")
                .in_doc("Where the axis should go, in degrees.")
                .out_doc("Where it is now, which is where it was asked to go.")
                .exec_exclusive(Motor::move_to))
            .command(CommandDef::new("Stop")
                .out_doc("Nothing: the axis stops where it is.")
                .exec_exclusive(Motor::stop))
    }

    /// Build the model of one device and give it the part of its interface that is its
    /// own, which is what an initialization is: the class below gives every device the
    /// rest of it before this runs.
    ///
    /// Failing here does not take the device away: it stays reachable, in `FAULT`, and
    /// says in its status why it is not running, so that a client can fix its properties
    /// and call `Init` on it rather than restart the whole server.
    fn init(dev: &Dev<Self>) -> Result<Self> {

        // A property of the device, which the database answers and which therefore differs
        // from one device of this class to the next.
        let limit = dev.server().database()
            .get_dev_prop::<f64, _>(dev.name(), "Limit")
            .unwrap_or(180.0);

        // The one part of the interface that is not the class', added device by device:
        // an axis that can turn all the way round is not bounded, and only says so here.
        if limit <= 0.0 {
            return Err(Error::new(ErrorKind::InvalidProp)
                .with_description(format!("The Limit property of {} is {limit}, which \
                    leaves the axis nowhere to go", dev.name())));
        }

        dev.add_attr(AttrDef::new("limit")
            .doc("How far the axis may go either way.")
            .unit("deg")
            .read(Motor::limit))?;

        Ok(Self { position: 0.0, target: 0.0, limit })

    }

    /// What the device says of itself, derived from the model: this is the `State` every
    /// Tango device has, and the server answers it whether the class defines one or not.
    fn state(&self, _dev: &Dev<Self>) -> Result<DevState> {
        match self.position == self.target {
            true => Ok(DevState::On),
            false => Ok(DevState::Moving),
        }
    }

    /// The counterpart of the state in words, borrowed from the model when it has nothing
    /// to build: a status that is a constant costs no allocation.
    fn status(&self, _dev: &Dev<Self>) -> Result<Cow<'_, str>> {
        match self.position == self.target {
            true => Ok(Cow::Borrowed("The axis is idle")),
            false => Ok(Cow::Owned(format!("The axis is moving to {:.3} deg", self.target))),
        }
    }

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

    /// Write it, alone on the model for the time it takes.
    fn set_position(&mut self, dev: &Dev<Self>, value: f64) -> Result<()> {
        self.move_to(dev, value).map(|_| ())
    }

    fn limit(&self, _dev: &Dev<Self>) -> Result<f64> {
        Ok(self.limit)
    }

    /// Execute the command. A real axis would start moving and give the hand back, its
    /// state saying `MOVING` until it arrives; this one is there instantly.
    fn move_to(&mut self, dev: &Dev<Self>, target: f64) -> Result<f64> {

        if target.abs() > self.limit {
            return Err(Error::new(ErrorKind::InvalidData)
                .with_description(format!("The device {} cannot go to {target} deg, which \
                    is past its limit of {} deg", dev.name(), self.limit)));
        }

        self.target = target;
        self.position = target;

        // The server does not poll, so the subscribers of the position only hear of it
        // moving from the device itself, which declares it does so in the class above.
        dev.push_change_event("position", self.position)?;
        Ok(self.position)

    }

    fn stop(&mut self, _dev: &Dev<Self>, _input: ()) -> Result<()> {
        self.target = self.position;
        Ok(())
    }

}


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

    // The instance of the server is the one of its command line, which is what a Tango
    // server is always given: the same executable is run several times, each under its own
    // name, each with its own devices. The logs are printed as it asks, see `--help`.
    //
    // Nothing else is added here: the devices this server runs are the ones the database
    // lists for it, which its own administration device reads at startup.
    ServerBuilder::from_raw_args(Some("Motor"), None)
        .class(Motor::class())?
        .run()

}