Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions lampOS/crates/ring/src/asleep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
//! authority. A faint warm glow at the bottom of the ring that breathes
//! slowly, as a sleeping thing does. It says only that the lamp is on and
//! cannot hear (muted, or the privacy switch closed), so it needs no permit:
//! [`AsleepRing`] can show this picture and black, and nothing else.
//! [`AsleepRing`] can show this picture and black, and nothing else but the
//! same picture with one soft arc of its own warmth going around, when a
//! reminder is owed (#1545): no channel of either is above [`PEAK`].

use crate::{EncodedFrame, FrameSink, PIXEL_COUNT, Rgb, encode};
use std::io;
Expand Down Expand Up @@ -33,7 +35,34 @@ pub fn asleep_pixels(breath: f32) -> [Rgb; PIXEL_COUNT] {
pixels
}

/// A ring writer that shows only the asleep face or black. Opening it sends
/// The asleep face with one soft arc going around the ring, `around` of the
/// way (0 to 1) clockwise from [`BOTTOM`] back to it: a sixth of the ring
/// wide, at most [`PEAK`] at its middle, eased at its edges. Where the arc
/// and the face meet, the brighter of the two is shown, so no channel is
/// above [`PEAK`]'s, and outside the arc the face is as [`asleep_pixels`].
pub fn owed_pixels(breath: f32, around: f32) -> [Rgb; PIXEL_COUNT] {
let mut pixels = asleep_pixels(breath);
let head = (BOTTOM as f32 + around.clamp(0.0, 1.0) * PIXEL_COUNT as f32) % PIXEL_COUNT as f32;
let half = PIXEL_COUNT as f32 / 12.0;
for (index, pixel) in pixels.iter_mut().enumerate() {
let apart = (index as f32 - head).rem_euclid(PIXEL_COUNT as f32);
let apart = apart.min(PIXEL_COUNT as f32 - apart);
if apart >= half {
continue;
}
let near = 0.5 + 0.5 * (std::f32::consts::PI * apart / half).cos();
let scale = |channel: u8| (f32::from(channel) * near).round() as u8;
*pixel = Rgb::new(
pixel.red.max(scale(PEAK.red)),
pixel.green.max(scale(PEAK.green)),
pixel.blue.max(scale(PEAK.blue)),
);
}
pixels
}

/// A ring writer that shows only the asleep face, the face with an owed
/// reminder's arc over it ([`owed_pixels`], #1545), or black. Opening it sends
/// black; [`AsleepRing::shutdown`] sends black again. It holds no authority
/// and grants none: the voice uses it only while no turn can run, and closes
/// it before its ring worker opens the device again.
Expand All @@ -53,6 +82,18 @@ impl<S: FrameSink> AsleepRing<S> {
.write_frame(&encode(&asleep_pixels(breath), ceiling))
}

/// The asleep face with a reminder's soft arc `around` of the way
/// around ([`owed_pixels`]), under `ceiling`.
pub fn show_owed(
&mut self,
breath: f32,
around: f32,
ceiling: crate::ChannelCeiling,
) -> io::Result<()> {
self.sink
.write_frame(&encode(&owed_pixels(breath, around), ceiling))
}

/// Black, then the writer is gone.
pub fn shutdown(mut self) -> io::Result<()> {
self.sink.write_frame(&EncodedFrame::black())
Expand Down Expand Up @@ -123,4 +164,38 @@ mod tests {
)
);
}

