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 0000000..bd67ce9 Binary files /dev/null and b/js/flipboard/fal/mntm-001/js_dialog.fal differ 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 0000000..cb93c22 Binary files /dev/null and b/js/flipboard/fal/mntm-001/js_gpio.fal differ 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 0000000..41ca505 Binary files /dev/null and b/js/flipboard/fal/mntm-001/js_infrared.fal differ diff --git a/js/flipboard/fal/mntm-001/js_rgbleds.fal b/js/flipboard/fal/mntm-001/js_rgbleds.fal new file mode 100644 index 0000000..8801ecc Binary files /dev/null and b/js/flipboard/fal/mntm-001/js_rgbleds.fal differ 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 0000000..71c697d Binary files /dev/null and b/js/flipboard/fal/mntm-001/js_speaker.fal differ 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 0000000..18da69f Binary files /dev/null and b/js/flipboard/fal/mntm-001/js_textbox.fal differ 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 0000000..43744a6 Binary files /dev/null and b/js/flipboard/fal/mntm-001/js_widget.fal differ 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 0000000..385c700 Binary files /dev/null and b/js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_infrared.fal differ diff --git a/js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_rgbleds.fal b/js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_rgbleds.fal new file mode 100644 index 0000000..8dacab1 Binary files /dev/null and b/js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_rgbleds.fal differ diff --git a/js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_speaker.fal b/js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_speaker.fal new file mode 100644 index 0000000..413aaf3 Binary files /dev/null and b/js/flipboard/fal/mntm-dev-2024-04-19-a2fc553/js_speaker.fal differ 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 0000000..8be6861 Binary files /dev/null and b/js/flipboard/png/down.png differ diff --git a/js/flipboard/png/flipboard.png b/js/flipboard/png/flipboard.png new file mode 100644 index 0000000..bad3fe8 Binary files /dev/null and b/js/flipboard/png/flipboard.png differ diff --git a/js/flipboard/png/flippy.png b/js/flipboard/png/flippy.png new file mode 100644 index 0000000..1c0a5d6 Binary files /dev/null and b/js/flipboard/png/flippy.png differ diff --git a/js/flipboard/png/up.png b/js/flipboard/png/up.png new file mode 100644 index 0000000..45f7e4d Binary files /dev/null and b/js/flipboard/png/up.png differ 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 0000000..452bd5a Binary files /dev/null and b/js/flipboard/scripts/down.fxbm differ 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 0000000..d367a69 Binary files /dev/null and b/js/flipboard/scripts/flippy.fxbm differ 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 0000000..27e8098 Binary files /dev/null and b/js/flipboard/scripts/up.fxbm differ