Compare commits
	
		
			3 commits
		
	
	
		
			30b887dc03
			...
			31280abff6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 31280abff6 | ||
|   | ef19ab8b3f | ||
|   | 9e299f69f6 | 
					 9 changed files with 1929 additions and 12 deletions
				
			
		
							
								
								
									
										1626
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1626
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -3,7 +3,7 @@ name = "servicepoint-cli" | |||
| description = "A command line interface for the ServicePoint display." | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| rust-version = "1.78.0" | ||||
| rust-version = "1.80.0" | ||||
| publish = true | ||||
| resolver = "2" | ||||
| readme = "README.md" | ||||
|  | @ -14,6 +14,8 @@ keywords = ["cccb", "cccb-servicepoint", "cli"] | |||
| 
 | ||||
| [dependencies] | ||||
| servicepoint = { version = "0.13.0", features = ["protocol_websocket"] } | ||||
| clap = { version = "4.5", features = ["derive"]} | ||||
| clap = { version = "4.5", features = ["derive"] } | ||||
| env_logger = "0.11" | ||||
| log = "0.4" | ||||
| scap = "0.0.8" | ||||
| image = "0.25.5" | ||||
|  |  | |||
							
								
								
									
										64
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										64
									
								
								README.md
									
										
									
									
									
								
							|  | @ -31,13 +31,75 @@ cd servicepoint-cli | |||
| cargo run -- <args> | ||||
| ``` | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| ``` | ||||
| Usage: servicepoint-cli [OPTIONS] <COMMAND> | ||||
| 
 | ||||
| Commands: | ||||
|   reset-everything  [aliases: r] | ||||
|   pixels            [aliases: p] | ||||
|   brightness        [aliases: b] | ||||
|   stream            [aliases: s] | ||||
|   help              Print this message or the help of the given subcommand(s) | ||||
| 
 | ||||
| Options: | ||||
|   -d, --destination <DESTINATION>  ip:port of the servicepoint display [default: 127.0.0.1:2342] | ||||
|   -t, --transport <TRANSPORT>      protocol to use for communication with display [default: udp] [possible values: udp, web-socket, fake] | ||||
|   -v, --verbose                    verbose logging | ||||
|   -h, --help                       Print help | ||||
|   -V, --version                    Print version | ||||
| ``` | ||||
| 
 | ||||
| ### Stream | ||||
| 
 | ||||
| ``` | ||||
| Usage: servicepoint-cli stream <COMMAND> | ||||
| 
 | ||||
| Commands: | ||||
|   stdin    | ||||
|   screen   | ||||
|   help    Print this message or the help of the given subcommand(s) | ||||
| 
 | ||||
| Options: | ||||
|   -h, --help  Print help | ||||
| ``` | ||||
| 
 | ||||
| ### Brightness | ||||
| 
 | ||||
| ``` | ||||
| Usage: servicepoint-cli brightness <COMMAND> | ||||
| 
 | ||||
| Commands: | ||||
|   reset  [aliases: r] | ||||
|   set    [aliases: s] | ||||
|   min     | ||||
|   max     | ||||
|   help   Print this message or the help of the given subcommand(s) | ||||
| 
 | ||||
| Options: | ||||
|   -h, --help  Print help | ||||
| ``` | ||||
| 
 | ||||
| Pixels subcommands: | ||||
| ``` | ||||
| Usage: servicepoint-cli pixels <COMMAND> | ||||
| 
 | ||||
| Commands: | ||||
|   reset  [aliases: r] | ||||
|   help   Print this message or the help of the given subcommand(s) | ||||
| 
 | ||||
