use std::fmt;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use crate::fault::{FaultId, FaultOptions};
use crate::ids::{DeviceId, JobId};
use crate::nfc::{NdefRecord, NfcEvent};
use crate::time::Millis;
pub trait DevicePlugin: Send + Sync + 'static {
fn kind(&self) -> &'static str;
fn config_schema(&self) -> schemars::Schema;
fn faults(&self) -> Vec<FaultId>;
fn create(
&self,
id: DeviceId,
config: serde_json::Value,
) -> Result<Box<dyn Device>, ConfigError>;
}
pub trait Device: Send {
fn kind(&self) -> &'static str;
fn state(&self) -> serde_json::Value;
fn on_host_bytes(&mut self, data: &[u8], now: Millis) -> Vec<DeviceEvent>;
fn on_control(&mut self, cmd: ControlCommand, now: Millis) -> Vec<DeviceEvent>;
fn on_tick(&mut self, now: Millis) -> Vec<DeviceEvent>;
fn next_deadline(&self) -> Option<Millis>;
fn faults(&self) -> Vec<FaultId>;
fn set_fault(
&mut self,
fault: FaultId,
active: bool,
opts: FaultOptions,
now: Millis,
) -> Vec<DeviceEvent>;
fn reset(&mut self, now: Millis) -> Vec<DeviceEvent>;
fn attached_to(&self) -> Option<DeviceId> {
None
}
fn on_peripheral(
&mut self,
from: &DeviceId,
signal: PeripheralSignal,
now: Millis,
) -> Vec<DeviceEvent> {
let _ = (from, signal, now);
Vec::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum PeripheralSignal {
#[serde(rename_all = "camelCase")]
Pulse {
pin: u8,
on_ms: u64,
off_ms: u64,
},
Sensor {
open: bool,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum DeviceEvent {
ToHost(Bytes),
StateChanged,
JobCompleted(Job),
Connected,
Disconnected,
Log(LogEntry),
Peripheral(PeripheralSignal),
Wedge(WedgeScan),
Nfc(NfcEvent),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct WedgeScan {
pub code: String,
pub delay_ms: u64,
pub terminator: Terminator,
}
impl WedgeScan {
pub fn keys(&self) -> u64 {
let terminator = u64::from(self.terminator != Terminator::None);
self.code.chars().count() as u64 + terminator
}
pub fn typing_ms(&self) -> u64 {
self.keys().saturating_sub(1).saturating_mul(self.delay_ms)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct Job {
pub id: JobId,
pub at: Millis,
pub ast: serde_json::Value,
pub text: String,
#[serde(rename = "bytesHex", with = "crate::hex::serde_bytes")]
#[cfg_attr(feature = "ts", ts(rename = "bytesHex", type = "string"))]
pub bytes: Bytes,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct LogEntry {
pub level: LogLevel,
pub message: String,
}
impl LogEntry {
pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
Self {
level,
message: message.into(),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum Terminator {
#[default]
Enter,
Tab,
None,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ControlCommand {
#[serde(rename_all = "camelCase")]
Scan {
code: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
terminator: Option<Terminator>,
#[serde(default, skip_serializing_if = "Option::is_none")]
delay_ms: Option<u64>,
},
Tap {
uid: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts", ts(optional))]
records: Option<Vec<NdefRecord>>,
},
NfcWrite {
records: Vec<NdefRecord>,
#[serde(default = "yes")]
overwrite: bool,
},
NfcMakeReadOnly,
CloseDrawer,
Custom {
name: String,
#[serde(default)]
payload: serde_json::Value,
},
}
fn yes() -> bool {
true
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConfigError {
message: String,
pointer: Option<String>,
}
impl ConfigError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
pointer: None,
}
}
pub fn at(mut self, pointer: impl Into<String>) -> Self {
self.pointer = Some(pointer.into());
self
}
pub fn message(&self) -> &str {
&self.message
}
pub fn pointer(&self) -> Option<&str> {
self.pointer.as_deref()
}
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.pointer {
Some(p) => write!(f, "{p}: {}", self.message),
None => f.write_str(&self.message),
}
}
}
impl std::error::Error for ConfigError {}