#![warn(missing_docs, rustdoc::missing_crate_level_docs)]
extern crate self as nt_client;
use core::panic;
use std::{collections::VecDeque, convert::Into, error::Error, fmt::Debug, net::Ipv4Addr, ops::Deref, sync::Arc, time::{Duration, Instant}};
use futures_util::{stream::{SplitSink, SplitStream}, Future, SinkExt, StreamExt, TryStreamExt};
use time::ext::InstantExt;
use tokio::{net::TcpStream, select, sync::{broadcast, mpsc, Notify, RwLock}, task::JoinHandle, time::{interval, timeout}};
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, tungstenite::{self, Bytes, ClientRequestBuilder, Message, http::{Response, Uri}}};
use topic::{collection::TopicCollection, AnnouncedTopic, AnnouncedTopics, Topic};
use tracing::{debug, error, info, trace, warn};
#[cfg(feature = "protobuf")]
use ::protobuf::reflect::FileDescriptor;
#[cfg(feature = "struct")]
use crate::r#struct::StructData;
#[cfg(any(feature = "struct", feature = "protobuf"))]
use crate::schema::SchemaManager;
use crate::{error::{ConnectError, ConnectionClosedError, IntoAddrError, PingError, ReceiveMessageError, ReconnectError, SendMessageError, UpdateTimeError}, net::{BinaryData, ClientboundData, ClientboundTextData, PropertiesData, ServerboundMessage, ServerboundTextData, Subscribe, Unpublish, Unsubscribe}};
mod net;
pub mod error;
pub mod data;
pub mod topic;
pub mod subscribe;
pub mod publish;
#[cfg(feature = "math")]
pub mod math;
#[cfg(feature = "struct")]
pub mod r#struct;
#[cfg(feature = "protobuf")]
pub mod protobuf;
#[cfg(any(feature = "struct", feature = "protobuf"))]
pub mod schema;
type NTServerSender = mpsc::UnboundedSender<ServerboundMessage>;
type NTServerReceiver = mpsc::UnboundedReceiver<ServerboundMessage>;
type NTClientSender = broadcast::Sender<Arc<ClientboundData>>;
type NTClientReceiver = broadcast::Receiver<Arc<ClientboundData>>;
#[derive(Clone)]
pub struct ClientHandle {
time: Arc<RwLock<NetworkTablesTime>>,
announced_topics: Arc<RwLock<AnnouncedTopics>>,
server_send: NTServerSender,
client_send: NTClientSender,
}
impl Debug for ClientHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientHandle")
.field("time", &self.time)
.field("announced_topics", &self.announced_topics)
.finish()
}
}
impl ClientHandle {
fn new(server_send: NTServerSender, client_send: NTClientSender) -> Self {
Self {
time: Default::default(),
announced_topics: Default::default(),
server_send,
client_send,
}
}
pub fn time(&self) -> Arc<RwLock<NetworkTablesTime>> {
self.time.clone()
}
pub async fn announced_topic_from_id(&self, id: i32) -> Option<AnnouncedTopic> {
self.announced_topics.read().await.get_from_id(id).cloned()
}
pub async fn announced_topic_from_name(&self, name: &str) -> Option<AnnouncedTopic> {
self.announced_topics.read().await.get_from_name(name).cloned()
}
pub fn topic(&self, name: impl ToString) -> Topic {
Topic::new(name.to_string(), self.clone())
}
pub fn topics(&self, names: Vec<String>) -> TopicCollection {
TopicCollection::new(names, self.clone())
}
pub fn schema_topic(&self) -> Topic {
self.topic("/.schema/")
}
#[cfg(feature = "struct")]
pub fn struct_schema_topic<T: StructData>(&self) -> Topic {
self.schema_topic().child(format!("struct:{}", T::struct_type_name()))
}
#[cfg(feature = "protobuf")]
pub fn protobuf_schema_topic(&self, descriptor: &FileDescriptor) -> Topic {
self.schema_topic().child(format!("proto:{}", descriptor.name()))
}
pub fn clients_meta_topic(&self) -> Topic {
self.topic("$clients")
}
pub fn client_subs_meta_topic(&self, client_name: impl ToString) -> Topic {
self.topic(format!("$clientsub${}", client_name.to_string()))
}
pub fn server_subs_meta_topic(&self) -> Topic {
self.topic("$serversub")
}
pub fn topic_subs_meta_topic(&self, topic: impl ToString) -> Topic {
self.topic(format!("$sub${}", topic.to_string()))
}
pub fn client_pubs_meta_topic(&self, client_name: impl ToString) -> Topic {
self.topic(format!("$clientpub${}", client_name.to_string()))
}
pub fn server_pubs_meta_topic(&self) -> Topic {
self.topic("$serverpub")
}
pub fn topic_pubs_meta_topic(&self, topic: impl ToString) -> Topic {
self.topic(format!("$pub${}", topic.to_string()))
}
#[cfg(any(feature = "struct", feature = "protobuf"))]
pub fn schema_manager(&self) -> SchemaManager {
SchemaManager::new(self.clone())
}
}
pub struct Client {
addr: Ipv4Addr,
options: NewClientOptions,
handle: ClientHandle,
server_recv: NTServerReceiver,
}
impl Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("addr", &self.addr)
.field("options", &self.options)
.field("handle", &self.handle)
.finish()
}
}
impl Deref for Client {
type Target = ClientHandle;
fn deref(&self) -> &Self::Target {
&self.handle
}
}
impl AsRef<ClientHandle> for Client {
fn as_ref(&self) -> &ClientHandle {
&self.handle
}
}
impl Client {
pub fn new(options: NewClientOptions) -> Self {
let addr = match options.addr.clone().into_addr() {
Ok(addr) => addr,
Err(err) => panic!("{err}"),
};
let (server_send, server_recv) = mpsc::unbounded_channel();
let client_send = broadcast::Sender::new(1024);
Client {
addr,
options,
handle: ClientHandle::new(server_send, client_send),
server_recv,
}
}
pub fn handle(&self) -> &ClientHandle {
self.as_ref()
}
pub async fn connect(self) -> Result<(), ConnectError> {
self.connect_setup(|_| {}).await
}
pub async fn connect_setup<F>(self, setup: F) -> Result<(), ConnectError>
where F: FnOnce(&Self)
{
let (ws_stream, _) = if let Some(secure_port) = self.options.secure_port {
match self.try_connect("wss", secure_port).await {
Ok(ok) => ok,
Err(tungstenite::Error::Io(_)) => self.try_connect("ws", self.options.unsecure_port).await?,
Err(err) => return Err(err.into()),
}
} else {
self.try_connect("ws", self.options.unsecure_port).await?
};
setup(&self);
let handle = self.handle;
let (write, read) = ws_stream.split();
let pong_notify_recv = Arc::new(Notify::new());
let pong_notify_send = pong_notify_recv.clone();
let ping_task = Client::start_ping_task(pong_notify_recv, handle.server_send.clone(), self.options.ping_interval, self.options.response_timeout);
let (update_time_sender, update_time_recv) = mpsc::channel(1);
let update_time_task = Client::start_update_time_task(self.options.update_time_interval, handle.time(), handle.server_send.clone(), update_time_recv);
let announced_topics = handle.announced_topics.clone();
let write_task = Client::start_write_task(self.server_recv, write);
let read_task = Client::start_read_task(read, update_time_sender, pong_notify_send, announced_topics, handle.client_send);
let result = select! {
task = ping_task => task?.map_err(|err| err.into()),
task = write_task => task?.map_err(|err| err.into()),
task = read_task => task?.map_err(|err| err.into()),
task = update_time_task => task?.map_err(|err| err.into()),
};
info!("closing connection");
result
}
async fn try_connect(
&self,
scheme: &str,
port: u16,
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response<Option<Vec<u8>>>), tungstenite::Error> {
let uri: Uri = format!("{scheme}://{}:{port}/nt/{}", self.addr, self.options.name).try_into().expect("valid websocket uri");
let conn_str = uri.to_string();
debug!("attempting connection at {conn_str}");
let client_request = ClientRequestBuilder::new(uri)
.with_sub_protocol("v4.1.networktables.first.wpi.edu");
let res = tokio_tungstenite::connect_async(client_request).await;
if res.is_ok() { info!("connected to server at {conn_str}") };
res
}
fn start_ping_task(
pong_recv: Arc<Notify>,
ws_sender: NTServerSender,
ping_interval: Duration,
response_timeout: Duration,
) -> JoinHandle<Result<(), PingError>> {
tokio::spawn(async move {
let mut interval = interval(ping_interval);
interval.tick().await;
loop {
interval.tick().await;
ws_sender.send(ServerboundMessage::Ping).map_err(|_| ConnectionClosedError)?;
if (timeout(response_timeout, pong_recv.notified()).await).is_err() {
return Err(PingError::PongTimeout);
}
}
})
}
fn start_update_time_task(
update_time_interval: Duration,
time: Arc<RwLock<NetworkTablesTime>>,
ws_sender: NTServerSender,
mut time_recv: mpsc::Receiver<(Duration, Duration)>,
) -> JoinHandle<Result<(), UpdateTimeError>> {
tokio::spawn(async move {
let mut interval = interval(update_time_interval);
loop {
interval.tick().await;
let client_time = {
let time = time.read().await;
time.client_time()
};
let data = BinaryData::new::<u64>(
-1,
Duration::ZERO,
client_time.whole_microseconds().try_into().map_err(|_| UpdateTimeError::TimeOverflow)?,
);
ws_sender.send(ServerboundMessage::Binary(data)).map_err(|_| ConnectionClosedError)?;
if let Some((timestamp, client_send_time)) = time_recv.recv().await {
let offset = {
let now = time.read().await.client_time();
let rtt = now - client_send_time;
let server_time = timestamp - rtt / 2;
server_time - now
};
let mut time = time.write().await;
time.offset = offset;
trace!("updated time, offset = {offset:?}");
}
}
})
}
fn start_write_task(
mut server_recv: NTServerReceiver,
mut write: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
) -> JoinHandle<Result<(), SendMessageError>> {
tokio::spawn(async move {
loop {
match server_recv.recv().await {
Some(message) => {
let packet = match message {
ServerboundMessage::Text(json) => {
match json {
ServerboundTextData::Unpublish(Unpublish { pubuid }) => debug!("[pub {pubuid}] unpublished"),
ServerboundTextData::Subscribe(Subscribe { ref topics, subuid, ref options }) => {
debug!("[sub {subuid}] subscribed to {topics:?} with {options:?}");
},
ServerboundTextData::Unsubscribe(Unsubscribe { subuid }) => debug!("[sub {subuid}] unsubscribed"),
_ => {},
};
serde_json::to_string(&[json]).map_err(|err| err.into()).map(|string| Message::Text(string.into()))
},
ServerboundMessage::Binary(binary) => {
if binary.id != -1 {
debug!("[pub {}] set to {} at {:?}", binary.id, binary.data, binary.timestamp);
};
rmp_serde::to_vec(&binary).map_err(|err| err.into()).map(|bytes| Message::Binary(bytes.into()))
},
ServerboundMessage::Ping => Ok(Message::Ping(Bytes::new())),
};
match packet {
Ok(packet) => {
if !matches!(packet, Message::Ping(_)) { trace!("sent message: {packet:?}"); };
if write.send(packet).await.is_err() { return Err(SendMessageError::ConnectionClosed(ConnectionClosedError)); };
},
Err(err) => return Err(err),
};
},
None => return Err(SendMessageError::ConnectionClosed(ConnectionClosedError)),
};
}
})
}
fn start_read_task(
read: SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
update_time_sender: mpsc::Sender<(Duration, Duration)>,
pong_send: Arc<Notify>,
announced_topics: Arc<RwLock<AnnouncedTopics>>,
client_sender: NTClientSender,
) -> JoinHandle<Result<(), ReceiveMessageError>> {
tokio::spawn(async move {
read.err_into().try_for_each(|message| async {
let message = match message {
Message::Binary(binary) => {
let mut binary = VecDeque::from(Vec::from(binary));
let mut binary_data = Vec::new();
while !binary.is_empty() {
let Ok(binary) = rmp_serde::from_read::<_, BinaryData>(&mut binary) else {
warn!("malformed binary data");
continue;
};
if binary.id == -1 {
let Some(micros) = binary.data.as_u64() else {
warn!("malformed timestamp data");
continue;
};
let client_send_time = Duration::from_micros(micros);
if update_time_sender.send((binary.timestamp, client_send_time)).await.is_err() {
return Err(ReceiveMessageError::ConnectionClosed(ConnectionClosedError));
};
}
binary_data.push(ClientboundData::Binary(binary));
};
Some(binary_data)
},
Message::Text(json) => {
match serde_json::from_str::<'_, Vec<ClientboundTextData>>(&json) {
Ok(text_data) => Some(text_data.into_iter().map(ClientboundData::Text).collect()),
Err(_) => {
warn!("malformed json data: {json}");
None
},
}
},
Message::Pong(_) => {
pong_send.notify_one();
None
},
Message::Close(_) => return Err(ReceiveMessageError::ConnectionClosed(ConnectionClosedError)),
_ => None,
};
if let Some(data_frame) = message {
trace!("received message(s): {data_frame:?}");
for data in data_frame {
match &data {
ClientboundData::Text(ClientboundTextData::Announce(announce)) => {
if let Some(pubuid) = announce.pubuid {
debug!("[pub {pubuid}] publishing {:?}s to `{}` with {:?}", announce.r#type, announce.name, announce.properties);
} else {
debug!("[topic {}] announced, type {:?} with {:?}", announce.name, announce.r#type, announce.properties);
}
let mut announced_topics = announced_topics.write().await;
announced_topics.insert(announce);
},
ClientboundData::Text(ClientboundTextData::Unannounce(unannounce)) => {
debug!("[topic {}] unannounced", unannounce.name);
let mut announced_topics = announced_topics.write().await;
announced_topics.remove(unannounce);
},
ClientboundData::Text(ClientboundTextData::Properties(PropertiesData { name, update, .. })) => {
let mut announced_topics = announced_topics.write().await;
let Some(topic) = announced_topics.get_mut_from_name(name) else {
continue;
};
let properties = &mut topic.properties;
for (key, value) in update {
match (key.as_ref(), value) {
("persistent", Some(serde_json::Value::Bool(persistent))) => properties.persistent = Some(*persistent),
("persistent", None) => properties.persistent = None,
("retained", Some(serde_json::Value::Bool(retained))) => properties.retained = Some(*retained),
("retained", None) => properties.retained = None,
("cached", Some(serde_json::Value::Bool(cached))) => properties.cached = Some(*cached),
("cached", None) => properties.cached = None,
(key, Some(value)) => {
properties.extra.insert(key.to_owned(), value.clone());
},
(key, None) => {
properties.extra.remove(key);
},
};
};
debug!("[topic {name}] updated properties to {:?}", properties);
},
ClientboundData::Binary(BinaryData { id, timestamp, data, .. }) => {
let announced_topics = announced_topics.read().await;
if let Some(topic) = announced_topics.get_from_id(*id) {
debug!("[topic {}] updated to {data} at {timestamp:?}", topic.name());
}
},
};
let _ = client_sender.send(data.into());
}
};
Ok(())
}).await
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NewClientOptions {
pub addr: NTAddr,
pub unsecure_port: u16,
pub secure_port: Option<u16>,
pub name: String,
pub response_timeout: Duration,
pub ping_interval: Duration,
pub update_time_interval: Duration,
}
impl Default for NewClientOptions {
fn default() -> Self {
Self {
addr: Default::default(),
unsecure_port: 5810,
secure_port: Some(5811),
name: format!("rust-client-{}", rand::random::<u16>()),
response_timeout: Duration::from_secs(1),
ping_interval: Duration::from_millis(200),
update_time_interval: Duration::from_secs(5),
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub enum NTAddr {
TeamNumber(u16),
#[default]
Local,
Custom(Ipv4Addr),
}
impl NTAddr {
pub fn into_addr(self) -> Result<Ipv4Addr, IntoAddrError> {
let addr = match self {
NTAddr::TeamNumber(team_number) => {
if team_number > 25599 { return Err(IntoAddrError::InvalidTeamNumber(team_number)); };
let first_section = team_number / 100;
let last_two = team_number % 100;
Ipv4Addr::new(10, first_section.try_into().unwrap(), last_two.try_into().unwrap(), 2)
},
NTAddr::Local => Ipv4Addr::LOCALHOST,
NTAddr::Custom(addr) => addr,
};
Ok(addr)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NetworkTablesTime {
started: Instant,
offset: time::Duration,
}
impl Default for NetworkTablesTime {
fn default() -> Self {
Self::new()
}
}
impl NetworkTablesTime {
pub fn new() -> Self {
Self { started: Instant::now(), offset: time::Duration::ZERO }
}
pub fn client_time(&self) -> time::Duration {
Instant::now().signed_duration_since(self.started)
}
pub fn server_time(&self) -> Duration {
match (self.client_time() + self.offset).try_into() {
Ok(duration) => duration,
Err(_) => panic!("expected server time to be positive"),
}
}
}
pub async fn reconnect<F, I>(options: NewClientOptions, mut init: I) -> Result<(), Box<dyn Error + Send + Sync>>
where
F: Future<Output = Result<(), ReconnectError>>,
I: FnMut(Client) -> F,
{
loop {
match init(Client::new(options.clone())).await {
Ok(_) => return Ok(()),
Err(ReconnectError::Fatal(err)) => {
error!("fatal error occurred: {err}");
return Err(err);
},
Err(ReconnectError::Nonfatal(err)) => {
error!("client crashed! {err}");
info!("attempting to reconnect");
},
}
}
}
pub(crate) async fn recv_until<T, F>(recv_ws: &mut NTClientReceiver, mut filter: F) -> Result<T, broadcast::error::RecvError>
where F: FnMut(Arc<ClientboundData>) -> Option<T>
{
loop {
if let Some(data) = filter(recv_ws.recv().await?) {
return Ok(data);
}
};
}
pub(crate) async fn recv_until_async<T, F, Fu>(recv_ws: &mut NTClientReceiver, mut filter: F) -> Result<T, broadcast::error::RecvError>
where
Fu: Future<Output = Option<T>>,
F: FnMut(Arc<ClientboundData>) -> Fu,
{
loop {
if let Some(data) = filter(recv_ws.recv().await?).await {
return Ok(data);
}
};
}