pebble/src/fw/console/prompt_commands.c
Josh Soref 5dfb79c189 spelling: certain
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-01-28 21:32:34 -05:00

1503 lines
48 KiB
C

/*
* 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 "prompt_commands.h"
#include "applib/graphics/8_bit/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/gtypes.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/bt_lock.h"
#include "console_internal.h"
#include "dbgserial.h"
#include "debug/flash_logging.h"
#include "drivers/flash.h"
#include "drivers/task_watchdog.h"
#include "flash_region/flash_region.h"
#include "kernel/event_loop.h"
#include "kernel/logging_private.h"
#include "kernel/pbl_malloc.h"
#include "kernel/pebble_tasks.h"
#include "kernel/util/delay.h"
#include "kernel/util/factory_reset.h"
#include "kernel/util/sleep.h"
#include "kernel/util/stop.h"
#include "mfg/mfg_apps/mfg_flash_test.h"
#include "process_management/app_manager.h"
#include "process_management/worker_manager.h"
#include "prompt.h"
#include "resource/resource_storage_flash.h"
#include "services/common/compositor/compositor.h"
#include "services/common/system_task.h"
#include "services/normal/filesystem/pfs.h"
#include "syscall/syscall.h"
#include "system/bootbits.h"
#include "system/hexdump.h"
#include "system/logging.h"
#include "system/passert.h"
#include "system/reboot_reason.h"
#include "system/reset.h"
#include "util/math.h"
#include "util/net.h"
#include "util/string.h"
#define CMSIS_COMPATIBLE
#include <mcu.h>
#include <bluetooth/bt_test.h>
#include <bluetooth/responsiveness.h>
#include <bluetooth/gatt_discovery.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
static TimerID s_console_button_timer = TIMER_INVALID_ID;
static void prv_pfs_stress_callback(void *data) {
pfs_remove_files(NULL);
system_task_add_callback(prv_pfs_stress_callback, NULL);
}
// Issue regular pfs accesses from KernelBG
void pfs_command_stress(void) {
prompt_send_response("PFS stress from kernel BG");
system_task_add_callback(prv_pfs_stress_callback, NULL);
}
extern void command_read_word(const char* address_str) {
int32_t address = str_to_address(address_str);
if (address == -1) {
prompt_send_response("Invalid address");
return;
}
uint32_t word = *(uint32_t*) address;
char buffer[32];
prompt_send_response_fmt(buffer, sizeof(buffer), "0x%"PRIx32" = 0x%"PRIx32, address, word);
}
void command_format_flash(void) {
flash_erase_bulk();
}
void command_erase_flash(const char *address_str, const char *length_str) {
int32_t address = str_to_address(address_str);
if (address < 0) {
prompt_send_response("Invalid address");
return;
}
int length = atoi(length_str);
if (length <= 0) {
prompt_send_response("Invalid length");
return;
}
char buffer[128];
prompt_send_response_fmt(buffer, 128, "Erasing sectors from 0x%"PRIx32" for %ub",
address, length);
const uint32_t end_address = address + length;
const uint32_t aligned_end_address =
(end_address + (SUBSECTOR_SIZE_BYTES - 1)) & SUBSECTOR_ADDR_MASK;
flash_region_erase_optimal_range_no_watchdog(address, address, end_address, aligned_end_address);
prompt_send_response("OK");
}
void command_dump_flash(const char* address_str, const char* length_str) {
int32_t address = str_to_address(address_str);
if (address == -1) {
prompt_send_response("Invalid address");
return;
}
int length = atoi(length_str);
if (length == 0) {
prompt_send_response("Invalid length");
return;
}
// Temporarily turn on logging so the hexdump comes out.
serial_console_set_state(SERIAL_CONSOLE_STATE_LOGGING);
uint8_t buffer[128];
while (length) {
uint32_t chunk_size = MIN(length, 128);
flash_read_bytes(buffer, address, chunk_size);
PBL_LOG(LOG_LEVEL_ALWAYS, "Data at address 0x%"PRIx32, address);
hexdump_log(LOG_LEVEL_ALWAYS, buffer, chunk_size);
address += chunk_size;
length -= chunk_size;
}
// Go back to the prompt.
serial_console_set_state(SERIAL_CONSOLE_STATE_PROMPT);
}
void command_crc_flash(const char* address_str, const char* length_str) {
int32_t address = str_to_address(address_str);
if (address == -1) {
prompt_send_response("Invalid address");
return;
}
int length = atoi(length_str);
if (length == 0) {
prompt_send_response("Invalid length");
return;
}
uint32_t crc = flash_calculate_legacy_defective_checksum(address, length);
char buffer[32];
prompt_send_response_fmt(buffer, sizeof(buffer), "CRC: %"PRIx32, crc);
}
#define MAX_READ_FLASH_SIZE 1024 // 1KB
void command_flash_read(const char* address_str, const char* length_str) {
// Read data from flash and output the data directly to serial port in segmented chunks
int32_t address = str_to_address(address_str);
if (address == -1) {
prompt_send_response("Invalid address");
return;
}
int length = atoi(length_str);
if (length == 0) {
prompt_send_response("Invalid length");
return;
}
// Allocate a 1KB buffer to read data in segments
uint8_t *buffer = (uint8_t *) kernel_malloc(MIN(MAX_READ_FLASH_SIZE,length));
if (buffer == 0) {
prompt_send_response("Unable to allocate read buffer");
return;
}
while (length > 0) {
uint32_t read_length = MAX_READ_FLASH_SIZE;
if (length < MAX_READ_FLASH_SIZE){
read_length = length;
}
flash_read_bytes(buffer, address, read_length);
// Output to serial
for (uint32_t i = 0; i < read_length; i++) {
dbgserial_putchar(buffer[i]);
}
address += read_length;
length -= read_length;
}
kernel_free(buffer);
}
void command_flash_switch_mode (const char* mode_str) {
int mode = atoi(mode_str);
flash_switch_mode(mode);
}
#define WRITE_PAGE_SIZE_BYTES 64
void command_flash_fill (const char* address_str, const char* length_str, const char* value_str) {
int32_t address = str_to_address(address_str);
if (address == -1) {
prompt_send_response("Invalid address");
return;
}
int length = atoi(length_str);
if (length <= 0) {
prompt_send_response("Invalid length");
return;
}
int value = atoi(value_str);
if ((value < 0) || (value > 0xFF)) {
prompt_send_response("Invalid value");
return;
}
// Fill flash with a character value
uint8_t page[WRITE_PAGE_SIZE_BYTES];
for (uint32_t i = 0; i < WRITE_PAGE_SIZE_BYTES; i++) {
page[i] = (uint8_t)(value++ & 0xFF);
}
uint32_t bytes_remaining = length;
while (bytes_remaining > 0)
{
uint32_t bytes_to_write = WRITE_PAGE_SIZE_BYTES;
if (bytes_remaining < WRITE_PAGE_SIZE_BYTES) {
bytes_to_write = bytes_remaining;
}
flash_write_bytes(page, address, bytes_to_write);
bytes_remaining -= bytes_to_write;
address += bytes_to_write;
}
}
// Pass in test case number and number of iterations to run
// Currently iterations only applies to FLASH_TEST_CASE_RUN_STRESS_ADDR_TEST
// All other tests run once
void command_flash_test(const char* test_case_num_str, const char* iterations_str) {
int32_t test_case_num = atoi(test_case_num_str);
int32_t iterations = atoi(iterations_str);
int32_t status = FLASH_TEST_ERR_OTHER;
if (!((test_case_num == FLASH_TEST_CASE_RUN_STRESS_ADDR_TEST) && (iterations <= 0))) {
// Check to make sure stress test has at least 1 iteration or its another test case
status = run_flash_test_case(test_case_num, iterations);
}
char buffer[80];
if (status == 0) {
prompt_send_response_fmt(buffer, sizeof(buffer), "PASS: TEST CASE %"PRId32, test_case_num);
} else {
prompt_send_response_fmt(buffer, sizeof(buffer),
">FAIL: TEST CASE %"PRId32", Status: %"PRId32,
test_case_num, status);
}
}
void command_flash_validate(void) {
// just test one sector, which is probably less than the size of the region
const uint32_t TEST_ADDR = FLASH_REGION_FIRMWARE_SCRATCH_BEGIN;
const uint32_t TEST_LENGTH = SECTOR_SIZE_BYTES;
PBL_ASSERTN((TEST_ADDR & SECTOR_ADDR_MASK) == TEST_ADDR);
PBL_ASSERTN((TEST_ADDR + TEST_LENGTH) <= FLASH_REGION_FIRMWARE_SCRATCH_END);
// erase a sector
flash_erase_sector_blocking(TEST_ADDR);
if (!flash_sector_is_erased(TEST_ADDR)) {
prompt_send_response("FAIL: sector not erased");
return;
}
// write data into the sector
const uint32_t BUFFER_SIZE = 256;
uint8_t buffer[BUFFER_SIZE];
for (uint32_t i = 0; i < BUFFER_SIZE; i++) {
buffer[i] = i;
}
for (uint32_t offset = 0; offset < TEST_LENGTH; offset += BUFFER_SIZE) {
const uint32_t addr = TEST_ADDR + offset;
flash_write_bytes(buffer, addr, BUFFER_SIZE);
}
// read it back
for (uint32_t offset = 0; offset < TEST_LENGTH; offset += BUFFER_SIZE) {
memset(buffer, 0, BUFFER_SIZE);
const uint32_t addr = TEST_ADDR + offset;
flash_read_bytes(buffer, addr, BUFFER_SIZE);
for (uint32_t i = 0; i < BUFFER_SIZE; i++) {
if (buffer[i] != i) {
char err_buf[80];
prompt_send_response_fmt(err_buf, sizeof(err_buf), "FAIL: Incorrect value at 0x%"PRIx32,
addr + i);
return;
}
}
}
// read it back, albeit awkwardly. We have seen issues that arise when stitching different
// types of flash ops together (i.e single byte reads followed by memmaps)
const uint32_t SHORT_TEST_LENGTH = 1000; // single byte reads are slow so do a shorter test length
for (uint32_t offset = 0; offset < SHORT_TEST_LENGTH ; offset++) {
uint8_t memmap_buffer[130]; // > 128 bytes, triggers a memmap read for QSPI
memset(memmap_buffer, 0x00, sizeof(memmap_buffer));
const uint32_t pre_addr = TEST_ADDR + offset - MIN(offset, 1);
uint8_t pre_byte;
flash_read_bytes(&pre_byte, pre_addr, sizeof(pre_byte));
const uint32_t addr = TEST_ADDR + offset;
size_t read_size = MIN(sizeof(memmap_buffer), SHORT_TEST_LENGTH - offset);
flash_read_bytes(&memmap_buffer[0], addr, read_size);
for (size_t i = 0; i < read_size; i++) {
uint8_t want = (offset + i) & 0xff;
if (memmap_buffer[i] != want) {
char err_buf[80];
prompt_send_response_fmt(err_buf, sizeof(err_buf), "FAIL at ADDR %d Got: %d Wanted %d",
(int)offset, (int)memmap_buffer[i], (int)want);
break;
}
}
}
// clean up
flash_erase_sector_blocking(TEST_ADDR);
if (!flash_sector_is_erased(TEST_ADDR)) {
prompt_send_response("FAIL: sector not erased");
return;
}
prompt_send_response("OK");
}
//! Some flash chips have an accelerated method of checking for erased sectors. This is a sanity
//! check against that method. It reads the bytes in raw form and makes sure it is really erased.
static bool prv_is_really_erased(uint32_t addr, bool is_subsector) {
bool erased = (is_subsector) ? flash_subsector_is_erased(addr) : flash_sector_is_erased(addr);
if (erased) {
char buffer[64];
uint32_t end_addr = addr + (is_subsector ? SUBSECTOR_SIZE_BYTES : SECTOR_SIZE_BYTES);
for (uint32_t i_addr = addr; i_addr < end_addr; i_addr += sizeof(buffer)) {
flash_read_bytes((uint8_t *)buffer, i_addr, sizeof(buffer));
for (uint32_t j = 0; j < sizeof(buffer); j++) {
if (buffer[j] != 0xFF) {
erased = false;
prompt_send_response_fmt(buffer, sizeof(buffer),
"(Sub)Sector at addr: 0x%"PRIX32" not really erased. is_subsector: %d",
addr, is_subsector);
goto done;
}
}
}
}
done:
return erased;
}
// ARG:
// 0 - Only show sectors
// 1 - Show subsectors too if sector is not erased
void command_flash_show_erased_sectors(const char *arg) {
const bool show_subsectors = (atoi(arg) == 1);
char buffer[64];
uint32_t addr = 0;
while (addr < BOARD_NOR_FLASH_SIZE) {
bool erased = prv_is_really_erased(addr, false);
prompt_send_response_fmt(buffer, sizeof(buffer), "SECTOR - 0x%-6"PRIX32" :: %s",
addr, erased ? "true" : "false");
if (show_subsectors && !erased) {
for (uint32_t i = 0; i < (SECTOR_SIZE_BYTES / SUBSECTOR_SIZE_BYTES); i++) {
const uint32_t sub_addr = (addr + (i * SUBSECTOR_SIZE_BYTES));
bool sub_erased = prv_is_really_erased(sub_addr, true);
prompt_send_response_fmt(buffer, sizeof(buffer), " SUBSECTOR - 0X%-6"PRIx32" :: %s",
sub_addr, sub_erased ? "true" : "false");
}
}
addr += SECTOR_SIZE_BYTES;
task_watchdog_bit_set(pebble_task_get_current());
}
}
/*
* Commented out by default because this command can harm your flash chip!!
static void prv_flash_stress_callback(void *data) {
uint32_t flash_addr = FLASH_REGION_DEBUG_DB_BEGIN;
uint32_t sector_address = flash_get_sector_base_address(flash_addr);
PBL_LOG(LOG_LEVEL_DEBUG, "erasing flash address %x", sector_address);
flash_erase_sector_blocking(sector_address);
const char* data_to_write = "hello world";
PBL_LOG(LOG_LEVEL_DEBUG, "writing to flash address %x", flash_addr);
flash_write_bytes((const uint8_t *)data_to_write, flash_addr, strlen(data_to_write));
system_task_add_callback(prv_flash_stress_callback, NULL);
}
void command_flash_stress(void) {
// WARNING!! Running this test can shorten the life of your flash chip because it violates the
// "wait 90 seconds between erases of the same sector" spec.
prompt_send_response("Stress flash");
system_task_add_callback(prv_flash_stress_callback, NULL);
}
*/
void command_reset() {
prompt_command_finish();
RebootReason reason = { RebootReasonCode_Serial, 0 };
reboot_reason_set(&reason);
system_reset();
}
void command_crash() {
prompt_command_finish();
RebootReason reason = { RebootReasonCode_LauncherPanic, 0 };
reboot_reason_set(&reason);
system_reset();
}
void command_hard_crash() {
prompt_command_finish();
RebootReason reason = { RebootReasonCode_HardFault, 0 };
reboot_reason_set(&reason);
boot_bit_set(BOOT_BIT_FW_START_FAIL_STRIKE_TWO);
boot_bit_set(BOOT_BIT_SOFTWARE_FAILURE_OCCURRED);
boot_bit_clear(BOOT_BIT_FW_STABLE);
system_hard_reset();
}
void command_boot_prf(void) {
prompt_command_finish();
RebootReason reason = { RebootReasonCode_Serial, 0 };
reboot_reason_set(&reason);
boot_bit_set(BOOT_BIT_FORCE_PRF);
system_reset();
}
void command_infinite_loop(void) {
while(1);
}
void stuck_timer_cb(void* data) {
while(1);
}
#include "services/common/new_timer/new_timer.h"
void command_stuck_timer(void) {
TimerID timer = new_timer_create();
new_timer_start(timer, 10, stuck_timer_cb, NULL, 0 /*flags*/);
}
#include "drivers/rtc.h"
void command_assert_fail(void) {
prompt_command_finish();
RtcTicks ticks = rtc_get_ticks();
PBL_ASSERT(false, "The world doesn't make sense anymore! Tick count: 0x%08" PRIx32 "%08" PRIx32,
SPLIT_64_BIT_ARG(ticks));
}
void command_croak(void) {
prompt_command_finish();
PBL_CROAK("You asked for this!");
}
typedef void (*KaboomCallback)(void);
void command_hardfault(void) {
prompt_command_finish();
KaboomCallback kaboom = 0;
kaboom();
}
void command_boot_bit_set(const char* bit, const char* value) {
int len = strlen(bit);
int bit_number = 0;
for (int i = 0; i < len; ++i) {
bit_number *= 10;
int next_digit = bit[i] - '0';
if (next_digit < 0 || next_digit > 9) {
prompt_send_response("invalid bit number");
return;
}
bit_number += next_digit;
}
int bit_mask = 1 << bit_number;
if (value[0] == '0') {
boot_bit_clear(bit_mask);
} else if (value[0] == '1') {
boot_bit_set(bit_mask);
} else {
prompt_send_response("invalid bit value, pick 1 or 0");
return;
}
prompt_send_response("OK bit assigned");
}
typedef struct {
ButtonId button_id;
bool button_is_held_down;
uint32_t num_presses_remaining;
uint32_t hold_down_time_ms;
uint32_t delay_between_presses_ms;
} ButtonPressNewTimerContext;
// This is a callback to only be used in conjunction with command_button_press() and
// command_button_press_multiple()
static void command_button_press_callback(void *cb_data) {
ButtonPressNewTimerContext *context = cb_data;
const bool button_is_held_down = context->button_is_held_down;
// Choose the next event type to emit and the next timeout based on the current button state
PebbleEventType next_event_type = button_is_held_down ? PEBBLE_BUTTON_UP_EVENT :
PEBBLE_BUTTON_DOWN_EVENT;
const uint32_t next_timeout_ms = button_is_held_down ? context->delay_between_presses_ms :
context->hold_down_time_ms;
// Add the next button event to the queue
PebbleEvent next_button_event = {
.type = next_event_type,
.button.button_id = context->button_id,
};
event_put(&next_button_event);
// Decrement the number of presses remaining if the button is currently held down (because we
// just pushed it up by adding that event)
if (button_is_held_down) {
context->num_presses_remaining--;
}
if (context->num_presses_remaining > 0) {
// Toggle the state of the button
context->button_is_held_down = !button_is_held_down;
// Restart the timer
new_timer_start(s_console_button_timer, next_timeout_ms, command_button_press_callback,
context, 0 /* flags */);
} else {
kernel_free(context);
}
}
static bool prv_convert_and_validate_timeout_value(const char *timeout_string,
uint32_t default_value,
uint32_t *result) {
if (!result) {
return false;
}
if (!timeout_string) {
*result = default_value;
return true;
}
char *end;
*result = MAX(0, strtol(timeout_string, &end, 10));
return (*end == '\0');
}
//! Press a button multiple times.
//! @param button_index The index of the button in \ref ButtonId to press.
//! @param presses The number of times to press the button. If NULL, defaults to 1. If <= 0, errors.
//! @param hold_down_time_ms The time (in ms) to hold down the button for each press. If NULL,
//! defaults to 20 ms. If < 0, errors.
//! @param delay_between_presses_ms The time (in ms) to delay between successive button presses.
//! If NULL, defaults to 0. If < 0, errors.
static void prv_button_press_multiple(const char *button_index, const char *presses,
const char *hold_down_time_ms,
const char *delay_between_presses_ms) {
const uint32_t default_delay = 20;
// Convert and validate the button value
int button = atoi(button_index);
if (!WITHIN(button, 0, NUM_BUTTONS - 1)) {
goto error;
}
const ButtonId button_id = (ButtonId)button;
uint32_t num_presses = 1;
// If presses is NULL, default to 1; otherwise convert the char string to an integer
if (presses) {
char *end;
num_presses = MAX(0, strtol(presses, &end, 10));
// Validate the num_presses value
if (*end != '\0') {
goto error;
}
}
// If hold_down_time_ms is NULL it's a short press so use default value
// Otherwise convert the char string to an integer; error if the converted value is negative
uint32_t hold_down_timeout_ms;
if (!prv_convert_and_validate_timeout_value(hold_down_time_ms, default_delay,
&hold_down_timeout_ms)) {
goto error;
}
// If delay_between_presses_ms is NULL then default to 0 milliseconds
// Otherwise convert the char string to an integer; error if the converted value is negative
uint32_t delay_between_presses_timeout_ms;
if (!prv_convert_and_validate_timeout_value(delay_between_presses_ms, 0,
&delay_between_presses_timeout_ms)) {
goto error;
}
// Initialize timer on first use
if (s_console_button_timer == TIMER_INVALID_ID) {
s_console_button_timer = new_timer_create();
}
// If the callback is already scheduled, notify busy and exit
if (new_timer_scheduled(s_console_button_timer, NULL)) {
prompt_send_response("BUSY");
return;
}
// Construct our new_timer context, will be freed in command_button_press_callback()
ButtonPressNewTimerContext *new_timer_context = kernel_malloc(sizeof(ButtonPressNewTimerContext));
if (!new_timer_context) {
goto error;
}
*new_timer_context = (ButtonPressNewTimerContext) {
.button_id = button_id,
.button_is_held_down = false,
.num_presses_remaining = num_presses,
.hold_down_time_ms = hold_down_timeout_ms,
.delay_between_presses_ms = delay_between_presses_timeout_ms,
};
// In order to avoid race conditions between button events and timers being registered, drive the
// entire multi click sequence in new_timers. The callback will re-register this timer as needed
// for each subsequent click event in the sequence.
const bool timer_started = new_timer_start(s_console_button_timer, 0,
command_button_press_callback, new_timer_context,
0 /* flags */);
if (!timer_started) {
kernel_free(new_timer_context);
goto error;
}
prompt_send_response("OK");
return;
error:
prompt_send_response("ERROR");
}
// Perform a button press from the serial console. Three responses are provided
// to users/tools using the interface to indicate status. They are: OK, BUSY, and ERROR.
void command_button_press(const char *button_index, const char *hold_down_time_ms) {
prv_button_press_multiple(button_index, NULL, hold_down_time_ms, NULL);
}
// Perform multiple presses of the same button from the serial console. Three responses are
// provided to users/tools using the interface to indicate status. They are: OK, BUSY, and ERROR.
void command_button_press_multiple(const char *button_index, const char *num_presses,
const char *hold_down_time_ms,
const char *delay_between_presses_ms) {
prv_button_press_multiple(button_index, num_presses, hold_down_time_ms, delay_between_presses_ms);
}
static void prv_button_press_short_launcher_task_cb(void *data) {
uintptr_t button = (uintptr_t)data;
PebbleEvent e = {
.type = PEBBLE_BUTTON_DOWN_EVENT,
.button.button_id = button
};
event_put(&e);
e = (PebbleEvent) {
.type = PEBBLE_BUTTON_UP_EVENT,
.button.button_id = button
};
event_put(&e);
}
void command_button_press_short(const char* button_index) {
uintptr_t button = (uintptr_t)atoi(button_index);
launcher_task_add_callback(prv_button_press_short_launcher_task_cb, (void *)button);
prompt_send_response("OK");
}
void command_factory_reset(void) {
prompt_command_finish();
factory_reset(false /* should_shutdown */);
}
void command_factory_reset_fast(void) {
prompt_command_finish();
worker_manager_disable();
while (worker_manager_get_current_worker_md()) {
// Wait for the worker to die
psleep(3);
}
launcher_task_add_callback(factory_reset_fast, NULL);
}
static bool prv_serial_dump_chunk_callback(uint8_t* msg, uint32_t total_length) {
LogBinaryMessage* message = (LogBinaryMessage *)msg;
char buffer[256];
char time_buffer[TIME_STRING_BUFFER_SIZE];
message->message[message->message_length] = 0;
prompt_send_response_fmt(buffer, sizeof(buffer), "%c %s %s:%d> %s",
pbl_log_get_level_char(message->log_level),
time_t_to_string(time_buffer, htonl(message->timestamp)),
message->filename,
(int)htons(message->line_number),
message->message);
return true;
}
static void prv_serial_dump_completed_callback(bool success) {
prompt_command_finish();
}
void command_log_dump_current(void) {
flash_dump_log_file(0, prv_serial_dump_chunk_callback, prv_serial_dump_completed_callback);
prompt_command_continues_after_returning();
}
void command_log_dump_last(void) {
flash_dump_log_file(1, prv_serial_dump_chunk_callback, prv_serial_dump_completed_callback);
prompt_command_continues_after_returning();
}
void command_log_dump_generation(const char* generation_str) {
int generation = atoi(generation_str);
flash_dump_log_file(generation, prv_serial_dump_chunk_callback,
prv_serial_dump_completed_callback);
prompt_command_continues_after_returning();
}
static void spam_callback(void *data) {
uint32_t iteration = (uintptr_t) data;
uint8_t buffer[128];
time_t base = sys_get_time();
for (int i = 0; i < 16; ++i) {
LogBinaryMessage* msg = (LogBinaryMessage*) buffer;
msg->timestamp = htonl(base + iteration * 16 + i);
msg->log_level = LOG_LEVEL_ERROR;
msg->message_length = sizeof(buffer) - sizeof(LogBinaryMessage);
msg->line_number = 0;
strncpy(msg->filename, "spam.exe", sizeof(msg->filename));
char letter = 'A' + i;
memset(msg->message, letter, msg->message_length - 1);
msg->message[msg->message_length - 1] = 0;
uint32_t flash_addr = flash_logging_log_start(sizeof(buffer));
flash_logging_write(buffer, flash_addr, sizeof(buffer));
}
(void)data;
}
void command_log_dump_spam(void) {
prompt_send_response("Spam logs!");
for (int i = 0; i < 16; ++i) {
system_task_add_callback(spam_callback, (void *)(uintptr_t) i);
}
}
#ifdef TEST_FLASH_LOCK_PROTECTION
#include "flash_region/flash_region.h"
#include "drivers/task_watchdog.h"
#include "drivers/watchdog.h"
// This test attempts to write over every region of the flash.
// If we can still boot PRF after running this, it means we have successfully
// protected those regions
void flash_expect_program_failure(bool expect_failure);
void command_flash_test_locked_sectors(void) {
// write 0's to the entire flash
static uint8_t buf[2048] = { 0 };
char status[80];
__disable_irq();
for (int i = 0; i < 2; i++) {
for (uint32_t addr = 0; addr < BOARD_NOR_FLASH_SIZE; addr += sizeof(buf)) {
if (addr >= FLASH_REGION_SAFE_FIRMWARE_BEGIN &&
addr < FLASH_REGION_SAFE_FIRMWARE_END) {
flash_expect_program_failure(true);
}
if ((addr % SECTOR_SIZE_BYTES) == 0) {
prompt_send_response_fmt(status, sizeof(status), "Validated: 0x%lx", addr);
flash_erase_sector_blocking(addr);
flash_erase_sector_blocking(addr); // exercise already erased check
}
flash_write_bytes(&buf[0], addr, sizeof(buf));
flash_expect_program_failure(false);
watchdog_feed();
}
}
task_watchdog_bit_set(pebble_task_get_current());
__enable_irq();
}
#endif
static TimerID s_abusive_timer = TIMER_INVALID_ID;
struct WasteTimerData {
uint16_t count;
uint16_t delay;
};
_Static_assert(sizeof(struct WasteTimerData) <= sizeof(uintptr_t),
"struct WasteTimerData too big");
static void prv_waste_time_cb(void *context) {
struct WasteTimerData data;
memcpy(&data, &context, sizeof data);
for (int i = 0; i < data.delay; ++i) delay_us(1000);
if (--data.count > 0) {
memcpy(&context, &data, sizeof context);
new_timer_start(s_abusive_timer, 1, prv_waste_time_cb, context, 0);
}
}
void command_waste_time(const char *count_arg, const char *delay_arg) {
int count = atoi(count_arg);
int delay = atoi(delay_arg);
if (count <= 0 || count > 0xFFFF || delay <= 0 || delay > 0xFFFF) {
prompt_send_response("Nope.");
return;
}
struct WasteTimerData data = { count, delay };
uintptr_t data_pack;
memcpy(&data_pack, &data, sizeof data_pack);
if (s_abusive_timer == TIMER_INVALID_ID) {
s_abusive_timer = new_timer_create();
}
if (new_timer_start(s_abusive_timer, 100, prv_waste_time_cb,
(void *)data_pack, 0)) {
prompt_send_response("OK");
} else {
prompt_send_response("ERROR");
}
}
#ifndef RELEASE
#include "system/profiler.h"
void command_audit_delay_us(void) {
profiler_init();
// don't let context switches skew our results
__disable_irq();
char buf[80];
// test short delays because we should really be using psleep() for longer stalls!
for (uint32_t i = 1; i <= 1000; i += 2) {
profiler_start();
delay_us(i);
profiler_stop();
uint32_t duration_us = profiler_get_total_duration(true);
// make sure we have idled for at least the time specified and have not exceeded
// the requested time by more than 5%
bool passed = ((duration_us >= i) && (duration_us <= ((i * 105) / 100)));
if (!passed) {
prompt_send_response_fmt(buf, sizeof(buf), "Audit Failed: Expected %"PRIu32", Got %"PRIu32,
i, duration_us);
}
}
prompt_send_response("delay_us audit complete");
__enable_irq();
}
// Simply parks the chip permanently in stop mode in whatever state it's currently in. This can be
// pretty handy when trying to profile power of the chip under certain states
// NOTE: If you did not configure with `--nowatchdog`, the HW watchdog will reboot you in ~8s
void command_enter_stop(void) {
dbgserial_putstr("Entering stop mode indefinitely ... reboot your board to get out!!");
__disable_irq();
RTC_ITConfig(RTC_IT_WUT, DISABLE);
RTC_WakeUpCmd(DISABLE);
// disable all IRQn_Type >= 0 interrupts
for (size_t i = 0; i < ARRAY_LENGTH(NVIC->ISER); i++) {
NVIC->ICER[i] = NVIC->ISER[i];
}
// disable SysTick
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
enter_stop_mode();
dbgserial_putstr("woah, failed to enter stop mode");
while (1) { }
}
#ifndef RECOVERY_FW
// Create a bunch of fragmentation in the filesystem by creating a large number
// of small files and only deleting a small number of them
void command_litter_filesystem(void) {
char name[10];
for (int i = 0; i < 100; i++) {
snprintf(name, sizeof(name), "litter%d", i);
int fd = pfs_open(name, OP_FLAG_WRITE, FILE_TYPE_STATIC, 300);
if (i % 5 == 0) {
pfs_close_and_remove(fd);
} else {
pfs_close(fd);
}
}
}
#endif
#endif
static GAPLEConnection *prv_get_le_connection_and_print_info(void) {
GAPLEConnection *conn = gap_le_connection_any();
if (!conn) {
prompt_send_response("No device connected");
} else {
char buf[80];
prompt_send_response_fmt(buf, sizeof(buf), "Connected to " BT_DEVICE_ADDRESS_FMT,
BT_DEVICE_ADDRESS_XPLODE(conn->device.address));
}
return conn;
}
void command_bt_conn_param_set(
char *interval_min_1_25ms, char *interval_max_1_25ms, char *slave_latency_events,
char *timeout_10ms) {
BleConnectionParamsUpdateReq req = {
.interval_min_1_25ms = atoi(interval_min_1_25ms),
.interval_max_1_25ms = atoi(interval_max_1_25ms),
.slave_latency_events = atoi(slave_latency_events),
.supervision_timeout_10ms = atoi(timeout_10ms),
};
GAPLEConnection *conn = prv_get_le_connection_and_print_info();
BTDeviceInternal addr = {};
if (conn) {
addr.address = conn->device.address;
}
bt_driver_le_connection_parameter_update(&addr, &req);
}
// Not in a header because it's really only used from within the gatt_service_changed module
extern void gatt_client_discovery_discover_range(
GAPLEConnection *connection, ATTHandleRange *hdl_range);
void command_bt_disc_start(char *start_handle, char *end_handle) {
bt_lock();
{
ATTHandleRange range = {
.start = atoi(start_handle),
.end = atoi(end_handle)
};
GAPLEConnection *conn = prv_get_le_connection_and_print_info();
if (conn) {
gatt_client_discovery_discover_range(conn, &range);
}
}
bt_unlock();
}
void command_bt_disc_stop(void) {
bt_lock();
{
GAPLEConnection *conn = prv_get_le_connection_and_print_info();
if (conn) {
bt_driver_gatt_stop_discovery(conn);
}
}
bt_unlock();
}
#if BT_CONTROLLER_DA14681
static TimerID s_sleep_test_timer = TIMER_INVALID_ID;
static uint32_t s_num_sleep_iters = 0;
static void prv_sleep_timer_test_cb(void *data) {
uint32_t count = (uint32_t)data;
for (int j = 0; j < 10; j++) {
bt_driver_send_sleep_test_cmd(true);
psleep(9);
}
if (count >= s_num_sleep_iters) {
bt_driver_send_sleep_test_cmd(false);
prompt_send_response("100 % - PASS");
new_timer_delete(s_sleep_test_timer);
prompt_command_finish();
} else {
char buf[80];
prompt_send_response_fmt(buf, sizeof(buf), "%d %%", (int)((count * 100) / s_num_sleep_iters));
count++;
new_timer_start(s_sleep_test_timer, 31, prv_sleep_timer_test_cb, (void *)(uintptr_t)count, 0);
}
}
// Attempts to check that the Dialog chip sleep mode is working correctly by forcing wakeups at
// different times. (See PBL-39777). The timeouts delays may seem a little random but I found with
// this combo of values I was able to hit the issue on both failing units I have pretty quickly.
extern bool bt_test_chip_in_test_mode(void);
void command_bt_sleep_check(const char *iters) {
s_num_sleep_iters = atoi(iters);
// It seems like advertising while doing this makes the issue more likely to happen
// (I suspect because there are even more enters/exits from sleep happening)
if (bt_test_chip_in_test_mode()) {
prompt_send_response("You must run \"bt test stop\" to conduct sleep test!");
return;
}
prompt_send_response("Starting BT sleep check test");
s_sleep_test_timer = new_timer_create();
new_timer_start(s_sleep_test_timer , 10, prv_sleep_timer_test_cb, (void *)(uintptr_t)0, 0);
prompt_command_continues_after_returning();
}
void command_btle_unmod_tx_start(char *tx_channel) {
bt_driver_start_unmodulated_tx(atoi(tx_channel));
}
void command_btle_unmod_tx_stop(void) {
bt_driver_stop_unmodulated_tx();
}
void command_btle_test_le_tx_start(
char *tx_channel, char *tx_packet_length, char *packet_payload_type) {
bt_driver_le_transmitter_test(
atoi(tx_channel), atoi(tx_packet_length), atoi(packet_payload_type));
}
void command_btle_test_rx_start(char *rx_channel) {
bt_driver_le_receiver_test(atoi(rx_channel));
}
void command_btle_test_end(void) {
bt_driver_le_test_end();
}
//# if PLATFORM_ROBERT
static const char *s_btle_pa_config_strings[3] = { "disable", "enable", "bypass" };
void command_btle_pa_set(char *command) {
char buffer[64];
int index;
for (index = 0; index < BtlePaConfigCount; ++index) {
if (strcmp(command, s_btle_pa_config_strings[index]) == 0) {
break;
}
}
if (index >= BtlePaConfigCount) {
prompt_send_response_fmt(buffer, sizeof(buffer), "BTLE PA options are: %s, %s, %s",
s_btle_pa_config_strings[BtlePaConfig_Disable],
s_btle_pa_config_strings[BtlePaConfig_Enable],
s_btle_pa_config_strings[BtlePaConfig_Bypass]);
} else {
bt_driver_le_test_pa(index);
prompt_send_response_fmt(buffer, sizeof(buffer), "BTLE PA set to: %s",
s_btle_pa_config_strings[index]);
}
}
//# endif
#endif
extern void hc_endpoint_logging_set_level(uint8_t level);
void command_ble_logging_set_level(const char *level) {
char buffer[32];
int log_level = atoi(level);
if (log_level < 0) {
log_level = 0;
} else if (log_level > 255) {
log_level = 255;
}
hc_endpoint_logging_set_level(log_level);
prompt_send_response_fmt(buffer, 32, "Ble Log level set to: %i", log_level);
}
extern bool hc_endpoint_logging_get_level(uint8_t *level);
void command_ble_logging_get_level(void) {
uint8_t log_level;
char buffer[32];
if (!hc_endpoint_logging_get_level(&log_level)) {
prompt_send_response("Unable to get Ble Log level");
} else {
prompt_send_response_fmt(buffer, 32, "Ble Log level: %d", log_level);
}
}
// ARG:
// 0 - Request BLE firmware to coredump
// 1 - Force BLE firmware to hard fault
// 2 - Force the BLE chip to watchdog (by wedging a task)
void command_ble_core_dump(const char *command) {
int option = atoi(command);
if ((option < 0) || (option >= BtleCoreDumpCount)) {
prompt_send_response("Invalid BLE core command");
return;
}
bt_driver_core_dump(option);
}
#if PERFORMANCE_TESTS
// for task_watchdog_bit_set_all
#include "drivers/task_watchdog.h"
// For taskYIELD()
#include "FreeRTOS.h"
#include "task.h"
// Average this many iterations of the text test for getting useful perf numbers.
#define PERFTEST_TEXT_ITERATIONS 5
static GContext s_perftest_ctx = {};
static GContext *prv_perftest_get_context(void) {
GContext *ctx = &s_perftest_ctx;
FrameBuffer *fb = compositor_get_framebuffer();
memset(fb->buffer, 0xff, FRAMEBUFFER_SIZE_BYTES);
graphics_context_init(ctx, fb, GContextInitializationMode_App);
return ctx;
}
void watchdog_feed(void);
void command_perftest_line(const char *do_aa, const char *width) {
watchdog_feed();
GContext *ctx = prv_perftest_get_context();
GColor color = { .argb = (uint8_t)0x33 };
graphics_context_set_stroke_color(ctx, color);
bool aa_enable = false;
if (strcmp(do_aa, "aa") == 0) {
aa_enable = true;
} else if (strcmp(do_aa, "noaa") != 0) {
prompt_send_response("Incorrect aa argument, must be 'aa' or 'noaa'.");
return;
}
graphics_context_set_antialiased(ctx, aa_enable);
uint8_t stroke_width = atoi(width);
graphics_context_set_stroke_width(ctx, stroke_width);
profiler_start();
// 45 degrees
graphics_draw_line(ctx, GPoint(0, 0), GPoint(DISP_COLS, DISP_ROWS));
// ~63 degrees
graphics_draw_line(ctx, GPoint(DISP_COLS/2, 0), GPoint(DISP_COLS, DISP_ROWS));
// ~33 degrees
graphics_draw_line(ctx, GPoint(0, DISP_ROWS/3), GPoint(DISP_COLS, DISP_ROWS));
// ~53 degrees
graphics_draw_line(ctx, GPoint(DISP_COLS/4, 0), GPoint(DISP_COLS, DISP_ROWS));
// ~39 degrees
graphics_draw_line(ctx, GPoint(0, DISP_ROWS/5), GPoint(DISP_COLS, DISP_ROWS));
profiler_stop();
uint32_t total_time = profiler_get_total_duration(false);
uint32_t us = profiler_get_total_duration(true);
char buf[80];
prompt_send_response_fmt(buf, sizeof(buf),
"%s, %s, %"PRIu32", %"PRIu32,
do_aa, width, us, total_time);
}
void command_perftest_line_all(void) {
prompt_send_response("Antialiasing?, Width, Total time (us), Total cycles");
command_perftest_line("noaa", "8");
command_perftest_line("noaa", "6");
command_perftest_line("noaa", "5");
command_perftest_line("noaa", "4");
command_perftest_line("noaa", "3");
command_perftest_line("noaa", "2");
command_perftest_line("noaa", "1");
command_perftest_line("aa", "8");
command_perftest_line("aa", "6");
command_perftest_line("aa", "5");
command_perftest_line("aa", "4");
command_perftest_line("aa", "3");
command_perftest_line("aa", "2");
command_perftest_line("aa", "1");
}
typedef struct PerftestTextArguments {
const char *string_type;
const char *font_key;
const char *y_offset;
} PerftestTextArguments;
static volatile PerftestTextArguments s_perftest_text_arguments;
enum {
TestString_Best, // The best case
TestString_Worst, // Entirely unique characters, in order to miss the font cache every time
TestString_Typical, // A very typical notification
TestStringCount,
};
enum {
TestStringFont_Gothic18,
TestStringFont_Gothic24B,
TestStringFont_Other,
TestStringFontCount,
};
// A very big number
#define STRING_LENGTH_MAX 99999
typedef struct PerftestTextString {
const char *string;
size_t lengths[TestStringFontCount];
} PerftestTextString;
static const PerftestTextString s_perftest_text_strings[TestStringCount] = {
[TestString_Best] = {
.string = "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM",
.lengths = {
#if PLATFORM_ROBERT
[TestStringFont_Gothic18] = 204,
[TestStringFont_Gothic24B] = 144,
[TestStringFont_Other] = STRING_LENGTH_MAX,
#elif PLATFORM_SNOWY
[TestStringFont_Gothic18] = 109,
[TestStringFont_Gothic24B] = 78,
[TestStringFont_Other] = STRING_LENGTH_MAX,
#endif
},
},
[TestString_Worst] = {
.string = "`1234567890-=qwertyuiop[]\\asdfghjkl;'zxcvbnm,./~!@#$%%^&*()_+QWERTYUIOP{}|A"
"SDFGHJKL:\"ZXCVBNM<>?èéêëēėęÿûüùúūîïíī"
"įìôöòóœøōõàáâäæãåāßśšłžźżçćčñń∑´®†¥¨ˆπ"
"∂ƒ©˙∆˚¬…Ω≈√∫˜µ≤≥÷¡™£¢∞§¶•ªº–≠`“‘"
"«ÈÉÊËĒĖĘŸÛÜÙÚŪÎÏÍĪĮÌÔÖÒÓŒØŌÕÀÁÂÄÆÃÅĀŚ"
"ŠŁŽŹŻÇĆČÑŃ∑ˇ∏”’»˝¸˛◊ı˜¯˘¿"
"あいうえおかきくけこさしすせそたちつてとなに"
"ぬねのはひふへほまみむめもやゆよらりるれろわ"
"をんアイウエオサシスセソタチツテトナニヌネノ"
"ハヒフヘホマミムメモヤユヨラリルレロワヲン",
.lengths = {
#if PLATFORM_ROBERT
[TestStringFont_Gothic18] = 579,
[TestStringFont_Gothic24B] = 291,
[TestStringFont_Other] = STRING_LENGTH_MAX,
#elif PLATFORM_SNOWY
[TestStringFont_Gothic18] = 256,
[TestStringFont_Gothic24B] = 113,
[TestStringFont_Other] = STRING_LENGTH_MAX,
#endif
},
},
[TestString_Typical] = {
.string = "Brian Gomberg\n"
"Re: Robert stand-up 06/06 • "
"y: - DDAD (enabling system apps to take advantage of memory mapped "
"FLASH access on Robe"
"\xe2\x80\xa6",
.lengths = {
#if PLATFORM_ROBERT
[TestStringFont_Gothic18] = 134,
[TestStringFont_Gothic24B] = 134,
[TestStringFont_Other] = STRING_LENGTH_MAX,
#elif PLATFORM_SNOWY
[TestStringFont_Gothic18] = 134,
[TestStringFont_Gothic24B] = 112,
[TestStringFont_Other] = STRING_LENGTH_MAX,
#endif
},
},
};
#define TEXT_ALIGNMENT (GTextAlignmentCenter)
#define TEXT_OVERFLOW (GTextOverflowModeWordWrap)
// Enable this to show the actual contents, to check lengths and such.
#define TEXT_PERFTEST_MODAL 0
#if TEXT_PERFTEST_MODAL
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/dialogs/dialog_private.h"
#include "kernel/ui/modals/modal_manager.h"
static void prv_dialog_appear(Window *window) {
Dialog *dialog = window_get_user_data(window);
dialog_appear(dialog);
}
static void prv_dialog_unload(Window *window) {
Dialog *dialog = window_get_user_data(window);
dialog_unload(dialog);
}
static void prv_dialog_load(Window *window) {
Dialog *dialog = window_get_user_data(window);
GFont font = fonts_get_system_font(s_perftest_text_arguments.font_key);
TextLayer *text_layer = &dialog->text_layer;
text_layer_init_with_parameters(text_layer, &GRect(0, 0, DISP_COLS, DISP_ROWS), dialog->buffer,
font, GColorBlack, GColorClear, TEXT_ALIGNMENT,
TEXT_OVERFLOW);
layer_add_child(&window->layer, &text_layer->layer);
#if PBL_ROUND
text_layer_enable_screen_text_flow_and_paging(text_layer, TEXT_FLOW_INSET_PX);
#endif
dialog_load(dialog);
}
static Dialog s_test_dialog;
static void prv_display_modal(WindowStack *stack, const char *string) {
Dialog *new_dialog = &s_test_dialog;
dialog_init(new_dialog, "");
dialog_set_text(new_dialog, string);
Window *window = &new_dialog->window;
window_set_window_handlers(window, &(WindowHandlers) {
.load = prv_dialog_load,
.unload = prv_dialog_unload,
.appear = prv_dialog_appear,
});
window_set_user_data(window, new_dialog);
dialog_push(new_dialog, stack);
}
#endif
static char s_text_test_str[1024];
static void prv_perftest_test_main(void *data) {
profiler_init();
GFont font = fonts_get_system_font(s_perftest_text_arguments.font_key);
int text_index;
int font_index;
if (strcmp(s_perftest_text_arguments.string_type, "best") == 0) {
text_index = TestString_Best;
} else if (strcmp(s_perftest_text_arguments.string_type, "worst") == 0) {
text_index = TestString_Worst;
} else if (strcmp(s_perftest_text_arguments.string_type, "typical") == 0) {
text_index = TestString_Typical;
} else {
prompt_send_response("Incorrect type argument, must be 'best', 'typical', or 'worst'.");
return;
}
if (strcmp(s_perftest_text_arguments.font_key, "RESOURCE_ID_GOTHIC_18") == 0) {
font_index = TestStringFont_Gothic18;
} else if (strcmp(s_perftest_text_arguments.font_key, "RESOURCE_ID_GOTHIC_24_BOLD") == 0) {
font_index = TestStringFont_Gothic24B;
} else {
font_index = TestStringFont_Other;
}
#if TEXT_PERFTEST_MODAL
// Length replaces the yoffset argument
size_t length = atoi(s_perftest_text_arguments.y_offset);
if (length == 0) {
length = STRING_LENGTH_MAX;
}
#else
size_t length = s_perftest_text_strings[text_index].lengths[font_index];
#endif
length = MIN(length, sizeof(s_text_test_str));
strncpy(s_text_test_str, s_perftest_text_strings[text_index].string,
length);
s_text_test_str[length] = '\0';
#if TEXT_PERFTEST_MODAL
prv_display_modal(modal_manager_get_window_stack(ModalPriorityAlert), s_text_test_str);
s_perftest_text_arguments.string_type = NULL;
return;
#endif
GRect bounds = GRect(0, 0, DISP_COLS, DISP_ROWS);
int y_offset = atoi(s_perftest_text_arguments.y_offset);
bounds.origin.y -= y_offset;
if (y_offset > 0) {
bounds.size.h = DISP_ROWS + y_offset;
}
uint32_t avg = 0;
for (int i = 0; i < PERFTEST_TEXT_ITERATIONS; i++) {
// Sometimes this loop takes long enough that we end up watchdogging
watchdog_feed();
task_watchdog_bit_set_all();
GContext *ctx = prv_perftest_get_context();
graphics_context_set_text_color(ctx, GColorBlack);
profiler_start();
graphics_draw_text(ctx, s_text_test_str, font, bounds,
TEXT_OVERFLOW, TEXT_ALIGNMENT, NULL);
profiler_stop();
avg += profiler_get_total_duration(true);
}
avg /= PERFTEST_TEXT_ITERATIONS;
char buf[80];
uint32_t flash_us_avg = PROFILER_NODE_GET_TOTAL_US(text_render_flash) / PERFTEST_TEXT_ITERATIONS;
prompt_send_response_fmt(buf, sizeof(buf), "%s, %s, %s, %"PRIu32", %"PRIu32,
s_perftest_text_arguments.font_key,
s_perftest_text_arguments.string_type,
s_perftest_text_arguments.y_offset,
avg, flash_us_avg);
s_perftest_text_arguments.string_type = NULL;
}
void command_perftest_text(const char *string_type, const char *fontkey, const char *yoffset) {
s_perftest_text_arguments.string_type = string_type;
s_perftest_text_arguments.font_key = fontkey;
s_perftest_text_arguments.y_offset = yoffset;
launcher_task_add_callback(prv_perftest_test_main, NULL);
while (s_perftest_text_arguments.string_type != NULL) {
taskYIELD();
watchdog_feed();
task_watchdog_bit_set_all();
}
}
void command_perftest_text_all(void) {
static const char *fonts[] = {
"RESOURCE_ID_GOTHIC_28",
"RESOURCE_ID_GOTHIC_24",
"RESOURCE_ID_GOTHIC_18",
"RESOURCE_ID_GOTHIC_28_BOLD",
"RESOURCE_ID_GOTHIC_24_BOLD",
"RESOURCE_ID_GOTHIC_18_BOLD",
};
static const char *types[] = {
"best", "worst", "typical",
};
static const char *offsets[] = {
"0", "2000",
};
prompt_send_response("Font, Type, Offset, Total avg us, Flash avg us");
for (unsigned int type = 0; type < ARRAY_LENGTH(types); type++) {
for (unsigned int font = 0; font < ARRAY_LENGTH(fonts); font++) {
for (unsigned int offset = 0; offset < ARRAY_LENGTH(offsets); offset++) {
command_perftest_text(types[type], fonts[font], offsets[offset]);
}
}
}
}
#endif