use std::collections::VecDeque;
use std::time::{Duration, Instant};
use crate::codec::{DecodedPacket, PacketCodec, PacketType, ACK, ETX, NAK, SOH, STX};
use crate::error::{Ecr17Error, Result};
use crate::lrc::LrcMode;
use crate::transport::Transport;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SessionConfig {
pub lrc_mode: LrcMode,
pub ack_timeout_ms: u64,
pub response_timeout_ms: u64,
pub retry_count: u32,
pub retry_delay_ms: u64,
pub receipt_drain_ms: u64,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
lrc_mode: LrcMode::Std,
ack_timeout_ms: 2000,
response_timeout_ms: 60_000,
retry_count: 3,
retry_delay_ms: 200,
receipt_drain_ms: 0,
}
}
}
type EventCallback = Box<dyn Fn(String) + Send + 'static>;
enum WaitOutcome {
Frame(DecodedPacket),
Timeout,
TransportError(Ecr17Error),
}
pub struct Ecr17Session<T: Transport> {
transport: T,
config: SessionConfig,
codec: PacketCodec,
rx_buffer: VecDeque<u8>,
pending_result: Option<DecodedPacket>,
on_progress: Option<EventCallback>,
on_receipt_line: Option<EventCallback>,
}
impl<T: Transport> std::fmt::Debug for Ecr17Session<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ecr17Session")
.field("config", &self.config)
.field("rx_buffered", &self.rx_buffer.len())
.field("has_pending_result", &self.pending_result.is_some())
.finish_non_exhaustive()
}
}
impl<T: Transport> Ecr17Session<T> {
pub fn new(transport: T, config: SessionConfig) -> Self {
let codec = PacketCodec::new(config.lrc_mode);
Self {
transport,
config,
codec,
rx_buffer: VecDeque::new(),
pending_result: None,
on_progress: None,
on_receipt_line: None,
}
}
pub fn set_on_progress(&mut self, cb: impl Fn(String) + Send + 'static) {
self.on_progress = Some(Box::new(cb));
}
pub fn set_on_receipt_line(&mut self, cb: impl Fn(String) + Send + 'static) {
self.on_receipt_line = Some(Box::new(cb));
}
pub fn transport(&self) -> &T {
&self.transport
}
pub fn transport_mut(&mut self) -> &mut T {
&mut self.transport
}
pub async fn connect(&mut self) -> Result<()> {
self.transport.connect().await
}
pub async fn disconnect(&mut self) {
self.transport.disconnect().await;
}
pub fn is_connected(&self) -> bool {
self.transport.is_connected()
}
pub async fn exchange(&mut self, request_payload: &str) -> Result<DecodedPacket> {
self.reset_for_new_transaction();
self.ack_handshake(request_payload).await?;
self.wait_for_result().await
}
pub async fn exchange_with_additional_data(
&mut self,
request_payload: &str,
additional_payload: &str,
) -> Result<DecodedPacket> {
self.reset_for_new_transaction();
self.ack_handshake(request_payload).await?;
if self.pending_result.is_none() && self.rx_buffer.is_empty() {
self.ack_handshake(additional_payload).await?;
}
self.wait_for_result().await
}
pub async fn send_ack_only(&mut self, request_payload: &str) -> Result<()> {
self.reset_for_new_transaction();
self.ack_handshake(request_payload).await
}
fn reset_for_new_transaction(&mut self) {
self.rx_buffer.clear();
self.pending_result = None;
}
async fn ack_handshake(&mut self, request_payload: &str) -> Result<()> {
let frame = self.codec.encode_application(request_payload.as_bytes());
self.transport.send(&frame).await?;
let ack_timeout = Duration::from_millis(self.config.ack_timeout_ms);
let mut attempts: u32 = 1;
let mut deadline = Instant::now() + ack_timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
if attempts > self.config.retry_count {
return Err(Ecr17Error::AckTimeout { attempts });
}
self.retransmit(&frame, &mut attempts, &mut deadline, ack_timeout)
.await?;
continue;
}
match self.wait_for_frame(remaining).await {
WaitOutcome::Frame(pkt) => match pkt.packet_type {
PacketType::Ack => return Ok(()),
PacketType::Nak => {
if attempts > self.config.retry_count {
return Err(Ecr17Error::Nak { attempts });
}
self.retransmit(&frame, &mut attempts, &mut deadline, ack_timeout)
.await?;
}
PacketType::Application => {
self.pending_result = Some(pkt);
return Ok(());
}
_ => {}
},
WaitOutcome::TransportError(e) => return Err(e),
WaitOutcome::Timeout => {}
}
}
}
async fn retransmit(
&mut self,
frame: &[u8],
attempts: &mut u32,
deadline: &mut Instant,
ack_timeout: Duration,
) -> Result<()> {
tokio::time::sleep(Duration::from_millis(self.config.retry_delay_ms)).await;
self.transport.send(frame).await?;
*attempts += 1;
*deadline = Instant::now() + ack_timeout;
Ok(())
}
async fn wait_for_result(&mut self) -> Result<DecodedPacket> {
let deadline = Instant::now() + Duration::from_millis(self.config.response_timeout_ms);
loop {
let pkt = if let Some(p) = self.pending_result.take() {
p
} else {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(Ecr17Error::ResponseTimeout);
}
match self.wait_for_frame(remaining).await {
WaitOutcome::Frame(p) => p,
WaitOutcome::TransportError(e) => return Err(e),
WaitOutcome::Timeout => continue,
}
};
match pkt.packet_type {
PacketType::Progress => self.emit_progress(&pkt.payload),
PacketType::Application => {
if !pkt.valid_lrc {
self.send_control(NAK).await;
} else {
self.send_control(ACK).await;
if Self::is_receipt(&pkt.payload) {
self.emit_receipt(&pkt.payload);
} else {
self.drain_receipts().await;
return Ok(pkt);
}
}
}
PacketType::Ack | PacketType::Nak => {}
PacketType::Unknown => self.send_control(NAK).await,
}
}
}
async fn drain_receipts(&mut self) {
if self.config.receipt_drain_ms == 0 {
return;
}
let drain = Duration::from_millis(self.config.receipt_drain_ms);
loop {
match self.wait_for_frame(drain).await {
WaitOutcome::Frame(pkt) => match pkt.packet_type {
PacketType::Application => {
if pkt.valid_lrc {
self.send_control(ACK).await;
if Self::is_receipt(&pkt.payload) {
self.emit_receipt(&pkt.payload);
}
} else {
self.send_control(NAK).await;
}
}
PacketType::Progress => self.emit_progress(&pkt.payload),
_ => {}
},
WaitOutcome::Timeout | WaitOutcome::TransportError(_) => return,
}
}
}
fn extract_frame(&mut self) -> Option<Vec<u8>> {
while let Some(&first) = self.rx_buffer.front() {
if first == ACK || first == NAK {
if self.rx_buffer.len() < 3 {
return None; }
if self.rx_buffer[1] != ETX
|| self.rx_buffer[2] != self.codec.lrc_mode().compute(&[first])
{
self.rx_buffer.pop_front();
continue;
}
return Some(self.rx_buffer.drain(0..3).collect());
}
if first == STX {
let Some(etx) = self.rx_buffer.iter().position(|&b| b == ETX) else {
return None; };
if etx + 1 >= self.rx_buffer.len() {
return None; }
return Some(self.rx_buffer.drain(0..=etx + 1).collect());
}
if first == SOH {
let Some(eot) = self.rx_buffer.iter().position(|&b| b == crate::codec::EOT) else {
return None; };
return Some(self.rx_buffer.drain(0..=eot).collect());
}
self.rx_buffer.pop_front();
}
None
}
async fn wait_for_frame(&mut self, timeout: Duration) -> WaitOutcome {
let deadline = Instant::now() + timeout;
loop {
if let Some(frame) = self.extract_frame() {
return WaitOutcome::Frame(self.codec.decode(&frame));
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return WaitOutcome::Timeout;
}
match tokio::time::timeout(remaining, self.transport.recv()).await {
Ok(Ok(bytes)) => self.rx_buffer.extend(bytes),
Ok(Err(err)) => return WaitOutcome::TransportError(err),
Err(_elapsed) => return WaitOutcome::Timeout,
}
}
}
async fn send_control(&mut self, control: u8) {
let frame = self.codec.encode_control(control);
let _ = self.transport.send(&frame).await;
}
fn emit_progress(&self, payload: &[u8]) {
if let Some(cb) = &self.on_progress {
cb(String::from_utf8_lossy(payload).into_owned());
}
}
fn emit_receipt(&self, payload: &[u8]) {
if let Some(cb) = &self.on_receipt_line {
cb(String::from_utf8_lossy(payload).into_owned());
}
}
fn is_receipt(payload: &[u8]) -> bool {
payload.get(9) == Some(&b'S')
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::FakeTransport;
use std::sync::{Arc, Mutex};
fn fast_config() -> SessionConfig {
SessionConfig {
lrc_mode: LrcMode::Std,
ack_timeout_ms: 40,
response_timeout_ms: 40,
retry_count: 2,
retry_delay_ms: 1,
receipt_drain_ms: 0,
}
}
fn codec() -> PacketCodec {
PacketCodec::new(LrcMode::Std)
}
fn concat(mut a: Vec<u8>, b: Vec<u8>) -> Vec<u8> {
a.extend(b);
a
}
fn progress_frame(msg20: &str) -> Vec<u8> {
let mut f = vec![SOH];
f.extend_from_slice(msg20.as_bytes());
f.push(crate::codec::EOT);
f
}
const RESULT: &str = "123456780E0000DATA"; const RECEIPT: &str = "123456780SLINE 1";
fn sent_any(t: &FakeTransport, lead: u8) -> bool {
t.sent_frames().iter().any(|f| f.first() == Some(&lead))
}
#[tokio::test]
async fn happy_path_returns_result_and_acks() {
let c = codec();
let mut t = FakeTransport::new();
t.enqueue_response(concat(
c.encode_control(ACK),
c.encode_application(RESULT.as_bytes()),
));
let mut session = Ecr17Session::new(t, fast_config());
let result = session.exchange("123456780P...").await.unwrap();
assert_eq!(result.packet_type, PacketType::Application);
assert!(result.valid_lrc);
assert_eq!(result.payload, RESULT.as_bytes());
assert_eq!(session.transport().application_request_count(), 1);
assert!(sent_any(session.transport(), ACK));
}
#[tokio::test]
async fn stray_ack_byte_is_resynced_not_a_false_ack() {
let c = codec();
let mut response = vec![ACK, 0xFF]; response.extend(c.encode_control(ACK)); response.extend(c.encode_application(RESULT.as_bytes()));
let mut t = FakeTransport::new();
t.enqueue_response(response);
let mut session = Ecr17Session::new(t, fast_config());
let result = session.exchange("123456780P...").await.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
}
#[tokio::test]
async fn control_frame_with_bad_lrc_is_resynced() {
let c = codec();
let mut response = vec![ACK, ETX, 0x00]; response.extend(c.encode_control(ACK)); response.extend(c.encode_application(RESULT.as_bytes()));
let mut t = FakeTransport::new();
t.enqueue_response(response);
let mut session = Ecr17Session::new(t, fast_config());
let result = session.exchange("123456780P...").await.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
}
#[tokio::test]
async fn nak_triggers_retransmit_then_succeeds() {
let c = codec();
let mut t = FakeTransport::new();
t.enqueue_response(c.encode_control(NAK)); t.enqueue_response(concat(
c.encode_control(ACK),
c.encode_application(RESULT.as_bytes()),
));
let mut session = Ecr17Session::new(t, fast_config());
let result = session.exchange("123456780P...").await.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
assert_eq!(session.transport().application_request_count(), 2); }
#[tokio::test]
async fn ack_timeout_exhausts_retries_then_errors() {
let mut session = Ecr17Session::new(FakeTransport::new(), fast_config());
let err = session.exchange("123456780P...").await.unwrap_err();
assert!(matches!(err, Ecr17Error::AckTimeout { attempts: 3 }));
assert_eq!(session.transport().application_request_count(), 3); }
#[tokio::test]
async fn bad_lrc_response_sends_nak() {
let c = codec();
let mut t = FakeTransport::new();
let mut bad = c.encode_application(RESULT.as_bytes());
*bad.last_mut().unwrap() ^= 0xFF; t.enqueue_response(concat(c.encode_control(ACK), bad));
let mut session = Ecr17Session::new(t, fast_config());
let err = session.exchange("123456780P...").await.unwrap_err();
assert!(matches!(err, Ecr17Error::ResponseTimeout));
assert!(sent_any(session.transport(), NAK));
}
#[tokio::test]
async fn progress_messages_forwarded() {
let c = codec();
let mut response = c.encode_control(ACK);
response.extend(progress_frame("ATTENDERE PREGO "));
response.extend(c.encode_application(RESULT.as_bytes()));
let mut t = FakeTransport::new();
t.enqueue_response(response);
let progress = Arc::new(Mutex::new(Vec::<String>::new()));
let sink = Arc::clone(&progress);
let mut session = Ecr17Session::new(t, fast_config());
session.set_on_progress(move |m| sink.lock().unwrap().push(m));
let result = session.exchange("123456780P...").await.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
assert_eq!(
*progress.lock().unwrap(),
vec!["ATTENDERE PREGO ".to_string()]
);
}
#[tokio::test]
async fn receipt_lines_forwarded_before_result() {
let c = codec();
let mut response = c.encode_control(ACK);
response.extend(c.encode_application(RECEIPT.as_bytes()));
response.extend(c.encode_application(RESULT.as_bytes()));
let mut t = FakeTransport::new();
t.enqueue_response(response);
let receipts = Arc::new(Mutex::new(Vec::<String>::new()));
let sink = Arc::clone(&receipts);
let mut session = Ecr17Session::new(t, fast_config());
session.set_on_receipt_line(move |l| sink.lock().unwrap().push(l));
let result = session.exchange("123456780P...").await.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
assert_eq!(*receipts.lock().unwrap(), vec![RECEIPT.to_string()]);
}
#[tokio::test]
async fn result_before_ack_is_not_lost() {
let c = codec();
let mut t = FakeTransport::new();
t.enqueue_response(c.encode_application(RESULT.as_bytes())); let mut session = Ecr17Session::new(t, fast_config());
let result = session.exchange("123456780P...").await.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
assert_eq!(session.transport().application_request_count(), 1); assert!(sent_any(session.transport(), ACK));
}
#[tokio::test]
async fn response_timeout_after_ack_errors() {
let c = codec();
let mut t = FakeTransport::new();
t.enqueue_response(c.encode_control(ACK)); let mut session = Ecr17Session::new(t, fast_config());
assert!(matches!(
session.exchange("123456780P...").await,
Err(Ecr17Error::ResponseTimeout)
));
}
#[tokio::test]
async fn disconnect_during_exchange_errors() {
let mut t = FakeTransport::new();
t.disconnect_on_next_request();
let mut session = Ecr17Session::new(t, fast_config());
assert!(matches!(
session.exchange("123456780P...").await,
Err(Ecr17Error::Disconnected)
));
}
#[tokio::test]
async fn recovers_and_succeeds_after_reconnect() {
let mut t = FakeTransport::new();
t.disconnect_on_next_request();
let mut session = Ecr17Session::new(t, fast_config());
assert!(matches!(
session.exchange("123456780P...").await,
Err(Ecr17Error::Disconnected)
));
let c = codec();
session.transport_mut().rearm();
session.transport_mut().enqueue_response(concat(
c.encode_control(ACK),
c.encode_application(RESULT.as_bytes()),
));
let result = session.exchange("123456780P...").await.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
}
#[tokio::test]
async fn send_ack_only_returns_on_ack() {
let c = codec();
let mut t = FakeTransport::new();
t.enqueue_response(c.encode_control(ACK));
let mut session = Ecr17Session::new(t, fast_config());
session.send_ack_only("123456780E1").await.unwrap();
assert_eq!(session.transport().application_request_count(), 1);
}
#[tokio::test]
async fn send_ack_only_retransmits_on_nak() {
let c = codec();
let mut t = FakeTransport::new();
t.enqueue_response(c.encode_control(NAK));
t.enqueue_response(c.encode_control(ACK));
let mut session = Ecr17Session::new(t, fast_config());
session.send_ack_only("123456780E0").await.unwrap();
assert_eq!(session.transport().application_request_count(), 2);
}
#[tokio::test]
async fn send_ack_only_times_out() {
let mut session = Ecr17Session::new(FakeTransport::new(), fast_config());
assert!(matches!(
session.send_ack_only("123456780E1").await,
Err(Ecr17Error::AckTimeout { .. })
));
}
#[tokio::test]
async fn exchange_with_additional_data_sends_two_requests() {
let c = codec();
let mut t = FakeTransport::new();
t.enqueue_response(c.encode_control(ACK)); t.enqueue_response(concat(
c.encode_control(ACK),
c.encode_application(RESULT.as_bytes()),
));
let mut session = Ecr17Session::new(t, fast_config());
let result = session
.exchange_with_additional_data("123456780P...", "123456780U...")
.await
.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
assert_eq!(session.transport().application_request_count(), 2); }
#[tokio::test]
async fn exchange_with_additional_data_skips_u_when_result_already_arrived() {
let c = codec();
let mut t = FakeTransport::new();
t.enqueue_response(concat(
c.encode_control(ACK),
c.encode_application(RESULT.as_bytes()),
));
let mut session = Ecr17Session::new(t, fast_config());
let result = session
.exchange_with_additional_data("123456780P...", "123456780U...")
.await
.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
assert_eq!(session.transport().application_request_count(), 1); }
#[tokio::test]
async fn receipt_drain_forwards_receipts_after_result() {
let c = codec();
let mut response = c.encode_control(ACK);
response.extend(c.encode_application(RESULT.as_bytes())); response.extend(c.encode_application(RECEIPT.as_bytes())); let mut t = FakeTransport::new();
t.enqueue_response(response);
let mut cfg = fast_config();
cfg.receipt_drain_ms = 30;
let receipts = Arc::new(Mutex::new(Vec::<String>::new()));
let sink = Arc::clone(&receipts);
let mut session = Ecr17Session::new(t, cfg);
session.set_on_receipt_line(move |l| sink.lock().unwrap().push(l));
let result = session.exchange("123456780P...").await.unwrap();
assert_eq!(result.payload, RESULT.as_bytes());
assert_eq!(*receipts.lock().unwrap(), vec![RECEIPT.to_string()]);
}
}