From d747d5cfac274b10f9670f2c295fca1780e2568c Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sun, 21 Jun 2026 22:33:18 +0300 Subject: [PATCH] Added support for parsing and printing history records --- client/src/parsers/hrtparser/hrtparser.c | 96 +++++++++++++++++- client/src/parsers/hrtparser/hrtparser.h | 4 +- client/src/parsers/parsehrt.c | 118 +++++++++++++++++++++-- 3 files changed, 202 insertions(+), 16 deletions(-) diff --git a/client/src/parsers/hrtparser/hrtparser.c b/client/src/parsers/hrtparser/hrtparser.c index eca9816b3..9597ed157 100644 --- a/client/src/parsers/hrtparser/hrtparser.c +++ b/client/src/parsers/hrtparser/hrtparser.c @@ -49,6 +49,7 @@ static const int64_t MINUTE_IN_MS = 60000LL; #define HRT_ETICKET_LEN 26 #define HRT_ETICKET_V2_LEN 45 #define HRT_HISTORY_LEN 96 +#define HRT_HISTORY_RECORD_LEN 12 #define HRT_HISTORY_RECORDS 8 int hrt_price_to_string(int cents, char *out, size_t out_len) { @@ -669,8 +670,95 @@ void hrt_read_stored_value(hrt_travel_card_t *card, const uint8_t *data, size_t ((uint32_t)data[1] << 4); } -// void readHistory(const uint8_t *data, size_t length); -// void readHistory_v2(const uint8_t *data, size_t length); +void hrt_read_history(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { + if (!card || !data || !card->history_fields) return; + + size_t record_count = data_len / HRT_HISTORY_RECORD_LEN; + if (record_count > HRT_HISTORY_RECORDS) { + record_count = HRT_HISTORY_RECORDS; + } + + memset(card->history_fields, 0, HRT_HISTORY_RECORDS * sizeof(hrt_history_t)); + card->history_len = 0; + + for (size_t i = 0; i < record_count; i++) { + size_t offset = i * HRT_HISTORY_RECORD_LEN; + + if (data[offset + 1] == 0 && + data[offset + 2] == 0 && + data[offset + 3] == 0 && + data[offset + 4] == 0) { + continue; + } + + hrt_history_t *history = &card->history_fields[card->history_len]; + hrt_history_init(history); + + history->transaction_type = (data[offset] & 0x80) >> 7; + + uint8_t date_time_byte = data[offset + 3]; + int days = ((date_time_byte & 0x3F) << 8) | data[offset + 4]; + int minutes = ((data[offset + 1] & 0x01) << 10) | + ((uint16_t)data[offset + 2] << 2) | + ((date_time_byte & 0xC0) >> 6); + + int transfer_end_minutes = ((uint16_t)data[offset + 5] << 3) | + ((data[offset + 6] & 0xE0) >> 5); + + if (transfer_end_minutes < minutes) { + days--; + } + + history->transaction_d_time = en5145_datetime_to_time(days, minutes); + history->price = ((uint16_t)data[offset + 7] << 1) | + ((data[offset + 6] & 0x1F) << 9) | + ((data[offset + 8] & 0x80) >> 7); + history->group_size = (data[offset + 8] & 0x7C) >> 2; + + card->history_len++; + } +} + +void hrt_read_history_v2(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { + if (!card || !data || !card->history_fields) return; + + size_t record_count = data_len / HRT_HISTORY_RECORD_LEN; + if (record_count > HRT_HISTORY_RECORDS) { + record_count = HRT_HISTORY_RECORDS; + } + + memset(card->history_fields, 0, HRT_HISTORY_RECORDS * sizeof(hrt_history_t)); + card->history_len = 0; + + for (size_t i = 0; i < record_count; i++) { + size_t byte_offset = i * HRT_HISTORY_RECORD_LEN; + + if (data[byte_offset + 1] == 0 && + data[byte_offset + 2] == 0 && + data[byte_offset + 3] == 0 && + data[byte_offset + 4] == 0) { + continue; + } + + int bit_offset = (int)(i * 96); + hrt_history_t *history = &card->history_fields[card->history_len]; + hrt_history_init(history); + + history->transaction_type = convert_get_byte_value(data, data_len, bit_offset, 1); + history->transaction_d_time = en5145_datetime_to_time( + convert_get_short_value(data, data_len, bit_offset + 1, 14), + convert_get_short_value(data, data_len, bit_offset + 15, 11) + ); + history->transfer_end_date = en5145_datetime_to_time( + convert_get_short_value(data, data_len, bit_offset + 26, 14), + convert_get_short_value(data, data_len, bit_offset + 40, 11) + ); + history->price = convert_get_short_value(data, data_len, bit_offset + 51, 14); + history->group_size = convert_get_byte_value(data, data_len, bit_offset + 65, 6); + + card->history_len++; + } +} void hrt_history_init(hrt_history_t *history) { if (!history) return; @@ -1069,14 +1157,14 @@ bool hrt_travelcard_set_history(hrt_travel_card_t *card, const uint8_t *buf, siz if (!card->history_data_v2) return false; if (len > HRT_HISTORY_LEN) len = HRT_HISTORY_LEN; memcpy(card->history_data_v2, buf, len); - // TODO: readHistory_v2(card->historyData_v2, len); + hrt_read_history_v2(card, card->history_data_v2, len); return true; } if (!card->history_data) return false; if (len > HRT_HISTORY_LEN) len = HRT_HISTORY_LEN; memcpy(card->history_data, buf, len); - // TODO: readHistory(card->historyData, len); + hrt_read_history(card, card->history_data, len); return true; } diff --git a/client/src/parsers/hrtparser/hrtparser.h b/client/src/parsers/hrtparser/hrtparser.h index c3925713f..ce56b4cbc 100644 --- a/client/src/parsers/hrtparser/hrtparser.h +++ b/client/src/parsers/hrtparser/hrtparser.h @@ -240,8 +240,8 @@ void hrt_read_control_info(hrt_travel_card_t *card, const uint8_t *data, size_t void hrt_read_period_pass(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); void hrt_read_period_pass_v2(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); void hrt_read_stored_value(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); -void hrt_read_history(const uint8_t *data, size_t length); -void hrt_read_history_v2(const uint8_t *data, size_t length); +void hrt_read_history(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); +void hrt_read_history_v2(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); // Getter Functions int hrt_travelcard_get_application_version(const hrt_travel_card_t *card); diff --git a/client/src/parsers/parsehrt.c b/client/src/parsers/parsehrt.c index c92afcc7b..4f54367df 100644 --- a/client/src/parsers/parsehrt.c +++ b/client/src/parsers/parsehrt.c @@ -154,19 +154,26 @@ static bool hrt_read_and_parse_application_info(DesfireContext_t *dctx, hrt_trav return application_id != NULL && application_id[0] != '\0'; } -static void hrt_print_time(const char *label, time_t value) { - if (value == (time_t)0) return; +static bool hrt_format_time(time_t value, char *out, size_t out_len) { + if (value == (time_t)0 || out == NULL || out_len == 0) return false; char buf[32] = {0}; struct tm tm_value = {0}; #if defined(_WIN32) - if (localtime_s(&tm_value, &value) != 0) return; + if (localtime_s(&tm_value, &value) != 0) return false; #else - if (localtime_r(&value, &tm_value) == NULL) return; + if (localtime_r(&value, &tm_value) == NULL) return false; #endif - strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_value); + if (strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_value) == 0) return false; + return snprintf(out, out_len, "%s", buf) > 0; +} + +static void hrt_print_time(const char *label, time_t value) { + char buf[32] = {0}; + if (hrt_format_time(value, buf, sizeof(buf)) == false) return; + PrintAndLogEx(SUCCESS, "%s " _GREEN_("%s"), label, buf); } @@ -181,6 +188,85 @@ static bool hrt_has_loaded_period_info(const hrt_travel_card_t *card) { hrt_travelcard_get_period_loading_device_number(card) > 0; } +static void hrt_print_boarding_location(int location_type, int location_num) { + if (location_num <= 0) return; + + if (location_type == 1) { + PrintAndLogEx(SUCCESS, " Boarding line.......... " _GREEN_("%d"), location_num); + } else if (location_type == 2) { + PrintAndLogEx(SUCCESS, " Boarding train......... " _GREEN_("%d"), location_num); + } +} + +static bool hrt_format_value_ticket_zone(const hrt_eticket_t *ticket, char *out, size_t out_len) { + if (ticket == NULL || out == NULL || out_len == 0) return false; + + if (hrt_eticket_get_extra_zone(ticket) == 1 && hrt_eticket_get_group_size(ticket) <= 1) { + char ext1[32] = {0}; + char ext2[32] = {0}; + bool has_ext1 = false; + bool has_ext2 = false; + int ext1_area = hrt_eticket_get_ext1_validity_area(ticket); + int ext2_area = hrt_eticket_get_ext2_validity_area(ticket); + + if (ext1_area != 56) { + has_ext1 = hrt_format_area(HRT_AREA_TYPE_ZONE_RANGE, ext1_area, ext1, sizeof(ext1)); + } + if (ext2_area != 56) { + has_ext2 = hrt_format_area(HRT_AREA_TYPE_ZONE_RANGE, ext2_area, ext2, sizeof(ext2)); + } + + if (has_ext1 && has_ext2) { + return snprintf(out, out_len, "%s+%s", ext1, ext2) > 0; + } + if (has_ext1) return snprintf(out, out_len, "%s", ext1) > 0; + if (has_ext2) return snprintf(out, out_len, "%s", ext2) > 0; + } + + return hrt_format_area(hrt_eticket_get_validity_area_type(ticket), + hrt_eticket_get_validity_area(ticket), out, out_len); +} + +static const char *hrt_history_transaction_type_name(int transaction_type) { + return transaction_type == 0 ? "Season ticket" : "Value ticket"; +} + +static void hrt_print_history(const hrt_travel_card_t *card) { + if (card == NULL) return; + + const hrt_history_t *history = hrt_travelcard_get_history(card); + int history_len = hrt_travelcard_get_history_len(card); + if (history == NULL || history_len <= 0) return; + + PrintAndLogEx(SUCCESS, ""); + PrintAndLogEx(SUCCESS, "History records"); + + for (int i = history_len - 1; i >= 0; i--) { + char transaction_time[32] = {0}; + char price_buf[32] = {0}; + + if (hrt_format_time(history[i].transaction_d_time, transaction_time, sizeof(transaction_time)) == false) { + continue; + } + + PrintAndLogEx(SUCCESS, " %s " _GREEN_("%s"), transaction_time, + hrt_history_transaction_type_name(history[i].transaction_type)); + + if (history[i].transaction_type != 0 && + (history[i].group_size > 1 || history[i].price > 0)) { + if (history[i].group_size > 1) { + PrintAndLogEx(SUCCESS, " Group size.......... " _GREEN_("%d"), history[i].group_size); + } + if (history[i].price > 0) { + hrt_price_to_string(history[i].price, price_buf, sizeof(price_buf)); + PrintAndLogEx(SUCCESS, " Price............... " _GREEN_("%s"), price_buf); + } + } + + hrt_print_time(" Transfer ends.......", history[i].transfer_end_date); + } +} + static void hrt_print_card(const hrt_travel_card_t *card) { if (card == NULL) return; @@ -191,6 +277,7 @@ static void hrt_print_card(const hrt_travel_card_t *card) { const char *product_name = hrt_lookup_product_name(product_code); int area_type = hrt_travelcard_get_validity_area_type1(card); int area = hrt_travelcard_get_validity_area1(card); + char boarding_area_buf[64] = {0}; const hrt_eticket_t *value_ticket = hrt_travelcard_get_value_ticket(card); hrt_price_to_string( @@ -229,6 +316,15 @@ static void hrt_print_card(const hrt_travel_card_t *card) { hrt_print_time(" Start date.............", hrt_travelcard_get_period_start_date1(card)); hrt_print_time(" End date...............", hrt_travelcard_get_period_end_date1(card)); PrintAndLogEx(SUCCESS, " Length................. " _GREEN_("%d days"), hrt_travelcard_get_period_length1(card)); + hrt_print_time(" Boarding time..........", hrt_travelcard_get_boarding_date(card)); + hrt_print_boarding_location(hrt_travelcard_get_boarding_location_num_type(card), + hrt_travelcard_get_boarding_location_num(card)); + + if (hrt_format_area(hrt_travelcard_get_boarding_area_type(card), + hrt_travelcard_get_boarding_area(card), + boarding_area_buf, sizeof(boarding_area_buf))) { + PrintAndLogEx(SUCCESS, " Boarding zone.......... " _GREEN_("%s"), boarding_area_buf); + } if (hrt_has_loaded_period_info(card)) { PrintAndLogEx(SUCCESS, " Organization........... " _GREEN_("%d"), hrt_travelcard_get_period_loading_organization(card)); @@ -240,8 +336,6 @@ static void hrt_print_card(const hrt_travel_card_t *card) { char value_area_buf[64] = {0}; int value_product_code = hrt_eticket_get_product_code(value_ticket); const char *value_product_name = hrt_lookup_product_name(value_product_code); - int value_area_type = hrt_eticket_get_validity_area_type(value_ticket); - int value_area = hrt_eticket_get_validity_area(value_ticket); hrt_price_to_string(hrt_eticket_get_total_fare(value_ticket), fare_buf, sizeof(fare_buf)); @@ -254,10 +348,10 @@ static void hrt_print_card(const hrt_travel_card_t *card) { PrintAndLogEx(SUCCESS, " Product code........... " _GREEN_("%d (unknown)"), value_product_code); } - if (hrt_format_area(value_area_type, value_area, value_area_buf, sizeof(value_area_buf))) { - PrintAndLogEx(SUCCESS, " Area................... " _GREEN_("%s"), value_area_buf); + if (hrt_format_value_ticket_zone(value_ticket, value_area_buf, sizeof(value_area_buf))) { + PrintAndLogEx(SUCCESS, " Zone................... " _GREEN_("%s"), value_area_buf); } else { - PrintAndLogEx(SUCCESS, " Area................... " _GREEN_("%d (type %d)"), value_area, value_area_type); + PrintAndLogEx(SUCCESS, " Zone................... " _GREEN_("unknown")); } PrintAndLogEx(SUCCESS, " Fare................... " _GREEN_("%s"), fare_buf); @@ -266,7 +360,11 @@ static void hrt_print_card(const hrt_travel_card_t *card) { hrt_print_time(" Valid until............", hrt_eticket_get_validity_end_date(value_ticket)); hrt_print_time(" Boarding time..........", hrt_eticket_get_boarding_date(value_ticket)); PrintAndLogEx(SUCCESS, " Boarding vehicle....... " _GREEN_("%d"), hrt_eticket_get_boarding_vehicle(value_ticket)); + hrt_print_boarding_location(hrt_eticket_get_boarding_location_num_type(value_ticket), + hrt_eticket_get_boarding_location_num(value_ticket)); } + + hrt_print_history(card); } bool is_valid_hrt_card(DesfireContext_t *dctx, const uint8_t *aidbuf, size_t aidbuflen) {