view_info.c 13 KB

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