#[cfg(not(feature = "host"))]
use postcard::experimental::max_size::MaxSize;
use serde::Serialize;
use serde::de::DeserializeOwned;
use super::message::{RynkHeader, encode_frame};
use super::{
BehaviorConfig, DeviceCapabilities, DeviceInfo, GetComboBulkRequest, GetComboBulkResponse, GetEncoderRequest,
GetKeymapBulkRequest, GetKeymapBulkResponse, GetMacroRequest, GetMorseBulkRequest, GetMorseBulkResponse,
KeyPosition, LayoutChunk, LockStatus, MacroData, MatrixState, ProtocolVersion, RynkError, SetComboBulkRequest,
SetComboRequest, SetEncoderRequest, SetForkRequest, SetKeyRequest, SetKeymapBulkRequest, SetMacroRequest,
SetMorseBulkRequest, SetMorseRequest, StorageResetMode,
};
use crate::action::{EncoderAction, KeyAction};
#[cfg(feature = "_ble")]
use crate::battery::BatteryStatus;
#[cfg(feature = "_ble")]
use crate::ble::BleStatus;
use crate::combo::Combo;
use crate::connection::{ConnectionStatus, ConnectionType};
use crate::fork::Fork;
use crate::led_indicator::LedIndicator;
use crate::morse::Morse;
#[cfg(feature = "split")]
use crate::protocol::rynk::PeripheralStatus;
const RYNK_TOPIC_BIT: u16 = 0x8000;
pub trait Endpoint {
const CMD: Cmd;
type Request: Serialize + DeserializeOwned;
type Response: Serialize + DeserializeOwned;
}
#[repr(transparent)]
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct Cmd(u16);
impl Cmd {
pub const fn from_raw(raw: u16) -> Self {
Self(raw)
}
pub const fn from_le_bytes(bytes: [u8; 2]) -> Self {
Self(u16::from_le_bytes(bytes))
}
pub const fn raw(self) -> u16 {
self.0
}
pub const fn to_le_bytes(self) -> [u8; 2] {
self.0.to_le_bytes()
}
pub const fn is_topic(self) -> bool {
self.0 & RYNK_TOPIC_BIT != 0
}
}
impl core::fmt::Debug for Cmd {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Cmd(0x{:04x})", self.0)
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for Cmd {
fn format(&self, fmt: defmt::Formatter) {
defmt::write!(fmt, "Cmd(0x{=u16:04x})", self.0)
}
}
#[cfg(test)]
#[derive(Debug, Clone, Copy)]
pub struct EndpointMeta {
pub name: &'static str,
pub cmd: u16,
pub request: &'static str,
pub response: &'static str,
pub attrs: &'static str,
}
#[cfg(test)]
#[derive(Debug, Clone, Copy)]
pub struct TopicMeta {
pub name: &'static str,
pub cmd: u16,
pub payload: &'static str,
pub attrs: &'static str,
}
const fn assert_unique(cmds: &[u16]) {
let mut i = 0;
while i < cmds.len() {
let mut j = i + 1;
while j < cmds.len() {
core::assert!(cmds[i] != cmds[j], "duplicate CMD value in the command table");
j += 1;
}
i += 1;
}
}
macro_rules! endpoints {
($( $(#[$meta:meta])* $name:ident = $cmd:literal : $req:ty => $resp:ty; )*) => {
#[allow(non_upper_case_globals)]
impl Cmd {
$( $(#[$meta])* pub const $name: Self = Cmd::from_raw($cmd); )*
}
$(
$(#[$meta])*
pub enum $name {}
$(#[$meta])*
impl Endpoint for $name {
const CMD: Cmd = Cmd::$name;
type Request = $req;
type Response = $resp;
}
)*
const _: () = {
$( core::assert!(!Cmd::from_raw($cmd).is_topic(), "request CMD value in the topic range"); )*
assert_unique(&[$($cmd),*]);
};
#[cfg(not(feature = "host"))]
#[allow(unused_doc_comments)] const MAX_ENDPOINT_PAYLOAD: usize = {
let mut m = 0;
$( $(#[$meta])* {
let req = <$req as MaxSize>::POSTCARD_MAX_SIZE;
if req > m { m = req; }
let resp = <Result<$resp, RynkError> as MaxSize>::POSTCARD_MAX_SIZE;
if resp > m { m = resp; }
} )*
m
};
#[cfg(test)]
pub const ENDPOINT_META: &[EndpointMeta] = &[
$( EndpointMeta {
name: stringify!($name),
cmd: $cmd,
request: stringify!($req),
response: stringify!($resp),
attrs: stringify!($(#[$meta])*),
}, )*
];
};
}
macro_rules! topics {
($( $(#[$meta:meta])* $name:ident = $cmd:literal : $payload:ty; )*) => {
#[allow(non_upper_case_globals)]
impl Cmd {
$( $(#[$meta])* pub const $name: Self = Cmd::from_raw($cmd); )*
}
const _: () = {
$( core::assert!(Cmd::from_raw($cmd).is_topic(), "topic CMD value outside the topic range"); )*
assert_unique(&[$($cmd),*]);
};
#[cfg(not(feature = "host"))]
#[allow(unused_doc_comments)]
const MAX_TOPIC_PAYLOAD: usize = {
let mut m = 0;
$( $(#[$meta])* {
let p = <$payload as MaxSize>::POSTCARD_MAX_SIZE;
if p > m { m = p; }
} )*
m
};
#[cfg(test)]
pub const TOPIC_META: &[TopicMeta] = &[
$( TopicMeta {
name: stringify!($name),
cmd: $cmd,
payload: stringify!($payload),
attrs: stringify!($(#[$meta])*),
}, )*
];
#[derive(Debug, Clone, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
pub enum TopicEvent {
$( $(#[$meta])* $name($payload), )*
}
impl TopicEvent {
pub fn decode(cmd: Cmd, payload: &[u8]) -> Option<Self> {
match cmd {
$( $(#[$meta])* Cmd::$name => postcard::take_from_bytes::<$payload>(payload)
.ok()
.map(|(v, _)| TopicEvent::$name(v)), )*
_ => None,
}
}
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, RynkError> {
match self {
$( $(#[$meta])* TopicEvent::$name(v) =>
encode_frame(buf, RynkHeader { cmd: Cmd::$name, seq: 0 }, v), )*
}
}
}
};
}
endpoints! {
GetVersion = 0x0001: () => ProtocolVersion;
GetCapabilities = 0x0002: () => DeviceCapabilities;
Reboot = 0x0003: () => ();
BootloaderJump = 0x0004: () => ();
StorageReset = 0x0005: StorageResetMode => ();
GetLockStatus = 0x0006: () => LockStatus;
UnlockPoll = 0x0007: () => LockStatus;
Lock = 0x0008: () => ();
GetLayout = 0x0009: u32 => LayoutChunk;
GetDeviceInfo = 0x000A: () => DeviceInfo;
GetKeyAction = 0x0101: KeyPosition => KeyAction;
SetKeyAction = 0x0102: SetKeyRequest => ();
GetDefaultLayer = 0x0103: () => u8;
SetDefaultLayer = 0x0104: u8 => ();
GetEncoderAction = 0x0105: GetEncoderRequest => EncoderAction;
SetEncoderAction = 0x0106: SetEncoderRequest => ();
GetKeymapBulk = 0x0107: GetKeymapBulkRequest => GetKeymapBulkResponse;
SetKeymapBulk = 0x0108: SetKeymapBulkRequest => ();
GetMacro = 0x0201: GetMacroRequest => MacroData;
SetMacro = 0x0202: SetMacroRequest => ();
GetCombo = 0x0301: u8 => Combo;
SetCombo = 0x0302: SetComboRequest => ();
GetComboBulk = 0x0303: GetComboBulkRequest => GetComboBulkResponse;
SetComboBulk = 0x0304: SetComboBulkRequest => ();
GetMorse = 0x0401: u8 => Morse;
SetMorse = 0x0402: SetMorseRequest => ();
GetMorseBulk = 0x0403: GetMorseBulkRequest => GetMorseBulkResponse;
SetMorseBulk = 0x0404: SetMorseBulkRequest => ();
GetFork = 0x0501: u8 => Fork;
SetFork = 0x0502: SetForkRequest => ();
GetBehaviorConfig = 0x0601: () => BehaviorConfig;
SetBehaviorConfig = 0x0602: BehaviorConfig => ();
GetConnectionType = 0x0701: () => ConnectionType;
GetConnectionStatus = 0x0702: () => ConnectionStatus;
#[cfg(feature = "_ble")]
GetBleStatus = 0x0703: () => BleStatus;
#[cfg(feature = "_ble")]
SwitchBleProfile = 0x0704: u8 => ();
#[cfg(feature = "_ble")]
ClearBleProfile = 0x0705: u8 => ();
GetCurrentLayer = 0x0801: () => u8;
GetMatrixState = 0x0802: () => MatrixState;
#[cfg(feature = "_ble")]
GetBatteryStatus = 0x0803: () => BatteryStatus;
#[cfg(feature = "split")]
GetPeripheralStatus = 0x0804: u8 => PeripheralStatus;
GetWpm = 0x0805: () => u16;
GetSleepState = 0x0806: () => bool;
GetLedIndicator = 0x0807: () => LedIndicator;
}
topics! {
LayerChange = 0x8001: u8;
WpmUpdate = 0x8002: u16;
ConnectionChange = 0x8003: ConnectionStatus;
SleepState = 0x8004: bool;
LedIndicatorChange = 0x8005: LedIndicator;
#[cfg(feature = "_ble")]
BatteryStatusChange = 0x8006: BatteryStatus;
}
#[cfg(not(feature = "host"))]
const _: () = core::assert!(
super::message::RYNK_MAX_PAYLOAD_SIZE >= MAX_ENDPOINT_PAYLOAD
&& super::message::RYNK_MAX_PAYLOAD_SIZE >= MAX_TOPIC_PAYLOAD,
"rynk_buffer_size is too small to hold the largest rynk frame (including bulk and COBS overhead); increase it"
);
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::format;
use postcard::experimental::max_size::MaxSize;
use super::*;
use crate::protocol::rynk::{Deframer, RYNK_HEADER_SIZE, RynkError, RynkHeader};
#[test]
fn topic_mask_is_the_high_bit() {
assert!(Cmd::from_raw(0x8000).is_topic());
assert!(Cmd::from_raw(0x80ff).is_topic());
assert!(!Cmd::from_raw(0x0001).is_topic());
assert!(!Cmd::from_raw(0x7fff).is_topic());
}
#[test]
fn raw_values_round_trip() {
for cmd in [Cmd::from_raw(0x0001), Cmd::from_raw(0x8001), Cmd::from_raw(0xffff)] {
assert_eq!(Cmd::from_raw(cmd.raw()), cmd);
assert_eq!(Cmd::from_le_bytes(cmd.to_le_bytes()), cmd);
}
}
#[test]
fn debug_is_compact_raw_value() {
assert_eq!(format!("{:?}", Cmd::from_raw(0x0001)), "Cmd(0x0001)");
assert_eq!(format!("{:?}", Cmd::from_raw(0x80ff)), "Cmd(0x80ff)");
}
#[test]
fn table_cmds_land_in_their_ranges() {
assert!(Cmd::LayerChange.is_topic());
assert!(Cmd::WpmUpdate.is_topic());
assert!(!Cmd::GetVersion.is_topic());
assert!(!Cmd::SetKeyAction.is_topic());
}
#[test]
fn topic_event_round_trips_through_the_wire() {
let mut buf = [0u8; 64];
let ev = TopicEvent::LayerChange(7);
let framed_len = ev.encode(&mut buf).unwrap();
let mut df = Deframer::new();
df.commit(framed_len);
let n = df.next(&mut buf).expect("one whole topic frame");
let header = RynkHeader::parse(buf[..RYNK_HEADER_SIZE].try_into().unwrap());
assert_eq!(header.cmd, Cmd::LayerChange);
assert_eq!(header.seq, 0, "topics push with SEQ 0");
let decoded = TopicEvent::decode(header.cmd, &buf[RYNK_HEADER_SIZE..n]);
assert!(matches!(decoded, Some(TopicEvent::LayerChange(7))));
}
#[test]
fn topic_event_decode_rejects_non_topic_and_garbage() {
assert!(TopicEvent::decode(Cmd::GetVersion, &[]).is_none());
assert!(TopicEvent::decode(Cmd::LayerChange, &[]).is_none());
}
#[test]
fn response_wrapping_adds_one_byte() {
let bare = <DeviceCapabilities as MaxSize>::POSTCARD_MAX_SIZE;
let wrapped = <Result<DeviceCapabilities, RynkError> as MaxSize>::POSTCARD_MAX_SIZE;
assert_eq!(wrapped, bare + 1);
}
}