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
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;
/// The max box-name length on the wire, in bytes (printable ASCII), matching the firmware's `CTRL_NAME_MAX`.
pub const NAME_MAX: usize = 32;
/// The emit-rate ceiling in Hz: the firmware clamps a [`EmitPace::Fixed`](crate::EmitPace::Fixed) rate above this down to it (the 1 ms frame-clock limit).
pub const EMIT_MAX_HZ: u16 = 1000;
/// Encode a movement-riding window to the wire `u16` ms: `None` = 0 (off); a non-zero `Some` is at least 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),
}
}
/// The bearing window a box boots with, before any host sets one.
pub const BEARING_WINDOW_DEFAULT: Duration = Duration::from_millis(20);
/// Encode an [`EmitPace`] to the wire `(mode, rate_hz)`: `Fixed(hz)` carries its rate, the other paces send 0.
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)`: opt into cloning an over-capacity device (one interface left dead) or back to faithful-only; persisted in NVS.
///
/// Turning the opt-in off clears the box's rewrite table, so the held rules are dropped to match
/// (restored if the frame never goes out); otherwise the keepalive re-asserts them the moment the
/// opt-in comes back on. Toggling the opt-in off concurrently with a `set_rewrite` on another thread
/// is not ordered: the rule may reach the box after the opt-off and be refused.
pub fn allow_imperfect_clones(&self, allow: bool) -> Result<()> {
if !allow {
// Serialised against the keepalive/reconnect re-assert like the rewrite mutators, and rolled
// back on a failed send so DesiredState and the box stay in step.
let _serial = self.link.reassert_guard();
let held = {
let mut d = self.link.desired().lock();
let held = d.held_rewrites();
d.clear_rewrites();
held
};
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);
}
}
return sent;
}
self.link.send(FrameType::Option, &imperfect_payload(true))
}
/// `OPTION(MOVE_RIDE)`: injected motion rides a native motion report seen within `window` (else dropped)
/// so its density matches the native mouse's; `None` off; persisted in NVS. A single move can override
/// it ([`MoveTiming::Now`](crate::MoveTiming), [`move_rel_now`](Self::move_rel_now)), and clip playback
/// bypasses it unless [`ClipHandle::set_ride`](crate::ClipHandle::set_ride) is on; while rendering is on
/// with a profile armed, a clip's cursor motion follows it like 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)`: set the pace ([`EmitPace`], the rate ceiling) and the forced wire rate (`force_hz`); persisted in NVS.
///
/// A non-zero `force_hz` re-clones the box to advertise a `bInterval` the device did not (needs
/// [`allow_imperfect_clones`](Self::allow_imperfect_clones)), snapping to `1000/n` 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)`: set the box's persistent name (leading printable-ASCII run, capped at [`NAME_MAX`] bytes); read it back off [`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: clear the custom name, reverting the box to its synthesised `Medius-XXXX` default.
pub fn clear_name(&self) -> Result<()> {
self.set_name("")
}
/// `OPTION(BEARING)`: how long the direction of the last injected delta stays the thing
/// [`Direction::With`](crate::Direction) and [`Direction::Against`](crate::Direction) are measured
/// against, and whether it is read per axis or as one vector; persisted in NVS.
///
/// `None` turns the bearing off, which leaves 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 at least 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)`: set the texture motion is rendered with, and whether native motion is
/// rendered by the model too rather than relayed; persisted in NVS.
///
/// Rendering adds a small amount of latency, which reaches native motion when `full` is
/// on, so `full` is off by default. Nothing is rendered until the box has learned a profile for the
/// attached device: until then motion is relayed and injection takes the paced fill
/// ([`RenderStatus::ready`](crate::RenderStatus)).
///
/// 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, and 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 report the box emits. 100 releases that delta evenly
/// across one command interval. Above 100 overlaps, and is allowed. At a loop matched to the native report
/// rate the interval is one report period, so the emitted stream is what it was.
///
/// The box learns the interval from `MOVE` arrivals and releases nothing across one 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))
}
}