Compare commits
3 commits
30b887dc03
...
31280abff6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
31280abff6 | ||
![]() |
ef19ab8b3f | ||
![]() |
9e299f69f6 |
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."
|
description = "A command line interface for the ServicePoint display."
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.78.0"
|
rust-version = "1.80.0"
|
||||||
publish = true
|
publish = true
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -14,6 +14,8 @@ keywords = ["cccb", "cccb-servicepoint", "cli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
servicepoint = { version = "0.13.0", features = ["protocol_websocket"] }
|
servicepoint = { version = "0.13.0", features = ["protocol_websocket"] }
|
||||||
clap = { version = "4.5", features = ["derive"]}
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
log = "0.4"
|
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>
|
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
|
## Contributing
|
||||||
|
|
||||||
If you have ideas on how to improve the code, add features or improve documentation feel free to open a pull request.
|
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.
|
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.
|
All creatures welcome.
|
||||||
|
|
||||||
|
|
23
flake.nix
23
flake.nix
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
description = "Flake for servicepoint-cli";
|
description = "Flake for command line interface of the ServicePoint display.";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
|
||||||
|
@ -55,12 +55,25 @@
|
||||||
};
|
};
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
pkg-config
|
pkg-config
|
||||||
|
libclang
|
||||||
|
rustPlatform.bindgenHook
|
||||||
];
|
];
|
||||||
strictDeps = true;
|
strictDeps = true;
|
||||||
buildInputs = with pkgs; [
|
buildInputs =
|
||||||
xe
|
with pkgs;
|
||||||
xz
|
[
|
||||||
];
|
xe
|
||||||
|
xz
|
||||||
|
clang
|
||||||
|
]
|
||||||
|
++ lib.optionals pkgs.stdenv.isLinux (
|
||||||
|
with pkgs;
|
||||||
|
[
|
||||||
|
dbus
|
||||||
|
pipewire
|
||||||
|
libclang
|
||||||
|
]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
default = servicepoint-cli;
|
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)]
|
#[derive(clap::Parser, std::fmt::Debug)]
|
||||||
#[clap(version, arg_required_else_help = true)]
|
#[clap(version, arg_required_else_help = true)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
|
@ -36,6 +38,11 @@ pub enum Mode {
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
brightness_command: BrightnessCommand,
|
brightness_command: BrightnessCommand,
|
||||||
},
|
},
|
||||||
|
#[command(visible_alias = "s")]
|
||||||
|
Stream {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
stream_command: StreamCommand,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser, std::fmt::Debug)]
|
#[derive(clap::Parser, std::fmt::Debug)]
|
||||||
|
@ -62,3 +69,15 @@ pub enum Protocol {
|
||||||
WebSocket,
|
WebSocket,
|
||||||
Fake,
|
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 log::info;
|
||||||
use servicepoint::{Brightness, Command, Connection};
|
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::Pixels { pixel_command } => pixels(&connection, pixel_command),
|
||||||
Mode::Brightness { brightness_command } => brightness(&connection, brightness_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 cli;
|
||||||
mod execute;
|
mod execute;
|
||||||
|
mod stream_stdin;
|
||||||
|
mod stream_window;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
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…
Reference in a new issue