mirror of
https://github.com/google/pebble.git
synced 2025-11-22 15:30:55 -05:00
Import of the watch repository from Pebble
This commit is contained in:
116
src/fw/apps/prf_apps/mfg_accel_app.c
Normal file
116
src/fw/apps/prf_apps/mfg_accel_app.c
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 "applib/app.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "util/trig.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "applib/ui/window_private.h"
|
||||
#include "applib/ui/path_layer.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "kernel/util/sleep.h"
|
||||
#include "drivers/accel.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "services/common/evented_timer.h"
|
||||
#include "util/bitset.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define STATUS_STRING_LEN 50
|
||||
|
||||
static EventedTimerID s_timer;
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
|
||||
TextLayer title;
|
||||
TextLayer status;
|
||||
char status_string[STATUS_STRING_LEN];
|
||||
} AppData;
|
||||
|
||||
static void prv_update_display(void *context) {
|
||||
AppData *data = context;
|
||||
|
||||
AccelDriverSample sample;
|
||||
|
||||
int ret = accel_peek(&sample);
|
||||
|
||||
if (ret == 0) {
|
||||
sniprintf(data->status_string, sizeof(data->status_string),
|
||||
"X: %"PRIi16"\nY: %"PRIi16"\nZ:%"PRIi16"", sample.x, sample.y, sample.z);
|
||||
} else {
|
||||
sniprintf(data->status_string, sizeof(data->status_string),
|
||||
"ACCEL ERROR:\n%d", ret);
|
||||
}
|
||||
|
||||
text_layer_set_text(&data->status, data->status_string);
|
||||
}
|
||||
|
||||
static void prv_handle_init(void) {
|
||||
AppData *data = app_malloc_check(sizeof(AppData));
|
||||
*data = (AppData) {};
|
||||
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, "");
|
||||
window_set_fullscreen(window, true);
|
||||
|
||||
TextLayer *title = &data->title;
|
||||
text_layer_init(title, &window->layer.bounds);
|
||||
text_layer_set_font(title, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text_alignment(title, GTextAlignmentCenter);
|
||||
text_layer_set_text(title, "ACCEL TEST");
|
||||
layer_add_child(&window->layer, &title->layer);
|
||||
|
||||
TextLayer *status = &data->status;
|
||||
text_layer_init(status,
|
||||
&GRect(5, 40,
|
||||
window->layer.bounds.size.w - 5, window->layer.bounds.size.h - 40));
|
||||
text_layer_set_font(status, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text_alignment(status, GTextAlignmentCenter);
|
||||
layer_add_child(&window->layer, &status->layer);
|
||||
|
||||
app_window_stack_push(window, true /* Animated */);
|
||||
|
||||
s_timer = evented_timer_register(100, true /* repeating */, prv_update_display, data);
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
prv_handle_init();
|
||||
|
||||
app_event_loop();
|
||||
|
||||
evented_timer_cancel(s_timer);
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_accel_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &s_main,
|
||||
// UUID: ED2E214A-D4B5-4360-B5EC-612B9E49FB95
|
||||
.common.uuid = { 0xED, 0x2E, 0x21, 0x4A, 0xD4, 0xB5, 0x43, 0x60,
|
||||
0xB5, 0xEC, 0x61, 0x2B, 0x9E, 0x49, 0xFB, 0x95,
|
||||
},
|
||||
.name = "MfgAccel",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
21
src/fw/apps/prf_apps/mfg_accel_app.h
Normal file
21
src/fw/apps/prf_apps/mfg_accel_app.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_accel_app_get_info(void);
|
||||
120
src/fw/apps/prf_apps/mfg_als_app.c
Normal file
120
src/fw/apps/prf_apps/mfg_als_app.c
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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_als_app.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "drivers/ambient_light.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "mfg/mfg_info.h"
|
||||
#include "mfg/results_ui.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define AMBIENT_READING_STR_LEN 32
|
||||
typedef struct {
|
||||
Window *window;
|
||||
TextLayer *title_text_layer;
|
||||
TextLayer *reading_text_layer;
|
||||
char ambient_reading[AMBIENT_READING_STR_LEN];
|
||||
uint32_t latest_als_value;
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
MfgResultsUI results_ui;
|
||||
#endif
|
||||
} AmbientLightAppData;
|
||||
|
||||
static void prv_update_reading(AmbientLightAppData *data) {
|
||||
uint32_t level = ambient_light_get_light_level();
|
||||
snprintf(data->ambient_reading, AMBIENT_READING_STR_LEN, "%"PRIu32, level);
|
||||
data->latest_als_value = level;
|
||||
}
|
||||
|
||||
static void prv_timer_callback(void *cb_data) {
|
||||
AmbientLightAppData *data = app_state_get_user_data();
|
||||
|
||||
prv_update_reading(data);
|
||||
layer_mark_dirty(window_get_root_layer(data->window));
|
||||
|
||||
app_timer_register(500, prv_timer_callback, NULL);
|
||||
}
|
||||
|
||||
static void prv_record_als_reading(void) {
|
||||
AmbientLightAppData *data = app_state_get_user_data();
|
||||
mfg_info_write_als_result(data->latest_als_value);
|
||||
}
|
||||
|
||||
static void prv_handle_init(void) {
|
||||
AmbientLightAppData *data = task_zalloc_check(sizeof(AmbientLightAppData));
|
||||
|
||||
data->window = window_create();
|
||||
|
||||
Layer *window_layer = window_get_root_layer(data->window);
|
||||
GRect bounds = window_layer->bounds;
|
||||
bounds.origin.y += 40;
|
||||
|
||||
data->title_text_layer = text_layer_create(bounds);
|
||||
text_layer_set_font(data->title_text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text(data->title_text_layer, "ALS");
|
||||
text_layer_set_text_alignment(data->title_text_layer, GTextAlignmentCenter);
|
||||
layer_add_child(window_layer, text_layer_get_layer(data->title_text_layer));
|
||||
|
||||
bounds.origin.y += 30;
|
||||
data->reading_text_layer = text_layer_create(bounds);
|
||||
|
||||
prv_update_reading(data);
|
||||
|
||||
text_layer_set_font(data->reading_text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text(data->reading_text_layer, data->ambient_reading);
|
||||
text_layer_set_text_alignment(data->reading_text_layer, GTextAlignmentCenter);
|
||||
layer_add_child(window_layer, text_layer_get_layer(data->reading_text_layer));
|
||||
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
mfg_results_ui_init(&data->results_ui, MfgTest_ALS, data->window);
|
||||
mfg_results_ui_set_callback(&data->results_ui, prv_record_als_reading);
|
||||
#endif
|
||||
|
||||
app_state_set_user_data(data);
|
||||
app_window_stack_push(data->window, true);
|
||||
|
||||
app_timer_register(10, prv_timer_callback, NULL);
|
||||
}
|
||||
|
||||
static void prv_handle_deinit(void) {
|
||||
AmbientLightAppData *data = app_state_get_user_data();
|
||||
text_layer_destroy(data->title_text_layer);
|
||||
text_layer_destroy(data->reading_text_layer);
|
||||
window_destroy(data->window);
|
||||
task_free(data);
|
||||
}
|
||||
|
||||
static void prv_main(void) {
|
||||
prv_handle_init();
|
||||
app_event_loop();
|
||||
prv_handle_deinit();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_als_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_ambient_light_info = {
|
||||
.common.main_func = prv_main,
|
||||
.name = "MfgALS"
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_ambient_light_info;
|
||||
}
|
||||
21
src/fw/apps/prf_apps/mfg_als_app.h
Normal file
21
src/fw/apps/prf_apps/mfg_als_app.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_als_app_get_info(void);
|
||||
84
src/fw/apps/prf_apps/mfg_bt_sig_rf_app.c
Normal file
84
src/fw/apps/prf_apps/mfg_bt_sig_rf_app.c
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 "applib/app.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "services/common/bluetooth/bt_compliance_tests.h"
|
||||
#include "system/logging.h"
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
|
||||
TextLayer title;
|
||||
|
||||
//! How many times we've vibrated
|
||||
int vibe_count;
|
||||
} AppData;
|
||||
|
||||
static void prv_handle_init(void) {
|
||||
AppData *data = app_malloc_check(sizeof(AppData));
|
||||
*data = (AppData) {
|
||||
.vibe_count = 0
|
||||
};
|
||||
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, "");
|
||||
window_set_fullscreen(window, true);
|
||||
|
||||
TextLayer *title = &data->title;
|
||||
text_layer_init(title, &window->layer.bounds);
|
||||
text_layer_set_font(title, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text_alignment(title, GTextAlignmentCenter);
|
||||
text_layer_set_text(title, "BT_SIG_RF\nTEST");
|
||||
layer_add_child(&window->layer, &title->layer);
|
||||
|
||||
app_window_stack_push(window, true /* Animated */);
|
||||
|
||||
// Enter the bluetooth test mode
|
||||
if (!bt_test_bt_sig_rf_test_mode()) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Failed to enter bt_sig_rf!");
|
||||
}
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
prv_handle_init();
|
||||
|
||||
app_event_loop();
|
||||
|
||||
// Bring us out of test mode. Do this on the kernel main thread as this app is currently
|
||||
// closing and if we take too long we'll get force-killed.
|
||||
bt_ctl_reset_bluetooth();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_bt_sig_rf_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &s_main,
|
||||
// UUID: 278f66e0-11a1-4139-a5f4-fceb64efcf55
|
||||
.common.uuid = { 0x27, 0x8f, 0x66, 0xe0, 0x11, 0xa1, 0x41, 0x39,
|
||||
0xa5, 0xf4, 0xfc, 0xeb, 0x64, 0xef, 0xcf, 0x55 },
|
||||
.name = "MfgBtSigRf",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
|
||||
26
src/fw/apps/prf_apps/mfg_bt_sig_rf_app.h
Normal file
26
src/fw/apps/prf_apps/mfg_bt_sig_rf_app.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
//! @file mfg_bt_sig_rf_app.h
|
||||
//!
|
||||
//! Boring test app that puts us into bt_sig_rf mode for testing.
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_bt_sig_rf_app_get_info(void);
|
||||
|
||||
794
src/fw/apps/prf_apps/mfg_btle_app.c
Normal file
794
src/fw/apps/prf_apps/mfg_btle_app.c
Normal file
@@ -0,0 +1,794 @@
|
||||
/*
|
||||
* 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 <stdio.h>
|
||||
|
||||
#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 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 | recieved 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 differentely
|
||||
// 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
|
||||
21
src/fw/apps/prf_apps/mfg_btle_app.h
Normal file
21
src/fw/apps/prf_apps/mfg_btle_app.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_btle_app_get_info(void);
|
||||
197
src/fw/apps/prf_apps/mfg_button_app.c
Normal file
197
src/fw/apps/prf_apps/mfg_button_app.c
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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 "applib/app.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "util/trig.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "applib/ui/window_private.h"
|
||||
#include "applib/ui/path_layer.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "mfg/mfg_info.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "util/bitset.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// How long after test pass / fail to wait before popping the window
|
||||
#define WINDOW_POP_TIME_S (3)
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
|
||||
PathLayer arrows[NUM_BUTTONS];
|
||||
|
||||
//! bitset of buttons pressed so far
|
||||
uint32_t buttons_pressed;
|
||||
|
||||
TextLayer title;
|
||||
TextLayer status;
|
||||
char status_string[35];
|
||||
|
||||
uint32_t seconds_remaining;
|
||||
bool test_complete;
|
||||
} AppData;
|
||||
|
||||
static void prv_handle_second_tick(struct tm *tick_time, TimeUnits units_changed) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
|
||||
if (data->test_complete) {
|
||||
if (--data->seconds_remaining == 0) {
|
||||
app_window_stack_pop(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bool test_passed = (data->buttons_pressed == 0x0f);
|
||||
if (data->seconds_remaining == 0 || test_passed) {
|
||||
data->test_complete = true;
|
||||
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
mfg_info_write_test_result(MfgTest_Buttons, test_passed);
|
||||
#endif
|
||||
|
||||
if (test_passed) {
|
||||
// pass
|
||||
sniprintf(data->status_string, sizeof(data->status_string), "PASS!");
|
||||
} else {
|
||||
// fail
|
||||
sniprintf(data->status_string, sizeof(data->status_string), "FAIL!");
|
||||
}
|
||||
data->seconds_remaining = WINDOW_POP_TIME_S;
|
||||
} else {
|
||||
sniprintf(data->status_string, sizeof(data->status_string),
|
||||
"TIME REMAINING: %"PRIu32"s", data->seconds_remaining);
|
||||
data->seconds_remaining--;
|
||||
}
|
||||
|
||||
text_layer_set_text(&data->status, data->status_string);
|
||||
}
|
||||
|
||||
static void prv_button_click_handler(ClickRecognizerRef recognizer, void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
ButtonId button_id_pressed = click_recognizer_get_button_id(recognizer);
|
||||
bitset32_set(&app_data->buttons_pressed, button_id_pressed);
|
||||
layer_set_hidden((struct Layer*)&app_data->arrows[button_id_pressed], true);
|
||||
|
||||
if (app_data->test_complete) {
|
||||
app_window_stack_remove(&app_data->window, false /* Animated */);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_config_provider(void *data) {
|
||||
window_single_click_subscribe(BUTTON_ID_BACK, prv_button_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_UP, prv_button_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_SELECT, prv_button_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_DOWN, prv_button_click_handler);
|
||||
}
|
||||
|
||||
static void init_arrow_layer_for_button(AppData *data, ButtonId id) {
|
||||
static GPoint ARROW_PATH_POINTS[] =
|
||||
{{0, 7}, {14, 7}, {14, 0}, {26, 12}, {14, 24}, {14, 17}, {0, 17}};
|
||||
|
||||
static const GPathInfo ARROW_PATH_INFO = {
|
||||
.num_points = ARRAY_LENGTH(ARROW_PATH_POINTS),
|
||||
.points = ARROW_PATH_POINTS
|
||||
};
|
||||
|
||||
#define ARROW_SIZE {26, 24}
|
||||
static const GRect ARROW_RECTS[] = {
|
||||
{{ 5, 55}, ARROW_SIZE}, // BACK
|
||||
{{112, 30}, ARROW_SIZE}, // UP
|
||||
{{112, 90}, ARROW_SIZE}, // SELECT
|
||||
{{112, 140}, ARROW_SIZE}, // DOWN
|
||||
};
|
||||
|
||||
PathLayer *arrow = &data->arrows[id];
|
||||
path_layer_init(arrow, &ARROW_PATH_INFO);
|
||||
path_layer_set_fill_color(arrow, GColorBlack);
|
||||
path_layer_set_stroke_color(arrow, GColorBlack);
|
||||
|
||||
layer_set_frame(&arrow->layer, &ARROW_RECTS[id]);
|
||||
|
||||
if (id == BUTTON_ID_BACK) {
|
||||
gpath_rotate_to(&arrow->path, (TRIG_MAX_ANGLE / 2));
|
||||
gpath_move_to(&arrow->path, GPoint(26, 24));
|
||||
}
|
||||
|
||||
layer_add_child(&data->window.layer, &arrow->layer);
|
||||
}
|
||||
|
||||
static void prv_handle_init(void) {
|
||||
AppData *data = app_malloc_check(sizeof(AppData));
|
||||
*data = (AppData) {
|
||||
.seconds_remaining = 10,
|
||||
.buttons_pressed = 0,
|
||||
.test_complete = false,
|
||||
};
|
||||
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, "");
|
||||
window_set_fullscreen(window, true);
|
||||
window_set_overrides_back_button(window, true);
|
||||
window_set_click_config_provider(window, prv_config_provider);
|
||||
|
||||
TextLayer *title = &data->title;
|
||||
text_layer_init(title, &window->layer.bounds);
|
||||
text_layer_set_font(title, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text_alignment(title, GTextAlignmentCenter);
|
||||
text_layer_set_text(title, "BUTTON TEST");
|
||||
layer_add_child(&window->layer, &title->layer);
|
||||
|
||||
TextLayer *status = &data->status;
|
||||
text_layer_init(status,
|
||||
&GRect(5, 110,
|
||||
window->layer.bounds.size.w - 5, window->layer.bounds.size.h - 110));
|
||||
text_layer_set_font(status, fonts_get_system_font(FONT_KEY_GOTHIC_24));
|
||||
text_layer_set_text_alignment(status, GTextAlignmentCenter);
|
||||
layer_add_child(&window->layer, &status->layer);
|
||||
|
||||
for (ButtonId id = 0; id < NUM_BUTTONS; ++id) {
|
||||
init_arrow_layer_for_button(data, id);
|
||||
}
|
||||
|
||||
app_window_stack_push(window, true /* Animated */);
|
||||
|
||||
tick_timer_service_subscribe(SECOND_UNIT, prv_handle_second_tick);
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
prv_handle_init();
|
||||
|
||||
app_event_loop();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_button_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &s_main,
|
||||
// UUID: eed03647-fa9e-4bae-9254-608aa297e4e4
|
||||
.common.uuid = { 0xee, 0xd0, 0x36, 0x47, 0xfa, 0x9e, 0x4b, 0xae,
|
||||
0x92, 0x54, 0x60, 0x8a, 0xa2, 0x97, 0xe4, 0xe4},
|
||||
.name = "MfgButton",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
|
||||
23
src/fw/apps/prf_apps/mfg_button_app.h
Normal file
23
src/fw/apps/prf_apps/mfg_button_app.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_button_app_get_info(void);
|
||||
|
||||
101
src/fw/apps/prf_apps/mfg_certification_app.c
Normal file
101
src/fw/apps/prf_apps/mfg_certification_app.c
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 "applib/app.h"
|
||||
#include "applib/app_light.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/inverter_layer.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/vibes.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "util/size.h"
|
||||
|
||||
//! How long to wait between vibes
|
||||
static int INTER_VIBE_PERIOD_MS = 5000;
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
|
||||
TextLayer title;
|
||||
|
||||
InverterLayer inverter;
|
||||
} AppData;
|
||||
|
||||
static void prv_handle_second_tick(struct tm *tick_time, TimeUnits units_changed) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
|
||||
Layer *inverter = &data->inverter.layer;
|
||||
layer_set_hidden(inverter, !layer_get_hidden(inverter));
|
||||
}
|
||||
|
||||
static void prv_vibe_timer_callback(void *data) {
|
||||
static const uint32_t SECOND_PULSE_DURATIONS[] = { 1000 };
|
||||
VibePattern pat = {
|
||||
.durations = SECOND_PULSE_DURATIONS,
|
||||
.num_segments = ARRAY_LENGTH(SECOND_PULSE_DURATIONS)
|
||||
};
|
||||
vibes_enqueue_custom_pattern(pat);
|
||||
|
||||
app_timer_register(INTER_VIBE_PERIOD_MS, prv_vibe_timer_callback, NULL);
|
||||
}
|
||||
|
||||
static void prv_handle_init(void) {
|
||||
AppData *data = app_malloc_check(sizeof(AppData));
|
||||
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, "");
|
||||
window_set_fullscreen(window, true);
|
||||
|
||||
TextLayer *title = &data->title;
|
||||
text_layer_init(title, &window->layer.frame);
|
||||
text_layer_set_font(title, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text_alignment(title, GTextAlignmentCenter);
|
||||
text_layer_set_text(title, "Certification");
|
||||
layer_add_child(&window->layer, &title->layer);
|
||||
|
||||
InverterLayer *inverter = &data->inverter;
|
||||
inverter_layer_init(inverter, &window->layer.frame);
|
||||
layer_add_child(&window->layer, &inverter->layer);
|
||||
|
||||
app_window_stack_push(window, true /* Animated */);
|
||||
|
||||
prv_vibe_timer_callback(NULL);
|
||||
app_light_enable(true);
|
||||
tick_timer_service_subscribe(SECOND_UNIT, prv_handle_second_tick);
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
prv_handle_init();
|
||||
|
||||
app_event_loop();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_certification_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &s_main,
|
||||
// UUID: 266135d1-827f-4f64-9752-fffe604e1dbe
|
||||
.common.uuid = { 0x26, 0x61, 0x35, 0xd1, 0x82, 0x7f, 0x4f, 0x64,
|
||||
0x97, 0x52, 0xff, 0xfe, 0x60, 0x4e, 0x1d, 0xbe },
|
||||
.name = "MfgCertification",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
26
src/fw/apps/prf_apps/mfg_certification_app.h
Normal file
26
src/fw/apps/prf_apps/mfg_certification_app.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
//! @file mfg_certification_app.h
|
||||
//!
|
||||
//! Boring test app that tweaks various hardware levers to create a worst case scenario for
|
||||
//! certification testing.
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_certification_app_get_info(void);
|
||||
331
src/fw/apps/prf_apps/mfg_display_app.c
Normal file
331
src/fw/apps/prf_apps/mfg_display_app.c
Normal file
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* 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 "applib/app.h"
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "applib/graphics/text.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/vibes.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "console/prompt.h"
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "mfg/mfg_mode/mfg_factory_mode.h"
|
||||
#include "mfg/results_ui.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "resource/system_resource.h"
|
||||
#include "services/common/light.h"
|
||||
#include "util/size.h"
|
||||
|
||||
typedef enum {
|
||||
TestPattern_Black,
|
||||
TestPattern_Gray,
|
||||
TestPattern_White,
|
||||
TestPattern_Crosshair,
|
||||
#if PBL_COLOR
|
||||
TestPattern_Red,
|
||||
TestPattern_Green,
|
||||
TestPattern_Blue,
|
||||
#endif
|
||||
#if PBL_ROUND
|
||||
TestPattern_Border1,
|
||||
TestPattern_Border2,
|
||||
TestPattern_Border3,
|
||||
#endif
|
||||
#if PBL_COLOR
|
||||
TestPattern_Pinwheel,
|
||||
TestPattern_Veggies,
|
||||
#endif
|
||||
|
||||
NumTestPatterns
|
||||
} TestPattern;
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
|
||||
TestPattern test_pattern;
|
||||
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
Window results_window;
|
||||
MfgResultsUI results_ui;
|
||||
#endif
|
||||
} AppData;
|
||||
|
||||
static void prv_draw_solid(Layer *layer, GContext *ctx, GColor color) {
|
||||
graphics_context_set_fill_color(ctx, color);
|
||||
graphics_fill_rect(ctx, &layer->bounds);
|
||||
}
|
||||
|
||||
static void prv_fill_cols(GContext *ctx, uint8_t color, int16_t *row, int16_t column,
|
||||
uint8_t num_pixels) {
|
||||
const GRect rect = { { column, *row }, { 1, num_pixels } };
|
||||
|
||||
// Set alpha bits to make it opaque
|
||||
graphics_context_set_fill_color(ctx, (GColor) { .argb = (0b11000000 | color) });
|
||||
graphics_fill_rect(ctx, &rect);
|
||||
|
||||
*row += num_pixels;
|
||||
}
|
||||
|
||||
#if PBL_ROUND
|
||||
|
||||
static void prv_draw_round_border(Layer *layer, GContext *ctx, uint8_t radial_padding_size) {
|
||||
for (int i = 0; i < layer->bounds.size.h / 2 - radial_padding_size; ++i) {
|
||||
const GBitmapDataRowInfoInternal *data_row_infos = g_gbitmap_spalding_data_row_infos;
|
||||
const uint8_t mask = data_row_infos[i].min_x + radial_padding_size;
|
||||
const int offset = i + radial_padding_size;
|
||||
// Draw both row-wise and column-wise to fill in any discontinuities
|
||||
// in the border circle.
|
||||
|
||||
// Top-left quadrant
|
||||
graphics_draw_pixel(ctx, GPoint(mask, offset));
|
||||
graphics_draw_pixel(ctx, GPoint(offset, mask));
|
||||
// Top-right quadrant
|
||||
graphics_draw_pixel(ctx, GPoint(mask, layer->bounds.size.h - offset - 1));
|
||||
graphics_draw_pixel(ctx, GPoint(layer->bounds.size.w - offset - 1, mask));
|
||||
// Bottom-left quadrant
|
||||
graphics_draw_pixel(ctx, GPoint(layer->bounds.size.w - mask - 1, offset));
|
||||
graphics_draw_pixel(ctx, GPoint(offset, layer->bounds.size.h - mask - 1));
|
||||
// Bottom-right quadrant
|
||||
graphics_draw_pixel(ctx, GPoint(layer->bounds.size.w - mask - 1,
|
||||
layer->bounds.size.h - offset - 1));
|
||||
graphics_draw_pixel(ctx, GPoint(layer->bounds.size.w - offset - 1,
|
||||
layer->bounds.size.h - mask - 1));
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_draw_border(Layer *layer, GContext *ctx, uint8_t radial_padding_size) {
|
||||
if (radial_padding_size != 0) {
|
||||
prv_draw_round_border(layer, ctx, 0);
|
||||
}
|
||||
prv_draw_round_border(layer, ctx, radial_padding_size);
|
||||
|
||||
// Draw letter to identify screen
|
||||
if (radial_padding_size >= 2) {
|
||||
GRect identifier_area = GRect(40, 40, 20, 20);
|
||||
char identifier[] = {'A' + radial_padding_size - 2, 0};
|
||||
graphics_context_set_text_color(ctx, GColorWhite);
|
||||
graphics_draw_text(ctx, identifier, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
|
||||
identifier_area, GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static void prv_draw_border(Layer *layer, GContext *ctx, uint8_t radial_padding_size) {
|
||||
graphics_draw_rect(ctx, &layer->bounds);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static void prv_draw_crosshair_screen(Layer *layer, GContext *ctx, uint8_t radial_padding_size) {
|
||||
graphics_context_set_fill_color(ctx, GColorBlack);
|
||||
graphics_fill_rect(ctx, &layer->bounds);
|
||||
|
||||
// Draw crosshair
|
||||
graphics_context_set_stroke_color(ctx, GColorWhite);
|
||||
graphics_draw_line(
|
||||
ctx, GPoint(layer->bounds.size.w / 2, radial_padding_size),
|
||||
GPoint(layer->bounds.size.w / 2, layer->bounds.size.h - radial_padding_size - 1));
|
||||
graphics_draw_line(
|
||||
ctx, GPoint(radial_padding_size, layer->bounds.size.h / 2),
|
||||
GPoint(layer->bounds.size.w - radial_padding_size - 1, layer->bounds.size.h / 2));
|
||||
|
||||
prv_draw_border(layer, ctx, radial_padding_size);
|
||||
}
|
||||
|
||||
static void prv_draw_bitmap(struct Layer *layer, GContext *ctx, uint32_t res) {
|
||||
GBitmap *bitmap = gbitmap_create_with_resource(res);
|
||||
graphics_draw_bitmap_in_rect(ctx, bitmap, &layer->bounds);
|
||||
gbitmap_destroy(bitmap);
|
||||
}
|
||||
|
||||
static void prv_update_proc(struct Layer *layer, GContext* ctx) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
switch (app_data->test_pattern) {
|
||||
case TestPattern_Black:
|
||||
prv_draw_solid(layer, ctx, GColorBlack);
|
||||
break;
|
||||
case TestPattern_Gray:
|
||||
prv_draw_solid(layer, ctx, GColorDarkGray);
|
||||
break;
|
||||
case TestPattern_White:
|
||||
prv_draw_solid(layer, ctx, GColorWhite);
|
||||
break;
|
||||
case TestPattern_Crosshair:
|
||||
prv_draw_crosshair_screen(layer, ctx, 0);
|
||||
break;
|
||||
#if PBL_COLOR
|
||||
case TestPattern_Red:
|
||||
prv_draw_solid(layer, ctx, GColorRed);
|
||||
break;
|
||||
case TestPattern_Green:
|
||||
prv_draw_solid(layer, ctx, GColorGreen);
|
||||
break;
|
||||
case TestPattern_Blue:
|
||||
prv_draw_solid(layer, ctx, GColorBlue);
|
||||
break;
|
||||
#endif
|
||||
#if PBL_ROUND
|
||||
case TestPattern_Border1:
|
||||
prv_draw_crosshair_screen(layer, ctx, 2);
|
||||
break;
|
||||
case TestPattern_Border2:
|
||||
prv_draw_crosshair_screen(layer, ctx, 3);
|
||||
break;
|
||||
case TestPattern_Border3:
|
||||
prv_draw_crosshair_screen(layer, ctx, 4);
|
||||
break;
|
||||
#endif
|
||||
#if PBL_COLOR
|
||||
case TestPattern_Pinwheel:
|
||||
prv_draw_bitmap(layer, ctx, RESOURCE_ID_TEST_IMAGE_PINWHEEL);
|
||||
break;
|
||||
case TestPattern_Veggies:
|
||||
prv_draw_bitmap(layer, ctx, RESOURCE_ID_TEST_IMAGE_VEGGIES);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_button_click_handler(ClickRecognizerRef recognizer, void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
app_data->test_pattern = (app_data->test_pattern + 1) % NumTestPatterns;
|
||||
|
||||
layer_mark_dirty(&app_data->window.layer);
|
||||
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
if (app_data->test_pattern == 0) {
|
||||
app_window_stack_pop(false);
|
||||
app_window_stack_push(&app_data->results_window, false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void prv_change_pattern(void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
app_data->test_pattern = (TestPattern) data;
|
||||
|
||||
layer_mark_dirty(&app_data->window.layer);
|
||||
}
|
||||
|
||||
static void prv_config_provider(void *data) {
|
||||
window_single_click_subscribe(BUTTON_ID_SELECT, prv_button_click_handler);
|
||||
}
|
||||
|
||||
static void prv_handle_init(void) {
|
||||
AppData *data = app_malloc_check(sizeof(AppData));
|
||||
*data = (AppData) {
|
||||
.test_pattern = (TestPattern) app_manager_get_task_context()->args
|
||||
};
|
||||
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, "");
|
||||
window_set_fullscreen(window, true);
|
||||
window_set_click_config_provider(window, prv_config_provider);
|
||||
|
||||
Layer *layer = window_get_root_layer(window);
|
||||
layer_set_update_proc(layer, prv_update_proc);
|
||||
|
||||
app_window_stack_push(window, true /* Animated */);
|
||||
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
window_init(&data->results_window, "");
|
||||
window_set_fullscreen(&data->results_window, true);
|
||||
mfg_results_ui_init(&data->results_ui, MfgTest_Display, &data->results_window);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
light_enable(true);
|
||||
|
||||
prv_handle_init();
|
||||
|
||||
app_event_loop();
|
||||
|
||||
light_enable(false);
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_display_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &s_main,
|
||||
// UUID: df582042-5beb-410f-9fed-76eccd31821e
|
||||
.common.uuid = { 0xdf, 0x58, 0x20, 0x42, 0x5b, 0xeb, 0x41, 0x0f,
|
||||
0x9f, 0xed, 0x76, 0xec, 0xcd, 0x31, 0x82, 0x1e },
|
||||
.name = "MfgDisplay",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
|
||||
|
||||
// Prompt Commands
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void prv_launch_app_cb(void *data) {
|
||||
if (app_manager_get_current_app_md() == mfg_display_app_get_info()) {
|
||||
process_manager_send_callback_event_to_process(PebbleTask_App, prv_change_pattern, data);
|
||||
} else {
|
||||
app_manager_launch_new_app(&(AppLaunchConfig) {
|
||||
.md = mfg_display_app_get_info(),
|
||||
.common.args = data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void command_display_set(const char *color) {
|
||||
const char * const ARGS[] = {
|
||||
[TestPattern_Black] = "black",
|
||||
[TestPattern_Gray] = "gray",
|
||||
[TestPattern_White] = "white",
|
||||
[TestPattern_Crosshair] = "crosshair",
|
||||
#if PBL_COLOR
|
||||
[TestPattern_Veggies] = "veggies",
|
||||
[TestPattern_Pinwheel] = "pinwheel",
|
||||
[TestPattern_Red] = "red",
|
||||
[TestPattern_Green] = "green",
|
||||
[TestPattern_Blue] = "blue",
|
||||
#endif
|
||||
};
|
||||
|
||||
for (unsigned int i = 0; i < ARRAY_LENGTH(ARGS); ++i) {
|
||||
if (!strcmp(color, ARGS[i])) {
|
||||
// Do this first because it launches the mfg menu using a callback, as if we did this in the
|
||||
// callback we send below to launch the display app it would end up launching the menu on
|
||||
// top of the display app.
|
||||
if (!mfg_is_mfg_mode()) {
|
||||
mfg_enter_mfg_mode_and_launch_app();
|
||||
}
|
||||
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) i);
|
||||
prompt_send_response("OK");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
prompt_send_response("Invalid command");
|
||||
}
|
||||
25
src/fw/apps/prf_apps/mfg_display_app.h
Normal file
25
src/fw/apps/prf_apps/mfg_display_app.h
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
//! @file mfg_display_app.h
|
||||
//!
|
||||
//! Test app that shows various display pattern
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_display_app_get_info(void);
|
||||
275
src/fw/apps/prf_apps/mfg_display_calibration_app.c
Normal file
275
src/fw/apps/prf_apps/mfg_display_calibration_app.c
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* 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 "applib/app.h"
|
||||
#include "applib/app_timer.h"
|
||||
#include "applib/fonts/fonts.h"
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "applib/graphics/text.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "mfg/mfg_serials.h"
|
||||
#include "mfg/mfg_info.h"
|
||||
#include "services/common/light.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/system_resource.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define TICK_LENGTH 20
|
||||
#define LINE_HEIGHT 20
|
||||
|
||||
typedef enum {
|
||||
X_AdjustState,
|
||||
Y_AdjustState,
|
||||
|
||||
NumAdjustStates
|
||||
} AppState;
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
|
||||
AppTimer *exit_timer;
|
||||
AppState app_state;
|
||||
int8_t axis_offsets[NumAdjustStates];
|
||||
|
||||
char text_buffer[16];
|
||||
char device_serial[MFG_SERIAL_NUMBER_SIZE + 1];
|
||||
bool is_saving;
|
||||
} AppData;
|
||||
|
||||
static void prv_draw_solid(Layer *layer, GContext *ctx, GColor color) {
|
||||
graphics_context_set_fill_color(ctx, color);
|
||||
graphics_fill_rect(ctx, &layer->bounds);
|
||||
}
|
||||
|
||||
static void prv_display_offsets(Layer *layer, GContext *ctx) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
graphics_context_set_text_color(ctx, GColorWhite);
|
||||
|
||||
const GRect *bounds = &data->window.layer.bounds;
|
||||
int pixel_max = bounds->origin.x + bounds->size.w - 1;
|
||||
|
||||
GFont font;
|
||||
for (int i = 0; i < NumAdjustStates; i++) {
|
||||
font = fonts_get_system_font((i == data->app_state) ? FONT_KEY_GOTHIC_24_BOLD
|
||||
: FONT_KEY_GOTHIC_24);
|
||||
|
||||
snprintf(data->text_buffer, sizeof(data->text_buffer), "%c: %"PRIi8, 'X' + i,
|
||||
data->axis_offsets[X_AdjustState + i]);
|
||||
graphics_draw_text(ctx, data->text_buffer, font,
|
||||
GRect(bounds->origin.x, bounds->origin.y + 35 + LINE_HEIGHT * i,
|
||||
pixel_max, pixel_max),
|
||||
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_display_serial_number(Layer *layer, GContext *ctx) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
|
||||
const GRect *bounds = &data->window.layer.bounds;
|
||||
graphics_context_set_text_color(ctx, GColorWhite);
|
||||
graphics_draw_text(ctx, data->device_serial, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD),
|
||||
GRect(bounds->origin.x, (bounds->origin.x + bounds->size.w) / 2,
|
||||
bounds->size.w, bounds->size.h),
|
||||
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
|
||||
}
|
||||
|
||||
static void prv_display_saving_message(Layer *layer, GContext *ctx) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
|
||||
const GRect *bounds = &data->window.layer.bounds;
|
||||
graphics_context_set_text_color(ctx, GColorWhite);
|
||||
graphics_draw_text(ctx, "Saving...", fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
|
||||
GRect(bounds->origin.x, (bounds->origin.y + bounds->size.h) / 2 + LINE_HEIGHT,
|
||||
bounds->size.w, bounds->size.h),
|
||||
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
|
||||
}
|
||||
|
||||
static void prv_draw_crosshair(Layer *layer, GContext *ctx) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
|
||||
const GRect *bounds = &data->window.layer.bounds;
|
||||
const int mid_pixel_minus_one = (bounds->origin.x + bounds->size.w - 1) / 2;
|
||||
const int pixel_min = bounds->origin.x;
|
||||
const int pixel_max = bounds->origin.x + bounds->size.w - 1;
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
graphics_context_set_stroke_color(ctx, i ? GColorWhite : GColorWhite);
|
||||
|
||||
graphics_draw_line(ctx, (GPoint) {.x = mid_pixel_minus_one + i, .y = pixel_min},
|
||||
(GPoint) {.x = mid_pixel_minus_one + i, .y = pixel_min + TICK_LENGTH});
|
||||
graphics_draw_line(ctx, (GPoint) {.x = mid_pixel_minus_one + i, .y = pixel_max - TICK_LENGTH},
|
||||
(GPoint) {.x = mid_pixel_minus_one + i, .y = pixel_max});
|
||||
graphics_draw_line(ctx, (GPoint) {.x = pixel_min, .y = mid_pixel_minus_one + i},
|
||||
(GPoint) {.x = pixel_min + TICK_LENGTH, .y = mid_pixel_minus_one + i});
|
||||
graphics_draw_line(ctx, (GPoint) {.x = pixel_max - TICK_LENGTH, .y = mid_pixel_minus_one + i},
|
||||
(GPoint) {.x = pixel_max, .y = mid_pixel_minus_one + i});
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_draw_border_stripes(Layer *layer, GContext *ctx) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
|
||||
const GRect *bounds = &data->window.layer.bounds;
|
||||
const int pixel_min = bounds->origin.x;
|
||||
const int pixel_max = bounds->origin.x + bounds->size.w - 1;
|
||||
|
||||
for (int j = 0; j < 2; j++) {
|
||||
graphics_context_set_stroke_color(ctx, j ? GColorGreen : GColorRed);
|
||||
for (int i = 0 + j; i < 5; i += 2) {
|
||||
graphics_draw_line(ctx, (GPoint) {.x = i, .y = pixel_min},
|
||||
(GPoint) {.x = i, .y = pixel_max});
|
||||
graphics_draw_line(ctx, (GPoint) {.x = pixel_max - i, .y = pixel_min},
|
||||
(GPoint) {.x = pixel_max - i, .y = pixel_max});
|
||||
graphics_draw_line(ctx, (GPoint) {.x = pixel_min, .y = i},
|
||||
(GPoint) {.x = pixel_max, .y = i});
|
||||
graphics_draw_line(ctx, (GPoint) {.x = pixel_min, .y = pixel_max - i},
|
||||
(GPoint) {.x = pixel_max, .y = pixel_max - i});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_layer_update_proc(Layer *layer, GContext *ctx) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
|
||||
prv_draw_solid(layer, ctx, GColorBlack);
|
||||
|
||||
ctx->draw_state.drawing_box.origin.x += data->axis_offsets[X_AdjustState];
|
||||
ctx->draw_state.drawing_box.origin.y += data->axis_offsets[Y_AdjustState];
|
||||
|
||||
prv_draw_border_stripes(layer, ctx);
|
||||
prv_draw_crosshair(layer, ctx);
|
||||
|
||||
prv_display_offsets(layer, ctx);
|
||||
prv_display_serial_number(layer, ctx);
|
||||
if (data->is_saving) {
|
||||
prv_display_saving_message(layer, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_select_click_handler(ClickRecognizerRef recognizer, void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
app_data->app_state = (app_data->app_state + 1) % (NumAdjustStates);
|
||||
|
||||
layer_mark_dirty(&app_data->window.layer);
|
||||
}
|
||||
|
||||
static void prv_up_click_handler(ClickRecognizerRef recognizer, void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
--(app_data->axis_offsets[app_data->app_state]);
|
||||
|
||||
layer_mark_dirty(&app_data->window.layer);
|
||||
}
|
||||
|
||||
static void prv_down_click_handler(ClickRecognizerRef recognizer, void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
++(app_data->axis_offsets[app_data->app_state]);
|
||||
|
||||
layer_mark_dirty(&app_data->window.layer);
|
||||
}
|
||||
|
||||
static void prv_save_offsets_callback(void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
mfg_info_set_disp_offsets((GPoint) {
|
||||
.x = app_data->axis_offsets[X_AdjustState],
|
||||
.y = app_data->axis_offsets[Y_AdjustState]
|
||||
});
|
||||
|
||||
app_window_stack_pop_all(false);
|
||||
}
|
||||
|
||||
static void prv_back_click_handler(ClickRecognizerRef recognizer, void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
if (app_data->axis_offsets[X_AdjustState] == mfg_info_get_disp_offsets().x &&
|
||||
app_data->axis_offsets[Y_AdjustState] == mfg_info_get_disp_offsets().y) {
|
||||
app_window_stack_pop_all(true);
|
||||
return;
|
||||
}
|
||||
|
||||
app_data->is_saving = true;
|
||||
layer_mark_dirty(&app_data->window.layer);
|
||||
|
||||
app_data->exit_timer = app_timer_register(200, prv_save_offsets_callback, NULL);
|
||||
}
|
||||
|
||||
static void prv_config_provider(void *data) {
|
||||
window_single_click_subscribe(BUTTON_ID_SELECT, prv_select_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_UP, prv_up_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_DOWN, prv_down_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_BACK, prv_back_click_handler);
|
||||
}
|
||||
|
||||
static void prv_handle_init(void) {
|
||||
AppData *data = app_malloc_check(sizeof(AppData));
|
||||
*data = (AppData) {
|
||||
.app_state = (AppState) app_manager_get_task_context()->args,
|
||||
.axis_offsets[X_AdjustState] = mfg_info_get_disp_offsets().x,
|
||||
.axis_offsets[Y_AdjustState] = mfg_info_get_disp_offsets().y
|
||||
};
|
||||
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, "MfgDisplayCalibration");
|
||||
window_set_fullscreen(window, true);
|
||||
window_set_click_config_provider(window, prv_config_provider);
|
||||
|
||||
Layer *layer = window_get_root_layer(window);
|
||||
layer_set_update_proc(layer, prv_layer_update_proc);
|
||||
|
||||
mfg_info_get_serialnumber(data->device_serial, sizeof(data->device_serial));
|
||||
|
||||
light_enable(true);
|
||||
|
||||
app_window_stack_push(window, true /* Animated */);
|
||||
}
|
||||
|
||||
static void prv_handle_deinit(void) {
|
||||
light_enable(false);
|
||||
|
||||
AppData *data = app_state_get_user_data();
|
||||
app_timer_cancel(data->exit_timer);
|
||||
app_free(data);
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
prv_handle_init();
|
||||
|
||||
app_event_loop();
|
||||
|
||||
prv_handle_deinit();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_display_calibration_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &s_main,
|
||||
// UUID: d0582042-5beb-410f-9fed-76eccd31821e
|
||||
.common.uuid = { 0xd0, 0x58, 0x20, 0x42, 0x5b, 0xeb, 0x41, 0x0f,
|
||||
0x9f, 0xed, 0x76, 0xec, 0xcd, 0x31, 0x82, 0x1e },
|
||||
.name = "MfgDisplayCalibration",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
25
src/fw/apps/prf_apps/mfg_display_calibration_app.h
Normal file
25
src/fw/apps/prf_apps/mfg_display_calibration_app.h
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
//! @file mfg_display_calibration_app.h
|
||||
//!
|
||||
//! Test app for display calibration
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_display_calibration_app_get_info(void);
|
||||
141
src/fw/apps/prf_apps/mfg_hrm_app.c
Normal file
141
src/fw/apps/prf_apps/mfg_hrm_app.c
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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_hrm_app.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "applib/ui/window_private.h"
|
||||
#include "drivers/accel.h"
|
||||
#include "drivers/hrm.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "kernel/util/sleep.h"
|
||||
#include "mfg/mfg_info.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "process_management/process_manager.h"
|
||||
#include "services/common/evented_timer.h"
|
||||
#include "services/common/hrm/hrm_manager.h"
|
||||
#include "util/bitset.h"
|
||||
#include "util/size.h"
|
||||
#include "util/trig.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#if CAPABILITY_HAS_BUILTIN_HRM
|
||||
|
||||
#define STATUS_STRING_LEN 32
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
EventServiceInfo hrm_event_info;
|
||||
|
||||
TextLayer title_text_layer;
|
||||
TextLayer status_text_layer;
|
||||
char status_string[STATUS_STRING_LEN];
|
||||
HRMSessionRef hrm_session;
|
||||
} AppData;
|
||||
|
||||
static void prv_handle_hrm_data(PebbleEvent *e, void *context) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
if (e->type == PEBBLE_HRM_EVENT) {
|
||||
snprintf(app_data->status_string, STATUS_STRING_LEN,
|
||||
"TIA: %"PRIu16"\nLED: %"PRIu16" mA", e->hrm.led.tia, e->hrm.led.current_ua);
|
||||
layer_mark_dirty(&app_data->window.layer);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_handle_init(void) {
|
||||
const bool has_hrm = mfg_info_is_hrm_present();
|
||||
|
||||
AppData *data = task_zalloc(sizeof(*data));
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, "");
|
||||
window_set_fullscreen(window, true);
|
||||
|
||||
TextLayer *title = &data->title_text_layer;
|
||||
text_layer_init(title, &window->layer.bounds);
|
||||
text_layer_set_font(title, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text_alignment(title, GTextAlignmentCenter);
|
||||
text_layer_set_text(title, "HRM TEST");
|
||||
layer_add_child(&window->layer, &title->layer);
|
||||
|
||||
if (has_hrm) {
|
||||
sniprintf(data->status_string, STATUS_STRING_LEN, "Starting...");
|
||||
} else {
|
||||
sniprintf(data->status_string, STATUS_STRING_LEN, "Not an HRM device");
|
||||
}
|
||||
TextLayer *status = &data->status_text_layer;
|
||||
text_layer_init(status,
|
||||
&GRect(5, 40, window->layer.bounds.size.w - 5, window->layer.bounds.size.h - 40));
|
||||
text_layer_set_font(status, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text_alignment(status, GTextAlignmentCenter);
|
||||
text_layer_set_text(status, data->status_string);
|
||||
layer_add_child(&window->layer, &status->layer);
|
||||
|
||||
|
||||
if (has_hrm) {
|
||||
data->hrm_event_info = (EventServiceInfo){
|
||||
.type = PEBBLE_HRM_EVENT,
|
||||
.handler = prv_handle_hrm_data,
|
||||
};
|
||||
event_service_client_subscribe(&data->hrm_event_info);
|
||||
|
||||
// Use app data as session ref
|
||||
AppInstallId app_id = 1;
|
||||
data->hrm_session = sys_hrm_manager_app_subscribe(app_id, 1, SECONDS_PER_HOUR,
|
||||
HRMFeature_LEDCurrent);
|
||||
}
|
||||
|
||||
app_window_stack_push(window, true);
|
||||
}
|
||||
|
||||
static void prv_handle_deinit(void) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
event_service_client_unsubscribe(&data->hrm_event_info);
|
||||
if (mfg_info_is_hrm_present()) {
|
||||
sys_hrm_manager_unsubscribe(data->hrm_session);
|
||||
}
|
||||
|
||||
text_layer_deinit(&data->title_text_layer);
|
||||
text_layer_deinit(&data->status_text_layer);
|
||||
window_deinit(&data->window);
|
||||
app_free(data);
|
||||
}
|
||||
|
||||
static void prv_main(void) {
|
||||
prv_handle_init();
|
||||
app_event_loop();
|
||||
prv_handle_deinit();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_hrm_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &prv_main,
|
||||
.name = "MfgHRM",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
|
||||
#endif // CAPABILITY_HAS_BUILTIN_HRM
|
||||
21
src/fw/apps/prf_apps/mfg_hrm_app.h
Normal file
21
src/fw/apps/prf_apps/mfg_hrm_app.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_hrm_app_get_info(void);
|
||||
284
src/fw/apps/prf_apps/mfg_menu_app.c
Normal file
284
src/fw/apps/prf_apps/mfg_menu_app.c
Normal file
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* 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 "applib/app.h"
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/ui/ui.h"
|
||||
#include "applib/ui/window_private.h"
|
||||
#include "apps/prf_apps/mfg_accel_app.h"
|
||||
#include "apps/prf_apps/mfg_als_app.h"
|
||||
#include "apps/prf_apps/mfg_bt_sig_rf_app.h"
|
||||
#include "apps/prf_apps/mfg_btle_app.h"
|
||||
#include "apps/prf_apps/mfg_button_app.h"
|
||||
#include "apps/prf_apps/mfg_certification_app.h"
|
||||
#include "apps/prf_apps/mfg_display_app.h"
|
||||
#include "apps/prf_apps/mfg_hrm_app.h"
|
||||
#include "apps/prf_apps/mfg_runin_app.h"
|
||||
#include "apps/prf_apps/mfg_vibe_app.h"
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "kernel/util/standby.h"
|
||||
#include "mfg/mfg_info.h"
|
||||
#include "mfg/mfg_serials.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/bluetooth/local_id.h"
|
||||
#include "services/common/bluetooth/pairability.h"
|
||||
#include "system/reset.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#if PBL_ROUND
|
||||
#include "apps/prf_apps/mfg_display_calibration_app.h"
|
||||
#endif
|
||||
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
Window *window;
|
||||
SimpleMenuLayer *menu_layer;
|
||||
SimpleMenuSection menu_section;
|
||||
} MfgMenuAppData;
|
||||
|
||||
static uint16_t s_menu_position = 0;
|
||||
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
static GBitmap *s_menu_icons[2];
|
||||
#define ICON_IDX_CHECK 0
|
||||
#define ICON_IDX_X 1
|
||||
#endif
|
||||
|
||||
//! Callback to run from the kernel main task
|
||||
static void prv_launch_app_cb(void *data) {
|
||||
app_manager_launch_new_app(&(AppLaunchConfig) { .md = data });
|
||||
}
|
||||
|
||||
#if PBL_ROUND
|
||||
static void prv_select_calibrate_display(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_display_calibration_app_get_info());
|
||||
}
|
||||
#endif
|
||||
|
||||
static void prv_select_accel(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_accel_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_button(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_button_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_display(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_display_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_runin(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_runin_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_vibe(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_vibe_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_als(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_als_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_bt_sig_rf(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_bt_sig_rf_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_hrm(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_hrm_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_certification(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_certification_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_btle(int index, void *context) {
|
||||
launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_btle_app_get_info());
|
||||
}
|
||||
|
||||
static void prv_select_reset(int index, void *context) {
|
||||
system_reset();
|
||||
}
|
||||
|
||||
static void prv_select_shutdown(int index, void *context) {
|
||||
enter_standby(RebootReasonCode_ShutdownMenuItem);
|
||||
}
|
||||
|
||||
static GBitmap * prv_get_icon_for_test(MfgTest test) {
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
const bool passed = mfg_info_get_test_result(test);
|
||||
if (passed) {
|
||||
return s_menu_icons[ICON_IDX_CHECK];
|
||||
}
|
||||
return s_menu_icons[ICON_IDX_X];
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void prv_load_icons(void) {
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
// The icons in resources are black boxes with either a white checkmark or X.
|
||||
// In order to make them look correct in the way we are using them, we want to
|
||||
// invert the icons so that they are black icon on a white background.
|
||||
//
|
||||
// To do this, load each resource temporarily and then create two new bitmaps.
|
||||
// Then bitblt the original resource into the new bitmap using GCompOpAssignInverted.
|
||||
|
||||
const uint32_t icon_id[] = { RESOURCE_ID_ACTION_BAR_ICON_CHECK, RESOURCE_ID_ACTION_BAR_ICON_X };
|
||||
|
||||
for (unsigned i = 0; i < ARRAY_LENGTH(icon_id); ++i) {
|
||||
GBitmap tmp;
|
||||
gbitmap_init_with_resource(&tmp, icon_id[i]);
|
||||
|
||||
GBitmap *icon = gbitmap_create_blank(tmp.bounds.size, tmp.info.format);
|
||||
bitblt_bitmap_into_bitmap(icon, &tmp, GPointZero, GCompOpAssignInverted, GColorBlack);
|
||||
|
||||
s_menu_icons[i] = icon;
|
||||
gbitmap_deinit(&tmp);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
//! @param[out] out_items
|
||||
static size_t prv_create_menu_items(SimpleMenuItem** out_menu_items) {
|
||||
prv_load_icons();
|
||||
|
||||
// Define a const blueprint on the stack.
|
||||
const SimpleMenuItem s_menu_items[] = {
|
||||
{ .title = "BT Device Name" },
|
||||
{ .title = "Device Serial" },
|
||||
#if PBL_ROUND
|
||||
{ .title = "Calibrate Display", .callback = prv_select_calibrate_display },
|
||||
#endif
|
||||
{ .title = "Test Accel", .callback = prv_select_accel },
|
||||
{ .icon = prv_get_icon_for_test(MfgTest_Buttons),
|
||||
.title = "Test Buttons", .callback = prv_select_button },
|
||||
{ .icon = prv_get_icon_for_test(MfgTest_Display),
|
||||
.title = "Test Display", .callback = prv_select_display },
|
||||
{ .title = "Test Runin", .callback = prv_select_runin },
|
||||
{ .icon = prv_get_icon_for_test(MfgTest_Vibe),
|
||||
.title = "Test Vibe", .callback = prv_select_vibe },
|
||||
{ .icon = prv_get_icon_for_test(MfgTest_ALS),
|
||||
.title = "Test ALS", .callback = prv_select_als },
|
||||
#if !PLATFORM_SILK
|
||||
{ .title = "Test bt_sig_rf", .callback = prv_select_bt_sig_rf },
|
||||
#endif
|
||||
#if CAPABILITY_HAS_BUILTIN_HRM
|
||||
{ .title = "Test HRM", .callback = prv_select_hrm },
|
||||
#endif
|
||||
#if BT_CONTROLLER_DA14681
|
||||
{ .title = "Test BTLE", .callback = prv_select_btle },
|
||||
#endif
|
||||
{ .title = "Certification", .callback = prv_select_certification },
|
||||
{ .title = "Reset", .callback = prv_select_reset },
|
||||
{ .title = "Shutdown", .callback = prv_select_shutdown }
|
||||
};
|
||||
|
||||
// Copy it into the heap so we can modify it.
|
||||
*out_menu_items = app_malloc(sizeof(s_menu_items));
|
||||
memcpy(*out_menu_items, s_menu_items, sizeof(s_menu_items));
|
||||
|
||||
// Now we're going to modify the first two elements in the menu to include data available only
|
||||
// at runtime. If it was available at compile time we could have just shoved it in the
|
||||
// s_menu_items array but it's not. Note that we allocate a few buffers here that we never
|
||||
// bother freeing for simplicity. It's all on the app heap so it will automatically get cleaned
|
||||
// up on app exit.
|
||||
|
||||
// Poke in the bluetooth name
|
||||
int buffer_size = BT_DEVICE_NAME_BUFFER_SIZE;
|
||||
char *bt_dev_name = app_malloc(buffer_size);
|
||||
bt_local_id_copy_device_name(bt_dev_name, false);
|
||||
|
||||
(*out_menu_items)[0].subtitle = bt_dev_name;
|
||||
|
||||
// Poke in the serial number
|
||||
buffer_size = MFG_SERIAL_NUMBER_SIZE + 1;
|
||||
char *device_serial = app_malloc(buffer_size);
|
||||
mfg_info_get_serialnumber(device_serial, buffer_size);
|
||||
|
||||
(*out_menu_items)[1].subtitle = device_serial;
|
||||
|
||||
// We've now populated out_menu_items with the correct data. Return the number of items by
|
||||
// looking at the original list of menu items.
|
||||
return ARRAY_LENGTH(s_menu_items);
|
||||
}
|
||||
|
||||
static void prv_window_load(Window *window) {
|
||||
MfgMenuAppData *data = app_state_get_user_data();
|
||||
|
||||
Layer *window_layer = window_get_root_layer(data->window);
|
||||
GRect bounds = window_layer->bounds;
|
||||
#ifdef PLATFORM_SPALDING
|
||||
bounds.origin.x += 25;
|
||||
bounds.origin.y += 25;
|
||||
bounds.size.w -= 50;
|
||||
bounds.size.h -= 25;
|
||||
#endif
|
||||
|
||||
SimpleMenuItem* menu_items;
|
||||
size_t num_items = prv_create_menu_items(&menu_items);
|
||||
|
||||
data->menu_section = (SimpleMenuSection) {
|
||||
.num_items = num_items,
|
||||
.items = menu_items
|
||||
};
|
||||
|
||||
data->menu_layer = simple_menu_layer_create(bounds, data->window, &data->menu_section, 1, NULL);
|
||||
layer_add_child(window_layer, simple_menu_layer_get_layer(data->menu_layer));
|
||||
|
||||
// Set the menu layer back to it's previous highlight position
|
||||
simple_menu_layer_set_selected_index(data->menu_layer, s_menu_position, false);
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
bt_pairability_use();
|
||||
|
||||
MfgMenuAppData *data = app_malloc_check(sizeof(MfgMenuAppData));
|
||||
*data = (MfgMenuAppData){};
|
||||
|
||||
app_state_set_user_data(data);
|
||||
|
||||
data->window = window_create();
|
||||
window_init(data->window, "");
|
||||
window_set_window_handlers(data->window, &(WindowHandlers) {
|
||||
.load = prv_window_load,
|
||||
});
|
||||
window_set_overrides_back_button(data->window, true);
|
||||
window_set_fullscreen(data->window, true);
|
||||
app_window_stack_push(data->window, true /*animated*/);
|
||||
|
||||
app_event_loop();
|
||||
|
||||
bt_pairability_release();
|
||||
|
||||
s_menu_position = simple_menu_layer_get_selected_index(data->menu_layer);
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_menu_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &s_main,
|
||||
// UUID: ddfdf403-664e-47dd-a620-b1a14ce2b59b
|
||||
.common.uuid = { 0xdd, 0xfd, 0xf4, 0x03, 0x66, 0x4e, 0x47, 0xdd,
|
||||
0xa6, 0x20, 0xb1, 0xa1, 0x4c, 0xe2, 0xb5, 0x9b },
|
||||
.name = "MfgMenu",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
|
||||
22
src/fw/apps/prf_apps/mfg_menu_app.h
Normal file
22
src/fw/apps/prf_apps/mfg_menu_app.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_menu_app_get_info(void);
|
||||
|
||||
243
src/fw/apps/prf_apps/mfg_runin_app.c
Normal file
243
src/fw/apps/prf_apps/mfg_runin_app.c
Normal file
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* 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_runin_app.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "applib/ui/ui.h"
|
||||
#include "applib/ui/window_private.h"
|
||||
#include "drivers/battery.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "services/common/battery/battery_curve.h"
|
||||
#include "system/logging.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
typedef enum {
|
||||
RuninStateStart = 0,
|
||||
RuninStatePlugCharger,
|
||||
RuninStateRunning,
|
||||
RuninStatePass,
|
||||
RuninStateFail,
|
||||
} RuninTestState;
|
||||
|
||||
static const char* status_text[] = {
|
||||
[RuninStateStart] = "Start",
|
||||
[RuninStatePlugCharger] = "Plug Charger",
|
||||
[RuninStateRunning] = "Running...",
|
||||
[RuninStatePass] = "Pass",
|
||||
[RuninStateFail] = "Fail",
|
||||
};
|
||||
|
||||
#ifdef PLATFORM_TINTIN
|
||||
static const int SLOW_THRESHOLD_PERCENTAGE = 42; // ~3850mv
|
||||
static const int PASS_BATTERY_PERCENTAGE = 84; // ~4050mv
|
||||
#else
|
||||
static const int SLOW_THRESHOLD_PERCENTAGE = 0; // Always go "slow" on snowy
|
||||
static const int PASS_BATTERY_PERCENTAGE = 60; // ~4190mv
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
|
||||
TextLayer status;
|
||||
char status_string[20];
|
||||
|
||||
TextLayer details;
|
||||
char details_string[45];
|
||||
|
||||
RuninTestState test_state;
|
||||
uint32_t seconds_remaining;
|
||||
bool countdown_running;
|
||||
bool fastcharge_enabled;
|
||||
|
||||
int pass_count;
|
||||
} AppData;
|
||||
|
||||
static void prv_handle_second_tick(struct tm *tick_time, TimeUnits units_changed) {
|
||||
AppData *data = app_state_get_user_data();
|
||||
|
||||
RuninTestState next_state = data->test_state;
|
||||
|
||||
const int charge_mv = battery_get_millivolts();
|
||||
const int charge_percent = battery_curve_lookup_percent_by_voltage(charge_mv,
|
||||
battery_charge_controller_thinks_we_are_charging());
|
||||
const int usb_is_connected = battery_is_usb_connected();
|
||||
|
||||
switch (data->test_state) {
|
||||
case RuninStateStart:
|
||||
if (usb_is_connected) {
|
||||
next_state = RuninStateRunning;
|
||||
} else {
|
||||
next_state = RuninStatePlugCharger;
|
||||
}
|
||||
break;
|
||||
case RuninStatePlugCharger:
|
||||
if (usb_is_connected) {
|
||||
next_state = RuninStateRunning;
|
||||
}
|
||||
break;
|
||||
case RuninStateRunning:
|
||||
if (!data->countdown_running) {
|
||||
data->countdown_running = true;
|
||||
}
|
||||
if (!usb_is_connected) {
|
||||
data->pass_count = 0;
|
||||
next_state = RuninStatePlugCharger;
|
||||
break;
|
||||
}
|
||||
if (charge_percent > SLOW_THRESHOLD_PERCENTAGE && data->fastcharge_enabled) {
|
||||
// go slow for a bit
|
||||
battery_set_fast_charge(false);
|
||||
data->fastcharge_enabled = false;
|
||||
} else if (charge_percent > PASS_BATTERY_PERCENTAGE) {
|
||||
// The reading can be a bit shaky in the short term (i.e. a flaky USB connection), or we
|
||||
// just started charging. Make sure we have settled before transitioning into the
|
||||
// RuninStatePass state
|
||||
if (data->pass_count > 5) {
|
||||
next_state = RuninStatePass;
|
||||
|
||||
data->countdown_running = false;
|
||||
// disable the charger so that we don't overcharge the battery
|
||||
battery_set_charge_enable(false);
|
||||
}
|
||||
data->pass_count++;
|
||||
} else {
|
||||
data->pass_count = 0;
|
||||
}
|
||||
break;
|
||||
case RuninStatePass:
|
||||
case RuninStateFail:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (data->countdown_running) {
|
||||
--data->seconds_remaining;
|
||||
if (data->seconds_remaining == 0) {
|
||||
// Time's up!
|
||||
next_state = RuninStateFail;
|
||||
data->countdown_running = false;
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Failed runin testing");
|
||||
}
|
||||
}
|
||||
|
||||
data->test_state = next_state;
|
||||
|
||||
sniprintf(data->status_string, sizeof(data->status_string),
|
||||
"RUNIN\n%s", status_text[data->test_state]);
|
||||
text_layer_set_text(&data->status, data->status_string);
|
||||
|
||||
int mins_remaining = data->seconds_remaining / 60;
|
||||
int secs_remaining = data->seconds_remaining % 60;
|
||||
sniprintf(data->details_string, sizeof(data->details_string),
|
||||
"Time:%02u:%02u\r\n%umV (%"PRIu8"%%)\r\nUSB: %s",
|
||||
mins_remaining, secs_remaining, charge_mv,
|
||||
charge_percent, usb_is_connected ? "yes":"no");
|
||||
text_layer_set_text(&data->details, data->details_string);
|
||||
}
|
||||
|
||||
static void prv_back_click_handler(ClickRecognizerRef recognizer, void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
if (!app_data->countdown_running &&
|
||||
(app_data->test_state == RuninStateStart ||
|
||||
app_data->test_state == RuninStatePlugCharger)) {
|
||||
|
||||
// if the test has not yet started, it is ok to push the back button to leave.
|
||||
app_window_stack_pop(true);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_select_click_handler(ClickRecognizerRef recognizer, void *data) {
|
||||
AppData *app_data = app_state_get_user_data();
|
||||
|
||||
if (app_data->test_state == RuninStateFail || app_data->test_state == RuninStatePass) {
|
||||
// we've finished the runin test - long-press to close the app
|
||||
app_window_stack_pop(true);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_config_provider(void *data) {
|
||||
window_long_click_subscribe(BUTTON_ID_SELECT, 3000, NULL, prv_select_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_BACK, prv_back_click_handler);
|
||||
}
|
||||
|
||||
static void app_init(void) {
|
||||
AppData *data = app_malloc_check(sizeof(AppData));
|
||||
|
||||
app_state_set_user_data(data);
|
||||
|
||||
*data = (AppData) {
|
||||
.test_state = RuninStateStart,
|
||||
.countdown_running = false,
|
||||
.seconds_remaining = 5400, //1.5h
|
||||
.fastcharge_enabled = true,
|
||||
.pass_count = 0,
|
||||
};
|
||||
|
||||
battery_set_fast_charge(true);
|
||||
battery_set_charge_enable(true);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, "Runin Test");
|
||||
// NF: Quanta wants this app to prevent resetting. I think it is overly restrictive
|
||||
// but they claim that it will minimize operator error if there is only one path
|
||||
// that can be followed.
|
||||
window_set_overrides_back_button(window, true);
|
||||
|
||||
TextLayer *status = &data->status;
|
||||
text_layer_init(status, &window->layer.bounds);
|
||||
text_layer_set_font(status, fonts_get_system_font(FONT_KEY_GOTHIC_24));
|
||||
text_layer_set_text_alignment(status, GTextAlignmentCenter);
|
||||
text_layer_set_text(status, status_text[data->test_state]);
|
||||
layer_add_child(&window->layer, &status->layer);
|
||||
|
||||
TextLayer *details = &data->details;
|
||||
text_layer_init(details,
|
||||
&GRect(0, 65, window->layer.bounds.size.w, window->layer.bounds.size.h - 65));
|
||||
text_layer_set_font(details, fonts_get_system_font(FONT_KEY_GOTHIC_24));
|
||||
text_layer_set_text_alignment(details, GTextAlignmentCenter);
|
||||
layer_add_child(&window->layer, &details->layer);
|
||||
|
||||
window_set_click_config_provider(window, prv_config_provider);
|
||||
window_set_fullscreen(window, true);
|
||||
|
||||
tick_timer_service_subscribe(SECOND_UNIT, prv_handle_second_tick);
|
||||
|
||||
app_window_stack_push(window, true /* Animated */);
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
app_init();
|
||||
|
||||
app_event_loop();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_runin_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &s_main,
|
||||
// UUID: fbb6d0e6-2d7d-40bc-8b01-f2f8beb9c394
|
||||
.common.uuid = { 0xfb, 0xb6, 0xd0, 0xe6, 0x2d, 0x7d, 0x40, 0xbc,
|
||||
0x8b, 0x01, 0xf2, 0xf8, 0xbe, 0xb9, 0xc3, 0x94 },
|
||||
.name = "Runin App",
|
||||
};
|
||||
|
||||
return (PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
|
||||
23
src/fw/apps/prf_apps/mfg_runin_app.h
Normal file
23
src/fw/apps/prf_apps/mfg_runin_app.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_runin_app_get_info(void);
|
||||
|
||||
103
src/fw/apps/prf_apps/mfg_vibe_app.c
Normal file
103
src/fw/apps/prf_apps/mfg_vibe_app.c
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 "applib/app.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/vibes.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "mfg/mfg_info.h"
|
||||
#include "mfg/results_ui.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
|
||||
TextLayer title;
|
||||
|
||||
//! How many times we've vibrated
|
||||
int vibe_count;
|
||||
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
MfgResultsUI results_ui;
|
||||
#endif
|
||||
} AppData;
|
||||
|
||||
static void prv_handle_second_tick(struct tm *tick_time, TimeUnits units_changed) {
|
||||
#if !MFG_INFO_RECORDS_TEST_RESULTS
|
||||
AppData *data = app_state_get_user_data();
|
||||
|
||||
const int MAX_VIBE_COUNT = 5;
|
||||
if (data->vibe_count >= MAX_VIBE_COUNT) {
|
||||
// We've vibed the number of times we wanted to, time to leave!
|
||||
app_window_stack_pop(true /* animated */);
|
||||
return;
|
||||
}
|
||||
|
||||
++data->vibe_count;
|
||||
#endif
|
||||
|
||||
vibes_short_pulse();
|
||||
}
|
||||
|
||||
static void prv_handle_init(void) {
|
||||
AppData *data = app_malloc_check(sizeof(AppData));
|
||||
*data = (AppData) {
|
||||
.vibe_count = 0
|
||||
};
|
||||
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, "");
|
||||
window_set_fullscreen(window, true);
|
||||
|
||||
TextLayer *title = &data->title;
|
||||
text_layer_init(title, &window->layer.bounds);
|
||||
text_layer_set_font(title, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
||||
text_layer_set_text_alignment(title, GTextAlignmentCenter);
|
||||
text_layer_set_text(title, "VIBE TEST");
|
||||
layer_add_child(&window->layer, &title->layer);
|
||||
|
||||
#if MFG_INFO_RECORDS_TEST_RESULTS
|
||||
mfg_results_ui_init(&data->results_ui, MfgTest_Vibe, window);
|
||||
#endif
|
||||
|
||||
app_window_stack_push(window, true /* Animated */);
|
||||
|
||||
tick_timer_service_subscribe(SECOND_UNIT, prv_handle_second_tick);
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
prv_handle_init();
|
||||
|
||||
app_event_loop();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* mfg_vibe_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common.main_func = &s_main,
|
||||
// UUID: f676085a-b130-4492-b6a1-85492602ba00
|
||||
.common.uuid = { 0xf6, 0x76, 0x08, 0x5a, 0xb1, 0x30, 0x44, 0x92,
|
||||
0xb6, 0xa1, 0x85, 0x49, 0x26, 0x02, 0xba, 0x00 },
|
||||
.name = "MfgVibe",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
|
||||
26
src/fw/apps/prf_apps/mfg_vibe_app.h
Normal file
26
src/fw/apps/prf_apps/mfg_vibe_app.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
//! @file mfg_vibe_app.h
|
||||
//!
|
||||
//! Boring test app that vibes 5 times and quits
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* mfg_vibe_app_get_info(void);
|
||||
|
||||
149
src/fw/apps/prf_apps/prf_low_power_app.c
Normal file
149
src/fw/apps/prf_apps/prf_low_power_app.c
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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 "applib/ui/ui.h"
|
||||
#include "applib/app.h"
|
||||
#include "applib/app_timer.h"
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/kino/kino_layer.h"
|
||||
#include "applib/ui/kino/kino_reel.h"
|
||||
#include "applib/ui/layer.h"
|
||||
#include "applib/ui/window_private.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/i18n/i18n.h"
|
||||
|
||||
|
||||
#define LOW_POWER_APP_STATE_UPDATE_TIME_MS 2000
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
KinoLayer kino_layer;
|
||||
GRect charging_kino_area;
|
||||
GRect discharging_kino_area;
|
||||
BatteryChargeState saved_state;
|
||||
AppTimer *timer;
|
||||
} LowPowerAppData;
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Update Logic
|
||||
|
||||
static void prv_refresh_state(void *data_in) {
|
||||
LowPowerAppData *data = (LowPowerAppData*) data_in;
|
||||
BatteryChargeState current_state = battery_get_charge_state();
|
||||
uint32_t res_id;
|
||||
GRect kino_area;
|
||||
|
||||
if (current_state.is_charging && !data->saved_state.is_charging) {
|
||||
kino_area = data->charging_kino_area;
|
||||
res_id = RESOURCE_ID_RECOVERY_LOW_POWER_CHARGING;
|
||||
} else if (!current_state.is_charging && data->saved_state.is_charging) {
|
||||
kino_area = data->discharging_kino_area;
|
||||
res_id = RESOURCE_ID_RECOVERY_LOW_POWER_DISCHARGING;
|
||||
} else {
|
||||
goto reschedule;
|
||||
}
|
||||
|
||||
layer_set_frame((Layer *) &data->kino_layer, &kino_area);
|
||||
kino_layer_set_reel_with_resource(&data->kino_layer, res_id);
|
||||
layer_mark_dirty(&data->kino_layer.layer);
|
||||
|
||||
reschedule:
|
||||
data->saved_state = current_state;
|
||||
data->timer = app_timer_register(LOW_POWER_APP_STATE_UPDATE_TIME_MS, prv_refresh_state, data);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Window loading, unloading, initializing
|
||||
|
||||
static void prv_window_unload_handler(Window* window) {
|
||||
LowPowerAppData *data = window_get_user_data(window);
|
||||
if (!data) {
|
||||
// Sanity check
|
||||
return;
|
||||
}
|
||||
|
||||
kino_layer_deinit(&data->kino_layer);
|
||||
app_timer_cancel(data->timer);
|
||||
app_free(data);
|
||||
}
|
||||
|
||||
static void prv_window_load_handler(Window* window) {
|
||||
LowPowerAppData *data = window_get_user_data(window);
|
||||
|
||||
data->discharging_kino_area = GRect(PBL_IF_RECT_ELSE(4, 5),
|
||||
PBL_IF_RECT_ELSE(2, 4),
|
||||
data->window.layer.bounds.size.w,
|
||||
data->window.layer.bounds.size.h);
|
||||
data->charging_kino_area = GRect(0, 0, data->window.layer.bounds.size.w,
|
||||
data->window.layer.bounds.size.h);
|
||||
|
||||
kino_layer_init(&data->kino_layer, &data->discharging_kino_area);
|
||||
kino_layer_set_reel_with_resource(&data->kino_layer, RESOURCE_ID_RECOVERY_LOW_POWER_DISCHARGING);
|
||||
layer_add_child(&window->layer, &data->kino_layer.layer);
|
||||
|
||||
data->timer = app_timer_register(LOW_POWER_APP_STATE_UPDATE_TIME_MS, prv_refresh_state, data);
|
||||
}
|
||||
|
||||
static void prv_prf_low_power_app_window_push(void) {
|
||||
LowPowerAppData *data = app_malloc_check(sizeof(LowPowerAppData));
|
||||
|
||||
*data = (LowPowerAppData){};
|
||||
|
||||
Window* window = &data->window;
|
||||
window_init(window, WINDOW_NAME("Low Power App"));
|
||||
window_set_user_data(window, data);
|
||||
window_set_overrides_back_button(window, true);
|
||||
window_set_fullscreen(window, true);
|
||||
window_set_background_color(window, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite));
|
||||
window_set_window_handlers(window, &(WindowHandlers){
|
||||
.load = prv_window_load_handler,
|
||||
.unload = prv_window_unload_handler,
|
||||
});
|
||||
app_window_stack_push(window, false);
|
||||
}
|
||||
|
||||
static void s_main(void) {
|
||||
launcher_block_popups(true);
|
||||
|
||||
prv_prf_low_power_app_window_push();
|
||||
|
||||
app_event_loop();
|
||||
|
||||
launcher_block_popups(false);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Public functions
|
||||
|
||||
const PebbleProcessMd* prf_low_power_app_get_info() {
|
||||
static const PebbleProcessMdSystem s_app_info = {
|
||||
.common = {
|
||||
.main_func = &s_main,
|
||||
.visibility = ProcessVisibilityHidden,
|
||||
// UUID: f29f18ac-bbec-452b-9262-49b5f6e5c920
|
||||
.uuid = {0xf2, 0x9f, 0x18, 0xac, 0xbb, 0xec, 0x45, 0x2b,
|
||||
0x92, 0x62, 0x49, 0xb5, 0xf6, 0xe5, 0xc9, 0x20},
|
||||
},
|
||||
.name = "Low Power App",
|
||||
.run_level = ProcessAppRunLevelSystem,
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_info;
|
||||
}
|
||||
21
src/fw/apps/prf_apps/prf_low_power_app.h
Normal file
21
src/fw/apps/prf_apps/prf_low_power_app.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* prf_low_power_app_get_info();
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 "getting_started_button_combo.h"
|
||||
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/graphics/gtypes.h"
|
||||
|
||||
#include "apps/core_apps/spinner_ui_window.h"
|
||||
#include "kernel/util/factory_reset.h"
|
||||
#include "mfg/mfg_mode/mfg_factory_mode.h"
|
||||
#include "process_management/process_manager.h"
|
||||
#include "services/common/system_task.h"
|
||||
#if CAPABILITY_HAS_ACCESSORY_CONNECTOR
|
||||
#include "services/prf/accessory/accessory_imaging.h"
|
||||
#endif
|
||||
#include "system/logging.h"
|
||||
#include "util/size.h"
|
||||
|
||||
void getting_started_button_combo_init(GettingStartedButtonComboState *state,
|
||||
GettingStartedButtonComboCallback select_callback) {
|
||||
*state = (GettingStartedButtonComboState) {
|
||||
.buttons_held_bitset = 0,
|
||||
.combo_timer = new_timer_create(),
|
||||
.select_callback = select_callback
|
||||
};
|
||||
}
|
||||
|
||||
void getting_started_button_combo_deinit(GettingStartedButtonComboState *state) {
|
||||
new_timer_delete(state->combo_timer);
|
||||
}
|
||||
|
||||
static void prv_factory_reset(void *not_used) {
|
||||
factory_reset(false /* should_shutdown */);
|
||||
}
|
||||
|
||||
static void prv_down_cb(void *data) {
|
||||
Window *spinner_window = spinner_ui_window_get(PBL_IF_COLOR_ELSE(GColorBlue, GColorDarkGray));
|
||||
app_window_stack_push(spinner_window, false /* animated */);
|
||||
|
||||
// Factory reset on KernelBG so the animation gets priority
|
||||
system_task_add_callback(prv_factory_reset, NULL);
|
||||
}
|
||||
|
||||
#ifdef RECOVERY_FW
|
||||
static void prv_mfg_mode_cb(void *data) {
|
||||
#if CAPABILITY_HAS_ACCESSORY_CONNECTOR
|
||||
accessory_imaging_enable();
|
||||
#endif
|
||||
mfg_enter_mfg_mode_and_launch_app();
|
||||
}
|
||||
#endif
|
||||
|
||||
static void prv_timeout_expired(void *data) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Button combo timeout expired!");
|
||||
|
||||
// Timeout expired, jump over the app thread to do the thing.
|
||||
void (*real_callback)(void*) = data;
|
||||
process_manager_send_callback_event_to_process(PebbleTask_App, real_callback, NULL);
|
||||
}
|
||||
|
||||
static void prv_update_state(GettingStartedButtonComboState *state) {
|
||||
const uint32_t COMBO_HOLD_MS = 5 * 1000; // Wait for 5 seconds
|
||||
|
||||
// Map of button combos -> callback to call if we hit it.
|
||||
const struct {
|
||||
uint8_t desired_bitset;
|
||||
void (*callback)(void*);
|
||||
} BUTTON_COMBOS[] = {
|
||||
{ (1 << BUTTON_ID_SELECT), state->select_callback },
|
||||
{ (1 << BUTTON_ID_DOWN), prv_down_cb },
|
||||
#ifdef RECOVERY_FW
|
||||
{ (1 << BUTTON_ID_UP) | (1 << BUTTON_ID_SELECT), prv_mfg_mode_cb },
|
||||
#endif
|
||||
};
|
||||
|
||||
for (unsigned int i = 0; i < ARRAY_LENGTH(BUTTON_COMBOS); ++i) {
|
||||
if (state->buttons_held_bitset == BUTTON_COMBOS[i].desired_bitset) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Starting timer for combo #%d", i);
|
||||
|
||||
new_timer_start(state->combo_timer, COMBO_HOLD_MS, prv_timeout_expired,
|
||||
BUTTON_COMBOS[i].callback, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Stopping combo timer");
|
||||
|
||||
// No combo found, cancel the timer. It's harmless to call this if the timer isn't running.
|
||||
new_timer_stop(state->combo_timer);
|
||||
}
|
||||
|
||||
void getting_started_button_combo_button_pressed(GettingStartedButtonComboState *state,
|
||||
ButtonId button_id) {
|
||||
bitset8_set(&state->buttons_held_bitset, button_id);
|
||||
prv_update_state(state);
|
||||
}
|
||||
|
||||
void getting_started_button_combo_button_released(GettingStartedButtonComboState *state,
|
||||
ButtonId button_id) {
|
||||
bitset8_clear(&state->buttons_held_bitset, button_id);
|
||||
prv_update_state(state);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "kernel/events.h"
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
#include "util/bitset.h"
|
||||
|
||||
//! @file prf_button_combo.h
|
||||
//!
|
||||
//! This file implements a monitor that watches which buttons are held down. This file looks for
|
||||
//! 3 combinations.
|
||||
//!
|
||||
//! 1) Select held for 5 seconds: Invoke a user provided callback.
|
||||
//! 2) Down held for 5 seconds: factory reset
|
||||
//! 3) (PRF ONLY) Up+Down held for 5 seconds: Enter mfg mode
|
||||
//!
|
||||
//! The reason it's not just a boring set of long click handlers is because we don't support
|
||||
//! registering a long click handler for a combination of buttons like up+down.
|
||||
//!
|
||||
//! I tried to split this out from a seperate file from the recovery_first_use.c file so I could
|
||||
//! test this behaviour in a unit test independant in the UI. I think it turned out /okay/. The
|
||||
//! callback specification is a little odd (only for select but not for the other ones, should we
|
||||
//! be blowing memory on static behaviour like this?) but it was worth a shot.
|
||||
|
||||
typedef void (*GettingStartedButtonComboCallback)(void *data);
|
||||
|
||||
typedef struct {
|
||||
//! Track which buttons are held. Use this instead of the driver function button_get_state_bits
|
||||
//! because that value isn't debounced.
|
||||
uint8_t buttons_held_bitset;
|
||||
|
||||
//! Timer for how long the combination has been held for. We use new_timer instead of app_timer
|
||||
//! even though it's a little more dangerous (doesn't automatically get cleaned up by the app)
|
||||
//! because the api is nicer for starting/stopping/resceduling the same timer over and over
|
||||
//! again with different callbacks.
|
||||
TimerID combo_timer;
|
||||
|
||||
//! The callback to call when select is held.
|
||||
GettingStartedButtonComboCallback select_callback;
|
||||
} GettingStartedButtonComboState;
|
||||
|
||||
//! Initialize resources associated with the state
|
||||
//! @param state the state to initialize
|
||||
//! @param select_callback the function to call when select is held
|
||||
void getting_started_button_combo_init(GettingStartedButtonComboState *state,
|
||||
GettingStartedButtonComboCallback select_callback);
|
||||
|
||||
//! Deallocate resources associated with the state
|
||||
void getting_started_button_combo_deinit(GettingStartedButtonComboState *state);
|
||||
|
||||
void getting_started_button_combo_button_pressed(GettingStartedButtonComboState *state,
|
||||
ButtonId button_id);
|
||||
|
||||
void getting_started_button_combo_button_released(GettingStartedButtonComboState *state,
|
||||
ButtonId button_id);
|
||||
@@ -0,0 +1,510 @@
|
||||
/*
|
||||
* 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 "recovery_first_use_app.h"
|
||||
|
||||
#include "getting_started_button_combo.h"
|
||||
|
||||
#include "apps/core_apps/spinner_ui_window.h"
|
||||
#include "applib/fonts/fonts.h"
|
||||
#include "comm/ble/gap_le_connection.h"
|
||||
#include "comm/ble/gap_le_device_name.h"
|
||||
#include "comm/ble/gap_le_connect.h"
|
||||
#include "drivers/backlight.h"
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "process_management/app_manager.h"
|
||||
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/event_service_client.h"
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/bitmap_layer.h"
|
||||
#include "applib/ui/kino/kino_layer.h"
|
||||
#include "applib/ui/kino/kino_reel.h"
|
||||
#include "applib/ui/layer.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "applib/ui/window_private.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
|
||||
#include "apps/system_apps/settings/settings_time.h"
|
||||
|
||||
#include "services/common/bluetooth/local_id.h"
|
||||
#include "services/common/bluetooth/pairability.h"
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "services/common/shared_prf_storage/shared_prf_storage.h"
|
||||
|
||||
#include "git_version.auto.h"
|
||||
|
||||
#include <bluetooth/classic_connect.h>
|
||||
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define URL_BUFFER_SIZE 32
|
||||
#define NAME_BUFFER_SIZE (BT_DEVICE_NAME_BUFFER_SIZE + 2)
|
||||
|
||||
typedef struct RecoveryFUAppData {
|
||||
Window launch_app_window;
|
||||
|
||||
KinoLayer kino_layer;
|
||||
|
||||
TextLayer url_text_layer;
|
||||
char url_text_buffer[URL_BUFFER_SIZE];
|
||||
bool is_showing_version;
|
||||
TextLayer name_text_layer;
|
||||
char name_text_buffer[NAME_BUFFER_SIZE];
|
||||
|
||||
AppTimer *spinner_close_timer;
|
||||
|
||||
//! Is the mobile app currently connected (comm session is up?)
|
||||
bool is_pebble_mobile_app_connected;
|
||||
//! Has the mobile app ever connected during this boot? Used to avoid flickering the layout
|
||||
//! for brief disconnects.
|
||||
bool has_pebble_mobile_app_connected;
|
||||
bool is_pairing_allowed;
|
||||
bool spinner_is_visible;
|
||||
bool spinner_should_close;
|
||||
|
||||
EventServiceInfo pebble_mobile_app_event_info;
|
||||
EventServiceInfo bt_connection_event_info;
|
||||
EventServiceInfo pebble_gather_logs_event_info;
|
||||
EventServiceInfo ble_device_name_updated_event_info;
|
||||
|
||||
GettingStartedButtonComboState button_combo_state;
|
||||
} RecoveryFUAppData;
|
||||
|
||||
// Unfortunately, the event_service_client_subscribe doesn't take a void *context...
|
||||
static RecoveryFUAppData *s_fu_app_data;
|
||||
|
||||
static void prv_update_name_text(RecoveryFUAppData *data);
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Spinner Logic
|
||||
|
||||
static void prv_pop_spinner(void *not_used) {
|
||||
if (s_fu_app_data && s_fu_app_data->spinner_should_close) {
|
||||
app_window_stack_pop(false /* animated */);
|
||||
s_fu_app_data->spinner_is_visible = false;
|
||||
s_fu_app_data->spinner_should_close = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_show_spinner(RecoveryFUAppData *data) {
|
||||
if (!data->spinner_is_visible) {
|
||||
Window *spinner_window = spinner_ui_window_get(PBL_IF_COLOR_ELSE(GColorRed, GColorDarkGray));
|
||||
app_window_stack_push(spinner_window, false /* animated */);
|
||||
}
|
||||
data->spinner_is_visible = true;
|
||||
data->spinner_should_close = false;
|
||||
}
|
||||
|
||||
static void prv_hide_spinner(RecoveryFUAppData *data) {
|
||||
data->spinner_should_close = true;
|
||||
data->spinner_close_timer = app_timer_register(3000, prv_pop_spinner, data);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Button Handlers
|
||||
static void prv_select_combo_callback(void *cb_data) {
|
||||
// When the user holds select for a long period of time, toggle between showing the help URL
|
||||
// and the version of the firmware.
|
||||
|
||||
RecoveryFUAppData *data = app_state_get_user_data();
|
||||
data->is_showing_version = !data->is_showing_version;
|
||||
|
||||
prv_update_name_text(data);
|
||||
}
|
||||
|
||||
static void prv_raw_down_handler(ClickRecognizerRef recognizer, void *context) {
|
||||
RecoveryFUAppData *data = app_state_get_user_data();
|
||||
|
||||
getting_started_button_combo_button_pressed(&data->button_combo_state,
|
||||
click_recognizer_get_button_id(recognizer));
|
||||
}
|
||||
|
||||
static void prv_raw_up_handler(ClickRecognizerRef recognizer, void *context) {
|
||||
RecoveryFUAppData *data = app_state_get_user_data();
|
||||
|
||||
getting_started_button_combo_button_released(&data->button_combo_state,
|
||||
click_recognizer_get_button_id(recognizer));
|
||||
}
|
||||
|
||||
static void prv_click_configure(void* context) {
|
||||
window_raw_click_subscribe(BUTTON_ID_UP, prv_raw_down_handler, prv_raw_up_handler, NULL);
|
||||
window_raw_click_subscribe(BUTTON_ID_SELECT, prv_raw_down_handler, prv_raw_up_handler, NULL);
|
||||
window_raw_click_subscribe(BUTTON_ID_DOWN, prv_raw_down_handler, prv_raw_up_handler, NULL);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Windows
|
||||
|
||||
static void prv_update_background_image_and_url_text(RecoveryFUAppData *data) {
|
||||
uint32_t icon_res_id;
|
||||
const char *url_string;
|
||||
GColor background;
|
||||
GRect kino_area;
|
||||
int16_t icon_x_offset;
|
||||
int16_t icon_y_offset;
|
||||
int16_t text_y_offset;
|
||||
|
||||
// Have we gone through first use before? If not, show first use UI. Otherwise show recovery UI.
|
||||
const bool first_use_is_complete = shared_prf_storage_get_getting_started_complete();
|
||||
|
||||
// Pick the right layout for the screen
|
||||
if (first_use_is_complete || data->has_pebble_mobile_app_connected) {
|
||||
// If first use was completed, it means we're in recovery mode.
|
||||
icon_res_id = RESOURCE_ID_LAUNCH_APP;
|
||||
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
|
||||
icon_x_offset = 41;
|
||||
icon_y_offset = -21;
|
||||
text_y_offset = 140;
|
||||
#else
|
||||
icon_x_offset = PBL_IF_RECT_ELSE(49, 67);
|
||||
icon_y_offset = 28;
|
||||
text_y_offset = 124;
|
||||
#endif
|
||||
} else {
|
||||
icon_res_id = RESOURCE_ID_MOBILE_APP_ICON;
|
||||
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
|
||||
icon_x_offset = 74;
|
||||
icon_y_offset = 56;
|
||||
text_y_offset = 121;
|
||||
#else
|
||||
icon_x_offset = PBL_IF_RECT_ELSE(49, 67);
|
||||
icon_y_offset = 38;
|
||||
text_y_offset = 90;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (first_use_is_complete) {
|
||||
#if PBL_BW && !PLATFORM_TINTIN
|
||||
// Override the icon to use the fullscreen version
|
||||
icon_res_id = RESOURCE_ID_RECOVERY_LAUNCH_APP;
|
||||
icon_x_offset = 0;
|
||||
icon_y_offset = 0;
|
||||
|
||||
url_string = ""; // URL is baked into the background image
|
||||
background = GColorWhite;
|
||||
#else
|
||||
url_string = "pebble.com/sos";
|
||||
background = PBL_IF_COLOR_ELSE(GColorRed, GColorWhite);;
|
||||
#endif
|
||||
} else {
|
||||
url_string = "pebble.com/app";
|
||||
background = PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite);
|
||||
}
|
||||
|
||||
// Create the icon
|
||||
KinoReel *icon_reel = kino_reel_create_with_resource(icon_res_id);
|
||||
if (!icon_reel) {
|
||||
PBL_CROAK("Couldn't create kino reel");
|
||||
}
|
||||
|
||||
// Position the icon
|
||||
kino_area = GRect(icon_x_offset, icon_y_offset, data->launch_app_window.layer.bounds.size.w,
|
||||
data->launch_app_window.layer.bounds.size.h);
|
||||
layer_set_frame((Layer *) &data->kino_layer, &kino_area);
|
||||
kino_layer_set_alignment(&data->kino_layer, GAlignTopLeft);
|
||||
window_set_background_color(&data->launch_app_window, background);
|
||||
|
||||
kino_layer_set_reel(&data->kino_layer, icon_reel, /* take_ownership */ true);
|
||||
|
||||
// Configure the url text layer
|
||||
data->url_text_layer.layer.frame.origin.y = text_y_offset;
|
||||
text_layer_set_text(&data->url_text_layer, url_string);
|
||||
}
|
||||
|
||||
static void prv_update_name_text(RecoveryFUAppData *data) {
|
||||
const GAPLEConnection *gap_conn = gap_le_connection_any();
|
||||
|
||||
// Set the name text
|
||||
if (data->is_showing_version) {
|
||||
strncpy(data->name_text_buffer, GIT_TAG, sizeof(data->name_text_buffer));
|
||||
} else if (bt_driver_classic_is_connected()) {
|
||||
// If BT Classic connected, show the name of the connected device
|
||||
bt_driver_classic_copy_connected_device_name(data->name_text_buffer);
|
||||
} else if ((comm_session_get_system_session() != NULL) && (gap_conn != NULL)) {
|
||||
// If we have connected to a device and we have a connection to the mobile app, show the device
|
||||
// name (we are required to have a connection to mobile app to get the name).
|
||||
gap_le_connection_copy_device_name(gap_conn, data->name_text_buffer,
|
||||
BT_DEVICE_NAME_BUFFER_SIZE);
|
||||
} else {
|
||||
// If we aren't connected and/or don't have a session, display the name of the device
|
||||
// so it's easier for a user to figure out what they should be trying to connect to
|
||||
bt_local_id_copy_device_name(data->name_text_buffer, false);
|
||||
|
||||
// For debugging purposes, we are going to add -'s to the beginning and end of the name
|
||||
// if we are connected to a BLE device but don't have a session
|
||||
if (gap_le_connect_is_connected_as_slave()) {
|
||||
memmove(&data->name_text_buffer[1], &data->name_text_buffer[0], BT_DEVICE_NAME_BUFFER_SIZE);
|
||||
data->name_text_buffer[0] = '-';
|
||||
strcat(data->name_text_buffer, "-");
|
||||
}
|
||||
}
|
||||
text_layer_set_text(&data->name_text_layer, data->name_text_buffer);
|
||||
|
||||
// Set the name font
|
||||
#if !PLATFORM_ROBERT && !PLATFORM_CALCULUS
|
||||
const bool first_use_is_complete = shared_prf_storage_get_getting_started_complete();
|
||||
const char *name_font_key;
|
||||
if (first_use_is_complete || data->has_pebble_mobile_app_connected || data->is_showing_version) {
|
||||
name_font_key = FONT_KEY_GOTHIC_14;
|
||||
} else {
|
||||
name_font_key = FONT_KEY_GOTHIC_24;
|
||||
}
|
||||
text_layer_set_font(&data->name_text_layer, fonts_get_system_font(name_font_key));
|
||||
#endif
|
||||
|
||||
// Update the size of the name text layer based on the new content.
|
||||
|
||||
// First set the text layer to be the width of the entire window and only a single line of text
|
||||
// high.
|
||||
layer_set_frame(&data->name_text_layer.layer,
|
||||
&(GRect) { { 0, 0 }, { data->launch_app_window.layer.frame.size.w, 26 } });
|
||||
|
||||
// Ask the text layer for a content size based on the frame we just set. If there's no text,
|
||||
// hide the layer by setting the size to zero.
|
||||
GSize content_size = { 0, 0 };
|
||||
if (strlen(data->name_text_layer.text)) {
|
||||
content_size = text_layer_get_content_size(app_state_get_graphics_context(),
|
||||
&data->name_text_layer);
|
||||
content_size.w += 4;
|
||||
content_size.h += 4;
|
||||
}
|
||||
|
||||
// Actually set the frame centered on the screen and just below the url_text_layer.
|
||||
const int16_t window_width = data->launch_app_window.layer.frame.size.w;
|
||||
const int16_t text_x_offset = (window_width - content_size.w) / 2;
|
||||
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
|
||||
const int16_t text_y_offset = 33;
|
||||
#else
|
||||
const int16_t text_y_offset = 22;
|
||||
#endif
|
||||
const GRect frame = { { text_x_offset, text_y_offset }, content_size };
|
||||
layer_set_frame(&data->name_text_layer.layer, &frame);
|
||||
}
|
||||
|
||||
static void prv_window_load(Window* window) {
|
||||
struct RecoveryFUAppData *data = (struct RecoveryFUAppData*) window_get_user_data(window);
|
||||
|
||||
KinoLayer *kino_layer = &data->kino_layer;
|
||||
kino_layer_init(kino_layer, &window->layer.bounds);
|
||||
layer_add_child(&window->layer, &kino_layer->layer);
|
||||
|
||||
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
|
||||
const char *url_font_key = FONT_KEY_GOTHIC_28_BOLD;
|
||||
const GColor name_bg_color = GColorClear;
|
||||
const char *name_font_key = FONT_KEY_GOTHIC_24;
|
||||
#else
|
||||
const char *url_font_key = FONT_KEY_GOTHIC_18_BOLD;
|
||||
const GColor name_bg_color = GColorWhite;
|
||||
const char *name_font_key = FONT_KEY_GOTHIC_14;
|
||||
#endif
|
||||
|
||||
TextLayer* url_text_layer = &data->url_text_layer;
|
||||
text_layer_init_with_parameters(url_text_layer,
|
||||
&GRect(0, 124, window->layer.bounds.size.w, 64),
|
||||
NULL, fonts_get_system_font(url_font_key),
|
||||
GColorBlack, GColorClear, GTextAlignmentCenter,
|
||||
GTextOverflowModeTrailingEllipsis);
|
||||
layer_add_child(&window->layer, &url_text_layer->layer);
|
||||
|
||||
TextLayer* name_text_layer = &data->name_text_layer;
|
||||
text_layer_init_with_parameters(name_text_layer,
|
||||
&data->launch_app_window.layer.frame,
|
||||
NULL, fonts_get_system_font(name_font_key),
|
||||
GColorBlack, name_bg_color, GTextAlignmentCenter,
|
||||
GTextOverflowModeTrailingEllipsis);
|
||||
layer_add_child(&url_text_layer->layer, &name_text_layer->layer);
|
||||
data->is_showing_version = false;
|
||||
|
||||
prv_update_background_image_and_url_text(data);
|
||||
prv_update_name_text(data);
|
||||
}
|
||||
|
||||
static void prv_push_window(struct RecoveryFUAppData* data) {
|
||||
Window* window = &data->launch_app_window;
|
||||
|
||||
window_init(window, WINDOW_NAME("First Use / Recovery"));
|
||||
window_set_user_data(window, data);
|
||||
window_set_window_handlers(window, &(WindowHandlers){
|
||||
.load = prv_window_load,
|
||||
});
|
||||
window_set_click_config_provider_with_context(window, prv_click_configure, window);
|
||||
|
||||
window_set_fullscreen(window, true);
|
||||
window_set_overrides_back_button(window, true);
|
||||
|
||||
const bool animated = false;
|
||||
app_window_stack_push(window, animated);
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// App Event Handler + Loop
|
||||
|
||||
static void prv_allow_pairing(RecoveryFUAppData* data, bool allow) {
|
||||
if (data->is_pairing_allowed == allow) {
|
||||
return;
|
||||
}
|
||||
data->is_pairing_allowed = allow;
|
||||
if (allow) {
|
||||
bt_pairability_use();
|
||||
} else {
|
||||
bt_pairability_release();
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_pebble_mobile_app_event_handler(PebbleEvent *event, void *context) {
|
||||
if (!s_fu_app_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event->bluetooth.comm_session_event.is_system) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool is_connected = event->bluetooth.comm_session_event.is_open;
|
||||
// When the Pebble app is connected, to being discoverable / pairable. We do this to avoid
|
||||
// stressing out the BT controller during the FW update with discoverability advertisements, etc.
|
||||
prv_allow_pairing(s_fu_app_data, !is_connected);
|
||||
|
||||
s_fu_app_data->is_pebble_mobile_app_connected = event->bluetooth.comm_session_event.is_open;
|
||||
if (is_connected) {
|
||||
s_fu_app_data->has_pebble_mobile_app_connected = true;
|
||||
gap_le_device_name_request_all();
|
||||
}
|
||||
prv_update_background_image_and_url_text(s_fu_app_data);
|
||||
prv_update_name_text(s_fu_app_data);
|
||||
}
|
||||
|
||||
static void prv_bt_event_handler(PebbleEvent *event, void *context) {
|
||||
if (!s_fu_app_data) {
|
||||
return;
|
||||
}
|
||||
prv_update_name_text(s_fu_app_data);
|
||||
}
|
||||
|
||||
static void prv_gather_debug_info_event_handler(PebbleEvent *event, void *context) {
|
||||
if (!s_fu_app_data) {
|
||||
return;
|
||||
}
|
||||
if (event->debug_info.state == DebugInfoStateStarted) {
|
||||
prv_show_spinner(s_fu_app_data);
|
||||
} else {
|
||||
prv_hide_spinner(s_fu_app_data);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// App boilerplate
|
||||
|
||||
static void handle_init(void) {
|
||||
launcher_block_popups(true);
|
||||
|
||||
RecoveryFUAppData *data = app_malloc_check(sizeof(RecoveryFUAppData));
|
||||
s_fu_app_data = data;
|
||||
|
||||
*data = (RecoveryFUAppData){};
|
||||
app_state_set_user_data(data);
|
||||
|
||||
const bool is_connected = (comm_session_get_system_session() != NULL);
|
||||
data->is_pebble_mobile_app_connected = is_connected;
|
||||
prv_allow_pairing(data, !is_connected);
|
||||
|
||||
data->pebble_mobile_app_event_info = (EventServiceInfo) {
|
||||
.type = PEBBLE_COMM_SESSION_EVENT,
|
||||
.handler = prv_pebble_mobile_app_event_handler,
|
||||
};
|
||||
event_service_client_subscribe(&data->pebble_mobile_app_event_info);
|
||||
|
||||
data->pebble_gather_logs_event_info = (EventServiceInfo) {
|
||||
.type = PEBBLE_GATHER_DEBUG_INFO_EVENT,
|
||||
.handler = prv_gather_debug_info_event_handler,
|
||||
};
|
||||
event_service_client_subscribe(&data->pebble_gather_logs_event_info);
|
||||
|
||||
data->bt_connection_event_info = (EventServiceInfo) {
|
||||
.type = PEBBLE_BT_CONNECTION_EVENT,
|
||||
.handler = prv_bt_event_handler,
|
||||
};
|
||||
event_service_client_subscribe(&data->bt_connection_event_info);
|
||||
|
||||
data->ble_device_name_updated_event_info = (EventServiceInfo) {
|
||||
.type = PEBBLE_BLE_DEVICE_NAME_UPDATED_EVENT,
|
||||
.handler = prv_bt_event_handler,
|
||||
};
|
||||
event_service_client_subscribe(&data->ble_device_name_updated_event_info);
|
||||
|
||||
getting_started_button_combo_init(&data->button_combo_state, prv_select_combo_callback);
|
||||
|
||||
prv_push_window(data);
|
||||
}
|
||||
|
||||
static void handle_deinit(void) {
|
||||
RecoveryFUAppData *data = app_state_get_user_data();
|
||||
|
||||
getting_started_button_combo_deinit(&data->button_combo_state);
|
||||
|
||||
kino_layer_deinit(&data->kino_layer);
|
||||
|
||||
event_service_client_unsubscribe(&data->pebble_mobile_app_event_info);
|
||||
event_service_client_unsubscribe(&data->bt_connection_event_info);
|
||||
event_service_client_unsubscribe(&data->pebble_gather_logs_event_info);
|
||||
event_service_client_unsubscribe(&data->ble_device_name_updated_event_info);
|
||||
|
||||
app_window_stack_pop_all(false);
|
||||
|
||||
prv_allow_pairing(data, false);
|
||||
|
||||
app_free(data);
|
||||
s_fu_app_data = NULL;
|
||||
|
||||
launcher_block_popups(false);
|
||||
}
|
||||
|
||||
static void prv_main(void) {
|
||||
handle_init();
|
||||
|
||||
app_event_loop();
|
||||
|
||||
handle_deinit();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* recovery_first_use_app_get_app_info(void) {
|
||||
static const PebbleProcessMdSystem s_recovery_first_use_app = {
|
||||
.common = {
|
||||
.main_func = prv_main,
|
||||
.visibility = ProcessVisibilityHidden,
|
||||
// UUID: 85b80081-d78f-41aa-96fa-a821c79f3f0f
|
||||
.uuid = {
|
||||
0x85, 0xb8, 0x00, 0x81, 0xd7, 0x8f, 0x41, 0xaa,
|
||||
0x96, 0xfa, 0xa8, 0x21, 0xc7, 0x9f, 0x3f, 0x0f
|
||||
},
|
||||
},
|
||||
.name = "Getting Started",
|
||||
.run_level = ProcessAppRunLevelSystem,
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_recovery_first_use_app;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
typedef struct PebbleProcessMd PebbleProcessMd;
|
||||
|
||||
const PebbleProcessMd* recovery_first_use_app_get_app_info(void);
|
||||
Reference in New Issue
Block a user