action_qpl.c 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. // Methods for Quac Playlist transmission
  2. #include <toolbox/stream/stream.h>
  3. #include <toolbox/stream/file_stream.h>
  4. #include <toolbox/path.h>
  5. #include <toolbox/args.h>
  6. #include <notification/notification_messages.h>
  7. #include "action_i.h"
  8. #include "quac.h"
  9. /** Open the Playlist file and then transmit each action
  10. *
  11. * Each line of the playlist file is one of:
  12. * <file_path>
  13. * Full SD card path, or relative path to action to be transmitted. Must be
  14. * one of the supported filetypes (.sub, .rfid, [.ir coming soon])
  15. *
  16. * If an .rfid file has a space followed by a number, that will be the
  17. * duration for that RFID transmission. All other .rfid files will use
  18. * the value specified in the Settings
  19. *
  20. * pause <ms>
  21. * Pauses the playback for 'ms' milliseconds.
  22. *
  23. * Blank lines, and comments (start with '#') are ignored. Whitespace is trimmed.
  24. *
  25. */
  26. void action_qpl_tx(void* context, const FuriString* action_path, FuriString* error) {
  27. App* app = context;
  28. // Save the current durations, in case the are changed during playback
  29. uint32_t orig_rfid_duration = app->settings.rfid_duration;
  30. uint32_t orig_nfc_duration = app->settings.nfc_duration;
  31. uint32_t orig_ibutton_duration = app->settings.ibutton_duration;
  32. FuriString* buffer;
  33. buffer = furi_string_alloc();
  34. Stream* file = file_stream_alloc(app->storage);
  35. if(file_stream_open(file, furi_string_get_cstr(action_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
  36. while(stream_read_line(file, buffer)) {
  37. furi_string_trim(buffer); // remove '\n\r' line endings, cleanup spaces
  38. // FURI_LOG_I(TAG, "line: %s", furi_string_get_cstr(buffer));
  39. // Skip blank lines
  40. if(furi_string_size(buffer) == 0) {
  41. continue;
  42. }
  43. // Skip comments
  44. char first_char = furi_string_get_char(buffer, 0);
  45. if(first_char == '#') {
  46. continue;
  47. }
  48. // Check if buffer is a "command", and not just a filename
  49. // Commands will contain spaces
  50. bool processed_special_command = false;
  51. FuriString* args_tmp;
  52. args_tmp = furi_string_alloc();
  53. do {
  54. if(!args_read_string_and_trim(buffer, args_tmp)) {
  55. // No spaces found, buffer and args_tmp are now have same contents
  56. break;
  57. }
  58. // FURI_LOG_I(
  59. // TAG,
  60. // "args_temp: '%s', buffer: '%s'",
  61. // furi_string_get_cstr(args_tmp),
  62. // furi_string_get_cstr(buffer));
  63. // OK, there's a space, and args_tmp is the first token, buffer is the rest
  64. if(furi_string_cmpi_str(args_tmp, "pause") == 0) {
  65. processed_special_command = true;
  66. uint32_t pause_length = 0;
  67. if(sscanf(furi_string_get_cstr(buffer), "%lu", &pause_length) == 1) {
  68. FURI_LOG_I(TAG, "Pausing playlist for %lu ms", pause_length);
  69. furi_delay_ms(pause_length);
  70. } else {
  71. ACTION_SET_ERROR("Playlist: Invalid or missing pause time");
  72. }
  73. break;
  74. }
  75. // First token wasn't "pause", so maybe args_tmp is a filename followed
  76. // by a transmit duration in ms in buffer
  77. // Note: Not using path_extract_extension since it expects to find slashes in the
  78. // path, and thus won't work if we have a relative path file
  79. char ext[MAX_EXT_LEN + 1] = "";
  80. size_t dot = furi_string_search_rchar(args_tmp, '.');
  81. if(dot != FURI_STRING_FAILURE && furi_string_size(args_tmp) - dot <= MAX_EXT_LEN) {
  82. strlcpy(ext, &(furi_string_get_cstr(args_tmp))[dot], MAX_EXT_LEN);
  83. }
  84. // FURI_LOG_I(TAG, " - Found extension of %s", ext);
  85. if(!strcmp(ext, ".rfid")) {
  86. uint32_t rfid_duration = 0;
  87. // FURI_LOG_I(TAG, "RFID file with duration");
  88. if(sscanf(furi_string_get_cstr(buffer), "%lu", &rfid_duration) == 1) {
  89. FURI_LOG_I(TAG, "RFID duration = %lu", rfid_duration);
  90. app->settings.rfid_duration = rfid_duration;
  91. }
  92. } else if(!strcmp(ext, ".nfc")) {
  93. uint32_t nfc_duration = 0;
  94. if(sscanf(furi_string_get_cstr(buffer), "%lu", &nfc_duration) == 1) {
  95. FURI_LOG_I(TAG, "NFC duration = %lu", nfc_duration);
  96. app->settings.nfc_duration = nfc_duration;
  97. }
  98. } else if(!strcmp(ext, ".ibtn")) {
  99. uint32_t ibutton_duration = 0;
  100. if(sscanf(furi_string_get_cstr(buffer), "%lu", &ibutton_duration) == 1) {
  101. FURI_LOG_I(TAG, "iButton duration = %lu", ibutton_duration);
  102. app->settings.ibutton_duration = ibutton_duration;
  103. }
  104. }
  105. } while(false);
  106. furi_string_swap(buffer, args_tmp);
  107. furi_string_free(args_tmp);
  108. if(processed_special_command) {
  109. continue;
  110. }
  111. first_char = furi_string_get_char(buffer, 0);
  112. // Using relative paths? Prepend path of our playlist file
  113. if(first_char != '/') {
  114. FuriString* dirname;
  115. dirname = furi_string_alloc();
  116. path_extract_dirname(furi_string_get_cstr(action_path), dirname);
  117. furi_string_cat_printf(dirname, "/%s", furi_string_get_cstr(buffer));
  118. furi_string_swap(dirname, buffer);
  119. furi_string_free(dirname);
  120. }
  121. char ext[MAX_EXT_LEN + 1] = "";
  122. path_extract_extension(buffer, ext, MAX_EXT_LEN);
  123. if(!strcmp(ext, ".sub")) {
  124. action_subghz_tx(context, buffer, error);
  125. } else if(!strcmp(ext, ".rfid")) {
  126. action_rfid_tx(context, buffer, error);
  127. // Reset our default duration back - in case it was changed during playback
  128. app->settings.rfid_duration = orig_rfid_duration;
  129. } else if(!strcmp(ext, ".ir")) {
  130. action_ir_tx(context, buffer, error);
  131. } else if(!strcmp(ext, ".nfc")) {
  132. action_nfc_tx(context, buffer, error);
  133. // Reset our default duration back - in case it was changed during playback
  134. app->settings.nfc_duration = orig_nfc_duration;
  135. } else if(!strcmp(ext, ".ibtn")) {
  136. action_ibutton_tx(context, buffer, error);
  137. // Reset our default duration back - in case it was changed during playback
  138. app->settings.ibutton_duration = orig_ibutton_duration;
  139. } else if(!strcmp(ext, ".qpl")) {
  140. ACTION_SET_ERROR("Playlist: Can't call playlist from playlist");
  141. } else {
  142. ACTION_SET_ERROR(
  143. "Playlist: Unknown file/command! %s", furi_string_get_cstr(buffer));
  144. }
  145. if(furi_string_size(error)) {
  146. // Abort playing the playlist - one of our actions failed
  147. break;
  148. }
  149. // Playlist action complete!
  150. // TODO: Do we need a small delay (say 25ms) between actions?
  151. // TODO: Should we blip the light a diff color to indicate that
  152. // we're done with one command and moving to the next?
  153. // furi_delay_ms(25);
  154. }
  155. } else {
  156. ACTION_SET_ERROR("Could not open playlist");
  157. }
  158. furi_string_free(buffer);
  159. file_stream_close(file);
  160. stream_free(file);
  161. }