use core::fmt::Write;
use crate::button::Button;
use crate::flash_block::FlashBlock;
#[cfg(not(test))]
use embassy_time::Timer;
use heapless::String;
use super::super::CydDisplay;
use super::calibration::{
CALIBRATION_MAX_DRAW_ITEMS, CALIBRATION_TEXT_RECTANGLE, CalibrationConfig, CalibrationCorner,
VERIFY_HIT_RADIUS_PIXELS, calibration_ack_dot_item, calibration_rejected_target_items,
calibration_target_items, calibration_verify_target_center, calibration_verify_target_items,
validate_calibration_points,
};
use super::flow::CalibrationFlow;
use super::flow::CalibrationFlowEvent;
use super::flow::{ReleaseTouchCapture, ReleaseTouchCaptureEvent};
use crate::cyd::backend::TouchUncalibrated;
use crate::cyd::display::{CydFrame, DrawItem};
use crate::cyd::{SCREEN_HEIGHT, SCREEN_WIDTH};
use embedded_graphics::{
geometry::{Point, Size},
primitives::Rectangle,
};
pub const CAPTURE_ACK_FRAME_COUNT: usize = 8;
pub const REJECTED_FRAME_COUNT: usize = 30;
pub const MAX_RAW_EVENTS_PER_FRAME: usize = 64;
const VERIFY_TIMEOUT_SECONDS: usize = 10;
const CALIBRATION_DRAW_FRAMES_PER_SECOND: usize = 10;
pub const VERIFY_TIMEOUT_FRAMES: usize =
VERIFY_TIMEOUT_SECONDS * CALIBRATION_DRAW_FRAMES_PER_SECOND;
const CALIBRATION_SHAPES_RECTANGLE: Rectangle = Rectangle::new(
Point::zero(),
Size::new(SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32),
);
#[derive(Debug)]
pub enum Error<DeviceError, FlashError> {
Device(DeviceError),
Flash(FlashError),
}
#[derive(Clone, Copy, Debug)]
struct EnsureCalibrationSettings {
verify_timeout_frames: usize,
}
impl EnsureCalibrationSettings {
const DEFAULT: Self = Self {
verify_timeout_frames: VERIFY_TIMEOUT_FRAMES,
};
#[must_use]
const fn verify_timeout_frames(self) -> usize {
self.verify_timeout_frames
}
}
enum CalibrationDriverState {
Capturing,
ShowCaptured {
calibration_corner: CalibrationCorner,
frames_remaining: usize,
},
ShowRejected {
worst_residual_pixels: Option<f32>,
frames_remaining: usize,
},
Verifying {
candidate_config: CalibrationConfig,
release_touch_capture: ReleaseTouchCapture,
polls_remaining: usize,
},
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CalibrationShape {
Capturing(Option<CalibrationCorner>),
ShowCaptured {
calibration_corner: CalibrationCorner,
next_corner: Option<CalibrationCorner>,
},
ShowRejected(Option<CalibrationCorner>),
Verifying,
}
pub async fn ensure_calibration<D, T, F, R>(
display: &mut D,
touch: T,
calibration_flash_block: &mut F,
recalibration_button: &mut R,
confirmed_message: Option<&str>,
orientation: crate::cyd::display::Orientation,
) -> Result<T::Calibrated, Error<D::Error, F::Error>>
where
D: CydDisplay,
T: TouchUncalibrated<Error = D::Error>,
F: FlashBlock,
R: Button,
{
ensure_calibration_with_settings(
display,
touch,
calibration_flash_block,
recalibration_button,
confirmed_message,
EnsureCalibrationSettings::DEFAULT,
orientation,
)
.await
}
async fn ensure_calibration_with_settings<D, T, F, R>(
display: &mut D,
mut touch: T,
calibration_flash_block: &mut F,
recalibration_button: &mut R,
confirmed_message: Option<&str>,
ensure_calibration_settings: EnsureCalibrationSettings,
orientation: crate::cyd::display::Orientation,
) -> Result<T::Calibrated, Error<D::Error, F::Error>>
where
D: CydDisplay,
T: TouchUncalibrated<Error = D::Error>,
F: FlashBlock,
R: Button,
{
if let Some(calibration_config) = calibration_flash_block
.load::<CalibrationConfig>()
.unwrap_or(None)
{
return Ok(touch.calibrate(calibration_config, orientation));
}
let mut calibration_flow = CalibrationFlow::new();
let mut calibration_driver_state = CalibrationDriverState::Capturing;
let mut last_calibration_shape = None;
let mut calibration_button_released = true;
loop {
let calibration_button_pressed = recalibration_button.is_pressed();
if calibration_button_pressed && calibration_button_released {
calibration_flow.restart();
calibration_driver_state = CalibrationDriverState::Capturing;
}
calibration_button_released = !calibration_button_pressed;
let mut saw_idle = false;
for _raw_event_index in 0..MAX_RAW_EVENTS_PER_FRAME {
let raw_touch_event = match touch.read_raw_touch_event() {
Ok(raw_touch_event) => raw_touch_event,
Err(error) => {
return Err(Error::Device(error));
}
};
let Some(raw_touch_event) = raw_touch_event else {
saw_idle = true;
break;
};
match &mut calibration_driver_state {
CalibrationDriverState::Capturing => {
let Some(calibration_flow_event) =
calibration_flow.handle_raw_touch_event(Some(raw_touch_event))
else {
continue;
};
match calibration_flow_event {
CalibrationFlowEvent::PointCaptured {
calibration_corner,
raw_point: _raw_point,
next_corner: _next_corner,
usable_sample_count: _usable_sample_count,
} => {
calibration_driver_state = CalibrationDriverState::ShowCaptured {
calibration_corner,
frames_remaining: CAPTURE_ACK_FRAME_COUNT,
};
}
CalibrationFlowEvent::Completed {
raw_points,
calibration_corner: _calibration_corner,
usable_sample_count: _usable_sample_count,
} => match validate_calibration_points(raw_points) {
Ok(calibration_validation) => {
calibration_driver_state = CalibrationDriverState::Verifying {
candidate_config: calibration_validation.calibration_config(),
release_touch_capture: ReleaseTouchCapture::new(),
polls_remaining: ensure_calibration_settings
.verify_timeout_frames(),
};
}
Err(crate::Error::CalibrationResidualTooLarge {
worst_residual_pixels,
}) => {
calibration_flow.restart();
calibration_driver_state = CalibrationDriverState::ShowRejected {
worst_residual_pixels: Some(worst_residual_pixels),
frames_remaining: REJECTED_FRAME_COUNT,
};
}
Err(crate::Error::CalibrationDegenerateGeometry) => {
calibration_flow.restart();
calibration_driver_state = CalibrationDriverState::ShowRejected {
worst_residual_pixels: None,
frames_remaining: REJECTED_FRAME_COUNT,
};
}
Err(_) => {
calibration_flow.restart();
calibration_driver_state = CalibrationDriverState::ShowRejected {
worst_residual_pixels: None,
frames_remaining: REJECTED_FRAME_COUNT,
};
}
},
}
}
CalibrationDriverState::Verifying {
candidate_config,
release_touch_capture,
..
} => {
let Some(ReleaseTouchCaptureEvent::Captured { raw_point, .. }) =
release_touch_capture.handle_raw_touch_event(Some(raw_touch_event))
else {
continue;
};
let (mapped_x, mapped_y) =
candidate_config.map_raw_to_screen(raw_point.x, raw_point.y);
if hit_verify_target(mapped_x, mapped_y) {
if let Some(confirmed_message) = confirmed_message
&& let Err(error) =
draw_message_screen(display, confirmed_message).await
{
return Err(Error::Device(error));
}
if let Err(error) = calibration_flash_block.save(candidate_config) {
return Err(Error::Flash(error));
}
let calibration_config = *candidate_config;
return Ok(touch.calibrate(calibration_config, orientation));
} else {
calibration_flow.restart();
calibration_driver_state = CalibrationDriverState::ShowRejected {
worst_residual_pixels: None,
frames_remaining: REJECTED_FRAME_COUNT,
};
}
}
CalibrationDriverState::ShowCaptured { .. }
| CalibrationDriverState::ShowRejected { .. } => {}
}
}
if saw_idle {
advance_driver_state_after_idle(&mut calibration_flow, &mut calibration_driver_state);
}
if let Err(error) = draw_calibration_screen(
display,
&calibration_flow,
&calibration_driver_state,
&mut last_calibration_shape,
)
.await
{
return Err(Error::Device(error));
}
if saw_idle {
pace_verification_frame(&calibration_driver_state).await;
}
}
}
#[cfg(not(test))]
async fn pace_verification_frame(calibration_driver_state: &CalibrationDriverState) {
if matches!(
calibration_driver_state,
CalibrationDriverState::Verifying { .. }
) {
Timer::after_millis(100).await;
}
}
#[cfg(test)]
async fn pace_verification_frame(_calibration_driver_state: &CalibrationDriverState) {}
async fn draw_message_screen<D>(display: &mut D, message: &str) -> Result<(), D::Error>
where
D: CydDisplay,
{
display.clear()?;
display
.frame_mut(CALIBRATION_TEXT_RECTANGLE)
.write_text(message)
.flush()
.await
}
fn advance_driver_state_after_idle(
calibration_flow: &mut CalibrationFlow,
calibration_driver_state: &mut CalibrationDriverState,
) {
match calibration_driver_state {
CalibrationDriverState::Capturing => {
calibration_flow.handle_raw_touch_event(None);
}
CalibrationDriverState::ShowCaptured {
frames_remaining, ..
} => {
if *frames_remaining > 0 {
*frames_remaining -= 1;
}
if *frames_remaining == 0 {
*calibration_driver_state = CalibrationDriverState::Capturing;
}
}
CalibrationDriverState::ShowRejected {
frames_remaining, ..
} => {
if *frames_remaining > 0 {
*frames_remaining -= 1;
}
if *frames_remaining == 0 {
*calibration_driver_state = CalibrationDriverState::Capturing;
}
}
CalibrationDriverState::Verifying {
release_touch_capture,
polls_remaining,
..
} => {
release_touch_capture.handle_raw_touch_event(None);
if *polls_remaining > 0 {
*polls_remaining -= 1;
}
if *polls_remaining == 0 {
calibration_flow.restart();
*calibration_driver_state = CalibrationDriverState::ShowRejected {
worst_residual_pixels: None,
frames_remaining: REJECTED_FRAME_COUNT,
};
}
}
}
}
async fn draw_calibration_screen<D>(
display: &mut D,
calibration_flow: &CalibrationFlow,
calibration_driver_state: &CalibrationDriverState,
last_calibration_shape: &mut Option<CalibrationShape>,
) -> Result<(), D::Error>
where
D: CydDisplay,
{
let mut shape_items: heapless::Vec<DrawItem, CALIBRATION_MAX_DRAW_ITEMS> = heapless::Vec::new();
let mut message = String::<48>::new();
let calibration_shape;
match calibration_driver_state {
CalibrationDriverState::Capturing => {
let next_corner = calibration_flow.next_corner();
calibration_shape = CalibrationShape::Capturing(next_corner);
if let Some(calibration_corner) = next_corner {
push_calibration_items(
&mut shape_items,
calibration_target_items(calibration_corner),
);
}
push_calibration_message(&mut message, "Tap target, then lift");
}
CalibrationDriverState::ShowCaptured {
calibration_corner, ..
} => {
let next_corner = calibration_flow.next_corner();
calibration_shape = CalibrationShape::ShowCaptured {
calibration_corner: *calibration_corner,
next_corner,
};
push_calibration_items(
&mut shape_items,
[calibration_ack_dot_item(*calibration_corner)],
);
if let Some(next_corner) = next_corner {
push_calibration_items(&mut shape_items, calibration_target_items(next_corner));
}
push_calibration_message(&mut message, "Corner captured");
}
CalibrationDriverState::ShowRejected {
worst_residual_pixels,
..
} => {
let next_corner = calibration_flow.next_corner();
calibration_shape = CalibrationShape::ShowRejected(next_corner);
if let Some(calibration_corner) = next_corner {
push_calibration_items(
&mut shape_items,
calibration_rejected_target_items(calibration_corner),
);
}
match worst_residual_pixels {
Some(worst_residual_pixels) => {
match write!(&mut message, "Try again ({worst_residual_pixels:.1}px)") {
Ok(()) => {}
Err(_) => unreachable!("heapless message capacity is sufficient"),
}
}
None => push_calibration_message(&mut message, "Try again"),
}
}
CalibrationDriverState::Verifying { .. } => {
calibration_shape = CalibrationShape::Verifying;
push_calibration_items(&mut shape_items, calibration_verify_target_items());
push_calibration_message(&mut message, "Tap center to save");
}
}
if *last_calibration_shape != Some(calibration_shape) {
let background = display.background_565();
display.draw_items::<CALIBRATION_MAX_DRAW_ITEMS>(
CALIBRATION_SHAPES_RECTANGLE,
background,
shape_items,
)?;
*last_calibration_shape = Some(calibration_shape);
}
display
.frame_mut(CALIBRATION_TEXT_RECTANGLE)
.write_text(message.as_str())
.flush()
.await
}
fn push_calibration_items<const N: usize, const M: usize>(
shape_items: &mut heapless::Vec<DrawItem, N>,
items: [DrawItem; M],
) {
for item in items {
shape_items
.push(item)
.expect("calibration draw items fit CALIBRATION_MAX_DRAW_ITEMS");
}
}
fn push_calibration_message(message: &mut String<48>, text: &str) {
message
.push_str(text)
.expect("calibration message fits fixed buffer");
}
fn hit_verify_target(mapped_x: f32, mapped_y: f32) -> bool {
let verify_target_center = calibration_verify_target_center();
let delta_x = mapped_x - verify_target_center.x as f32;
let delta_y = mapped_y - verify_target_center.y as f32;
delta_x * delta_x + delta_y * delta_y <= VERIFY_HIT_RADIUS_PIXELS * VERIFY_HIT_RADIUS_PIXELS
}