/// #1545: a reminder's arc goes around in the face's own warmth, never
/// above its peak, a sixth of the ring wide; at its start and end it is
/// at the bottom, where the face is.
#[test]
fn an_owed_arc_goes_around_warm_and_never_above_the_peak() {
for step in 0..=20 {
let around = step as f32 / 20.0;
let pixels = owed_pixels(0.5, around);
let lit: Vec<usize> = (0..PIXEL_COUNT)
.filter(|&index| pixels[index] != Rgb::BLACK)
.collect();
for pixel in pixels {
assert!(
pixel.red <= PEAK.red && pixel.green <= PEAK.green && pixel.blue <= PEAK.blue
);
assert!(
pixel.red >= pixel.green && pixel.green >= pixel.blue,
"{pixel:?}"
);
}
// The face's five and the arc's at most five more.
assert!(lit.len() <= 10, "{around}: {lit:?}");
}
// Half way, the arc is at the top, far from the face.
let half = owed_pixels(0.5, 0.5);
assert_eq!(half[0], PEAK);
assert_eq!(half[BOTTOM], asleep_pixels(0.5)[BOTTOM]);
// Outside the arc the face is unchanged.
let quarter = owed_pixels(0.5, 0.25);
for index in [BOTTOM - 2, BOTTOM, BOTTOM + 2] {
assert_eq!(quarter[index], asleep_pixels(0.5)[index]);
}
}
}
8 changes: 8 additions & 0 deletions lampOS/crates/voice/src/bus_out.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,14 @@ pub enum Waiting {
pub const WAIT_UNREADABLE_AFTER_US: u64 = 1_000_000;

impl Waiting {
/// Whether an owed reminder may pulse on the asleep face (#1545): only
/// while the switch or the person's mute holds the microphones, not a
/// hand on the head, whose face the asleep face is then, nor a fault
/// (Thirteen on #1545).
pub fn pulses(self) -> bool {
matches!(self, Self::Switch | Self::Muted)
}

/// Whether the microphones are held closed: by the switch, a mute, a
/// hand, the restart guard or a switch that cannot be read. Not while
/// the switch is first read or has read open, which every start of the
Expand Down
96 changes: 95 additions & 1 deletion lampOS/crates/voice/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,19 +871,78 @@ fn tap_face_allowed(head_touch: bool, asleep_on: bool, privacy_controls: bool) -
head_touch && !asleep_on && !privacy_controls
}

/// What the journal says, once, when `LAMP_REMINDER_PULSE=on` has no
/// asleep face to draw on (#1545, the first builder, 18 Oct 18:03): the
/// pulse is drawn over that face, so rather than pulse nothing, it says
/// why there is no pulse.
fn pulse_without_face(
pulse_on: bool,
ring: bool,
asleep_on: bool,
privacy_controls: bool,
) -> Option<String> {
let why = match (pulse_on, ring, asleep_on, privacy_controls) {
(false, ..) | (true, true, true, false) => return None,
(true, false, ..) => "there is no ring",
(true, true, false, _) => "LAMP_RING_ASLEEP is off",
(true, true, true, true) => "the Controls privacy route has the ring while muted",
};
Some(format!(
"lamp-voice: LAMP_REMINDER_PULSE is on but {why}, so an owed reminder does not pulse"
))
}

/// Whether an owed reminder's pulse may show on this look of the muted wait
/// (#1545): something `owed`, and the wait the privacy switch's or the
/// person's mute (`Waiting::pulses`: not the restart guard or a switch that
/// can't be read, Thirteen on #1545), and not the sleep a hand held on the
/// head chose, even with the switch closed too, which `Waiting::of` reads
/// first (the reviewer on #1581).
fn pulse_shown(owed: Option<bool>, waiting: crate::bus_out::Waiting, asleep: bool) -> bool {
owed.is_some() && waiting.pulses() && !asleep
}

/// The privacy wait (`wait`) of one muted cycle, with what the service does
/// meanwhile: each state into the trace and the slide's last state kept;
/// at each look, the reminders, the controls and a tap's face (#711). The
/// face is stopped, black and closed, before this returns. The wait is a
/// parameter so a test can run these very closures without a worker (the
/// reviewer on #1288).
#[cfg(test)]
fn muted_wait<F: FnMut() -> Option<crate::ring_asleep::Asleep>>(
trace: &mut SessionState,
cues: &mut Option<CueSink>,
controls: &RefCell<Option<HandControls>>,
reminders: &RefCell<Option<crate::reminders::Reminders>>,
tap_face: Option<crate::ring_asleep::TapFace<F>>,
health: Option<crate::bus_out::WaitHealth>,
wait: impl FnOnce(
&mut dyn FnMut(crate::privacy::PrivacyState, BootId, u64) -> Result<()>,
&mut dyn FnMut() -> Result<bool>,
) -> Result<crate::service_privacy::WaitOutcome>,
) -> Result<crate::service_privacy::WaitOutcome> {
muted_wait_pulsing(
trace, cues, controls, reminders, tap_face, health, None, wait,
)
}

/// The muted wait, and with `pulse` (the asleep face's [`Asleep::owed`],
/// `LAMP_REMINDER_PULSE=on`) on each look since when a reminder has been
/// owed, while its pulse may show ([`pulse_shown`]). That time is kept for
/// the attempt, from the look that first finds it owed, so a pulse held
/// back for a while neither restarts the half-hour nor comes at once when
/// it may show again (Thirteen and the reviewer on #1581).
///
/// [`Asleep::owed`]: crate::ring_asleep::Asleep::owed
#[allow(clippy::too_many_arguments)]
fn muted_wait_pulsing<F: FnMut() -> Option<crate::ring_asleep::Asleep>>(
trace: &mut SessionState,
cues: &mut Option<CueSink>,
controls: &RefCell<Option<HandControls>>,
reminders: &RefCell<Option<crate::reminders::Reminders>>,
tap_face: Option<crate::ring_asleep::TapFace<F>>,
mut health: Option<crate::bus_out::WaitHealth>,
pulse: Option<std::sync::Arc<crate::ring_asleep::Owed>>,
wait: impl FnOnce(
&mut dyn FnMut(crate::privacy::PrivacyState, BootId, u64) -> Result<()>,
&mut dyn FnMut() -> Result<bool>,
Expand All @@ -897,6 +956,7 @@ fn muted_wait<F: FnMut() -> Option<crate::ring_asleep::Asleep>>(
// "cannot be read" (#1376): only past the wait's own grace.
let unread_since = std::cell::Cell::new(None::<u64>);
let tap_face = RefCell::new(tap_face);
let owed_since = std::cell::Cell::new(None::<Instant>);
let outcome = wait(
&mut |state, boot, at_us| {
slide.set(Some(state));
Expand Down Expand Up @@ -954,6 +1014,22 @@ fn muted_wait<F: FnMut() -> Option<crate::ring_asleep::Asleep>>(
json!({"kind":"reminder_owed_while_muted", "at_us":now}),
)?;
}
if let Some(pulse) = &pulse {
owed_since.set(match owed {
// Its attempt's first look owed: its due time.
Some(true) => Some(Instant::now()),
// Still owed: kept, or from now in a new muted cycle.
Some(false) => owed_since.get().or_else(|| Some(Instant::now())),
// Nothing offered: kept through a lapse of the offer,
// and a new attempt's first look starts it again.
None => owed_since.get(),
});
pulse.tell(
owed_since
.get()
.filter(|_| pulse_shown(owed, waiting, asleep)),
);
}
// No session runs while it waits, so the wait says how the
// voice is: its microphones off, and why (#1371).
if let Some(health) = &mut health {
Expand Down Expand Up @@ -1780,6 +1856,14 @@ fn run_source(
privacy_controls,
)
});
if let Some(line) = pulse_without_face(
crate::ring_asleep::pulse_enabled_from_environment(),
options.ring_channel_ceiling.is_some(),
crate::ring_asleep::enabled_from_environment(),
privacy_controls,
) {
eprintln!("{line}");
}
service::supervise(
&mut trace,
&mut cues,
Expand All @@ -1792,13 +1876,23 @@ fn run_source(
let tap_face = tap_face_ceiling.map(|ceiling| {
crate::ring_asleep::TapFace::new(move || tapped_face(ceiling))
});
let outcome = muted_wait(
// A reminder owed while it waits pulses on the asleep
// face (#1545, `LAMP_REMINDER_PULSE=on`).
#[cfg(target_os = "linux")]
let pulse = asleep
.as_ref()
.filter(|_| crate::ring_asleep::pulse_enabled_from_environment())
.map(crate::ring_asleep::Asleep::owed);
#[cfg(not(target_os = "linux"))]
let pulse = None;
let outcome = muted_wait_pulsing(
trace,
cues,
&controls,
&reminders,
tap_face,
crate::bus_out::WaitHealth::from_environment(),
pulse,
|on_state, gate| {
crate::service_privacy_controls::wait_selected(
&executable,
Expand Down
Loading
Loading