Merge pull request 'Stream video files, update servicepoint, fix dithering' (#3) from next-sp-ver into main
All checks were successful
Rust / build (push) Successful in 9m12s

Reviewed-on: #3
This commit is contained in:
vinzenz 2025-05-04 17:48:34 +02:00
commit 0da50ed7dc
16 changed files with 677 additions and 286 deletions

View file

@ -26,10 +26,10 @@ jobs:
- name: Install rust toolchain
run: sudo apt-get install -qy cargo-1.80 rust-1.80-clippy
- name: Install system dependencies
run: sudo apt-get install -qy liblzma-dev libpipewire-0.3-dev libclang-dev libdbus-1-dev
run: sudo apt-get install -qy liblzma-dev libpipewire-0.3-dev libclang-dev libdbus-1-dev ffmpeg libavutil-dev libavformat-dev libavfilter-dev libavdevice-dev
- name: Run Clippy
run: cargo clippy --all-targets --all-features
- name: Build
run: cargo build --release --verbose
run: cargo build --release

531
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package]
name = "servicepoint-cli"
description = "A command line interface for the ServicePoint display."
version = "0.3.0"
version = "0.4.0"
edition = "2021"
rust-version = "1.80.0"
publish = true
@ -13,10 +13,20 @@ homepage = "https://crates.io/crates/servicepoint-cli"
keywords = ["cccb", "cccb-servicepoint", "cli"]
[dependencies]
servicepoint = { version = "0.13.2", features = ["protocol_websocket"] }
clap = { version = "4.5", features = ["derive"] }
env_logger = "0.11"
log = "0.4"
scap = "0.0.8"
image = "0.25.5"
fast_image_resize = { version = "5.1.2", features = ["image"] }
fast_image_resize = { version = "5.1", features = ["image"] }
tungstenite = "0.26"
ffmpeg-next = "7.1.0"
[dependencies.servicepoint]
package = "servicepoint"
version = "0.14.1"
[profile.release]
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
strip = true # Strip symbols from binary

View file

