Implement support for reading Opal card (Sydney, Australia) (#2683)
* Implement support for reading Opal card (Sydney, Australia) * stub_parser_verify_read: used UNUSED macro * furi_hal_rtc: expose calendaring as functions * opal: use bit-packed struct to parse, rather than manually shifting about * Update f18 api symbols Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
parent
66961dab06
commit
363f555ed7
@ -52,6 +52,7 @@ void nfc_scene_device_info_on_enter(void* context) {
|
||||
}
|
||||
} else if(
|
||||
dev_data->protocol == NfcDeviceProtocolMifareClassic ||
|
||||
dev_data->protocol == NfcDeviceProtocolMifareDesfire ||
|
||||
dev_data->protocol == NfcDeviceProtocolMifareUl) {
|
||||
furi_string_set(temp_str, nfc->dev->dev_data.parsed_data);
|
||||
}
|
||||
|
||||
@ -20,35 +20,40 @@ void nfc_scene_mf_desfire_read_success_on_enter(void* context) {
|
||||
Widget* widget = nfc->widget;
|
||||
|
||||
// Prepare string for data display
|
||||
FuriString* temp_str = furi_string_alloc_printf("\e#MIFARE DESfire\n");
|
||||
furi_string_cat_printf(temp_str, "UID:");
|
||||
for(size_t i = 0; i < nfc_data->uid_len; i++) {
|
||||
furi_string_cat_printf(temp_str, " %02X", nfc_data->uid[i]);
|
||||
}
|
||||
|
||||
uint32_t bytes_total = 1UL << (data->version.sw_storage >> 1);
|
||||
uint32_t bytes_free = data->free_memory ? data->free_memory->bytes : 0;
|
||||
furi_string_cat_printf(temp_str, "\n%lu", bytes_total);
|
||||
if(data->version.sw_storage & 1) {
|
||||
furi_string_push_back(temp_str, '+');
|
||||
}
|
||||
furi_string_cat_printf(temp_str, " bytes, %lu bytes free\n", bytes_free);
|
||||
|
||||
uint16_t n_apps = 0;
|
||||
uint16_t n_files = 0;
|
||||
for(MifareDesfireApplication* app = data->app_head; app; app = app->next) {
|
||||
n_apps++;
|
||||
for(MifareDesfireFile* file = app->file_head; file; file = file->next) {
|
||||
n_files++;
|
||||
FuriString* temp_str = NULL;
|
||||
if(furi_string_size(nfc->dev->dev_data.parsed_data)) {
|
||||
temp_str = furi_string_alloc_set(nfc->dev->dev_data.parsed_data);
|
||||
} else {
|
||||
temp_str = furi_string_alloc_printf("\e#MIFARE DESFire\n");
|
||||
furi_string_cat_printf(temp_str, "UID:");
|
||||
for(size_t i = 0; i < nfc_data->uid_len; i++) {
|
||||
furi_string_cat_printf(temp_str, " %02X", nfc_data->uid[i]);
|
||||
}
|
||||
|
||||
uint32_t bytes_total = 1UL << (data->version.sw_storage >> 1);
|
||||
uint32_t bytes_free = data->free_memory ? data->free_memory->bytes : 0;
|
||||
furi_string_cat_printf(temp_str, "\n%lu", bytes_total);
|
||||
if(data->version.sw_storage & 1) {
|
||||
furi_string_push_back(temp_str, '+');
|
||||
}
|
||||
furi_string_cat_printf(temp_str, " bytes, %lu bytes free\n", bytes_free);
|
||||
|
||||
uint16_t n_apps = 0;
|
||||
uint16_t n_files = 0;
|
||||
for(MifareDesfireApplication* app = data->app_head; app; app = app->next) {
|
||||
n_apps++;
|
||||
for(MifareDesfireFile* file = app->file_head; file; file = file->next) {
|
||||
n_files++;
|
||||
}
|
||||
}
|
||||
furi_string_cat_printf(temp_str, "%d Application", n_apps);
|
||||
if(n_apps != 1) {
|
||||
furi_string_push_back(temp_str, 's');
|
||||
}
|
||||
furi_string_cat_printf(temp_str, ", %d file", n_files);
|
||||
if(n_files != 1) {
|
||||
furi_string_push_back(temp_str, 's');
|
||||
}
|
||||
}
|
||||
furi_string_cat_printf(temp_str, "%d Application", n_apps);
|
||||
if(n_apps != 1) {
|
||||
furi_string_push_back(temp_str, 's');
|
||||
}
|
||||
furi_string_cat_printf(temp_str, ", %d file", n_files);
|
||||
if(n_files != 1) {
|
||||
furi_string_push_back(temp_str, 's');
|
||||
}
|
||||
|
||||
notification_message_block(nfc->notifications, &sequence_set_green_255);
|
||||
|
||||
@ -40,7 +40,7 @@ void nfc_scene_nfc_data_info_on_enter(void* context) {
|
||||
furi_string_cat_printf(
|
||||
temp_str, "\e#%s\n", nfc_mf_classic_type(dev_data->mf_classic_data.type));
|
||||
} else if(protocol == NfcDeviceProtocolMifareDesfire) {
|
||||
furi_string_cat_printf(temp_str, "\e#MIFARE DESfire\n");
|
||||
furi_string_cat_printf(temp_str, "\e#MIFARE DESFire\n");
|
||||
} else {
|
||||
furi_string_cat_printf(temp_str, "\e#Unknown ISO tag\n");
|
||||
}
|
||||
|
||||
@ -148,6 +148,7 @@ bool nfc_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
application_info_present = true;
|
||||
} else if(
|
||||
dev_data->protocol == NfcDeviceProtocolMifareClassic ||
|
||||
dev_data->protocol == NfcDeviceProtocolMifareDesfire ||
|
||||
dev_data->protocol == NfcDeviceProtocolMifareUl) {
|
||||
application_info_present = nfc_supported_card_verify_and_parse(dev_data);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
entry,status,name,type,params
|
||||
Version,+,27.0,,
|
||||
Version,+,27.1,,
|
||||
Header,+,applications/services/bt/bt_service/bt.h,,
|
||||
Header,+,applications/services/cli/cli.h,,
|
||||
Header,+,applications/services/cli/cli_vcp.h,,
|
||||
@ -1048,6 +1048,8 @@ Function,+,furi_hal_rtc_datetime_to_timestamp,uint32_t,FuriHalRtcDateTime*
|
||||
Function,-,furi_hal_rtc_deinit_early,void,
|
||||
Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode,
|
||||
Function,+,furi_hal_rtc_get_datetime,void,FuriHalRtcDateTime*
|
||||
Function,+,furi_hal_rtc_get_days_per_month,uint8_t,"_Bool, uint8_t"
|
||||
Function,+,furi_hal_rtc_get_days_per_year,uint16_t,uint16_t
|
||||
Function,+,furi_hal_rtc_get_fault_data,uint32_t,
|
||||
Function,+,furi_hal_rtc_get_heap_track_mode,FuriHalRtcHeapTrackMode,
|
||||
Function,+,furi_hal_rtc_get_locale_dateformat,FuriHalRtcLocaleDateFormat,
|
||||
@ -1060,6 +1062,7 @@ Function,+,furi_hal_rtc_get_timestamp,uint32_t,
|
||||
Function,-,furi_hal_rtc_init,void,
|
||||
Function,-,furi_hal_rtc_init_early,void,
|
||||
Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag
|
||||
Function,+,furi_hal_rtc_is_leap_year,_Bool,uint16_t
|
||||
Function,+,furi_hal_rtc_reset_flag,void,FuriHalRtcFlag
|
||||
Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode
|
||||
Function,+,furi_hal_rtc_set_datetime,void,FuriHalRtcDateTime*
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
entry,status,name,type,params
|
||||
Version,+,27.0,,
|
||||
Version,+,27.1,,
|
||||
Header,+,applications/services/bt/bt_service/bt.h,,
|
||||
Header,+,applications/services/cli/cli.h,,
|
||||
Header,+,applications/services/cli/cli_vcp.h,,
|
||||
@ -1313,6 +1313,8 @@ Function,+,furi_hal_rtc_datetime_to_timestamp,uint32_t,FuriHalRtcDateTime*
|
||||
Function,-,furi_hal_rtc_deinit_early,void,
|
||||
Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode,
|
||||
Function,+,furi_hal_rtc_get_datetime,void,FuriHalRtcDateTime*
|
||||
Function,+,furi_hal_rtc_get_days_per_month,uint8_t,"_Bool, uint8_t"
|
||||
Function,+,furi_hal_rtc_get_days_per_year,uint16_t,uint16_t
|
||||
Function,+,furi_hal_rtc_get_fault_data,uint32_t,
|
||||
Function,+,furi_hal_rtc_get_heap_track_mode,FuriHalRtcHeapTrackMode,
|
||||
Function,+,furi_hal_rtc_get_locale_dateformat,FuriHalRtcLocaleDateFormat,
|
||||
@ -1325,6 +1327,7 @@ Function,+,furi_hal_rtc_get_timestamp,uint32_t,
|
||||
Function,-,furi_hal_rtc_init,void,
|
||||
Function,-,furi_hal_rtc_init_early,void,
|
||||
Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag
|
||||
Function,+,furi_hal_rtc_is_leap_year,_Bool,uint16_t
|
||||
Function,+,furi_hal_rtc_reset_flag,void,FuriHalRtcFlag
|
||||
Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode
|
||||
Function,+,furi_hal_rtc_set_datetime,void,FuriHalRtcDateTime*
|
||||
@ -1991,6 +1994,8 @@ Function,-,mf_df_cat_key_settings,void,"MifareDesfireKeySettings*, FuriString*"
|
||||
Function,-,mf_df_cat_version,void,"MifareDesfireVersion*, FuriString*"
|
||||
Function,-,mf_df_check_card_type,_Bool,"uint8_t, uint8_t, uint8_t"
|
||||
Function,-,mf_df_clear,void,MifareDesfireData*
|
||||
Function,-,mf_df_get_application,MifareDesfireApplication*,"MifareDesfireData*, const uint8_t[3]*"
|
||||
Function,-,mf_df_get_file,MifareDesfireFile*,"MifareDesfireApplication*, uint8_t"
|
||||
Function,-,mf_df_parse_get_application_ids_response,_Bool,"uint8_t*, uint16_t, MifareDesfireApplication**"
|
||||
Function,-,mf_df_parse_get_file_ids_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile**"
|
||||
Function,-,mf_df_parse_get_file_settings_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile*"
|
||||
|
||||
|
@ -44,10 +44,8 @@ _Static_assert(sizeof(SystemReg) == 4, "SystemReg size mismatch");
|
||||
#define FURI_HAL_RTC_SECONDS_PER_DAY (FURI_HAL_RTC_SECONDS_PER_HOUR * 24)
|
||||
#define FURI_HAL_RTC_MONTHS_COUNT 12
|
||||
#define FURI_HAL_RTC_EPOCH_START_YEAR 1970
|
||||
#define FURI_HAL_RTC_IS_LEAP_YEAR(year) \
|
||||
((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0))
|
||||
|
||||
static const uint8_t furi_hal_rtc_days_per_month[][FURI_HAL_RTC_MONTHS_COUNT] = {
|
||||
static const uint8_t furi_hal_rtc_days_per_month[2][FURI_HAL_RTC_MONTHS_COUNT] = {
|
||||
{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
|
||||
{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
|
||||
|
||||
@ -395,7 +393,7 @@ uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime) {
|
||||
uint8_t leap_years = 0;
|
||||
|
||||
for(uint16_t y = FURI_HAL_RTC_EPOCH_START_YEAR; y < datetime->year; y++) {
|
||||
if(FURI_HAL_RTC_IS_LEAP_YEAR(y)) {
|
||||
if(furi_hal_rtc_is_leap_year(y)) {
|
||||
leap_years++;
|
||||
} else {
|
||||
years++;
|
||||
@ -406,10 +404,10 @@ uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime) {
|
||||
((years * furi_hal_rtc_days_per_year[0]) + (leap_years * furi_hal_rtc_days_per_year[1])) *
|
||||
FURI_HAL_RTC_SECONDS_PER_DAY;
|
||||
|
||||
uint8_t year_index = (FURI_HAL_RTC_IS_LEAP_YEAR(datetime->year)) ? 1 : 0;
|
||||
bool leap_year = furi_hal_rtc_is_leap_year(datetime->year);
|
||||
|
||||
for(uint8_t m = 0; m < (datetime->month - 1); m++) {
|
||||
timestamp += furi_hal_rtc_days_per_month[year_index][m] * FURI_HAL_RTC_SECONDS_PER_DAY;
|
||||
for(uint8_t m = 1; m < datetime->month; m++) {
|
||||
timestamp += furi_hal_rtc_get_days_per_month(leap_year, m) * FURI_HAL_RTC_SECONDS_PER_DAY;
|
||||
}
|
||||
|
||||
timestamp += (datetime->day - 1) * FURI_HAL_RTC_SECONDS_PER_DAY;
|
||||
@ -419,3 +417,15 @@ uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime) {
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
uint16_t furi_hal_rtc_get_days_per_year(uint16_t year) {
|
||||
return furi_hal_rtc_days_per_year[furi_hal_rtc_is_leap_year(year) ? 1 : 0];
|
||||
}
|
||||
|
||||
bool furi_hal_rtc_is_leap_year(uint16_t year) {
|
||||
return (((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0);
|
||||
}
|
||||
|
||||
uint8_t furi_hal_rtc_get_days_per_month(bool leap_year, uint8_t month) {
|
||||
return furi_hal_rtc_days_per_month[leap_year ? 1 : 0][month - 1];
|
||||
}
|
||||
|
||||
@ -255,6 +255,30 @@ uint32_t furi_hal_rtc_get_timestamp();
|
||||
*/
|
||||
uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime);
|
||||
|
||||
/** Gets the number of days in the year according to the Gregorian calendar.
|
||||
*
|
||||
* @param year Input year.
|
||||
*
|
||||
* @return number of days in `year`.
|
||||
*/
|
||||
uint16_t furi_hal_rtc_get_days_per_year(uint16_t year);
|
||||
|
||||
/** Check if a year a leap year in the Gregorian calendar.
|
||||
*
|
||||
* @param year Input year.
|
||||
*
|
||||
* @return true if `year` is a leap year.
|
||||
*/
|
||||
bool furi_hal_rtc_is_leap_year(uint16_t year);
|
||||
|
||||
/** Get the number of days in the month.
|
||||
*
|
||||
* @param leap_year true to calculate based on leap years
|
||||
* @param month month to check, where 1 = January
|
||||
* @return the number of days in the month
|
||||
*/
|
||||
uint8_t furi_hal_rtc_get_days_per_month(bool leap_year, uint8_t month);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -219,6 +219,19 @@ static bool nfc_worker_read_mf_desfire(NfcWorker* nfc_worker, FuriHalNfcTxRxCont
|
||||
do {
|
||||
if(!furi_hal_nfc_detect(&nfc_worker->dev_data->nfc_data, 300)) break;
|
||||
if(!mf_df_read_card(tx_rx, data)) break;
|
||||
FURI_LOG_I(TAG, "Trying to parse a supported card ...");
|
||||
|
||||
// The model for parsing DESFire is a little different to other cards;
|
||||
// we don't have parsers to provide encryption keys, so we can read the
|
||||
// data normally, and then pass the read data to a parser.
|
||||
//
|
||||
// There are fully-protected DESFire cards, but providing keys for them
|
||||
// is difficult (and unnessesary for many transit cards).
|
||||
for(size_t i = 0; i < NfcSupportedCardTypeEnd; i++) {
|
||||
if(nfc_supported_card[i].protocol == NfcDeviceProtocolMifareDesfire) {
|
||||
if(nfc_supported_card[i].parse(nfc_worker->dev_data)) break;
|
||||
}
|
||||
}
|
||||
read_success = true;
|
||||
} while(false);
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include "troika_4k_parser.h"
|
||||
#include "two_cities.h"
|
||||
#include "all_in_one.h"
|
||||
#include "opal.h"
|
||||
|
||||
NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd] = {
|
||||
[NfcSupportedCardTypePlantain] =
|
||||
@ -50,6 +51,14 @@ NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd] = {
|
||||
.read = all_in_one_parser_read,
|
||||
.parse = all_in_one_parser_parse,
|
||||
},
|
||||
[NfcSupportedCardTypeOpal] =
|
||||
{
|
||||
.protocol = NfcDeviceProtocolMifareDesfire,
|
||||
.verify = stub_parser_verify_read,
|
||||
.read = stub_parser_verify_read,
|
||||
.parse = opal_parser_parse,
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
bool nfc_supported_card_verify_and_parse(NfcDeviceData* dev_data) {
|
||||
@ -65,3 +74,9 @@ bool nfc_supported_card_verify_and_parse(NfcDeviceData* dev_data) {
|
||||
|
||||
return card_parsed;
|
||||
}
|
||||
|
||||
bool stub_parser_verify_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
|
||||
UNUSED(nfc_worker);
|
||||
UNUSED(tx_rx);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ typedef enum {
|
||||
NfcSupportedCardTypeTroika4K,
|
||||
NfcSupportedCardTypeTwoCities,
|
||||
NfcSupportedCardTypeAllInOne,
|
||||
NfcSupportedCardTypeOpal,
|
||||
|
||||
NfcSupportedCardTypeEnd,
|
||||
} NfcSupportedCardType;
|
||||
@ -31,3 +32,8 @@ typedef struct {
|
||||
extern NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd];
|
||||
|
||||
bool nfc_supported_card_verify_and_parse(NfcDeviceData* dev_data);
|
||||
|
||||
// stub_parser_verify_read does nothing, and always reports that it does not
|
||||
// support the card. This is needed for DESFire card parsers which can't
|
||||
// provide keys, and only use NfcSupportedCard->parse.
|
||||
bool stub_parser_verify_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
|
||||
|
||||
204
lib/nfc/parsers/opal.c
Normal file
204
lib/nfc/parsers/opal.c
Normal file
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* opal.c - Parser for Opal card (Sydney, Australia).
|
||||
*
|
||||
* Copyright 2023 Michael Farrell <micolous+git@gmail.com>
|
||||
*
|
||||
* This will only read "standard" MIFARE DESFire-based Opal cards. Free travel
|
||||
* cards (including School Opal cards, veteran, vision-impaired persons and
|
||||
* TfNSW employees' cards) and single-trip tickets are MIFARE Ultralight C
|
||||
* cards and not supported.
|
||||
*
|
||||
* Reference: https://github.com/metrodroid/metrodroid/wiki/Opal
|
||||
*
|
||||
* Note: The card values are all little-endian (like Flipper), but the above
|
||||
* reference was originally written based on Java APIs, which are big-endian.
|
||||
* This implementation presumes a little-endian system.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "nfc_supported_card.h"
|
||||
#include "opal.h"
|
||||
|
||||
#include <applications/services/locale/locale.h>
|
||||
#include <gui/modules/widget.h>
|
||||
#include <nfc_worker_i.h>
|
||||
|
||||
#include <furi_hal.h>
|
||||
|
||||
static const uint8_t opal_aid[3] = {0x31, 0x45, 0x53};
|
||||
static const char* opal_modes[5] =
|
||||
{"Rail / Metro", "Ferry / Light Rail", "Bus", "Unknown mode", "Manly Ferry"};
|
||||
static const char* opal_usages[14] = {
|
||||
"New / Unused",
|
||||
"Tap on: new journey",
|
||||
"Tap on: transfer from same mode",
|
||||
"Tap on: transfer from other mode",
|
||||
"", // Manly Ferry: new journey
|
||||
"", // Manly Ferry: transfer from ferry
|
||||
"", // Manly Ferry: transfer from other
|
||||
"Tap off: distance fare",
|
||||
"Tap off: flat fare",
|
||||
"Automated tap off: failed to tap off",
|
||||
"Tap off: end of trip without start",
|
||||
"Tap off: reversal",
|
||||
"Tap on: rejected",
|
||||
"Unknown usage",
|
||||
};
|
||||
|
||||
// Opal file 0x7 structure. Assumes a little-endian CPU.
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
uint32_t serial : 32;
|
||||
uint8_t check_digit : 4;
|
||||
bool blocked : 1;
|
||||
uint16_t txn_number : 16;
|
||||
int32_t balance : 21;
|
||||
uint16_t days : 15;
|
||||
uint16_t minutes : 11;
|
||||
uint8_t mode : 3;
|
||||
uint16_t usage : 4;
|
||||
bool auto_topup : 1;
|
||||
uint8_t weekly_journeys : 4;
|
||||
uint16_t checksum : 16;
|
||||
} OpalFile;
|
||||
|
||||
static_assert(sizeof(OpalFile) == 16);
|
||||
|
||||
// Converts an Opal timestamp to FuriHalRtcDateTime.
|
||||
//
|
||||
// Opal measures days since 1980-01-01 and minutes since midnight, and presumes
|
||||
// all days are 1440 minutes.
|
||||
void opal_date_time_to_furi(uint16_t days, uint16_t minutes, FuriHalRtcDateTime* out) {
|
||||
if(!out) return;
|
||||
uint16_t diy;
|
||||
out->year = 1980;
|
||||
out->month = 1;
|
||||
// 1980-01-01 is a Tuesday
|
||||
out->weekday = ((days + 1) % 7) + 1;
|
||||
out->hour = minutes / 60;
|
||||
out->minute = minutes % 60;
|
||||
out->second = 0;
|
||||
|
||||
// What year is it?
|
||||
for(;;) {
|
||||
diy = furi_hal_rtc_get_days_per_year(out->year);
|
||||
if(days < diy) break;
|
||||
days -= diy;
|
||||
out->year++;
|
||||
}
|
||||
|
||||
// 1-index the day of the year
|
||||
days++;
|
||||
// What month is it?
|
||||
bool is_leap = furi_hal_rtc_is_leap_year(out->year);
|
||||
|
||||
for(;;) {
|
||||
uint8_t dim = furi_hal_rtc_get_days_per_month(is_leap, out->month);
|
||||
if(days <= dim) break;
|
||||
days -= dim;
|
||||
out->month++;
|
||||
}
|
||||
|
||||
out->day = days;
|
||||
}
|
||||
|
||||
bool opal_parser_parse(NfcDeviceData* dev_data) {
|
||||
if(dev_data->protocol != NfcDeviceProtocolMifareDesfire) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MifareDesfireApplication* app = mf_df_get_application(&dev_data->mf_df_data, &opal_aid);
|
||||
if(app == NULL) {
|
||||
return false;
|
||||
}
|
||||
MifareDesfireFile* f = mf_df_get_file(app, 0x07);
|
||||
if(f == NULL || f->type != MifareDesfireFileTypeStandard || f->settings.data.size != 16 ||
|
||||
!f->contents) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OpalFile* o = (OpalFile*)f->contents;
|
||||
|
||||
uint8_t serial2 = o->serial / 10000000;
|
||||
uint16_t serial3 = (o->serial / 1000) % 10000;
|
||||
uint16_t serial4 = (o->serial % 1000);
|
||||
|
||||
if(o->check_digit > 9) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char* sign = "";
|
||||
if(o->balance < 0) {
|
||||
// Negative balance. Make this a positive value again and record the
|
||||
// sign separately, because then we can handle balances of -99..-1
|
||||
// cents, as the "dollars" division below would result in a positive
|
||||
// zero value.
|
||||
o->balance = abs(o->balance);
|
||||
sign = "-";
|
||||
}
|
||||
uint8_t cents = o->balance % 100;
|
||||
int32_t dollars = o->balance / 100;
|
||||
|
||||
FuriHalRtcDateTime timestamp;
|
||||
opal_date_time_to_furi(o->days, o->minutes, ×tamp);
|
||||
|
||||
if(o->mode >= 3) {
|
||||
// 3..7 are "reserved", but we use 4 to indicate the Manly Ferry.
|
||||
o->mode = 3;
|
||||
}
|
||||
|
||||
if(o->usage >= 4 && o->usage <= 6) {
|
||||
// Usages 4..6 associated with the Manly Ferry, which correspond to
|
||||
// usages 1..3 for other modes.
|
||||
o->usage -= 3;
|
||||
o->mode = 4;
|
||||
}
|
||||
|
||||
const char* mode_str = (o->mode <= 4 ? opal_modes[o->mode] : opal_modes[3]);
|
||||
const char* usage_str = (o->usage <= 12 ? opal_usages[o->usage] : opal_usages[13]);
|
||||
|
||||
furi_string_printf(
|
||||
dev_data->parsed_data,
|
||||
"\e#Opal: $%s%ld.%02hu\n3085 22%02hhu %04hu %03hu%01hhu\n%s, %s\n",
|
||||
sign,
|
||||
dollars,
|
||||
cents,
|
||||
serial2,
|
||||
serial3,
|
||||
serial4,
|
||||
o->check_digit,
|
||||
mode_str,
|
||||
usage_str);
|
||||
FuriString* timestamp_str = furi_string_alloc();
|
||||
locale_format_date(timestamp_str, ×tamp, locale_get_date_format(), "-");
|
||||
furi_string_cat(dev_data->parsed_data, timestamp_str);
|
||||
furi_string_cat_str(dev_data->parsed_data, " at ");
|
||||
|
||||
locale_format_time(timestamp_str, ×tamp, locale_get_time_format(), false);
|
||||
furi_string_cat(dev_data->parsed_data, timestamp_str);
|
||||
|
||||
furi_string_free(timestamp_str);
|
||||
furi_string_cat_printf(
|
||||
dev_data->parsed_data,
|
||||
"\nWeekly journeys: %hhu, Txn #%hu\n",
|
||||
o->weekly_journeys,
|
||||
o->txn_number);
|
||||
|
||||
if(o->auto_topup) {
|
||||
furi_string_cat_str(dev_data->parsed_data, "Auto-topup enabled\n");
|
||||
}
|
||||
if(o->blocked) {
|
||||
furi_string_cat_str(dev_data->parsed_data, "Card blocked\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
5
lib/nfc/parsers/opal.h
Normal file
5
lib/nfc/parsers/opal.h
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "nfc_supported_card.h"
|
||||
|
||||
bool opal_parser_parse(NfcDeviceData* dev_data);
|
||||
@ -42,6 +42,30 @@ void mf_df_clear(MifareDesfireData* data) {
|
||||
data->app_head = NULL;
|
||||
}
|
||||
|
||||
MifareDesfireApplication* mf_df_get_application(MifareDesfireData* data, const uint8_t (*aid)[3]) {
|
||||
if(!data) {
|
||||
return NULL;
|
||||
}
|
||||
for(MifareDesfireApplication* app = data->app_head; app; app = app->next) {
|
||||
if(memcmp(aid, app->id, 3) == 0) {
|
||||
return app;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
MifareDesfireFile* mf_df_get_file(MifareDesfireApplication* app, uint8_t id) {
|
||||
if(!app) {
|
||||
return NULL;
|
||||
}
|
||||
for(MifareDesfireFile* file = app->file_head; file; file = file->next) {
|
||||
if(file->id == id) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void mf_df_cat_data(MifareDesfireData* data, FuriString* out) {
|
||||
mf_df_cat_card_info(data, out);
|
||||
for(MifareDesfireApplication* app = data->app_head; app; app = app->next) {
|
||||
|
||||
@ -130,6 +130,9 @@ void mf_df_cat_file(MifareDesfireFile* file, FuriString* out);
|
||||
|
||||
bool mf_df_check_card_type(uint8_t ATQA0, uint8_t ATQA1, uint8_t SAK);
|
||||
|
||||
MifareDesfireApplication* mf_df_get_application(MifareDesfireData* data, const uint8_t (*aid)[3]);
|
||||
MifareDesfireFile* mf_df_get_file(MifareDesfireApplication* app, uint8_t id);
|
||||
|
||||
uint16_t mf_df_prepare_get_version(uint8_t* dest);
|
||||
bool mf_df_parse_get_version_response(uint8_t* buf, uint16_t len, MifareDesfireVersion* out);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user