1use core::future::Future;
27use core::pin::Pin;
28use core::task::{Context, Poll, ready};
29use std::collections::VecDeque;
30use std::time::Duration;
31
32use thiserror::Error;
33use tokio_stream::wrappers::BroadcastStream;
34use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
35use tokio_stream::{Stream, StreamExt};
36
37use crate::protocol::{BatteryStatus, BoardEvent, Command, PieceStatus};
38#[cfg(doc)]
39use crate::transport;
40use crate::transport::{
41 DecodeNotificationError, DecodedNotification, MAX_NOTIFICATION_LEN, Notification,
42 NotificationSource, decode_notification,
43};
44
45pub trait TokioTransport: Send + 'static {
91 type Error: Send + 'static;
93
94 fn subscribe(
101 &mut self,
102 source: NotificationSource,
103 ) -> impl Future<Output = Result<(), Self::Error>> + Send + '_;
104
105 fn unsubscribe(
114 &mut self,
115 _source: NotificationSource,
116 ) -> impl Future<Output = Result<(), Self::Error>> + Send + '_ {
117 async { Ok(()) }
118 }
119
120 fn write_command<'a>(
127 &'a mut self,
128 command: &'a Command,
129 ) -> impl Future<Output = Result<(), Self::Error>> + Send + 'a;
130
131 fn next_notification<'a>(
142 &'a mut self,
143 buffer: &'a mut [u8],
144 ) -> impl Future<Output = Result<Notification<'a>, Self::Error>> + Send + 'a;
145
146 fn close(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send + '_ {
154 async { Ok(()) }
155 }
156}
157
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub struct ActorConfig {
174 pub command_capacity: usize,
176
177 pub event_capacity: usize,
179
180 pub query_capacity: usize,
182
183 pub request_timeout: Duration,
185}
186
187impl Default for ActorConfig {
188 fn default() -> Self {
189 Self {
190 command_capacity: 32,
191 event_capacity: 64,
192 query_capacity: 32,
193 request_timeout: Duration::from_secs(5),
194 }
195 }
196}
197
198#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
200pub enum SpawnError {
201 #[error("command channel capacity must be greater than zero")]
203 ZeroCommandCapacity,
204
205 #[error("event channel capacity must be greater than zero")]
207 ZeroEventCapacity,
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
212pub enum LifecycleState {
213 Starting,
215
216 Running,
218
219 ShuttingDown,
221
222 Stopped,
224
225 Faulted,
227}
228
229#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
231pub enum HandleError {
232 #[error("the board actor has stopped")]
234 ActorStopped,
235
236 #[error("the board actor is shutting down")]
238 ShuttingDown,
239
240 #[error("the transport operation failed; inspect BoardTask for the underlying error")]
244 TransportFailed,
245
246 #[error("the board request timed out")]
248 RequestTimedOut,
249
250 #[error("the pending query queue is full")]
252 QueryQueueFull,
253}
254
255#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
257pub enum EventStreamError {
258 #[error("event subscriber lagged and missed {0} events")]
260 Lagged(u64),
261
262 #[error(transparent)]
264 Decode(#[from] DecodeNotificationError),
265
266 #[error("the board event stream is closed")]
268 Closed,
269}
270
271#[derive(Debug, Error)]
273pub enum ActorError<E> {
274 #[error("transport error: {0}")]
276 Transport(E),
277}
278
279#[derive(Clone)]
297pub struct BoardHandle {
298 request_tx: ::tokio::sync::mpsc::Sender<Message>,
299 lifecycle_rx: ::tokio::sync::watch::Receiver<LifecycleState>,
300}
301
302impl BoardHandle {
303 pub fn lifecycle(&self) -> LifecycleState {
317 *self.lifecycle_rx.borrow()
318 }
319
320 pub fn subscribe_lifecycle(&self) -> ::tokio::sync::watch::Receiver<LifecycleState> {
326 self.lifecycle_rx.clone()
327 }
328
329 pub async fn send(&self, command: Command) -> Result<(), HandleError> {
342 trace_event!(
343 command_len = command.bytes().len(),
344 write_kind = ?command.write_kind(),
345 "submitting command to board actor"
346 );
347 let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
348 self
349 .send_message(Message::Send {
350 command,
351 reply: reply_tx,
352 })
353 .await?;
354 receive_reply(reply_rx).await?
355 }
356
357 pub async fn battery_status(&self) -> Result<BatteryStatus, HandleError> {
387 debug_event!(query = "battery_status", "submitting board query");
388 let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
389 self
390 .send_message(Message::Query(QueryRequest::Battery(reply_tx)))
391 .await?;
392 receive_reply(reply_rx).await?
393 }
394
395 pub async fn piece_status(&self) -> Result<PieceStatus, HandleError> {
432 debug_event!(query = "piece_status", "submitting board query");
433 let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
434 self
435 .send_message(Message::Query(QueryRequest::Pieces(reply_tx)))
436 .await?;
437 receive_reply(reply_rx).await?
438 }
439
440 pub async fn subscribe_events(&self) -> Result<EventStream, HandleError> {
467 debug_event!("subscribing to board actor events");
468 let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
469 self
470 .send_message(Message::SubscribeEvents { reply: reply_tx })
471 .await?;
472 receive_reply(reply_rx).await?
473 }
474
475 pub async fn shutdown(&self) -> Result<(), HandleError> {
498 info_event!("requesting graceful board actor shutdown");
499 let (reply_tx, reply_rx) = ::tokio::sync::oneshot::channel();
500 self
501 .send_message(Message::Shutdown { reply: reply_tx })
502 .await?;
503 receive_reply(reply_rx).await?
504 }
505
506 async fn send_message(&self, message: Message) -> Result<(), HandleError> {
508 let lifecycle = self.lifecycle();
509 match lifecycle {
510 LifecycleState::Starting | LifecycleState::Running => {}
511 LifecycleState::ShuttingDown => {
512 debug_event!(?lifecycle, "board actor rejected handle request");
513 return Err(HandleError::ShuttingDown);
514 }
515 LifecycleState::Stopped | LifecycleState::Faulted => {
516 debug_event!(?lifecycle, "board actor rejected handle request");
517 return Err(HandleError::ActorStopped);
518 }
519 }
520
521 self.request_tx.send(message).await.map_err(|_| {
522 debug_event!("board actor request channel is closed");
523 HandleError::ActorStopped
524 })
525 }
526}
527
528pub struct EventStream {
537 inner: BroadcastStream<Result<BoardEvent, DecodeNotificationError>>,
538}
539
540impl EventStream {
541 fn new(
543 receiver: ::tokio::sync::broadcast::Receiver<Result<BoardEvent, DecodeNotificationError>>,
544 ) -> Self {
545 Self {
546 inner: BroadcastStream::new(receiver),
547 }
548 }
549
550 pub async fn recv(&mut self) -> Result<BoardEvent, EventStreamError> {
579 self.next().await.unwrap_or(Err(EventStreamError::Closed))
580 }
581}
582
583impl Stream for EventStream {
584 type Item = Result<BoardEvent, EventStreamError>;
585
586 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
587 let item = ready!(Pin::new(&mut self.inner).poll_next(cx));
588 Poll::Ready(match item {
589 Some(Ok(Ok(event))) => Some(Ok(event)),
590 Some(Ok(Err(error))) => Some(Err(EventStreamError::Decode(error))),
591 Some(Err(BroadcastStreamRecvError::Lagged(count))) => {
592 warn_event!(missed_events = count, "board event subscriber lagged");
593 Some(Err(EventStreamError::Lagged(count)))
594 }
595 None => {
596 debug_event!("board event stream closed");
597 None
598 }
599 })
600 }
601}
602
603pub struct ActorExit<T: TokioTransport> {
609 transport: T,
610 result: Result<(), ActorError<T::Error>>,
611}
612
613impl<T: TokioTransport> ActorExit<T> {
614 pub const fn transport(&self) -> &T {
616 &self.transport
617 }
618
619 pub fn into_transport(self) -> T {
621 self.transport
622 }
623
624 pub fn into_parts(self) -> (T, Result<(), ActorError<T::Error>>) {
626 (self.transport, self.result)
627 }
628
629 pub const fn result(&self) -> Result<(), &ActorError<T::Error>> {
636 match &self.result {
637 Ok(()) => Ok(()),
638 Err(error) => Err(error),
639 }
640 }
641
642 pub fn into_result(self) -> Result<(), ActorError<T::Error>> {
649 self.result
650 }
651}
652
653pub struct BoardTask<T: TokioTransport> {
659 join: ::tokio::task::JoinHandle<ActorExit<T>>,
660}
661
662impl<T: TokioTransport> BoardTask<T> {
663 pub fn abort(&self) {
670 self.join.abort();
671 }
672
673 pub fn is_finished(&self) -> bool {
675 self.join.is_finished()
676 }
677}
678
679impl<T: TokioTransport> Future for BoardTask<T> {
680 type Output = Result<ActorExit<T>, ::tokio::task::JoinError>;
681
682 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
683 Pin::new(&mut self.join).poll(cx)
684 }
685}
686
687pub fn spawn<T: TokioTransport>(
717 transport: T,
718 config: ActorConfig,
719) -> Result<(BoardHandle, BoardTask<T>), SpawnError> {
720 if config.command_capacity == 0 {
721 warn_event!(
722 field = "command_capacity",
723 "invalid board actor configuration"
724 );
725 return Err(SpawnError::ZeroCommandCapacity);
726 }
727 if config.event_capacity == 0 {
728 warn_event!(
729 field = "event_capacity",
730 "invalid board actor configuration"
731 );
732 return Err(SpawnError::ZeroEventCapacity);
733 }
734
735 info_event!(
736 command_capacity = config.command_capacity,
737 event_capacity = config.event_capacity,
738 query_capacity = config.query_capacity,
739 request_timeout = ?config.request_timeout,
740 "spawning board actor"
741 );
742 let (request_tx, request_rx) = ::tokio::sync::mpsc::channel(config.command_capacity);
743 let (event_tx, _) = ::tokio::sync::broadcast::channel(config.event_capacity);
744 let (lifecycle_tx, lifecycle_rx) = ::tokio::sync::watch::channel(LifecycleState::Starting);
745
746 let actor = run_actor(transport, config, request_rx, event_tx, lifecycle_tx);
747 #[cfg(feature = "tracing")]
748 let actor = {
749 use ::tracing::Instrument as _;
750
751 actor.instrument(::tracing::info_span!("board_actor"))
752 };
753 let join = ::tokio::spawn(actor);
754
755 Ok((
756 BoardHandle {
757 request_tx,
758 lifecycle_rx,
759 },
760 BoardTask { join },
761 ))
762}
763
764enum Message {
766 Send {
767 command: Command,
768 reply: ::tokio::sync::oneshot::Sender<Result<(), HandleError>>,
769 },
770 Query(QueryRequest),
771 SubscribeEvents {
772 reply: ::tokio::sync::oneshot::Sender<Result<EventStream, HandleError>>,
773 },
774 Shutdown {
775 reply: ::tokio::sync::oneshot::Sender<Result<(), HandleError>>,
776 },
777}
778
779enum QueryRequest {
781 Battery(::tokio::sync::oneshot::Sender<Result<BatteryStatus, HandleError>>),
782 Pieces(::tokio::sync::oneshot::Sender<Result<PieceStatus, HandleError>>),
783}
784
785struct PendingQuery {
787 request: QueryRequest,
788 deadline: ::tokio::time::Instant,
789}
790
791impl QueryRequest {
792 #[cfg(feature = "tracing")]
794 const fn kind(&self) -> &'static str {
795 match self {
796 Self::Battery(_) => "battery_status",
797 Self::Pieces(_) => "piece_status",
798 }
799 }
800
801 fn command(&self) -> Command {
803 match self {
804 Self::Battery(_) => Command::read_battery_level(),
805 Self::Pieces(_) => Command::read_piece_status(),
806 }
807 }
808
809 fn fail(self, error: HandleError) {
811 match self {
812 Self::Battery(reply) => {
813 let _ = reply.send(Err(error));
814 }
815 Self::Pieces(reply) => {
816 let _ = reply.send(Err(error));
817 }
818 }
819 }
820}
821
822impl PendingQuery {
823 fn fail(self, error: HandleError) {
825 self.request.fail(error);
826 }
827}
828
829async fn receive_reply<T>(
831 reply: ::tokio::sync::oneshot::Receiver<Result<T, HandleError>>,
832) -> Result<Result<T, HandleError>, HandleError> {
833 reply.await.map_err(|_| HandleError::ActorStopped)
834}
835
836async fn run_actor<T: TokioTransport>(
842 mut transport: T,
843 config: ActorConfig,
844 mut request_rx: ::tokio::sync::mpsc::Receiver<Message>,
845 event_tx: ::tokio::sync::broadcast::Sender<Result<BoardEvent, DecodeNotificationError>>,
846 lifecycle_tx: ::tokio::sync::watch::Sender<LifecycleState>,
847) -> ActorExit<T> {
848 info_event!("board actor is starting");
849 let mut notification_buffer = [0; MAX_NOTIFICATION_LEN];
850 let mut pending_query: Option<PendingQuery> = None;
851 let mut queued_queries: VecDeque<QueryRequest> = VecDeque::new();
852 let mut shutdown_reply = None;
853
854 let mut result = initialize_transport(&mut transport)
855 .await
856 .map_err(ActorError::Transport);
857
858 if result.is_ok() {
859 lifecycle_tx.send_replace(LifecycleState::Running);
860 info_event!("board actor is running");
861
862 result = loop {
863 if pending_query.is_none()
864 && let Some(query) = queued_queries.pop_front()
865 {
866 debug_event!(
867 query = query.kind(),
868 queued_queries = queued_queries.len(),
869 "starting queued board query"
870 );
871 match start_query(&mut transport, query, config.request_timeout).await {
872 Ok(query) => pending_query = Some(query),
873 Err(error) => break Err(error),
874 }
875 }
876
877 let deadline = pending_query
878 .as_ref()
879 .map(|query| query.deadline)
880 .unwrap_or_else(::tokio::time::Instant::now);
881
882 ::tokio::select! {
883 message = request_rx.recv() => {
884 match message {
885 Some(Message::Send { command, reply }) => {
886 trace_event!(
887 command_len = command.bytes().len(),
888 write_kind = ?command.write_kind(),
889 "board actor is writing a command"
890 );
891 match transport.write_command(&command).await {
892 Ok(()) => {
893 let _ = reply.send(Ok(()));
894 }
895 Err(error) => {
896 error_event!(operation = "write_command", "board transport failed");
897 let _ = reply.send(Err(HandleError::TransportFailed));
898 break Err(ActorError::Transport(error));
899 }
900 }
901 }
902 Some(Message::Query(query)) => {
903 debug_event!(
904 query = query.kind(),
905 queued_queries = queued_queries.len(),
906 has_active_query = pending_query.is_some(),
907 "board actor received a query"
908 );
909 if pending_query.is_none() && queued_queries.is_empty() {
910 match start_query(&mut transport, query, config.request_timeout).await {
911 Ok(query) => pending_query = Some(query),
912 Err(error) => break Err(error),
913 }
914 } else if queued_queries.len() < config.query_capacity {
915 queued_queries.push_back(query);
916 } else {
917 warn_event!(
918 query = query.kind(),
919 query_capacity = config.query_capacity,
920 "board query queue is full"
921 );
922 query.fail(HandleError::QueryQueueFull);
923 }
924 }
925 Some(Message::SubscribeEvents { reply }) => {
926 debug_event!(
927 subscribers = event_tx.receiver_count() + 1,
928 "creating board event subscription"
929 );
930 let _ = reply.send(Ok(EventStream::new(event_tx.subscribe())));
931 }
932 Some(Message::Shutdown { reply }) => {
933 info_event!("board actor received shutdown request");
934 shutdown_reply = Some(reply);
935 break Ok(());
936 }
937 None => {
938 info_event!("all board handles were dropped");
939 break Ok(());
940 }
941 }
942 }
943 notification = transport.next_notification(&mut notification_buffer) => {
944 let notification = match notification {
945 Ok(notification) => notification,
946 Err(error) => {
947 error_event!(operation = "next_notification", "board transport failed");
948 break Err(ActorError::Transport(error));
949 }
950 };
951 trace_event!(
952 source = ?notification.source(),
953 notification_len = notification.bytes().len(),
954 "board actor received a notification"
955 );
956 let event = match decode_notification(notification) {
957 Ok(DecodedNotification::Event(event)) => event,
958 Ok(DecodedNotification::RealtimeUpdatesAcknowledged) => continue,
959 Err(error) => {
960 warn_event!(error = ?error, "board actor skipped a malformed notification");
961 let _ = event_tx.send(Err(error));
962 continue;
963 }
964 };
965
966 resolve_query(&mut pending_query, event);
967 let _ = event_tx.send(Ok(event));
968 }
969 _ = ::tokio::time::sleep_until(deadline), if pending_query.is_some() => {
970 if let Some(query) = pending_query.take() {
971 warn_event!(query = query.request.kind(), "board query timed out");
972 query.fail(HandleError::RequestTimedOut);
973 }
974 }
975 }
976 };
977 } else {
978 error_event!(
979 operation = "initialize",
980 "board actor initialization failed"
981 );
982 }
983
984 lifecycle_tx.send_replace(LifecycleState::ShuttingDown);
985 info_event!("board actor is shutting down");
986 request_rx.close();
987
988 if let Some(query) = pending_query.take() {
989 query.fail(HandleError::ShuttingDown);
990 }
991 for query in queued_queries {
992 query.fail(HandleError::ShuttingDown);
993 }
994 while let Ok(message) = request_rx.try_recv() {
995 fail_message(message, HandleError::ShuttingDown);
996 }
997
998 let cleanup_result = shutdown_transport(&mut transport)
999 .await
1000 .map_err(ActorError::Transport);
1001 if cleanup_result.is_err() {
1002 error_event!(operation = "shutdown", "board transport cleanup failed");
1003 }
1004 if result.is_ok() {
1005 result = cleanup_result;
1006 }
1007
1008 let final_state = if result.is_ok() {
1009 LifecycleState::Stopped
1010 } else {
1011 LifecycleState::Faulted
1012 };
1013 lifecycle_tx.send_replace(final_state);
1014 match final_state {
1015 LifecycleState::Stopped => info_event!("board actor stopped"),
1016 LifecycleState::Faulted => error_event!("board actor stopped after a fatal error"),
1017 _ => {}
1018 }
1019
1020 if let Some(reply) = shutdown_reply {
1021 let reply_result = if result.is_ok() {
1022 Ok(())
1023 } else {
1024 Err(HandleError::TransportFailed)
1025 };
1026 let _ = reply.send(reply_result);
1027 }
1028
1029 ActorExit { transport, result }
1030}
1031
1032async fn initialize_transport<T: TokioTransport>(transport: &mut T) -> Result<(), T::Error> {
1038 trace_event!(
1039 operation = "subscribe",
1040 source = ?NotificationSource::Position,
1041 "initializing board transport"
1042 );
1043 transport.subscribe(NotificationSource::Position).await?;
1044 trace_event!(
1045 operation = "enable_realtime_updates",
1046 "initializing board transport"
1047 );
1048 transport
1049 .write_command(&Command::enable_realtime_updates())
1050 .await?;
1051 trace_event!(
1052 operation = "subscribe",
1053 source = ?NotificationSource::CommandResponse,
1054 "initializing board transport"
1055 );
1056 transport
1057 .subscribe(NotificationSource::CommandResponse)
1058 .await
1059}
1060
1061async fn shutdown_transport<T: TokioTransport>(transport: &mut T) -> Result<(), T::Error> {
1066 trace_event!(
1067 operation = "unsubscribe",
1068 source = ?NotificationSource::CommandResponse,
1069 "shutting down board transport"
1070 );
1071 transport
1072 .unsubscribe(NotificationSource::CommandResponse)
1073 .await?;
1074 trace_event!(
1075 operation = "unsubscribe",
1076 source = ?NotificationSource::Position,
1077 "shutting down board transport"
1078 );
1079 transport.unsubscribe(NotificationSource::Position).await?;
1080 trace_event!(operation = "close", "shutting down board transport");
1081 transport.close().await
1082}
1083
1084async fn start_query<T: TokioTransport>(
1086 transport: &mut T,
1087 query: QueryRequest,
1088 timeout: Duration,
1089) -> Result<PendingQuery, ActorError<T::Error>> {
1090 debug_event!(
1091 query = query.kind(),
1092 timeout = ?timeout,
1093 "writing serialized board query"
1094 );
1095 if let Err(error) = transport.write_command(&query.command()).await {
1096 error_event!(
1097 operation = "write_query",
1098 query = query.kind(),
1099 "board transport failed"
1100 );
1101 query.fail(HandleError::TransportFailed);
1102 return Err(ActorError::Transport(error));
1103 }
1104
1105 Ok(PendingQuery {
1106 request: query,
1107 deadline: ::tokio::time::Instant::now() + timeout,
1108 })
1109}
1110
1111fn resolve_query(pending: &mut Option<PendingQuery>, event: BoardEvent) {
1116 let matches = matches!(
1117 (pending.as_ref().map(|query| &query.request), event),
1118 (Some(QueryRequest::Battery(_)), BoardEvent::BatteryStatus(_))
1119 | (Some(QueryRequest::Pieces(_)), BoardEvent::PieceStatus(_))
1120 );
1121
1122 if !matches {
1123 return;
1124 }
1125
1126 let query = pending.take().expect("matching pending query exists");
1127 debug_event!(
1128 query = query.request.kind(),
1129 "board query received its response"
1130 );
1131 match (query.request, event) {
1132 (QueryRequest::Battery(reply), BoardEvent::BatteryStatus(status)) => {
1133 let _ = reply.send(Ok(status));
1134 }
1135 (QueryRequest::Pieces(reply), BoardEvent::PieceStatus(status)) => {
1136 let _ = reply.send(Ok(status));
1137 }
1138 _ => unreachable!("query and event variants were checked before taking the query"),
1139 }
1140}
1141
1142fn fail_message(message: Message, error: HandleError) {
1144 match message {
1145 Message::Send { reply, .. } | Message::Shutdown { reply } => {
1146 let _ = reply.send(Err(error));
1147 }
1148 Message::Query(query) => query.fail(error),
1149 Message::SubscribeEvents { reply } => {
1150 let _ = reply.send(Err(error));
1151 }
1152 }
1153}
1154
1155#[cfg(test)]
1156mod tests {
1157 use std::sync::{Arc, Mutex};
1158
1159 use super::*;
1160
1161 #[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
1162 #[error("mock notification channel closed")]
1163 struct MockError;
1164
1165 struct OwnedNotification {
1166 source: NotificationSource,
1167 bytes: Vec<u8>,
1168 }
1169
1170 #[derive(Debug, PartialEq, Eq)]
1171 enum Operation {
1172 Subscribe(NotificationSource),
1173 Write(Vec<u8>),
1174 }
1175
1176 #[derive(Default)]
1177 struct Record {
1178 subscriptions: Vec<NotificationSource>,
1179 unsubscriptions: Vec<NotificationSource>,
1180 writes: Vec<Vec<u8>>,
1181 operations: Vec<Operation>,
1182 closed: bool,
1183 }
1184
1185 struct MockTransport {
1186 record: Arc<Mutex<Record>>,
1187 notification_rx: ::tokio::sync::mpsc::Receiver<OwnedNotification>,
1188 }
1189
1190 impl MockTransport {
1191 fn new() -> (
1192 Self,
1193 Arc<Mutex<Record>>,
1194 ::tokio::sync::mpsc::Sender<OwnedNotification>,
1195 ) {
1196 let record = Arc::new(Mutex::new(Record::default()));
1197 let (notification_tx, notification_rx) = ::tokio::sync::mpsc::channel(8);
1198
1199 (
1200 Self {
1201 record: Arc::clone(&record),
1202 notification_rx,
1203 },
1204 record,
1205 notification_tx,
1206 )
1207 }
1208 }
1209
1210 impl TokioTransport for MockTransport {
1211 type Error = MockError;
1212
1213 async fn subscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
1214 let mut record = self.record.lock().unwrap();
1215 record.subscriptions.push(source);
1216 record.operations.push(Operation::Subscribe(source));
1217 Ok(())
1218 }
1219
1220 async fn unsubscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
1221 self.record.lock().unwrap().unsubscriptions.push(source);
1222 Ok(())
1223 }
1224
1225 async fn write_command(&mut self, command: &Command) -> Result<(), Self::Error> {
1226 let bytes = command.bytes().to_vec();
1227 let mut record = self.record.lock().unwrap();
1228 record.writes.push(bytes.clone());
1229 record.operations.push(Operation::Write(bytes));
1230 Ok(())
1231 }
1232
1233 async fn next_notification<'a>(
1234 &'a mut self,
1235 buffer: &'a mut [u8],
1236 ) -> Result<Notification<'a>, Self::Error> {
1237 let notification = self.notification_rx.recv().await.ok_or(MockError)?;
1238 buffer[..notification.bytes.len()].copy_from_slice(¬ification.bytes);
1239 Ok(Notification::new(
1240 notification.source,
1241 &buffer[..notification.bytes.len()],
1242 ))
1243 }
1244
1245 async fn close(&mut self) -> Result<(), Self::Error> {
1246 self.record.lock().unwrap().closed = true;
1247 Ok(())
1248 }
1249 }
1250
1251 #[::tokio::test]
1252 async fn actor_correlates_queries_without_stealing_events() {
1253 let (transport, record, notification_tx) = MockTransport::new();
1254 let (handle, task) = spawn(transport, ActorConfig::default()).unwrap();
1255 let mut events = handle.subscribe_events().await.unwrap();
1256 wait_until_running(&handle).await;
1257
1258 assert_eq!(
1259 record.lock().unwrap().operations[..3],
1260 [
1261 Operation::Subscribe(NotificationSource::Position),
1262 Operation::Write(Command::enable_realtime_updates().bytes().to_vec()),
1263 Operation::Subscribe(NotificationSource::CommandResponse),
1264 ]
1265 );
1266
1267 notification_tx
1268 .send(OwnedNotification {
1269 source: NotificationSource::CommandResponse,
1270 bytes: vec![0x23, 0x01, 0x00],
1271 })
1272 .await
1273 .unwrap();
1274
1275 let query_handle = handle.clone();
1276 let query = ::tokio::spawn(async move { query_handle.battery_status().await });
1277 wait_for_write(&record, Command::read_battery_level().bytes()).await;
1278
1279 notification_tx
1280 .send(OwnedNotification {
1281 source: NotificationSource::Position,
1282 bytes: vec![0; 34],
1283 })
1284 .await
1285 .unwrap();
1286
1287 assert!(matches!(
1288 events.recv().await,
1289 Ok(BoardEvent::PositionChanged(_))
1290 ));
1291 assert!(!query.is_finished());
1292
1293 notification_tx
1294 .send(OwnedNotification {
1295 source: NotificationSource::CommandResponse,
1296 bytes: vec![0x41, 0x03, 0x0c, 0x01, 88],
1297 })
1298 .await
1299 .unwrap();
1300
1301 assert_eq!(
1302 query.await.unwrap(),
1303 Ok(BatteryStatus {
1304 charging: true,
1305 percentage: 88,
1306 })
1307 );
1308 assert_eq!(
1309 events.recv().await,
1310 Ok(BoardEvent::BatteryStatus(BatteryStatus {
1311 charging: true,
1312 percentage: 88,
1313 }))
1314 );
1315
1316 let query_handle = handle.clone();
1317 let query = ::tokio::spawn(async move { query_handle.piece_status().await });
1318 wait_for_write(&record, Command::read_piece_status().bytes()).await;
1319 notification_tx
1320 .send(OwnedNotification {
1321 source: NotificationSource::CommandResponse,
1322 bytes: piece_status_response(),
1323 })
1324 .await
1325 .unwrap();
1326
1327 let piece_status = query.await.unwrap().unwrap();
1328 assert_eq!(piece_status.pieces[0].battery_percentage, Some(50));
1329 assert_eq!(piece_status.pieces[33].battery_percentage, Some(83));
1330 assert!(matches!(
1331 events.recv().await,
1332 Ok(BoardEvent::PieceStatus(_))
1333 ));
1334
1335 handle.shutdown().await.unwrap();
1336 let exit = task.await.unwrap();
1337 assert!(exit.result().is_ok());
1338 assert_eq!(handle.lifecycle(), LifecycleState::Stopped);
1339 assert_eq!(events.recv().await, Err(EventStreamError::Closed));
1340
1341 let record = record.lock().unwrap();
1342 assert_eq!(
1343 record.subscriptions,
1344 [
1345 NotificationSource::Position,
1346 NotificationSource::CommandResponse,
1347 ]
1348 );
1349 assert_eq!(
1350 record.unsubscriptions,
1351 [
1352 NotificationSource::CommandResponse,
1353 NotificationSource::Position,
1354 ]
1355 );
1356 assert!(record.closed);
1357 }
1358
1359 #[::tokio::test(start_paused = true)]
1360 async fn request_timeout_does_not_stop_the_actor() {
1361 let (transport, record, _notification_tx) = MockTransport::new();
1362 let config = ActorConfig {
1363 request_timeout: Duration::from_secs(5),
1364 ..ActorConfig::default()
1365 };
1366 let (handle, task) = spawn(transport, config).unwrap();
1367 wait_until_running(&handle).await;
1368
1369 let query_handle = handle.clone();
1370 let query = ::tokio::spawn(async move { query_handle.battery_status().await });
1371 wait_for_write(&record, Command::read_battery_level().bytes()).await;
1372
1373 ::tokio::time::advance(Duration::from_secs(6)).await;
1374 assert_eq!(query.await.unwrap(), Err(HandleError::RequestTimedOut));
1375 assert_eq!(handle.lifecycle(), LifecycleState::Running);
1376
1377 handle.shutdown().await.unwrap();
1378 assert!(task.await.unwrap().result().is_ok());
1379 }
1380
1381 #[::tokio::test]
1382 async fn malformed_notification_is_reported_without_stopping_actor() {
1383 let (transport, _record, notification_tx) = MockTransport::new();
1384 let (handle, task) = spawn(transport, ActorConfig::default()).unwrap();
1385 let mut events = handle.subscribe_events().await.unwrap();
1386 wait_until_running(&handle).await;
1387
1388 notification_tx
1389 .send(OwnedNotification {
1390 source: NotificationSource::Position,
1391 bytes: vec![0; 29],
1392 })
1393 .await
1394 .unwrap();
1395
1396 assert!(matches!(
1397 events.recv().await,
1398 Err(EventStreamError::Decode(DecodeNotificationError::Position(
1399 crate::protocol::DecodePositionNotificationError::NotificationTooShort(
1400 crate::protocol::NotificationTooShortError {
1401 expected: 34,
1402 actual: 29,
1403 }
1404 )
1405 )))
1406 ));
1407 assert_eq!(handle.lifecycle(), LifecycleState::Running);
1408
1409 handle.shutdown().await.unwrap();
1410 assert!(task.await.unwrap().result().is_ok());
1411 }
1412
1413 #[::tokio::test]
1414 async fn event_stream_reports_lag() {
1415 let (sender, receiver) = ::tokio::sync::broadcast::channel(1);
1416 let mut events = EventStream::new(receiver);
1417
1418 sender
1419 .send(Ok(BoardEvent::BatteryStatus(BatteryStatus {
1420 charging: false,
1421 percentage: 10,
1422 })))
1423 .unwrap();
1424 sender
1425 .send(Ok(BoardEvent::BatteryStatus(BatteryStatus {
1426 charging: false,
1427 percentage: 11,
1428 })))
1429 .unwrap();
1430
1431 assert_eq!(events.recv().await, Err(EventStreamError::Lagged(1)));
1432 assert_eq!(
1433 events.recv().await,
1434 Ok(BoardEvent::BatteryStatus(BatteryStatus {
1435 charging: false,
1436 percentage: 11,
1437 }))
1438 );
1439 }
1440
1441 async fn wait_until_running(handle: &BoardHandle) {
1442 let mut lifecycle = handle.subscribe_lifecycle();
1443 while *lifecycle.borrow() != LifecycleState::Running {
1444 lifecycle.changed().await.unwrap();
1445 }
1446 }
1447
1448 async fn wait_for_write(record: &Arc<Mutex<Record>>, expected: &[u8]) {
1449 loop {
1450 if record
1451 .lock()
1452 .unwrap()
1453 .writes
1454 .iter()
1455 .any(|write| write == expected)
1456 {
1457 return;
1458 }
1459 ::tokio::task::yield_now().await;
1460 }
1461 }
1462
1463 fn piece_status_response() -> Vec<u8> {
1464 const IDENTITIES: [u8; 34] = [
1465 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 9, 9, 10,
1466 10, 11, 11, 12,
1467 ];
1468
1469 let mut response = vec![0; MAX_NOTIFICATION_LEN];
1470 response[..3].copy_from_slice(&[0x41, 0x89, 0x0b]);
1471 for (index, identity) in IDENTITIES.iter().copied().enumerate() {
1472 let offset = 3 + index * 4;
1473 response[offset] = identity;
1474 response[offset + 1] = index as u8;
1475 response[offset + 2] = u8::MAX - index as u8;
1476 response[offset + 3] = 50 + index as u8;
1477 }
1478 response
1479 }
1480}