Skip to main content

synapto_interface/
types.rs

1pub mod documents;
2use crate::{cognitive_output_text::types::CognitiveOutputText, llm::LLMSafe};
3use crate::peer_input_text::types::PeerInputText;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
8pub enum DocumentIngestionPolicy {
9    /// Save the raw document only, do not attempt to parse or extract text.
10    Store,
11    /// Save the raw document and run active parsers (e.g. PDF parser) to extract UTF-8 text.
12    StoreAndParse,
13}
14
15#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
16pub struct DocumentRegistrationRequest {
17    pub original_filename: String,
18    pub mime_type: String,
19    pub data: Vec<u8>,
20    pub policy: DocumentIngestionPolicy,
21}
22
23pub struct AddDocumentRequest {
24    pub request: DocumentRegistrationRequest,
25    pub reply_tx: tokio::sync::oneshot::Sender<DocumentId>,
26}
27crate::register_channel_name!(AddDocumentRequest, "add_document_request");
28
29#[derive(
30    Serialize,
31    Deserialize,
32    JsonSchema,
33    PartialEq,
34    Eq,
35    Debug,
36    Clone,
37    derive_more::Display,
38    derive_more::From,
39    derive_more::Deref,
40)]
41pub struct ToolCallId(pub String);
42crate::register_channel_name!(ToolCallId, "tool_call_id");
43
44/// A unique identifier for a message sender.
45#[derive(
46    Serialize,
47    Deserialize,
48    JsonSchema,
49    PartialEq,
50    Eq,
51    Debug,
52    Clone,
53    derive_more::Display,
54    derive_more::From,
55    derive_more::Deref,
56)]
57pub struct SenderId(pub String);
58
59/// Represents the text content of a message.
60#[derive(
61    Serialize,
62    Deserialize,
63    JsonSchema,
64    PartialEq,
65    Eq,
66    Debug,
67    Clone,
68    derive_more::Display,
69    derive_more::From,
70    derive_more::Deref,
71)]
72pub struct MessageText(pub String);
73
74/// A unique identifier for a document resource.
75#[derive(
76    Serialize,
77    Deserialize,
78    JsonSchema,
79    PartialEq,
80    Eq,
81    Debug,
82    Clone,
83    derive_more::Display,
84    derive_more::From,
85    derive_more::Deref,
86)]
87pub struct DocumentId(pub String);
88crate::register_channel_name!(DocumentId, "document_id");
89
90/// An opaque channel identifier used to route messages within the system.
91#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug, Clone)]
92pub struct MessageChannel {
93    /// Opaque JSON context provided by plugins or core modules.
94    pub context: serde_json::Value,
95}
96
97pub use crate::speech_to_text::types::SpeakerId;
98
99/// Represents the identity of a speaker.
100#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
101pub enum Speaker {
102    /// An unknown speaker, optionally with a temporary ID.
103    Unknown(Option<SpeakerId>),
104    /// A recognized speaker with a stable ID.
105    Recognized(SpeakerId),
106}
107
108/// Input message originating from speech transcription.
109#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
110pub struct PeerInputSpeech {
111    /// The channel where the speech was captured.
112    pub channel: MessageChannel,
113    /// The speaker who produced the speech.
114    pub speaker: Speaker,
115    /// The transcribed text.
116    pub transcript: MessageText,
117}
118crate::register_channel_name!(PeerInputSpeech, "peer_input_speech");
119
120/// Unified input message type for the cognitive loop.
121#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, JsonSchema)]
122pub enum PeerInput {
123    /// Input from a speech source.
124    Speech(PeerInputSpeech),
125    /// Input from a text source (e.g. chat).
126    Text(crate::peer_input_text::types::PeerInputText),
127}
128
129/// Output message intended to be spoken by the system.
130#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug, Clone)]
131pub struct CognitiveOutputSpeech {
132    /// The target channel for the speech output.
133    pub target_channel: MessageChannel,
134    /// The text to be spoken.
135    pub text: String,
136}
137crate::register_channel_name!(CognitiveOutputSpeech, "ai_output_speech");
138
139/// Current operational state of the cognitive loop.
140#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug, Clone)]
141pub enum CognitiveState {
142    /// The AI is actively thinking/processing.
143    Thinking,
144    /// The AI is performing document search or RAG.
145    Searching,
146    /// The AI is executing an external command.
147    Acting,
148    /// The AI is waiting for new input.
149    Idle,
150}
151
152/// Update event for the system's cognitive state.
153#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug, Clone)]
154pub struct CognitiveStateUpdate {
155    /// The context of the state update (e.g. plugin-specific metadata).
156    pub context: serde_json::Value,
157    /// The new cognitive state.
158    pub state: CognitiveState,
159}
160crate::register_channel_name!(CognitiveStateUpdate, "cognitive_state");
161
162/// A generic envelope wrapping a payload with its source plugin identity.
163#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
164pub struct Enveloped<T> {
165    /// The name/ID of the plugin associated with this payload.
166    pub plugin: String,
167    /// The actual data being carried.
168    pub payload: T,
169}
170
171impl<T> Enveloped<T> {
172    /// Creates a new enveloped payload.
173    pub fn new(plugin: impl Into<String>, payload: T) -> Self {
174        Self {
175            plugin: plugin.into(),
176            payload,
177        }
178    }
179}
180
181crate::register_channel_name!(Enveloped<PeerInputText>, "peer_input_text_enveloped");
182crate::register_channel_name!(
183    Enveloped<CognitiveOutputText>,
184    "cognitive_output_text_enveloped"
185);
186crate::register_channel_name!(Enveloped<CognitiveStateUpdate>, "cognitive_state_enveloped");
187
188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189pub enum TemporalScope {
190    Historical,
191    Current, // TODO document what is usesd for in the core
192    Prospective,
193}
194
195#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
196pub struct ContextInteraction {
197    pub peer_input: Option<String>,
198    pub ai_reasoning: Option<String>,
199    pub ai_output: Option<String>,
200}
201
202#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
203pub struct ContextRequest {
204    /// The sliding window of recent conversational flow.
205    /// Used by plugins to perform Associative RAG.
206    /// An empty list implies a request for the unfiltered, baseline state.
207    pub recent_interactions: Vec<ContextInteraction>,
208    pub initial_run: bool,
209}
210
211#[async_trait::async_trait]
212pub trait ContextProvider: Send + Sync + 'static {
213    type Context: schemars::JsonSchema + serde::Serialize + LLMSafe + Send + Sync + 'static;
214
215    /// Declarative compile-time semantic key (e.g., "state", "active_tasks")
216    const NAME: &'static str;
217
218    /// The dimension this context belongs to
219    const SCOPE: TemporalScope;
220
221    /// Provide the JSON-serializable context view, filtered associatively via ContextRequest
222    async fn context(&self, request: &ContextRequest) -> Result<Self::Context, String>;
223
224    /// Decentralized Wakeup Signal:
225    /// Returns a receiver that signals when this specific context mutates.
226    fn subscribe(&self) -> Option<tokio::sync::watch::Receiver<()>> {
227        None
228    }
229}
230
231#[async_trait::async_trait]
232pub trait ErasedContextProvider: Send + Sync + 'static {
233    fn name(&self) -> &'static str;
234    fn scope(&self) -> TemporalScope;
235    fn schema(&self) -> schemars::Schema;
236    async fn erased_context(&self, request: &ContextRequest) -> Result<serde_json::Value, String>;
237    fn subscribe(&self) -> Option<tokio::sync::watch::Receiver<()>>;
238}
239
240#[async_trait::async_trait]
241impl<T> ErasedContextProvider for T
242where
243    T: ContextProvider,
244{
245    fn name(&self) -> &'static str {
246        <T as ContextProvider>::NAME
247    }
248    fn scope(&self) -> TemporalScope {
249        <T as ContextProvider>::SCOPE
250    }
251    fn schema(&self) -> schemars::Schema {
252        schemars::schema_for!(<T as ContextProvider>::Context)
253    }
254    async fn erased_context(&self, request: &ContextRequest) -> Result<serde_json::Value, String> {
255        let view = <T as ContextProvider>::context(self, request).await?;
256        serde_json::to_value(view).map_err(|e| e.to_string())
257    }
258    fn subscribe(&self) -> Option<tokio::sync::watch::Receiver<()>> {
259        <T as ContextProvider>::subscribe(self)
260    }
261}
262
263pub struct ContextRegistryBuilder {
264    pub providers: std::sync::RwLock<Vec<std::sync::Arc<dyn ErasedContextProvider>>>,
265    change_tx: tokio::sync::watch::Sender<()>,
266    change_rx: tokio::sync::watch::Receiver<()>,
267}
268
269impl Default for ContextRegistryBuilder {
270    fn default() -> Self {
271        let (change_tx, change_rx) = tokio::sync::watch::channel(());
272        Self {
273            providers: std::sync::RwLock::new(Vec::new()),
274            change_tx,
275            change_rx,
276        }
277    }
278}
279
280impl ContextRegistryBuilder {
281    pub fn register<T>(&self, provider: T)
282    where
283        T: ErasedContextProvider + 'static,
284    {
285        let provider_arc: std::sync::Arc<dyn ErasedContextProvider> = std::sync::Arc::new(provider);
286        self.register_erased(provider_arc);
287    }
288
289    pub fn register_erased(&self, provider: std::sync::Arc<dyn ErasedContextProvider>) {
290        self.providers
291            .write()
292            .unwrap_or_else(|e| panic!("Failed to acquire write lock on providers: {:?}", e))
293            .push(provider.clone());
294
295        // Forward individual watch changes to the unified registry-wide watch channel
296        if let Some(mut sub_rx) = provider.subscribe() {
297            let change_tx = self.change_tx.clone();
298            tokio::spawn(async move {
299                while sub_rx.changed().await.is_ok() {
300                    change_tx
301                        .send(())
302                        .inspect_err(|e| tracing::error!("{}", e))
303                        .ok();
304                }
305            });
306        }
307    }
308
309    pub fn subscribe(&self) -> tokio::sync::watch::Receiver<()> {
310        self.change_rx.clone()
311    }
312}
313
314#[derive(Clone)]
315pub struct PluginContext {
316    llm_executor: std::sync::Arc<dyn crate::llm::LlmExecutor>,
317    plugin_config: serde_json::Value,
318
319    // Private fields: Plugins cannot access these directly.
320    // They are exclusively used to securely bootstrap storage connections.
321    storage: std::sync::Arc<crate::storage::StorageRegistry>,
322    plugin_namespace: String,
323    data_dir: std::path::PathBuf,
324    storage_config_resolver: std::sync::Arc<dyn crate::storage::StorageConfigResolver>,
325    current_context_rx: tokio::sync::watch::Receiver<serde_json::Value>,
326}
327
328impl PluginContext {
329    /// Internal constructor used by the Core AI engine.
330    pub fn new(
331        data_dir: std::path::PathBuf,
332        llm_executor: std::sync::Arc<dyn crate::llm::LlmExecutor>,
333        plugin_config: serde_json::Value,
334        storage: std::sync::Arc<crate::storage::StorageRegistry>,
335        plugin_namespace: String,
336        storage_config_resolver: std::sync::Arc<dyn crate::storage::StorageConfigResolver>,
337        current_context_rx: tokio::sync::watch::Receiver<serde_json::Value>,
338    ) -> Self {
339        Self {
340            data_dir,
341            llm_executor,
342            plugin_config,
343            storage,
344            plugin_namespace,
345            storage_config_resolver,
346            current_context_rx,
347        }
348    }
349
350    pub fn llm_executor(&self) -> std::sync::Arc<dyn crate::llm::LlmExecutor> {
351        self.llm_executor.clone()
352    }
353
354    /// Deserializes the raw JSON configuration into the plugin's requested config struct.
355    ///
356    /// **Note on Serde Configuration Defaults:**
357    /// This performs strict structural deserialization. If the JSON object provided by the
358    /// `ConfigProvider` is missing a field that your Rust struct (which must derive `Deserialize`) expects, `serde` will
359    /// return a `missing field` error — even if your struct implements `Default`.
360    ///
361    /// To make a configuration field optional, use the `#[serde(default)]`
362    /// attribute on the struct field. This instructs `serde` to fall back to `Default::default()`
363    /// when the key is omitted.
364    pub fn config<C: serde::de::DeserializeOwned>(&self) -> Result<C, String> {
365        serde_json::from_value(self.plugin_config.clone())
366            .map_err(|e| format!("Failed to parse plugin config: {}", e))
367    }
368
369    /// Initializes and returns a database connection scoped strictly to this plugin's namespace.
370    pub async fn store<S: crate::storage::StorageConnection>(&self) -> Result<S, String> {
371        let full_path = std::any::type_name::<S>();
372        let crate_name = full_path
373            .split("::")
374            .next()
375            .unwrap_or("")
376            .to_string()
377            .replace('-', "_");
378        let base_path = full_path.split('<').next().unwrap_or(full_path);
379        let storage_type_name = base_path.split("::").last().unwrap_or("").to_string();
380
381        let config_val = self
382            .storage_config_resolver
383            .resolve_config(&crate_name, &storage_type_name)
384            .unwrap_or_else(|| serde_json::json!({}));
385
386        let config: S::Config = serde_json::from_value(config_val).map_err(|e| {
387            format!(
388                "Failed to parse config for storage '{}::{}': {}",
389                crate_name, storage_type_name, e
390            )
391        })?;
392
393        S::connect(
394            config,
395            self.storage.clone(),
396            &self.data_dir,
397            &self.plugin_namespace,
398        )
399        .await
400    }
401
402    /// Read-only subscription channel to receive the global state updates
403    pub fn subscribe_context_updates(&self) -> tokio::sync::watch::Receiver<serde_json::Value> {
404        self.current_context_rx.clone()
405    }
406}
407
408#[derive(Default)]
409pub struct ContextRegistries {
410    pub historical: ContextRegistryBuilder,
411    pub current: ContextRegistryBuilder,
412    pub prospective: ContextRegistryBuilder,
413}
414
415impl ContextRegistries {
416    pub fn subscribe(&self, scope: TemporalScope) -> tokio::sync::watch::Receiver<()> {
417        match scope {
418            TemporalScope::Historical => self.historical.subscribe(),
419            TemporalScope::Current => self.current.subscribe(),
420            TemporalScope::Prospective => self.prospective.subscribe(),
421        }
422    }
423}
424
425#[async_trait::async_trait]
426pub trait Command: Send + Sync + 'static {
427    type Arguments: schemars::JsonSchema
428        + serde::de::DeserializeOwned
429        + LLMSafe
430        + Send
431        + Sync
432        + 'static;
433
434    const NAME: &'static str;
435
436    async fn execute(&self, args: Self::Arguments) -> Result<(), String>;
437}
438
439#[async_trait::async_trait]
440pub trait ErasedCommand: Send + Sync + 'static {
441    fn name(&self) -> &'static str;
442    fn schema(&self) -> schemars::Schema;
443    async fn erased_execute(&self, args: serde_json::Value) -> Result<(), String>;
444}
445
446#[async_trait::async_trait]
447impl<T> ErasedCommand for T
448where
449    T: Command,
450{
451    fn name(&self) -> &'static str {
452        <T as Command>::NAME
453    }
454    fn schema(&self) -> schemars::Schema {
455        schemars::schema_for!(<T as Command>::Arguments)
456    }
457    async fn erased_execute(&self, args: serde_json::Value) -> Result<(), String> {
458        let parsed_args = serde_json::from_value(args).map_err(|e| e.to_string())?;
459        <T as Command>::execute(self, parsed_args).await
460    }
461}
462
463#[derive(Default)]
464pub struct CommandRegistryBuilder {
465    pub commands:
466        std::sync::RwLock<std::collections::HashMap<String, std::sync::Arc<dyn ErasedCommand>>>,
467}
468
469impl CommandRegistryBuilder {
470    pub fn register<T>(&self, command: T)
471    where
472        T: ErasedCommand + 'static,
473    {
474        let command_arc: std::sync::Arc<dyn ErasedCommand> = std::sync::Arc::new(command);
475        self.register_erased(command_arc);
476    }
477
478    pub fn register_erased(&self, command: std::sync::Arc<dyn ErasedCommand>) {
479        self.commands
480            .write()
481            .unwrap_or_else(|e| panic!("Failed to acquire write lock on commands: {:?}", e))
482            .insert(command.name().to_string(), command);
483    }
484}
485
486#[async_trait::async_trait]
487pub trait Tool: Send + Sync + 'static {
488    type Arguments: schemars::JsonSchema
489        + serde::de::DeserializeOwned
490        + LLMSafe
491        + Send
492        + Sync
493        + 'static;
494
495    const NAME: &'static str;
496    const DESCRIPTION: &'static str;
497
498    /// Evaluated dynamically every turn AFTER the ContextProviders have compiled the World State.
499    /// `compiled_context` is the JSON value generated by all ContextProviders that the LLM is about to see.
500    async fn is_available(
501        &self,
502        _ctx_request: &ContextRequest,
503        _compiled_context: &serde_json::Value,
504    ) -> Result<bool, String> {
505        Ok(true)
506    }
507
508    /// Executes the tool. The result is serialized and fed back to the LLM.
509    async fn execute(
510        &self,
511        ctx_request: &ContextRequest,
512        args: Self::Arguments,
513    ) -> Result<serde_json::Value, String>;
514}
515
516#[async_trait::async_trait]
517pub trait ErasedTool: Send + Sync + 'static {
518    fn name(&self) -> &'static str;
519    fn description(&self) -> &'static str;
520    fn schema(&self) -> schemars::Schema;
521    async fn erased_is_available(
522        &self,
523        ctx_request: &ContextRequest,
524        compiled_context: &serde_json::Value,
525    ) -> Result<bool, String>;
526    async fn erased_execute(
527        &self,
528        ctx_request: &ContextRequest,
529        args: serde_json::Value,
530    ) -> Result<serde_json::Value, String>;
531}
532
533#[async_trait::async_trait]
534impl<T> ErasedTool for T
535where
536    T: Tool,
537{
538    fn name(&self) -> &'static str {
539        <T as Tool>::NAME
540    }
541    fn description(&self) -> &'static str {
542        <T as Tool>::DESCRIPTION
543    }
544    fn schema(&self) -> schemars::Schema {
545        schemars::schema_for!(<T as Tool>::Arguments)
546    }
547    async fn erased_is_available(
548        &self,
549        ctx_request: &ContextRequest,
550        compiled_context: &serde_json::Value,
551    ) -> Result<bool, String> {
552        <T as Tool>::is_available(self, ctx_request, compiled_context).await
553    }
554    async fn erased_execute(
555        &self,
556        ctx_request: &ContextRequest,
557        args: serde_json::Value,
558    ) -> Result<serde_json::Value, String> {
559        let parsed_args = serde_json::from_value(args).map_err(|e| e.to_string())?;
560        <T as Tool>::execute(self, ctx_request, parsed_args).await
561    }
562}
563
564#[derive(Default)]
565pub struct ToolRegistryBuilder {
566    pub tools: std::sync::RwLock<std::collections::HashMap<String, std::sync::Arc<dyn ErasedTool>>>,
567}
568
569impl ToolRegistryBuilder {
570    pub fn register<T>(&self, tool: T)
571    where
572        T: ErasedTool + 'static,
573    {
574        let tool_arc: std::sync::Arc<dyn ErasedTool> = std::sync::Arc::new(tool);
575        self.register_erased(tool_arc);
576    }
577
578    pub fn register_erased(&self, tool: std::sync::Arc<dyn ErasedTool>) {
579        self.tools
580            .write()
581            .unwrap_or_else(|e| panic!("Failed to acquire write lock on tools: {:?}", e))
582            .insert(tool.name().to_string(), tool);
583    }
584
585    pub fn get(&self, name: &str) -> Option<std::sync::Arc<dyn ErasedTool>> {
586        self.tools
587            .read()
588            .unwrap_or_else(|e| panic!("Failed to acquire read lock on tools: {:?}", e))
589            .get(name)
590            .cloned()
591    }
592
593    pub fn get_all(&self) -> Vec<std::sync::Arc<dyn ErasedTool>> {
594        self.tools
595            .read()
596            .unwrap_or_else(|e| panic!("Failed to acquire read lock on tools: {:?}", e))
597            .values()
598            .cloned()
599            .collect()
600    }
601}
602
603#[derive(
604    Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug, Clone, PartialOrd, Ord, Copy,
605)]
606pub struct Timestamp(pub i64);
607crate::register_channel_name!(Timestamp, "timestamp");
608
609#[derive(
610    Serialize,
611    Deserialize,
612    JsonSchema,
613    PartialEq,
614    Eq,
615    Debug,
616    Clone,
617    derive_more::Display,
618    derive_more::From,
619    derive_more::Deref,
620)]
621pub struct SpaceId(pub String);
622
623#[derive(
624    Serialize,
625    Deserialize,
626    JsonSchema,
627    PartialEq,
628    Eq,
629    Debug,
630    Clone,
631    derive_more::Display,
632    derive_more::From,
633    derive_more::Deref,
634)]
635pub struct ThreadId(pub String);
636
637#[derive(
638    Serialize,
639    Deserialize,
640    JsonSchema,
641    PartialEq,
642    Eq,
643    Debug,
644    Clone,
645    derive_more::Display,
646    derive_more::From,
647    derive_more::Deref,
648    Default,
649)]
650pub struct MessageId(pub String);
651
652#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug, Clone)]
653pub struct AiSpoken(pub String);
654
655#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug, Clone)]
656pub struct AiWritten {
657    pub target_channel: MessageChannel,
658    pub text: String,
659}
660
661#[derive(
662    Serialize,
663    Deserialize,
664    JsonSchema,
665    PartialEq,
666    Eq,
667    Debug,
668    Clone,
669    derive_more::Display,
670    derive_more::From,
671    derive_more::Deref,
672)]
673pub struct CognitiveReasoning(pub String);
674
675#[derive(
676    Clone, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema, PartialEq, Eq,
677)]
678pub struct ObservedInteraction {
679    pub timestamp: Timestamp,
680    pub user_messages: Vec<PeerInput>,
681    pub ai_spoken: Option<AiSpoken>,
682    pub ai_written: Option<AiWritten>,
683    pub ai_reasoning: Option<CognitiveReasoning>,
684}
685
686crate::register_channel_name!(ObservedInteraction, "observed_interaction");
687
688#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, schemars::JsonSchema)]
689pub struct NotClearInteraction {
690    pub timestamp: Timestamp,
691    pub user_messages: Vec<PeerInput>,
692    pub ai_spoken: Option<AiSpoken>,
693    pub ai_written: Option<AiWritten>,
694}
695crate::register_channel_name!(NotClearInteraction, "not_clear_interaction");
696
697#[derive(
698    Serialize,
699    Deserialize,
700    PartialEq,
701    Eq,
702    Debug,
703    Clone,
704    Default,
705    schemars::JsonSchema,
706    derive_more::Deref,
707    derive_more::DerefMut,
708    derive_more::IntoIterator,
709)]
710pub struct NotClearInteractionMemory(pub std::collections::VecDeque<NotClearInteraction>);
711
712impl From<Vec<NotClearInteraction>> for NotClearInteractionMemory {
713    fn from(value: Vec<NotClearInteraction>) -> Self {
714        Self(value.into())
715    }
716}
717
718/// Represents a single visual frame captured from a camera peripheral.
719#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
720pub struct CameraInputFrame {
721    /// JPEG encoded binary frame data.
722    pub data: Vec<u8>,
723}
724
725crate::register_channel_name!(CameraInputFrame, "camera_input_frame");