split out UdpServer

This commit is contained in:
Vinzenz Schroeter 2025-01-27 22:24:38 +01:00
commit 205cd566cb
5 changed files with 124 additions and 92 deletions

247
src/command_executor.rs Normal file
View file

@ -0,0 +1,247 @@
use crate::command_executor::ExecutionResult::{Failure, Shutdown, Success};
use crate::cp437_font::Cp437Font;
use crate::font_renderer::FontRenderer8x8;
use log::{debug, error, info, trace, warn};
use servicepoint::{
BitVec, Bitmap, BrightnessGrid, CharGrid, Command, Cp437Grid, Grid, Offset,
Origin, Tiles, PIXEL_COUNT, PIXEL_WIDTH, TILE_SIZE,
};
use std::ops::{BitAnd, BitOr, BitXor};
use std::sync::RwLock;
#[derive(Debug)]
pub struct CommandExecutor<'t> {
display: &'t RwLock<Bitmap>,
luma: &'t RwLock<BrightnessGrid>,
cp437_font: Cp437Font,
font_renderer: FontRenderer8x8,
}
#[must_use]
pub enum ExecutionResult {
Success,
Failure,
Shutdown,
}
impl<'t> CommandExecutor<'t> {
pub fn new(
display: &'t RwLock<Bitmap>,
luma: &'t RwLock<BrightnessGrid>,
font_renderer: FontRenderer8x8,
) -> Self {
CommandExecutor {
display,
luma,
font_renderer,
cp437_font: Cp437Font::default(),
}
}
pub(crate) fn execute(&self, command: Command) -> ExecutionResult {
debug!("received {command:?}");
match command {
Command::Clear => {
info!("clearing display");
self.display.write().unwrap().fill(false);
Success
}
Command::HardReset => {
warn!("display shutting down");
Shutdown
}
Command::BitmapLinearWin(Origin { x, y, .. }, pixels, _) => {
self.print_pixel_grid(x, y, &pixels)
}
Command::Cp437Data(origin, grid) => {
self.print_cp437_data(origin, &grid)
}
#[allow(deprecated)]
Command::BitmapLegacy => {
warn!("ignoring deprecated command {:?}", command);
Failure
}
Command::BitmapLinearAnd(offset, vec, _) => {
self.execute_bitmap_linear(offset, vec, BitAnd::bitand)
}
Command::BitmapLinearOr(offset, vec, _) => {
self.execute_bitmap_linear(offset, vec, BitOr::bitor)
}
Command::BitmapLinearXor(offset, vec, _) => {
self.execute_bitmap_linear(offset, vec, BitXor::bitxor)
}
Command::BitmapLinear(offset, vec, _) => {
self.execute_bitmap_linear(offset, vec, move |_, new| new)
}
Command::CharBrightness(origin, grid) => {
self.execute_char_brightness(origin, grid)
}
Command::Brightness(brightness) => {
self.luma.write().unwrap().fill(brightness);
Success
}
Command::FadeOut => {
error!("command not implemented: {command:?}");
Success
}
Command::Utf8Data(origin, grid) => {
self.print_utf8_data(origin, &grid)
}
}
}
fn execute_char_brightness(
&self,
origin: Origin<Tiles>,
grid: BrightnessGrid,
) -> ExecutionResult {
let mut luma = self.luma.write().unwrap();
for inner_y in 0..grid.height() {
for inner_x in 0..grid.width() {
let brightness = grid.get(inner_x, inner_y);
luma.set(origin.x + inner_x, origin.y + inner_y, brightness);
}
}
Success
}
fn execute_bitmap_linear<Op>(
&self,
offset: Offset,
vec: BitVec,
op: Op,
) -> ExecutionResult
where
Op: Fn(bool, bool) -> bool,
{
if !Self::check_bitmap_valid(offset as u16, vec.len()) {
return Failure;
}
let mut display = self.display.write().unwrap();
for bitmap_index in 0..vec.len() {
let (x, y) = Self::get_coordinates_for_index(offset, bitmap_index);
let old_value = display.get(x, y);
display.set(x, y, op(old_value, vec[bitmap_index]));
}
Success
}
fn check_bitmap_valid(offset: u16, payload_len: usize) -> bool {
if offset as usize + payload_len > PIXEL_COUNT {
error!(
"bitmap with offset {offset} is too big ({payload_len} bytes)"
);
return false;
}
true
}
fn print_cp437_data(
&self,
origin: Origin<Tiles>,
grid: &Cp437Grid,
) -> ExecutionResult {
let font = &self.cp437_font;
let Origin { x, y, .. } = origin;
for char_y in 0usize..grid.height() {
for char_x in 0usize..grid.width() {
let char_code = grid.get(char_x, char_y);
trace!(
"drawing char_code {char_code:#04x} (if this was UTF-8, it would be {})",
char::from(char_code)
);
let tile_x = char_x + x;
let tile_y = char_y + y;
match self.print_pixel_grid(
tile_x * TILE_SIZE,
tile_y * TILE_SIZE,
&font[char_code],
) {
Success => {}
Failure => {
error!(
"stopping drawing text because char draw failed"
);
return Failure;
}
Shutdown => return Shutdown,
}
}
}
Success
}
fn print_utf8_data(
&self,
origin: Origin<Tiles>,
grid: &CharGrid,
) -> ExecutionResult {
let mut display = self.display.write().unwrap();
let Origin { x, y, .. } = origin;
for char_y in 0usize..grid.height() {
for char_x in 0usize..grid.width() {
let char = grid.get(char_x, char_y);
trace!("drawing {char}");
let tile_x = char_x + x;
let tile_y = char_y + y;
if let Err(e) = self.font_renderer.render(
char,
&mut display,
Origin::new(tile_x * TILE_SIZE, tile_y * TILE_SIZE),
) {
error!(
"stopping drawing text because char draw failed: {e}"
);
return Failure;
}
}
}
Success
}
fn print_pixel_grid(
&self,
offset_x: usize,
offset_y: usize,
pixels: &Bitmap,
) -> ExecutionResult {
debug!(
"printing {}x{} grid at {offset_x} {offset_y}",
pixels.width(),
pixels.height()
);
let mut display = self.display.write().unwrap();
for inner_y in 0..pixels.height() {
for inner_x in 0..pixels.width() {
let is_set = pixels.get(inner_x, inner_y);
let x = offset_x + inner_x;
let y = offset_y + inner_y;
if x >= display.width() || y >= display.height() {
error!("stopping pixel grid draw because coordinate {x} {y} is out of bounds");
return Failure;
}
display.set(x, y, is_set);
}
}
Success
}
fn get_coordinates_for_index(
offset: usize,
index: usize,
) -> (usize, usize) {
let pixel_index = offset + index;
(pixel_index % PIXEL_WIDTH, pixel_index / PIXEL_WIDTH)
}
}

