playlist.c 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  1. #include <furi.h>
  2. #include <gui/gui.h>
  3. #include <input/input.h>
  4. #include <dialogs/dialogs.h>
  5. #include <storage/storage.h>
  6. #include <lib/toolbox/path.h>
  7. #include <subghz_playlist_icons.h>
  8. #include <lib/subghz/protocols/protocol_items.h>
  9. #include <flipper_format/flipper_format_i.h>
  10. #include "helpers/subghz_txrx.h"
  11. #include <lib/subghz/blocks/custom_btn.h>
  12. #include <lib/subghz/protocols/raw.h>
  13. #include "flipper_format_stream.h"
  14. #include "flipper_format_stream_i.h"
  15. #include "playlist_file.h"
  16. #include "canvas_helper.h"
  17. #define PLAYLIST_EXT ".txt"
  18. #define TAG "Playlist"
  19. #define STATE_NONE 0
  20. #define STATE_OVERVIEW 1
  21. #define STATE_SENDING 2
  22. #define WIDTH 128
  23. #define HEIGHT 64
  24. typedef struct {
  25. int current_count; // number of processed files
  26. int total_count; // number of items in the playlist
  27. int playlist_repetitions; // number of times to repeat the whole playlist
  28. int current_playlist_repetition; // current playlist repetition
  29. // last 3 files
  30. FuriString* prev_0_text; // current file
  31. FuriString* prev_1_text; // previous file
  32. FuriString* prev_2_text; // previous previous file
  33. FuriString* prev_3_text; // you get the idea
  34. int state; // current state
  35. ViewPort* view_port;
  36. FuriMutex* mutex;
  37. } DisplayMeta;
  38. typedef struct {
  39. FuriThread* thread;
  40. Storage* storage;
  41. FlipperFormat* format;
  42. DisplayMeta* meta;
  43. FuriString* file_path; // path to the playlist file
  44. SubGhzTxRx* txrx; // subghz txrx instance
  45. bool ctl_request_exit; // can be set to true if the worker should exit
  46. bool ctl_pause; // can be set to true if the worker should pause
  47. bool ctl_request_skip; // can be set to true if the worker should skip the current file
  48. bool ctl_request_prev; // can be set to true if the worker should go to the previous file
  49. bool is_running; // indicates if the worker is running
  50. bool raw_file_is_tx;
  51. } PlaylistWorker;
  52. typedef struct {
  53. FuriMutex* mutex;
  54. FuriMessageQueue* input_queue;
  55. ViewPort* view_port;
  56. Gui* gui;
  57. DisplayMeta* meta;
  58. PlaylistWorker* worker;
  59. FuriString* file_path; // Path to the playlist file
  60. } Playlist;
  61. ////////////////////////////////////////////////////////////////////////////////
  62. void meta_set_state(DisplayMeta* meta, int state) {
  63. meta->state = state;
  64. view_port_update(meta->view_port);
  65. }
  66. typedef struct SubGhzNeedSaveContext {
  67. PlaylistWorker* worker;
  68. SubGhzTxRx* txrx;
  69. char* file_path;
  70. } SubGhzNeedSaveContext;
  71. static void playlist_subghz_need_save_callback(void* context) {
  72. FURI_LOG_I(TAG, "Saving updated subghz signal");
  73. SubGhzNeedSaveContext* savectx = (SubGhzNeedSaveContext*)context;
  74. FlipperFormat* ff = subghz_txrx_get_fff_data(savectx->txrx);
  75. Stream* ff_stream = flipper_format_get_raw_stream(ff);
  76. flipper_format_delete_key(ff, "Repeat");
  77. do {
  78. if(!storage_simply_remove(savectx->worker->storage, savectx->file_path)) {
  79. FURI_LOG_E(TAG, "Failed to delete subghz file before re-save");
  80. break;
  81. }
  82. stream_seek(ff_stream, 0, StreamOffsetFromStart);
  83. stream_save_to_file(
  84. ff_stream, savectx->worker->storage, savectx->file_path, FSOM_CREATE_ALWAYS);
  85. if(storage_common_stat(savectx->worker->storage, savectx->file_path, NULL) != FSE_OK) {
  86. FURI_LOG_E(TAG, "Error verifying new subghz file after re-save");
  87. break;
  88. }
  89. } while(0);
  90. }
  91. void playlist_subghz_raw_end_callback(void* context) {
  92. FURI_LOG_I(TAG, "Stopping TX on RAW");
  93. furi_assert(context);
  94. PlaylistWorker* worker = context;
  95. worker->raw_file_is_tx = false;
  96. }
  97. // -4: missing protocol
  98. // -3: missing or wrong preset
  99. // -2: transmit error
  100. // -1: error
  101. // 0: ok
  102. // 1: resend
  103. // 2: exited
  104. static int playlist_worker_process(PlaylistWorker* worker, const char* file_name) {
  105. // actual sending of .sub file
  106. FuriString* preset_name = furi_string_alloc();
  107. FuriString* protocol_name = furi_string_alloc();
  108. FuriString* temp_str = furi_string_alloc();
  109. uint8_t status = 0;
  110. do {
  111. FlipperFormat* fff_data_file = flipper_format_file_alloc(worker->storage);
  112. SubGhzNeedSaveContext save_context = {worker, worker->txrx, (char*)file_name};
  113. subghz_txrx_set_need_save_callback(
  114. worker->txrx, playlist_subghz_need_save_callback, &save_context);
  115. Stream* fff_data_stream =
  116. flipper_format_get_raw_stream(subghz_txrx_get_fff_data(worker->txrx));
  117. stream_clean(fff_data_stream);
  118. furi_string_reset(temp_str);
  119. furi_string_reset(preset_name);
  120. furi_string_reset(protocol_name);
  121. worker->raw_file_is_tx = false;
  122. subghz_custom_btns_reset();
  123. uint32_t temp_data32;
  124. uint32_t frequency = 0;
  125. if(!flipper_format_file_open_existing(fff_data_file, file_name)) {
  126. FURI_LOG_E(TAG, "Error opening %s", file_name);
  127. flipper_format_free(fff_data_file);
  128. furi_string_free(preset_name);
  129. furi_string_free(protocol_name);
  130. furi_string_free(temp_str);
  131. return -1;
  132. }
  133. if(!flipper_format_read_header(fff_data_file, temp_str, &temp_data32)) {
  134. FURI_LOG_E(TAG, "Missing or incorrect header");
  135. flipper_format_file_close(fff_data_file);
  136. flipper_format_free(fff_data_file);
  137. furi_string_free(preset_name);
  138. furi_string_free(protocol_name);
  139. furi_string_free(temp_str);
  140. return -1;
  141. }
  142. if(((!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_KEY_FILE_TYPE)) ||
  143. (!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_RAW_FILE_TYPE))) &&
  144. temp_data32 == SUBGHZ_KEY_FILE_VERSION) {
  145. } else {
  146. FURI_LOG_E(TAG, "Type or version mismatch");
  147. flipper_format_file_close(fff_data_file);
  148. flipper_format_free(fff_data_file);
  149. furi_string_free(preset_name);
  150. furi_string_free(protocol_name);
  151. furi_string_free(temp_str);
  152. return -1;
  153. }
  154. SubGhzSetting* setting = subghz_txrx_get_setting(worker->txrx);
  155. if(!flipper_format_read_uint32(fff_data_file, "Frequency", &frequency, 1)) {
  156. FURI_LOG_W(TAG, "Missing Frequency. Setting default frequency");
  157. frequency = subghz_setting_get_default_frequency(setting);
  158. } else if(!subghz_txrx_radio_device_is_frequecy_valid(worker->txrx, frequency)) {
  159. FURI_LOG_E(TAG, "Frequency not supported on the chosen radio module");
  160. flipper_format_file_close(fff_data_file);
  161. flipper_format_free(fff_data_file);
  162. furi_string_free(preset_name);
  163. furi_string_free(protocol_name);
  164. furi_string_free(temp_str);
  165. return -1;
  166. }
  167. if(!flipper_format_read_string(fff_data_file, "Preset", temp_str)) {
  168. FURI_LOG_E(TAG, "Missing Preset");
  169. flipper_format_file_close(fff_data_file);
  170. flipper_format_free(fff_data_file);
  171. furi_string_free(preset_name);
  172. furi_string_free(protocol_name);
  173. furi_string_free(temp_str);
  174. return -3;
  175. }
  176. furi_string_set_str(
  177. temp_str, subghz_txrx_get_preset_name(worker->txrx, furi_string_get_cstr(temp_str)));
  178. if(!strcmp(furi_string_get_cstr(temp_str), "")) {
  179. FURI_LOG_E(TAG, "Unknown preset");
  180. flipper_format_file_close(fff_data_file);
  181. flipper_format_free(fff_data_file);
  182. furi_string_free(preset_name);
  183. furi_string_free(protocol_name);
  184. furi_string_free(temp_str);
  185. return -3;
  186. }
  187. if(!strcmp(furi_string_get_cstr(temp_str), "CUSTOM")) {
  188. subghz_setting_delete_custom_preset(setting, furi_string_get_cstr(temp_str));
  189. if(!subghz_setting_load_custom_preset(
  190. setting, furi_string_get_cstr(temp_str), fff_data_file)) {
  191. FURI_LOG_E(TAG, "Missing Custom preset");
  192. flipper_format_file_close(fff_data_file);
  193. flipper_format_free(fff_data_file);
  194. furi_string_free(preset_name);
  195. furi_string_free(protocol_name);
  196. furi_string_free(temp_str);
  197. return -1;
  198. }
  199. }
  200. furi_string_set(preset_name, temp_str);
  201. size_t preset_index =
  202. subghz_setting_get_inx_preset_by_name(setting, furi_string_get_cstr(preset_name));
  203. subghz_txrx_set_preset(
  204. worker->txrx,
  205. furi_string_get_cstr(preset_name),
  206. frequency,
  207. subghz_setting_get_preset_data(setting, preset_index),
  208. subghz_setting_get_preset_data_size(setting, preset_index));
  209. // Load Protocol
  210. if(!flipper_format_read_string(fff_data_file, "Protocol", protocol_name)) {
  211. FURI_LOG_E(TAG, "Missing protocol");
  212. flipper_format_file_close(fff_data_file);
  213. flipper_format_free(fff_data_file);
  214. furi_string_free(preset_name);
  215. furi_string_free(protocol_name);
  216. furi_string_free(temp_str);
  217. return -4;
  218. }
  219. FlipperFormat* fff_data = subghz_txrx_get_fff_data(worker->txrx);
  220. if(!strcmp(furi_string_get_cstr(protocol_name), "RAW")) {
  221. subghz_protocol_raw_gen_fff_data(
  222. fff_data, file_name, subghz_txrx_radio_device_get_name(worker->txrx));
  223. } else {
  224. stream_copy_full(
  225. flipper_format_get_raw_stream(fff_data_file),
  226. flipper_format_get_raw_stream(fff_data));
  227. }
  228. uint32_t repeat = 200;
  229. if(!flipper_format_insert_or_update_uint32(fff_data, "Repeat", &repeat, 1)) {
  230. FURI_LOG_E(TAG, "Unable Repeat");
  231. //
  232. }
  233. if(subghz_txrx_load_decoder_by_name_protocol(
  234. worker->txrx, furi_string_get_cstr(protocol_name))) {
  235. SubGhzProtocolStatus status = subghz_protocol_decoder_base_deserialize(
  236. subghz_txrx_get_decoder(worker->txrx), fff_data);
  237. if(status != SubGhzProtocolStatusOk) {
  238. flipper_format_file_close(fff_data_file);
  239. flipper_format_free(fff_data_file);
  240. furi_string_free(preset_name);
  241. furi_string_free(protocol_name);
  242. furi_string_free(temp_str);
  243. return -2;
  244. }
  245. } else {
  246. FURI_LOG_E(TAG, "Protocol not found: %s", furi_string_get_cstr(protocol_name));
  247. flipper_format_file_close(fff_data_file);
  248. flipper_format_free(fff_data_file);
  249. furi_string_free(preset_name);
  250. furi_string_free(protocol_name);
  251. furi_string_free(temp_str);
  252. return -2;
  253. }
  254. flipper_format_file_close(fff_data_file);
  255. flipper_format_free(fff_data_file);
  256. FURI_LOG_I(TAG, "Starting TX");
  257. if(subghz_txrx_tx_start(worker->txrx, subghz_txrx_get_fff_data(worker->txrx)) !=
  258. SubGhzTxRxStartTxStateOk) {
  259. FURI_LOG_E(TAG, "Failed to start TX");
  260. }
  261. bool skip_extra_stop = false;
  262. FURI_LOG_D(TAG, "Checking if file is RAW...");
  263. if(!strcmp(furi_string_get_cstr(protocol_name), "RAW")) {
  264. subghz_txrx_set_raw_file_encoder_worker_callback_end(
  265. worker->txrx, playlist_subghz_raw_end_callback, worker);
  266. worker->raw_file_is_tx = true;
  267. skip_extra_stop = true;
  268. }
  269. do {
  270. if(worker->ctl_request_exit) {
  271. FURI_LOG_D(TAG, " (TX) Requested to exit. Cancelling sending...");
  272. status = 2;
  273. break;
  274. }
  275. if(worker->ctl_pause) {
  276. FURI_LOG_D(TAG, " (TX) Requested to pause. Cancelling...");
  277. status = 1;
  278. break;
  279. }
  280. if(worker->ctl_request_skip) {
  281. worker->ctl_request_skip = false;
  282. FURI_LOG_D(TAG, " (TX) Requested to skip. Cancelling and skipping...");
  283. status = 0;
  284. break;
  285. }
  286. if(worker->ctl_request_prev) {
  287. worker->ctl_request_prev = false;
  288. FURI_LOG_D(TAG, " (TX) Requested to prev. Cancelling and resending...");
  289. status = 3;
  290. break;
  291. }
  292. furi_delay_ms(1);
  293. } while(worker->raw_file_is_tx);
  294. if(!worker->raw_file_is_tx && !skip_extra_stop) {
  295. if(worker->ctl_request_exit) {
  296. FURI_LOG_D(TAG, " (TX) Requested to exit. Cancelling sending...");
  297. status = 2;
  298. }
  299. if(worker->ctl_pause) {
  300. FURI_LOG_D(TAG, " (TX) Requested to pause. Cancelling...");
  301. status = 1;
  302. }
  303. if(worker->ctl_request_skip) {
  304. worker->ctl_request_skip = false;
  305. FURI_LOG_D(TAG, " (TX) Requested to skip. Cancelling and skipping...");
  306. status = 0;
  307. }
  308. if(worker->ctl_request_prev) {
  309. worker->ctl_request_prev = false;
  310. FURI_LOG_D(TAG, " (TX) Requested to prev. Cancelling and resending...");
  311. status = 3;
  312. }
  313. furi_delay_ms(1600);
  314. subghz_txrx_stop(worker->txrx);
  315. } else {
  316. furi_delay_ms(20);
  317. subghz_txrx_stop(worker->txrx);
  318. }
  319. skip_extra_stop = false;
  320. } while(false);
  321. FURI_LOG_D(TAG, "TX Finished");
  322. subghz_custom_btns_reset();
  323. furi_string_free(preset_name);
  324. furi_string_free(protocol_name);
  325. furi_string_free(temp_str);
  326. return status;
  327. }
  328. // true - the worker can continue
  329. // false - the worker should exit
  330. static bool playlist_worker_wait_pause(PlaylistWorker* worker) {
  331. // wait if paused
  332. while(worker->ctl_pause && !worker->ctl_request_exit) {
  333. furi_delay_ms(50);
  334. }
  335. // exit loop if requested to stop
  336. if(worker->ctl_request_exit) {
  337. FURI_LOG_D(TAG, "Requested to exit. Exiting loop...");
  338. return false;
  339. }
  340. return true;
  341. }
  342. void updatePlayListView(PlaylistWorker* worker, const char* str) {
  343. furi_check(furi_mutex_acquire(worker->meta->mutex, FuriWaitForever) == FuriStatusOk);
  344. furi_string_set(worker->meta->prev_3_text, furi_string_get_cstr(worker->meta->prev_2_text));
  345. furi_string_set(worker->meta->prev_2_text, furi_string_get_cstr(worker->meta->prev_1_text));
  346. furi_string_set(worker->meta->prev_1_text, furi_string_get_cstr(worker->meta->prev_0_text));
  347. furi_string_set(worker->meta->prev_0_text, str);
  348. furi_mutex_release(worker->meta->mutex);
  349. view_port_update(worker->meta->view_port);
  350. }
  351. static bool playlist_worker_play_playlist_once(
  352. PlaylistWorker* worker,
  353. FlipperFormat* fff_head,
  354. FuriString* data) {
  355. //
  356. if(!flipper_format_rewind(fff_head)) {
  357. FURI_LOG_E(TAG, "Failed to rewind file");
  358. return false;
  359. }
  360. while(flipper_format_read_string(fff_head, "sub", data)) {
  361. if(!playlist_worker_wait_pause(worker)) {
  362. break;
  363. }
  364. // update state to sending
  365. meta_set_state(worker->meta, STATE_SENDING);
  366. ++worker->meta->current_count;
  367. const char* str = furi_string_get_cstr(data);
  368. // it's not fancy, but it works for now :)
  369. FuriString* filename = furi_string_alloc();
  370. path_extract_filename(data, filename, true);
  371. updatePlayListView(worker, furi_string_get_cstr(filename));
  372. furi_string_free(filename);
  373. for(int i = 0; i < 1; i++) {
  374. if(!playlist_worker_wait_pause(worker)) {
  375. break;
  376. }
  377. view_port_update(worker->meta->view_port);
  378. FURI_LOG_D(TAG, "(worker) Sending %s", str);
  379. int status = playlist_worker_process(worker, str);
  380. FURI_LOG_D(TAG, "Finished file, status %d", status);
  381. // if there was an error, fff_file is not already freed
  382. // why do you do this with numbers without meaning x_x
  383. if(status < 0) {
  384. if(status > -5) {
  385. //
  386. }
  387. furi_check(
  388. furi_mutex_acquire(worker->meta->mutex, FuriWaitForever) == FuriStatusOk);
  389. furi_string_cat(worker->meta->prev_0_text, " FAIL");
  390. furi_mutex_release(worker->meta->mutex);
  391. view_port_update(worker->meta->view_port);
  392. }
  393. // re-send file is paused mid-send
  394. if(status == 1) {
  395. i -= 1;
  396. // errored, skip to next file
  397. } else if(status < 0) {
  398. break;
  399. // exited, exit loop
  400. } else if(status == 2) {
  401. return false;
  402. } else if(status == 3) {
  403. //aqui rebobinamos y avanzamos de nuevo el fichero n-1 veces
  404. //decrementamos el contador de ficheros enviados
  405. worker->meta->current_count--;
  406. if(worker->meta->current_count > 0) {
  407. worker->meta->current_count--;
  408. }
  409. //rebobinamos el fichero
  410. if(!flipper_format_rewind(fff_head)) {
  411. FURI_LOG_E(TAG, "Failed to rewind file");
  412. return false;
  413. }
  414. //avanzamos el fichero n-1 veces
  415. for(int j = 0; j < worker->meta->current_count; j++) {
  416. flipper_format_read_string(fff_head, "sub", data);
  417. }
  418. break;
  419. }
  420. }
  421. } // end of loop
  422. return true;
  423. }
  424. static int32_t playlist_worker_thread(void* ctx) {
  425. PlaylistWorker* worker = ctx;
  426. FlipperFormat* fff_head = flipper_format_file_alloc(worker->storage);
  427. if(!flipper_format_file_open_existing(fff_head, furi_string_get_cstr(worker->file_path))) {
  428. FURI_LOG_E(TAG, "Failed to open %s", furi_string_get_cstr(worker->file_path));
  429. worker->is_running = false;
  430. flipper_format_free(fff_head);
  431. return 0;
  432. }
  433. playlist_worker_wait_pause(worker);
  434. FuriString* data = furi_string_alloc();
  435. for(int i = 0; i < MAX(1, worker->meta->playlist_repetitions); i++) {
  436. // infinite repetitions if playlist_repetitions is 0
  437. if(worker->meta->playlist_repetitions <= 0) {
  438. --i;
  439. }
  440. ++worker->meta->current_playlist_repetition;
  441. // send playlist
  442. worker->meta->current_count = 0;
  443. if(worker->ctl_request_exit) {
  444. break;
  445. }
  446. FURI_LOG_D(
  447. TAG,
  448. "Sending playlist (i %d rep %d b %d)",
  449. i,
  450. worker->meta->current_playlist_repetition,
  451. worker->meta->playlist_repetitions);
  452. if(!playlist_worker_play_playlist_once(worker, fff_head, data)) {
  453. break;
  454. }
  455. }
  456. flipper_format_free(fff_head);
  457. furi_string_free(data);
  458. FURI_LOG_D(TAG, "Done reading. Read %d data lines.", worker->meta->current_count);
  459. worker->is_running = false;
  460. // update state to overview
  461. meta_set_state(worker->meta, STATE_OVERVIEW);
  462. return 0;
  463. }
  464. ////////////////////////////////////////////////////////////////////////////////
  465. void playlist_meta_reset(DisplayMeta* instance) {
  466. instance->current_count = 0;
  467. instance->current_playlist_repetition = 0;
  468. furi_string_reset(instance->prev_0_text);
  469. furi_string_reset(instance->prev_1_text);
  470. furi_string_reset(instance->prev_2_text);
  471. furi_string_reset(instance->prev_3_text);
  472. }
  473. DisplayMeta* playlist_meta_alloc() {
  474. DisplayMeta* instance = malloc(sizeof(DisplayMeta));
  475. instance->prev_0_text = furi_string_alloc();
  476. instance->prev_1_text = furi_string_alloc();
  477. instance->prev_2_text = furi_string_alloc();
  478. instance->prev_3_text = furi_string_alloc();
  479. playlist_meta_reset(instance);
  480. instance->state = STATE_NONE;
  481. instance->playlist_repetitions = 1;
  482. return instance;
  483. }
  484. void playlist_meta_free(DisplayMeta* instance) {
  485. furi_string_free(instance->prev_0_text);
  486. furi_string_free(instance->prev_1_text);
  487. furi_string_free(instance->prev_2_text);
  488. furi_string_free(instance->prev_3_text);
  489. free(instance);
  490. }
  491. ////////////////////////////////////////////////////////////////////////////////
  492. PlaylistWorker* playlist_worker_alloc(DisplayMeta* meta) {
  493. PlaylistWorker* instance = malloc(sizeof(PlaylistWorker));
  494. instance->thread = furi_thread_alloc();
  495. furi_thread_set_name(instance->thread, "PlaylistWorker");
  496. furi_thread_set_stack_size(instance->thread, 6 * 1024);
  497. furi_thread_set_context(instance->thread, instance);
  498. furi_thread_set_callback(instance->thread, playlist_worker_thread);
  499. instance->storage = furi_record_open(RECORD_STORAGE);
  500. instance->meta = meta;
  501. instance->ctl_pause = true; // require the user to manually start the worker
  502. instance->file_path = furi_string_alloc();
  503. instance->txrx = subghz_txrx_alloc();
  504. return instance;
  505. }
  506. void playlist_worker_free(PlaylistWorker* instance) {
  507. furi_assert(instance);
  508. furi_thread_free(instance->thread);
  509. furi_string_free(instance->file_path);
  510. subghz_txrx_free(instance->txrx);
  511. furi_record_close(RECORD_STORAGE);
  512. free(instance);
  513. }
  514. void playlist_worker_stop(PlaylistWorker* worker) {
  515. furi_assert(worker);
  516. furi_assert(worker->is_running);
  517. worker->ctl_request_exit = true;
  518. furi_thread_join(worker->thread);
  519. }
  520. bool playlist_worker_running(PlaylistWorker* worker) {
  521. furi_assert(worker);
  522. return worker->is_running;
  523. }
  524. void playlist_worker_start(PlaylistWorker* instance, const char* file_path) {
  525. furi_assert(instance);
  526. furi_assert(!instance->is_running);
  527. furi_string_set(instance->file_path, file_path);
  528. instance->is_running = true;
  529. // reset meta (current/total)
  530. playlist_meta_reset(instance->meta);
  531. FURI_LOG_D(TAG, "Starting thread...");
  532. furi_thread_start(instance->thread);
  533. }
  534. ////////////////////////////////////////////////////////////////////////////////
  535. static void render_callback(Canvas* canvas, void* ctx) {
  536. Playlist* app = ctx;
  537. furi_check(furi_mutex_acquire(app->mutex, FuriWaitForever) == FuriStatusOk);
  538. canvas_clear(canvas);
  539. canvas_set_color(canvas, ColorBlack);
  540. canvas_set_font(canvas, FontSecondary);
  541. FuriString* temp_str;
  542. temp_str = furi_string_alloc();
  543. switch(app->meta->state) {
  544. case STATE_NONE:
  545. canvas_set_font(canvas, FontPrimary);
  546. canvas_draw_str_aligned(
  547. canvas, WIDTH / 2, HEIGHT / 2, AlignCenter, AlignCenter, "No playlist loaded");
  548. break;
  549. case STATE_OVERVIEW:
  550. // draw file name
  551. {
  552. path_extract_filename(app->file_path, temp_str, true);
  553. canvas_set_font(canvas, FontPrimary);
  554. draw_centered_boxed_str(canvas, 1, 1, 15, 6, furi_string_get_cstr(temp_str));
  555. }
  556. canvas_set_font(canvas, FontSecondary);
  557. // draw loaded count
  558. {
  559. furi_string_printf(temp_str, "%d Items in playlist", app->meta->total_count);
  560. canvas_draw_str_aligned(
  561. canvas, 1, 19, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
  562. if(app->meta->playlist_repetitions <= 0) {
  563. furi_string_set(temp_str, "Repeat: inf");
  564. } else if(app->meta->playlist_repetitions == 1) {
  565. furi_string_set(temp_str, "Repeat: no");
  566. } else {
  567. furi_string_printf(temp_str, "Repeat: %dx", app->meta->playlist_repetitions);
  568. }
  569. canvas_draw_str_aligned(
  570. canvas, 1, 29, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
  571. }
  572. // draw buttons
  573. draw_corner_aligned(canvas, 40, 15, AlignCenter, AlignBottom);
  574. canvas_set_color(canvas, ColorWhite);
  575. canvas_draw_str_aligned(canvas, WIDTH / 2 - 7, HEIGHT - 11, AlignLeft, AlignTop, "Start");
  576. canvas_draw_disc(canvas, WIDTH / 2 - 14, HEIGHT - 8, 3);
  577. //
  578. canvas_set_color(canvas, ColorBlack);
  579. draw_corner_aligned(canvas, 20, 15, AlignLeft, AlignBottom);
  580. canvas_set_color(canvas, ColorWhite);
  581. canvas_draw_str_aligned(canvas, 4, HEIGHT - 11, AlignLeft, AlignTop, "R-");
  582. //
  583. canvas_set_color(canvas, ColorBlack);
  584. draw_corner_aligned(canvas, 20, 15, AlignRight, AlignBottom);
  585. canvas_set_color(canvas, ColorWhite);
  586. canvas_draw_str_aligned(canvas, WIDTH - 4, HEIGHT - 11, AlignRight, AlignTop, "R+");
  587. canvas_set_color(canvas, ColorBlack);
  588. break;
  589. case STATE_SENDING:
  590. canvas_set_color(canvas, ColorBlack);
  591. if(app->worker->ctl_pause) {
  592. canvas_draw_icon(canvas, 2, HEIGHT - 8, &I_ButtonRight_4x7);
  593. } else {
  594. canvas_draw_box(canvas, 2, HEIGHT - 8, 2, 7);
  595. canvas_draw_box(canvas, 5, HEIGHT - 8, 2, 7);
  596. }
  597. // draw progress text
  598. {
  599. canvas_set_font(canvas, FontSecondary);
  600. furi_string_printf(
  601. temp_str, "[%d/%d]", app->meta->current_count, app->meta->total_count);
  602. canvas_draw_str_aligned(
  603. canvas, 11, HEIGHT - 8, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
  604. int h = canvas_string_width(canvas, furi_string_get_cstr(temp_str));
  605. int xs = 11 + h + 2;
  606. int w = WIDTH - xs - 1;
  607. canvas_draw_box(canvas, xs, HEIGHT - 5, w, 1);
  608. float progress = (float)app->meta->current_count / (float)app->meta->total_count;
  609. int wp = (int)(progress * w);
  610. canvas_draw_box(canvas, xs + wp - 1, HEIGHT - 7, 2, 5);
  611. }
  612. {
  613. if(app->meta->playlist_repetitions <= 0) {
  614. furi_string_printf(temp_str, "[%d/Inf]", app->meta->current_playlist_repetition);
  615. } else {
  616. furi_string_printf(
  617. temp_str,
  618. "[%d/%d]",
  619. app->meta->current_playlist_repetition,
  620. app->meta->playlist_repetitions);
  621. }
  622. canvas_set_color(canvas, ColorBlack);
  623. int w = canvas_string_width(canvas, furi_string_get_cstr(temp_str));
  624. draw_corner_aligned(canvas, w + 6, 13, AlignRight, AlignTop);
  625. canvas_set_color(canvas, ColorWhite);
  626. canvas_draw_str_aligned(
  627. canvas, WIDTH - 3, 3, AlignRight, AlignTop, furi_string_get_cstr(temp_str));
  628. }
  629. // draw last and current file
  630. {
  631. canvas_set_color(canvas, ColorBlack);
  632. canvas_set_font(canvas, FontSecondary);
  633. // current
  634. if(!furi_string_empty(app->meta->prev_0_text)) {
  635. int w = canvas_string_width(canvas, furi_string_get_cstr(app->meta->prev_0_text));
  636. canvas_set_color(canvas, ColorBlack);
  637. canvas_draw_rbox(canvas, 1, 1, w + 4, 12, 2);
  638. canvas_set_color(canvas, ColorWhite);
  639. canvas_draw_str_aligned(
  640. canvas,
  641. 3,
  642. 3,
  643. AlignLeft,
  644. AlignTop,
  645. furi_string_get_cstr(app->meta->prev_0_text));
  646. }
  647. // last 3
  648. canvas_set_color(canvas, ColorBlack);
  649. if(!furi_string_empty(app->meta->prev_1_text)) {
  650. canvas_draw_str_aligned(
  651. canvas,
  652. 3,
  653. 15,
  654. AlignLeft,
  655. AlignTop,
  656. furi_string_get_cstr(app->meta->prev_1_text));
  657. }
  658. if(!furi_string_empty(app->meta->prev_2_text)) {
  659. canvas_draw_str_aligned(
  660. canvas,
  661. 3,
  662. 26,
  663. AlignLeft,
  664. AlignTop,
  665. furi_string_get_cstr(app->meta->prev_2_text));
  666. }
  667. if(!furi_string_empty(app->meta->prev_3_text)) {
  668. canvas_draw_str_aligned(
  669. canvas,
  670. 3,
  671. 37,
  672. AlignLeft,
  673. AlignTop,
  674. furi_string_get_cstr(app->meta->prev_3_text));
  675. }
  676. }
  677. break;
  678. default:
  679. break;
  680. }
  681. furi_string_free(temp_str);
  682. furi_mutex_release(app->mutex);
  683. }
  684. static void input_callback(InputEvent* event, void* ctx) {
  685. Playlist* app = ctx;
  686. furi_message_queue_put(app->input_queue, event, 0);
  687. }
  688. ////////////////////////////////////////////////////////////////////////////////
  689. Playlist* playlist_alloc(DisplayMeta* meta) {
  690. Playlist* app = malloc(sizeof(Playlist));
  691. app->file_path = furi_string_alloc();
  692. furi_string_set(app->file_path, PLAYLIST_FOLDER);
  693. app->meta = meta;
  694. app->worker = NULL;
  695. app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
  696. app->input_queue = furi_message_queue_alloc(32, sizeof(InputEvent));
  697. // view port
  698. app->view_port = view_port_alloc();
  699. view_port_draw_callback_set(app->view_port, render_callback, app);
  700. view_port_input_callback_set(app->view_port, input_callback, app);
  701. // gui
  702. app->gui = furi_record_open(RECORD_GUI);
  703. gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
  704. return app;
  705. }
  706. void playlist_start_worker(Playlist* app, DisplayMeta* meta) {
  707. app->worker = playlist_worker_alloc(meta);
  708. // count playlist items
  709. app->meta->total_count =
  710. playlist_count_playlist_items(app->worker->storage, furi_string_get_cstr(app->file_path));
  711. // start thread
  712. playlist_worker_start(app->worker, furi_string_get_cstr(app->file_path));
  713. }
  714. void playlist_free(Playlist* app) {
  715. furi_string_free(app->file_path);
  716. gui_remove_view_port(app->gui, app->view_port);
  717. furi_record_close(RECORD_GUI);
  718. view_port_free(app->view_port);
  719. furi_message_queue_free(app->input_queue);
  720. furi_mutex_free(app->mutex);
  721. playlist_meta_free(app->meta);
  722. free(app);
  723. }
  724. int32_t playlist_app(char* p) {
  725. // create playlist folder
  726. {
  727. Storage* storage = furi_record_open(RECORD_STORAGE);
  728. if(!storage_simply_mkdir(storage, PLAYLIST_FOLDER)) {
  729. FURI_LOG_E(TAG, "Could not create folder %s", PLAYLIST_FOLDER);
  730. }
  731. furi_record_close(RECORD_STORAGE);
  732. }
  733. // create app
  734. DisplayMeta* meta = playlist_meta_alloc();
  735. Playlist* app = playlist_alloc(meta);
  736. meta->view_port = app->view_port;
  737. meta->mutex = app->mutex;
  738. furi_hal_power_suppress_charge_enter();
  739. // select playlist file
  740. if(p && strlen(p)) {
  741. furi_string_set(app->file_path, p);
  742. } else {
  743. DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
  744. DialogsFileBrowserOptions browser_options;
  745. dialog_file_browser_set_basic_options(&browser_options, PLAYLIST_EXT, &I_sub1_10px);
  746. browser_options.base_path = PLAYLIST_FOLDER;
  747. const bool res =
  748. dialog_file_browser_show(dialogs, app->file_path, app->file_path, &browser_options);
  749. furi_record_close(RECORD_DIALOGS);
  750. // check if a file was selected
  751. if(!res) {
  752. FURI_LOG_E(TAG, "No file selected");
  753. goto exit_cleanup;
  754. }
  755. }
  756. ////////////////////////////////////////////////////////////////////////////////
  757. playlist_start_worker(app, meta);
  758. meta_set_state(app->meta, STATE_OVERVIEW);
  759. bool exit_loop = false;
  760. InputEvent input;
  761. while(1) { // close application if no file was selected
  762. furi_check(
  763. furi_message_queue_get(app->input_queue, &input, FuriWaitForever) == FuriStatusOk);
  764. furi_check(furi_mutex_acquire(app->mutex, FuriWaitForever) == FuriStatusOk);
  765. switch(input.key) {
  766. case InputKeyLeft:
  767. if(app->meta->state == STATE_OVERVIEW) {
  768. if(input.type == InputTypeShort && app->meta->playlist_repetitions > 0) {
  769. --app->meta->playlist_repetitions;
  770. }
  771. } else if(app->meta->state == STATE_SENDING) {
  772. if(input.type == InputTypeShort) {
  773. app->worker->ctl_request_prev = true;
  774. }
  775. }
  776. break;
  777. case InputKeyRight:
  778. if(app->meta->state == STATE_OVERVIEW) {
  779. if(input.type == InputTypeShort) {
  780. ++app->meta->playlist_repetitions;
  781. }
  782. } else if(app->meta->state == STATE_SENDING) {
  783. if(input.type == InputTypeShort) {
  784. app->worker->ctl_request_skip = true;
  785. }
  786. }
  787. break;
  788. case InputKeyOk:
  789. if(input.type == InputTypeShort) {
  790. // toggle pause state
  791. if(!app->worker->is_running) {
  792. app->worker->ctl_pause = false;
  793. app->worker->ctl_request_exit = false;
  794. playlist_worker_start(app->worker, furi_string_get_cstr(app->file_path));
  795. } else {
  796. app->worker->ctl_pause = !app->worker->ctl_pause;
  797. }
  798. }
  799. break;
  800. case InputKeyBack:
  801. FURI_LOG_D(TAG, "Pressed Back button. Application will exit");
  802. exit_loop = true;
  803. break;
  804. default:
  805. break;
  806. }
  807. furi_mutex_release(app->mutex);
  808. // exit application
  809. if(exit_loop == true) {
  810. break;
  811. }
  812. view_port_update(app->view_port);
  813. }
  814. exit_cleanup:
  815. furi_hal_power_suppress_charge_exit();
  816. if(app->worker != NULL) {
  817. if(playlist_worker_running(app->worker)) {
  818. FURI_LOG_D(TAG, "Thread is still running. Requesting thread to finish ...");
  819. playlist_worker_stop(app->worker);
  820. }
  821. FURI_LOG_D(TAG, "Freeing Worker ...");
  822. playlist_worker_free(app->worker);
  823. }
  824. FURI_LOG_D(TAG, "Freeing Playlist ...");
  825. playlist_free(app);
  826. return 0;
  827. }