mirror of
https://github.com/google/pebble.git
synced 2025-04-30 15:21:41 -04:00
1503 lines
48 KiB
C
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
|