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
All checks were successful
Rust / build (push) Successful in 9m12s
Reviewed-on: #3
This commit is contained in:
commit
0da50ed7dc
4
.github/workflows/rust.yml
vendored
4
.github/workflows/rust.yml
vendored
|
@ -26,10 +26,10 @@ jobs:
|
||||||
- name: Install rust toolchain
|
- name: Install rust toolchain
|
||||||
run: sudo apt-get install -qy cargo-1.80 rust-1.80-clippy
|
run: sudo apt-get install -qy cargo-1.80 rust-1.80-clippy
|
||||||
- name: Install system dependencies
|
- 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
|
- name: Run Clippy
|
||||||
run: cargo clippy --all-targets --all-features
|
run: cargo clippy --all-targets --all-features
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release --verbose
|
run: cargo build --release
|
||||||
|
|
531
Cargo.lock
generated
531
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "servicepoint-cli"
|
name = "servicepoint-cli"
|
||||||
description = "A command line interface for the ServicePoint display."
|
description = "A command line interface for the ServicePoint display."
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.80.0"
|
rust-version = "1.80.0"
|
||||||
publish = true
|
publish = true
|
||||||
|
@ -13,10 +13,20 @@ homepage = "https://crates.io/crates/servicepoint-cli"
|
||||||
keywords = ["cccb", "cccb-servicepoint", "cli"]
|
keywords = ["cccb", "cccb-servicepoint", "cli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
servicepoint = { version = "0.13.2", 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"
|
scap = "0.0.8"
|
||||||
image = "0.25.5"
|
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
|
||||||
|
|
62
README.md
62
README.md
|
@ -1,5 +1,11 @@
|
||||||
# servicepoint-cli
|
# servicepoint-cli
|
||||||
|
|
||||||
|
[](https://git.berlin.ccc.de/servicepoint/servicepoint-cli/releases)
|
||||||
|
[](https://crates.io/crates/servicepoint-cli)
|
||||||
|
[](https://crates.io/crates/servicepoint-cli)
|
||||||
|

|
||||||
|
[](https://git.berlin.ccc.de/servicepoint/servicepoint-cli)
|
||||||
|
|
||||||
This repository contains a command line interface for the ServicePoint display.
|
This repository contains a command line interface for the ServicePoint display.
|
||||||
|
|
||||||
To send commands, this uses the [servicepoint crate](https://crates.io/crates/servicepoint).
|
To send commands, this uses the [servicepoint crate](https://crates.io/crates/servicepoint).
|
||||||
|
@ -33,15 +39,15 @@ cargo run -- <args>
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```text
|
||||||
Usage: servicepoint-cli [OPTIONS] <COMMAND>
|
Usage: servicepoint-cli [OPTIONS] <COMMAND>
|
||||||
|
|
||||||
Commands:
|
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]
|
pixels Commands for manipulating pixels [aliases: p]
|
||||||
brightness Commands for manipulating the brightness [aliases: b]
|
brightness Commands for manipulating the brightness [aliases: b]
|
||||||
text Commands for sending text to the screen [aliases: t]
|
text Commands for sending text to the screen [aliases: t]
|
||||||
help Print this message or the help of the given subcommand(s)
|
help Print this message or the help of the given subcommand(s)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-d, --destination <DESTINATION> ip:port of the servicepoint display [default: 127.0.0.1:2342]
|
-d, --destination <DESTINATION> ip:port of the servicepoint display [default: 127.0.0.1:2342]
|
||||||
|
@ -53,7 +59,7 @@ Options:
|
||||||
|
|
||||||
### Pixels
|
### Pixels
|
||||||
|
|
||||||
```
|
```text
|
||||||
Commands for manipulating pixels
|
Commands for manipulating pixels
|
||||||
|
|
||||||
Usage: servicepoint-cli pixels <COMMAND>
|
Usage: servicepoint-cli pixels <COMMAND>
|
||||||
|
@ -63,12 +69,13 @@ Commands:
|
||||||
flip Invert the state of all pixels [aliases: f]
|
flip Invert the state of all pixels [aliases: f]
|
||||||
on Set all pixels to the on state
|
on Set all pixels to the on state
|
||||||
image Send an image file (e.g. jpeg or png) to the display. [aliases: i]
|
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]
|
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
|
#### Image
|
||||||
|
|
||||||
```
|
```text
|
||||||
Send an image file (e.g. jpeg or png) to the display.
|
Send an image file (e.g. jpeg or png) to the display.
|
||||||
|
|
||||||
Usage: servicepoint-cli pixels image [OPTIONS] <FILE_NAME>
|
Usage: servicepoint-cli pixels image [OPTIONS] <FILE_NAME>
|
||||||
|
@ -85,9 +92,28 @@ Options:
|
||||||
--no-aspect Do not keep aspect ratio when resizing.
|
--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
|
#### 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.
|
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]
|
Usage: servicepoint-cli pixels screen [OPTIONS]
|
||||||
|
@ -104,7 +130,7 @@ Options:
|
||||||
|
|
||||||
### Brightness
|
### Brightness
|
||||||
|
|
||||||
```
|
```text
|
||||||
Commands for manipulating the brightness
|
Commands for manipulating the brightness
|
||||||
|
|
||||||
Usage: servicepoint-cli brightness <COMMAND>
|
Usage: servicepoint-cli brightness <COMMAND>
|
||||||
|
@ -117,18 +143,18 @@ Commands:
|
||||||
|
|
||||||
### Text
|
### Text
|
||||||
|
|
||||||
```
|
```text
|
||||||
Commands for sending text to the screen
|
Commands for sending text to the screen
|
||||||
|
|
||||||
Usage: servicepoint-cli text <COMMAND>
|
Usage: servicepoint-cli text <COMMAND>
|
||||||
|
|
||||||
Commands:
|
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
|
#### Stdin
|
||||||
|
|
||||||
```
|
```text
|
||||||
Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`
|
Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`
|
||||||
|
|
||||||
Usage: servicepoint-cli stream stdin [OPTIONS]
|
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
|
-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
|
## Contributing
|
||||||
|
|
||||||
|
|
12
flake.lock
12
flake.lock
|
@ -7,11 +7,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1739824009,
|
"lastModified": 1745925850,
|
||||||
"narHash": "sha256-fcNrCMUWVLMG3gKC5M9CBqVOAnJtyRvGPxptQFl5mVg=",
|
"narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "naersk",
|
"repo": "naersk",
|
||||||
"rev": "e5130d37369bfa600144c2424270c96f0ef0e11d",
|
"rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -37,11 +37,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1740603184,
|
"lastModified": 1746183838,
|
||||||
"narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=",
|
"narHash": "sha256-kwaaguGkAqTZ1oK0yXeQ3ayYjs8u/W7eEfrFpFfIDFA=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49",
|
"rev": "bf3287dac860542719fe7554e21e686108716879",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -90,8 +90,7 @@
|
||||||
{
|
{
|
||||||
default = pkgs.mkShell rec {
|
default = pkgs.mkShell rec {
|
||||||
inputsFrom = [ self.packages.${system}.default ];
|
inputsFrom = [ self.packages.${system}.default ];
|
||||||
packages = [
|
packages = with pkgs; [
|
||||||
pkgs.gdb
|
|
||||||
(pkgs.symlinkJoin {
|
(pkgs.symlinkJoin {
|
||||||
name = "rust-toolchain";
|
name = "rust-toolchain";
|
||||||
paths = with pkgs; [
|
paths = with pkgs; [
|
||||||
|
@ -103,7 +102,11 @@
|
||||||
cargo-expand
|
cargo-expand
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
pkgs.cargo-flamegraph
|
|
||||||
|
cargo-flamegraph
|
||||||
|
gdb
|
||||||
|
|
||||||
|
ffmpeg-headless
|
||||||
];
|
];
|
||||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}";
|
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}";
|
||||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::cli::BrightnessCommand;
|
use crate::{cli::BrightnessCommand, transport::Transport};
|
||||||
use log::info;
|
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 {
|
match brightness_command {
|
||||||
BrightnessCommand::Max => brightness_set(connection, Brightness::MAX),
|
BrightnessCommand::Max => brightness_set(connection, Brightness::MAX),
|
||||||
BrightnessCommand::Min => brightness_set(connection, Brightness::MIN),
|
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
|
connection
|
||||||
.send(Command::Brightness(brightness))
|
.send_command(GlobalBrightnessCommand::from(brightness))
|
||||||
.expect("Failed to set brightness");
|
.expect("Failed to set brightness");
|
||||||
info!("set brightness to {brightness:?}");
|
info!("set brightness to {brightness:?}");
|
||||||
}
|
}
|
||||||
|
|
21
src/cli.rs
21
src/cli.rs
|
@ -19,7 +19,7 @@ pub struct Cli {
|
||||||
value_enum,
|
value_enum,
|
||||||
default_value = "udp"
|
default_value = "udp"
|
||||||
)]
|
)]
|
||||||
pub transport: Protocol,
|
pub transport: TransportType,
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
pub command: Mode,
|
pub command: Mode,
|
||||||
#[clap(short, long, help = "verbose logging")]
|
#[clap(short, long, help = "verbose logging")]
|
||||||
|
@ -29,7 +29,10 @@ pub struct Cli {
|
||||||
#[derive(clap::Parser, std::fmt::Debug)]
|
#[derive(clap::Parser, std::fmt::Debug)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
#[command(visible_alias = "r", about = "Reset both pixels and brightness")]
|
#[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")]
|
#[command(visible_alias = "p")]
|
||||||
Pixels {
|
Pixels {
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
|
@ -71,6 +74,16 @@ pub enum PixelCommand {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
image_processing_options: ImageProcessingOptions,
|
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(
|
#[command(
|
||||||
visible_alias = "s",
|
visible_alias = "s",
|
||||||
about = "Stream the default screen capture source to the display. \
|
about = "Stream the default screen capture source to the display. \
|
||||||
|
@ -104,7 +117,7 @@ pub enum BrightnessCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::ValueEnum, Clone, Debug)]
|
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||||
pub enum Protocol {
|
pub enum TransportType {
|
||||||
Udp,
|
Udp,
|
||||||
WebSocket,
|
WebSocket,
|
||||||
Fake,
|
Fake,
|
||||||
|
@ -114,7 +127,7 @@ pub enum Protocol {
|
||||||
#[clap(about = "Commands for sending text to the screen")]
|
#[clap(about = "Commands for sending text to the screen")]
|
||||||
pub enum TextCommand {
|
pub enum TextCommand {
|
||||||
#[command(
|
#[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 {
|
Stdin {
|
||||||
#[arg(
|
#[arg(
|
||||||
|
|
|
@ -35,6 +35,7 @@ impl ImageProcessingPipeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn process(&mut self, frame: DynamicImage) -> Bitmap {
|
pub fn process(&mut self, frame: DynamicImage) -> Bitmap {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
|
@ -100,7 +101,7 @@ impl ImageProcessingPipeline {
|
||||||
let result = if self.options.no_dither {
|
let result = if self.options.no_dither {
|
||||||
let cutoff = median_brightness(&orig);
|
let cutoff = median_brightness(&orig);
|
||||||
let bits = orig.iter().map(move |x| x > &cutoff).collect();
|
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 {
|
} else {
|
||||||
ostromoukhov_dither(orig, u8::MAX / 2)
|
ostromoukhov_dither(orig, u8::MAX / 2)
|
||||||
};
|
};
|
||||||
|
@ -113,7 +114,7 @@ impl ImageProcessingPipeline {
|
||||||
|
|
||||||
let width = source.width();
|
let width = source.width();
|
||||||
let result_height = Self::calc_height_without_spacers(source.height());
|
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;
|
let mut source_y = 0;
|
||||||
for result_y in 0..result_height {
|
for result_y in 0..result_height {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
//! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand
|
//! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand
|
||||||
|
|
||||||
use image::GrayImage;
|
use image::GrayImage;
|
||||||
use servicepoint::{BitVec, Bitmap, PIXEL_HEIGHT};
|
use log::debug;
|
||||||
|
use servicepoint::{Bitmap, DisplayBitVec, PIXEL_HEIGHT};
|
||||||
|
|
||||||
type GrayHistogram = [usize; 256];
|
type GrayHistogram = [usize; 256];
|
||||||
|
|
||||||
|
@ -169,10 +170,11 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
|
||||||
assert_eq!(width % 8, 0);
|
assert_eq!(width % 8, 0);
|
||||||
|
|
||||||
let mut source = source.into_raw();
|
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 {
|
for y in 0..height as usize {
|
||||||
let start = y * width as usize;
|
let start = y * width as usize;
|
||||||
|
let last_row = y == (height - 1) as usize;
|
||||||
if y % 2 == 0 {
|
if y % 2 == 0 {
|
||||||
for x in start..start + width as usize {
|
for x in start..start + width as usize {
|
||||||
ostromoukhov_dither_pixel(
|
ostromoukhov_dither_pixel(
|
||||||
|
@ -180,7 +182,7 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
|
||||||
&mut destination,
|
&mut destination,
|
||||||
x,
|
x,
|
||||||
width as usize,
|
width as usize,
|
||||||
y == (height - 1) as usize,
|
last_row,
|
||||||
1,
|
1,
|
||||||
bias,
|
bias,
|
||||||
);
|
);
|
||||||
|
@ -192,7 +194,7 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
|
||||||
&mut destination,
|
&mut destination,
|
||||||
x,
|
x,
|
||||||
width as usize,
|
width as usize,
|
||||||
y == (height - 1) as usize,
|
last_row,
|
||||||
-1,
|
-1,
|
||||||
bias,
|
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]
|
#[inline]
|
||||||
fn ostromoukhov_dither_pixel(
|
fn ostromoukhov_dither_pixel(
|
||||||
source: &mut [u8],
|
source: &mut [u8],
|
||||||
destination: &mut BitVec,
|
destination: &mut DisplayBitVec,
|
||||||
position: usize,
|
position: usize,
|
||||||
width: usize,
|
width: usize,
|
||||||
last_row: bool,
|
last_row: bool,
|
||||||
|
@ -217,8 +219,16 @@ fn ostromoukhov_dither_pixel(
|
||||||
destination.set(position, destination_value);
|
destination.set(position, destination_value);
|
||||||
|
|
||||||
let mut diffuse = |to: usize, mat: i16| {
|
let mut diffuse = |to: usize, mat: i16| {
|
||||||
let diffuse_value = source[to] as i16 + mat;
|
match source.get(to) {
|
||||||
source[to] = diffuse_value.clamp(u8::MIN.into(), u8::MAX.into()) as u8;
|
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 {
|
let lookup = if destination_value {
|
||||||
|
@ -229,11 +239,14 @@ fn ostromoukhov_dither_pixel(
|
||||||
diffuse((position as isize + direction) as usize, lookup[0]);
|
diffuse((position as isize + direction) as usize, lookup[0]);
|
||||||
|
|
||||||
if !last_row {
|
if !last_row {
|
||||||
|
debug!("begin");
|
||||||
diffuse(
|
diffuse(
|
||||||
((position + width) as isize - direction) as usize,
|
((position + width) as isize - direction) as usize,
|
||||||
lookup[1],
|
lookup[1],
|
||||||
);
|
);
|
||||||
|
debug!("mit");
|
||||||
diffuse(((position + width) as isize) as usize, lookup[2]);
|
diffuse(((position + width) as isize) as usize, lookup[2]);
|
||||||
|
debug!("end");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
41
src/main.rs
41
src/main.rs
|
@ -1,12 +1,13 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
brightness::{brightness, brightness_set},
|
brightness::{brightness, brightness_set},
|
||||||
cli::{Cli, Mode, Protocol},
|
cli::{Cli, Mode},
|
||||||
pixels::{pixels, pixels_off},
|
pixels::{pixels, pixels_off},
|
||||||
text::text
|
text::text,
|
||||||
|
transport::Transport,
|
||||||
};
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use servicepoint::{Brightness, Connection};
|
use servicepoint::{Brightness, HardResetCommand};
|
||||||
|
|
||||||
mod brightness;
|
mod brightness;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
@ -16,40 +17,32 @@ mod pixels;
|
||||||
mod stream_stdin;
|
mod stream_stdin;
|
||||||
mod stream_window;
|
mod stream_window;
|
||||||
mod text;
|
mod text;
|
||||||
|
mod transport;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
init_logging(cli.verbose);
|
init_logging(cli.verbose);
|
||||||
debug!("running with arguments: {:?}", cli);
|
debug!("running with arguments: {:?}", cli);
|
||||||
|
|
||||||
let connection = make_connection(cli.destination, cli.transport);
|
let transport = Transport::connect(cli.transport, &cli.destination);
|
||||||
debug!("connection established: {:#?}", connection);
|
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 {
|
match mode {
|
||||||
Mode::ResetEverything => {
|
Mode::Reset { force } => {
|
||||||
brightness_set(&connection, Brightness::MAX);
|
if force {
|
||||||
pixels_off(&connection);
|
connection.send_command(HardResetCommand).unwrap()
|
||||||
|
} else {
|
||||||
|
brightness_set(&connection, Brightness::MAX);
|
||||||
|
pixels_off(&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::Text { text_command} => text(&connection, text_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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
127
src/pixels.rs
127
src/pixels.rs
|
@ -1,12 +1,18 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
image_processing::ImageProcessingPipeline,
|
|
||||||
cli::{ImageProcessingOptions, PixelCommand, SendImageOptions},
|
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 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 {
|
match pixel_command {
|
||||||
PixelCommand::Off => pixels_off(connection),
|
PixelCommand::Off => pixels_off(connection),
|
||||||
PixelCommand::Flip => pixels_invert(connection),
|
PixelCommand::Flip => pixels_invert(connection),
|
||||||
|
@ -19,34 +25,50 @@ pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) {
|
||||||
stream_options,
|
stream_options,
|
||||||
image_processing,
|
image_processing,
|
||||||
} => stream_window(connection, 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) {
|
fn pixels_on(connection: &Transport) {
|
||||||
let mask = BitVec::repeat(true, PIXEL_COUNT);
|
let mask = DisplayBitVec::repeat(true, PIXEL_COUNT);
|
||||||
|
let command = BitVecCommand {
|
||||||
|
offset: 0,
|
||||||
|
bitvec: mask,
|
||||||
|
compression: CompressionCode::Lzma,
|
||||||
|
operation: BinaryOperation::Overwrite,
|
||||||
|
};
|
||||||
connection
|
connection
|
||||||
.send(Command::BitmapLinear(0, mask, CompressionCode::Lzma))
|
.send_command(command)
|
||||||
.expect("could not send command");
|
.expect("could not send command");
|
||||||
info!("turned on all pixels")
|
info!("turned on all pixels")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pixels_invert(connection: &Connection) {
|
fn pixels_invert(connection: &Transport) {
|
||||||
let mask = BitVec::repeat(true, PIXEL_COUNT);
|
let mask = DisplayBitVec::repeat(true, PIXEL_COUNT);
|
||||||
|
let command = BitVecCommand {
|
||||||
|
offset: 0,
|
||||||
|
bitvec: mask,
|
||||||
|
compression: CompressionCode::Lzma,
|
||||||
|
operation: BinaryOperation::Xor,
|
||||||
|
};
|
||||||
connection
|
connection
|
||||||
.send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma))
|
.send_command(command)
|
||||||
.expect("could not send command");
|
.expect("could not send command");
|
||||||
info!("inverted all pixels");
|
info!("inverted all pixels");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pixels_off(connection: &Connection) {
|
pub(crate) fn pixels_off(connection: &Transport) {
|
||||||
connection
|
connection
|
||||||
.send(Command::Clear)
|
.send_command(ClearCommand)
|
||||||
.expect("failed to clear pixels");
|
.expect("failed to clear pixels");
|
||||||
info!("reset pixels");
|
info!("reset pixels");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pixels_image(
|
fn pixels_image(
|
||||||
connection: &Connection,
|
connection: &Transport,
|
||||||
options: SendImageOptions,
|
options: SendImageOptions,
|
||||||
processing_options: ImageProcessingOptions,
|
processing_options: ImageProcessingOptions,
|
||||||
) {
|
) {
|
||||||
|
@ -54,11 +76,82 @@ fn pixels_image(
|
||||||
let mut pipeline = ImageProcessingPipeline::new(processing_options);
|
let mut pipeline = ImageProcessingPipeline::new(processing_options);
|
||||||
let bitmap = pipeline.process(image);
|
let bitmap = pipeline.process(image);
|
||||||
connection
|
connection
|
||||||
.send(Command::BitmapLinearWin(
|
.send_command(BitmapCommand {
|
||||||
Origin::ZERO,
|
origin: Origin::ZERO,
|
||||||
bitmap,
|
bitmap,
|
||||||
CompressionCode::default(),
|
compression: CompressionCode::default(),
|
||||||
))
|
})
|
||||||
.expect("failed to send image command");
|
.expect("failed to send image command");
|
||||||
info!("sent image to display");
|
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");
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
use crate::transport::Transport;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use servicepoint::*;
|
use servicepoint::*;
|
||||||
use std::thread::sleep;
|
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.");
|
warn!("This mode will break when using multi-byte characters and does not support ANSI escape sequences yet.");
|
||||||
let mut app = App {
|
let mut app = App {
|
||||||
connection,
|
connection,
|
||||||
|
@ -14,7 +15,7 @@ pub(crate) fn stream_stdin(connection: &Connection, slow: bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App<'t> {
|
struct App<'t> {
|
||||||
connection: &'t Connection,
|
connection: &'t Transport,
|
||||||
mirror: CharGrid,
|
mirror: CharGrid,
|
||||||
y: usize,
|
y: usize,
|
||||||
slow: bool,
|
slow: bool,
|
||||||
|
@ -23,7 +24,7 @@ struct App<'t> {
|
||||||
impl App<'_> {
|
impl App<'_> {
|
||||||
fn run(&mut self) {
|
fn run(&mut self) {
|
||||||
self.connection
|
self.connection
|
||||||
.send(Command::Clear)
|
.send_command(ClearCommand)
|
||||||
.expect("couldn't clear screen");
|
.expect("couldn't clear screen");
|
||||||
let last_y = self.mirror.height() - 1;
|
let last_y = self.mirror.height() - 1;
|
||||||
for line in std::io::stdin().lines() {
|
for line in std::io::stdin().lines() {
|
||||||
|
@ -63,10 +64,10 @@ impl App<'_> {
|
||||||
|
|
||||||
fn send_mirror(&self) {
|
fn send_mirror(&self) {
|
||||||
self.connection
|
self.connection
|
||||||
.send(Command::Utf8Data(
|
.send_command(CharGridCommand {
|
||||||
Origin::ZERO,
|
origin: Origin::ZERO,
|
||||||
self.mirror.clone(),
|
grid: self.mirror.clone(),
|
||||||
))
|
})
|
||||||
.expect("couldn't send screen to display");
|
.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 line_grid, 0, line);
|
||||||
Self::line_onto_grid(&mut self.mirror, self.y, line);
|
Self::line_onto_grid(&mut self.mirror, self.y, line);
|
||||||
self.connection
|
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");
|
.expect("couldn't send single line to screen");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{ImageProcessingOptions, StreamScreenOptions},
|
cli::{ImageProcessingOptions, StreamScreenOptions},
|
||||||
image_processing::ImageProcessingPipeline,
|
image_processing::ImageProcessingPipeline,
|
||||||
|
transport::Transport,
|
||||||
};
|
};
|
||||||
use image::{DynamicImage, ImageBuffer, Rgb, Rgba};
|
use image::{DynamicImage, ImageBuffer, Rgb, Rgba};
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
|
@ -9,11 +10,11 @@ use scap::{
|
||||||
frame::convert_bgra_to_rgb,
|
frame::convert_bgra_to_rgb,
|
||||||
frame::Frame,
|
frame::Frame,
|
||||||
};
|
};
|
||||||
use servicepoint::{Command, CompressionCode, Connection, Origin, FRAME_PACING};
|
use servicepoint::{BitmapCommand, CompressionCode, Origin, FRAME_PACING};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
pub fn stream_window(
|
pub fn stream_window(
|
||||||
connection: &Connection,
|
connection: &Transport,
|
||||||
options: StreamScreenOptions,
|
options: StreamScreenOptions,
|
||||||
processing_options: ImageProcessingOptions,
|
processing_options: ImageProcessingOptions,
|
||||||
) {
|
) {
|
||||||
|
@ -36,11 +37,11 @@ pub fn stream_window(
|
||||||
trace!("bitmap ready to send in: {:?}", start.elapsed());
|
trace!("bitmap ready to send in: {:?}", start.elapsed());
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send(Command::BitmapLinearWin(
|
.send_command(BitmapCommand {
|
||||||
Origin::ZERO,
|
origin: Origin::ZERO,
|
||||||
bitmap.clone(),
|
bitmap: bitmap.clone(),
|
||||||
CompressionCode::default(),
|
compression: CompressionCode::default(),
|
||||||
))
|
})
|
||||||
.expect("failed to send frame to display");
|
.expect("failed to send frame to display");
|
||||||
|
|
||||||
debug!("frame time: {:?}", start.elapsed());
|
debug!("frame time: {:?}", start.elapsed());
|
||||||
|
|
10
src/text.rs
10
src/text.rs
|
@ -1,7 +1,7 @@
|
||||||
use servicepoint::Connection;
|
use crate::{cli::TextCommand, stream_stdin::stream_stdin, transport::Transport};
|
||||||
use crate::cli::TextCommand;
|
|
||||||
use crate::stream_stdin::stream_stdin;
|
|
||||||
|
|
||||||
pub fn text(connection: &Connection, command: TextCommand) {
|
pub fn text(connection: &Transport, command: TextCommand) {
|
||||||
match command { TextCommand::Stdin { slow } => stream_stdin(connection, slow), }
|
match command {
|
||||||
|
TextCommand::Stdin { slow } => stream_stdin(connection, slow),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
51
src/transport.rs
Normal file
51
src/transport.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue