action_qpl.c 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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_subghz_duration = app->settings.subghz_duration;
  30. uint32_t orig_rfid_duration = app->settings.rfid_duration;
  31. uint32_t orig_nfc_duration = app->settings.nfc_duration;
  32. uint32_t orig_ibutton_duration = app->settings.ibutton_duration;
  33. FuriString* buffer;
  34. buffer = furi_string_alloc();
  35. Stream* file = file_stream_alloc(app->storage);
  36. if(file_stream_open(file, furi_string_get_cstr(action_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
  37. while(stream_read_line(file, buffer)) {
  38. furi_string_trim(buffer); // remove '\n\r' line endings, cleanup spaces
  39. // FURI_LOG_I(TAG, "line: %s", furi_string_get_cstr(buffer));
  40. // Skip blank lines
  41. if(furi_string_size(buffer) == 0) {
  42. continue;
  43. }
  44. // Skip comments
  45. char first_char = furi_string_get_char(buffer, 0);
  46. if(first_char == '#') {
  47. continue;
  48. }
  49. // Check if buffer is a "command", and not just a filename
  50. // Commands will contain spaces
  51. bool processed_special_command = false;
  52. FuriString* args_tmp;
  53. args_tmp = furi_string_alloc();
  54. do {
  55. if(!args_read_string_and_trim(buffer, args_tmp)) {
  56. // No spaces found, buffer and args_tmp are now have same contents
  57. break;
  58. }
  59. // FURI_LOG_I(
  60. // TAG,
  61. // "args_temp: '%s', buffer: '%s'",
  62. // furi_string_get_cstr(args_tmp),
  63. // furi_string_get_cstr(buffer));
  64. // OK, there's a space, and args_tmp is the first token, buffer is the rest
  65. if(furi_string_cmpi_str(args_tmp, "pause") == 0) {
  66. processed_special_command = true;
  67. uint32_t pause_length = 0;
  68. if(sscanf(furi_string_get_cstr(buffer), "%lu", &pause_length) == 1) {
  69. FURI_LOG_I(TAG, "Pausing playlist for %lu ms", pause_length);
  70. furi_delay_ms(pause_length);
  71. } else {
  72. ACTION_SET_ERROR("Playlist: Invalid or missing pause time");
  73. }
  74. break;
  75. }
  76. // First token wasn't "pause", so maybe args_tmp is a filename followed
  77. // by a transmit duration in ms in buffer
  78. // Note: Not using path_extract_extension since it expects to find slashes in the
  79. // path, and thus won't work if we have a relative path file
  80. char ext[MAX_EXT_LEN + 1] = "";
  81. size_t dot = furi_string_search_rchar(args_tmp, '.');
  82. if(dot != FURI_STRING_FAILURE && furi_string_size(args_tmp) - dot <= MAX_EXT_LEN) {
  83. strlcpy(ext, &(furi_string_get_cstr(args_tmp))[dot], MAX_EXT_LEN);
  84. }
  85. // FURI_LOG_I(TAG, " - Found extension of %s", ext);
  86. if(!strcmp(ext, ".sub")) {
  87. uint32_t subghz_duration = 0;
  88. // FURI_LOG_I(TAG, "SubGhz file with duration");
  89. if(sscanf(furi_string_get_cstr(buffer), "%lu", &subghz_duration) == 1) {
  90. FURI_LOG_I(TAG, "SubGhz duration = %lu", subghz_duration);
  91. app->settings.subghz_duration = subghz_duration;
  92. }
  93. } else if(!strcmp(ext, ".rfid")) {
  94. uint32_t rfid_duration = 0;
  95. // FURI_LOG_I(TAG, "RFID file with duration");
  96. if(sscanf(furi_string_get_cstr(buffer), "%lu", &rfid_duration) == 1) {
  97. FURI_LOG_I(TAG, "RFID duration = %lu", rfid_duration);
  98. app->settings.rfid_duration = rfid_duration;
  99. }
  100. } else if(!strcmp(ext, ".nfc")) {
  101. uint32_t nfc_duration = 0;
  102. if(sscanf(furi_string_get_cstr(buffer), "%lu", &nfc_duration) == 1) {
  103. FURI_LOG_I(TAG, "NFC duration = %lu", nfc_duration);
  104. app->settings.nfc_duration = nfc_duration;
  105. }
  106. } else if(!strcmp(ext, ".ibtn")) {
  107. uint32_t ibutton_duration = 0;
  108. if(sscanf(furi_string_get_cstr(buffer), "%lu", &ibutton_duration) == 1) {
  109. FURI_LOG_I(TAG, "iButton duration = %lu", ibutton_duration);
  110. app->settings.ibutton_duration = ibutton_duration;
  111. }
  112. }
  113. } while(false);
  114. furi_string_swap(buffer, args_tmp);
  115. furi_string_free(args_tmp);
  116. if(processed_special_command) {
  117. continue;
  118. }
  119. first_char = furi_string_get_char(buffer, 0);
  120. // Using relative paths? Prepend path of our playlist file
  121. if(first_char != '/') {
  122. FuriString* dirname;
  123. dirname = furi_string_alloc();
  124. path_extract_dirname(furi_string_get_cstr(action_path), dirname);
  125. furi_string_cat_printf(dirname, "/%s", furi_string_get_cstr(buffer));
  126. furi_string_swap(dirname, buffer);
  127. furi_string_free(dirname);
  128. }
  129. char ext[MAX_EXT_LEN + 1] = "";
  130. path_extract_extension(buffer, ext, MAX_EXT_LEN);
  131. if(!strcmp(ext, ".sub")) {
  132. action_subghz_tx(context, buffer, error);
  133. // Reset our default duration back - in case it was changed during playback
  134. app->settings.subghz_duration = orig_subghz_duration;
  135. } else if(!strcmp(ext, ".rfid")) {
  136. action_rfid_tx(context, buffer, error);
  137. // Reset our default duration back - in case it was changed during playback
  138. app->settings.rfid_duration = orig_rfid_duration;
  139. } else if(!strcmp(ext, ".ir")) {
  140. action_ir_tx(context, buffer, error);
  141. } else if(!strcmp(ext, ".nfc")) {
  142. action_nfc_tx(context, buffer, error);
  143. // Reset our default duration back - in case it was changed during playback
  144. app->settings.nfc_duration = orig_nfc_duration;
  145. } else if(!strcmp(ext, ".ibtn")) {
  146. action_ibutton_tx(context, buffer, error);
  147. // Reset our default duration back - in case it was changed during playback
  148. app->settings.ibutton_duration = orig_ibutton_duration;
  149. } else if(!strcmp(ext, ".qpl")) {
  150. ACTION_SET_ERROR("Playlist: Can't call playlist from playlist");
  151. } else {
  152. ACTION_SET_ERROR(
  153. "Playlist: Unknown file/command! %s", furi_string_get_cstr(buffer));
  154. }
  155. if(furi_string_size(error)) {
  156. // Abort playing the playlist - one of our actions failed
  157. break;
  158. }
  159. // Playlist action complete!
  160. // TODO: Do we need a small delay (say 25ms) between actions?
  161. // TODO: Should we blip the light a diff color to indicate that
  162. // we're done with one command and moving to the next?
  163. // furi_delay_ms(25);
  164. }
  165. } else {
  166. ACTION_SET_ERROR("Could not open playlist");
  167. }
  168. furi_string_free(buffer);
  169. file_stream_close(file);
  170. stream_free(file);
  171. }