Compare commits

..

No commits in common. "next" and "main" have entirely different histories.
next ... main

8 changed files with 224 additions and 246 deletions

3
Cargo.lock generated
View file

@ -1440,7 +1440,8 @@ dependencies = [
[[package]] [[package]]
name = "servicepoint" name = "servicepoint"
version = "0.13.2" version = "0.13.2"
source = "git+https://git.berlin.ccc.de/servicepoint/servicepoint/?branch=next#300bb5d6474f0f6152ab04feed4478995fcb3ec8" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33abd53582a995aaf5d387be4a1f7eb294a084185f88f8cf61652b6272041660"
dependencies = [ dependencies = [
"bitvec", "bitvec",
"bzip2", "bzip2",

View file

@ -20,7 +20,7 @@ clap = { version = "4.5", features = ["derive"] }
thiserror = "2.0" thiserror = "2.0"
# package parsing # package parsing
servicepoint = { features = ["all_compressions"], git = "https://git.berlin.ccc.de/servicepoint/servicepoint/", branch = "next" } servicepoint = { version = "0.13.2", features = ["all_compressions"] }
# font rendering # font rendering
font-kit = "0.14.2" font-kit = "0.14.2"

View file

@ -17,10 +17,6 @@ Use cases:
Uses the [servicepoint](https://github.com/cccb/servicepoint) library for reading the packets. Uses the [servicepoint](https://github.com/cccb/servicepoint) library for reading the packets.
The screenshot above shows the output of two example projects running in parallel (game_of_life and random_brightness). The screenshot above shows the output of two example projects running in parallel (game_of_life and random_brightness).
This repository moved
to [git.berlin.ccc.de/servicepoint/servicepoint-simulator](https://git.berlin.ccc.de/servicepoint/servicepoint-simulator/).
The [GitHub repository](https://github.com/kaesaecracker/servicepoint-simulator) will remain as a mirror.
## Running ## Running
With cargo installed: `cargo install servicepoint-simulator` With cargo installed: `cargo install servicepoint-simulator`
@ -48,8 +44,7 @@ Options:
See [env_logger](https://docs.rs/env_logger/latest/env_logger/) to configure logging. See [env_logger](https://docs.rs/env_logger/latest/env_logger/) to configure logging.
Because this program renders to an RGB pixel buffer, you can enjoy the following additional features not available on Because this program renders to an RGB pixel buffer, you can enjoy the following additional features not available on the real display:
the real display:
- enable or disable the empty space between tile rows (`./servicepoint-simulator --spacers` to enable) - enable or disable the empty space between tile rows (`./servicepoint-simulator --spacers` to enable)
- render pixels in red, green, blue or a combination of the three (`./servicepoint-simulator -rgb` for white pixels) - render pixels in red, green, blue or a combination of the three (`./servicepoint-simulator -rgb` for white pixels)
@ -69,7 +64,6 @@ All creatures welcome.
## Legal stuff ## Legal stuff
The included font is https://int10h.org/oldschool-pc-fonts/fontlist/font?ibm_bios (included in the download The included font is https://int10h.org/oldschool-pc-fonts/fontlist/font?ibm_bios (included in the download from https://int10h.org/oldschool-pc-fonts/download/). The font is CC BY-SA 4.0.
from https://int10h.org/oldschool-pc-fonts/download/). The font is CC BY-SA 4.0.
For everything else see the LICENSE file. For everything else see the LICENSE file.

View file

@ -1,22 +1,16 @@
use crate::{ use crate::command_executor::ExecutionResult::{Failure, Shutdown, Success};
command_executor::ExecutionResult::{Failure, Shutdown, Success}, use crate::cp437_font::Cp437Font;
cp437_font::Cp437Font, use crate::font_renderer::FontRenderer8x8;
font_renderer::FontRenderer8x8,
};
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use servicepoint::{ use servicepoint::{
BinaryOperation, BitVecCommand, Bitmap, BitmapCommand, BrightnessCommand, BitVec, Bitmap, BrightnessGrid, CharGrid, Command, Cp437Grid, Grid, Offset,
BrightnessGrid, BrightnessGridCommand, CharGridCommand, ClearCommand, Origin, Tiles, PIXEL_COUNT, PIXEL_WIDTH, TILE_SIZE,
CompressionCode, Cp437GridCommand, FadeOutCommand, Grid, HardResetCommand,
Origin, TypedCommand, PIXEL_COUNT, PIXEL_WIDTH, TILE_SIZE,
};
use std::{
ops::{BitAnd, BitOr, BitXor},
sync::RwLock,
}; };
use std::ops::{BitAnd, BitOr, BitXor};
use std::sync::RwLock;
#[derive(Debug)] #[derive(Debug)]
pub struct CommandExecutionContext<'t> { 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,
@ -30,36 +24,201 @@ pub enum ExecutionResult {
Shutdown, Shutdown,
} }
pub trait CommandExecute { impl<'t> CommandExecutor<'t> {
fn execute(&self, context: &CommandExecutionContext) -> ExecutionResult; pub fn new(
} display: &'t RwLock<Bitmap>,
luma: &'t RwLock<BrightnessGrid>,
font_renderer: FontRenderer8x8,
) -> Self {
CommandExecutor {
display,
luma,
font_renderer,
cp437_font: Cp437Font::default(),
}
}
impl CommandExecute for ClearCommand { pub(crate) fn execute(&self, command: Command) -> ExecutionResult {
fn execute(&self, context: &CommandExecutionContext) -> ExecutionResult { debug!("received {command:?}");
match command {
Command::Clear => {
info!("clearing display"); info!("clearing display");
context.display.write().unwrap().fill(false); self.display.write().unwrap().fill(false);
Success 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)
}
}
}
impl CommandExecute for BitmapCommand { fn execute_char_brightness(
fn execute(&self, context: &CommandExecutionContext) -> ExecutionResult { &self,
let Self { origin: Origin<Tiles>,
origin: grid: BrightnessGrid,
Origin { ) -> ExecutionResult {
x: offset_x, let mut luma = self.luma.write().unwrap();
y: offset_y, for inner_y in 0..grid.height() {
.. for inner_x in 0..grid.width() {
}, let brightness = grid.get(inner_x, inner_y);
bitmap: pixels, luma.set(origin.x + inner_x, origin.y + inner_y, brightness);
.. }
} = self; }
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!( debug!(
"printing {}x{} grid at {offset_x} {offset_y}", "printing {}x{} grid at {offset_x} {offset_y}",
pixels.width(), pixels.width(),
pixels.height() pixels.height()
); );
let mut display = context.display.write().unwrap(); let mut display = self.display.write().unwrap();
for inner_y in 0..pixels.height() { for inner_y in 0..pixels.height() {
for inner_x in 0..pixels.width() { for inner_x in 0..pixels.width() {
let is_set = pixels.get(inner_x, inner_y); let is_set = pixels.get(inner_x, inner_y);
@ -77,186 +236,12 @@ impl CommandExecute for BitmapCommand {
Success Success
} }
}
impl CommandExecute for HardResetCommand { fn get_coordinates_for_index(
fn execute(&self, _: &CommandExecutionContext) -> ExecutionResult { offset: usize,
warn!("display shutting down"); index: usize,
Shutdown ) -> (usize, usize) {
} let pixel_index = offset + index;
} (pixel_index % PIXEL_WIDTH, pixel_index / PIXEL_WIDTH)
impl CommandExecute for BitVecCommand {
fn execute(&self, context: &CommandExecutionContext) -> ExecutionResult {
let BitVecCommand {
offset,
bitvec,
operation,
..
} = self;
fn overwrite(_: bool, new: bool) -> bool {
new
}
let operation = match operation {
BinaryOperation::Overwrite => overwrite,
BinaryOperation::And => BitAnd::bitand,
BinaryOperation::Or => BitOr::bitor,
BinaryOperation::Xor => BitXor::bitxor,
};
if self.offset + bitvec.len() > PIXEL_COUNT {
error!(
"bitmap with offset {offset} is too big ({} bytes)",
bitvec.len()
);
return Failure;
}
let mut display = context.display.write().unwrap();
for bitmap_index in 0..bitvec.len() {
let pixel_index = offset + bitmap_index;
let (x, y) = (pixel_index % PIXEL_WIDTH, pixel_index / PIXEL_WIDTH);
let old_value = display.get(x, y);
display.set(x, y, operation(old_value, bitvec[bitmap_index]));
}
Success
}
}
impl CommandExecute for Cp437GridCommand {
fn execute(&self, context: &CommandExecutionContext) -> ExecutionResult {
let Cp437GridCommand { origin, grid } = self;
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;
let execute_result = BitmapCommand {
origin: Origin::new(tile_x * TILE_SIZE, tile_y * TILE_SIZE),
bitmap: context.cp437_font[char_code].clone(),
compression: CompressionCode::default(),
}
.execute(context);
match execute_result {
Success => {}
Failure => {
error!(
"stopping drawing text because char draw failed"
);
return Failure;
}
Shutdown => return Shutdown,
}
}
}
Success
}
}
#[allow(deprecated)]
impl CommandExecute for servicepoint::BitmapLegacyCommand {
fn execute(&self, _: &CommandExecutionContext) -> ExecutionResult {
warn!("ignoring deprecated command {:?}", self);
Failure
}
}
impl CommandExecute for BrightnessGridCommand {
fn execute(&self, context: &CommandExecutionContext) -> ExecutionResult {
let BrightnessGridCommand { origin, grid } = self;
let mut luma = context.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
}
}
impl CommandExecute for CharGridCommand {
fn execute(&self, context: &CommandExecutionContext) -> ExecutionResult {
let CharGridCommand { origin, grid } = self;
let mut display = context.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) = context.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
}
}
impl CommandExecute for BrightnessCommand {
fn execute(&self, context: &CommandExecutionContext) -> ExecutionResult {
context.luma.write().unwrap().fill(self.brightness);
Success
}
}
impl CommandExecute for FadeOutCommand {
fn execute(&self, _: &CommandExecutionContext) -> ExecutionResult {
error!("command not implemented: {self:?}");
Success
}
}
impl CommandExecute for TypedCommand {
fn execute(&self, context: &CommandExecutionContext) -> ExecutionResult {
match self {
TypedCommand::Clear(command) => command.execute(context),
TypedCommand::HardReset(command) => command.execute(context),
TypedCommand::Bitmap(command) => command.execute(context),
TypedCommand::Cp437Grid(command) => command.execute(context),
#[allow(deprecated)]
TypedCommand::BitmapLegacy(command) => command.execute(context),
TypedCommand::BitVec(command) => command.execute(context),
TypedCommand::BrightnessGrid(command) => command.execute(context),
TypedCommand::Brightness(command) => command.execute(context),
TypedCommand::FadeOut(command) => command.execute(context),
TypedCommand::CharGrid(command) => command.execute(context),
}
}
}
impl<'t> CommandExecutionContext<'t> {
pub fn new(
display: &'t RwLock<Bitmap>,
luma: &'t RwLock<BrightnessGrid>,
font_renderer: FontRenderer8x8,
) -> Self {
CommandExecutionContext {
display,
luma,
font_renderer,
cp437_font: Cp437Font::default(),
}
} }
} }

View file

@ -17,7 +17,7 @@ impl Cp437Font {
impl Default for Cp437Font { impl Default for Cp437Font {
fn default() -> Self { fn default() -> Self {
let mut bitmaps = let mut bitmaps =
core::array::from_fn(|_| Bitmap::new(TILE_SIZE, TILE_SIZE).unwrap()); core::array::from_fn(|_| Bitmap::new(TILE_SIZE, TILE_SIZE));
for (char_code, bitmap) in bitmaps.iter_mut().enumerate() { for (char_code, bitmap) in bitmaps.iter_mut().enumerate() {
let bits = CP437_FONT_LINEAR[char_code]; let bits = CP437_FONT_LINEAR[char_code];

View file

@ -1,6 +1,7 @@
use std::{sync::mpsc::Sender, sync::RwLock};
use log::{info, warn}; use log::{info, warn};
use servicepoint::*; use servicepoint::*;
use std::{sync::mpsc::Sender, sync::RwLock};
use winit::{ use winit::{
application::ApplicationHandler, dpi::LogicalSize, event::WindowEvent, application::ApplicationHandler, dpi::LogicalSize, event::WindowEvent,
event_loop::ActiveEventLoop, keyboard::KeyCode::KeyC, window::WindowId, event_loop::ActiveEventLoop, keyboard::KeyCode::KeyC, window::WindowId,

View file

@ -2,7 +2,7 @@
use crate::font_renderer::FontRenderer8x8; use crate::font_renderer::FontRenderer8x8;
use crate::udp_server::UdpServer; use crate::udp_server::UdpServer;
use crate::{command_executor::CommandExecutionContext, gui::Gui}; use crate::{command_executor::CommandExecutor, gui::Gui};
use clap::Parser; use clap::Parser;
use cli::Cli; use cli::Cli;
use log::{info, LevelFilter}; use log::{info, LevelFilter};
@ -32,14 +32,14 @@ fn main() {
.expect("could not create event loop"); .expect("could not create event loop");
event_loop.set_control_flow(ControlFlow::Wait); event_loop.set_control_flow(ControlFlow::Wait);
let display = RwLock::new(Bitmap::max_sized()); let display = RwLock::new(Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT));
let luma = RwLock::new(BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT)); let luma = RwLock::new(BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT));
let (stop_udp_tx, stop_udp_rx) = mpsc::channel(); let (stop_udp_tx, stop_udp_rx) = mpsc::channel();
let font_renderer = cli let font_renderer = cli
.font .font
.map(FontRenderer8x8::from_name) .map(FontRenderer8x8::from_name)
.unwrap_or_else(FontRenderer8x8::default); .unwrap_or_else(FontRenderer8x8::default);
let command_executor = CommandExecutionContext::new(&display, &luma, font_renderer); let command_executor = CommandExecutor::new(&display, &luma, font_renderer);
let mut udp_server = UdpServer::new( let mut udp_server = UdpServer::new(
cli.bind, cli.bind,
stop_udp_rx, stop_udp_rx,

View file

@ -1,13 +1,11 @@
use crate::command_executor::CommandExecute; use crate::command_executor::{CommandExecutor, ExecutionResult};
use crate::{ use crate::gui::AppEvents;
command_executor::{CommandExecutionContext, ExecutionResult}, use log::{error, warn};
gui::AppEvents, use servicepoint::Command;
}; use std::io::ErrorKind;
use log::{debug, error, warn}; use std::net::UdpSocket;
use servicepoint::TypedCommand; use std::sync::mpsc::Receiver;
use std::{ use std::time::Duration;
io::ErrorKind, net::UdpSocket, sync::mpsc::Receiver, time::Duration,
};
use winit::event_loop::EventLoopProxy; use winit::event_loop::EventLoopProxy;
const BUF_SIZE: usize = 8985; const BUF_SIZE: usize = 8985;
@ -16,7 +14,7 @@ const BUF_SIZE: usize = 8985;
pub struct UdpServer<'t> { pub struct UdpServer<'t> {
socket: UdpSocket, socket: UdpSocket,
stop_rx: Receiver<()>, stop_rx: Receiver<()>,
command_executor: CommandExecutionContext<'t>, command_executor: CommandExecutor<'t>,
app_events: EventLoopProxy<AppEvents>, app_events: EventLoopProxy<AppEvents>,
buf: [u8; BUF_SIZE], buf: [u8; BUF_SIZE],
} }
@ -25,7 +23,7 @@ impl<'t> UdpServer<'t> {
pub fn new( pub fn new(
bind: String, bind: String,
stop_rx: Receiver<()>, stop_rx: Receiver<()>,
command_executor: CommandExecutionContext<'t>, command_executor: CommandExecutor<'t>,
app_events: EventLoopProxy<AppEvents>, app_events: EventLoopProxy<AppEvents>,
) -> Self { ) -> Self {
let socket = UdpSocket::bind(bind).expect("could not bind socket"); let socket = UdpSocket::bind(bind).expect("could not bind socket");
@ -47,8 +45,7 @@ impl<'t> UdpServer<'t> {
if let Some(cmd) = self.receive_into_buf().and_then(|amount| { if let Some(cmd) = self.receive_into_buf().and_then(|amount| {
Self::command_from_slice(&self.buf[..amount]) Self::command_from_slice(&self.buf[..amount])
}) { }) {
debug!("received {cmd:?}"); match self.command_executor.execute(cmd) {
match cmd.execute(&self.command_executor) {
ExecutionResult::Success => { ExecutionResult::Success => {
self.app_events self.app_events
.send_event(AppEvents::UdpPacketHandled) .send_event(AppEvents::UdpPacketHandled)
@ -68,13 +65,13 @@ impl<'t> UdpServer<'t> {
} }
} }
fn command_from_slice(slice: &[u8]) -> Option<TypedCommand> { fn command_from_slice(slice: &[u8]) -> Option<Command> {
let packet = servicepoint::Packet::try_from(slice) let packet = servicepoint::Packet::try_from(slice)
.inspect_err(|_| { .inspect_err(|_| {
warn!("could not load packet with length {}", slice.len()) warn!("could not load packet with length {}", slice.len())
}) })
.ok()?; .ok()?;
TypedCommand::try_from(packet) Command::try_from(packet)
.inspect_err(move |err| { .inspect_err(move |err| {
warn!("could not read command for packet: {:?}", err) warn!("could not read command for packet: {:?}", err)
}) })