Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
|
ee7d0b96c6 | ||
|
bac744d506 | ||
|
1777b5e39f | ||
|
cf5e0f9386 | ||
|
94c69b85cc | ||
|
3f003075ef | ||
|
c41b9eb69c | ||
|
c5880964e7 | ||
|
66aa62de98 | ||
|
27ce4bceed | ||
|
3ab0b3f35e | ||
|
1507bc6bac | ||
|
af87f9166f | ||
|
5ef49cc2c2 | ||
|
39c62db5f5 | ||
|
95f628ce44 | ||
|
f00e981a3c | ||
|
fcd782140e | ||
|
b54626ea84 | ||
|
6488402700 | ||
|
3a16194a5f | ||
|
1d3196c56e | ||
|
64580dcda7 | ||
|
3562bca493 | ||
|
d3851d1e56 |
10
.gitignore
vendored
|
@ -1,4 +1,6 @@
|
||||||
/target
|
target
|
||||||
settings.toml
|
settings.toml
|
||||||
.direnv/*
|
.direnv/*
|
||||||
logs/*
|
logs/*
|
||||||
|
ui/static/node_modules/*
|
||||||
|
ui/static/@tauri-apps/*
|
||||||
|
|
4438
Cargo.lock
generated
41
Cargo.toml
|
@ -4,29 +4,40 @@ version = "2.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1.5.1", features = [] }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
tracker-state-debug = []
|
tokio-debug = []
|
||||||
tokio-logging = []
|
tokio-logging = []
|
||||||
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
|
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||||
|
# DO NOT REMOVE!!
|
||||||
|
custom-protocol = [ "tauri/custom-protocol" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-channel = "2.2.0"
|
async-channel = "2.3.1"
|
||||||
|
bincode = "1.3.3"
|
||||||
config = "0.14.0"
|
config = "0.14.0"
|
||||||
|
futures = "0.3.30"
|
||||||
futures-core = "0.3.30"
|
futures-core = "0.3.30"
|
||||||
futures-util = { version = "0.3.30", features = ["tokio-io"] }
|
futures-util = { version = "0.3.30" }
|
||||||
gilrs = "0.10.6"
|
gilrs = "0.11.0"
|
||||||
gstreamer = { version = "0.22.4", features = ["v1_22"] }
|
log = "0.4.22"
|
||||||
gstreamer-app = { version = "0.22.0", features = ["v1_22"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
gst-plugin-gtk4 = { version = "0.12.2", features = ["gtk_v4_12"] }
|
serde_json = "1.0"
|
||||||
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
|
tokio = { version = "1.40", features = ["rt-multi-thread", "time", "sync"] }
|
||||||
log = "0.4.21"
|
tokio-tungstenite = "0.24.0"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
toml = "0.8.19"
|
||||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "time", "sync"] }
|
|
||||||
tokio-tungstenite = "0.21.0"
|
|
||||||
toml = "0.8.12"
|
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["tracing-log"] }
|
tracing-subscriber = { version = "0.3.18", features = ["tracing-log"] }
|
||||||
tracing-appender = "0.2.3"
|
tracing-appender = "0.2.3"
|
||||||
snafu = "0.8.2"
|
snafu = "0.8.4"
|
||||||
console-subscriber = "0.3.0"
|
console-subscriber = "0.3.0"
|
||||||
async-recursion = "1.1.1"
|
tauri = { version = "1.8", features = [] }
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
|
||||||
|
vcs-common = { git = "https://git.nickiel.net/VCC/vcs-common.git", branch = "main" }
|
||||||
|
|
||||||
|
|
||||||
|
|
3
build.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build();
|
||||||
|
}
|
52
flake.lock
|
@ -1,30 +1,12 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1705309234,
|
|
||||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1711163522,
|
"lastModified": 1725103162,
|
||||||
"narHash": "sha256-YN/Ciidm+A0fmJPWlHBGvVkcarYWSC+s3NTPk/P+q3c=",
|
"narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "44d0940ea560dee511026a53f0e2e2cde489b4d4",
|
"rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -36,11 +18,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1706487304,
|
"lastModified": 1718428119,
|
||||||
"narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=",
|
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "90f456026d284c22b3e3497be980b2e47d0b28ac",
|
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -58,15 +40,14 @@
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"rust-overlay": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1711332768,
|
"lastModified": 1725330199,
|
||||||
"narHash": "sha256-SFnlIwnrwJxEawLcrH7+zGb8spePcYyai5asMZnm0BM=",
|
"narHash": "sha256-oUkdPJIxP3r3YyVOBLkDVLIJiQV9YlrVqA+jNcdpCvM=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "8a8e3ea9a9a4b2225cb5e33e07c3a337f820168c",
|
"rev": "a562172c72d00350f9f2ff830e6515b6e7bee6d5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -74,21 +55,6 @@
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|
37
flake.nix
|
@ -36,13 +36,9 @@ Some utility commands:
|
||||||
src = ./.;
|
src = ./.;
|
||||||
nativeBuildInputs = [ pkg-config ];
|
nativeBuildInputs = [ pkg-config ];
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
|
tailwindcss
|
||||||
openssl
|
openssl
|
||||||
systemd
|
systemd
|
||||||
gtk4
|
|
||||||
gst_all_1.gstreamer
|
|
||||||
gst_all_1.gst-plugins-base
|
|
||||||
gst_all_1.gst-plugins-good
|
|
||||||
gst_all_1.gst-plugins-bad
|
|
||||||
];
|
];
|
||||||
cargoHash = nixpkgs.lib.fakeHash;
|
cargoHash = nixpkgs.lib.fakeHash;
|
||||||
};
|
};
|
||||||
|
@ -52,16 +48,44 @@ Some utility commands:
|
||||||
platforms = [ system ];
|
platforms = [ system ];
|
||||||
#maintainers = with maintainers; [ ];
|
#maintainers = with maintainers; [ ];
|
||||||
};
|
};
|
||||||
|
libraries = with pkgs;[
|
||||||
|
gtk3
|
||||||
|
glib
|
||||||
|
dbus
|
||||||
|
openssl_3
|
||||||
|
librsvg
|
||||||
|
];
|
||||||
|
|
||||||
|
tauri_packages = with pkgs; [
|
||||||
|
curl
|
||||||
|
wget
|
||||||
|
pkg-config
|
||||||
|
dbus
|
||||||
|
openssl_3
|
||||||
|
glib
|
||||||
|
gtk3
|
||||||
|
libsoup
|
||||||
|
webkitgtk
|
||||||
|
librsvg
|
||||||
|
];
|
||||||
in {
|
in {
|
||||||
devShells.${system}.default = with pkgs; mkShell {
|
devShells.${system}.default = with pkgs; mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
(pkgs.rust-bin.stable.latest.default.override {
|
(pkgs.rust-bin.stable.latest.default.override {
|
||||||
extensions = [ "rust-src" ];
|
extensions = [ "rust-src" ];
|
||||||
})
|
})
|
||||||
|
cargo-watch
|
||||||
|
cargo-tauri
|
||||||
cargo-edit
|
cargo-edit
|
||||||
bacon
|
bacon
|
||||||
];
|
typescript # .js language server
|
||||||
|
] ++ tauri_packages;
|
||||||
inputsFrom = with self.packages.${system}; [ joystick-controller-client ];
|
inputsFrom = with self.packages.${system}; [ joystick-controller-client ];
|
||||||
|
shellHook =
|
||||||
|
''
|
||||||
|
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
|
||||||
|
export XDG_DATA_DIRS=${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
packages.${system} = {
|
packages.${system} = {
|
||||||
default = self.packages.${system}.joystick-controller-client;
|
default = self.packages.${system}.joystick-controller-client;
|
||||||
|
@ -117,3 +141,4 @@ Some utility commands:
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
icons/128x128.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
icons/32x32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
icons/icon.icns
Normal file
BIN
icons/icon.ico
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
icons/icon.png
Normal file
After Width: | Height: | Size: 49 KiB |
|
@ -1,6 +0,0 @@
|
||||||
camera_ip = "10.0.0.33"
|
|
||||||
camera_port = 8765
|
|
||||||
tracker_ip = "localhost"
|
|
||||||
tracker_port = 6543
|
|
||||||
tracker_refresh_rate_millis = 10
|
|
||||||
tracker_jpeg_quality = 80
|
|
|
@ -5,10 +5,20 @@ use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ConnectionString {
|
||||||
|
pub ip: String,
|
||||||
|
pub port: u32,
|
||||||
|
}
|
||||||
|
impl ConnectionString {
|
||||||
|
pub fn build_conn_string(&self) -> String {
|
||||||
|
format!("ws://{}:{}", self.ip, self.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub camera_ip: String,
|
pub cameras: Vec<ConnectionString>,
|
||||||
pub camera_port: u32,
|
|
||||||
|
|
||||||
pub tracker_ip: String,
|
pub tracker_ip: String,
|
||||||
pub tracker_port: u32,
|
pub tracker_port: u32,
|
||||||
|
@ -19,8 +29,10 @@ pub struct AppConfig {
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
AppConfig {
|
AppConfig {
|
||||||
camera_ip: "10.0.0.33".to_string(),
|
cameras: vec![ConnectionString {
|
||||||
camera_port: 8765,
|
ip: "127.0.0.1".to_owned(),
|
||||||
|
port: 8765,
|
||||||
|
}],
|
||||||
|
|
||||||
tracker_ip: "10.0.0.210".to_string(),
|
tracker_ip: "10.0.0.210".to_string(),
|
||||||
tracker_port: 6543,
|
tracker_port: 6543,
|
||||||
|
|
|
@ -1,230 +0,0 @@
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::{
|
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
|
||||||
};
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use async_channel::{Receiver, Sender};
|
|
||||||
use futures_util::{stream::SplitSink, SinkExt, StreamExt};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio::runtime::Handle;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
|
|
||||||
use tracing::{debug, error, info, instrument};
|
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
|
||||||
use crate::coordinator::socket_listen;
|
|
||||||
use crate::coordinator::tracker_state::TrackerState;
|
|
||||||
use crate::gstreamer_pipeline;
|
|
||||||
use crate::{sources::joystick_source::joystick_loop, ui::GuiUpdate};
|
|
||||||
|
|
||||||
use super::perf_state::TrackerMetrics;
|
|
||||||
use super::remote_video_processor::remote_video_loop;
|
|
||||||
use super::{ApplicationEvent, ConnectionType};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SocketState {
|
|
||||||
pub is_connected: AtomicBool,
|
|
||||||
pub stay_connected: AtomicBool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CoordState<'a> {
|
|
||||||
pub settings: Arc<RwLock<AppConfig>>,
|
|
||||||
pub tracker_metrics: TrackerMetrics,
|
|
||||||
|
|
||||||
pub sck_outbound: Option<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>,
|
|
||||||
pub stay_alive_sck_recvr: Arc<AtomicBool>,
|
|
||||||
pub joystick_loop_alive: Arc<AtomicBool>,
|
|
||||||
|
|
||||||
pub current_priority: ConnectionType,
|
|
||||||
pub last_update_of_priority: Instant,
|
|
||||||
|
|
||||||
pub mec: Pin<&'a mut Receiver<ApplicationEvent>>,
|
|
||||||
pub to_mec: Sender<ApplicationEvent>,
|
|
||||||
pub to_gui: Sender<GuiUpdate>,
|
|
||||||
pub rt: Handle,
|
|
||||||
|
|
||||||
pub pipeline: gstreamer_pipeline::WebcamPipeline,
|
|
||||||
|
|
||||||
pub tracker_state: TrackerState,
|
|
||||||
pub tracker_connection_state: Arc<SocketState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> CoordState<'a> {
|
|
||||||
pub fn new(
|
|
||||||
mec: Pin<&'a mut Receiver<ApplicationEvent>>,
|
|
||||||
to_mec: Sender<ApplicationEvent>,
|
|
||||||
to_gui: Sender<GuiUpdate>,
|
|
||||||
rt: Handle,
|
|
||||||
settings: Arc<RwLock<AppConfig>>,
|
|
||||||
jpeg_quality: i32,
|
|
||||||
) -> Self {
|
|
||||||
CoordState {
|
|
||||||
settings,
|
|
||||||
tracker_metrics: TrackerMetrics::new(to_gui.clone()),
|
|
||||||
|
|
||||||
sck_outbound: None,
|
|
||||||
stay_alive_sck_recvr: Arc::new(AtomicBool::new(false)),
|
|
||||||
joystick_loop_alive: Arc::new(AtomicBool::new(false)),
|
|
||||||
|
|
||||||
current_priority: ConnectionType::Local,
|
|
||||||
last_update_of_priority: Instant::now(),
|
|
||||||
|
|
||||||
mec,
|
|
||||||
to_mec,
|
|
||||||
to_gui,
|
|
||||||
rt,
|
|
||||||
|
|
||||||
pipeline: gstreamer_pipeline::WebcamPipeline::new(jpeg_quality).unwrap(),
|
|
||||||
|
|
||||||
tracker_state: TrackerState{
|
|
||||||
tracking_id: 0,
|
|
||||||
highlighted_id: None,
|
|
||||||
last_detect: Instant::now(),
|
|
||||||
enabled: true,
|
|
||||||
|
|
||||||
identity_boxes: vec![],
|
|
||||||
update_ids: false,
|
|
||||||
},
|
|
||||||
tracker_connection_state: Arc::new(SocketState {
|
|
||||||
stay_connected: AtomicBool::new(false),
|
|
||||||
is_connected: AtomicBool::new(false),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
pub async fn socket_send(&mut self, message: Message) {
|
|
||||||
if let Some(mut socket) = self.sck_outbound.take() {
|
|
||||||
if let Err(e) = socket.send(message).await {
|
|
||||||
error!("There was an error sending to the socket: {:#?}", e);
|
|
||||||
} else {
|
|
||||||
self.sck_outbound = Some(socket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn socket_connected(&self) -> bool {
|
|
||||||
self.sck_outbound.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn socket_start(&mut self) {
|
|
||||||
self.stay_alive_sck_recvr.store(true, Ordering::SeqCst);
|
|
||||||
|
|
||||||
if let Err(e) = self.to_gui.send(GuiUpdate::SocketConnecting).await {
|
|
||||||
error!("Cannot send message to gui thread: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let conn_string: String = {
|
|
||||||
let read_settings = self.settings.read().await;
|
|
||||||
|
|
||||||
format!(
|
|
||||||
"ws://{}:{}",
|
|
||||||
read_settings.camera_ip,
|
|
||||||
read_settings.camera_port
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
match connect_async(conn_string).await {
|
|
||||||
Ok((val, _)) => {
|
|
||||||
info!("Socket connection to camera made successfully");
|
|
||||||
|
|
||||||
let (outbound, inbound) = val.split();
|
|
||||||
self.rt.spawn(socket_listen(
|
|
||||||
self.to_mec.clone(),
|
|
||||||
self.stay_alive_sck_recvr.clone(),
|
|
||||||
inbound,
|
|
||||||
));
|
|
||||||
self.sck_outbound = Some(outbound);
|
|
||||||
|
|
||||||
if let Err(e) = self.to_gui.send(GuiUpdate::SocketConnected).await {
|
|
||||||
error!("Cannot send message to gui thread: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
error!("Couldn't connect to URL!");
|
|
||||||
if let Err(e) = self.to_gui.send(GuiUpdate::SocketDisconnected).await {
|
|
||||||
error!("Cannot send message to gui thread: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn socket_close(&mut self) {
|
|
||||||
debug!("Cleaning up camera socket state");
|
|
||||||
|
|
||||||
if let Some(mut socket) = self.sck_outbound.take() {
|
|
||||||
if let Err(e) = socket.close().await {
|
|
||||||
error!("Couldn't close socket during shutdown: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = self.to_gui.send(GuiUpdate::SocketDisconnected).await {
|
|
||||||
error!("Cannot send message to gui thread: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.stay_alive_sck_recvr.store(false, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start_video_loop(&mut self) {
|
|
||||||
let conn_string: String = {
|
|
||||||
let read_settings = self.settings.read().await;
|
|
||||||
|
|
||||||
format!(
|
|
||||||
"ws://{}:{}",
|
|
||||||
read_settings.tracker_ip,
|
|
||||||
read_settings.tracker_port
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
self.rt.spawn(remote_video_loop(
|
|
||||||
conn_string,
|
|
||||||
self.pipeline.sink_frame.clone(),
|
|
||||||
self.to_mec.clone(),
|
|
||||||
self.tracker_connection_state.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_states(&mut self) {
|
|
||||||
// This one needs to always be alive, and restart after a crash
|
|
||||||
if !self.joystick_loop_alive.load(Ordering::SeqCst) {
|
|
||||||
info!("Restarting joystick loop");
|
|
||||||
self.rt.spawn(joystick_loop(
|
|
||||||
self.to_mec.clone(),
|
|
||||||
self.joystick_loop_alive.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If tracker state is not connected, and it should be
|
|
||||||
if !self
|
|
||||||
.tracker_connection_state
|
|
||||||
.is_connected
|
|
||||||
.load(Ordering::SeqCst)
|
|
||||||
&& self
|
|
||||||
.tracker_connection_state
|
|
||||||
.stay_connected
|
|
||||||
.load(Ordering::SeqCst)
|
|
||||||
{
|
|
||||||
self.start_video_loop().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if stay alive is false, and there is a connection, kill it
|
|
||||||
if !self.stay_alive_sck_recvr.load(Ordering::SeqCst) && self.sck_outbound.is_some() {
|
|
||||||
self.socket_close().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn close(&mut self) {
|
|
||||||
info!("closing coord state");
|
|
||||||
self.tracker_connection_state
|
|
||||||
.stay_connected
|
|
||||||
.store(false, Ordering::SeqCst);
|
|
||||||
self.socket_close().await;
|
|
||||||
|
|
||||||
self.joystick_loop_alive.store(false, Ordering::SeqCst);
|
|
||||||
self.to_gui.close();
|
|
||||||
self.mec.close();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::coordinator::{ApplicationEvent, MoveEvent};
|
use crate::coordinator::{ApplicationEvent, Point};
|
||||||
|
|
||||||
use async_channel::Sender;
|
use async_channel::Sender;
|
||||||
use gilrs::{ev::filter::FilterFn, Axis, Button, Event, EventType, Filter, Gilrs, GilrsBuilder};
|
use gilrs::{ev::filter::FilterFn, Axis, Button, Event, EventType, Filter, Gilrs, GilrsBuilder};
|
||||||
|
@ -53,6 +53,7 @@ pub async fn joystick_loop(tx: Sender<ApplicationEvent>, is_alive: Arc<AtomicBoo
|
||||||
// get the next event, and if it is an axis we are interested in, update the
|
// get the next event, and if it is an axis we are interested in, update the
|
||||||
// corresponding variable
|
// corresponding variable
|
||||||
while let Some(evt) = gilrs.next_event().filter_ev(&UnknownSlayer {}, &mut gilrs) {
|
while let Some(evt) = gilrs.next_event().filter_ev(&UnknownSlayer {}, &mut gilrs) {
|
||||||
|
info!("got a new joystick event");
|
||||||
match evt.event {
|
match evt.event {
|
||||||
gilrs::EventType::AxisChanged(gilrs::Axis::LeftStickY, val, _) => {
|
gilrs::EventType::AxisChanged(gilrs::Axis::LeftStickY, val, _) => {
|
||||||
curr_y = (val * 100.0) as i32;
|
curr_y = (val * 100.0) as i32;
|
||||||
|
@ -86,13 +87,10 @@ pub async fn joystick_loop(tx: Sender<ApplicationEvent>, is_alive: Arc<AtomicBoo
|
||||||
count_zeros = 0;
|
count_zeros = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
match tx.try_send(ApplicationEvent::MoveEvent(
|
match tx.try_send(ApplicationEvent::JoystickMove(Point {
|
||||||
MoveEvent {
|
x: curr_x,
|
||||||
x: curr_x,
|
y: curr_y,
|
||||||
y: curr_y,
|
})) {
|
||||||
},
|
|
||||||
crate::coordinator::ConnectionType::Local,
|
|
||||||
)) {
|
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(async_channel::TrySendError::Closed(_)) => {
|
Err(async_channel::TrySendError::Closed(_)) => {
|
||||||
info!("MEC is closed, stopping Joystick loop");
|
info!("MEC is closed, stopping Joystick loop");
|
|
@ -1,268 +1,363 @@
|
||||||
use std::pin::pin;
|
use std::fmt::Display;
|
||||||
use std::sync::{
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
atomic::{AtomicBool, Ordering},
|
use std::time::Duration;
|
||||||
Arc,
|
|
||||||
};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use async_channel::{Receiver, Sender};
|
use async_channel::{Receiver, Sender, TryRecvError};
|
||||||
use futures_util::{stream::SplitStream, StreamExt};
|
use serde::{Deserialize, Serialize};
|
||||||
use gstreamer::prelude::ElementExt;
|
use tauri::Manager;
|
||||||
use gstreamer::State;
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tokio::sync::RwLock;
|
use tracing::{debug, error, info, warn};
|
||||||
use tokio_tungstenite::{tungstenite::Message, MaybeTlsStream, WebSocketStream};
|
use vcs_common::ApplicationMessage;
|
||||||
use tracing::{debug, error, info, instrument};
|
|
||||||
|
|
||||||
mod coord_state;
|
|
||||||
mod process_box_string;
|
|
||||||
mod remote_video_processor;
|
|
||||||
|
|
||||||
use crate::states::perf_state;
|
|
||||||
use crate::states::tracker_state;
|
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::ui::{GuiUpdate, NormalizedBoxCoords};
|
use crate::APP_HANDLE;
|
||||||
pub use coord_state::{CoordState, SocketState};
|
|
||||||
|
|
||||||
const PRIORITY_TIMEOUT: Duration = Duration::from_secs(2);
|
mod joystick_source;
|
||||||
|
mod satellite_connection;
|
||||||
|
|
||||||
#[derive(Clone)]
|
use joystick_source::joystick_loop;
|
||||||
pub struct MoveEvent {
|
use satellite_connection::SatelliteConnection;
|
||||||
|
|
||||||
|
pub enum ApplicationEvent {
|
||||||
|
SupportsWebRTC(bool),
|
||||||
|
WebRTCMessage((String, vcs_common::ApplicationMessage)),
|
||||||
|
ChangeTargetSatellite(String),
|
||||||
|
JoystickMove(Point),
|
||||||
|
RetryDisconnectedSatellites,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Point {
|
||||||
pub x: i32,
|
pub x: i32,
|
||||||
pub y: i32,
|
pub y: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)]
|
impl Display for Point {
|
||||||
pub enum ConnectionType {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
Local,
|
write!(f, "({}, {})", self.x, self.y)
|
||||||
Remote,
|
}
|
||||||
Automated,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum TrackerUpdate {
|
pub struct AppState {
|
||||||
Clear,
|
|
||||||
Fail,
|
|
||||||
Update(TrackerUpdatePackage),
|
|
||||||
HeaderUpdate(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TrackerUpdatePackage {
|
|
||||||
pub boxes: Vec<NormalizedBoxCoords>,
|
|
||||||
pub time: Instant,
|
|
||||||
pub request_duration: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ApplicationEvent {
|
|
||||||
CameraConnectionPress,
|
|
||||||
SocketMessage(Message),
|
|
||||||
MoveEvent(MoveEvent, ConnectionType),
|
|
||||||
TrackerUpdate(TrackerUpdate),
|
|
||||||
ChangeTracking(u32),
|
|
||||||
EnableAutomatic(bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn start_coordinator(
|
|
||||||
// Main_Event_Channel
|
|
||||||
mec: Receiver<ApplicationEvent>,
|
|
||||||
to_mec: Sender<ApplicationEvent>,
|
to_mec: Sender<ApplicationEvent>,
|
||||||
to_gui: Sender<GuiUpdate>,
|
mec: Receiver<ApplicationEvent>,
|
||||||
runtime: Handle,
|
pub runtime: Handle,
|
||||||
settings: Arc<RwLock<AppConfig>>,
|
pub has_webrtc_support: Option<bool>,
|
||||||
) {
|
|
||||||
info!("Starting coordinator!");
|
|
||||||
|
|
||||||
let mec = pin!(mec);
|
_config: Arc<tokio::sync::RwLock<AppConfig>>,
|
||||||
|
|
||||||
let jpeg_quality = settings.read().await.tracker_jpeg_quality;
|
pub target_satellite: Option<usize>,
|
||||||
|
|
||||||
let mut state = CoordState::new(
|
pub camera_satellites: Vec<SatelliteConnection>,
|
||||||
mec,
|
pub _endpoint_satellites: Vec<SatelliteConnection>,
|
||||||
to_mec,
|
|
||||||
to_gui,
|
|
||||||
runtime,
|
|
||||||
settings,
|
|
||||||
jpeg_quality,
|
|
||||||
);
|
|
||||||
|
|
||||||
state
|
pub joystick_task_is_alive: Arc<AtomicBool>,
|
||||||
.pipeline
|
}
|
||||||
.pipeline
|
|
||||||
.set_state(State::Playing)
|
|
||||||
.expect("Could not set pipeline state to playing");
|
|
||||||
|
|
||||||
if let Err(e) = state
|
impl AppState {
|
||||||
.to_gui
|
pub async fn new(
|
||||||
.send(GuiUpdate::UpdatePaintable(
|
mec: Receiver<ApplicationEvent>,
|
||||||
state.pipeline.sink_paintable.clone(),
|
to_mec: Sender<ApplicationEvent>,
|
||||||
))
|
config: Arc<tokio::sync::RwLock<AppConfig>>,
|
||||||
.await
|
rt: Handle,
|
||||||
{
|
) -> Self {
|
||||||
error!("Could not send new paintable to GUI: {e}");
|
let camera_satellites = config
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.cameras
|
||||||
|
.iter()
|
||||||
|
.map(|x| SatelliteConnection::new(x.clone()))
|
||||||
|
.collect::<Vec<SatelliteConnection>>();
|
||||||
|
|
||||||
|
AppState {
|
||||||
|
to_mec,
|
||||||
|
mec,
|
||||||
|
runtime: rt,
|
||||||
|
has_webrtc_support: None,
|
||||||
|
|
||||||
|
_config: config,
|
||||||
|
|
||||||
|
target_satellite: None,
|
||||||
|
|
||||||
|
camera_satellites,
|
||||||
|
_endpoint_satellites: vec![],
|
||||||
|
|
||||||
|
joystick_task_is_alive: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.check_states().await;
|
pub fn update_satellite_names(&self) {
|
||||||
|
let new_names = self
|
||||||
|
.camera_satellites
|
||||||
|
.iter()
|
||||||
|
.map(|x| SatelliteName {
|
||||||
|
name: x.name.clone(),
|
||||||
|
is_connected: x.is_connected(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<SatelliteName>>();
|
||||||
|
send_ui_message(
|
||||||
|
"satellite_names".to_owned(),
|
||||||
|
serde_json::to_string(&new_names).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
while let Some(msg) = state.mec.next().await {
|
pub async fn check_alive_things(&mut self) {
|
||||||
state.check_states().await;
|
/*
|
||||||
|
if !self
|
||||||
|
.joystick_task_is_alive
|
||||||
|
.load(std::sync::atomic::Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
self.runtime.spawn(joystick_loop(
|
||||||
|
self.to_mec.clone(),
|
||||||
|
Arc::clone(&self.joystick_task_is_alive),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
match msg {
|
let mut resend_names = false;
|
||||||
ApplicationEvent::CameraConnectionPress => {
|
for i in self
|
||||||
if state.socket_connected() {
|
.camera_satellites
|
||||||
state.socket_close().await;
|
.iter_mut()
|
||||||
} else {
|
.filter(|x| x.was_connected && !x.is_connected())
|
||||||
state.socket_start().await;
|
{
|
||||||
}
|
info!("new dead connection found, cleaning up!");
|
||||||
}
|
i.was_connected = false;
|
||||||
ApplicationEvent::SocketMessage(socket_message) => {
|
resend_names = true;
|
||||||
state.socket_send(socket_message).await;
|
}
|
||||||
}
|
|
||||||
ApplicationEvent::ChangeTracking(new_id) => {
|
|
||||||
state.tracker_state.tracking_id = new_id;
|
|
||||||
}
|
|
||||||
ApplicationEvent::EnableAutomatic(do_enable) => {
|
|
||||||
state.tracker_state.enabled = do_enable;
|
|
||||||
state
|
|
||||||
.tracker_connection_state
|
|
||||||
.stay_connected
|
|
||||||
.store(do_enable, Ordering::SeqCst);
|
|
||||||
|
|
||||||
state.check_states().await;
|
if resend_names {
|
||||||
}
|
info!("Refreshing UI connection names");
|
||||||
ApplicationEvent::MoveEvent(coord, priority) => {
|
self.update_satellite_names();
|
||||||
// If Automatic control, but local event happens, override the automatice events for 2 seconds
|
}
|
||||||
if priority <= state.current_priority
|
|
||||||
|| Instant::now() > state.last_update_of_priority + PRIORITY_TIMEOUT
|
|
||||||
{
|
|
||||||
state.last_update_of_priority = Instant::now();
|
|
||||||
state.current_priority = priority;
|
|
||||||
|
|
||||||
if let Err(e) = state.to_gui.send(GuiUpdate::MoveEvent(coord.clone())).await {
|
for i in self
|
||||||
panic!("Could not set message to gui channel; Unrecoverable: {e}");
|
.camera_satellites
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|x| !x.is_connected() && x.try_connecting)
|
||||||
|
{
|
||||||
|
i.connect(self.runtime.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_main_event_loop(
|
||||||
|
mec: Receiver<ApplicationEvent>,
|
||||||
|
to_mec: Sender<ApplicationEvent>,
|
||||||
|
to_ui: Sender<ApplicationEvent>,
|
||||||
|
config: Arc<tokio::sync::RwLock<AppConfig>>,
|
||||||
|
rt: Handle,
|
||||||
|
) {
|
||||||
|
let mut state = AppState::new(mec, to_mec, config, rt).await;
|
||||||
|
loop {
|
||||||
|
state.check_alive_things().await;
|
||||||
|
|
||||||
|
match state.mec.try_recv() {
|
||||||
|
Err(TryRecvError::Empty) => tokio::time::sleep(Duration::from_millis(50)).await,
|
||||||
|
Err(TryRecvError::Closed) => break,
|
||||||
|
Ok(msg) => {
|
||||||
|
match msg {
|
||||||
|
ApplicationEvent::SupportsWebRTC(has_support) => {
|
||||||
|
state.has_webrtc_support = Some(has_support);
|
||||||
|
state
|
||||||
|
.camera_satellites
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|x| x.is_connected())
|
||||||
|
.for_each(|x| {
|
||||||
|
x.send_blocking(
|
||||||
|
ApplicationMessage::ConnectionSupportsWebRTC(has_support),
|
||||||
|
state.runtime.clone(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Use this as a 'ui is ready' event as well
|
||||||
|
state.update_satellite_names();
|
||||||
}
|
}
|
||||||
|
ApplicationEvent::Close => {
|
||||||
if state.socket_connected() {
|
state.mec.close(); // cleanup is handled on reading from a closed mec
|
||||||
let message = format!(
|
}
|
||||||
"{}{}:{}{}",
|
ApplicationEvent::ChangeTargetSatellite(new_target_name) => {
|
||||||
if coord.y > 0 { "D" } else { "U" },
|
state.target_satellite = state
|
||||||
coord.y.abs(),
|
.camera_satellites
|
||||||
if coord.x > 0 { "R" } else { "L" },
|
.iter()
|
||||||
coord.x.abs()
|
.position(|x| x.name == new_target_name);
|
||||||
|
info!(
|
||||||
|
"Changed active satellite index to: {:?}",
|
||||||
|
state.target_satellite
|
||||||
);
|
);
|
||||||
|
|
||||||
state.socket_send(Message::Text(message)).await;
|
|
||||||
}
|
}
|
||||||
}
|
ApplicationEvent::RetryDisconnectedSatellites => {
|
||||||
}
|
state.camera_satellites.iter_mut().for_each(|x| {
|
||||||
ApplicationEvent::TrackerUpdate(update) => match update {
|
info!("Resetting connections");
|
||||||
TrackerUpdate::HeaderUpdate(_) => {}
|
x.try_connecting = true;
|
||||||
TrackerUpdate::Clear => {
|
x.retry_attempts = 0;
|
||||||
state.tracker_state.clear();
|
});
|
||||||
state.tracker_metrics.clear_times();
|
|
||||||
if let Err(e) = state.to_gui.send(GuiUpdate::TrackerUpdate(TrackerUpdate::Clear)).await {
|
|
||||||
error!("Could not send message to GUI: {e}");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
ApplicationEvent::WebRTCMessage((name, msg)) => {
|
||||||
TrackerUpdate::Fail => {
|
info!(
|
||||||
let fail_count: usize = state.tracker_metrics.fail_count + 1;
|
"Valid camera names are: {:?}",
|
||||||
state.tracker_metrics.starting_connection(Some(fail_count));
|
state
|
||||||
}
|
.camera_satellites
|
||||||
TrackerUpdate::Update(update) => {
|
.iter()
|
||||||
let mut x_adj: i32 = 0;
|
.map(|x| x.name.clone())
|
||||||
let mut y_adj: i32 = 0;
|
.collect::<Vec<String>>()
|
||||||
|
);
|
||||||
if let Err(e) = state.to_gui
|
info!("Reqested name is: {}", name);
|
||||||
.send(GuiUpdate::TrackerUpdate(TrackerUpdate::Update(
|
info!(
|
||||||
update.clone(),
|
"Count of is_connected: {}",
|
||||||
)))
|
state
|
||||||
.await
|
.camera_satellites
|
||||||
{
|
.iter()
|
||||||
error!("Could not send message to the GUI: {e}");
|
.filter(|x| x.is_connected())
|
||||||
break;
|
.count()
|
||||||
}
|
);
|
||||||
|
for conn in state
|
||||||
state.tracker_state.update_from_boxes(update.boxes);
|
.camera_satellites
|
||||||
state.tracker_state.last_detect = update.time;
|
.iter_mut()
|
||||||
|
.filter(|x| x.name == name && x.is_connected())
|
||||||
match state.tracker_state.calculate_tracking() {
|
{
|
||||||
Ok((x, y, _tracker_enabled)) => {
|
info!("Sending message");
|
||||||
x_adj = x;
|
if let Err(_) = conn.send(msg.clone()).await {
|
||||||
y_adj = y;
|
error!("The websocket gave an error when I tried to send a message! I hope your logging is good enough");
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
if state.tracker_state.tracking_id > 0 {
|
|
||||||
info!("Could not calculate the tracking!: {e}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ApplicationEvent::JoystickMove(mut coord) => {
|
||||||
let me = MoveEvent { x: x_adj, y: y_adj };
|
if coord.x < 10 && coord.x > -10 {
|
||||||
if let Err(e) = state
|
coord.x = 0;
|
||||||
.to_mec
|
|
||||||
.send(ApplicationEvent::MoveEvent(
|
|
||||||
me.clone(),
|
|
||||||
ConnectionType::Automated,
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!("Could not send to MEC... even though in the MEC?! {e}");
|
|
||||||
}
|
|
||||||
if let Err(e) = state.to_gui.send(GuiUpdate::MoveEvent(me)).await {
|
|
||||||
error!("Could not send to MEC... even though in the MEC?! {e}");
|
|
||||||
}
|
|
||||||
state.tracker_metrics.insert_time(update.request_duration);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state
|
|
||||||
.pipeline
|
|
||||||
.pipeline
|
|
||||||
.set_state(State::Null)
|
|
||||||
.expect("Could not set pipeline state to playing");
|
|
||||||
|
|
||||||
state.close().await;
|
|
||||||
|
|
||||||
info!("Stopping Coordinator");
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn socket_listen(
|
|
||||||
mec: Sender<ApplicationEvent>,
|
|
||||||
stay_alive_sck_recvr: Arc<AtomicBool>,
|
|
||||||
mut reader: SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
|
|
||||||
) {
|
|
||||||
if stay_alive_sck_recvr.load(std::sync::atomic::Ordering::SeqCst) {
|
|
||||||
while let Some(msg) = reader.next().await {
|
|
||||||
match msg {
|
|
||||||
Ok(val) => {
|
|
||||||
match val {
|
|
||||||
Message::Ping(_) => {
|
|
||||||
// Do nothing because pings are handled on reads and write by tungestenite
|
|
||||||
}
|
}
|
||||||
_ => {
|
if coord.y < 10 && coord.y > -10 {
|
||||||
info!("Received message from the camera websocket? {:#?}", val);
|
coord.y = 0;
|
||||||
|
}
|
||||||
|
if let Some(target) = state.target_satellite {
|
||||||
|
if state.camera_satellites.len() >= target {
|
||||||
|
if let Err(e) = state.camera_satellites[target]
|
||||||
|
.send(ApplicationMessage::ManualMovementOverride((
|
||||||
|
coord.x, coord.y,
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("There was an error sending the joystick movement message to the target! {:?}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("That was not a valid target! Need to notifiy the UI about this");
|
||||||
|
debug!("Count of camera satellites: {}, target: {:?}", state.camera_satellites.len(), target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
error!("Websocket error: {:#?}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setting this will call the internal state.socket_close next check states
|
let mut update_names: bool = false;
|
||||||
stay_alive_sck_recvr.store(false, Ordering::SeqCst);
|
for connection in state.camera_satellites.iter_mut() {
|
||||||
|
match connection.try_next().await {
|
||||||
|
Some(msg) => match msg {
|
||||||
|
ApplicationMessage::NameRequest(None) => {
|
||||||
|
if let Err(e) = connection
|
||||||
|
.send(ApplicationMessage::NameRequest(Some(
|
||||||
|
"Controller".to_owned(),
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
info!("Was not able to send name to remote? {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApplicationMessage::NameRequest(Some(name)) => {
|
||||||
|
warn!("Got a name update!");
|
||||||
|
connection.name = name;
|
||||||
|
update_names = true;
|
||||||
|
}
|
||||||
|
ApplicationMessage::ChangeTrackingID(_) => {}
|
||||||
|
ApplicationMessage::ManualMovementOverride(_) => {}
|
||||||
|
ApplicationMessage::TrackingBoxes(_update) => {}
|
||||||
|
|
||||||
|
ApplicationMessage::ConnectionSupportsWebRTCRequest => {
|
||||||
|
let does_support_webrtc = true;
|
||||||
|
if let Err(e) = connection
|
||||||
|
.send(ApplicationMessage::ConnectionSupportsWebRTC(
|
||||||
|
does_support_webrtc,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"Was not able to send webrtc support status to remote: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApplicationMessage::ConnectionSupportsWebRTC(does_support) => {
|
||||||
|
info!(
|
||||||
|
"Cool, the camera satellite supports webrtc: {}",
|
||||||
|
does_support
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ApplicationMessage::WebRTCIceCandidateInit(pkt) => {
|
||||||
|
send_frontend_message(serde_json::to_string(&pkt).unwrap())
|
||||||
|
}
|
||||||
|
ApplicationMessage::WebRTCIceCandidate(pkt) => {
|
||||||
|
send_frontend_message(serde_json::to_string(&pkt).unwrap())
|
||||||
|
}
|
||||||
|
ApplicationMessage::WebRTCPacket(pkt) => {
|
||||||
|
send_frontend_message(serde_json::to_string(&pkt).unwrap())
|
||||||
|
}
|
||||||
|
ApplicationMessage::CloseConnection => {
|
||||||
|
error!("Cannot handle close connection from satellite yet");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if update_names {
|
||||||
|
state.update_satellite_names();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
info!("Closing the MEC loop");
|
||||||
|
state
|
||||||
|
.joystick_task_is_alive
|
||||||
|
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
// If the mec is closed or full, then this socket should be closing anyways
|
let close_handles: Vec<_> = state
|
||||||
// as there was most likely an unrecoverable error
|
.camera_satellites
|
||||||
let _ = mec
|
.iter_mut()
|
||||||
.send(ApplicationEvent::SocketMessage(Message::Close(None)))
|
.filter(|x| x.is_connected())
|
||||||
.await;
|
.map(|x| x.close())
|
||||||
|
.collect();
|
||||||
debug!("Closed socket reading thread");
|
futures::future::join_all(close_handles).await;
|
||||||
|
info!("Satellite connections all closed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_frontend_message(message: String) {
|
||||||
|
if let Ok(mut e) = APP_HANDLE.lock() {
|
||||||
|
if e.is_none() {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
e.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.emit_all("frontend_message", message)
|
||||||
|
.expect("Could not send message to the tauri frontend!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct SatelliteName {
|
||||||
|
pub is_connected: bool,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_ui_message(handle_name: String, message: String) {
|
||||||
|
if let Ok(mut e) = APP_HANDLE.lock() {
|
||||||
|
if e.is_none() {
|
||||||
|
error!("Could not get app handle!");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
info!("sending event '{}' with payload '{}'", handle_name, message);
|
||||||
|
e.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.emit_all(&handle_name, message)
|
||||||
|
.expect("Could not send message to the tauri frontend!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("Could not get lock on APP_HANDLE!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
use crate::ui::NormalizedBoxCoords;
|
|
||||||
|
|
||||||
pub fn process_incoming_string(message: String) -> Result<Vec<NormalizedBoxCoords>, String> {
|
|
||||||
let mut boxes: Vec<NormalizedBoxCoords> = Vec::new();
|
|
||||||
|
|
||||||
for line in message.lines() {
|
|
||||||
let parts: Vec<&str> = line.split(' ').collect();
|
|
||||||
|
|
||||||
let id = parts[0]
|
|
||||||
.replace(['[', ']'], "")
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| "Invalid ID")?;
|
|
||||||
|
|
||||||
if parts.len() != 3 {
|
|
||||||
return Err("Invalid socket input format: number of parts".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let coords: Vec<&str> = parts[1].split(':').collect();
|
|
||||||
if coords.len() != 2 {
|
|
||||||
return Err("Invalid socket input format: coords 1".to_string());
|
|
||||||
}
|
|
||||||
let x1: u32 = coords[0].parse().map_err(|_| "Invalid x coordinate")?;
|
|
||||||
let y1: u32 = coords[1].parse().map_err(|_| "Invalid y coordinate")?;
|
|
||||||
|
|
||||||
let coords2: Vec<&str> = parts[2].split(':').collect();
|
|
||||||
if coords2.len() != 2 {
|
|
||||||
return Err("Invalid socket input format: coords 2".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let x2: u32 = coords2[0].parse().map_err(|_| "Invalid width")?;
|
|
||||||
let y2: u32 = coords2[1].parse().map_err(|_| "Invalid width")?;
|
|
||||||
|
|
||||||
boxes.push(NormalizedBoxCoords {
|
|
||||||
id,
|
|
||||||
x1: (x1 as f32 / 1000.0),
|
|
||||||
x2: (x2 as f32 / 1000.0),
|
|
||||||
y1: (y1 as f32 / 1000.0),
|
|
||||||
y2: (y2 as f32 / 1000.0),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(boxes)
|
|
||||||
}
|
|
|
@ -1,196 +0,0 @@
|
||||||
use std::{
|
|
||||||
sync::{atomic::Ordering, Arc},
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use async_recursion::async_recursion;
|
|
||||||
|
|
||||||
use async_channel::Sender;
|
|
||||||
use futures_util::{stream::SplitStream, SinkExt, StreamExt, TryStreamExt};
|
|
||||||
use gstreamer_app::AppSink;
|
|
||||||
use tokio::{net::TcpStream, sync::Mutex, time::sleep_until};
|
|
||||||
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
|
|
||||||
use tracing::{error, info, instrument, warn};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
process_box_string::process_incoming_string, ApplicationEvent, SocketState, TrackerUpdate,
|
|
||||||
TrackerUpdatePackage,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn remote_video_loop(
|
|
||||||
conn_string: String,
|
|
||||||
appsink: Arc<Mutex<AppSink>>,
|
|
||||||
to_mec: Sender<ApplicationEvent>,
|
|
||||||
socket_state: Arc<SocketState>,
|
|
||||||
) {
|
|
||||||
info!(
|
|
||||||
"Starting remote tracker processing connection to: {}",
|
|
||||||
conn_string
|
|
||||||
);
|
|
||||||
|
|
||||||
socket_state.is_connected.store(true, Ordering::SeqCst);
|
|
||||||
|
|
||||||
match connect_async(&conn_string).await {
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Could not connect to remote computer: {e}");
|
|
||||||
if let Err(e) = to_mec
|
|
||||||
.send(ApplicationEvent::TrackerUpdate(TrackerUpdate::Fail))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!("Could not send message to MEC! {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok((connection, _)) => {
|
|
||||||
let (mut sender, mut recvr) = connection.split();
|
|
||||||
|
|
||||||
let mut last_iter: Instant;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
last_iter = Instant::now();
|
|
||||||
// Do this in an encloser to not keep a lock on the appsink
|
|
||||||
let image_message = {
|
|
||||||
let res = {
|
|
||||||
let appsnk = appsink.lock().await;
|
|
||||||
|
|
||||||
get_video_frame(&appsnk)
|
|
||||||
};
|
|
||||||
match res {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Could not get video frame! {e}");
|
|
||||||
if let Err(e) = sender.close().await {
|
|
||||||
error!("Could not close socket to remote computer: {e}")
|
|
||||||
}
|
|
||||||
socket_state.is_connected.store(false, Ordering::SeqCst);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = sender.send(image_message).await {
|
|
||||||
error!("There was an error sending the video frame to the server: {e}");
|
|
||||||
if let Err(e) = sender.close().await {
|
|
||||||
error!("Could not close socket to remote computer: {e}")
|
|
||||||
}
|
|
||||||
socket_state.is_connected.store(false, Ordering::SeqCst);
|
|
||||||
socket_state.stay_connected.store(false, Ordering::SeqCst);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let do_not_break = handle_message(&mut recvr, &to_mec, last_iter).await;
|
|
||||||
|
|
||||||
if !do_not_break { break; }
|
|
||||||
|
|
||||||
|
|
||||||
if !socket_state.stay_connected.load(Ordering::SeqCst) {
|
|
||||||
info!("Shutting down remote video loop");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// rate limit updates
|
|
||||||
sleep_until(tokio::time::Instant::now() + Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info!("Shutting down remote video loop");
|
|
||||||
|
|
||||||
if let Err(e) = to_mec
|
|
||||||
.send(ApplicationEvent::TrackerUpdate(TrackerUpdate::Clear))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!("Could not send message to MEC! {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// This message forces a redraw after clearing the queue
|
|
||||||
if let Err(e) = to_mec
|
|
||||||
.send(ApplicationEvent::MoveEvent(
|
|
||||||
crate::coordinator::MoveEvent { x: 0, y: 0 },
|
|
||||||
crate::coordinator::ConnectionType::Automated,
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!(
|
|
||||||
"Error sending message to MEC during shutdown of tracker thread: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
socket_state.is_connected.store(false, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_video_frame(appsink: &AppSink) -> Result<Message, String> {
|
|
||||||
let sample = appsink
|
|
||||||
.pull_sample()
|
|
||||||
.map_err(|e| format!("Could not get sample: {e}"))?;
|
|
||||||
let buffer = sample.buffer().ok_or("Could not get buffer, was None")?;
|
|
||||||
let map = buffer
|
|
||||||
.map_readable()
|
|
||||||
.map_err(|e| format!("Could not get readable map: {e}"))?;
|
|
||||||
Ok(Message::binary(map.to_vec()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_recursion]
|
|
||||||
async fn handle_message(
|
|
||||||
recvr: &mut SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
|
|
||||||
// sender: &mut SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
|
|
||||||
to_mec: &Sender<ApplicationEvent>,
|
|
||||||
last_iter: Instant,
|
|
||||||
) -> bool {
|
|
||||||
match recvr.try_next().await {
|
|
||||||
Ok(Some(message)) => {
|
|
||||||
match message {
|
|
||||||
Message::Close(_) => {
|
|
||||||
info!("Close packet received from remote computer: {:?}", message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Message::Pong(_) | Message::Frame(_) | Message::Text(_) => {
|
|
||||||
warn!("There was an unhandled message type from the camera: {}\n{}", message, message.to_string());
|
|
||||||
// this was not the expected response, recursion!
|
|
||||||
return handle_message(recvr, to_mec, last_iter).await;
|
|
||||||
}
|
|
||||||
Message::Ping(_) => {
|
|
||||||
// Ping/Pongs are handled by tokio tungstenite on reads and writes
|
|
||||||
// this was not the expected response, recursion!
|
|
||||||
return handle_message(recvr, to_mec, last_iter).await;
|
|
||||||
}
|
|
||||||
Message::Binary(bin) => {
|
|
||||||
let message = std::str::from_utf8(&bin);
|
|
||||||
if let Err(e) = message {
|
|
||||||
error!("Could not decode binary message! Assuming corrupted response! {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
match process_incoming_string(message.unwrap().to_string()) {
|
|
||||||
Ok(v) => {
|
|
||||||
if let Err(e) = to_mec
|
|
||||||
.send(ApplicationEvent::TrackerUpdate(TrackerUpdate::Update(
|
|
||||||
TrackerUpdatePackage {
|
|
||||||
boxes: v,
|
|
||||||
time: Instant::now(),
|
|
||||||
request_duration: Instant::now() - last_iter,
|
|
||||||
},
|
|
||||||
)))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!("Could not send to MEC! {e}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Could not parse incoming string! {}\n{}", e, message.unwrap());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
info!("Recieved an empty message from the remote computer: Aborting");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Got an error on while recieving from remote computer: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
162
src/coordinator/satellite_connection.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
|
|
||||||
|
use async_channel::TryRecvError;
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
use tracing::{error, info, instrument, warn};
|
||||||
|
|
||||||
|
use crate::config::ConnectionString;
|
||||||
|
|
||||||
|
use vcs_common::{AppReceiver, AppSender, ApplicationMessage};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SatelliteConnectionError {
|
||||||
|
SocketIsClosed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SatelliteConnection {
|
||||||
|
pub name: String,
|
||||||
|
pub retry_attempts: usize,
|
||||||
|
pub try_connecting: bool,
|
||||||
|
pub was_connected: bool,
|
||||||
|
connection: ConnectionString,
|
||||||
|
|
||||||
|
currently_connecting: bool,
|
||||||
|
|
||||||
|
to_socket: Option<AppSender>,
|
||||||
|
from_socket: Option<AppReceiver>,
|
||||||
|
socket_is_dead: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SatelliteConnection {
|
||||||
|
#[instrument]
|
||||||
|
pub fn new(conn_string: ConnectionString) -> Self {
|
||||||
|
SatelliteConnection {
|
||||||
|
name: String::new(),
|
||||||
|
connection: conn_string.clone(),
|
||||||
|
|
||||||
|
retry_attempts: 0,
|
||||||
|
try_connecting: false,
|
||||||
|
// external flag for 'have a checked this since it disocnnected'
|
||||||
|
was_connected: false,
|
||||||
|
|
||||||
|
currently_connecting: false,
|
||||||
|
|
||||||
|
to_socket: None,
|
||||||
|
from_socket: None,
|
||||||
|
socket_is_dead: Arc::new(AtomicBool::new(true)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn close(&mut self) {
|
||||||
|
if self.to_socket.is_some() {
|
||||||
|
if let Err(_) = self
|
||||||
|
.to_socket
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.send(ApplicationMessage::CloseConnection)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
info!("Could not send close connection to active satellite");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.socket_is_dead
|
||||||
|
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
self.to_socket.take(); // closing all senders will dispose of the
|
||||||
|
self.from_socket.take(); // channel
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub fn is_connected(&self) -> bool {
|
||||||
|
return !self
|
||||||
|
.socket_is_dead
|
||||||
|
.load(std::sync::atomic::Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, rt))]
|
||||||
|
pub async fn connect(&mut self, rt: Handle) {
|
||||||
|
if self.currently_connecting
|
||||||
|
|| !self
|
||||||
|
.socket_is_dead
|
||||||
|
.load(std::sync::atomic::Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.currently_connecting = true;
|
||||||
|
match vcs_common::connect_to_server(self.connection.build_conn_string(), rt).await {
|
||||||
|
Ok((sender, recvr, is_alive)) => {
|
||||||
|
if let Err(e) = sender.send(ApplicationMessage::NameRequest(None)).await {
|
||||||
|
error!(
|
||||||
|
"Couldn't send message to fresh socket sender! '{}' \n {}",
|
||||||
|
self.connection.build_conn_string(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.to_socket = Some(sender);
|
||||||
|
self.from_socket = Some(recvr);
|
||||||
|
self.socket_is_dead = is_alive;
|
||||||
|
self.currently_connecting = false;
|
||||||
|
self.was_connected = true;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.retry_attempts += 1;
|
||||||
|
self.currently_connecting = false;
|
||||||
|
if self.retry_attempts > 5 {
|
||||||
|
info!("Retry attempts maxed out. Stopping the retries");
|
||||||
|
self.try_connecting = false;
|
||||||
|
}
|
||||||
|
error!(
|
||||||
|
"Could not connect to socket remote: '{}' \nRetry Attempt: {} \n {}",
|
||||||
|
self.connection.build_conn_string(),
|
||||||
|
self.retry_attempts,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is not the recommneded method, as it ignores error, but sometimes
|
||||||
|
/// you just really don't want to be async
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub fn send_blocking(&mut self, msg: ApplicationMessage, rt: tokio::runtime::Handle) {
|
||||||
|
if self.to_socket.is_some() {
|
||||||
|
let sender = self.to_socket.as_ref().unwrap().clone();
|
||||||
|
rt.spawn(async move {
|
||||||
|
if let Err(e) = sender.send(msg).await {
|
||||||
|
error!("Unhandled send error while in send_blocking! {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn send(&mut self, msg: ApplicationMessage) -> Result<(), SatelliteConnectionError> {
|
||||||
|
if self.to_socket.is_some() {
|
||||||
|
if let Err(_) = self.to_socket.as_ref().unwrap().send(msg).await {
|
||||||
|
self.close().await;
|
||||||
|
return Err(SatelliteConnectionError::SocketIsClosed);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(SatelliteConnectionError::SocketIsClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn try_next(&mut self) -> Option<ApplicationMessage> {
|
||||||
|
if self.from_socket.is_some() {
|
||||||
|
match self.from_socket.as_ref().unwrap().try_recv() {
|
||||||
|
Ok(msg) => match msg {
|
||||||
|
_ => Some(msg),
|
||||||
|
},
|
||||||
|
Err(TryRecvError::Empty) => None,
|
||||||
|
Err(TryRecvError::Closed) => {
|
||||||
|
self.close().await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,228 +0,0 @@
|
||||||
use gstreamer::{prelude::*, PadLinkError};
|
|
||||||
use gstreamer::{Element, ElementFactory, Pipeline};
|
|
||||||
use gstreamer_app::AppSink;
|
|
||||||
use gtk::glib::BoolError;
|
|
||||||
use snafu::prelude::*;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct WebcamPipeline {
|
|
||||||
pub pipeline: Pipeline,
|
|
||||||
|
|
||||||
pub sink_paintable: Element,
|
|
||||||
|
|
||||||
pub sink_frame: Arc<Mutex<AppSink>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebcamPipeline {
|
|
||||||
pub fn new(jpeg_quality: i32) -> Result<WebcamPipeline, PipelineError> {
|
|
||||||
let pipeline = Pipeline::with_name("webcam_pipeline");
|
|
||||||
|
|
||||||
// All of the following errors are unrecoverable
|
|
||||||
|
|
||||||
let source = ElementFactory::make("mfvideosrc")
|
|
||||||
.build()
|
|
||||||
.context(BuildSnafu {
|
|
||||||
element: "mfvideosrc",
|
|
||||||
})?;
|
|
||||||
let convert = ElementFactory::make("videoconvert")
|
|
||||||
.build()
|
|
||||||
.context(BuildSnafu {
|
|
||||||
element: "videoconvert",
|
|
||||||
})?;
|
|
||||||
let rate = ElementFactory::make("videorate")
|
|
||||||
.build()
|
|
||||||
.context(BuildSnafu {
|
|
||||||
element: "videorate",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let tee = ElementFactory::make("tee")
|
|
||||||
.build()
|
|
||||||
.context(BuildSnafu { element: "tee" })?;
|
|
||||||
|
|
||||||
let queue_app = ElementFactory::make("queue")
|
|
||||||
.property("max-size-time", 1u64)
|
|
||||||
.property("max-size-buffers", 0u32)
|
|
||||||
.property("max-size-bytes", 0u32)
|
|
||||||
.build()
|
|
||||||
.context(BuildSnafu {
|
|
||||||
element: "paintable queue",
|
|
||||||
})?;
|
|
||||||
let sink_paintable = ElementFactory::make("gtk4paintablesink")
|
|
||||||
.name("gtk4_output")
|
|
||||||
.build()
|
|
||||||
.context(BuildSnafu {
|
|
||||||
element: "gtkpaintablesink",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// queue.connect_closure("overrun", false, glib::closure!(|queue: Element| {
|
|
||||||
// println!("The queue is full!");
|
|
||||||
// }));
|
|
||||||
|
|
||||||
let appsink_queue = ElementFactory::make("queue")
|
|
||||||
.property("max-size-time", 1u64)
|
|
||||||
.property("max-size-buffers", 0u32)
|
|
||||||
.property("max-size-bytes", 0u32)
|
|
||||||
.build()
|
|
||||||
.context(BuildSnafu {
|
|
||||||
element: "appsink queue",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let resize = ElementFactory::make("videoscale")
|
|
||||||
.build()
|
|
||||||
.context(BuildSnafu {
|
|
||||||
element: "videoscale",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let jpeg_enc = ElementFactory::make("jpegenc")
|
|
||||||
.property("quality", jpeg_quality)
|
|
||||||
.build()
|
|
||||||
.context(BuildSnafu { element: "jpegenc" })?;
|
|
||||||
|
|
||||||
let caps_string = "image/jpeg,width=640,height=640";
|
|
||||||
let appsrc_caps = gstreamer::Caps::from_str(caps_string).context(BuildSnafu {
|
|
||||||
element: "appsink caps",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let sink_frame = AppSink::builder()
|
|
||||||
.name("frame_appsink")
|
|
||||||
.sync(false)
|
|
||||||
.max_buffers(1u32)
|
|
||||||
.drop(true)
|
|
||||||
.caps(&appsrc_caps)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
sink_frame.set_property("caps", &appsrc_caps.to_value());
|
|
||||||
|
|
||||||
pipeline
|
|
||||||
.add_many([
|
|
||||||
&source,
|
|
||||||
&convert,
|
|
||||||
&rate,
|
|
||||||
&tee,
|
|
||||||
&queue_app,
|
|
||||||
&sink_paintable,
|
|
||||||
&appsink_queue,
|
|
||||||
&resize,
|
|
||||||
&jpeg_enc,
|
|
||||||
&sink_frame.upcast_ref(),
|
|
||||||
])
|
|
||||||
.context(LinkSnafu {
|
|
||||||
from: "all",
|
|
||||||
to: "pipeline",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Element::link_many([&source, &convert, &rate]).context(LinkSnafu {
|
|
||||||
from: "source et. al.",
|
|
||||||
to: "rate",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// -- BEGIN PAINTABLE SINK PIPELINE
|
|
||||||
let tee_caps =
|
|
||||||
// gstreamer::caps::Caps::from_str("video/x-raw,framerate=15/1").context(BuildSnafu {
|
|
||||||
gstreamer::caps::Caps::from_str("video/x-raw").context(BuildSnafu {
|
|
||||||
element: "tee caps",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
rate.link_filtered(&tee, &tee_caps).context(LinkSnafu {
|
|
||||||
from: "videorate",
|
|
||||||
to: "tee",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let tee_src_1 = tee
|
|
||||||
.request_pad_simple("src_%u")
|
|
||||||
.ok_or(PipelineError::PadRequest {
|
|
||||||
element: "tee pad 1".to_string(),
|
|
||||||
})?;
|
|
||||||
let paintable_queue_sinkpad =
|
|
||||||
queue_app
|
|
||||||
.static_pad("sink")
|
|
||||||
.ok_or(PipelineError::PadRequest {
|
|
||||||
element: "gtk4 sink".to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
tee_src_1
|
|
||||||
.link(&paintable_queue_sinkpad)
|
|
||||||
.context(PadLinkSnafu {
|
|
||||||
from: "tee src pad",
|
|
||||||
to: "gtk4 paintable queue",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
queue_app.link(&sink_paintable).context(LinkSnafu {
|
|
||||||
from: "gtk4 paintable queue",
|
|
||||||
to: "gtk4 paintable",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// -- END PAINTABLE SINK PIPELINE
|
|
||||||
|
|
||||||
// -- BEGIN APPSINK PIPELINE
|
|
||||||
let tee_src_2 = tee
|
|
||||||
.request_pad_simple("src_%u")
|
|
||||||
.ok_or(PipelineError::PadRequest {
|
|
||||||
element: "tee pad 2".to_string(),
|
|
||||||
})?;
|
|
||||||
let appsink_queue_sinkpad =
|
|
||||||
appsink_queue
|
|
||||||
.static_pad("sink")
|
|
||||||
.ok_or(PipelineError::PadRequest {
|
|
||||||
element: "appsink queue".to_string(),
|
|
||||||
})?;
|
|
||||||
tee_src_2
|
|
||||||
.link(&appsink_queue_sinkpad)
|
|
||||||
.context(PadLinkSnafu {
|
|
||||||
from: "tee src pad 2",
|
|
||||||
to: "appsink queue sinkpad",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
appsink_queue.link(&resize).context(LinkSnafu {
|
|
||||||
from: "appsink_queue",
|
|
||||||
to: "resize",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let resize_caps =
|
|
||||||
gstreamer::caps::Caps::from_str("video/x-raw,format=RGB,width=640,height=640")
|
|
||||||
.context(BuildSnafu {
|
|
||||||
element: "resize_caps",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
resize
|
|
||||||
.link_filtered(&jpeg_enc, &resize_caps)
|
|
||||||
.context(LinkSnafu {
|
|
||||||
from: "jpeg_enc",
|
|
||||||
to: "resize_caps",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Element::link_many([&jpeg_enc, &sink_frame.upcast_ref()]).context(LinkSnafu {
|
|
||||||
from: "jpeg_enc",
|
|
||||||
to: "appsink",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(WebcamPipeline {
|
|
||||||
pipeline,
|
|
||||||
sink_paintable,
|
|
||||||
sink_frame: Arc::new(Mutex::new(sink_frame)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
pub enum PipelineError {
|
|
||||||
#[snafu(display("Error during element linking"))]
|
|
||||||
Link {
|
|
||||||
source: BoolError,
|
|
||||||
from: String,
|
|
||||||
to: String,
|
|
||||||
},
|
|
||||||
#[snafu(display("Error linking pads"))]
|
|
||||||
PadLink {
|
|
||||||
source: PadLinkError,
|
|
||||||
from: String,
|
|
||||||
to: String,
|
|
||||||
},
|
|
||||||
#[snafu(display("Error creating element"))]
|
|
||||||
Build { source: BoolError, element: String },
|
|
||||||
#[snafu(display("Error getting pad from element"))]
|
|
||||||
PadRequest { element: String },
|
|
||||||
}
|
|
152
src/main.rs
|
@ -1,26 +1,32 @@
|
||||||
use gtk::prelude::{ApplicationExt, ApplicationExtManual};
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
use gtk::{glib, Application};
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
use std::{env, sync::Arc};
|
|
||||||
|
use async_channel::Sender;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
use tokio::{runtime, sync::RwLock};
|
use tokio::{runtime, sync::RwLock};
|
||||||
use tracing::{self, info, Level};
|
use tracing::{self, debug, error, info};
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
use tracing_subscriber;
|
use tracing_subscriber;
|
||||||
|
use vcs_common::ApplicationMessage;
|
||||||
|
|
||||||
use crate::config::{load_config, AppConfig};
|
use crate::config::{load_config, AppConfig};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod coordinator;
|
mod coordinator;
|
||||||
mod gstreamer_pipeline;
|
mod tauri_functions;
|
||||||
mod sources;
|
mod webrtc_remote;
|
||||||
mod states;
|
|
||||||
mod ui;
|
|
||||||
const APP_ID: &str = "net.nickiel.joystick-controller-client";
|
|
||||||
|
|
||||||
fn main() -> glib::ExitCode {
|
use coordinator::{run_main_event_loop, ApplicationEvent};
|
||||||
// set the environment var to make gtk use window's default action bar
|
|
||||||
env::set_var("gtk_csd", "0");
|
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref TO_MEC_REF: Mutex<Option<Sender<ApplicationEvent>>> = Mutex::new(None);
|
||||||
|
static ref APP_HANDLE: Mutex<Option<AppHandle>> = Mutex::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
{
|
{
|
||||||
let file_appender = tracing_appender::rolling::daily(".\\logs", "camera-controller");
|
let file_appender = tracing_appender::rolling::daily(".\\logs", "camera-controller");
|
||||||
|
@ -33,45 +39,125 @@ fn main() -> glib::ExitCode {
|
||||||
}
|
}
|
||||||
#[cfg(all(not(feature = "tokio-debug"), debug_assertions))]
|
#[cfg(all(not(feature = "tokio-debug"), debug_assertions))]
|
||||||
{
|
{
|
||||||
let sub = tracing_subscriber::FmtSubscriber::new();
|
let _sub = tracing_subscriber::fmt()
|
||||||
if let Err(e) = tracing::subscriber::set_global_default(sub) {
|
.with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG)
|
||||||
panic!("Could not set tracing global: {e}");
|
.init();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[cfg(feature = "tokio-debug")]
|
#[cfg(feature = "tokio-debug")]
|
||||||
{
|
{
|
||||||
console_subscriber::init();
|
console_subscriber::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (to_mec, mec) = async_channel::bounded::<ApplicationEvent>(10);
|
||||||
let span = tracing::span!(Level::TRACE, "main");
|
let (to_ui, ui_ec) = async_channel::bounded::<ApplicationEvent>(10);
|
||||||
let _enter = span.enter();
|
|
||||||
|
|
||||||
info!("Logging intialized");
|
info!("Logging intialized");
|
||||||
|
|
||||||
let config: Arc<RwLock<AppConfig>> = Arc::new(RwLock::new(load_config()));
|
let config: Arc<RwLock<AppConfig>> = Arc::new(RwLock::new(load_config()));
|
||||||
|
|
||||||
gstreamer::init().expect("Unable to start gstreamer");
|
|
||||||
gstgtk4::plugin_register_static().expect("Unable to register gtk4 plugin");
|
|
||||||
|
|
||||||
let rt = runtime::Runtime::new().expect("Could not start tokio runtime");
|
let rt = runtime::Runtime::new().expect("Could not start tokio runtime");
|
||||||
let handle = rt.handle().clone();
|
let handle = rt.handle().clone();
|
||||||
|
let handle2 = handle.clone();
|
||||||
|
|
||||||
let app = Application::builder().application_id(APP_ID).build();
|
let _coordinator = rt.handle().spawn(run_main_event_loop(
|
||||||
|
mec,
|
||||||
|
to_mec.clone(),
|
||||||
|
to_ui,
|
||||||
|
config,
|
||||||
|
handle,
|
||||||
|
));
|
||||||
|
|
||||||
app.connect_startup(ui::on_activate);
|
*TO_MEC_REF.lock().unwrap() = Some(to_mec.clone());
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
tauri::Builder::default()
|
||||||
ui::build_ui(app, config.clone(), handle.clone());
|
.manage(tauri_functions::TauriState {
|
||||||
});
|
to_mec: to_mec.clone(),
|
||||||
|
rt: handle2.clone(),
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
tauri_functions::connect_to_camera,
|
||||||
|
tauri_functions::supports_webrtc,
|
||||||
|
tauri_functions::change_target_satellite,
|
||||||
|
tauri_functions::send_joystick_event,
|
||||||
|
])
|
||||||
|
.setup(|app| {
|
||||||
|
*APP_HANDLE.lock().unwrap() = Some(app.handle());
|
||||||
|
|
||||||
let exit_code = app.run();
|
let _id2 = app.listen_global("webrtc-message", |event| {
|
||||||
|
debug!("Got webrtc-message event from Tauri client! {:#?}", event);
|
||||||
|
match event.payload() {
|
||||||
|
Some(payload) => {
|
||||||
|
if let Ok(e) = TO_MEC_REF.lock() {
|
||||||
|
match e.as_ref() {
|
||||||
|
Some(to_mec) => {
|
||||||
|
debug!("Sending message to the webrtc connection");
|
||||||
|
let message: Option<ApplicationMessage> = match payload {
|
||||||
|
s if s.starts_with("{\"type") => {
|
||||||
|
Some(ApplicationMessage::WebRTCPacket(
|
||||||
|
serde_json::from_str(payload)
|
||||||
|
.expect("Could not decode the browser's sdp"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
s if s.starts_with("{\"candidate") => {
|
||||||
|
Some(ApplicationMessage::WebRTCIceCandidateInit(
|
||||||
|
serde_json::from_str(payload).expect(
|
||||||
|
"Could not decode the browser's Ice Candidate",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
info!("Gtk application has closed");
|
if message.is_some() {
|
||||||
|
if let Err(e) =
|
||||||
|
to_mec.send_blocking(ApplicationEvent::WebRTCMessage((
|
||||||
|
"CameraSatellite_1".to_owned(),
|
||||||
|
message.unwrap(),
|
||||||
|
)))
|
||||||
|
{
|
||||||
|
error!("Could not send to mec! {e}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("Could not deserialize ui webrtc message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!("TO_MEC_REF was none!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("There was an empty payload!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
rt.block_on(async {});
|
// let _id = app.listen_global("webrtc-event", |event| {
|
||||||
|
// match event.payload() {
|
||||||
|
// Some(payload) => {
|
||||||
|
// if let Ok(e) = TO_MEC_REF.lock() {
|
||||||
|
// match e.as_ref() {
|
||||||
|
// Some(to_mec) => {
|
||||||
|
// if let Err(e) = to_mec.send_blocking(ApplicationEvent::WebRTCMessage(payload.to_string())) {
|
||||||
|
// error!("Could not send to mec! {e}");
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// None => {
|
||||||
|
// error!("TO_MEC_REF was none!");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// None => {
|
||||||
|
// info!("There was an empty payload!");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
|
||||||
info!("Tokio runtime has shut down");
|
let _ = to_mec.send_blocking(ApplicationEvent::Close);
|
||||||
|
|
||||||
exit_code
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
pub mod joystick_source;
|
|
|
@ -1,4 +0,0 @@
|
||||||
|
|
||||||
pub mod perf_state;
|
|
||||||
pub mod tracker_state;
|
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
use std::{collections::VecDeque, time::Duration};
|
|
||||||
|
|
||||||
use async_channel::Sender;
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::coordinator::TrackerUpdate;
|
|
||||||
use crate::ui::GuiUpdate;
|
|
||||||
|
|
||||||
const MAX_RECORDED_TIMES: usize = 10;
|
|
||||||
const DEGRADED_TRACKER_TIME: u128 = 150;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TrackerMetrics {
|
|
||||||
pub header_text: String,
|
|
||||||
pub fail_count: usize,
|
|
||||||
tracker_times: VecDeque<u128>,
|
|
||||||
to_gui: Sender<GuiUpdate>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TrackerMetrics {
|
|
||||||
pub fn new(to_gui: Sender<GuiUpdate>) -> Self {
|
|
||||||
let mut ret = TrackerMetrics {
|
|
||||||
header_text: String::from(""),
|
|
||||||
fail_count: 0,
|
|
||||||
|
|
||||||
tracker_times: VecDeque::with_capacity(MAX_RECORDED_TIMES),
|
|
||||||
to_gui,
|
|
||||||
};
|
|
||||||
ret.clear_times();
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_gui(&mut self) {
|
|
||||||
if let Err(e) = self.to_gui.send_blocking(GuiUpdate::TrackerUpdate(TrackerUpdate::HeaderUpdate(self.header_text.clone()))) {
|
|
||||||
error!("TrackerMetrics couldnt' send update to GUI: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn starting_connection(&mut self, fail_count: Option<usize>) {
|
|
||||||
self.clear_times();
|
|
||||||
|
|
||||||
self.header_text.clear();
|
|
||||||
match fail_count {
|
|
||||||
None => self.header_text.push_str("Status: Connecting ..."),
|
|
||||||
Some(v) => self.header_text.push_str(&format!("Status: Attempt {}/5", v)),
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_gui();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_times(&mut self) {
|
|
||||||
for _ in 0..10 {
|
|
||||||
self.tracker_times.pop_front();
|
|
||||||
}
|
|
||||||
self.header_text = "Status: Disconnected".to_string();
|
|
||||||
|
|
||||||
self.update_gui();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_time(&mut self, new_measurement: Duration) {
|
|
||||||
if self.tracker_times.len() > MAX_RECORDED_TIMES {
|
|
||||||
let _ = self.tracker_times.pop_front();
|
|
||||||
}
|
|
||||||
self.tracker_times.push_back(new_measurement.as_millis());
|
|
||||||
|
|
||||||
let avg_time = self.tracker_times.iter().sum::<u128>() / self.tracker_times.len() as u128;
|
|
||||||
|
|
||||||
if avg_time == 0 {
|
|
||||||
self.header_text = format!(
|
|
||||||
"Status: Failed Avg Response: {} ms",
|
|
||||||
avg_time
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if avg_time > DEGRADED_TRACKER_TIME {
|
|
||||||
self.header_text = format!(
|
|
||||||
"Status: Degraded Avg Response: {} ms",
|
|
||||||
avg_time
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.header_text = format!(
|
|
||||||
"Status: Nominal Avg Response: {} ms",
|
|
||||||
avg_time
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_gui();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
use std::{
|
|
||||||
cmp::{max, min},
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::ui::NormalizedBoxCoords;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TrackerState {
|
|
||||||
pub tracking_id: u32,
|
|
||||||
pub highlighted_id: Option<u32>,
|
|
||||||
pub last_detect: Instant,
|
|
||||||
pub enabled: bool,
|
|
||||||
|
|
||||||
pub update_ids: bool,
|
|
||||||
|
|
||||||
pub identity_boxes: Vec<NormalizedBoxCoords>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TrackerState {
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.tracking_id = 0;
|
|
||||||
self.highlighted_id = None;
|
|
||||||
self.last_detect = Instant::now();
|
|
||||||
self.enabled = false;
|
|
||||||
self.update_ids = false;
|
|
||||||
self.identity_boxes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_from_boxes(&mut self, new_boxes: Vec<NormalizedBoxCoords>) {
|
|
||||||
let mut old_ids: Vec<u32> = self.identity_boxes.iter().map(|x| x.id).collect();
|
|
||||||
old_ids.sort();
|
|
||||||
let mut new_ids: Vec<u32> = new_boxes.iter().map(|x| x.id).collect();
|
|
||||||
new_ids.sort();
|
|
||||||
|
|
||||||
self.update_ids = new_ids == old_ids;
|
|
||||||
|
|
||||||
self.identity_boxes = new_boxes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: have a return type that says "highlighted ID not found" and "no highlighted id selected"
|
|
||||||
// It's not really worth logging...
|
|
||||||
pub fn calculate_tracking(&mut self) -> core::result::Result<(i32, i32, bool), String> {
|
|
||||||
if let Some(target_box) = self
|
|
||||||
.identity_boxes
|
|
||||||
.iter()
|
|
||||||
.find(|e| e.id == self.tracking_id)
|
|
||||||
{
|
|
||||||
let x_adjust = calc_x_adjust(target_box.x1, target_box.x2);
|
|
||||||
let y_adjust = calc_y_adjust(target_box.y1);
|
|
||||||
self.last_detect = std::time::Instant::now();
|
|
||||||
Ok((x_adjust, y_adjust, self.enabled))
|
|
||||||
} else {
|
|
||||||
Err("Couldn't find target in results".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calc_x_adjust(x1: f32, x2: f32) -> i32 {
|
|
||||||
let dist_from_center = ((x1 + x2) / 2.0) - 0.5;
|
|
||||||
let mut x_adjust = ((dist_from_center / 0.5 * 2.0) * 100.0) as i32;
|
|
||||||
if x_adjust < 15 && x_adjust > -15 {
|
|
||||||
x_adjust = 0;
|
|
||||||
}
|
|
||||||
min(max(x_adjust, -100), 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calc_y_adjust(y1: f32) -> i32 {
|
|
||||||
// All values are normalized, then multiplied by 1000. 500 == 50% of the screen
|
|
||||||
let mut y_adjust = ((y1 - 0.1) * 250.0) as i32;
|
|
||||||
if y_adjust < 0 {
|
|
||||||
y_adjust -= 20;
|
|
||||||
} else if y_adjust < 30 {
|
|
||||||
y_adjust = 0;
|
|
||||||
} else {
|
|
||||||
y_adjust = (y_adjust as f32 * 0.75) as i32;
|
|
||||||
}
|
|
||||||
min(max(y_adjust, -100), 100)
|
|
||||||
}
|
|
46
src/tauri_functions.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
use async_channel::Sender;
|
||||||
|
use tauri::State;
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::coordinator::{ApplicationEvent, Point};
|
||||||
|
|
||||||
|
pub struct TauriState {
|
||||||
|
pub to_mec: Sender<ApplicationEvent>,
|
||||||
|
pub rt: Handle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn connect_to_camera(state: State<'_, TauriState>) {
|
||||||
|
let mec = state.to_mec.clone();
|
||||||
|
state.rt.spawn(async move {
|
||||||
|
let _ = mec.send_blocking(ApplicationEvent::RetryDisconnectedSatellites);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command(rename_all = "snake_case")]
|
||||||
|
pub fn supports_webrtc(has_support: bool, state: State<'_, TauriState>) {
|
||||||
|
let mec = state.to_mec.clone();
|
||||||
|
state.rt.spawn(async move {
|
||||||
|
let _ = mec.send_blocking(ApplicationEvent::SupportsWebRTC(has_support));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command(rename_all = "snake_case")]
|
||||||
|
pub fn change_target_satellite(new_target_name: String, state: State<'_, TauriState>) {
|
||||||
|
let mec = state.to_mec.clone();
|
||||||
|
state.rt.spawn(async move {
|
||||||
|
let _ = mec.send_blocking(ApplicationEvent::ChangeTargetSatellite(new_target_name));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command(rename_all = "snake_case")]
|
||||||
|
pub fn send_joystick_event(x: f32, y: f32, state: State<'_, TauriState>) {
|
||||||
|
let mec = state.to_mec.clone();
|
||||||
|
state.rt.spawn(async move {
|
||||||
|
let _ = mec.send_blocking(ApplicationEvent::JoystickMove(Point {
|
||||||
|
x: (x * 100.0) as i32,
|
||||||
|
y: (y * 100.0) as i32,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,188 +0,0 @@
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use async_channel::Sender;
|
|
||||||
use gtk::{
|
|
||||||
glib::{self, object::CastNone},
|
|
||||||
prelude::{
|
|
||||||
BoxExt, ButtonExt, Cast, GObjectPropertyExpressionExt, ListItemExt, ToggleButtonExt,
|
|
||||||
},
|
|
||||||
Box, Button, Expander, Label, ListItem, ListView, ScrolledWindow, SignalListItemFactory,
|
|
||||||
SingleSelection, StringList, StringObject, ToggleButton, Widget,
|
|
||||||
};
|
|
||||||
use tracing::{error, event, span, Level};
|
|
||||||
|
|
||||||
#[cfg(feature = "tracker-state-debug")]
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::coordinator::ApplicationEvent;
|
|
||||||
use crate::states::tracker_state::TrackerState;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ControlPanel {
|
|
||||||
top_level: Box,
|
|
||||||
|
|
||||||
pub connection_buttons: ExpanderMenu,
|
|
||||||
pub current_id: Label,
|
|
||||||
|
|
||||||
pub items: StringList,
|
|
||||||
pub list_view: ListView,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ExpanderMenu {
|
|
||||||
pub top_level: Expander,
|
|
||||||
|
|
||||||
pub camera_connection: Button,
|
|
||||||
pub tracker_enable_toggle: ToggleButton,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ControlPanel {
|
|
||||||
pub fn new(tracker_state: Arc<Mutex<TrackerState>>) -> ControlPanel {
|
|
||||||
let factory = SignalListItemFactory::new();
|
|
||||||
factory.connect_setup(move |_, list_item| {
|
|
||||||
let list_item = list_item
|
|
||||||
.downcast_ref::<ListItem>()
|
|
||||||
.expect("Needs to be a List Item");
|
|
||||||
|
|
||||||
let label = Label::new(None);
|
|
||||||
list_item.set_child(Some(&label));
|
|
||||||
|
|
||||||
list_item
|
|
||||||
.property_expression("item")
|
|
||||||
.chain_property::<StringObject>("string")
|
|
||||||
.bind(&label, "label", Widget::NONE);
|
|
||||||
});
|
|
||||||
|
|
||||||
let items = StringList::new(&["Please connect automatic source"]);
|
|
||||||
|
|
||||||
let model = SingleSelection::builder()
|
|
||||||
.model(&items)
|
|
||||||
.autoselect(false)
|
|
||||||
.can_unselect(false)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
model.connect_selected_item_notify(move |x| {
|
|
||||||
let item = x.selected_item().and_downcast::<StringObject>();
|
|
||||||
|
|
||||||
if let Some(item) = item {
|
|
||||||
if let Ok(id) = item.string().parse::<u32>() {
|
|
||||||
#[cfg(feature = "tracker-state-debug")]
|
|
||||||
debug!("Getting lock on tracker state for setting active tracking id!");
|
|
||||||
if let Ok(mut ts) = tracker_state.lock() {
|
|
||||||
ts.tracking_id = id;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("An unparsable ID was clicked");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("An invalid id was selected from the selection");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let list_view = ListView::new(Some(model), Some(factory));
|
|
||||||
|
|
||||||
let scrolled_window = ScrolledWindow::builder()
|
|
||||||
.child(&list_view)
|
|
||||||
.hscrollbar_policy(gtk::PolicyType::Never)
|
|
||||||
.height_request(200)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let top_level = Box::builder()
|
|
||||||
.orientation(gtk::Orientation::Vertical)
|
|
||||||
.spacing(5)
|
|
||||||
.margin_top(24)
|
|
||||||
.margin_start(24)
|
|
||||||
.margin_end(24)
|
|
||||||
.margin_bottom(12)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let expander = ExpanderMenu::new();
|
|
||||||
|
|
||||||
let current_id = Label::builder()
|
|
||||||
.label("Not Tracking")
|
|
||||||
.can_focus(false)
|
|
||||||
.can_target(false)
|
|
||||||
.css_classes(["current-id"])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
top_level.append(&expander.top_level);
|
|
||||||
top_level.append(¤t_id);
|
|
||||||
top_level.append(&scrolled_window);
|
|
||||||
|
|
||||||
ControlPanel {
|
|
||||||
top_level,
|
|
||||||
|
|
||||||
connection_buttons: expander,
|
|
||||||
current_id,
|
|
||||||
|
|
||||||
items,
|
|
||||||
list_view,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_top_level(&self) -> &Box {
|
|
||||||
&self.top_level
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect_button_callbacks(&self, to_mec: Sender<ApplicationEvent>) {
|
|
||||||
self.connection_buttons
|
|
||||||
.tracker_enable_toggle
|
|
||||||
.connect_clicked(glib::clone!(@strong to_mec => move |button| {
|
|
||||||
let span = span!(Level::TRACE, "tracker_enable_toggle callback");
|
|
||||||
let _enter = span.enter();
|
|
||||||
if let Err(e) =
|
|
||||||
to_mec.send_blocking(ApplicationEvent::EnableAutomatic(button.is_active()))
|
|
||||||
{
|
|
||||||
event!(Level::ERROR, error = ?e, "Could not send message to the MEC");
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.connection_buttons.camera_connection.connect_clicked(glib::clone!(@strong to_mec => move |_button| {
|
|
||||||
let span = span!(Level::TRACE, "camera_connection callback");
|
|
||||||
let _enter = span.enter();
|
|
||||||
match to_mec.try_send(ApplicationEvent::CameraConnectionPress) {
|
|
||||||
Ok(_) => {},
|
|
||||||
Err(async_channel::TrySendError::Closed(_)) => panic!("Coordinator MEC is closed. Unrecoverable error."),
|
|
||||||
Err(e) => event!(Level::ERROR, error = ?e, message = "There was an error sending to the MEC"),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExpanderMenu {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let content_box = Box::builder()
|
|
||||||
.orientation(gtk::Orientation::Vertical)
|
|
||||||
.spacing(10)
|
|
||||||
.margin_top(12)
|
|
||||||
.margin_start(24)
|
|
||||||
.margin_end(24)
|
|
||||||
.margin_bottom(12)
|
|
||||||
.build();
|
|
||||||
let expander = Expander::builder()
|
|
||||||
.child(&content_box)
|
|
||||||
.expanded(true)
|
|
||||||
.label("Connections")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let camera_connection = Button::builder()
|
|
||||||
.label("Connect to Camera")
|
|
||||||
.margin_top(12)
|
|
||||||
.build();
|
|
||||||
let tracker_enable_toggle = ToggleButton::builder()
|
|
||||||
.label("Connect to Tracker Computer")
|
|
||||||
.active(false)
|
|
||||||
.margin_top(12)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
content_box.append(&camera_connection);
|
|
||||||
content_box.append(&tracker_enable_toggle);
|
|
||||||
|
|
||||||
ExpanderMenu {
|
|
||||||
top_level: expander,
|
|
||||||
|
|
||||||
camera_connection,
|
|
||||||
tracker_enable_toggle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,214 +0,0 @@
|
||||||
use std::{
|
|
||||||
cmp::Ordering,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use async_channel::Sender;
|
|
||||||
use gtk::{
|
|
||||||
gdk::Paintable,
|
|
||||||
prelude::{BoxExt, GestureExt, WidgetExt},
|
|
||||||
AspectFrame, Box, DrawingArea, EventControllerMotion, GestureClick, Label, Overlay, Picture,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::coordinator::ApplicationEvent;
|
|
||||||
use crate::states::tracker_state::TrackerState;
|
|
||||||
|
|
||||||
use super::NormalizedBoxCoords;
|
|
||||||
|
|
||||||
pub struct LiveViewPanel {
|
|
||||||
top_level: gtk::Box,
|
|
||||||
|
|
||||||
pub tracker_status_label: Label,
|
|
||||||
pub cam_status_label: Label,
|
|
||||||
pub adjustment_label: Label,
|
|
||||||
picture: Picture,
|
|
||||||
overlay: Overlay,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LiveViewPanel {
|
|
||||||
pub fn new(tracker_state: Arc<Mutex<TrackerState>>, to_mec: Sender<ApplicationEvent>) -> Self {
|
|
||||||
let right_box = gtk::Box::builder()
|
|
||||||
.orientation(gtk::Orientation::Vertical)
|
|
||||||
.hexpand(true)
|
|
||||||
.valign(gtk::Align::Center)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let tracker_status_label = Label::builder()
|
|
||||||
.label("No Status Yet".to_string())
|
|
||||||
.can_focus(true)
|
|
||||||
.css_classes(vec!["large-label", "NoConnection"])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let cam_status_label = Label::builder()
|
|
||||||
.label("No Connection".to_string())
|
|
||||||
.css_classes(vec!["NoConnection"])
|
|
||||||
.can_focus(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let adjustment_label = Label::builder()
|
|
||||||
.label("X: 0 Y: )")
|
|
||||||
.justify(gtk::Justification::Center)
|
|
||||||
.css_classes(vec!["JoystickCurrent"])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let webcam_picture = gtk::Picture::builder().can_focus(false).build();
|
|
||||||
|
|
||||||
let overlay_box = gtk::Overlay::builder().build();
|
|
||||||
overlay_box.set_child(Some(&webcam_picture));
|
|
||||||
|
|
||||||
let click_handler = GestureClick::builder()
|
|
||||||
.button(gtk::gdk::ffi::GDK_BUTTON_PRIMARY as u32)
|
|
||||||
.propagation_limit(gtk::PropagationLimit::SameNative)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let move_handler = EventControllerMotion::builder()
|
|
||||||
.propagation_limit(gtk::PropagationLimit::SameNative)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let tracker_state_2 = tracker_state.clone();
|
|
||||||
|
|
||||||
let handler_picture = webcam_picture.clone();
|
|
||||||
move_handler.connect_motion(move |_motion_handler, x, y| {
|
|
||||||
LiveViewPanel::motion_callback(&handler_picture, &tracker_state_2, x, y);
|
|
||||||
});
|
|
||||||
|
|
||||||
let handler_picture = webcam_picture.clone();
|
|
||||||
|
|
||||||
click_handler.connect_pressed(move |gesture, _id, x, y| {
|
|
||||||
gesture.set_state(gtk::EventSequenceState::Claimed);
|
|
||||||
LiveViewPanel::click_gesture_callback(&handler_picture, &tracker_state, &to_mec, x, y)
|
|
||||||
});
|
|
||||||
|
|
||||||
overlay_box.add_controller(click_handler);
|
|
||||||
overlay_box.add_controller(move_handler);
|
|
||||||
|
|
||||||
let aspect = AspectFrame::builder()
|
|
||||||
.ratio(16.0 / 9.0)
|
|
||||||
.obey_child(false)
|
|
||||||
.child(&overlay_box)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
right_box.append(&tracker_status_label);
|
|
||||||
right_box.append(&aspect);
|
|
||||||
right_box.append(&cam_status_label);
|
|
||||||
right_box.append(&adjustment_label);
|
|
||||||
|
|
||||||
LiveViewPanel {
|
|
||||||
adjustment_label,
|
|
||||||
tracker_status_label,
|
|
||||||
cam_status_label,
|
|
||||||
overlay: overlay_box,
|
|
||||||
picture: webcam_picture,
|
|
||||||
top_level: right_box,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn click_gesture_callback(
|
|
||||||
overlay: &Picture,
|
|
||||||
tracker_state: &Arc<Mutex<TrackerState>>,
|
|
||||||
to_mec: &Sender<ApplicationEvent>,
|
|
||||||
x_coord: f64,
|
|
||||||
y_coord: f64,
|
|
||||||
) {
|
|
||||||
let x_size = overlay.size(gtk::Orientation::Horizontal);
|
|
||||||
let y_size = overlay.size(gtk::Orientation::Vertical);
|
|
||||||
let x_coord = x_coord as f32 / x_size as f32;
|
|
||||||
let y_coord = y_coord as f32 / y_size as f32;
|
|
||||||
|
|
||||||
if let Ok(mut ts) = tracker_state.lock() {
|
|
||||||
if let Some(v) = calc_box_under_mouse(&ts.identity_boxes, x_coord, y_coord) {
|
|
||||||
ts.tracking_id = v;
|
|
||||||
if let Err(e) = to_mec.send_blocking(ApplicationEvent::ChangeTracking(v)) {
|
|
||||||
panic!("Could not send message to MEC, unrecoverable: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn motion_callback(
|
|
||||||
overlay: &Picture,
|
|
||||||
tracker_state: &Arc<Mutex<TrackerState>>,
|
|
||||||
x_coord: f64,
|
|
||||||
y_coord: f64,
|
|
||||||
) {
|
|
||||||
let x_size = overlay.size(gtk::Orientation::Horizontal);
|
|
||||||
let y_size = overlay.size(gtk::Orientation::Vertical);
|
|
||||||
let x_coord = x_coord as f32 / x_size as f32;
|
|
||||||
let y_coord = y_coord as f32 / y_size as f32;
|
|
||||||
|
|
||||||
if let Ok(mut ts) = tracker_state.lock() {
|
|
||||||
ts.highlighted_id = calc_box_under_mouse(&ts.identity_boxes, x_coord, y_coord);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_top_level(&self) -> &Box {
|
|
||||||
&self.top_level
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_drawable(&self, new_drawable: &DrawingArea) {
|
|
||||||
self.overlay.add_overlay(new_drawable);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_paintable(&self, new_paintable: &Paintable) {
|
|
||||||
self.picture.set_paintable(Some(new_paintable));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calc_box_under_mouse(
|
|
||||||
boxes: &[NormalizedBoxCoords],
|
|
||||||
x_coord: f32,
|
|
||||||
y_coord: f32,
|
|
||||||
) -> Option<u32> {
|
|
||||||
let mut mouse_over: Vec<NormalizedBoxCoords> = vec![];
|
|
||||||
|
|
||||||
for nb in boxes.iter() {
|
|
||||||
if nb.x1 < x_coord
|
|
||||||
&& nb.y1 < y_coord
|
|
||||||
&& nb.x2 > x_coord
|
|
||||||
&& nb.y2 > y_coord
|
|
||||||
{
|
|
||||||
mouse_over.push(*nb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if mouse_over.len() > 1 {
|
|
||||||
let mut x_coords = mouse_over
|
|
||||||
.iter()
|
|
||||||
.flat_map(|b| [b.x1, b.x2])
|
|
||||||
.collect::<Vec<f32>>();
|
|
||||||
x_coords.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
||||||
|
|
||||||
let mid = x_coords.len() / 2;
|
|
||||||
let median_two_x = &x_coords[mid - 1..mid + 1];
|
|
||||||
|
|
||||||
let mut y_coords = mouse_over
|
|
||||||
.iter()
|
|
||||||
.flat_map(|b| [b.y1, b.y2])
|
|
||||||
.collect::<Vec<f32>>();
|
|
||||||
y_coords.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
||||||
let median_two_y = &y_coords[mid - 1..mid + 1];
|
|
||||||
|
|
||||||
let overlap_area =
|
|
||||||
(median_two_x[1] - median_two_x[0]) * (median_two_y[1] - median_two_y[0]);
|
|
||||||
|
|
||||||
// We want the one with the largest percentage of it's area as overlap
|
|
||||||
// to be the highlighted one
|
|
||||||
mouse_over.sort_by(|a, b| {
|
|
||||||
let result =
|
|
||||||
((a.area() - overlap_area) / a.area()) - ((b.area() - overlap_area) / b.area());
|
|
||||||
if 0.0001 > result && result > -0.0001 {
|
|
||||||
Ordering::Equal
|
|
||||||
} else if result > 0.0 {
|
|
||||||
Ordering::Greater
|
|
||||||
} else {
|
|
||||||
Ordering::Less
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mouse_over
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.id)
|
|
||||||
.collect::<Vec<u32>>()
|
|
||||||
.first()
|
|
||||||
.copied()
|
|
||||||
}
|
|
411
src/ui/mod.rs
|
@ -1,411 +0,0 @@
|
||||||
use std::fmt::Display;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use gtk::cairo::Context;
|
|
||||||
use gtk::gdk::Paintable;
|
|
||||||
use gtk::glib::clone;
|
|
||||||
use gtk::{gio, glib, prelude::*, CssProvider};
|
|
||||||
use gtk::{Application, ApplicationWindow};
|
|
||||||
use log::{error, info};
|
|
||||||
use tokio::runtime::Handle;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
|
||||||
use crate::states::tracker_state::TrackerState;
|
|
||||||
use crate::coordinator::{start_coordinator, ApplicationEvent, MoveEvent, TrackerUpdate};
|
|
||||||
|
|
||||||
mod control_panel;
|
|
||||||
mod liveview_panel;
|
|
||||||
mod settings_modal;
|
|
||||||
|
|
||||||
use control_panel::ControlPanel;
|
|
||||||
use liveview_panel::LiveViewPanel;
|
|
||||||
|
|
||||||
pub enum GuiUpdate {
|
|
||||||
SocketDisconnected,
|
|
||||||
SocketConnecting,
|
|
||||||
SocketConnected,
|
|
||||||
MoveEvent(MoveEvent),
|
|
||||||
UpdatePaintable(gstreamer::Element),
|
|
||||||
TrackerUpdate(TrackerUpdate),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct BoxCoords {
|
|
||||||
pub id: u32,
|
|
||||||
pub x1: u32,
|
|
||||||
pub y1: u32,
|
|
||||||
pub x2: u32,
|
|
||||||
pub y2: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for BoxCoords {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Absolute Box {}, x1: {}, y1: {}, x2: {}, y2: {}",
|
|
||||||
self.id, self.x1, self.y1, self.x2, self.y2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct NormalizedBoxCoords {
|
|
||||||
pub id: u32,
|
|
||||||
pub x1: f32,
|
|
||||||
pub y1: f32,
|
|
||||||
pub x2: f32,
|
|
||||||
pub y2: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NormalizedBoxCoords {
|
|
||||||
fn absolute_coords(&self, width: i32, height: i32) -> BoxCoords {
|
|
||||||
BoxCoords {
|
|
||||||
id: self.id,
|
|
||||||
x1: (self.x1 * width as f32) as u32,
|
|
||||||
y1: (self.y1 * height as f32) as u32,
|
|
||||||
x2: (self.x2 * width as f32) as u32,
|
|
||||||
y2: (self.y2 * height as f32) as u32,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn area(&self) -> f32 {
|
|
||||||
(self.x2 - self.x1) * (self.y2 - self.y1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for NormalizedBoxCoords {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Normalized Box {}, x1: {}, y1: {}, x2: {}, y2: {}",
|
|
||||||
self.id, self.x1, self.y1, self.x2, self.y2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_activate(app: &Application) {
|
|
||||||
let provider = CssProvider::new();
|
|
||||||
provider.load_from_string(include_str!("../../style.css"));
|
|
||||||
|
|
||||||
gtk::style_context_add_provider_for_display(
|
|
||||||
>k::gdk::Display::default().expect("Could not connect to a display"),
|
|
||||||
&provider,
|
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
|
|
||||||
let menubar = {
|
|
||||||
let connections_menu = {
|
|
||||||
let settings = gio::MenuItem::new(Some("Edit IPs"), Some("app.connections"));
|
|
||||||
|
|
||||||
let connections_menu = gio::Menu::new();
|
|
||||||
connections_menu.append_item(&settings);
|
|
||||||
|
|
||||||
connections_menu
|
|
||||||
};
|
|
||||||
|
|
||||||
let menubar = gio::Menu::new();
|
|
||||||
menubar.append_submenu(Some("Settings"), &connections_menu);
|
|
||||||
|
|
||||||
menubar
|
|
||||||
};
|
|
||||||
|
|
||||||
app.set_menubar(Some(&menubar));
|
|
||||||
info!("Menu bar set up");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_ui(app: &Application, config: Arc<RwLock<AppConfig>>, runtime: Handle) {
|
|
||||||
let main_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
|
||||||
let left_box = gtk::Box::builder()
|
|
||||||
.width_request(300)
|
|
||||||
.orientation(gtk::Orientation::Vertical)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Create a window
|
|
||||||
let window = ApplicationWindow::builder()
|
|
||||||
.application(app)
|
|
||||||
.title("VCC Camera Controller")
|
|
||||||
.show_menubar(true)
|
|
||||||
.default_width(840)
|
|
||||||
.default_height(480)
|
|
||||||
.child(&main_box)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let rt = runtime.clone();
|
|
||||||
let connections_modal_config = config.clone();
|
|
||||||
let connections_activate = gio::ActionEntry::builder("connections")
|
|
||||||
.activate(clone!(@weak window => move |app: >k::Application, _, _| {
|
|
||||||
let connections_modal = settings_modal::ConnectionsModal::new(app, &window, &rt, &connections_modal_config);
|
|
||||||
|
|
||||||
connections_modal.window.present();
|
|
||||||
}))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
app.add_action_entries([connections_activate]);
|
|
||||||
|
|
||||||
// Main Event Channel
|
|
||||||
let (to_mec, mec) = async_channel::bounded::<ApplicationEvent>(10);
|
|
||||||
let (to_gui, gui_recv) = async_channel::bounded::<GuiUpdate>(10);
|
|
||||||
let tracker_state = Arc::new(Mutex::new(TrackerState {
|
|
||||||
tracking_id: 0,
|
|
||||||
highlighted_id: None,
|
|
||||||
last_detect: Instant::now(),
|
|
||||||
enabled: true,
|
|
||||||
|
|
||||||
identity_boxes: vec![],
|
|
||||||
update_ids: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let coord_config = config.clone();
|
|
||||||
|
|
||||||
runtime.spawn(start_coordinator(
|
|
||||||
mec,
|
|
||||||
to_mec.clone(),
|
|
||||||
to_gui,
|
|
||||||
runtime.clone(),
|
|
||||||
coord_config,
|
|
||||||
));
|
|
||||||
|
|
||||||
let control_panel = ControlPanel::new(tracker_state.clone());
|
|
||||||
control_panel.connect_button_callbacks(to_mec.clone());
|
|
||||||
|
|
||||||
left_box.append(control_panel.get_top_level());
|
|
||||||
|
|
||||||
main_box.append(&left_box);
|
|
||||||
|
|
||||||
let liveview_panel = LiveViewPanel::new(tracker_state.clone(), to_mec.clone());
|
|
||||||
main_box.append(liveview_panel.get_top_level());
|
|
||||||
|
|
||||||
let drawable = gtk::DrawingArea::builder().build();
|
|
||||||
|
|
||||||
let drawable_ts = tracker_state.clone();
|
|
||||||
drawable.set_draw_func(move |_, ctx, width, height| {
|
|
||||||
draw_boxes(width, height, ctx, &drawable_ts);
|
|
||||||
});
|
|
||||||
|
|
||||||
liveview_panel.set_drawable(&drawable);
|
|
||||||
|
|
||||||
let items = control_panel.items.clone();
|
|
||||||
let id_label = control_panel.current_id.clone();
|
|
||||||
let model = control_panel
|
|
||||||
.list_view
|
|
||||||
.model()
|
|
||||||
.expect("The list view should have a model!");
|
|
||||||
|
|
||||||
let tracker_state_2 = tracker_state.clone();
|
|
||||||
|
|
||||||
glib::timeout_add_local(Duration::from_millis(500), move || {
|
|
||||||
#[cfg(feature = "tracker-state-debug")]
|
|
||||||
debug!("Getting lock on tracker state for checking identity boxes");
|
|
||||||
|
|
||||||
// don't update the stringlist until after letting go of the tracker state
|
|
||||||
// due to async interweaving causing a mutex deadlock
|
|
||||||
let mut ids: Option<Vec<String>> = None;
|
|
||||||
let mut current_id: u32 = 0;
|
|
||||||
if let Ok(ts) = tracker_state.lock() {
|
|
||||||
current_id = ts.tracking_id;
|
|
||||||
if ts.update_ids {
|
|
||||||
ids = Some(ts.identity_boxes.iter().map(|t| t.id.to_string()).collect());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_id > 0 {
|
|
||||||
id_label.set_text(current_id.to_string().as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_id: String = current_id.to_string();
|
|
||||||
|
|
||||||
if let Some(mut ids) = ids {
|
|
||||||
let mut active_index = 0;
|
|
||||||
let mut to_delete_indexes: Vec<u32> = vec![];
|
|
||||||
|
|
||||||
// find all indexes where matching item does not exist in
|
|
||||||
// the ids, list, and remove all items in the ids list
|
|
||||||
// that already exists in the stringList
|
|
||||||
for i in 0..items.n_items() {
|
|
||||||
let item = items.string(i).unwrap_or("Empty".into()).to_string();
|
|
||||||
if item == current_id {
|
|
||||||
active_index = i;
|
|
||||||
}
|
|
||||||
if !ids.contains(&item) {
|
|
||||||
to_delete_indexes.push(i);
|
|
||||||
} else if let Some(pos) = ids.iter().position(|x| **x == item) {
|
|
||||||
ids.remove(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if active_index > 0 {
|
|
||||||
model.select_item(active_index, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// invert the order so we don't have to adjust after each deletion
|
|
||||||
to_delete_indexes.sort();
|
|
||||||
to_delete_indexes.reverse();
|
|
||||||
|
|
||||||
to_delete_indexes.iter().for_each(|x| {
|
|
||||||
items.remove(*x);
|
|
||||||
});
|
|
||||||
items.splice(
|
|
||||||
items.n_items(),
|
|
||||||
0,
|
|
||||||
&ids.iter().map(|x| x.as_str()).collect::<Vec<&str>>()[0..],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
glib::ControlFlow::Continue
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut tracker_status_label = liveview_panel.tracker_status_label.clone();
|
|
||||||
let mut tracker_enable_toggle = control_panel
|
|
||||||
.connection_buttons
|
|
||||||
.tracker_enable_toggle
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
glib::spawn_future_local(glib::clone!(@weak drawable => async move {
|
|
||||||
while let Ok(d) = gui_recv.recv().await {
|
|
||||||
drawable.queue_draw();
|
|
||||||
match d {
|
|
||||||
GuiUpdate::MoveEvent(msg) => {
|
|
||||||
liveview_panel.adjustment_label.set_text(
|
|
||||||
format!("X: {:>4} Y: {:>4}", msg.x, msg.y).as_str()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
GuiUpdate::SocketConnected => {
|
|
||||||
control_panel.connection_buttons.camera_connection.set_sensitive(true);
|
|
||||||
control_panel.connection_buttons.camera_connection.set_label("Disconnect Camera");
|
|
||||||
liveview_panel.cam_status_label.set_label("Connected");
|
|
||||||
|
|
||||||
liveview_panel.cam_status_label.set_css_classes(&["YesConnection"]);
|
|
||||||
|
|
||||||
},
|
|
||||||
GuiUpdate::SocketConnecting => {
|
|
||||||
control_panel.connection_buttons.camera_connection.set_sensitive(false);
|
|
||||||
control_panel.connection_buttons.camera_connection.set_label("Please wait");
|
|
||||||
liveview_panel.cam_status_label.set_label("Connecting");
|
|
||||||
|
|
||||||
liveview_panel.cam_status_label.set_css_classes(&["LoadingConnection"]);
|
|
||||||
|
|
||||||
},
|
|
||||||
GuiUpdate::SocketDisconnected => {
|
|
||||||
control_panel.connection_buttons.camera_connection.set_sensitive(true);
|
|
||||||
control_panel.connection_buttons.camera_connection.set_label("Connect to Camera");
|
|
||||||
liveview_panel.cam_status_label.set_label("Not Connected to Camera");
|
|
||||||
|
|
||||||
liveview_panel.cam_status_label.set_css_classes(&["NoConnection"]);
|
|
||||||
}
|
|
||||||
GuiUpdate::UpdatePaintable(sink) => {
|
|
||||||
let paintable = sink.property::<Paintable>("paintable");
|
|
||||||
|
|
||||||
liveview_panel.set_paintable(&paintable);
|
|
||||||
}
|
|
||||||
GuiUpdate::TrackerUpdate(update) => match update {
|
|
||||||
TrackerUpdate::Fail => {}
|
|
||||||
TrackerUpdate::HeaderUpdate(new_header) => {
|
|
||||||
update_tracker_header(new_header, &mut tracker_status_label, &mut tracker_enable_toggle)
|
|
||||||
}
|
|
||||||
TrackerUpdate::Clear => {
|
|
||||||
if let Ok(mut ts) = tracker_state_2.lock() {
|
|
||||||
ts.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TrackerUpdate::Update(update) => {
|
|
||||||
if let Ok(mut ts) = tracker_state_2.lock() {
|
|
||||||
ts.update_from_boxes(update.boxes);
|
|
||||||
ts.last_detect = update.time;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info!("Closing update loop");
|
|
||||||
}));
|
|
||||||
|
|
||||||
window.connect_close_request(move |_| glib::Propagation::Proceed);
|
|
||||||
|
|
||||||
// Present window
|
|
||||||
window.present();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_tracker_header(new_header: String, tracker_status_label: &mut gtk::Label, tracker_enable_toggle: &mut gtk::ToggleButton) {
|
|
||||||
tracker_status_label.set_text(&new_header);
|
|
||||||
if new_header.contains("Failed") || new_header.contains("Disconnected") {
|
|
||||||
tracker_status_label.set_css_classes(&["NoConnection"]);
|
|
||||||
|
|
||||||
tracker_enable_toggle.set_label("Connect to Tracker Computer");
|
|
||||||
tracker_enable_toggle.set_active(false);
|
|
||||||
tracker_enable_toggle.set_sensitive(true);
|
|
||||||
} else if new_header.contains("Degraded") {
|
|
||||||
tracker_status_label.set_css_classes(&["LoadingConnection"]);
|
|
||||||
|
|
||||||
tracker_enable_toggle.set_label("Disconnect Tracker Computer");
|
|
||||||
tracker_enable_toggle.set_active(true);
|
|
||||||
tracker_enable_toggle.set_sensitive(true);
|
|
||||||
} else if new_header.contains("Connecting") {
|
|
||||||
tracker_status_label.set_css_classes(&["LoadingConnection"]);
|
|
||||||
|
|
||||||
tracker_enable_toggle.set_label("Please Wait");
|
|
||||||
tracker_enable_toggle.set_active(false);
|
|
||||||
tracker_enable_toggle.set_sensitive(false);
|
|
||||||
} else if new_header.contains("Nominal") {
|
|
||||||
tracker_status_label.set_css_classes(&["YesConnection"]);
|
|
||||||
|
|
||||||
tracker_enable_toggle.set_label("Disconnect Tracker Computer");
|
|
||||||
tracker_enable_toggle.set_active(true);
|
|
||||||
tracker_enable_toggle.set_sensitive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_boxes(width: i32, height: i32, ctx: &Context, tracker_state: &Arc<Mutex<TrackerState>>) {
|
|
||||||
ctx.set_line_width(5.0);
|
|
||||||
ctx.select_font_face(
|
|
||||||
"Arial",
|
|
||||||
gtk::cairo::FontSlant::Normal,
|
|
||||||
gtk::cairo::FontWeight::Bold,
|
|
||||||
);
|
|
||||||
ctx.set_font_size(24.0);
|
|
||||||
|
|
||||||
#[cfg(feature = "tracker-state-debug")]
|
|
||||||
debug!("Getting tracker state for drawing boxes");
|
|
||||||
if let Ok(ts) = tracker_state.lock() {
|
|
||||||
let active = ts.tracking_id;
|
|
||||||
let highlighted_id = ts.highlighted_id.unwrap_or(0);
|
|
||||||
for nb in ts.identity_boxes.iter() {
|
|
||||||
if nb.id == active {
|
|
||||||
ctx.set_source_rgb(1.0, 0.0, 0.0);
|
|
||||||
} else if nb.id == highlighted_id {
|
|
||||||
ctx.set_source_rgb(0.0, 1.0, 1.0);
|
|
||||||
} else {
|
|
||||||
ctx.set_source_rgb(0.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if nb.id == active && nb.id == highlighted_id {
|
|
||||||
ctx.set_source_rgb(1.0, 1.0, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let b = nb.absolute_coords(width, height);
|
|
||||||
ctx.rectangle(
|
|
||||||
b.x1 as f64,
|
|
||||||
b.y1 as f64,
|
|
||||||
(b.x2 - b.x1) as f64,
|
|
||||||
(b.y2 - b.y1) as f64,
|
|
||||||
);
|
|
||||||
if let Err(e) = ctx.stroke() {
|
|
||||||
error!("Could not draw cairo stroke: {e}");
|
|
||||||
}
|
|
||||||
ctx.stroke()
|
|
||||||
.expect("GTK Cario context did not have a stroke!");
|
|
||||||
|
|
||||||
ctx.move_to(b.x1 as f64 + 5.0, b.y1 as f64 + 18.0);
|
|
||||||
ctx.set_source_rgb(0.0, 0.0, 0.0);
|
|
||||||
ctx.show_text(&format!("[{}]", b.id))
|
|
||||||
.expect("Couldn't show text!");
|
|
||||||
|
|
||||||
ctx.stroke()
|
|
||||||
.expect("GTK Cario context did not have a stroke!");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("Could not get lock on boxes for drawing on the draw area!");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,161 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use gtk::glib::{self, clone};
|
|
||||||
use gtk::{
|
|
||||||
prelude::{BoxExt, ButtonExt, EditableExt, GtkWindowExt},
|
|
||||||
Application, ApplicationWindow, Button, Entry, Label, Window,
|
|
||||||
};
|
|
||||||
use log::{error, info};
|
|
||||||
use tokio::runtime::Handle;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
use crate::config::{save_config, AppConfig};
|
|
||||||
|
|
||||||
pub struct ConnectionsModal {
|
|
||||||
pub window: Window,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectionsModal {
|
|
||||||
pub fn new(
|
|
||||||
app: &Application,
|
|
||||||
parent: &ApplicationWindow,
|
|
||||||
rt: &Handle,
|
|
||||||
app_config: &Arc<RwLock<AppConfig>>,
|
|
||||||
) -> Self {
|
|
||||||
// Send help :(
|
|
||||||
let config_read = rt.block_on(async { app_config.read().await });
|
|
||||||
|
|
||||||
let main_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
||||||
|
|
||||||
let window = Window::builder()
|
|
||||||
.application(app)
|
|
||||||
.title("Change Connection Information")
|
|
||||||
.modal(true)
|
|
||||||
.transient_for(parent)
|
|
||||||
.destroy_with_parent(true)
|
|
||||||
.resizable(false)
|
|
||||||
.child(&main_box)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let camera_label = Label::builder()
|
|
||||||
.label("Camera Connection")
|
|
||||||
.margin_end(5)
|
|
||||||
.margin_start(5)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let camera_ip_entry = Entry::builder()
|
|
||||||
.placeholder_text("IP Address")
|
|
||||||
.text(config_read.camera_ip.clone())
|
|
||||||
.margin_top(5)
|
|
||||||
.margin_end(5)
|
|
||||||
.margin_start(5)
|
|
||||||
.margin_bottom(5)
|
|
||||||
.can_focus(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let camera_port_entry = Entry::builder()
|
|
||||||
.placeholder_text("Port")
|
|
||||||
.text(config_read.camera_port.to_string())
|
|
||||||
.margin_top(5)
|
|
||||||
.margin_end(5)
|
|
||||||
.margin_start(5)
|
|
||||||
.margin_bottom(5)
|
|
||||||
.can_focus(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let tracker_label = Label::builder()
|
|
||||||
.label("Tracker Connection")
|
|
||||||
.margin_end(5)
|
|
||||||
.margin_start(5)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let tracker_ip_entry = Entry::builder()
|
|
||||||
.placeholder_text("IP Address")
|
|
||||||
.text(config_read.tracker_ip.clone())
|
|
||||||
.margin_top(5)
|
|
||||||
.margin_end(5)
|
|
||||||
.margin_start(5)
|
|
||||||
.margin_bottom(5)
|
|
||||||
.can_focus(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let tracker_port_entry = Entry::builder()
|
|
||||||
.placeholder_text("Port")
|
|
||||||
.text(config_read.tracker_port.to_string())
|
|
||||||
.margin_top(5)
|
|
||||||
.margin_end(5)
|
|
||||||
.margin_start(5)
|
|
||||||
.margin_bottom(5)
|
|
||||||
.can_focus(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let tracker_refresh_millis = Entry::builder()
|
|
||||||
.placeholder_text("Refresh timeout")
|
|
||||||
.text(config_read.tracker_refresh_rate_millis.to_string())
|
|
||||||
.margin_top(5)
|
|
||||||
.margin_end(5)
|
|
||||||
.margin_start(5)
|
|
||||||
.margin_bottom(5)
|
|
||||||
.can_focus(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let quit_button = Button::builder()
|
|
||||||
.label("Save")
|
|
||||||
// .action_name("window.close")
|
|
||||||
.margin_top(5)
|
|
||||||
.margin_end(5)
|
|
||||||
.margin_start(5)
|
|
||||||
.margin_bottom(5)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
main_box.append(&camera_label);
|
|
||||||
main_box.append(&camera_ip_entry);
|
|
||||||
main_box.append(&camera_port_entry);
|
|
||||||
main_box.append(&tracker_label);
|
|
||||||
main_box.append(&tracker_ip_entry);
|
|
||||||
main_box.append(&tracker_port_entry);
|
|
||||||
main_box.append(&tracker_refresh_millis);
|
|
||||||
main_box.append(&quit_button);
|
|
||||||
|
|
||||||
let new_ref = app_config.clone();
|
|
||||||
quit_button.connect_clicked(clone!(
|
|
||||||
@strong rt, @weak window,
|
|
||||||
@weak camera_ip_entry, @weak camera_port_entry,
|
|
||||||
@weak tracker_ip_entry, @weak tracker_port_entry,
|
|
||||||
@weak tracker_refresh_millis => move |_| {
|
|
||||||
|
|
||||||
let new_camera_ip = camera_ip_entry.text().to_string();
|
|
||||||
let new_camera_port = camera_port_entry.text().parse::<u32>().unwrap();
|
|
||||||
|
|
||||||
let new_tracker_ip = tracker_ip_entry.text().to_string();
|
|
||||||
let new_tracker_port = tracker_port_entry.text().parse::<u32>().unwrap();
|
|
||||||
let new_tracker_millis = tracker_refresh_millis.text().parse::<u32>().unwrap();
|
|
||||||
info!("Starting config save");
|
|
||||||
|
|
||||||
{
|
|
||||||
// maybe just send the police.
|
|
||||||
let mut write_lock = rt.block_on(async {new_ref.write().await});
|
|
||||||
|
|
||||||
write_lock.camera_ip = new_camera_ip;
|
|
||||||
write_lock.camera_port = new_camera_port;
|
|
||||||
|
|
||||||
write_lock.tracker_ip = new_tracker_ip;
|
|
||||||
write_lock.tracker_port = new_tracker_port;
|
|
||||||
write_lock.tracker_refresh_rate_millis = new_tracker_millis;
|
|
||||||
// why does this feel like the borrow checker gave me a pass?
|
|
||||||
if let Err(e) = save_config(&write_lock) {
|
|
||||||
error!("Could not save config! {e}");
|
|
||||||
}
|
|
||||||
// FBI!!! OPEN UP!!!!
|
|
||||||
}
|
|
||||||
|
|
||||||
window.close();
|
|
||||||
|
|
||||||
info!("Please nicholas, add a non-crashing parse");
|
|
||||||
}));
|
|
||||||
|
|
||||||
ConnectionsModal {
|
|
||||||
window,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
120
src/webrtc_remote.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio_tungstenite::{accept_async, tungstenite::Message};
|
||||||
|
use tracing::{debug, error, info, instrument};
|
||||||
|
|
||||||
|
use vcs_common::{AppReceiver, AppSender, ApplicationMessage};
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn start_listener(from_app: AppReceiver, to_ui: AppSender) {
|
||||||
|
info!("starting tcplistener");
|
||||||
|
let listener = TcpListener::bind("localhost:7891").await.unwrap();
|
||||||
|
|
||||||
|
info!("Listening for webrtc connections on localhost:7891");
|
||||||
|
|
||||||
|
if let Ok((stream, _)) = listener.accept().await {
|
||||||
|
match accept_async(stream).await {
|
||||||
|
Err(e) => error!("Could not convert incoming stream to websocket: {e}"),
|
||||||
|
Ok(ws_stream) => {
|
||||||
|
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(msg) = from_app.recv().await {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
// serialized message
|
||||||
|
match serde_json::to_string(&msg) {
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not serialize ApplicationMessage to JSON! {e}")
|
||||||
|
}
|
||||||
|
Ok(msg) => {
|
||||||
|
if let Err(e) = ws_sender.send(Message::text(msg)).await {
|
||||||
|
error!("Could not send text ApplicationMessage to websocket! Closing websocket\n{e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
match bincode::serialize(&msg) {
|
||||||
|
Err(e) => error!(
|
||||||
|
"Could not serialize ApplicationMessage into binary! {e}"
|
||||||
|
),
|
||||||
|
Ok(e) => {
|
||||||
|
if let Err(e) = sender.send(Message::binary(msg)).await {
|
||||||
|
error!("Could not send binary ApplicationMessage to websocket! Closing websocket\n{e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while let Some(msg) = ws_receiver.next().await {
|
||||||
|
match msg {
|
||||||
|
Err(e) => {
|
||||||
|
error!("There was an error getting a message from the remote! {e}");
|
||||||
|
}
|
||||||
|
Ok(msg) => match msg {
|
||||||
|
Message::Ping(_) | Message::Pong(_) => {}
|
||||||
|
Message::Close(_) => {
|
||||||
|
info!("Received WebSocket close message! Closing the websocket");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Message::Frame(_) => {
|
||||||
|
info!("Received a Frame websocket message?");
|
||||||
|
}
|
||||||
|
Message::Text(text) => {
|
||||||
|
debug!("Recieved text from websocket: {text}");
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
match serde_json::from_str(&text) {
|
||||||
|
Ok(msg) => {
|
||||||
|
if let Err(e) = to_ui.send(msg).await {
|
||||||
|
error!("Could not send message from ws to application! Closing and exiting\n{e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Received a malformed JSON message from the websocket!\n{text}\nmsg: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
warn!("Recieved a `Text` message from the remote while running in release mode! " +
|
||||||
|
"Was the other endpoint running release mode?\n msg: {text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Binary(msg) => {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
match bincode::deserialize::<ApplicationMessage>(&msg) {
|
||||||
|
Ok(m) => {
|
||||||
|
if let Err(e) = to_ui.send(m).await {
|
||||||
|
error!("Could not send message to application! Closing and exiting\n{e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Received a malformed binary message from the websocket!\n{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
warn!("Recieved a `Binary` message from the remote while running in debug mode! " +
|
||||||
|
"Was the other endpoing running debug mode?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
tailwind.config.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./ui/**/*.html", "./ui/**/*.js"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
aspectRatio: {
|
||||||
|
albumCarousel: "1/1.25",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
72
tauri.conf.json
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"beforeBuildCommand": {
|
||||||
|
"script": "tailwindcss -i ./ui/input.css -o ./ui/static/main.css;",
|
||||||
|
"cwd": "."
|
||||||
|
},
|
||||||
|
"beforeDevCommand": {
|
||||||
|
"script": "tailwindcss -i ./ui/input.css -o ./ui/static/main.css;",
|
||||||
|
"cwd": "."
|
||||||
|
},
|
||||||
|
"devPath": "./ui",
|
||||||
|
"distDir": "./ui",
|
||||||
|
"withGlobalTauri": true
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"productName": "VCS-Controller",
|
||||||
|
"version": "0.1.0"
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"allowlist": {
|
||||||
|
"all": false
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"category": "DeveloperTool",
|
||||||
|
"copyright": "",
|
||||||
|
"deb": {
|
||||||
|
"depends": []
|
||||||
|
},
|
||||||
|
"externalBin": [],
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"identifier": "com.tauri.dev",
|
||||||
|
"longDescription": "",
|
||||||
|
"macOS": {
|
||||||
|
"entitlements": null,
|
||||||
|
"exceptionDomain": "",
|
||||||
|
"frameworks": [],
|
||||||
|
"providerShortName": null,
|
||||||
|
"signingIdentity": null
|
||||||
|
},
|
||||||
|
"resources": [],
|
||||||
|
"shortDescription": "",
|
||||||
|
"targets": "all",
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"updater": {
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"fullscreen": false,
|
||||||
|
"height": 600,
|
||||||
|
"resizable": true,
|
||||||
|
"title": "VCS Controller",
|
||||||
|
"width": 800
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
10
todo.md
|
@ -1,6 +1,14 @@
|
||||||
# Functional
|
# Functional
|
||||||
- Up-direction maxes at -50 instead of 100
|
- Up-direction maxes at -50 instead of 100
|
||||||
|
|
||||||
|
# Tauri Rewrite Todo
|
||||||
|
- propogate changes to the UI from internal state - replace GuiUpdate
|
||||||
|
- Draw boxes onto video feed
|
||||||
|
- UI Can update config options
|
||||||
|
- UI get's 'socket connected/failed' and tracker metrics
|
||||||
|
- UI can select active tracking target
|
||||||
|
|
||||||
|
|
||||||
## QoL
|
## QoL
|
||||||
- Fine tuning the tracking speeds to be non-linear, make sure the pi doesn't have that speed cap (remember could be expecting 6v max speed).
|
- Fine tuning the tracking speeds to be non-linear, make sure the pi doesn't have that speed cap (remember could be expecting 6v max speed).
|
||||||
- left and right need to hit 50 fast
|
- left and right need to hit 50 fast
|
||||||
|
@ -9,4 +17,4 @@
|
||||||
|
|
||||||
|
|
||||||
## Future ideas
|
## Future ideas
|
||||||
- person recognition
|
- person recognition
|
||||||
|
|
55
ui/index.html
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link href="/static/main.css" rel="stylesheet"/>
|
||||||
|
<script src="/static/feather.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="bg-fuchsia-300">
|
||||||
|
<div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
|
||||||
|
<div class="relative flex h-16 items-center justify-between">
|
||||||
|
<div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
|
||||||
|
<div class="hidden sm:ml-6 sm:block">
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
|
||||||
|
<a href="#" class="rounded-md bg-slate-800 px-3 py-2 text-sm font-medium text-white" aria-current="page">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="me-0 ms-auto py-2">
|
||||||
|
<button class="rounded-full p-2 hover:bg-cyan-700 transition-colors">
|
||||||
|
<i data-feather="menu"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex flex-row h-dvh w-100 text-neutral-400">
|
||||||
|
|
||||||
|
<div class="flex flex-col bg-neutral-300 h-dvh w-1/8">
|
||||||
|
<button id="camera_connect_button" class="rounded-full font-semibold mx-3 mt-2 px-4 py-2 text-white bg-cyan-600">Connect to Camera</button>
|
||||||
|
|
||||||
|
<button class="rounded-full font-semibold mx-3 mt-2 px-4 py-2 text-white bg-cyan-600">Connect to Computer</button>
|
||||||
|
<ul id="connections_list" class="m-3 border-y-1 border-neutral-800 bg-neutral-400">
|
||||||
|
<li>No Camera Connections</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="bg-emerald-700 h-dvh w-7/8">
|
||||||
|
<video style="width: 320px; height: 240px;" autoplay id="remoteview"></video>
|
||||||
|
<video style="width: 320px; height: 240px;" id="capture"></video>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script type=module>
|
||||||
|
import { init } from "/static/index.js";
|
||||||
|
feather.replace();
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
3
ui/input.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
72
ui/static/async_listeners.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { invoke, event } from "./node_modules/@tauri-apps/api/index.js";
|
||||||
|
|
||||||
|
async function setup_listeners() {
|
||||||
|
await listen_for_new_satellites();
|
||||||
|
start_joystick_callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listen_for_new_satellites() {
|
||||||
|
const new_satellite = await event.listen("satellite_names", async (msg) => {
|
||||||
|
const payload = JSON.parse(msg.payload);
|
||||||
|
|
||||||
|
console.log(payload);
|
||||||
|
|
||||||
|
let cam_button = document.getElementById("camera_connect_button");
|
||||||
|
cam_button.innerText = "Connect to Camera";
|
||||||
|
cam_button.classList.add("text-semibold");
|
||||||
|
cam_button.classList.add("text-white");
|
||||||
|
cam_button.classList.remove("text-neutral-400")
|
||||||
|
|
||||||
|
let out_ul = document.getElementById("connections_list");
|
||||||
|
out_ul.innerHTML = "";
|
||||||
|
payload.forEach((item) => {
|
||||||
|
let li = document.createElement("li");
|
||||||
|
|
||||||
|
li.classList.add("flex", "py-2");
|
||||||
|
if (!item.is_connected) {
|
||||||
|
li.classList.add("text-neutral-300", "bg-neutral-500");
|
||||||
|
li.innerHTML = `<span class="ms-2 text-sm"><i class="inline-block ms-1 me-2" data-feather="wifi-off"></i>${item.name}</span>`;
|
||||||
|
} else {
|
||||||
|
li.innerHTML = `<span class="ms-2 text-sm"><i class="inline-block ms-1 me-2" data-feather="wifi"></i>${item.name}</span>`;
|
||||||
|
li.classList.add("text-white");
|
||||||
|
}
|
||||||
|
|
||||||
|
out_ul.appendChild(li);
|
||||||
|
feather.replace();
|
||||||
|
})
|
||||||
|
register_list_elements();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function register_list_elements() {
|
||||||
|
Array.from(document.getElementById("connections_list").children).forEach((li) => {
|
||||||
|
const target_name = li.querySelector("span").innerText;
|
||||||
|
li.addEventListener("click", () => {
|
||||||
|
invoke("change_target_satellite", { new_target_name: target_name })
|
||||||
|
.catch((e) => console.log(e));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const denoise_level = 0.01;
|
||||||
|
function start_joystick_callback() {
|
||||||
|
window.addEventListener("gamepadconnected", (e) => {
|
||||||
|
setInterval(() => {
|
||||||
|
var cur_obj = {};
|
||||||
|
const gp = navigator.getGamepads()[e.gamepad.index];
|
||||||
|
|
||||||
|
if (Math.abs(gp.axes[0]) > denoise_level || Math.abs(gp.axes[1]) > denoise_level) {
|
||||||
|
invoke("send_joystick_event", {x: gp.axes[0], y: gp.axes[1]});
|
||||||
|
}
|
||||||
|
// THIS DISABLES THE RIGHT STICK
|
||||||
|
// if (Math.abs(gp.axes[2]) > denoise_level || Math.abs(gp.axes[3]) > denoise_level) {
|
||||||
|
// invoke("send_joystick_event", {x: gp.axes[2], y: gp.axes[3]});
|
||||||
|
//}
|
||||||
|
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { setup_listeners };
|
||||||
|
|
||||||
|
|
13
ui/static/feather.min.js
vendored
Normal file
46
ui/static/index.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { invoke, event } from "./node_modules/@tauri-apps/api/index.js";
|
||||||
|
import { setup_listeners } from "./async_listeners.js";
|
||||||
|
import { rtc_init } from "./rtc.js";
|
||||||
|
|
||||||
|
function call_camera_connect() {
|
||||||
|
invoke("connect_to_camera", {})
|
||||||
|
.then(() => {
|
||||||
|
let cam_button = document.getElementById("camera_connect_button");
|
||||||
|
cam_button.innerText = "Connecting to Camera";
|
||||||
|
cam_button.classList.remove("text-semibold");
|
||||||
|
cam_button.classList.remove("text-white");
|
||||||
|
cam_button.classList.add("text-neutral-400")
|
||||||
|
})
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
function supports_webrtc() {
|
||||||
|
var isWebRTCSupported = false;
|
||||||
|
|
||||||
|
['RTCPeerConnection', 'webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer'].forEach(function(item) {
|
||||||
|
if (isWebRTCSupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item in window) {
|
||||||
|
isWebRTCSupported = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return isWebRTCSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
console.log("Setting up");
|
||||||
|
document.getElementById("camera_connect_button").addEventListener("click", call_camera_connect);
|
||||||
|
|
||||||
|
await setup_listeners();
|
||||||
|
|
||||||
|
let webrtc_support = supports_webrtc();
|
||||||
|
|
||||||
|
invoke("supports_webrtc", { has_support: webrtc_support });
|
||||||
|
if (webrtc_support) {
|
||||||
|
await rtc_init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { init };
|
928
ui/static/main.css
Normal file
|
@ -0,0 +1,928 @@
|
||||||
|
/*
|
||||||
|
! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||||
|
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
border-width: 0;
|
||||||
|
/* 2 */
|
||||||
|
border-style: solid;
|
||||||
|
/* 2 */
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
--tw-content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use a consistent sensible line-height in all browsers.
|
||||||
|
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
3. Use a more readable tab size.
|
||||||
|
4. Use the user's configured `sans` font-family by default.
|
||||||
|
5. Use the user's configured `sans` font-feature-settings by default.
|
||||||
|
6. Use the user's configured `sans` font-variation-settings by default.
|
||||||
|
7. Disable tap highlights on iOS
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
:host {
|
||||||
|
line-height: 1.5;
|
||||||
|
/* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
/* 2 */
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
/* 4 */
|
||||||
|
font-feature-settings: normal;
|
||||||
|
/* 5 */
|
||||||
|
font-variation-settings: normal;
|
||||||
|
/* 6 */
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
/* 7 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove the margin in all browsers.
|
||||||
|
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Add the correct height in Firefox.
|
||||||
|
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||||
|
3. Ensure horizontal rules are visible by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-top-width: 1px;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr:where([title]) {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the default font size and weight for headings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset links to optimize for opt-in styling instead of opt-out.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font weight in Edge and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use the user's configured `mono` font-family by default.
|
||||||
|
2. Use the user's configured `mono` font-feature-settings by default.
|
||||||
|
3. Use the user's configured `mono` font-variation-settings by default.
|
||||||
|
4. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-feature-settings: normal;
|
||||||
|
/* 2 */
|
||||||
|
font-variation-settings: normal;
|
||||||
|
/* 3 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||||
|
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||||
|
3. Remove gaps between table borders by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-indent: 0;
|
||||||
|
/* 1 */
|
||||||
|
border-color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Change the font styles in all browsers.
|
||||||
|
2. Remove the margin in Firefox and Safari.
|
||||||
|
3. Remove default padding in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-feature-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-variation-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 100%;
|
||||||
|
/* 1 */
|
||||||
|
font-weight: inherit;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 1 */
|
||||||
|
letter-spacing: inherit;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 1 */
|
||||||
|
margin: 0;
|
||||||
|
/* 2 */
|
||||||
|
padding: 0;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inheritance of text transform in Edge and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Remove default button styles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input:where([type='button']),
|
||||||
|
input:where([type='reset']),
|
||||||
|
input:where([type='submit']) {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
background-color: transparent;
|
||||||
|
/* 2 */
|
||||||
|
background-image: none;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use the modern Firefox focus style for all focusable elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct vertical alignment in Chrome and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Correct the cursor style of increment and decrement buttons in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button,
|
||||||
|
::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='search'] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
/* 1 */
|
||||||
|
outline-offset: -2px;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
font: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct display in Chrome and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Removes the default spacing and border for appropriate elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
dd,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
hr,
|
||||||
|
figure,
|
||||||
|
p,
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
menu {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset default styling for dialogs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent resizing textareas horizontally by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||||
|
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
/* 1 */
|
||||||
|
color: #9ca3af;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
/* 1 */
|
||||||
|
color: #9ca3af;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set the default cursor for buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make sure disabled buttons don't get the pointer cursor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||||
|
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
audio,
|
||||||
|
iframe,
|
||||||
|
embed,
|
||||||
|
object {
|
||||||
|
display: block;
|
||||||
|
/* 1 */
|
||||||
|
vertical-align: middle;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, ::before, ::after {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
::backdrop {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.container {
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 768px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1024px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1536px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1536px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse {
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.static {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-3 {
|
||||||
|
margin: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-3 {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-1 {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.me-0 {
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-auto {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-2 {
|
||||||
|
margin-inline-start: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.me-2 {
|
||||||
|
margin-inline-end: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-1 {
|
||||||
|
margin-inline-start: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-block {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contents {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-16 {
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-dvh {
|
||||||
|
height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-w-7xl {
|
||||||
|
max-width: 80rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform {
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize {
|
||||||
|
resize: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||||
|
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-full {
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-neutral-800 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(38 38 38 / var(--tw-border-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-cyan-600 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(8 145 178 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-emerald-700 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(4 120 87 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-fuchsia-300 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(240 171 252 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray-900 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-neutral-300 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(212 212 212 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-neutral-400 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-neutral-500 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-slate-800 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-2 {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-2 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-2 {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-medium {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-300 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(209 213 219 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-neutral-300 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(212 212 212 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-neutral-400 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring {
|
||||||
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blur {
|
||||||
|
--tw-blur: blur(8px);
|
||||||
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-colors {
|
||||||
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-cyan-700:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(14 116 144 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-gray-700:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:text-white:hover {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:ml-6 {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm\:block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm\:items-stretch {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm\:justify-start {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm\:px-6 {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.lg\:px-8 {
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
56
ui/static/package-lock.json
generated
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"name": "vcs-controller",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "vcs-controller",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.6.11",
|
||||||
|
"@tauri-apps/api": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.6.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
|
||||||
|
"integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.6.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz",
|
||||||
|
"integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.6.0",
|
||||||
|
"@floating-ui/utils": "^0.2.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
|
||||||
|
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/api": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6.0",
|
||||||
|
"npm": ">= 6.6.0",
|
||||||
|
"yarn": ">= 1.19.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/tauri"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
ui/static/package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "vcs-controller",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.6.11",
|
||||||
|
"@tauri-apps/api": "^1.6.0"
|
||||||
|
}
|
||||||
|
}
|
101
ui/static/rtc.js
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
|
||||||
|
|
||||||
|
import { event } from "./node_modules/@tauri-apps/api/index.js";
|
||||||
|
|
||||||
|
async function rtc_init() {
|
||||||
|
const videoview = document.getElementById("remoteview");
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
iceServers: [{ urls: "stun:localhost" }]
|
||||||
|
};
|
||||||
|
const polite = true;
|
||||||
|
|
||||||
|
const pc = new RTCPeerConnection(config);
|
||||||
|
window.pc = pc;
|
||||||
|
|
||||||
|
pc.ontrack = (e) => {
|
||||||
|
console.log(e);
|
||||||
|
e.track.onunmute = () => {
|
||||||
|
console.log("Unmuted?");
|
||||||
|
if (remoteview.srcObject) {
|
||||||
|
console.log("Skipping srcobject");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
videoview.srcObject = e.streams[0];
|
||||||
|
Object.assign(videoview.style, { "background-color": "black"});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let makingOffer = false;
|
||||||
|
|
||||||
|
pc.onnegotionationneeded = async () => {
|
||||||
|
try {
|
||||||
|
makingOffer = true;
|
||||||
|
await pc.setLocalDescription();
|
||||||
|
console.log("emitting response webrtc packet");
|
||||||
|
event.emit("webrtc-message", pc.localDescription );
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
makingOffer = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pc.onicecandidate = ({ candidate }) => {
|
||||||
|
console.log("emitting response webrtc packet");
|
||||||
|
event.emit("webrtc-message", candidate);
|
||||||
|
};
|
||||||
|
pc.oniceconnectionstatechange = () => {
|
||||||
|
console.log('ICE state: ',pc.iceConnectionState);
|
||||||
|
if (pc.iceConnectionState === "failed") {
|
||||||
|
pc.restartIce();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ignoreOffer = false;
|
||||||
|
|
||||||
|
console.log("registering listener");
|
||||||
|
const application_message = await event.listen('frontend_message', async (msg) => {
|
||||||
|
|
||||||
|
const payload = JSON.parse(msg.payload);
|
||||||
|
|
||||||
|
console.log(payload);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (payload.type) {
|
||||||
|
const offerCollision =
|
||||||
|
payload.type === "offer" &&
|
||||||
|
(makingOffer || pc.signalingState !== "stable");
|
||||||
|
|
||||||
|
ignoreOffer = !polite && offerCollision;
|
||||||
|
|
||||||
|
if (ignoreOffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pc.setRemoteDescription(payload);
|
||||||
|
if (payload.type === "offer") {
|
||||||
|
console.log("Settings local description");
|
||||||
|
await pc.setLocalDescription();
|
||||||
|
console.log("emitting response webrtc packet");
|
||||||
|
event.emit( "webrtc-message", pc.localDescription);
|
||||||
|
}
|
||||||
|
} else if (payload.candidate) {
|
||||||
|
try {
|
||||||
|
console.log("Adding trickle ICE candidate");
|
||||||
|
await pc.addIceCandidate(payload.candidate);
|
||||||
|
} catch (err) {
|
||||||
|
if (!ignoreOffer) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export { rtc_init };
|