suica.c 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. /*
  2. * Suica Scene
  3. *
  4. * This program is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. #include "../../api/suica/suica_drawings.h"
  18. #include "../../metroflip_plugins.h"
  19. #include <lib/nfc/protocols/felica/felica.h>
  20. #include <lib/nfc/protocols/felica/felica_poller.h>
  21. #include <lib/nfc/protocols/felica/felica_poller_i.h>
  22. #include <lib/nfc/helpers/felica_crc.h>
  23. #include <lib/bit_lib/bit_lib.h>
  24. #include <applications/services/locale/locale.h>
  25. #include <datetime.h>
  26. // Probably not needed after upstream include this in their suica_i.h
  27. #define TAG "Metroflip:Scene:Suica"
  28. const char* suica_service_names[] = {
  29. "Travel History",
  30. "Taps Log",
  31. };
  32. static void suica_model_initialize(SuicaHistoryViewModel* model, size_t initial_capacity) {
  33. model->travel_history =
  34. (uint8_t*)malloc(initial_capacity * FELICA_DATA_BLOCK_SIZE); // Each entry is 16 bytes
  35. model->size = 0;
  36. model->capacity = initial_capacity;
  37. model->entry = 1;
  38. model->page = 0;
  39. model->animator_tick = 0;
  40. model->history.entry_station.name = furi_string_alloc_set("Unknown");
  41. model->history.entry_station.jr_header = furi_string_alloc_set("0");
  42. model->history.exit_station.name = furi_string_alloc_set("Unknown");
  43. model->history.exit_station.jr_header = furi_string_alloc_set("0");
  44. model->history.entry_line = RailwaysList[SUICA_RAILWAY_NUM];
  45. model->history.exit_line = RailwaysList[SUICA_RAILWAY_NUM];
  46. }
  47. static void suica_model_initialize_after_load(SuicaHistoryViewModel* model) {
  48. model->entry = 1;
  49. model->page = 0;
  50. model->animator_tick = 0;
  51. model->history.entry_station.name = furi_string_alloc_set("Unknown");
  52. model->history.entry_station.jr_header = furi_string_alloc_set("0");
  53. model->history.exit_station.name = furi_string_alloc_set("Unknown");
  54. model->history.exit_station.jr_header = furi_string_alloc_set("0");
  55. model->history.entry_line = RailwaysList[SUICA_RAILWAY_NUM];
  56. model->history.exit_line = RailwaysList[SUICA_RAILWAY_NUM];
  57. }
  58. static void suica_add_entry(SuicaHistoryViewModel* model, const uint8_t* entry) {
  59. if(model->size <= 0) {
  60. suica_model_initialize(model, 3);
  61. }
  62. // Check if resizing is needed
  63. if(model->size == model->capacity) {
  64. size_t new_capacity = model->capacity * 2; // Double the capacity
  65. uint8_t* new_data =
  66. (uint8_t*)realloc(model->travel_history, new_capacity * FELICA_DATA_BLOCK_SIZE);
  67. model->travel_history = new_data;
  68. model->capacity = new_capacity;
  69. }
  70. // Copy the 16-byte entry to the next slot
  71. for(size_t i = 0; i < FELICA_DATA_BLOCK_SIZE; i++) {
  72. model->travel_history[(model->size * FELICA_DATA_BLOCK_SIZE) + i] = entry[i];
  73. }
  74. model->size++;
  75. }
  76. static void suica_parse_train_code(
  77. uint8_t line_code,
  78. uint8_t station_code,
  79. SuicaTrainRideType ride_type,
  80. SuicaHistoryViewModel* model) {
  81. Storage* storage = furi_record_open(RECORD_STORAGE);
  82. Stream* stream = file_stream_alloc(storage);
  83. FuriString* line = furi_string_alloc();
  84. FuriString* line_code_str = furi_string_alloc();
  85. FuriString* line_and_station_code_str = furi_string_alloc();
  86. furi_string_printf(line_code_str, "0x%02X", line_code);
  87. furi_string_printf(line_and_station_code_str, "0x%02X,0x%02X", line_code, station_code);
  88. FuriString* line_candidate = furi_string_alloc_set(SUICA_RAILWAY_UNKNOWN_NAME);
  89. FuriString* station_candidate = furi_string_alloc_set(SUICA_RAILWAY_UNKNOWN_NAME);
  90. FuriString* station_num_candidate = furi_string_alloc_set("0");
  91. FuriString* station_JR_header_candidate = furi_string_alloc_set("0");
  92. FuriString* line_copy = furi_string_alloc();
  93. size_t line_comma_ind = 0;
  94. size_t station_comma_ind = 0;
  95. size_t station_num_comma_ind = 0;
  96. size_t station_JR_header_comma_ind = 0;
  97. bool station_found = false;
  98. FuriString* file_name = furi_string_alloc();
  99. furi_string_printf(file_name, "%s0x%02X.txt", SUICA_STATION_LIST_PATH, line_code);
  100. if(file_stream_open(stream, furi_string_get_cstr(file_name), FSAM_READ, FSOM_OPEN_EXISTING)) {
  101. while(stream_read_line(stream, line) && !station_found) {
  102. // file is in csv format: station_group_id,station_id,station_sub_id,station_name
  103. // search for the station
  104. furi_string_replace_all(line, "\r", "");
  105. furi_string_replace_all(line, "\n", "");
  106. furi_string_set(line_copy, line); // 0xD5,0x02,Keikyu Main,Shinagawa,1,0
  107. if(furi_string_start_with(line, line_code_str)) {
  108. // set line name here
  109. furi_string_right(line_copy, 10); // Keikyu Main,Shinagawa,1,0
  110. furi_string_set(line_candidate, line_copy);
  111. line_comma_ind = furi_string_search_char(line_candidate, ',', 0);
  112. furi_string_left(line_candidate, line_comma_ind); // Keikyu Main
  113. // we cut the line and station code in the line line copy
  114. // and we leave only the line name for the line candidate
  115. if(furi_string_start_with(line, line_and_station_code_str)) {
  116. furi_string_set(station_candidate, line_copy); // Keikyu Main,Shinagawa,1,0
  117. furi_string_right(station_candidate, line_comma_ind + 1);
  118. station_comma_ind =
  119. furi_string_search_char(station_candidate, ',', 0); // Shinagawa,1,0
  120. furi_string_left(station_candidate, station_comma_ind); // Shinagawa
  121. station_found = true;
  122. break;
  123. }
  124. }
  125. }
  126. } else {
  127. FURI_LOG_E(TAG, "Failed to open stations.txt");
  128. }
  129. furi_string_set(station_num_candidate, line_copy); // Keikyu Main,Shinagawa,1,0
  130. furi_string_right(station_num_candidate, line_comma_ind + station_comma_ind + 2); // 1,0
  131. station_num_comma_ind = furi_string_search_char(station_num_candidate, ',', 0);
  132. furi_string_left(station_num_candidate, station_num_comma_ind); // 1
  133. furi_string_set(station_JR_header_candidate, line_copy); // Keikyu Main,Shinagawa,1,0
  134. furi_string_right(
  135. station_JR_header_candidate,
  136. line_comma_ind + station_comma_ind + station_num_comma_ind + 3); // 0
  137. station_JR_header_comma_ind = furi_string_search_char(station_JR_header_candidate, ',', 0);
  138. furi_string_left(station_JR_header_candidate, station_JR_header_comma_ind); // 0
  139. switch(ride_type) {
  140. case SuicaTrainRideEntry:
  141. for(size_t i = 0; i < SUICA_RAILWAY_NUM; i++) {
  142. if(furi_string_equal_str(line_candidate, RailwaysList[i].long_name)) {
  143. model->history.entry_line = RailwaysList[i];
  144. furi_string_set(model->history.entry_station.name, station_candidate);
  145. model->history.entry_station.station_number =
  146. atoi(furi_string_get_cstr(station_num_candidate));
  147. furi_string_set(
  148. model->history.entry_station.jr_header, station_JR_header_candidate);
  149. break;
  150. }
  151. }
  152. break;
  153. case SuicaTrainRideExit:
  154. for(size_t i = 0; i < SUICA_RAILWAY_NUM; i++) {
  155. if(furi_string_equal_str(line_candidate, RailwaysList[i].long_name)) {
  156. model->history.exit_line = RailwaysList[i];
  157. furi_string_set(model->history.exit_station.name, station_candidate);
  158. model->history.exit_station.station_number =
  159. atoi(furi_string_get_cstr(station_num_candidate));
  160. furi_string_set(
  161. model->history.exit_station.jr_header, station_JR_header_candidate);
  162. break;
  163. }
  164. }
  165. break;
  166. default:
  167. UNUSED(model);
  168. break;
  169. }
  170. furi_string_free(line);
  171. furi_string_free(line_copy);
  172. furi_string_free(line_code_str);
  173. furi_string_free(line_and_station_code_str);
  174. furi_string_free(line_candidate);
  175. furi_string_free(station_candidate);
  176. furi_string_free(station_num_candidate);
  177. furi_string_free(station_JR_header_candidate);
  178. file_stream_close(stream);
  179. stream_free(stream);
  180. furi_record_close(RECORD_STORAGE);
  181. }
  182. static void suica_parse(SuicaHistoryViewModel* my_model) {
  183. uint8_t current_block[FELICA_DATA_BLOCK_SIZE];
  184. // Parse the current block/entry
  185. for(size_t i = 0; i < FELICA_DATA_BLOCK_SIZE; i++) {
  186. current_block[i] = my_model->travel_history[((my_model->entry - 1) * 16) + i];
  187. }
  188. if(((uint8_t)current_block[4] + (uint8_t)current_block[5]) != 0) {
  189. my_model->history.year = ((uint8_t)current_block[4] & 0xFE) >> 1;
  190. my_model->history.month = (((uint8_t)current_block[4] & 0x01) << 3) |
  191. (((uint8_t)current_block[5] & 0xE0) >> 5);
  192. my_model->history.day = (uint8_t)current_block[5] & 0x1F;
  193. } else {
  194. my_model->history.year = 0;
  195. my_model->history.month = 0;
  196. my_model->history.day = 0;
  197. }
  198. my_model->history.balance = ((uint16_t)current_block[11] << 8) | (uint16_t)current_block[10];
  199. // FURI_LOG_I(TAG,"%02X", (uint8_t)current_block[0]);
  200. my_model->history.area_code = current_block[15];
  201. if((uint8_t)current_block[0] >= TERMINAL_TICKET_VENDING_MACHINE &&
  202. (uint8_t)current_block[0] <= TERMINAL_IN_CAR_SUPP_MACHINE) {
  203. // Train rides
  204. // Will be overwritton is is ticket sale (TERMINAL_TICKET_VENDING_MACHINE)
  205. my_model->history.history_type = SuicaHistoryTrain;
  206. uint8_t entry_line = current_block[6];
  207. uint8_t entry_station = current_block[7];
  208. uint8_t exit_line = current_block[8];
  209. uint8_t exit_station = current_block[9];
  210. if((uint8_t)current_block[0] != TERMINAL_MOBILE_PHONE) {
  211. suica_parse_train_code(entry_line, entry_station, SuicaTrainRideEntry, my_model);
  212. }
  213. if((uint8_t)current_block[1] != PROCESSING_CODE_NEW_ISSUE) {
  214. suica_parse_train_code(exit_line, exit_station, SuicaTrainRideExit, my_model);
  215. }
  216. if(((uint8_t)current_block[4] + (uint8_t)current_block[5]) != 0) {
  217. my_model->history.year = ((uint8_t)current_block[4] & 0xFE) >> 1;
  218. my_model->history.month = (((uint8_t)current_block[4] & 0x01) << 3) |
  219. (((uint8_t)current_block[5] & 0xE0) >> 5);
  220. my_model->history.day = (uint8_t)current_block[5] & 0x1F;
  221. }
  222. }
  223. switch((uint8_t)current_block[0]) {
  224. case TERMINAL_BUS:
  225. // 6 & 7 bus line code
  226. // 8 & 9 bus stop code
  227. my_model->history.history_type = SuicaHistoryBus;
  228. break;
  229. case TERMINAL_POS_AND_TAXI:
  230. case TERMINAL_VENDING_MACHINE:
  231. // 6 & 7 are hour and minute
  232. my_model->history.history_type = ((uint8_t)current_block[0] == TERMINAL_POS_AND_TAXI) ?
  233. SuicaHistoryPosAndTaxi :
  234. SuicaHistoryVendingMachine;
  235. my_model->history.hour = ((uint8_t)current_block[6] & 0xF8) >> 3;
  236. my_model->history.minute = (((uint8_t)current_block[6] & 0x07) << 3) |
  237. (((uint8_t)current_block[7] & 0xE0) >> 5);
  238. my_model->history.shop_code = (uint8_t*)malloc(2);
  239. my_model->history.shop_code[0] = current_block[8];
  240. my_model->history.shop_code[1] = current_block[9];
  241. break;
  242. case TERMINAL_MOBILE_PHONE:
  243. if((uint8_t)current_block[1] == PROCESSING_CODE_NEW_ISSUE) {
  244. my_model->history.hour = ((uint8_t)current_block[6] & 0xF8) >> 3;
  245. my_model->history.minute = (((uint8_t)current_block[6] & 0x07) << 3) |
  246. (((uint8_t)current_block[7] & 0xE0) >> 5);
  247. }
  248. break;
  249. case TERMINAL_TICKET_VENDING_MACHINE:
  250. my_model->history.history_type = SuicaHistoryHappyBirthday;
  251. break;
  252. default:
  253. if((uint8_t)current_block[0] <= TERMINAL_NULL) {
  254. my_model->history.history_type = SuicaHistoryNull;
  255. }
  256. break;
  257. }
  258. if((uint8_t)current_block[1] == PROCESSING_CODE_NEW_ISSUE) {
  259. my_model->history.history_type = SuicaHistoryHappyBirthday;
  260. }
  261. }
  262. static void suica_parse_detail_callback(GuiButtonType result, InputType type, void* context) {
  263. Metroflip* app = context;
  264. UNUSED(result);
  265. if(type == InputTypeShort) {
  266. SuicaHistoryViewModel* my_model = view_get_model(app->suica_context->view_history);
  267. suica_parse(my_model);
  268. FURI_LOG_I(TAG, "Draw Callback: We have %d entries", my_model->size);
  269. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewCanvas);
  270. }
  271. }
  272. static NfcCommand suica_poller_callback(NfcGenericEvent event, void* context) {
  273. furi_assert(event.protocol == NfcProtocolFelica);
  274. NfcCommand command = NfcCommandContinue;
  275. MetroflipPollerEventType stage = MetroflipPollerEventTypeStart;
  276. Metroflip* app = context;
  277. FuriString* parsed_data = furi_string_alloc();
  278. SuicaHistoryViewModel* model = view_get_model(app->suica_context->view_history);
  279. Widget* widget = app->widget;
  280. const uint16_t service_code[2] = {SERVICE_CODE_HISTORY_IN_LE, SERVICE_CODE_TAPS_LOG_IN_LE};
  281. const FelicaPollerEvent* felica_event = event.event_data;
  282. FelicaPollerReadCommandResponse* rx_resp;
  283. rx_resp->SF1 = 0;
  284. rx_resp->SF2 = 0;
  285. uint8_t blocks[1] = {0x00};
  286. FelicaPoller* felica_poller = event.instance;
  287. FURI_LOG_I(TAG, "Poller set");
  288. if(felica_event->type == FelicaPollerEventTypeRequestAuthContext &&
  289. felica_poller->data->pmm.data[1] == SUICA_IC_TYPE_CODE) {
  290. view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardDetected);
  291. command = NfcCommandContinue;
  292. if(stage == MetroflipPollerEventTypeStart) {
  293. nfc_device_set_data(
  294. app->nfc_device, NfcProtocolFelica, nfc_poller_get_data(app->poller));
  295. furi_string_printf(parsed_data, "\e#Suica\n");
  296. FelicaError error = FelicaErrorNone;
  297. int service_code_index = 0;
  298. // Authenticate with the card
  299. // Iterate through the two services
  300. while(service_code_index < 2 && error == FelicaErrorNone) {
  301. furi_string_cat_printf(
  302. parsed_data, "%s: \n", suica_service_names[service_code_index]);
  303. rx_resp->SF1 = 0;
  304. rx_resp->SF2 = 0;
  305. blocks[0] = 0; // firmware api requires this to be a list
  306. while((rx_resp->SF1 + rx_resp->SF2) == 0 &&
  307. blocks[0] < SUICA_MAX_HISTORY_ENTRIES && error == FelicaErrorNone) {
  308. uint8_t block_data[16] = {0};
  309. error = felica_poller_read_blocks(
  310. felica_poller, 1, blocks, service_code[service_code_index], &rx_resp);
  311. if(error != FelicaErrorNone) {
  312. view_dispatcher_send_custom_event(
  313. app->view_dispatcher, MetroflipCustomEventCardLost);
  314. command = NfcCommandStop;
  315. break;
  316. }
  317. furi_string_cat_printf(parsed_data, "Block %02X\n", blocks[0]);
  318. blocks[0]++;
  319. for(size_t i = 0; i < FELICA_DATA_BLOCK_SIZE; i++) {
  320. furi_string_cat_printf(parsed_data, "%02X ", rx_resp->data[i]);
  321. block_data[i] = rx_resp->data[i];
  322. }
  323. furi_string_cat_printf(parsed_data, "\n");
  324. if(service_code_index == 0) {
  325. FURI_LOG_I(
  326. TAG,
  327. "Service code %d, adding entry %x",
  328. service_code_index,
  329. model->size);
  330. suica_add_entry(model, block_data);
  331. }
  332. }
  333. service_code_index++;
  334. }
  335. metroflip_app_blink_stop(app);
  336. if(model->size == 1) { // Have to let the poller run once before knowing we failed
  337. furi_string_printf(
  338. parsed_data,
  339. "\e#Suica\nSorry, no data found.\nPlease let the developers know and we will add support.");
  340. }
  341. widget_add_text_scroll_element(
  342. widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
  343. widget_add_button_element(
  344. widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
  345. if(model->size > 1) {
  346. widget_add_button_element(
  347. widget, GuiButtonTypeCenter, "Parse", suica_parse_detail_callback, app);
  348. }
  349. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
  350. }
  351. }
  352. furi_string_free(parsed_data);
  353. command = NfcCommandStop;
  354. return command;
  355. }
  356. static bool suica_history_input_callback(InputEvent* event, void* context) {
  357. Metroflip* app = (Metroflip*)context;
  358. if(event->type == InputTypeShort) {
  359. switch(event->key) {
  360. case InputKeyLeft: {
  361. bool redraw = true;
  362. with_view_model(
  363. app->suica_context->view_history,
  364. SuicaHistoryViewModel * model,
  365. {
  366. if(model->entry > 1) {
  367. model->entry--;
  368. }
  369. suica_parse(model);
  370. FURI_LOG_I(TAG, "Viewing entry %d", model->entry);
  371. },
  372. redraw);
  373. break;
  374. }
  375. case InputKeyRight: {
  376. bool redraw = true;
  377. with_view_model(
  378. app->suica_context->view_history,
  379. SuicaHistoryViewModel * model,
  380. {
  381. if(model->entry < model->size) {
  382. model->entry++;
  383. }
  384. suica_parse(model);
  385. FURI_LOG_I(TAG, "Viewing entry %d", model->entry);
  386. },
  387. redraw);
  388. break;
  389. }
  390. case InputKeyUp: {
  391. bool redraw = true;
  392. with_view_model(
  393. app->suica_context->view_history,
  394. SuicaHistoryViewModel * model,
  395. {
  396. if(model->page > 0) {
  397. model->page--;
  398. }
  399. },
  400. redraw);
  401. break;
  402. }
  403. case InputKeyDown: {
  404. bool redraw = true;
  405. with_view_model(
  406. app->suica_context->view_history,
  407. SuicaHistoryViewModel * model,
  408. {
  409. if(model->page < HISTORY_VIEW_PAGE_NUM - 1) {
  410. model->page++;
  411. }
  412. },
  413. redraw);
  414. break;
  415. }
  416. default:
  417. // Handle other keys or do nothing
  418. break;
  419. }
  420. }
  421. return false;
  422. }
  423. static void suica_view_history_timer_callback(void* context) {
  424. Metroflip* app = (Metroflip*)context;
  425. view_dispatcher_send_custom_event(app->view_dispatcher, 0);
  426. }
  427. static void suica_view_history_enter_callback(void* context) {
  428. uint32_t period = furi_ms_to_ticks(ARROW_ANIMATION_FRAME_MS);
  429. Metroflip* app = (Metroflip*)context;
  430. furi_assert(app->suica_context->timer == NULL);
  431. app->suica_context->timer =
  432. furi_timer_alloc(suica_view_history_timer_callback, FuriTimerTypePeriodic, context);
  433. furi_timer_start(app->suica_context->timer, period);
  434. }
  435. static void suica_view_history_exit_callback(void* context) {
  436. Metroflip* app = (Metroflip*)context;
  437. furi_timer_stop(app->suica_context->timer);
  438. furi_timer_free(app->suica_context->timer);
  439. app->suica_context->timer = NULL;
  440. }
  441. static bool suica_view_history_custom_event_callback(uint32_t event, void* context) {
  442. Metroflip* app = (Metroflip*)context;
  443. switch(event) {
  444. case 0:
  445. // Redraw screen by passing true to last parameter of with_view_model.
  446. {
  447. bool redraw = true;
  448. with_view_model(
  449. app->suica_context->view_history,
  450. SuicaHistoryViewModel * model,
  451. { model->animator_tick++; },
  452. redraw);
  453. return true;
  454. }
  455. default:
  456. return false;
  457. }
  458. }
  459. static uint32_t suica_navigation_raw_callback(void* _context) {
  460. UNUSED(_context);
  461. return MetroflipViewWidget;
  462. }
  463. static void suica_on_enter(Metroflip* app) {
  464. // Gui* gui = furi_record_open(RECORD_GUI);
  465. dolphin_deed(DolphinDeedNfcRead);
  466. if(app->data_loaded == false) {
  467. app->suica_context = malloc(sizeof(SuicaContext));
  468. app->suica_context->view_history = view_alloc();
  469. view_set_context(app->suica_context->view_history, app);
  470. view_allocate_model(
  471. app->suica_context->view_history,
  472. ViewModelTypeLockFree,
  473. sizeof(SuicaHistoryViewModel));
  474. }
  475. view_set_input_callback(app->suica_context->view_history, suica_history_input_callback);
  476. view_set_previous_callback(app->suica_context->view_history, suica_navigation_raw_callback);
  477. view_set_enter_callback(app->suica_context->view_history, suica_view_history_enter_callback);
  478. view_set_exit_callback(app->suica_context->view_history, suica_view_history_exit_callback);
  479. view_set_custom_callback(
  480. app->suica_context->view_history, suica_view_history_custom_event_callback);
  481. view_set_draw_callback(app->suica_context->view_history, suica_history_draw_callback);
  482. view_dispatcher_add_view(
  483. app->view_dispatcher, MetroflipViewCanvas, app->suica_context->view_history);
  484. if(app->data_loaded == false) {
  485. popup_set_header(app->popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
  486. popup_set_icon(app->popup, 0, 3, &I_RFIDDolphinReceive_97x61);
  487. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
  488. nfc_scanner_alloc(app->nfc);
  489. app->poller = nfc_poller_alloc(app->nfc, NfcProtocolFelica);
  490. nfc_poller_start(app->poller, suica_poller_callback, app);
  491. FURI_LOG_I(TAG, "Poller started");
  492. metroflip_app_blink_start(app);
  493. } else {
  494. SuicaHistoryViewModel* model = view_get_model(app->suica_context->view_history);
  495. suica_model_initialize_after_load(model);
  496. Widget* widget = app->widget;
  497. FuriString* parsed_data = furi_string_alloc();
  498. furi_string_printf(parsed_data, "\e#Suica\n");
  499. for(uint8_t i = 0; i < model->size; i++) {
  500. furi_string_cat_printf(parsed_data, "Block %02X\n", i);
  501. for(size_t j = 0; j < FELICA_DATA_BLOCK_SIZE; j++) {
  502. furi_string_cat_printf(parsed_data, "%02X ", model->travel_history[i * 16 + j]);
  503. }
  504. furi_string_cat_printf(parsed_data, "\n");
  505. }
  506. widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
  507. widget_add_button_element(
  508. widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
  509. if(model->size > 1) {
  510. widget_add_button_element(
  511. widget, GuiButtonTypeCenter, "Parse", suica_parse_detail_callback, app);
  512. }
  513. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
  514. }
  515. }
  516. static bool suica_on_event(Metroflip* app, SceneManagerEvent event) {
  517. bool consumed = false;
  518. Popup* popup = app->popup;
  519. if(event.type == SceneManagerEventTypeCustom) {
  520. if(event.event == MetroflipCustomEventCardDetected) {
  521. popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop);
  522. consumed = true;
  523. } else if(event.event == MetroflipCustomEventCardLost) {
  524. popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop);
  525. // popup_set_timeout(popup, 2000);
  526. // popup_enable_timeout(popup);
  527. // view_dispatcher_switch_to_view(app->view_dispatcher, SuicaViewPopup);
  528. // popup_disable_timeout(popup);
  529. scene_manager_search_and_switch_to_previous_scene(
  530. app->scene_manager, MetroflipSceneStart);
  531. consumed = true;
  532. } else if(event.event == MetroflipCustomEventWrongCard) {
  533. popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop);
  534. scene_manager_search_and_switch_to_previous_scene(
  535. app->scene_manager, MetroflipSceneStart);
  536. consumed = true;
  537. } else if(event.event == MetroflipCustomEventPollerFail) {
  538. popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop);
  539. scene_manager_search_and_switch_to_previous_scene(
  540. app->scene_manager, MetroflipSceneStart);
  541. consumed = true;
  542. }
  543. } else if(event.type == SceneManagerEventTypeBack) {
  544. UNUSED(popup);
  545. scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
  546. consumed = true;
  547. }
  548. return consumed;
  549. }
  550. static void suica_on_exit(Metroflip* app) {
  551. widget_reset(app->widget);
  552. view_free(app->suica_context->view_history);
  553. view_dispatcher_remove_view(app->view_dispatcher, MetroflipViewCanvas);
  554. free(app->suica_context);
  555. metroflip_app_blink_stop(app);
  556. if(app->poller) {
  557. nfc_poller_stop(app->poller);
  558. nfc_poller_free(app->poller);
  559. }
  560. }
  561. /* Actual implementation of app<>plugin interface */
  562. static const MetroflipPlugin suica_plugin = {
  563. .card_name = "Suica",
  564. .plugin_on_enter = suica_on_enter,
  565. .plugin_on_event = suica_on_event,
  566. .plugin_on_exit = suica_on_exit,
  567. };
  568. /* Plugin descriptor to comply with basic plugin specification */
  569. static const FlipperAppPluginDescriptor suica_plugin_descriptor = {
  570. .appid = METROFLIP_SUPPORTED_CARD_PLUGIN_APP_ID,
  571. .ep_api_version = METROFLIP_SUPPORTED_CARD_PLUGIN_API_VERSION,
  572. .entry_point = &suica_plugin,
  573. };
  574. /* Plugin entry point - must return a pointer to const descriptor */
  575. const FlipperAppPluginDescriptor* suica_plugin_ep(void) {
  576. return &suica_plugin_descriptor;
  577. }