mirror of
https://github.com/kaesaecracker/servicepoint-simulator.git
synced 2025-01-31 01:30:13 +01:00
split out gui options
This commit is contained in:
parent
31a8f6a40a
commit
3049a8bd7a
57
src/cli.rs
Normal file
57
src/cli.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
default_value = "0.0.0.0:2342",
|
||||||
|
help = "address and port to bind to"
|
||||||
|
)]
|
||||||
|
pub bind: String,
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "Set default log level lower. You can also change this via the RUST_LOG environment variable."
|
||||||
|
)]
|
||||||
|
pub debug: bool,
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "The name of the font family to use. This defaults to the system monospace font."
|
||||||
|
)]
|
||||||
|
pub font: Option<String>,
|
||||||
|
#[clap(flatten)]
|
||||||
|
pub gui: GuiOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct GuiOptions {
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value_t = false,
|
||||||
|
help = "Use the red color channel"
|
||||||
|
)]
|
||||||
|
pub red: bool,
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value_t = false,
|
||||||
|
help = "Use the green color channel"
|
||||||
|
)]
|
||||||
|
pub green: bool,
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value_t = false,
|
||||||
|
help = "Use the blue color channel"
|
||||||
|
)]
|
||||||
|
pub blue: bool,
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value_t = false,
|
||||||
|
help = "add spacers between tile rows to simulate gaps in real display"
|
||||||
|
)]
|
||||||
|
pub spacers: bool,
|
||||||
|
}
|
|
@ -18,7 +18,17 @@ impl Cp437Font {
|
||||||
|
|
||||||
impl Default for Cp437Font {
|
impl Default for Cp437Font {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
load_static()
|
let mut bitmaps =
|
||||||
|
core::array::from_fn(|_| Bitmap::new(TILE_SIZE, TILE_SIZE));
|
||||||
|
|
||||||
|
for (char_code, bitmap) in bitmaps.iter_mut().enumerate() {
|
||||||
|
let bits = CP437_FONT_LINEAR[char_code];
|
||||||
|
let mut bytes = bits.to_be_bytes();
|
||||||
|
bytes.reverse();
|
||||||
|
bitmap.data_ref_mut().copy_from_slice(bytes.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::new(bitmaps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,17 +291,3 @@ pub(crate) const CP437_FONT_LINEAR: [u64; 256] = [
|
||||||
0x00007c7c7c7c7c00, // 0xfe
|
0x00007c7c7c7c7c00, // 0xfe
|
||||||
0x0000000000000000, // 0xff
|
0x0000000000000000, // 0xff
|
||||||
];
|
];
|
||||||
|
|
||||||
fn load_static() -> Cp437Font {
|
|
||||||
let mut bitmaps =
|
|
||||||
core::array::from_fn(|_| Bitmap::new(TILE_SIZE, TILE_SIZE));
|
|
||||||
|
|
||||||
for (char_code, bitmap) in bitmaps.iter_mut().enumerate() {
|
|
||||||
let bits = CP437_FONT_LINEAR[char_code];
|
|
||||||
let mut bytes = bits.to_be_bytes();
|
|
||||||
bytes.reverse();
|
|
||||||
bitmap.data_ref_mut().copy_from_slice(bytes.as_slice());
|
|
||||||
}
|
|
||||||
|
|
||||||
Cp437Font::new(bitmaps)
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::execute_command::ExecutionResult::{Failure, Shutdown, Success};
|
use crate::execute_command::ExecutionResult::{Failure, Shutdown, Success};
|
||||||
use crate::font::Cp437Font;
|
use crate::cp437_font::Cp437Font;
|
||||||
use crate::font_renderer::FontRenderer8x8;
|
use crate::font_renderer::FontRenderer8x8;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use servicepoint::{
|
use servicepoint::{
|
||||||
|
@ -13,7 +13,7 @@ pub struct CommandExecutor<'t> {
|
||||||
display: &'t RwLock<Bitmap>,
|
display: &'t RwLock<Bitmap>,
|
||||||
luma: &'t RwLock<BrightnessGrid>,
|
luma: &'t RwLock<BrightnessGrid>,
|
||||||
cp437_font: Cp437Font,
|
cp437_font: Cp437Font,
|
||||||
utf8_font: FontRenderer8x8,
|
font_renderer: FontRenderer8x8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
@ -27,11 +27,12 @@ impl<'t> CommandExecutor<'t> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
display: &'t RwLock<Bitmap>,
|
display: &'t RwLock<Bitmap>,
|
||||||
luma: &'t RwLock<BrightnessGrid>,
|
luma: &'t RwLock<BrightnessGrid>,
|
||||||
|
font_renderer: FontRenderer8x8,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
CommandExecutor {
|
CommandExecutor {
|
||||||
display,
|
display,
|
||||||
luma,
|
luma,
|
||||||
utf8_font: FontRenderer8x8::default(),
|
font_renderer,
|
||||||
cp437_font: Cp437Font::default(),
|
cp437_font: Cp437Font::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +191,7 @@ impl<'t> CommandExecutor<'t> {
|
||||||
let tile_x = char_x + x;
|
let tile_x = char_x + x;
|
||||||
let tile_y = char_y + y;
|
let tile_y = char_y + y;
|
||||||
|
|
||||||
if let Err(e) = self.utf8_font.render(
|
if let Err(e) = self.font_renderer.render(
|
||||||
char,
|
char,
|
||||||
&mut display,
|
&mut display,
|
||||||
Origin::new(tile_x * TILE_SIZE, tile_y * TILE_SIZE),
|
Origin::new(tile_x * TILE_SIZE, tile_y * TILE_SIZE),
|
||||||
|
|
|
@ -43,12 +43,13 @@ pub enum RenderError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontRenderer8x8 {
|
impl FontRenderer8x8 {
|
||||||
pub fn new(font: Font, fallback_char: Option<char>) -> Self {
|
const FALLBACK_CHAR: char = '?';
|
||||||
|
pub fn new(font: Font) -> Self {
|
||||||
let canvas =
|
let canvas =
|
||||||
Canvas::new(vec2i(TILE_SIZE as i32, TILE_SIZE as i32), Format::A8);
|
Canvas::new(vec2i(TILE_SIZE as i32, TILE_SIZE as i32), Format::A8);
|
||||||
assert_eq!(canvas.pixels.len(), TILE_SIZE * TILE_SIZE);
|
assert_eq!(canvas.pixels.len(), TILE_SIZE * TILE_SIZE);
|
||||||
assert_eq!(canvas.stride, TILE_SIZE);
|
assert_eq!(canvas.stride, TILE_SIZE);
|
||||||
let fallback_char = fallback_char.and_then(|c| font.glyph_for_char(c));
|
let fallback_char = font.glyph_for_char(Self::FALLBACK_CHAR);
|
||||||
let result = Self {
|
let result = Self {
|
||||||
font: SendFont(font),
|
font: SendFont(font),
|
||||||
fallback_char,
|
fallback_char,
|
||||||
|
@ -57,6 +58,15 @@ impl FontRenderer8x8 {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_name(family_name: String) -> Self {
|
||||||
|
let font = SystemSource::new()
|
||||||
|
.select_best_match(&[FamilyName::Title(family_name)], &Properties::new())
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.unwrap();
|
||||||
|
Self::new(font)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
&self,
|
&self,
|
||||||
char: char,
|
char: char,
|
||||||
|
@ -104,17 +114,10 @@ impl FontRenderer8x8 {
|
||||||
impl Default for FontRenderer8x8 {
|
impl Default for FontRenderer8x8 {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let utf8_font = SystemSource::new()
|
let utf8_font = SystemSource::new()
|
||||||
.select_best_match(
|
.select_best_match(&[FamilyName::Monospace], &Properties::new())
|
||||||
&[
|
|
||||||
FamilyName::Title("Roboto Mono".to_string()),
|
|
||||||
FamilyName::Title("Fira Mono".to_string()),
|
|
||||||
FamilyName::Monospace,
|
|
||||||
],
|
|
||||||
&Properties::new(),
|
|
||||||
)
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.load()
|
.load()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
FontRenderer8x8::new(utf8_font, Some('?'))
|
FontRenderer8x8::new(utf8_font)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
src/gui.rs
20
src/gui.rs
|
@ -12,14 +12,14 @@ use winit::event_loop::ActiveEventLoop;
|
||||||
use winit::keyboard::KeyCode::KeyC;
|
use winit::keyboard::KeyCode::KeyC;
|
||||||
use winit::window::{Window, WindowId};
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
use crate::Cli;
|
use crate::cli::{GuiOptions};
|
||||||
|
|
||||||
pub struct Gui<'t> {
|
pub struct Gui<'t> {
|
||||||
display: &'t RwLock<Bitmap>,
|
display: &'t RwLock<Bitmap>,
|
||||||
luma: &'t RwLock<BrightnessGrid>,
|
luma: &'t RwLock<BrightnessGrid>,
|
||||||
window: Option<Window>,
|
window: Option<Window>,
|
||||||
stop_udp_tx: Sender<()>,
|
stop_udp_tx: Sender<()>,
|
||||||
cli: &'t Cli,
|
options: GuiOptions,
|
||||||
logical_size: LogicalSize<u16>,
|
logical_size: LogicalSize<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,15 +41,15 @@ impl<'t> Gui<'t> {
|
||||||
display: &'t RwLock<Bitmap>,
|
display: &'t RwLock<Bitmap>,
|
||||||
luma: &'t RwLock<BrightnessGrid>,
|
luma: &'t RwLock<BrightnessGrid>,
|
||||||
stop_udp_tx: Sender<()>,
|
stop_udp_tx: Sender<()>,
|
||||||
cli: &'t Cli,
|
options: GuiOptions,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Gui {
|
Gui {
|
||||||
|
window: None,
|
||||||
|
logical_size: Self::get_logical_size(options.spacers),
|
||||||
display,
|
display,
|
||||||
luma,
|
luma,
|
||||||
stop_udp_tx,
|
stop_udp_tx,
|
||||||
cli,
|
options,
|
||||||
window: None,
|
|
||||||
logical_size: Self::get_logical_size(cli.spacers),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ impl<'t> Gui<'t> {
|
||||||
let brightness_scale = (u8::MAX as f32) / (u8::from(Brightness::MAX) as f32);
|
let brightness_scale = (u8::MAX as f32) / (u8::from(Brightness::MAX) as f32);
|
||||||
|
|
||||||
for tile_y in 0..TILE_HEIGHT {
|
for tile_y in 0..TILE_HEIGHT {
|
||||||
if self.cli.spacers && tile_y != 0 {
|
if self.options.spacers && tile_y != 0 {
|
||||||
// cannot just frame.skip(PIXEL_WIDTH as usize * SPACER_HEIGHT as usize) because of typing
|
// cannot just frame.skip(PIXEL_WIDTH as usize * SPACER_HEIGHT as usize) because of typing
|
||||||
for _ in 0..PIXEL_WIDTH * SPACER_HEIGHT {
|
for _ in 0..PIXEL_WIDTH * SPACER_HEIGHT {
|
||||||
frame.next().unwrap();
|
frame.next().unwrap();
|
||||||
|
@ -105,9 +105,9 @@ impl<'t> Gui<'t> {
|
||||||
|
|
||||||
fn get_on_color(&self, brightness: u8) -> [u8; 4] {
|
fn get_on_color(&self, brightness: u8) -> [u8; 4] {
|
||||||
[
|
[
|
||||||
if self.cli.red { brightness } else { 0u8 },
|
if self.options.red { brightness } else { 0u8 },
|
||||||
if self.cli.green { brightness } else { 0u8 },
|
if self.options.green { brightness } else { 0u8 },
|
||||||
if self.cli.blue { brightness } else { 0u8 },
|
if self.options.blue { brightness } else { 0u8 },
|
||||||
255,
|
255,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
126
src/main.rs
126
src/main.rs
|
@ -11,89 +11,37 @@ use std::io::ErrorKind;
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
use std::sync::{mpsc, RwLock};
|
use std::sync::{mpsc, RwLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use winit::event_loop::{ControlFlow, EventLoop};
|
use winit::event_loop::{ControlFlow, EventLoop, EventLoopProxy};
|
||||||
|
use cli::Cli;
|
||||||
|
use crate::font_renderer::FontRenderer8x8;
|
||||||
|
|
||||||
mod execute_command;
|
mod execute_command;
|
||||||
mod font;
|
mod cp437_font;
|
||||||
mod font_renderer;
|
mod font_renderer;
|
||||||
mod gui;
|
mod gui;
|
||||||
|
mod cli;
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
struct Cli {
|
|
||||||
#[arg(
|
|
||||||
long,
|
|
||||||
default_value = "0.0.0.0:2342",
|
|
||||||
help = "address and port to bind to"
|
|
||||||
)]
|
|
||||||
bind: String,
|
|
||||||
#[arg(
|
|
||||||
short,
|
|
||||||
long,
|
|
||||||
default_value_t = false,
|
|
||||||
help = "add spacers between tile rows to simulate gaps in real display"
|
|
||||||
)]
|
|
||||||
spacers: bool,
|
|
||||||
#[arg(
|
|
||||||
short,
|
|
||||||
long,
|
|
||||||
help = "Set default log level lower. You can also change this via the RUST_LOG environment variable."
|
|
||||||
)]
|
|
||||||
debug: bool,
|
|
||||||
#[arg(
|
|
||||||
short,
|
|
||||||
long,
|
|
||||||
default_value_t = false,
|
|
||||||
help = "Use the red color channel"
|
|
||||||
)]
|
|
||||||
red: bool,
|
|
||||||
#[arg(
|
|
||||||
short,
|
|
||||||
long,
|
|
||||||
default_value_t = false,
|
|
||||||
help = "Use the green color channel"
|
|
||||||
)]
|
|
||||||
green: bool,
|
|
||||||
#[arg(
|
|
||||||
short,
|
|
||||||
long,
|
|
||||||
default_value_t = false,
|
|
||||||
help = "Use the blue color channel"
|
|
||||||
)]
|
|
||||||
blue: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
const BUF_SIZE: usize = 8985;
|
const BUF_SIZE: usize = 8985;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut cli = Cli::parse();
|
let mut cli = Cli::parse();
|
||||||
|
if !(cli.gui.red || cli.gui.blue || cli.gui.green) {
|
||||||
env_logger::builder()
|
cli.gui.green = true;
|
||||||
.filter_level(if cli.debug {
|
|
||||||
LevelFilter::Debug
|
|
||||||
} else {
|
|
||||||
LevelFilter::Info
|
|
||||||
})
|
|
||||||
.parse_default_env()
|
|
||||||
.init();
|
|
||||||
|
|
||||||
if !(cli.red || cli.blue || cli.green) {
|
|
||||||
cli.green = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init_logging(cli.debug);
|
||||||
info!("starting with args: {:?}", &cli);
|
info!("starting with args: {:?}", &cli);
|
||||||
|
|
||||||
let socket = UdpSocket::bind(&cli.bind).expect("could not bind socket");
|
let socket = UdpSocket::bind(&cli.bind).expect("could not bind socket");
|
||||||
socket
|
socket
|
||||||
.set_nonblocking(true)
|
.set_nonblocking(true)
|
||||||
.expect("could not enter non blocking mode");
|
.expect("could not enter non blocking mode");
|
||||||
|
|
||||||
let display = RwLock::new(Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT));
|
let display = RwLock::new(Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT));
|
||||||
|
let luma = RwLock::new(BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT));
|
||||||
let mut luma = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT);
|
|
||||||
luma.fill(Brightness::MAX);
|
|
||||||
let luma = RwLock::new(luma);
|
|
||||||
|
|
||||||
let (stop_udp_tx, stop_udp_rx) = mpsc::channel();
|
let (stop_udp_tx, stop_udp_rx) = mpsc::channel();
|
||||||
let mut app = Gui::new(&display, &luma, stop_udp_tx, &cli);
|
let mut gui = Gui::new(&display, &luma, stop_udp_tx, cli.gui);
|
||||||
|
|
||||||
let event_loop = EventLoop::with_user_event()
|
let event_loop = EventLoop::with_user_event()
|
||||||
.build()
|
.build()
|
||||||
|
@ -101,7 +49,9 @@ fn main() {
|
||||||
event_loop.set_control_flow(ControlFlow::Wait);
|
event_loop.set_control_flow(ControlFlow::Wait);
|
||||||
|
|
||||||
let event_proxy = event_loop.create_proxy();
|
let event_proxy = event_loop.create_proxy();
|
||||||
let command_executor = CommandExecutor::new(&display, &luma);
|
let font_renderer = cli.font.map(move |font| FontRenderer8x8::from_name(font))
|
||||||
|
.unwrap_or_else(move || FontRenderer8x8::default());
|
||||||
|
let command_executor = CommandExecutor::new(&display, &luma, font_renderer);
|
||||||
|
|
||||||
std::thread::scope(move |scope| {
|
std::thread::scope(move |scope| {
|
||||||
scope.spawn(move || {
|
scope.spawn(move || {
|
||||||
|
@ -117,29 +67,45 @@ fn main() {
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
match command_executor.execute(command) {
|
handle_command(&event_proxy, &command_executor, command);
|
||||||
ExecutionResult::Success => {
|
|
||||||
event_proxy
|
|
||||||
.send_event(AppEvents::UdpPacketHandled)
|
|
||||||
.expect("could not send packet handled event");
|
|
||||||
}
|
|
||||||
ExecutionResult::Failure => {
|
|
||||||
error!("failed to execute command");
|
|
||||||
}
|
|
||||||
ExecutionResult::Shutdown => {
|
|
||||||
event_proxy
|
|
||||||
.send_event(AppEvents::UdpThreadClosed)
|
|
||||||
.expect("could not send close event");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
event_loop
|
event_loop
|
||||||
.run_app(&mut app)
|
.run_app(&mut gui)
|
||||||
.expect("could not run event loop");
|
.expect("could not run event loop");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_command(event_proxy: &EventLoopProxy<AppEvents>, command_executor: &CommandExecutor, command: Command) {
|
||||||
|
match command_executor.execute(command) {
|
||||||
|
ExecutionResult::Success => {
|
||||||
|
event_proxy
|
||||||
|
.send_event(AppEvents::UdpPacketHandled)
|
||||||
|
.expect("could not send packet handled event");
|
||||||
|
}
|
||||||
|
ExecutionResult::Failure => {
|
||||||
|
error!("failed to execute command");
|
||||||
|
}
|
||||||
|
ExecutionResult::Shutdown => {
|
||||||
|
event_proxy
|
||||||
|
.send_event(AppEvents::UdpThreadClosed)
|
||||||
|
.expect("could not send close event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logging(debug: bool) {
|
||||||
|
let filter = if debug {
|
||||||
|
LevelFilter::Debug
|
||||||
|
} else {
|
||||||
|
LevelFilter::Info
|
||||||
|
};
|
||||||
|
env_logger::builder()
|
||||||
|
.filter_level(filter)
|
||||||
|
.parse_default_env()
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
fn command_from_slice(slice: &[u8]) -> Option<Command> {
|
fn command_from_slice(slice: &[u8]) -> Option<Command> {
|
||||||
let package = match servicepoint::Packet::try_from(slice) {
|
let package = match servicepoint::Packet::try_from(slice) {
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
Loading…
Reference in a new issue