use crate::{Result, SharedCallback, Transport, TransportMetrics, TransportStatus, TransportType};
use offline_protocol_core::{Message, MutexExt};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct WifiDirectPeer {
pub device_name: String,
pub device_address: String,
pub is_group_owner: bool,
pub last_seen: SystemTime,
pub connected: bool,
}
#[derive(Debug, Clone)]
pub struct WifiDirectConfig {
pub device_name: String,
pub auto_accept: bool,
pub group_owner_intent: u8,
}
impl Default for WifiDirectConfig {
fn default() -> Self {
Self {
device_name: "OfflineProtocolDevice".to_string(),
auto_accept: false,
group_owner_intent: 7,
}
}
}
pub struct WifiDirectTransport {
device_id: String,
config: WifiDirectConfig,
layer_status: Arc<Mutex<TransportStatus>>,
peers: Arc<Mutex<HashMap<String, WifiDirectPeer>>>,
connected_links: Arc<Mutex<HashMap<String, SystemTime>>>,
receive_queue: Arc<Mutex<VecDeque<Message>>>,
send_queue: Arc<Mutex<VecDeque<(String, Message)>>>,
metrics: Arc<Mutex<TransportMetrics>>,
platform_handle: Arc<Mutex<Option<usize>>>,
on_messages_available: SharedCallback,
}
impl WifiDirectTransport {
pub fn new(device_id: impl Into<String>) -> Self {
Self::with_config(device_id, WifiDirectConfig::default())
}
pub fn with_config(device_id: impl Into<String>, config: WifiDirectConfig) -> Self {
Self {
device_id: device_id.into(),
config,
layer_status: Arc::new(Mutex::new(TransportStatus::Unavailable)),
peers: Arc::new(Mutex::new(HashMap::new())),
connected_links: Arc::new(Mutex::new(HashMap::new())),
receive_queue: Arc::new(Mutex::new(VecDeque::new())),
send_queue: Arc::new(Mutex::new(VecDeque::new())),
metrics: Arc::new(Mutex::new(TransportMetrics::default())),
platform_handle: Arc::new(Mutex::new(None)),
on_messages_available: Arc::new(Mutex::new(None)),
}
}
fn notify_messages_available(&self) {
let callback = self.on_messages_available.lock_or_recover().clone();
if let Some(cb) = callback {
cb();
}
}
pub fn device_id(&self) -> &str {
&self.device_id
}
pub fn config(&self) -> &WifiDirectConfig {
&self.config
}
pub fn set_platform_handle(&self, handle: usize) {
crate::common::set_platform_handle(&self.platform_handle, handle);
}
pub fn platform_handle(&self) -> Option<usize> {
crate::common::platform_handle(&self.platform_handle)
}
pub fn on_peer_discovered(&self, peer: WifiDirectPeer) {
let mut peers = self.peers.lock_or_recover();
peers.insert(peer.device_address.clone(), peer);
}
pub fn on_peer_lost(&self, device_address: &str) {
let mut peers = self.peers.lock_or_recover();
peers.remove(device_address);
}
pub fn on_peer_connected(&self, peer_id: impl Into<String>) {
let peer_id = peer_id.into();
if peer_id.is_empty() {
return;
}
self.connected_links
.lock_or_recover()
.insert(peer_id, SystemTime::now());
}
pub fn on_peer_disconnected(&self, peer_id: &str) -> bool {
self.connected_links
.lock_or_recover()
.remove(peer_id)
.is_some()
}
pub fn on_message_received(&self, message: Message) {
crate::common::on_message_received(&self.receive_queue, message);
}
pub fn on_message_received_from(&self, message: Message, peer_id: String) {
crate::common::on_message_received_from(&self.receive_queue, message, peer_id);
}
pub fn get_peers(&self) -> Vec<WifiDirectPeer> {
let peers = self.peers.lock_or_recover();
peers.values().cloned().collect()
}
pub fn get_peer(&self, device_address: &str) -> Option<WifiDirectPeer> {
let peers = self.peers.lock_or_recover();
peers.get(device_address).cloned()
}
pub fn update_metrics(&self, metrics: TransportMetrics) {
*self.metrics.lock_or_recover() = metrics;
}
pub fn serialize_message(&self, message: &Message) -> Result<Vec<u8>> {
crate::common::serialize_message_with(message)
}
pub fn layer_status(&self) -> TransportStatus {
*self.layer_status.lock_or_recover()
}
pub fn has_pending_sends(&self) -> bool {
let queue = self.send_queue.lock_or_recover();
!queue.is_empty()
}
}
impl Transport for WifiDirectTransport {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn transport_type(&self) -> TransportType {
TransportType::WiFiDirect
}
fn status(&self) -> TransportStatus {
let layer = self.layer_status();
if layer == TransportStatus::Available && self.connected_links.lock_or_recover().is_empty()
{
return TransportStatus::Connecting;
}
layer
}
fn metrics(&self) -> TransportMetrics {
self.metrics.lock_or_recover().clone()
}
fn send(&self, message: &Message) -> Result<()> {
if self.layer_status() != TransportStatus::Available {
return Err(crate::Error::TransportNotAvailable(
"Wi-Fi Direct transport is not available".to_string(),
));
}
let recipient = message.recipient.as_str().to_string();
if !self
.connected_links
.lock_or_recover()
.contains_key(&recipient)
{
return Err(crate::Error::PeerNotReachable(format!(
"peer stream: no stream has proved {}",
recipient
)));
}
{
let mut queue = self.send_queue.lock_or_recover();
queue.push_back((recipient, message.clone()));
let mut metrics = self.metrics.lock_or_recover();
metrics.queue_depth = queue.len();
metrics.congestion = ((metrics.queue_depth as f32) / 20.0).clamp(0.0, 1.0);
}
self.notify_messages_available();
Ok(())
}
fn send_to_peer(&self, peer_id: &str, message: &Message) -> Result<()> {
if self.layer_status() != TransportStatus::Available {
return Err(crate::Error::TransportNotAvailable(
"Wi-Fi Direct transport is not available".to_string(),
));
}
if !self.connected_links.lock_or_recover().contains_key(peer_id) {
return Err(crate::Error::PeerNotReachable(format!(
"Wi-Fi Direct: {} is not a connected peer",
peer_id
)));
}
{
let mut queue = self.send_queue.lock_or_recover();
queue.push_back((peer_id.to_string(), message.clone()));
let mut metrics = self.metrics.lock_or_recover();
metrics.queue_depth = queue.len();
metrics.congestion = ((metrics.queue_depth as f32) / 20.0).clamp(0.0, 1.0);
}
self.notify_messages_available();
Ok(())
}
fn connected_peers(&self) -> Vec<crate::PeerLink> {
if self.layer_status() != TransportStatus::Available {
return Vec::new();
}
self.connected_links
.lock_or_recover()
.keys()
.map(crate::PeerLink::new)
.collect()
}
fn receive(&self) -> Result<Option<Message>> {
let mut queue = self.receive_queue.lock_or_recover();
Ok(queue.pop_front())
}
fn start(&self) -> Result<()> {
Ok(())
}
fn stop(&self) -> Result<()> {
*self.layer_status.lock_or_recover() = TransportStatus::Disconnected;
self.peers.lock_or_recover().clear();
self.connected_links.lock_or_recover().clear();
self.send_queue.lock_or_recover().clear();
self.receive_queue.lock_or_recover().clear();
let mut metrics = self.metrics.lock_or_recover();
metrics.queue_depth = 0;
metrics.congestion = 0.0;
Ok(())
}
fn on_status_changed(&self, status: TransportStatus) {
let previous = {
let mut guard = self.layer_status.lock_or_recover();
let prev = *guard;
*guard = status;
prev
};
if previous == TransportStatus::Available && status != TransportStatus::Available {
self.peers.lock_or_recover().clear();
self.connected_links.lock_or_recover().clear();
self.send_queue.lock_or_recover().clear();
self.receive_queue.lock_or_recover().clear();
let mut metrics = self.metrics.lock_or_recover();
metrics.queue_depth = 0;
metrics.congestion = 0.0;
}
}
fn on_data_received(&self, data: Vec<u8>) -> Result<()> {
crate::common::on_data_received(&self.receive_queue, data)
}
fn on_data_received_from(&self, data: Vec<u8>, peer_id: String) -> Result<()> {
crate::common::on_data_received_from(&self.receive_queue, data, peer_id)
}
fn get_next_message(&self) -> Result<Option<(String, Vec<u8>)>> {
let (recipient, message) = {
let mut queue = self.send_queue.lock_or_recover();
match queue.pop_front() {
Some((r, m)) => (r, m),
None => return Ok(None),
}
};
let data = self.serialize_message(&message)?;
Ok(Some((recipient, data)))
}
fn set_on_messages_available(&self, callback: Arc<dyn Fn() + Send + Sync>) {
*self.on_messages_available.lock_or_recover() = Some(callback);
}
}
pub struct WifiDirectTransportBuilder {
device_id: String,
config: WifiDirectConfig,
}
impl WifiDirectTransportBuilder {
pub fn new(device_id: impl Into<String>) -> Self {
Self {
device_id: device_id.into(),
config: WifiDirectConfig::default(),
}
}
pub fn device_name(mut self, name: impl Into<String>) -> Self {
self.config.device_name = name.into();
self
}
pub fn auto_accept(mut self, enabled: bool) -> Self {
self.config.auto_accept = enabled;
self
}
pub fn group_owner_intent(mut self, intent: u8) -> Self {
self.config.group_owner_intent = intent.min(15);
self
}
pub fn build(self) -> WifiDirectTransport {
WifiDirectTransport::with_config(self.device_id, self.config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::DEFAULT_MAX_MESSAGE_SIZE;
use offline_protocol_core::{AppId, UserId};
fn create_test_message() -> Message {
Message::new(
UserId::new("alice").unwrap(),
UserId::new("bob").unwrap(),
AppId::new("test").unwrap(),
"Test message",
)
}
#[test]
fn test_wifi_direct_transport_creation() {
let transport = WifiDirectTransport::new("test-device");
assert_eq!(transport.device_id(), "test-device");
assert_eq!(transport.transport_type(), TransportType::WiFiDirect);
assert_eq!(transport.status(), TransportStatus::Unavailable);
}
#[test]
fn test_builder() {
let transport = WifiDirectTransportBuilder::new("test-device")
.device_name("MyDevice")
.auto_accept(true)
.group_owner_intent(10)
.build();
assert_eq!(transport.config().device_name, "MyDevice");
assert!(transport.config().auto_accept);
assert_eq!(transport.config().group_owner_intent, 10);
}
#[test]
fn test_peer_discovery() {
let transport = WifiDirectTransport::new("test-device");
let peer = WifiDirectPeer {
device_name: "Peer1".to_string(),
device_address: "00:11:22:33:44:55".to_string(),
is_group_owner: false,
last_seen: SystemTime::now(),
connected: true,
};
transport.on_peer_discovered(peer.clone());
let peers = transport.get_peers();
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].device_name, "Peer1");
}
#[test]
fn test_send_receive() {
let transport = WifiDirectTransport::new("test-device");
transport.on_status_changed(TransportStatus::Available);
transport.on_peer_connected("bob");
let message = create_test_message();
assert!(transport.send(&message).is_ok());
assert!(transport.has_pending_sends());
let next = transport.get_next_message().unwrap();
assert!(next.is_some());
}
#[test]
fn test_serialization() {
let transport = WifiDirectTransport::new("test-device");
let message = create_test_message();
let data = transport.serialize_message(&message).unwrap();
assert!(!data.is_empty());
let deserialized = transport.deserialize_message(&data).unwrap();
assert_eq!(deserialized.id, message.id);
}
#[test]
fn test_send_when_unavailable_fails() {
let transport = WifiDirectTransport::new("test-device");
let message = create_test_message();
let result = transport.send(&message);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::Error::TransportNotAvailable(_)
));
}
#[test]
fn send_to_a_recipient_no_stream_proved_is_peer_not_reachable() {
let transport = WifiDirectTransport::new("test-device");
transport.on_status_changed(TransportStatus::Available);
let result = transport.send(&create_test_message());
assert!(
matches!(result, Err(crate::Error::PeerNotReachable(_))),
"got {result:?}"
);
assert!(!transport.has_pending_sends(), "nothing may be queued");
transport.on_peer_connected("carol");
assert!(matches!(
transport.send(&create_test_message()),
Err(crate::Error::PeerNotReachable(_))
));
transport.on_peer_connected("bob");
assert!(transport.send(&create_test_message()).is_ok());
assert!(transport.has_pending_sends());
transport.on_peer_disconnected("bob");
assert!(matches!(
transport.send(&create_test_message()),
Err(crate::Error::PeerNotReachable(_))
));
}
#[test]
fn a_status_drop_clears_the_proved_links() {
let transport = WifiDirectTransport::new("test-device");
transport.on_status_changed(TransportStatus::Available);
transport.on_peer_connected("bob");
assert_eq!(transport.connected_peers().len(), 1);
transport.on_status_changed(TransportStatus::Disconnected);
transport.on_status_changed(TransportStatus::Available);
assert!(transport.connected_peers().is_empty());
assert!(matches!(
transport.send(&create_test_message()),
Err(crate::Error::PeerNotReachable(_))
));
}
#[test]
fn the_slot_is_available_only_while_a_stream_has_proved_a_peer() {
let transport = WifiDirectTransport::new("test-device");
assert_eq!(transport.status(), TransportStatus::Unavailable);
transport.on_status_changed(TransportStatus::Available);
assert_eq!(transport.layer_status(), TransportStatus::Available);
assert_eq!(
transport.status(),
TransportStatus::Connecting,
"a layer up with no proved stream is not a carrier"
);
transport.on_peer_connected("bob");
assert_eq!(transport.status(), TransportStatus::Available);
transport.on_peer_disconnected("bob");
assert_eq!(transport.status(), TransportStatus::Connecting);
transport.on_status_changed(TransportStatus::Disconnected);
transport.on_peer_connected("bob");
assert_eq!(transport.status(), TransportStatus::Disconnected);
}
#[test]
fn test_receive_when_empty_returns_none() {
let transport = WifiDirectTransport::new("test-device");
assert!(transport.receive().unwrap().is_none());
}
#[test]
fn test_on_data_received_invalid_json_drops_ok() {
let transport = WifiDirectTransport::new("test-device");
let result = transport.on_data_received(b"not json".to_vec());
assert!(result.is_ok());
assert!(transport.receive().unwrap().is_none());
}
#[test]
fn test_on_data_received_valid_queues_message() {
let transport = WifiDirectTransport::new("test-device");
let message = create_test_message();
let data = transport.serialize_message(&message).unwrap();
transport.on_data_received(data).unwrap();
let received = transport.receive().unwrap();
assert!(received.is_some());
assert_eq!(received.unwrap().id, message.id);
}
#[test]
fn test_on_peer_lost() {
let transport = WifiDirectTransport::new("test-device");
let peer = WifiDirectPeer {
device_name: "P".to_string(),
device_address: "00:11:22:33:44:55".to_string(),
is_group_owner: false,
last_seen: SystemTime::now(),
connected: true,
};
transport.on_peer_discovered(peer);
assert_eq!(transport.get_peers().len(), 1);
transport.on_peer_lost("00:11:22:33:44:55");
assert_eq!(transport.get_peers().len(), 0);
}
#[test]
fn test_get_peer() {
let transport = WifiDirectTransport::new("test-device");
let peer = WifiDirectPeer {
device_name: "P".to_string(),
device_address: "aa:bb:cc:dd:ee:ff".to_string(),
is_group_owner: true,
last_seen: SystemTime::now(),
connected: false,
};
transport.on_peer_discovered(peer.clone());
let found = transport.get_peer("aa:bb:cc:dd:ee:ff");
assert!(found.is_some());
assert_eq!(found.unwrap().device_name, "P");
assert!(transport.get_peer("other").is_none());
}
#[test]
fn test_update_metrics() {
let transport = WifiDirectTransport::new("test-device");
let mut m = TransportMetrics::default();
m.rssi = Some(-72);
transport.update_metrics(m);
assert_eq!(transport.metrics().rssi, Some(-72));
}
#[test]
fn test_platform_handle() {
let transport = WifiDirectTransport::new("test-device");
assert_eq!(transport.platform_handle(), None);
transport.set_platform_handle(99);
assert_eq!(transport.platform_handle(), Some(99));
}
#[test]
fn test_has_pending_sends_empty() {
let transport = WifiDirectTransport::new("test-device");
assert!(!transport.has_pending_sends());
}
#[test]
fn test_stop_sets_disconnected() {
let transport = WifiDirectTransport::new("test-device");
transport.on_status_changed(TransportStatus::Available);
transport.stop().unwrap();
assert_eq!(transport.status(), TransportStatus::Disconnected);
}
#[test]
fn test_stop_clears_session_state() {
let transport = WifiDirectTransport::new("test-device");
transport.on_status_changed(TransportStatus::Available);
transport.on_peer_connected("bob");
let peer = WifiDirectPeer {
device_name: "P".to_string(),
device_address: "00:11:22:33:44:55".to_string(),
is_group_owner: false,
last_seen: SystemTime::now(),
connected: true,
};
transport.on_peer_discovered(peer);
transport.send(&create_test_message()).unwrap();
transport.stop().unwrap();
assert!(transport.peers.lock().unwrap().is_empty());
assert!(transport.send_queue.lock().unwrap().is_empty());
assert!(transport.receive_queue.lock().unwrap().is_empty());
assert_eq!(transport.metrics.lock().unwrap().queue_depth, 0);
assert_eq!(transport.metrics.lock().unwrap().congestion, 0.0);
}
#[test]
fn test_wifi_direct_on_status_changed_clears_state() {
let transport = WifiDirectTransport::new("test-device");
transport.on_status_changed(TransportStatus::Available);
transport.on_peer_connected("bob");
let peer = WifiDirectPeer {
device_name: "P".to_string(),
device_address: "00:11:22:33:44:55".to_string(),
is_group_owner: false,
last_seen: SystemTime::now(),
connected: true,
};
transport.on_peer_discovered(peer);
transport.send(&create_test_message()).unwrap();
transport.on_status_changed(TransportStatus::Disconnected);
assert!(transport.peers.lock().unwrap().is_empty());
assert!(transport.send_queue.lock().unwrap().is_empty());
assert!(transport.receive_queue.lock().unwrap().is_empty());
assert_eq!(transport.metrics.lock().unwrap().queue_depth, 0);
}
#[test]
fn test_wifi_direct_on_status_changed_available_preserves_state() {
let transport = WifiDirectTransport::new("test-device");
transport.on_status_changed(TransportStatus::Available);
transport.on_peer_connected("bob");
let peer = WifiDirectPeer {
device_name: "P".to_string(),
device_address: "00:11:22:33:44:55".to_string(),
is_group_owner: false,
last_seen: SystemTime::now(),
connected: true,
};
transport.on_peer_discovered(peer);
transport.send(&create_test_message()).unwrap();
transport.on_status_changed(TransportStatus::Available);
assert_eq!(transport.peers.lock().unwrap().len(), 1);
assert!(!transport.send_queue.lock().unwrap().is_empty());
}
#[test]
fn test_set_on_messages_available_callback_invoked_on_send() {
use std::sync::atomic::{AtomicBool, Ordering};
let transport = WifiDirectTransport::new("test-device");
transport.on_status_changed(TransportStatus::Available);
transport.on_peer_connected("bob");
let called = std::sync::Arc::new(AtomicBool::new(false));
let c = called.clone();
transport.set_on_messages_available(std::sync::Arc::new(move || {
c.store(true, Ordering::SeqCst)
}));
let _ = transport.send(&create_test_message());
assert!(called.load(Ordering::SeqCst));
}
#[test]
fn test_get_next_message_returns_recipient_and_data() {
let transport = WifiDirectTransport::new("test-device");
transport.on_status_changed(TransportStatus::Available);
transport.on_peer_connected("bob");
let message = create_test_message();
transport.send(&message).unwrap();
let next = transport.get_next_message().unwrap();
assert!(next.is_some());
let (recipient, data) = next.unwrap();
assert_eq!(recipient, message.recipient.as_str());
assert!(!data.is_empty());
let deserialized = transport.deserialize_message(&data).unwrap();
assert_eq!(deserialized.id, message.id);
}
#[test]
fn test_on_data_received_rejects_oversized_payload() {
let transport = WifiDirectTransport::new("test-device");
let oversized = vec![0u8; DEFAULT_MAX_MESSAGE_SIZE + 1];
let result = transport.on_data_received(oversized);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::Error::MessageTooLarge(_, _)
));
}
#[test]
fn test_on_data_received_from_rejects_oversized_payload() {
let transport = WifiDirectTransport::new("test-device");
let oversized = vec![0u8; DEFAULT_MAX_MESSAGE_SIZE + 1];
let result = transport.on_data_received_from(oversized, "peer1".to_string());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::Error::MessageTooLarge(_, _)
));
}
}