From bac1f0d6313a18fc0db3366c3850a9567088b1a9 Mon Sep 17 00:00:00 2001 From: Dima Dorezyuk Date: Wed, 21 Jan 2026 17:22:36 +0100 Subject: [PATCH 1/4] Update the configure logic Signed-off-by: Dima Dorezyuk --- .github/workflows/cargo.yml | 1 + zvt/src/constants.rs | 52 ++++++++++++++- zvt/src/sequences.rs | 5 +- zvt_cli/src/main.rs | 82 ++++++++++++++++++----- zvt_feig_terminal/src/feig.rs | 119 +++++++++++++++++++++++++++------- 5 files changed, 216 insertions(+), 43 deletions(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index b91cd0e..ff97dfa 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -9,6 +9,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - run: cargo build --all - run: cargo build --all --examples + - run: cargo build --all -F with_lavego_error_codes test: name: cargo test runs-on: ubuntu-latest diff --git a/zvt/src/constants.rs b/zvt/src/constants.rs index 5401c55..5891f81 100644 --- a/zvt/src/constants.rs +++ b/zvt/src/constants.rs @@ -7,7 +7,7 @@ use thiserror::Error; pub enum ErrorMessages { #[cfg(feature = "with_lavego_error_codes")] #[error("declined, referred voice authorization possible")] - Declined = 0x02, + DeclinedReferredVoiceAuthorizationPossible = 0x02, #[cfg(feature = "with_lavego_error_codes")] #[error("declined")] Declined = 0x05, @@ -190,3 +190,53 @@ pub enum ErrorMessages { #[error("system error (= other/unknown error), See TLV tags 1F16 and 1F17")] SystemError = 0xff, } + +/// Messages as defined under chapter 11. +#[derive(Debug, PartialEq, FromPrimitive, Clone, Copy, Error)] +#[repr(u8)] +pub enum TerminalStatusCode { + #[error("PT ready")] + PtReady = 0x00, + #[error("Initialisation required")] + InitialisationRequired = 0x51, + #[error("Date/time incorrect")] + DateTimeIncorrect = 0x62, + #[error("Please wait (e.g. software-update still running)")] + PleaseWait = 0x9c, + #[error("Partial issue of goods")] + PartialIssueOfGoods = 0x9d, + #[error("Memory full")] + MemoryFull = 0xb1, + #[error("Merchant-journal full")] + MerchantJournalFull = 0xb2, + #[error("Voltage supply too low (external power supply)")] + VoltageSupplyTooLow = 0xbf, + #[error("Card locking mechanism defect")] + CardLockingMechanismDefect = 0xc0, + #[error("Merchant card locked")] + MerchantCardLocked = 0xc1, + #[error("Diagnosis required")] + DiagnosisRequired = 0xc2, + #[error("Card-profile invalid. New card-profiles must be loaded")] + CardProfileInvalid = 0xc4, + #[error("Printer not ready")] + PrinterNotReady = 0xcc, + #[error("Card inserted")] + CardInserted = 0xdc, + #[error("Out-of-order")] + OutOfOrder = 0xdf, + #[error("Remote-maintenance activated")] + RemoteMaintenanceActivated = 0xe0, + #[error("Card not completely removed")] + CardNotCompletelyRemoved = 0xe1, + #[error("Card-reader does not answer / card-reader defective")] + CardReaderDoesNotAnswer = 0xe2, + #[error("Shutter closed")] + ShutterClosed = 0xe3, + #[error("Terminal activation required")] + TerminalActivationRequired = 0xe4, + #[error("Reconciliation required")] + ReconciliationRequired = 0xf0, + #[error("OPT-data not available (= OPT-Personalisation required)")] + OptDataNotAvailable = 0xf6, +} diff --git a/zvt/src/sequences.rs b/zvt/src/sequences.rs index a33fd27..c5be928 100644 --- a/zvt/src/sequences.rs +++ b/zvt/src/sequences.rs @@ -591,8 +591,9 @@ pub enum StatusEnquiryResponse { PrintLine(packets::PrintLine), /// 2.55.4 PrintTextBlock(packets::PrintTextBlock), - /// 2.55.5 - CompletionData(packets::CompletionData), + /// 2.55.5. The possible tags in the status enquiry correspond to the + /// ReceiptPrintoutCompletion. + CompletionData(packets::ReceiptPrintoutCompletion), } impl Sequence for StatusEnquiry { diff --git a/zvt_cli/src/main.rs b/zvt_cli/src/main.rs index 920e0af..ff98239 100644 --- a/zvt_cli/src/main.rs +++ b/zvt_cli/src/main.rs @@ -3,6 +3,7 @@ use argh::FromArgs; use env_logger::{Builder, Env}; use std::io::Write; use std::net::Ipv4Addr; +use std::str::FromStr; use tokio::net::TcpStream; use tokio_stream::StreamExt; use zvt::sequences::Sequence; @@ -27,10 +28,36 @@ enum SubCommands { ChangeHostConfiguration(ChangeHostConfigurationArgs), } +#[derive(Debug, PartialEq)] +enum StatusType { + Feig, + Zvt, +} + +impl FromStr for StatusType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "feig" => Ok(StatusType::Feig), + "zvt" => Ok(StatusType::Zvt), + _ => Err(format!("'{}' is not a valid StatusType (feig | zvt)", s)), + } + } +} + #[derive(FromArgs, PartialEq, Debug)] -/// Query the cVEND status from the terminal and print to stdout. +/// Query the status. #[argh(subcommand, name = "status")] -struct StatusArgs {} +struct StatusArgs { + /// which type of status to use (feig | zvt) + #[argh(option, default = "StatusType::Zvt")] + r#type: StatusType, + + /// in case of zvt - which service byte to use. See section 2.55.1 for more details. + #[argh(option)] + service_byte: Option, +} #[derive(FromArgs, PartialEq, Debug)] /// Factory resets the terminal. @@ -240,18 +267,41 @@ fn init_logger() { .init(); } -async fn status(socket: &mut PacketTransport) -> Result<()> { - // Check the current version of the software - let request = feig::packets::CVendFunctions { - password: None, - instr: feig::constants::CVendFunctions::SystemsInfo as u16, - }; - let mut stream = feig::sequences::GetSystemInfo::into_stream(&request, socket); - while let Some(response) = stream.next().await { - use feig::sequences::GetSystemInfoResponse::*; - match response? { - CVendFunctionsEnhancedSystemInformationCompletion(data) => log::info!("{data:#?}"), - Abort(_) => bail!("Failed to get system info. Received Abort."), +async fn status( + socket: &mut PacketTransport, + password: usize, + status_type: StatusType, + service_byte: Option, +) -> Result<()> { + match status_type { + StatusType::Feig => { + // Check the current version of the software + let request = feig::packets::CVendFunctions { + password: None, + instr: feig::constants::CVendFunctions::SystemsInfo as u16, + }; + let mut stream = feig::sequences::GetSystemInfo::into_stream(&request, socket); + while let Some(response) = stream.next().await { + use feig::sequences::GetSystemInfoResponse::*; + match response? { + CVendFunctionsEnhancedSystemInformationCompletion(data) => { + log::info!("{data:#?}") + } + Abort(_) => bail!("Failed to get system info. Received Abort."), + } + } + } + StatusType::Zvt => { + let request = packets::StatusEnquiry { + password: Some(password), + service_byte: service_byte, + tlv: None, + }; + + let mut stream = sequences::StatusEnquiry::into_stream(&request, socket); + while let Some(response) = stream.next().await { + log::info!("{response:#?}"); + } } } Ok(()) @@ -542,7 +592,9 @@ async fn main() -> Result<()> { }; match args.command { - SubCommands::Status(_) => status(&mut socket).await?, + SubCommands::Status(a) => { + status(&mut socket, args.password, a.r#type, a.service_byte).await? + } SubCommands::FactoryReset(_) => factory_reset(&mut socket, args.password).await?, SubCommands::Registration(a) => registration(&mut socket, args.password, &a).await?, SubCommands::SetTerminalId(a) => set_terminal_id(&mut socket, args.password, &a).await?, diff --git a/zvt_feig_terminal/src/feig.rs b/zvt_feig_terminal/src/feig.rs index 1c6a103..ff34783 100644 --- a/zvt_feig_terminal/src/feig.rs +++ b/zvt_feig_terminal/src/feig.rs @@ -139,9 +139,6 @@ pub struct Feig { /// The last end of day job. end_of_day_last_instant: std::time::Instant, - - /// Was the terminal successfully configured - successfully_configured: bool, } impl Feig { @@ -155,16 +152,11 @@ impl Feig { transactions_max_num, end_of_day_max_interval, end_of_day_last_instant: std::time::Instant::now(), - successfully_configured: false, }; - // Ignore the errors from configure beyond setting the flag - // (call fails if e.x. the terminal id is invalid) - let mut successfully_configured = false; - if let Ok(_) = this.configure().await { - successfully_configured = true; - } - this.successfully_configured = successfully_configured; + // Ignore the errors from configure. (call fails if e.x. the terminal id + // is invalid) + let _unused = this.configure().await; Ok(this) } @@ -176,9 +168,6 @@ impl Feig { config }; self.socket = TcpStream::new(config)?; - // Reset this to trigger a network call inside the `configure` call - // below. - self.successfully_configured = false; // This checks if the new connection is sound. self.configure().await } @@ -433,22 +422,86 @@ impl Feig { /// Initializes the connection. /// - /// We're doing the following - /// * Set the terminal id if required. - /// * Initialize the terminal. - /// * Run end-of-day job. + /// We're doing the following based on the terminal status code: + /// * If PtReady - return + /// * If ReconciliationRequired - run end-of-day + /// * If InitialisationRequired or DiagnosisRequired - set terminal id and run emv diagnostics pub async fn configure(&mut self) -> Result<()> { - if self.successfully_configured { - return Ok(()); + // Get the status inquiry so we can reason on the terminal_status_code. + let password = self.socket.config().feig_config.password; + let request = packets::StatusEnquiry { + password: Some(password), + service_byte: None, + tlv: None, + }; + + let mut error = zvt::ZVTError::IncompleteData.into(); + let mut terminal_status_code = None; + let mut stream = sequences::StatusEnquiry::into_stream(request, &mut self.socket); + + while let Some(response) = stream.next().await { + let response = match response { + Ok(response) => response, + Err(err) => { + error = err; + continue; + } + }; + match response { + sequences::StatusEnquiryResponse::CompletionData(completion_data) => { + terminal_status_code = Some(completion_data.terminal_status_code); + } + other => { + log::debug!("{other:#?}"); + } + } } + drop(stream); + + let Some(terminal_status_code) = terminal_status_code else { + return Err(error); + }; + + let status = + constants::TerminalStatusCode::from_u8(terminal_status_code).ok_or(anyhow!( + "Unknown terminal status code: 0x{:02x}", + terminal_status_code + ))?; + + match status { + constants::TerminalStatusCode::PtReady => { + log::debug!("Terminal is ready"); + } + constants::TerminalStatusCode::ReconciliationRequired => { + info!("Reconciliation required, running end of day"); + self.end_of_day().await?; + } + constants::TerminalStatusCode::InitialisationRequired + | constants::TerminalStatusCode::DiagnosisRequired + | constants::TerminalStatusCode::TerminalActivationRequired => { + info!("Initialization or diagnosis required"); + self.set_terminal_id().await?; + self.run_diagnosis(packets::DiagnosisType::EmvConfiguration) + .await?; + self.initialize().await?; + } + _ => { + bail!("Unexpected terminal status: {}", status) + } + } + + // If we've an outdated tid, we would actually still receive PtReady or + // ReconciliationRequired. After running potential end of day jobs on + // what ever tid is currently stored in the payment terminal we now + // force the payment terminal to have our desired tid. If the tid didn't + // change this call returns right away. let tid_changed = self.set_terminal_id().await?; if tid_changed { + info!("Tid change detected"); self.run_diagnosis(packets::DiagnosisType::EmvConfiguration) .await?; + self.initialize().await?; } - self.initialize().await?; - self.successfully_configured = true; - Ok(()) } @@ -588,6 +641,7 @@ impl Feig { let mut error = zvt::ZVTError::IncompleteData.into(); let mut receipt_no = None; + let mut abort_error = None; let mut stream = sequences::Reservation::into_stream(request, &mut self.socket); while let Some(response) = stream.next().await { let response = match response { @@ -601,8 +655,7 @@ impl Feig { sequences::AuthorizationResponse::Abort(data) => { let err = zvt::constants::ErrorMessages::from_u8(data.error) .ok_or(anyhow!("Unknown error code: 0x{:X}", data.error))?; - - bail!(err); + abort_error = Some(err); } sequences::AuthorizationResponse::StatusInformation(data) => { // Only overwrite the receipt_no if it is contained in the @@ -614,6 +667,22 @@ impl Feig { _ => {} } } + drop(stream); + + // Handle TidNotActivated error specifically + #[cfg(feature = "with_lavego_error_codes")] + if let Some(constants::ErrorMessages::TidNotActivated) = abort_error { + info!("TID not activated, setting terminal ID"); + self.set_terminal_id().await?; + self.run_diagnosis(packets::DiagnosisType::EmvConfiguration) + .await?; + self.initialize().await?; + } + + // Handle other abort errors + if let Some(err) = abort_error { + bail!(err); + } match receipt_no { None => Err(error), From a173fd5ca5e4db664dc9e3704c0f3468e826af49 Mon Sep 17 00:00:00 2001 From: Dima Dorezyuk Date: Fri, 23 Jan 2026 08:36:50 +0100 Subject: [PATCH 2/4] remove the tid not initialized handling Signed-off-by: Dima Dorezyuk --- zvt/src/constants.rs | 2 ++ zvt_feig_terminal/src/feig.rs | 20 ++------------------ 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/zvt/src/constants.rs b/zvt/src/constants.rs index 5891f81..252c3b1 100644 --- a/zvt/src/constants.rs +++ b/zvt/src/constants.rs @@ -22,6 +22,8 @@ pub enum ErrorMessages { PinEntryRequiredx33 = 0x33, #[cfg(feature = "with_lavego_error_codes")] #[error("TID not activated")] + /// Note: This does **not** mean that we have to activate the tid on the + /// payment terminal side. TidNotActivated = 0x3a, #[cfg(feature = "with_lavego_error_codes")] #[error("PIN entry required")] diff --git a/zvt_feig_terminal/src/feig.rs b/zvt_feig_terminal/src/feig.rs index ff34783..d4d2a1d 100644 --- a/zvt_feig_terminal/src/feig.rs +++ b/zvt_feig_terminal/src/feig.rs @@ -641,7 +641,6 @@ impl Feig { let mut error = zvt::ZVTError::IncompleteData.into(); let mut receipt_no = None; - let mut abort_error = None; let mut stream = sequences::Reservation::into_stream(request, &mut self.socket); while let Some(response) = stream.next().await { let response = match response { @@ -655,7 +654,8 @@ impl Feig { sequences::AuthorizationResponse::Abort(data) => { let err = zvt::constants::ErrorMessages::from_u8(data.error) .ok_or(anyhow!("Unknown error code: 0x{:X}", data.error))?; - abort_error = Some(err); + + bail!(err); } sequences::AuthorizationResponse::StatusInformation(data) => { // Only overwrite the receipt_no if it is contained in the @@ -667,22 +667,6 @@ impl Feig { _ => {} } } - drop(stream); - - // Handle TidNotActivated error specifically - #[cfg(feature = "with_lavego_error_codes")] - if let Some(constants::ErrorMessages::TidNotActivated) = abort_error { - info!("TID not activated, setting terminal ID"); - self.set_terminal_id().await?; - self.run_diagnosis(packets::DiagnosisType::EmvConfiguration) - .await?; - self.initialize().await?; - } - - // Handle other abort errors - if let Some(err) = abort_error { - bail!(err); - } match receipt_no { None => Err(error), From e39df885df6947301f0bac1304b60beb3bfcd57b Mon Sep 17 00:00:00 2001 From: Dima Dorezyuk Date: Tue, 3 Feb 2026 10:42:56 +0100 Subject: [PATCH 3/4] reviewer remarks Signed-off-by: Dima Dorezyuk --- zvt_cli/src/main.rs | 14 +++++++++++-- zvt_feig_terminal/src/feig.rs | 39 ++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/zvt_cli/src/main.rs b/zvt_cli/src/main.rs index ff98239..f2bf893 100644 --- a/zvt_cli/src/main.rs +++ b/zvt_cli/src/main.rs @@ -35,13 +35,15 @@ enum StatusType { } impl FromStr for StatusType { - type Err = String; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "feig" => Ok(StatusType::Feig), "zvt" => Ok(StatusType::Zvt), - _ => Err(format!("'{}' is not a valid StatusType (feig | zvt)", s)), + _ => Err(anyhow::anyhow!( + "'{s}' is not a valid StatusType (feig | zvt)" + )), } } } @@ -298,6 +300,14 @@ async fn status( tlv: None, }; + // See table 12 in the definition. We cannot parse this reqeust + // correctly. + if let Some(sb) = service_byte { + if (sb & 0x02) == 0 { + log::warn!("The 'Do send SW-Version' is not supported. The output will be not correctly parsed."); + } + } + let mut stream = sequences::StatusEnquiry::into_stream(&request, socket); while let Some(response) = stream.next().await { log::info!("{response:#?}"); diff --git a/zvt_feig_terminal/src/feig.rs b/zvt_feig_terminal/src/feig.rs index d4d2a1d..0640688 100644 --- a/zvt_feig_terminal/src/feig.rs +++ b/zvt_feig_terminal/src/feig.rs @@ -420,13 +420,7 @@ impl Feig { Err(error) } - /// Initializes the connection. - /// - /// We're doing the following based on the terminal status code: - /// * If PtReady - return - /// * If ReconciliationRequired - run end-of-day - /// * If InitialisationRequired or DiagnosisRequired - set terminal id and run emv diagnostics - pub async fn configure(&mut self) -> Result<()> { + async fn status_enquiry(&mut self) -> Result { // Get the status inquiry so we can reason on the terminal_status_code. let password = self.socket.config().feig_config.password; let request = packets::StatusEnquiry { @@ -462,12 +456,22 @@ impl Feig { return Err(error); }; - let status = - constants::TerminalStatusCode::from_u8(terminal_status_code).ok_or(anyhow!( - "Unknown terminal status code: 0x{:02x}", - terminal_status_code - ))?; + constants::TerminalStatusCode::from_u8(terminal_status_code).ok_or(anyhow!( + "Unknown terminal status code: 0x{:02x}", + terminal_status_code + )) + } + /// Initializes the connection. + /// + /// We're doing the following based on the terminal status code: + /// * If PtReady - return + /// * If ReconciliationRequired - run end-of-day + /// * If InitialisationRequired, DiagnosisRequired or TerminalActivationRequired + /// - set terminal id, run emv diagnostics and initialize the terminal. + pub async fn configure(&mut self) -> Result<()> { + let status = self.status_enquiry().await?; + let mut force_init = false; match status { constants::TerminalStatusCode::PtReady => { log::debug!("Terminal is ready"); @@ -480,13 +484,10 @@ impl Feig { | constants::TerminalStatusCode::DiagnosisRequired | constants::TerminalStatusCode::TerminalActivationRequired => { info!("Initialization or diagnosis required"); - self.set_terminal_id().await?; - self.run_diagnosis(packets::DiagnosisType::EmvConfiguration) - .await?; - self.initialize().await?; + force_init = true; } _ => { - bail!("Unexpected terminal status: {}", status) + warn!("Unexpected terminal status: {status}") } } @@ -496,8 +497,8 @@ impl Feig { // force the payment terminal to have our desired tid. If the tid didn't // change this call returns right away. let tid_changed = self.set_terminal_id().await?; - if tid_changed { - info!("Tid change detected"); + if tid_changed || force_init { + info!("tid_changed: {tid_changed} and force_init {force_init}"); self.run_diagnosis(packets::DiagnosisType::EmvConfiguration) .await?; self.initialize().await?; From b76ab5941a652bc98ff0159e25ed2fe79894e1b7 Mon Sep 17 00:00:00 2001 From: Dima Dorezyuk Date: Tue, 3 Feb 2026 11:26:20 +0100 Subject: [PATCH 4/4] change verbosity of tid uptodate Signed-off-by: Dima Dorezyuk --- zvt_feig_terminal/src/feig.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zvt_feig_terminal/src/feig.rs b/zvt_feig_terminal/src/feig.rs index 0640688..dbee013 100644 --- a/zvt_feig_terminal/src/feig.rs +++ b/zvt_feig_terminal/src/feig.rs @@ -213,7 +213,7 @@ impl Feig { // Set the terminal id if required. if config.terminal_id == system_info.terminal_id { - info!("Terminal id already up-to-date"); + log::debug!("Terminal id already up-to-date"); return Ok(false); }