Compare commits

..
20 Commits
Author SHA1 Message Date
sbaudlr 7e973af192 fix: Not ticking 2024-03-16 17:41:16 +00:00
sbaudlr 689138b282 chore: Begone &Box<T> 2024-03-16 17:14:31 +00:00
sbaudlr 2325645bb5 feat: Handle program input 2024-03-11 00:30:16 +00:00
sbaudlr 46cca11e00 chore: More cleanups 2024-03-11 00:04:05 +00:00
sbaudlr 90b2cfd984 chore: Various cleanups 2024-03-10 21:53:30 +00:00
sbaudlr 2bbf2c5c6b wip: More command handling 2024-03-10 02:08:35 +00:00
sbaudlr 4a075c3d1e wip: handle received commands 2024-03-08 17:43:53 +00:00
sbaudlr 9dd8cc0574 chore: motd 2024-03-07 22:35:10 +00:00
sbaudlr 676ff7630e feat: Wait for connect 2024-03-04 21:33:46 +00:00
sbaudlr 4a41d1f5d7 feat: Atem wrapper 2024-03-01 17:11:57 +00:00
sbaudlr 5db8843ce7 feat: Use AtemPacket struct 2024-02-28 18:43:21 +00:00
sbaudlr 34a268f0bf chore: Stubs for tally events 2024-02-27 10:19:17 +00:00
sbaudlr e3a0d7973d fix: Don't spawn threads 2024-02-27 10:15:44 +00:00
sbaudlr c30913f823 feat: Deserializing commands 2024-02-24 17:01:28 +00:00
sbaudlr c80d7643ca feat: Program Input deserialization 2024-02-23 11:40:57 +00:00
sbaudlr 9d872eecc9 feat: Very basic tally parsing 2024-02-12 22:53:55 +00:00
sbaudlr 2ee3d71e78 feat: IP arg 2024-02-05 21:34:03 +00:00
sbaudlr 80b73922ac chore: Some logging 2024-02-05 21:33:48 +00:00
sbaudlr de6f4b2a4d chore: Derive default 2024-02-05 21:13:53 +00:00
sbaudlr 621b085381 chore: cargo fmt 2024-02-05 21:12:17 +00:00
21 changed files with 279 additions and 747 deletions
+11 -34
View File
@@ -1,26 +1,20 @@
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
net::SocketAddr, net::SocketAddr,
ops::DerefMut,
sync::Arc, sync::Arc,
time::Duration,
}; };
use tokio::{select, sync::Semaphore}; use tokio::{select, sync::Semaphore};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::{ use crate::{
atem_lib::atem_socket::{ atem_lib::atem_socket::{AtemEvent, AtemSocketCommand, AtemSocketMessage, TrackingId},
AtemSocket, AtemSocketCommand, AtemSocketEvent, AtemSocketMessage, TrackingId,
},
commands::{ commands::{
command_base::{BasicWritableCommand, DeserializedCommand}, command_base::{BasicWritableCommand, DeserializedCommand},
device_profile::version::DESERIALIZE_VERSION_RAW_NAME, device_profile::DESERIALIZE_VERSION_RAW_NAME,
init_complete::DESERIALIZE_INIT_COMPLETE_RAW_NAME, init_complete::DESERIALIZE_INIT_COMPLETE_RAW_NAME,
parse_commands::deserialize_commands,
time::DESERIALIZE_TIME_RAW_NAME, time::DESERIALIZE_TIME_RAW_NAME,
}, },
enums::ProtocolVersion,
state::AtemState, state::AtemState,
}; };
@@ -33,24 +27,13 @@ pub enum AtemConnectionStatus {
} }
pub struct Atem { pub struct Atem {
protocol_version: tokio::sync::RwLock<ProtocolVersion>,
socket: tokio::sync::RwLock<AtemSocket>,
waiting_semaphores: tokio::sync::RwLock<HashMap<TrackingId, Arc<Semaphore>>>, waiting_semaphores: tokio::sync::RwLock<HashMap<TrackingId, Arc<Semaphore>>>,
socket_message_tx: tokio::sync::mpsc::Sender<AtemSocketMessage>, socket_message_tx: tokio::sync::mpsc::Sender<AtemSocketMessage>,
} }
impl Atem { impl Atem {
pub fn new( pub fn new(socket_message_tx: tokio::sync::mpsc::Sender<AtemSocketMessage>) -> Self {
socket: AtemSocket,
socket_message_tx: tokio::sync::mpsc::Sender<AtemSocketMessage>,
) -> Self {
Self { Self {
protocol_version: tokio::sync::RwLock::new(ProtocolVersion::V7_2),
socket: tokio::sync::RwLock::new(socket),
waiting_semaphores: tokio::sync::RwLock::new(HashMap::new()), waiting_semaphores: tokio::sync::RwLock::new(HashMap::new()),
socket_message_tx, socket_message_tx,
} }
@@ -71,30 +54,25 @@ impl Atem {
pub async fn run( pub async fn run(
&self, &self,
mut atem_event_rx: tokio::sync::mpsc::UnboundedReceiver<AtemSocketEvent>, mut atem_event_rx: tokio::sync::mpsc::UnboundedReceiver<AtemEvent>,
cancel: CancellationToken, cancel: CancellationToken,
) { ) {
let mut status = AtemConnectionStatus::default(); let mut status = AtemConnectionStatus::default();
let mut state = AtemState::default(); let mut state = AtemState::default();
let mut poll_interval = tokio::time::interval(Duration::from_millis(5));
while !cancel.is_cancelled() { while !cancel.is_cancelled() {
let tick = poll_interval.tick();
select! { select! {
_ = cancel.cancelled() => {}, _ = cancel.cancelled() => {},
_ = tick => {},
message = atem_event_rx.recv() => match message { message = atem_event_rx.recv() => match message {
Some(event) => match event { Some(event) => match event {
AtemSocketEvent::Connected => { AtemEvent::Connected => {
log::info!("Atem connected"); log::info!("Atem connected");
} }
AtemSocketEvent::Disconnected => todo!("Disconnected"), AtemEvent::Disconnected => todo!("Disconnected"),
AtemSocketEvent::ReceivedCommands(payload) => { AtemEvent::ReceivedCommands(commands) => {
let commands = deserialize_commands(&payload, self.protocol_version.write().await.deref_mut());
self.mutate_state(&mut state, &mut status, commands).await self.mutate_state(&mut state, &mut status, commands).await
} }
AtemSocketEvent::AckedCommand(tracking_id) => { AtemEvent::AckedCommand(tracking_id) => {
log::debug!("Received tracking Id {tracking_id}"); log::debug!("Received tracking Id {tracking_id}");
if let Some(semaphore) = if let Some(semaphore) =
self.waiting_semaphores.read().await.get(&tracking_id) self.waiting_semaphores.read().await.get(&tracking_id)
@@ -111,19 +89,18 @@ impl Atem {
} }
} }
} }
self.socket.write().await.poll().await;
} }
} }
pub async fn send_commands(&self, commands: Vec<Box<dyn BasicWritableCommand>>) { pub async fn send_commands(&self, commands: Vec<Box<dyn BasicWritableCommand>>) {
let protocol_version = { self.protocol_version.read().await.clone() };
let (callback_tx, callback_rx) = tokio::sync::oneshot::channel(); let (callback_tx, callback_rx) = tokio::sync::oneshot::channel();
self.socket_message_tx self.socket_message_tx
.send(AtemSocketMessage::SendCommands { .send(AtemSocketMessage::SendCommands {
commands: commands commands: commands
.iter() .iter()
.map(|command| AtemSocketCommand::new(command, &protocol_version)) .map(|command| {
AtemSocketCommand::new(command, &crate::enums::ProtocolVersion::Unknown)
})
.collect(), .collect(),
tracking_ids_callback: callback_tx, tracking_ids_callback: callback_tx,
}) })
+23 -26
View File
@@ -54,10 +54,10 @@ pub struct TrackingIdsCallback {
} }
#[derive(Clone)] #[derive(Clone)]
pub enum AtemSocketEvent { pub enum AtemEvent {
Connected, Connected,
Disconnected, Disconnected,
ReceivedCommands(Vec<u8>), ReceivedCommands(VecDeque<Arc<dyn DeserializedCommand>>),
AckedCommand(TrackingId), AckedCommand(TrackingId),
} }
@@ -103,19 +103,14 @@ pub struct AtemSocket {
socket: Option<UdpSocket>, socket: Option<UdpSocket>,
address: SocketAddr, address: SocketAddr,
protocol_version: ProtocolVersion,
last_received_at: SystemTime, last_received_at: SystemTime,
last_received_packed_id: u16, last_received_packed_id: u16,
in_flight: Vec<InFlightPacket>, in_flight: Vec<InFlightPacket>,
ack_timer: Option<SystemTime>, ack_timer: Option<SystemTime>,
received_without_ack: u16, received_without_ack: u16,
atem_message_rx: tokio::sync::mpsc::Receiver<AtemSocketMessage>, atem_event_tx: tokio::sync::mpsc::UnboundedSender<AtemEvent>,
atem_event_tx: tokio::sync::mpsc::UnboundedSender<AtemSocketEvent>,
connected_callbacks: Mutex<Vec<tokio::sync::oneshot::Sender<bool>>>, connected_callbacks: Mutex<Vec<tokio::sync::oneshot::Sender<bool>>>,
tick_interval: tokio::time::Interval,
} }
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
@@ -150,11 +145,7 @@ enum AtemSocketReceiveError {
} }
impl AtemSocket { impl AtemSocket {
pub fn new( pub fn new(atem_event_tx: tokio::sync::mpsc::UnboundedSender<AtemEvent>) -> Self {
atem_message_rx: tokio::sync::mpsc::Receiver<AtemSocketMessage>,
atem_event_tx: tokio::sync::mpsc::UnboundedSender<AtemSocketEvent>,
) -> Self {
let tick_interval = tokio::time::interval(Duration::from_millis(5));
Self { Self {
connection_state: ConnectionState::Closed, connection_state: ConnectionState::Closed,
reconnect_timer: None, reconnect_timer: None,
@@ -168,27 +159,29 @@ impl AtemSocket {
socket: None, socket: None,
address: "0.0.0.0:0".parse().unwrap(), address: "0.0.0.0:0".parse().unwrap(),
protocol_version: ProtocolVersion::V7_2,
last_received_at: SystemTime::now(), last_received_at: SystemTime::now(),
last_received_packed_id: 0, last_received_packed_id: 0,
in_flight: vec![], in_flight: vec![],
ack_timer: None, ack_timer: None,
received_without_ack: 0, received_without_ack: 0,
atem_message_rx,
atem_event_tx, atem_event_tx,
connected_callbacks: Mutex::default(), connected_callbacks: Mutex::default(),
tick_interval,
} }
} }
pub async fn poll(&mut self) { pub async fn run(
let tick = self.tick_interval.tick(); &mut self,
mut atem_message_rx: tokio::sync::mpsc::Receiver<AtemSocketMessage>,
cancel: tokio_util::sync::CancellationToken,
) {
let mut interval = tokio::time::interval(Duration::from_millis(5));
while !cancel.is_cancelled() {
let tick = interval.tick();
select! { select! {
_ = cancel.cancelled() => {},
_ = tick => {}, _ = tick => {},
message = self.atem_message_rx.recv() => { message = atem_message_rx.recv() => {
match message { match message {
Some(AtemSocketMessage::Connect { Some(AtemSocketMessage::Connect {
address, address,
@@ -237,7 +230,8 @@ impl AtemSocket {
barrier.wait().await; barrier.wait().await;
}, },
None => { None => {
log::info!("ATEM message channel has closed."); log::info!("ATEM message channel has closed, exiting event loop.");
cancel.cancel();
} }
} }
} }
@@ -245,6 +239,7 @@ impl AtemSocket {
self.tick().await; self.tick().await;
} }
}
pub async fn connect(&mut self, address: SocketAddr) -> Result<(), io::Error> { pub async fn connect(&mut self, address: SocketAddr) -> Result<(), io::Error> {
let socket = UdpSocket::bind("0.0.0.0:0").await?; let socket = UdpSocket::bind("0.0.0.0:0").await?;
@@ -537,21 +532,23 @@ impl AtemSocket {
} }
fn on_commands_received(&mut self, payload: &[u8]) { fn on_commands_received(&mut self, payload: &[u8]) {
let commands = deserialize_commands(payload);
let _ = self let _ = self
.atem_event_tx .atem_event_tx
.send(AtemSocketEvent::ReceivedCommands(payload.to_vec())); .send(AtemEvent::ReceivedCommands(commands));
} }
fn on_command_acknowledged(&mut self, packets: Vec<AckedPacket>) { fn on_command_acknowledged(&mut self, packets: Vec<AckedPacket>) {
for ack in packets { for ack in packets {
let _ = self let _ = self
.atem_event_tx .atem_event_tx
.send(AtemSocketEvent::AckedCommand(TrackingId(ack.tracking_id))); .send(AtemEvent::AckedCommand(TrackingId(ack.tracking_id)));
} }
} }
async fn on_connect(&mut self) { async fn on_connect(&mut self) {
let _ = self.atem_event_tx.send(AtemSocketEvent::Connected); let _ = self.atem_event_tx.send(AtemEvent::Connected);
let mut connected_callbacks = self.connected_callbacks.lock().await; let mut connected_callbacks = self.connected_callbacks.lock().await;
for callback in connected_callbacks.drain(0..) { for callback in connected_callbacks.drain(0..) {
let _ = callback.send(false); let _ = callback.send(false);
@@ -559,7 +556,7 @@ impl AtemSocket {
} }
fn on_disconnect(&mut self) { fn on_disconnect(&mut self) {
let _ = self.atem_event_tx.send(AtemSocketEvent::Disconnected); let _ = self.atem_event_tx.send(AtemEvent::Disconnected);
} }
fn start_timers(&mut self) { fn start_timers(&mut self) {
@@ -1,4 +1,4 @@
use std::{collections::HashMap, fmt::Debug, process::Command, sync::Arc}; use std::{collections::HashMap, fmt::Debug, sync::Arc};
use crate::{enums::ProtocolVersion, state::AtemState}; use crate::{enums::ProtocolVersion, state::AtemState};
@@ -8,11 +8,10 @@ pub trait DeserializedCommand: Send + Sync + Debug {
} }
pub trait CommandDeserializer: Send + Sync { pub trait CommandDeserializer: Send + Sync {
fn deserialize(&self, buffer: &[u8], version: &ProtocolVersion) fn deserialize(&self, buffer: &[u8]) -> Arc<dyn DeserializedCommand>;
-> Arc<dyn DeserializedCommand>;
} }
pub trait SerializableCommand: Send + Sync { pub trait SerializableCommand {
fn payload(&self, version: &ProtocolVersion) -> Vec<u8>; fn payload(&self, version: &ProtocolVersion) -> Vec<u8>;
} }
@@ -28,7 +27,7 @@ impl<C: SerializableCommand + ?Sized> SerializableCommand for &'_ Box<C> {
} }
} }
pub trait BasicWritableCommand: SerializableCommand + Send + Sync { pub trait BasicWritableCommand: SerializableCommand {
fn get_raw_name(&self) -> &'static str; fn get_raw_name(&self) -> &'static str;
fn get_minimum_version(&self) -> ProtocolVersion; fn get_minimum_version(&self) -> ProtocolVersion;
} }
@@ -1,7 +1,34 @@
pub mod audio_mixer_config; use std::sync::Arc;
pub mod media_pool_config;
pub mod mix_effect_block_config; use crate::enums::ProtocolVersion;
pub mod multiviewer_config;
pub mod product_identifier; use super::command_base::{CommandDeserializer, DeserializedCommand};
pub mod topology;
pub mod version; pub const DESERIALIZE_VERSION_RAW_NAME: &str = "_ver";
#[derive(Debug)]
pub struct VersionCommand {
pub version: ProtocolVersion,
}
impl DeserializedCommand for VersionCommand {
fn raw_name(&self) -> &'static str {
DESERIALIZE_VERSION_RAW_NAME
}
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
state.info.api_version = self.version;
}
}
#[derive(Default)]
pub struct VersionCommandDeserializer {}
impl CommandDeserializer for VersionCommandDeserializer {
fn deserialize(&self, buffer: &[u8]) -> std::sync::Arc<dyn DeserializedCommand> {
let version = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
let version: ProtocolVersion = version.try_into().expect("Invalid protocol version");
Arc::new(VersionCommand { version })
}
}
@@ -1,47 +0,0 @@
use std::sync::Arc;
use crate::{
commands::command_base::{CommandDeserializer, DeserializedCommand},
state::{audio::AtemClassicAudioState, info::AudioMixerInfo},
};
pub const DESERIALIZE_AUDIO_MIXER_CONFIG_NAME: &str = "_AMC";
#[derive(Debug)]
pub struct AudioMixerConfig {
inputs: u8,
monitors: u8,
headphones: u8,
}
impl DeserializedCommand for AudioMixerConfig {
fn raw_name(&self) -> &'static str {
DESERIALIZE_AUDIO_MIXER_CONFIG_NAME
}
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
state.info.audio_mixer = Some(AudioMixerInfo::new(
self.inputs,
self.monitors,
self.headphones,
));
state.audio = Some(AtemClassicAudioState::new(self.inputs, self.monitors != 0))
}
}
#[derive(Default)]
pub struct AudioMixerConfigDeserializer {}
impl CommandDeserializer for AudioMixerConfigDeserializer {
fn deserialize(
&self,
buffer: &[u8],
version: &crate::enums::ProtocolVersion,
) -> std::sync::Arc<dyn DeserializedCommand> {
Arc::new(AudioMixerConfig {
inputs: buffer[0],
monitors: buffer[1],
headphones: buffer[2],
})
}
}
@@ -1,40 +0,0 @@
use std::sync::Arc;
use crate::{
commands::command_base::{CommandDeserializer, DeserializedCommand},
state::info::MediaPoolInfo,
};
pub const DESERIALIZE_MEDIA_POOL_CONFIG_NAME: &str = "_mpl";
#[derive(Debug)]
pub struct MediaPoolConfig {
still_count: u8,
clip_count: u8,
}
impl DeserializedCommand for MediaPoolConfig {
fn raw_name(&self) -> &'static str {
DESERIALIZE_MEDIA_POOL_CONFIG_NAME
}
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
state.info.media_pool = Some(MediaPoolInfo::new(self.still_count, self.clip_count))
}
}
#[derive(Default)]
pub struct MediaPoolConfigDeserializer {}
impl CommandDeserializer for MediaPoolConfigDeserializer {
fn deserialize(
&self,
buffer: &[u8],
_version: &crate::enums::ProtocolVersion,
) -> std::sync::Arc<dyn DeserializedCommand> {
Arc::new(MediaPoolConfig {
still_count: buffer[0],
clip_count: buffer[1],
})
}
}
@@ -1,40 +0,0 @@
use std::sync::Arc;
use crate::{
commands::command_base::{CommandDeserializer, DeserializedCommand},
state::info::MixEffectInfo,
};
pub const DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME: &str = "_MeC";
#[derive(Debug)]
pub struct MixEffectBlockConfig {
mix_effect: u8,
key_count: u8,
}
impl DeserializedCommand for MixEffectBlockConfig {
fn raw_name(&self) -> &'static str {
DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME
}
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
state.info.mix_effects[self.mix_effect as usize] = Some(MixEffectInfo::new(self.key_count));
}
}
#[derive(Default)]
pub struct MixEffectBlockConfigDeserializer {}
impl CommandDeserializer for MixEffectBlockConfigDeserializer {
fn deserialize(
&self,
buffer: &[u8],
_version: &crate::enums::ProtocolVersion,
) -> std::sync::Arc<dyn DeserializedCommand> {
Arc::new(MixEffectBlockConfig {
mix_effect: buffer[0],
key_count: buffer[1],
})
}
}
@@ -1,58 +0,0 @@
use std::sync::Arc;
use crate::{
commands::command_base::{CommandDeserializer, DeserializedCommand},
enums::ProtocolVersion,
state::info::MultiviewerInfo,
};
pub const DESERIALIZE_MULTIVIEWER_NAME: &str = "_MvC";
#[derive(Debug)]
pub struct MultiviewerConfig {
count: Option<u8>,
window_count: u8,
}
impl DeserializedCommand for MultiviewerConfig {
fn raw_name(&self) -> &'static str {
DESERIALIZE_MULTIVIEWER_NAME
}
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
// TODO: This can't be right...
let existing_count = match &state.info.multiviewer {
Some(multiviewer) => multiviewer.count().as_ref().copied(),
None => None,
};
let count = match self.count {
Some(count) => Some(count),
None => existing_count,
};
state.info.multiviewer = Some(MultiviewerInfo::new(count, self.window_count));
}
}
#[derive(Default)]
pub struct MultiviewerConfigDeserializer {}
impl CommandDeserializer for MultiviewerConfigDeserializer {
fn deserialize(
&self,
buffer: &[u8],
version: &crate::enums::ProtocolVersion,
) -> std::sync::Arc<dyn DeserializedCommand> {
if *version >= ProtocolVersion::V8_1_1 {
Arc::new(MultiviewerConfig {
count: None,
window_count: buffer[1],
})
} else {
Arc::new(MultiviewerConfig {
count: Some(buffer[0]),
window_count: buffer[1],
})
}
}
}
@@ -1,68 +0,0 @@
use std::{ffi::CString, sync::Arc};
use crate::{
commands::command_base::{CommandDeserializer, DeserializedCommand},
enums::{Model, ProtocolVersion},
};
pub const DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME: &str = "_pin";
#[derive(Debug)]
pub struct ProductIdentifier {
pub product_identifier: String,
pub model: Model,
}
impl DeserializedCommand for ProductIdentifier {
fn raw_name(&self) -> &'static str {
DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME
}
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
state.info.product_identifier = Some(self.product_identifier.clone());
state.info.model = self.model.clone();
match state.info.model {
Model::TwoME
| Model::TwoME4K
| Model::TwoMEBS4K
| Model::Constellation
| Model::Constellation8K
| Model::ConstellationHD4ME
| Model::Constellation4K4ME => {
state.info.power = vec![false, false];
}
_ => {
state.info.power = vec![false];
}
}
}
}
#[derive(Default)]
pub struct ProductIdentifierDeserializer {}
impl CommandDeserializer for ProductIdentifierDeserializer {
fn deserialize(
&self,
buffer: &[u8],
version: &ProtocolVersion,
) -> Arc<dyn DeserializedCommand> {
let null_byte_index = buffer
.iter()
.position(|byte| *byte == b'\0')
.expect("No null byte");
let product_identifier =
CString::from_vec_with_nul(buffer[..(null_byte_index + 1)].to_vec())
.expect("Malformed string");
let model = buffer[40];
Arc::new(ProductIdentifier {
product_identifier: product_identifier
.to_str()
.expect("Invalid rust string")
.to_string(),
model: model.into(),
})
}
}
@@ -1,117 +0,0 @@
use std::sync::Arc;
use crate::{
commands::command_base::{CommandDeserializer, DeserializedCommand},
enums::ProtocolVersion,
state::info::{AtemCapabilites, MultiviewerInfo},
};
pub const DESERIALIZE_TOPOLOGY_RAW_NAME: &str = "_top";
#[derive(Debug)]
pub struct Topology {
mix_effects: u8,
sources: u8,
auxilliaries: u8,
mix_minus_outputs: u8,
media_players: u8,
multiviewers: Option<u8>,
serial_ports: u8,
max_hyperdecks: u8,
dves: u8,
stingers: u8,
super_sources: u8,
talkback_channels: u8,
downstream_keyers: u8,
camera_control: bool,
advanced_chroma_keyers: bool,
only_configurable_outputs: bool,
}
impl DeserializedCommand for Topology {
fn raw_name(&self) -> &'static str {
DESERIALIZE_TOPOLOGY_RAW_NAME
}
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
state.info.capabilities = Some(AtemCapabilites::new(
self.mix_effects,
self.sources,
self.auxilliaries,
self.mix_minus_outputs,
self.media_players,
self.serial_ports,
self.max_hyperdecks,
self.dves,
self.stingers,
self.super_sources,
self.talkback_channels,
self.downstream_keyers,
self.camera_control,
self.advanced_chroma_keyers,
self.only_configurable_outputs,
));
let window_count = if let Some(mv) = &state.info.multiviewer {
*mv.window_count()
} else {
10
};
state.info.multiviewer = Some(MultiviewerInfo::new(self.multiviewers, window_count));
}
}
#[derive(Default)]
pub struct TopologyDeserializer {}
impl CommandDeserializer for TopologyDeserializer {
fn deserialize(
&self,
buffer: &[u8],
version: &ProtocolVersion,
) -> Arc<dyn DeserializedCommand> {
let v230offset = if *version > ProtocolVersion::V8_0_1 {
1
} else {
0
};
let multiviewers = if v230offset > 0 {
Some(buffer[6])
} else {
None
};
let advanced_chroma_keyers = if buffer.len() > 20 {
buffer[21 + v230offset] == 1
} else {
false
};
let only_configurable_outputs = if buffer.len() > 20 {
buffer[22 + v230offset] == 1
} else {
false
};
Arc::new(Topology {
mix_effects: buffer[0],
sources: buffer[1],
downstream_keyers: buffer[2],
auxilliaries: buffer[3],
mix_minus_outputs: buffer[4],
media_players: buffer[5],
multiviewers,
serial_ports: buffer[6 + v230offset],
max_hyperdecks: buffer[7 + v230offset],
dves: buffer[8 + v230offset],
stingers: buffer[9 + v230offset],
super_sources: buffer[10 + v230offset],
talkback_channels: buffer[12 + v230offset],
camera_control: buffer[17 + v230offset] == 1,
advanced_chroma_keyers,
only_configurable_outputs,
})
}
}
@@ -1,25 +0,0 @@
use crate::{commands::command_base::DeserializedCommand, enums::ProtocolVersion};
pub const DESERIALIZE_VERSION_RAW_NAME: &str = "_ver";
#[derive(Debug)]
pub struct Version {
pub version: ProtocolVersion,
}
impl DeserializedCommand for Version {
fn raw_name(&self) -> &'static str {
DESERIALIZE_VERSION_RAW_NAME
}
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
state.info.api_version = self.version.clone();
}
}
pub fn deserialize_version(buffer: &[u8]) -> Version {
let version = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
let version: ProtocolVersion = version.try_into().expect("Invalid protocol version");
Version { version }
}
@@ -1,7 +1,5 @@
use std::sync::Arc; use std::sync::Arc;
use crate::enums::ProtocolVersion;
use super::command_base::{CommandDeserializer, DeserializedCommand}; use super::command_base::{CommandDeserializer, DeserializedCommand};
pub const DESERIALIZE_INIT_COMPLETE_RAW_NAME: &str = "InCm"; pub const DESERIALIZE_INIT_COMPLETE_RAW_NAME: &str = "InCm";
@@ -21,11 +19,7 @@ impl DeserializedCommand for InitComplete {
pub struct InitCompleteDeserializer {} pub struct InitCompleteDeserializer {}
impl CommandDeserializer for InitCompleteDeserializer { impl CommandDeserializer for InitCompleteDeserializer {
fn deserialize( fn deserialize(&self, _buffer: &[u8]) -> std::sync::Arc<dyn DeserializedCommand> {
&self,
_buffer: &[u8],
version: &ProtocolVersion,
) -> Arc<dyn DeserializedCommand> {
Arc::new(InitComplete {}) Arc::new(InitComplete {})
} }
} }
@@ -4,7 +4,6 @@ use crate::{
commands::command_base::{ commands::command_base::{
BasicWritableCommand, CommandDeserializer, DeserializedCommand, SerializableCommand, BasicWritableCommand, CommandDeserializer, DeserializedCommand, SerializableCommand,
}, },
enums::ProtocolVersion,
state::util::get_mix_effect, state::util::get_mix_effect,
}; };
@@ -59,11 +58,7 @@ impl DeserializedCommand for ProgramInput {
pub struct ProgramInputDeserializer {} pub struct ProgramInputDeserializer {}
impl CommandDeserializer for ProgramInputDeserializer { impl CommandDeserializer for ProgramInputDeserializer {
fn deserialize( fn deserialize(&self, buffer: &[u8]) -> Arc<dyn DeserializedCommand> {
&self,
buffer: &[u8],
version: &ProtocolVersion,
) -> Arc<dyn DeserializedCommand> {
let mix_effect = buffer[0]; let mix_effect = buffer[0];
let source = u16::from_be_bytes([buffer[2], buffer[3]]); let source = u16::from_be_bytes([buffer[2], buffer[3]]);
@@ -1,35 +1,16 @@
use std::{collections::VecDeque, sync::Arc}; use std::{collections::VecDeque, sync::Arc};
use crate::{
commands::device_profile::version::{deserialize_version, DESERIALIZE_VERSION_RAW_NAME},
enums::ProtocolVersion,
};
use super::{ use super::{
command_base::{CommandDeserializer, DeserializedCommand}, command_base::{CommandDeserializer, DeserializedCommand},
device_profile::{ device_profile::{VersionCommandDeserializer, DESERIALIZE_VERSION_RAW_NAME},
audio_mixer_config::{AudioMixerConfigDeserializer, DESERIALIZE_AUDIO_MIXER_CONFIG_NAME},
media_pool_config::{MediaPoolConfigDeserializer, DESERIALIZE_MEDIA_POOL_CONFIG_NAME},
mix_effect_block_config::{
MixEffectBlockConfigDeserializer, DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME,
},
multiviewer_config::{MultiviewerConfigDeserializer, DESERIALIZE_MULTIVIEWER_NAME},
product_identifier::{
ProductIdentifierDeserializer, DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME,
},
topology::{TopologyDeserializer, DESERIALIZE_TOPOLOGY_RAW_NAME},
},
init_complete::{InitCompleteDeserializer, DESERIALIZE_INIT_COMPLETE_RAW_NAME}, init_complete::{InitCompleteDeserializer, DESERIALIZE_INIT_COMPLETE_RAW_NAME},
mix_effects::program_input::{ProgramInputDeserializer, DESERIALIZE_PROGRAM_INPUT_RAW_NAME}, mix_effects::program_input::{ProgramInputDeserializer, DESERIALIZE_PROGRAM_INPUT_RAW_NAME},
tally_by_source::{TallyBySourceDeserializer, DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME}, tally_by_source::{TallyBySourceDeserializer, DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME},
time::{TimeDeserializer, DESERIALIZE_TIME_RAW_NAME}, time::{TimeDeserializer, DESERIALIZE_TIME_RAW_NAME},
}; };
pub fn deserialize_commands( pub fn deserialize_commands(payload: &[u8]) -> VecDeque<Arc<dyn DeserializedCommand>> {
payload: &[u8], let mut parsed_commands = VecDeque::new();
version: &mut ProtocolVersion,
) -> VecDeque<Arc<dyn DeserializedCommand>> {
let mut parsed_commands: VecDeque<Arc<dyn DeserializedCommand>> = VecDeque::new();
let mut head = 0; let mut head = 0;
while payload.len() > head + 8 { while payload.len() > head + 8 {
@@ -44,21 +25,10 @@ pub fn deserialize_commands(
log::debug!("Received command {} with length {}", name, length); log::debug!("Received command {} with length {}", name, length);
let command_buffer = &payload[head + 8..head + length]; if let Some(deserializer) = command_deserializer_from_string(name.as_str()) {
let deserialized_command = deserializer.deserialize(&payload[head + 8..head + length]);
if name == DESERIALIZE_VERSION_RAW_NAME {
let version_command = deserialize_version(command_buffer);
*version = version_command.version.clone();
log::info!("Switched to protocol version {}", version);
parsed_commands.push_back(Arc::new(version_command));
} else if let Some(deserializer) = command_deserializer_from_string(name.as_str()) {
let deserialized_command = deserializer.deserialize(command_buffer, version);
log::debug!("Received {:?}", deserialized_command); log::debug!("Received {:?}", deserialized_command);
parsed_commands.push_back(deserialized_command); parsed_commands.push_back(deserialized_command);
} else {
log::warn!("Received command {name} for which there is no deserializer.");
// TODO: Remove!
todo!("Write deserializer for {name}.");
} }
head += length; head += length;
@@ -69,20 +39,11 @@ pub fn deserialize_commands(
fn command_deserializer_from_string(command_str: &str) -> Option<Box<dyn CommandDeserializer>> { fn command_deserializer_from_string(command_str: &str) -> Option<Box<dyn CommandDeserializer>> {
match command_str { match command_str {
DESERIALIZE_VERSION_RAW_NAME => Some(Box::<VersionCommandDeserializer>::default()),
DESERIALIZE_INIT_COMPLETE_RAW_NAME => Some(Box::<InitCompleteDeserializer>::default()), DESERIALIZE_INIT_COMPLETE_RAW_NAME => Some(Box::<InitCompleteDeserializer>::default()),
DESERIALIZE_PROGRAM_INPUT_RAW_NAME => Some(Box::<ProgramInputDeserializer>::default()), DESERIALIZE_PROGRAM_INPUT_RAW_NAME => Some(Box::<ProgramInputDeserializer>::default()),
DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME => Some(Box::<TallyBySourceDeserializer>::default()), DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME => Some(Box::<TallyBySourceDeserializer>::default()),
DESERIALIZE_TIME_RAW_NAME => Some(Box::<TimeDeserializer>::default()), DESERIALIZE_TIME_RAW_NAME => Some(Box::<TimeDeserializer>::default()),
DESERIALIZE_TOPOLOGY_RAW_NAME => Some(Box::<TopologyDeserializer>::default()),
DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME => {
Some(Box::<MixEffectBlockConfigDeserializer>::default())
}
DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME => {
Some(Box::<ProductIdentifierDeserializer>::default())
}
DESERIALIZE_MEDIA_POOL_CONFIG_NAME => Some(Box::<MediaPoolConfigDeserializer>::default()),
DESERIALIZE_MULTIVIEWER_NAME => Some(Box::<MultiviewerConfigDeserializer>::default()),
DESERIALIZE_AUDIO_MIXER_CONFIG_NAME => Some(Box::<AudioMixerConfigDeserializer>::default()),
_ => None, _ => None,
} }
} }
@@ -1,7 +1,5 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use crate::enums::ProtocolVersion;
use super::command_base::{CommandDeserializer, DeserializedCommand}; use super::command_base::{CommandDeserializer, DeserializedCommand};
pub const DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME: &str = "TlSr"; pub const DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME: &str = "TlSr";
@@ -31,11 +29,7 @@ impl DeserializedCommand for TallyBySource {
pub struct TallyBySourceDeserializer {} pub struct TallyBySourceDeserializer {}
impl CommandDeserializer for TallyBySourceDeserializer { impl CommandDeserializer for TallyBySourceDeserializer {
fn deserialize( fn deserialize(&self, buffer: &[u8]) -> Arc<dyn DeserializedCommand> {
&self,
buffer: &[u8],
version: &ProtocolVersion,
) -> Arc<dyn DeserializedCommand> {
let source_count = u16::from_be_bytes([buffer[0], buffer[1]]) as usize; let source_count = u16::from_be_bytes([buffer[0], buffer[1]]) as usize;
log::debug!("{:?}", buffer); log::debug!("{:?}", buffer);
+1 -4
View File
@@ -1,7 +1,5 @@
use std::sync::Arc; use std::sync::Arc;
use crate::enums::ProtocolVersion;
use super::command_base::{CommandDeserializer, DeserializedCommand}; use super::command_base::{CommandDeserializer, DeserializedCommand};
pub const DESERIALIZE_TIME_RAW_NAME: &str = "Time"; pub const DESERIALIZE_TIME_RAW_NAME: &str = "Time";
@@ -35,8 +33,7 @@ impl CommandDeserializer for TimeDeserializer {
fn deserialize( fn deserialize(
&self, &self,
buffer: &[u8], buffer: &[u8],
version: &ProtocolVersion, ) -> std::sync::Arc<dyn super::command_base::DeserializedCommand> {
) -> Arc<dyn DeserializedCommand> {
let info = TimeInfo { let info = TimeInfo {
hour: buffer[0], hour: buffer[0],
minute: buffer[1], minute: buffer[1],
+2 -67
View File
@@ -1,6 +1,4 @@
use std::fmt::Display; #[derive(Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq)]
pub enum Model { pub enum Model {
#[default] #[default]
Unknown = 0x00, Unknown = 0x00,
@@ -21,59 +19,9 @@ pub enum Model {
MiniProISO = 0x0f, MiniProISO = 0x0f,
MiniExtreme = 0x10, MiniExtreme = 0x10,
MiniExtremeISO = 0x11, MiniExtremeISO = 0x11,
ConstellationHD1ME = 0x12,
ConstellationHD2ME = 0x13,
ConstellationHD4ME = 0x14,
SDI = 0x15,
SDIProISO = 0x16,
SDIExtremeISO = 0x17,
// 0x18 ??
// 0x19 ??
TelevisionStudioHD8 = 0x1a,
TelevisionStudioHD8ISO = 0x1b,
// 0x1c ??
// 0x1d ??
Constellation4K4ME = 0x1e,
// 0x1f ??
TelevisionStudio4K8 = 0x20,
} }
impl From<u8> for Model { #[derive(Debug, Default, Clone, Copy, PartialEq)]
fn from(value: u8) -> Self {
match value {
0x01 => Model::TVS,
0x02 => Model::OneME,
0x03 => Model::TwoME,
0x04 => Model::PS4K,
0x05 => Model::OneME4K,
0x06 => Model::TwoME4K,
0x07 => Model::TwoMEBS4K,
0x08 => Model::TVSHD,
0x09 => Model::TVSProHD,
0x0a => Model::TVSPro4K,
0x0b => Model::Constellation,
0x0c => Model::Constellation8K,
0x0d => Model::Mini,
0x0e => Model::MiniPro,
0x0f => Model::MiniProISO,
0x10 => Model::MiniExtreme,
0x11 => Model::MiniExtremeISO,
0x12 => Model::ConstellationHD1ME,
0x13 => Model::ConstellationHD2ME,
0x14 => Model::ConstellationHD4ME,
0x15 => Model::SDI,
0x16 => Model::SDIProISO,
0x17 => Model::SDIExtremeISO,
0x1a => Model::TelevisionStudioHD8,
0x1b => Model::TelevisionStudioHD8ISO,
0x1e => Model::Constellation4K4ME,
0x20 => Model::TelevisionStudio4K8,
_ => Model::Unknown,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
pub enum ProtocolVersion { pub enum ProtocolVersion {
#[default] #[default]
Unknown = 0, Unknown = 0,
@@ -100,19 +48,6 @@ impl TryFrom<u32> for ProtocolVersion {
} }
} }
impl Display for ProtocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProtocolVersion::Unknown => write!(f, "Unknown"),
ProtocolVersion::V7_2 => write!(f, "v7.2"),
ProtocolVersion::V7_5_2 => write!(f, "v7.5.2"),
ProtocolVersion::V8_0 => write!(f, "v8.0"),
ProtocolVersion::V8_0_1 => write!(f, "v8.0.1"),
ProtocolVersion::V8_1_1 => write!(f, "v8.1.1"),
}
}
}
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub enum TransitionStyle { pub enum TransitionStyle {
MIX = 0x00, MIX = 0x00,
+4 -18
View File
@@ -44,28 +44,14 @@ pub struct ClassicAudioHeadphoneOutputChannel {
pub talkback_gain: f64, pub talkback_gain: f64,
} }
#[derive(Clone, PartialEq, Getters)] #[derive(Clone, PartialEq, Getters, new)]
pub struct AtemClassicAudioState { pub struct AtemClassicAudioState {
number_of_channels: u8, number_of_channels: Option<f64>,
has_monitor: bool, has_monitor: Option<bool>,
pub channels: HashMap<u64, ClassicAudioChannel>, pub channels: HashMap<u64, ClassicAudioChannel>,
pub monitor: Option<ClassicAudioMonitorChannel>, pub monitor: Option<ClassicAudioMonitorChannel>,
pub headphones: Option<ClassicAudioHeadphoneOutputChannel>, pub headphones: Option<ClassicAudioHeadphoneOutputChannel>,
pub master: Option<ClassicAudioMasterChannel>, pub master: Option<ClassicAudioMasterChannel>,
pub audio_follow_video_crossfade_transition_enabled: bool, pub audio_follow_video_crossfade_transition_enabled: Option<bool>,
}
impl AtemClassicAudioState {
pub fn new(number_of_channels: u8, has_monitor: bool) -> Self {
Self {
number_of_channels,
has_monitor,
channels: Default::default(),
monitor: Default::default(),
headphones: Default::default(),
master: Default::default(),
audio_follow_video_crossfade_transition_enabled: false,
}
}
} }
+19 -19
View File
@@ -3,17 +3,17 @@ use crate::enums::{Model, ProtocolVersion};
#[derive(Clone, PartialEq, Getters, new)] #[derive(Clone, PartialEq, Getters, new)]
pub struct AtemCapabilites { pub struct AtemCapabilites {
mix_effects: u8, mix_effects: u8,
sources: u8, sources: u64,
auxilliaries: u8, auxilliaries: u64,
mix_minus_outputs: u8, mix_minus_outputs: u64,
media_players: u8, media_players: u64,
serial_ports: u8, serial_ports: u64,
max_hyperdecks: u8, max_hyperdecks: u64,
dves: u8, dves: u64,
stingers: u8, stingers: u64,
super_sources: u8, super_sources: u64,
talkback_channels: u8, talkback_channels: u64,
downstream_keyers: u8, downstream_keyers: u64,
camera_control: bool, camera_control: bool,
advanced_chroma_keyers: bool, advanced_chroma_keyers: bool,
only_configurable_outputs: bool, only_configurable_outputs: bool,
@@ -21,7 +21,7 @@ pub struct AtemCapabilites {
#[derive(Clone, PartialEq, Getters, new)] #[derive(Clone, PartialEq, Getters, new)]
pub struct MixEffectInfo { pub struct MixEffectInfo {
key_count: u8, key_count: u64,
} }
#[derive(Clone, PartialEq, Getters, new)] #[derive(Clone, PartialEq, Getters, new)]
@@ -31,9 +31,9 @@ pub struct SuperSourceInfo {
#[derive(Clone, PartialEq, Getters, new)] #[derive(Clone, PartialEq, Getters, new)]
pub struct AudioMixerInfo { pub struct AudioMixerInfo {
inputs: u8, inputs: u64,
monitors: u8, monitors: u64,
headphones: u8, headphones: u64,
} }
#[derive(Clone, PartialEq, Getters, new)] #[derive(Clone, PartialEq, Getters, new)]
@@ -49,14 +49,14 @@ pub struct MacroPoolInfo {
#[derive(Clone, PartialEq, Getters, new)] #[derive(Clone, PartialEq, Getters, new)]
pub struct MediaPoolInfo { pub struct MediaPoolInfo {
still_count: u8, still_count: u64,
clip_count: u8, clip_count: u64,
} }
#[derive(Clone, PartialEq, Getters, new)] #[derive(Clone, PartialEq, Getters, new)]
pub struct MultiviewerInfo { pub struct MultiviewerInfo {
count: Option<u8>, count: u64,
window_count: u8, window_count: u64,
} }
#[derive(Clone, PartialEq, new)] #[derive(Clone, PartialEq, new)]
+10 -13
View File
@@ -13,7 +13,7 @@ use atem_connection_rs::{
use clap::Parser; use clap::Parser;
use color_eyre::Report; use color_eyre::Report;
use tokio::{select, time::sleep}; use tokio::time::sleep;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
/// ATEM Rust Library Test App /// ATEM Rust Library Test App
@@ -35,14 +35,19 @@ async fn main() {
tokio::sync::mpsc::channel::<AtemSocketMessage>(10); tokio::sync::mpsc::channel::<AtemSocketMessage>(10);
let (atem_event_tx, atem_event_rx) = tokio::sync::mpsc::unbounded_channel(); let (atem_event_tx, atem_event_rx) = tokio::sync::mpsc::unbounded_channel();
let cancel = CancellationToken::new(); let cancel = CancellationToken::new();
let cancel_task = cancel.clone();
let mut atem_socket = AtemSocket::new(socket_message_rx, atem_event_tx); let mut atem_socket = AtemSocket::new(atem_event_tx);
tokio::spawn(async move {
atem_socket.run(socket_message_rx, cancel_task).await;
});
let atem = Arc::new(Atem::new(atem_socket, socket_message_tx)); let atem = Arc::new(Atem::new(socket_message_tx));
let atem_thread = atem.clone(); let atem_thread = atem.clone();
let atem_run = atem_thread.run(atem_event_rx, cancel); tokio::spawn(async move {
atem_thread.run(atem_event_rx, cancel).await;
});
let switch_loop = tokio::spawn(async move {
let address = Ipv4Addr::from_str(&args.ip).unwrap(); let address = Ipv4Addr::from_str(&args.ip).unwrap();
let socket = SocketAddrV4::new(address, 9910); let socket = SocketAddrV4::new(address, 9910);
atem.connect(socket.into()).await; atem.connect(socket.into()).await;
@@ -52,18 +57,10 @@ async fn main() {
log::info!("Switch to source 1"); log::info!("Switch to source 1");
atem.send_commands(vec![Box::new(ProgramInput::new(0, 1))]) atem.send_commands(vec![Box::new(ProgramInput::new(0, 1))])
.await; .await;
log::info!("Switched to source 1");
sleep(Duration::from_millis(5000)).await; sleep(Duration::from_millis(5000)).await;
log::info!("Switch to source 2"); log::info!("Switch to source 2");
atem.send_commands(vec![Box::new(ProgramInput::new(0, 2))]) atem.send_commands(vec![Box::new(ProgramInput::new(0, 2))])
.await; .await;
log::info!("Switched to source 2");
}
});
select! {
_ = atem_run => {},
_ = switch_loop => {}
} }
} }
Generated
+98 -30
View File
@@ -2,14 +2,15 @@
"nodes": { "nodes": {
"devshell": { "devshell": {
"inputs": { "inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1741473158, "lastModified": 1705332421,
"narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", "narHash": "sha256-USpGLPme1IuqG78JNqSaRabilwkCyHmVWY0M9vYyqEA=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", "rev": "83cb93d6d063ad290beee669f4badf9914cc16ec",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -18,16 +19,52 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": { "naersk": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1745925850, "lastModified": 1698420672,
"narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=", "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f", "rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -38,11 +75,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1722073938, "lastModified": 1704161960,
"narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=", "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae", "rev": "63143ac2c9186be6d9da6035fa22620018c85932",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -54,26 +91,26 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1749401433, "lastModified": 1705883077,
"narHash": "sha256-HXIQzULIG/MEUW2Q/Ss47oE3QrjxvpUX7gUl4Xp6lnc=", "narHash": "sha256-ByzHHX3KxpU1+V0erFy8jpujTufimh6KaS/Iv3AciHk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "08fcb0dcb59df0344652b38ea6326a2d8271baff", "rev": "5f5210aa20e343b7e35f40c033000db0ef80d7b9",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "id": "nixpkgs",
"ref": "nixpkgs-unstable", "type": "indirect"
"repo": "nixpkgs",
"type": "github"
} }
}, },
"nixpkgs_3": { "nixpkgs_3": {
"locked": { "locked": {
"lastModified": 0, "lastModified": 1705883077,
"narHash": "sha256-DDe16FJk18sadknQKKG/9FbwEro7A57tg9vB5kxZ8kY=", "narHash": "sha256-ByzHHX3KxpU1+V0erFy8jpujTufimh6KaS/Iv3AciHk=",
"path": "/nix/store/2d1ahim48jhzg4bbm97mvjlb4p7fpan3-source", "owner": "NixOS",
"type": "path" "repo": "nixpkgs",
"rev": "5f5210aa20e343b7e35f40c033000db0ef80d7b9",
"type": "github"
}, },
"original": { "original": {
"id": "nixpkgs", "id": "nixpkgs",
@@ -82,11 +119,11 @@
}, },
"nixpkgs_4": { "nixpkgs_4": {
"locked": { "locked": {
"lastModified": 1744536153, "lastModified": 1681358109,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -107,14 +144,15 @@
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_4" "nixpkgs": "nixpkgs_4"
}, },
"locked": { "locked": {
"lastModified": 1749436897, "lastModified": 1705976279,
"narHash": "sha256-OkDtaCGQQVwVFz5HWfbmrMJR99sFIMXHCHEYXzUJEJY=", "narHash": "sha256-Zx97bJ3+O8IP70uJPD//rRsr8bcxICISMTZUT/L9eFk=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "e7876c387e35dc834838aff254d8e74cf5bd4f19", "rev": "f889dc31ef97835834bdc3662394ebdb3c96b974",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -138,16 +176,46 @@
"type": "github" "type": "github"
} }
}, },
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": { "utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems_3"
}, },
"locked": { "locked": {
"lastModified": 1731533236, "lastModified": 1705309234,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github" "type": "github"
}, },
"original": { "original": {