suica.c 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480
  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 "metroflip_i.h"
  18. #include <flipper_application.h>
  19. #include "../../metroflip_plugins.h"
  20. #include "../../api/metroflip/metroflip_api.h"
  21. #include "../../api/suica/suica_assets.h"
  22. #include <lib/nfc/protocols/felica/felica.h>
  23. #include <lib/nfc/protocols/felica/felica_poller.h>
  24. #include <lib/nfc/protocols/felica/felica_poller_i.h>
  25. #include <lib/nfc/helpers/felica_crc.h>
  26. #include <lib/bit_lib/bit_lib.h>
  27. #include <applications/services/locale/locale.h>
  28. #include <datetime.h>
  29. // Probably not needed after upstream include this in their suica_i.h
  30. #include <toolbox/stream/stream.h>
  31. #include <toolbox/stream/file_stream.h>
  32. #define TAG "Suica:Scene:Suica"
  33. #define SUICA_STATION_LIST_PATH APP_ASSETS_PATH("suica/line_")
  34. #define SUICA_IC_TYPE_CODE 0x31
  35. #define SERVICE_CODE_HISTORY_IN_LE (0x090FU)
  36. #define SERVICE_CODE_TAPS_LOG_IN_LE (0x108FU)
  37. #define BLOCK_COUNT 1
  38. #define HISTORY_VIEW_PAGE_NUM 3
  39. #define TERMINAL_NULL 0x02
  40. #define TERMINAL_BUS 0x05
  41. #define TERMINAL_TICKET_VENDING_MACHINE 0x12
  42. #define TERMINAL_TURNSTILE 0x16
  43. #define TERMINAL_MOBILE_PHONE 0x1B
  44. #define TERMINAL_IN_CAR_SUPP_MACHINE 0x24
  45. #define TERMINAL_POS_AND_TAXI 0xC7
  46. #define TERMINAL_VENDING_MACHINE 0xC8
  47. #define PROCESSING_CODE_NEW_ISSUE 0x02
  48. #define ARROW_ANIMATION_FRAME_MS 350
  49. const char* suica_service_names[] = {
  50. "Travel History",
  51. "Taps Log",
  52. };
  53. typedef enum {
  54. SuicaTrainRideEntry,
  55. SuicaTrainRideExit,
  56. } SuicaTrainRideType;
  57. typedef enum {
  58. SuicaRedrawScreen,
  59. } MetroflipCustomEvent;
  60. static void suica_model_initialize(SuicaHistoryViewModel* model, size_t initial_capacity) {
  61. model->travel_history =
  62. (uint8_t*)malloc(initial_capacity * FELICA_DATA_BLOCK_SIZE); // Each entry is 16 bytes
  63. model->size = 0;
  64. model->capacity = initial_capacity;
  65. model->entry = 1;
  66. model->page = 0;
  67. model->animator_tick = 0;
  68. model->history.entry_station.name = furi_string_alloc_set("Unknown");
  69. model->history.entry_station.jr_header = furi_string_alloc_set("0");
  70. model->history.exit_station.name = furi_string_alloc_set("Unknown");
  71. model->history.exit_station.jr_header = furi_string_alloc_set("0");
  72. model->history.entry_line = RailwaysList[SUICA_RAILWAY_NUM];
  73. model->history.exit_line = RailwaysList[SUICA_RAILWAY_NUM];
  74. }
  75. static void suica_model_initialize_after_load(SuicaHistoryViewModel* model) {
  76. model->entry = 1;
  77. model->page = 0;
  78. model->animator_tick = 0;
  79. model->history.entry_station.name = furi_string_alloc_set("Unknown");
  80. model->history.entry_station.jr_header = furi_string_alloc_set("0");
  81. model->history.exit_station.name = furi_string_alloc_set("Unknown");
  82. model->history.exit_station.jr_header = furi_string_alloc_set("0");
  83. model->history.entry_line = RailwaysList[SUICA_RAILWAY_NUM];
  84. model->history.exit_line = RailwaysList[SUICA_RAILWAY_NUM];
  85. }
  86. static void suica_add_entry(SuicaHistoryViewModel* model, const uint8_t* entry) {
  87. if(model->size <= 0) {
  88. suica_model_initialize(model, 3);
  89. }
  90. // Check if resizing is needed
  91. if(model->size == model->capacity) {
  92. size_t new_capacity = model->capacity * 2; // Double the capacity
  93. uint8_t* new_data =
  94. (uint8_t*)realloc(model->travel_history, new_capacity * FELICA_DATA_BLOCK_SIZE);
  95. model->travel_history = new_data;
  96. model->capacity = new_capacity;
  97. }
  98. // Copy the 16-byte entry to the next slot
  99. for(size_t i = 0; i < FELICA_DATA_BLOCK_SIZE; i++) {
  100. model->travel_history[(model->size * FELICA_DATA_BLOCK_SIZE) + i] = entry[i];
  101. }
  102. model->size++;
  103. }
  104. void suica_parse_train_code(
  105. uint8_t line_code,
  106. uint8_t station_code,
  107. SuicaTrainRideType ride_type,
  108. SuicaHistoryViewModel* model) {
  109. Storage* storage = furi_record_open(RECORD_STORAGE);
  110. Stream* stream = file_stream_alloc(storage);
  111. FuriString* line = furi_string_alloc();
  112. FuriString* line_code_str = furi_string_alloc();
  113. FuriString* line_and_station_code_str = furi_string_alloc();
  114. furi_string_printf(line_code_str, "0x%02X", line_code);
  115. furi_string_printf(line_and_station_code_str, "0x%02X,0x%02X", line_code, station_code);
  116. FuriString* line_candidate = furi_string_alloc_set(SUICA_RAILWAY_UNKNOWN_NAME);
  117. FuriString* station_candidate = furi_string_alloc_set(SUICA_RAILWAY_UNKNOWN_NAME);
  118. FuriString* station_num_candidate = furi_string_alloc_set("0");
  119. FuriString* station_JR_header_candidate = furi_string_alloc_set("0");
  120. FuriString* line_copy = furi_string_alloc();
  121. size_t line_comma_ind = 0;
  122. size_t station_comma_ind = 0;
  123. size_t station_num_comma_ind = 0;
  124. size_t station_JR_header_comma_ind = 0;
  125. bool station_found = false;
  126. FuriString* file_name = furi_string_alloc();
  127. furi_string_printf(file_name, "%s0x%02X.txt", SUICA_STATION_LIST_PATH, line_code);
  128. if(file_stream_open(stream, furi_string_get_cstr(file_name), FSAM_READ, FSOM_OPEN_EXISTING)) {
  129. while(stream_read_line(stream, line) && !station_found) {
  130. // file is in csv format: station_group_id,station_id,station_sub_id,station_name
  131. // search for the station
  132. furi_string_replace_all(line, "\r", "");
  133. furi_string_replace_all(line, "\n", "");
  134. furi_string_set(line_copy, line); // 0xD5,0x02,Keikyu Main,Shinagawa,1,0
  135. if(furi_string_start_with(line, line_code_str)) {
  136. // set line name here
  137. furi_string_right(line_copy, 10); // Keikyu Main,Shinagawa,1,0
  138. furi_string_set(line_candidate, line_copy);
  139. line_comma_ind = furi_string_search_char(line_candidate, ',', 0);
  140. furi_string_left(line_candidate, line_comma_ind); // Keikyu Main
  141. // we cut the line and station code in the line line copy
  142. // and we leave only the line name for the line candidate
  143. if(furi_string_start_with(line, line_and_station_code_str)) {
  144. furi_string_set(station_candidate, line_copy); // Keikyu Main,Shinagawa,1,0
  145. furi_string_right(station_candidate, line_comma_ind + 1);
  146. station_comma_ind =
  147. furi_string_search_char(station_candidate, ',', 0); // Shinagawa,1,0
  148. furi_string_left(station_candidate, station_comma_ind); // Shinagawa
  149. station_found = true;
  150. break;
  151. }
  152. }
  153. }
  154. } else {
  155. FURI_LOG_E("Suica:Scene:Suica", "Failed to open stations.txt");
  156. }
  157. furi_string_set(station_num_candidate, line_copy); // Keikyu Main,Shinagawa,1,0
  158. furi_string_right(station_num_candidate, line_comma_ind + station_comma_ind + 2); // 1,0
  159. station_num_comma_ind = furi_string_search_char(station_num_candidate, ',', 0);
  160. furi_string_left(station_num_candidate, station_num_comma_ind); // 1
  161. furi_string_set(station_JR_header_candidate, line_copy); // Keikyu Main,Shinagawa,1,0
  162. furi_string_right(
  163. station_JR_header_candidate,
  164. line_comma_ind + station_comma_ind + station_num_comma_ind + 3); // 0
  165. station_JR_header_comma_ind = furi_string_search_char(station_JR_header_candidate, ',', 0);
  166. furi_string_left(station_JR_header_candidate, station_JR_header_comma_ind); // 0
  167. switch(ride_type) {
  168. case SuicaTrainRideEntry:
  169. for(size_t i = 0; i < SUICA_RAILWAY_NUM; i++) {
  170. if(furi_string_equal_str(line_candidate, RailwaysList[i].long_name)) {
  171. model->history.entry_line = RailwaysList[i];
  172. furi_string_set(model->history.entry_station.name, station_candidate);
  173. model->history.entry_station.station_number =
  174. atoi(furi_string_get_cstr(station_num_candidate));
  175. furi_string_set(
  176. model->history.entry_station.jr_header, station_JR_header_candidate);
  177. break;
  178. }
  179. }
  180. break;
  181. case SuicaTrainRideExit:
  182. for(size_t i = 0; i < SUICA_RAILWAY_NUM; i++) {
  183. if(furi_string_equal_str(line_candidate, RailwaysList[i].long_name)) {
  184. model->history.exit_line = RailwaysList[i];
  185. furi_string_set(model->history.exit_station.name, station_candidate);
  186. model->history.exit_station.station_number =
  187. atoi(furi_string_get_cstr(station_num_candidate));
  188. furi_string_set(
  189. model->history.exit_station.jr_header, station_JR_header_candidate);
  190. break;
  191. }
  192. }
  193. break;
  194. default:
  195. UNUSED(model);
  196. break;
  197. }
  198. furi_string_free(line);
  199. furi_string_free(line_copy);
  200. furi_string_free(line_code_str);
  201. furi_string_free(line_and_station_code_str);
  202. furi_string_free(line_candidate);
  203. furi_string_free(station_candidate);
  204. furi_string_free(station_num_candidate);
  205. furi_string_free(station_JR_header_candidate);
  206. file_stream_close(stream);
  207. stream_free(stream);
  208. furi_record_close(RECORD_STORAGE);
  209. }
  210. static void suica_parse(SuicaHistoryViewModel* my_model) {
  211. uint8_t current_block[FELICA_DATA_BLOCK_SIZE];
  212. // Parse the current block/entry
  213. for(size_t i = 0; i < FELICA_DATA_BLOCK_SIZE; i++) {
  214. current_block[i] = my_model->travel_history[((my_model->entry - 1) * 16) + i];
  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. } else {
  222. my_model->history.year = 0;
  223. my_model->history.month = 0;
  224. my_model->history.day = 0;
  225. }
  226. my_model->history.balance = ((uint16_t)current_block[11] << 8) | (uint16_t)current_block[10];
  227. // FURI_LOG_I(TAG,"%02X", (uint8_t)current_block[0]);
  228. my_model->history.area_code = current_block[15];
  229. if((uint8_t)current_block[0] >= TERMINAL_TICKET_VENDING_MACHINE &&
  230. (uint8_t)current_block[0] <= TERMINAL_IN_CAR_SUPP_MACHINE) {
  231. // Train rides
  232. // Will be overwritton is is ticket sale (TERMINAL_TICKET_VENDING_MACHINE)
  233. my_model->history.history_type = SuicaHistoryTrain;
  234. uint8_t entry_line = current_block[6];
  235. uint8_t entry_station = current_block[7];
  236. uint8_t exit_line = current_block[8];
  237. uint8_t exit_station = current_block[9];
  238. if((uint8_t)current_block[0] != TERMINAL_MOBILE_PHONE) {
  239. suica_parse_train_code(entry_line, entry_station, SuicaTrainRideEntry, my_model);
  240. }
  241. if((uint8_t)current_block[1] != PROCESSING_CODE_NEW_ISSUE) {
  242. suica_parse_train_code(exit_line, exit_station, SuicaTrainRideExit, my_model);
  243. }
  244. if(((uint8_t)current_block[4] + (uint8_t)current_block[5]) != 0) {
  245. my_model->history.year = ((uint8_t)current_block[4] & 0xFE) >> 1;
  246. my_model->history.month = (((uint8_t)current_block[4] & 0x01) << 3) |
  247. (((uint8_t)current_block[5] & 0xE0) >> 5);
  248. my_model->history.day = (uint8_t)current_block[5] & 0x1F;
  249. }
  250. }
  251. switch((uint8_t)current_block[0]) {
  252. case TERMINAL_BUS:
  253. // 6 & 7 bus line code
  254. // 8 & 9 bus stop code
  255. my_model->history.history_type = SuicaHistoryBus;
  256. break;
  257. case TERMINAL_POS_AND_TAXI:
  258. case TERMINAL_VENDING_MACHINE:
  259. // 6 & 7 are hour and minute
  260. my_model->history.history_type = ((uint8_t)current_block[0] == TERMINAL_POS_AND_TAXI) ?
  261. SuicaHistoryPosAndTaxi :
  262. SuicaHistoryVendingMachine;
  263. my_model->history.hour = ((uint8_t)current_block[6] & 0xF8) >> 3;
  264. my_model->history.minute = (((uint8_t)current_block[6] & 0x07) << 3) |
  265. (((uint8_t)current_block[7] & 0xE0) >> 5);
  266. my_model->history.shop_code = (uint8_t*)malloc(2);
  267. my_model->history.shop_code[0] = current_block[8];
  268. my_model->history.shop_code[1] = current_block[9];
  269. break;
  270. case TERMINAL_MOBILE_PHONE:
  271. if((uint8_t)current_block[1] == PROCESSING_CODE_NEW_ISSUE) {
  272. my_model->history.hour = ((uint8_t)current_block[6] & 0xF8) >> 3;
  273. my_model->history.minute = (((uint8_t)current_block[6] & 0x07) << 3) |
  274. (((uint8_t)current_block[7] & 0xE0) >> 5);
  275. }
  276. break;
  277. case TERMINAL_TICKET_VENDING_MACHINE:
  278. my_model->history.history_type = SuicaHistoryHappyBirthday;
  279. break;
  280. default:
  281. if((uint8_t)current_block[0] <= TERMINAL_NULL) {
  282. my_model->history.history_type = SuicaHistoryNull;
  283. }
  284. break;
  285. }
  286. if((uint8_t)current_block[1] == PROCESSING_CODE_NEW_ISSUE) {
  287. my_model->history.history_type = SuicaHistoryHappyBirthday;
  288. }
  289. }
  290. static void suica_draw_train_page_1(
  291. Canvas* canvas,
  292. SuicaHistory history,
  293. SuicaHistoryViewModel* model,
  294. bool is_birthday) {
  295. // Entry logo
  296. switch(history.entry_line.type) {
  297. case SuicaKeikyu:
  298. canvas_draw_icon(canvas, 2, 11, &I_Suica_KeikyuLogo);
  299. break;
  300. case SuicaJR:
  301. canvas_draw_icon(canvas, 1, 12, &I_Suica_JRLogo);
  302. break;
  303. case SuicaTokyoMetro:
  304. canvas_draw_icon(canvas, 2, 12, &I_Suica_TokyoMetroLogo);
  305. break;
  306. case SuicaToei:
  307. canvas_draw_icon(canvas, 4, 11, &I_Suica_ToeiLogo);
  308. break;
  309. case SuicaTWR:
  310. canvas_draw_icon(canvas, 0, 12, &I_Suica_TWRLogo);
  311. break;
  312. case SuicaTokyoMonorail:
  313. canvas_draw_icon(canvas, 0, 11, &I_Suica_TokyoMonorailLogo);
  314. break;
  315. case SuicaRailwayTypeMax:
  316. canvas_draw_icon(canvas, 5, 11, &I_Suica_QuestionMarkSmall);
  317. break;
  318. default:
  319. break;
  320. }
  321. // Entry Text
  322. canvas_set_font(canvas, FontPrimary);
  323. canvas_draw_str(canvas, 26, 23, history.entry_line.long_name);
  324. canvas_set_font(canvas, FontSecondary);
  325. canvas_draw_str(canvas, 2, 34, furi_string_get_cstr(history.entry_station.name));
  326. if(!is_birthday) {
  327. // Exit logo
  328. switch(history.exit_line.type) {
  329. case SuicaKeikyu:
  330. canvas_draw_icon(canvas, 2, 39, &I_Suica_KeikyuLogo);
  331. break;
  332. case SuicaJR:
  333. canvas_draw_icon(canvas, 1, 40, &I_Suica_JRLogo);
  334. break;
  335. case SuicaTokyoMetro:
  336. canvas_draw_icon(canvas, 2, 40, &I_Suica_TokyoMetroLogo);
  337. break;
  338. case SuicaToei:
  339. canvas_draw_icon(canvas, 4, 39, &I_Suica_ToeiLogo);
  340. break;
  341. case SuicaTWR:
  342. canvas_draw_icon(canvas, 0, 40, &I_Suica_TWRLogo);
  343. break;
  344. case SuicaTokyoMonorail:
  345. canvas_draw_icon(canvas, 0, 39, &I_Suica_TokyoMonorailLogo);
  346. break;
  347. case SuicaRailwayTypeMax:
  348. canvas_draw_icon(canvas, 5, 39, &I_Suica_QuestionMarkSmall);
  349. break;
  350. default:
  351. break;
  352. }
  353. // Exit Text
  354. canvas_set_font(canvas, FontPrimary);
  355. canvas_draw_str(canvas, 26, 51, history.exit_line.long_name);
  356. canvas_set_font(canvas, FontSecondary);
  357. canvas_draw_str(canvas, 2, 62, furi_string_get_cstr(history.exit_station.name));
  358. } else {
  359. // Birthday
  360. canvas_draw_icon(canvas, 5, 42, &I_Suica_CrackingEgg);
  361. canvas_set_font(canvas, FontPrimary);
  362. canvas_draw_str(canvas, 28, 56, "Suica issued");
  363. }
  364. // Separator
  365. canvas_draw_icon(canvas, 0, 37, &I_Suica_DashLine);
  366. // Arrow
  367. uint8_t arrow_bits[4] = {0b1000, 0b0100, 0b0010, 0b0001};
  368. // Arrow
  369. if(model->animator_tick > 3) {
  370. // 4 steps of animation
  371. model->animator_tick = 0;
  372. }
  373. uint8_t current_arrow_bits = arrow_bits[model->animator_tick];
  374. canvas_draw_icon(
  375. canvas,
  376. 110,
  377. 19,
  378. (current_arrow_bits & 0b1000) ? &I_Suica_FilledArrowDown : &I_Suica_EmptyArrowDown);
  379. canvas_draw_icon(
  380. canvas,
  381. 110,
  382. 29,
  383. (current_arrow_bits & 0b0100) ? &I_Suica_FilledArrowDown : &I_Suica_EmptyArrowDown);
  384. canvas_draw_icon(
  385. canvas,
  386. 110,
  387. 39,
  388. (current_arrow_bits & 0b0010) ? &I_Suica_FilledArrowDown : &I_Suica_EmptyArrowDown);
  389. canvas_draw_icon(
  390. canvas,
  391. 110,
  392. 49,
  393. (current_arrow_bits & 0b0001) ? &I_Suica_FilledArrowDown : &I_Suica_EmptyArrowDown);
  394. }
  395. static void
  396. suica_draw_train_page_2(Canvas* canvas, SuicaHistory history, SuicaHistoryViewModel* model) {
  397. FuriString* buffer = furi_string_alloc();
  398. // Entry
  399. switch(history.entry_line.type) {
  400. case SuicaKeikyu:
  401. canvas_draw_disc(canvas, 24, 38, 24);
  402. canvas_set_color(canvas, ColorWhite);
  403. canvas_draw_disc(canvas, 24, 38, 21);
  404. canvas_set_color(canvas, ColorBlack);
  405. canvas_set_font(canvas, FontKeyboard);
  406. canvas_draw_icon(canvas, 16, 24, history.entry_line.logo_icon);
  407. canvas_set_font(canvas, FontBigNumbers);
  408. furi_string_printf(buffer, "%02d", history.entry_station.station_number);
  409. canvas_draw_str(canvas, 13, 51, furi_string_get_cstr(buffer));
  410. break;
  411. case SuicaTokyoMonorail:
  412. canvas_draw_rbox(canvas, 9, 23, 32, 32, 5);
  413. canvas_set_color(canvas, ColorWhite);
  414. canvas_draw_box(canvas, 12, 26, 26, 26);
  415. canvas_set_color(canvas, ColorBlack);
  416. canvas_set_font(canvas, FontPrimary);
  417. canvas_draw_str_aligned(
  418. canvas, 25, 38, AlignCenter, AlignBottom, history.entry_line.short_name);
  419. canvas_draw_str(canvas, 17, 36, history.entry_line.short_name);
  420. canvas_set_font(canvas, FontBigNumbers);
  421. furi_string_printf(buffer, "%02d", history.entry_station.station_number);
  422. canvas_draw_str(canvas, 14, 51, furi_string_get_cstr(buffer));
  423. break;
  424. case SuicaJR:
  425. if(!furi_string_equal_str(history.entry_station.jr_header, "0")) {
  426. canvas_draw_rbox(canvas, 6, 14, 38, 48, 7);
  427. canvas_set_color(canvas, ColorWhite);
  428. canvas_set_font(canvas, FontPrimary);
  429. canvas_draw_str_aligned(
  430. canvas,
  431. 25,
  432. 24,
  433. AlignCenter,
  434. AlignBottom,
  435. furi_string_get_cstr(history.entry_station.jr_header));
  436. canvas_draw_rbox(canvas, 9, 26, 32, 32, 5);
  437. canvas_set_color(canvas, ColorBlack);
  438. canvas_draw_frame(canvas, 12, 29, 26, 26);
  439. canvas_set_font(canvas, FontKeyboard);
  440. canvas_draw_str_aligned(
  441. canvas, 25, 38, AlignCenter, AlignBottom, history.entry_line.short_name);
  442. canvas_set_font(canvas, FontBigNumbers);
  443. furi_string_printf(buffer, "%02d", history.entry_station.station_number);
  444. canvas_draw_str(canvas, 14, 53, furi_string_get_cstr(buffer));
  445. } else {
  446. canvas_draw_rframe(canvas, 9, 23, 32, 32, 5);
  447. canvas_draw_frame(canvas, 12, 26, 26, 26);
  448. canvas_set_font(canvas, FontKeyboard);
  449. canvas_draw_str_aligned(
  450. canvas, 25, 35, AlignCenter, AlignBottom, history.entry_line.short_name);
  451. canvas_set_font(canvas, FontBigNumbers);
  452. furi_string_printf(buffer, "%02d", history.entry_station.station_number);
  453. canvas_draw_str(canvas, 14, 50, furi_string_get_cstr(buffer));
  454. }
  455. break;
  456. case SuicaTokyoMetro:
  457. case SuicaToei:
  458. canvas_draw_disc(canvas, 24, 38, 24);
  459. canvas_set_color(canvas, ColorWhite);
  460. canvas_draw_disc(canvas, 24, 38, 19);
  461. canvas_set_color(canvas, ColorBlack);
  462. canvas_set_font(canvas, FontBigNumbers);
  463. canvas_draw_icon(
  464. canvas,
  465. 17 + history.entry_line.logo_offset[0],
  466. 22 + history.entry_line.logo_offset[1],
  467. history.entry_line.logo_icon);
  468. furi_string_printf(buffer, "%02d", history.entry_station.station_number);
  469. canvas_draw_str(canvas, 13, 53, furi_string_get_cstr(buffer));
  470. break;
  471. case SuicaTWR:
  472. canvas_draw_circle(canvas, 24, 38, 24);
  473. canvas_draw_circle(canvas, 24, 38, 20);
  474. canvas_draw_disc(canvas, 24, 38, 18);
  475. canvas_set_color(canvas, ColorWhite);
  476. canvas_draw_icon(canvas, 20, 23, history.entry_line.logo_icon);
  477. canvas_set_font(canvas, FontBigNumbers);
  478. furi_string_printf(buffer, "%02d", history.entry_station.station_number);
  479. canvas_draw_str(canvas, 13, 53, furi_string_get_cstr(buffer));
  480. canvas_set_color(canvas, ColorBlack);
  481. break;
  482. case SuicaRailwayTypeMax:
  483. canvas_draw_circle(canvas, 24, 38, 24);
  484. canvas_draw_circle(canvas, 24, 38, 19);
  485. canvas_draw_icon(canvas, 14, 22, &I_Suica_QuestionMarkBig);
  486. break;
  487. default:
  488. break;
  489. }
  490. // Exit
  491. switch(history.exit_line.type) {
  492. case SuicaKeikyu:
  493. canvas_draw_disc(canvas, 103, 38, 24);
  494. canvas_set_color(canvas, ColorWhite);
  495. canvas_draw_disc(canvas, 103, 38, 21);
  496. canvas_set_color(canvas, ColorBlack);
  497. canvas_draw_icon(canvas, 95, 24, history.exit_line.logo_icon);
  498. canvas_set_font(canvas, FontBigNumbers);
  499. furi_string_printf(buffer, "%02d", history.exit_station.station_number);
  500. canvas_draw_str(canvas, 92, 52, furi_string_get_cstr(buffer));
  501. break;
  502. case SuicaTokyoMonorail:
  503. canvas_draw_rbox(canvas, 86, 23, 32, 32, 5);
  504. canvas_set_color(canvas, ColorWhite);
  505. canvas_draw_box(canvas, 89, 26, 26, 26);
  506. canvas_set_color(canvas, ColorBlack);
  507. canvas_set_font(canvas, FontPrimary);
  508. canvas_draw_str_aligned(
  509. canvas, 101, 35, AlignCenter, AlignBottom, history.exit_line.short_name);
  510. canvas_set_font(canvas, FontBigNumbers);
  511. furi_string_printf(buffer, "%02d", history.exit_station.station_number);
  512. canvas_draw_str(canvas, 91, 51, furi_string_get_cstr(buffer));
  513. break;
  514. case SuicaJR:
  515. if(!furi_string_equal_str(history.exit_station.jr_header, "0")) {
  516. canvas_draw_rbox(canvas, 83, 14, 38, 48, 7);
  517. canvas_set_color(canvas, ColorWhite);
  518. canvas_set_font(canvas, FontPrimary);
  519. canvas_draw_str_aligned(
  520. canvas,
  521. 101,
  522. 24,
  523. AlignCenter,
  524. AlignBottom,
  525. furi_string_get_cstr(history.exit_station.jr_header));
  526. canvas_draw_rbox(canvas, 86, 26, 32, 32, 5);
  527. canvas_set_color(canvas, ColorBlack);
  528. canvas_draw_frame(canvas, 89, 29, 26, 26);
  529. canvas_set_font(canvas, FontKeyboard);
  530. canvas_draw_str_aligned(
  531. canvas, 102, 38, AlignCenter, AlignBottom, history.exit_line.short_name);
  532. canvas_set_font(canvas, FontBigNumbers);
  533. furi_string_printf(buffer, "%02d", history.exit_station.station_number);
  534. canvas_draw_str(canvas, 91, 53, furi_string_get_cstr(buffer));
  535. } else {
  536. canvas_draw_rframe(canvas, 86, 23, 32, 32, 5);
  537. canvas_draw_frame(canvas, 89, 26, 26, 26);
  538. canvas_set_font(canvas, FontKeyboard);
  539. canvas_draw_str_aligned(
  540. canvas, 102, 35, AlignCenter, AlignBottom, history.exit_line.short_name);
  541. canvas_set_font(canvas, FontBigNumbers);
  542. furi_string_printf(buffer, "%02d", history.exit_station.station_number);
  543. canvas_draw_str(canvas, 91, 50, furi_string_get_cstr(buffer));
  544. }
  545. break;
  546. case SuicaTokyoMetro:
  547. case SuicaToei:
  548. canvas_draw_disc(canvas, 103, 38, 24);
  549. canvas_set_color(canvas, ColorWhite);
  550. canvas_draw_disc(canvas, 103, 38, 19);
  551. canvas_set_color(canvas, ColorBlack);
  552. canvas_draw_icon(
  553. canvas,
  554. 96 + history.exit_line.logo_offset[0],
  555. 22 + history.exit_line.logo_offset[1],
  556. history.exit_line.logo_icon);
  557. canvas_set_font(canvas, FontBigNumbers);
  558. furi_string_printf(buffer, "%02d", history.exit_station.station_number);
  559. canvas_draw_str(canvas, 92, 53, furi_string_get_cstr(buffer));
  560. break;
  561. case SuicaTWR:
  562. canvas_draw_circle(canvas, 103, 38, 24);
  563. canvas_draw_circle(canvas, 103, 38, 20);
  564. canvas_draw_disc(canvas, 103, 38, 18);
  565. canvas_set_color(canvas, ColorWhite);
  566. canvas_draw_icon(canvas, 99, 23, history.exit_line.logo_icon);
  567. canvas_set_font(canvas, FontBigNumbers);
  568. furi_string_printf(buffer, "%02d", history.exit_station.station_number);
  569. canvas_draw_str(canvas, 92, 53, furi_string_get_cstr(buffer));
  570. canvas_set_color(canvas, ColorBlack);
  571. break;
  572. case SuicaRailwayTypeMax:
  573. canvas_draw_circle(canvas, 103, 38, 24);
  574. canvas_draw_circle(canvas, 103, 38, 19);
  575. canvas_draw_icon(canvas, 93, 22, &I_Suica_QuestionMarkBig);
  576. default:
  577. break;
  578. }
  579. uint8_t arrow_bits[3] = {0b100, 0b010, 0b001};
  580. // Arrow
  581. if(model->animator_tick > 2) {
  582. // 4 steps of animation
  583. model->animator_tick = 0;
  584. }
  585. uint8_t current_arrow_bits = arrow_bits[model->animator_tick];
  586. canvas_draw_icon(
  587. canvas,
  588. 51,
  589. 32,
  590. (current_arrow_bits & 0b100) ? &I_Suica_FilledArrowRight : &I_Suica_EmptyArrowRight);
  591. canvas_draw_icon(
  592. canvas,
  593. 59,
  594. 32,
  595. (current_arrow_bits & 0b010) ? &I_Suica_FilledArrowRight : &I_Suica_EmptyArrowRight);
  596. canvas_draw_icon(
  597. canvas,
  598. 67,
  599. 32,
  600. (current_arrow_bits & 0b001) ? &I_Suica_FilledArrowRight : &I_Suica_EmptyArrowRight);
  601. furi_string_free(buffer);
  602. }
  603. static void
  604. suica_draw_birthday_page_2(Canvas* canvas, SuicaHistory history, SuicaHistoryViewModel* model) {
  605. UNUSED(history);
  606. canvas_draw_icon(canvas, 27, 14, &I_Suica_PenguinHappyBirthday);
  607. canvas_draw_icon(canvas, 14, 14, &I_Suica_PenguinTodaysVIP);
  608. canvas_draw_rframe(canvas, 12, 12, 13, 52, 2); // VIP frame
  609. uint8_t star_bits[4] = {0b11000000, 0b11110000, 0b11111111, 0b00000000};
  610. // Arrow
  611. if(model->animator_tick > 3) {
  612. // 4 steps of animation
  613. model->animator_tick = 0;
  614. }
  615. uint8_t current_star_bits = star_bits[model->animator_tick];
  616. canvas_draw_icon(
  617. canvas, 87, 30, (current_star_bits & 0b10000000) ? &I_Suica_BigStar : &I_Suica_Nothing);
  618. canvas_draw_icon(
  619. canvas, 90, 12, (current_star_bits & 0b01000000) ? &I_Suica_PlusStar : &I_Suica_Nothing);
  620. canvas_draw_icon(
  621. canvas, 99, 34, (current_star_bits & 0b00100000) ? &I_Suica_SmallStar : &I_Suica_Nothing);
  622. canvas_draw_icon(
  623. canvas, 103, 12, (current_star_bits & 0b00010000) ? &I_Suica_SmallStar : &I_Suica_Nothing);
  624. canvas_draw_icon(
  625. canvas, 106, 21, (current_star_bits & 0b00001000) ? &I_Suica_BigStar : &I_Suica_Nothing);
  626. canvas_draw_icon(
  627. canvas, 109, 43, (current_star_bits & 0b00000100) ? &I_Suica_PlusStar : &I_Suica_Nothing);
  628. canvas_draw_icon(
  629. canvas, 117, 28, (current_star_bits & 0b00000010) ? &I_Suica_BigStar : &I_Suica_Nothing);
  630. canvas_draw_icon(
  631. canvas, 115, 16, (current_star_bits & 0b00000100) ? &I_Suica_PlusStar : &I_Suica_Nothing);
  632. }
  633. static void suica_draw_vending_machine_page_1(
  634. Canvas* canvas,
  635. SuicaHistory history,
  636. SuicaHistoryViewModel* model) {
  637. FuriString* buffer = furi_string_alloc();
  638. canvas_draw_icon(canvas, 0, 10, &I_Suica_VendingPage2Full);
  639. furi_string_printf(buffer, "%d", history.balance_change);
  640. canvas_set_font(canvas, FontPrimary);
  641. canvas_draw_str_aligned(
  642. canvas, 100, 39, AlignRight, AlignBottom, furi_string_get_cstr(buffer));
  643. // Animate Bubbles and LCD Refresh
  644. if(model->animator_tick > 14) {
  645. // 14 steps of animation
  646. model->animator_tick = 0;
  647. }
  648. canvas_set_color(canvas, ColorWhite);
  649. canvas_draw_line(canvas, 87, 50 + model->animator_tick, 128, 50 + model->animator_tick);
  650. switch(model->animator_tick % 7) {
  651. case 0:
  652. canvas_draw_circle(canvas, 12, 48, 1);
  653. canvas_draw_circle(canvas, 23, 39, 2);
  654. break;
  655. case 1:
  656. canvas_draw_circle(canvas, 11, 46, 1);
  657. canvas_draw_circle(canvas, 23, 39, 2);
  658. canvas_set_color(canvas, ColorBlack);
  659. canvas_draw_line(canvas, 24, 37, 22, 37);
  660. canvas_draw_line(canvas, 25, 40, 25, 38);
  661. canvas_set_color(canvas, ColorWhite);
  662. break;
  663. case 2:
  664. canvas_draw_circle(canvas, 12, 44, 1);
  665. canvas_draw_circle(canvas, 24, 50, 1);
  666. break;
  667. case 3:
  668. canvas_draw_icon(canvas, 12, 41, &I_Suica_SmallStar);
  669. canvas_draw_circle(canvas, 25, 48, 1);
  670. break;
  671. case 4:
  672. canvas_draw_icon(canvas, 14, 39, &I_Suica_SmallStar);
  673. canvas_draw_circle(canvas, 26, 46, 1);
  674. break;
  675. case 5:
  676. canvas_draw_icon(canvas, 24, 43, &I_Suica_SmallStar);
  677. canvas_draw_circle(canvas, 16, 38, 2);
  678. break;
  679. case 6:
  680. canvas_draw_icon(canvas, 23, 41, &I_Suica_SmallStar);
  681. canvas_draw_circle(canvas, 16, 38, 2);
  682. canvas_set_color(canvas, ColorBlack);
  683. canvas_draw_line(canvas, 15, 36, 17, 36);
  684. canvas_draw_line(canvas, 18, 39, 18, 37);
  685. canvas_set_color(canvas, ColorWhite);
  686. break;
  687. default:
  688. break;
  689. }
  690. furi_string_free(buffer);
  691. canvas_set_color(canvas, ColorBlack);
  692. }
  693. static void suica_draw_vending_machine_page_2(
  694. Canvas* canvas,
  695. SuicaHistory history,
  696. SuicaHistoryViewModel* model) {
  697. FuriString* buffer = furi_string_alloc();
  698. // Clock Component
  699. canvas_set_color(canvas, ColorWhite); // Erase part of old frame to allow for new frame
  700. canvas_draw_line(canvas, 91, 9, 94, 6);
  701. canvas_draw_line(canvas, 57, 9, 93, 9);
  702. canvas_set_color(canvas, ColorBlack);
  703. furi_string_printf(buffer, "%02d:%02d", history.hour, history.minute);
  704. canvas_draw_line(canvas, 63, 21, 60, 18);
  705. canvas_set_font(canvas, FontKeyboard);
  706. canvas_draw_str(canvas, 63, 19, furi_string_get_cstr(buffer));
  707. canvas_draw_line(canvas, 91, 21, 94, 18);
  708. canvas_draw_line(canvas, 64, 21, 91, 21);
  709. canvas_draw_line(canvas, 94, 6, 94, 17);
  710. canvas_draw_line(canvas, 60, 12, 60, 17);
  711. canvas_draw_line(canvas, 60, 12, 57, 9);
  712. // Vending Machine
  713. canvas_draw_icon(canvas, 5, 12, &I_Suica_VendingMachine);
  714. // Machine Code
  715. canvas_set_font(canvas, FontPrimary);
  716. canvas_draw_str(canvas, 75, 35, "Machine");
  717. canvas_draw_icon(canvas, 119, 25, &I_Suica_ShopPin);
  718. furi_string_printf(
  719. buffer, "%01d:%03d:%03d", history.area_code, history.shop_code[0], history.shop_code[1]);
  720. canvas_set_font(canvas, FontKeyboard);
  721. canvas_draw_str(canvas, 75, 45, furi_string_get_cstr(buffer));
  722. // Animate Vending Machine Flap
  723. if(model->animator_tick > 6) {
  724. // 6 steps of animation
  725. model->animator_tick = 0;
  726. }
  727. switch(model->animator_tick) {
  728. case 0:
  729. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlapHollow);
  730. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlap1);
  731. break;
  732. case 1:
  733. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlapHollow);
  734. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlap2);
  735. break;
  736. case 2:
  737. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlapHollow);
  738. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlap3);
  739. break;
  740. case 3:
  741. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlapHollow);
  742. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlap3);
  743. canvas_draw_icon(canvas, 59, 45, &I_Suica_VendingCan1);
  744. break;
  745. case 4:
  746. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlapHollow);
  747. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlap2);
  748. canvas_draw_icon(canvas, 74, 48, &I_Suica_VendingCan2);
  749. break;
  750. case 5:
  751. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlapHollow);
  752. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlap1);
  753. canvas_draw_icon(canvas, 89, 51, &I_Suica_VendingCan3);
  754. break;
  755. case 6:
  756. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlapHollow);
  757. canvas_draw_icon(canvas, 44, 40, &I_Suica_VendingFlap1);
  758. canvas_draw_icon(canvas, 110, 54, &I_Suica_VendingCan4);
  759. break;
  760. default:
  761. break;
  762. }
  763. furi_string_free(buffer);
  764. }
  765. static void
  766. suica_draw_store_page_1(Canvas* canvas, SuicaHistory history, SuicaHistoryViewModel* model) {
  767. FuriString* buffer = furi_string_alloc();
  768. furi_string_printf(buffer, "%d", history.balance_change);
  769. canvas_draw_icon(canvas, 0, 15, &I_Suica_StoreP1Counter);
  770. canvas_set_font(canvas, FontPrimary);
  771. canvas_draw_str_aligned(canvas, 99, 39, AlignRight, AlignBottom, furi_string_get_cstr(buffer));
  772. canvas_draw_icon(canvas, 59, 27, &I_Suica_StoreReceiptDashLine);
  773. // Animate Taxi and LCD Refresh
  774. if(model->animator_tick > 11) {
  775. // 14 steps of animation
  776. model->animator_tick = 0;
  777. }
  778. switch(model->animator_tick % 6) {
  779. case 0:
  780. case 1:
  781. case 2:
  782. canvas_draw_icon(canvas, 41, 18, &I_Suica_StoreReceiptFrame1);
  783. break;
  784. case 3:
  785. case 4:
  786. case 5:
  787. canvas_draw_icon(canvas, 41, 18, &I_Suica_StoreReceiptFrame2);
  788. break;
  789. default:
  790. break;
  791. }
  792. switch(model->animator_tick % 6) {
  793. case 0:
  794. case 1:
  795. canvas_draw_icon(canvas, 0, 24, &I_Suica_StoreLightningVertical);
  796. break;
  797. case 2:
  798. case 3:
  799. canvas_draw_icon(canvas, 3, 31, &I_Suica_StoreLightningHorizontal);
  800. break;
  801. case 4:
  802. case 5:
  803. canvas_draw_icon(canvas, 0, 24, &I_Suica_StoreLightningVertical);
  804. canvas_draw_icon(canvas, 3, 31, &I_Suica_StoreLightningHorizontal);
  805. break;
  806. default:
  807. break;
  808. }
  809. furi_string_free(buffer);
  810. }
  811. static void
  812. suica_draw_store_page_2(Canvas* canvas, SuicaHistory history, SuicaHistoryViewModel* model) {
  813. FuriString* buffer = furi_string_alloc();
  814. // Clock Component
  815. canvas_set_color(canvas, ColorWhite); // Erase part of old frame to allow for new frame
  816. canvas_draw_line(canvas, 91, 9, 94, 6);
  817. canvas_draw_line(canvas, 57, 9, 93, 9);
  818. canvas_set_color(canvas, ColorBlack);
  819. furi_string_printf(buffer, "%02d:%02d", history.hour, history.minute);
  820. canvas_draw_line(canvas, 63, 21, 60, 18);
  821. canvas_set_font(canvas, FontKeyboard);
  822. canvas_draw_str(canvas, 63, 19, furi_string_get_cstr(buffer));
  823. canvas_draw_line(canvas, 91, 21, 94, 18);
  824. canvas_draw_line(canvas, 64, 21, 91, 21);
  825. canvas_draw_line(canvas, 94, 6, 94, 17);
  826. canvas_draw_line(canvas, 60, 12, 60, 17);
  827. canvas_draw_line(canvas, 60, 12, 57, 9);
  828. // Machine Code
  829. canvas_set_font(canvas, FontPrimary);
  830. canvas_draw_str(canvas, 75, 35, "Store");
  831. canvas_draw_icon(canvas, 104, 25, &I_Suica_ShopPin);
  832. furi_string_printf(
  833. buffer, "%01d:%03d:%03d", history.area_code, history.shop_code[0], history.shop_code[1]);
  834. canvas_set_font(canvas, FontKeyboard);
  835. canvas_draw_str(canvas, 75, 45, furi_string_get_cstr(buffer));
  836. // Store Frame
  837. canvas_draw_icon(canvas, 0, 13, &I_Suica_StoreFrame);
  838. // Sliding Door
  839. uint8_t door_position[7] = {20, 18, 14, 6, 2, 0, 0};
  840. if(model->animator_tick > 20) {
  841. // 14 steps of animation
  842. model->animator_tick = 0;
  843. }
  844. if(model->animator_tick < 7) {
  845. canvas_draw_icon(
  846. canvas, -1 - door_position[6 - model->animator_tick], 28, &I_Suica_StoreSlidingDoor);
  847. } else if(model->animator_tick < 14) {
  848. canvas_draw_icon(
  849. canvas, -1 - door_position[model->animator_tick - 7], 28, &I_Suica_StoreSlidingDoor);
  850. } else {
  851. canvas_draw_icon(canvas, -1, 28, &I_Suica_StoreSlidingDoor);
  852. }
  853. // Animate Neon and Fan
  854. switch(model->animator_tick % 4) {
  855. case 0:
  856. case 1:
  857. canvas_draw_icon(canvas, 37, 18, &I_Suica_StoreFan1);
  858. break;
  859. case 2:
  860. case 3:
  861. canvas_draw_icon(canvas, 37, 18, &I_Suica_StoreFan2);
  862. break;
  863. default:
  864. break;
  865. }
  866. furi_string_free(buffer);
  867. }
  868. static void
  869. suica_draw_balance_page(Canvas* canvas, SuicaHistory history, SuicaHistoryViewModel* model) {
  870. FuriString* buffer = furi_string_alloc();
  871. // Balance
  872. canvas_set_font(canvas, FontBigNumbers);
  873. canvas_draw_icon(canvas, 0, 48, &I_Suica_YenSign);
  874. canvas_draw_icon(canvas, 111, 48, &I_Suica_YenKanji);
  875. furi_string_printf(buffer, "%d", history.balance);
  876. canvas_draw_str_aligned(
  877. canvas, 109, 64, AlignRight, AlignBottom, furi_string_get_cstr(buffer));
  878. furi_string_printf(buffer, "%d", history.previous_balance);
  879. canvas_draw_str_aligned(
  880. canvas, 109, 26, AlignRight, AlignBottom, furi_string_get_cstr(buffer));
  881. furi_string_printf(buffer, "%d", history.balance_change);
  882. canvas_draw_str_aligned(
  883. canvas, 109, 43, AlignRight, AlignBottom, furi_string_get_cstr(buffer));
  884. // Separator
  885. canvas_draw_line(canvas, 26, 45, 128, 45);
  886. canvas_draw_line(canvas, 26, 46, 128, 46);
  887. if(history.balance_sign == SuicaBalanceAdd) {
  888. // Animate plus sign
  889. if(model->animator_tick > 2) {
  890. // 9 steps of animation
  891. model->animator_tick = 0;
  892. }
  893. switch(model->animator_tick) {
  894. case 0:
  895. canvas_draw_icon(canvas, 28, 28, &I_Suica_PlusSign1);
  896. break;
  897. case 1:
  898. canvas_draw_icon(canvas, 27, 27, &I_Suica_PlusSign2);
  899. break;
  900. case 2:
  901. canvas_draw_icon(canvas, 26, 26, &I_Suica_PlusSign3);
  902. break;
  903. default:
  904. break;
  905. }
  906. } else if(history.balance_sign == SuicaBalanceSub) {
  907. // Animate plus sign
  908. if(model->animator_tick > 12) {
  909. // 9 steps of animation
  910. model->animator_tick = 0;
  911. }
  912. switch(model->animator_tick) {
  913. case 0:
  914. case 1:
  915. case 2:
  916. case 3:
  917. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign0);
  918. break;
  919. case 4:
  920. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign1);
  921. break;
  922. case 5:
  923. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign2);
  924. break;
  925. case 6:
  926. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign3);
  927. break;
  928. case 7:
  929. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign4);
  930. break;
  931. case 8:
  932. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign5);
  933. break;
  934. case 9:
  935. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign6);
  936. break;
  937. case 10:
  938. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign7);
  939. break;
  940. case 11:
  941. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign8);
  942. break;
  943. case 12:
  944. canvas_draw_icon(canvas, 28, 32, &I_Suica_MinusSign9);
  945. break;
  946. default:
  947. break;
  948. }
  949. } else {
  950. canvas_draw_str(canvas, 30, 28, "=");
  951. }
  952. }
  953. static void suica_history_draw_callback(Canvas* canvas, void* model) {
  954. canvas_set_bitmap_mode(canvas, true);
  955. SuicaHistoryViewModel* my_model = (SuicaHistoryViewModel*)model;
  956. FuriString* buffer = furi_string_alloc();
  957. // catch the case where the page and entry are not initialized
  958. if(my_model->entry > my_model->size || my_model->entry < 1) {
  959. my_model->entry = 1;
  960. }
  961. // Get previous balance if we are not at the earliest entry
  962. if(my_model->entry < my_model->size) {
  963. my_model->history.previous_balance = my_model->travel_history[(my_model->entry * 16) + 10];
  964. my_model->history.previous_balance |= my_model->travel_history[(my_model->entry * 16) + 11]
  965. << 8;
  966. } else {
  967. my_model->history.previous_balance = 0;
  968. }
  969. // Calculate balance change
  970. if(my_model->history.previous_balance < my_model->history.balance) {
  971. my_model->history.balance_change =
  972. my_model->history.balance - my_model->history.previous_balance;
  973. my_model->history.balance_sign = SuicaBalanceAdd;
  974. } else if(my_model->history.previous_balance > my_model->history.balance) {
  975. my_model->history.balance_change =
  976. my_model->history.previous_balance - my_model->history.balance;
  977. my_model->history.balance_sign = SuicaBalanceSub;
  978. } else {
  979. my_model->history.balance_change = 0;
  980. my_model->history.balance_sign = SuicaBalanceEqual;
  981. }
  982. // Main title
  983. canvas_set_font(canvas, FontPrimary);
  984. canvas_draw_str(canvas, 0, 8, "Suica");
  985. // Date
  986. furi_string_printf(
  987. buffer,
  988. "20%02d-%02d-%02d",
  989. my_model->history.year,
  990. my_model->history.month,
  991. my_model->history.day);
  992. canvas_set_font(canvas, FontPrimary);
  993. canvas_draw_str(canvas, 34, 8, furi_string_get_cstr(buffer));
  994. // Entry Num
  995. furi_string_printf(buffer, "%02d/%02d", my_model->entry, my_model->size);
  996. canvas_set_font(canvas, FontPrimary);
  997. canvas_draw_str(canvas, 99, 8, furi_string_get_cstr(buffer));
  998. // Frame
  999. canvas_draw_line(canvas, 0, 9, 26, 9);
  1000. canvas_draw_line(canvas, 27, 9, 29, 7);
  1001. canvas_draw_line(canvas, 29, 0, 29, 6);
  1002. canvas_draw_line(canvas, 31, 0, 31, 7);
  1003. canvas_draw_line(canvas, 33, 9, 31, 7);
  1004. canvas_draw_line(canvas, 90, 9, 34, 9);
  1005. canvas_draw_line(canvas, 91, 9, 94, 6);
  1006. canvas_draw_line(canvas, 94, 0, 94, 6);
  1007. canvas_draw_line(canvas, 96, 0, 96, 6);
  1008. canvas_draw_line(canvas, 99, 9, 96, 6);
  1009. canvas_draw_line(canvas, 100, 9, 128, 9);
  1010. switch((uint8_t)my_model->page) {
  1011. case 0:
  1012. switch(my_model->history.history_type) {
  1013. case SuicaHistoryTrain:
  1014. suica_draw_train_page_1(canvas, my_model->history, my_model, false);
  1015. break;
  1016. case SuicaHistoryHappyBirthday:
  1017. suica_draw_train_page_1(canvas, my_model->history, my_model, true);
  1018. break;
  1019. case SuicaHistoryVendingMachine:
  1020. suica_draw_vending_machine_page_1(canvas, my_model->history, my_model);
  1021. break;
  1022. case SuicaHistoryPosAndTaxi:
  1023. suica_draw_store_page_1(canvas, my_model->history, my_model);
  1024. break;
  1025. default:
  1026. break;
  1027. }
  1028. break;
  1029. case 1:
  1030. switch(my_model->history.history_type) {
  1031. case SuicaHistoryTrain:
  1032. suica_draw_train_page_2(canvas, my_model->history, my_model);
  1033. break;
  1034. case SuicaHistoryHappyBirthday:
  1035. suica_draw_birthday_page_2(canvas, my_model->history, my_model);
  1036. break;
  1037. case SuicaHistoryVendingMachine:
  1038. suica_draw_vending_machine_page_2(canvas, my_model->history, my_model);
  1039. break;
  1040. case SuicaHistoryPosAndTaxi:
  1041. suica_draw_store_page_2(canvas, my_model->history, my_model);
  1042. break;
  1043. default:
  1044. break;
  1045. }
  1046. break;
  1047. case 2:
  1048. suica_draw_balance_page(canvas, my_model->history, my_model);
  1049. break;
  1050. default:
  1051. break;
  1052. }
  1053. furi_string_free(buffer);
  1054. }
  1055. static void suica_parse_detail_callback(GuiButtonType result, InputType type, void* context) {
  1056. Metroflip* app = context;
  1057. UNUSED(result);
  1058. if(type == InputTypeShort) {
  1059. SuicaHistoryViewModel* my_model = view_get_model(app->suica_context->view_history);
  1060. suica_parse(my_model);
  1061. FURI_LOG_I(TAG, "Draw Callback: We have %d entries", my_model->size);
  1062. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewCanvas);
  1063. }
  1064. }
  1065. static uint32_t suica_navigation_raw_callback(void* _context) {
  1066. UNUSED(_context);
  1067. return MetroflipViewWidget;
  1068. }
  1069. static NfcCommand suica_poller_callback(NfcGenericEvent event, void* context) {
  1070. furi_assert(event.protocol == NfcProtocolFelica);
  1071. NfcCommand command = NfcCommandContinue;
  1072. MetroflipPollerEventType stage = MetroflipPollerEventTypeStart;
  1073. Metroflip* app = context;
  1074. FuriString* parsed_data = furi_string_alloc();
  1075. SuicaHistoryViewModel* model = view_get_model(app->suica_context->view_history);
  1076. Widget* widget = app->widget;
  1077. const uint16_t service_code[2] = {SERVICE_CODE_HISTORY_IN_LE, SERVICE_CODE_TAPS_LOG_IN_LE};
  1078. const FelicaPollerEvent* felica_event = event.event_data;
  1079. FelicaPollerReadCommandResponse* rx_resp;
  1080. rx_resp->SF1 = 0;
  1081. rx_resp->SF2 = 0;
  1082. uint8_t blocks[1] = {0x00};
  1083. FelicaPoller* felica_poller = event.instance;
  1084. FURI_LOG_I(TAG, "Poller set");
  1085. if(felica_event->type == FelicaPollerEventTypeRequestAuthContext &&
  1086. felica_poller->data->pmm.data[1] == SUICA_IC_TYPE_CODE) {
  1087. view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardDetected);
  1088. command = NfcCommandContinue;
  1089. if(stage == MetroflipPollerEventTypeStart) {
  1090. nfc_device_set_data(
  1091. app->nfc_device, NfcProtocolFelica, nfc_poller_get_data(app->poller));
  1092. furi_string_printf(parsed_data, "\e#Suica\n");
  1093. FelicaError error = FelicaErrorNone;
  1094. int service_code_index = 0;
  1095. // Authenticate with the card
  1096. // Iterate through the two services
  1097. while(service_code_index < 2 && error == FelicaErrorNone) {
  1098. furi_string_cat_printf(
  1099. parsed_data, "%s: \n", suica_service_names[service_code_index]);
  1100. rx_resp->SF1 = 0;
  1101. rx_resp->SF2 = 0;
  1102. blocks[0] = 0; // firmware api requires this to be a list
  1103. while((rx_resp->SF1 + rx_resp->SF2) == 0 &&
  1104. blocks[0] < SUICA_MAX_HISTORY_ENTRIES && error == FelicaErrorNone) {
  1105. uint8_t block_data[16] = {0};
  1106. error = felica_poller_read_blocks(
  1107. felica_poller, 1, blocks, service_code[service_code_index], &rx_resp);
  1108. if(error != FelicaErrorNone) {
  1109. view_dispatcher_send_custom_event(
  1110. app->view_dispatcher, MetroflipCustomEventCardLost);
  1111. command = NfcCommandStop;
  1112. break;
  1113. }
  1114. furi_string_cat_printf(parsed_data, "Block %02X\n", blocks[0]);
  1115. blocks[0]++;
  1116. for(size_t i = 0; i < FELICA_DATA_BLOCK_SIZE; i++) {
  1117. furi_string_cat_printf(parsed_data, "%02X ", rx_resp->data[i]);
  1118. block_data[i] = rx_resp->data[i];
  1119. }
  1120. furi_string_cat_printf(parsed_data, "\n");
  1121. if(service_code_index == 0) {
  1122. FURI_LOG_I(
  1123. TAG,
  1124. "Service code %d, adding entry %x",
  1125. service_code_index,
  1126. model->size);
  1127. suica_add_entry(model, block_data);
  1128. }
  1129. }
  1130. service_code_index++;
  1131. }
  1132. metroflip_app_blink_stop(app);
  1133. if(model->size == 1) { // Have to let the poller run once before knowing we failed
  1134. furi_string_printf(
  1135. parsed_data,
  1136. "\e#Suica\nSorry, no data found.\nPlease let the developers know and we will add support.");
  1137. }
  1138. widget_add_text_scroll_element(
  1139. widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
  1140. widget_add_button_element(
  1141. widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
  1142. if(model->size > 1) {
  1143. widget_add_button_element(
  1144. widget, GuiButtonTypeCenter, "Parse", suica_parse_detail_callback, app);
  1145. }
  1146. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
  1147. }
  1148. }
  1149. furi_string_free(parsed_data);
  1150. command = NfcCommandStop;
  1151. return command;
  1152. }
  1153. static bool suica_history_input_callback(InputEvent* event, void* context) {
  1154. Metroflip* app = (Metroflip*)context;
  1155. if(event->type == InputTypeShort) {
  1156. switch(event->key) {
  1157. case InputKeyLeft: {
  1158. bool redraw = true;
  1159. with_view_model(
  1160. app->suica_context->view_history,
  1161. SuicaHistoryViewModel * model,
  1162. {
  1163. if(model->entry > 1) {
  1164. model->entry--;
  1165. }
  1166. suica_parse(model);
  1167. FURI_LOG_I(TAG, "Viewing entry %d", model->entry);
  1168. },
  1169. redraw);
  1170. break;
  1171. }
  1172. case InputKeyRight: {
  1173. bool redraw = true;
  1174. with_view_model(
  1175. app->suica_context->view_history,
  1176. SuicaHistoryViewModel * model,
  1177. {
  1178. if(model->entry < model->size) {
  1179. model->entry++;
  1180. }
  1181. suica_parse(model);
  1182. FURI_LOG_I(TAG, "Viewing entry %d", model->entry);
  1183. },
  1184. redraw);
  1185. break;
  1186. }
  1187. case InputKeyUp: {
  1188. bool redraw = true;
  1189. with_view_model(
  1190. app->suica_context->view_history,
  1191. SuicaHistoryViewModel * model,
  1192. {
  1193. if(model->page > 0) {
  1194. model->page--;
  1195. }
  1196. },
  1197. redraw);
  1198. break;
  1199. }
  1200. case InputKeyDown: {
  1201. bool redraw = true;
  1202. with_view_model(
  1203. app->suica_context->view_history,
  1204. SuicaHistoryViewModel * model,
  1205. {
  1206. if(model->page < HISTORY_VIEW_PAGE_NUM - 1) {
  1207. model->page++;
  1208. }
  1209. },
  1210. redraw);
  1211. break;
  1212. }
  1213. default:
  1214. // Handle other keys or do nothing
  1215. break;
  1216. }
  1217. }
  1218. return false;
  1219. }
  1220. static void suica_view_history_timer_callback(void* context) {
  1221. Metroflip* app = (Metroflip*)context;
  1222. view_dispatcher_send_custom_event(app->view_dispatcher, 0);
  1223. }
  1224. static void suica_view_history_enter_callback(void* context) {
  1225. uint32_t period = furi_ms_to_ticks(ARROW_ANIMATION_FRAME_MS);
  1226. Metroflip* app = (Metroflip*)context;
  1227. furi_assert(app->suica_context->timer == NULL);
  1228. app->suica_context->timer =
  1229. furi_timer_alloc(suica_view_history_timer_callback, FuriTimerTypePeriodic, context);
  1230. furi_timer_start(app->suica_context->timer, period);
  1231. }
  1232. static void suica_view_history_exit_callback(void* context) {
  1233. Metroflip* app = (Metroflip*)context;
  1234. furi_timer_stop(app->suica_context->timer);
  1235. furi_timer_free(app->suica_context->timer);
  1236. app->suica_context->timer = NULL;
  1237. }
  1238. static bool suica_view_history_custom_event_callback(uint32_t event, void* context) {
  1239. Metroflip* app = (Metroflip*)context;
  1240. switch(event) {
  1241. case 0:
  1242. // Redraw screen by passing true to last parameter of with_view_model.
  1243. {
  1244. bool redraw = true;
  1245. with_view_model(
  1246. app->suica_context->view_history,
  1247. SuicaHistoryViewModel * model,
  1248. { model->animator_tick++; },
  1249. redraw);
  1250. return true;
  1251. }
  1252. default:
  1253. return false;
  1254. }
  1255. }
  1256. static void suica_on_enter(Metroflip* app) {
  1257. // Gui* gui = furi_record_open(RECORD_GUI);
  1258. dolphin_deed(DolphinDeedNfcRead);
  1259. if(app->data_loaded == false) {
  1260. app->suica_context = malloc(sizeof(SuicaContext));
  1261. app->suica_context->view_history = view_alloc();
  1262. view_set_context(app->suica_context->view_history, app);
  1263. view_allocate_model(
  1264. app->suica_context->view_history,
  1265. ViewModelTypeLockFree,
  1266. sizeof(SuicaHistoryViewModel));
  1267. }
  1268. view_set_input_callback(app->suica_context->view_history, suica_history_input_callback);
  1269. view_set_previous_callback(app->suica_context->view_history, suica_navigation_raw_callback);
  1270. view_set_enter_callback(app->suica_context->view_history, suica_view_history_enter_callback);
  1271. view_set_exit_callback(app->suica_context->view_history, suica_view_history_exit_callback);
  1272. view_set_custom_callback(
  1273. app->suica_context->view_history, suica_view_history_custom_event_callback);
  1274. view_set_draw_callback(app->suica_context->view_history, suica_history_draw_callback);
  1275. view_dispatcher_add_view(
  1276. app->view_dispatcher, MetroflipViewCanvas, app->suica_context->view_history);
  1277. if(app->data_loaded == false) {
  1278. popup_set_header(app->popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
  1279. popup_set_icon(app->popup, 0, 3, &I_RFIDDolphinReceive_97x61);
  1280. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
  1281. nfc_scanner_alloc(app->nfc);
  1282. app->poller = nfc_poller_alloc(app->nfc, NfcProtocolFelica);
  1283. nfc_poller_start(app->poller, suica_poller_callback, app);
  1284. FURI_LOG_I(TAG, "Poller started");
  1285. metroflip_app_blink_start(app);
  1286. } else {
  1287. SuicaHistoryViewModel* model = view_get_model(app->suica_context->view_history);
  1288. suica_model_initialize_after_load(model);
  1289. Widget* widget = app->widget;
  1290. FuriString* parsed_data = furi_string_alloc();
  1291. furi_string_printf(parsed_data, "\e#Suica\n");
  1292. for(uint8_t i = 0; i < model->size; i++) {
  1293. furi_string_cat_printf(parsed_data, "Block %02X\n", i);
  1294. for(size_t j = 0; j < FELICA_DATA_BLOCK_SIZE; j++) {
  1295. furi_string_cat_printf(parsed_data, "%02X ", model->travel_history[i * 16 + j]);
  1296. }
  1297. furi_string_cat_printf(parsed_data, "\n");
  1298. }
  1299. widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
  1300. widget_add_button_element(
  1301. widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
  1302. if(model->size > 1) {
  1303. widget_add_button_element(
  1304. widget, GuiButtonTypeCenter, "Parse", suica_parse_detail_callback, app);
  1305. }
  1306. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
  1307. }
  1308. }
  1309. static bool suica_on_event(Metroflip* app, SceneManagerEvent event) {
  1310. bool consumed = false;
  1311. Popup* popup = app->popup;
  1312. if(event.type == SceneManagerEventTypeCustom) {
  1313. if(event.event == MetroflipCustomEventCardDetected) {
  1314. popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop);
  1315. consumed = true;
  1316. } else if(event.event == MetroflipCustomEventCardLost) {
  1317. popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop);
  1318. // popup_set_timeout(popup, 2000);
  1319. // popup_enable_timeout(popup);
  1320. // view_dispatcher_switch_to_view(app->view_dispatcher, SuicaViewPopup);
  1321. // popup_disable_timeout(popup);
  1322. scene_manager_search_and_switch_to_previous_scene(
  1323. app->scene_manager, MetroflipSceneStart);
  1324. consumed = true;
  1325. } else if(event.event == MetroflipCustomEventWrongCard) {
  1326. popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop);
  1327. scene_manager_search_and_switch_to_previous_scene(
  1328. app->scene_manager, MetroflipSceneStart);
  1329. consumed = true;
  1330. } else if(event.event == MetroflipCustomEventPollerFail) {
  1331. popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop);
  1332. scene_manager_search_and_switch_to_previous_scene(
  1333. app->scene_manager, MetroflipSceneStart);
  1334. consumed = true;
  1335. }
  1336. } else if(event.type == SceneManagerEventTypeBack) {
  1337. UNUSED(popup);
  1338. scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
  1339. consumed = true;
  1340. }
  1341. return consumed;
  1342. }
  1343. static void suica_on_exit(Metroflip* app) {
  1344. widget_reset(app->widget);
  1345. view_free(app->suica_context->view_history);
  1346. view_dispatcher_remove_view(app->view_dispatcher, MetroflipViewCanvas);
  1347. free(app->suica_context);
  1348. metroflip_app_blink_stop(app);
  1349. if(app->poller) {
  1350. nfc_poller_stop(app->poller);
  1351. nfc_poller_free(app->poller);
  1352. }
  1353. }
  1354. /* Actual implementation of app<>plugin interface */
  1355. static const MetroflipPlugin suica_plugin = {
  1356. .card_name = "Suica",
  1357. .plugin_on_enter = suica_on_enter,
  1358. .plugin_on_event = suica_on_event,
  1359. .plugin_on_exit = suica_on_exit,
  1360. };
  1361. /* Plugin descriptor to comply with basic plugin specification */
  1362. static const FlipperAppPluginDescriptor suica_plugin_descriptor = {
  1363. .appid = METROFLIP_SUPPORTED_CARD_PLUGIN_APP_ID,
  1364. .ep_api_version = METROFLIP_SUPPORTED_CARD_PLUGIN_API_VERSION,
  1365. .entry_point = &suica_plugin,
  1366. };
  1367. /* Plugin entry point - must return a pointer to const descriptor */
  1368. const FlipperAppPluginDescriptor* suica_plugin_ep(void) {
  1369. return &suica_plugin_descriptor;
  1370. }