@ -1,5 +1,11 @@
# servicepoint-cli
[![Release](https://git.berlin.ccc.de/servicepoint/servicepoint-cli/badges/release.svg)](https://git.berlin.ccc.de/servicepoint/servicepoint-cli/releases)
[![crates.io](https://img.shields.io/crates/v/servicepoint-cli.svg)](https://crates.io/crates/servicepoint-cli)
[![Crates.io Total Downloads](https://img.shields.io/crates/d/servicepoint-cli)](https://crates.io/crates/servicepoint-cli)
![GPLv3 licensed](https://img.shields.io/crates/l/servicepoint-cli)
[![CI](https://git.berlin.ccc.de/servicepoint/servicepoint-cli/badges/workflows/rust.yml/badge.svg)](https://git.berlin.ccc.de/servicepoint/servicepoint-cli)
This repository contains a command line interface for the ServicePoint display.
To send commands, this uses the [servicepoint crate](https://crates.io/crates/servicepoint).
@ -33,11 +39,11 @@ cargo run -- <args>
## Usage
```
```text
Usage: servicepoint-cli [OPTIONS] <COMMAND>
Commands:
reset-everything Reset both pixels and brightness [aliases: r]
reset Reset both pixels and brightness [aliases: r]
pixels Commands for manipulating pixels [aliases: p]
brightness Commands for manipulating the brightness [aliases: b]
text Commands for sending text to the screen [aliases: t]
@ -53,7 +59,7 @@ Options:
### Pixels
```
```text
Commands for manipulating pixels
Usage: servicepoint-cli pixels <COMMAND>
@ -63,12 +69,13 @@ Commands:
flip Invert the state of all pixels [aliases: f]
on Set all pixels to the on state
image Send an image file (e.g. jpeg or png) to the display. [aliases: i]
video Stream a video file (e.g. mp4) to the display. [aliases: v]
screen Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. [aliases: s]
```
#### Image
```
```text
Send an image file (e.g. jpeg or png) to the display.
Usage: servicepoint-cli pixels image [OPTIONS] <FILE_NAME>
@ -85,9 +92,28 @@ Options:
--no-aspect Do not keep aspect ratio when resizing.
```
#### Video file
```text
Stream a video file (e.g. mp4) to the display.
Usage: servicepoint-cli pixels video [OPTIONS] <FILE_NAME>
Arguments:
<FILE_NAME>
Options:
--no-hist Disable histogram correction
--no-blur Disable blur
--no-sharp Disable sharpening
--no-dither Disable dithering. Brightness will be adjusted so that around half of the pixels are on.
--no-spacers Do not remove the spacers from the image.
--no-aspect Do not keep aspect ratio when resizing.
```
#### Screen
```
```text
Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen.
Usage: servicepoint-cli pixels screen [OPTIONS]
@ -104,7 +130,7 @@ Options:
### Brightness
```
```text
Commands for manipulating the brightness
Usage: servicepoint-cli brightness <COMMAND>
@ -117,18 +143,18 @@ Commands:
### Text
```
```text
Commands for sending text to the screen
Usage: servicepoint-cli text <COMMAND>
Commands:
stdin Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`
stdin Pipe text to the display, example: `journalctl | servicepoint-cli text stdin`
```
#### Stdin
```
```text
Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`
Usage: servicepoint-cli stream stdin [OPTIONS]
@ -137,6 +163,16 @@ Options:
-s, --slow Wait for a short amount of time before sending the next line
```
### Reset
```text
Reset both pixels and brightness
Usage: servicepoint-cli reset [OPTIONS]
Options:
-f, --force hard reset screen
```
## Contributing

View file

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1739824009,
"narHash": "sha256-fcNrCMUWVLMG3gKC5M9CBqVOAnJtyRvGPxptQFl5mVg=",
"lastModified": 1745925850,
"narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
"owner": "nix-community",
"repo": "naersk",
"rev": "e5130d37369bfa600144c2424270c96f0ef0e11d",
"rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
"type": "github"
},
"original": {
@ -37,11 +37,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1740603184,
"narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=",
"lastModified": 1746183838,
"narHash": "sha256-kwaaguGkAqTZ1oK0yXeQ3ayYjs8u/W7eEfrFpFfIDFA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49",
"rev": "bf3287dac860542719fe7554e21e686108716879",
"type": "github"
},
"original": {

View file

@ -90,8 +90,7 @@
{
default = pkgs.mkShell rec {
inputsFrom = [ self.packages.${system}.default ];
packages = [
pkgs.gdb
packages = with pkgs; [
(pkgs.symlinkJoin {
name = "rust-toolchain";
paths = with pkgs; [
@ -103,7 +102,11 @@
cargo-expand
];
})
pkgs.cargo-flamegraph
cargo-flamegraph
gdb
ffmpeg-headless
];
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}";
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";

View file

@ -1,8 +1,8 @@
use crate::cli::BrightnessCommand;
use crate::{cli::BrightnessCommand, transport::Transport};
use log::info;
use servicepoint::{Brightness, Command, Connection};
use servicepoint::{Brightness, GlobalBrightnessCommand};
pub(crate) fn brightness(connection: &Connection, brightness_command: BrightnessCommand) {
pub(crate) fn brightness(connection: &Transport, brightness_command: BrightnessCommand) {
match brightness_command {
BrightnessCommand::Max => brightness_set(connection, Brightness::MAX),
BrightnessCommand::Min => brightness_set(connection, Brightness::MIN),
@ -12,9 +12,9 @@ pub(crate) fn brightness(connection: &Connection, brightness_command: Brightness
}
}
pub(crate) fn brightness_set(connection: &Connection, brightness: Brightness) {
pub(crate) fn brightness_set(connection: &Transport, brightness: Brightness) {
connection
.send(Command::Brightness(brightness))
.send_command(GlobalBrightnessCommand::from(brightness))
.expect("Failed to set brightness");
info!("set brightness to {brightness:?}");
}

View file

@ -19,7 +19,7 @@ pub struct Cli {
value_enum,
default_value = "udp"
)]
pub transport: Protocol,
pub transport: TransportType,
#[clap(subcommand)]
pub command: Mode,
#[clap(short, long, help = "verbose logging")]
@ -29,7 +29,10 @@ pub struct Cli {
#[derive(clap::Parser, std::fmt::Debug)]
pub enum Mode {
#[command(visible_alias = "r", about = "Reset both pixels and brightness")]
ResetEverything,
Reset {
#[arg(short, long, help = "hard reset screen")]
force: bool,
},
#[command(visible_alias = "p")]
Pixels {
#[clap(subcommand)]
@ -71,6 +74,16 @@ pub enum PixelCommand {
#[command(flatten)]
image_processing_options: ImageProcessingOptions,
},
#[command(
visible_alias = "v",
about = "Stream a video file (e.g. mp4) to the display."
)]
Video {
#[command(flatten)]
send_image_options: SendImageOptions,
#[command(flatten)]
image_processing_options: ImageProcessingOptions,
},
#[command(
visible_alias = "s",
about = "Stream the default screen capture source to the display. \
@ -104,7 +117,7 @@ pub enum BrightnessCommand {
}
#[derive(clap::ValueEnum, Clone, Debug)]
pub enum Protocol {
pub enum TransportType {
Udp,
WebSocket,
Fake,
@ -114,7 +127,7 @@ pub enum Protocol {
#[clap(about = "Commands for sending text to the screen")]
pub enum TextCommand {
#[command(
about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`"
about = "Pipe text to the display, example: `journalctl | servicepoint-cli text stdin`"
)]
Stdin {
#[arg(

View file

@ -35,6 +35,7 @@ impl ImageProcessingPipeline {
}
}
#[must_use]
pub fn process(&mut self, frame: DynamicImage) -> Bitmap {
let start_time = Instant::now();
@ -100,7 +101,7 @@ impl ImageProcessingPipeline {
let result = if self.options.no_dither {
let cutoff = median_brightness(&orig);
let bits = orig.iter().map(move |x| x > &cutoff).collect();
Bitmap::from_bitvec(orig.width() as usize, bits)
Bitmap::from_bitvec(orig.width() as usize, bits).unwrap()
} else {
ostromoukhov_dither(orig, u8::MAX / 2)
};
@ -113,7 +114,7 @@ impl ImageProcessingPipeline {
let width = source.width();
let result_height = Self::calc_height_without_spacers(source.height());
let mut result = Bitmap::new(width, result_height);
let mut result = Bitmap::new(width, result_height).unwrap();
let mut source_y = 0;
for result_y in 0..result_height {

View file

@ -1,7 +1,8 @@
//! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand
use image::GrayImage;
use servicepoint::{BitVec, Bitmap, PIXEL_HEIGHT};
use log::debug;
use servicepoint::{Bitmap, DisplayBitVec, PIXEL_HEIGHT};
type GrayHistogram = [usize; 256];
@ -169,10 +170,11 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
assert_eq!(width % 8, 0);
let mut source = source.into_raw();
let mut destination = BitVec::repeat(false, source.len());
let mut destination = DisplayBitVec::repeat(false, source.len());
for y in 0..height as usize {
let start = y * width as usize;
let last_row = y == (height - 1) as usize;
if y % 2 == 0 {
for x in start..start + width as usize {
ostromoukhov_dither_pixel(
@ -180,7 +182,7 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
&mut destination,
x,
width as usize,
y == (height - 1) as usize,
last_row,
1,
bias,
);
@ -192,7 +194,7 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
&mut destination,
x,
width as usize,
y == (height - 1) as usize,
last_row,
-1,
bias,
);
@ -200,13 +202,13 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
}
}
Bitmap::from_bitvec(width as usize, destination)
Bitmap::from_bitvec(width as usize, destination).unwrap()
}
#[inline]
fn ostromoukhov_dither_pixel(
source: &mut [u8],
destination: &mut BitVec,
destination: &mut DisplayBitVec,
position: usize,
width: usize,
last_row: bool,
@ -217,8 +219,16 @@ fn ostromoukhov_dither_pixel(
destination.set(position, destination_value);
let mut diffuse = |to: usize, mat: i16| {
let diffuse_value = source[to] as i16 + mat;
match source.get(to) {
None => {
// last row has a out of bounds error on the last pixel
// TODO fix the iter bounds instead of ignoring here
}
Some(val) => {
let diffuse_value = *val as i16 + mat;
source[to] = diffuse_value.clamp(u8::MIN.into(), u8::MAX.into()) as u8;
}
};
};
let lookup = if destination_value {
@ -229,11 +239,14 @@ fn ostromoukhov_dither_pixel(
diffuse((position as isize + direction) as usize, lookup[0]);
if !last_row {
debug!("begin");
diffuse(
((position + width) as isize - direction) as usize,
lookup[1],
);
debug!("mit");
diffuse(((position + width) as isize) as usize, lookup[2]);
debug!("end");
}
}

View file

@ -1,12 +1,13 @@
use crate::{
brightness::{brightness, brightness_set},
cli::{Cli, Mode, Protocol},
cli::{Cli, Mode},
pixels::{pixels, pixels_off},
text::text
text::text,
transport::Transport,
};
use clap::Parser;
use log::debug;
use servicepoint::{Brightness, Connection};
use servicepoint::{Brightness, HardResetCommand};
mod brightness;
mod cli;
@ -16,43 +17,35 @@ mod pixels;
mod stream_stdin;
mod stream_window;
mod text;
mod transport;
fn main() {
let cli = Cli::parse();
init_logging(cli.verbose);
debug!("running with arguments: {:?}", cli);
let connection = make_connection(cli.destination, cli.transport);
debug!("connection established: {:#?}", connection);
let transport = Transport::connect(cli.transport, &cli.destination);
debug!("connection established: {:#?}", transport);
execute_mode(cli.command, connection);
execute_mode(cli.command, transport);
}
pub fn execute_mode(mode: Mode, connection: Connection) {
pub fn execute_mode(mode: Mode, connection: Transport) {
match mode {
Mode::ResetEverything => {
Mode::Reset { force } => {
if force {
connection.send_command(HardResetCommand).unwrap()
} else {
brightness_set(&connection, Brightness::MAX);
pixels_off(&connection);
}
}
Mode::Pixels { pixel_command } => pixels(&connection, pixel_command),
Mode::Brightness { brightness_command } => brightness(&connection, brightness_command),
Mode::Text { text_command } => text(&connection, text_command),
}
}
fn make_connection(destination: String, transport: Protocol) -> Connection {
match transport {
Protocol::Udp => Connection::open(destination).expect("Failed to open UDP connection"),
Protocol::WebSocket => {
let url = destination.parse().expect(
"provided destination is not a valid url - make sure it starts with 'ws://'",
);
Connection::open_websocket(url).expect("Failed to open WebSocket connection")
}
Protocol::Fake => Connection::Fake,
}
}
fn init_logging(debug: bool) {
let filter = if debug {
log::LevelFilter::Debug

View file

@ -1,12 +1,18 @@
use crate::{
image_processing::ImageProcessingPipeline,
cli::{ImageProcessingOptions, PixelCommand, SendImageOptions},
stream_window::stream_window
image_processing::ImageProcessingPipeline,
stream_window::stream_window,
transport::Transport,
};
use ffmpeg_next as ffmpeg;
use image::{DynamicImage, RgbImage};
use log::info;
use servicepoint::{BitVec, Command, CompressionCode, Connection, Origin, PIXEL_COUNT};
use servicepoint::{
BinaryOperation, BitVecCommand, BitmapCommand, ClearCommand, CompressionCode, DisplayBitVec,
Origin, PIXEL_COUNT,
};
pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) {
pub(crate) fn pixels(connection: &Transport, pixel_command: PixelCommand) {
match pixel_command {
PixelCommand::Off => pixels_off(connection),
PixelCommand::Flip => pixels_invert(connection),
@ -19,34 +25,50 @@ pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) {
stream_options,
image_processing,
} => stream_window(connection, stream_options, image_processing),
PixelCommand::Video {
image_processing_options: processing_options,
send_image_options: image_options,
} => pixels_video(connection, image_options, processing_options),
}
}
fn pixels_on(connection: &Connection) {
let mask = BitVec::repeat(true, PIXEL_COUNT);
fn pixels_on(connection: &Transport) {
let mask = DisplayBitVec::repeat(true, PIXEL_COUNT);
let command = BitVecCommand {
offset: 0,
bitvec: mask,
compression: CompressionCode::Lzma,
operation: BinaryOperation::Overwrite,
};
connection
.send(Command::BitmapLinear(0, mask, CompressionCode::Lzma))
.send_command(command)
.expect("could not send command");
info!("turned on all pixels")
}
fn pixels_invert(connection: &Connection) {
let mask = BitVec::repeat(true, PIXEL_COUNT);
fn pixels_invert(connection: &Transport) {
let mask = DisplayBitVec::repeat(true, PIXEL_COUNT);
let command = BitVecCommand {
offset: 0,
bitvec: mask,
compression: CompressionCode::Lzma,
operation: BinaryOperation::Xor,
};
connection
.send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma))
.send_command(command)
.expect("could not send command");
info!("inverted all pixels");
}
pub(crate) fn pixels_off(connection: &Connection) {
pub(crate) fn pixels_off(connection: &Transport) {
connection
.send(Command::Clear)
.send_command(ClearCommand)
.expect("failed to clear pixels");
info!("reset pixels");
}
fn pixels_image(
connection: &Connection,
connection: &Transport,
options: SendImageOptions,
processing_options: ImageProcessingOptions,
) {
@ -54,11 +76,82 @@ fn pixels_image(
let mut pipeline = ImageProcessingPipeline::new(processing_options);
let bitmap = pipeline.process(image);
connection
.send(Command::BitmapLinearWin(
Origin::ZERO,
.send_command(BitmapCommand {
origin: Origin::ZERO,
bitmap,
CompressionCode::default(),
))
compression: CompressionCode::default(),
})
.expect("failed to send image command");
info!("sent image to display");
}
fn pixels_video(
connection: &Transport,
options: SendImageOptions,
processing_options: ImageProcessingOptions,
) {
ffmpeg::init().unwrap();
let mut ictx = ffmpeg::format::input(&options.file_name).expect("failed to open video input file");
let input = ictx
.streams()
.best(ffmpeg::media::Type::Video)
.ok_or(ffmpeg::Error::StreamNotFound)
.expect("could not get video stream from input file");
let video_stream_index = input.index();
let context_decoder = ffmpeg::codec::context::Context::from_parameters(input.parameters())
.expect("could not extract video context from parameters");
let mut decoder = context_decoder.decoder().video()
.expect("failed to create decoder for video stream");
let src_width = decoder.width();
let src_height = decoder.height();
let mut scaler = ffmpeg::software::scaling::Context::get(
decoder.format(),
src_width,
src_height,
ffmpeg::format::Pixel::RGB24,
src_width,
src_height,
ffmpeg::software::scaling::Flags::BILINEAR,
).expect("failed to create scaling context");
let mut frame_index = 0;
let mut processing_pipeline = ImageProcessingPipeline::new(processing_options);
let mut receive_and_process_decoded_frames =
|decoder: &mut ffmpeg::decoder::Video| -> Result<(), ffmpeg::Error> {
let mut decoded = ffmpeg::util::frame::video::Video::empty();
let mut rgb_frame = ffmpeg::util::frame::video::Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
scaler.run(&decoded, &mut rgb_frame)
.expect("failed to scale frame");
let image = RgbImage::from_raw(src_width, src_height, rgb_frame.data(0).to_owned())
.expect("could not read rgb data to image");
let image = DynamicImage::from(image);
let bitmap= processing_pipeline.process(image);
connection.send_command(BitmapCommand::from(bitmap))
.expect("failed to send image command");
frame_index += 1;
}
Ok(())
};
for (stream, packet) in ictx.packets() {
if stream.index() == video_stream_index {
decoder.send_packet(&packet)
.expect("failed to send video packet");
receive_and_process_decoded_frames(&mut decoder)
.expect("failed to process video packet");
}
}
decoder.send_eof().expect("failed to send eof");
receive_and_process_decoded_frames(&mut decoder)
.expect("failed to eof packet");
}

View file

@ -1,8 +1,9 @@
use crate::transport::Transport;
use log::warn;
use servicepoint::*;
use std::thread::sleep;
pub(crate) fn stream_stdin(connection: &Connection, slow: bool) {
pub(crate) fn stream_stdin(connection: &Transport, 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,
@ -14,7 +15,7 @@ pub(crate) fn stream_stdin(connection: &Connection, slow: bool) {
}
struct App<'t> {
connection: &'t Connection,
connection: &'t Transport,
mirror: CharGrid,
y: usize,
slow: bool,
@ -23,7 +24,7 @@ struct App<'t> {
impl App<'_> {
fn run(&mut self) {
self.connection
.send(Command::Clear)
.send_command(ClearCommand)
.expect("couldn't clear screen");
let last_y = self.mirror.height() - 1;
for line in std::io::stdin().lines() {
@ -63,10 +64,10 @@ impl App<'_> {
fn send_mirror(&self) {
self.connection
.send(Command::Utf8Data(
Origin::ZERO,
self.mirror.clone(),
))
.send_command(CharGridCommand {
origin: Origin::ZERO,
grid: self.mirror.clone(),
})
.expect("couldn't send screen to display");
}
@ -76,7 +77,10 @@ impl App<'_> {
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))
.send_command(CharGridCommand {
origin: Origin::new(0, self.y),
grid: line_grid,
})
.expect("couldn't send single line to screen");
}
}

View file

@ -1,6 +1,7 @@
use crate::{
cli::{ImageProcessingOptions, StreamScreenOptions},
image_processing::ImageProcessingPipeline,
transport::Transport,
};
use image::{DynamicImage, ImageBuffer, Rgb, Rgba};
use log::{debug, error, info, trace, warn};
@ -9,11 +10,11 @@ use scap::{
frame::convert_bgra_to_rgb,
frame::Frame,
};
use servicepoint::{Command, CompressionCode, Connection, Origin, FRAME_PACING};
use servicepoint::{BitmapCommand, CompressionCode, Origin, FRAME_PACING};
use std::time::{Duration, Instant};
pub fn stream_window(
connection: &Connection,
connection: &Transport,
options: StreamScreenOptions,
processing_options: ImageProcessingOptions,
) {
@ -36,11 +37,11 @@ pub fn stream_window(
trace!("bitmap ready to send in: {:?}", start.elapsed());
connection
.send(Command::BitmapLinearWin(
Origin::ZERO,
bitmap.clone(),
CompressionCode::default(),
))
.send_command(BitmapCommand {
origin: Origin::ZERO,
bitmap: bitmap.clone(),
compression: CompressionCode::default(),
})
.expect("failed to send frame to display");
debug!("frame time: {:?}", start.elapsed());

View file

@ -1,7 +1,7 @@
use servicepoint::Connection;
use crate::cli::TextCommand;
use crate::stream_stdin::stream_stdin;
use crate::{cli::TextCommand, stream_stdin::stream_stdin, transport::Transport};
pub fn text(connection: &Connection, command: TextCommand) {
match command { TextCommand::Stdin { slow } => stream_stdin(connection, slow), }
pub fn text(connection: &Transport, command: TextCommand) {
match command {
TextCommand::Stdin { slow } => stream_stdin(connection, slow),
}
}

51
src/transport.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::cli::TransportType;
use servicepoint::{FakeConnection, Packet, UdpSocketExt};
use std::fmt::Debug;
use std::net::{TcpStream, UdpSocket};
use std::sync::Mutex;
use tungstenite::client::IntoClientRequest;
use tungstenite::stream::MaybeTlsStream;
use tungstenite::{ClientRequestBuilder, WebSocket};
#[derive(Debug)]
pub enum Transport {
Fake,
Udp(UdpSocket),
WebSocket(Mutex<WebSocket<MaybeTlsStream<TcpStream>>>),
}
impl Transport {
pub fn connect(kind: TransportType, destination: &str) -> Transport {
match kind {
TransportType::Udp => {
Self::Udp(UdpSocket::bind_connect(destination).expect("failed to bind socket"))
}
TransportType::WebSocket => {
let request = ClientRequestBuilder::new(
destination.parse().expect("Invalid destination url"),
)
.into_client_request()
.unwrap();
let (sock, _) =
tungstenite::connect(request).expect("failed to connect to websocket");
Self::WebSocket(Mutex::new(sock))
}
TransportType::Fake => Self::Fake,
}
}
pub(crate) fn send_command<T: TryInto<Packet>>(&self, command: T) -> Option<()>
where
<T as TryInto<Packet>>::Error: Debug,
{
match self {
Self::Udp(socket) => socket.send_command(command),
Self::WebSocket(socket) => {
let bytes: Vec<u8> = command.try_into().unwrap().into();
let mut socket = socket.lock().unwrap();
socket.send(tungstenite::Message::Binary(bytes.into())).ok()
}
Self::Fake => FakeConnection.send_command(command),
}
}
}