1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Utilities for sharing a Modbus context

use super::*;

use futures::{future, Future};

use std::{cell::RefCell, io::Error, rc::Rc};

/// Helper for sharing a context between multiple clients,
/// i.e. when addressing multiple slave devices in turn.
#[derive(Default)]
struct SharedContextHolder {
    context: Option<Rc<RefCell<Context>>>,
}

impl SharedContextHolder {
    /// Create an instance by wrapping an initial, optional context.
    fn new(initial_context: Option<Context>) -> Self {
        Self {
            context: initial_context.map(RefCell::new).map(Rc::new),
        }
    }

    /// Disconnect and drop the wrapped context reference.
    fn disconnect(&mut self) -> impl Future<Item = (), Error = Error> {
        if let Some(context) = self.context.take() {
            future::Either::A(context.borrow().disconnect())
        } else {
            future::Either::B(future::ok(()))
        }
    }

    /// Reconnect by replacing the wrapped context reference.
    fn reconnect(&mut self, context: Context) {
        self.context = Some(Rc::new(RefCell::new(context)));
    }

    pub fn is_connected(&self) -> bool {
        self.context.is_some()
    }

    fn share_context(&self) -> Option<Rc<RefCell<Context>>> {
        self.context.as_ref().map(Rc::clone)
    }
}

/// Trait for (re-)creating new contexts on demand.
///
/// Implement this trait for reconnecting a `SharedContext` on demand.
pub trait NewContext {
    /// Create a new context.
    fn new_context(&self) -> Box<dyn Future<Item = Context, Error = Error>>;
}

/// Reconnectable environment with a shared context.
pub struct SharedContext {
    shared_context: SharedContextHolder,
    new_context: Box<dyn NewContext>,
}

impl SharedContext {
    /// Create a new instance with an optional, initial context and
    /// a trait object for reconnecting the shared context on demand.
    pub fn new(inital_context: Option<Context>, new_context: Box<dyn NewContext>) -> Self {
        Self {
            shared_context: SharedContextHolder::new(inital_context),
            new_context,
        }
    }

    /// Checks if a shared context is available.
    pub fn is_connected(&self) -> bool {
        self.shared_context.is_connected()
    }

    /// Try to obtain a shared context reference. The result is `None`
    /// if no context is available, i.e. if the shared context is not
    /// connected.
    ///
    /// The result should only be used temporarily for the next
    /// asynchronous request and must not be reused later!
    pub fn share_context(&self) -> Option<Rc<RefCell<Context>>> {
        self.shared_context.share_context()
    }
}

/// Asynchronously (disconnect and) reconnect the shared context.
pub fn reconnect_shared_context(
    shared_context: &Rc<RefCell<SharedContext>>,
) -> impl Future<Item = (), Error = Error> {
    let disconnected_context = Rc::clone(shared_context);
    // The existing context needs to be disconnected first to
    // release any resources that might be reused for the new
    // context, i.e. a serial port with exclusive access.
    shared_context
        .borrow_mut()
        .shared_context
        .disconnect()
        .and_then(move |()| {
            // After disconnecting the existing context create
            // a new instance...
            debug_assert!(!disconnected_context.borrow().is_connected());
            let reconnected_context = Rc::clone(&disconnected_context);
            disconnected_context
                .borrow()
                .new_context
                .new_context()
                .map(move |context| {
                    // ...and put it into the shared context. The new
                    // context will then be used for all subsequent
                    // client requests.
                    reconnected_context
                        .borrow_mut()
                        .shared_context
                        .reconnect(context)
                })
        })
}

#[cfg(test)]
mod tests {
    use super::*;

    use super::super::tests::*;

    struct NewContextMock;

    impl NewContext for NewContextMock {
        fn new_context(&self) -> Box<dyn Future<Item = Context, Error = Error>> {
            let client: Box<dyn Client> = Box::new(ClientMock::default());
            Box::new(future::ok(Context::from(client)))
        }
    }

    #[test]
    fn new_shared_context() {
        let disconnected = SharedContext::new(None, Box::new(NewContextMock));
        assert!(!disconnected.is_connected());
        assert!(disconnected.share_context().is_none());
        let client: Box<dyn Client> = Box::new(ClientMock::default());
        let connected = SharedContext::new(Some(Context::from(client)), Box::new(NewContextMock));
        assert!(connected.is_connected());
        assert!(connected.share_context().is_some());
    }

    #[test]
    fn reconnect_shared_context() {
        let sc = SharedContext::new(None, Box::new(NewContextMock));
        assert!(!sc.is_connected());
        assert!(sc.share_context().is_none());

        let sc = Rc::new(RefCell::new(sc));
        super::reconnect_shared_context(&sc).wait().unwrap();
        assert!(sc.borrow().is_connected());
        assert!(sc.borrow().share_context().is_some());
    }
}