From 77f386f0b9c57a770aaab416f37207346e6e79cb Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 31 Jan 2024 22:26:22 +0100 Subject: [PATCH] Simplify Hello World apps and update docs (#28) * Simplify Hello World apps * Updated docs --- README.md | 56 ++++++++++++++++++-------- app-esp/src/hello_world/hello_world.c | 6 +-- app-sim/src/hello_world/hello_world.c | 6 +-- docs/app-lifecycle.md | 12 ++++++ docs/app-lifecycle.puml | 9 +++++ docs/pics/app-lifecycle.png | Bin 0 -> 13321 bytes docs/pics/hello-world.png | Bin 0 -> 10373 bytes 7 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 docs/app-lifecycle.md create mode 100644 docs/app-lifecycle.puml create mode 100644 docs/pics/app-lifecycle.png create mode 100644 docs/pics/hello-world.png diff --git a/README.md b/README.md index fa71d41c..6ff60638 100644 --- a/README.md +++ b/README.md @@ -7,33 +7,53 @@ It provides an application framework that is based on code from the [Flipper Zer ![Tactility shown on a Lilygo T-Deck device and on PC](docs/pics/tactility-showcase.jpg) -Tactility features: -- A hardware abstraction layer -- UI capabilities (via LVGL) -- An application platform that can run apps and services -- PC app support to speed up development for ESP32 apps +Noteworthy features: +- Touch UI capabilities (via LVGL) with support for input devices such as on-device trackball or keyboard. +- An application platform that can run apps and services. +- Basic applications to boost productivity, such as a Wi-Fi connectivity app. +- Run Tactility apps on PC to speed up development. Requirements: - ESP32 (any?) with a display that has touch capability - [esp-idf 5.1.2](https://docs.espressif.com/projects/esp-idf/en/v5.1.2/esp32/get-started/index.html) or a newer v5.1.x -## Technologies +## Making apps is easy! -UI is created with [lvgl](https://github.com/lvgl/lvgl). -Any LVGL-capable device is supported. +Apps are described in "manifest". The manifest provides some basic information on +the app name and icon, but also tells Tactility what needs to happen when the app runs. -In general, [esp_lvgl_port](https://github.com/espressif/esp-bsp/tree/master/components/esp_lvgl_port) -is the preferred solution if it supports your hardware: -Those LCD and input drivers are based on [esp_lcd](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/lcd.html) -and [esp_lcd_touch](https://components.espressif.com/components/espressif/esp_lcd_touch). -They are generally available via the Espressif Registry: [here](https://components.espressif.com/components?q=esp_lcd) -and [here](https://components.espressif.com/components?q=esp_lcd_touch) +UI is created with [lvgl](https://github.com/lvgl/lvgl) which has lots of [widgets](https://docs.lvgl.io/8.3/widgets/index.html)! +Creating a touch-capable UI is [easy](https://docs.lvgl.io/8.3/get-started/quick-overview.html) and doesn't require your own render loop! + +```c +static void app_show(TT_UNUSED App app, lv_obj_t* parent) { + lv_obj_t* label = lv_label_create(parent); + lv_label_set_text(label, "Hello, world!"); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + // Widgets are auto-removed from the parent when the app is closed +} + +const AppManifest hello_world_app = { + .id = "helloworld", + .name = "Hello World", + .icon = NULL, + .type = AppTypeUser, + .on_start = NULL, + .on_stop = NULL, + .on_show = &app_show, + .on_hide = NULL +}; +``` + +This shows as follows: + +![hello world app](docs/pics/hello-world.png) ## Supported Hardware -### Devices - -Most hardware configurations should work, but it might require you to set up the drivers yourself. +Any ESP32 device with a touch screen should be able to run Tactility, +because LVGL is set up in a platform-agnostic manner. +Implementing drivers can take some effort, so Tactility provides support for several devices. Predefined configurations are available for: @@ -74,6 +94,8 @@ The build scripts will detect if ESP-IDF is available. They will adapter if you ### Development +Take a look at the [App Lifecycle](docs/app-lifecycle.md). + Directories explained: - `app-esp`: The ESP32 application example diff --git a/app-esp/src/hello_world/hello_world.c b/app-esp/src/hello_world/hello_world.c index ef805f10..3d80faa0 100644 --- a/app-esp/src/hello_world/hello_world.c +++ b/app-esp/src/hello_world/hello_world.c @@ -3,9 +3,6 @@ static void app_show(TT_UNUSED App app, lv_obj_t* parent) { lv_obj_t* label = lv_label_create(parent); - lv_label_set_recolor(label, true); - lv_obj_set_width(label, 200); - lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); lv_label_set_text(label, "Hello, world!"); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); } @@ -17,5 +14,6 @@ const AppManifest hello_world_app = { .type = AppTypeUser, .on_start = NULL, .on_stop = NULL, - .on_show = &app_show + .on_show = &app_show, + .on_hide = NULL }; diff --git a/app-sim/src/hello_world/hello_world.c b/app-sim/src/hello_world/hello_world.c index bb101e77..bfdb03a9 100644 --- a/app-sim/src/hello_world/hello_world.c +++ b/app-sim/src/hello_world/hello_world.c @@ -4,9 +4,6 @@ static void app_show(TT_UNUSED App app, lv_obj_t* parent) { lv_obj_t* label = lv_label_create(parent); - lv_label_set_recolor(label, true); - lv_obj_set_width(label, 200); - lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); lv_label_set_text(label, "Hello, world!"); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); } @@ -18,5 +15,6 @@ const AppManifest hello_world_app = { .type = AppTypeUser, .on_start = NULL, .on_stop = NULL, - .on_show = &app_show + .on_show = &app_show, + .on_hide = NULL }; diff --git a/docs/app-lifecycle.md b/docs/app-lifecycle.md new file mode 100644 index 00000000..6ad68830 --- /dev/null +++ b/docs/app-lifecycle.md @@ -0,0 +1,12 @@ +# Application Lifecycle + +The app goes through these states: + +![app lifecycle state diagram](pics/app-lifecycle.png) + +Let's look at a scenario where an app launches another app: + +1. `first` app starts: `first.on_create()` -> `first.on_show()` +2. `second` app starts: `first.on_hide()` -> `second.on_create()` -> `second.on_show()` +3. `second` app stops: `second.on_hide()` -> `second.on_destroy()` -> `first.on_show()` +4. `first` app stops: `first.on_hide()` -> `first.on_destroy()` diff --git a/docs/app-lifecycle.puml b/docs/app-lifecycle.puml new file mode 100644 index 00000000..2ed42a27 --- /dev/null +++ b/docs/app-lifecycle.puml @@ -0,0 +1,9 @@ +@startuml +[*] --> on_create : app is started +on_create --> on_show : app becomes visible +on_show --> on_hide : app is no longer visible +on_hide --> on_destroy : app is being closed +on_destroy --> [*] +skinparam ranksep 25 +skinparam padding 2 +@enduml \ No newline at end of file diff --git a/docs/pics/app-lifecycle.png b/docs/pics/app-lifecycle.png new file mode 100644 index 0000000000000000000000000000000000000000..9b5edf024a9d86c656f067590a2085472bdf1bb9 GIT binary patch literal 13321 zcmche2{@F0+xH9Enz1j#SQ7~`_9aX9EJcY(ma(SnWXqCmtPxTwQYf-yBudCK_C!gV zvhT{iW#>KB|Nq?g{XEb69?$z8_whQ8ImUIFxt8Dc`<>_a`#DcBhG(^r)a=v;4je%0 zoYpuG@9z&BIM_!?0>7;(wJyV(khi9(x2>DIzq7rA_W^BtS9?#ax4j*YjX%#7Z*O-m z1u-#qXRNFDbr)w*TQ?V9Gc*daq6di7UOL#(1fd5rA zw-Ty8>gZ{{b3-=q_kEgQKFM(Lc`dztZ3nj(q+E3q9f~kym3-8 zB7ujzP{@$wEJDX4M&aY!s$104J3o>|q}uXd@pzm)=~+Bx%zI5xYSQk-B4gKZa-rm= zU&U!14;r&4vo+0ZSNz>XXH-2R$@MTT6ne}W*#-8I=gSWzp%~x)vM3R~-7jONs@^2@ zf$mqfs?&N`ES@ zMr!$D@wNU2oBW_+`6hf?w90IL>e<=bmZb*{NSNtps9x~1oXxm#fl2#ab5Nu9g}~yQ zTJ%Vvmk;u;+MotA!eNPttSvN zgEyVrjCc~VTj&k=ax@eOFJHd2^cy~OPMGg@xc;J@n(~;$%*2O>>QOYzE)t{?Nn$Lj zr*Rs%FFif)eto}|rJc{i?HF^2UV=9Xn;G=${*~K@hD1tu)C7)7IL{DEvXohFmNtY@ z-oM?5pZ;1LH)o3IV2%alHqv+T#9_t7#k1sC!o3@$@3Wlu)D;LTE9mGK zk?}b3sn>ZHr2a^wk-0fh9Obc41mCO3f7|Qx*2YGLoQGPxZf%Ke^V{pw0$LeJiJ3vG zEw8mX3-L>=xo0!l+S{43^G$D1vzdfkxtiuuwb+Edu{Ov2_dZG-wwq1D?)29e7D`G= z7mF=FgiRqP8ziei@{SKL^0W$FImeG4t? zKuE2Si((^J`Tp9i#hKJ0Cb zWBn#-B2#s8bY#R7UszapE?eu{w{JooG_vubRIWrk_WeDvJ$^zruRiuNYpT$3J(EXE z{l0@`S3apNpu+i^@Z2HWNwdMb88K^vvcF)tIM4#)5z&-C_*Kph3VzX@u_DF z4L_E4D#txd)J-QSwsdxOwzk%Z>NdGEb8&Sr>T|Kny7pyh#;xEc55PH)Mvt;&3kee) z8F4WTtF7hu7;5i`~YqMs(qS{`~peDEHUYC)CaQ2vhOp(jVuqf-0$A_2><>uy2BMGR6`U?qBChUZtw5h4e6dAlC`0i1SouTu%;}nR5 z3b}JCx3LlBZxXN(>HEL@{W^U$ZQ*&Ub=soirHEIjuM#23eD3O6f6;|$@VGP2kj(cS z`}FBkxSq=ruBao?qf=tfBl=EXMNv{y|NK^ZEG3=|=b}N!YD5(Sk#1(jIeL6{7FmK1 zz36PKzW#wy&2vl|u|4Nov%IphvbY#(x*z;(7PDww$zP{d~(x(VR_f)*DuFj_A6CNFN>(+

qQBzZMaVhefj94{wL~Zg&NO(d> zu<3lm!N$fmJUpChkje7!CZ0hUQ5n1wU}{Q)mD4cbZefFnMyi^IKgIP|-(U?$O-+r- z82WjXpWo%%OHF5*dK(dGgkB1{bii>PeSN#ZW)0#^O&r!~kgms4sPJ;d609PdN;($n zB&?rjgUIpn0QC`g9G^aY+TUyV8XFlIiI#O`dRR4lkdR+c(AdNKGs~6WGuQVcAoIqz zQ(9V0NqpK3fA*Rbn)1r{gy-bv$A+j1W96@-S(usm_xEIFg&|5q$+5%)&XX*nmj$(o zqW)|@bbF_R+qopyj_-Z#)|qkgwcb6^qDy7el$3J8+j4ObZe3s$w$~SfIX|8F?V%60p92T^!Ti9;OYM`c^vWm>G2U(IXl$vyYvp zU=52Ce3SW>I!?~7Z*7%^j4XBOiQQt8#MwVO=}Tkq?#VJ9~?*S8;Y5ZW^=kJ+Um z<(ly3uU)0x1{8UDL$w2gg1)&Ql6PoNs~hc9TI+jh|9<^Na#$D%(@V!rF5U!&I}A+B z%n)2gdfjU5D_dJzQR$vuUj8I}A|mm(Oy4#mOBcLTZewMJPy`Iu2rE&u-DVF+z)ZI2TwD^F@g4-=w} z@$;9wF~(sF*C-Aiej^2{J@b^qJRlf;Kz^WOaoXjB0m%ybYG@(Pz=0IALfIUkM z?{;a-a2=;`W(WW08?fL|D3m5DP2TGnQ9;9^+r^aW7B=y*csqFuMi?FeoRS3Q`G0IM zZJHeH&FjT%Ixvw3NyfUs7o4377p>ByPjzIfWTvGFmeZVLy^g+{naRz_$XD)5u!R)A zHrLN+9+emO7D)=rmASVv^qh%_)}CprjEIB?FE6i>l7;gsuA$te_k~5JK>1 zb8|EomuPuFIIpa%FFbIYY`VMHBiiSifsDY!W%wzP@<$=urW8kQgufwfTYSpsimnUtjd+oy&%WpU*YPgN3o) z*<76_mW)EW_g7O@t&5;yq?;M2-h#bm3r)As#WlMDz2z?FzJtD?J1!!Uu5evQD7@XQ z)ZvG>g5}Jy%)l@kh)}nt9UL6arpwVgu11b_dmkg(x@~=X3`1fn zD=Xs;TsWwZ%#Yw!R+N%rs)M_|{nVKs z^8B7(2`I)%&!H8qTkOoNtPiWm2IL{hSiN@T6xN^`IEs&T;-se+clj>G;&?Yr@tjlI zB8*^)^o9Yw&*&Z8lai)*w$b_dL|@D0i4R6bMyt4{`mMA{>-5uS&zi(|-F_VS-e>CL zK`NBn$0-Z$P-|;zo-ibm?<>KFYC!@H7G5%CX zt(6;3MTe4=ay9j9+wVPWO*y8msfo=7m!h4>Wv}`oX)#8Z5I>)EEUh3$-CdcGz^yVj zG2yc~qFEkCGl9dB*r>LjnQ8=4mMWL~5L?$!E`9*7ns7#4jUxMSDj!R2ODH9`jHt!i zh<=zd)6CTcvbJfhdhVE2TC@xzJX6AN>f@7xq~tldxwd5ulL6zAD~XwTQtj*T90JbF zG;AQYwwF7rtExawJ62;`69-0c;4YX-X;~OmRaH$fC1s#HDX361ZvAf$NrKkMyBzf) z<0PvW#nST9627UKT*0p+U4AMDL^(ZY_$zuoEiwuU0$RTg5@f*E>}&s$pSb%~WKA{c z=^PyONSjmCw6uK2Olq(}PFU$v=wY4Um4geF_nKIl`V_9ygdqG)6t0>2 z_{cOYEiHwa5^IIaOL(^($av+K*vSRXrc*u*SeyG`MNgkXS&{S|f9DR90E>XO!2MPg zM;<$^tAv|1H8op&_!C?LIodu1SkR*&<*OHBT^2FCN-xnSMn%wba~4# z&*3OP_WZ9!Sm63=LP8);g8GPwiD_+X3)miHKgoFi{(ZEJbL}m`5R|SQSuax0z+ey5 zOL|fgug^WnQn$f(Aph9+s->}#GVJB{?^)u_ZT8-ffhVgy*&G^zA=Z{O;?F#k34$2} znw`$x-X1i2+k7sMbJJA+P1s1 zRno?^FYu=kThms1n*>^wXp7;jK2Y@q!6j6b!X@?lZZ1{$V(iXPSK#(yGe1ASjXGVA zbtHCgt7h-vg9k!BVR|X|@9U_zzGf6gn3r8mC6Xc!Z2ISAm|}^8yT4>hHl{gMRBEQC zu88OJzqwYzOL+}nj}cjJoGAAFIf&^iBhp1k-^N})h5HsGfa`x3bOREl#ar+F5PIrV z?c!*&8Aq`5e-Y%i{co=WBRFyC)xye3;NH%f86E#np~q;`6S9U>{;X!~ZJ?Dvwvs+<44^0p&| zB@t?F6}(|@X=!=>d>0Iz83+I4S0~KNFIRZ-D=S-?g|)_snwLAb+~yS-o_#$y2gz=YkYhhCB3)N z8S)4;c{aO-^XF%0E?v50V`I~t!0B;k@?kill(89&d&i`(1Fd21P)$s0mi`*_*D}y#}&R z(d>{!`&kY*6eSH!Qx>|^+`(bukMzb{f4#p(NGo;hoCBEwXlY|(10tdGVvf;?x<1X% zpFf}Up1N7Vp!owaY6Hg=zc|8B#lhTyIr*sp|MI1TcNOUDM-)-C+zC;tEEgBmhqgDC z0q7JV$;im8>pDh+_|qx{*FU%gOL~r-es|Kk_5dv{?Yjb+-?L{z7sh^R8Vwq9#GwH! zUSpLN65aB4;Oh$=98aNX#;U}oy|LKM>VryqUL@c<*ob-V22^lgh(gdYti^7uH znR3F})No%wmLL1GVH1g!2rM|Yqdq8PeGEP}7_UaW$&3S{G}Zmw$cFZfl$n`XrOzw_ zIt)(^_T6{hQXSn??lu^_JaOi)HMlSw#eMhgU0B+0Ma;|fok{9-3ngcO);*MUW7(?H zY;9>F)`Zj$$s$rs9n%Kz1DzcmEH?nRtc*B%~qBS64n2nZItJ@i+ly zwd0YJ@PHy#$plRfI+{QrSeU+qDO|YN*el>%&q7!sZ^a~!kBvoVjBW>l$AU-0=$3|O zMk7C?pS_=&nu0^UZU4-#HC_Yc8I(5DKEvom+Kx?3?2e0tz+Ig%IRVT8GMY_|4&rK@ z!XVrp+_m59Ld)v6RaKX()a#XJq~k-+!f1TF`oZ1TmsOo+yd{Zm#qFnby*SPZiC|QZ zNI8Y0|E~6LdHvYg-UgA%tHr7D+O;2egMe0u@rAj2+!7aEygXpe1=-k)fER4c3&%gv zd!;oVyus`BMTYfaDB?2<`mF}J;Vfh+@vEO_<(^{w3XuQM*cXc(RQt9;Qf>hyg)jyj z(4soPSg8YB^lPXt9FND(!Eu2ygzW9AZ5?q0z?-J=z1-#Gm5+~Ky^>-P`=hL{hLt*e zc<}V|k2{V(m@RBXKVNRGsHn)+PSnZ(nt=zCpHt%ZN2ME5=7Yryuxx=ufurT#ZD2GQ zQWJ^pDtGN~E3X4u2-HvOuJc^5ze`C895i@mb67C{iL?G)>zC{T9=Zfkt9OCpZOqQ; zN;k^E8NV^F9-bg2A#ojiXJKT-$iN`sK6FYd3xMOLSs*cszka>Q%R5De6l&*dVN>uH z6kP$POsl+ppt^BH(0`!HUu9>7ap@QgZ_uXqxLJVay|njI7nk7XSw&3U_jZF&;bg8T zDQYCLsHmvgXZ8}{@OQhL*Gwn})Bse#L=lX#--U%00xg=TTNABQ`FD@*|`7h-RxG%%Ra!`$@+Xu$;ie4Tx*Q5n|-vw=5 zpYC~*a!1?018hotxtL`Y@uj_9^>AWA zK2Cmh03ufKTEDLuFQ`HB!=4Mz!i*tw5fh@L(_~!O;|ltUyYTDlek@hK^IW2BXhy1u zd@Mt*6hcl1&K-TTig9rJteToD36e*Su4}1om>ekcVu8%n3r){%Iw@bz$ zucM>mj1CH)YbNjb*;gUi8A^AHUD`&e4I5ct_dtYAdZz2?;m+nC?HIePYn94bP;voa zQ+PXMXWB|JQ__EFEH6*cdFAJqX>f2Nd7d@8ggXQV(T2aSXHs%zeR0&))zz=E)MAB0 zRzy@Z;n4fGwvUm2FtwWrxj-S7fIDIJZu?$*v)3C6=kUaX_e3q~MJLL%kL{p+vUHzS zB#XCTb?p~-w$>yGL_5drLzML=+-W!!BJ3{*1Spv31-d-mKk<4?>{a1oWJl8qa60Lo z_)e9*zW48^Az$>M?`~soXxyr#UPPI- z1Mw*FBXV*mA?w`?DW@(#Ssx6r3ydG^_lIM9)-9ycZ_%c^qN z7B)6{pBYnEQ*ZBAiHWp#S*I9!0^vDbE0c8WSl`^VX{%j6-p2YaqB)1+abDh)wmZiK z*{DE-KU!o@$h@+@L{XBKfs2~@r%da`%**4^I8S_*nmnpyu{&&0M^IzPAY?NI<6O`u zCYI~i`N%48@en&Qjyk0?S)c&RFe%y2@x~t;`jpG}2o1M#yd_ii*)TmehovBAQBir* zGq%ZHXf7)8w8)5vXf^;10>{(XG6Q}-2Y@m%0Aj~!Zc=e+Q~^it`OhKp$a8Fcu1OfS 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 at-No~b2n5(Bl!J*L0`v6yAWxA`9A>MFSdUG literal 0 HcmV?d00001