view_info.c 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. /* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
  2. * See the LICENSE file for information about the license. */
  3. #include "app.h"
  4. #include <gui/view.h>
  5. #include <lib/toolbox/random_name.h>
  6. /* This view has subviews accessible navigating up/down. This
  7. * enumaration is used to track the currently active subview. */
  8. enum {
  9. SubViewInfoMain,
  10. SubViewInfoSave,
  11. SubViewInfoLast, /* Just a sentinel. */
  12. };
  13. /* Our view private data. */
  14. #define SAVE_FILENAME_LEN 64
  15. typedef struct {
  16. /* Our save view displays an oscilloscope-alike resampled signal,
  17. * so that the user can see what they are saving. With left/right
  18. * you can move to next rows. Here we store where we are. */
  19. uint32_t signal_display_start_row;
  20. char* filename;
  21. uint8_t cur_info_page; // Info page to display. Useful when there are
  22. // too many fields populated by the decoder that
  23. // a single page is not enough.
  24. } InfoViewPrivData;
  25. /* Draw the text label and value of the specified info field at x,y. */
  26. static void render_info_field(Canvas* const canvas, ProtoViewField* f, uint8_t x, uint8_t y) {
  27. char buf[64];
  28. char strval[32];
  29. field_to_string(strval, sizeof(strval), f);
  30. snprintf(buf, sizeof(buf), "%s: %s", f->name, strval);
  31. canvas_set_font(canvas, FontSecondary);
  32. canvas_draw_str(canvas, x, y, buf);
  33. }
  34. /* Render the view with the detected message information. */
  35. #define INFO_LINES_PER_PAGE 5
  36. static void render_subview_main(Canvas* const canvas, ProtoViewApp* app) {
  37. InfoViewPrivData* privdata = app->view_privdata;
  38. uint8_t pages =
  39. (app->msg_info->fieldset->numfields + (INFO_LINES_PER_PAGE - 1)) / INFO_LINES_PER_PAGE;
  40. privdata->cur_info_page %= pages;
  41. uint8_t current_page = privdata->cur_info_page;
  42. char buf[32];
  43. /* Protocol name as title. */
  44. canvas_set_font(canvas, FontPrimary);
  45. uint8_t y = 8, lineheight = 10;
  46. if(pages > 1) {
  47. snprintf(
  48. buf, sizeof(buf), "%s %u/%u", app->msg_info->decoder->name, current_page + 1, pages);
  49. canvas_draw_str(canvas, 0, y, buf);
  50. } else {
  51. canvas_draw_str(canvas, 0, y, app->msg_info->decoder->name);
  52. }
  53. y += lineheight;
  54. /* Draw the info fields. */
  55. uint8_t max_lines = INFO_LINES_PER_PAGE;
  56. uint32_t j = current_page * max_lines;
  57. while(j < app->msg_info->fieldset->numfields) {
  58. render_info_field(canvas, app->msg_info->fieldset->fields[j++], 0, y);
  59. y += lineheight;
  60. if(--max_lines == 0) break;
  61. }
  62. /* Draw a vertical "save" label. Temporary solution, to switch to
  63. * something better ASAP. */
  64. y = 37;
  65. lineheight = 7;
  66. canvas_draw_str(canvas, 119, y, "s");
  67. y += lineheight;
  68. canvas_draw_str(canvas, 119, y, "a");
  69. y += lineheight;
  70. canvas_draw_str(canvas, 119, y, "v");
  71. y += lineheight;
  72. canvas_draw_str(canvas, 119, y, "e");
  73. y += lineheight;
  74. }
  75. /* Render view with save option. */
  76. static void render_subview_save(Canvas* const canvas, ProtoViewApp* app) {
  77. InfoViewPrivData* privdata = app->view_privdata;
  78. /* Display our signal in digital form: here we don't show the
  79. * signal with the exact timing of the received samples, but as it
  80. * is in its logic form, in exact multiples of the short pulse length. */
  81. uint8_t rows = 6;
  82. uint8_t rowheight = 11;
  83. uint8_t bitwidth = 4;
  84. uint8_t bitheight = 5;
  85. uint32_t idx = privdata->signal_display_start_row * (128 / 4);
  86. bool prevbit = false;
  87. for(uint8_t y = bitheight + 12; y <= rows * rowheight; y += rowheight) {
  88. for(uint8_t x = 0; x < 128; x += 4) {
  89. bool bit = bitmap_get(app->msg_info->bits, app->msg_info->bits_bytes, idx);
  90. uint8_t prevy = y + prevbit * (bitheight * -1) - 1;
  91. uint8_t thisy = y + bit * (bitheight * -1) - 1;
  92. canvas_draw_line(canvas, x, prevy, x, thisy);
  93. canvas_draw_line(canvas, x, thisy, x + bitwidth - 1, thisy);
  94. prevbit = bit;
  95. if(idx >= app->msg_info->pulses_count) {
  96. canvas_set_color(canvas, ColorWhite);
  97. canvas_draw_dot(canvas, x + 1, thisy);
  98. canvas_draw_dot(canvas, x + 3, thisy);
  99. canvas_set_color(canvas, ColorBlack);
  100. }
  101. idx++; // Draw next bit
  102. }
  103. }
  104. canvas_set_font(canvas, FontSecondary);
  105. canvas_draw_str(canvas, 0, 6, "ok: send, long ok: save");
  106. }
  107. /* Render the selected subview of this view. */
  108. void render_view_info(Canvas* const canvas, ProtoViewApp* app) {
  109. if(app->signal_decoded == false) {
  110. canvas_set_font(canvas, FontSecondary);
  111. canvas_draw_str(canvas, 30, 36, "No signal decoded");
  112. return;
  113. }
  114. ui_show_available_subviews(canvas, app, SubViewInfoLast);
  115. switch(app->current_subview[app->current_view]) {
  116. case SubViewInfoMain:
  117. render_subview_main(canvas, app);
  118. break;
  119. case SubViewInfoSave:
  120. render_subview_save(canvas, app);
  121. break;
  122. }
  123. }
  124. /* The user typed the file name. Let's save it and remove the keyboard
  125. * view. */
  126. static void text_input_done_callback(void* context) {
  127. ProtoViewApp* app = context;
  128. InfoViewPrivData* privdata = app->view_privdata;
  129. FuriString* save_path =
  130. furi_string_alloc_printf("%s/%s.sub", EXT_PATH("subghz"), privdata->filename);
  131. save_signal(app, furi_string_get_cstr(save_path));
  132. furi_string_free(save_path);
  133. free(privdata->filename);
  134. privdata->filename = NULL; // Don't free it again on view exit
  135. ui_dismiss_keyboard(app);
  136. ui_show_alert(app, "Signal saved", 1500);
  137. }
  138. /* Replace all the occurrences of character c1 with c2 in the specified
  139. * string. */
  140. void str_replace(char* buf, char c1, char c2) {
  141. char* p = buf;
  142. while(*p) {
  143. if(*p == c1) *p = c2;
  144. p++;
  145. }
  146. }
  147. /* Set a random filename the user can edit. */
  148. void set_signal_random_filename(ProtoViewApp* app, char* buf, size_t buflen) {
  149. char suffix[6];
  150. set_random_name(suffix, sizeof(suffix));
  151. snprintf(buf, buflen, "%.10s-%s-%d", app->msg_info->decoder->name, suffix, rand() % 1000);
  152. str_replace(buf, ' ', '_');
  153. str_replace(buf, '-', '_');
  154. str_replace(buf, '/', '_');
  155. }
  156. /* ========================== Signal transmission =========================== */
  157. /* This is the context we pass to the data yield callback for
  158. * asynchronous tx. */
  159. typedef enum {
  160. SendSignalSendStartGap,
  161. SendSignalSendBits,
  162. SendSignalSendEndGap,
  163. SendSignalEndTransmission
  164. } SendSignalState;
  165. #define PROTOVIEW_SENDSIGNAL_START_GAP 10000 /* microseconds. */
  166. #define PROTOVIEW_SENDSIGNAL_END_GAP 10000 /* microseconds. */
  167. typedef struct {
  168. SendSignalState state; // Current state.
  169. uint32_t curpos; // Current bit position of data to send.
  170. ProtoViewApp* app; // App reference.
  171. uint32_t start_gap_dur; // Gap to send at the start.
  172. uint32_t end_gap_dur; // Gap to send at the end.
  173. } SendSignalCtx;
  174. /* Setup the state context for the callback responsible to feed data
  175. * to the subghz async tx system. */
  176. static void send_signal_init(SendSignalCtx* ss, ProtoViewApp* app) {
  177. ss->state = SendSignalSendStartGap;
  178. ss->curpos = 0;
  179. ss->app = app;
  180. ss->start_gap_dur = PROTOVIEW_SENDSIGNAL_START_GAP;
  181. ss->end_gap_dur = PROTOVIEW_SENDSIGNAL_END_GAP;
  182. }
  183. /* Send signal data feeder callback. When the asynchronous transmission is
  184. * active, this function is called to return new samples from the currently
  185. * decoded signal in app->msg_info. The subghz subsystem aspects this function,
  186. * that is the data feeder, to return LevelDuration types (that is a structure
  187. * with level, that is pulse or gap, and duration in microseconds).
  188. *
  189. * The position into the transmission is stored in the context 'ctx', that
  190. * references a SendSignalCtx structure.
  191. *
  192. * In the SendSignalCtx structure 'ss' we remember at which bit of the
  193. * message we are, in ss->curoff. We also send a start and end gap in order
  194. * to make sure the transmission is clear.
  195. */
  196. LevelDuration radio_tx_feed_data(void* ctx) {
  197. SendSignalCtx* ss = ctx;
  198. /* Send start gap. */
  199. if(ss->state == SendSignalSendStartGap) {
  200. ss->state = SendSignalSendBits;
  201. return level_duration_make(0, ss->start_gap_dur);
  202. }
  203. /* Send data. */
  204. if(ss->state == SendSignalSendBits) {
  205. uint32_t dur = 0, j;
  206. uint32_t level = 0;
  207. /* Let's see how many consecutive bits we have with the same
  208. * level. */
  209. for(j = 0; ss->curpos + j < ss->app->msg_info->pulses_count; j++) {
  210. uint32_t l =
  211. bitmap_get(ss->app->msg_info->bits, ss->app->msg_info->bits_bytes, ss->curpos + j);
  212. if(j == 0) {
  213. /* At the first bit of this sequence, we store the
  214. * level of the sequence. */
  215. level = l;
  216. dur += ss->app->msg_info->short_pulse_dur;
  217. continue;
  218. }
  219. /* As long as the level is the same, we update the duration.
  220. * Otherwise stop the loop and return this sample. */
  221. if(l != level) break;
  222. dur += ss->app->msg_info->short_pulse_dur;
  223. }
  224. ss->curpos += j;
  225. /* If this was the last set of bits, change the state to
  226. * send the final gap. */
  227. if(ss->curpos >= ss->app->msg_info->pulses_count) ss->state = SendSignalSendEndGap;
  228. return level_duration_make(level, dur);
  229. }
  230. /* Send end gap. */
  231. if(ss->state == SendSignalSendEndGap) {
  232. ss->state = SendSignalEndTransmission;
  233. return level_duration_make(0, ss->end_gap_dur);
  234. }
  235. /* End transmission. Here state is guaranteed
  236. * to be SendSignalEndTransmission */
  237. return level_duration_reset();
  238. }
  239. /* Vibrate and produce a click sound when a signal is sent. */
  240. void notify_signal_sent(ProtoViewApp* app) {
  241. static const NotificationSequence sent_seq = {
  242. &message_blue_255,
  243. &message_vibro_on,
  244. &message_note_g1,
  245. &message_delay_10,
  246. &message_sound_off,
  247. &message_vibro_off,
  248. &message_blue_0,
  249. NULL};
  250. notification_message(app->notification, &sent_seq);
  251. }
  252. /* Handle input for the info view. */
  253. void process_input_info(ProtoViewApp* app, InputEvent input) {
  254. /* If we don't have a decoded signal, we don't allow to go up/down
  255. * in the subviews: they are only useful when a loaded signal. */
  256. if(app->signal_decoded && ui_process_subview_updown(app, input, SubViewInfoLast)) return;
  257. InfoViewPrivData* privdata = app->view_privdata;
  258. int subview = ui_get_current_subview(app);
  259. /* Main subview. */
  260. if(subview == SubViewInfoMain) {
  261. if(input.type == InputTypeLong && input.key == InputKeyOk) {
  262. /* Reset the current sample to capture the next. */
  263. reset_current_signal(app);
  264. } else if(input.type == InputTypeShort && input.key == InputKeyOk) {
  265. /* Show next info page. */
  266. privdata->cur_info_page++;
  267. }
  268. } else if(subview == SubViewInfoSave) {
  269. /* Save subview. */
  270. if(input.type == InputTypePress && input.key == InputKeyRight) {
  271. privdata->signal_display_start_row++;
  272. } else if(input.type == InputTypePress && input.key == InputKeyLeft) {
  273. if(privdata->signal_display_start_row != 0) privdata->signal_display_start_row--;
  274. } else if(input.type == InputTypeLong && input.key == InputKeyOk) {
  275. // We have have the buffer already allocated, in case the
  276. // user aborted with BACK a previous saving.
  277. if(privdata->filename == NULL) privdata->filename = malloc(SAVE_FILENAME_LEN);
  278. set_signal_random_filename(app, privdata->filename, SAVE_FILENAME_LEN);
  279. ui_show_keyboard(app, privdata->filename, SAVE_FILENAME_LEN, text_input_done_callback);
  280. } else if(input.type == InputTypeShort && input.key == InputKeyOk) {
  281. SendSignalCtx send_state;
  282. send_signal_init(&send_state, app);
  283. radio_tx_signal(app, radio_tx_feed_data, &send_state);
  284. notify_signal_sent(app);
  285. }
  286. }
  287. }
  288. /* Called on view exit. */
  289. void view_exit_info(ProtoViewApp* app) {
  290. InfoViewPrivData* privdata = app->view_privdata;
  291. // When the user aborts the keyboard input, we are left with the
  292. // filename buffer allocated.
  293. if(privdata->filename) free(privdata->filename);
  294. }