add set SERVICES
This commit is contained in:
parent
91be666ec3
commit
d767a5c4ac
14 changed files with 4650 additions and 5166 deletions
4526
BTH01_v06.hex
4526
BTH01_v06.hex
File diff suppressed because it is too large
Load diff
4513
THB2_v06.hex
4513
THB2_v06.hex
File diff suppressed because it is too large
Load diff
|
|
@ -58,6 +58,8 @@ typedef struct _ota_par_t {
|
|||
uint32_t erase_addr;
|
||||
} ota_par_t;
|
||||
|
||||
#define OTA_MODE_SELECT_REG 0x4000f034 // == 0x55 -> OTA
|
||||
|
||||
extern ota_par_t ota;
|
||||
|
||||
int ota_parser(unsigned char *pout, unsigned char *pmsg, unsigned int msg_size);
|
||||
|
|
|
|||
|
|
@ -17,15 +17,27 @@
|
|||
#include "gatt_uuid.h"
|
||||
#include "gattservapp.h"
|
||||
#include "gapbondmgr.h"
|
||||
#include "flash.h"
|
||||
#include "flash_eep.h"
|
||||
#include "bleperipheral.h"
|
||||
#include "sbp_profile.h"
|
||||
#include "sensor.h"
|
||||
#include "cmd_parcer.h"
|
||||
|
||||
#include "devinfoservice.h"
|
||||
#include "ble_ota.h"
|
||||
#include "thb2_peripheral.h"
|
||||
/*********************************************************************/
|
||||
#define SEND_DATA_SIZE 16
|
||||
|
||||
const dev_id_t dev_id = {
|
||||
.pid = CMD_ID_DEVID,
|
||||
.revision = 1,
|
||||
.hw_version = DEVICE,
|
||||
.sw_version = APP_VERSION,
|
||||
.dev_spec_data = 0,
|
||||
.services = DEV_SERVICES
|
||||
};
|
||||
|
||||
int cmd_parser(uint8_t * obuf, uint8_t * ibuf, uint32_t len) {
|
||||
int olen = 0;
|
||||
if (len) {
|
||||
|
|
@ -33,14 +45,8 @@ int cmd_parser(uint8_t * obuf, uint8_t * ibuf, uint32_t len) {
|
|||
obuf[0] = cmd;
|
||||
obuf[1] = 0; // no err
|
||||
if (cmd == CMD_ID_DEVID) { // Get DEV_ID
|
||||
pdev_id_t p = (pdev_id_t) obuf;
|
||||
// p->pid = CMD_ID_DEV_ID;
|
||||
p->revision = 1;
|
||||
p->hw_version = DEVICE;
|
||||
p->sw_version = APP_VERSION;
|
||||
p->dev_spec_data = 0;
|
||||
p->services = 0;
|
||||
olen = sizeof(dev_id_t);
|
||||
osal_memcpy(obuf, &dev_id, sizeof(dev_id));
|
||||
olen = sizeof(dev_id);
|
||||
} else if (cmd == CMD_ID_CFG) { // Get/Set device config
|
||||
if (--len > sizeof(cfg))
|
||||
len = sizeof(cfg);
|
||||
|
|
@ -71,6 +77,29 @@ int cmd_parser(uint8_t * obuf, uint8_t * ibuf, uint32_t len) {
|
|||
init_sensor();
|
||||
osal_memcpy(&obuf[1], &thsensor_cfg, thsensor_cfg_size);
|
||||
olen = thsensor_cfg_size + 1;
|
||||
} else if (cmd == CMD_ID_SERIAL) {
|
||||
osal_memcpy(&obuf[1], devInfoSerialNumber, sizeof(devInfoSerialNumber)-1);
|
||||
olen = 1 + sizeof(devInfoSerialNumber)-1;
|
||||
} else if (cmd == CMD_ID_FLASH_ID) {
|
||||
osal_memcpy(&obuf[1], (uint8_t *)&phy_flash.IdentificationID, 8);
|
||||
olen = 1 + 8;
|
||||
} else if (cmd == CMD_ID_SEN_ID) {
|
||||
osal_memcpy(&obuf[1], (uint8_t *)&thsensor_cfg.mid, 5);
|
||||
olen = 1 + 5;
|
||||
// } else if (cmd == CMD_ID_DNAME) {
|
||||
// } else if (cmd == CMD_ID_DEV_MAC) {
|
||||
} else if (cmd == CMD_ID_MTU) {
|
||||
if (ibuf[1] >= MTU_SIZE)
|
||||
ATT_UpdateMtuSize(gapRole_ConnectionHandle, ibuf[1]);
|
||||
else
|
||||
obuf[1] = 0xff;
|
||||
olen = 2;
|
||||
} else if (cmd == CMD_ID_REBOOT) {
|
||||
GAPRole_TerminateConnection();
|
||||
if(len >= 2) {
|
||||
write_reg(OTA_MODE_SELECT_REG, ibuf[1]);
|
||||
}
|
||||
hal_system_soft_reset();
|
||||
|
||||
//---------- Debug commands (unsupported in different versions!):
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ enum CMD_ID_KEYS {
|
|||
CMD_ID_I2C_UTR = 0x04, // Universal I2C/SMBUS read-write
|
||||
CMD_ID_SEN_ID = 0x05, // Get sensor ID
|
||||
CMD_ID_FLASH_ID = 0x06, // Get Flash JEDEC ID
|
||||
CMD_ID_SERIAL = 0x07, // Get serial string
|
||||
CMD_ID_DEV_MAC = 0x10, // Get/Set MAC [+RandMAC], [size][mac[6][randmac[2]]]
|
||||
CMD_ID_BKEY = 0x18, // Get/Set beacon bindkey in EEP
|
||||
CMD_ID_COMFORT = 0x20, // Get/Set comfort parameters
|
||||
|
|
@ -50,6 +51,7 @@ enum CMD_ID_KEYS {
|
|||
CMD_ID_REBOOT = 0x72, // Set Reboot on disconnect
|
||||
CMD_ID_SET_OTA = 0x73, // Extension BigOTA: Get/set address and size OTA, erase sectors
|
||||
|
||||
|
||||
// Debug commands (unsupported in different versions!):
|
||||
|
||||
CMD_ID_OTAC = 0xD1, // OTA clear
|
||||
|
|
@ -65,6 +67,23 @@ enum CMD_ID_KEYS {
|
|||
|
||||
};
|
||||
|
||||
// supported services by the device
|
||||
typedef struct _dev_services_t{
|
||||
uint32_t ota: 1; //0 OTA
|
||||
uint32_t ota_ext: 1; //1 OTA extension
|
||||
uint32_t pincode: 1; //2 pin-code
|
||||
uint32_t bindkey: 1; //3 bindkey
|
||||
uint32_t history: 1; //4 history
|
||||
uint32_t screen: 1; //5 screen
|
||||
uint32_t long_range: 1; //6 LE Long Range
|
||||
uint32_t ths: 1; //7 T & H sensor
|
||||
uint32_t rds: 1; //8 Reed switch sensor
|
||||
uint32_t key: 1; //9 key
|
||||
uint32_t out_pins: 1; //10 Output pins
|
||||
uint32_t inp_pins: 1; //11 Input pins
|
||||
uint32_t reserved: 20;
|
||||
} dev_services_t;
|
||||
|
||||
|
||||
// CMD_ID_DEV_ID
|
||||
typedef struct _dev_id_t{
|
||||
|
|
@ -76,6 +95,8 @@ typedef struct _dev_id_t{
|
|||
uint32_t services; // supported services by the device
|
||||
} dev_id_t, * pdev_id_t;
|
||||
|
||||
extern const dev_id_t dev_id;
|
||||
|
||||
int cmd_parser(uint8_t * obuf, uint8_t * ibuf, uint32_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#ifndef APP_VERSION
|
||||
#define APP_VERSION 0x06 // BCD
|
||||
#endif
|
||||
|
||||
/*
|
||||
#define BOARD_LYWSD03MMC_B14 0 // number used for BLE firmware
|
||||
#define BOARD_MHO_C401 1
|
||||
|
|
@ -40,9 +41,23 @@
|
|||
#define DEVICE DEVICE_BTH01
|
||||
#endif
|
||||
|
||||
// supported services by the device (bits)
|
||||
#define SERVICE_OTA 0x0001
|
||||
#define SERVICE_OTA_EXT 0x0002
|
||||
#define SERVICE_PINCODE 0x0004 // пока нет
|
||||
#define SERVICE_BINDKEY 0x0008 // пока нет
|
||||
#define SERVICE_HISTORY 0x0010 // пока нет
|
||||
#define SERVICE_SCREEN 0x0020 // пока нет
|
||||
#define SERVICE_LE_LR 0x0040 // пока нет
|
||||
#define SERVICE_THS 0x0080
|
||||
#define SERVICE_RDS 0x0100 // пока нет
|
||||
#define SERVICE_KEY 0x0200
|
||||
#define SERVICE_OUTS 0x0400 // пока нет
|
||||
#define SERVICE_INS 0x0800 // пока нет
|
||||
|
||||
#define OTA_TYPE_NONE 0 // нет OTA
|
||||
#define OTA_TYPE_BOOT 1 // вариант для прошивки boot + OTA
|
||||
#define OTA_TYPE_APP 2 // переключение из APP на OTA + boot прошивку, пока не реализовано
|
||||
#define OTA_TYPE_BOOT (SERVICE_OTA | SERVICE_OTA_EXT) // вариант для прошивки boot + OTA
|
||||
#define OTA_TYPE_APP SERVICE_OTA_EXT // переключение из APP на OTA + boot прошивку, пока не реализовано
|
||||
|
||||
#ifndef OTA_TYPE
|
||||
#define OTA_TYPE OTA_TYPE_BOOT
|
||||
|
|
@ -52,6 +67,12 @@
|
|||
|
||||
#if DEVICE == DEVICE_THB2
|
||||
/* Model: THB2 */
|
||||
|
||||
#define DEV_SERVICES (OTA_TYPE \
|
||||
| SERVICE_THS \
|
||||
| SERVICE_KEY \
|
||||
)
|
||||
|
||||
#define ADC_PIN_USE_OUT 0
|
||||
#define ADC_PIN GPIO_P11
|
||||
#define ADC_CHL ADC_CH1N_P11
|
||||
|
|
@ -69,6 +90,10 @@
|
|||
|
||||
#elif DEVICE == DEVICE_BTH01
|
||||
/* Model: BTH01 */
|
||||
#define DEV_SERVICES (OTA_TYPE \
|
||||
| SERVICE_THS \
|
||||
| SERVICE_KEY \
|
||||
)
|
||||
|
||||
#define ADC_PIN_USE_OUT 1 // hal_gpio_write(ADC_PIN, 1);
|
||||
#define ADC_PIN GPIO_P11
|
||||
|
|
@ -89,10 +114,20 @@
|
|||
#elif DEVICE == DEVICE_TH05
|
||||
/* Model: TH05 */
|
||||
|
||||
#define DEV_SERVICES (OTA_TYPE \
|
||||
| SERVICE_THS \
|
||||
| SERVICE_KEY \
|
||||
| SERVICE_SCREEN \
|
||||
)
|
||||
|
||||
#define ADC_PIN_USE_OUT 1 // hal_gpio_write(ADC_PIN, 1);
|
||||
#define ADC_PIN GPIO_P11
|
||||
#define ADC_CHL ADC_CH1N_P11
|
||||
|
||||
#define USE_TH_SENSOR 1
|
||||
#define USE_RS_SENSOR 0
|
||||
#define USE_SECREEN 1
|
||||
|
||||
#define I2C_SDA GPIO_P33 // CHT8305_SDA
|
||||
#define I2C_SCL GPIO_P34 // CHT8305_SCL
|
||||
#define GPIO_SPWR GPIO_P00 // питание сенсора CHT8305_VDD
|
||||
|
|
@ -109,8 +144,6 @@
|
|||
#error "DEVICE Not released!"
|
||||
#endif
|
||||
|
||||
#define OTA_MODE_SELECT_REG 0x4000f034 // == 0x55 -> OTA
|
||||
|
||||
// Minimum connection interval (units of 1.25ms, 80=100ms) if automatic parameter update request is enabled
|
||||
#define DEFAULT_DESIRED_MIN_CONN_INTERVAL 24 // 12 -> 15 ms
|
||||
// Maximum connection interval (units of 1.25ms, 800=1000ms) if automatic parameter update request is enabled
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ const uint8 devInfoModelNumber[] = DEF_MODEL_NUMBER_STR;
|
|||
#if SERIAL_NUMBER_STR_ENABLE
|
||||
// Serial Number String characteristic
|
||||
static uint8 devInfoSerialNumberProps = GATT_PROP_READ;
|
||||
uint8 devInfoSerialNumber[21]; // = "000000-00000000-0000"; // FLASH_ID-SENSOR_ID-EFUSE
|
||||
uint8 devInfoSerialNumber[19]; // = "000000-00000000-00"; // FLASH_ID-SENSOR_ID-EFUSE
|
||||
#endif
|
||||
|
||||
#if FIRMWARE_REVISION_ENABLE
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ bStatus_t DevInfo_SetParameter( uint8 param, uint8 len, void* value );
|
|||
extern bStatus_t DevInfo_GetParameter( uint8 param, void* value );
|
||||
|
||||
|
||||
extern uint8 devInfoSerialNumber[21];
|
||||
extern uint8 devInfoSerialNumber[19];
|
||||
extern const uint8 devInfoModelNumber[];
|
||||
/*********************************************************************
|
||||
*********************************************************************/
|
||||
|
|
|
|||
|
|
@ -35,27 +35,29 @@
|
|||
/*********************************************************************
|
||||
* TYPEDEFS
|
||||
*/
|
||||
|
||||
#define SERVICE_BTHOME_UUID16 0xFCD2 // 16-bit UUID Service 0xFCD2 BTHOME
|
||||
#define CHARACTERISTIC_OTA_UUID16 0xFFF3
|
||||
#define CHARACTERISTIC_CMD_UUID16 0xFFF4
|
||||
/*********************************************************************
|
||||
* GLOBAL VARIABLES
|
||||
*/
|
||||
// Simple GATT Profile Service UUID: 0xFFF0
|
||||
// Simple GATT Profile Service UUID: 0xFCD2
|
||||
CONST uint8_t simpleProfileServUUID[ATT_BT_UUID_SIZE] =
|
||||
{
|
||||
LO_UINT16(SIMPLEPROFILE_SERV_UUID), HI_UINT16(SIMPLEPROFILE_SERV_UUID)
|
||||
LO_UINT16(SERVICE_BTHOME_UUID16), HI_UINT16(SERVICE_BTHOME_UUID16)
|
||||
};
|
||||
|
||||
#if OTA_TYPE
|
||||
// Characteristic 1 UUID: 0xFFF3
|
||||
// Characteristic 1 UUID: 0x0001
|
||||
CONST uint8_t simpleProfilechar1UUID[ATT_BT_UUID_SIZE] =
|
||||
{
|
||||
LO_UINT16(SIMPLEPROFILE_CHAR1_UUID), HI_UINT16(SIMPLEPROFILE_CHAR1_UUID)
|
||||
LO_UINT16(CHARACTERISTIC_OTA_UUID16), HI_UINT16(CHARACTERISTIC_OTA_UUID16)
|
||||
};
|
||||
#endif
|
||||
// Characteristic 2 UUID: 0xFFF4
|
||||
// Characteristic 2 UUID: 0x0002
|
||||
CONST uint8_t simpleProfilechar2UUID[ATT_BT_UUID_SIZE] =
|
||||
{
|
||||
LO_UINT16(SIMPLEPROFILE_CHAR2_UUID), HI_UINT16(SIMPLEPROFILE_CHAR2_UUID)
|
||||
LO_UINT16(CHARACTERISTIC_CMD_UUID16), HI_UINT16(CHARACTERISTIC_CMD_UUID16)
|
||||
};
|
||||
|
||||
/*********************************************************************
|
||||
|
|
@ -310,8 +312,8 @@ static bStatus_t simpleProfile_ReadAttrCB( uint16_t connHandle, gattAttribute_t
|
|||
break;
|
||||
#endif
|
||||
case SIMPLEPROFILE_CHAR2_UUID:
|
||||
*pLen = sizeof(cfg);
|
||||
osal_memcpy( pValue, &cfg, *pLen );
|
||||
*pLen = sizeof(dev_id);
|
||||
osal_memcpy( pValue, &dev_id, *pLen);
|
||||
LOG("Read_UUID2:\n");
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -109,12 +109,9 @@ struct __attribute__((packed)) _cht8305_config_t{
|
|||
|
||||
#define CHT8305_ID 0x5959
|
||||
|
||||
|
||||
/*
|
||||
---------------------------------------
|
||||
/*---------------------------------------
|
||||
Датчик влажности AHT25
|
||||
---------------------------------------
|
||||
*/
|
||||
---------------------------------------*/
|
||||
#define AHT30_I2C_ADDR 0x38
|
||||
|
||||
#define AHT30_CMD_INI 0x0E1 // Initialization Command
|
||||
|
|
|
|||
|
|
@ -140,27 +140,15 @@ static void set_mac(void)
|
|||
set_def_name(ownPublicAddr);
|
||||
}
|
||||
|
||||
typedef enum
|
||||
{
|
||||
EFUSE_BLOCK_0 = 0,
|
||||
EFUSE_BLOCK_1 = 1,
|
||||
EFUSE_BLOCK_2 = 2,
|
||||
EFUSE_BLOCK_3 = 3,
|
||||
|
||||
} EFUSE_block_t;
|
||||
|
||||
extern int efuse_read(EFUSE_block_t block,uint32_t* buf);
|
||||
|
||||
static void set_serial_number(void)
|
||||
{
|
||||
hal_get_flash_info();
|
||||
uint32_t temp_rd[2] = {0, 0};
|
||||
efuse_read(EFUSE_BLOCK_0, temp_rd);
|
||||
uint8_t *p = str_bin2hex(devInfoSerialNumber, (uint8_t *)&phy_flash.IdentificationID, 3);
|
||||
*p++ = '-';
|
||||
p = str_bin2hex(p, (uint8_t *)&thsensor_cfg.mid, 4);
|
||||
*p++ = '-';
|
||||
p = str_bin2hex(p, (uint8_t *)&temp_rd[0], 2);
|
||||
*p++ = '0';
|
||||
*p = '0';
|
||||
}
|
||||
|
||||
extern gapPeriConnectParams_t periConnParameters;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const FLASH_SIZE = 0x80000;
|
|||
var bluetoothDevice, gattServer, otaCharacteristic, cmdCharacteristic, infoService;
|
||||
|
||||
var devinfo = {};
|
||||
var devsrs = {};
|
||||
|
||||
var startTime = 0,
|
||||
connected = false;
|
||||
|
|
@ -57,8 +58,8 @@ function handleError(text) {
|
|||
|
||||
function connect() {
|
||||
var deviceOptions = {
|
||||
optionalServices: [0x1800, 0x180a, 0x180f, 0x181a, 0xfff0, 0x2a26],
|
||||
services: [0x1800, 0x180a, 0x180f, 0x181a, 0xfff0, 0x2a26],
|
||||
optionalServices: [0x1800, 0x180a, 0x180f, 0x181a, 0xfcd2],
|
||||
services: [0x1800, 0x180a, 0x180f, 0x181a, 0xfcd2],
|
||||
acceptAllDevices: true };
|
||||
const namePrefix = $('inpNamePrefix').value;
|
||||
if (namePrefix) {
|
||||
|
|
@ -154,7 +155,7 @@ function linkOta() {
|
|||
}).then(_ => {
|
||||
return otaCharacteristic.readValue();
|
||||
}).then(value => {
|
||||
if(value.byteLength >= 20)
|
||||
if(value.byteLength > 1)
|
||||
addLog("OTA ver: "+ hex(value.getUint8(1),2));
|
||||
return resolve(null);});
|
||||
}).catch(error => { console.log(error); return resolve(null);});
|
||||
|
|
@ -172,7 +173,7 @@ function phyConnect(info_flg) {
|
|||
addLog("Hardware: "+devinfo.hrstr);
|
||||
if(devinfo.vrstr != null)
|
||||
addLog("Software: "+devinfo.vrstr);
|
||||
return gattServer.getPrimaryService(0xfff0);
|
||||
return gattServer.getPrimaryService(0xfcd2);
|
||||
}).then(service => {
|
||||
console.log("Найден Main Service");
|
||||
mainService = service;
|
||||
|
|
@ -187,7 +188,8 @@ function phyConnect(info_flg) {
|
|||
return cmdCharacteristic.readValue();
|
||||
}).then(value => {
|
||||
if(value.byteLength >= 10)
|
||||
addLog("DevCfg: 55"+dump8(value, value.byteLength));
|
||||
if(value.getUint8(0) == 0)
|
||||
addLog("Device id: "+hex(value.getUint16(2, true)),4);
|
||||
otaCharacteristic = null;
|
||||
return linkOta();
|
||||
}).then(_ => {
|
||||
|
|
@ -216,7 +218,7 @@ function doConnect() {
|
|||
console.log("Services: " + services[i].uuid);
|
||||
if (services[i].uuid == "0000180a-0000-1000-8000-00805f9b34fb")
|
||||
info = true;
|
||||
else if (services[i].uuid == "0000fff0-0000-1000-8000-00805f9b34fb")
|
||||
else if (services[i].uuid == "0000fcd2-0000-1000-8000-00805f9b34fb")
|
||||
phy = true;
|
||||
}
|
||||
if(phy)
|
||||
|
|
@ -351,9 +353,7 @@ function crc16_modbus(buffer) {
|
|||
for (var j = 0; j < 8; j++) {
|
||||
odd = crc & 0x0001;
|
||||
crc = crc >> 1;
|
||||
if (odd) {
|
||||
crc = crc ^ 0xA001;
|
||||
}
|
||||
if (odd) crc = crc ^ 0xA001;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
|
|
|
|||
|
|
@ -1,601 +0,0 @@
|
|||
<html class="telFlasherClass"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PHY62x2-BTHome тест версия</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
//BLE values
|
||||
const FLASH_SIZE = 0x80000;
|
||||
var bluetoothDevice, gattServer, otaCharacteristic, myCharacteristic;
|
||||
//Firmware values
|
||||
var firmwareArray = null,
|
||||
startTime = 0,
|
||||
flgRdFF = false,
|
||||
fwmaxsize = 196608,
|
||||
fwname = "",
|
||||
blockCount = 0;
|
||||
//Connection values
|
||||
var connectTrys = 0;
|
||||
|
||||
var $ = function(id) { return document.getElementById(id);}
|
||||
|
||||
function resetVariables() {
|
||||
busy = false;
|
||||
gattServer = null;
|
||||
Theservice = null;
|
||||
otaCharacteristic = null;
|
||||
myCharacteristic = null;
|
||||
$('butReadAddr').disabled = true;
|
||||
$('butStartDFU').disabled = true;
|
||||
$('butWriteData').disabled = true;
|
||||
$('butCmdData').disabled = true;
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
addLog(error);
|
||||
resetVariables();
|
||||
if (connectTrys < 5) {
|
||||
connectTrys++;
|
||||
addLog("Переподключение " + connectTrys + " из " + 5);
|
||||
doConnect();
|
||||
} else {
|
||||
addLog("Подключится не удалось!");
|
||||
connectTrys = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function onDisconnected() {
|
||||
addLog('Disconnected.');
|
||||
}
|
||||
|
||||
function connect() {
|
||||
var deviceOptions = {
|
||||
optionalServices: [0xfff0],
|
||||
services: [0x180a, 0x181c, 0x181e, 0xfff0],
|
||||
acceptAllDevices: true };
|
||||
const namePrefix = $('namePrefix').value;
|
||||
if (namePrefix) {
|
||||
deviceOptions.acceptAllDevices = false;
|
||||
deviceOptions.filters = namePrefix.split(",")
|
||||
.map((x) => ({ namePrefix: x }));
|
||||
} else {
|
||||
deviceOptions.acceptAllDevices = false;
|
||||
deviceOptions.filters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz_#@!*0123456789';,.<>{}[]"
|
||||
.split("")
|
||||
.map((x) => ({ namePrefix: x }));
|
||||
}
|
||||
if (bluetoothDevice != null) bluetoothDevice.gatt.disconnect();
|
||||
resetVariables();
|
||||
addLog("Поиск устройств");
|
||||
connectTrys = 0;
|
||||
navigator.bluetooth.requestDevice(deviceOptions).then(device => {
|
||||
bluetoothDevice = device;
|
||||
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
|
||||
addLog("Connecting to: " + bluetoothDevice.name);
|
||||
doConnect();
|
||||
}).catch(handleError);
|
||||
}
|
||||
|
||||
|
||||
function doConnect() {
|
||||
bluetoothDevice.gatt.connect().then(server => {
|
||||
addClog("Найден GATT сервер");
|
||||
gattServer = server;
|
||||
return gattServer.getPrimaryService(0xfff0);
|
||||
}).then(service => {
|
||||
addClog("Найден Main сервис");
|
||||
Theservice = service;
|
||||
return service.getCharacteristic(0xfff3);
|
||||
}).then(characteristic => {
|
||||
addClog("Найдена OTA характеристика");
|
||||
otaCharacteristic = characteristic;
|
||||
return otaCharacteristic.addEventListener('characteristicvaluechanged', event => OtaBlkParse(event.target.value));
|
||||
}).then(_ => {
|
||||
return otaCharacteristic.readValue();
|
||||
}).then(value => {
|
||||
if(value.byteLength >= 20)
|
||||
addLog("OTA: "+dump8(value, value.byteLength));
|
||||
return Theservice.getCharacteristic(0xfff4);
|
||||
}).then(characteristic => {
|
||||
addClog("Найдена CMD характеристика");
|
||||
myCharacteristic = characteristic;
|
||||
return myCharacteristic.readValue();
|
||||
}).then(value => {
|
||||
if(value.byteLength >= 10)
|
||||
addLog("DevCfg: "+dump8(value, value.byteLength));
|
||||
return myCharacteristic.addEventListener('characteristicvaluechanged', event => CustomBlkParse(event.target.value));
|
||||
}).then(_ => {
|
||||
myCharacteristic.startNotifications().then(_ => {
|
||||
let s = "Устройство подключено.";
|
||||
addAlog(s);
|
||||
$('butCmdData').disabled = false;
|
||||
$('butReadAddr').disabled = false;
|
||||
$('butWriteData').disabled = false;
|
||||
if (firmwareArray != null) $('butStartDFU').disabled = false;
|
||||
})
|
||||
}).catch(handleError);
|
||||
}
|
||||
|
||||
function reConnect() {
|
||||
if (bluetoothDevice != null) bluetoothDevice.gatt.disconnect();
|
||||
resetVariables();
|
||||
addLog("Reconnect");
|
||||
connectTrys = 0;
|
||||
doConnect();
|
||||
}
|
||||
|
||||
function startDFU() {
|
||||
addLog("Старт программирования...");
|
||||
updateBegin();
|
||||
}
|
||||
|
||||
function addLog(logTXT) {
|
||||
console.log(logTXT)
|
||||
var time = new Date().toLocaleTimeString();
|
||||
var logString = time + ": " + logTXT;
|
||||
$("log").innerHTML += logString + "<br>";
|
||||
}
|
||||
|
||||
function addClog(logTXT) {
|
||||
console.log(logTXT);
|
||||
}
|
||||
|
||||
function addAlog(logTXT) {
|
||||
addLog(logTXT);
|
||||
setStatus(logTXT);
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
$("log").innerHTML = "";
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
// addClog("Status: " + status);
|
||||
$("percent").innerHTML = "Статус: " + status;
|
||||
}
|
||||
|
||||
function updateFail(err) {
|
||||
let s = "OTA error: " + err;
|
||||
addAlog(s);
|
||||
}
|
||||
|
||||
function decimalToHex(d, padding) {
|
||||
var hex = Number(d).toString(16);
|
||||
while (hex.length < 4) {
|
||||
hex = "0" + hex;
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
function hexToBytes(hex) {
|
||||
for (var bytes = [], c = 0; c < hex.length; c += 2)
|
||||
bytes.push(parseInt(hex.substr(c, 2), 16));
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
function bytesToHex(data) {
|
||||
return new Uint8Array(data).reduce(function(memo, i) {
|
||||
return memo + ("0" + i.toString(16)).slice(-2);
|
||||
}, "");
|
||||
}
|
||||
|
||||
function crc16_modbus(buffer) {
|
||||
var crc = 0xFFFF;
|
||||
var odd;
|
||||
for (var i = 0; i < buffer.length; i++) {
|
||||
crc = crc ^ buffer[i];
|
||||
for (var j = 0; j < 8; j++) {
|
||||
odd = crc & 0x0001;
|
||||
crc = crc >> 1;
|
||||
if (odd) {
|
||||
crc = crc ^ 0xA001;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
};
|
||||
|
||||
function getHexCRC(data) {
|
||||
var tempCRC = decimalToHex(crc16_modbus(hexToBytes(data)));
|
||||
return tempCRC.substring(2, 4) + tempCRC.substring(0, 2);
|
||||
}
|
||||
|
||||
function makeRandomID(length) {
|
||||
var result = '';
|
||||
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
var charactersLength = characters.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return bytesToHex(new TextEncoder("utf-8").encode(result));
|
||||
}
|
||||
|
||||
function hex(number, len) {
|
||||
var str = (number.toString(16)).toUpperCase();
|
||||
while (str.length < len) str = '0' + str;
|
||||
return str;
|
||||
}
|
||||
|
||||
function hexToBytes(hex) {
|
||||
for (var bytes = [], c = 0; c < hex.length; c += 2)
|
||||
bytes.push(parseInt(hex.substr(c, 2), 16));
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
function dump(ar, len) {
|
||||
let s = '';
|
||||
for(let i=0; i < len; i++) {
|
||||
s += hex(ar[i],2);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function dump8(ar, len) {
|
||||
let s = '';
|
||||
for(let i=0; i < len; i++) {
|
||||
s += hex(ar.getUint8(i),2);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function hex2ascii(hexx) {
|
||||
var hex = hexx.toString();
|
||||
var str = '';
|
||||
for (var i = 0;
|
||||
(i < hex.length && hex.substr(i, 2) !== '00'); i += 2)
|
||||
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
|
||||
return str;
|
||||
}
|
||||
|
||||
var crc32 = (function() {
|
||||
let table = new Uint32Array(256);
|
||||
for(var i=256; i--;) {
|
||||
let tmp = i;
|
||||
for(let k=8; k--;) {
|
||||
tmp = tmp & 1 ? 0xEDB88320 ^ tmp >>> 1 : tmp >>> 1;
|
||||
}
|
||||
table[i] = tmp;
|
||||
}
|
||||
return function( data ) {
|
||||
let crc = -1;
|
||||
let l = data.length;
|
||||
for(let i=0; i<l; i++) {
|
||||
crc = crc >>> 8 ^ table[ crc & 255 ^ data[i] ];
|
||||
}
|
||||
return (crc >>> 0);
|
||||
};
|
||||
})();
|
||||
|
||||
function testOTAFirmware(data) {
|
||||
let fsize = data.byteLength;
|
||||
addClog("File size = 0x"+ fsize.toString(16));
|
||||
if (fsize < 272)
|
||||
return "Неправильный размер двоичной прошивки PHY6 OTA!";
|
||||
if(fsize > 192*1024) // 208kB for BLE
|
||||
return "Размер прошивки более 192 кбайт!";
|
||||
let head = new DataView(data, 0, 272);
|
||||
let h = {};
|
||||
h.id = head.getUint32(0, true);
|
||||
h.segs = head.getUint32(4, true);
|
||||
h.start = head.getUint32(8, true);
|
||||
h.size = head.getUint32(12, true);
|
||||
addAlog("PHY6 OTA ID: "+hex(h.id, 4)+ ", Сегментов: " + h.segs + ", Старт: 0x"+hex(h.start, 8)+ ", Размер: " + h.size + " байт");
|
||||
if(h.id != 0x36594850) // "PHY6"
|
||||
return "Неверное id в заголовке PHY6 OTA!";
|
||||
if(h.segs > 16)
|
||||
return "Неверное количество сегментов в заголовке PHY6 OTA!";
|
||||
if(h.size != fsize-4)
|
||||
return "Неверный размер в заголовке PHY6 OTA!";
|
||||
let fcrc = new Uint32Array(2);
|
||||
fcrc[0] = crc32(new Uint8Array(data.slice(0, h.size)));
|
||||
let x = new DataView(data, h.size, 4);
|
||||
fcrc[1] = x.getUint32(0, true);
|
||||
addClog("Файл CRC = 0x" + fcrc[1].toString(16) + ", Расчет CRC = 0x" + fcrc[0].toString(16));
|
||||
if(fcrc[0] != fcrc[1]) {
|
||||
return "Неправильный CRC в файле OTA!";
|
||||
}
|
||||
return "ok";
|
||||
}
|
||||
|
||||
function getFirmwareArray(data, filename) {
|
||||
addAlog("Файл: " + filename);
|
||||
addClog("Файл: " + filename);
|
||||
let s = testOTAFirmware(data);
|
||||
if(s != "ok") {
|
||||
addAlog(s);
|
||||
blockCount = 0;
|
||||
firmwareArray = null;
|
||||
fwname = "";
|
||||
//$("ldfrmw").innerHTML=s;
|
||||
alert(s);
|
||||
return;
|
||||
}
|
||||
firmwareArray = bytesToHex(data);
|
||||
addAlog("Размер файла: " + (firmwareArray.length/2).toString(10) + " байт");
|
||||
if (firmwareArray.length % 32 !== 0) { // pad last block to 16bytes
|
||||
var padHex = "ffffffffffffffffffffffffffffffff";
|
||||
firmwareArray += padHex.substr(0, 32 - firmwareArray.length % 32);
|
||||
}
|
||||
blockCount = firmwareArray.length / 32;
|
||||
addAlog("Счетчик: " + blockCount + " блоков");
|
||||
fwname = filename;
|
||||
$('butStartDFU').disabled = false;
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
document.querySelector("#file").addEventListener("change", function() {
|
||||
let reader = new FileReader();
|
||||
reader.fname = "";
|
||||
reader.onload = function() {getFirmwareArray(this.result, this.fname);};
|
||||
if (this.files[0] != null)
|
||||
reader.readAsArrayBuffer(this.files[0]);
|
||||
else
|
||||
addLog("Файл не выбран"); }, false);
|
||||
}
|
||||
|
||||
var ota_errors = [
|
||||
'ok',
|
||||
'Неверная команда',
|
||||
'Не задан старт',
|
||||
'Не заданы параметры',
|
||||
'Неверные параметры',
|
||||
'Неправильный размер пакета',
|
||||
'Ошибка CRC16 пакета',
|
||||
'Потеря пакетов',
|
||||
'Ошибка записи в Flash',
|
||||
'Ошибка в номере пакета',
|
||||
'Ошибка идентификатора в файле программы',
|
||||
'Ошибка CRC32 переданной программы'];
|
||||
|
||||
function get_msg_ota_err(err) {
|
||||
if(err == 0)
|
||||
return "ok";
|
||||
if(err == 255)
|
||||
return "OTA end";
|
||||
if(err <= 11)
|
||||
return ota_errors[err];
|
||||
return "Неизвестная ошибка";
|
||||
}
|
||||
|
||||
function OtaBlkParse(value) {
|
||||
if(value.byteLength < 20) return;
|
||||
var ota = {};
|
||||
ota.err_flag = value.getUint8(0);
|
||||
ota.version = value.getUint8(1);
|
||||
ota.start_flag = value.getUint8(2);
|
||||
ota.debug_flag = value.getUint8(3);
|
||||
ota.program_offset = value.getUint32(4,true);
|
||||
ota.pkt_index = value.getUint16(8,true);
|
||||
ota.pkt_total = value.getUint16(10,true);
|
||||
ota.fw_value = value.getUint32(12,true);
|
||||
ota.crc32 = value.getUint32(16,true);
|
||||
//addClog('otablk: '+dump8(value, value.byteLength));
|
||||
addClog('OTA read: ver: '+hex(ota.version,2)+', err: '+ota.err_flag+' - '+get_msg_ota_err(ota.err_flag)+', dbg: '+ota.debug_flag+', start: '+ota.start_flag+', offs: 0x'+hex(ota.program_offset,8)+', idx: 0x'+hex(ota.pkt_index,4)+', total: 0x'+hex(ota.pkt_total,4)+', crc: 0x'+hex(ota.crc32,8));
|
||||
}
|
||||
|
||||
function updateBegin() {
|
||||
if (blockCount <= 0) {
|
||||
addLog("Не выбран файл!");
|
||||
return;
|
||||
}
|
||||
setTimeout(function() {
|
||||
otaCharSend("00ff")
|
||||
.then(_ => { otaCharacteristic.readValue().then(value => {
|
||||
// otaCharSend("01ff"+firmwareArray.substring(16,24)+firmwareArray.substring(0,8)+hex((blockCount >> 8) | ((blockCount & 0xff) << 8),4))
|
||||
otaCharSend("01ff")
|
||||
.then(_ => { otaCharacteristic.readValue().then(value => {
|
||||
if(value.byteLength >= 2 && value.getUint8(0) == 0) {
|
||||
setTimeout(function() {
|
||||
startTime = new Date().getTime();
|
||||
sendOTAblock(0);
|
||||
}, 100);
|
||||
} else
|
||||
addAlog("Ошибка N"+value.getUint8(0)+" OTA!");
|
||||
}).catch(function(err) {updateFail(err); });
|
||||
}).catch(function(err) {updateFail(err); });
|
||||
}).catch(function(err) {updateFail(err); });
|
||||
}).catch(function(err) {updateFail(err); });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function sendLastOTA() {
|
||||
otaCharacteristic.readValue().then(value => {
|
||||
if(value.byteLength >= 1 && value.getUint8(0) == 0xff)
|
||||
addAlog("Программирование завершено за " + (new Date().getTime() - startTime) / 1000 + " секунды");
|
||||
else
|
||||
addAlog("Ошибка ("+value.getUint8(0)+") OTA: " + get_msg_ota_err(value.getUint8(0)));
|
||||
}).catch(function(err) { updateFail(err); });
|
||||
/* Сброс - отключен для теста
|
||||
var data = "02ff";
|
||||
otaCharSend(data).then(_ => {
|
||||
addAlog("Программирование завершено за " + (new Date().getTime() - startTime) / 1000 + " секунды");
|
||||
}).catch(function(err) {
|
||||
updateFail(err);
|
||||
}); */
|
||||
}
|
||||
|
||||
function sendOTAblock(blockNr) {
|
||||
if (blockNr >= blockCount) {
|
||||
sendLastOTA();
|
||||
return;
|
||||
}
|
||||
setStatus("Передан блок N: " + blockNr + " из " + blockCount + ", " + Math.floor(blockNr / (blockCount * 1.0) * 100) + "% успеха, время от старта " + (new Date().getTime() - startTime) / 1000.0 + " сек");
|
||||
var blockNrString = getHexBLockCount(blockNr);
|
||||
var blockString = blockNrString + firmwareArray.substring(blockNr * 32, blockNr * 32 + 32);
|
||||
var blockCRC = getHexCRC(blockString);
|
||||
otaCharSend(blockString + blockCRC).then(_ => {
|
||||
if (blockNr >= blockCount - 1) {
|
||||
sendLastOTA();
|
||||
return;
|
||||
}
|
||||
setTimeout(function() {
|
||||
if ((blockNr + 1) % 8 == 0) {
|
||||
otaCharacteristic.readValue().then(value => {
|
||||
if(value.byteLength >= 1 && value.getUint8(0) == 0)
|
||||
sendOTAblock(blockNr + 1);
|
||||
else {
|
||||
let s = get_msg_ota_err(value.getUint8(0));
|
||||
if(s != "ok")
|
||||
addAlog("Ошибка ("+value.getUint8(0)+") на передаче блока "+blockNr+" OTA: "+s);
|
||||
}
|
||||
}).catch(function(err) { updateFail(err); });
|
||||
} else
|
||||
sendOTAblock(blockNr + 1);
|
||||
}, 0);
|
||||
}).catch(function(err) {
|
||||
updateFail(err);
|
||||
});
|
||||
}
|
||||
|
||||
function getHexBLockCount(count) {
|
||||
var tempHEX = decimalToHex(count);
|
||||
return tempHEX.substring(2, 4) + tempHEX.substring(0, 2);
|
||||
}
|
||||
|
||||
|
||||
var otaCharSend = function(data) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
//addClog("OTA send: " + data);
|
||||
otaCharacteristic.writeValue(hexToBytes(data)).then(function(character) {
|
||||
resolve("ok");
|
||||
}).catch(function(err) {
|
||||
reject("Ошибка при отправке данных");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var mainCharSend = function(data, characteristic) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
addClog("Send: " + data);
|
||||
characteristic.writeValue(hexToBytes(data)).then(function(character) {
|
||||
resolve("ok");
|
||||
}).catch(function(err) {
|
||||
reject("Ошибка при отправке данных");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ReadAddr(addr) {
|
||||
if(myCharacteristic) {
|
||||
let blk = new Uint8Array([0xdb, addr&0xff, (addr>>8)&0xff, (addr>>16)&0xff, (addr>>24)&0xff]);
|
||||
myCharacteristic.writeValue(blk).then(_ => {
|
||||
startTime = new Date().getTime();
|
||||
addAlog("Чтение 16 байт из 0x"+hex(addr,8)+"...");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function WriteAddr(addr, data) {
|
||||
if(myCharacteristic) {
|
||||
len = data.length;
|
||||
if(len != 0 && len <= 16) {
|
||||
let blk = new Uint8Array(len + 5);
|
||||
blk.set([0xdb, addr&0xff, (addr>>8)&0xff, (addr>>16)&0xff, (addr>>24)&0xff]);
|
||||
blk.set(data, 5);
|
||||
console.log(blk);
|
||||
addAlog("Запись "+len+" байт в 0x"+hex(addr,6)+"...");
|
||||
myCharacteristic.writeValue(blk);
|
||||
} else {
|
||||
console.log(data);
|
||||
addClog('Должно быть от 1 до 16 байт!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function InfoReadAddr() {
|
||||
if(myCharacteristic) {
|
||||
let faddr = parseInt($('inputAddr').value, 16);
|
||||
ReadAddr(faddr);
|
||||
}
|
||||
}
|
||||
|
||||
function WriteCmd(data) {
|
||||
if(myCharacteristic) {
|
||||
len = data.length;
|
||||
if(len != 0 && len <= 20) {
|
||||
let blk = new Uint8Array(data);
|
||||
console.log(blk);
|
||||
myCharacteristic.writeValue(blk);
|
||||
} else {
|
||||
console.log(data);
|
||||
addClog('Должно быть от 1 до 20 байт!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function WriteData() {
|
||||
let addr = parseInt($('inputAddr').value, 16);
|
||||
let data = hexToBytes($('inputData').value);
|
||||
if(data.length != 0 && data.length <= 16)
|
||||
WriteAddr(addr, data);
|
||||
else
|
||||
addClog('Должно быть от 1 до 16 hex байт!');
|
||||
|
||||
}
|
||||
|
||||
function CmdData() {
|
||||
let data = hexToBytes($('inputCmdData').value);
|
||||
if(data.length != 0 && data.length <= 20)
|
||||
WriteCmd(data);
|
||||
else
|
||||
addClog('Должно быть от 1 до 20 hex байт!');
|
||||
}
|
||||
|
||||
|
||||
function CustomBlkParse(value) {
|
||||
let len = value.byteLength;
|
||||
if(len == 0) return;
|
||||
len--; // size from cmd
|
||||
let blkid = value.getUint8(0);
|
||||
s = 'Ответ на команду id: '+hex(blkid,2)+' data: '+bytesToHex(value.buffer.slice(1));
|
||||
addLog(s);
|
||||
if(blkid == 0xdb && value.byteLength > 4) {
|
||||
len -= 4;
|
||||
let addr = value.getUint32(1,true);
|
||||
let s = bytesToHex(value.buffer.slice(5), len);
|
||||
$('inputData').value = s;
|
||||
addLog(hex(addr,8)+':'+s);
|
||||
setStatus("Считано "+len+" байт из 0x" + hex(addr,8));
|
||||
} else
|
||||
addClog('blk: '+dump8(value, value.byteLength));
|
||||
}
|
||||
|
||||
var url;
|
||||
function download(data, filename, type) {
|
||||
var file = new Blob([data], {type: type});
|
||||
if (window.navigator.msSaveOrOpenBlob) { // ie10+
|
||||
window.navigator.msSaveOrOpenBlob(file, filename);
|
||||
} else { // ff, chrome
|
||||
url = URL.createObjectURL(file);
|
||||
let a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function(){document.body.removeChild(a);window.URL.revokeObjectURL(url);},0);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<big><big>PHY62x2-BTHome</big></big> <a href="https://github.com/pvvx/THB2">ⓘ</a><hr>
|
||||
<button type="button" onclick="connect();">Соединение</button>
|
||||
<button type="button" onclick="reConnect();">Переподключение</button><br><br>
|
||||
<label for="namePrefix">Префикс названия устройств(а)</label>
|
||||
<input type="text" id="namePrefix" value="" placeholder="THB, BT"><br><hr>
|
||||
Выбор файла прошивки: <input type="file" accept=".bin" id="file"/><br>
|
||||
<div id="percent">Состояние: Ожидание соединения с устройством</div>
|
||||
<button type="button"id="butStartDFU" disabled="true" onclick="startDFU();">Старт программирования</button>
|
||||
<br><hr>
|
||||
Чтение и запись памяти:<br>
|
||||
Адрес (hex): <input size="8" type="text" id="inputAddr" value="11000000" maxlength="8">
|
||||
<input type="button" id="butReadAddr" onclick="InfoReadAddr()" disabled="true" value="Читать">
|
||||
Данные (hex): <input size="40" type="text" id="inputData" value="?" maxlength="32">
|
||||
<input type="button" id="butWriteData" onclick="WriteData()" disabled="true" value="Записать"><hr>
|
||||
<input type="button" id="butCmdData" onclick="CmdData()" disabled="true" value="Команда">
|
||||
<input size="40" type="text" id="inputCmdData" value="55" maxlength="40"><hr>
|
||||
<button type="button" onclick="clearLog();">Очистить лог</button><br>
|
||||
<div id="log"></div>
|
||||
</body></html>
|
||||
BIN
bthome_phy6222/web/test.bin
Normal file
BIN
bthome_phy6222/web/test.bin
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue