mcu_hi3321_watch/tjd/ble/protocol/ble_file_trans.c
2025-05-26 20:15:20 +08:00

1333 lines
48 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*----------------------------------------------------------------------------
* Copyright (c) Fenda Technologies Co., Ltd. 2021. All rights reserved.
*
* Description: 蓝牙文件传输中间层,供蓝牙调用
*
* Author: george
*
* Create: 2022-10-10
*--------------------------------------------------------------------------*/
#include <string.h>
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "am_util_debug.h"
#include "am_bootloader.h"
#include "am_multi_boot.h"
#include "crc32.h"
#include "amotas_api.h"
#include "sys_config.h"
#include "sys_memory.h"
#include "ble_data_transmission.h"
#include "ble_ota_execute.h"
#include "ble_file_trans.h"
#include "task_ble.h"
#include "watchdog_api.h"
#include "rom_api.h"
#include "flash_api.h"
//#include "fs_adapt.h"
//#include "fs_user_app.h"
#include "fs_api.h"
#include "fs_user_init.h"
#include "user_pack_analysis.h"
#include "ui_event.h"
#include "task_ui.h"
#include "ui_port_resource.h"
#define ENABLE_STATIC_PRINT 0
extern uint32_t am_util_stdio_printf(const char *pcFmt, ...);
#define static_print_remind(...) am_util_stdio_printf(__VA_ARGS__)
#if ENABLE_STATIC_PRINT
#define static_print_info(...) am_util_stdio_printf(__VA_ARGS__)
#else
#define static_print_info(...)
#endif
#define ENABLE_MALLOC_BUFF 0
FileTransCtrlBlock_t g_fileTransCtrlBlock;
static volatile bool g_file_trans_busy = false;
//static bool g_file_trans_use_diff = true; //差分传输的标志false表示不使用差分升级
static bool g_file_trans_use_diff = false;
static uint32_t g_file_trans_percent_count = 0;
static TimerHandle_t g_fileTransOverTimer = NULL;
static head_desc_parm g_file_trans_head_param = {0};
static head_struct_parm g_file_trans_head_struct = {0};
static lfs_file_t g_lfs_file_trans = {0};
static am_multiboot_flash_info_t *g_file_trans_pFlash = NULL;
static am_multiboot_flash_info_t g_file_trans_ext_flash = {0};
//
// Data structure for flash operation
//
typedef struct
{
uint8_t* write_buffer; // needs to be 32-bit word aligned.
uint16_t buffer_index;
}file_trans_flash_op_t;
file_trans_flash_op_t file_trans_flash_op = {
.write_buffer = NULL,
.buffer_index = 0,
};
static void FileTransOverTimerCallback(TimerHandle_t xTimer);
static void SaveFileTransFile(void);
static void LoadFileTransFile(void);
static OtaUiMsgType GetTransTypeByAction(FileActionType action);
void switch_to_unix_path(char *path);
void delete_all_complete_ota_files(void);
int32_t copy_file_to_rom(void);
/********** 片外flash操作接口 ***********/
int file_trans_flash_read_func(uint32_t ui32DestAddr, uint32_t *pSrc, uint32_t ui32Length)
{
return flash_api_read(ui32DestAddr, pSrc, ui32Length);
}
int file_trans_flash_write_func(uint32_t ui32DestAddr, uint32_t *pSrc, uint32_t ui32Length)
{
return flash_api_write(ui32DestAddr, pSrc, ui32Length);
}
int file_trans_flash_erase_func(uint32_t ui32Addr)
{
return flash_api_erase(ui32Addr);
}
int file_trans_ext_flash_info_init(am_multiboot_flash_info_t *pFlash)
{
pFlash->flashPageSize = flash_api_get_erase_unit_size();
pFlash->flashSectorSize = flash_api_get_erase_unit_size();
pFlash->flash_init = NULL;
pFlash->flash_deinit = NULL;
pFlash->flash_enable = NULL;
pFlash->flash_disable = NULL;
pFlash->flash_read_page = file_trans_flash_read_func;
pFlash->flash_write_page = file_trans_flash_write_func;
pFlash->flash_erase_sector = file_trans_flash_erase_func;
return true;
}
/********** 片外flash操作接口 ***********/
void FileTransInit(void)
{
if (g_fileTransOverTimer == NULL)
{
g_fileTransOverTimer = xTimerCreate("fileTransOverTimer", (1000 * 15), false, NULL, FileTransOverTimerCallback);
}
LoadFileTransFile();
}
/**
*****************************************************************************************
* @brief 获取文件传输状态,是否繁忙.
* @retval true-正在传输文件false-空闲.
*****************************************************************************************
*/
bool file_trans_busy(void)
{
return g_file_trans_busy;
}
void file_trans_start(uint32_t id, char *name, uint32_t size, FileActionType action)
{
// g_file_trans_use_diff = false; //for test -- 默认为true, 在此强行赋值为false用于禁止差分升级
// memset(&g_fileTransCtrlBlock, 0, sizeof(FileTransCtrlBlock_t));
g_fileTransCtrlBlock.state = FILE_TRANS_STATE_START;
g_fileTransCtrlBlock.fileId = id;
// strcpy(g_fileTransCtrlBlock.fileName, name);
snprintf(g_fileTransCtrlBlock.fileName, sizeof(g_fileTransCtrlBlock.fileName), "%s/%s", DIR_RECEIVED, name);
switch_to_unix_path(g_fileTransCtrlBlock.fileName);
g_fileTransCtrlBlock.fileSize = size;
g_fileTransCtrlBlock.offset = 0;
g_fileTransCtrlBlock.fileAction = action;
if (g_file_trans_use_diff == false)
{
g_fileTransCtrlBlock.nextPkgSize = FILE_HEAD_INFO_IEN; //必须整除一个flash擦除单元
}
else
{
// g_fileTransCtrlBlock.nextPkgSize = HEAD_DESC_LEN + HEAD_STRUCT_LEN;
g_fileTransCtrlBlock.nextPkgSize = flash_api_get_erase_unit_size();
/* if (g_fileTransCtrlBlock.nextPkgSize < 4096)
{
g_fileTransCtrlBlock.nextPkgSize = 4096;
} */
}
g_fileTransCtrlBlock.fsfd = -1;
g_fileTransCtrlBlock.type = GetTransTypeByAction(action);
g_file_trans_percent_count = 101;
amota_conn_param_update(CONN_PARAM_OTA); // 使用OTA连接参数
if (g_fileTransOverTimer != NULL)
{
xTimerStart(g_fileTransOverTimer, 0);
}
task_ui_notify(EVENT_OTA_TRANS_START, 0, NULL, 0);
}
void FileTransSetCrc(uint32_t crc)
{
static_print_info("file trans crc=0x%08X\r\n", crc);
g_fileTransCtrlBlock.crc32 = crc;
}
void FileTransSetMd5(uint8_t *md5)
{
if(memcmp(md5, g_fileTransCtrlBlock.md5, 16) != 0) {
g_fileTransCtrlBlock.offset = 0;
g_fileTransCtrlBlock.fsfd = -1;
static_print_info("different md5\r\n");
static_print_info("this time md5:");
for(uint32_t i = 0; i < 16; i++) {
static_print_info(" %02X", md5[i]);
}
static_print_info("\r\n");
static_print_info("last time md5:");
for(uint32_t i = 0; i < 16; i++) {
static_print_info(" %02X", g_fileTransCtrlBlock.md5[i]);
}
static_print_info("\r\n");
}
memcpy(g_fileTransCtrlBlock.md5, md5, 16);
static_print_info("file trans md5:");
for(uint32_t i = 0; i < 16; i++) {
static_print_info(" %02X", g_fileTransCtrlBlock.md5[i]);
}
static_print_info("\r\n");
}
/**
*****************************************************************************************
* @brief 刷新进度条
*****************************************************************************************
*/
static void file_trans_refresh_percent(void)
{
uint32_t percent;
percent = (g_fileTransCtrlBlock.offset * 100) / g_fileTransCtrlBlock.fileSize;
if (percent != g_file_trans_percent_count) {
g_file_trans_percent_count = percent;
static_print_info("file trans pct=%d%%, %d\r\n", percent, g_fileTransCtrlBlock.offset);
if ((FileActionType)g_fileTransCtrlBlock.fileAction != FILE_ACTION_WATCHFACE) {
task_ui_notify(EVENT_OTA_TRANSMITTING, g_file_trans_percent_count, NULL, 0);
}
}
}
/**
*****************************************************************************************
* @brief 根据固件头信息判断是否需要续传
*****************************************************************************************
*/
static int32_t file_trans_handle_resume(const uint8_t *data, uint32_t length, const char *file_name)
{
const char *path = NULL;
int32_t file_size = 0;
char *buffer = NULL;
uint32_t read_len = 0;
int ret = 0;
bool is_dir = false;
head_desc_parm head_desc_temp;
head_struct_parm head_struct_temp;
if(g_fileTransCtrlBlock.file_storage_cs == STORAGE_IN_FILE)
{
// path = pvPortMalloc(128);
// snprintf(path, 128, "%s/%s", DIR_RECEIVED, g_fileTransCtrlBlock.fileName);
path = file_name;
file_size = fs_api_stat(path, &is_dir);
if (file_size <= 0 || is_dir)
{
static_print_info("No need to resume because NO THIS FILE.\r\n");
// vPortFree(path);
return 0;
}
read_len = HEAD_DESC_LEN + HEAD_STRUCT_LEN;
static_print_info("FSStat(%s)=%d\r\n", path, file_size);
if(file_size < (read_len + MCU_ROM_PAGE_SIZE))
{
static_print_info("No need to resume trans because the size.\r\n");
// vPortFree(path);
return 0;
}
else
{
static_print_info("file_size=%d\r\n", file_size);
buffer = pvPortMalloc(read_len + 1);
if(buffer==NULL)
{
static_print_remind("pvPortMalloc fail.%s\r\n", path);
return 0;
}
fs_api_file_load(path, 0, buffer, read_len);
buffer[read_len] = 0;
}
if(user_pack_json_head(buffer, &head_desc_temp, &head_struct_temp)==false)
{
ret = 0;
}
else if(head_desc_temp.pack_len == g_file_trans_head_param.pack_len
&& head_struct_temp.file_crc32 == g_file_trans_head_struct.file_crc32
)
{
g_fileTransCtrlBlock.fileSize = head_desc_temp.pack_len;
ret = file_size;
}
static_print_info("pack_len = %d.crc32_desc = %d.crc32_detail = %d\r\n",
head_desc_temp.pack_len,head_struct_temp.file_crc32,head_desc_temp.crc32_detail);
// vPortFree(path);
vPortFree(buffer);
}
else if(g_fileTransCtrlBlock.file_storage_cs == STORAGE_IN_ADDR)
{
static_print_info("resume crc32 = %x, fileSize = %x\r\n", g_fileTransCtrlBlock.file_resume_crc32, g_fileTransCtrlBlock.file_resume_size);
static_print_info("file crc32 = %x, fileSize = %x\r\n", g_file_trans_head_struct.file_crc32, g_file_trans_head_struct.file_size);
if(g_file_trans_head_param.pack_len == g_fileTransCtrlBlock.file_resume_size
&& g_file_trans_head_struct.file_crc32 == g_fileTransCtrlBlock.file_resume_crc32)
{
g_fileTransCtrlBlock.fileSize = g_file_trans_head_param.pack_len;
if(g_fileTransCtrlBlock.file_resume_offset > MCU_ROM_PAGE_SIZE)
{
ret = g_fileTransCtrlBlock.file_resume_offset;
}
}
}
return ret;
}
static int process_file_head(uint8_t* data, uint32_t length)
{
int ret = 0;
ret = user_pack_json_head((char *)data, &g_file_trans_head_param, &g_file_trans_head_struct);
am_util_stdio_printf("process file head dest_addr = %x - %d, h_len = %d - %d - %d, struct_len = %d- %d - %x\r\n",
g_file_trans_head_struct.dest_addr, length,
g_file_trans_head_param.h_len, g_file_trans_head_param.pack_len, g_file_trans_head_param.ext_offset,
g_file_trans_head_param.struct_len, g_file_trans_head_param.struct_offset, g_file_trans_head_struct.file_crc32);
if(ret == false)
{
return FILE_ILLEGAL_ERR; // 非法固件
}
if(g_file_trans_head_param.mark != MARK_ALL_IN_ONE
&& g_file_trans_head_param.mark != MARK_MORE_THAN_ONE
&& g_file_trans_head_struct.dest_cs != FILE_SOUR_CS_FS)
{// 固件不以文件的形式存储须初始化flash操作接口
g_fileTransCtrlBlock.file_storage_cs = STORAGE_IN_ADDR;
if(g_file_trans_head_struct.dest_cs == FILE_SOUR_CS_FLASH)
{// 片外升级
static_print_info("FILE_SOUR_CS_FLASH\r\n");
if(g_file_trans_head_param.mark == MARK_MCU_FW_BIN) // is a run image
{
if(g_file_trans_head_struct.file_size > MAX_IMAGE_SIZE)
{
return FILE_ILLEGAL_ERR;
}
}
file_trans_ext_flash_info_init(&g_file_trans_ext_flash);
g_file_trans_pFlash = &g_file_trans_ext_flash;
static_print_info("flashPageSize = %d\r\n", g_file_trans_pFlash->flashPageSize);
}
else if(g_file_trans_head_struct.dest_cs == FILE_SOUR_CS_ROM)
{// 片内升级
static_print_info("FILE_SOUR_CS_ROM\r\n");
// Check to make sure the incoming image will fit in the space allocated for OTA
if (g_file_trans_head_struct.file_size > AMOTA_INT_FLASH_OTA_MAX_SIZE)
{
return FILE_ILLEGAL_ERR;
}
g_file_trans_pFlash = &g_intFlash;
static_print_info("flashPageSize = %d\r\n", g_file_trans_pFlash->flashPageSize);
}
else
{// 其他存储模式暂不支持
return FILE_ILLEGAL_ERR;
}
}
else
{
g_fileTransCtrlBlock.file_storage_cs = STORAGE_IN_FILE;
}
return 0;
}
static uint8_t _file_trans_temp_buff[4096 + 16] = {0}; //必须要比4096多加8个字节或以上
static int check_file_crc32_and_init_crc16_area(void) //add by hes.hezhao -- 差分升级增加的代码
{
return FILE_NO_DIFF_CONTENT_TRANS;
}
static int check_file_crc16_area_and_redirect_offset(void) //add by hes.hezhao -- 差分升级增加的代码
{
return FILE_NO_DIFF_CONTENT_TRANS;
}
static int check_file_trans_succ_crc32(const char *path, uint8_t type, uint8_t cs) //add by hes.hezhao -- 差分升级增加的代码
{
// const char *path = g_fileTransCtrlBlock.fileName;
uint32_t base_addr = g_file_trans_head_struct.dest_addr;
if (g_file_trans_use_diff == false)
{
#if 0
void ate_user_function_test(uint8_t *factory_data, uint16_t len);
uint8_t ate_param[14] = {0xFD, 0x01, 0x0, 0x27, 0x01, 0x07, 0x10, 0x02, 0x05, 0x0, 0x0, 0x0, 0x0, 0x0};
uint32_t file_size = g_file_trans_head_struct.file_size + (HEAD_DESC_LEN + HEAD_STRUCT_LEN);
ate_param[9] = file_size >> 24;
ate_param[10] = file_size >> 16;
ate_param[11] = file_size >> 8;
ate_param[12] = file_size;
ate_user_function_test(ate_param, sizeof(ate_param));
#endif
return FILE_NO_DIFF_CONTENT_TRANS;
}
if (type == OTA_UI_TRANS_IMAGE) //图片资源
{
int32_t ret;
uint8_t *temp_buff = _file_trans_temp_buff;
uint16_t temp_len = 4096;
uint16_t head_size = HEAD_DESC_LEN + HEAD_STRUCT_LEN;
uint32_t temp_crc32 = 0;
uint32_t proc_size = 0;
uint32_t ctx_len = g_file_trans_head_struct.file_size; //(g_fileTransCtrlBlock.fileSize - head_size) == g_file_trans_head_struct.file_size
am_util_stdio_printf("start calc file crc32, base_addr = %x -- file size = %d \r\n", base_addr, ctx_len);
watchdog_api_feed();
while (proc_size < ctx_len)
{
if ((ctx_len - proc_size) < temp_len)
{
temp_len = ctx_len - proc_size;
}
if (cs == STORAGE_IN_FILE)
{
ret = fs_api_file_load(path, head_size + proc_size, temp_buff, temp_len);
}
else //STORAGE_IN_ADDR
{
// ret = g_file_trans_pFlash->flash_read_page(base_addr + head_size + proc_size, (uint32_t *)temp_buff, temp_len);
ret = flash_api_read(base_addr + head_size + proc_size, temp_buff, temp_len); //目前只针对保存片外的文件作差分升级处理
}
if (ret < 0)
{
// break;
}
temp_crc32 = crc32_ex(temp_crc32, temp_buff, temp_len);
proc_size += temp_len;
if (proc_size % (128 * 1024) == 0) //喂狗
{
watchdog_api_feed();
}
}
am_util_stdio_printf("base_addr: 0x%x -- temp_crc32: 0x%x \r\n", base_addr, temp_crc32);
if (temp_crc32 != g_file_trans_head_struct.file_crc32)
{
am_util_stdio_printf("calc crc32 != g_file_trans_head_struct.file_crc32\n");
return FILE_WRITE_PKG_FILE_ERR;
}
}
return 0;
}
void file_trans_set_diff_content_trans_flag(bool flag)
{
g_file_trans_use_diff = flag;
}
bool file_trans_get_diff_content_trans_flag(void)
{
return g_file_trans_use_diff;
}
uint8_t* file_trans_get_temp_buff_para(uint16_t* buff_size_p)
{
if (buff_size_p != NULL)
{
*buff_size_p = sizeof(_file_trans_temp_buff) - 4;
}
return _file_trans_temp_buff;
}
/**
*****************************************************************************************
* @brief Verify the currently written data
*****************************************************************************************
*/
static int file_trans_verify_flash_content(uint32_t flashAddr, uint8_t *source, uint32_t len, am_multiboot_flash_info_t *pFlash)
{
// read back and check
uint32_t remaining = len;
int ret = 0;
if(remaining < g_file_trans_pFlash->flashPageSize)
{
remaining = g_file_trans_pFlash->flashPageSize;
}
uint8_t* verify_buff = pvPortMalloc(remaining);
if(verify_buff == NULL)
{
return -1;
}
ret = pFlash->flash_read_page(flashAddr, (uint32_t *)verify_buff, remaining);
ret = memcmp(verify_buff, source, remaining);
if ( ret != 0 )
{
// there is write failure happened.
static_print_info("\r\n??????flash write verify failed. address = 0x%x. length = %d????? \r\n", flashAddr, len);
}
vPortFree(verify_buff);
return ret;
}
static int file_write_to_flash(uint8_t *buf, uint16_t len, uint32_t addr)
{
if(g_fileTransCtrlBlock.file_storage_cs == STORAGE_IN_FILE)
{
int ret = 0;
/* 若后续解决flash擦写失败问题不用每次都打开文件再写入 */
const char *path = g_fileTransCtrlBlock.fileName;
uint8_t num = 10;
do{
//ret = fs_api_file_oft_write(&g_lfs_file_trans, g_fileTransCtrlBlock.offset, buf, len);// 由于出现文件写失败后无法再操作文件暂不使用fs_api_file_oft_write接口
ret = fs_api_file_store(path, g_fileTransCtrlBlock.offset, buf, len);
if (ret != len)
{
vTaskDelay(100);
num --;
static_print_info("\r\n write failed occur!!!! ret = %d. num = %d \r\n", ret, num);
}
else
{
break;
}
}while(num > 0);
if(num == 0)
{
static_print_info("\r\n\r\n ?????? try 9 times file_trans_write_pkg -> write failed!!! ?????\r\n\r\n");
return FILE_WRITE_PKG_FILE_ERR;
}
}
else if(g_fileTransCtrlBlock.file_storage_cs == STORAGE_IN_ADDR)
{
uint16_t bytes_remaining = len;
uint32_t target_address = 0;
uint16_t bytes_to_write = 0;
uint8_t page_count = 0;
uint8_t indx = 0;
addr -= file_trans_flash_op.buffer_index;
if (addr & (g_file_trans_pFlash->flashPageSize - 1))
{
static_print_info(" application is trying to write to wrong address addr = %x\n",addr);
return FILE_ILLEGAL_ERR;
}
while (bytes_remaining)
{
bytes_to_write = g_file_trans_pFlash->flashPageSize - file_trans_flash_op.buffer_index;
if (bytes_to_write > bytes_remaining)
{
bytes_to_write = bytes_remaining;
}
// move data into buffer
memcpy(file_trans_flash_op.write_buffer, buf, bytes_to_write);
file_trans_flash_op.buffer_index += bytes_to_write;
bytes_remaining -= bytes_to_write;
buf += bytes_to_write;
//
// Write to flash when there is data more than 1 page size
// For last fragment write even if it is less than one page.
//
if((g_fileTransCtrlBlock.fileSize == (g_fileTransCtrlBlock.offset + len))
|| (file_trans_flash_op.buffer_index == g_file_trans_pFlash->flashPageSize))
{
target_address = addr + (page_count * g_file_trans_pFlash->flashPageSize);
for(indx = 0; indx < 3; indx++)
{
if(g_file_trans_pFlash->flash_erase_sector(target_address) == 0)
{
g_file_trans_pFlash->flash_write_page(target_address, (uint32_t *)file_trans_flash_op.write_buffer, g_file_trans_pFlash->flashPageSize);
if(file_trans_verify_flash_content(target_address, file_trans_flash_op.write_buffer, file_trans_flash_op.buffer_index, g_file_trans_pFlash) == 0)
{
static_print_info("Flash write succeeded to address 0x%x. length %d\n", target_address, file_trans_flash_op.buffer_index);
page_count++;
file_trans_flash_op.buffer_index = 0;
break;
}
else
{
static_print_remind("\r\n\r\n\n\nFlash verify failed to address: 0x%x. indx = %d\r \n", target_address, indx);
}
}
else
{
static_print_remind("\r\n\r\n\n\nFlash erase sector failed to address: 0x%x. indx = %d\r\n", target_address, indx);
}
vTaskDelay(100);
}
if(indx >= 3)
{
static_print_remind("try 3 times to write flash, but all failed\r\n");
return FILE_WRITE_PKG_FILE_ERR;
}
}
}
}
return 0;
}
static int file_write_to_storage(uint8_t *buf, uint16_t len, uint32_t addr)
{
if(g_fileTransCtrlBlock.file_storage_cs == STORAGE_IN_FILE)
{
int32_t ret = 0;
/* 若后续解决flash擦写失败问题不用每次都打开文件再写入 */
const char *path = g_fileTransCtrlBlock.fileName;
uint8_t num = 10;
do{
//ret = fs_api_file_oft_write(&g_lfs_file_trans, g_fileTransCtrlBlock.offset, buf, len);// 由于出现文件写失败后无法再操作文件暂不使用fs_api_file_oft_write接口
ret = fs_api_file_store(path, g_fileTransCtrlBlock.offset, buf, len);
if (ret != len)
{
vTaskDelay(100);
num --;
static_print_info("\r\n write failed occur!!!! ret = %d. num = %d \r\n\r\n", ret, num);
}
else
{
break;
}
}while(num > 0);
if(num == 0)
{
static_print_info("\r\n ?????? try 9 times file_trans_write_pkg -> write failed!!! ????? \r\n\r\n");
return FILE_WRITE_PKG_FILE_ERR;
}
}
else if(g_fileTransCtrlBlock.file_storage_cs == STORAGE_IN_ADDR)
{
int32_t ret = 0;
uint16_t bytes_remaining = len;
uint32_t target_address = 0;
uint16_t bytes_to_write = 0;
uint16_t write_count = 0;
uint8_t indx = 0;
if (addr & (g_file_trans_pFlash->flashPageSize - 1))
{
am_util_stdio_printf(" application is trying to write to wrong address addr = %x\r\n", addr);
return FILE_ILLEGAL_ERR;
}
while (bytes_remaining)
{
target_address = addr + write_count;
// if ((len > g_file_trans_pFlash->flashPageSize) && (target_address == 0) && (target_address % g_file_trans_pFlash->flashPageSize) != 0)
if ((target_address % g_file_trans_pFlash->flashPageSize) != 0)
{
// static_print_info("target_address = %x\r\n", target_address);
bytes_to_write = g_file_trans_pFlash->flashPageSize - (target_address % g_file_trans_pFlash->flashPageSize);
// static_print_info("bytes_to_write = %x\r\n", bytes_to_write);
}
else if (bytes_remaining >= g_file_trans_pFlash->flashPageSize)
{
bytes_to_write = g_file_trans_pFlash->flashPageSize;
}
else
{
bytes_to_write = bytes_remaining;
// static_print_info("bytes_to_write = %x\r\n", bytes_to_write);
#if 1 //后一半数据将被擦掉,先读取出来暂存
if (write_count > 0)
{
g_file_trans_pFlash->flash_read_page(target_address + bytes_to_write,
(uint32_t *)_file_trans_temp_buff, g_file_trans_pFlash->flashPageSize - bytes_to_write);
}
#endif
}
ret = 0;
// for(indx = 0; indx < 2; indx++)
for(indx = 0; indx < 1; indx++)
{
if ((target_address % g_file_trans_pFlash->flashPageSize) == 0)
{
ret = g_file_trans_pFlash->flash_erase_sector(target_address);
}
if (ret == 0)
{
ret = g_file_trans_pFlash->flash_write_page(target_address, (uint32_t *)buf, bytes_to_write);
if (ret >= 0)
{
#if 1 //擦除操作后,将暂存的数据写回去
if (bytes_to_write == bytes_remaining && write_count > 0)
{
g_file_trans_pFlash->flash_write_page(target_address + bytes_to_write,
(uint32_t *)_file_trans_temp_buff, g_file_trans_pFlash->flashPageSize - bytes_to_write);
}
#endif
break;
}
am_util_stdio_printf("\r\nFlash write failed to address: 0x%x. indx = %d\r\n\r\n", target_address, indx);
}
else
{
am_util_stdio_printf("\r\nFlash erase failed to address: 0x%x. indx = %d\r\n\r\n", target_address, indx);
}
vTaskDelay(100);
}
// if(indx >= 2)
if(indx >= 1)
{
am_util_stdio_printf("try 3 times to write flash, but all failed\r\n\r\n");
return FILE_WRITE_PKG_FILE_ERR;
}
write_count += bytes_to_write;
buf += bytes_to_write;
bytes_remaining -= bytes_to_write;
}
}
return 0;
}
/**
*****************************************************************************************
* @brief 写入一包数据,返回下一包请求大小
*****************************************************************************************
*/
int32_t file_trans_write_pkg(uint8_t *data, uint32_t length)
{
uint16_t psn = 0;
int ret = 0;
int32_t resume_file_size = 0;
//判断重复包。如果是重复包,参数不变重新请求
psn = (data[1] << 8) + data[2];
if (psn != g_fileTransCtrlBlock.psn)
{
static_print_info("rcv psn=%d,expected psn=%d\r\n", psn, g_fileTransCtrlBlock.psn);
return FILE_WRITE_PKG_NO_REQUEST;
}
g_fileTransCtrlBlock.nextPkgSize = FILE_TRANS_PKG_SIZE;
if (g_fileTransCtrlBlock.offset > g_fileTransCtrlBlock.fileSize)
{
static_print_info("file trans offset error,offset=%d,fileSize=%d\r\n", g_fileTransCtrlBlock.offset, g_fileTransCtrlBlock.fileSize);
return FILE_WRITE_PKG_NO_REQUEST;
}
if (g_fileTransCtrlBlock.state == FILE_TRANS_STATE_CANCEL)
{
static_print_info("file trans canceled,do not request anymore\r\n");
return FILE_WRITE_PKG_NO_REQUEST;
}
if (g_fileTransCtrlBlock.state == FILE_TRANS_STATE_IDLE)
{
static_print_info("file trans idle,invalid request\r\n");
return FILE_WRITE_PKG_NO_REQUEST;
}
if (g_fileTransCtrlBlock.state == FILE_TRANS_STATE_START)
{
ret = process_file_head(data + 3, length - 3);
if (ret != 0) {
static_print_info("process_file_head error ret=%d\r\n", ret);
return ret;
}
g_fileTransCtrlBlock.state = FILE_TRANS_STATE_TRANS;
/* 判断设备是否已有此固件 */
//根据相应固件名的crc以及版本号判断后续添加
#if 1 //add by hes.hezhao -- 差分升级增加的代码
ret = check_file_crc32_and_init_crc16_area();
if (ret == FILE_WRITE_PKG_COMPLETE)
{
g_fileTransCtrlBlock.nextPkgSize = 0;
g_fileTransCtrlBlock.state = FILE_TRANS_STATE_IDLE;
}
#endif
/* 判断是否需要续传 */
if (g_fileTransCtrlBlock.psn > 0 && g_fileTransCtrlBlock.nextPkgSize > 0)
{
resume_file_size = file_trans_handle_resume(data, length, g_fileTransCtrlBlock.fileName);
}
if (resume_file_size > 0 && resume_file_size == g_fileTransCtrlBlock.fileSize)
{
static_print_info("file already exists\r\n");
if (g_fileTransCtrlBlock.fsfd >= 0) {
fs_api_file_close(&g_lfs_file_trans);
static_print_info("trans file closed\r\n");
}
g_fileTransCtrlBlock.nextPkgSize = 0;
g_fileTransCtrlBlock.state = FILE_TRANS_STATE_IDLE;
}
else if (resume_file_size > 0 && resume_file_size < g_fileTransCtrlBlock.fileSize)
{
static_print_info("rusume file trans,size=%d,total=%d\r\n", resume_file_size, g_fileTransCtrlBlock.fileSize);
g_fileTransCtrlBlock.offset = resume_file_size;
g_fileTransCtrlBlock.lastOffset = g_fileTransCtrlBlock.offset;
// if (g_fileTransCtrlBlock.fileSize - g_fileTransCtrlBlock.offset < FILE_TRANS_PKG_SIZE) {
// g_fileTransCtrlBlock.nextPkgSize = g_fileTransCtrlBlock.fileSize - g_fileTransCtrlBlock.offset;
// }
g_fileTransCtrlBlock.fsfd = -2;
// g_fileTransCtrlBlock.psn++;
// g_fileTransCtrlBlock.state = FILE_TRANS_STATE_TRANS;
g_fileTransCtrlBlock.state = FILE_TRANS_STATE_WAIT;
}
else
{
static_print_info("Block.fileSize=%d,pack_len=%d\r\n", g_fileTransCtrlBlock.fileSize, g_file_trans_head_param.pack_len);
if (g_fileTransCtrlBlock.fileSize > 256 && g_fileTransCtrlBlock.fileSize != g_file_trans_head_param.pack_len)
{
static_print_info("new file\r\n");
#if 0
delete_all_complete_ota_files();
#else
if (g_fileTransCtrlBlock.file_storage_cs == STORAGE_IN_FILE) {
delete_all_complete_ota_files();
}
#endif
}
g_fileTransCtrlBlock.fileSize = g_file_trans_head_param.pack_len;
}
// 申请8kflash操作缓存
if (g_file_trans_use_diff == false)
{
#if (ENABLE_MALLOC_BUFF)
if (file_trans_flash_op.write_buffer == NULL)
{
file_trans_flash_op.write_buffer = pvPortMalloc(MCU_ROM_PAGE_SIZE);
}
#else
file_trans_flash_op.write_buffer = otaFlashOp.writeBuffer; //8KB
#endif
file_trans_flash_op.buffer_index = 0;
if (file_trans_flash_op.write_buffer == NULL)
{
return FILE_TEMP_MEM_APY_ERR;
}
}
else
{
if(file_trans_flash_op.write_buffer != NULL)
{
#if (ENABLE_MALLOC_BUFF)
vPortFree(file_trans_flash_op.write_buffer);
#endif
file_trans_flash_op.write_buffer = NULL;
}
}
}
if (g_fileTransCtrlBlock.state == FILE_TRANS_STATE_TRANS)
{
if (g_file_trans_use_diff == false)
{
ret = file_write_to_flash(data + 3, length - 3, g_file_trans_head_struct.dest_addr + g_fileTransCtrlBlock.offset);
}
else
{
ret = file_write_to_storage(data + 3, length - 3, g_file_trans_head_struct.dest_addr + g_fileTransCtrlBlock.offset);
}
if (ret != 0)
{
static_print_info("file_write_to_flash error ret=%d\r\n", ret);
return ret;
}
g_fileTransCtrlBlock.offset += (length - 3);
g_fileTransCtrlBlock.lastOffset = g_fileTransCtrlBlock.offset;
if(g_fileTransCtrlBlock.fileSize - g_fileTransCtrlBlock.offset < FILE_TRANS_PKG_SIZE)
{
g_fileTransCtrlBlock.nextPkgSize = g_fileTransCtrlBlock.fileSize - g_fileTransCtrlBlock.offset;
}
#if 1 //add by hes.hezhao -- 差分升级增加的代码
ret = check_file_crc16_area_and_redirect_offset();
if (ret == FILE_WRITE_PKG_COMPLETE)
{
g_fileTransCtrlBlock.nextPkgSize = 0;
g_fileTransCtrlBlock.state = FILE_TRANS_STATE_IDLE;
}
#endif
}
file_trans_refresh_percent();
if (g_fileTransOverTimer != NULL)
{
xTimerReset(g_fileTransOverTimer, 0);
}
if (g_fileTransCtrlBlock.nextPkgSize == 0)
{ //文件已传输完毕
static_print_info("trans file finish\r\n");
if (g_fileTransCtrlBlock.fsfd >= 0)
{
fs_api_file_close(&g_lfs_file_trans);
g_fileTransCtrlBlock.fsfd = -1;
static_print_info("trans file closed\r\n");
}
}
else
{
g_fileTransCtrlBlock.psn++;
g_fileTransCtrlBlock.state = FILE_TRANS_STATE_TRANS;
}
return g_fileTransCtrlBlock.nextPkgSize;
}
FileTransCtrlBlock_t* file_trans_get_ctrl_block_ptr(void)
{
return &g_fileTransCtrlBlock;
}
void file_trans_get_ctrl_block(FileTransCtrlBlock_t *ctrl)
{
memcpy(ctrl, &g_fileTransCtrlBlock, sizeof(FileTransCtrlBlock_t));
}
/**
*****************************************************************************************
* @brief 该文件传输成功
* @note 若后续支持拆分升级,须在此函数中保存此文件的偏移,以及多文件的总大小等参数
*****************************************************************************************
*/
void file_trans_succ(void)
{
char path_filename[128] = {0};
// uint16_t file_action = 0;
uint8_t storage_cs = 0;
uint8_t file_type = 0;
if (g_fileTransOverTimer != NULL)
{
xTimerStop(g_fileTransOverTimer, 0);
}
g_file_trans_busy = false;
static_print_info("FileTransSucc\n");
strncpy(path_filename, g_fileTransCtrlBlock.fileName, sizeof(path_filename) - 2);
// file_action = g_fileTransCtrlBlock.fileAction;
storage_cs = g_fileTransCtrlBlock.file_storage_cs;
file_type = g_fileTransCtrlBlock.type;
memset(&g_fileTransCtrlBlock, 0, sizeof(FileTransCtrlBlock_t));
SaveFileTransFile(); //清空并保存文件传输参数
if(file_trans_flash_op.write_buffer != NULL)
{
#if (ENABLE_MALLOC_BUFF)
vPortFree(file_trans_flash_op.write_buffer);
#endif
file_trans_flash_op.write_buffer = NULL;
}
if(storage_cs == STORAGE_IN_FILE)
{
//task_ui_notify(EVENT_OTA_INSTALLING, 0, NULL, 0);
user_pack_analysis_dir(DIR_PACK);
}
else if(g_file_trans_head_param.mark == MARK_MCU_FW_BIN)
{
user_update_ota_info(&g_file_trans_head_struct, &g_file_trans_head_param);
// task_ble_notify(BLE_RESET_AFTER_OTA_MSG, 0);
}
else if(g_file_trans_head_param.mark == MARK_PASS_THROUGH_BIN)
{
if(g_file_trans_head_struct.sour_cs == FILE_SOUR_CS_ROM)
{
copy_file_to_rom();
}
}
amota_conn_param_update(CONN_PARAM_LOW_POWER);
#if 0 //add by hes.hezhao -- 差分升级增加的代码
int32_t ret = check_file_trans_succ_crc32(path_filename, file_type, storage_cs);
if(ret < 0)
{
static_print_info("EVENT_OTA_FAIL\n");
}
/* if (file_action != FILE_ACTION_WATCHFACE)
{
task_ui_notify(EVENT_OTA_TRANSMITTING, 100, NULL, 0);
} */
#endif
task_ui_notify(EVENT_OTA_SUCCESS, 0, NULL, 0);
task_ble_notify(BLE_RESET_AFTER_OTA_MSG, 0);
}
/**
*****************************************************************************************
* @brief 文件传输异常处理
*****************************************************************************************
*/
void file_trans_break(bool save_flag)
{
static_print_info("file trans break\r\n");
task_ui_notify(EVENT_OTA_FAIL, 0, NULL, 0);
if(file_trans_flash_op.write_buffer != NULL)
{
#if (ENABLE_MALLOC_BUFF)
vPortFree(file_trans_flash_op.write_buffer);
#endif
file_trans_flash_op.write_buffer = NULL;
}
if (g_fileTransCtrlBlock.fsfd >= 0)
{
fs_api_file_close(&g_lfs_file_trans);
g_fileTransCtrlBlock.fsfd = -2;
static_print_info("trans file closed\r\n");
}
//处理此情形文件接收完毕但未接收到APP发送的4.11.9指令导致原因可能为蓝牙断连或者APP程序奔溃等
if ((g_fileTransCtrlBlock.nextPkgSize == 0 && g_fileTransCtrlBlock.lastOffset == g_fileTransCtrlBlock.fileSize) || !save_flag) {
// char path[128] = {0};
// snprintf(path, 128, "%s/%s", DIR_RECEIVED, g_fileTransCtrlBlock.fileName);
const char *path = g_fileTransCtrlBlock.fileName;
fs_api_unlink(path);
memset(&g_fileTransCtrlBlock, 0, sizeof(FileTransCtrlBlock_t));
}
//memset(&g_fileTransCtrlBlock, 0, sizeof(FileTransCtrlBlock_t));
if(g_fileTransCtrlBlock.state == FILE_TRANS_STATE_TRANS && g_fileTransCtrlBlock.file_storage_cs == STORAGE_IN_ADDR)
{
g_fileTransCtrlBlock.file_resume_crc32 = g_file_trans_head_struct.file_crc32;
g_fileTransCtrlBlock.file_resume_offset = g_fileTransCtrlBlock.offset;
g_fileTransCtrlBlock.file_resume_size = g_file_trans_head_param.pack_len;
static_print_info("crc32 = %x, offset = %x, fileSize = %x\r\n",g_file_trans_head_struct.file_crc32,
g_fileTransCtrlBlock.offset, g_file_trans_head_struct.file_size);
}
g_fileTransCtrlBlock.state = FILE_TRANS_STATE_IDLE;
SaveFileTransFile();
g_file_trans_busy = false;
task_ui_notify(EVENT_OTA_FAIL, 0, NULL, 0);
if(g_fileTransCtrlBlock.file_storage_cs == STORAGE_IN_FILE)
{// 目前由于flash bug原因文件传输失败后不能再操作文件古重启解决
task_ble_notify(BLE_RESET_AFTER_OTA_MSG, 0);
}
}
void FileTransDisconn(void)
{
if (g_fileTransOverTimer != NULL)
{
if(!xTimerIsTimerActive(g_fileTransOverTimer))
{
return;
}
xTimerStop(g_fileTransOverTimer, 0);
}
task_ble_notify(BLE_SAVE_TRANS_FILE_INFO, 0);
}
//文件传输结束,关闭超时定时器
void FileTransOver(void)
{
static_print_info("FileTransOver\n");
}
static void FileTransOverTimerCallback(TimerHandle_t xTimer)
{
static_print_info("FileTransOverTimerCallback\n");
amota_conn_param_update(CONN_PARAM_LOW_POWER);
task_ble_notify(BLE_SAVE_TRANS_FILE_INFO, 0);
}
static int delete_all_complete_files_cb(bool flag, const char *path, int32_t size, void* arg)
{
bool isDir = false;
int fileSize = 0;
fileSize = fs_api_stat(path, &isDir);
if (fileSize <= 0) {
return 0;
}
if (isDir) {
return 0;
}
static_print_info("unlink %s \r\n", path);
fs_api_unlink(path);
return 0;
}
/**
*****************************************************************************************
* @brief 删除全部在 DIR_RECEIVED 目录下的文件
* @note 在APP下发一个新文件时调用目的是删除未处理或未升级成功的文件
*****************************************************************************************
*/
void delete_all_complete_ota_files(void)
{
fs_api_dir_scan(DIR_RECEIVED, delete_all_complete_files_cb, NULL);
}
//用于续传,蓝牙中断等导致的升级失败时,调用该函数进行续传记录
static void SaveFileTransFile(void)
{
fs_api_file_store(FILE_FILE_TRANS_INFO, 0, &g_fileTransCtrlBlock, sizeof(FileTransCtrlBlock_t));
}
static void LoadFileTransFile(void)
{
char path[64];
fs_user_get_dir_by_full_path(FILE_FILE_TRANS_INFO, path); // 创建文件断点 续传目录,后续可能不在此处创建
if(fs_api_file_exist(path) == false) {
fs_user_create_dir(path);
}
if(fs_api_file_exist(DIR_RECEIVED) == false) { // 创建文件接收的目录,后续可能不在此处创建
fs_user_create_dir(DIR_RECEIVED);
}
if(fs_api_file_exist(FILE_FILE_TRANS_INFO) == sizeof(FileTransCtrlBlock_t)) {// FSFileExist ---> get file size
fs_api_file_load(FILE_FILE_TRANS_INFO, 0, &g_fileTransCtrlBlock, sizeof(FileTransCtrlBlock_t));
}
}
static OtaUiMsgType GetTransTypeByAction(FileActionType action)
{
OtaUiMsgType msgType;
switch(action) {
case FILE_ACTION_WATCHFACE: {
msgType = OTA_UI_TRANS_WATCHFACE;
}
break;
case FILE_ACTION_IMAGE: {
msgType = OTA_UI_TRANS_IMAGE;
}
break;
case FILE_ACTION_FONT: {
msgType = OTA_UI_TRANS_FONT;
}
break;
case FILE_ACTION_MCU_FIRMWARE:
case FILE_ACTION_NXP_FIRMWARE:
case FILE_ACTION_GPS_FIRMWARE:
case FILE_ACTION_NFC_FIRMWARE:
case FILE_ACTION_TP_FIRMWARE:
case FILE_ACTION_BTL_FIRMWARE: {
msgType = OTA_UI_TRANS_FIRMWARE;
}
break;
default: {
msgType = OTA_UI_UNKNOWN_TYPE;
}
break;
}
return msgType;
}
/**
*****************************************************************************************
* @brief 将收到的文件(写入固定地址的透传包),复制到指定片内空间
*****************************************************************************************
*/
int32_t copy_file_to_rom(void)
{
uint8_t *buffer = pvPortMalloc(AM_HAL_FLASH_PAGE_SIZE);
uint32_t ui32_crc;
uint8_t rw_times = 3;
uint32_t r_addr = 0;
uint32_t w_addr = 0;
uint32_t rw_len = 0;
uint32_t op_len = 0;
if(g_file_trans_head_struct.sour_addr >= APP_CODE_BASE_ADDR
&& g_file_trans_head_struct.sour_addr < APP_CODE_END_ADDR)
{/* 试图写入代码区 */
return -1;
}
if(g_file_trans_head_struct.sour_addr % MCU_ROM_PAGE_SIZE)
{/* 地址未页对齐 */
return -1;
}
while(rw_times--)
{
r_addr = g_file_trans_head_struct.dest_addr + g_file_trans_head_param.h_len;
w_addr = g_file_trans_head_struct.sour_addr;
ui32_crc = 0;
for(rw_len = g_file_trans_head_param.h_len; rw_len < g_file_trans_head_struct.file_size + g_file_trans_head_param.h_len; )
{
op_len = (g_file_trans_head_struct.file_size > MCU_ROM_PAGE_SIZE)? MCU_ROM_PAGE_SIZE:(g_file_trans_head_struct.file_size);
static_print_info("r_addr=0x%x,w_addr=0x%x,op_len=0x%x\n",r_addr,w_addr,op_len);
flash_api_read(r_addr, buffer, op_len);
rom_api_erase(w_addr);
rom_api_write(w_addr, buffer, op_len);
r_addr += op_len;
w_addr += op_len;
rw_len += op_len;
}
ui32_crc = crc32_ex(ui32_crc, (uint8_t *)g_file_trans_head_struct.sour_addr, g_file_trans_head_struct.file_size);
if(ui32_crc == g_file_trans_head_struct.file_crc32)
{
break;
}
static_print_info("error:obj_crc=0x%x,temp_crc=0x%x\n", g_file_trans_head_struct.file_crc32,ui32_crc);
}
vPortFree(buffer);
static_print_info("copy_file_to_rom succ\r\n");
return 0;
}
///////////以下为文件上传部分的代码///////////
typedef struct {
FileTransUploadFileInfo_t *pFileList;
uint32_t index;
uint32_t fileNum;
} FileListInfo_t;
static int GetFileNumCb(bool isDir, const char *path, int32_t size, void* arg)
{
if (fs_api_stat(path, &isDir) <= 0) {
return 0;
}
if (isDir) {
return 0;
}
FileListInfo_t *fileListInfo = arg;
fileListInfo->fileNum++;
return 0;
}
static int GetFileInfoCb(bool isDir, const char *path, int32_t size, void* arg)
{
FileListInfo_t *fileListInfo = arg;
if (fileListInfo->index >= fileListInfo->fileNum) {
return 0;
}
if (fs_api_stat(path, &isDir) <= 0) {
return 0;
}
if (isDir) {
return 0;
}
if(fileListInfo->index >= fileListInfo->fileNum) {
return 0;
}
strcpy(fileListInfo->pFileList[fileListInfo->index].filePathName, path);
fileListInfo->pFileList[fileListInfo->index].fileSize = size;
if(size > 0) {
fileListInfo->pFileList[fileListInfo->index].fileFrames = (size - 1) / FILE_UPLOAD_FRAME_SIZE + 1;
} else {
fileListInfo->pFileList[fileListInfo->index].fileFrames = 0;
}
fileListInfo->pFileList[fileListInfo->index].fileId = fileListInfo->index;
ble_printf("file path = %s\n", fileListInfo->pFileList[fileListInfo->index].filePathName);
ble_printf("file fileId = %d\n", fileListInfo->pFileList[fileListInfo->index].fileId);
ble_printf("file fileFrames = %d\n", fileListInfo->pFileList[fileListInfo->index].fileFrames);
fileListInfo->index++;
return 0;
}
extern int log_api_save_at_power_off(void);
uint8_t GetFileListByAction(FileActionType action, FileTransUploadFileInfo_t **pFileList)
{
uint32_t ret;
FileListInfo_t fileListInfo = {0};
char *FilePath = NULL;
memset(&fileListInfo, 0, sizeof(FileListInfo_t));
switch(action) {
case FILE_ACTION_LOG:
case FILE_ACTION_DATA_EXERCISE:
case FILE_ACTION_DATA_SLEEP:
case FILE_ACTION_DATA_MIN:
case FILE_ACTION_DATA_DAY:
case FILE_ALL: {
if(action == FILE_ACTION_LOG) {
FilePath = (char *)DIR_DATA_LOG;
//flush log buf to file
log_api_save_at_power_off();
}
else if(action == FILE_ACTION_DATA_EXERCISE) {
FilePath = (char *)DIR_DATA_EXERCISE;
}
else if(action == FILE_ACTION_DATA_SLEEP) {
FilePath = (char *)DIR_DATA_SLEEP;
}
else if(action == FILE_ACTION_DATA_MIN) {
FilePath = (char *)DIR_DATA_MIN;
} else if(action == FILE_ACTION_DATA_DAY) {
FilePath = (char *)DIR_DATA_DAY;
}
else if(action == FILE_ALL) {
FilePath = (char *)DIR_FD;
}
ble_printf("get file list\r\n");
fs_api_dir_scan(FilePath, GetFileNumCb, &fileListInfo);
if(fileListInfo.fileNum > 255) {
fileListInfo.fileNum = 255;
}
ble_printf("get fileNum=%d\r\n", fileListInfo.fileNum);
*pFileList = pvPortMalloc(sizeof(FileTransUploadFileInfo_t) * fileListInfo.fileNum);
fileListInfo.pFileList = *pFileList;
ret = fileListInfo.fileNum;
fs_api_dir_scan(FilePath, GetFileInfoCb, &fileListInfo);
}
break;
default: {
ret = 0;
}
break;
}
return ret;
}
/* 将'\'转换为'/',将空格' '转换为'_' */
void switch_to_unix_path(char *path)
{
if (path == NULL) {
return;
}
for (int i = 0; path[i] != 0; i++) {
if (path[i] == '\\') {
path[i] = '/';
} else if (path[i] == ' ') {
path[i] = '_';
}
}
}