1use ::core::time::Duration as CoreDuration;
2use embassy_futures::select::{select, select4, Either, Either4};
3use embassy_net::tcp::{Error as TcpIoError, TcpSocket};
4use embassy_net::{IpEndpoint, Stack};
5use embassy_time::{with_timeout, Duration, Instant, Timer};
6use embedded_io_async_07::Write;
7
8use prns_core::engine::InstantMillis;
9use prns_core::interfaces::rns_serial_framing::{self, RnsSerialDecoder};
10use prns_core::interfaces::{
11 tcp, BitrateBps, ConnectionState, InterfaceDescriptor, InterfaceId, InterfaceKind,
12};
13use prns_runtime::manifold::airtime::{frame_airtime_us, AirtimeLedger};
14use prns_runtime::manifold::driver::EmbassyInterfaceStatus;
15use prns_runtime::manifold::interface_seam::{Interface, InterfaceSeam, EMBEDDED_MAX_LINK_MTU};
16use prns_runtime::manifold::reconnect::ReconnectPolicy;
17use prns_runtime::manifold::throughput::ThroughputLedger;
18
19pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
20pub const SOCKET_TIMEOUT: Duration = Duration::from_secs(24);
22pub const KEEP_ALIVE: Duration = Duration::from_secs(5);
23pub const TCP_DNS_HOSTNAME_MAX_BYTES: usize = 253;
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum TcpClientExitCause {
27 PeerClosed,
28 ReadFailure(TcpIoError),
29 WriteFailure(TcpIoError),
30 Timeout,
31 NetworkUnavailable,
32 Disabled,
33}
34
35enum TcpConnectionAttempt {
36 Initial,
37 Retry,
38}
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41enum TcpNetworkFamily {
42 Ipv4,
43 Ipv6,
44}
45
46impl TcpConnectionAttempt {
47 const fn connection_state(&self) -> ConnectionState {
48 match self {
49 TcpConnectionAttempt::Initial => ConnectionState::Initializing,
50 TcpConnectionAttempt::Retry => ConnectionState::Reconnecting,
51 }
52 }
53}
54
55pub struct TcpSocketBuffers<'a> {
56 pub rx: &'a mut [u8],
57 pub tx: &'a mut [u8],
58}
59
60pub struct TcpClientInput<'a> {
61 pub stack: Stack<'a>,
62 pub target: TcpClientTarget,
63 pub channel_tag: &'a [u8],
64 pub bitrate: BitrateBps,
65 pub reconnect_policy: ReconnectPolicy,
66 pub socket_buffers: TcpSocketBuffers<'a>,
67 pub status: &'a EmbassyInterfaceStatus,
68}
69
70#[derive(Debug)]
71pub struct TcpClientTarget {
72 endpoint: Option<IpEndpoint>,
73 #[cfg(feature = "tcp-dns")]
74 hostname: heapless::String<TCP_DNS_HOSTNAME_MAX_BYTES>,
75 #[cfg(feature = "tcp-dns")]
76 port: u16,
77}
78
79impl TcpClientTarget {
80 #[must_use]
81 pub fn endpoint(endpoint: IpEndpoint) -> Self {
82 Self {
83 endpoint: Some(endpoint),
84 #[cfg(feature = "tcp-dns")]
85 hostname: heapless::String::new(),
86 #[cfg(feature = "tcp-dns")]
87 port: endpoint.port,
88 }
89 }
90
91 fn network_family(&self) -> TcpNetworkFamily {
92 match self.endpoint.map(|endpoint| endpoint.addr) {
93 Some(embassy_net::IpAddress::Ipv4(_)) | None => TcpNetworkFamily::Ipv4,
94 Some(embassy_net::IpAddress::Ipv6(_)) => TcpNetworkFamily::Ipv6,
95 }
96 }
97
98 #[cfg(feature = "tcp-dns")]
99 #[must_use]
100 pub fn dns(hostname: heapless::String<TCP_DNS_HOSTNAME_MAX_BYTES>, port: u16) -> Self {
101 Self {
102 endpoint: None,
103 hostname,
104 port,
105 }
106 }
107}
108
109pub struct TcpClient<'a> {
110 id: InterfaceId,
111 stack: Stack<'a>,
112 target: TcpClientTarget,
113 tag: &'a [u8],
114 bitrate: BitrateBps,
115 reconnect_policy: ReconnectPolicy,
116 rx_buffer: &'a mut [u8],
117 tx_buffer: &'a mut [u8],
118 status: &'a EmbassyInterfaceStatus,
119}
120
121impl<'a> TcpClient<'a> {
122 #[must_use]
123 pub fn interface_id(tag: &[u8]) -> InterfaceId {
124 InterfaceId::from_channel_tag(InterfaceKind::TcpClient, tag)
125 }
126
127 #[must_use]
128 pub fn new(input: TcpClientInput<'a>) -> Self {
129 let TcpClientInput {
130 stack,
131 target,
132 channel_tag,
133 bitrate,
134 reconnect_policy,
135 socket_buffers,
136 status,
137 } = input;
138 Self {
139 id: Self::interface_id(channel_tag),
140 stack,
141 target,
142 tag: channel_tag,
143 bitrate,
144 reconnect_policy,
145 rx_buffer: socket_buffers.rx,
146 tx_buffer: socket_buffers.tx,
147 status,
148 }
149 }
150
151 #[must_use]
152 pub fn id(&self) -> InterfaceId {
153 self.id
154 }
155}
156
157impl Interface for TcpClient<'_> {
158 const HW_MTU: usize = EMBEDDED_MAX_LINK_MTU;
159 const KIND: InterfaceKind = InterfaceKind::TcpClient;
160
161 fn descriptor(&self) -> InterfaceDescriptor {
162 tcp::descriptor(self.id, tcp::policy_for_bitrate(self.bitrate))
163 }
164
165 fn channel_tag(&self) -> &[u8] {
166 self.tag
167 }
168
169 async fn run<Seam: InterfaceSeam>(self, mut seam: Seam) {
170 let TcpClient {
171 id: _,
172 stack,
173 target,
174 tag: _,
175 bitrate,
176 reconnect_policy,
177 rx_buffer,
178 tx_buffer,
179 status,
180 } = self;
181 let mut decoder = RnsSerialDecoder::<{ tcp::EMBEDDED_FRAME_CAP }>::new();
182 let mut read_buf = [0u8; tcp::EMBEDDED_READ_BUF_LEN];
183 let mut frame_buf = [0u8; tcp::EMBEDDED_FRAMED_LEN];
184 let mut airtime = AirtimeLedger::new();
185 let mut throughput = ThroughputLedger::new();
186 let started = Instant::now();
187 let mut reconnect = reconnect_policy.schedule();
188 let mut connection_attempt = TcpConnectionAttempt::Initial;
189
190 loop {
191 if !status.is_enabled() {
192 status.set_connection(ConnectionState::Disabled);
193 status.wait_until_enabled().await;
194 continue;
195 }
196 status.set_connection(connection_attempt.connection_state());
197 connection_attempt = TcpConnectionAttempt::Retry;
198 let network_family = target.network_family();
199 let network_ready = select(
200 wait_until_network_ready(stack, network_family),
201 status.wait_until_disabled(),
202 )
203 .await;
204 if matches!(network_ready, Either::Second(())) {
205 status.set_connection(ConnectionState::Disabled);
206 continue;
207 }
208 crate::diagnostic_log::info!("tcp-client [configured]: resolving target={target:?}");
209 let resolved_target = select(
210 with_timeout(CONNECT_TIMEOUT, resolve_target(stack, &target)),
211 status.wait_until_disabled(),
212 )
213 .await;
214 let resolved_target = match resolved_target {
215 Either::First(Ok(Some(resolved_target))) => {
216 crate::diagnostic_log::info!(
217 "tcp-client [configured]: resolved target={target:?} endpoint={resolved_target:?}"
218 );
219 resolved_target
220 }
221 Either::First(Ok(None)) => {
222 crate::diagnostic_log::warn!(
223 "tcp-client [configured]: resolution failed target={target:?}"
224 );
225 status.set_connection(ConnectionState::Disconnected);
226 let reconnect_delay = reconnect.next_delay(|bytes| seam.fill_entropy(bytes));
227 crate::diagnostic_log::info!(
228 "tcp-client [configured]: target={target:?} retry_delay_ms={}",
229 reconnect_delay.as_millis()
230 );
231 let _ = select(
232 Timer::after(Duration::from_millis(reconnect_delay.as_millis() as u64)),
233 status.wait_until_disabled(),
234 )
235 .await;
236 continue;
237 }
238 Either::First(Err(_)) => {
239 crate::diagnostic_log::warn!(
240 "tcp-client [configured]: resolution failed target={target:?} cause={:?}",
241 TcpClientExitCause::Timeout
242 );
243 status.set_connection(ConnectionState::Disconnected);
244 let reconnect_delay = reconnect.next_delay(|bytes| seam.fill_entropy(bytes));
245 crate::diagnostic_log::info!(
246 "tcp-client [configured]: target={target:?} retry_delay_ms={}",
247 reconnect_delay.as_millis()
248 );
249 let _ = select(
250 Timer::after(Duration::from_millis(reconnect_delay.as_millis() as u64)),
251 status.wait_until_disabled(),
252 )
253 .await;
254 continue;
255 }
256 Either::Second(()) => {
257 status.set_connection(ConnectionState::Disabled);
258 crate::diagnostic_log::info!(
259 "tcp-client [configured]: target={target:?} exit={:?}",
260 TcpClientExitCause::Disabled
261 );
262 continue;
263 }
264 };
265 let mut socket = TcpSocket::new(stack, &mut *rx_buffer, &mut *tx_buffer);
266 socket.set_timeout(Some(SOCKET_TIMEOUT));
267 socket.set_keep_alive(Some(KEEP_ALIVE));
268 crate::diagnostic_log::info!(
269 "tcp-client [configured]: connecting target={target:?} endpoint={resolved_target:?}"
270 );
271 let connected = select(
272 with_timeout(CONNECT_TIMEOUT, socket.connect(resolved_target)),
273 status.wait_until_disabled(),
274 )
275 .await;
276 match connected {
277 Either::First(Ok(Ok(()))) => {
278 let connected_at = Instant::now();
279 reset_decoder_for_connection(&mut decoder);
280 status.set_connection(ConnectionState::Connected);
281 crate::diagnostic_log::info!(
282 "tcp-client [configured]: connected target={target:?} endpoint={resolved_target:?}"
283 );
284 let exit = serve(
285 &mut socket,
286 &mut seam,
287 status,
288 &mut decoder,
289 &mut read_buf,
290 &mut frame_buf,
291 &mut airtime,
292 &mut throughput,
293 bitrate,
294 started,
295 stack,
296 network_family,
297 )
298 .await;
299 let lifetime_ms = connected_at.elapsed().as_millis();
300 reconnect.record_connection_lifetime(CoreDuration::from_millis(lifetime_ms));
301 crate::diagnostic_log::info!(
302 "tcp-client [configured]: target={target:?} endpoint={resolved_target:?} exit={exit:?} lifetime_ms={lifetime_ms}"
303 );
304 }
305 Either::First(Ok(Err(error))) => {
306 crate::diagnostic_log::warn!(
307 "tcp-client [configured]: connect failed target={target:?} endpoint={resolved_target:?} error={error:?}"
308 );
309 }
310 Either::First(Err(_)) => {
311 crate::diagnostic_log::warn!(
312 "tcp-client [configured]: connect failed target={target:?} endpoint={resolved_target:?} cause={:?}",
313 TcpClientExitCause::Timeout
314 );
315 }
316 Either::Second(()) => {
317 crate::diagnostic_log::info!(
318 "tcp-client [configured]: connect stopped target={target:?} endpoint={resolved_target:?} exit={:?}",
319 TcpClientExitCause::Disabled
320 );
321 }
322 }
323 socket.abort();
324 if status.is_enabled() {
326 status.set_connection(ConnectionState::Disconnected);
327 let reconnect_delay = reconnect.next_delay(|bytes| seam.fill_entropy(bytes));
328 crate::diagnostic_log::info!(
329 "tcp-client [configured]: target={target:?} retry_delay_ms={}",
330 reconnect_delay.as_millis()
331 );
332 let _ = select(
333 Timer::after(Duration::from_millis(reconnect_delay.as_millis() as u64)),
334 status.wait_until_disabled(),
335 )
336 .await;
337 } else {
338 status.set_connection(ConnectionState::Disabled);
339 }
340 }
341 }
342}
343
344fn reset_decoder_for_connection(decoder: &mut RnsSerialDecoder<{ tcp::EMBEDDED_FRAME_CAP }>) {
345 decoder.reset();
346}
347
348async fn resolve_target(_stack: Stack<'_>, target: &TcpClientTarget) -> Option<IpEndpoint> {
349 if let Some(endpoint) = target.endpoint {
350 return Some(endpoint);
351 }
352 #[cfg(feature = "tcp-dns")]
353 {
354 use embassy_net::dns::DnsQueryType;
355 use embassy_net::IpAddress;
356
357 return _stack
358 .dns_query(target.hostname.as_str(), DnsQueryType::A)
359 .await
360 .ok()?
361 .into_iter()
362 .find_map(|address| match address {
363 IpAddress::Ipv4(address) => {
364 Some(IpEndpoint::new(IpAddress::Ipv4(address), target.port))
365 }
366 IpAddress::Ipv6(_) => None,
367 });
368 }
369 #[cfg(not(feature = "tcp-dns"))]
370 None
371}
372
373fn network_ready(stack: Stack<'_>, family: TcpNetworkFamily) -> bool {
374 if !stack.is_link_up() {
375 return false;
376 }
377 match family {
378 TcpNetworkFamily::Ipv4 => stack.config_v4().is_some(),
379 TcpNetworkFamily::Ipv6 => stack.config_v6().is_some(),
380 }
381}
382
383async fn wait_until_network_ready(stack: Stack<'_>, family: TcpNetworkFamily) {
384 while !network_ready(stack, family) {
385 Timer::after(Duration::from_millis(100)).await;
386 }
387}
388
389async fn wait_until_network_unavailable(stack: Stack<'_>, family: TcpNetworkFamily) {
390 while network_ready(stack, family) {
391 Timer::after(Duration::from_millis(100)).await;
392 }
393}
394
395#[expect(
396 clippy::too_many_arguments,
397 reason = "embedded serve-loop internals pass the loop's split-borrowed locals; bundling awaits an on-hardware validation pass"
398)]
399async fn serve<Seam: InterfaceSeam>(
400 socket: &mut TcpSocket<'_>,
401 seam: &mut Seam,
402 status: &EmbassyInterfaceStatus,
403 decoder: &mut RnsSerialDecoder<{ tcp::EMBEDDED_FRAME_CAP }>,
404 read_buf: &mut [u8],
405 frame_buf: &mut [u8],
406 airtime: &mut AirtimeLedger,
407 throughput: &mut ThroughputLedger,
408 bitrate: BitrateBps,
409 started: Instant,
410 stack: Stack<'_>,
411 network_family: TcpNetworkFamily,
412) -> TcpClientExitCause {
413 let (mut reader, mut writer) = socket.split();
414 loop {
415 match select4(
416 reader.read(read_buf),
417 seam.next_outbound(),
418 status.wait_until_disabled(),
419 wait_until_network_unavailable(stack, network_family),
420 )
421 .await
422 {
423 Either4::Fourth(()) => return TcpClientExitCause::NetworkUnavailable,
424 Either4::Third(()) => return TcpClientExitCause::Disabled,
425 Either4::First(read) => {
426 let read = match read {
427 Ok(0) => return TcpClientExitCause::PeerClosed,
428 Err(error) => return TcpClientExitCause::ReadFailure(error),
429 Ok(read) => read,
430 };
431 status.add_rx(read as u64);
432 let now = InstantMillis(started.elapsed().as_millis());
433 throughput.record_rx(now, read as u64);
434 status.set_transfer_rates(throughput.rates());
435 let mut offset = 0;
436 let chunk = &read_buf[..read];
437 while offset < chunk.len() {
438 match decoder.feed_slice_next(chunk, &mut offset) {
439 Ok(Some(frame)) => {
440 if !frame.is_empty() {
441 seam.next_inbound(frame).await;
442 }
443 }
444 Ok(None) => break,
445 Err(error) => crate::diagnostic_log::warn!(
446 "tcp-client [configured]: decode failed error={error:?}"
447 ),
448 }
449 }
450 }
451 Either4::Second(outbound) => match rns_serial_framing::encode(outbound, frame_buf) {
452 Ok(framed) => {
453 if let Err(error) = writer.write_all(&frame_buf[..framed]).await {
454 return TcpClientExitCause::WriteFailure(error);
455 }
456 status.add_tx(framed as u64);
457 let now = InstantMillis(started.elapsed().as_millis());
458 throughput.record_tx(now, framed as u64);
459 status.set_transfer_rates(throughput.rates());
460 let frame_airtime = frame_airtime_us(framed, bitrate);
461 status.set_airtime(airtime.record_tx(now, frame_airtime));
462 }
463 Err(error) => crate::diagnostic_log::warn!(
464 "tcp-client [configured]: encode failed error={error:?}"
465 ),
466 },
467 }
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn a_new_connection_discards_partial_decoder_state() {
477 let mut decoder = RnsSerialDecoder::<{ tcp::EMBEDDED_FRAME_CAP }>::new();
478 let mut encoded = [0u8; tcp::EMBEDDED_FRAMED_LEN];
479 let len = rns_serial_framing::encode(b"second connection", &mut encoded).unwrap();
480 let mut offset = 0;
481 let _ = decoder.feed_slice_next(&encoded[..len / 2], &mut offset);
482
483 reset_decoder_for_connection(&mut decoder);
484 offset = 0;
485 let decoded = decoder
486 .feed_slice_next(&encoded[..len], &mut offset)
487 .unwrap()
488 .expect("the complete frame decodes after reset");
489
490 assert_eq!(decoded, b"second connection");
491 }
492
493 #[test]
494 fn connection_attempts_distinguish_initialization_from_retries() {
495 assert_eq!(
496 TcpConnectionAttempt::Initial.connection_state(),
497 ConnectionState::Initializing
498 );
499 assert_eq!(
500 TcpConnectionAttempt::Retry.connection_state(),
501 ConnectionState::Reconnecting
502 );
503 }
504
505 #[test]
506 fn tcp_targets_select_their_required_network_family() {
507 let v4 = TcpClientTarget::endpoint(IpEndpoint::new(
508 embassy_net::Ipv4Address::new(192, 0, 2, 1).into(),
509 4242,
510 ));
511 let v6 = TcpClientTarget::endpoint(IpEndpoint::new(
512 embassy_net::Ipv6Address::LOCALHOST.into(),
513 4242,
514 ));
515 assert_eq!(v4.network_family(), TcpNetworkFamily::Ipv4);
516 assert_eq!(v6.network_family(), TcpNetworkFamily::Ipv6);
517 }
518}