View file

@ -3,6 +3,7 @@ use std::ops::Index;
const CHAR_COUNT: usize = u8::MAX as usize + 1;
#[derive(Debug)]
pub struct Cp437Font {
bitmaps: [Bitmap; CHAR_COUNT],
}

View file

@ -15,6 +15,7 @@ use pathfinder_geometry::{
use servicepoint::{Bitmap, Grid, Origin, Pixels, TILE_SIZE};
use std::sync::{Mutex, MutexGuard};
#[derive(Debug)]
struct SendFont(Font);
// struct is only using primitives and pointers - lets try if it is only missing the declaration
@ -26,6 +27,7 @@ impl AsRef<Font> for SendFont {
}
}
#[derive(Debug)]
pub struct FontRenderer8x8 {
font: SendFont,
canvas: Mutex<Canvas>,

View file

@ -1,27 +1,21 @@
#![deny(clippy::all)]
use crate::font_renderer::FontRenderer8x8;
use crate::{
execute_command::{CommandExecutor, ExecutionResult},
gui::{AppEvents, Gui},
};
use crate::udp_server::UdpServer;
use crate::{command_executor::CommandExecutor, gui::Gui};
use clap::Parser;
use cli::Cli;
use log::{error, info, warn, LevelFilter};
use log::{info, LevelFilter};
use servicepoint::*;
use std::io::ErrorKind;
use std::net::UdpSocket;
use std::sync::{mpsc, RwLock};
use std::time::Duration;
use winit::event_loop::{ControlFlow, EventLoop, EventLoopProxy};
use winit::event_loop::{ControlFlow, EventLoop};
mod cli;
mod command_executor;
mod cp437_font;
mod execute_command;
mod font_renderer;
mod gui;
const BUF_SIZE: usize = 8985;
mod udp_server;
fn main() {
let mut cli = Cli::parse();
@ -32,68 +26,35 @@ fn main() {
init_logging(cli.verbose);
info!("starting with args: {:?}", &cli);
let socket = UdpSocket::bind(&cli.bind).expect("could not bind socket");
socket
.set_nonblocking(true)
.expect("could not enter non blocking mode");
let display = RwLock::new(Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT));
let luma = RwLock::new(BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT));
let (stop_udp_tx, stop_udp_rx) = mpsc::channel();
let mut gui = Gui::new(&display, &luma, stop_udp_tx, cli.gui);
let event_loop = EventLoop::with_user_event()
.build()
.expect("could not create event loop");
event_loop.set_control_flow(ControlFlow::Wait);
let event_proxy = event_loop.create_proxy();
let display = RwLock::new(Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT));
let luma = RwLock::new(BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT));
let (stop_udp_tx, stop_udp_rx) = mpsc::channel();
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);
let mut udp_server = UdpServer::new(
cli.bind,
stop_udp_rx,
command_executor,
event_loop.create_proxy(),
);
let mut gui = Gui::new(&display, &luma, stop_udp_tx, cli.gui);
std::thread::scope(move |scope| {
scope.spawn(move || {
let mut buf = [0; BUF_SIZE];
while stop_udp_rx.try_recv().is_err() {
receive_into_buf(&socket, &mut buf)
.and_then(move |amount| command_from_slice(&buf[..amount]))
.map(|cmd| {
handle_command(&event_proxy, &command_executor, cmd)
});
}
});
scope.spawn(move || udp_server.run());
event_loop
.run_app(&mut gui)
.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
@ -105,38 +66,3 @@ fn init_logging(debug: bool) {
.parse_default_env()
.init();
}
fn command_from_slice(slice: &[u8]) -> Option<Command> {
let packet = servicepoint::Packet::try_from(slice)
.inspect_err(|_| {
warn!("could not load packet with length {}", slice.len())
})
.ok()?;
Command::try_from(packet)
.inspect_err(move |err| {
warn!("could not read command for packet: {:?}", err)
})
.ok()
}
fn receive_into_buf(
socket: &UdpSocket,
buf: &mut [u8; BUF_SIZE],
) -> Option<usize> {
let (amount, _) = match socket.recv_from(buf) {
Err(err) if err.kind() == ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(1));
return None;
}
Ok(result) => result,
other => other.unwrap(),
};
if amount == buf.len() {
warn!(
"the received package may have been truncated to a length of {}",
amount
);
}
Some(amount)
}

