/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "mfg_btle_app.h" #include "applib/app.h" #include "applib/ui/ui.h" #include "applib/ui/option_menu_window.h" #include "bluetooth/bt_test.h" #include "board/board.h" #if CAPABILITY_HAS_MICROPHONE #include "drivers/mic.h" #endif #include "services/common/bluetooth/bt_compliance_tests.h" #if CAPABILITY_HAS_BUILTIN_HRM #include "services/common/hrm/hrm_manager.h" #endif #include "kernel/pbl_malloc.h" #include "process_management/app_manager.h" #include "process_state/app_state/app_state.h" #include "system/logging.h" #include "system/passert.h" #include "util/size.h" #include "util/string.h" #include "FreeRTOS.h" #include "semphr.h" #include #if BT_CONTROLLER_DA14681 #define TXRX_NUM_SUBTITLES 2 #define TXRX_SUBTITLE_LENGTH 8 #define STATUS_STRING_LENGTH 32 typedef enum { BTLETestType_None = 0, BTLETestType_TX, BTLETestType_RX, BTLETestTypeCount } BTLETestType; typedef enum { BTLETestStep_None = 0, BTLETestStep_BTStart, BTLETestStep_BTEnd, BTLETestStep_BTLETransmitStart, BTLETestStep_BTLEReceiverStart, BTLETestStep_BTLEStop, BTLETestStepCount } BTLETestStep; typedef enum { BTLEPayloadType_PRBS9 = 0, BTLEPayloadType_11110000, BTLEPayloadType_10101010, BTLEPayloadType_PRBS15, BTLEPayloadType_11111111, BTLEPayloadType_00000000, BTLEPayloadType_00001111, BTLEPayloadType_01010101, BTLEPayloadTypeCount } BTLEPayloadType; static const char *s_payload_names[BTLEPayloadTypeCount] = { [BTLEPayloadType_PRBS9] = "PRBS9", [BTLEPayloadType_11110000] = "11110000", [BTLEPayloadType_10101010] = "10101010", [BTLEPayloadType_PRBS15] = "PRBS15", [BTLEPayloadType_11111111] = "11111111", [BTLEPayloadType_00000000] = "00000000", [BTLEPayloadType_00001111] = "00001111", [BTLEPayloadType_01010101] = "01010101" }; typedef struct { // Main Menu Window main_menu_window; SimpleMenuLayer *main_menu_layer; SimpleMenuSection main_menu_section; SimpleMenuItem *main_menu_items; // TX / RX Menu Window txrx_window; MenuLayer txrx_menu_layer; NumberWindow txrx_number_window; // Payload Selection Window payload_window; SimpleMenuLayer *payload_menu_layer; SimpleMenuSection payload_menu_section; SimpleMenuItem *payload_menu_items; // Status Window Window status_window; TextLayer status_text; char status_string[STATUS_STRING_LENGTH]; // Testing State BTLETestType current_test; uint8_t channel; uint8_t payload_length; BTLEPayloadType payload_type; bool is_unmodulated_cw_enabled; #if CAPABILITY_HAS_BUILTIN_HRM bool is_hrm_enabled; #endif #if CAPABILITY_HAS_MICROPHONE bool is_mic_enabled; int16_t *mic_buffer; #endif BTLETestStep current_test_step; bool last_test_step_result; uint16_t rx_test_received_packets; SemaphoreHandle_t btle_test_semaphore; HRMSessionRef hrm_session; } AppData; // Forward declarations static void prv_txrx_menu_update(AppData *data); //-------------------------------------------------------------------------------- // Running Tests //-------------------------------------------------------------------------------- // Running the actual test is an asynchronous operations which expects a // callback to come from the bt test driver. // We keep track of our current test progress with AppData.current_test_step, // and use that to know how to proceed through. // // A BTLE test gets started, and needs to be manually stopped. // This means that setup goes like this: // // 1. User Signals "RUN" // 2. bt_test_start() // 3. bt_driver_le_transmitter_test / bt_driver_le_receiver_test // 4. User Signals "STOP" // 5. bt_driver_le_test_end() // 6. In case of RX test, gather results // 7. bt_test_stop() static void prv_response_cb(HciStatusCode status, const uint8_t *payload) { AppData *data = app_state_get_user_data(); const bool success = (status == HciStatusCode_Success); PBL_LOG(LOG_LEVEL_DEBUG, "Step %d complete", data->current_test_step); if (data->current_test_step == BTLETestStep_BTLEStop && data->current_test == BTLETestType_RX) { // RX Test, need to keep track of received packets // Payload is as follows: // | 1 byte | 2 bytes | // | success | received packets | // So we want grab a uint16_t from 1 byte into the payload const uint16_t *received_packets = (uint16_t *)(payload + 1); data->rx_test_received_packets = *received_packets; } data->last_test_step_result = success; xSemaphoreGive(data->btle_test_semaphore); } #if CAPABILITY_HAS_MICROPHONE static void prv_mic_cb(int16_t *samples, size_t sample_count, void *context) { // Just throw away the recorded samples. } #endif static bool prv_run_test_step(BTLETestStep step, AppData *data) { data->current_test_step = step; PBL_LOG(LOG_LEVEL_DEBUG, "Run test step: %d", step); bool wait_for_result = false; switch (step) { case BTLETestStep_BTStart: bt_test_start(); break; case BTLETestStep_BTEnd: bt_test_stop(); break; case BTLETestStep_BTLETransmitStart: if (data->is_unmodulated_cw_enabled) { bt_driver_start_unmodulated_tx(data->channel); wait_for_result = false; } else { bt_driver_le_transmitter_test(data->channel, data->payload_length, data->payload_type); wait_for_result = true; } break; case BTLETestStep_BTLEReceiverStart: bt_driver_le_receiver_test(data->channel); wait_for_result = true; break; case BTLETestStep_BTLEStop: if (data->current_test == BTLETestType_TX && data->is_unmodulated_cw_enabled) { bt_driver_stop_unmodulated_tx(); wait_for_result = false; } else { bt_driver_le_test_end(); wait_for_result = true; } break; default: WTF; } // Waiting for results is OK because it should not block the app task for very long. // The result is not for the entire test, it is a result for the step itself. // The test result for an RX test is received in prv_response_cb after BTLETestStep_BTLEStop. if (wait_for_result) { xSemaphoreTake(data->btle_test_semaphore, portMAX_DELAY); return data->last_test_step_result; } return true; } static void prv_stop_mic_and_cleanup(AppData *data) { mic_stop(MIC); app_free(data->mic_buffer); data->mic_buffer = NULL; } static void prv_run_test(AppData *data) { PBL_ASSERTN(data->current_test_step == BTLETestStep_None); bool failed = false; bt_driver_register_response_callback(prv_response_cb); #if CAPABILITY_HAS_BUILTIN_HRM if (data->is_hrm_enabled) { AppInstallId app_id = 1; uint16_t expire_s = SECONDS_PER_HOUR; data->hrm_session = sys_hrm_manager_app_subscribe(app_id, 1, expire_s, HRMFeature_LEDCurrent); } #endif #if CAPABILITY_HAS_MICROPHONE if (data->is_mic_enabled) { const size_t BUFFER_SIZE = 50; data->mic_buffer = app_malloc_check(BUFFER_SIZE * sizeof(int16_t)); if (!mic_start(MIC, &prv_mic_cb, NULL, data->mic_buffer, BUFFER_SIZE)) { failed = true; goto cleanup; } } #endif if (!prv_run_test_step(BTLETestStep_BTStart, data)) { failed = true; goto cleanup; } switch (data->current_test) { case BTLETestType_TX: if (!prv_run_test_step(BTLETestStep_BTLETransmitStart, data)) { failed = true; goto cleanup; } break; case BTLETestType_RX: data->rx_test_received_packets = 0; if (!prv_run_test_step(BTLETestStep_BTLEReceiverStart, data)) { failed = true; goto cleanup; } break; default: WTF; } cleanup: prv_txrx_menu_update(data); if (failed) { #if CAPABILITY_HAS_BUILTIN_HRM sys_hrm_manager_unsubscribe(data->hrm_session); #endif #if CAPABILITY_HAS_MICROPHONE prv_stop_mic_and_cleanup(data); #endif bt_driver_register_response_callback(NULL); snprintf(data->status_string, STATUS_STRING_LENGTH, "Test Failed to Start"); app_window_stack_push(&data->status_window, true); if (data->current_test_step != BTLETestStep_BTStart) { // A BTLE Test start failed, stop BT Test const bool success = prv_run_test_step(BTLETestStep_BTEnd, data); PBL_ASSERTN(success); } data->current_test_step = BTLETestStep_None; } } static void prv_stop_test(AppData *data) { bool failed = false; if (!prv_run_test_step(BTLETestStep_BTLEStop, data)) { failed = true; goto cleanup; } else if (data->current_test == BTLETestType_RX) { snprintf(data->status_string, STATUS_STRING_LENGTH, "Packets Received: %"PRIu16, data->rx_test_received_packets); app_window_stack_push(&data->status_window, true); } if (!prv_run_test_step(BTLETestStep_BTEnd, data)) { failed = true; goto cleanup; } cleanup: data->current_test_step = BTLETestStep_None; bt_driver_register_response_callback(NULL); prv_txrx_menu_update(data); #if CAPABILITY_HAS_BUILTIN_HRM sys_hrm_manager_unsubscribe(data->hrm_session); #endif #if CAPABILITY_HAS_MICROPHONE prv_stop_mic_and_cleanup(data); #endif if (failed) { snprintf(data->status_string, STATUS_STRING_LENGTH, "Test Failed"); app_window_stack_push(&data->status_window, true); } } static bool prv_test_is_running(AppData *data) { return (data->current_test_step != BTLETestStep_None); } //-------------------------------------------------------------------------------- // Number Windows //-------------------------------------------------------------------------------- // Number window is used / reused for getting channel / payload length from the user. static void prv_number_window_selected_cb(NumberWindow *number_window, void *context) { uint8_t *result = context; *result = (uint8_t)number_window_get_value(number_window); app_window_stack_pop(true); } //! Automatically uses 0 as min //! @param max The max value that can be selected //! @param value Pointer to the value data, which will be used to populate as well as return result. static void prv_txrx_number_window(uint8_t max, uint8_t *value, const char *label, AppData *data) { NumberWindow *number_window = &data->txrx_number_window; number_window_init(number_window, label, (NumberWindowCallbacks){ .selected = prv_number_window_selected_cb, }, value); number_window_set_min(number_window, 0); number_window_set_max(number_window, max); number_window_set_value(number_window, *value); app_window_stack_push(number_window_get_window(number_window), true); } //-------------------------------------------------------------------------------- // Payload Selection Window //-------------------------------------------------------------------------------- // Payload selection allows the user to select the payload type that should be // used in the TX test. static void prv_register_payload(int index, void *context) { PBL_ASSERTN(index < BTLEPayloadTypeCount); AppData *data = app_state_get_user_data(); data->payload_type = (BTLEPayloadType)index; app_window_stack_pop(true); } static void prv_payload_window_load(Window *window) { AppData *data = app_state_get_user_data(); data->payload_menu_items = app_malloc_check(BTLEPayloadTypeCount * sizeof(SimpleMenuItem)); for (int i = 0; i < BTLEPayloadTypeCount; ++i) { data->payload_menu_items[i] = (SimpleMenuItem) { .title = s_payload_names[i], .callback = prv_register_payload, }; } data->payload_menu_section = (SimpleMenuSection) { .num_items = BTLEPayloadTypeCount, .items = data->payload_menu_items, }; Layer *window_layer = window_get_root_layer(&data->payload_window); const GRect bounds = window_layer->bounds; data->payload_menu_layer = simple_menu_layer_create(bounds, &data->payload_window, &data->payload_menu_section, 1, data); layer_add_child(window_layer, simple_menu_layer_get_layer(data->payload_menu_layer)); } static void prv_payload_window_unload(Window *window) { AppData *data = app_state_get_user_data(); layer_remove_child_layers(window_get_root_layer(&data->payload_window)); simple_menu_layer_destroy(data->payload_menu_layer); app_free(data->payload_menu_items); } static void prv_payload_type_window(AppData *data) { window_set_window_handlers(&data->payload_window, &(WindowHandlers){ .load = prv_payload_window_load, .unload = prv_payload_window_unload, }); app_window_stack_push(&data->payload_window, true); } //-------------------------------------------------------------------------------- // Status Window //-------------------------------------------------------------------------------- // Status window is just a simple window which will display to the user whatever // data->status_string has been set to. static void prv_status_window_init(AppData *data) { // Init window window_init(&data->status_window, ""); Layer *window_layer = window_get_root_layer(&data->status_window); GRect bounds = window_layer->bounds; bounds.origin.y += 40; // Init text layer text_layer_init(&data->status_text, &bounds); text_layer_set_text(&data->status_text, data->status_string); text_layer_set_text_alignment(&data->status_text, GTextAlignmentCenter); text_layer_set_font(&data->status_text, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD)); layer_add_child(window_layer, text_layer_get_layer(&data->status_text)); } //-------------------------------------------------------------------------------- // TX/RX Menus & Windows //-------------------------------------------------------------------------------- // The same menu layer is reused for TX / RX, we just handle it differently // based on whether we are currently executing a TX or RX test. #define TX_MENU_NUM_PAYLOAD_ROWS (2) enum { TXMenuIdx_Channel = 0, TXMenuIdx_UnmodulatedContinuousWave, TXMenuIdx_PayloadLength, TXMenuIdx_PayloadType, #if CAPABILITY_HAS_BUILTIN_HRM TXMenuIdx_HRM, #endif #if CAPABILITY_HAS_MICROPHONE TXMenuIdx_Microphone, #endif TXMenuIdx_RunStop, TXMenuIdx_Count, }; enum { RXMenuIdx_Channel = 0, #if CAPABILITY_HAS_BUILTIN_HRM RXMenuIdx_HRM, #endif #if CAPABILITY_HAS_MICROPHONE RXMenuIdx_Microphone, #endif RXMenuIdx_RunStop, RXMenuIdx_Count, }; static void prv_txrx_menu_update(AppData *data) { layer_mark_dirty(menu_layer_get_layer(&data->txrx_menu_layer)); } static uint16_t prv_menu_get_num_rows(MenuLayer *menu_layer, uint16_t section, void *context) { AppData *data = context; if (data->current_test == BTLETestType_TX) { uint16_t count = TXMenuIdx_Count; if (data->is_unmodulated_cw_enabled) { count -= TX_MENU_NUM_PAYLOAD_ROWS; // Payload rows are hidden } return count; } else { return RXMenuIdx_Count; } } static uint16_t prv_compensated_tx_menu_row_idx(const MenuIndex *index, const AppData *data) { uint16_t row = index->row; if (data->is_unmodulated_cw_enabled && row > TXMenuIdx_UnmodulatedContinuousWave) { // Payload length and payload type rows are removed when unmodulated continuous wave is // enabled, compensate so the enum still matches: row += TX_MENU_NUM_PAYLOAD_ROWS; } return row; } static void prv_menu_draw_row(GContext *ctx, const Layer *cell, MenuIndex *index, void *context) { AppData *data = context; char subtitle_buffer[TXRX_SUBTITLE_LENGTH]; const char *title = NULL; const char *subtitle = NULL; if (data->current_test == BTLETestType_TX) { switch (prv_compensated_tx_menu_row_idx(index, data)) { case TXMenuIdx_Channel: title = "Channel"; itoa_int(data->channel, subtitle_buffer, 10); subtitle = subtitle_buffer; break; case TXMenuIdx_UnmodulatedContinuousWave: title = "Unmodulated CW"; subtitle = data->is_unmodulated_cw_enabled ? "Enabled" : "Disabled"; break; case TXMenuIdx_PayloadLength: title = "Payload Length"; itoa_int(data->payload_length, subtitle_buffer, 10); subtitle = subtitle_buffer; break; case TXMenuIdx_PayloadType: title = "Payload Type"; subtitle = s_payload_names[data->payload_type]; break; #if CAPABILITY_HAS_BUILTIN_HRM case TXMenuIdx_HRM: title = "HRM"; subtitle = data->is_hrm_enabled ? "Enabled" : "Disabled"; break; #endif #if CAPABILITY_HAS_MICROPHONE case TXMenuIdx_Microphone: title = "Microphone"; subtitle = data->is_mic_enabled ? "Enabled" : "Disabled"; break; #endif case TXMenuIdx_RunStop: title = (prv_test_is_running(data)) ? "Stop" : "Run"; break; } } else if (data->current_test == BTLETestType_RX) { switch (index->row) { case RXMenuIdx_Channel: title = "Channel"; itoa_int(data->channel, subtitle_buffer, 10); subtitle = subtitle_buffer; break; #if CAPABILITY_HAS_BUILTIN_HRM case RXMenuIdx_HRM: title = "HRM"; subtitle = data->is_hrm_enabled ? "Enabled" : "Disabled"; break; #endif #if CAPABILITY_HAS_MICROPHONE case RXMenuIdx_Microphone: title = "Microphone"; subtitle = data->is_mic_enabled ? "Enabled" : "Disabled"; break; #endif case RXMenuIdx_RunStop: title = (prv_test_is_running(data)) ? "Stop" : "Run"; break; } } else { WTF; } menu_cell_basic_draw(ctx, cell, title, subtitle, NULL); } static void prv_menu_select_click(MenuLayer *menu_layer, MenuIndex *index, void *context) { AppData *data = context; if (data->current_test == BTLETestType_TX) { const uint16_t row = prv_compensated_tx_menu_row_idx(index, data); if (prv_test_is_running(data) && row != TXMenuIdx_RunStop) { // Can't change params while running return; } switch (row) { case TXMenuIdx_Channel: prv_txrx_number_window(39, &data->channel, "Channel", data); break; case TXMenuIdx_UnmodulatedContinuousWave: data->is_unmodulated_cw_enabled = !data->is_unmodulated_cw_enabled; menu_layer_reload_data(menu_layer); break; case TXMenuIdx_PayloadLength: prv_txrx_number_window(255, &data->payload_length, "Payload Length", data); break; case TXMenuIdx_PayloadType: prv_payload_type_window(data); break; #if CAPABILITY_HAS_BUILTIN_HRM case TXMenuIdx_HRM: data->is_hrm_enabled = !data->is_hrm_enabled; menu_layer_reload_data(menu_layer); break; #endif #if CAPABILITY_HAS_MICROPHONE case TXMenuIdx_Microphone: data->is_mic_enabled = !data->is_mic_enabled; menu_layer_reload_data(menu_layer); break; #endif case TXMenuIdx_RunStop: // Run / Stop if (data->current_test_step == BTLETestStep_None) { prv_run_test(data); } else { prv_stop_test(data); } break; } } else if (data->current_test == BTLETestType_RX) { if (prv_test_is_running(data) && index->row != RXMenuIdx_RunStop) { // Can't change params while running return; } switch (index->row) { case RXMenuIdx_Channel: prv_txrx_number_window(39, &data->channel, "Channel", data); break; #if CAPABILITY_HAS_BUILTIN_HRM case RXMenuIdx_HRM: data->is_hrm_enabled = !data->is_hrm_enabled; menu_layer_reload_data(menu_layer); break; #endif #if CAPABILITY_HAS_MICROPHONE case RXMenuIdx_Microphone: data->is_mic_enabled = !data->is_mic_enabled; menu_layer_reload_data(menu_layer); break; #endif case RXMenuIdx_RunStop: if (data->current_test_step == BTLETestStep_None) { prv_run_test(data); } else { prv_stop_test(data); } break; } } else { WTF; } } static void prv_txrx_window_load(Window *window) { AppData *data = app_state_get_user_data(); Layer *window_layer = window_get_root_layer(&data->txrx_window); menu_layer_init(&data->txrx_menu_layer, &window_layer->bounds); menu_layer_set_callbacks(&data->txrx_menu_layer, data, &(MenuLayerCallbacks) { .get_num_rows = prv_menu_get_num_rows, .draw_row = prv_menu_draw_row, .select_click = prv_menu_select_click, }); layer_add_child(window_layer, menu_layer_get_layer(&data->txrx_menu_layer)); menu_layer_set_click_config_onto_window(&data->txrx_menu_layer, &data->txrx_window); } // Shared unload handler which destroys all common data static void prv_txrx_window_unload(Window *window) { AppData *data = app_state_get_user_data(); layer_remove_child_layers(window_get_root_layer(&data->txrx_window)); menu_layer_deinit(&data->txrx_menu_layer); if (data->current_test_step != BTLETestStep_None) { // Currently outstanding test not complete, finish it. switch (data->current_test_step) { case BTLETestStep_BTStart: case BTLETestStep_BTEnd: prv_run_test_step(BTLETestStep_BTEnd, data); break; case BTLETestStep_BTLETransmitStart: case BTLETestStep_BTLEReceiverStart: prv_run_test_step(BTLETestStep_BTLEStop, data); // fallthrough case BTLETestStep_BTLEStop: prv_run_test_step(BTLETestStep_BTEnd, data); break; default: WTF; } } data->current_test_step = BTLETestStep_None; data->current_test = BTLETestType_None; } static void prv_enter_txrx_menu(AppData *data, BTLETestType test) { switch (test) { case BTLETestType_TX: case BTLETestType_RX: break; // OK default: WTF; } data->current_test = test; window_set_window_handlers(&data->txrx_window, &(WindowHandlers){ .load = prv_txrx_window_load, .unload = prv_txrx_window_unload, }); app_window_stack_push(&data->txrx_window, true); } //-------------------------------------------------------------------------------- // Main Menu //-------------------------------------------------------------------------------- static void prv_enter_tx_menu(int index, void *context) { prv_enter_txrx_menu(context, BTLETestType_TX); } static void prv_enter_rx_menu(int index, void *context) { prv_enter_txrx_menu(context, BTLETestType_RX); } static void prv_init_main_menu(AppData *data) { const SimpleMenuItem menu_items[] = { { .title = "BTLE TX", .callback = prv_enter_tx_menu }, { .title = "BTLE RX", .callback = prv_enter_rx_menu }, }; const size_t size = sizeof(menu_items); data->main_menu_items = app_malloc_check(size); memcpy(data->main_menu_items, menu_items, size); data->main_menu_section = (SimpleMenuSection) { .num_items = ARRAY_LENGTH(menu_items), .items = data->main_menu_items, }; Layer *window_layer = window_get_root_layer(&data->main_menu_window); const GRect bounds = window_layer->bounds; data->main_menu_layer = simple_menu_layer_create(bounds, &data->main_menu_window, &data->main_menu_section, 1, data); layer_add_child(window_layer, simple_menu_layer_get_layer(data->main_menu_layer)); } static void prv_main(void) { AppData *data = app_malloc_check(sizeof(*data)); *data = (AppData) { .current_test = BTLETestType_None, .current_test_step = BTLETestStep_None, .btle_test_semaphore = xSemaphoreCreateBinary(), }; app_state_set_user_data(data); window_init(&data->main_menu_window, ""); window_init(&data->txrx_window, ""); window_init(&data->payload_window, ""); prv_status_window_init(data); prv_init_main_menu(data); app_window_stack_push(&data->main_menu_window, true); app_event_loop(); } const PebbleProcessMd* mfg_btle_app_get_info(void) { static const PebbleProcessMdSystem s_app_info = { .common.main_func = &prv_main, .name = "Test BTLE", }; return (const PebbleProcessMd*) &s_app_info; } #endif // BT_CONTROLLER_DA14681