gocard.c 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. #include <flipper_application.h>
  2. #include "../../metroflip_i.h"
  3. #include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
  4. #include <nfc/protocols/mf_classic/mf_classic.h>
  5. #include <nfc/protocols/mf_classic/mf_classic_poller.h>
  6. #include <dolphin/dolphin.h>
  7. #include <bit_lib.h>
  8. #include <furi_hal.h>
  9. #include <nfc/nfc.h>
  10. #include <nfc/nfc_device.h>
  11. #include <nfc/nfc_listener.h>
  12. #include "../../api/metroflip/metroflip_api.h"
  13. #include "../../metroflip_plugins.h"
  14. #include <stdio.h>
  15. #include <stdlib.h>
  16. #include <stdint.h>
  17. #define TAG "Metroflip:Scene:gocard"
  18. typedef enum {
  19. CHILD = 2051, // 0x803
  20. ADULT = 3073 // 0xc01
  21. } ConcessionType;
  22. bool hasTravelPassAvailable = false;
  23. // Function to print concession type
  24. void printConcessionType(unsigned short concession_type, FuriString* parsed_data) {
  25. switch(concession_type) {
  26. case CHILD:
  27. furi_string_cat_printf(parsed_data, "Concession Type: Child\n");
  28. break;
  29. case ADULT:
  30. furi_string_cat_printf(parsed_data, "Concession Type: Adult\n");
  31. break;
  32. default:
  33. furi_string_cat_printf(parsed_data, "Concession Type: 0x%X\n", concession_type);
  34. break;
  35. }
  36. }
  37. unsigned short byteArrayToIntReversed(unsigned int dec1, unsigned int dec2) {
  38. unsigned char byte1 = (unsigned char)dec1;
  39. unsigned char byte2 = (unsigned char)dec2;
  40. return ((unsigned short)byte2 << 8) | byte1;
  41. }
  42. // Function to extract a substring and convert binary to decimal
  43. uint32_t extract_and_convert(const char* str, int start, int length) {
  44. uint32_t value = 0;
  45. for(int i = 0; i < length; i++) {
  46. if(str[start + i] == '1') {
  47. value |= (1U << (length - 1 - i));
  48. }
  49. }
  50. return value;
  51. }
  52. void parse_gocard_time(int block, int offset, const MfClassicData* data, FuriString* parsed_data) {
  53. //byte to start at
  54. int num_bytes = 4;
  55. char gocard_date_bit_representation[num_bytes * 8 + 1];
  56. memset(gocard_date_bit_representation, 0, sizeof(gocard_date_bit_representation));
  57. for(int i = (offset + num_bytes - 1), j = 0; i >= offset;
  58. i--, j++) { // Reverse the order of bytes and converty to binary
  59. char bits[9];
  60. byte_to_binary(data->block[block].data[i], bits);
  61. memcpy(&gocard_date_bit_representation[j * 8], bits, 8);
  62. }
  63. gocard_date_bit_representation[num_bytes * 8] = '\0';
  64. int len = strlen(gocard_date_bit_representation);
  65. FURI_LOG_I(TAG, "len %d", len); // I get 34
  66. if(len != 32 && len != 33) {
  67. FURI_LOG_I(TAG, "Invalid input length");
  68. return;
  69. }
  70. // Field layout (from rightmost bit):
  71. // - Day: 5 bits
  72. // - Month: 4 bits
  73. // - Year: 6 bits (years since 2000)
  74. // - Minutes: 11 bits (minutes from midnight)
  75. // Extract values from right to left using bit_slice_to_dec
  76. uint32_t day = bit_slice_to_dec(gocard_date_bit_representation, len - 5, len);
  77. uint32_t month = bit_slice_to_dec(gocard_date_bit_representation, len - 9, len - 6);
  78. uint32_t year = bit_slice_to_dec(gocard_date_bit_representation, len - 15, len - 10);
  79. uint32_t minutes = bit_slice_to_dec(gocard_date_bit_representation, len - 26, len - 16);
  80. // Convert year from offset 2000
  81. year += 2000;
  82. // Convert minutes since midnight to HH:MM
  83. uint32_t hours = minutes / 60;
  84. uint32_t mins = minutes % 60;
  85. // Format output string: "YYYY-MM-DD HH:MM"
  86. furi_string_cat_printf(
  87. parsed_data, "%04lu-%02lu-%02lu %02lu:%02lu\n", year, month, day, hours, mins);
  88. }
  89. void parse_gocard_topup_info(FuriString* parsed_data, const MfClassicData* data) {
  90. furi_string_cat_printf(parsed_data, "\n\e#Top-Up Info:");
  91. bool fully_empty = true;
  92. int block_num = 8;
  93. for(int i = block_num; i < block_num + 3; i++) {
  94. /******* Check if it's empty ******/
  95. bool is_block_empty = true;
  96. for(int j = 2; j < 8; j++) {
  97. if(data->block[i].data[j] != 0) {
  98. FURI_LOG_I(TAG, "Not 0, proceeding");
  99. is_block_empty = false;
  100. break;
  101. }
  102. }
  103. if(is_block_empty) {
  104. FURI_LOG_I(TAG, "Block %d is empty", i);
  105. continue;
  106. } else {
  107. fully_empty = false;
  108. }
  109. /**** If not fully empty, proceed ******/
  110. unsigned short creditcents =
  111. byteArrayToIntReversed(data->block[i].data[6], data->block[i].data[7]);
  112. creditcents &= 0x7FFF;
  113. double credit = creditcents / 100.0;
  114. furi_string_cat_printf(parsed_data, "\nCredit Added: A$%.2f\nTime: ", credit);
  115. parse_gocard_time(i, 2, data, parsed_data);
  116. }
  117. if(fully_empty) {
  118. FURI_LOG_I(TAG, "All checked blocks are empty, returning.");
  119. }
  120. }
  121. static bool gocard_parse(FuriString* parsed_data, const MfClassicData* data) {
  122. bool parsed = false;
  123. do {
  124. int balance_slot = 4;
  125. if(data->block[balance_slot].data[13] <= data->block[balance_slot + 1].data[13])
  126. balance_slot++;
  127. unsigned short balancecents = byteArrayToIntReversed(
  128. data->block[balance_slot].data[2], data->block[balance_slot].data[3]);
  129. // Check if the sign flag is set in 'balance'
  130. if((balancecents & 0x8000) == 0x8000) {
  131. balancecents = balancecents & 0x7fff; // Clear the sign flag.
  132. balancecents *= -1; // Negate the balance.
  133. }
  134. // Otherwise, check the sign flag in data->block[4].data[1]
  135. else if((data->block[balance_slot].data[1] & 0x80) == 0x80) {
  136. // seq_go uses a sign flag in an adjacent byte.
  137. balancecents *= -1;
  138. }
  139. double balance = balancecents / 100.0;
  140. furi_string_printf(parsed_data, "\e#go card\nValue: A$%.2f\n", balance); //show balance
  141. hasTravelPassAvailable = (data->block[balance_slot].data[7] != 0x00) ? true : false;
  142. int start_index = 4; //byte to start at
  143. int config_block = 6; //block number containing card configuration
  144. furi_string_cat_printf(parsed_data, "Expiry:\n");
  145. parse_gocard_time(config_block, start_index, data, parsed_data);
  146. //concession type:
  147. unsigned short concession_type = byteArrayToIntReversed(
  148. data->block[config_block].data[8], data->block[config_block].data[9]);
  149. printConcessionType(concession_type, parsed_data);
  150. parse_gocard_topup_info(parsed_data, data);
  151. parsed = true;
  152. } while(false);
  153. return parsed;
  154. }
  155. static void gocard_on_enter(Metroflip* app) {
  156. dolphin_deed(DolphinDeedNfcRead);
  157. app->sec_num = 0;
  158. if(app->data_loaded) {
  159. Storage* storage = furi_record_open(RECORD_STORAGE);
  160. FlipperFormat* ff = flipper_format_file_alloc(storage);
  161. if(flipper_format_file_open_existing(ff, app->file_path)) {
  162. MfClassicData* mfc_data = mf_classic_alloc();
  163. mf_classic_load(mfc_data, ff, 2);
  164. FuriString* parsed_data = furi_string_alloc();
  165. Widget* widget = app->widget;
  166. furi_string_reset(app->text_box_store);
  167. if(!gocard_parse(parsed_data, mfc_data)) {
  168. furi_string_reset(app->text_box_store);
  169. FURI_LOG_I(TAG, "Unknown card type");
  170. furi_string_printf(parsed_data, "\e#Unknown card\n");
  171. }
  172. widget_add_text_scroll_element(
  173. widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
  174. widget_add_button_element(
  175. widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
  176. widget_add_button_element(
  177. widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
  178. mf_classic_free(mfc_data);
  179. furi_string_free(parsed_data);
  180. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
  181. }
  182. flipper_format_free(ff);
  183. } else {
  184. // Setup view
  185. Popup* popup = app->popup;
  186. popup_set_header(popup, "unsupported", 68, 30, AlignLeft, AlignTop);
  187. popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
  188. }
  189. }
  190. static bool gocard_on_event(Metroflip* app, SceneManagerEvent event) {
  191. bool consumed = false;
  192. if(event.type == SceneManagerEventTypeCustom) {
  193. if(event.event == MetroflipCustomEventCardDetected) {
  194. Popup* popup = app->popup;
  195. popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop);
  196. consumed = true;
  197. } else if(event.event == MetroflipCustomEventCardLost) {
  198. Popup* popup = app->popup;
  199. popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop);
  200. consumed = true;
  201. } else if(event.event == MetroflipCustomEventWrongCard) {
  202. Popup* popup = app->popup;
  203. popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop);
  204. consumed = true;
  205. } else if(event.event == MetroflipCustomEventPollerFail) {
  206. Popup* popup = app->popup;
  207. popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop);
  208. consumed = true;
  209. }
  210. } else if(event.type == SceneManagerEventTypeBack) {
  211. scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
  212. scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
  213. consumed = true;
  214. }
  215. return consumed;
  216. }
  217. static void gocard_on_exit(Metroflip* app) {
  218. widget_reset(app->widget);
  219. if(app->poller && !app->data_loaded) {
  220. nfc_poller_stop(app->poller);
  221. nfc_poller_free(app->poller);
  222. }
  223. // Clear view
  224. popup_reset(app->popup);
  225. metroflip_app_blink_stop(app);
  226. }
  227. /* Actual implementation of app<>plugin interface */
  228. static const MetroflipPlugin gocard_plugin = {
  229. .card_name = "gocard",
  230. .plugin_on_enter = gocard_on_enter,
  231. .plugin_on_event = gocard_on_event,
  232. .plugin_on_exit = gocard_on_exit,
  233. };
  234. /* Plugin descriptor to comply with basic plugin specification */
  235. static const FlipperAppPluginDescriptor gocard_plugin_descriptor = {
  236. .appid = METROFLIP_SUPPORTED_CARD_PLUGIN_APP_ID,
  237. .ep_api_version = METROFLIP_SUPPORTED_CARD_PLUGIN_API_VERSION,
  238. .entry_point = &gocard_plugin,
  239. };
  240. /* Plugin entry point - must return a pointer to const descriptor */
  241. const FlipperAppPluginDescriptor* gocard_plugin_ep(void) {
  242. return &gocard_plugin_descriptor;
  243. }