Skip to content

Commit

Permalink
Style
Browse files Browse the repository at this point in the history
  • Loading branch information
hazardousvoltage committed Nov 24, 2024
1 parent 4d61b67 commit def679a
Showing 1 changed file with 122 additions and 119 deletions.
241 changes: 122 additions & 119 deletions applications/main/nfc/plugins/supported_cards/ventra.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@
#include "datetime.h"
#include <furi_hal.h>


#define TAG "Ventra"

DateTime ventra_exp_date = {0}, ventra_validity_date = {0};
uint8_t ventra_high_seq=0, ventra_cur_blk=0, ventra_mins_active=0;

uint8_t ventra_high_seq = 0, ventra_cur_blk = 0, ventra_mins_active = 0;

uint32_t time_now() {
return furi_hal_rtc_get_timestamp();
Expand All @@ -32,7 +30,8 @@ uint32_t time_now() {
static DateTime dt_delta(DateTime dt, uint8_t delta_days) {
// returns shifted DateTime, from initial DateTime and time offset in seconds
DateTime dt_shifted = {0};
datetime_timestamp_to_datetime(datetime_datetime_to_timestamp(&dt) - (uint64_t)delta_days*86400, &dt_shifted);
datetime_timestamp_to_datetime(
datetime_datetime_to_timestamp(&dt) - (uint64_t)delta_days * 86400, &dt_shifted);
return dt_shifted;
}

Expand All @@ -51,76 +50,80 @@ static long dt_diff(DateTime dta, DateTime dtb) {
// - For passes, n days after first use
// - For tickets, 2 hours after first use
// Calculating these is dumber than it needs to be, see xact record parser.
bool isExpired(void)
{
bool isExpired(void) {
uint32_t ts_hard_exp = datetime_datetime_to_timestamp(&ventra_exp_date);
uint32_t ts_soft_exp = datetime_datetime_to_timestamp(&ventra_validity_date);
uint32_t ts_now = time_now();
return(ts_now >= ts_hard_exp || ts_now > ts_soft_exp);
return (ts_now >= ts_hard_exp || ts_now > ts_soft_exp);
}

static FuriString *ventra_parse_xact(const MfUltralightData *data, uint8_t blk, bool is_pass) {
static FuriString* ventra_parse_xact(const MfUltralightData* data, uint8_t blk, bool is_pass) {
FuriString* ventra_xact_str = furi_string_alloc();
uint16_t ts = data->page[blk].data[0]|data->page[blk].data[1]<<8;
uint16_t ts = data->page[blk].data[0] | data->page[blk].data[1] << 8;
uint8_t tran_type = ts & 0x1F;
ts >>= 5;
uint8_t day = data->page[blk].data[2];
uint32_t work = data->page[blk+1].data[0]|data->page[blk+1].data[1]<<8|data->page[blk+1].data[2]<<16;
uint32_t work = data->page[blk + 1].data[0] | data->page[blk + 1].data[1] << 8 |
data->page[blk + 1].data[2] << 16;
uint8_t seq = work & 0x7F;
uint16_t exp = (work >> 7) & 0x7FF;
uint8_t exp_day = data->page[blk+2].data[0];
uint16_t locus = data->page[blk+2].data[1]|data->page[blk+2].data[2]<<8;
uint8_t line = data->page[blk+2].data[3];
uint8_t exp_day = data->page[blk + 2].data[0];
uint16_t locus = data->page[blk + 2].data[1] | data->page[blk + 2].data[2] << 8;
uint8_t line = data->page[blk + 2].data[3];

// This computes the block timestamp, based on the card expiration date and delta from it
DateTime dt = dt_delta(ventra_exp_date, day);
dt.hour = (ts & 0x7FF)/60;
dt.minute = (ts & 0x7FF)%60;
dt.hour = (ts & 0x7FF) / 60;
dt.minute = (ts & 0x7FF) % 60;

// If sequence is 0, block isn't used yet (new card with only one active block, typically the first one.
// Otherwise, the block with higher sequence is the latest transaction, and the other block is prior transaction.
// Not necessarily in that order on the card. We need the latest data to compute validity and pretty-print them
// in reverse chrono. So this mess sets some globals as to which block is current, computes the validity times, etc.
if (seq == 0) {
if(seq == 0) {
furi_string_printf(ventra_xact_str, "-- EMPTY --");
return(ventra_xact_str);
return (ventra_xact_str);
}
if (seq > ventra_high_seq) {
ventra_high_seq=seq;
ventra_cur_blk=blk;
ventra_mins_active = data->page[blk+1].data[3];
if(seq > ventra_high_seq) {
ventra_high_seq = seq;
ventra_cur_blk = blk;
ventra_mins_active = data->page[blk + 1].data[3];
// Figure out the soft expiration. For passes it's easy, the readers update the "exp" field in the transaction record.
// Tickets, not so much, readers don't update "exp", but each xact record has "minutes since last tap" which is
// Tickets, not so much, readers don't update "exp", but each xact record has "minutes since last tap" which is
// updated and carried forward. That, plus transaction timestamp, gives the expiration time.
if (tran_type == 6) { // Furthermore, purchase transactions set bogus expiration dates
if (is_pass) {
if(tran_type == 6) { // Furthermore, purchase transactions set bogus expiration dates
if(is_pass) {
ventra_validity_date = dt_delta(ventra_exp_date, exp_day);
ventra_validity_date.hour = (exp & 0x7FF)/60;
ventra_validity_date.minute = (exp & 0x7FF)%60;
}
else
{
ventra_validity_date.hour = (exp & 0x7FF) / 60;
ventra_validity_date.minute = (exp & 0x7FF) % 60;
} else {
uint32_t validity_ts = datetime_datetime_to_timestamp(&dt);
validity_ts += (120 - ventra_mins_active)*60;
validity_ts += (120 - ventra_mins_active) * 60;
datetime_timestamp_to_datetime(validity_ts, &ventra_validity_date);
}
}
}

// Type 0 = Purchase, 1 = Train ride, 2 = Bus ride
// TODO: Check PACE and see if it uses a different line code
char linemap[3] = "PTB";
char *xact_fmt = (line==2)?"%c %5d %04d-%02d-%02d %02d:%02d":"%c %04X %04d-%02d-%02d %02d:%02d";
char* xact_fmt = (line == 2) ? "%c %5d %04d-%02d-%02d %02d:%02d" :
"%c %04X %04d-%02d-%02d %02d:%02d";
// I like a nice concise display showing all the relevant infos without having to scroll...
// Line StopID DateTime
furi_string_printf(ventra_xact_str, xact_fmt,
(line<3)?linemap[line]:'?',
furi_string_printf(
ventra_xact_str,
xact_fmt,
(line < 3) ? linemap[line] : '?',
locus,
dt.year, dt.month, dt.day,
dt.hour, dt.minute);
return(ventra_xact_str);
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute);
return (ventra_xact_str);
}

static bool ventra_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_assert(device);
furi_assert(parsed_data);
Expand All @@ -132,136 +135,136 @@ static bool ventra_parse(const NfcDevice* device, FuriString* parsed_data) {
do {
// This test can probably be improved -- it matches every Ventra I've seen, but will also match others
// in the same family. Or maybe we just generalize this parser.
if(data->page[4].data[0] != 0x0A || data->page[4].data[1] != 4 || data->page[4].data[2] != 0 ||
data->page[6].data[0] != 0 || data->page[6].data[1] != 0 || data->page[6].data[2] != 0) {
if(data->page[4].data[0] != 0x0A || data->page[4].data[1] != 4 ||
data->page[4].data[2] != 0 || data->page[6].data[0] != 0 ||
data->page[6].data[1] != 0 || data->page[6].data[2] != 0) {
FURI_LOG_D(TAG, "Not Ventra Ultralight");
break;
}

// Parse the product record, display interesting data & extract info needed to parse transaction blocks
// Had this in its own function, ended up just setting a bunch of shitty globals, so inlined it instead.
FuriString* ventra_prod_str = furi_string_alloc();
uint8_t otp = data->page[3].data[0];
uint8_t prod_code = data->page[5].data[2];
bool is_pass = false;
switch(prod_code)
{
case 2:
case 0x1F: // Only ever seen one of these, it parses like a Single
furi_string_cat_printf(ventra_prod_str, "Single");
break;
case 3:
case 0x3F:
is_pass=true;
furi_string_cat_printf(ventra_prod_str, "1-Day");
break;
case 4: // Last I checked, 3 day passes only available at airport TVMs & social service agencies
is_pass=true;
furi_string_cat_printf(ventra_prod_str, "3-Day");
break;
default:
is_pass=true; // There are some card types I don't know what they are, but they parse like a pass, not a ticket.
furi_string_cat_printf(ventra_prod_str, "0x%02X", data->page[5].data[2]);
break;
switch(prod_code) {
case 2:
case 0x1F: // Only ever seen one of these, it parses like a Single
furi_string_cat_printf(ventra_prod_str, "Single");
break;
case 3:
case 0x3F:
is_pass = true;
furi_string_cat_printf(ventra_prod_str, "1-Day");
break;
case 4: // Last I checked, 3 day passes only available at airport TVMs & social service agencies
is_pass = true;
furi_string_cat_printf(ventra_prod_str, "3-Day");
break;
default:
is_pass =
true; // There are some card types I don't know what they are, but they parse like a pass, not a ticket.
furi_string_cat_printf(ventra_prod_str, "0x%02X", data->page[5].data[2]);
break;
}
uint16_t date_y = data->page[4].data[3] | (data->page[5].data[0]<<8);

uint16_t date_y = data->page[4].data[3] | (data->page[5].data[0] << 8);
uint8_t date_d = date_y & 0x1F;
uint8_t date_m = (date_y >> 5) & 0x0F;
date_y >>= 9;
date_y += 2000;
ventra_exp_date.day = date_d;
ventra_exp_date.month = date_m;
ventra_exp_date.year = date_y;
ventra_validity_date = ventra_exp_date; // Until we know otherwise
ventra_validity_date = ventra_exp_date; // Until we know otherwise

// Parse the transaction blocks. This sets a few sloppy globals, but it's too complex and repetitive to inline.
FuriString *ventra_xact_str1 = ventra_parse_xact(data, 8, is_pass);
FuriString *ventra_xact_str2 = ventra_parse_xact(data, 12, is_pass);
FuriString* ventra_xact_str1 = ventra_parse_xact(data, 8, is_pass);
FuriString* ventra_xact_str2 = ventra_parse_xact(data, 12, is_pass);

uint8_t card_state = 1;
uint8_t rides_left = 0;

char *card_states[5] = {"???","NEW","ACT","USED","EXP"};

if (ventra_high_seq > 1) card_state = 2;
char* card_states[5] = {"???", "NEW", "ACT", "USED", "EXP"};

if(ventra_high_seq > 1) card_state = 2;
// On "ticket" product, the OTP bits mark off rides used. Bit 0 seems to be unused, the next 3 are set as rides are used.
// Some, not all, readers will set the high bits to 0x7 when a card is tapped after it's expired or depleted. Have not
// seen other combinations, but if we do, we'll make a nice ???. 1-day passes set the OTP bit 1 on first use. 3-day
// passes do not. But we don't really care, since they don't matter on passes, unless you're trying to rollback one.
if (!is_pass)
{
switch(otp)
{
case 0:
rides_left = 3;
break;
case 2:
card_state = 2;
rides_left = 2;
break;
case 6:
card_state = 2;
rides_left = 1;
break;
case 0x0E:
case 0x7E:
card_state = 3;
rides_left = 0;
break;
default:
card_state = 0;
rides_left = 0;
break;
if(!is_pass) {
switch(otp) {
case 0:
rides_left = 3;
break;
case 2:
card_state = 2;
rides_left = 2;
break;
case 6:
card_state = 2;
rides_left = 1;
break;
case 0x0E:
case 0x7E:
card_state = 3;
rides_left = 0;
break;
default:
card_state = 0;
rides_left = 0;
break;
}
}
if (isExpired()) {
if(isExpired()) {
card_state = 4;
rides_left = 0;
}

furi_string_printf(
parsed_data,
"\e#Ventra %s (%s)\n",
"\e#Ventra %s (%s)\n",
furi_string_get_cstr(ventra_prod_str),
card_states[card_state]);

furi_string_cat_printf(parsed_data, "Exp: %04d-%02d-%02d %02d:%02d\n",
ventra_validity_date.year,
ventra_validity_date.month,

furi_string_cat_printf(
parsed_data,
"Exp: %04d-%02d-%02d %02d:%02d\n",
ventra_validity_date.year,
ventra_validity_date.month,
ventra_validity_date.day,
ventra_validity_date.hour,
ventra_validity_date.minute);

if (rides_left)
{
furi_string_cat_printf(
parsed_data,
"Rides left: %d\n",
rides_left);

if(rides_left) {
furi_string_cat_printf(parsed_data, "Rides left: %d\n", rides_left);
}

furi_string_cat_printf(
parsed_data,
"%s\n",
furi_string_get_cstr(ventra_cur_blk==8?ventra_xact_str1:ventra_xact_str2));
furi_string_get_cstr(ventra_cur_blk == 8 ? ventra_xact_str1 : ventra_xact_str2));

furi_string_cat_printf(
parsed_data,
"%s\n",
furi_string_get_cstr(ventra_cur_blk==8?ventra_xact_str2:ventra_xact_str1));

furi_string_cat_printf(parsed_data, "TVM ID: %02X%02X\n", data->page[7].data[1], data->page[7].data[0]);
furi_string_get_cstr(ventra_cur_blk == 8 ? ventra_xact_str2 : ventra_xact_str1));

furi_string_cat_printf(
parsed_data, "TVM ID: %02X%02X\n", data->page[7].data[1], data->page[7].data[0]);
furi_string_cat_printf(parsed_data, "Tx count: %d\n", ventra_high_seq);
furi_string_cat_printf(parsed_data, "Hard Expiry: %04d-%02d-%02d",
ventra_exp_date.year,
ventra_exp_date.month,
furi_string_cat_printf(
parsed_data,
"Hard Expiry: %04d-%02d-%02d",
ventra_exp_date.year,
ventra_exp_date.month,
ventra_exp_date.day);

furi_string_free(ventra_prod_str);
furi_string_free(ventra_xact_str1);
furi_string_free(ventra_xact_str2);

parsed = true;
} while(false);

Expand Down

0 comments on commit def679a

Please sign in to comment.