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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Motor management.
use std::{ops::RangeInclusive, time::Duration};
use crate::{
actix::*,
pin::{Error as PinError, Pin, Pwm},
};
/// A message that can be sent to a motor to change its position.
#[derive(Clone, Copy, Debug)]
pub enum Message {
/// Requests that the motor be set to the closed position.
Close,
/// Requests that the motor be set to the open position.
Open,
/// Requests that the motor be set to the shut (not closed) position.
Shut,
/// Turns off the motor's output signal.
Stop,
}
impl ActixMessage for Message {
type Result = ();
}
/// A motor connected to the syringe manifold.
///
/// Moving a motor (physically) will cause the control knob to rotate.
#[derive(Debug)]
pub struct Motor {
/// The characteristic period of the motor.
period: Duration,
/// The output pin controlling the physical motor.
pin: Pin,
/// The range of acceptable signal lengths.
///
/// The motor is assumed to have 180º of motion, meaning the minimum and signals should
/// correspond to antiparallel positions.
///
/// The closed position is assumed to be 0º; the open position is at 90º.
signal_range: RangeInclusive<Duration>,
/// The duration for which the signal should be high in each period.
///
/// Changing this property will change the position of the motor.
pulse_width: Duration,
/// The handle to the main loop for this motor (for cancellation).
main_handle: Option<SpawnHandle>,
}
impl PartialEq for Motor {
fn eq(&self, other: &Self) -> bool {
self.pin.number == other.pin.number
}
}
impl Eq for Motor {}
impl Motor {
fn set_pulse_width(&mut self, width: Duration) -> Result<(), PinError> {
log::debug!(
"Setting pulse width of motor on pin {} to {:?}",
self.pin.number,
width
);
self.pulse_width = width;
self.pin.set_pwm(self.period, width)
}
/// Sets the motor's angle in degrees (relative to the closed position).
///
/// ## Panics
/// This method will panic if `angle` is greater than 180.
pub fn set_angle(&mut self, angle: u16) -> Result<(), PinError> {
assert!(angle <= 180);
let (start, end) = (self.signal_range.start(), self.signal_range.end());
// Dereference, since auto-deref doesn't seem to work for std::ops::Sub?
let (start, end) = (*start, *end);
let delta = end - start;
// Assume a range of motion of 180º.
let range = 180;
// Calculate the change in signal per unit angle (dT/dθ).
let step = delta / range;
// Multiply the step by the desired angle to get the offset from the baseline (∆T).
let offset = step * angle.into();
log::trace!(
"Setting motor angle to {} (pulse width: {:?})",
angle,
start + offset
);
self.set_pulse_width(start + offset)
}
/// Sets the motor to the closed position (angle of 90º).
///
/// Fluid will flow through the valve, but not from the associated buffer.
pub fn close(&mut self) -> Result<(), PinError> {
log::trace!("Closing motor on pin {}.", self.pin.number);
self.set_angle(90)
}
/// Sets the motor to the shut position, where no fluid will flow through it.
pub fn shut(&mut self) -> Result<(), PinError> {
log::trace!("Shutting motor on pin {}.", self.pin.number);
self.set_angle(180)
}
/// Sets the motor to the open position (angle of 0º).
///
/// Fluid from the associated buffer will flow through the valve.
pub fn open(&mut self) -> Result<(), PinError> {
log::trace!("Opening motor on pin {}.", self.pin.number);
self.set_angle(0)
}
///
/// Constructs a new motor with the given period and signal range on the given pin number, if
/// possible.
///
/// The motor will be set to the closed position initially.
pub fn try_new<R>(period: Duration, range: R, pin: u16) -> Result<Self, PinError>
where
R: Into<RangeInclusive<Duration>>,
{
let pin = Pin::try_new(pin)?;
let signal_range = range.into();
Ok(Self {
period,
pin,
pulse_width: *signal_range.start(),
signal_range,
main_handle: None,
})
}
/// Constructs a new motor with the given period and signal range on the given pin number.
///
/// The motor will be set to the closed position initially.
///
/// ## Panics
/// This method will panic if opening the pin fails. For a fallible initializer, see
/// [`Motor::try_new`](#method.try_new).
pub fn new<R>(period: Duration, range: R, pin: u16) -> Self
where
R: Into<RangeInclusive<Duration>>,
{
Self::try_new(period, range, pin).expect("Motor construction failed.")
}
}
impl Actor for Motor {
type Context = Context<Self>;
}
impl Handle<Message> for Motor {
type Result = ();
fn handle(&mut self, message: Message, _context: &mut Self::Context) -> Self::Result {
match message {
Message::Open => self.open().unwrap(),
Message::Close => self.close().unwrap(),
Message::Shut => self.shut().unwrap(),
Message::Stop => {
log::trace!("Stopping motor motion.");
self.set_pulse_width(Duration::new(0, 0)).unwrap()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// This test makes sure the panic in validate_motor_angle isn't from constructing the motor and unwrapping it.
#[test]
fn make_fake_motor() {
let _motor = Motor::try_new(
Duration::new(2, 0),
Duration::new(0, 0)..=Duration::new(1, 0),
1,
)
.unwrap();
}
#[test]
#[should_panic]
fn validate_motor_angle() {
let mut motor = Motor::try_new(
Duration::new(2, 0),
Duration::new(0, 0)..=Duration::new(1, 0),
1,
)
.unwrap();
let _ = motor.set_angle(181);
}
}