102
src/udp_server.rs Normal file
View file

@ -0,0 +1,102 @@
use crate::command_executor::{CommandExecutor, ExecutionResult};
use crate::gui::AppEvents;
use log::{error, warn};
use servicepoint::Command;
use std::io::ErrorKind;
use std::net::UdpSocket;
use std::sync::mpsc::Receiver;
use std::time::Duration;
use winit::event_loop::EventLoopProxy;
const BUF_SIZE: usize = 8985;
#[derive(Debug)]
pub struct UdpServer<'t> {
socket: UdpSocket,
stop_rx: Receiver<()>,
command_executor: CommandExecutor<'t>,
app_events: EventLoopProxy<AppEvents>,
buf: [u8; BUF_SIZE],
}
impl<'t> UdpServer<'t> {
pub fn new(
bind: String,
stop_rx: Receiver<()>,
command_executor: CommandExecutor<'t>,
app_events: EventLoopProxy<AppEvents>,
) -> Self {
let socket = UdpSocket::bind(bind).expect("could not bind socket");
socket
.set_nonblocking(true)
.expect("could not enter non blocking mode");
Self {
socket,
stop_rx,
command_executor,
app_events,
buf: [0; BUF_SIZE],
}
}
pub(crate) fn run(&mut self) {
while self.stop_rx.try_recv().is_err() {
self.receive_into_buf()
.and_then(|amount| {
Self::command_from_slice(&self.buf[..amount])
})
.map(|cmd| self.handle_command(cmd));
}
}
fn command_from_slice(slice: &[u8]) -> Option<Command> {
let packet = servicepoint::Packet::try_from(slice)
.inspect_err(|_| {
warn!("could not load packet with length {}", slice.len())
})
.ok()?;
Command::try_from(packet)
.inspect_err(move |err| {
warn!("could not read command for packet: {:?}", err)
})
.ok()
}
fn receive_into_buf(&mut self) -> Option<usize> {
let (amount, _) = match self.socket.recv_from(&mut self.buf) {
Err(err) if err.kind() == ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(1));
return None;
}
Ok(result) => result,
other => other.unwrap(),
};
if amount == self.buf.len() {
warn!(
"the received package may have been truncated to a length of {}",
amount
);
}
Some(amount)
}
fn handle_command(&mut self, command: Command) {
match self.command_executor.execute(command) {
ExecutionResult::Success => {
self.app_events
.send_event(AppEvents::UdpPacketHandled)
.expect("could not send packet handled event");
}
ExecutionResult::Failure => {
error!("failed to execute command");
}
ExecutionResult::Shutdown => {
self.app_events
.send_event(AppEvents::UdpThreadClosed)
.expect("could not send close event");
}
}
}
}