feat: The rest of the owl

This commit is contained in:
Baud 2024-05-25 17:44:14 +01:00
parent 8cce21193a
commit be96c86f74
9 changed files with 275 additions and 117 deletions

View File

@ -44,6 +44,10 @@ fn main() {
panic!("Running `trunk build` failed, reason {err}");
}
if let Ok(trunk_output_string) = String::from_utf8(trunk_build_output.stdout) {
println!("{trunk_output_string}");
}
let dist_paths = fs::read_dir("./frontend/dist").expect("Could not read dist directory");
let paths: Vec<Result<DirEntry, Error>> = dist_paths.collect();
let paths: Vec<(String, PathBuf)> = paths

View File

@ -1,17 +1,16 @@
use std::{
collections::VecDeque,
fmt::Display,
collections::{HashMap, VecDeque},
mem,
net::{IpAddr, Ipv4Addr},
};
use egui::{Button, Color32, RichText, Sense, Stroke, Vec2};
use egui::{Button, Color32, RichText, Rounding, Sense, Stroke, Vec2};
use ewebsock::{Options, WsReceiver, WsSender};
use wasm_timer::Instant;
use crate::websocket::{
AddHyperdeckRequest, ClientRequest, HyperdeckConnectionState, RemoveHyperdeckRequest,
ServerEvent,
AddHyperdeckRequest, ClientRequest, HyperdeckConnectionState, HyperdeckRecordBay,
RecordingState, RemoveHyperdeckRequest, ServerEvent,
};
pub struct HyperdeckMonitorApp {
@ -37,28 +36,69 @@ impl Default for HyperdeckMonitorApp {
new_hyperdeck_name: "".to_owned(),
new_hyperdeck_port: 9993.to_string(),
hyperdecks: vec![
// Hyperdeck {
// id: "test-1".to_string(),
// name: "Test Hyperdeck 1".to_string(),
// ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 1)),
// status: HyperdeckStatus::Connected,
// recording_bays: vec![HyperdeckRecordBay {
// status: RecordingStatus::NotRecording,
// storage_capacity_mb: 500_000,
// recording_time_remaining: TimeRemaining(60),
// }],
// },
// Hyperdeck {
// id: "test-2".to_string(),
// name: "Test Hyperdeck 2".to_string(),
// ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 2)),
// status: HyperdeckStatus::Disconnected,
// recording_bays: vec![HyperdeckRecordBay {
// status: RecordingStatus::NotRecording,
// storage_capacity_mb: 500_000,
// recording_time_remaining: TimeRemaining(3600 * 5), // 5 Hours
// }],
// },
Hyperdeck {
id: "test-1".to_string(),
name: "Description: Connected Hyperdeck - Not Recording - Not Much Time"
.to_string(),
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 1)),
status: HyperdeckStatus::Connected,
recording_status: RecordingState::NotRecording,
slots: vec![(
0usize,
HyperdeckRecordBay {
recording_time_remaining: 60,
},
)]
.into_iter()
.collect::<HashMap<usize, HyperdeckRecordBay>>(),
},
Hyperdeck {
id: "test-2".to_string(),
name: "Description: Connected Hyperdeck - Recording - Not Much Time"
.to_string(),
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 2)),
status: HyperdeckStatus::Connected,
recording_status: RecordingState::Recording,
slots: vec![(
0usize,
HyperdeckRecordBay {
recording_time_remaining: 60,
},
)]
.into_iter()
.collect::<HashMap<usize, HyperdeckRecordBay>>(),
},
Hyperdeck {
id: "test-3".to_string(),
name: "Description: Connected Hyperdeck - Recording - Plenty of Time"
.to_string(),
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 3)),
status: HyperdeckStatus::Connected,
recording_status: RecordingState::Recording,
slots: vec![(
0usize,
HyperdeckRecordBay {
recording_time_remaining: 60 * 30,
},
)]
.into_iter()
.collect::<HashMap<usize, HyperdeckRecordBay>>(),
},
Hyperdeck {
id: "test-4".to_string(),
name: "Description: Disconnected Hyperdeck".to_string(),
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 4)),
status: HyperdeckStatus::Disconnected,
recording_status: RecordingState::NotRecording,
slots: vec![(
0usize,
HyperdeckRecordBay {
recording_time_remaining: 3600 * 5, // 5 Hours
},
)]
.into_iter()
.collect::<HashMap<usize, HyperdeckRecordBay>>(),
},
],
websocket_message_queue: VecDeque::new(),
ws_sender,
@ -87,18 +127,14 @@ impl eframe::App for HyperdeckMonitorApp {
name: hyperdeck.name,
ip: hyperdeck.ip.parse().unwrap(),
status: hyperdeck.connection_state.into(),
recording_bays: vec![],
recording_status: hyperdeck.recording_status,
slots: hyperdeck.slots,
})
}
}
}
}
}
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
egui::widgets::global_dark_light_mode_buttons(ui);
});
});
egui::CentralPanel::default().show(ctx, |ui| {
add_hyperdeck_panel(
@ -118,11 +154,6 @@ impl eframe::App for HyperdeckMonitorApp {
&mut self.websocket_message_queue,
);
});
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
connection_status(ui);
egui::warn_if_debug_build(ui);
});
});
if self.last_blink_change.elapsed().as_secs() >= 1 {
@ -195,27 +226,41 @@ fn hyperdeck_list(
));
}
});
if !hyperdeck.recording_bays.is_empty()
&& matches!(hyperdeck.status, HyperdeckStatus::Connected)
{
let recording_bays_text: RichText = "Recording Bays".into();
ui.label(recording_bays_text.size(16.0).strong());
for (index, bay) in hyperdeck.recording_bays.iter().enumerate() {
if matches!(hyperdeck.status, HyperdeckStatus::Connected) {
ui.horizontal(|ui| {
let bay_label: RichText = format!("Bay {}", index + 1).into();
ui.label(bay_label.strong());
match bay.status {
RecordingStatus::Recording => ui.label("Recording"),
RecordingStatus::NotRecording => ui.label("Not Recording"),
match hyperdeck.recording_status {
RecordingState::Recording => {
let (response, painter) =
ui.allocate_painter(Vec2 { x: 16.0, y: 16.0 }, Sense::hover());
let rect = response.rect;
painter.rect(
rect,
Rounding::ZERO,
Color32::from_rgb(255, 255, 255),
Stroke::NONE,
);
let recording_text: RichText = "[Recording]".into();
ui.label(
recording_text
.color(Color32::from_rgb(255, 255, 255))
.strong(),
);
}
RecordingState::NotRecording => {
ui.label("[Not Recording]");
}
};
ui.label(format!(
"Total Storage Capacity: {}GB",
bay.storage_capacity_mb / 1000,
));
let time_remaining_text: RichText =
format!("Time remaining: {}", bay.recording_time_remaining).into();
});
if bay.recording_time_remaining.0 > 15 * 60 || !blink {
for (index, slot) in hyperdeck.slots.iter() {
ui.horizontal(|ui| {
let slot_label: RichText = format!("Slot {}", index + 1).into();
ui.label(slot_label.strong());
let time_remaining_text: RichText =
format!("Time remaining: {}", slot.recording_time_remaining).into();
if slot.recording_time_remaining > 15 * 60 || !blink {
ui.label(time_remaining_text);
} else {
ui.label(time_remaining_text.color(Color32::RED));
@ -228,20 +273,14 @@ fn hyperdeck_list(
}
}
fn connection_status(ui: &mut egui::Ui) {
ui.horizontal(|ui| {
// TODO: Make it real
ui.label("Connected");
});
}
#[derive(serde::Deserialize, serde::Serialize)]
struct Hyperdeck {
id: String,
name: String,
ip: IpAddr,
status: HyperdeckStatus,
recording_bays: Vec<HyperdeckRecordBay>,
recording_status: RecordingState,
slots: HashMap<usize, HyperdeckRecordBay>,
}
#[derive(serde::Deserialize, serde::Serialize)]
@ -258,28 +297,3 @@ impl From<HyperdeckConnectionState> for HyperdeckStatus {
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
struct HyperdeckRecordBay {
status: RecordingStatus,
/// Storage capacity in MB.
storage_capacity_mb: u64,
/// Recording time available in seconds.
recording_time_remaining: TimeRemaining,
}
#[derive(serde::Deserialize, serde::Serialize)]
struct TimeRemaining(u64);
impl Display for TimeRemaining {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let time = hrtime::from_sec_padded(self.0);
write!(f, "{time}")
}
}
#[derive(serde::Deserialize, serde::Serialize)]
enum RecordingStatus {
Recording,
NotRecording,
}

View File

@ -1,5 +1,6 @@
import { Hyperdeck, Commands } from 'hyperdeck-connection';
import WebSocket from 'ws';
import { v4 as uuidv4 } from 'uuid';
interface WrappedHyperdeck {
ip: String,
@ -26,8 +27,11 @@ type WebSocketMessage = {
const wss = new WebSocket.Server({ port: 7867 });
wss.on('connection', function connection(ws) {
ws.on('message', function message(data) {
const connected_clients: Map<string, WebSocket> = new Map();
wss.on('connection', (ws) => {
const clientId = uuidv4();
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString()) as Partial<WebSocketMessage>;
handle_message(message)
@ -35,11 +39,16 @@ wss.on('connection', function connection(ws) {
return;
}
});
ws.on('close', () => {
connected_clients.delete(clientId)
})
ws.send(JSON.stringify({
event: "log",
message: "Hello"
}));
connected_clients.set(clientId, ws);
});
function exhaustiveMatch(_never: never) {
@ -68,24 +77,64 @@ function handle_message(message: Partial<WebSocketMessage>) {
hyperdeck: newHyperdeck
});
newHyperdeck.on('connected', (info) => {
console.log(JSON.stringify(info))
newHyperdeck.on('connected', (_info) => {
notifyClients({
event: "hyperdeck_connected",
id: message.id
})
setInterval(() => {
newHyperdeck.sendCommand(new Commands.TransportInfoCommand()).then((transportInfo) => {
console.log(JSON.stringify(transportInfo))
notifyClients({
event: "record_state",
hyperdeckId: message.id,
state: transportInfo.status,
})
})
})
newHyperdeck.on('notify.slot', function (state) {
console.log(JSON.stringify(state)) // catch the slot state change.
newHyperdeck.sendCommand(new Commands.DeviceInfoCommand()).then((info) => {
for (let index = 0; index < info.slots; index++) {
setInterval(() => {
newHyperdeck.sendCommand(new Commands.SlotInfoCommand(index)).then((slot) => {
notifyClients({
event: "record_time_remaining",
hyperdeckId: message.id,
slotId: slot.slotId,
remaining: slot.recordingTime
})
})
})
}
})
})
newHyperdeck.on('notify.slot', function (slot) {
notifyClients({
event: "record_time_remaining",
hyperdeckId: message.id,
slotId: slot.slotId,
remaining: slot.recordingTime
})
})
newHyperdeck.on('notify.transport', function (state) {
console.log(JSON.stringify(state)) // catch the transport state change.
notifyClients({
event: "record_state",
hyperdeckId: message.id,
state: state.status
})
})
newHyperdeck.on('error', (err) => {
console.log('Hyperdeck error', JSON.stringify(err))
})
newHyperdeck.on('disconnected', () => {
notifyClients({
event: "hyperdeck_disconnected",
id: message.id
})
})
newHyperdeck.connect(message.ip, message.port)
break;
@ -105,3 +154,9 @@ function handle_message(message: Partial<WebSocketMessage>) {
exhaustiveMatch(message)
}
}
function notifyClients(message: object) {
connected_clients.forEach((client) => {
client.send(JSON.stringify(message))
})
}

13
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"hyperdeck-connection": "^2.0.1",
"uuid": "^9.0.1",
"ws": "^8.17.0"
},
"devDependencies": {
@ -75,6 +76,18 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",

View File

@ -7,6 +7,7 @@
},
"dependencies": {
"hyperdeck-connection": "^2.0.1",
"uuid": "^9.0.1",
"ws": "^8.17.0"
},
"devDependencies": {

View File

@ -4,7 +4,6 @@ use axum::response::Html;
use axum::Json;
use axum::{
body::Bytes,
extract::Path,
http::{header, HeaderValue, Method},
response::IntoResponse,
routing::get,
@ -66,7 +65,7 @@ pub async fn initialize_api(
let clients = state_clients.lock().await;
let state_json = serde_json::to_string(&ServerEvent::HyperdeckMonitorState(
hyperdeck_monitor_state.into(),
hyperdeck_monitor_state,
))
.unwrap();
for (_, client) in clients.iter() {

View File

@ -40,6 +40,9 @@ pub struct HyperdeckState {
pub ip: String,
pub port: u16,
pub connection_state: HyperdeckConnectionState,
pub recording_status: RecordingState,
// HashMap to allow for sparse entries.
pub slots: HashMap<usize, HyperdeckRecordBay>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -47,3 +50,15 @@ pub enum HyperdeckConnectionState {
Connected,
Disconnected,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RecordingState {
Recording,
NotRecording,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperdeckRecordBay {
/// Recording time available in seconds.
pub recording_time_remaining: u64,
}

View File

@ -32,7 +32,7 @@ pub async fn client_connection(
let current_state = state.read().await.clone();
let state_json =
serde_json::to_string(&ServerEvent::HyperdeckMonitorState(current_state.into())).unwrap();
serde_json::to_string(&ServerEvent::HyperdeckMonitorState(current_state)).unwrap();
client_sender.send(Message::Text(state_json.clone())).ok();
client.sender = Some(client_sender);

View File

@ -1,8 +1,8 @@
use std::{process::Stdio, time::Duration};
use std::{collections::HashMap, process::Stdio, time::Duration};
use api::message::{
AddHyperdeckRequest, ClientRequest, HyperdeckConnectionState, HyperdeckMonitorState,
HyperdeckState, RemoveHyperdeckRequest,
HyperdeckRecordBay, HyperdeckState, RecordingState, RemoveHyperdeckRequest,
};
use color_eyre::Report;
use futures_util::{
@ -31,9 +31,8 @@ async fn main() {
let (node_ws_message_tx, node_ws_message_rx) = tokio::sync::mpsc::unbounded_channel();
let (node_commands_tx, node_commands_rx) = tokio::sync::mpsc::unbounded_channel();
let state = AppState::default();
let node_ws_communication =
talk_to_node_ws(state, node_ws_message_tx, node_commands_rx, cancel.clone()).fuse();
talk_to_node_ws(node_ws_message_tx, node_commands_rx, cancel.clone()).fuse();
let (state_tx, state_rx) = tokio::sync::broadcast::channel(1);
let (client_request_tx, client_request_rx) = tokio::sync::mpsc::unbounded_channel();
@ -67,7 +66,7 @@ async fn main() {
async fn run(
mut node_commands_tx: tokio::sync::mpsc::UnboundedSender<NodeWsCommand>,
mut node_ws_message_rx: tokio::sync::mpsc::UnboundedReceiver<NodeWsMessageReceived>,
mut state_tx: tokio::sync::broadcast::Sender<HyperdeckMonitorState>,
state_tx: tokio::sync::broadcast::Sender<HyperdeckMonitorState>,
mut client_request_rx: tokio::sync::mpsc::UnboundedReceiver<ClientRequest>,
cancel: CancellationToken,
) {
@ -100,7 +99,7 @@ async fn run(
async fn handle_message_from_node(
msg: NodeWsMessageReceived,
node_commands_tx: &mut tokio::sync::mpsc::UnboundedSender<NodeWsCommand>,
_node_commands_tx: &mut tokio::sync::mpsc::UnboundedSender<NodeWsCommand>,
state: &mut HyperdeckMonitorState,
) -> bool {
match msg {
@ -120,6 +119,38 @@ async fn handle_message_from_node(
});
true
}
NodeWsMessageReceived::RecordState {
hyperdeck_id,
status,
} => {
if let Some(hyperdeck) = state.hyperdecks.get_mut(&hyperdeck_id) {
hyperdeck.recording_status = if matches!(status, TransportStatus::Record) {
RecordingState::Recording
} else {
RecordingState::NotRecording
};
true
} else {
false
}
}
NodeWsMessageReceived::RecordTimeRemaining {
hyperdeck_id,
slot_id,
remaining,
} => {
if let Some(hyperdeck) = state.hyperdecks.get_mut(&hyperdeck_id) {
hyperdeck
.slots
.entry(slot_id)
.or_insert(HyperdeckRecordBay {
recording_time_remaining: remaining,
});
true
} else {
false
}
}
}
}
@ -139,6 +170,8 @@ async fn handle_message_from_client(
ip: ip.clone(),
port,
connection_state: api::message::HyperdeckConnectionState::Disconnected,
recording_status: api::message::RecordingState::NotRecording,
slots: HashMap::new(),
},
);
let _ = node_commands_tx.send(NodeWsCommand::AddHyperdeck(AddHyperdeckCommand {
@ -210,9 +243,6 @@ async fn run_node_process(cancel: CancellationToken) {
}
}
#[derive(Default)]
struct AppState {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum NodeWsCommand {
@ -223,12 +253,40 @@ enum NodeWsCommand {
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
#[serde(tag = "event")]
enum NodeWsMessageReceived {
Log { message: String },
HyperdeckConnected { id: String },
HypderdeckDisconnected { id: String },
Log {
message: String,
},
HyperdeckConnected {
id: String,
},
HypderdeckDisconnected {
id: String,
},
RecordState {
hyperdeck_id: String,
status: TransportStatus,
},
RecordTimeRemaining {
hyperdeck_id: String,
slot_id: usize,
remaining: u64,
},
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum TransportStatus {
Preview,
Stopped,
Play,
Forward,
Rewind,
Jog,
Shuttle,
Record,
}
#[derive(Debug, Serialize, Deserialize)]
@ -246,7 +304,6 @@ struct RemoveHyperdeckCommand {
}
async fn talk_to_node_ws(
state: AppState,
ws_message_tx: tokio::sync::mpsc::UnboundedSender<NodeWsMessageReceived>,
commands_rx: tokio::sync::mpsc::UnboundedReceiver<NodeWsCommand>,
cancel: CancellationToken,