From a08a27baaf2e02a5c5562deef2b1c2f8f82aecc3 Mon Sep 17 00:00:00 2001 From: Sandro Kalatozishvili Date: Sat, 7 Oct 2023 04:22:05 +0400 Subject: [PATCH] Finished learn mode and signal analyzer (#3) * Implemented signal receiver and learn mode view skeleton * Receive and display infrared message in learn mode * Fixed variable type * Added buttons to received ir signal view * Stable IR signal receiver and analyzer * Stable signal handling and learn navigation skeleton * Ask to enter new remote name when finishing learn * Implemented exit dialog warning for learning mode * Finished learn mode and signal analyzer --------- Co-authored-by: Sandro Kalatozishvili --- .vscode/settings.json | 5 +- README.md | 17 +- infrared/infrared_remote.c | 15 ++ infrared/infrared_remote.h | 4 + screens/app_menu.png | Bin 6283 -> 6642 bytes screens/learn_mode.png | Bin 0 -> 6837 bytes screens/saved_remote_apps.png | Bin 10660 -> 10904 bytes screens/signal_view.png | Bin 0 -> 7315 bytes views/xremote_common_view.c | 60 ++++- views/xremote_common_view.h | 24 +- views/xremote_learn_view.c | 228 +++++++++++++++++- views/xremote_learn_view.h | 3 +- views/xremote_player_view.c | 14 +- views/xremote_signal_view.c | 203 ++++++++++++++++ views/xremote_signal_view.h | 14 ++ xremote.c | 5 + xremote.h | 6 +- xremote_analyzer.c | 177 ++++++++++++++ xremote_analyzer.h | 21 ++ xremote_app.c | 23 +- xremote_app.h | 13 +- xremote_learn.c | 424 +++++++++++++++++++++++++++++++++- xremote_learn.h | 13 ++ xremote_signal.c | 131 +++++++++++ xremote_signal.h | 28 +++ 25 files changed, 1380 insertions(+), 48 deletions(-) create mode 100644 screens/learn_mode.png create mode 100644 screens/signal_view.png create mode 100644 views/xremote_signal_view.c create mode 100644 views/xremote_signal_view.h create mode 100644 xremote_analyzer.c create mode 100644 xremote_analyzer.h create mode 100644 xremote_signal.c create mode 100644 xremote_signal.h diff --git a/.vscode/settings.json b/.vscode/settings.json index fc28144..24f44c6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,9 @@ "infrared_remote.h": "c", "xremote_navigation_view.h": "c", "xremote_settings_view.h": "c", - "xremote_learn_view.h": "c" + "xremote_learn_view.h": "c", + "system_error": "c", + "typeinfo": "c", + "xremote_analyzer.h": "c" } } \ No newline at end of file diff --git a/README.md b/README.md index 372d8fb..39b902b 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ Button name | Description ## Progress - [x] Application menu -- [ ] Learn new remote -- [ ] Signal analyzer +- [x] Learn new remote +- [x] Signal analyzer - [x] Use saved remote - [x] General button page - [x] Control buttons page @@ -49,7 +49,7 @@ Button name | Description - [x] Player buttons page - [ ] Custom buttons page - [ ] Full button list - - [ ] Edit remote file + - [ ] Rename remote file - [ ] Delete remote file - [x] Application settings - [x] GUI to change settings @@ -81,6 +81,17 @@ Button name | Description + + + + + + + + + +
Learn modeReceived signal
XRemote learn modeXRemote received signal
+ diff --git a/infrared/infrared_remote.c b/infrared/infrared_remote.c index ff0d19f..07fb7d8 100644 --- a/infrared/infrared_remote.c +++ b/infrared/infrared_remote.c @@ -6,6 +6,8 @@ Modifications made: - Added function infrared_remote_get_button_by_name() + - Added function infrared_remote_delete_button_by_name() + - Added function infrared_remote_push_button() */ #include "infrared_remote.h" @@ -113,6 +115,13 @@ bool infrared_remote_add_button(InfraredRemote* remote, const char* name, Infrar return infrared_remote_store(remote); } +void infrared_remote_push_button(InfraredRemote* remote, const char* name, InfraredSignal* signal) { + InfraredRemoteButton* button = infrared_remote_button_alloc(); + infrared_remote_button_set_name(button, name); + infrared_remote_button_set_signal(button, signal); + InfraredButtonArray_push_back(remote->buttons, button); +} + bool infrared_remote_rename_button(InfraredRemote* remote, const char* new_name, size_t index) { furi_assert(index < InfraredButtonArray_size(remote->buttons)); InfraredRemoteButton* button = *InfraredButtonArray_get(remote->buttons, index); @@ -128,6 +137,12 @@ bool infrared_remote_delete_button(InfraredRemote* remote, size_t index) { return infrared_remote_store(remote); } +bool infrared_remote_delete_button_by_name(InfraredRemote* remote, const char* name) { + size_t index = 0; + if (!infrared_remote_find_button_by_name(remote, name, &index)) return false; + return infrared_remote_delete_button(remote, index); +} + void infrared_remote_move_button(InfraredRemote* remote, size_t index_orig, size_t index_dest) { furi_assert(index_orig < InfraredButtonArray_size(remote->buttons)); furi_assert(index_dest < InfraredButtonArray_size(remote->buttons)); diff --git a/infrared/infrared_remote.h b/infrared/infrared_remote.h index c6d9df9..850c528 100644 --- a/infrared/infrared_remote.h +++ b/infrared/infrared_remote.h @@ -6,6 +6,8 @@ Modifications made: - Added function infrared_remote_get_button_by_name() + - Added function infrared_remote_delete_button_by_name() + - Added function infrared_remote_push_button() */ #pragma once @@ -32,8 +34,10 @@ bool infrared_remote_find_button_by_name(InfraredRemote* remote, const char* nam InfraredRemoteButton* infrared_remote_get_button_by_name(InfraredRemote* remote, const char* name); bool infrared_remote_add_button(InfraredRemote* remote, const char* name, InfraredSignal* signal); +void infrared_remote_push_button(InfraredRemote* remote, const char* name, InfraredSignal* signal); bool infrared_remote_rename_button(InfraredRemote* remote, const char* new_name, size_t index); bool infrared_remote_delete_button(InfraredRemote* remote, size_t index); +bool infrared_remote_delete_button_by_name(InfraredRemote* remote, const char* name); void infrared_remote_move_button(InfraredRemote* remote, size_t index_orig, size_t index_dest); bool infrared_remote_store(InfraredRemote* remote); diff --git a/screens/app_menu.png b/screens/app_menu.png index 44022d0f5ec1f301261a7701e5e07abe33d945e8..23791b3dfee35fa7859c589e63b0e8ca8698bf84 100644 GIT binary patch literal 6642 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 ntABZqvOX@WZPk@ literal 0 HcmV?d00001 diff --git a/screens/saved_remote_apps.png b/screens/saved_remote_apps.png index 1e32e5b38f86f73ef62e0c368be714e06f7681fb..8acaee4655c9e1c354ff697e7598a2a4db818a49 100644 GIT binary patch literal 10904 zcmeHMdt8k9{(llGvRPS#tx;CRPE$QoDoumjLKh0THKw^Wsiw}f0$ssDzGtwte?hqzSqNYxmU9wZ*^)( zo!=6p%X?Fa9+?#2M-G4WZ zbZboYn)O}u3v{RDBX8Xq6LTtlxOh%>CH`>H*UZ&FwPil|o-AyJEwgReW5!1;b0dz46zt7nMVh*7tN*S1&hL*D_3Pvk7$wL5L*?z6uueqQ<09Ftuwm9dif-K7T8Pj1QHj?-KIFc?0z z{>SS>7v`{5&slI`f#9CN_SHhmX|2-8WT;j0+__ra!pkL~ef4#J_+5{B!oFv6Pk(lB zXp8ybOPBIy@C=hrJwo-EHGGxa>X5o?Rt)u#fcon1nZNs=|0%_aO7S+!Fg<+!q{qNY zAt|MA7A=1Vt;=O#s@Wtqg)8elLp1X!_4x5``swEEp<4w6)6|s( zQ|36%^4dT7iFCjErsTUkjOUct<>Gk#qCRGCz6X`tUS;h7yHq$?uL1=@ac z%iv@OhlzLAA1~|f>n`uUu8hmU@lEpc!ppzZBX&X17dzjZw+JjR%!hDn%vai+fRUMN>aI0c2Z!dh9Q zSI5w!Y)xIJpq#_WRHx0W){L?M-*Bb@OlGJP1`{0}Z50h$F~VsW8%IY+jI}Ms))oyY z&=IjAOkxZ=B*IL?VuZ&kN(3pK9?GOMLQoo>M1Mvk6K84)-lN9!3l4R6f6qQ7V$=)3 zAD9?oD8|Oh8WS9h`S3&pb9EG8GO7^bi3lH%teDM|2u5T$iLyG162dh5ki}Xzcdz#x zG$zsL!J)%O0q2bqMkc+d3ylm98m1wWFq9xlFkl)1^s^a9&!kiTbk;cCG;fB@`A`uc z?mho;)MK^}69X!Dcc)bhQlv(x+bW!?Mqei~gG47g4gU&LDUK9de@C zJ5tePdo0P`nrch5!`gqq!ZVx>P)Q8>P%8~9GGIl5VMo~3k&Lz@lc;Dc6>Eofw6!6l ziAx>)u{Ms@cJ?IOVOE+rIIZw}~CAM^qc;x0Mne0RX4rX=7~#kJwEnIjsR4i2ykCU?Poz2@RnQ z^VNjK2^a$$OVr>La2|dRY;kf8rx2Npa32OE2xqF{jnc4u|Jog-6Pd^)t|BriK$^8J z>||r@WWChK2J2+&=!CUdVvTjO{(zoArc-186SXFJP|hPEUq_Ds@?(b|jbxNJC2Zu| z$k!nHa4Mlt!%5*pB#oF5L5!l1hxG)!MqZHuh#@ozs2-zm9eYkkfPxLy*2bD*M?q7l zBr+OnizT8R{cWw$b`+975eqw#mtra7xkoUl%xGdbWd#lR5%>zgbJ$lXi{S`bj;D?e zplHGa9EP^BMqAtZ*erFjT?(8x>M+cI-$WwT!NK1KOF`RP``e(gWa3h^zb%=BrY^N3 z!~Tv$TN~Jaywd+C6aNJhqb53IG-YutsLq&w3Hu1a2Xz#5~K1FBB z*OsSC?=OTPBVV^wD|})ayP9`~n_d5MlGpl8)ei*jlkHz_K+LbvVBp7?yzwF*Z9 z&VKdL?G@jo+?+@-R!gNlGO^a%7-zp$JYjtZ1`iErRT=Dw39--uVvekF)dn9wWS-G{ z``uBfz9m+)^HmiMl{+6`EXw$~O{t67Hy+W;;vXvps}01J(op3~onGyM#G$7qg}fQy z6hy~@D;p@mVeVY;u{_PJON&rrolZL~R0mI2FAgdf6vx)It{m^M@;H-#fd{je!$naSS5bDlP61kCID*Gp^-ytN8%RI>6cK; zy`IZ!RSwh(QQc9vBPJPzBjU7xfp6#Y#;H2$d<~QY98|9o?&CePtPhIfn+=ttZSyOPexz~)% zzdQBk`cukZl>1&xGizlWFIOMIv~X;Cq+1a@(AQuzw^UR#L3GRn5_m8evMQ~ zg-azlbGU@)_6-ub&Kn6K?aYkw*5e=&(7N&7@Zp`=kZc%5qQGH|Owm5Yb9CU;% z8#-5WthDbpfdx!2S76Q>KIbBDc9R4NVHZUA zEtJN+#ZN^0LKjVf{wS0wCl@jNisaHr>Yc`YD?iNP(csLF#wR?Xoe=ombfC^pq!D=O z=e3?HpTmW-TNnolb&!0+Nk6k`*|8nVZuzh{`8Be40^MU|`OvytK6g`Ra+)$q-j>QJ z5~}i=@wx5ihpL_%4(I#$h_}C#H3IAj^l_Oxt|I&oK={AJ?teZO z(XW~WtiPb24r-Dmz-`eth?!U;y6?c_67QG{UTg&=Em&>583E+ukv6_qRR(&#mMV*1 z%Lpb6Z+v;F38skQBJ?o+4pdE0dn#XvHy~6?k3Wp&Zi9WZVE24Up-)$v-K|N2pJ~x} zLaZ(*8wud-seC*VNY)M-I*7hai5G=+i9`ia+4;;hLH%8Od7f_u6H;XIjsb>xO!9N_ zCIrdCep4v71=xeA=%&cyI12^nZF%!0GC$xptGS_zHq9v~U(9wWs4x{iuTK zM;Y7+NlnSZM^HwMVXRg~vfn4LvfqLl_{^n*_V~n6o%oaHy%Q2l$}*7p9#^G*s88^s zZX@MQQuG^TPJ_5ZmM@WXw;DJX$^=QZimRLz8C6qHGx_pz+_A#hpK#2%^u!LFS-d@K zO|Db|YD_6d-7G!$!r8q#VvirSF>#Z`;Kg04*cHJmns{Wv-X(IKqN%aEkN~v9vQm+5 zW*^H70S^&*vhM{6+Wld6`&q))TuGuF*8?r@YK<$AcZ! z-yl5cR9-;p-mho@?=lE;Dw9xdH;f@DzqQqgMCy~LBBku%7Jfz245+ot*uh&HN|@_1 zGDVDU4;j3I5h z*U$Xi{Mm&V0C64ATQ3f{>ASlICX6OST#w%HkTJfb6b~8AZ8s_?rDag#oC{{>Rjaf#J=AwlvK9#; z)#+!#q-B*tO~p!>q*84|P}q=j;7%RaC=1V#bLVD>+BfbV(`eMrd|6W3Wo2GQ$Mp!@ZWHltT%C8kucYIA#%<|ab3-8imrU6SEK$-z3>%!|fa&@U|YSp?ix8qM9GlCf!1 z9H-=}Cm;WgBiu)PDSfT~1+=GB?u~>>0HtrMxkjCrjYTx7gZF-JgS?KFx0_#&uUJ%w zM>_Ef-jyWhy1|)wLH$NXr8Q)E29c1TQJlY)7Wi0lci<^&d+V}>CTWlAH`cc?E>@O^ z4tz?B;yu6~3&eF_EL=W0uH{ZobH%ZtXv378tDuPg0SKujWCzXj(%!Q9C z#H7_3*U3Q5k7rG<2?@OIY^vf%ZlE|UM;e@sQ?Q;G^x`Z%Ul_P4I9G1GMFM)8{xnp5 zojyTqehtW`}m@4jR|5Wncl3TdqsS~R^ z%#wX$aZYeTieEB|_e{HDF5(F+-u0F~CW*vP3Co_mq;Rdm1bW;pS%8#U2WLQ~;jf^2 zGibRLBI2rfXL<%z1C<<+edfOSecQU@hl(2v6?Sh1&gZbYcX>8cYVbOOd0H@zjWkl- z7*bU>R5lgPigsv95(Or=cel6cAG+TEkI^}YV~?+;h3qY9!GlbIAC1d|`CE`P;ouDN zd0a$hzp5Z!)uDf|7B*flDU2+;7L6-(fgi`=F66L;A899M4SqkoIEpA>e1vQ@*ECY@%#y1n?CjNV+W4$}S#rFhFI~y{?Ov?oGR3^H%1a<1>G522 z|22Ej?bnrfg|eZgH7HqU>!l{HZF~C?1I-2n_CHdwGzUR$yKk4?ZUDkNdcLP{5#SGg4#GWNwen)6xcQS|9|`+T}XiIG(akjuZgZZ_{gG zjNtSHs4OMp=1W1r?S}mbU3)$)0D5bdMI-NOZsE>@D!!so-xfCPL{Py)4r2w@EAT?p zo|iYSlxrpQDH3z#OR}z-4=HmIIH!SmAdPlhu-p|dn}=@3MFc8;b1B?bay51KQ{}zl z*k>CL-S#6Ojq1aWV~NsEChEkRViFpaEee^S2B{PthOv{EiIT2lJnLYqE6N2G+L^tp z{Ghj)d=64X99-L|C2(dI+zI8MNYr7y)PsgzONdBip@$Z5Ruq&fJe{giXGu!|8u5fU zPCmmRsTzi$*Np~9@idG+u;vYS8hyP;a4w%9h8L&BYW2N(8K?-BHfRF~Y6EONea`yS z99e6W)dIU`(kfVL7kJMRafcwiSyl?ijsX~hm|fX;*G%S`YJKnB?8Hb}F?Y)tIR%mY z@MWDFO-c35(s?+uSA#40%84j&&2n=+VjZ3T#uF91CRg5uE>kyFK2*rLH^IXAbQvP& z|Ge7#yJZAv|7s6l9|XGzXGktqS~Vinh6oE*3gIc?_gzgLKL4v@d2!hXj}3lXfDK3hzT#h}4-7G>gS4M5re@3a}ov zD)gO2M$>zB<{UHWodqr0SB5lwf%KmS=U5+B$|O?ghzV5mR(usHkzDl>W|qQE4RS`X zs_xb5H`1eP^M>MAjdX z$>L8rXN_oiGfumHP=0E`t$1GhKwi`uT}_s0sqY|FF|ajMe7jYhAt|M8+(7f&Al6hG z69a&8u%0A>g}Rs2qs&DJ#MqTd$+cT`wEKG&mC2gG9qfj?UA&s)X8zS(E|&tV?ivhe zuJbBR{DD-lA}q8*Q)?yP5dK_P-Z~?WLEyUKaqyyP-Ip6To;C7Z9uM_uzq3T7Tq_~$ zS)|!{0+4I=xNFGIEeYc%m+|}J=N6#(S@^fZlO2bByL?I&no`zHBS`E9(V3AyN zOEdkW|3v7Hhe}1xHL|k1)Y?WrUccmT8R=3>;9ORAab>7eia>6nZbk*NlrAKk)^&ghtzka2P)xGd<)dLiotT{~e2 hI?#RqsZOY&Rh3`$)L&VW3AjOSt36f~tlab6KLI81AKCx_ literal 10660 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 diff --git a/screens/signal_view.png b/screens/signal_view.png new file mode 100644 index 0000000000000000000000000000000000000000..63ec75dac91b406c069c791d02d4e1ecd2322776 GIT binary patch literal 7315 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 literal 0 HcmV?d00001 diff --git a/views/xremote_common_view.c b/views/xremote_common_view.c index cd5e712..9b246d0 100644 --- a/views/xremote_common_view.c +++ b/views/xremote_common_view.c @@ -9,8 +9,50 @@ #include "xremote_common_view.h" #include "../xremote_app.h" +typedef struct { + int index; + const char *name; +} XRemoteButton; + +static const XRemoteButton g_buttons[XREMOTE_BUTTON_COUNT + 1] = +{ + { 0, XREMOTE_COMMAND_POWER }, + { 1, XREMOTE_COMMAND_SETUP }, + { 2, XREMOTE_COMMAND_INPUT }, + { 3, XREMOTE_COMMAND_MENU }, + { 4, XREMOTE_COMMAND_LIST }, + { 5, XREMOTE_COMMAND_INFO }, + { 6, XREMOTE_COMMAND_BACK }, + { 7, XREMOTE_COMMAND_OK }, + { 8, XREMOTE_COMMAND_UP }, + { 9, XREMOTE_COMMAND_DOWN }, + { 10, XREMOTE_COMMAND_LEFT }, + { 11, XREMOTE_COMMAND_RIGHT }, + { 12, XREMOTE_COMMAND_JUMP_FORWARD }, + { 13, XREMOTE_COMMAND_JUMP_BACKWARD }, + { 14, XREMOTE_COMMAND_FAST_FORWARD }, + { 15, XREMOTE_COMMAND_FAST_BACKWARD }, + { 16, XREMOTE_COMMAND_PLAY_PAUSE }, + { 17, XREMOTE_COMMAND_PAUSE }, + { 18, XREMOTE_COMMAND_PLAY }, + { 19, XREMOTE_COMMAND_STOP }, + { 20, XREMOTE_COMMAND_MUTE }, + { 21, XREMOTE_COMMAND_MODE }, + { 22, XREMOTE_COMMAND_VOL_UP }, + { 23, XREMOTE_COMMAND_VOL_DOWN }, + { 24, XREMOTE_COMMAND_NEXT_CHAN }, + { 25, XREMOTE_COMMAND_PREV_CHAN }, + { -1, NULL } +}; + +const char* xremote_button_get_name(int index) +{ + if (index > XREMOTE_BUTTON_COUNT) return NULL; + return g_buttons[index].name; +} + struct XRemoteView { - XRemoteViewClearCallback on_clear; + XRemoteClearCallback on_clear; XRemoteAppContext* app_ctx; View* view; void *context; @@ -39,17 +81,14 @@ void xremote_view_clear_context(XRemoteView* rview) { furi_assert(rview); - if (rview->context != NULL && - rview->on_clear != NULL) - { + if (rview->context && rview->on_clear) rview->on_clear(rview->context); - rview->context = NULL; - } + + rview->context = NULL; } -void xremote_view_set_context(XRemoteView* rview, void *context, XRemoteViewClearCallback on_clear) +void xremote_view_set_context(XRemoteView* rview, void *context, XRemoteClearCallback on_clear) { - furi_assert(rview); xremote_view_clear_context(rview); rview->context = context; rview->on_clear = on_clear; @@ -209,9 +248,10 @@ void xremote_canvas_draw_header(Canvas* canvas, ViewOrientation orient, const ch canvas_set_font(canvas, FontPrimary); elements_multiline_text_aligned(canvas, x, 0, align, AlignTop, "XRemote"); - canvas_set_font(canvas, FontSecondary); - elements_multiline_text_aligned(canvas, x, 12, align, AlignTop, section); + + if (section != NULL) + elements_multiline_text_aligned(canvas, x, 12, align, AlignTop, section); } void xremote_canvas_draw_exit_footer(Canvas* canvas, ViewOrientation orient, const char *text) diff --git a/views/xremote_common_view.h b/views/xremote_common_view.h index cc5ab0d..cb74606 100644 --- a/views/xremote_common_view.h +++ b/views/xremote_common_view.h @@ -20,6 +20,7 @@ #include "../infrared/infrared_remote.h" +#define XREMOTE_BUTTON_COUNT 26 #define XREMOTE_COMMAND_POWER "Power" #define XREMOTE_COMMAND_SETUP "Setup" #define XREMOTE_COMMAND_INPUT "Input" @@ -47,6 +48,18 @@ #define XREMOTE_COMMAND_NEXT_CHAN "Ch_next" #define XREMOTE_COMMAND_PREV_CHAN "Ch_prev" +typedef enum { + XRemoteEventReserved = 200, + XRemoteEventSignalReceived, + XRemoteEventSignalFinish, + XRemoteEventSignalSave, + XRemoteEventSignalRetry, + XRemoteEventSignalSend, + XRemoteEventSignalSkip, + XRemoteEventSignalAskExit, + XRemoteEventSignalExit +} XRemoteEvent; + typedef enum { /* Navigation */ XRemoteIconOk, @@ -80,11 +93,15 @@ typedef struct { typedef enum { XRemoteViewNone, + XRemoteViewSignal, + XRemoteViewTextInput, + XRemoteViewDialogExit, /* Main page */ XRemoteViewSubmenu, XRemoteViewLearn, XRemoteViewSaved, + XRemoteViewAnalyzer, XRemoteViewSettings, XRemoteViewAbout, @@ -98,8 +115,11 @@ typedef enum { } XRemoteViewID; typedef struct XRemoteView XRemoteView; -typedef void (*XRemoteViewClearCallback)(void *context); +typedef void (*XRemoteClearCallback)(void *context); typedef void (*XRemoteViewDrawFunction)(Canvas*, XRemoteViewModel*); +typedef XRemoteView* (*XRemoteViewAllocator)(void* app_ctx); + +const char* xremote_button_get_name(int index); void xremote_canvas_draw_header(Canvas* canvas, ViewOrientation orient, const char* section); void xremote_canvas_draw_exit_footer(Canvas* canvas, ViewOrientation orient, const char *text); @@ -118,7 +138,7 @@ InfraredRemoteButton* xremote_view_get_button_by_name(XRemoteView *rview, const bool xremote_view_press_button(XRemoteView *rview, InfraredRemoteButton* button); bool xremote_view_send_ir_msg_by_name(XRemoteView *rview, const char *name); -void xremote_view_set_context(XRemoteView* rview, void *context, XRemoteViewClearCallback on_clear); +void xremote_view_set_context(XRemoteView* rview, void *context, XRemoteClearCallback on_clear); void* xremote_view_get_context(XRemoteView* rview); void xremote_view_clear_context(XRemoteView* rview); void* xremote_view_get_app_context(XRemoteView* rview); diff --git a/views/xremote_learn_view.c b/views/xremote_learn_view.c index d02eb31..0b4a992 100644 --- a/views/xremote_learn_view.c +++ b/views/xremote_learn_view.c @@ -7,32 +7,242 @@ */ #include "xremote_learn_view.h" +#include "../xremote_learn.h" #include "../xremote_app.h" static void xremote_learn_view_draw_callback(Canvas* canvas, void* context) { furi_assert(context); XRemoteViewModel* model = context; - XRemoteAppContext *app_ctx = model->context; + XRemoteLearnContext* learn_ctx = model->context; - ViewOrientation orientation = app_ctx->app_settings->orientation; - uint64_t x = orientation == ViewOrientationVertical ? 70 : 34; + XRemoteAppContext* app_ctx = xremote_learn_get_app_context(learn_ctx); + const char *button_name = xremote_learn_get_curr_button_name(learn_ctx); + ViewOrientation orientation = app_ctx->app_settings->orientation; xremote_canvas_draw_header(canvas, orientation, "Learn"); - canvas_set_font(canvas, FontSecondary); - canvas_draw_str(canvas, 0, x, "Coming Soon."); - xremote_canvas_draw_exit_footer(canvas, orientation, "Press to exit"); + + char info_text[128]; + snprintf(info_text, sizeof(info_text), + "Press\n\"%s\"\nbutton on\nthe remote.", + button_name != NULL ? button_name : ""); + + if (orientation == ViewOrientationHorizontal) + { + elements_multiline_text_aligned(canvas, 0, 12, AlignLeft, AlignTop, info_text); + xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 68, 22, "Finish", XRemoteIconEnter); + xremote_canvas_draw_button_wide(canvas, model->right_pressed, 68, 40, "Skip", XRemoteIconArrowRight); + } + else + { + elements_multiline_text_aligned(canvas, 0, 30, AlignLeft, AlignTop, info_text); + xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 0, 82, "Finish", XRemoteIconEnter); + xremote_canvas_draw_button_wide(canvas, model->right_pressed, 0, 100, "Skip", XRemoteIconArrowRight); + } + + const char *exit_str = xremote_app_context_get_exit_str(app_ctx); + xremote_canvas_draw_exit_footer(canvas, orientation, exit_str); +} + +static void xremote_learn_success_view_draw_callback(Canvas* canvas, void* context) +{ + furi_assert(context); + XRemoteViewModel* model = context; + XRemoteLearnContext* learn_ctx = model->context; + + XRemoteAppContext* app_ctx = xremote_learn_get_app_context(learn_ctx); + InfraredSignal *ir_signal = xremote_learn_get_ir_signal(learn_ctx); + + xremote_canvas_draw_header(canvas, app_ctx->app_settings->orientation, NULL); + const char *button_name = xremote_learn_get_curr_button_name(learn_ctx); + char signal_info[128]; + + if (infrared_signal_is_raw(ir_signal)) + { + InfraredRawSignal* raw = infrared_signal_get_raw_signal(ir_signal); + + snprintf(signal_info, sizeof(signal_info), + "Name: %s\n" + "Type: RAW\n" + "T-Size: %u\n" + "D-Cycle: %.2f\n", + button_name, + raw->timings_size, + (double)raw->duty_cycle); + } + else + { + InfraredMessage* message = infrared_signal_get_message(ir_signal); + const char *infrared_protocol = infrared_get_protocol_name(message->protocol); + + snprintf(signal_info, sizeof(signal_info), + "Name: %s\n" + "Proto: %s\n" + "Addr: 0x%lX\n" + "Cmd: 0x%lX\n", + button_name, + infrared_protocol, + message->address, + message->command); + } + + if (app_ctx->app_settings->orientation == ViewOrientationHorizontal) + { + canvas_draw_str_aligned(canvas, 0, 0, AlignLeft, AlignTop, "Received signal"); + elements_multiline_text_aligned(canvas, 0, 16, AlignLeft, AlignTop, signal_info); + xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 68, 12, "Finish", XRemoteIconEnter); + xremote_canvas_draw_button_wide(canvas, model->right_pressed, 68, 30, "Next", XRemoteIconArrowRight); + xremote_canvas_draw_button_wide(canvas, model->back_pressed, 68, 48, "Retry", XRemoteIconBack); + } + else + { + canvas_draw_str_aligned(canvas, 0, 12, AlignLeft, AlignTop, "Received signal"); + elements_multiline_text_aligned(canvas, 0, 27, AlignLeft, AlignTop, signal_info); + xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 0, 76, "Finish", XRemoteIconEnter); + xremote_canvas_draw_button_wide(canvas, model->right_pressed, 0, 94, "Next", XRemoteIconArrowRight); + xremote_canvas_draw_button_wide(canvas, model->back_pressed, 0, 112, "Retry", XRemoteIconBack); + } +} + +static void xremote_learn_success_view_process(XRemoteView* view, InputEvent* event) +{ + with_view_model( + xremote_view_get_view(view), + XRemoteViewModel* model, + { + XRemoteLearnContext *learn_ctx = xremote_view_get_context(view); + model->context = learn_ctx; + + if (event->type == InputTypePress) + { + if (event->key == InputKeyOk) + { + model->ok_pressed = true; + xremote_learn_send_event(learn_ctx, XRemoteEventSignalFinish); + } + else if (event->key == InputKeyBack) + { + model->back_pressed = true; + xremote_learn_send_event(learn_ctx, XRemoteEventSignalRetry); + } + else if (event->key == InputKeyRight) + { + model->right_pressed = true; + xremote_learn_send_event(learn_ctx, XRemoteEventSignalSave); + } + } + else if (event->type == InputTypeRelease) + { + if (event->key == InputKeyOk) model->ok_pressed = false; + else if (event->key == InputKeyBack) model->back_pressed = false; + else if (event->key == InputKeyRight) model->right_pressed = false; + } + }, + true + ); +} + +static void xremote_learn_view_process(XRemoteView* view, InputEvent* event) +{ + with_view_model( + xremote_view_get_view(view), + XRemoteViewModel* model, + { + XRemoteLearnContext *learn_ctx = xremote_view_get_context(view); + XRemoteAppContext *app_ctx = xremote_view_get_app_context(view); + + XRemoteAppExit exit = app_ctx->app_settings->exit_behavior; + model->context = learn_ctx; + + if (event->type == InputTypePress) + { + if (event->key == InputKeyOk && xremote_learn_has_buttons(learn_ctx)) + { + model->ok_pressed = true; + xremote_learn_send_event(learn_ctx, XRemoteEventSignalFinish); + } + else if (event->key == InputKeyRight) + { + model->right_pressed = true; + xremote_learn_send_event(learn_ctx, XRemoteEventSignalSkip); + } + } + else if ((event->type == InputTypeShort || + event->type == InputTypeLong) && + event->key == InputKeyBack) + { + if ((event->type == InputTypeShort && exit == XRemoteAppExitPress) || + (event->type == InputTypeLong && exit == XRemoteAppExitHold)) + { + model->back_pressed = true; + xremote_learn_send_event(learn_ctx, XRemoteEventSignalAskExit); + } + } + else if (event->type == InputTypeRelease) + { + if (event->key == InputKeyOk) model->ok_pressed = false; + else if (event->key == InputKeyBack) model->back_pressed = false; + else if (event->key == InputKeyRight) model->right_pressed = false; + } + }, + true + ); +} + +static bool xremote_learn_success_view_input_callback(InputEvent* event, void* context) +{ + furi_assert(context); + XRemoteView* view = (XRemoteView*)context; + xremote_learn_success_view_process(view, event); + return true; +} + +static bool xremote_learn_view_input_callback(InputEvent* event, void* context) +{ + furi_assert(context); + XRemoteView* view = (XRemoteView*)context; + xremote_learn_view_process(view, event); + return true; +} + +XRemoteView* xremote_learn_success_view_alloc(void* app_ctx, void *learn_ctx) +{ + XRemoteView *view = xremote_view_alloc(app_ctx, + xremote_learn_success_view_input_callback, + xremote_learn_success_view_draw_callback); + xremote_view_set_context(view, learn_ctx, NULL); + + with_view_model( + xremote_view_get_view(view), + XRemoteViewModel* model, + { + model->context = learn_ctx; + model->right_pressed = false; + model->back_pressed = false; + model->ok_pressed = false; + }, + true + ); + + return view; } -XRemoteView* xremote_learn_view_alloc(void* app_ctx) +XRemoteView* xremote_learn_view_alloc(void* app_ctx, void *learn_ctx) { XRemoteView *view = xremote_view_alloc(app_ctx, - NULL, xremote_learn_view_draw_callback); + xremote_learn_view_input_callback, + xremote_learn_view_draw_callback); + xremote_view_set_context(view, learn_ctx, NULL); with_view_model( xremote_view_get_view(view), XRemoteViewModel* model, - { model->context = app_ctx; }, + { + model->context = learn_ctx; + model->right_pressed = false; + model->back_pressed = false; + model->ok_pressed = false; + }, true ); diff --git a/views/xremote_learn_view.h b/views/xremote_learn_view.h index 40e8538..ef9c259 100644 --- a/views/xremote_learn_view.h +++ b/views/xremote_learn_view.h @@ -10,4 +10,5 @@ #include "xremote_common_view.h" -XRemoteView* xremote_learn_view_alloc(void* app_ctx); +XRemoteView* xremote_learn_view_alloc(void* app_ctx, void *learn_ctx); +XRemoteView* xremote_learn_success_view_alloc(void* app_ctx, void *rx_ctx); diff --git a/views/xremote_player_view.c b/views/xremote_player_view.c index bc8cd23..6dbf3d2 100644 --- a/views/xremote_player_view.c +++ b/views/xremote_player_view.c @@ -17,8 +17,8 @@ static void xremote_player_view_draw_vertical(Canvas* canvas, XRemoteViewModel* xremote_canvas_draw_button(canvas, model->down_pressed, 23, 72, XRemoteIconJumpBackward); xremote_canvas_draw_button(canvas, model->left_pressed, 2, 51, XRemoteIconFastBackward); xremote_canvas_draw_button(canvas, model->right_pressed, 44, 51, XRemoteIconFastForward); - xremote_canvas_draw_button(canvas, model->back_pressed, 2, 95, XRemoteIconPause); - xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 51, XRemoteIconPlay); + xremote_canvas_draw_button(canvas, model->back_pressed, 2, 95, XRemoteIconPlay); + xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 51, XRemoteIconPause); if (app_ctx->app_settings->exit_behavior == XRemoteAppExitPress) canvas_draw_icon(canvas, 22, 107, &I_Hold_Text_17x4); @@ -32,8 +32,8 @@ static void xremote_player_view_draw_horizontal(Canvas* canvas, XRemoteViewModel xremote_canvas_draw_button(canvas, model->down_pressed, 23, 44, XRemoteIconJumpBackward); xremote_canvas_draw_button(canvas, model->left_pressed, 2, 23, XRemoteIconFastBackward); xremote_canvas_draw_button(canvas, model->right_pressed, 44, 23, XRemoteIconFastForward); - xremote_canvas_draw_button(canvas, model->back_pressed, 70, 33, XRemoteIconPause); - xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 23, XRemoteIconPlay); + xremote_canvas_draw_button(canvas, model->back_pressed, 70, 33, XRemoteIconPlay); + xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 23, XRemoteIconPause); if (app_ctx->app_settings->exit_behavior == XRemoteAppExitPress) canvas_draw_icon(canvas, 90, 45, &I_Hold_Text_17x4); @@ -91,7 +91,7 @@ static void xremote_player_view_process(XRemoteView* view, InputEvent* event) } else if (event->key == InputKeyOk) { - button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PLAY); + button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PAUSE); if (xremote_view_press_button(view, button)) model->ok_pressed = true; } } @@ -99,14 +99,14 @@ static void xremote_player_view_process(XRemoteView* view, InputEvent* event) event->key == InputKeyBack && exit == XRemoteAppExitHold) { - button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PAUSE); + button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PLAY); if (xremote_view_press_button(view, button)) model->back_pressed = true; } else if (event->type == InputTypeLong && event->key == InputKeyBack && exit == XRemoteAppExitPress) { - button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PAUSE); + button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PLAY); if (xremote_view_press_button(view, button)) model->back_pressed = true; } else if (event->type == InputTypeRelease) diff --git a/views/xremote_signal_view.c b/views/xremote_signal_view.c new file mode 100644 index 0000000..fcc00fb --- /dev/null +++ b/views/xremote_signal_view.c @@ -0,0 +1,203 @@ +/*! + * @file flipper-xremote/views/xremote_signal_view.c + @license This project is released under the GNU GPLv3 License + * @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com) + * + * @brief Signal analyzer page view components and functionality. + */ + +#include "xremote_signal_view.h" +#include "../xremote_analyzer.h" +#include "../xremote_app.h" + +static void xremote_signal_view_draw_callback(Canvas* canvas, void* context) +{ + furi_assert(context); + XRemoteViewModel* model = context; + XRemoteSignalAnalyzer* analyzer = model->context; + XRemoteAppContext* app_ctx = xremote_signal_analyzer_get_app_context(analyzer); + + ViewOrientation orientation = app_ctx->app_settings->orientation; + uint8_t y = orientation == ViewOrientationHorizontal ? 17 : 49; + const char *text = "Press any\nbutton on\nthe remote."; + + xremote_canvas_draw_header(canvas, orientation, "Analyzer"); + elements_multiline_text_aligned(canvas, 0, y, AlignLeft, AlignTop, text); + + const char *exit_str = xremote_app_context_get_exit_str(app_ctx); + xremote_canvas_draw_exit_footer(canvas, orientation, exit_str); +} + +static void xremote_signal_success_view_draw_callback(Canvas* canvas, void* context) +{ + furi_assert(context); + XRemoteViewModel* model = context; + XRemoteSignalAnalyzer* analyzer = model->context; + + XRemoteAppContext* app_ctx = xremote_signal_analyzer_get_app_context(analyzer); + InfraredSignal *ir_signal = xremote_signal_analyzer_get_ir_signal(analyzer); + + xremote_canvas_draw_header(canvas, app_ctx->app_settings->orientation, "IR Signal"); + char signal_info[128]; + + if (infrared_signal_is_raw(ir_signal)) + { + InfraredRawSignal* raw = infrared_signal_get_raw_signal(ir_signal); + + snprintf(signal_info, sizeof(signal_info), + "Type: RAW\n" + "T-Size: %u\n" + "D-Cycle: %.2f\n", + raw->timings_size, + (double)raw->duty_cycle); + } + else + { + InfraredMessage* message = infrared_signal_get_message(ir_signal); + const char *infrared_protocol = infrared_get_protocol_name(message->protocol); + + snprintf(signal_info, sizeof(signal_info), + "Proto: %s\n" + "Addr: 0x%lX\n" + "Cmd: 0x%lX\n", + infrared_protocol, + message->address, + message->command); + } + + if (app_ctx->app_settings->orientation == ViewOrientationHorizontal) + { + elements_multiline_text_aligned(canvas, 0, 17, AlignLeft, AlignTop, signal_info); + xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 68, 26, "Send", XRemoteIconEnter); + xremote_canvas_draw_button_wide(canvas, model->back_pressed, 68, 44, "Retry", XRemoteIconBack); + } + else + { + elements_multiline_text_aligned(canvas, 0, 39, AlignLeft, AlignTop, signal_info); + xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 0, 88, "Send", XRemoteIconEnter); + xremote_canvas_draw_button_wide(canvas, model->back_pressed, 0, 106, "Retry", XRemoteIconBack); + } +} + +static void xremote_signal_success_view_process(XRemoteView* view, InputEvent* event) +{ + with_view_model( + xremote_view_get_view(view), + XRemoteViewModel* model, + { + XRemoteSignalAnalyzer *analyzer = xremote_view_get_context(view); + model->context = analyzer; + + if (event->type == InputTypePress) + { + if (event->key == InputKeyOk) + { + model->ok_pressed = true; + xremote_signal_analyzer_send_event(analyzer, XRemoteEventSignalSend); + } + else if (event->key == InputKeyBack) + { + model->back_pressed = true; + xremote_signal_analyzer_send_event(analyzer, XRemoteEventSignalRetry); + } + } + else if (event->type == InputTypeRelease) + { + if (event->key == InputKeyOk) model->ok_pressed = false; + else if (event->key == InputKeyBack) model->back_pressed = false; + } + }, + true + ); +} + +static void xremote_signal_view_process(XRemoteView* view, InputEvent* event) +{ + with_view_model( + xremote_view_get_view(view), + XRemoteViewModel* model, + { + XRemoteSignalAnalyzer *analyzer = xremote_view_get_context(view); + XRemoteAppContext *app_ctx = xremote_view_get_app_context(view); + + XRemoteAppExit exit = app_ctx->app_settings->exit_behavior; + model->context = analyzer; + + if ((event->type == InputTypeShort || + event->type == InputTypeLong) && + event->key == InputKeyBack) + { + if ((event->type == InputTypeShort && exit == XRemoteAppExitPress) || + (event->type == InputTypeLong && exit == XRemoteAppExitHold)) + { + model->back_pressed = true; + xremote_signal_analyzer_send_event(analyzer, XRemoteEventSignalExit); + } + } + else if (event->type == InputTypeRelease) + { + if (event->key == InputKeyOk) model->ok_pressed = false; + else if (event->key == InputKeyBack) model->back_pressed = false; + else if (event->key == InputKeyRight) model->right_pressed = false; + } + }, + true + ); +} + +static bool xremote_signal_success_view_input_callback(InputEvent* event, void* context) +{ + furi_assert(context); + XRemoteView* view = (XRemoteView*)context; + xremote_signal_success_view_process(view, event); + return true; +} + +static bool xremote_signal_view_input_callback(InputEvent* event, void* context) +{ + furi_assert(context); + XRemoteView* view = (XRemoteView*)context; + xremote_signal_view_process(view, event); + return true; +} + +XRemoteView* xremote_signal_success_view_alloc(void* app_ctx, void *analyzer) +{ + XRemoteView *view = xremote_view_alloc(app_ctx, + xremote_signal_success_view_input_callback, + xremote_signal_success_view_draw_callback); + xremote_view_set_context(view, analyzer, NULL); + + with_view_model( + xremote_view_get_view(view), + XRemoteViewModel* model, + { + model->context = analyzer; + model->back_pressed = false; + model->ok_pressed = false; + }, + true + ); + + return view; +} + +XRemoteView* xremote_signal_view_alloc(void* app_ctx, void *analyzer) +{ + XRemoteView *view = xremote_view_alloc(app_ctx, + xremote_signal_view_input_callback, + xremote_signal_view_draw_callback); + xremote_view_set_context(view, analyzer, NULL); + + with_view_model( + xremote_view_get_view(view), + XRemoteViewModel* model, + { + model->context = analyzer; + model->back_pressed = false; + }, + true + ); + + return view; +} diff --git a/views/xremote_signal_view.h b/views/xremote_signal_view.h new file mode 100644 index 0000000..6b49110 --- /dev/null +++ b/views/xremote_signal_view.h @@ -0,0 +1,14 @@ +/*! + * @file flipper-xremote/views/xremote_signal_view.h + @license This project is released under the GNU GPLv3 License + * @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com) + * + * @brief Signal analyzer page view components and functionality. + */ + +#pragma once + +#include "xremote_common_view.h" + +XRemoteView* xremote_signal_view_alloc(void* app_ctx, void *learn_ctx); +XRemoteView* xremote_signal_success_view_alloc(void* app_ctx, void *rx_ctx); diff --git a/xremote.c b/xremote.c index df9544d..13e3ae0 100644 --- a/xremote.c +++ b/xremote.c @@ -10,9 +10,11 @@ #include "xremote_learn.h" #include "xremote_control.h" #include "xremote_settings.h" +#include "xremote_analyzer.h" #include "views/xremote_about_view.h" #include "views/xremote_learn_view.h" +#include "views/xremote_signal_view.h" #define TAG "XRemote" @@ -64,6 +66,8 @@ void xremote_submenu_callback(void* context, uint32_t index) child = xremote_learn_alloc(app->app_ctx); else if (index == XRemoteViewIRSubmenu) child = xremote_control_alloc(app->app_ctx); + else if (index == XRemoteViewAnalyzer) + child = xremote_analyzer_alloc(app->app_ctx); else if (index == XRemoteViewSettings) child = xremote_settings_alloc(app->app_ctx); else if (index == XRemoteViewAbout) @@ -87,6 +91,7 @@ int32_t xremote_main(void* p) xremote_app_submenu_alloc(app, XRemoteViewSubmenu, xremote_exit_callback); xremote_app_submenu_add(app, "Learn", XRemoteViewLearn, xremote_submenu_callback); xremote_app_submenu_add(app, "Saved", XRemoteViewIRSubmenu, xremote_submenu_callback); + xremote_app_submenu_add(app, "Analyzer", XRemoteViewAnalyzer, xremote_submenu_callback); xremote_app_submenu_add(app, "Settings", XRemoteViewSettings, xremote_submenu_callback); xremote_app_submenu_add(app, "About", XRemoteViewAbout, xremote_submenu_callback); diff --git a/xremote.h b/xremote.h index 11e92e8..74df08f 100644 --- a/xremote.h +++ b/xremote.h @@ -8,8 +8,8 @@ #include "xremote_app.h" -#define XREMOTE_VERSION_MAJOR 0 -#define XREMOTE_VERSION_MINOR 9 -#define XREMOTE_BUILD_NUMBER 26 +#define XREMOTE_VERSION_MAJOR 1 +#define XREMOTE_VERSION_MINOR 0 +#define XREMOTE_BUILD_NUMBER 1 void xremote_get_version(char *version, size_t length); diff --git a/xremote_analyzer.c b/xremote_analyzer.c new file mode 100644 index 0000000..4dafa58 --- /dev/null +++ b/xremote_analyzer.c @@ -0,0 +1,177 @@ +/*! + * @file flipper-xremote/xremote_analyzer.c + @license This project is released under the GNU GPLv3 License + * @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com) + * + * @brief Infrared remote singnal analyzer and custom view events. + */ + +#include "xremote_analyzer.h" +#include "views/xremote_signal_view.h" + +#define XREMOTE_TEXT_MAX 128 + +struct XRemoteSignalAnalyzer { + XRemoteSignalReceiver* ir_receiver; + XRemoteAppContext* app_ctx; + XRemoteView* signal_view; + InfraredSignal* ir_signal; + XRemoteClearCallback on_clear; + void* context; + bool pause; +}; + +InfraredSignal* xremote_signal_analyzer_get_ir_signal(XRemoteSignalAnalyzer *analyzer) +{ + xremote_app_assert(analyzer, NULL); + return analyzer->ir_signal; +} + +XRemoteSignalReceiver* xremote_signal_analyzer_get_ir_receiver(XRemoteSignalAnalyzer *analyzer) +{ + xremote_app_assert(analyzer, NULL); + return analyzer->ir_receiver; +} + +XRemoteAppContext* xremote_signal_analyzer_get_app_context(XRemoteSignalAnalyzer *analyzer) +{ + xremote_app_assert(analyzer, NULL); + return analyzer->app_ctx; +} + +void xremote_signal_analyzer_send_event(XRemoteSignalAnalyzer* analyzer, XRemoteEvent event) +{ + xremote_app_assert_void(analyzer); + ViewDispatcher* view_disp = analyzer->app_ctx->view_dispatcher; + view_dispatcher_send_custom_event(view_disp, event); +} + +static void xremote_signal_analyzer_switch_to_view(XRemoteSignalAnalyzer* analyzer, XRemoteViewID view_id) +{ + xremote_app_assert_void(analyzer); + ViewDispatcher* view_disp = analyzer->app_ctx->view_dispatcher; + view_dispatcher_switch_to_view(view_disp, view_id); +} + +static void xremote_signal_analyzer_rx_stop(XRemoteSignalAnalyzer *analyzer) +{ + xremote_app_assert_void(analyzer); + analyzer->pause = true; + xremote_signal_receiver_stop(analyzer->ir_receiver); +} + +static void xremote_signal_analyzer_rx_start(XRemoteSignalAnalyzer *analyzer) +{ + xremote_app_assert_void(analyzer); + analyzer->pause = false; + xremote_signal_receiver_start(analyzer->ir_receiver); +} + +static uint32_t xremote_signal_analyzer_view_exit_callback(void* context) +{ + UNUSED(context); + return XRemoteViewAnalyzer; +} + +static void xremote_signal_analyzer_signal_callback(void *context, InfraredSignal* signal) +{ + XRemoteSignalAnalyzer* analyzer = context; + xremote_app_assert_void(!analyzer->pause); + analyzer->pause = true; + + infrared_signal_set_signal(analyzer->ir_signal, signal); + xremote_signal_analyzer_send_event(analyzer, XRemoteEventSignalReceived); +} + +static bool xremote_signal_analyzer_custom_event_callback(void* context, uint32_t event) +{ + xremote_app_assert(context, false); + XRemoteSignalAnalyzer *analyzer = context; + + if (event == XRemoteEventSignalExit) + { + xremote_signal_analyzer_rx_stop(analyzer); + xremote_signal_analyzer_switch_to_view(analyzer, XRemoteViewSubmenu); + } + else if (event == XRemoteEventSignalReceived) + { + xremote_signal_analyzer_rx_stop(analyzer); + xremote_signal_analyzer_switch_to_view(analyzer, XRemoteViewSignal); + } + else if (event == XRemoteEventSignalRetry) + { + xremote_signal_analyzer_rx_start(analyzer); + xremote_signal_analyzer_switch_to_view(analyzer, XRemoteViewAnalyzer); + } + else if (event == XRemoteEventSignalSend) + { + XRemoteAppContext* app_ctx = analyzer->app_ctx; + xremote_app_send_signal(app_ctx, analyzer->ir_signal); + } + + return true; +} + +static XRemoteSignalAnalyzer* xremote_signal_analyzer_alloc(XRemoteAppContext* app_ctx) +{ + XRemoteSignalAnalyzer *analyzer = malloc(sizeof(XRemoteSignalAnalyzer)); + analyzer->ir_signal = infrared_signal_alloc(); + analyzer->app_ctx = app_ctx; + analyzer->pause = false; + + analyzer->signal_view = xremote_signal_success_view_alloc(app_ctx, analyzer); + View* view = xremote_view_get_view(analyzer->signal_view); + view_set_previous_callback(view, xremote_signal_analyzer_view_exit_callback); + view_dispatcher_add_view(app_ctx->view_dispatcher, XRemoteViewSignal, view); + + view_dispatcher_set_custom_event_callback(app_ctx->view_dispatcher, xremote_signal_analyzer_custom_event_callback); + view_dispatcher_set_event_callback_context(app_ctx->view_dispatcher, analyzer); + + analyzer->ir_receiver = xremote_signal_receiver_alloc(app_ctx); + xremote_signal_receiver_set_context(analyzer->ir_receiver, analyzer, NULL); + xremote_signal_receiver_set_rx_callback(analyzer->ir_receiver, xremote_signal_analyzer_signal_callback); + + return analyzer; +} + +static void xremote_signal_analyzer_free(XRemoteSignalAnalyzer* analyzer) +{ + xremote_app_assert_void(analyzer); + xremote_signal_receiver_stop(analyzer->ir_receiver); + + ViewDispatcher* view_disp = analyzer->app_ctx->view_dispatcher; + view_dispatcher_set_custom_event_callback(view_disp, NULL); + view_dispatcher_set_event_callback_context(view_disp, NULL); + + view_dispatcher_remove_view(view_disp, XRemoteViewSignal); + xremote_view_free(analyzer->signal_view); + + xremote_signal_receiver_free(analyzer->ir_receiver); + infrared_signal_free(analyzer->ir_signal); + free(analyzer); +} + +static void xremote_signal_analyzer_clear_callback(void* context) +{ + XRemoteSignalAnalyzer *analyzer = context; + xremote_signal_analyzer_free(analyzer); +} + +XRemoteApp* xremote_analyzer_alloc(XRemoteAppContext* app_ctx) +{ + XRemoteApp* app = xremote_app_alloc(app_ctx); + app->view_id = XRemoteViewAnalyzer; + + XRemoteSignalAnalyzer* analyzer = xremote_signal_analyzer_alloc(app_ctx); + app->view_ctx = xremote_signal_view_alloc(app->app_ctx, analyzer); + View* view = xremote_view_get_view(app->view_ctx); + + ViewDispatcher* view_disp = app_ctx->view_dispatcher; + view_dispatcher_add_view(view_disp, app->view_id, view); + + xremote_app_view_set_previous_callback(app, xremote_signal_analyzer_view_exit_callback); + xremote_app_set_view_context(app, analyzer, xremote_signal_analyzer_clear_callback); + + xremote_signal_receiver_start(analyzer->ir_receiver); + return app; +} diff --git a/xremote_analyzer.h b/xremote_analyzer.h new file mode 100644 index 0000000..d6f5efe --- /dev/null +++ b/xremote_analyzer.h @@ -0,0 +1,21 @@ +/*! + * @file flipper-xremote/xremote_analyzer.h + @license This project is released under the GNU GPLv3 License + * @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com) + * + * @brief Infrared remote singnal analyzer and custom view events. + */ + +#pragma once + +#include "xremote_app.h" +#include "xremote_signal.h" + +typedef struct XRemoteSignalAnalyzer XRemoteSignalAnalyzer; + +void xremote_signal_analyzer_send_event(XRemoteSignalAnalyzer* analyzer, XRemoteEvent event); +XRemoteSignalReceiver* xremote_signal_analyzer_get_ir_receiver(XRemoteSignalAnalyzer *analyzer); +XRemoteAppContext* xremote_signal_analyzer_get_app_context(XRemoteSignalAnalyzer *analyzer); +InfraredSignal* xremote_signal_analyzer_get_ir_signal(XRemoteSignalAnalyzer *analyzer); + +XRemoteApp* xremote_analyzer_alloc(XRemoteAppContext* app_ctx); diff --git a/xremote_app.c b/xremote_app.c index 3f0a689..39ef6fc 100644 --- a/xremote_app.c +++ b/xremote_app.c @@ -115,6 +115,7 @@ XRemoteAppContext* xremote_app_context_alloc(void *arg) ctx->view_dispatcher = view_dispatcher_alloc(); view_dispatcher_enable_queue(ctx->view_dispatcher); view_dispatcher_attach_to_gui(ctx->view_dispatcher, ctx->gui, ViewDispatcherTypeFullscreen); + return ctx; } @@ -144,11 +145,25 @@ const char* xremote_app_context_get_exit_str(XRemoteAppContext* ctx) return exit_behavior == XRemoteAppExitHold ? "Hold to exit" : "Press to exit"; } +void xremote_app_notification_blink(NotificationApp* notifications) +{ + xremote_app_assert_void(notifications); + notification_message(notifications, &g_sequence_blink_purple_50); +} + void xremote_app_context_notify_led(XRemoteAppContext* app_ctx) { xremote_app_assert_void(app_ctx); - NotificationApp* notifications = app_ctx->notifications; - notification_message(notifications, &g_sequence_blink_purple_50); + xremote_app_notification_blink(app_ctx->notifications); +} + +bool xremote_app_send_signal(XRemoteAppContext* app_ctx, InfraredSignal* signal) +{ + xremote_app_assert(signal, false); + XRemoteAppSettings* settings = app_ctx->app_settings; + infrared_signal_transmit_times(signal, settings->repeat_count); + xremote_app_context_notify_led(app_ctx); + return true; } void xremote_app_view_alloc(XRemoteApp *app, uint32_t view_id, XRemoteViewAllocator allocator) @@ -259,14 +274,14 @@ void xremote_app_view_set_previous_callback(XRemoteApp* app, ViewNavigationCallb view_set_previous_callback(view, callback); } -void xremote_app_set_view_context(XRemoteApp* app, void *context, XRemoteViewClearCallback on_clear) +void xremote_app_set_view_context(XRemoteApp* app, void *context, XRemoteClearCallback on_clear) { furi_assert(app); xremote_app_assert_void(app->view_ctx); xremote_view_set_context(app->view_ctx, context, on_clear); } -void xremote_app_set_user_context(XRemoteApp* app, void *context, XRemoteAppClearCallback on_clear) +void xremote_app_set_user_context(XRemoteApp* app, void *context, XRemoteClearCallback on_clear) { furi_assert(app); app->on_clear = on_clear; diff --git a/xremote_app.h b/xremote_app.h index 08cd19b..3dbd390 100644 --- a/xremote_app.h +++ b/xremote_app.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include #include "views/xremote_common_view.h" #include "xc_icons.h" @@ -63,12 +65,11 @@ void xremote_app_context_free(XRemoteAppContext* ctx); const char* xremote_app_context_get_exit_str(XRemoteAppContext* ctx); void xremote_app_context_notify_led(XRemoteAppContext* app_ctx); - -typedef XRemoteView* (*XRemoteViewAllocator)(void* app_ctx); -typedef void (*XRemoteAppClearCallback)(void *context); +void xremote_app_notification_blink(NotificationApp* notifications); +bool xremote_app_send_signal(XRemoteAppContext* app_ctx, InfraredSignal* signal); typedef struct { - XRemoteAppClearCallback on_clear; + XRemoteClearCallback on_clear; XRemoteAppContext* app_ctx; XRemoteViewID submenu_id; XRemoteViewID view_id; @@ -85,8 +86,8 @@ void xremote_app_view_alloc(XRemoteApp *app, uint32_t view_id, XRemoteViewAlloca void xremote_app_view_free(XRemoteApp* app); void xremote_app_view_set_previous_callback(XRemoteApp* app, ViewNavigationCallback callback); -void xremote_app_set_view_context(XRemoteApp* app, void *context, XRemoteViewClearCallback on_clear); -void xremote_app_set_user_context(XRemoteApp* app, void *context, XRemoteAppClearCallback on_clear); +void xremote_app_set_view_context(XRemoteApp* app, void *context, XRemoteClearCallback on_clear); +void xremote_app_set_user_context(XRemoteApp* app, void *context, XRemoteClearCallback on_clear); void xremote_app_user_context_free(XRemoteApp* app); bool xremote_app_has_view(XRemoteApp *app, uint32_t view_id); diff --git a/xremote_learn.c b/xremote_learn.c index 9846af5..6246419 100644 --- a/xremote_learn.c +++ b/xremote_learn.c @@ -9,16 +9,436 @@ #include "xremote_learn.h" #include "views/xremote_learn_view.h" +#define XREMOTE_TEXT_MAX 128 + +struct XRemoteLearnContext { + /* XRemote context */ + XRemoteSignalReceiver* ir_receiver; + XRemoteAppContext* app_ctx; + XRemoteView* signal_view; + XRemoteViewID curr_view; + XRemoteViewID prev_view; + + /* Main infrared app context */ + InfraredRemote* ir_remote; + InfraredSignal* ir_signal; + + /* User interactions */ + TextInput* text_input; + DialogEx* dialog_ex; + + /* User context and clear callback */ + XRemoteClearCallback on_clear; + void* context; + + /* Private control flags */ + char text_store[XREMOTE_TEXT_MAX + 1]; + uint8_t current_button; + bool finish_learning; + bool stop_receiver; + bool is_dirty; +}; + +void xremote_learn_send_event(XRemoteLearnContext* learn_ctx, XRemoteEvent event) +{ + xremote_app_assert_void(learn_ctx); + if (event == XRemoteEventSignalFinish) learn_ctx->finish_learning = true; + ViewDispatcher* view_disp = learn_ctx->app_ctx->view_dispatcher; + view_dispatcher_send_custom_event(view_disp, event); +} + +const char* xremote_learn_get_curr_button_name(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert(learn_ctx, NULL); + return xremote_button_get_name(learn_ctx->current_button); +} + +int xremote_learn_get_curr_button_index(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert(learn_ctx, -1); + return learn_ctx->current_button; +} + +InfraredRemote* xremote_learn_get_ir_remote(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert(learn_ctx, NULL); + return learn_ctx->ir_remote; +} + +InfraredSignal* xremote_learn_get_ir_signal(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert(learn_ctx, NULL); + return learn_ctx->ir_signal; +} + +XRemoteSignalReceiver* xremote_learn_get_ir_receiver(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert(learn_ctx, NULL); + return learn_ctx->ir_receiver; +} + +XRemoteAppContext* xremote_learn_get_app_context(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert(learn_ctx, NULL); + return learn_ctx->app_ctx; +} + +bool xremote_learn_has_buttons(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert(learn_ctx, false); + return infrared_remote_get_button_count(learn_ctx->ir_remote) > 0; +} + +static void xremote_learn_switch_to_view(XRemoteLearnContext* learn_ctx, XRemoteViewID view_id) +{ + xremote_app_assert_void(learn_ctx); + learn_ctx->prev_view = learn_ctx->curr_view; + learn_ctx->curr_view = view_id; + ViewDispatcher* view_disp = learn_ctx->app_ctx->view_dispatcher; + view_dispatcher_switch_to_view(view_disp, view_id); +} + +static void xremote_learn_context_rx_stop(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert_void(learn_ctx); + learn_ctx->stop_receiver = true; + xremote_signal_receiver_stop(learn_ctx->ir_receiver); +} + +static void xremote_learn_context_rx_start(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert_void(learn_ctx); + learn_ctx->finish_learning = false; + learn_ctx->stop_receiver = false; + xremote_signal_receiver_start(learn_ctx->ir_receiver); +} + + +static void xremote_learn_exit_dialog_free(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert_void(learn_ctx); + xremote_app_assert_void(learn_ctx->dialog_ex); + + ViewDispatcher* view_disp = learn_ctx->app_ctx->view_dispatcher; + view_dispatcher_remove_view(view_disp, XRemoteViewDialogExit); + + dialog_ex_free(learn_ctx->dialog_ex); + learn_ctx->dialog_ex = NULL; +} + +static void xremote_learn_exit_dialog_alloc(XRemoteLearnContext *learn_ctx, DialogExResultCallback callback) +{ + xremote_app_assert_void(learn_ctx); + xremote_learn_exit_dialog_free(learn_ctx); + + ViewDispatcher *view_disp = learn_ctx->app_ctx->view_dispatcher; + const char* dialog_text = "All unsaved data\nwill be lost!"; + const char* header_text = "Exit to XRemote Menu?"; + + learn_ctx->dialog_ex = dialog_ex_alloc(); + View *view = dialog_ex_get_view(learn_ctx->dialog_ex); + view_dispatcher_add_view(view_disp, XRemoteViewDialogExit, view); + + dialog_ex_set_header(learn_ctx->dialog_ex, header_text, 64, 11, AlignCenter, AlignTop); + dialog_ex_set_text(learn_ctx->dialog_ex, dialog_text, 64, 25, AlignCenter, AlignTop); + dialog_ex_set_icon(learn_ctx->dialog_ex, 0, 0, NULL); + + dialog_ex_set_left_button_text(learn_ctx->dialog_ex, "Exit"); + dialog_ex_set_center_button_text(learn_ctx->dialog_ex, "Save"); + dialog_ex_set_right_button_text(learn_ctx->dialog_ex, "Stay"); + + dialog_ex_set_result_callback(learn_ctx->dialog_ex, callback); + dialog_ex_set_context(learn_ctx->dialog_ex, learn_ctx); +} + static uint32_t xremote_learn_view_exit_callback(void* context) { UNUSED(context); - return XRemoteViewSubmenu; + return XRemoteViewLearn; +} + +static void xremote_learn_dialog_exit_callback(DialogExResult result, void* context) +{ + XRemoteLearnContext* learn_ctx = (XRemoteLearnContext*)context; + xremote_learn_switch_to_view(learn_ctx, XRemoteViewSubmenu); + + if (result == DialogExResultLeft) + xremote_learn_send_event(learn_ctx, XRemoteEventSignalExit); + else if (result == DialogExResultRight) + xremote_learn_send_event(learn_ctx, XRemoteEventSignalRetry); + else if (result == DialogExResultCenter) + xremote_learn_send_event(learn_ctx, XRemoteEventSignalFinish); +} + +static uint32_t xremote_learn_text_input_exit_callback(void* context) +{ + TextInput* text_input = context; + XRemoteLearnContext* learn_ctx; + + learn_ctx = text_input_get_validator_callback_context(text_input); + xremote_app_assert(learn_ctx, XRemoteViewSubmenu); + + XRemoteEvent event = learn_ctx->prev_view == XRemoteViewSignal ? + XRemoteEventSignalReceived : XRemoteEventSignalRetry; + + if (learn_ctx->current_button >= XREMOTE_BUTTON_COUNT) + learn_ctx->current_button = XREMOTE_BUTTON_COUNT - 1; + + learn_ctx->finish_learning = false; + xremote_learn_send_event(learn_ctx, event); + + return XRemoteViewTextInput; +} + +static void xremote_learn_text_input_callback(void* context) +{ + xremote_app_assert_void(context); + XRemoteLearnContext *learn_ctx = context; + + if (learn_ctx->is_dirty) + { + const char* name = xremote_learn_get_curr_button_name(learn_ctx); + if (!infrared_remote_get_button_by_name(learn_ctx->ir_remote, name)) + { + InfraredSignal* signal = xremote_learn_get_ir_signal(learn_ctx); + infrared_remote_push_button(learn_ctx->ir_remote, name, signal); + } + + learn_ctx->is_dirty = false; + } + + if (infrared_remote_get_button_count(learn_ctx->ir_remote) && learn_ctx->text_store[0] != '\0') + { + char output_file[256]; + snprintf(output_file, sizeof(output_file), "%s/%s.ir", + XREMOTE_APP_FOLDER, learn_ctx->text_store); + + infrared_remote_set_name(learn_ctx->ir_remote, learn_ctx->text_store); + infrared_remote_set_path(learn_ctx->ir_remote, output_file); + infrared_remote_store(learn_ctx->ir_remote); + infrared_remote_reset(learn_ctx->ir_remote); + } + + xremote_learn_send_event(learn_ctx, XRemoteEventSignalExit); +} + +static void xremote_learn_signal_callback(void *context, InfraredSignal* signal) +{ + XRemoteLearnContext* learn_ctx = context; + xremote_app_assert_void(!learn_ctx->stop_receiver); + xremote_app_assert_void(!learn_ctx->finish_learning); + + learn_ctx->stop_receiver = true; + learn_ctx->is_dirty = true; + + infrared_signal_set_signal(learn_ctx->ir_signal, signal); + xremote_learn_send_event(learn_ctx, XRemoteEventSignalReceived); +} + +static void xremote_learn_exit_dialog_ask(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert_void(learn_ctx); + + if (infrared_remote_get_button_count(learn_ctx->ir_remote) || learn_ctx->is_dirty) + { + xremote_learn_exit_dialog_alloc(learn_ctx, xremote_learn_dialog_exit_callback); + xremote_learn_switch_to_view(learn_ctx, XRemoteViewDialogExit); + return; + } + + learn_ctx->is_dirty = false; + xremote_learn_switch_to_view(learn_ctx, XRemoteViewSubmenu); +} + +static void xremote_learn_finish(XRemoteLearnContext *learn_ctx) +{ + xremote_app_assert_void(learn_ctx); + xremote_app_assert_void(learn_ctx->text_input); + + if (infrared_remote_get_button_count(learn_ctx->ir_remote) || learn_ctx->is_dirty) + { + snprintf(learn_ctx->text_store, XREMOTE_TEXT_MAX, "Remote_"); + text_input_set_header_text(learn_ctx->text_input, "Name new remote"); + + text_input_set_result_callback( + learn_ctx->text_input, + xremote_learn_text_input_callback, + learn_ctx, + learn_ctx->text_store, + XREMOTE_TEXT_MAX, + true); + + xremote_learn_switch_to_view(learn_ctx, XRemoteViewTextInput); + return; + } + + learn_ctx->is_dirty = false; + xremote_learn_switch_to_view(learn_ctx, XRemoteViewSubmenu); +} + +static bool xremote_learn_custom_event_callback(void* context, uint32_t event) +{ + xremote_app_assert(context, false); + XRemoteLearnContext *learn_ctx = context; + + if (learn_ctx->finish_learning && + event != XRemoteEventSignalFinish && + event != XRemoteEventSignalAskExit && + event != XRemoteEventSignalExit) return true; + + if (event == XRemoteEventSignalReceived) + { + xremote_learn_context_rx_stop(learn_ctx); + xremote_learn_switch_to_view(learn_ctx, XRemoteViewSignal); + } + else if (event == XRemoteEventSignalSave) + { + const char* name = xremote_learn_get_curr_button_name(learn_ctx); + infrared_remote_delete_button_by_name(learn_ctx->ir_remote, name); + + InfraredSignal* signal = xremote_learn_get_ir_signal(learn_ctx); + infrared_remote_push_button(learn_ctx->ir_remote, name, signal); + learn_ctx->is_dirty = false; + + if (++learn_ctx->current_button >= XREMOTE_BUTTON_COUNT) + { + xremote_learn_context_rx_stop(learn_ctx); + xremote_learn_finish(learn_ctx); + return true; + } + + xremote_learn_context_rx_start(learn_ctx); + xremote_learn_switch_to_view(learn_ctx, XRemoteViewLearn); + } + else if (event == XRemoteEventSignalSkip) + { + learn_ctx->current_button++; + learn_ctx->is_dirty = false; + + if (learn_ctx->current_button >= XREMOTE_BUTTON_COUNT) + { + if (xremote_learn_has_buttons(learn_ctx)) + { + xremote_learn_context_rx_stop(learn_ctx); + xremote_learn_finish(learn_ctx); + return true; + } + + learn_ctx->current_button = 0; + } + + xremote_learn_context_rx_start(learn_ctx); + xremote_learn_switch_to_view(learn_ctx, XRemoteViewLearn); + } + else if (event == XRemoteEventSignalRetry) + { + learn_ctx->is_dirty = false; + xremote_learn_context_rx_start(learn_ctx); + xremote_learn_switch_to_view(learn_ctx, XRemoteViewLearn); + } + else if (event == XRemoteEventSignalFinish) + { + xremote_learn_context_rx_stop(learn_ctx); + xremote_learn_finish(learn_ctx); + } + else if (event == XRemoteEventSignalAskExit) + { + xremote_learn_context_rx_stop(learn_ctx); + xremote_learn_exit_dialog_ask(learn_ctx); + } + else if (event == XRemoteEventSignalExit) + { + learn_ctx->is_dirty = false; + xremote_learn_context_rx_stop(learn_ctx); + xremote_learn_switch_to_view(learn_ctx, XRemoteViewSubmenu); + } + + return true; +} + +static XRemoteLearnContext* xremote_learn_context_alloc(XRemoteAppContext* app_ctx) +{ + XRemoteLearnContext *learn_ctx = malloc(sizeof(XRemoteLearnContext)); + learn_ctx->ir_signal = infrared_signal_alloc(); + learn_ctx->ir_remote = infrared_remote_alloc(); + + learn_ctx->app_ctx = app_ctx; + learn_ctx->dialog_ex = NULL; + learn_ctx->text_store[0] = 0; + learn_ctx->current_button = 0; + + learn_ctx->curr_view = XRemoteViewLearn; + learn_ctx->prev_view = XRemoteViewNone; + + learn_ctx->finish_learning = false; + learn_ctx->stop_receiver = false; + learn_ctx->is_dirty = false; + + learn_ctx->signal_view = xremote_learn_success_view_alloc(app_ctx, learn_ctx); + View* view = xremote_view_get_view(learn_ctx->signal_view); + view_set_previous_callback(view, xremote_learn_view_exit_callback); + view_dispatcher_add_view(app_ctx->view_dispatcher, XRemoteViewSignal, view); + + learn_ctx->text_input = text_input_alloc(); + text_input_set_validator(learn_ctx->text_input, NULL, learn_ctx); + + view = text_input_get_view(learn_ctx->text_input); + view_set_previous_callback(view, xremote_learn_text_input_exit_callback); + view_dispatcher_add_view(app_ctx->view_dispatcher, XRemoteViewTextInput, view); + + view_dispatcher_set_custom_event_callback(app_ctx->view_dispatcher, xremote_learn_custom_event_callback); + view_dispatcher_set_event_callback_context(app_ctx->view_dispatcher, learn_ctx); + + learn_ctx->ir_receiver = xremote_signal_receiver_alloc(app_ctx); + xremote_signal_receiver_set_context(learn_ctx->ir_receiver, learn_ctx, NULL); + xremote_signal_receiver_set_rx_callback(learn_ctx->ir_receiver, xremote_learn_signal_callback); + + return learn_ctx; +} + +static void xremote_learn_context_free(XRemoteLearnContext* learn_ctx) +{ + xremote_app_assert_void(learn_ctx); + xremote_signal_receiver_stop(learn_ctx->ir_receiver); + xremote_learn_exit_dialog_free(learn_ctx); + + ViewDispatcher* view_disp = learn_ctx->app_ctx->view_dispatcher; + view_dispatcher_set_custom_event_callback(view_disp, NULL); + view_dispatcher_set_event_callback_context(view_disp, NULL); + + view_dispatcher_remove_view(view_disp, XRemoteViewTextInput); + text_input_free(learn_ctx->text_input); + + view_dispatcher_remove_view(view_disp, XRemoteViewSignal); + xremote_view_free(learn_ctx->signal_view); + + xremote_signal_receiver_free(learn_ctx->ir_receiver); + infrared_signal_free(learn_ctx->ir_signal); + infrared_remote_free(learn_ctx->ir_remote); + free(learn_ctx); +} + +static void xremote_learn_context_clear_callback(void* context) +{ + XRemoteLearnContext *learn = context; + xremote_learn_context_free(learn); } XRemoteApp* xremote_learn_alloc(XRemoteAppContext* app_ctx) { XRemoteApp* app = xremote_app_alloc(app_ctx); - xremote_app_view_alloc(app, XRemoteViewLearn, xremote_learn_view_alloc); + app->view_id = XRemoteViewLearn; + + XRemoteLearnContext* learn = xremote_learn_context_alloc(app_ctx); + app->view_ctx = xremote_learn_view_alloc(app->app_ctx, learn); + View* view = xremote_view_get_view(app->view_ctx); + + ViewDispatcher* view_disp = app_ctx->view_dispatcher; + view_dispatcher_add_view(view_disp, app->view_id, view); + xremote_app_view_set_previous_callback(app, xremote_learn_view_exit_callback); + xremote_app_set_view_context(app, learn, xremote_learn_context_clear_callback); + + xremote_signal_receiver_start(learn->ir_receiver); return app; } diff --git a/xremote_learn.h b/xremote_learn.h index 9a20409..cb36d56 100644 --- a/xremote_learn.h +++ b/xremote_learn.h @@ -9,5 +9,18 @@ #pragma once #include "xremote_app.h" +#include "xremote_signal.h" + +typedef struct XRemoteLearnContext XRemoteLearnContext; + +void xremote_learn_send_event(XRemoteLearnContext* learn_ctx, XRemoteEvent event); +const char* xremote_learn_get_curr_button_name(XRemoteLearnContext *learn_ctx); +int xremote_learn_get_curr_button_index(XRemoteLearnContext *learn_ctx); +bool xremote_learn_has_buttons(XRemoteLearnContext *learn_ctx); + +XRemoteSignalReceiver* xremote_learn_get_ir_receiver(XRemoteLearnContext *learn_ctx); +XRemoteAppContext* xremote_learn_get_app_context(XRemoteLearnContext *learn_ctx); +InfraredRemote* xremote_learn_get_ir_remote(XRemoteLearnContext *learn_ctx); +InfraredSignal* xremote_learn_get_ir_signal(XRemoteLearnContext *learn_ctx); XRemoteApp* xremote_learn_alloc(XRemoteAppContext* app_ctx); diff --git a/xremote_signal.c b/xremote_signal.c new file mode 100644 index 0000000..029d84f --- /dev/null +++ b/xremote_signal.c @@ -0,0 +1,131 @@ +/*! + * @file flipper-xremote/xremote_signal.h + @license This project is released under the GNU GPLv3 License + * @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com) + * + * @brief Implementation of infrared signal receiver functionality + */ + +#include "xremote_signal.h" + +struct XRemoteSignalReceiver { + XRemoteClearCallback on_clear; + XRemoteRxCallback rx_callback; + + NotificationApp* notifications; + InfraredWorker* worker; + InfraredSignal* signal; + + void* context; + bool started; +}; + +static void xremote_signal_receiver_rx_callback(void* context, InfraredWorkerSignal* ir_signal) +{ + furi_assert(context); + XRemoteSignalReceiver *rx_ctx = context; + xremote_app_notification_blink(rx_ctx->notifications); + + if (infrared_worker_signal_is_decoded(ir_signal)) + { + const InfraredMessage* message; + message = infrared_worker_get_decoded_signal(ir_signal); + infrared_signal_set_message(rx_ctx->signal, message); + } + else + { + const uint32_t* timings; + size_t timings_size = 0; + + infrared_worker_get_raw_signal(ir_signal, &timings, &timings_size); + infrared_signal_set_raw_signal(rx_ctx->signal, timings, timings_size, + INFRARED_COMMON_CARRIER_FREQUENCY, INFRARED_COMMON_DUTY_CYCLE); + } + + if (rx_ctx->rx_callback != NULL) + rx_ctx->rx_callback(rx_ctx->context, rx_ctx->signal); +} + +static void xremote_signal_receiver_clear_context(XRemoteSignalReceiver* rx_ctx) +{ + xremote_app_assert_void(rx_ctx); + xremote_app_assert_void(rx_ctx->context); + xremote_app_assert_void(rx_ctx->on_clear); + rx_ctx->on_clear(rx_ctx->context); + rx_ctx->context = NULL; +} + +XRemoteSignalReceiver* xremote_signal_receiver_alloc(XRemoteAppContext* app_ctx) +{ + XRemoteSignalReceiver *rx_ctx = malloc(sizeof(XRemoteSignalReceiver)); + rx_ctx->signal = infrared_signal_alloc(); + rx_ctx->worker = infrared_worker_alloc(); + + rx_ctx->notifications = app_ctx->notifications; + rx_ctx->rx_callback = NULL; + rx_ctx->on_clear = NULL; + rx_ctx->context = NULL; + rx_ctx->started = false; + return rx_ctx; +} + +void xremote_signal_receiver_free(XRemoteSignalReceiver* rx_ctx) +{ + xremote_app_assert_void(rx_ctx); + xremote_signal_receiver_stop(rx_ctx); + infrared_worker_free(rx_ctx->worker); + infrared_signal_free(rx_ctx->signal); + xremote_signal_receiver_clear_context(rx_ctx); + free(rx_ctx); +} + +void xremote_signal_receiver_set_context(XRemoteSignalReceiver* rx_ctx, void *context, XRemoteClearCallback on_clear) +{ + xremote_signal_receiver_clear_context(rx_ctx); + rx_ctx->on_clear = on_clear; + rx_ctx->context = context; +} + +void xremote_signal_receiver_set_rx_callback(XRemoteSignalReceiver* rx_ctx, XRemoteRxCallback rx_callback) +{ + xremote_app_assert_void(rx_ctx); + rx_ctx->rx_callback = rx_callback; +} + +void xremote_signal_receiver_start(XRemoteSignalReceiver *rx_ctx) +{ + xremote_app_assert_void((rx_ctx && rx_ctx->worker && !rx_ctx->started)); + infrared_worker_rx_set_received_signal_callback(rx_ctx->worker, + xremote_signal_receiver_rx_callback, (void*)rx_ctx); + + infrared_worker_rx_start(rx_ctx->worker); + xremote_app_notification_blink(rx_ctx->notifications); + rx_ctx->started = true; +} + +void xremote_signal_receiver_stop(XRemoteSignalReceiver *rx_ctx) +{ + xremote_app_assert_void((rx_ctx && rx_ctx->worker && rx_ctx->started)); + infrared_worker_rx_set_received_signal_callback(rx_ctx->worker, NULL, NULL); + infrared_worker_rx_stop(rx_ctx->worker); + rx_ctx->started = false; +} + +void xremote_signal_receiver_pause(XRemoteSignalReceiver *rx_ctx) +{ + xremote_app_assert_void((rx_ctx && rx_ctx->worker)); + infrared_worker_rx_set_received_signal_callback(rx_ctx->worker, NULL, NULL); +} + +void xremote_signal_receiver_resume(XRemoteSignalReceiver *rx_ctx) +{ + xremote_app_assert_void((rx_ctx && rx_ctx->worker)); + infrared_worker_rx_set_received_signal_callback(rx_ctx->worker, + xremote_signal_receiver_rx_callback, (void*)rx_ctx); +} + +InfraredSignal* xremote_signal_receiver_get_signal(XRemoteSignalReceiver *rx_ctx) +{ + xremote_app_assert(rx_ctx, NULL); + return rx_ctx->signal; +} \ No newline at end of file diff --git a/xremote_signal.h b/xremote_signal.h new file mode 100644 index 0000000..e1421f9 --- /dev/null +++ b/xremote_signal.h @@ -0,0 +1,28 @@ +/*! + * @file flipper-xremote/xremote_signal.h + @license This project is released under the GNU GPLv3 License + * @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com) + * + * @brief Implementation of infrared signal receiver functionality + */ + +#pragma once + +#include "xremote_app.h" +#include "infrared/infrared_signal.h" + +typedef void (*XRemoteRxCallback)(void *context, InfraredSignal* signal); +typedef struct XRemoteSignalReceiver XRemoteSignalReceiver; + +XRemoteSignalReceiver* xremote_signal_receiver_alloc(XRemoteAppContext* app_ctx); +void xremote_signal_receiver_free(XRemoteSignalReceiver* rx_ctx); + +void xremote_signal_receiver_set_context(XRemoteSignalReceiver* rx_ctx, void *context, XRemoteClearCallback on_clear); +void xremote_signal_receiver_set_rx_callback(XRemoteSignalReceiver* rx_ctx, XRemoteRxCallback rx_callback); +InfraredSignal* xremote_signal_receiver_get_signal(XRemoteSignalReceiver *rx_ctx); + +void xremote_signal_receiver_start(XRemoteSignalReceiver *rx_ctx); +void xremote_signal_receiver_stop(XRemoteSignalReceiver *rx_ctx); + +void xremote_signal_receiver_pause(XRemoteSignalReceiver *rx_ctx); +void xremote_signal_receiver_resume(XRemoteSignalReceiver *rx_ctx);

Settings