| Options: | ||||
|   -h, --help  Print help | ||||
| ``` | ||||
| 
 | ||||
| ## Contributing | ||||
| 
 | ||||
| If you have ideas on how to improve the code, add features or improve documentation feel free to open a pull request. | ||||
| 
 | ||||
| You think you found a bug? Please open an issue. | ||||
| 
 | ||||
| Submissions on Forgejo are preferred, but you can also use GitHub.  | ||||
| Submissions on [Forgejo](https://git.berlin.ccc.de/servicepoint/servicepoint-cli) are preferred, but you can also use [GitHub](https://github.com/kaesaecracker/servicepoint-cli).  | ||||
| 
 | ||||
| All creatures welcome. | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										19
									
								
								flake.nix
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								flake.nix
									
										
									
									
									
								
							|  | @ -1,5 +1,5 @@ | |||
| { | ||||
|   description = "Flake for servicepoint-cli"; | ||||
|   description = "Flake for command line interface of the ServicePoint display."; | ||||
| 
 | ||||
|   inputs = { | ||||
|     nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; | ||||
|  | @ -55,12 +55,25 @@ | |||
|             }; | ||||
|             nativeBuildInputs = with pkgs; [ | ||||
|               pkg-config | ||||
|               libclang | ||||
|               rustPlatform.bindgenHook | ||||
|             ]; | ||||
|             strictDeps = true; | ||||
|             buildInputs = with pkgs; [ | ||||
|             buildInputs = | ||||
|               with pkgs; | ||||
|               [ | ||||
|                 xe | ||||
|                 xz | ||||
|             ]; | ||||
|                 clang | ||||
|               ] | ||||
|               ++ lib.optionals pkgs.stdenv.isLinux ( | ||||
|                 with pkgs; | ||||
|                 [ | ||||
|                   dbus | ||||
|                   pipewire | ||||
|                   libclang | ||||
|                 ] | ||||
|               ); | ||||
|           }; | ||||
| 
 | ||||
|           default = servicepoint-cli; | ||||
|  |  | |||
							
								
								
									
										19
									
								
								src/cli.rs
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								src/cli.rs
									
										
									
									
									
								
							|  | @ -1,3 +1,5 @@ | |||
| use crate::stream_window::StreamScreenOptions; | ||||
| 
 | ||||
| #[derive(clap::Parser, std::fmt::Debug)] | ||||
| #[clap(version, arg_required_else_help = true)] | ||||
| pub struct Cli { | ||||
|  | @ -36,6 +38,11 @@ pub enum Mode { | |||
|         #[clap(subcommand)] | ||||
|         brightness_command: BrightnessCommand, | ||||
|     }, | ||||
|     #[command(visible_alias = "s")] | ||||
|     Stream { | ||||
|         #[clap(subcommand)] | ||||
|         stream_command: StreamCommand, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(clap::Parser, std::fmt::Debug)] | ||||
|  | @ -62,3 +69,15 @@ pub enum Protocol { | |||
|     WebSocket, | ||||
|     Fake, | ||||
| } | ||||
| 
 | ||||
| #[derive(clap::Parser, std::fmt::Debug)] | ||||
| pub enum StreamCommand { | ||||
|     Stdin { | ||||
|         #[arg(long, short, default_value_t = false)] | ||||
|         slow: bool, | ||||
|     }, | ||||
|     Screen { | ||||
|         #[command(flatten)] | ||||
|         options: StreamScreenOptions, | ||||
|     }, | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,6 @@ | |||
| use crate::cli::{BrightnessCommand, Mode, PixelCommand}; | ||||
| use crate::cli::{BrightnessCommand, Mode, PixelCommand, StreamCommand}; | ||||
| use crate::stream_stdin::stream_stdin; | ||||
| use crate::stream_window::stream_window; | ||||
| use log::info; | ||||
| use servicepoint::{Brightness, Command, Connection}; | ||||
| 
 | ||||
|  | @ -10,6 +12,10 @@ pub fn execute_mode(mode: Mode, connection: Connection) { | |||
|         } | ||||
|         Mode::Pixels { pixel_command } => pixels(&connection, pixel_command), | ||||
|         Mode::Brightness { brightness_command } => brightness(&connection, brightness_command), | ||||
|         Mode::Stream { stream_command } => match stream_command { | ||||
|             StreamCommand::Stdin { slow } => stream_stdin(&connection, slow), | ||||
|             StreamCommand::Screen { options } => stream_window(&connection, options), | ||||
|         }, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ use servicepoint::Connection; | |||
| 
 | ||||
| mod cli; | ||||
| mod execute; | ||||
| mod stream_stdin; | ||||
| mod stream_window; | ||||
| 
 | ||||
| fn main() { | ||||
|     let cli = Cli::parse(); | ||||
|  |  | |||
							
								
								
									
										81
									
								
								src/stream_stdin.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/stream_stdin.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | |||
| use log::warn; | ||||
| use servicepoint::*; | ||||
| use std::thread::sleep; | ||||
| 
 | ||||
| pub(crate) fn stream_stdin(connection: &Connection, slow: bool) { | ||||
|     warn!("This mode will break when using multi-byte characters and does not support ANSI escape sequences yet."); | ||||
|     let mut app = App { | ||||
|         connection, | ||||
|         mirror: CharGrid::new(TILE_WIDTH, TILE_HEIGHT), | ||||
|         y: 0, | ||||
|         slow, | ||||
|     }; | ||||
|     app.run() | ||||
| } | ||||
| 
 | ||||
| struct App<'t> { | ||||
|     connection: &'t Connection, | ||||
|     mirror: CharGrid, | ||||
|     y: usize, | ||||
|     slow: bool, | ||||
| } | ||||
| 
 | ||||
| impl<'t> App<'t> { | ||||
|     fn run(&mut self) { | ||||
|         self.connection | ||||
|             .send(Command::Clear) | ||||
|             .expect("couldn't clear screen"); | ||||
|         let last_y = self.mirror.height() - 1; | ||||
|         for line in std::io::stdin().lines() { | ||||
|             let line = line.expect("could not read from stdin"); | ||||
| 
 | ||||
|             if self.y <= last_y { | ||||
|                 self.single_line(&line); | ||||
|                 self.y += 1; | ||||
|             } else { | ||||
|                 self.shift_rows(); | ||||
|                 Self::line_onto_grid(&mut self.mirror, last_y, &line); | ||||
|                 self.send_mirror() | ||||
|                 // we stay on last y
 | ||||
|             } | ||||
| 
 | ||||
|             if self.slow { | ||||
|                 sleep(FRAME_PACING); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn shift_rows(&mut self) { | ||||
|         let data = self.mirror.data_ref_mut(); | ||||
|         data.rotate_left(TILE_WIDTH); | ||||
|         if let Some(row) = data.last_chunk_mut::<TILE_WIDTH>() { | ||||
|             row.fill(' ') | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn line_onto_grid(grid: &mut CharGrid, y: usize, line: &str) { | ||||
|         for (x, char) in line.chars().enumerate() { | ||||
|             if x < grid.width() { | ||||
|                 grid.set(x, y, char); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn send_mirror(&self) { | ||||
|         self.connection | ||||
|             .send(Command::Cp437Data( | ||||
|                 Origin::ZERO, | ||||
|                 Cp437Grid::from(&self.mirror), | ||||
|             )) | ||||
|             .expect("couldn't send screen to display"); | ||||
|     } | ||||
| 
 | ||||
|     fn single_line(&mut self, line: &str) { | ||||
|         let mut line_grid = CharGrid::new(TILE_WIDTH, 1); | ||||
|         Self::line_onto_grid(&mut line_grid, 0, line); | ||||
|         Self::line_onto_grid(&mut self.mirror, self.y, line); | ||||
|         self.connection | ||||
|             .send(Command::Utf8Data(Origin::new(0, self.y), line_grid)) | ||||
|             .expect("couldn't send single line to screen"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										112
									
								
								src/stream_window.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/stream_window.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| use image::{ | ||||
|     imageops::{dither, resize, BiLevel, FilterType}, | ||||
|     DynamicImage, ImageBuffer, Rgb, Rgba, | ||||
| }; | ||||
| use log::{error, warn}; | ||||
| use scap::{ | ||||
|     capturer::{Capturer, Options}, | ||||
|     frame::convert_bgra_to_rgb, | ||||
|     frame::Frame, | ||||
| }; | ||||
| use servicepoint::{ | ||||
|     Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, | ||||
| }; | ||||
| use std::time::Duration; | ||||
| 
 | ||||
| #[derive(clap::Parser, std::fmt::Debug, Clone)] | ||||
| pub struct StreamScreenOptions { | ||||
|     #[arg(long, short, default_value_t = false)] | ||||
|     pub no_dither: bool, | ||||
| } | ||||
| 
 | ||||
| pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { | ||||
|     let capturer = match start_capture() { | ||||
|         Some(value) => value, | ||||
|         None => return, | ||||
|     }; | ||||
| 
 | ||||
|     let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT); | ||||
|     loop { | ||||
|         let frame = capturer.get_next_frame().expect("failed to capture frame"); | ||||
|         let frame = frame_to_image(frame); | ||||
|         let frame = frame.grayscale().to_luma8(); | ||||
|         let mut frame = resize( | ||||
|             &frame, | ||||
|             PIXEL_WIDTH as u32, | ||||
|             PIXEL_HEIGHT as u32, | ||||
|             FilterType::Nearest, | ||||
|         ); | ||||
| 
 | ||||
|         if !options.no_dither { | ||||
|             dither(&mut frame, &BiLevel); | ||||
|         } | ||||
| 
 | ||||
|         for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) { | ||||
|             *dest = src.0[0] > u8::MAX / 2; | ||||
|         } | ||||
| 
 | ||||
|         connection | ||||
|             .send(Command::BitmapLinearWin( | ||||
|                 Origin::ZERO, | ||||
|                 bitmap.clone(), | ||||
|                 CompressionCode::Uncompressed, | ||||
|             )) | ||||
|             .expect("failed to send frame to display"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn start_capture() -> Option<Capturer> { | ||||
|     if !scap::is_supported() { | ||||
|         error!("platform not supported by scap"); | ||||
|         return None; | ||||
|     } | ||||
| 
 | ||||
|     if !scap::has_permission() { | ||||
|         warn!("requesting screen recording permission"); | ||||
|         if !scap::request_permission() { | ||||
|             error!("screen recording ermission denied"); | ||||
|             return None; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     let mut capturer = Capturer::build(Options { | ||||
|         fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32, | ||||
|         target: None, | ||||
|         show_cursor: true, | ||||
|         show_highlight: true, | ||||
|         excluded_targets: None, | ||||
|         output_type: scap::frame::FrameType::BGR0, | ||||
|         ..Default::default() | ||||
|     }) | ||||
|     .expect("failed to create screen capture"); | ||||
|     capturer.start_capture(); | ||||
|     Some(capturer) | ||||
| } | ||||
| 
 | ||||
| fn frame_to_image(frame: Frame) -> DynamicImage { | ||||
|     match frame { | ||||
|         Frame::BGRx(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data), | ||||
|         Frame::RGBx(frame) => DynamicImage::from( | ||||
|             ImageBuffer::<Rgba<_>, _>::from_raw( | ||||
|                 frame.width as u32, | ||||
|                 frame.height as u32, | ||||
|                 frame.data, | ||||
|             ) | ||||
|             .unwrap(), | ||||
|         ), | ||||
|         Frame::BGR0(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data), | ||||
|         Frame::RGB(frame) => DynamicImage::from( | ||||
|             ImageBuffer::<Rgb<_>, _>::from_raw(frame.width as u32, frame.height as u32, frame.data) | ||||
|                 .unwrap(), | ||||
|         ), | ||||
|         Frame::BGRA(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data), | ||||
|         Frame::YUVFrame(_) | Frame::XBGR(_) => panic!("unsupported frame format"), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn bgrx_to_rgb(width: i32, height: i32, data: Vec<u8>) -> DynamicImage { | ||||
|     DynamicImage::from( | ||||
|         ImageBuffer::<Rgb<_>, _>::from_raw(width as u32, height as u32, convert_bgra_to_rgb(data)) | ||||
|             .unwrap(), | ||||
|     ) | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue