165 lines
5.6 KiB
Rust
165 lines
5.6 KiB
Rust
pub mod structs;
|
|
use std::{cmp::min};
|
|
use chrono::{Datelike, Local, Timelike};
|
|
use log::{error, trace, warn};
|
|
use sdl3::{Sdl, pixels::Color, rect::Rect};
|
|
use structs::{Prefs, DisplayData};
|
|
|
|
use crate::{config::Error, renderer::structs::Screen};
|
|
|
|
const LINE_COUNT: i32 = 5;
|
|
const COLOR_LCD_AMBER : Color = Color::RGB(0xf4, 0xcb, 0x60);
|
|
const COLOR_LCD_GREEN : Color = Color::RGB(0xb3, 0xff, 0x00);
|
|
const COLOR_LCD_RED : Color = Color::RGB(0xff, 0x3a, 0x4a);
|
|
const COLOR_BACKGROUND : Color = Color::RGB(0x0, 0x0, 0x0 );
|
|
const _COLOR_TEXT_BG : Color = Color::RGBA(0x0, 0x0, 0x0, 0x0);
|
|
const TEXT_SIZE: f32 = 160.0; // Size of the font in pixels
|
|
|
|
// Offsets of each part within a line
|
|
const XOFFSET_ROUTE: u32 = 24;
|
|
const XOFFSET_DESTINATION: u32 = 300;
|
|
const XOFFSEET_TIME_LEFT: u32 = 1606;
|
|
const INTER_LINE_OVERLAP: u32 = 15;
|
|
|
|
impl Screen<'_> {
|
|
|
|
pub fn get_context(&self) -> &Sdl {
|
|
return &self.context;
|
|
}
|
|
|
|
fn color_for(&self, due_in: i32) -> Color {
|
|
if due_in > 15 { return COLOR_LCD_GREEN };
|
|
if due_in > 10 { return COLOR_LCD_AMBER };
|
|
return COLOR_LCD_RED;
|
|
}
|
|
|
|
fn format_due_for(&self, due_in_mins: i32, departure_time: u32) -> String {
|
|
trace!("Due in mins: {:02}", due_in_mins);
|
|
if due_in_mins <= 1 {
|
|
return String::from("due");
|
|
}
|
|
if due_in_mins < 60 {
|
|
return due_in_mins.to_string() + "min";
|
|
}
|
|
return format!("{:02}:{:02}", (departure_time / 3600) as i32, ((departure_time / 60) % 60) as i32);
|
|
}
|
|
|
|
|
|
pub fn update_information(&mut self, display_data: &DisplayData) {
|
|
let local_time = Local::now();
|
|
let seconds_since_midnight = local_time.num_seconds_from_midnight();
|
|
|
|
self.do_clear();
|
|
|
|
// Print status first
|
|
self.color = COLOR_LCD_AMBER;
|
|
if display_data.status.is_some() {
|
|
// If the update has some information text, show it
|
|
self.do_print_at(5, display_data.status.as_ref().unwrap(), 0);
|
|
} else {
|
|
// Display date and time otherwise
|
|
self.do_print_at(5, &format!("Current time: {:02}/{:02}/{:4} {:02}:{:02}",
|
|
local_time.day(), local_time.month(), local_time.year(),
|
|
local_time.hour(), local_time.minute()
|
|
).to_string(), 0);
|
|
}
|
|
|
|
// Then data lines from the bottom
|
|
let num_arrivals: i32 = min(LINE_COUNT, display_data.lines.len() as i32);
|
|
for index in 0..num_arrivals {
|
|
let line: u32 = ((num_arrivals - 1) - index) as u32;
|
|
// Compose a line of text with all the information
|
|
let entry = display_data.lines.get(line as usize).unwrap();
|
|
let due_in_mins = ((entry.departure_time - seconds_since_midnight) / 60) as i32;
|
|
let due_color: Color = self.color_for(due_in_mins);
|
|
let due_text = self.format_due_for(due_in_mins, entry.departure_time);
|
|
|
|
self.color = COLOR_LCD_AMBER;
|
|
self.do_print_at(line, &entry.route, XOFFSET_ROUTE);
|
|
self.do_print_at(line, &entry.destination, XOFFSET_DESTINATION);
|
|
self.color = due_color;
|
|
self.do_print_at(line, &due_text, XOFFSEET_TIME_LEFT);
|
|
};
|
|
self.do_update();
|
|
}
|
|
|
|
|
|
pub fn _print(&mut self, line: u32, text: &str) {
|
|
self.do_print_at(line, text, 0);
|
|
self.do_update();
|
|
}
|
|
|
|
|
|
fn do_print_at(&mut self, line: u32, text: &str, left: u32) {
|
|
if text.len() == 0 {
|
|
warn!("do_print_at called with a 0-length string");
|
|
return;
|
|
}
|
|
|
|
let render_result = self.font.render(text).lcd(self.color, COLOR_BACKGROUND);
|
|
|
|
if render_result.is_err() {
|
|
error!("Error rendering text \"{}\": {:#?}", text, render_result.err());
|
|
return;
|
|
}
|
|
|
|
let rendered_text = render_result.unwrap();
|
|
let texture_creator = self.canvas.texture_creator();
|
|
let texture = rendered_text.as_texture(&texture_creator);
|
|
|
|
if texture.is_err() {
|
|
error!("Error creating texture from rendered text: {:#?}", texture.err());
|
|
return;
|
|
}
|
|
|
|
let _= self.canvas.copy(&texture.unwrap(),
|
|
Rect::new(0, 0, rendered_text.width(), rendered_text.height()),
|
|
Rect::new(left.try_into().unwrap(), (line * (rendered_text.height() - INTER_LINE_OVERLAP)).try_into().unwrap(), rendered_text.width(), rendered_text.height()));
|
|
}
|
|
|
|
|
|
pub fn clear(&mut self) {
|
|
self.do_clear();
|
|
self.do_update();
|
|
}
|
|
|
|
|
|
fn do_update (&mut self) {
|
|
self.canvas.present();
|
|
}
|
|
|
|
|
|
fn do_clear(&mut self) {
|
|
self.canvas.set_draw_color(COLOR_BACKGROUND);
|
|
self.canvas.clear();
|
|
}
|
|
|
|
/// Initialize video, allocate buffers and load fonts
|
|
/// Based on https://github.com/vhspace/sdl3-rs/blob/master/examples/ttf-demo.rs
|
|
pub fn init(prefs: &Prefs) -> Result<Screen<'_>, Error> {
|
|
// Initialize the screen
|
|
let sdl_context = sdl3::init().unwrap();
|
|
let video_subsys = sdl_context.video()?;
|
|
|
|
let window = video_subsys
|
|
.window("Dublin Bus", prefs.screen_width, prefs.screen_height)
|
|
.position_centered()
|
|
.borderless()
|
|
//.fullscreen()
|
|
.build().unwrap();
|
|
|
|
// Load font
|
|
let ttf_context = sdl3::ttf::init().unwrap();
|
|
let font = ttf_context.load_font(&prefs.font_path, TEXT_SIZE)?;
|
|
|
|
let mut screen: Screen = Screen {
|
|
canvas: Box::new(window.into_canvas()),
|
|
font: Box::new(font),
|
|
color: COLOR_LCD_AMBER,
|
|
context: Box::new(sdl_context),
|
|
};
|
|
|
|
screen.clear();
|
|
return Ok(screen);
|
|
}
|
|
} |