From 466ea087a0f2cb684b0fdfec46444344088c4926 Mon Sep 17 00:00:00 2001 From: rusdacent <57439765+rusdacent@users.noreply.github.com> Date: Sun, 15 Nov 2020 13:14:52 +0300 Subject: [PATCH 1/6] Refactoring firmware uploading to update server (#238) * Replace f2 to f3 --- .github/assets/Born2bSportyV2.ttf | Bin 0 -> 55532 bytes .github/assets/latest-firmware-template.png | Bin 0 -> 19783 bytes .github/workflows/ci.yml | 27 ++++++-------------- README.md | 2 +- 4 files changed, 9 insertions(+), 20 deletions(-) create mode 100644 .github/assets/Born2bSportyV2.ttf create mode 100644 .github/assets/latest-firmware-template.png diff --git a/.github/assets/Born2bSportyV2.ttf b/.github/assets/Born2bSportyV2.ttf new file mode 100644 index 0000000000000000000000000000000000000000..02001782191bb61a6e111d0c10cae154ef9a298f GIT binary patch literal 55532 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 literal 0 HcmV?d00001 diff --git a/.github/assets/latest-firmware-template.png b/.github/assets/latest-firmware-template.png new file mode 100644 index 0000000000000000000000000000000000000000..11eb523a9db90fc4cc0ef12b03c9b2e47c86a392 GIT binary patch literal 19783 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 I+y3!?0aQ?!j{pDw literal 0 HcmV?d00001 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3774154e..fe850e87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,12 +110,12 @@ jobs: - name: Get bootloader uses: actions/download-artifact@v2 with: - name: bootloader_f2 + name: bootloader_f3 path: bootloader - name: Get firmware uses: actions/download-artifact@v2 with: - name: firmware_f2 + name: firmware_f3 path: firmware - name: Upload bootloader uses: burnett01/rsync-deployments@4.1 @@ -145,18 +145,13 @@ jobs: - name: Get bootloader uses: actions/download-artifact@v2 with: - name: bootloader_f2 + name: bootloader_f3 path: bootloader - name: Get firmware uses: actions/download-artifact@v2 with: - name: firmware_f2 - path: firmware - - name: Get firmware - uses: actions/download-artifact@v2 - with: - name: firmware_f2 - path: firmware + name: firmware_f3 + path: firmware - name: cp run: cp ./bootloader/bootloader.bin full_firmware_latest.bin - name: truncate @@ -185,14 +180,8 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - ref: master + ref: master submodules: true - - name: Echo debug - run: echo ${{ github.ref }} - - name: Get template and font - run: | - wget https://zhovner.com/tmp/latest-firmware-template.png - wget https://zhovner.com/tmp/Born2bSportyV2.ttf - name: Set test env run: echo "NUMBER_OF_COMMITS=$(git rev-list --count HEAD)" >> $GITHUB_ENV - name: Test output NUMBER_OF_COMMITS @@ -205,12 +194,12 @@ jobs: - name: Test output PREP_DATE run: echo $PREP_DATE - name: Gen pic - run: convert latest-firmware-template.png -font ./Born2bSportyV2.ttf -weight 700 -pointsize 140 -annotate +900+330 "$NUMBER_OF_COMMITS $PREP_DATE" out.png + run: convert ./.github/assets/latest-firmware-template.png -font ./.github/assets/Born2bSportyV2.ttf -weight 700 -pointsize 140 -annotate +900+330 "$NUMBER_OF_COMMITS $PREP_DATE" latest-firmware-banner.png - name: Upload pic uses: burnett01/rsync-deployments@4.1 with: switches: -avzp --delete - path: out.png + path: latest-firmware-banner.png remote_path: "${{ secrets.RSYNC_DEPLOY_BASE_PATH }}/" remote_host: ${{ secrets.RSYNC_DEPLOY_HOST }} remote_user: ${{ secrets.RSYNC_DEPLOY_USER }} diff --git a/README.md b/README.md index 93691271..cec4908f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Welcome to [Flipper Zero](https://flipperzero.one/)'s Firmware repo! Our goal is # Update firmware - + Flipper Zero's firmware consists of two components: Bootloader and main firmware. Bootloader controls firmware update process over USB. You need working bootloader installed before update firmware over USB. From 3749eb0eed6b6e7d59b2cbc5f8cdafda47e72cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Mon, 16 Nov 2020 13:16:34 +0300 Subject: [PATCH 2/6] USB VCP Cli (#237) * Core: ring buffer. * Api: usb vcp. F3: vcp glue code. * Applications: cli draft version. * Cli: basic working version, includes help and version commands * HAL: vcp on f2 * Makefile: update openocd conf * F3: vcp rx with freertos stream * Cli: help * Cli: standard commands, api-hal-uid * Power: cli poweroff. --- applications/applications.h | 7 +- applications/applications.mk | 8 + applications/cli/cli.c | 173 ++++++++++++++++++ applications/cli/cli.h | 42 +++++ applications/cli/cli_commands.c | 54 ++++++ applications/cli/cli_commands.h | 5 + applications/cli/cli_i.h | 42 +++++ applications/power/power.c | 13 ++ core/ring.c | 138 ++++++++++++++ core/ring.h | 22 +++ debug/stm32wbx.cfg | 105 ----------- firmware/targets/Inc/api-hal-uid.h | 10 + firmware/targets/Inc/api-hal-vcp.h | 24 +++ .../targets/{f2/api-hal => Inc}/api-hal.h | 3 + firmware/targets/f2/Inc/usbd_cdc_if.h | 4 +- firmware/targets/f2/Src/main.c | 1 + firmware/targets/f2/Src/usbd_cdc_if.c | 100 ++++------ firmware/targets/f2/api-hal/api-hal-uid.c | 10 + firmware/targets/f2/api-hal/api-hal-vcp.c | 92 ++++++++++ firmware/targets/f2/target.mk | 2 +- firmware/targets/f3/Inc/usbd_cdc_if.h | 4 +- firmware/targets/f3/Src/main.c | 1 + firmware/targets/f3/Src/usbd_cdc_if.c | 100 ++++------ firmware/targets/f3/api-hal/api-hal-uuid.c | 10 + firmware/targets/f3/api-hal/api-hal-vcp.c | 92 ++++++++++ firmware/targets/f3/api-hal/api-hal.h | 7 - firmware/targets/f3/f3.ioc | 4 +- firmware/targets/f3/target.mk | 2 +- lib/mlib | 2 +- make/rules.mk | 2 +- 30 files changed, 837 insertions(+), 242 deletions(-) create mode 100644 applications/cli/cli.c create mode 100644 applications/cli/cli.h create mode 100644 applications/cli/cli_commands.c create mode 100644 applications/cli/cli_commands.h create mode 100644 applications/cli/cli_i.h create mode 100644 core/ring.c create mode 100644 core/ring.h delete mode 100644 debug/stm32wbx.cfg create mode 100644 firmware/targets/Inc/api-hal-uid.h create mode 100644 firmware/targets/Inc/api-hal-vcp.h rename firmware/targets/{f2/api-hal => Inc}/api-hal.h (65%) create mode 100644 firmware/targets/f2/api-hal/api-hal-uid.c create mode 100644 firmware/targets/f2/api-hal/api-hal-vcp.c create mode 100644 firmware/targets/f3/api-hal/api-hal-uuid.c create mode 100644 firmware/targets/f3/api-hal/api-hal-vcp.c delete mode 100644 firmware/targets/f3/api-hal/api-hal.h diff --git a/applications/applications.h b/applications/applications.h index 8c9018e0..1af3f23f 100644 --- a/applications/applications.h +++ b/applications/applications.h @@ -35,12 +35,17 @@ void power_task(void* p); void sd_card_test(void* p); void application_vibro(void* p); void app_gpio_test(void* p); +void cli_task(void* p); const FlipperStartupApp FLIPPER_STARTUP[] = { #ifdef APP_DISPLAY {.app = display_u8g2, .name = "display_u8g2", .libs = {0}}, #endif +#ifdef APP_CLI + {.app = cli_task, .name = "cli_task", .libs = {0}}, +#endif + #ifdef APP_EXAMPLE_BLINK {.app = application_blink, .name = "blink", .libs = {1, FURI_LIB{"input_task"}}}, #endif @@ -68,7 +73,7 @@ const FlipperStartupApp FLIPPER_STARTUP[] = { #endif #ifdef APP_POWER - {.app = power_task, .name = "power_task", .libs = {1, FURI_LIB{"gui_task"}}}, + {.app = power_task, .name = "power_task", .libs = {2, FURI_LIB{"cli_task", "gui_task"}}}, #endif #ifdef APP_CC1101 diff --git a/applications/applications.mk b/applications/applications.mk index a4d99c1b..3aae9d92 100644 --- a/applications/applications.mk +++ b/applications/applications.mk @@ -11,6 +11,7 @@ ifeq ($(APP_RELEASE), 1) APP_MENU = 1 APP_NFC = 1 APP_POWER = 1 +APP_CLI = 1 BUILD_IRDA = 1 APP_IRUKAGOTCHI = 1 BUILD_EXAMPLE_BLINK = 1 @@ -45,6 +46,13 @@ CFLAGS += -DAPP_POWER C_SOURCES += $(wildcard $(APP_DIR)/power/*.c) endif +APP_CLI ?= 0 +ifeq ($(APP_CLI), 1) +APP_GUI = 1 +CFLAGS += -DAPP_CLI +C_SOURCES += $(wildcard $(APP_DIR)/cli/*.c) +endif + APP_MENU ?= 0 ifeq ($(APP_MENU), 1) CFLAGS += -DAPP_MENU diff --git a/applications/cli/cli.c b/applications/cli/cli.c new file mode 100644 index 00000000..f6fdfb1d --- /dev/null +++ b/applications/cli/cli.c @@ -0,0 +1,173 @@ +#include "cli_i.h" +#include "cli_commands.h" + +#include + +Cli* cli_alloc() { + Cli* cli = furi_alloc(sizeof(Cli)); + CliCommandDict_init(cli->commands); + + cli->mutex = osMutexNew(NULL); + furi_check(cli->mutex); + + cli_reset_state(cli); + + return cli; +} + +void cli_free(Cli* cli) { + free(cli); +} + +void cli_reset_state(Cli* cli) { + string_clear(cli->line); + string_init(cli->line); +} + +void cli_putc(char c) { + api_hal_vcp_tx((uint8_t*)&c, 1); +} + +void cli_print(const char* str) { + api_hal_vcp_tx((uint8_t*)str, strlen(str)); +} + +void cli_print_version() { + cli_print("Build date:" BUILD_DATE ". " + "Git Commit:" GIT_COMMIT ". " + "Git Branch:" GIT_BRANCH ". " + "Commit Number:" GIT_BRANCH_NUM "."); +} + +void cli_motd() { + cli_print("Flipper cli.\r\n"); + cli_print_version(); +} + +void cli_nl() { + cli_print("\r\n"); +} + +void cli_prompt() { + cli_print("\r\n>: "); +} + +void cli_backspace(Cli* cli) { + size_t s = string_size(cli->line); + if(s > 0) { + s--; + string_left(cli->line, s); + cli_putc(CliSymbolAsciiBackspace); + cli_putc(CliSymbolAsciiSpace); + cli_putc(CliSymbolAsciiBackspace); + } else { + cli_putc(CliSymbolAsciiBell); + } +} + +void cli_enter(Cli* cli) { + // Normalize input + string_strim(cli->line); + if(string_size(cli->line) == 0) { + cli_prompt(); + return; + } + + // Get first word as command name + string_t command; + string_init(command); + size_t ws = string_search_char(cli->line, ' '); + if(ws == STRING_FAILURE) { + string_set(command, cli->line); + string_clear(cli->line); + string_init(cli->line); + } else { + string_set_n(command, cli->line, 0, ws); + string_right(cli->line, ws); + string_strim(cli->line); + } + + // Search for command + furi_check(osMutexAcquire(cli->mutex, osWaitForever) == osOK); + CliCommand* cli_command = CliCommandDict_get(cli->commands, command); + furi_check(osMutexRelease(cli->mutex) == osOK); + if(cli_command) { + cli_nl(); + cli_command->callback(cli->line, cli_command->context); + cli_prompt(); + } else { + cli_nl(); + cli_print("Command not found: "); + cli_print(string_get_cstr(command)); + cli_prompt(); + cli_putc(CliSymbolAsciiBell); + } + + // Always finish with clean state + cli_reset_state(cli); +} + +void cli_process_input(Cli* cli) { + char c; + size_t r; + + r = api_hal_vcp_rx((uint8_t*)&c, 1); + if(r == 0) { + cli_reset_state(cli); + } + + if(c == CliSymbolAsciiTab) { + cli_putc(CliSymbolAsciiBell); + } else if(c == CliSymbolAsciiSOH) { + cli_motd(); + cli_prompt(); + } else if(c == CliSymbolAsciiEOT) { + cli_reset_state(cli); + } else if(c == CliSymbolAsciiEsc) { + r = api_hal_vcp_rx((uint8_t*)&c, 1); + if(r && c == '[') { + api_hal_vcp_rx((uint8_t*)&c, 1); + } else { + cli_putc(CliSymbolAsciiBell); + } + } else if(c == CliSymbolAsciiBackspace || c == CliSymbolAsciiDel) { + cli_backspace(cli); + } else if(c == CliSymbolAsciiCR) { + cli_enter(cli); + } else if(c >= 0x20 && c < 0x7F) { + string_push_back(cli->line, c); + cli_putc(c); + } else { + cli_putc(CliSymbolAsciiBell); + } +} + +void cli_add_command(Cli* cli, const char* name, CliCallback callback, void* context) { + string_t name_str; + string_init_set_str(name_str, name); + CliCommand c; + c.callback = callback; + c.context = context; + + furi_check(osMutexAcquire(cli->mutex, osWaitForever) == osOK); + CliCommandDict_set_at(cli->commands, name_str, c); + furi_check(osMutexRelease(cli->mutex) == osOK); +} + +void cli_task(void* p) { + Cli* cli = cli_alloc(); + + // Init basic cli commands + cli_commands_init(cli); + + if(!furi_create("cli", cli)) { + printf("[cli_task] cannot create the cli record\n"); + furiac_exit(NULL); + } + + furiac_ready(); + + while(1) { + cli_process_input(cli); + } +} diff --git a/applications/cli/cli.h b/applications/cli/cli.h new file mode 100644 index 00000000..a7aa742a --- /dev/null +++ b/applications/cli/cli.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +/* Cli type + * Anonymous structure. Use cli_i.h if you need to go deeper. + */ +typedef struct Cli Cli; + +/* Cli callback function pointer. + * Implement this interface and use add_cli_command + * @param args - string with what was passed after command + * @param context - pointer to whatever you gave us on cli_add_command + */ +typedef void (*CliCallback)(string_t args, void* context); + +/* Add cli command + * Registers you command callback + * @param cli - pointer to cli instance + * @param name - command name + * @param callback - callback function + * @param context - pointer to whatever we need to pass to callback + */ +void cli_add_command(Cli* cli, const char* name, CliCallback callback, void* context); + +/* Read terminal input. + * Do it only from inside of callback. + * @param buffer - buffer pointer to char buffer + * @param size - size of buffer in bytes + */ +void cli_read(char* buffer, size_t size); + +/* Print to terminal + * Do it only from inside of callback. + * @param buffer - pointer to null terminated string to print. + */ +void cli_print(const char* buffer); + +/* New line + * Send new ine sequence + */ +void cli_nl(); diff --git a/applications/cli/cli_commands.c b/applications/cli/cli_commands.c new file mode 100644 index 00000000..46c08723 --- /dev/null +++ b/applications/cli/cli_commands.c @@ -0,0 +1,54 @@ +#include "cli_commands.h" +#include + +void cli_command_help(string_t args, void* context) { + (void)args; + Cli* cli = context; + cli_print("Commands we have:"); + + furi_check(osMutexAcquire(cli->mutex, osWaitForever) == osOK); + CliCommandDict_it_t it; + for(CliCommandDict_it(it, cli->commands); !CliCommandDict_end_p(it); CliCommandDict_next(it)) { + CliCommandDict_itref_t* ref = CliCommandDict_ref(it); + cli_print(" "); + cli_print(string_get_cstr(ref->key)); + }; + furi_check(osMutexRelease(cli->mutex) == osOK); + + if(string_size(args) > 0) { + cli_nl(); + cli_print("Also I have no clue what '"); + cli_print(string_get_cstr(args)); + cli_print("' is."); + } +} + +void cli_command_version(string_t args, void* context) { + (void)args; + (void)context; + cli_print_version(); +} + +void cli_command_uuid(string_t args, void* context) { + (void)args; + (void)context; + size_t uid_size = api_hal_uid_size(); + const uint8_t* uid = api_hal_uid(); + + string_t byte_str; + string_init(byte_str); + string_cat_printf(byte_str, "UID:"); + for(size_t i = 0; i < uid_size; i++) { + uint8_t uid_byte = uid[i]; + string_cat_printf(byte_str, "%02X", uid_byte); + } + cli_print(string_get_cstr(byte_str)); +} + +void cli_commands_init(Cli* cli) { + cli_add_command(cli, "help", cli_command_help, cli); + cli_add_command(cli, "?", cli_command_help, cli); + cli_add_command(cli, "version", cli_command_version, cli); + cli_add_command(cli, "!", cli_command_version, cli); + cli_add_command(cli, "uid", cli_command_uuid, cli); +} \ No newline at end of file diff --git a/applications/cli/cli_commands.h b/applications/cli/cli_commands.h new file mode 100644 index 00000000..184eeb37 --- /dev/null +++ b/applications/cli/cli_commands.h @@ -0,0 +1,5 @@ +#pragma once + +#include "cli_i.h" + +void cli_commands_init(Cli* cli); diff --git a/applications/cli/cli_i.h b/applications/cli/cli_i.h new file mode 100644 index 00000000..e8a8363d --- /dev/null +++ b/applications/cli/cli_i.h @@ -0,0 +1,42 @@ +#pragma once + +#include "cli.h" + +#include +#include + +#include + +#define CLI_LINE_SIZE_MAX + +typedef struct { + CliCallback callback; + void* context; +} CliCommand; + +DICT_DEF2(CliCommandDict, string_t, STRING_OPLIST, CliCommand, M_POD_OPLIST) + +typedef enum { + CliSymbolAsciiSOH = 0x01, + CliSymbolAsciiEOT = 0x04, + CliSymbolAsciiBell = 0x07, + CliSymbolAsciiBackspace = 0x08, + CliSymbolAsciiTab = 0x09, + CliSymbolAsciiCR = 0x0D, + CliSymbolAsciiEsc = 0x1B, + CliSymbolAsciiUS = 0x1F, + CliSymbolAsciiSpace = 0x20, + CliSymbolAsciiDel = 0x7F, +} CliSymbols; + +struct Cli { + CliCommandDict_t commands; + osMutexId_t mutex; + string_t line; +}; + +Cli* cli_alloc(); +void cli_free(Cli* cli); +void cli_reset_state(Cli* cli); +void cli_print_version(); +void cli_putc(char c); diff --git a/applications/power/power.c b/applications/power/power.c index 3bbd5142..c8c0d5d6 100644 --- a/applications/power/power.c +++ b/applications/power/power.c @@ -8,6 +8,7 @@ #include #include #include +#include struct Power { Icon* usb_icon; @@ -17,6 +18,7 @@ struct Power { Widget* battery_widget; ValueMutex* menu_vm; + Cli* cli; MenuItem* menu; uint8_t charge; @@ -54,6 +56,9 @@ Power* power_alloc() { power->menu_vm = furi_open("menu"); furi_check(power->menu_vm); + power->cli = furi_open("cli"); + furi_check(power->cli); + power->menu = menu_item_alloc_menu("Power", NULL); menu_item_subitem_add( power->menu, menu_item_alloc_function("Poweroff", NULL, power_off_callback, power)); @@ -82,10 +87,18 @@ void power_free(Power* power) { free(power); } +void power_cli_poweroff(string_t args, void* context) { + cli_print("Poweroff in 5 seconds"); + osDelay(5000); + api_hal_power_off(); +} + void power_task(void* p) { (void)p; Power* power = power_alloc(); + cli_add_command(power->cli, "poweroff", power_cli_poweroff, power); + FuriRecordSubscriber* gui_record = furi_open_deprecated("gui", false, false, NULL, NULL, NULL); assert(gui_record); GuiApi* gui = furi_take(gui_record); diff --git a/core/ring.c b/core/ring.c new file mode 100644 index 00000000..e8fdd767 --- /dev/null +++ b/core/ring.c @@ -0,0 +1,138 @@ +#include "ring.h" +#include + +struct Ring { + uint8_t* data; + size_t size; + volatile size_t read_ptr; + volatile size_t write_ptr; +}; + +Ring* ring_alloc(size_t size) { + Ring* ring = furi_alloc(sizeof(Ring)); + ring->size = size + 1; + ring->data = furi_alloc(ring->size); + ring_clear(ring); + return ring; +} + +void ring_free(Ring* ring) { + furi_assert(ring); + free(ring->data); + free(ring); +} + +size_t ring_size(Ring* ring) { + furi_assert(ring); + return ring->size - 1; +} + +inline static size_t ring_read_calculate(Ring* ring, size_t r, size_t w) { + if(w >= r) { + return w - r; + } else { + return ring->size - (r - w); + } +} + +size_t ring_read_space(Ring* ring) { + furi_assert(ring); + + const size_t r = ring->read_ptr; + const size_t w = ring->write_ptr; + + return ring_read_calculate(ring, r, w); +} + +inline static size_t ring_write_calculate(Ring* ring, size_t r, size_t w) { + if(r > w) { + return r - w - 1; + } else { + return ring->size - (r - w); + } +} + +size_t ring_write_space(Ring* ring) { + furi_assert(ring); + + const size_t r = ring->read_ptr; + const size_t w = ring->write_ptr; + + return ring_write_calculate(ring, r, w); +} + +size_t ring_push(Ring* ring, const uint8_t* data, size_t size) { + furi_assert(ring); + furi_assert(data); + + const size_t r = ring->read_ptr; + size_t w = ring->write_ptr; + const size_t write_space = ring_write_calculate(ring, r, w); + + if(write_space == 0) return 0; + + const size_t to_write = size > write_space ? write_space : size; + size_t end, first, second; + + end = w + to_write; + if(end > ring->size) { + first = ring->size - w; + second = end % ring->size; + } else { + first = to_write; + second = 0; + } + + memcpy(ring->data + w, data, first); + w = (w + first) % ring->size; + + if(second) { + memcpy(ring->data + w, data + first, second); + w = (w + second) % ring->size; + } + + ring->write_ptr = w; + + return to_write; +} + +size_t ring_pull(Ring* ring, uint8_t* data, size_t size) { + furi_assert(ring); + furi_assert(data); + + size_t r = ring->read_ptr; + const size_t w = ring->write_ptr; + const size_t read_space = ring_read_calculate(ring, r, w); + + if(read_space == 0) return 0; + + size_t to_read = size > read_space ? read_space : size; + size_t end, first, second; + + end = r + to_read; + if(end > ring->size) { + first = ring->size - r; + second = end % ring->size; + } else { + first = to_read; + second = 0; + } + + memcpy(data, ring->data + r, first); + r = (r + first) % ring->size; + + if(second) { + memcpy(data + first, ring->data + r, second); + r = (r + second) % ring->size; + } + + ring->read_ptr = r; + + return to_read; +} + +void ring_clear(Ring* ring) { + furi_assert(ring); + ring->read_ptr = 0; + ring->write_ptr = 0; +} diff --git a/core/ring.h b/core/ring.h new file mode 100644 index 00000000..069a81cd --- /dev/null +++ b/core/ring.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +typedef struct Ring Ring; + +Ring* ring_alloc(size_t size); + +void ring_free(Ring* ring); + +size_t ring_size(Ring* ring); + +size_t ring_read_space(Ring* ring); + +size_t ring_write_space(Ring* ring); + +size_t ring_push(Ring* ring, const uint8_t* data, size_t size); + +size_t ring_pull(Ring* ring, uint8_t* data, size_t size); + +void ring_clear(Ring* ring); diff --git a/debug/stm32wbx.cfg b/debug/stm32wbx.cfg deleted file mode 100644 index ccb70d17..00000000 --- a/debug/stm32wbx.cfg +++ /dev/null @@ -1,105 +0,0 @@ -# script for stm32wbx family - -gdb_port 4242 - -# -# stm32wb devices support both JTAG and SWD transports. -# -source [find target/swj-dp.tcl] -source [find mem_helper.tcl] - -if { [info exists CHIPNAME] } { - set _CHIPNAME $CHIPNAME -} else { - set _CHIPNAME stm32wbx -} - -set _ENDIAN little - -# Work-area is a space in RAM used for flash programming -# By default use 64kB -if { [info exists WORKAREASIZE] } { - set _WORKAREASIZE $WORKAREASIZE -} else { - set _WORKAREASIZE 0x10000 -} - -#jtag scan chain -if { [info exists CPUTAPID] } { - set _CPUTAPID $CPUTAPID -} else { - if { [using_jtag] } { - set _CPUTAPID 0x6ba00477 - } else { - # SWD IDCODE (single drop, arm) - set _CPUTAPID 0x6ba02477 - } -} - -swj_newdap $_CHIPNAME cpu -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id $_CPUTAPID -dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu - -if {[using_jtag]} { - jtag newtap $_CHIPNAME bs -irlen 5 -} - -set _TARGETNAME $_CHIPNAME.cpu -target create $_TARGETNAME cortex_m -endian $_ENDIAN -dap $_CHIPNAME.dap - -$_TARGETNAME configure -work-area-phys 0x20000000 -work-area-size $_WORKAREASIZE -work-area-backup 0 - -set _FLASHNAME $_CHIPNAME.flash -flash bank $_FLASHNAME stm32l4x 0 0 0 0 $_TARGETNAME - -# Common knowledges tells JTAG speed should be <= F_CPU/6. -# F_CPU after reset is MSI 4MHz, so use F_JTAG = 500 kHz to stay on -# the safe side. -# -# Note that there is a pretty wide band where things are -# more or less stable, see http://openocd.zylin.com/#/c/3366/ -adapter speed 500 - -adapter srst delay 100 -if {[using_jtag]} { - jtag_ntrst_delay 100 -} - -reset_config srst_nogate - -if {![using_hla]} { - # if srst is not fitted use SYSRESETREQ to - # perform a soft reset - cortex_m reset_config sysresetreq -} - -$_TARGETNAME configure -event reset-init { - # CPU comes out of reset with MSI_ON | MSI_RDY | MSI Range 4 MHz. - # Configure system to use MSI 24 MHz clock, compliant with VOS default Range1. - # 2 WS compliant with VOS=Range1 and 24 MHz. - mmw 0x58004000 0x00000102 0 ;# FLASH_ACR |= PRFTBE | 2(Latency) - mmw 0x58000000 0x00000091 0 ;# RCC_CR = MSI_ON | MSI Range 24 MHz - # Boost JTAG frequency - adapter speed 4000 -} - -$_TARGETNAME configure -event reset-start { - # Reset clock is MSI (4 MHz) - adapter speed 500 -} - -$_TARGETNAME configure -event examine-end { - # Enable debug during low power modes (uses more power) - # DBGMCU_CR |= DBG_STANDBY | DBG_STOP | DBG_SLEEP - mmw 0xE0042004 0x00000007 0 - - # Stop watchdog counters during halt - # DBGMCU_APB1_FZR1 |= DBG_IWDG_STOP | DBG_WWDG_STOP - mmw 0xE004203C 0x00001800 0 -} - -$_TARGETNAME configure -event trace-config { - # Set TRACE_IOEN; TRACE_MODE is set to async; when using sync - # change this value accordingly to configure trace pins - # assignment - mmw 0xE0042004 0x00000020 0 -} diff --git a/firmware/targets/Inc/api-hal-uid.h b/firmware/targets/Inc/api-hal-uid.h new file mode 100644 index 00000000..7d97aa1d --- /dev/null +++ b/firmware/targets/Inc/api-hal-uid.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +/* Get platform UID size in bytes */ +size_t api_hal_uid_size(); + +/* Get const pointer to UID */ +const uint8_t* api_hal_uid(); diff --git a/firmware/targets/Inc/api-hal-vcp.h b/firmware/targets/Inc/api-hal-vcp.h new file mode 100644 index 00000000..ac3a5dc4 --- /dev/null +++ b/firmware/targets/Inc/api-hal-vcp.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +/* Init VCP HAL + * Allocates ring buffer and initializes state + */ +void api_hal_vcp_init(); + +/* Recieve data from VCP + * Waits till some data arrives, never returns 0 + * @param buffer - pointer to buffer + * @param size - buffer size + * @return items copied in buffer, 0 if channel closed + */ +size_t api_hal_vcp_rx(uint8_t* buffer, size_t size); + +/* Transmit data to VCP + * @param buffer - pointer to buffer + * @param size - buffer size + */ +void api_hal_vcp_tx(uint8_t* buffer, size_t size); diff --git a/firmware/targets/f2/api-hal/api-hal.h b/firmware/targets/Inc/api-hal.h similarity index 65% rename from firmware/targets/f2/api-hal/api-hal.h rename to firmware/targets/Inc/api-hal.h index 556f617b..e91b05bd 100644 --- a/firmware/targets/f2/api-hal/api-hal.h +++ b/firmware/targets/Inc/api-hal.h @@ -5,3 +5,6 @@ #include "api-hal-pwm.h" #include "api-hal-task.h" #include "api-hal-tim.h" +#include "api-hal-power.h" +#include "api-hal-vcp.h" +#include "api-hal-uid.h" diff --git a/firmware/targets/f2/Inc/usbd_cdc_if.h b/firmware/targets/f2/Inc/usbd_cdc_if.h index 38a7059c..32c1a228 100644 --- a/firmware/targets/f2/Inc/usbd_cdc_if.h +++ b/firmware/targets/f2/Inc/usbd_cdc_if.h @@ -51,8 +51,8 @@ /* USER CODE BEGIN EXPORTED_DEFINES */ /* Define size for the receive and transmit buffer over CDC */ /* It's up to user to redefine and/or remove those define */ -#define APP_RX_DATA_SIZE 2048 -#define APP_TX_DATA_SIZE 2048 +#define APP_RX_DATA_SIZE CDC_DATA_HS_MAX_PACKET_SIZE +#define APP_TX_DATA_SIZE CDC_DATA_HS_MAX_PACKET_SIZE /* USER CODE END EXPORTED_DEFINES */ diff --git a/firmware/targets/f2/Src/main.c b/firmware/targets/f2/Src/main.c index bf0c980e..d5a5d06b 100644 --- a/firmware/targets/f2/Src/main.c +++ b/firmware/targets/f2/Src/main.c @@ -104,6 +104,7 @@ int main(void) /* USER CODE BEGIN 2 */ MX_FATFS_Init(); delay_us_init_DWT(); + api_hal_vcp_init(); /* USER CODE END 2 */ /* Init scheduler */ diff --git a/firmware/targets/f2/Src/usbd_cdc_if.c b/firmware/targets/f2/Src/usbd_cdc_if.c index 21b4d13f..9299f924 100644 --- a/firmware/targets/f2/Src/usbd_cdc_if.c +++ b/firmware/targets/f2/Src/usbd_cdc_if.c @@ -51,6 +51,12 @@ /* USER CODE BEGIN PRIVATE_TYPES */ +extern void _api_hal_vcp_init(); +extern void _api_hal_vcp_deinit(); +extern void _api_hal_vcp_control_line(uint8_t state); +extern void _api_hal_vcp_rx_callback(char* buffer, size_t size); +extern void _api_hal_vcp_tx_complete(size_t size); + /* USER CODE END PRIVATE_TYPES */ /** @@ -156,6 +162,7 @@ static int8_t CDC_Init_FS(void) /* Set Application Buffers */ USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0); USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); + _api_hal_vcp_init(); return (USBD_OK); /* USER CODE END 3 */ } @@ -167,6 +174,7 @@ static int8_t CDC_Init_FS(void) static int8_t CDC_DeInit_FS(void) { /* USER CODE BEGIN 4 */ + _api_hal_vcp_deinit(); return (USBD_OK); /* USER CODE END 4 */ } @@ -181,63 +189,34 @@ static int8_t CDC_DeInit_FS(void) static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) { /* USER CODE BEGIN 5 */ - switch(cmd) - { - case CDC_SEND_ENCAPSULATED_COMMAND: - - break; - - case CDC_GET_ENCAPSULATED_RESPONSE: - - break; - - case CDC_SET_COMM_FEATURE: - - break; - - case CDC_GET_COMM_FEATURE: - - break; - - case CDC_CLEAR_COMM_FEATURE: - - break; - - /*******************************************************************************/ - /* Line Coding Structure */ - /*-----------------------------------------------------------------------------*/ - /* Offset | Field | Size | Value | Description */ - /* 0 | dwDTERate | 4 | Number |Data terminal rate, in bits per second*/ - /* 4 | bCharFormat | 1 | Number | Stop bits */ - /* 0 - 1 Stop bit */ - /* 1 - 1.5 Stop bits */ - /* 2 - 2 Stop bits */ - /* 5 | bParityType | 1 | Number | Parity */ - /* 0 - None */ - /* 1 - Odd */ - /* 2 - Even */ - /* 3 - Mark */ - /* 4 - Space */ - /* 6 | bDataBits | 1 | Number Data bits (5, 6, 7, 8 or 16). */ - /*******************************************************************************/ - case CDC_SET_LINE_CODING: - - break; - - case CDC_GET_LINE_CODING: - - break; - - case CDC_SET_CONTROL_LINE_STATE: - - break; - - case CDC_SEND_BREAK: - - break; - - default: - break; + if (cmd == CDC_SEND_ENCAPSULATED_COMMAND) { + } else if (cmd == CDC_GET_ENCAPSULATED_RESPONSE) { + } else if (cmd == CDC_SET_COMM_FEATURE) { + } else if (cmd == CDC_GET_COMM_FEATURE) { + } else if (cmd == CDC_CLEAR_COMM_FEATURE) { + } else if (cmd == CDC_SET_LINE_CODING) { + /*******************************************************************************/ + /* Line Coding Structure */ + /*-----------------------------------------------------------------------------*/ + /* Offset | Field | Size | Value | Description */ + /* 0 | dwDTERate | 4 | Number |Data terminal rate, in bits per second*/ + /* 4 | bCharFormat | 1 | Number | Stop bits */ + /* 0 - 1 Stop bit */ + /* 1 - 1.5 Stop bits */ + /* 2 - 2 Stop bits */ + /* 5 | bParityType | 1 | Number | Parity */ + /* 0 - None */ + /* 1 - Odd */ + /* 2 - Even */ + /* 3 - Mark */ + /* 4 - Space */ + /* 6 | bDataBits | 1 | Number Data bits (5, 6, 7, 8 or 16). */ + /*******************************************************************************/ + } else if (cmd == CDC_GET_LINE_CODING) { + } else if (cmd == CDC_SET_CONTROL_LINE_STATE) { + _api_hal_vcp_control_line(((USBD_SetupReqTypedef*)pbuf)->wValue); + } else if (cmd == CDC_SEND_BREAK) { + } else { } return (USBD_OK); @@ -262,7 +241,7 @@ static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { /* USER CODE BEGIN 6 */ - USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); + _api_hal_vcp_rx_callback((char*)Buf, *Len); USBD_CDC_ReceivePacket(&hUsbDeviceFS); return (USBD_OK); /* USER CODE END 6 */ @@ -287,7 +266,8 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) if (hcdc->TxState != 0){ return USBD_BUSY; } - USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); + memcpy(UserTxBufferFS, Buf, Len); + USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, Len); result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); /* USER CODE END 7 */ return result; @@ -310,8 +290,8 @@ static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum) uint8_t result = USBD_OK; /* USER CODE BEGIN 13 */ UNUSED(Buf); - UNUSED(Len); UNUSED(epnum); + _api_hal_vcp_tx_complete(*Len); /* USER CODE END 13 */ return result; } diff --git a/firmware/targets/f2/api-hal/api-hal-uid.c b/firmware/targets/f2/api-hal/api-hal-uid.c new file mode 100644 index 00000000..2849a8b4 --- /dev/null +++ b/firmware/targets/f2/api-hal/api-hal-uid.c @@ -0,0 +1,10 @@ +#include +#include + +size_t api_hal_uid_size() { + return 96/8; +} + +const uint8_t* api_hal_uid() { + return (const uint8_t *)UID_BASE; +} diff --git a/firmware/targets/f2/api-hal/api-hal-vcp.c b/firmware/targets/f2/api-hal/api-hal-vcp.c new file mode 100644 index 00000000..bf98d7ef --- /dev/null +++ b/firmware/targets/f2/api-hal/api-hal-vcp.c @@ -0,0 +1,92 @@ +#include +#include +#include +#include + +#define API_HAL_VCP_RX_BUFFER_SIZE 600 + +typedef struct { + StreamBufferHandle_t rx_stream; + osSemaphoreId_t tx_semaphore; + volatile bool alive; + volatile bool underrun; +} ApiHalVcp; + +ApiHalVcp api_hal_vcp; + +static const uint8_t ascii_soh = 0x01; +static const uint8_t ascii_eot = 0x04; + +void _api_hal_vcp_init(); +void _api_hal_vcp_deinit(); +void _api_hal_vcp_control_line(uint8_t state); +void _api_hal_vcp_rx_callback(const uint8_t* buffer, size_t size); +void _api_hal_vcp_tx_complete(size_t size); + +void api_hal_vcp_init() { + api_hal_vcp.rx_stream = xStreamBufferCreate(API_HAL_VCP_RX_BUFFER_SIZE, 1); + api_hal_vcp.tx_semaphore = osSemaphoreNew(1, 1, NULL); + api_hal_vcp.alive = false; + api_hal_vcp.underrun = false; +} + +void _api_hal_vcp_init() { + osSemaphoreRelease(api_hal_vcp.tx_semaphore); +} + +void _api_hal_vcp_deinit() { + api_hal_vcp.alive = false; + osSemaphoreRelease(api_hal_vcp.tx_semaphore); +} + +void _api_hal_vcp_control_line(uint8_t state) { + // bit 0: DTR state, bit 1: RTS state + // bool dtr = state & 0b01; + bool rts = state & 0b10; + + if (rts) { + api_hal_vcp.alive = true; + _api_hal_vcp_rx_callback(&ascii_soh, 1); // SOH + } else { + api_hal_vcp.alive = false; + _api_hal_vcp_rx_callback(&ascii_eot, 1); // EOT + } + + osSemaphoreRelease(api_hal_vcp.tx_semaphore); +} + +void _api_hal_vcp_rx_callback(const uint8_t* buffer, size_t size) { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + size_t ret = xStreamBufferSendFromISR(api_hal_vcp.rx_stream, buffer, size, &xHigherPriorityTaskWoken); + if (ret != size) { + api_hal_vcp.underrun = true; + } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); +} + +void _api_hal_vcp_tx_complete(size_t size) { + osSemaphoreRelease(api_hal_vcp.tx_semaphore); +} + +size_t api_hal_vcp_rx(uint8_t* buffer, size_t size) { + return xStreamBufferReceive(api_hal_vcp.rx_stream, buffer, size, portMAX_DELAY); +} + +void api_hal_vcp_tx(uint8_t* buffer, size_t size) { + while (size > 0 && api_hal_vcp.alive) { + furi_check(osSemaphoreAcquire(api_hal_vcp.tx_semaphore, osWaitForever) == osOK); + + size_t batch_size = size; + if (batch_size > APP_TX_DATA_SIZE) { + batch_size = APP_TX_DATA_SIZE; + } + + if (CDC_Transmit_FS(buffer, batch_size) == USBD_OK) { + size -= batch_size; + buffer += batch_size; + } else { + // Shouldn't be there + osDelay(100); + } + } +} diff --git a/firmware/targets/f2/target.mk b/firmware/targets/f2/target.mk index 41b82d21..c7a63b38 100644 --- a/firmware/targets/f2/target.mk +++ b/firmware/targets/f2/target.mk @@ -1,6 +1,6 @@ TOOLCHAIN = arm -DEBUG_AGENT = set -m; st-util -n --semihosting +DEBUG_AGENT = openocd -f interface/stlink-v2.cfg -c "transport select hla_swd" -f target/stm32l4x.cfg -c "init" BOOT_ADDRESS = 0x08000000 FW_ADDRESS = 0x08008000 diff --git a/firmware/targets/f3/Inc/usbd_cdc_if.h b/firmware/targets/f3/Inc/usbd_cdc_if.h index 1bd4eb8e..40223874 100644 --- a/firmware/targets/f3/Inc/usbd_cdc_if.h +++ b/firmware/targets/f3/Inc/usbd_cdc_if.h @@ -51,8 +51,8 @@ /* USER CODE BEGIN EXPORTED_DEFINES */ /* Define size for the receive and transmit buffer over CDC */ /* It's up to user to redefine and/or remove those define */ -#define APP_RX_DATA_SIZE 2048 -#define APP_TX_DATA_SIZE 2048 +#define APP_RX_DATA_SIZE CDC_DATA_HS_MAX_PACKET_SIZE +#define APP_TX_DATA_SIZE CDC_DATA_HS_MAX_PACKET_SIZE /* USER CODE END EXPORTED_DEFINES */ diff --git a/firmware/targets/f3/Src/main.c b/firmware/targets/f3/Src/main.c index 43f82d5a..c089ee16 100644 --- a/firmware/targets/f3/Src/main.c +++ b/firmware/targets/f3/Src/main.c @@ -111,6 +111,7 @@ int main(void) /* USER CODE BEGIN 2 */ MX_FATFS_Init(); delay_us_init_DWT(); + api_hal_vcp_init(); /* USER CODE END 2 */ /* Init scheduler */ diff --git a/firmware/targets/f3/Src/usbd_cdc_if.c b/firmware/targets/f3/Src/usbd_cdc_if.c index 2141aec8..cd6946c4 100644 --- a/firmware/targets/f3/Src/usbd_cdc_if.c +++ b/firmware/targets/f3/Src/usbd_cdc_if.c @@ -51,6 +51,12 @@ /* USER CODE BEGIN PRIVATE_TYPES */ +extern void _api_hal_vcp_init(); +extern void _api_hal_vcp_deinit(); +extern void _api_hal_vcp_control_line(uint8_t state); +extern void _api_hal_vcp_rx_callback(char* buffer, size_t size); +extern void _api_hal_vcp_tx_complete(size_t size); + /* USER CODE END PRIVATE_TYPES */ /** @@ -156,6 +162,7 @@ static int8_t CDC_Init_FS(void) /* Set Application Buffers */ USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0); USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); + _api_hal_vcp_init(); return (USBD_OK); /* USER CODE END 3 */ } @@ -167,6 +174,7 @@ static int8_t CDC_Init_FS(void) static int8_t CDC_DeInit_FS(void) { /* USER CODE BEGIN 4 */ + _api_hal_vcp_deinit(); return (USBD_OK); /* USER CODE END 4 */ } @@ -181,63 +189,34 @@ static int8_t CDC_DeInit_FS(void) static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) { /* USER CODE BEGIN 5 */ - switch(cmd) - { - case CDC_SEND_ENCAPSULATED_COMMAND: - - break; - - case CDC_GET_ENCAPSULATED_RESPONSE: - - break; - - case CDC_SET_COMM_FEATURE: - - break; - - case CDC_GET_COMM_FEATURE: - - break; - - case CDC_CLEAR_COMM_FEATURE: - - break; - - /*******************************************************************************/ - /* Line Coding Structure */ - /*-----------------------------------------------------------------------------*/ - /* Offset | Field | Size | Value | Description */ - /* 0 | dwDTERate | 4 | Number |Data terminal rate, in bits per second*/ - /* 4 | bCharFormat | 1 | Number | Stop bits */ - /* 0 - 1 Stop bit */ - /* 1 - 1.5 Stop bits */ - /* 2 - 2 Stop bits */ - /* 5 | bParityType | 1 | Number | Parity */ - /* 0 - None */ - /* 1 - Odd */ - /* 2 - Even */ - /* 3 - Mark */ - /* 4 - Space */ - /* 6 | bDataBits | 1 | Number Data bits (5, 6, 7, 8 or 16). */ - /*******************************************************************************/ - case CDC_SET_LINE_CODING: - - break; - - case CDC_GET_LINE_CODING: - - break; - - case CDC_SET_CONTROL_LINE_STATE: - - break; - - case CDC_SEND_BREAK: - - break; - - default: - break; + if (cmd == CDC_SEND_ENCAPSULATED_COMMAND) { + } else if (cmd == CDC_GET_ENCAPSULATED_RESPONSE) { + } else if (cmd == CDC_SET_COMM_FEATURE) { + } else if (cmd == CDC_GET_COMM_FEATURE) { + } else if (cmd == CDC_CLEAR_COMM_FEATURE) { + } else if (cmd == CDC_SET_LINE_CODING) { + /*******************************************************************************/ + /* Line Coding Structure */ + /*-----------------------------------------------------------------------------*/ + /* Offset | Field | Size | Value | Description */ + /* 0 | dwDTERate | 4 | Number |Data terminal rate, in bits per second*/ + /* 4 | bCharFormat | 1 | Number | Stop bits */ + /* 0 - 1 Stop bit */ + /* 1 - 1.5 Stop bits */ + /* 2 - 2 Stop bits */ + /* 5 | bParityType | 1 | Number | Parity */ + /* 0 - None */ + /* 1 - Odd */ + /* 2 - Even */ + /* 3 - Mark */ + /* 4 - Space */ + /* 6 | bDataBits | 1 | Number Data bits (5, 6, 7, 8 or 16). */ + /*******************************************************************************/ + } else if (cmd == CDC_GET_LINE_CODING) { + } else if (cmd == CDC_SET_CONTROL_LINE_STATE) { + _api_hal_vcp_control_line(((USBD_SetupReqTypedef*)pbuf)->wValue); + } else if (cmd == CDC_SEND_BREAK) { + } else { } return (USBD_OK); @@ -262,7 +241,7 @@ static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { /* USER CODE BEGIN 6 */ - USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); + _api_hal_vcp_rx_callback((char*)Buf, *Len); USBD_CDC_ReceivePacket(&hUsbDeviceFS); return (USBD_OK); /* USER CODE END 6 */ @@ -287,7 +266,8 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) if (hcdc->TxState != 0){ return USBD_BUSY; } - USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); + memcpy(UserTxBufferFS, Buf, Len); + USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, Len); result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); /* USER CODE END 7 */ return result; @@ -310,8 +290,8 @@ static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum) uint8_t result = USBD_OK; /* USER CODE BEGIN 13 */ UNUSED(Buf); - UNUSED(Len); UNUSED(epnum); + _api_hal_vcp_tx_complete(*Len); /* USER CODE END 13 */ return result; } diff --git a/firmware/targets/f3/api-hal/api-hal-uuid.c b/firmware/targets/f3/api-hal/api-hal-uuid.c new file mode 100644 index 00000000..7ef256f1 --- /dev/null +++ b/firmware/targets/f3/api-hal/api-hal-uuid.c @@ -0,0 +1,10 @@ +#include +#include + +size_t api_hal_uid_size() { + return 64/8; +} + +const uint8_t* api_hal_uid() { + return (const uint8_t *)UID64_BASE; +} diff --git a/firmware/targets/f3/api-hal/api-hal-vcp.c b/firmware/targets/f3/api-hal/api-hal-vcp.c new file mode 100644 index 00000000..bf98d7ef --- /dev/null +++ b/firmware/targets/f3/api-hal/api-hal-vcp.c @@ -0,0 +1,92 @@ +#include +#include +#include +#include + +#define API_HAL_VCP_RX_BUFFER_SIZE 600 + +typedef struct { + StreamBufferHandle_t rx_stream; + osSemaphoreId_t tx_semaphore; + volatile bool alive; + volatile bool underrun; +} ApiHalVcp; + +ApiHalVcp api_hal_vcp; + +static const uint8_t ascii_soh = 0x01; +static const uint8_t ascii_eot = 0x04; + +void _api_hal_vcp_init(); +void _api_hal_vcp_deinit(); +void _api_hal_vcp_control_line(uint8_t state); +void _api_hal_vcp_rx_callback(const uint8_t* buffer, size_t size); +void _api_hal_vcp_tx_complete(size_t size); + +void api_hal_vcp_init() { + api_hal_vcp.rx_stream = xStreamBufferCreate(API_HAL_VCP_RX_BUFFER_SIZE, 1); + api_hal_vcp.tx_semaphore = osSemaphoreNew(1, 1, NULL); + api_hal_vcp.alive = false; + api_hal_vcp.underrun = false; +} + +void _api_hal_vcp_init() { + osSemaphoreRelease(api_hal_vcp.tx_semaphore); +} + +void _api_hal_vcp_deinit() { + api_hal_vcp.alive = false; + osSemaphoreRelease(api_hal_vcp.tx_semaphore); +} + +void _api_hal_vcp_control_line(uint8_t state) { + // bit 0: DTR state, bit 1: RTS state + // bool dtr = state & 0b01; + bool rts = state & 0b10; + + if (rts) { + api_hal_vcp.alive = true; + _api_hal_vcp_rx_callback(&ascii_soh, 1); // SOH + } else { + api_hal_vcp.alive = false; + _api_hal_vcp_rx_callback(&ascii_eot, 1); // EOT + } + + osSemaphoreRelease(api_hal_vcp.tx_semaphore); +} + +void _api_hal_vcp_rx_callback(const uint8_t* buffer, size_t size) { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + size_t ret = xStreamBufferSendFromISR(api_hal_vcp.rx_stream, buffer, size, &xHigherPriorityTaskWoken); + if (ret != size) { + api_hal_vcp.underrun = true; + } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); +} + +void _api_hal_vcp_tx_complete(size_t size) { + osSemaphoreRelease(api_hal_vcp.tx_semaphore); +} + +size_t api_hal_vcp_rx(uint8_t* buffer, size_t size) { + return xStreamBufferReceive(api_hal_vcp.rx_stream, buffer, size, portMAX_DELAY); +} + +void api_hal_vcp_tx(uint8_t* buffer, size_t size) { + while (size > 0 && api_hal_vcp.alive) { + furi_check(osSemaphoreAcquire(api_hal_vcp.tx_semaphore, osWaitForever) == osOK); + + size_t batch_size = size; + if (batch_size > APP_TX_DATA_SIZE) { + batch_size = APP_TX_DATA_SIZE; + } + + if (CDC_Transmit_FS(buffer, batch_size) == USBD_OK) { + size -= batch_size; + buffer += batch_size; + } else { + // Shouldn't be there + osDelay(100); + } + } +} diff --git a/firmware/targets/f3/api-hal/api-hal.h b/firmware/targets/f3/api-hal/api-hal.h deleted file mode 100644 index 556f617b..00000000 --- a/firmware/targets/f3/api-hal/api-hal.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include "api-hal-gpio.h" -#include "api-hal-delay.h" -#include "api-hal-pwm.h" -#include "api-hal-task.h" -#include "api-hal-tim.h" diff --git a/firmware/targets/f3/f3.ioc b/firmware/targets/f3/f3.ioc index a68bc4de..96ae441e 100644 --- a/firmware/targets/f3/f3.ioc +++ b/firmware/targets/f3/f3.ioc @@ -270,6 +270,7 @@ Mcu.Pin12=PA2 PD0.GPIOParameters=GPIO_Speed,PinState,GPIO_Label Mcu.Pin10=PA0 SH.GPXTI10.ConfNb=1 +USB_DEVICE.APP_RX_DATA_SIZE=512 TIM2.AutoReloadPreload=TIM_AUTORELOAD_PRELOAD_ENABLE PC3.GPIO_Label=PC3 PA3.PinState=GPIO_PIN_SET @@ -453,7 +454,7 @@ OSC_IN.GPIOParameters=GPIO_Label PB12.Locked=true ProjectManager.DeletePrevious=true PB10.Locked=true -USB_DEVICE.IPParameters=VirtualMode,VirtualModeFS,CLASS_NAME_FS,MANUFACTURER_STRING,PRODUCT_STRING_CDC_FS +USB_DEVICE.IPParameters=VirtualMode,VirtualModeFS,CLASS_NAME_FS,MANUFACTURER_STRING,PRODUCT_STRING_CDC_FS,APP_RX_DATA_SIZE,APP_TX_DATA_SIZE TIM16.Channel=TIM_CHANNEL_1 RCC.AHB2CLKDivider=RCC_SYSCLK_DIV2 RCC.FamilyName=M @@ -528,6 +529,7 @@ PA13.Locked=true RF1.Mode=RF1_Activate PB7.Mode=Asynchronous NVIC.EXTI9_5_IRQn=true\:5\:0\:true\:false\:true\:false\:true\:true +USB_DEVICE.APP_TX_DATA_SIZE=512 PA14.Signal=SYS_JTCK-SWCLK PB2.GPIO_Label=PB2 PC6.GPIOParameters=GPIO_Label diff --git a/firmware/targets/f3/target.mk b/firmware/targets/f3/target.mk index bed22073..7eda0fa1 100644 --- a/firmware/targets/f3/target.mk +++ b/firmware/targets/f3/target.mk @@ -1,6 +1,6 @@ TOOLCHAIN = arm -DEBUG_AGENT = openocd -f interface/stlink-v2.cfg -c "transport select hla_swd" -f ../debug/stm32wbx.cfg -c "init" -c "reset halt" +DEBUG_AGENT = openocd -f interface/stlink.cfg -c "transport select hla_swd" -f target/stm32wbx.cfg -c "init" BOOT_ADDRESS = 0x08000000 FW_ADDRESS = 0x08008000 diff --git a/lib/mlib b/lib/mlib index eb7556f8..ebf44073 160000 --- a/lib/mlib +++ b/lib/mlib @@ -1 +1 @@ -Subproject commit eb7556f88faf0bbfd6a4ae99a3d53dcbe2064b88 +Subproject commit ebf440731d44f5eba926e0f648758676461bbba4 diff --git a/make/rules.mk b/make/rules.mk index f4d0c95e..57e87ce1 100644 --- a/make/rules.mk +++ b/make/rules.mk @@ -73,7 +73,7 @@ upload: $(OBJ_DIR)/upload debug: flash $(DEBUG_AGENT) & echo $$! > $(OBJ_DIR)/agent.PID arm-none-eabi-gdb \ - -ex "target extended-remote 127.0.0.1:4242" \ + -ex "target extended-remote 127.0.0.1:3333" \ -ex "set confirm off" \ -ex "source ../debug/FreeRTOS/FreeRTOS.py" \ $(OBJ_DIR)/$(PROJECT).elf; \ From ee1e4bbabd782cbf778d516009ff69370e51ed50 Mon Sep 17 00:00:00 2001 From: rusdacent <57439765+rusdacent@users.noreply.github.com> Date: Mon, 16 Nov 2020 17:51:48 +0300 Subject: [PATCH 3/6] Add option for checkout code --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe850e87..21d61278 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,8 @@ jobs: - name: Checkout code uses: actions/checkout@v2 with: - submodules: true + fetch-depth: 0 + submodules: true - uses: satackey/action-docker-layer-caching@v0.0.8 continue-on-error: true with: From 3d5563b169f6c752c06e06879b240a1ae7907f3b Mon Sep 17 00:00:00 2001 From: coreglitch Date: Mon, 16 Nov 2020 21:26:34 +0600 Subject: [PATCH 4/6] init nfc later (#239) --- applications/nfc/nfc.c | 28 ++++++++++++++++++++++++++-- firmware/targets/f2/Src/spi.c | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/applications/nfc/nfc.c b/applications/nfc/nfc.c index 719d3c37..26ae41bf 100644 --- a/applications/nfc/nfc.c +++ b/applications/nfc/nfc.c @@ -68,6 +68,12 @@ void nfc_test_callback(void* context) { nfc->screen = 0; widget_enabled_set(nfc->widget, true); + // TODO only for workaround + if(nfc->ret != ERR_NONE) { + nfc->ret = rfalNfcInitialize(); + rfalLowPowerModeStart(); + } + if(nfc->ret == ERR_NONE && !nfc->worker) { // TODO change to fuirac_start nfc->worker = osThreadNew(nfc_worker_task, nfc, &nfc->worker_attr); @@ -77,11 +83,29 @@ void nfc_test_callback(void* context) { } void nfc_field_on_callback(void* context) { + furi_assert(context); + Nfc* nfc = context; + + // TODO only for workaround + if(nfc->ret != ERR_NONE) { + nfc->ret = rfalNfcInitialize(); + rfalLowPowerModeStart(); + } + st25r3916OscOn(); st25r3916TxRxOn(); } void nfc_field_off_callback(void* context) { + furi_assert(context); + Nfc* nfc = context; + + // TODO only for workaround + if(nfc->ret != ERR_NONE) { + nfc->ret = rfalNfcInitialize(); + rfalLowPowerModeStart(); + } + st25r3916TxRxOff(); } @@ -158,8 +182,8 @@ void nfc_task(void* p) { furiac_exit(NULL); } - nfc->ret = rfalNfcInitialize(); - rfalLowPowerModeStart(); + // TODO only for workaround + nfc->ret = ERR_WRONG_STATE; furiac_ready(); diff --git a/firmware/targets/f2/Src/spi.c b/firmware/targets/f2/Src/spi.c index 7e767e11..72c50b01 100644 --- a/firmware/targets/f2/Src/spi.c +++ b/firmware/targets/f2/Src/spi.c @@ -120,7 +120,7 @@ void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle) */ GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; - GPIO_InitStruct.Pull = GPIO_PULLDOWN; + GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF6_SPI3; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); From 3d6af91dd1f266f34df9749fbc7e96a857aa1186 Mon Sep 17 00:00:00 2001 From: DrZlo13 Date: Mon, 16 Nov 2020 20:12:05 +0300 Subject: [PATCH 5/6] simple music player app (#240) * split power-cli dependence * music_player application * add player to release mode * fix stupid error --- applications/applications.h | 9 + applications/applications.mk | 12 + applications/music-player/music-player.c | 444 +++++++++++++++++++++++ applications/power/power.c | 5 +- 4 files changed, 468 insertions(+), 2 deletions(-) create mode 100644 applications/music-player/music-player.c diff --git a/applications/applications.h b/applications/applications.h index 1af3f23f..ff337ca5 100644 --- a/applications/applications.h +++ b/applications/applications.h @@ -36,6 +36,7 @@ void sd_card_test(void* p); void application_vibro(void* p); void app_gpio_test(void* p); void cli_task(void* p); +void music_player(void* p); const FlipperStartupApp FLIPPER_STARTUP[] = { #ifdef APP_DISPLAY @@ -121,6 +122,10 @@ const FlipperStartupApp FLIPPER_STARTUP[] = { {.app = sd_card_test, .name = "sd_card_test", .libs = {1, FURI_LIB{"gui_task"}}}, #endif +#ifdef APP_MUSIC_PLAYER + {.app = music_player, .name = "music player", .libs = {1, FURI_LIB{"gui_task"}}}, +#endif + #ifdef APP_GPIO_DEMO { .app = app_gpio_test, @@ -169,4 +174,8 @@ const FlipperStartupApp FLIPPER_APPS[] = { #ifdef BUILD_GPIO_DEMO {.app = app_gpio_test, .name = "gpio test", .libs = {1, FURI_LIB{"gui_task"}}}, #endif + +#ifdef BUILD_MUSIC_PLAYER + {.app = music_player, .name = "music player", .libs = {1, FURI_LIB{"gui_task"}}}, +#endif }; \ No newline at end of file diff --git a/applications/applications.mk b/applications/applications.mk index 3aae9d92..ba21c519 100644 --- a/applications/applications.mk +++ b/applications/applications.mk @@ -23,6 +23,7 @@ BUILD_SPEAKER_DEMO = 1 BUILD_VIBRO_DEMO = 1 BUILD_SD_TEST = 1 BUILD_GPIO_DEMO = 1 +BUILD_MUSIC_PLAYER = 1 endif APP_NFC ?= 0 @@ -246,6 +247,17 @@ CFLAGS += -DBUILD_GPIO_DEMO C_SOURCES += $(wildcard $(APP_DIR)/gpio-tester/*.c) endif +APP_MUSIC_PLAYER ?= 0 +ifeq ($(APP_MUSIC_PLAYER), 1) +CFLAGS += -DAPP_MUSIC_PLAYER +BUILD_MUSIC_PLAYER = 1 +endif +BUILD_MUSIC_PLAYER ?= 0 +ifeq ($(BUILD_MUSIC_PLAYER), 1) +CFLAGS += -DBUILD_MUSIC_PLAYER +C_SOURCES += $(wildcard $(APP_DIR)/music-player/*.c) +endif + # device drivers APP_GUI ?= 0 diff --git a/applications/music-player/music-player.c b/applications/music-player/music-player.c new file mode 100644 index 00000000..09e3ae47 --- /dev/null +++ b/applications/music-player/music-player.c @@ -0,0 +1,444 @@ +#include "flipper_v2.h" + +// TODO float note freq +typedef enum { + // Delay + N = 0, + // Octave 4 + B4 = 494, + // Octave 5 + C5 = 523, + D5 = 587, + E5 = 659, + F_5 = 740, + G5 = 784, + A5 = 880, + B5 = 988, + // Octave 6 + C6 = 1046, + D6 = 1175, + E6 = 1319, +} MelodyEventNote; + +typedef enum { + L1 = 1, + L2 = 2, + L4 = 4, + L8 = 8, + L16 = 16, + L32 = 32, + L64 = 64, + L128 = 128, +} MelodyEventLength; + +typedef struct { + MelodyEventNote note; + MelodyEventLength length; +} MelodyEventRecord; + +typedef struct { + const MelodyEventRecord* record; + int8_t loop_count; +} SongPattern; + +const MelodyEventRecord melody_start[] = { + {E6, L8}, {N, L8}, {E5, L8}, {B5, L8}, {N, L4}, {E5, L8}, {A5, L8}, {G5, L8}, {A5, L8}, + {E5, L8}, {B5, L8}, {N, L8}, {G5, L8}, {A5, L8}, {D6, L8}, {N, L4}, {D5, L8}, {B5, L8}, + {N, L4}, {D5, L8}, {A5, L8}, {G5, L8}, {A5, L8}, {D5, L8}, {F_5, L8}, {N, L8}, {G5, L8}, + {A5, L8}, {D6, L8}, {N, L4}, {F_5, L8}, {B5, L8}, {N, L4}, {F_5, L8}, {D6, L8}, {C6, L8}, + {B5, L8}, {F_5, L8}, {A5, L8}, {N, L8}, {G5, L8}, {F_5, L8}, {E5, L8}, {N, L8}, {C5, L8}, + {E5, L8}, {B5, L8}, {B4, L8}, {C5, L8}, {D5, L8}, {D6, L8}, {C6, L8}, {B5, L8}, {F_5, L8}, + {A5, L8}, {N, L8}, {G5, L8}, {A5, L8}, {E6, L8}}; + +const MelodyEventRecord melody_loop[] = { + {N, L4}, {E5, L8}, {B5, L8}, {N, L4}, {E5, L8}, {A5, L8}, {G5, L8}, {A5, L8}, {E5, L8}, + {B5, L8}, {N, L8}, {G5, L8}, {A5, L8}, {D6, L8}, {N, L4}, {D5, L8}, {B5, L8}, {N, L4}, + {D5, L8}, {A5, L8}, {G5, L8}, {A5, L8}, {D5, L8}, {F_5, L8}, {N, L8}, {G5, L8}, {A5, L8}, + {D6, L8}, {N, L4}, {F_5, L8}, {B5, L8}, {N, L4}, {F_5, L8}, {D6, L8}, {C6, L8}, {B5, L8}, + {F_5, L8}, {A5, L8}, {N, L8}, {G5, L8}, {F_5, L8}, {E5, L8}, {N, L8}, {C5, L8}, {E5, L8}, + {B5, L8}, {B4, L8}, {C5, L8}, {D5, L8}, {D6, L8}, {C6, L8}, {B5, L8}, {F_5, L8}, {A5, L8}, + {N, L8}, {G5, L8}, {A5, L8}, {E6, L8}}; + +const MelodyEventRecord melody_chords_1bar[] = { + {E6, L8}, {N, L8}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, + {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, + {B4, L128}, {E5, L128}, {B5, L8}, {N, L4}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, + {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, + {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {A5, L8}}; + +const SongPattern song[] = {{melody_start, 1}, {melody_loop, -1}}; + +typedef enum { + EventTypeTick, + EventTypeKey, + EventTypeNote, + // add your events type +} MusicDemoEventType; + +typedef struct { + union { + InputEvent input; + const MelodyEventRecord* note_record; + } value; + MusicDemoEventType type; +} MusicDemoEvent; + +typedef struct { + ValueMutex* state_mutex; + osMessageQueueId_t event_queue; + +} MusicDemoContext; + +#define note_stack_size 4 +typedef struct { + // describe state here + const MelodyEventRecord* note_record; + const MelodyEventRecord* note_stack[note_stack_size]; + uint8_t volume_id; + uint8_t volume_id_max; +} State; + +float volumes[] = {0, 0.02, 0.05, 0.1, 0.5}; + +bool is_white_note(const MelodyEventRecord* note_record, uint8_t id) { + if(note_record == NULL) return false; + + switch(note_record->note) { + case C5: + case C6: + if(id == 0) return true; + break; + case D5: + case D6: + if(id == 1) return true; + break; + case E5: + case E6: + if(id == 2) return true; + break; + case G5: + if(id == 4) return true; + break; + case A5: + if(id == 5) return true; + break; + case B4: + case B5: + if(id == 6) return true; + break; + default: + break; + } + + return false; +} + +bool is_black_note(const MelodyEventRecord* note_record, uint8_t id) { + if(note_record == NULL) return false; + + switch(note_record->note) { + case F_5: + if(id == 3) return true; + break; + default: + break; + } + + return false; +} + +const char* get_note_name(const MelodyEventRecord* note_record) { + if(note_record == NULL) return ""; + + switch(note_record->note) { + case N: + return "---"; + break; + case B4: + return "B4-"; + break; + case C5: + return "C5-"; + break; + case D5: + return "D5-"; + break; + case E5: + return "E5-"; + break; + case F_5: + return "F#5"; + break; + case G5: + return "G5-"; + break; + case A5: + return "A5-"; + break; + case B5: + return "B5-"; + break; + case C6: + return "C6-"; + break; + case D6: + return "D6-"; + break; + case E6: + return "E6-"; + break; + default: + return "UNK"; + break; + } +} +const char* get_note_len_name(const MelodyEventRecord* note_record) { + if(note_record == NULL) return ""; + + switch(note_record->length) { + case L1: + return "1-"; + break; + case L2: + return "2-"; + break; + case L4: + return "4-"; + break; + case L8: + return "8-"; + break; + case L16: + return "16"; + break; + case L32: + return "32"; + break; + case L64: + return "64"; + break; + case L128: + return "1+"; + break; + default: + return "--"; + break; + } +} + +static void render_callback(CanvasApi* canvas, void* ctx) { + State* state = (State*)acquire_mutex((ValueMutex*)ctx, 25); + + canvas->clear(canvas); + canvas->set_color(canvas, ColorBlack); + canvas->set_font(canvas, FontPrimary); + canvas->draw_str(canvas, 0, 12, "MusicPlayer"); + + uint8_t x_pos = 0; + uint8_t y_pos = 24; + const uint8_t white_w = 10; + const uint8_t white_h = 40; + + const int8_t black_x = 6; + const int8_t black_y = -5; + const uint8_t black_w = 8; + const uint8_t black_h = 32; + + // white keys + for(size_t i = 0; i < 7; i++) { + if(is_white_note(state->note_record, i)) { + canvas->draw_box(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h); + } else { + canvas->draw_frame(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h); + } + } + + // black keys + for(size_t i = 0; i < 7; i++) { + if(i != 2 && i != 6) { + canvas->set_color(canvas, ColorWhite); + canvas->draw_box( + canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); + canvas->set_color(canvas, ColorBlack); + if(is_black_note(state->note_record, i)) { + canvas->draw_box( + canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); + } else { + canvas->draw_frame( + canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); + } + } + } + + // volume widget + x_pos = 124; + y_pos = 0; + const uint8_t volume_h = (64 / (state->volume_id_max - 1)) * state->volume_id; + canvas->draw_frame(canvas, x_pos, y_pos, 4, 64); + canvas->draw_box(canvas, x_pos, y_pos + (64 - volume_h), 4, volume_h); + + // note stack widget + x_pos = 73; + y_pos = 0; + canvas->set_color(canvas, ColorBlack); + canvas->set_font(canvas, FontPrimary); + canvas->draw_frame(canvas, x_pos, y_pos, 49, 64); + canvas->draw_line(canvas, x_pos + 28, 0, x_pos + 28, 64); + + for(uint8_t i = 0; i < note_stack_size; i++) { + if(i == 0) { + canvas->draw_box(canvas, x_pos, y_pos + 48, 49, 16); + canvas->set_color(canvas, ColorWhite); + } else { + canvas->set_color(canvas, ColorBlack); + } + canvas->draw_str(canvas, x_pos + 4, 64 - 16 * i - 3, get_note_name(state->note_stack[i])); + canvas->draw_str( + canvas, x_pos + 31, 64 - 16 * i - 3, get_note_len_name(state->note_stack[i])); + canvas->draw_line(canvas, x_pos, 64 - 16 * i, x_pos + 48, 64 - 16 * i); + } + + release_mutex((ValueMutex*)ctx, state); +} + +static void input_callback(InputEvent* input_event, void* ctx) { + osMessageQueueId_t event_queue = (QueueHandle_t)ctx; + + MusicDemoEvent event; + event.type = EventTypeKey; + event.value.input = *input_event; + osMessageQueuePut(event_queue, &event, 0, 0); +} + +void process_note( + const MelodyEventRecord* note_record, + float bar_length_ms, + MusicDemoContext* context) { + MusicDemoEvent event; + // send note event + event.type = EventTypeNote; + event.value.note_record = note_record; + osMessageQueuePut(context->event_queue, &event, 0, 0); + + // read volume + State* state = (State*)acquire_mutex(context->state_mutex, 25); + float volume = volumes[state->volume_id]; + release_mutex(context->state_mutex, state); + + // play note + float note_delay = bar_length_ms / (float)note_record->length; + if(note_record->note != N) { + hal_pwm_set(volume, note_record->note, &SPEAKER_TIM, SPEAKER_CH); + } + delay(note_delay); + hal_pwm_stop(&SPEAKER_TIM, SPEAKER_CH); +} + +void music_player_thread(void* p) { + MusicDemoContext* context = (MusicDemoContext*)p; + + const float bpm = 130.0f; + // 4/4 + const float bar_length_ms = (60.0f * 1000.0f / bpm) * 4; + const uint16_t melody_start_events_count = sizeof(melody_start) / sizeof(melody_start[0]); + const uint16_t melody_loop_events_count = sizeof(melody_loop) / sizeof(melody_loop[0]); + + for(size_t i = 0; i < melody_start_events_count; i++) { + process_note(&melody_start[i], bar_length_ms, context); + } + + while(1) { + for(size_t i = 0; i < melody_loop_events_count; i++) { + process_note(&melody_loop[i], bar_length_ms, context); + } + } +} + +void music_player(void* p) { + osMessageQueueId_t event_queue = osMessageQueueNew(1, sizeof(MusicDemoEvent), NULL); + + State _state; + _state.note_record = NULL; + for(size_t i = 0; i < note_stack_size; i++) { + _state.note_stack[i] = NULL; + } + _state.volume_id = 1; + _state.volume_id_max = sizeof(volumes) / sizeof(volumes[0]); + + ValueMutex state_mutex; + if(!init_mutex(&state_mutex, &_state, sizeof(State))) { + printf("cannot create mutex\n"); + furiac_exit(NULL); + } + + Widget* widget = widget_alloc(); + widget_draw_callback_set(widget, render_callback, &state_mutex); + widget_input_callback_set(widget, input_callback, event_queue); + + // Open GUI and register widget + GuiApi* gui = (GuiApi*)furi_open("gui"); + if(gui == NULL) { + printf("gui is not available\n"); + furiac_exit(NULL); + } + gui->add_widget(gui, widget, GuiLayerFullscreen); + + // open input record + PubSub* input_events_record = furi_open("input_events"); + // prepare "do nothing" event + InputEvent input_event = {InputRight, true}; + + // start player thread + // TODO change to fuirac_start + osThreadAttr_t player_attr = {.name = "music_player_thread", .stack_size = 512}; + MusicDemoContext context = {.state_mutex = &state_mutex, .event_queue = event_queue}; + osThreadId_t player = osThreadNew(music_player_thread, &context, &player_attr); + + MusicDemoEvent event; + while(1) { + osStatus_t event_status = osMessageQueueGet(event_queue, &event, NULL, 100); + + State* state = (State*)acquire_mutex_block(&state_mutex); + + if(event_status == osOK) { + if(event.type == EventTypeKey) { + // press events + if(event.value.input.state && event.value.input.input == InputBack) { + } + + if(event.value.input.state && event.value.input.input == InputUp) { + if(state->volume_id < state->volume_id_max - 1) state->volume_id++; + } + + if(event.value.input.state && event.value.input.input == InputDown) { + if(state->volume_id > 0) state->volume_id--; + } + + if(event.value.input.state && event.value.input.input == InputLeft) { + } + + if(event.value.input.state && event.value.input.input == InputRight) { + } + + if(event.value.input.input == InputOk) { + } + + } else if(event.type == EventTypeNote) { + // send "do nothing" event to prevent display backlight off + notify_pubsub(input_events_record, &input_event); + + state->note_record = event.value.note_record; + + for(size_t i = note_stack_size - 1; i > 0; i--) { + state->note_stack[i] = state->note_stack[i - 1]; + } + state->note_stack[0] = state->note_record; + } + } else { + // event timeout + } + + widget_update(widget); + release_mutex(&state_mutex, state); + } +} diff --git a/applications/power/power.c b/applications/power/power.c index c8c0d5d6..bf4789f9 100644 --- a/applications/power/power.c +++ b/applications/power/power.c @@ -57,7 +57,6 @@ Power* power_alloc() { furi_check(power->menu_vm); power->cli = furi_open("cli"); - furi_check(power->cli); power->menu = menu_item_alloc_menu("Power", NULL); menu_item_subitem_add( @@ -97,7 +96,9 @@ void power_task(void* p) { (void)p; Power* power = power_alloc(); - cli_add_command(power->cli, "poweroff", power_cli_poweroff, power); + if(power->cli) { + cli_add_command(power->cli, "poweroff", power_cli_poweroff, power); + } FuriRecordSubscriber* gui_record = furi_open_deprecated("gui", false, false, NULL, NULL, NULL); assert(gui_record); From 2ba3722de202de49150a02155689bbbc6964f70e Mon Sep 17 00:00:00 2001 From: DrZlo13 Date: Mon, 16 Nov 2020 20:22:28 +0300 Subject: [PATCH 6/6] add mutex in furi_create_deprecated (#242) --- core/flipper_v2.c | 12 +++++++++++- core/furi-deprecated.c | 14 ++++++++++++++ core/furi-deprecated.h | 3 +++ core/furi_ac.c | 4 ++-- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/core/flipper_v2.c b/core/flipper_v2.c index 9a4e4bc6..87ea47ce 100644 --- a/core/flipper_v2.c +++ b/core/flipper_v2.c @@ -1,5 +1,15 @@ #include "flipper_v2.h" bool init_flipper_api(void) { - return gpio_api_init(); + bool no_errors = true; + + if(!furi_init()) { + no_errors = false; + } + + if(!gpio_api_init()) { + no_errors = false; + } + + return no_errors; } \ No newline at end of file diff --git a/core/furi-deprecated.c b/core/furi-deprecated.c index 0df40efe..af420d49 100644 --- a/core/furi-deprecated.c +++ b/core/furi-deprecated.c @@ -11,6 +11,13 @@ static FuriRecord records[MAX_RECORD_COUNT]; static size_t current_buffer_idx = 0; +osMutexId_t furi_core_mutex; + +bool furi_init(void) { + furi_core_mutex = osMutexNew(NULL); + if(furi_core_mutex == NULL) return false; + return true; +} // find record pointer by name static FuriRecord* find_record(const char* name) { @@ -32,6 +39,11 @@ bool furi_create_deprecated(const char* name, void* value, size_t size) { printf("[FURI] creating %s record\n", name); #endif + // acquire mutex to prevent simultaneous write to record with same index + if(osMutexAcquire(furi_core_mutex, osWaitForever) != osOK) { + return false; + } + FuriRecord* record = find_record(name); if(record != NULL) { @@ -69,6 +81,8 @@ bool furi_create_deprecated(const char* name, void* value, size_t size) { current_buffer_idx++; + osMutexRelease(furi_core_mutex); + return true; } diff --git a/core/furi-deprecated.h b/core/furi-deprecated.h index 5f401bbe..0aa4cac6 100644 --- a/core/furi-deprecated.h +++ b/core/furi-deprecated.h @@ -87,6 +87,9 @@ typedef struct { FlipperAppLibrary libs; } FlipperStartupApp; +// Init core +bool furi_init(void); + /*! Simply starts application. It call app entrypoint with param passed as argument. diff --git a/core/furi_ac.c b/core/furi_ac.c index fc535dbc..75cc110d 100644 --- a/core/furi_ac.c +++ b/core/furi_ac.c @@ -31,12 +31,12 @@ void furiac_wait_libs(const FlipperAppLibrary* libs) { if(app_id == INVALID_TASK_ID) { #ifdef FURI_DEBUG - printf("[FURIAC] Invalid library name %s\n", lib_name); + printf("[FURIAC] Invalid library name %s\n", libs->name[i]); #endif } else { while(!task_buffer[app_id].ready) { #ifdef FURI_DEBUG - printf("[FURIAC] waiting for library \"%s\"\n", lib_name); + printf("[FURIAC] waiting for library \"%s\"\n", libs->name[i]); #endif osDelay(50); }