Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson
2024-12-12 16:43:03 -08:00
committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View 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;
}

View 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);

View 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;
}

View 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);

View 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;
}

View 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);

View 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

View 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);

View 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;
}

View 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);

View 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;
}

View 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);

View 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");
}

View 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);

View 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;
}

View 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);

View 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

View 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);

View 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;
}

View 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);

View 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;
}

View 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);

View 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;
}

View 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);

View 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;
}

View 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();

View 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 "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);
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);