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..252c3b1 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, @@ -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")] @@ -190,3 +192,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..f2bf893 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,38 @@ enum SubCommands { ChangeHostConfiguration(ChangeHostConfigurationArgs), } +#[derive(Debug, PartialEq)] +enum StatusType { + Feig, + Zvt, +} + +impl FromStr for StatusType { + 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(anyhow::anyhow!( + "'{s}' is not a valid StatusType (feig | zvt)" + )), + } + } +} + #[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 +269,49 @@ 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, + }; + + // 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:#?}"); + } } } Ok(()) @@ -542,7 +602,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..dbee013 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 } @@ -224,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); } @@ -431,24 +420,89 @@ impl Feig { Err(error) } + 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 { + 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); + }; + + 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 - /// * 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, DiagnosisRequired or TerminalActivationRequired + /// - set terminal id, run emv diagnostics and initialize the terminal. pub async fn configure(&mut self) -> Result<()> { - if self.successfully_configured { - return Ok(()); + let status = self.status_enquiry().await?; + let mut force_init = false; + 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"); + force_init = true; + } + _ => { + warn!("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 { + 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?; } - self.initialize().await?; - self.successfully_configured = true; - Ok(()) }