Add J-Link RTT logging module with RAM ring buffer and dump tool.
Log boot/power/touch/key/UI/NVM events; export via rtt_log_dump.py for field diagnostics. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b8eaf2c808
commit
7fea921727
|
|
@ -0,0 +1,207 @@
|
|||
#include "includes.h"
|
||||
#include "SEGGER_RTT.h"
|
||||
#include <stdarg.h>
|
||||
|
||||
#if APP_LOG_ENABLE
|
||||
|
||||
static char s_slots[APP_LOG_SLOT_COUNT][APP_LOG_SLOT_SIZE];
|
||||
static uint16_t s_head;
|
||||
static uint16_t s_count;
|
||||
static uint32_t s_overflow;
|
||||
static uint32_t s_boot_count;
|
||||
static char s_ui_page[16] = "boot";
|
||||
static char s_debug_level = 'D';
|
||||
static char s_cmd_buf[48];
|
||||
static uint8_t s_cmd_len;
|
||||
|
||||
static uint32_t app_log_ms(void)
|
||||
{
|
||||
return (uint32_t)(rt_tick_get() * 1000U / RT_TICK_PER_SECOND);
|
||||
}
|
||||
|
||||
static void app_log_slot_store(const char *line)
|
||||
{
|
||||
uint16_t idx = s_head;
|
||||
|
||||
strncpy(s_slots[idx], line, APP_LOG_SLOT_SIZE - 1U);
|
||||
s_slots[idx][APP_LOG_SLOT_SIZE - 1U] = '\0';
|
||||
s_head = (uint16_t)((s_head + 1U) % APP_LOG_SLOT_COUNT);
|
||||
if (s_count < APP_LOG_SLOT_COUNT) {
|
||||
s_count++;
|
||||
} else {
|
||||
s_overflow++;
|
||||
}
|
||||
}
|
||||
|
||||
void app_log_set_ui_page(const char *name)
|
||||
{
|
||||
if (name == NULL) {
|
||||
return;
|
||||
}
|
||||
strncpy(s_ui_page, name, sizeof(s_ui_page) - 1U);
|
||||
s_ui_page[sizeof(s_ui_page) - 1U] = '\0';
|
||||
}
|
||||
|
||||
void app_log_write(char level, const char *cat, const char *fmt, ...)
|
||||
{
|
||||
char line[APP_LOG_SLOT_SIZE];
|
||||
va_list ap;
|
||||
int n;
|
||||
|
||||
if (level == 'D' && s_debug_level != 'D') {
|
||||
return;
|
||||
}
|
||||
|
||||
n = snprintf(line, sizeof(line), "[%05u][%c][%-4s] ",
|
||||
(unsigned)app_log_ms(), level, cat);
|
||||
if (n < 0) {
|
||||
return;
|
||||
}
|
||||
if ((size_t)n >= sizeof(line)) {
|
||||
n = (int)sizeof(line) - 1;
|
||||
}
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(line + n, sizeof(line) - (size_t)n, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
app_log_slot_store(line);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "\n");
|
||||
}
|
||||
|
||||
static void app_log_dump_ram(void)
|
||||
{
|
||||
uint16_t start;
|
||||
uint16_t i;
|
||||
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "LOG_DUMP_BEGIN\n");
|
||||
{
|
||||
char meta[APP_LOG_SLOT_SIZE];
|
||||
snprintf(meta, sizeof(meta),
|
||||
"meta boot=%u lines=%u overflow=%u ui=%s",
|
||||
(unsigned)s_boot_count,
|
||||
(unsigned)s_count,
|
||||
(unsigned)s_overflow,
|
||||
s_ui_page);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, meta);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "\n");
|
||||
}
|
||||
|
||||
if (s_count == 0U) {
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "LOG_DUMP_END\n");
|
||||
return;
|
||||
}
|
||||
|
||||
start = (s_count < APP_LOG_SLOT_COUNT)
|
||||
? 0U
|
||||
: s_head;
|
||||
for (i = 0; i < s_count; i++) {
|
||||
uint16_t idx = (uint16_t)((start + i) % APP_LOG_SLOT_COUNT);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, s_slots[idx]);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "\n");
|
||||
}
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "LOG_DUMP_END\n");
|
||||
}
|
||||
|
||||
static void app_log_clear_ram(void)
|
||||
{
|
||||
s_head = 0U;
|
||||
s_count = 0U;
|
||||
s_overflow = 0U;
|
||||
memset(s_slots, 0, sizeof(s_slots));
|
||||
LOG_I("LOG", "ram cleared");
|
||||
}
|
||||
|
||||
static void app_log_print_status(void)
|
||||
{
|
||||
char line[APP_LOG_SLOT_SIZE];
|
||||
|
||||
snprintf(line, sizeof(line),
|
||||
"LOG_STATUS boot=%u lines=%u overflow=%u level=%c ui=%s tick=%u",
|
||||
(unsigned)s_boot_count,
|
||||
(unsigned)s_count,
|
||||
(unsigned)s_overflow,
|
||||
s_debug_level,
|
||||
s_ui_page,
|
||||
(unsigned)app_log_ms());
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "\n");
|
||||
}
|
||||
|
||||
uint8_t app_log_try_command(const char *cmd)
|
||||
{
|
||||
if (cmd == NULL || cmd[0] == '\0') {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
if (strncmp(cmd, "log dump", 8) == 0) {
|
||||
app_log_dump_ram();
|
||||
return 1U;
|
||||
}
|
||||
if (strncmp(cmd, "log clear", 9) == 0) {
|
||||
app_log_clear_ram();
|
||||
return 1U;
|
||||
}
|
||||
if (strncmp(cmd, "log status", 10) == 0) {
|
||||
app_log_print_status();
|
||||
return 1U;
|
||||
}
|
||||
if (strncmp(cmd, "log level ", 10) == 0 && cmd[10] != '\0') {
|
||||
s_debug_level = cmd[10];
|
||||
LOG_I("LOG", "level set %c", s_debug_level);
|
||||
return 1U;
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
static void app_log_feed_char(char c)
|
||||
{
|
||||
if (c == '\r' || c == '\n' || c == '\0') {
|
||||
if (s_cmd_len > 0U) {
|
||||
s_cmd_buf[s_cmd_len] = '\0';
|
||||
(void)app_log_try_command(s_cmd_buf);
|
||||
}
|
||||
s_cmd_len = 0U;
|
||||
s_cmd_buf[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_cmd_len + 1U >= sizeof(s_cmd_buf)) {
|
||||
s_cmd_len = 0U;
|
||||
}
|
||||
s_cmd_buf[s_cmd_len++] = c;
|
||||
s_cmd_buf[s_cmd_len] = '\0';
|
||||
}
|
||||
|
||||
void app_log_poll(void)
|
||||
{
|
||||
char rx[16];
|
||||
unsigned read_len;
|
||||
unsigned i;
|
||||
|
||||
read_len = SEGGER_RTT_Read(APP_LOG_RTT_CHANNEL, rx, sizeof(rx));
|
||||
if (read_len == 0U) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < read_len; i++) {
|
||||
app_log_feed_char(rx[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void app_log_init(void)
|
||||
{
|
||||
s_boot_count++;
|
||||
LOG_I("BOOT", "app_log init boot=%u", (unsigned)s_boot_count);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void app_log_init(void) {}
|
||||
void app_log_write(char level, const char *cat, const char *fmt, ...) { (void)level; (void)cat; (void)fmt; }
|
||||
void app_log_poll(void) {}
|
||||
uint8_t app_log_try_command(const char *cmd) { (void)cmd; return 0U; }
|
||||
void app_log_set_ui_page(const char *name) { (void)name; }
|
||||
|
||||
#endif /* APP_LOG_ENABLE */
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#ifndef __APP_LOG_H__
|
||||
#define __APP_LOG_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef APP_LOG_ENABLE
|
||||
#define APP_LOG_ENABLE 1
|
||||
#endif
|
||||
|
||||
#ifndef APP_LOG_DEBUG
|
||||
#define APP_LOG_DEBUG 1
|
||||
#endif
|
||||
|
||||
#ifndef APP_LOG_RAM_SIZE
|
||||
#define APP_LOG_RAM_SIZE (4U * 1024U)
|
||||
#endif
|
||||
|
||||
#ifndef APP_LOG_SLOT_SIZE
|
||||
#define APP_LOG_SLOT_SIZE 128U
|
||||
#endif
|
||||
|
||||
#ifndef APP_LOG_RTT_CHANNEL
|
||||
#define APP_LOG_RTT_CHANNEL 0
|
||||
#endif
|
||||
|
||||
#ifndef APP_LOG_TP_VERBOSE
|
||||
#define APP_LOG_TP_VERBOSE 1
|
||||
#endif
|
||||
|
||||
#define APP_LOG_SLOT_COUNT (APP_LOG_RAM_SIZE / APP_LOG_SLOT_SIZE)
|
||||
|
||||
void app_log_init(void);
|
||||
void app_log_write(char level, const char *cat, const char *fmt, ...);
|
||||
void app_log_poll(void);
|
||||
uint8_t app_log_try_command(const char *cmd);
|
||||
void app_log_set_ui_page(const char *name);
|
||||
|
||||
#if APP_LOG_ENABLE
|
||||
|
||||
#define LOG_I(cat, fmt, ...) app_log_write('I', (cat), (fmt), ##__VA_ARGS__)
|
||||
#define LOG_W(cat, fmt, ...) app_log_write('W', (cat), (fmt), ##__VA_ARGS__)
|
||||
#define LOG_E(cat, fmt, ...) app_log_write('E', (cat), (fmt), ##__VA_ARGS__)
|
||||
|
||||
#if APP_LOG_DEBUG
|
||||
#define LOG_D(cat, fmt, ...) app_log_write('D', (cat), (fmt), ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_D(cat, fmt, ...) ((void)0)
|
||||
#endif
|
||||
|
||||
#if APP_LOG_TP_VERBOSE
|
||||
#define LOG_TP(fmt, ...) LOG_D("TP", fmt, ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_TP(fmt, ...) ((void)0)
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#define LOG_I(cat, fmt, ...) ((void)0)
|
||||
#define LOG_W(cat, fmt, ...) ((void)0)
|
||||
#define LOG_E(cat, fmt, ...) ((void)0)
|
||||
#define LOG_D(cat, fmt, ...) ((void)0)
|
||||
#define LOG_TP(fmt, ...) ((void)0)
|
||||
|
||||
#endif
|
||||
|
||||
#endif /* __APP_LOG_H__ */
|
||||
|
|
@ -78,6 +78,7 @@ uint8_t app_tm1617_scan_key(void)
|
|||
if (key != key_last)
|
||||
{
|
||||
key_last = key;
|
||||
LOG_I("KEY", "tm1617 key=%u", (unsigned)key);
|
||||
TM1617_Handle(key);
|
||||
return key;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ uint8_t app_tm1629_Scan_Key(void)
|
|||
if(key != key_last)
|
||||
{
|
||||
key_last = key;
|
||||
LOG_I("KEY", "tm1629 key=%u", (unsigned)key);
|
||||
TM1629_Handle(key);
|
||||
return key;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#include "includes.h"
|
||||
|
||||
/* 按下即发点击;抬起仅补发未发出的短按 */
|
||||
/* ?????????????????????????????? */
|
||||
|
||||
typedef enum {
|
||||
TOUCH_IDLE,
|
||||
|
|
@ -58,6 +58,7 @@ app_touch_result_t app_touch_process(uint16_t x, uint16_t y, uint8_t pressed)
|
|||
r.gesture = GESTURE_RELEASE;
|
||||
r.x = down_x;
|
||||
r.y = down_y;
|
||||
LOG_D("TP", "release x=%u y=%u", (unsigned)down_x, (unsigned)down_y);
|
||||
MainTask_Sendmsg(MSG_ID_TOUCH, 1, down_x, down_y);
|
||||
}
|
||||
touch_state = TOUCH_IDLE;
|
||||
|
|
@ -79,8 +80,9 @@ app_touch_result_t app_touch_process(uint16_t x, uint16_t y, uint8_t pressed)
|
|||
click_sent = 0;
|
||||
repeat_tick = 0;
|
||||
touch_state = TOUCH_PRESS_WAIT_STABLE;
|
||||
/* 第一帧就发点击,模式选择等页跟手 */
|
||||
/* ??????????????????????? */
|
||||
click_sent = 1;
|
||||
LOG_D("TP", "press down x=%u y=%u", (unsigned)down_x, (unsigned)down_y);
|
||||
MainTask_Sendmsg(MSG_ID_TOUCH, 1, down_x, down_y);
|
||||
break;
|
||||
|
||||
|
|
@ -101,6 +103,7 @@ app_touch_result_t app_touch_process(uint16_t x, uint16_t y, uint8_t pressed)
|
|||
long_press_fired = 1;
|
||||
repeat_tick = now;
|
||||
r.gesture = GESTURE_LONG_PRESS;
|
||||
LOG_D("TP", "long_press x=%u y=%u", (unsigned)down_x, (unsigned)down_y);
|
||||
MainTask_Sendmsg(MSG_ID_TOUCH_LONG_REPEAT, 1, down_x, down_y);
|
||||
return r;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,12 +96,22 @@ bool AutoCloseFlag = true;
|
|||
void MainTask_Sendmsg(uint16_t ID, uint16_t ID2, uint16_t HiByte, uint16_t LoByte)
|
||||
{
|
||||
DisplayTaskMessage_Type msg;
|
||||
rt_err_t ret;
|
||||
|
||||
msg.MessageType = ID; /* 业务消息 ID(MSG_ID_xxx) */
|
||||
msg.ID = ID2;
|
||||
msg.HiByte = HiByte;
|
||||
msg.LoByte = LoByte;
|
||||
rt_mq_send(MainTask_msg, &msg, sizeof(DisplayTaskMessage_Type));
|
||||
ret = rt_mq_send(MainTask_msg, &msg, sizeof(DisplayTaskMessage_Type));
|
||||
|
||||
if (ret != RT_EOK) {
|
||||
LOG_E("MSG", "send fail id=%u ret=%d", (unsigned)ID, (int)ret);
|
||||
} else if (ID == MSG_ID_TOUCH || ID == MSG_ID_TOUCH_LONG_REPEAT ||
|
||||
ID == MSG_ID_POWER_ON || ID == MSG_ID_POWER_OFF ||
|
||||
ID == MSG_ID_BP_KEY || ID == MSG_ID_TAP_SPEED ||
|
||||
ID == MSG_ID_BP_TRANSPOSE) {
|
||||
LOG_I("MSG", "id=%u hi=%u lo=%u", (unsigned)ID, (unsigned)HiByte, (unsigned)LoByte);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -153,6 +163,7 @@ bool powon = false;
|
|||
/* ==================== 开机/关机动作 ==================== */
|
||||
void System_PowerOn(void)
|
||||
{
|
||||
LOG_I("PWR", "power_on begin");
|
||||
/* 控制电源硬件 */
|
||||
BSP_MainPowerEnable(1);
|
||||
BSP_DreamCorePowerEnable(1);
|
||||
|
|
@ -165,6 +176,7 @@ void System_PowerOn(void)
|
|||
app_tm1617_init();
|
||||
XPT2046_Init();
|
||||
App_Auto_Init();
|
||||
LOG_I("PWR", "periph ready send POWER_ON");
|
||||
/* 通知 UI 层画开机界面 */
|
||||
MainTask_Sendmsg(MSG_ID_POWER_ON,0,0,0);
|
||||
}
|
||||
|
|
@ -176,10 +188,14 @@ static void PowerOn(void)
|
|||
|
||||
static void PowerOff(void)
|
||||
{
|
||||
uint8_t nvm_ret;
|
||||
|
||||
LOG_I("PWR", "power_off begin");
|
||||
powon = false; /* 更新关机标志 */
|
||||
StopFullTask();
|
||||
|
||||
drv_nvm_save_to_flash();
|
||||
nvm_ret = drv_nvm_save_to_flash();
|
||||
LOG_I("PWR", "nvm_save=%u", (unsigned)nvm_ret);
|
||||
|
||||
LCD_FillByColor(0, 0, 240, 320, BLACK);
|
||||
LCD_BLK_Clr();
|
||||
|
|
@ -192,6 +208,7 @@ static void PowerOff(void)
|
|||
BSP_DreamCorePowerEnable(0);
|
||||
BSP_HT7178PowerEnable(0);
|
||||
BSP_BlueToothPowerEnable(0);
|
||||
LOG_I("PWR", "reset");
|
||||
__disable_irq();
|
||||
nvic_system_reset();
|
||||
//MainTask_Sendmsg(MSG_ID_POWER_OFF,0,0,0);
|
||||
|
|
@ -210,6 +227,7 @@ void Power_Key_Scan()
|
|||
if (s_auto_poweron_pending)
|
||||
{
|
||||
s_auto_poweron_pending = 0;
|
||||
LOG_I("PWR", "auto power_on");
|
||||
PowerOn();
|
||||
pwr_key_state = KEY_STATE_NONE; /* 忽略开机瞬间可能残留的按键电平 */
|
||||
return;
|
||||
|
|
@ -238,11 +256,13 @@ void Power_Key_Scan()
|
|||
/* 用 powon 区分:关机长按 3s 开机,开机长按 2s 关机 */
|
||||
if(powon == false && holdtick >= PWR_ON_HOLD_MS)
|
||||
{
|
||||
LOG_I("PWR", "key power_on hold=%u", (unsigned)holdtick);
|
||||
PowerOn();
|
||||
pwr_key_state = KEY_STATE_NONE;
|
||||
}
|
||||
else if(powon == true && holdtick >= PWR_OFF_HOLD_MS)
|
||||
{
|
||||
LOG_I("PWR", "key power_off hold=%u", (unsigned)holdtick);
|
||||
PowerOff();
|
||||
pwr_key_state = KEY_STATE_NONE;
|
||||
}
|
||||
|
|
|
|||
33
UI/UI_Idle.c
33
UI/UI_Idle.c
|
|
@ -211,18 +211,29 @@ void IdleProcess(DisplayTaskMessage_Type msg)
|
|||
break;
|
||||
|
||||
case MSG_ID_TOUCH:
|
||||
for(uint8_t k=0; k<TOUCH_AREA_MODE_SELECT_NUM; k++)
|
||||
{
|
||||
if( msg.HiByte >= Touch_ModeSelect_Areas[k].x_min && msg.HiByte <= Touch_ModeSelect_Areas[k].x_max &&
|
||||
msg.LoByte >= Touch_ModeSelect_Areas[k].y_min && msg.LoByte <= Touch_ModeSelect_Areas[k].y_max )
|
||||
{
|
||||
uint8_t row = (uint8_t)Touch_ModeSelect_Areas[k].Flag;
|
||||
if (row == s_mode_select_focus)
|
||||
Touch_Mode_Select_Action(Touch_ModeSelect_Areas[k].Flag, Touch_ModeSelect_Areas[k].ID, k);
|
||||
else
|
||||
UI_ModeSelect_SetFocus(row);
|
||||
break;
|
||||
}
|
||||
uint8_t hit = 0U;
|
||||
for(uint8_t k=0; k<TOUCH_AREA_MODE_SELECT_NUM; k++)
|
||||
{
|
||||
if( msg.HiByte >= Touch_ModeSelect_Areas[k].x_min && msg.HiByte <= Touch_ModeSelect_Areas[k].x_max &&
|
||||
msg.LoByte >= Touch_ModeSelect_Areas[k].y_min && msg.LoByte <= Touch_ModeSelect_Areas[k].y_max )
|
||||
{
|
||||
hit = 1U;
|
||||
uint8_t row = (uint8_t)Touch_ModeSelect_Areas[k].Flag;
|
||||
LOG_I("UI", "mode_select hit area=%u row=%u x=%u y=%u",
|
||||
(unsigned)k, (unsigned)row,
|
||||
(unsigned)msg.HiByte, (unsigned)msg.LoByte);
|
||||
if (row == s_mode_select_focus)
|
||||
Touch_Mode_Select_Action(Touch_ModeSelect_Areas[k].Flag, Touch_ModeSelect_Areas[k].ID, k);
|
||||
else
|
||||
UI_ModeSelect_SetFocus(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hit) {
|
||||
LOG_I("UI", "mode_select miss x=%u y=%u",
|
||||
(unsigned)msg.HiByte, (unsigned)msg.LoByte);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
|
|||
|
|
@ -377,14 +377,25 @@ void UI_SongMode_Process(DisplayTaskMessage_Type msg)
|
|||
switch(msg.MessageType)
|
||||
{
|
||||
case MSG_ID_TOUCH:
|
||||
for(uint8_t k=0; k<TOUCH_AREA_NUM; k++)
|
||||
{
|
||||
if( msg.HiByte >= pCurrentTouch_Area[k].x_min && msg.HiByte <= pCurrentTouch_Area[k].x_max &&
|
||||
msg.LoByte >= pCurrentTouch_Area[k].y_min && msg.LoByte <= pCurrentTouch_Area[k].y_max )
|
||||
{
|
||||
Touch_Action(pCurrentTouch_Area[k].Flag,pCurrentTouch_Area[k].ID,k,pCurrentTouch_Area[k].Value);
|
||||
ResetAutoPowerCount();
|
||||
break;
|
||||
{
|
||||
uint8_t hit = 0U;
|
||||
for(uint8_t k=0; k<TOUCH_AREA_NUM; k++)
|
||||
{
|
||||
if( msg.HiByte >= pCurrentTouch_Area[k].x_min && msg.HiByte <= pCurrentTouch_Area[k].x_max &&
|
||||
msg.LoByte >= pCurrentTouch_Area[k].y_min && msg.LoByte <= pCurrentTouch_Area[k].y_max )
|
||||
{
|
||||
hit = 1U;
|
||||
LOG_I("UI", "song hit area=%u id=%u x=%u y=%u",
|
||||
(unsigned)k, (unsigned)pCurrentTouch_Area[k].ID,
|
||||
(unsigned)msg.HiByte, (unsigned)msg.LoByte);
|
||||
Touch_Action(pCurrentTouch_Area[k].Flag,pCurrentTouch_Area[k].ID,k,pCurrentTouch_Area[k].Value);
|
||||
ResetAutoPowerCount();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hit) {
|
||||
LOG_I("UI", "song miss x=%u y=%u",
|
||||
(unsigned)msg.HiByte, (unsigned)msg.LoByte);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -32,44 +32,60 @@ void InitMenuUI(void) {
|
|||
// <20><><EFBFBD><EFBFBD> UI <20><><EFBFBD>ú<EFBFBD><C3BA><EFBFBD>
|
||||
// ==============================================
|
||||
void CallUI_Idle(void) {
|
||||
CurrUIProcress = IdleProcess;
|
||||
CurrUIProcress = IdleProcess;
|
||||
app_log_set_ui_page("Idle");
|
||||
LOG_I("UI", "page Idle");
|
||||
}
|
||||
|
||||
void CallUI_SongMode(void) {
|
||||
UI_SongMode_Init();
|
||||
CurrUIProcress = UI_SongMode_Process;
|
||||
app_log_set_ui_page("Song");
|
||||
LOG_I("UI", "page SongMode tab=%u", (unsigned)mGuiData[GUI_TAB_INDEX].Current);
|
||||
}
|
||||
|
||||
void CallUI_ExpertMode(void) {
|
||||
UI_ExpertMode_Init();
|
||||
// CurrUIProcress = UI_ExpertMode_Process;
|
||||
CurrUIProcress = UI_SongMode_Process;
|
||||
app_log_set_ui_page("Expert");
|
||||
LOG_I("UI", "page ExpertMode");
|
||||
}
|
||||
|
||||
void CallUI_FreeMode(void) {
|
||||
UI_FreeMode_Init();
|
||||
UI_FreeMode_Init();
|
||||
// CurrUIProcress = UI_FreeMode_Process;
|
||||
CurrUIProcress = UI_SongMode_Process;
|
||||
app_log_set_ui_page("Free");
|
||||
LOG_I("UI", "page FreeMode");
|
||||
}
|
||||
|
||||
void CallUI_Setting(void) {
|
||||
CurrUIProcress = UI_Setting_Process;
|
||||
UI_Setting_Init();
|
||||
app_log_set_ui_page("Setting");
|
||||
LOG_I("UI", "page Setting");
|
||||
}
|
||||
|
||||
void CallUI_Mixer(void) {
|
||||
CurrUIProcress = UI_Mixer_Process;
|
||||
UI_Mixer_Init();
|
||||
app_log_set_ui_page("Mixer");
|
||||
LOG_I("UI", "page Mixer");
|
||||
}
|
||||
|
||||
void CallUI_SystemSetting(void) {
|
||||
CurrUIProcress = UI_SystemSet_Process;
|
||||
UI_SystemSet_Init();
|
||||
app_log_set_ui_page("SysSet");
|
||||
LOG_I("UI", "page SystemSetting");
|
||||
}
|
||||
|
||||
void CallUI_RestoreSelect(void) {
|
||||
CurrUIProcress = UI_Restore_Select_Process;
|
||||
UI_Restore_Select_Init();
|
||||
app_log_set_ui_page("Restore");
|
||||
LOG_I("UI", "page Restore");
|
||||
}
|
||||
|
||||
static uint8_t s_ui_focus_id = UI_FOCUS_NONE;
|
||||
|
|
|
|||
|
|
@ -198,6 +198,14 @@ static uint8_t LCD_Dump_CommandPending(void)
|
|||
char c = rx[i];
|
||||
|
||||
if (c == '\r' || c == '\n' || c == '\0') {
|
||||
if (s_cmd_len > 0U) {
|
||||
s_cmd_buf[s_cmd_len] = '\0';
|
||||
if (app_log_try_command(s_cmd_buf)) {
|
||||
s_cmd_len = 0U;
|
||||
s_cmd_buf[0] = '\0';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
s_cmd_len = 0U;
|
||||
s_cmd_buf[0] = '\0';
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -143,9 +143,12 @@ TP_Point TP_ReadRaw(void)
|
|||
TP_Point p = {0, 0, 0};
|
||||
uint32_t x_sum = 0, y_sum = 0;
|
||||
uint8_t i;
|
||||
static uint8_t s_raw_logged;
|
||||
|
||||
if (!TP_CheckPressed(3))
|
||||
if (!TP_CheckPressed(3)) {
|
||||
s_raw_logged = 0U;
|
||||
return p;
|
||||
}
|
||||
|
||||
for (i = 0; i < 5; i++)
|
||||
{
|
||||
|
|
@ -178,6 +181,11 @@ TP_Point TP_ReadRaw(void)
|
|||
}
|
||||
|
||||
p.pressed = 1;
|
||||
if (!s_raw_logged) {
|
||||
LOG_TP("raw x=%u y=%u irq=%u", (unsigned)p.x, (unsigned)p.y,
|
||||
(unsigned)(TP_IRQ_Read() == 0U));
|
||||
s_raw_logged = 1U;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ void drv_nvm_init(void)
|
|||
dataSum1 = ~dataSum0;
|
||||
|
||||
if ((dataSum0 != mNvmData.sum[0]) || (dataSum1 != mNvmData.sum[1]) || (mNvmData.data.nvm_version != NVM_VERSION)) {
|
||||
// load default value;
|
||||
LOG_W("NVM", "checksum fail load default ver=%u", (unsigned)mNvmData.data.nvm_version);
|
||||
drv_nvm_load_default_value(&mNvmData.data);
|
||||
drv_nvm_save_to_flash();
|
||||
}else
|
||||
|
|
@ -88,7 +88,8 @@ void drv_nvm_init(void)
|
|||
drv_nvm_save_to_flash();
|
||||
}
|
||||
memcpy(&mNvmDataShadow, &mNvmData, sizeof(NvmData_Type));
|
||||
}
|
||||
LOG_I("NVM", "init ok ver=%u", (unsigned)mNvmData.data.nvm_version);
|
||||
}
|
||||
|
||||
NvmParam_Type * drv_nvm_param_ptr(void)
|
||||
{
|
||||
|
|
@ -146,8 +147,9 @@ uint8_t drv_nvm_save_to_flash(void)
|
|||
uint32_t addr = INNER_FLASH_SAVE_ADDRESS;
|
||||
NvmData_Type * NvmDataToSave;
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>仯<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// 无变化不保存
|
||||
if (memcmp(&mNvmDataShadow, &mNvmData, sizeof(NvmData_Type)) == 0) {
|
||||
LOG_D("NVM", "save skip unchanged");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +174,7 @@ uint8_t drv_nvm_save_to_flash(void)
|
|||
flash_write_nocheck(addr,pData16,len);
|
||||
flash_lock();
|
||||
|
||||
LOG_I("NVM", "saved to flash");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2001,6 +2001,12 @@
|
|||
<file>
|
||||
<name>$PROJ_DIR$\..\..\APP\app_touch.h</name>
|
||||
</file>
|
||||
<file>
|
||||
<name>$PROJ_DIR$\..\..\APP\app_log.c</name>
|
||||
</file>
|
||||
<file>
|
||||
<name>$PROJ_DIR$\..\..\APP\app_log.h</name>
|
||||
</file>
|
||||
</group>
|
||||
<group>
|
||||
<name>AutoBand</name>
|
||||
|
|
|
|||
|
|
@ -76,5 +76,6 @@
|
|||
#include "app_tm1617.h"
|
||||
#include "app_touch.h"
|
||||
#include "app_adc.h"
|
||||
#include "app_log.h"
|
||||
|
||||
#endif
|
||||
|
|
@ -83,6 +83,7 @@ int main(void)
|
|||
bsp_init();
|
||||
LCD_Init();
|
||||
LCD_Dump_Init();
|
||||
app_log_init();
|
||||
drv_nvm_init();
|
||||
TaskInit();
|
||||
timer_init();
|
||||
|
|
@ -90,6 +91,7 @@ int main(void)
|
|||
BSP_HT7178PowerEnable (1);
|
||||
BSP_ChargerEnable(1);
|
||||
CurrUIProcress = IdleProcess;
|
||||
LOG_I("BOOT", "main init done ui=Idle");
|
||||
/* add user code end 2 */
|
||||
|
||||
while(1)
|
||||
|
|
@ -102,6 +104,9 @@ int main(void)
|
|||
/* add user code begin 3 */
|
||||
Power_Key_Scan();
|
||||
LCD_Dump_Poll();
|
||||
#if !DEBUG_LCD_DUMP
|
||||
app_log_poll();
|
||||
#endif
|
||||
|
||||
rt_thread_delay(1);
|
||||
/* add user code end 3 */
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#include "includes.h"
|
||||
#include "SEGGER_RTT.h"
|
||||
|
||||
static struct rt_thread TaskScanThread;
|
||||
static struct rt_thread TaskScanThread;
|
||||
static struct rt_thread TaskUIThread;
|
||||
static struct rt_thread TaskTouchThread;
|
||||
static struct rt_thread TaskMainThread;
|
||||
|
|
@ -51,9 +50,7 @@ void TaskTouchThread_entry(void* parameter)
|
|||
TP_Point tp = TP_Read(); /* 已含轴交换/镜像,直接进手势 */
|
||||
if (tp.pressed && !s_last_pressed)
|
||||
{
|
||||
char buf[40];
|
||||
sprintf(buf, "TP %u,%u\n", (unsigned)tp.x, (unsigned)tp.y);
|
||||
SEGGER_RTT_WriteString(0, buf);
|
||||
LOG_TP("down cal x=%u y=%u", (unsigned)tp.x, (unsigned)tp.y);
|
||||
}
|
||||
s_last_pressed = tp.pressed;
|
||||
app_touch_process(tp.x, tp.y, tp.pressed);
|
||||
|
|
@ -154,7 +151,7 @@ rt_err_t TaskInit(void)
|
|||
BLTask_msg = rt_mq_create("BLTask msgQ",sizeof(DisplayTaskMessage_Type),128,RT_IPC_FLAG_FIFO);
|
||||
if(MainTask_msg == RT_NULL || UITask_msg == RT_NULL)
|
||||
{
|
||||
__NOP();
|
||||
LOG_E("BOOT", "mq create fail main=%p ui=%p", MainTask_msg, UITask_msg);
|
||||
}
|
||||
|
||||
ret = rt_thread_init(&TaskScanThread,
|
||||
|
|
@ -316,18 +313,32 @@ void StartScanTask(void)
|
|||
|
||||
void StopFullTask(void)
|
||||
{
|
||||
if((TaskScanThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE)
|
||||
LOG_I("TASK", "StopFullTask begin");
|
||||
if((TaskScanThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||
LOG_I("TASK", "stop ScanThread");
|
||||
rt_thread_detach(&TaskScanThread);
|
||||
if((TaskUIThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE)
|
||||
}
|
||||
if((TaskUIThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||
LOG_I("TASK", "stop UIThread");
|
||||
rt_thread_detach(&TaskUIThread);
|
||||
if((TaskTouchThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE)
|
||||
}
|
||||
if((TaskTouchThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||
LOG_I("TASK", "stop TouchThread");
|
||||
rt_thread_detach(&TaskTouchThread);
|
||||
if((TaskMainThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE)
|
||||
}
|
||||
if((TaskMainThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||
LOG_I("TASK", "stop MainThread");
|
||||
rt_thread_detach(&TaskMainThread);
|
||||
if((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE)
|
||||
}
|
||||
if((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||
LOG_I("TASK", "stop BTHandleThread");
|
||||
rt_thread_detach(&TaskBTHandleThread);
|
||||
if((TaskBTRecvThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE)
|
||||
}
|
||||
if((TaskBTRecvThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||
LOG_I("TASK", "stop BTRecvThread");
|
||||
rt_thread_detach(&TaskBTRecvThread);
|
||||
}
|
||||
LOG_I("TASK", "StopFullTask done");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,30 +3,20 @@
|
|||
* The Embedded Experts *
|
||||
* www.segger.com *
|
||||
**********************************************************************
|
||||
* *
|
||||
* SEGGER RTT * Real Time Transfer for embedded targets *
|
||||
* https://github.com/SEGGERMicro/RTT *
|
||||
* *
|
||||
**********************************************************************
|
||||
|
||||
---------------------------END-OF-HEADER------------------------------
|
||||
Purpose : User configuration file for RTT.
|
||||
For available configuration,
|
||||
refer to SEGGER_RTT_ConfDefaults.h.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#ifndef SEGGER_RTT_CONF_H
|
||||
#define SEGGER_RTT_CONF_H
|
||||
|
||||
|
||||
/*********************************************************************
|
||||
*
|
||||
* Defines, configurable
|
||||
*
|
||||
**********************************************************************
|
||||
*/
|
||||
#define SEGGER_RTT_MAX_NUM_UP_BUFFERS (3)
|
||||
#define SEGGER_RTT_MAX_NUM_DOWN_BUFFERS (3)
|
||||
#define BUFFER_SIZE_UP (2048)
|
||||
#define BUFFER_SIZE_DOWN (256)
|
||||
#define SEGGER_RTT_PRINTF_BUFFER_SIZE (256u)
|
||||
|
||||
#endif
|
||||
/*************************** End of file ****************************/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
# K1 设备日志导出(J-Link RTT)
|
||||
|
||||
## 用途
|
||||
|
||||
固件运行时将启动/关机、触摸、按键、UI 切换、异常等事件写入 RAM 环形缓冲(约 4KB / 32 条),并通过 SEGGER RTT 实时输出。现场复现问题后,用 J-Link 一键导出日志文件发回分析。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- J-Link 调试器 + SWD 连接设备
|
||||
- Python 3.8+
|
||||
- 依赖:`pip install pylink-square`
|
||||
- 已烧录包含 `app_log` 模块的固件
|
||||
|
||||
## 操作步骤
|
||||
|
||||
1. 设备上电,复现问题(例如模式选择页触摸无响应)。
|
||||
2. PC 连接 J-Link,**不要**随意 Reset(会丢失 RAM 日志;若必须复位,请先导出或复现后再导出)。
|
||||
3. 在项目 `tools` 目录执行:
|
||||
|
||||
```bash
|
||||
python rtt_log_dump.py --out problem.log
|
||||
```
|
||||
|
||||
4. 将生成的 `problem.log` 发送给研发分析。
|
||||
|
||||
## 其他 RTT 命令(RTT Viewer 终端输入)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `log dump` | 导出 RAM 中全部历史日志 |
|
||||
| `log clear` | 清空 RAM 日志 |
|
||||
| `log status` | 查看 boot 次数、条数、溢出、当前 UI 页 |
|
||||
| `log level D` | 开启 Debug 级(含触摸细节);`I` 仅 Info 以上 |
|
||||
|
||||
## 日志格式
|
||||
|
||||
```
|
||||
[tick_ms][LVL][CAT] message key=val ...
|
||||
```
|
||||
|
||||
类别 CAT:`BOOT` `PWR` `UI` `TP` `KEY` `MSG` `NVM` `TASK` `ERR` `ADC`
|
||||
|
||||
## 配套工具
|
||||
|
||||
- 屏幕截图:`python rtt_lcd_capture.py --out screen.png`
|
||||
- 日志导出:`python rtt_log_dump.py --out problem.log`
|
||||
|
||||
## 触摸问题排查要点
|
||||
|
||||
导出后重点 grep:
|
||||
|
||||
- `[TP]` — raw/cal 坐标、irq 状态
|
||||
- `[MSG] id=12` — 触摸消息是否发出
|
||||
- `[UI] mode_select miss` — UI 命中测试是否失败
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Pull K1 device log ring buffer via J-Link RTT (log dump command)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
DEFAULT_DEVICES = ("Cortex-M4", "AT32F403AC", "AT32F403A")
|
||||
DUMP_CMD = b"log dump\n"
|
||||
END_MARKERS = (b"LOG_DUMP_END",)
|
||||
|
||||
|
||||
def import_deps():
|
||||
try:
|
||||
import pylink # noqa: F401
|
||||
except ImportError as exc:
|
||||
raise SystemExit("Missing dependency: pip install pylink-square") from exc
|
||||
|
||||
|
||||
def connect_jlink(device: str):
|
||||
import pylink
|
||||
|
||||
jlink = pylink.JLink()
|
||||
jlink.open()
|
||||
try:
|
||||
jlink.exec_command("HideDeviceSelection = 1")
|
||||
except Exception:
|
||||
pass
|
||||
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||
|
||||
last_error = None
|
||||
for candidate in (device, *DEFAULT_DEVICES):
|
||||
try:
|
||||
try:
|
||||
jlink.exec_command(f"Device = {candidate}")
|
||||
except Exception:
|
||||
pass
|
||||
jlink.connect(candidate)
|
||||
print(f"Connected as {candidate}")
|
||||
return jlink
|
||||
except pylink.errors.JLinkException as exc:
|
||||
last_error = exc
|
||||
jlink.close()
|
||||
raise SystemExit(f"Failed to connect J-Link: {last_error}")
|
||||
|
||||
|
||||
def find_rtt_control_block(jlink, ram_base: int = 0x20000000, ram_size: int = 0x18000) -> int | None:
|
||||
needle = b"SEGGER RTT"
|
||||
chunk = 0x1000
|
||||
was_halted = jlink.halted()
|
||||
if not was_halted:
|
||||
jlink.halt()
|
||||
try:
|
||||
for off in range(0, ram_size, chunk):
|
||||
data = bytes(jlink.memory_read8(ram_base + off, min(chunk, ram_size - off)))
|
||||
idx = data.find(needle)
|
||||
if idx >= 0:
|
||||
return ram_base + off + idx
|
||||
finally:
|
||||
if not was_halted:
|
||||
if hasattr(jlink, "restart"):
|
||||
jlink.restart()
|
||||
else:
|
||||
jlink.go()
|
||||
time.sleep(0.1)
|
||||
return None
|
||||
|
||||
|
||||
def wait_rtt_ready(jlink, timeout_s: float = 15.0) -> None:
|
||||
deadline = time.time() + timeout_s
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
if jlink.rtt_get_num_up_buffers() > 0:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
raise SystemExit("RTT control block found but buffers not ready")
|
||||
|
||||
|
||||
def send_command(jlink, payload: bytes, retries: int = 30) -> None:
|
||||
for _ in range(retries):
|
||||
wrote = jlink.rtt_write(0, list(payload))
|
||||
if wrote > 0:
|
||||
print(f"Sent command ({wrote} bytes): {payload.decode('ascii', errors='replace').strip()}")
|
||||
return
|
||||
time.sleep(0.2)
|
||||
raise SystemExit("Failed to send RTT down command on channel 0")
|
||||
|
||||
|
||||
def dump_log(out_path: str, device: str, timeout_s: float) -> None:
|
||||
jlink = connect_jlink(device)
|
||||
lines: list[str] = []
|
||||
try:
|
||||
cb = find_rtt_control_block(jlink)
|
||||
if cb is None:
|
||||
raise SystemExit("SEGGER RTT control block not found in SRAM")
|
||||
print(f"RTT CB @ 0x{cb:08X}")
|
||||
|
||||
jlink.rtt_start(cb)
|
||||
wait_rtt_ready(jlink)
|
||||
|
||||
# Drain stale RTT output.
|
||||
for _ in range(5):
|
||||
stale = jlink.rtt_read(0, 4096)
|
||||
if stale:
|
||||
text = bytes(stale).decode("utf-8", errors="replace")
|
||||
if text.strip():
|
||||
print("RTT0 stale:", text.strip()[:200])
|
||||
time.sleep(0.05)
|
||||
|
||||
send_command(jlink, DUMP_CMD)
|
||||
|
||||
buffer = b""
|
||||
deadline = time.time() + timeout_s
|
||||
started = False
|
||||
|
||||
while time.time() < deadline:
|
||||
chunk = jlink.rtt_read(0, 4096)
|
||||
if chunk:
|
||||
buffer += bytes(chunk)
|
||||
if not started and b"LOG_DUMP_BEGIN" in buffer:
|
||||
started = True
|
||||
if b"LOG_DUMP_END" in buffer:
|
||||
break
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
|
||||
if b"LOG_DUMP_END" not in buffer:
|
||||
raise SystemExit("Timeout waiting for LOG_DUMP_END (is firmware with app_log flashed?)")
|
||||
|
||||
text = buffer.decode("utf-8", errors="replace")
|
||||
capture = False
|
||||
for line in text.splitlines():
|
||||
if line.strip() == "LOG_DUMP_BEGIN":
|
||||
capture = True
|
||||
continue
|
||||
if line.strip() == "LOG_DUMP_END":
|
||||
break
|
||||
if capture:
|
||||
lines.append(line)
|
||||
|
||||
header = (
|
||||
f"# K1 log dump {datetime.now().isoformat(timespec='seconds')}\n"
|
||||
f"# device={device}\n"
|
||||
)
|
||||
body = "\n".join(lines) + ("\n" if lines else "")
|
||||
with open(out_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(header)
|
||||
f.write(body)
|
||||
|
||||
print(f"Saved {len(lines)} log lines to {out_path}")
|
||||
finally:
|
||||
try:
|
||||
jlink.rtt_stop()
|
||||
except Exception:
|
||||
pass
|
||||
jlink.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Dump K1 RAM log ring via J-Link RTT")
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
default=f"k1_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log",
|
||||
help="Output log file path",
|
||||
)
|
||||
parser.add_argument("--device", default="AT32F403AC", help="J-Link device name")
|
||||
parser.add_argument("--timeout", type=float, default=20.0, help="Seconds to wait for dump")
|
||||
args = parser.parse_args()
|
||||
|
||||
import_deps()
|
||||
dump_log(args.out, args.device, args.timeout)
|
||||
|
||||
if os.name == "nt" and os.path.isfile(args.out):
|
||||
print(f"Full path: {os.path.abspath(args.out)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue