From 3f74c682b1ae0ce9c8f25eaf78cdc363a7d08933 Mon Sep 17 00:00:00 2001 From: Derek Jamison Date: Fri, 19 Apr 2024 18:00:58 -0500 Subject: [PATCH] JS: Flipboard --- js/flipboard/fal/mntm-001/js_dialog.fal | Bin 0 -> 4864 bytes js/flipboard/fal/mntm-001/js_gpio.fal | Bin 0 -> 5832 bytes js/flipboard/fal/mntm-001/js_infrared.fal | Bin 0 -> 2488 bytes js/flipboard/fal/mntm-001/js_rgbleds.fal | Bin 0 -> 10124 bytes js/flipboard/fal/mntm-001/js_speaker.fal | Bin 0 -> 5664 bytes js/flipboard/fal/mntm-001/js_textbox.fal | Bin 0 -> 7364 bytes js/flipboard/fal/mntm-001/js_widget.fal | Bin 0 -> 19688 bytes .../js_infrared.fal | Bin 0 -> 2488 bytes .../js_rgbleds.fal | Bin 0 -> 10124 bytes .../js_speaker.fal | Bin 0 -> 5664 bytes js/flipboard/modules/js_infrared.c | 61 ++++ js/flipboard/modules/js_rgbleds/js_rgbleds.c | 213 +++++++++++++ js/flipboard/modules/js_rgbleds/led_driver.c | 299 ++++++++++++++++++ js/flipboard/modules/js_rgbleds/led_driver.h | 42 +++ .../modules/js_rgbleds/led_driver_i.h | 26 ++ js/flipboard/modules/js_rgbleds/rgbleds.c | 131 ++++++++ js/flipboard/modules/js_rgbleds/rgbleds.h | 63 ++++ js/flipboard/modules/js_speaker.c | 210 ++++++++++++ js/flipboard/png/down.png | Bin 0 -> 153 bytes js/flipboard/png/flipboard.png | Bin 0 -> 45675 bytes js/flipboard/png/flippy.png | Bin 0 -> 1902 bytes js/flipboard/png/up.png | Bin 0 -> 158 bytes js/flipboard/readme.md | 79 +++++ js/flipboard/scripts/a_runner.js | 44 +++ js/flipboard/scripts/badusb_textbox_fb.js | 56 ++++ js/flipboard/scripts/badusb_widget_fb.js | 96 ++++++ js/flipboard/scripts/color_api.js | 15 + js/flipboard/scripts/down.fxbm | Bin 0 -> 30 bytes js/flipboard/scripts/fb_button_api.js | 55 ++++ js/flipboard/scripts/fb_leds_api.js | 28 ++ js/flipboard/scripts/flippy.fxbm | Bin 0 -> 524 bytes js/flipboard/scripts/infrared_fb.js | 66 ++++ js/flipboard/scripts/loader_api.js | 67 ++++ js/flipboard/scripts/subghz_fb.js | 41 +++ js/flipboard/scripts/up.fxbm | Bin 0 -> 30 bytes 35 files changed, 1592 insertions(+) create mode 100644 js/flipboard/fal/mntm-001/js_dialog.fal create mode 100644 js/flipboard/fal/mntm-001/js_gpio.fal create mode 100644 js/flipboard/fal/mntm-001/js_infrared.fal create mode 100644 js/flipboard/fal/mntm-001/js_rgbleds.fal create mode 100644 js/flipboard/fal/mntm-001/js_speaker.fal create mode 100644 js/flipboard/fal/mntm-001/js_textbox.fal create mode 100644 js/flipboard/fal/mntm-001/js_widget.fal create mode 100644 js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_infrared.fal create mode 100644 js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_rgbleds.fal create mode 100644 js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_speaker.fal create mode 100644 js/flipboard/modules/js_infrared.c create mode 100644 js/flipboard/modules/js_rgbleds/js_rgbleds.c create mode 100644 js/flipboard/modules/js_rgbleds/led_driver.c create mode 100644 js/flipboard/modules/js_rgbleds/led_driver.h create mode 100644 js/flipboard/modules/js_rgbleds/led_driver_i.h create mode 100644 js/flipboard/modules/js_rgbleds/rgbleds.c create mode 100644 js/flipboard/modules/js_rgbleds/rgbleds.h create mode 100644 js/flipboard/modules/js_speaker.c create mode 100644 js/flipboard/png/down.png create mode 100644 js/flipboard/png/flipboard.png create mode 100644 js/flipboard/png/flippy.png create mode 100644 js/flipboard/png/up.png create mode 100644 js/flipboard/readme.md create mode 100644 js/flipboard/scripts/a_runner.js create mode 100644 js/flipboard/scripts/badusb_textbox_fb.js create mode 100644 js/flipboard/scripts/badusb_widget_fb.js create mode 100644 js/flipboard/scripts/color_api.js create mode 100644 js/flipboard/scripts/down.fxbm create mode 100644 js/flipboard/scripts/fb_button_api.js create mode 100644 js/flipboard/scripts/fb_leds_api.js create mode 100644 js/flipboard/scripts/flippy.fxbm create mode 100644 js/flipboard/scripts/infrared_fb.js create mode 100644 js/flipboard/scripts/loader_api.js create mode 100644 js/flipboard/scripts/subghz_fb.js create mode 100644 js/flipboard/scripts/up.fxbm diff --git a/js/flipboard/fal/mntm-001/js_dialog.fal b/js/flipboard/fal/mntm-001/js_dialog.fal new file mode 100644 index 0000000000000000000000000000000000000000..bd67ce9257e21f261f47eb728a594259cbcb4d9f GIT binary patch literal 4864 zcmbtY4Qv$06@L43_8A*P2r*6|WQ`3u0=VWUm19sa6lQHN;G}5^1lr5lUjKl7XYS4o zk)qZhq7WsmK(1P=w2>>J%5|t>i?qT~k*OLfL||%K)B=K<3RBxsDwz^h*A|I>Z+7>5 ztgN(BN1A){y?OKI&CHwqM)#eq+k~PhaqcI+lGO$jH5am%Wt(tsbF0w{b z@AjZ}>SB)QHnYTRVTi)&5wmvc!E*~N>KdL@l{I387*6TO3~|mh#60xQo3SS!J^5}A z+w(XcqZpq|lCAfjotS!yhMb6|e}isw&a?`#O_`O}rtRPB*AbN(${X2Ov(X9`89>il zw8Rk0MLzp8gXUjUC)=p8*KB4~H zz`E$z8Iw+A32h)a_)N{6aV5i#4nM)cWi{8wl|Efs>#$Z%WJ~L!noHwFR#8>AIe$sD zFsx3I)7x!6WtAa<>~D3Oi)*ulDio3Zex*2L_6#hv7S@(w1xm3Zu)k{zXgOX(^qOVj zF{|6muj1eL#{-D>-uU!h6{h#*-h+_a{tDzPs2whT*OmWGU~g*=SjVxm2N3xIMBQz+ zTLDCY$U>DT`o4y9;%SL=BDi6Kw-o`AEH?pB-gB#U)dABx+9J#unS-zdK zr`P0DRb=bg?ZcB<%1z*2nR9?T6J-yidJzEQh4hyX>3mFj{ zx(-i{?vPk&Ev*U;UBix-ATztoJz}>gt{F}B_6LwzAKj%n!4bV!`2lq;-og3yB2KB_ z9E3gxdR1oB<2yK?`270K<)XkUho1r~3z%=;eC+q>#k)7}-h}TQ+I)(~+x7tLSL66NyNZP4yj#WUP(aTk7|VmR6f~#$(YYBNA`wXtSfq1Z|EaY@H;MX|Usz zgbCGkB$KgdxYceFKG+&Sk%b`l*W zI)4l!DfF>I=M@^kuSPdCx~0*WMiUx+rjf2wS%9_$=xYIbBj7mxC%p3nN8E{i zf;MBMh5E`bvtQsguB3YXn|D6tb~oB@|8dV%4&o%{(P(9W)JxRyH`eqjl+vgjn*Ap) z5NrAs3hJ~Dnz0?n$651hh34zD1)5oBs{^dLh^RHHf~M@u;n_UP6@}FIUhXU7_Eoh1 z^oKJSxP1fdcUB#`&Fx!g-|xA0TSa?Jp%R_czppDmi)aE}JJ$T{7wr5qD0FIu^NC0A ze9D?Bg|29{0UA2;j|6KzS7=J3N@#9B{mx!?cu}Jl*x>Ze7Iui`s)K?28{GA|Mz5ml zU$e6%ysuFM;H4GL{>()fr*4mZ$d-jV?Lybly?;H+aFI@X0e@ex-siOBWNMx5=6e>iEvy4xi z`=0^kjMoEma_n{c*+2VoisKInk;dFiWni8Ur~FWVh=Y>F8IM#-V;*`i1M_$! zZMvT^w#O}+T+f&Xv3~CFOxruS+lS|8c>|pODj&8oaII$wIT!UCK5YAN%!d#8u=m|% zHT(OKum1xd{;dyR_Tf7|%%sR~=pf`goFCp4>B94s_;9%on?79Y!}4CZ9@$^Jum9UV z{9PaJ@!I6rw5g;aL+7zrhLyu-(+Xy`a3fP& z8&5$jyR#it+PHi=tyg8tUk<>apY}w8yj$VSYt-8ytWM39=F@=XhYbJ$7Auv zhc+yR4(R|x{K3k}9-3b6jT<_jO}H)|do5wd!!myVnh=CT`U&TIr!K&E1o^7sZgjFl z9vxwcyACRqnD3?vqCe(hev_((BtKG@N#Y~*aY=WiE|lae$MwUPx`LRAB?i@ziVx{I z7k}(Z5VY(E@sYZcn5`uS?U5Rilt=2*lJ3BXlIlo(Qqml$Pf30w^-B)7sZU5oBNZQT z@EEDfo$oCB#S_4phh-_|VTrgwf6|^=TwR2DSYqrgSK`GWF`f-l;_VkGZ9~?+3zgu)j#Dj;1vm#M~j9TyWd)88qOf#K?SOFVzrPU#;) zmXiqdWe-5|++0dfy*A_fu;Z7mSG;-GLXl}V7x(qw849nt*=LuFU~xs$#j|6{YxPPw s>~Buk6YrdNgC8=a2uk%Cx(P!7q7(AU(VP0IgoKt^fc4 literal 0 HcmV?d00001 diff --git a/js/flipboard/fal/mntm-001/js_gpio.fal b/js/flipboard/fal/mntm-001/js_gpio.fal new file mode 100644 index 0000000000000000000000000000000000000000..cb93c22359d8132d8f92303b0a9d7f4229210945 GIT binary patch literal 5832 zcmbVQYiu0V6+Ux!;zvy0PGZMKog}81#CDy;%>!t09?UwswPS(WB3iMGy<=yS_3mnS zmIR7gLsAjcs5nYQ-4>+$0AyN_oQhO&n>473N`QoJL?jF%5Q&7fDXGk(U?3{}&Ye5! z^^{gsuXOjE@0@ebea^j~{gYe2V``d4UeTzY7`>>wma$4HXS7IBPuEZdHI9E7tPQ_C zx&JU~zi!Sk2ZJT%EYmQnw&sG{CcfOTI7|o6jtgzIxzap4sSgo2vmE0Vqy__TC_iFstVcaWGZ$u5D-i4Y$ zy$^LC>Z7PbsJ}x!jyid?yrX>E4AWip8_Z+Ds*#fL?bw0I6I#oxhL6sX@fmWDsx{`d zUU#%QT-{Dfb)20FQg7uXJRl|HZm`C zB|i_btc~RZ+aKIl6I-&W>ZG=GcDU>$$+p6*FfXe4V2HB3K$Bl353$Gn?7onT^L4+?KKvThdl6K~~%`vx#m8Z3{6)wxYm<6;Jje zsbiRPf(&)7vBNa;+39HYht%-ZAcq}?)_=*f&$z>qFOY5e?{ zm7=mo;fIv`nUaqvc~Hq;DEVt84=eekl20r7tdd8RJPO&iC3u?)Z=_Q6tz1Q;`2W-$ znFudTB-&)hdyryu;?=EM6%=vxuQ$HVz3e7>kmZ4Z+w1+nP-Xc#e*5KRJwSAx=pr5? zjq32YYjjbgOB#)7^u9(D8hxzMq()bfNsun6Owcug)(dJCbWG4`K{-L&1GGCp{Q=q^ zp!;>&uhW1|hje;OrzdoJMyKa=I-|S8d0n`}*{gt8%H;^7QKIK{5;IS1ox$);qG7

