Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/cargo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 53 additions & 1 deletion zvt/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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")]
Expand Down Expand Up @@ -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,
}
5 changes: 3 additions & 2 deletions zvt/src/sequences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
92 changes: 77 additions & 15 deletions zvt_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Self, Self::Err> {
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<u8>,
}

#[derive(FromArgs, PartialEq, Debug)]
/// Factory resets the terminal.
Expand Down Expand Up @@ -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(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having trouble understanding how these changes here map to what's in the PR description. This seems to be modifying the CLI tool to log some data in response to a command. How does that relate to the rest of the changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we discussed it in person - but to be complete here: we're using the status inquiry in the zvt_feig_terminal and this adds the feature to debug the new status through the cli

socket: &mut PacketTransport,
password: usize,
status_type: StatusType,
service_byte: Option<u8>,
) -> 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(())
Expand Down Expand Up @@ -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?,
Expand Down
104 changes: 79 additions & 25 deletions zvt_feig_terminal/src/feig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -431,24 +420,89 @@ impl Feig {
Err(error)
}

async fn status_enquiry(&mut self) -> Result<constants::TerminalStatusCode> {
// 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?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wouldn't know a failure occurred here, since end_of_day doesn't log failure. Do we want to add some failure log there, here, or log the error when calling configure? Same goes for failure of set_terminal_id, initialize, and run_diagnosis below.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better yet - since if this function fails we won't have a working PT on a pole, better log it in the calling function and add an alert for this failure to track it

}
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(())
}

Expand Down