| | |
| | | // |
| | | |
| | | #include <cstring> |
| | | #include <pthread.h> |
| | | #include <thread> |
| | | #include <mutex> |
| | | #include <cstdlib> |
| | | #include "mcu_if.h" |
| | | #include "ahp.h" |
| | | #include "../common/apptimer.h" |
| | | #include "../utils/crc16.h" |
| | | #include "../defs.h" |
| | |
| | | #include "../rtk_module/parse_gps.h" |
| | | #include "../native-lib.h" |
| | | #include "../rtk_module/virtual_rtk.h" |
| | | #include "../common/string_util.h" |
| | | |
| | | #define DEBUG(fmt, args...) LOGD("<mcu_if> <%s>: " fmt, __func__, ##args) |
| | | |
| | | #define MCU_UART UART_1 |
| | | |
| | | enum parse_status_t { |
| | | SYNC_HEAD_ONE, |
| | | SYNC_HEAD_TWO, |
| | | GET_ID_HI, |
| | | GET_ID_LO, |
| | | GET_LENGTH_HI, |
| | | GET_LENGTH_LO, |
| | | GET_PAYLOAD, |
| | | GET_CRC16_HI, |
| | | GET_CRC16_LO |
| | | }; |
| | | |
| | | #define ID_CM_APP_BOOT 0x0001 |
| | | #define ID_MC_MCU_BOOT 0x8001 |
| | | #define ID_CM_DFU_UPLOAD 0x0002 |
| | | #define ID_MC_DFU_RSP 0x8002 |
| | | #define ID_CM_RW_INFO 0x0003 |
| | | #define ID_MC_RW_INFO_RSP 0x8003 |
| | | #define ID_CM_MCU_DFU_REQ 0x0004 |
| | | #define ID_MC_MCU_DFU_RSP 0x8004 |
| | | #define ID_CM_MCU_DFU_DATA 0x0005 |
| | | #define ID_CM_MCU_DFU_DATA_CMP 0x0006 |
| | | #define ID_MC_CAR_INFO2 0x8006 |
| | | #define ID_MC_CAR_INFO 0x8007 |
| | | #define ID_CM_AHP_INQ 0x0001 |
| | | #define ID_MC_AHP_RESP 0x8001 |
| | | #define ID_CM_AHP_DFU_DATA_UPLOAD 0x0002 |
| | | #define ID_MC_AHP_DFU_RESP 0x8002 |
| | | #define ID_MC_RTK_DATA 0x8008 |
| | | #define ID_CM_RTK_DATA 0x0008 |
| | | #define ID_CM_READ_RFCARD 0x0009 |
| | | #define ID_MC_RFCARD_RSP 0x8009 |
| | | #define ID_CM_SHUTDOWN 0x0020 |
| | | #define ID_MC_GNSS_DATA 0x800A |
| | | #define ID_CM_SINAN_CMD_DATA 0x000B |
| | | |
| | | static parse_status_t parse_status; |
| | | |
| | | static struct { |
| | | uint16_t id; |
| | | uint16_t length; |
| | | uint16_t rx_len; |
| | | uint8_t buffer[4096 + 4]; |
| | | uint16_t crc16; |
| | | }McuPkt; |
| | | |
| | | static uint8_t *dfuFile = NULL; |
| | | static uint8_t dfuFileBitmap[128]; |
| | | |
| | | static int dfuFileLength = 0; |
| | | static int dfuTryCnt = 0; |
| | | const int DFU_MAX_TRY = 3; |
| | | const int DFU_FILE_BLOCK_SIZE = 896; |
| | | |
| | | static int (*WriteMcu)(int id, const void *buf, int len); |
| | | |
| | | static int WriteBluetooth(int id, const void *buf, int len); |
| | | |
| | | static void *UartThread1(void *p); |
| | | static void ParseMcuTimeout(union sigval sig); |
| | | static void McuCommandEntry(uint16_t id, const uint8_t *data, int length); |
| | | static void ReadCardTimeout(apptimer_var_t val); |
| | | |
| | | static void SendDfuFile(int fileLen, int sentLen, int blockLen, const uint8_t *data); |
| | | static void GoNextDfuLater(union sigval sig); |
| | | static void GoNextDfu(void); |
| | | static void ReadCardTimeout(union sigval sig); |
| | | |
| | | void McuCommModeSel(int mode) |
| | | { |
| | | if (mode == 0) { |
| | | WriteMcu = WriteSerialPort; |
| | | } else { |
| | | WriteMcu = WriteBluetooth; |
| | | } |
| | | ParseUart::ParseUart(funptr fun) { |
| | | McuPkt.buffer = new uint8_t [4096 + 4]; |
| | | this->fun = fun; |
| | | parse_status = SYNC_HEAD_ONE; |
| | | } |
| | | |
| | | void ParseMcuInit(void) |
| | | { |
| | | dfuFile = NULL; |
| | | dfuFileLength = 0; |
| | | |
| | | parse_status = SYNC_HEAD_ONE; |
| | | AppTimer_delete(ParseMcuTimeout); |
| | | |
| | | // SendMcuCommand(ID_CM_APP_BOOT, NULL, 0); |
| | | ParseUart::~ParseUart() { |
| | | delete []McuPkt.buffer; |
| | | } |
| | | |
| | | static int WriteBluetooth(int id, const void *buf, int len) |
| | |
| | | return len; |
| | | } |
| | | |
| | | |
| | | #define PARSE_BUFF_SIZE 4096 |
| | | |
| | | static void *UartThread1(void *p) { |
| | | struct serial_config *cfg = (struct serial_config *) p; |
| | | |
| | | int res = InitSerialPort(MCU_UART, cfg->baud, cfg->data_bit, cfg->verify_bit, cfg->stop_bit, cfg->flow_ctrl); |
| | | DEBUG("Serial %s open %d", cfg->name, res); |
| | | uint8_t RxBuf[PARSE_BUFF_SIZE]; |
| | | int RxBufLen = 0; |
| | | |
| | | if (res == 0) |
| | | ParseMcuInit(); |
| | | |
| | | while (res == 0) { |
| | | int ul = ReadSerialPort(MCU_UART, (uint8_t *)RxBuf + RxBufLen, sizeof(RxBuf) - RxBufLen); |
| | | RxBufLen += ul; |
| | | |
| | | /*{ |
| | | static char buffd[16384]; |
| | | |
| | | buffd[0] = 0; |
| | | int i = 0; |
| | | for (i = 0; i < ul; i++) { |
| | | if ((i % 32) == 0) { |
| | | sprintf(buffd + strlen(buffd), "\n"); |
| | | } |
| | | sprintf(buffd + strlen(buffd), "%02X ", RxBuf[i]); |
| | | if (strlen(buffd) > 800) { |
| | | DEBUG("%s <- %s...", "UART", buffd); |
| | | buffd[0] = 0; |
| | | } |
| | | } |
| | | if (strlen(buffd) > 0) |
| | | DEBUG("%s <- %s", "UART", buffd); |
| | | }*/ |
| | | |
| | | if (RxBufLen > 0) { |
| | | // DEBUG("RECV LEN %d", RxBufLen); |
| | | if (Virtual2IsConnected()) { |
| | | |
| | | } else { |
| | | ParseMcu(RxBuf, RxBufLen); |
| | | } |
| | | RxBufLen = 0; |
| | | } |
| | | } |
| | | if (res == 0) { |
| | | UninitSerialPort(MCU_UART); |
| | | } |
| | | pthread_exit(NULL); |
| | | } |
| | | |
| | | void ParseMcu(const uint8_t *data, int length) |
| | | void ParseUart::ParseMcu(const uint8_t *data, int length) |
| | | { |
| | | int x = 0; |
| | | uint32_t now = AppTimer_GetTickCount(); |
| | | |
| | | if (now < sync_time) { |
| | | sync_time = now; |
| | | } |
| | | |
| | | if (now - sync_time >= D_SEC(5)) { |
| | | parse_status = SYNC_HEAD_ONE; |
| | | } |
| | | |
| | | while (x < length) { |
| | | uint8_t c = data[x]; |
| | | switch (parse_status) { |
| | | case SYNC_HEAD_ONE: |
| | | if (c == 0x55) { |
| | | parse_status = SYNC_HEAD_TWO; |
| | | AppTimer_delete(ParseMcuTimeout); |
| | | AppTimer_add(ParseMcuTimeout, D_SEC(5)); |
| | | sync_time = now; |
| | | } |
| | | x++; |
| | | break; |
| | |
| | | if (McuPkt.length >= 1500) { |
| | | DEBUG("Pkt Too large!"); |
| | | parse_status = SYNC_HEAD_ONE; |
| | | AppTimer_delete(ParseMcuTimeout); |
| | | } |
| | | |
| | | McuPkt.buffer[0] = HI_UINT16(McuPkt.id); |
| | |
| | | |
| | | uint16_t crc16 = CRCCCITT(McuPkt.buffer, McuPkt.length + 4, 0, 0); |
| | | |
| | | // DEBUG("mcuif crc16 but 0x%04X exp 0x%04X", McuPkt.crc16, crc16); |
| | | // DEBUG("mcuif 0x%02X: crc16 but 0x%04X exp 0x%04X", McuPkt.id, McuPkt.crc16, crc16); |
| | | |
| | | if (McuPkt.crc16 == crc16) { |
| | | McuCommandEntry(McuPkt.id, McuPkt.buffer + 4, McuPkt.length); |
| | | if (McuPkt.crc16 == crc16 && fun != nullptr) { |
| | | fun(McuPkt.id, McuPkt.buffer + 4, McuPkt.length); |
| | | //McuCommandEntry(McuPkt.id, McuPkt.buffer + 4, McuPkt.length); |
| | | } |
| | | |
| | | parse_status = SYNC_HEAD_ONE; |
| | | AppTimer_delete(ParseMcuTimeout); |
| | | x++; |
| | | break; |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | void SendMcuCommand(uint16_t id, const uint8_t *data, int length) |
| | | void SendMcuCommand(SerialPort *pClass, pSerialPortClassFun fun, uint16_t id, const uint8_t *data, int length) |
| | | { |
| | | if (pClass == nullptr) |
| | | return; |
| | | |
| | | uint8_t buffer[2048]; |
| | | int x = 0; |
| | | |
| | |
| | | buffer[x++] = HI_UINT16(crc16); |
| | | buffer[x++] = LO_UINT16(crc16); |
| | | |
| | | WriteMcu(MCU_UART, buffer, x); |
| | | } |
| | | |
| | | void ConfigMCU(bool ayDevice) |
| | | { |
| | | if (ayDevice) { |
| | | McuCommModeSel(0); |
| | | |
| | | // TODO |
| | | static struct serial_config serialConfig; |
| | | |
| | | strcpy(serialConfig.name, "/dev/ttyHSL1"); |
| | | serialConfig.baud = 115200; |
| | | serialConfig.data_bit = 8; |
| | | serialConfig.verify_bit = 'N'; |
| | | serialConfig.stop_bit = 1; |
| | | serialConfig.flow_ctrl = 0; |
| | | |
| | | pthread_t pid; |
| | | pthread_attr_t attr; |
| | | pthread_attr_init(&attr); |
| | | pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//detached |
| | | pthread_create(&pid, &attr, UartThread1, &serialConfig); |
| | | } else { |
| | | McuCommModeSel(1); |
| | | } |
| | | (pClass->*fun)(buffer, x); |
| | | } |
| | | |
| | | void SendRtkToMcu(const uint8_t *data, int length) |
| | | { |
| | | SendMcuCommand(ID_CM_RTK_DATA, data, length); |
| | | //SendMcuCommand(ID_CM_RTK_DATA, data, length); |
| | | } |
| | | |
| | | static void ParseMcuTimeout(union sigval sig) { |
| | | AppTimer_delete(ParseMcuTimeout); |
| | | parse_status = SYNC_HEAD_ONE; |
| | | } |
| | | |
| | | static void sendrtk(union sigval sig) { |
| | | static void sendrtk(apptimer_var_t val) { |
| | | uint8_t data[486]; |
| | | |
| | | memset(data, 0x86, sizeof(data)); |
| | | |
| | | SendMcuCommand(ID_CM_RTK_DATA, data, sizeof(data)); |
| | | //SendMcuCommand(ID_CM_RTK_DATA, data, sizeof(data)); |
| | | |
| | | SendMcuCommand(ID_CM_READ_RFCARD, NULL, 0); |
| | | //SendMcuCommand(ID_CM_READ_RFCARD, NULL, 0); |
| | | |
| | | AppTimer_delete(sendrtk); |
| | | AppTimer_add(sendrtk, D_SEC(1)); |
| | | } |
| | | |
| | | static uint8_t GnssBuf[PARSE_BUFF_SIZE]; |
| | | static int GnssBufLen = 0; |
| | | |
| | | static void McuCommandEntry(uint16_t id, const uint8_t *data, int length) |
| | | { |
| | | static int ii = 0; |
| | | |
| | | switch (id) { |
| | | case ID_MC_MCU_BOOT: |
| | | DEBUG("MCU BOOT"); |
| | | if (length == 8) { |
| | | PlatformStatusChanged(CAN_UPDATE_EVT, data, length); |
| | | } |
| | | |
| | | break; |
| | | case ID_MC_DFU_RSP: |
| | | DEBUG("ID_MC_DFU_RSP %d len %d", data[0], length); |
| | | |
| | | if (data[0] == 0) { |
| | | // 第一包传输成功 |
| | | dfuFileBitmap[0] |= 0x01; |
| | | } else if (data[0] == 10) { |
| | | // 升级完成 |
| | | memset(dfuFileBitmap, 0xFF, sizeof(dfuFileBitmap)); |
| | | } else if (data[0] == 11) { |
| | | // 放弃传输 |
| | | UploadDfuFileEnd(); |
| | | } else if (data[0] == 12) { |
| | | // 全部重传 |
| | | memset(dfuFileBitmap, 0, sizeof(dfuFileBitmap)); |
| | | } else if (data[0] == 13) { |
| | | // 部分重传,有后续字段 |
| | | DEBUG("BITMAP %02X %02X %02X %02X %02X", data[1], data[2], data[3], data[4], data[5]); |
| | | |
| | | int total = dfuFileLength / DFU_FILE_BLOCK_SIZE + ((dfuFileLength % DFU_FILE_BLOCK_SIZE)?1:0); |
| | | int a = 0, b = 0; |
| | | for (int i = 1; i < length; ++i) { |
| | | for (int j = 0; j < 8; ++j) { |
| | | if ((data[i] & BV(j))) b++; |
| | | a++; |
| | | if (a == total) { |
| | | i = length; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | DEBUG("BITMAP total %d succ %d", total, b); |
| | | |
| | | //memset(dfuFileBitmap, 0, sizeof(dfuFileBitmap)); |
| | | memcpy(dfuFileBitmap, data + 1, length - 1); |
| | | |
| | | if (total % 8) { |
| | | dfuFileBitmap[total/8] &= ~BV((total%8) - 1); |
| | | } else { |
| | | dfuFileBitmap[(total+7)/8 - 1] &= ~BV(7); |
| | | } |
| | | // dfuFileBitmap[total/8] &= ~BV(total%8); |
| | | } |
| | | |
| | | dfuTryCnt = 0; |
| | | AppTimer_delete(GoNextDfuLater); |
| | | GoNextDfu(); |
| | | break; |
| | | case ID_MC_RW_INFO_RSP: |
| | | break; |
| | | case ID_MC_MCU_DFU_RSP: |
| | | break; |
| | | case ID_MC_CAR_INFO2: |
| | | /* DEBUG("ID_MC_CAR_INFO2 %d", length); |
| | | |
| | | { |
| | | static char buffd[16384]; |
| | | |
| | | buffd[0] = 0; |
| | | int i = 0; |
| | | for (i = 0; i < length; i++) { |
| | | if ((i % 32) == 0) { |
| | | sprintf(buffd + strlen(buffd), "\n"); |
| | | } |
| | | sprintf(buffd + strlen(buffd), "%02X ", data[i]); |
| | | if (strlen(buffd) > 800) { |
| | | DEBUG("%s <- %s...", "车辆信号" , buffd); |
| | | buffd[0] = 0; |
| | | } |
| | | } |
| | | if (strlen(buffd) > 0) |
| | | DEBUG("%s <- %s", "车辆信号" , buffd); |
| | | }*/ |
| | | |
| | | if (length > 0) { |
| | | PlatformStatusChanged(CAR_SENSOR_UPDATE_EVT, data, length); |
| | | } |
| | | break; |
| | | case ID_MC_CAR_INFO: { |
| | | // DEBUG("ID_MC_CAR_INFO %d", length); |
| | | |
| | | if (length > 0) |
| | | PlatformStatusChanged(MCU_UPDATE_EVT, data, length); |
| | | break; |
| | | } |
| | | case ID_MC_RTK_DATA: |
| | | DEBUG("ID_MC_RTK_DATA"); |
| | | break; |
| | | case ID_MC_RFCARD_RSP: |
| | | DEBUG("ID_MC_RFCARD_RSP"); |
| | | AppTimer_delete(ReadCardTimeout); |
| | | |
| | | if (length > 0) |
| | | PlatformStatusChanged(CARD_UPDATE_EVT, data, length); |
| | | break; |
| | | case ID_MC_GNSS_DATA: { |
| | | length = (length > PARSE_BUFF_SIZE - GnssBufLen) ? (PARSE_BUFF_SIZE - GnssBufLen) : length; |
| | | |
| | | memcpy(GnssBuf + GnssBufLen, data, length); |
| | | GnssBufLen += length; |
| | | |
| | | if (GnssBufLen > 0) { |
| | | if (VirtualIsConnected()) { //PC模拟用时 |
| | | static bool first = false; |
| | | |
| | | if (!first) { |
| | | first = true; |
| | | |
| | | uint8_t buff[33] = "emulator"; |
| | | buff[32] = 1; |
| | | PlatformStatusChanged(RTK_STATUS_EVT, buff, 33); |
| | | } |
| | | |
| | | GnssBufLen = 0; |
| | | } else { |
| | | const uint8_t *ptr = parseGPS(GnssBuf, GnssBuf + GnssBufLen); |
| | | if (ptr != GnssBuf) { |
| | | memcpy(GnssBuf, ptr, GnssBufLen - (ptr - GnssBuf)); |
| | | GnssBufLen -= ptr - GnssBuf; |
| | | } else if (GnssBufLen == PARSE_BUFF_SIZE) { //填满了,且没有一个\r,都抛弃 |
| | | DEBUG("Parse GPS error"); |
| | | GnssBufLen = 0; |
| | | } |
| | | } |
| | | } |
| | | break; |
| | | } |
| | | default: |
| | | break; |
| | | } |
| | | } |
| | | |
| | | static void SendDfuFile(int fileLen, int sentLen, int blockLen, const uint8_t *data) { |
| | | uint8_t buffer[1024]; |
| | | int x = 0; |
| | | |
| | | DEBUG("SendDfuFile fileLen %d sentLen %d blockLen %d", fileLen, sentLen, blockLen); |
| | | |
| | | buffer[x++] = BREAK_UINT32(fileLen, 3); |
| | | buffer[x++] = BREAK_UINT32(fileLen, 2); |
| | | buffer[x++] = BREAK_UINT32(fileLen, 1); |
| | | buffer[x++] = BREAK_UINT32(fileLen, 0); |
| | | |
| | | buffer[x++] = BREAK_UINT32(sentLen, 3); |
| | | buffer[x++] = BREAK_UINT32(sentLen, 2); |
| | | buffer[x++] = BREAK_UINT32(sentLen, 1); |
| | | buffer[x++] = BREAK_UINT32(sentLen, 0); |
| | | |
| | | buffer[x++] = HI_UINT16(blockLen); |
| | | buffer[x++] = LO_UINT16(blockLen); |
| | | |
| | | memcpy(buffer + x, data, blockLen); |
| | | x += blockLen; |
| | | |
| | | SendMcuCommand(ID_CM_DFU_UPLOAD, buffer, x); |
| | | } |
| | | |
| | | static void GoNextDfuLater(union sigval sig) { |
| | | AppTimer_delete(GoNextDfuLater); |
| | | |
| | | GoNextDfu(); |
| | | } |
| | | |
| | | static void GoNextDfu(void) |
| | | { |
| | | int dfuFileSent = 0, currDfuBlockLength = 0; |
| | | |
| | | if (dfuFileLength == 0 || dfuFile == NULL) |
| | | return; |
| | | |
| | | int row = 0, col = 0; |
| | | |
| | | dfuFileSent = dfuFileLength; |
| | | |
| | | for (row = 0; row < sizeof(dfuFileBitmap); ++row) { |
| | | for (col = 0; col < 8; ++col) { |
| | | if ((dfuFileBitmap[row] & BV(col)) == 0) { |
| | | DEBUG("ROW = %d COL = %d", row, col); |
| | | dfuFileSent = row * DFU_FILE_BLOCK_SIZE * 8 + col * DFU_FILE_BLOCK_SIZE; |
| | | goto GET_FILE_START; |
| | | } |
| | | } |
| | | } |
| | | |
| | | GET_FILE_START: |
| | | currDfuBlockLength = (dfuFileLength - dfuFileSent > DFU_FILE_BLOCK_SIZE) ? DFU_FILE_BLOCK_SIZE : (dfuFileLength - dfuFileSent); |
| | | |
| | | if (dfuFileSent >= dfuFileLength || currDfuBlockLength == 0) { |
| | | UploadDfuFileEnd(); |
| | | return; |
| | | } |
| | | |
| | | SendDfuFile(dfuFileLength, dfuFileSent, currDfuBlockLength, dfuFile + dfuFileSent); |
| | | |
| | | if (dfuFileSent > 0 && dfuFileSent + currDfuBlockLength < dfuFileLength) { |
| | | dfuFileBitmap[row] |= BV(col); |
| | | } |
| | | |
| | | if (dfuFileSent == 0 || dfuFileSent + currDfuBlockLength == dfuFileLength) { |
| | | dfuTryCnt++; |
| | | if (dfuTryCnt > DFU_MAX_TRY) { |
| | | UploadDfuFileEnd(); |
| | | return; |
| | | } |
| | | DEBUG("GoNextDfuLater 3 sec..."); |
| | | AppTimer_add(GoNextDfuLater, D_SEC(3)); |
| | | } else { |
| | | AppTimer_add(GoNextDfuLater, 10); |
| | | } |
| | | } |
| | | |
| | | void UploadDfuFileEnd(void) |
| | | { |
| | | DEBUG("UploadDfuFileEnd"); |
| | | |
| | | if (dfuFile != NULL) |
| | | free(dfuFile); |
| | | dfuFileLength = 0; |
| | | |
| | | AppTimer_delete(GoNextDfuLater); |
| | | } |
| | | |
| | | void UploadDfuFile(const uint8_t *file, int length) |
| | | { |
| | | DEBUG("UploadDfuFile %ld", length); |
| | | |
| | | if (length > 0) { |
| | | if (dfuFile != NULL) |
| | | free(dfuFile); |
| | | |
| | | dfuTryCnt = 0; |
| | | |
| | | dfuFile = (uint8_t *) malloc(length); |
| | | dfuFileLength = length; |
| | | memcpy(dfuFile, file, length); |
| | | memset(dfuFileBitmap, 0, sizeof(dfuFileBitmap)); |
| | | AppTimer_delete(GoNextDfuLater); |
| | | |
| | | GoNextDfu(); |
| | | } |
| | | } |
| | | |
| | | static int readCartCnt = 0; |
| | | |
| | | static void ReadCardTimeout(union sigval sig) { |
| | | AppTimer_delete(ReadCardTimeout); |
| | | |
| | | static void ReadCardTimeout(apptimer_var_t val) { |
| | | readCartCnt++; |
| | | |
| | | if (readCartCnt < 2) { |
| | | AppTimer_add(ReadCardTimeout, D_SEC(3)); |
| | | SendMcuCommand(ID_CM_READ_RFCARD, NULL, 0); |
| | | //SendMcuCommand(ID_CM_READ_RFCARD, NULL, 0); |
| | | } else { |
| | | uint8_t data[8] = {0}; |
| | | PlatformStatusChanged(CARD_UPDATE_EVT, data, sizeof(data)); |
| | |
| | | |
| | | AppTimer_delete(ReadCardTimeout); |
| | | AppTimer_add(ReadCardTimeout, D_SEC(3)); |
| | | SendMcuCommand(ID_CM_READ_RFCARD, NULL, 0); |
| | | } |
| | | |
| | | void ShutdownInd(int timeout) |
| | | { |
| | | uint8_t v = timeout; |
| | | |
| | | SendMcuCommand(ID_CM_SHUTDOWN, &v, 1); |
| | | //SendMcuCommand(ID_CM_READ_RFCARD, NULL, 0); |
| | | } |