wkF@N8-8`9jl*umR)0bdEmP1(Sx--=UR>4 z)aYhGVr;?X7uoWRpdSXvLCh#%RwH0&6rfxVh5s7jC_ zs9sRB+&6h9I5XEXn%n#13gFr$X}Q^8+8=;lo)U-s8gqK8)+CP&ogSsQkB58RUHI|8T+U z_Wws8KI_A{@(P9XT`nNE{ii;xW3BlXYJr{mt1hg)8_!(GE1XZ>oXWts&FA-c5{Ixq z;`7IS_&y(g0GRK#OK8jE@!nD|-&4ML`KhxLh5P%9&;P0q<2EW3j{m@iMd9qb`4$56 z-P(%E<5}tRQ=LQJ+u9Ab2hLX)Em$NI^xQ5Z^pIqR9*EkYRmpoLcZt}$ok*&C74^Xz z%S1f7B%klht|-_Id-`@pl3hFOZl|b`wN|>Ubgz_PX0ml*ICjbn&g|-r@1&k=Iu=RW z-N|$`(j8BNFmm3s?PSvlH!jwdbsW&v0)dT0V;QR}ZWs00y{3?!`;(&t9dskI87u8X z(!0G8_axJHtTz$q0jVv@(Y3}4@FvS6$=Jnn>u#1YSP;cPA-elg|K2&%LmA4$QL&sZ zmA1jFqY)6~WV)xQcWYe{=vqB(U6N>0H3HvW9%-;;hfh$OfS6oUE2PyHAODS0d z^s5Tvfs=N;=u(1TNQqZnN`99(FL;*huT^1wkHig1ll1q`e z8C+f2&#+ug%rIqMa3~UM-S><0-HmsP^MJO=yr7{n4iru112L2Nz)htcyo4Zjs%FxsPfD6Q13er8-3^Do5Vy?=9A~)7W^MaVR;^y7^!`c=b-^_oWk-v;7<(Z zEj%7DJrcJPGf|1b_DHP3c~uyUk3=0YN0t5tmg|WLtHjLw#5Y{t!}+Kwb8nwxbwN65 zS8n&T9xK&nI~FzdCbE&J-IeW)#}YfKsmIDVO=&yctT0?MkB)nTzaOu7`GPPM=X(sqXHwa) z)rEXD&^fO*8`BA56fN9**%E3N>)_y`o%U)i+cuf?|4aye4C1NPVoo^`%hL> y@tUwKnjQ_z#CMF&Ac)7r{qj4`bLDw?WnwnbhN6aEJ}m3BxI>u4?Or2DFYaIaLJ`0K literal 0 HcmV?d00001 diff --git a/js/flipboard/fal/mntm-001/js_infrared.fal b/js/flipboard/fal/mntm-001/js_infrared.fal new file mode 100644 index 0000000000000000000000000000000000000000..41ca505ef046d184cacd5471adff49f1bd9c8a1a GIT binary patch literal 2488 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 TcL(^~agWa>LTHV*?705{WK5+A literal 0 HcmV?d00001 diff --git a/js/flipboard/fal/mntm-001/js_speaker.fal b/js/flipboard/fal/mntm-001/js_speaker.fal new file mode 100644 index 0000000000000000000000000000000000000000..71c697d957672d85c4d8cb0b0a24acf9cbab963d GIT binary patch literal 5664 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 literal 0 HcmV?d00001 diff --git a/js/flipboard/fal/mntm-001/js_textbox.fal b/js/flipboard/fal/mntm-001/js_textbox.fal new file mode 100644 index 0000000000000000000000000000000000000000..18da69fb2ee88af0a413816cf6e98c9b8ba42057 GIT binary patch literal 7364 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 literal 0 HcmV?d00001 diff --git a/js/flipboard/fal/mntm-001/js_widget.fal b/js/flipboard/fal/mntm-001/js_widget.fal new file mode 100644 index 0000000000000000000000000000000000000000..43744a6ed56a5939005e93aec67202c52c4eb596 GIT binary patch literal 19688 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 YbraF#3G_~ZfL`th#synr;*0D3Z?{6Ong9R* literal 0 HcmV?d00001 diff --git a/js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_infrared.fal b/js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_infrared.fal new file mode 100644 index 0000000000000000000000000000000000000000..385c700490365a6625f129592fa156dc3abdc0fa GIT binary patch literal 2488 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 literal 0 HcmV?d00001 diff --git a/js/flipboard/modules/js_infrared.c b/js/flipboard/modules/js_infrared.c new file mode 100644 index 0000000..3461440 --- /dev/null +++ b/js/flipboard/modules/js_infrared.c @@ -0,0 +1,61 @@ +#include "../js_modules.h" +#include +#include + +static void js_infrared_send_protocol(struct mjs* mjs) { + size_t num_args = mjs_nargs(mjs); + if(num_args != 3 || !mjs_is_string(mjs_arg(mjs, 0)) || !mjs_is_number(mjs_arg(mjs, 1)) || + !mjs_is_number(mjs_arg(mjs, 2))) { + mjs_prepend_errorf( + mjs, MJS_BAD_ARGS_ERROR, "Invalid args (protocolName, address, command)"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + mjs_val_t protocol_arg = mjs_arg(mjs, 0); + const char* protocol_name = mjs_get_string(mjs, &protocol_arg, NULL); + uint32_t address = mjs_get_int(mjs, mjs_arg(mjs, 1)); + uint32_t command = mjs_get_int(mjs, mjs_arg(mjs, 2)); + + InfraredMessage message; + message.protocol = infrared_get_protocol_by_name(protocol_name); + if(message.protocol == InfraredProtocolUnknown) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid protocol (%s)", protocol_name); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + int transmit_count = 1; + message.address = address; + message.command = command; + message.repeat = transmit_count != 1; + infrared_send(&message, transmit_count); + + mjs_return(mjs, mjs_mk_boolean(mjs, true)); +} + +static void* js_infrared_create(struct mjs* mjs, mjs_val_t* object) { + mjs_val_t infrared_obj = mjs_mk_object(mjs); + mjs_set(mjs, infrared_obj, "sendProtocol", ~0, MJS_MK_FN(js_infrared_send_protocol)); + *object = infrared_obj; + return (void*)1; +} + +static void js_infrared_destroy(void* inst) { + UNUSED(inst); +} + +static const JsModuleDescriptor js_infrared_desc = { + "infrared", + js_infrared_create, + js_infrared_destroy, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_infrared_desc, +}; + +const FlipperAppPluginDescriptor* js_infrared_ep(void) { + return &plugin_descriptor; +} \ No newline at end of file diff --git a/js/flipboard/modules/js_rgbleds/js_rgbleds.c b/js/flipboard/modules/js_rgbleds/js_rgbleds.c new file mode 100644 index 0000000..7853947 --- /dev/null +++ b/js/flipboard/modules/js_rgbleds/js_rgbleds.c @@ -0,0 +1,213 @@ +#include +#include "../../js_modules.h" +#include + +#include "rgbleds.h" + +typedef struct { + RgbLeds* leds; +} JsRgbledsInst; + +typedef struct { + const GpioPin* pin; + const char* name; +} GpioPinCtx; + +static const GpioPinCtx js_gpio_pins[] = { + {.pin = &gpio_ext_pa7, .name = "PA7"}, // 2 + {.pin = &gpio_ext_pa6, .name = "PA6"}, // 3 + {.pin = &gpio_ext_pa4, .name = "PA4"}, // 4 + {.pin = &gpio_ext_pb3, .name = "PB3"}, // 5 + {.pin = &gpio_ext_pb2, .name = "PB2"}, // 6 + {.pin = &gpio_ext_pc3, .name = "PC3"}, // 7 + {.pin = &gpio_swclk, .name = "PA14"}, // 10 + {.pin = &gpio_swdio, .name = "PA13"}, // 12 + {.pin = &gpio_usart_tx, .name = "PB6"}, // 13 + {.pin = &gpio_usart_rx, .name = "PB7"}, // 14 + {.pin = &gpio_ext_pc1, .name = "PC1"}, // 15 + {.pin = &gpio_ext_pc0, .name = "PC0"}, // 16 + {.pin = &gpio_ibutton, .name = "PB14"}, // 17 +}; + +static const GpioPin* get_gpio_pin(const char* name) { + for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) { + if(strcmp(js_gpio_pins[i].name, name) == 0) { + return js_gpio_pins[i].pin; + } + } + return NULL; +} + +static void js_rgbleds_setup(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsRgbledsInst* rgbleds = mjs_get_ptr(mjs, obj_inst); + furi_assert(rgbleds); + + if(mjs_nargs(mjs) != 1 || !mjs_is_object(mjs_arg(mjs, 0))) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + mjs_val_t pin_obj = mjs_get(mjs, mjs_arg(mjs, 0), "pin", ~0); + mjs_val_t count_obj = mjs_get(mjs, mjs_arg(mjs, 0), "count", ~0); + mjs_val_t spec_obj = mjs_get(mjs, mjs_arg(mjs, 0), "spec", ~0); + + if(!mjs_is_string(pin_obj)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "pin must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(!mjs_is_number(count_obj)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "count must be a number"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(!mjs_is_string(spec_obj)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "spec must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + const GpioPin* pin = get_gpio_pin(mjs_get_string(mjs, &pin_obj, NULL)); + if(!pin) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "invalid pin"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(rgbleds->leds) { + rgbleds_free(rgbleds->leds); + } + + uint16_t count = mjs_get_int(mjs, count_obj); + rgbleds->leds = rgbleds_alloc(count, pin); +} + +static void js_rgbleds_set(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsRgbledsInst* rgbleds = mjs_get_ptr(mjs, obj_inst); + furi_assert(rgbleds); + + if(!rgbleds->leds) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "LEDs not setup"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + uint32_t color; + if(mjs_nargs(mjs) == 2) { + if(mjs_is_number(mjs_arg(mjs, 0)) && mjs_is_number(mjs_arg(mjs, 1))) { + color = mjs_get_int(mjs, mjs_arg(mjs, 1)); + } else if(mjs_is_number(mjs_arg(mjs, 0)) && mjs_is_object(mjs_arg(mjs, 1))) { + mjs_val_t red_obj = mjs_get(mjs, mjs_arg(mjs, 1), "red", ~0); + mjs_val_t green_obj = mjs_get(mjs, mjs_arg(mjs, 1), "green", ~0); + mjs_val_t blue_obj = mjs_get(mjs, mjs_arg(mjs, 1), "blue", ~0); + if(!mjs_is_number(red_obj) || !mjs_is_number(green_obj) || !mjs_is_number(blue_obj)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + color = (mjs_get_int(mjs, red_obj) << 16) | (mjs_get_int(mjs, green_obj) << 8) | + mjs_get_int(mjs, blue_obj); + } else { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + } else if(mjs_nargs(mjs) == 4) { + if(!mjs_is_number(mjs_arg(mjs, 0)) || !mjs_is_number(mjs_arg(mjs, 1)) || + !mjs_is_number(mjs_arg(mjs, 2)) || !mjs_is_number(mjs_arg(mjs, 3))) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + color = (mjs_get_int(mjs, mjs_arg(mjs, 1)) << 16) | + (mjs_get_int(mjs, mjs_arg(mjs, 2)) << 8) | mjs_get_int(mjs, mjs_arg(mjs, 3)); + } else { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + uint16_t index = mjs_get_int(mjs, mjs_arg(mjs, 0)); + + uint16_t original_color = rgbleds_get(rgbleds->leds, index); + rgbleds_set(rgbleds->leds, index, color); + mjs_return(mjs, mjs_mk_number(mjs, original_color)); +} + +static void js_rgbleds_get(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsRgbledsInst* rgbleds = mjs_get_ptr(mjs, obj_inst); + furi_assert(rgbleds); + + if(!rgbleds->leds) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "LEDs not setup"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(mjs_nargs(mjs) != 1 || !mjs_is_number(mjs_arg(mjs, 0))) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + uint16_t index = mjs_get_int(mjs, mjs_arg(mjs, 0)); + mjs_return(mjs, mjs_mk_number(mjs, rgbleds_get(rgbleds->leds, index))); +} + +static void js_rgbleds_update(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsRgbledsInst* rgbleds = mjs_get_ptr(mjs, obj_inst); + furi_assert(rgbleds); + + if(!rgbleds->leds) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "LEDs not setup"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + rgbleds_update(rgbleds->leds); +} + +static void* js_rgbleds_create(struct mjs* mjs, mjs_val_t* object) { + JsRgbledsInst* rgbleds = malloc(sizeof(JsRgbledsInst)); + rgbleds->leds = NULL; + mjs_val_t rgbleds_obj = mjs_mk_object(mjs); + mjs_set(mjs, rgbleds_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, rgbleds)); + mjs_set(mjs, rgbleds_obj, "setup", ~0, MJS_MK_FN(js_rgbleds_setup)); + mjs_set(mjs, rgbleds_obj, "set", ~0, MJS_MK_FN(js_rgbleds_set)); + mjs_set(mjs, rgbleds_obj, "get", ~0, MJS_MK_FN(js_rgbleds_get)); + mjs_set(mjs, rgbleds_obj, "update", ~0, MJS_MK_FN(js_rgbleds_update)); + *object = rgbleds_obj; + return rgbleds; +} + +static void js_rgbleds_destroy(void* inst) { + JsRgbledsInst* rgbleds = inst; + if(rgbleds->leds) { + rgbleds_free(rgbleds->leds); + } + free(rgbleds); +} + +static const JsModuleDescriptor js_rgbleds_desc = { + "rgbleds", + js_rgbleds_create, + js_rgbleds_destroy, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_rgbleds_desc, +}; + +const FlipperAppPluginDescriptor* js_rgbleds_ep(void) { + return &plugin_descriptor; +} diff --git a/js/flipboard/modules/js_rgbleds/led_driver.c b/js/flipboard/modules/js_rgbleds/led_driver.c new file mode 100644 index 0000000..18a8717 --- /dev/null +++ b/js/flipboard/modules/js_rgbleds/led_driver.c @@ -0,0 +1,299 @@ +#include "led_driver_i.h" + +#define TAG "led_driver" + +struct LedDriver { + LL_DMA_InitTypeDef dma_gpio_update; + LL_DMA_InitTypeDef dma_led_transition_timer; + + const GpioPin* gpio; + uint32_t gpio_buf[2]; // On/Off for GPIO + + uint16_t timer_buffer[LED_DRIVER_BUFFER_SIZE + 2]; + uint32_t write_pos; + uint32_t read_pos; + + uint32_t count_leds; + uint32_t* led_data; +}; + +/** + * @brief Initializes the DMA for GPIO pin toggle via BSRR. + * @param led_driver The led driver to initialize. + * @param gpio The GPIO pin to toggle. + */ +static void led_driver_init_dma_gpio_update(LedDriver* led_driver, const GpioPin* gpio) { + led_driver->gpio = gpio; + + // Memory to Peripheral + led_driver->dma_gpio_update.Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH; + // Peripheral (GPIO - We populate GPIO port's BSRR register) + led_driver->dma_gpio_update.PeriphOrM2MSrcAddress = (uint32_t)&gpio->port->BSRR; + led_driver->dma_gpio_update.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT; + led_driver->dma_gpio_update.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD; + // Memory (State to set GPIO) + led_driver->dma_gpio_update.MemoryOrM2MDstAddress = (uint32_t)led_driver->gpio_buf; + led_driver->dma_gpio_update.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT; + led_driver->dma_gpio_update.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_WORD; + // Data + led_driver->dma_gpio_update.Mode = LL_DMA_MODE_CIRCULAR; + led_driver->dma_gpio_update.NbData = 2; // We cycle between two (HIGH/LOW)values + // When to perform data exchange + led_driver->dma_gpio_update.PeriphRequest = LL_DMAMUX_REQ_TIM2_UP; + led_driver->dma_gpio_update.Priority = LL_DMA_PRIORITY_VERYHIGH; +} + +/** + * @brief Initializes the DMA for the LED timings via ARR. + * @param led_driver The led driver to initialize. + */ +static void led_driver_init_dma_led_transition_timer(LedDriver* led_driver) { + // Timer that triggers based on user data. + led_driver->dma_led_transition_timer.Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH; + // Peripheral (Timer - We populate TIM2's ARR register) + led_driver->dma_led_transition_timer.PeriphOrM2MSrcAddress = (uint32_t)&TIM2->ARR; + led_driver->dma_led_transition_timer.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT; + led_driver->dma_led_transition_timer.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD; + // Memory (Timings) + led_driver->dma_led_transition_timer.MemoryOrM2MDstAddress = + (uint32_t)led_driver->timer_buffer; + led_driver->dma_led_transition_timer.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT; + led_driver->dma_led_transition_timer.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_HALFWORD; + // Data + led_driver->dma_led_transition_timer.Mode = LL_DMA_MODE_NORMAL; + led_driver->dma_led_transition_timer.NbData = LED_DRIVER_BUFFER_SIZE; + // When to perform data exchange + led_driver->dma_led_transition_timer.PeriphRequest = LL_DMAMUX_REQ_TIM2_UP; + led_driver->dma_led_transition_timer.Priority = LL_DMA_PRIORITY_HIGH; +} + +/** + * @brief Allocate and initialize LedDriver structure. + * @details This function allocate and initialize LedDriver structure. + * @return Pointer to allocated LedDriver structure. + */ +LedDriver* led_driver_alloc(int count_leds, const GpioPin* gpio) { + furi_assert(gpio); + furi_assert(count_leds && count_leds <= MAX_LED_COUNT); + + LedDriver* led_driver = malloc(sizeof(LedDriver)); + led_driver_init_dma_gpio_update(led_driver, gpio); + led_driver_init_dma_led_transition_timer(led_driver); + led_driver->led_data = malloc(MAX_LED_COUNT * sizeof(uint32_t)); + + led_driver->count_leds = count_leds; + + return led_driver; +} + +/** + * @brief Frees a led driver. + * @details Frees a led driver. + * @param led_driver The led driver to free. + */ +void led_driver_free(LedDriver* led_driver) { + furi_assert(led_driver); + + furi_hal_gpio_init_simple(led_driver->gpio, GpioModeAnalog); + free(led_driver->led_data); + free(led_driver); +} + +/** + * @brief Sets the LED at the given index to the given color. + * @note You must still call led_driver_transmit to actually update the LEDs. + * @param led_driver The led driver to use. + * @param index The index of the LED to set. + * @param rrggbb The color to set the LED to (0xrrggbb format). + * @return The previous color of the LED (0xrrggbb format). + */ +uint32_t led_driver_set_led(LedDriver* led_driver, uint32_t index, uint32_t rrggbb) { + furi_assert(led_driver); + if(index >= led_driver->count_leds) { + return 0xFFFFFFFF; + } + + uint32_t previous = led_driver->led_data[index]; + led_driver->led_data[index] = rrggbb; + return previous; +} + +/** + * @brief Gets the LED at the given index. + * @param led_driver The led driver to use. + * @param index The index of the LED to get. + * @return The color of the LED (0xrrggbb format). + */ +uint32_t led_driver_get_led(LedDriver* led_driver, uint32_t index) { + furi_assert(led_driver); + if(index >= led_driver->count_leds) { + return 0xFFFFFFFF; + } + + return led_driver->led_data[index]; +} + +/** + * @brief Initializes the DMA for GPIO pin toggle and led transititions. + * @param led_driver The led driver to initialize. + */ +static void led_driver_start_dma(LedDriver* led_driver) { + furi_assert(led_driver); + + LL_DMA_Init(DMA1, LL_DMA_CHANNEL_1, &led_driver->dma_gpio_update); + LL_DMA_Init(DMA1, LL_DMA_CHANNEL_2, &led_driver->dma_led_transition_timer); + + LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1); + LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_2); +} + +/** + * @brief Stops the DMA for GPIO pin toggle and led transititions. + * @param led_driver The led driver to initialize. + */ +static void led_driver_stop_dma() { + LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1); + LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_2); + LL_DMA_ClearFlag_TC1(DMA1); + LL_DMA_ClearFlag_TC2(DMA1); +} + +/** + * @brief Starts the timer for led transitions. + * @param led_driver The led driver to initialize. + */ +static void led_driver_start_timer() { + furi_hal_bus_enable(FuriHalBusTIM2); + + LL_TIM_SetCounterMode(TIM2, LL_TIM_COUNTERMODE_UP); + LL_TIM_SetClockDivision(TIM2, LL_TIM_CLOCKDIVISION_DIV1); + LL_TIM_SetPrescaler(TIM2, 0); + // Updated by led_driver->dma_led_transition_timer.PeriphOrM2MSrcAddress + LL_TIM_SetAutoReload(TIM2, LED_DRIVER_TIMER_SETINEL); + LL_TIM_SetCounter(TIM2, 0); + + LL_TIM_EnableCounter(TIM2); + LL_TIM_EnableUpdateEvent(TIM2); + LL_TIM_EnableDMAReq_UPDATE(TIM2); + LL_TIM_GenerateEvent_UPDATE(TIM2); +} + +/** + * @brief Stops the timer for led transitions. + * @param led_driver The led driver to initialize. + */ +static void led_driver_stop_timer() { + LL_TIM_DisableCounter(TIM2); + LL_TIM_DisableUpdateEvent(TIM2); + LL_TIM_DisableDMAReq_UPDATE(TIM2); + furi_hal_bus_disable(FuriHalBusTIM2); +} + +/** + * @brief Waits for the DMA to complete. + * @param led_driver The led driver to use. + */ +static void led_driver_spin_lock(LedDriver* led_driver) { + const uint32_t prev_timer = DWT->CYCCNT; + const uint32_t wait_time = LED_DRIVER_SETINEL_WAIT_MS * SystemCoreClock / 1000; + + do { + /* Make sure it's started (allow 100 ticks), but then check for sentinel value. */ + if(TIM2->ARR == LED_DRIVER_TIMER_SETINEL && DWT->CYCCNT - prev_timer > 100) { + break; + } + + // We should have seen it above, but just in case we also have a timeout. + if((DWT->CYCCNT - prev_timer > wait_time)) { + FURI_LOG_E( + TAG, + "0x%04x not found (ARR 0x%08lx, read %lu)", + LED_DRIVER_TIMER_SETINEL, + TIM2->ARR, + led_driver->read_pos); + led_driver->read_pos = led_driver->write_pos - 1; + break; + } + } while(true); +} + +static void led_driver_add_period_length(LedDriver* led_driver, uint32_t length) { + led_driver->timer_buffer[led_driver->write_pos++] = length; + led_driver->timer_buffer[led_driver->write_pos] = LED_DRIVER_TIMER_SETINEL; +} + +static void led_driver_add_period(LedDriver* led_driver, uint16_t duration_ns) { + furi_assert(led_driver); + + uint32_t reload_value = duration_ns / LED_DRIVER_TIMER_NANOSECOND; + + if(reload_value > 255) { + FURI_LOG_E(TAG, "reload_value: %ld", reload_value); + } + furi_check(reload_value > 0); + furi_check(reload_value < 256 * 256); + + led_driver_add_period_length(led_driver, reload_value - 1); +} + +static void led_driver_add_color(LedDriver* led_driver, uint32_t rrggbb) { + UNUSED(rrggbb); + + uint32_t ggrrbb = (rrggbb & 0xFF) | ((rrggbb & 0xFF00) << 8) | ((rrggbb & 0xFF0000) >> 8); + + for(int i = 23; i >= 0; i--) { + if(ggrrbb & (1 << i)) { + led_driver_add_period(led_driver, LED_DRIVER_T0L); + led_driver_add_period(led_driver, LED_DRIVER_T1L); + } else { + led_driver_add_period(led_driver, LED_DRIVER_T0H); + led_driver_add_period(led_driver, LED_DRIVER_T1H); + } + } +} + +/** + * @brief Send the LED data to the LEDs. + * @param led_driver The led driver to use. + */ +void led_driver_transmit(LedDriver* led_driver) { + furi_assert(led_driver); + + furi_assert(!led_driver->read_pos); + furi_assert(!led_driver->write_pos); + + furi_hal_gpio_init(led_driver->gpio, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh); + furi_hal_gpio_write(led_driver->gpio, false); + + const uint32_t bit_set = led_driver->gpio->pin << GPIO_BSRR_BS0_Pos; + const uint32_t bit_reset = led_driver->gpio->pin << GPIO_BSRR_BR0_Pos; + + // Always start with HIGH + led_driver->gpio_buf[0] = bit_set; + led_driver->gpio_buf[1] = bit_reset; + + for(size_t i = 0; i < LED_DRIVER_BUFFER_SIZE; i++) { + led_driver->timer_buffer[i] = LED_DRIVER_TIMER_SETINEL; + } + for(size_t i = 0; i < led_driver->count_leds; i++) { + led_driver_add_color(led_driver, led_driver->led_data[i]); + } + led_driver_add_period(led_driver, LED_DRIVER_TDONE); + led_driver->dma_led_transition_timer.NbData = led_driver->write_pos + 1; + + FURI_CRITICAL_ENTER(); + + led_driver_start_dma(led_driver); + led_driver_start_timer(); + + led_driver_spin_lock(led_driver); + + led_driver_stop_timer(); + led_driver_stop_dma(); + + FURI_CRITICAL_EXIT(); + + memset(led_driver->timer_buffer, LED_DRIVER_TIMER_SETINEL, LED_DRIVER_BUFFER_SIZE); + led_driver->read_pos = 0; + led_driver->write_pos = 0; +} diff --git a/js/flipboard/modules/js_rgbleds/led_driver.h b/js/flipboard/modules/js_rgbleds/led_driver.h new file mode 100644 index 0000000..4005216 --- /dev/null +++ b/js/flipboard/modules/js_rgbleds/led_driver.h @@ -0,0 +1,42 @@ +#include +#include + +typedef struct LedDriver LedDriver; + +/** + * @brief Allocate and initialize LedDriver structure. + * @details This function allocate and initialize LedDriver structure. + * @return Pointer to allocated LedDriver structure. + */ +LedDriver* led_driver_alloc(int count_leds, const GpioPin* gpio); + +/** + * @brief Frees a led driver. + * @details Frees a led driver. + * @param led_driver The led driver to free. + */ +void led_driver_free(LedDriver* led_driver); + +/** + * @brief Sets the LED at the given index to the given color. + * @note You must still call led_driver_transmit to actually update the LEDs. + * @param led_driver The led driver to use. + * @param index The index of the LED to set. + * @param rrggbb The color to set the LED to. + * @return The previous color of the LED. + */ +uint32_t led_driver_set_led(LedDriver* led_driver, uint32_t index, uint32_t rrggbb); + +/** + * @brief Gets the LED at the given index. + * @param led_driver The led driver to use. + * @param index The index of the LED to get. + * @return The color of the LED (0xrrggbb format). + */ +uint32_t led_driver_get_led(LedDriver* led_driver, uint32_t index); + +/** + * @brief Send the LED data to the LEDs. + * @param led_driver The led driver to use. + */ +void led_driver_transmit(LedDriver* led_driver); diff --git a/js/flipboard/modules/js_rgbleds/led_driver_i.h b/js/flipboard/modules/js_rgbleds/led_driver_i.h new file mode 100644 index 0000000..b63dfb8 --- /dev/null +++ b/js/flipboard/modules/js_rgbleds/led_driver_i.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include "led_driver.h" + +#define MAX_LED_COUNT 16 + +// We store the HIGH/LOW durations (2 values) for each color bit (24 bits per LED) +#define LED_DRIVER_BUFFER_SIZE (MAX_LED_COUNT * 2 * 24) + +// We use a setinel value to figure out when the timer is complete. +#define LED_DRIVER_TIMER_SETINEL 0xFFFFU + +/** 64 transitions per us @ 64MHz. Our timing is in NANO_SECONDS */ +#define LED_DRIVER_TIMER_NANOSECOND (1000U / (SystemCoreClock / 1000000U)) + +// Timings for WS2812B +#define LED_DRIVER_T0H 400U +#define LED_DRIVER_T1H 800U +#define LED_DRIVER_T0L 850U +#define LED_DRIVER_T1L 450U +#define LED_DRIVER_TRESETL 55 * 1000U +#define LED_DRIVER_TDONE 2000U + +// Max wait for the DMA to complete. NOTE: 4000 leds*(850ns+450ns)*24 = 124.8ms + 50ms blanking = 174.8ms +#define LED_DRIVER_SETINEL_WAIT_MS 200 \ No newline at end of file diff --git a/js/flipboard/modules/js_rgbleds/rgbleds.c b/js/flipboard/modules/js_rgbleds/rgbleds.c new file mode 100644 index 0000000..4c6075a --- /dev/null +++ b/js/flipboard/modules/js_rgbleds/rgbleds.c @@ -0,0 +1,131 @@ +#include + +#include "rgbleds.h" +#include "led_driver.h" + +#define LED_COUNT 16 + +struct RgbLeds { + uint16_t num_leds; + uint32_t* color; + uint16_t brightness; + LedDriver* led_driver; +}; + +/** + * @brief Allocates a RgbLeds struct. + * @details This method allocates a RgbLeds struct. This is used to + * control the addressable LEDs. + * @param num_leds The number of LEDs to allocate. + * @param leds_pin The GPIO pin to use for the LEDs. (&gpio_ext_pc3) + * @return The allocated RgbLeds struct. +*/ +RgbLeds* rgbleds_alloc(uint16_t num_leds, const GpioPin* const leds_pin) { + RgbLeds* leds = malloc(sizeof(RgbLeds)); + leds->num_leds = num_leds; + leds->color = malloc(sizeof(uint32_t) * leds->num_leds); + leds->brightness = 255; + leds->led_driver = led_driver_alloc(leds->num_leds, leds_pin); + + rgbleds_reset(leds); + return leds; +} + +/** + * @brief Frees a RgbLeds struct. + * @param leds The RgbLeds struct to free. +*/ +void rgbleds_free(RgbLeds* leds) { + if(leds->led_driver) { + led_driver_free(leds->led_driver); + } + free(leds->color); + free(leds); +} + +/** + * @brief Resets the LEDs to their default color pattern (off). + * @details This method resets the LEDs data to their default color pattern (off). + * You must still call rgbleds_update to update the LEDs. + * @param leds The RgbLeds struct to reset. +*/ +void rgbleds_reset(RgbLeds* leds) { + for(int i = 0; i < leds->num_leds; i++) { + leds->color[i] = 0x000000; + } +} + +/** + * @brief Sets the color of the LEDs. + * @details This method sets the color of the LEDs. + * @param leds The RgbLeds struct to set the color of. + * @param led The LED index to set the color of. + * @param color The color to set the LED to (Hex value: RRGGBB). + * @return True if the LED was set, false if the LED was out of range. +*/ +bool rgbleds_set(RgbLeds* leds, uint16_t led, uint32_t color) { + if(led > leds->num_leds) { + return false; + } + + leds->color[led] = color; + return true; +} + +/** + * @brief Gets the color of the LEDs. + * @details This method gets the color of the LEDs. + * @param leds The RgbLeds struct to get the color of. + * @param led The LED index to get the color of. + * @return The color of the LED (Hex value: RRGGBB). +*/ +uint32_t rgbleds_get(RgbLeds* leds, uint16_t led) { + if(led > leds->num_leds) { + return 0; + } + + return leds->color[led]; +} + +/** + * @brief Sets the brightness of the LEDs. + * @details This method sets the brightness of the LEDs. + * @param leds The RgbLeds struct to set the brightness of. + * @param brightness The brightness to set the LEDs to (0-255). +*/ +void rgbleds_set_brightness(RgbLeds* leds, uint8_t brightness) { + leds->brightness = brightness; +} + +/** + * @brief Adjusts the brightness of a color. + * @details This method adjusts the brightness of a color. + * @param color The color to adjust. + * @param brightness The brightness to adjust the color to (0-255). + * @return The adjusted color. +*/ +static uint32_t adjust_color_brightness(uint32_t color, uint8_t brightness) { + uint32_t red = (color & 0xFF0000) >> 16; + uint32_t green = (color & 0x00FF00) >> 8; + uint32_t blue = (color & 0x0000FF); + + red = (red * brightness) / 255; + green = (green * brightness) / 255; + blue = (blue * brightness) / 255; + + return (red << 16) | (green << 8) | blue; +} + +/** + * @brief Updates the LEDs. + * @details This method changes the LEDs to the colors set by rgbleds_set. + * @param leds The RgbLeds struct to update. +*/ +void rgbleds_update(RgbLeds* leds) { + for(int i = 0; i < leds->num_leds; i++) { + uint32_t color = adjust_color_brightness(leds->color[i], leds->brightness); + led_driver_set_led(leds->led_driver, i, color); + } + + led_driver_transmit(leds->led_driver); +} \ No newline at end of file diff --git a/js/flipboard/modules/js_rgbleds/rgbleds.h b/js/flipboard/modules/js_rgbleds/rgbleds.h new file mode 100644 index 0000000..4e8271e --- /dev/null +++ b/js/flipboard/modules/js_rgbleds/rgbleds.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +typedef struct RgbLeds RgbLeds; + +/** + * @brief Allocates a RgbLeds struct. + * @details This method allocates a RgbLeds struct. This is used to + * control the addressable LEDs. + * @param num_leds The number of LEDs to allocate. + * @param leds_pin The GPIO pin to use for the LEDs. (&gpio_ext_pc3) + * @return The allocated RgbLeds struct. +*/ +RgbLeds* rgbleds_alloc(uint16_t num_leds, const GpioPin* const leds_pin); + +/** + * @brief Frees a RgbLeds struct. + * @param leds The RgbLeds struct to free. +*/ +void rgbleds_free(RgbLeds* leds); + +/** + * @brief Resets the LEDs to their default color pattern (off). + * @details This method resets the LEDs data to their default color pattern (off). + * You must still call rgbleds_update to update the LEDs. + * @param leds The RgbLeds struct to reset. +*/ +void rgbleds_reset(RgbLeds* leds); + +/** + * @brief Sets the color of the LEDs. + * @details This method sets the color of the LEDs. + * @param leds The RgbLeds struct to set the color of. + * @param led The LED index to set the color of. + * @param color The color to set the LED to (Hex value: RRGGBB). + * @return True if the LED was set, false if the LED was out of range. +*/ +bool rgbleds_set(RgbLeds* leds, uint16_t led, uint32_t color); + +/** + * @brief Gets the color of the LEDs. + * @details This method gets the color of the LEDs. + * @param leds The RgbLeds struct to get the color of. + * @param led The LED index to get the color of. + * @return The color of the LED (Hex value: RRGGBB). +*/ +uint32_t rgbleds_get(RgbLeds* leds, uint16_t led); + +/** + * @brief Sets the brightness of the LEDs. + * @details This method sets the brightness of the LEDs. + * @param leds The RgbLeds struct to set the brightness of. + * @param brightness The brightness to set the LEDs to (0-255). +*/ +void rgbleds_set_brightness(RgbLeds* leds, uint8_t brightness); + +/** + * @brief Updates the LEDs. + * @details This method changes the LEDs to the colors set by rgbleds_set. + * @param leds The RgbLeds struct to update. +*/ +void rgbleds_update(RgbLeds* leds); \ No newline at end of file diff --git a/js/flipboard/modules/js_speaker.c b/js/flipboard/modules/js_speaker.c new file mode 100644 index 0000000..7eef029 --- /dev/null +++ b/js/flipboard/modules/js_speaker.c @@ -0,0 +1,210 @@ +#include "../js_modules.h" +#include + +typedef struct { + bool acquired; +} JsSpeakerInst; + +static void js_speaker_acquire(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSpeakerInst* speaker = mjs_get_ptr(mjs, obj_inst); + furi_assert(speaker); + + if(mjs_nargs(mjs) != 1) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid args (timeoutMs)"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(!mjs_is_number(mjs_arg(mjs, 0))) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid numeric arg (timeoutMs)"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + uint32_t timeout = mjs_get_int(mjs, mjs_arg(mjs, 0)); + + if(!speaker->acquired) { + speaker->acquired = furi_hal_speaker_acquire(timeout); + } + + mjs_return(mjs, mjs_mk_boolean(mjs, speaker->acquired)); +} + +static void js_speaker_release(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSpeakerInst* speaker = mjs_get_ptr(mjs, obj_inst); + furi_assert(speaker); + + if(mjs_nargs(mjs) != 0) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "No arguments expected"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(speaker->acquired) { + furi_hal_speaker_stop(); + furi_hal_speaker_release(); + speaker->acquired = false; + } + + mjs_return(mjs, mjs_mk_boolean(mjs, true)); +} + +static void js_speaker_start(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSpeakerInst* speaker = mjs_get_ptr(mjs, obj_inst); + furi_assert(speaker); + + size_t num_args = mjs_nargs(mjs); + float frequency; + float volume; + + if(num_args == 1) { + if(!mjs_is_number(mjs_arg(mjs, 0))) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid numeric arg (freq [, volume])"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + volume = 1.0; + } else if(num_args == 2) { + if(!mjs_is_number(mjs_arg(mjs, 0)) || !mjs_is_number(mjs_arg(mjs, 1))) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid numeric arg (freq [, volume])"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + volume = mjs_get_double(mjs, mjs_arg(mjs, 1)); + if(volume < 0 || volume > 1) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid volume (0 <= volume <= 1)"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + } else { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid args (freq [, volume])"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(!speaker->acquired) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Speaker must be acquired first"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + frequency = mjs_get_double(mjs, mjs_arg(mjs, 0)); + + furi_hal_speaker_start(frequency, volume); + + mjs_return(mjs, mjs_mk_boolean(mjs, true)); +} + +static void js_speaker_stop(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSpeakerInst* speaker = mjs_get_ptr(mjs, obj_inst); + furi_assert(speaker); + + if(mjs_nargs(mjs) != 0) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "No arguments expected"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(!speaker->acquired) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Speaker must be acquired first"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + furi_hal_speaker_stop(); + + mjs_return(mjs, mjs_mk_boolean(mjs, true)); +} + +static void js_speaker_play(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSpeakerInst* speaker = mjs_get_ptr(mjs, obj_inst); + furi_assert(speaker); + + size_t num_args = mjs_nargs(mjs); + float frequency; + float volume; + uint32_t duration; + uint32_t timeout = 1000; + bool acquired_in_play = false; + + if(num_args != 3) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid args (freq, volume, duration)"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(!mjs_is_number(mjs_arg(mjs, 0)) || !mjs_is_number(mjs_arg(mjs, 1)) || + !mjs_is_number(mjs_arg(mjs, 2))) { + mjs_prepend_errorf( + mjs, MJS_BAD_ARGS_ERROR, "Invalid numeric arg (freq, volume, duration)"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + frequency = mjs_get_double(mjs, mjs_arg(mjs, 0)); + volume = mjs_get_double(mjs, mjs_arg(mjs, 1)); + duration = mjs_get_int(mjs, mjs_arg(mjs, 2)); + + if(!speaker->acquired) { + acquired_in_play = true; + speaker->acquired = furi_hal_speaker_acquire(timeout); + } + + if(speaker->acquired) { + furi_hal_speaker_start(frequency, volume); + furi_delay_ms(duration); + furi_hal_speaker_stop(); + if(acquired_in_play) { + furi_hal_speaker_release(); + speaker->acquired = false; + } + mjs_return(mjs, mjs_mk_boolean(mjs, true)); + } else { + mjs_return(mjs, mjs_mk_boolean(mjs, false)); + } +} + +static void* js_speaker_create(struct mjs* mjs, mjs_val_t* object) { + JsSpeakerInst* speaker = malloc(sizeof(JsSpeakerInst)); + speaker->acquired = false; + mjs_val_t speaker_obj = mjs_mk_object(mjs); + mjs_set(mjs, speaker_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, speaker)); + mjs_set(mjs, speaker_obj, "acquire", ~0, MJS_MK_FN(js_speaker_acquire)); + mjs_set(mjs, speaker_obj, "release", ~0, MJS_MK_FN(js_speaker_release)); + mjs_set(mjs, speaker_obj, "start", ~0, MJS_MK_FN(js_speaker_start)); + mjs_set(mjs, speaker_obj, "stop", ~0, MJS_MK_FN(js_speaker_stop)); + mjs_set(mjs, speaker_obj, "play", ~0, MJS_MK_FN(js_speaker_play)); + *object = speaker_obj; + return speaker; +} + +static void js_speaker_destroy(void* inst) { + JsSpeakerInst* speaker = (JsSpeakerInst*)inst; + if(speaker->acquired) { + furi_hal_speaker_stop(); + furi_hal_speaker_release(); + speaker->acquired = false; + } + free(speaker); +} + +static const JsModuleDescriptor js_speaker_desc = { + "speaker", + js_speaker_create, + js_speaker_destroy, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_speaker_desc, +}; + +const FlipperAppPluginDescriptor* js_speaker_ep(void) { + return &plugin_descriptor; +} \ No newline at end of file diff --git a/js/flipboard/png/down.png b/js/flipboard/png/down.png new file mode 100644 index 0000000000000000000000000000000000000000..8be68614d42c4162098e848907cbf9bf8a2c2d7a GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>3?#4ne^UZdjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCils0(?ST|Ns9FWQHEPTnD5$3p^r=85sBufiR<}hF1enP{z~6F+?IfSwYO; p5W^f!an{q#GX&TsoIG&g00RT_WCp3(EWRLh44$rjF6*2UngD2fCSd>o literal 0 HcmV?d00001 diff --git a/js/flipboard/png/flipboard.png b/js/flipboard/png/flipboard.png new file mode 100644 index 0000000000000000000000000000000000000000..bad3fe897a34d411a851e7e42bf65cb3f08a6903 GIT binary patch literal 45675 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 z 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 literal 0 HcmV?d00001 diff --git a/js/flipboard/readme.md b/js/flipboard/readme.md new file mode 100644 index 0000000..d6f30c4 --- /dev/null +++ b/js/flipboard/readme.md @@ -0,0 +1,79 @@ +# FlipBoard + +![FlipBoard](png/flipboard.png) + +The FlipBoard is an MacroPad from [MakeItHackin](https://github.com/MakeItHackin/FlipBoard) with software from @CodeAllNight ([https://youtube.com/@MrDerekJamison]([https://youtube.com/@MrDerekJamison)). It connects to the Flipper Zero's GPIO pins and is a 4 button keypad with addressable LEDs. You can purchase it on [tindie](https://www.tindie.com/products/32844/) or [Etsy](https://www.etsy.com/listing/1601295558/). + +This tutorial is a guide to controlling the FlipBoard using JavaScript on Flipper Zero. This [video](https://www.youtube.com/@MrDerekJamison) also describes the process. + +## Requirements + +- [Flipper Zero](https://flipperzero.one/how-to-buy) +- [FlipBoard](https://github.com/MakeItHackin/FlipBoard) +- Latest custom firmware for Flipper Zero (requires **dev branch** for most firmware, due to JavaScript being so new for the Flipper Zero). + +## Software Installation + +- Load qFlipper. +- Click the `File Manager` tab. +- Navigate to `SD Card`/`apps`/`Scripts` folder. +- Right click and choose `New Folder` and name it `flipboard`. +- Navigate into the `flipboard` folder. +- Copy all of the files from the [scripts](./scripts/) into the `flipboard` folder. +- Navigate to the `SD Card`/`apps_data`/`js_app`/`plugins` folder. +- Copy the files from the [fal](./fal) folder that matches your firmware (for example if you are on Momentum dev branch from mid-April [mntm-dev-2024-04-19-a2fc553](./fal/mntm-dev-2024-04-19-a2fc553/)) into the `plugins` folder. + +## Running the Software + +- Connect the FlipBoard to the Flipper Zero. +- On the Flipper, press `OK` to open the main menu. +- Select `Apps`, `Scripts`, `flipboard` then `a_runner.js` and press `OK`. +- Select the flipboard script (ends with `fb.js`) that you want to run. +- Press buttons on your FlipBoard to interact with the script. + +## Building on custom firmware + +The steps above in Software Installation have you overlay the fal files on top of your existing firmware. If you want to build the fal files into your firmware, you can follow these steps instead. + +Clone your firmware. The following [wiki](https://github.com/jamisonderek/flipper-zero-tutorials/wiki/Install-Firmware-and-Apps#clone--deploy-firmware) describes the process. + +Copy the files from the [modules](./modules) folder into the `applications\system\js_app\modules` folder of your custom firmware source code. + +Edit your `applications\system\js_app\application.fam` file to include the following: + +```c +App( + appid="js_rgbleds", + apptype=FlipperAppType.PLUGIN, + entry_point="js_rgbleds_ep", + requires=["js_app"], + sources=["modules/js_rgbleds/*.c"], +) + +App( + appid="js_infrared", + apptype=FlipperAppType.PLUGIN, + entry_point="js_infrared_ep", + requires=["js_app"], + sources=["modules/js_infrared.c"], +) + +App( + appid="js_speaker", + apptype=FlipperAppType.PLUGIN, + entry_point="js_speaker_ep", + requires=["js_app"], + sources=["modules/js_speaker.c"], +) +``` + +Rebuild your firmware and flash it to your Flipper Zero. NOTE: In some cases, you may be able to rebuild just the FAL modules by first opening the `applications\system\js_app\js_app.c` file and then choosing `Terminal`, `Run Task...`, `[Release] Launch app on Flipper`. + +## Support +If you have any questions, please ask in my [Flipper Zero Discord](https://discord.com/invite/NsjCvqwPAd) server. There are also giveaways and other fun things happening there. + +Support my work: +- Option 1. [Like, Subscribe and click the Bell (to get notified)](https://youtu.be/DAUQGeG4pc4) +- Option 2. [https://ko-fi.com/codeallnight](https://ko-fi.com/codeallnight) (donate $3 via PayPal or Venmo) +- Option 3. Click the "Thanks" button on [YouTube](https://youtu.be/DAUQGeG4pc4). +- Option 4. Purchase a [FlipBoard](https://github.com/MakeItHackin/FlipBoard) (I get a portion of the sale). diff --git a/js/flipboard/scripts/a_runner.js b/js/flipboard/scripts/a_runner.js new file mode 100644 index 0000000..5357605 --- /dev/null +++ b/js/flipboard/scripts/a_runner.js @@ -0,0 +1,44 @@ +let __dirpath = "/ext/apps/Scripts/flipboard"; + +let loader = load(__dirpath + "/loader_api.js"); + +// Prompt user for the .fb file to load, this is a FlipBoard script file +let dialog = loader.require("dialog"); +let fb = dialog.pickFile(__dirpath, "fb.js"); +if (fb === undefined) { + die("No file selected"); +} +loader.load("fn", fb); +print("Loaded", loader.fn.title); + +// Initialize textbox +let textbox = loader.require("textbox"); +textbox.setConfig("end", "text"); +textbox.emptyText(); + +// Initialize access to the Flipboard buttons +let flipboardButton = loader.load("flipboardButton", __dirpath + "/fb_button_api.js"); +flipboardButton.init(); + +// Initialize access to the Flipboard addressable LEDs +let color = loader.load("color", __dirpath + "/color_api.js"); +let flipboardLeds = loader.load("flipboardLeds", __dirpath + "/fb_leds_api.js"); +flipboardLeds.init(color, [color.green, color.red, color.yellow, color.blue]); + +// Initialize the function callback +loader.fn.init(loader); + +// Main loop +let buttonNumber = 0; +while (true) { + // Wait for a button press + buttonNumber = flipboardButton.debounceButton(buttonNumber); + + // Convert the button press to an array of LEDs to light up + let pressedArray = flipboardButton.buttonNumberToArray(buttonNumber); + + // Update the LED brightness. + flipboardLeds.updateLeds(pressedArray); + + loader.fn.buttonPressed(buttonNumber, pressedArray); +} diff --git a/js/flipboard/scripts/badusb_textbox_fb.js b/js/flipboard/scripts/badusb_textbox_fb.js new file mode 100644 index 0000000..b83c0de --- /dev/null +++ b/js/flipboard/scripts/badusb_textbox_fb.js @@ -0,0 +1,56 @@ +({ + title: "BadUSB Textbox", + primaryAction: function (buttonNumber) { + // Do an action based on the button number. + if (buttonNumber === 1) { + this.api.badusb.println("https://www.youtube.com/@MrDerekJamison/playlists", 10); + } else if (buttonNumber === 2) { + this.api.badusb.print("Flipper Name: ", 10); + this.api.require("flipper"); + this.api.badusb.println(this.api.flipper.getName(), 10); + } else if (buttonNumber === 4) { + this.api.badusb.println("I TYPE SLOW!", 250); + } else if (buttonNumber === 8) { + this.api.badusb.altPrintln("This was printed with Alt+Numpad method!"); + } + }, + init: function (api) { + this.api = api; + + // Initialize access to the Flipper Zero speaker + this.api.initSpeaker(); + + // Initialize access to the BadUSB (virtual keyboard) device + this.api.initBadusb("/ext/badusb/assets/layouts/en-US.kl"); + + this.initTextbox(); + }, + initTextbox: function () { + this.api.textbox.addText(this.title + "\n"); + this.api.textbox.addText("Press a button.\n"); + this.api.textbox.show(); + }, + updateTextbox: function (buttonNumber, pressedArray) { + let text = "Button " + to_string(buttonNumber) + " pressed: "; + for (let i = 0; i < 4; i++) { + text += pressedArray[i] ? "X" : "_"; + } + text += "\n"; + this.api.textbox.addText(text); + }, + buttonPressed: function (buttonNumber, pressedArray) { + // Redraw the textbox to show the button press. + this.updateTextbox(buttonNumber, pressedArray); + + // A button press of 0 means the user released all of the buttons. + if (buttonNumber === 0) { + return; + } + + // Play a tone for 100ms when button pressed. + this.api.speaker.play(440 + 100 * buttonNumber, 1.00, 100); + + // Perform the primary action. + this.primaryAction(buttonNumber); + } +}) \ No newline at end of file diff --git a/js/flipboard/scripts/badusb_widget_fb.js b/js/flipboard/scripts/badusb_widget_fb.js new file mode 100644 index 0000000..0273fb6 --- /dev/null +++ b/js/flipboard/scripts/badusb_widget_fb.js @@ -0,0 +1,96 @@ +({ + title: "BadUSB Widget", + primaryAction: function (buttonNumber) { + // Do an action based on the button number. + if (buttonNumber === 1) { + this.api.badusb.hold("CTRL", "ALT"); + this.api.badusb.press("DELETE"); + this.api.badusb.release("CTRL", "ALT"); + delay(1000); + this.api.badusb.press("DOWN"); + this.api.badusb.press("DOWN"); + delay(2000); + this.api.badusb.press("DOWN"); + this.api.badusb.press("ENTER"); + } else if (buttonNumber === 2) { + // Trigger sticky keys by pressing shift 5 times. + for (let i = 0; i < 5; i++) { + this.api.badusb.press("SHIFT"); + } + } else if (buttonNumber === 4) { + this.api.badusb.println("SYMBOL TEST `!@#$%^&*()_+-=[]{};'\\:\"|,./<>?", 10); + } else if (buttonNumber === 8) { + this.api.badusb.altPrintln("Alt+Numpad `!@#$%^&*()_+-=[]{};'\\:\"|,./<>?"); + } + }, + init: function (api) { + this.api = api; + + this.splashScreen(); + + // Initialize access to the Flipper Zero speaker + this.api.initSpeaker(); + + // Initialize access to the BadUSB (virtual keyboard) device + this.api.initBadusb("prompt"); + + this.initWidget(); + }, + splashScreen: function () { + this.api.require("widget"); + this.api.widget.show(); + let fxbmFlippy = this.api.widget.loadImageXbm(__dirpath + "/flippy.fxbm"); + let splash = []; + splash.push(this.api.widget.addXbm(0, 0, fxbmFlippy)); + splash.push(this.api.widget.addText(70, 10, "Secondary", "Be sure")); + splash.push(this.api.widget.addText(70, 20, "Secondary", "to attach")); + splash.push(this.api.widget.addText(70, 30, "Secondary", "FlipBoard.")); + splash.push(this.api.widget.addText(70, 44, "Secondary", "Connect USB")); + splash.push(this.api.widget.addText(70, 54, "Secondary", "data cable")); + splash.push(this.api.widget.addText(70, 64, "Secondary", "to PC.")); + delay(5000); + for (let i = 0; i < splash.length; i++) { + this.api.widget.remove(splash[i]); + } + }, + initWidget: function () { + this.api.widget.addText(25, 15, "Primary", this.title); + this.status = this.api.widget.addText(10, 60, "Secondary", "Press a button!"); + + this.fxbmUp = this.api.widget.loadImageXbm(__dirpath + "/up.fxbm"); + this.fxbmDown = this.api.widget.loadImageXbm(__dirpath + "/down.fxbm"); + this.icons = []; + + for (let i = 0; i < 4; i++) { + this.icons.push(this.api.widget.addXbm(9 + i * 30, 32, this.fxbmUp)); + this.api.widget.addCircle(10 + i * 30 + 3, 36, 10); + } + }, + updateWidget: function (buttonNumber, pressedArray) { + for (let i = 0; i < 4; i++) { + this.api.widget.remove(this.icons[i]); + if (pressedArray[i]) { + this.icons[i] = this.api.widget.addXbm(9 + i * 30, 32, this.fxbmDown); + } else { + this.icons[i] = this.api.widget.addXbm(9 + i * 30, 32, this.fxbmUp); + } + } + this.api.widget.remove(this.status); + this.status = this.api.widget.addText(10, 60, "Secondary", "Button " + to_string(buttonNumber)); + }, + buttonPressed: function (buttonNumber, pressedArray) { + // Redraw the widget to show the button press. + this.updateWidget(buttonNumber, pressedArray); + + // A button press of 0 means the user released all of the buttons. + if (buttonNumber === 0) { + return; + } + + // Play a tone for 100ms when button pressed. + this.api.speaker.play(440 + 100 * buttonNumber, 1.00, 100); + + // Perform the primary action. + this.primaryAction(buttonNumber); + } +}) \ No newline at end of file diff --git a/js/flipboard/scripts/color_api.js b/js/flipboard/scripts/color_api.js new file mode 100644 index 0000000..ea44bf9 --- /dev/null +++ b/js/flipboard/scripts/color_api.js @@ -0,0 +1,15 @@ +({ + green: { red: 0x00, green: 0xFF, blue: 0x00 }, + red: { red: 0xFF, green: 0x00, blue: 0x00 }, + yellow: { red: 0xFF, green: 0x7F, blue: 0x00 }, + blue: { red: 0x00, green: 0x00, blue: 0xFF }, + default_glow: 0.10, + bright_glow: 0.90, + brightness: function (color, brightness) { + return { + red: color.red * brightness, + green: color.green * brightness, + blue: color.blue * brightness + }; + } +}) \ No newline at end of file diff --git a/js/flipboard/scripts/down.fxbm b/js/flipboard/scripts/down.fxbm new file mode 100644 index 0000000000000000000000000000000000000000..452bd5a2643bda5dc50ee9665d41c3e37d6c5fb5 GIT binary patch literal 30 bcmb1QU|`^cVju+u{~7-=)G$~u2rvKu8nFWJ literal 0 HcmV?d00001 diff --git a/js/flipboard/scripts/fb_button_api.js b/js/flipboard/scripts/fb_button_api.js new file mode 100644 index 0000000..e50e621 --- /dev/null +++ b/js/flipboard/scripts/fb_button_api.js @@ -0,0 +1,55 @@ +({ + gpio: require("gpio"), + button_pins: ["PB2", "PB3", "PA4", "PA6"], + repeat: false, + init: function () { + for (let i = 0; i < this.button_pins.length; i++) { + this.gpio.init(this.button_pins[i], "input", "up"); + } + }, + getButtons: function () { + let n = 0; + for (let i = 0; i < this.button_pins.length; i++) { + let isPressed = !this.gpio.read(this.button_pins[i]); + n += isPressed ? 1 << i : 0; + } + + return n; + }, + debounceButton: function (button) { + let threshold = 3; + let repeatThreshold = 5; + let debounce = { counter: threshold, button: button & ~16 }; + while (true) { + let button = this.getButtons(); + if (button !== debounce.button) { + debounce.counter = 0; + debounce.button = button; + continue; + } else { + debounce.counter++; + if (debounce.counter === threshold) { + break; + } else if (debounce.counter < threshold) { + continue; + } else if (debounce.counter > threshold) { + if (this.repeat && debounce.counter > repeatThreshold) { + debounce.button |= 16; + break; + } + delay(1); + continue; + } + } + } + + return debounce.button; + }, + buttonNumberToArray: function (button_number) { + let binary_array = []; + for (let i = 0; i < this.button_pins.length; i++) { + binary_array.push((button_number >> i) & 1); + } + return binary_array; + } +}) \ No newline at end of file diff --git a/js/flipboard/scripts/fb_leds_api.js b/js/flipboard/scripts/fb_leds_api.js new file mode 100644 index 0000000..da43d7c --- /dev/null +++ b/js/flipboard/scripts/fb_leds_api.js @@ -0,0 +1,28 @@ +({ + color_api: undefined, + rgbLeds: require("rgbleds"), + led_colors: [], + updateLeds: function (bright) { + let isChanged = false; + for (let i = 0; i < this.led_colors.length; i++) { + let b = bright[i] ? this.color_api.bright_glow : this.color_api.default_glow; + let c = this.color_api.brightness(this.led_colors[i], b); // using global 'color' object. + if (this.rgbLeds.set(i, c) !== c) { + isChanged = true; + } + } + // We always call update, so LEDs can be unplugged and reconnected. + this.rgbLeds.update(); + return isChanged; + }, + init: function (color_api, colors) { + this.color_api = color_api; + this.led_colors = colors; + this.rgbLeds.setup({ "pin": "PC3", "count": this.led_colors.length, "spec": "WS2812B" }); + let state = []; + for (let i = 0; i < this.led_colors.length; i++) { + state.push(false); + } + this.updateLeds(state); + } +}) \ No newline at end of file diff --git a/js/flipboard/scripts/flippy.fxbm b/js/flipboard/scripts/flippy.fxbm new file mode 100644 index 0000000000000000000000000000000000000000..d367a69698ec7842a077f1a74010900bb7eae10f GIT binary patch literal 524 zcmaKpJx;_h5QU!^`3;L`y96!;h!Z8hOc^wiI} zmJ7PS7JyjCsrPaQ<4N~l>4Fp470Q9*lwXkp-vY~c{LNQD?_Fme(DsOo z6?~6CSQ)SS?Yn6bD{4Bevtrk2-Okol+VJ9LPgWXIx^iJPA9sDf;bqQeO&|5$P2$Np zYW1bYsQqNoR>~Uy&2`?vCWS%&c|^^=21bsha>-H318ni=rW`4x_<&Zv@y~t%Ly2is literal 0 HcmV?d00001 diff --git a/js/flipboard/scripts/infrared_fb.js b/js/flipboard/scripts/infrared_fb.js new file mode 100644 index 0000000..a935066 --- /dev/null +++ b/js/flipboard/scripts/infrared_fb.js @@ -0,0 +1,66 @@ +({ + title: "Infrared Blast", + primaryAction: function (buttonNumber) { + // Do an action based on the button number. + if (buttonNumber === 1) { + this.api.textbox.addText("\nPower"); + this.api.infrared.sendProtocol("Samsung32", 0x07, 0x02); + } else if (buttonNumber === 2) { + this.api.textbox.addText("\nVolume Up"); + this.api.infrared.sendProtocol("Samsung32", 0x07, 0x07); + } else if (buttonNumber === 4) { + this.api.textbox.addText("\nVolume Down"); + this.api.infrared.sendProtocol("Samsung32", 0x07, 0x0B); + } else if (buttonNumber === (2 | 8)) { + this.api.textbox.addText("\nChannel Up"); + this.api.infrared.sendProtocol("Samsung32", 0x07, 0x12); + } else if (buttonNumber === (4 | 8)) { + this.api.textbox.addText("\nChannel Down"); + this.api.infrared.sendProtocol("Samsung32", 0x07, 0x10); + } + }, + init: function (api) { + this.api = api; + + // Allow the Flipboard button to repeat action [adding button 16] when held down. + this.api.flipboardButton.repeat = true; + + // Initialize access to the Flipper Zero speaker + this.api.initSpeaker(); + + // Initialize access to the Infrared module + this.api.require("infrared"); + + this.initTextbox(); + }, + initTextbox: function () { + this.api.textbox.addText(this.title + "\n"); + this.api.textbox.addText("Green: Power.\nRed = Volume +\nYellow = Volume -\nBlue + Red = Channel +\nBlue + Yellow = Channel -"); + this.api.textbox.show(); + }, + buttonPressed: function (buttonNumber, _pressedArray) { + // Ignore button 1 [Power] if repeat (16). + if (buttonNumber === (1 | 16)) { + return; + } + + // Remove the repeat flag. + buttonNumber = buttonNumber & ~16; + + // A button press of 0 means the user released all of the buttons. + if (buttonNumber === 0) { + return; + } + + // Ignore holding button 8 + if (buttonNumber === 8) { + return; + } + + // Play a tone for 100ms when button pressed. + this.api.speaker.play(440 + 100 * buttonNumber, 1.00, 100); + + // Perform the primary action. + this.primaryAction(buttonNumber); + } +}) \ No newline at end of file diff --git a/js/flipboard/scripts/loader_api.js b/js/flipboard/scripts/loader_api.js new file mode 100644 index 0000000..50eca7e --- /dev/null +++ b/js/flipboard/scripts/loader_api.js @@ -0,0 +1,67 @@ +({ + isDefined: function (name) { + return this[name] !== undefined; + }, + require: function (name) { + let lib = undefined; + if (!this.isDefined(name)) { + lib = require(name); + this[name] = lib; + } else { + lib = this[name]; + } + return lib; + }, + load: function (name, path) { + let lib = undefined; + if (!this.isDefined(name)) { + lib = load(path); + this[name] = lib; + } else { + lib = this[name]; + } + return lib; + }, + defaultBadusbLayout: "/ext/badusb/assets/layouts/en-US.kl", + initBadusb: function (layout_path) { + // Initialize access to the BadUSB (virtual keyboard) device + if (!this.isDefined("badusb")) { + this.require("badusb"); + if (layout_path === undefined) { + layout_path = this.defaultBadusbLayout; + } else if (layout_path.charCodeAt(0) !== 0x2F) { // If not an absolute path, prompt for file. + if (!this.isDefined("dialog")) { + this.require("dialog"); + } + layout_path = this.dialog.pickFile("/ext/badusb/assets/layouts", ".kl"); + if (layout_path === undefined) { + layout_path = this.defaultBadusbLayout; + } + } + this.badusb.setup({ + vid: 0x05ac, + pid: 0x021e, + mfr_name: "Apple", + prod_name: "Keyboard", + layout_path: layout_path + }); + } + }, + initSpeaker: function () { + // Initialize access to the speaker + if (!this.isDefined("speaker")) { + this.require("speaker"); + + this.speaker.acquire(1000); // NOTE: it will be released when the script exits. + } + }, + initSubghz: function () { + // Initialize access to the Sub-GHz radio + if (!this.isDefined("subghz")) { + this.require("subghz"); + this.subghz.setup(); + // For some reason subghz impacts our GPIO pins (so reset them). + this.flipboardButton.init(); + } + } +}) \ No newline at end of file diff --git a/js/flipboard/scripts/subghz_fb.js b/js/flipboard/scripts/subghz_fb.js new file mode 100644 index 0000000..466537b --- /dev/null +++ b/js/flipboard/scripts/subghz_fb.js @@ -0,0 +1,41 @@ +({ + title: "SubGHz Sender", + primaryAction: function (buttonNumber) { + // Do an action based on the button number. + if (buttonNumber === 1) { + this.api.speaker.start(540, 1.00); + this.api.subghz.transmitFile("/ext/subghz/Light_on.sub"); + this.api.speaker.stop(); + } else if (buttonNumber === 2) { + this.api.speaker.start(640, 1.00); + this.api.subghz.transmitFile("/ext/subghz/Light_off.sub"); + this.api.speaker.stop(); + } + }, + init: function (api) { + this.api = api; + + // Initialize access to the Flipper Zero speaker + this.api.initSpeaker(); + + // Initialize access to the SubGHz module + this.api.initSubghz(); + + this.initTextbox(); + }, + initTextbox: function () { + this.api.textbox.addText(this.title + "\n"); + this.api.textbox.addText("Button 1 = Light on.\n"); + this.api.textbox.addText("Button 2 = Light off.\n"); + this.api.textbox.show(); + }, + buttonPressed: function (buttonNumber, pressedArray) { + // We only use the first two buttons. + if (buttonNumber !== 1 && buttonNumber !== 2) { + return; + } + + // Perform the primary action. + this.primaryAction(buttonNumber); + } +}) \ No newline at end of file diff --git a/js/flipboard/scripts/up.fxbm b/js/flipboard/scripts/up.fxbm new file mode 100644 index 0000000000000000000000000000000000000000..27e8098c806349b17e00feecbe0bee49b2b86f42 GIT binary patch literal 30 bcmb1QU|`^cVgUvVh8l){4F4H{0$>0DCMg2% literal 0 HcmV?d00001