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
190
191
192
193
194
195
use std::time::Duration;
use crate::error::Result;
use crate::protocol::FrameType;
use crate::protocol::command::{
bearing_payload, emit_pace_payload, imperfect_payload, move_ride_payload, name_payload,
render_payload, spread_payload,
};
use crate::types::{BearingMode, EmitPace, RenderMode};
use super::Device;
/// Max box-name length in bytes (printable ASCII), the firmware's `CTRL_NAME_MAX`.
pub const NAME_MAX: usize = 32;
/// Emit-rate ceiling in Hz (the 1 ms frame-clock limit); the firmware clamps a higher
/// [`EmitPace::Fixed`](crate::EmitPace::Fixed) rate to it.
pub const EMIT_MAX_HZ: u16 = 1000;
// `None` = 0 (off); a non-zero `Some` is >=1 ms and saturates at `u16::MAX` ms.
pub(crate) fn ride_window_ms(window: Option<Duration>) -> u16 {
match window {
None => 0,
Some(d) => (d.as_millis().min(u16::MAX as u128) as u16).max(1),
}
}
/// Bearing window at boot, before any host sets one.
pub const BEARING_WINDOW_DEFAULT: Duration = Duration::from_millis(20);
pub(crate) fn emit_pace_wire(pace: EmitPace) -> (u8, u16) {
match pace {
EmitPace::Learned => (0, 0),
EmitPace::Interval => (1, 0),
EmitPace::Fixed(hz) => (2, hz),
}
}
impl Device {
/// `OPTION(IMPERFECT)`: allow cloning a device the box cannot clone faithfully, or return to
/// faithful-only; persisted in NVS.
///
/// Turning the opt-in off clears the box's rewrite table. The crate drops its held rules too
/// (restored if the frame is never sent), or the keepalive would re-assert them once the opt-in
/// is back on. The box also removes every clip packet trigger that
/// [consumes](crate::ClipPacketTrigger::consume), and the crate drops those the same way. If
/// another thread calls `set_rewrite` while the opt-in turns off, the two are unordered: the
/// rule may reach the box after the opt-in is off and be refused.
///
/// If a toggle changes the served patch set, the clone is presented again. That releases the
/// session like a device replug: once the new clone is up the crate re-sends what it holds, and
/// [`ClipHandle::lost`](crate::ClipHandle::lost) reports a dropped clip.
pub fn allow_imperfect_clones(&self, allow: bool) -> Result<()> {
self.link.restart_watch().expect_represent();
if !allow {
// Serialised against re-asserts like the rewrite mutators; rolled back on a failed send
// so DesiredState and the box stay in step.
let _serial = self.link.reassert_guard();
let (held, consuming) = {
let mut d = self.link.desired().lock();
let held = d.held_rewrites();
d.clear_rewrites();
(held, d.clip_packet_drop_consuming())
};
let sent = self.link.send(FrameType::Option, &imperfect_payload(false));
if sent.is_err() {
let mut d = self.link.desired().lock();
for r in held {
d.apply_rewrite(r);
}
for t in &consuming {
d.clip_packet_bind(t);
}
}
return sent;
}
self.link.send(FrameType::Option, &imperfect_payload(true))
}
/// `OPTION(MOVE_RIDE)`: injected motion rides a native motion report within `window`, so its
/// density matches native; persisted in NVS.
///
/// Motion with no native report within `window` is dropped. `None` turns riding off. A single
/// move can override riding ([`MoveTiming::Now`](crate::MoveTiming),
/// [`move_rel_now`](Self::move_rel_now)). Clip playback bypasses riding unless
/// [`ClipHandle::set_ride`](crate::ClipHandle::set_ride) is on. While rendering is on with a
/// profile armed, riding acts on a clip's cursor motion as on a rendered move.
pub fn set_movement_riding(&self, window: Option<Duration>) -> Result<()> {
self.link.send(
FrameType::Option,
&move_ride_payload(ride_window_ms(window)),
)
}
/// `OPTION(EMIT)`: pace ([`EmitPace`], the rate ceiling) and forced poll rate (`force_hz`);
/// persisted in NVS.
///
/// A non-zero `force_hz` re-clones with a `bInterval` the real device didn't advertise (needs
/// [`allow_imperfect_clones`](Self::allow_imperfect_clones)). The interval is 1000/`force_hz` ms
/// rounded to the nearest millisecond, then down to a power of two (600 Hz gives 500 Hz); on a
/// low-speed clone it floors at 62 Hz.
/// `Some(0)`/`None` is off.
pub fn set_emit_pace(&self, pace: EmitPace, force_hz: Option<u16>) -> Result<()> {
let (mode, hz) = emit_pace_wire(pace);
self.link.send(
FrameType::Option,
&emit_pace_payload(mode, hz, force_hz.unwrap_or(0)),
)
}
/// `OPTION(NAME)`: box name (leading printable-ASCII run, capped at [`NAME_MAX`] bytes); read
/// back by [`query_version`](Device::query_version). Persisted in NVS.
pub fn set_name(&self, name: &str) -> Result<()> {
self.link.send(FrameType::Option, &name_payload(name))
}
/// `OPTION(NAME)` with an empty value: revert to the derived `Medius-XXXX` default.
pub fn clear_name(&self) -> Result<()> {
self.set_name("")
}
/// `OPTION(BEARING)`: how long the last injected delta's direction stays the reference for
/// [`Direction::With`](crate::Direction) and [`Direction::Against`](crate::Direction), and whether
/// it is read per axis or as one vector; persisted in NVS.
///
/// `None` turns the bearing off, leaving the relative directions inert whatever their scale; the
/// absolute ones are unaffected. The box boots at [`BEARING_WINDOW_DEFAULT`] with
/// [`BearingMode::PerAxis`], so nothing engages until a scale is set.
///
/// The window rounds to whole milliseconds; a non-zero `Some` is >=1 ms and saturates at
/// 65535 ms.
///
/// ```no_run
/// # use medius::{Axis, BearingMode, Device, Direction, Result};
/// # use std::time::Duration;
/// # fn main() -> Result<()> {
/// let device = Device::find()?;
/// device.set_bearing(Some(Duration::from_millis(20)), BearingMode::PerAxis)?;
/// device.scale(Axis::X, Direction::Against, 40)?;
/// # Ok(()) }
/// ```
pub fn set_bearing(&self, window: Option<Duration>, mode: BearingMode) -> Result<()> {
self.link.send(
FrameType::Option,
&bearing_payload(ride_window_ms(window), mode.as_u8()),
)
}
/// `OPTION(RENDER)`: render texture, and whether the model renders native motion too instead of
/// relaying it; persisted in NVS.
///
/// Rendering adds latency; `full` adds it to native motion too. `full` is off by default.
/// Nothing renders until the box learns a profile for the real device
/// ([`RenderStatus::ready`](crate::RenderStatus)); until then motion is relayed and injection
/// takes the paced fill.
///
/// Motion asking for exact timing skips the model: [`move_rel_now`](Self::move_rel_now),
/// [`flush_motion`](Self::flush_motion) and [`discard_motion`](Self::discard_motion) take the paced
/// path. With `full` on, the rendered stream ignores
/// [`set_movement_riding`](Self::set_movement_riding).
///
/// ```no_run
/// # use medius::{Device, RenderMode, Result};
/// # fn main() -> Result<()> {
/// let device = Device::find()?;
/// device.set_render(RenderMode::Despiked, true)?;
/// # Ok(()) }
/// ```
pub fn set_render(&self, mode: RenderMode, full: bool) -> Result<()> {
self.link
.send(FrameType::Option, &render_payload(mode.to_wire(), full))
}
/// `OPTION(SPREAD)`: percent of the host's command interval an injected delta is released
/// across; persisted in NVS.
///
/// 0 puts the whole delta on the next emitted report; 100 releases it evenly across one command
/// interval; values above 100 overlap and are allowed. A command loop at the native report rate
/// keeps each command whole, on one report.
///
/// The box learns the interval from `MOVE` arrivals and spreads nothing until it has
/// ([`SpreadStatus::span_us`](crate::SpreadStatus)). Motion asking for exact timing is released
/// at once: [`move_rel_now`](Self::move_rel_now), [`flush_motion`](Self::flush_motion) and
/// [`discard_motion`](Self::discard_motion).
///
/// ```no_run
/// # use medius::{Device, Result};
/// # fn main() -> Result<()> {
/// let device = Device::find()?;
/// device.set_spread(100)?;
/// # Ok(()) }
/// ```
pub fn set_spread(&self, percent: u16) -> Result<()> {
self.link.send(FrameType::Option, &spread_payload(percent))
}
}