THB2/bthome_phy6222/web/phy62x2.html
froloffw7 1c49838ab1
Update phy62x2.html
fix Aux Controls state
2024-02-07 18:20:01 +01:00

1836 lines
64 KiB
HTML
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.

<html class="phy6222Class"><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 v0.99</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<link rel="stylesheet" type="text/css" href="chart.css" />
<script type="text/javascript" src="dygraph.min.js" /></script>
<!--link rel="stylesheet" type="text/css" href="https://dygraphs.com/dist/dygraph.min.css /-->
<!--script type="text/javascript" src="https://dygraphs.com/dist/dygraph.min.js" /></script-->
<style type="text/css">
div#divChart {
height: 100%;
width: 100%;
margin-top: 16px;
margin-bottom: 16px;
}
</style>
</head>
<body>
<script>
//BLE values
const FLASH_SIZE = 0x80000;
const OTA_MAX_SIZE = 0x30000; // 196608
const SERVICE_SCREEN = 0x00000020;
const SERVICE_TH_TRG = 0x00004000;
var bluetoothDevice, gattServer, otaCharacteristic, cmdCharacteristic, infoService;
var devInfo = {};
var devSrv = { services: 0 };
var devCfg = {};
var devSens = {};
var devTrig = {};
var devTime = {};
var devName = "";
var isConnected = false;
var isChartEnabled = true;
var isMemoActive = false;
var startTime = 0;
var memoCount = 0;
var ota = {
fwArray: null,
fwname: "",
fwsize: 0,
// fwmaxsize: 196608,
ext_flg: false,
blockCount: 0,
program_offset: 0x11010000
};
var flashBuffer = null;
//Connection values
var connectRetries = 0;
var $ = function(id) { return document.getElementById(id);}
const isEmpty = str => !str.trim().length;
function resetVariables() {
gattServer = null;
mainService = null;
otaCharacteristic = null;
cmdCharacteristic = null;
$('btnDisconnect').disabled = true;
$('btnStartDFU').disabled = true;
}
function handleError(text) {
showError(text);
resetVariables();
if ((bluetoothDevice != null) && (connectRetries < 5)) {
addLog("Переподключение " + connectRetries + " из " + 5);
$('btnDisconnect').disabled = true;
$('btnReconnect').disabled = true;
connectRetries++;
doConnect();
} else {
addLog("Подключиться не удалось!");
connectRetries = 0;
// disconnect();
$('btnConnect').disabled = false;
}
}
function connect() {
var deviceOptions = {
optionalServices: [0x1800, 0x180a, 0x180f, 0x181a, 0xfcd2],
services: [0x1800, 0x180a, 0x180f, 0x181a, 0xfcd2],
acceptAllDevices: true };
const namePrefix = $('inpNamePrefix').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();
chartData.length = 0;
resetVariables();
bluetoothDevice = null;
$('btnReconnect').disabled = true;
$('tabConfig').style.display = "none";
if(typeof(navigator.bluetooth) != "undefined") {
$('btnConnect').disabled = true;
showState("Поиск устройств");
connectRetries = 0;
//console.log()
navigator.bluetooth.requestDevice(deviceOptions).then(device => {
bluetoothDevice = device;
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
//addLog("Connecting to: " + bluetoothDevice.name);
doConnect();
}).catch(handleError);
} else {
showError("Browser doesn't support BLE API");
}
}
function disconnect() {
addLog("Отключение");
isConnected = false;
if (bluetoothDevice != null)
bluetoothDevice.gatt.disconnect();
}
function reconnect() {
addLog("Переподключение");
if (bluetoothDevice != null) {
bluetoothDevice.gatt.disconnect();
isConnected = false;
}
$('btnConnect').disabled = true;
$('btnReconnect').disabled = true;
connectRetries = 0;
doConnect();
}
function getStrCharacteristic(infosrv, suuid) {
return new Promise((resolve, reject) => {
infosrv.getCharacteristic(suuid).then(characteristic => {
characteristic.readValue().then(value => {
return resolve(new TextDecoder("utf-8").decode(value));
}).catch(error => {console.log(error); return resolve(null); });
}).catch(error => {console.log(error); return resolve(null); });
})};
function getDevInfo(devInfEnabled) {
return new Promise((resolve, reject) => {
devInfo.nrstr = null;
devInfo.srstr = null;
devInfo.frstr = null;
devInfo.hrstr = null;
devInfo.vrstr = null;
if (devInfEnabled == true) {
gattServer.getPrimaryService(0x180a).then(service => {
console.log("Найден Device Information Service");
infoService = service;
return getStrCharacteristic(infoService, 0x2a24).then(value => {
devInfo.nrstr = value;
//DOMException: getCharacteristic(s) called with blocklisted UUID. https://goo.gl/4NeimX
//return getStrCharacteristic(infoService, 0x2a25).then(value => {
// devInfo.srstr = value;
return getStrCharacteristic(infoService, 0x2a26).then(value => {
devInfo.frstr = value;
return getStrCharacteristic(infoService, 0x2a27).then(value => {
devInfo.hrstr = value;
return getStrCharacteristic(infoService, 0x2a28).then(value => {
devInfo.vrstr = value;
return resolve(devInfo.flg);
}).catch(error => { console.log(error); return resolve(null);});
}).catch(error => { console.log(error); return resolve(null);});
}).catch(error => { console.log(error); return resolve(null);});
//}).catch(error => { console.log(error); return resolve(null);});
}).catch(error => { console.log(error); return resolve(null);});
}).catch(error => { console.log(error); return resolve(null);});
} else return resolve(null);
}).catch(error => { console.log(error); return resolve(null);});
}
function linkOta() {
return new Promise((resolve, reject) => {
mainService.getCharacteristic(0xfff3).catch(error => { console.log(error); return resolve(null);})
.then(characteristic => {
console.log("Найдена OTA Characteristic");
otaCharacteristic = characteristic;
return otaCharacteristic.addEventListener('characteristicvaluechanged', event => parseBlkOTA(event.target.value));
}).then(_ => {
return otaCharacteristic.readValue();
}).then(value => {
if(value.byteLength > 1) {
addLog("OTA ver: "+ hex(value.getUint8(1),2));
$("btnStartDFU").value = "Старт программирования"
}
return resolve(null);});
}).catch(error => { console.log(error); return resolve(null);});
}
function phyConnect(info_flg) {
return getDevInfo(info_flg).then(_ => {
if(devInfo.nrstr != null)
addLog("Model: "+devInfo.nrstr);
if(devInfo.srstr != null)
addLog("Serial: "+devInfo.srstr);
if(devInfo.frstr != null)
addLog("Firmware: "+devInfo.frstr);
if(devInfo.hrstr != null)
addLog("Hardware: "+devInfo.hrstr);
if(devInfo.vrstr != null)
addLog("Software: "+devInfo.vrstr);
return gattServer.getPrimaryService(0xfcd2);
}).then(service => {
console.log("Найден Main Service");
mainService = service;
cmdCharacteristic = null;
return mainService.getCharacteristic(0xfff4);
}).then(characteristic => {
console.log("Найдена CMD Characteristic");
cmdCharacteristic = characteristic;
return cmdCharacteristic.addEventListener('characteristicvaluechanged', event => parseBlkCustom(event.target.value));
}).then(_ => {
return cmdCharacteristic.startNotifications();
}).then(_ => {
return cmdCharacteristic.readValue();
}).then(value => {
if(value.byteLength >= 10)
if(value.getUint8(0) != 0)
addLog("Странный ответ устройства!");
else if((value.getUint32(8, true) & 1) != 0) // SERVICE_OTA ?
return linkOta();
otaCharacteristic = null;
}).then(_ => {
showState("Устройство подключено.");
isConnected = true;
// $('btnConnect').disabled = true;
$('btnDisconnect').disabled = false;
// $('btnReconnect').disabled = false;
disableControls(false);
// selectConfigTab();
// $('tabConfig').style.display = "block";
let el = $('btnStartDFU');
if (otaCharacteristic != null) {
el.disabled = ota.fwArray == null;
el.innerHTML = "Старт";
el.title = "Старт программирования..."
} else {
el.innerHTML = "Режим OTA";
el.title = "Переключение на BootLoader ..."
}
return cmdCharacteristic.writeValue(new Uint8Array([0x33])); // get measure
}).catch(handleError);
}
function doConnect() {
isConnected = false;
showState("Ожидание соединения с " + bluetoothDevice.name)
return bluetoothDevice.gatt.connect().then(server => {
console.log("Найден GATT сервер");
gattServer = server;
gattServer.getPrimaryServices().then(services => {
let phy = false;
let info = false;
for (var i = 0; i < services.length; i++) {
console.log("Services: " + services[i].uuid);
if (services[i].uuid == "0000180a-0000-1000-8000-00805f9b34fb")
info = true;
else if (services[i].uuid == "0000fcd2-0000-1000-8000-00805f9b34fb")
phy = true;
}
if(phy)
return phyConnect(true);
addLog("Выбрано неверное устройтво!");
bluetoothDevice.gatt.disconnect();
connectRetries = 10;
return null;
}).catch(handleError);
}).catch(handleError);
}
function auxControls(state)
{
if ( devSrv.services & SERVICE_SCREEN ) {
$('tblChkCfg').style.display = "block";
$('tblComfort').style.display = "block";
} else {
$('tblChkCfg').style.display = "none";
$('tblComfort').style.display = "none";
}
if ( devSrv.services & SERVICE_TH_TRG ) {
$('tblTrigger').style.display = "block";
$('btnGetTrg').disabled = state;
$('btnSetTrg').disabled = state;
} else {
$('tblTrigger').style.display = "none";
$('btnGetTrg').style.display = "none";
$('btnSetTrg').style.display = "none";
}
if (devSrv.services & (SERVICE_SCREEN|SERVICE_TH_TRG))
$('hrPres').style.display = "block";
else
$('hrPres').style.display = "none";
}
function disableControls(state)
{
//--debug devSrv.services = SERVICE_SCREEN|SERVICE_TH_TRG;
auxControls(state);
if(state) { // hide
$('hrSensorData').style.display = "none";
$('tblSensorData').style.display = "none";
} else { // show
$('hrSensorData').style.display = "block";
$('tblSensorData').style.display = "block";
}
$('btnGetDev').disabled = state;
$('btnSetDev').disabled = state;
$('btnRstDev').disabled = state;
$('btnGetSens').disabled = state;
$('btnSetSens').disabled = state;
$('btnRstSens').disabled = state;
$('btnGetDevTime').disabled = state;
$('btnSetDevTime').disabled = state;
$('btnReadAddr').disabled = state;
$('btnWriteAddr').disabled = state;
$('btnSendCommand').disabled = state;
$('btnGetMAC').disabled = state;
$('btnSetMAC').disabled = state;
$('btnGetName').disabled = state;
$('btnSetName').disabled = state;
$('btnRstName').disabled = state;
}
function onDisconnected() {
isConnected = false;
resetVariables();
showState('Устройство отключено.');
$('btnConnect').disabled = false;
$('btnReconnect').disabled = false;
// $('tabConfig').style.display = "none";
disableControls(true);
}
function startDFU() {
if(otaCharacteristic != null && ota.ind.version == 1 && (devSrv.services & 1) == 1) {
addLog("Старт программирования...");
updateBegin();
} else {
addLog("Переключение на BootLoader...");
if(cmdCharacteristic != null) {
cmdCharacteristic.writeValue(new Uint8Array([0x72,0x55])).then(_ => {
console.log('Comand to reboot to bootloader sent');
reconnect();
}).catch(error => { addLog(error); });
}
}
}
function addLog(logTXT) {
console.log(logTXT)
var time = new Date().toLocaleTimeString();
var logString = time + ": " + logTXT;
$("log").innerHTML += logString + "<br>";
}
function showState(text) {
let s = "Состояние: " + text;
$("lblStatus").className = "shadowbox";
$("lblStatus").innerHTML = s;
addLog(text);
}
function showError(text) {
// console.log("Status: " + status);
let s = "Ошибка: " + text;
$("lblStatus").className = "shadowerror";
$("lblStatus").innerHTML = s;
addLog(text);
}
function showProgress(text) {
$("lblStatus").className = "shadowprogress";
$("lblStatus").innerHTML = text;
}
function clearLog() {
$("log").innerHTML = "";
}
function updateFail(err) {
let s = "OTA: " + err;
showError(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 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;
}
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);
}
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 testFwOTA(data) {
ota.fwsize = 0;
let fsize = data.byteLength;
console.log("File size = 0x"+ fsize.toString(16));
if (fsize < 20)
return "Неправильный размер двоичной прошивки PHY6 OTA!";
if(fsize > OTA_MAX_SIZE)
return "Размер прошивки более " + (OTA_MAX_SIZE/1024).toFixed(1) + " кбайт!";
if(fsize != (fsize & 0x1ffff0) + 4)
return "Неверный формат размера файла для прошивки PHY6 OTA!";
ota.fwsize = fsize;
let head = new DataView(data, 0, 16);
ota.h = {};
ota.h.id = head.getUint32(0, true);
ota.h.segs = head.getUint32(4, true);
ota.h.start = head.getUint32(8, true);
ota.h.size = head.getUint32(12, true);
if(ota.h.id != 0x36594850) { // "PHY6"
ota.ext_flg = true;
ota.h.size = fsize - 4;
ota.h.start = ota.program_offset;
addLog("Файл id:"+hex(ota.h.id, 4)+" , специальная прошивка.");
return "Расширенный формат OTA пока не поддерживается!";
} else {
ota.ext_flg = false;
addLog("Файл id:PHY6, Сегментов: " + ota.h.segs + ", Старт: 0x"+hex(ota.h.start, 8)+ ", Размер: " + ota.h.size + " байт");
if(ota.h.segs > 16)
return "Неверное количество сегментов в заголовке PHY6 OTA!";
if(ota.h.size + 4 != ota.fwsize)
return "Неверный размер в заголовке PHY6 OTA!";
}
let crc = crc32(new Uint8Array(data.slice(0, ota.h.size)));
let x = new DataView(data, ota.h.size, 4);
ota.fcrc = x.getUint32(0, true);
console.log("Файл CRC = 0x" + ota.fcrc.toString(16) + ", Расчет CRC = 0x" + crc.toString(16));
if(ota.fcrc != crc) {
return "Неправильный CRC в файле OTA!";
}
return "OK";
}
function getFwArray(data, filename) {
addLog("Файл: " + filename);
let s = testFwOTA(data);
if(s != "OK") {
$('btnStartDFU').disabled = true;
addLog(s);
ota.blockCount = 0;
ota.fwArray = null;
fwname = "";
alert(s);
showError("OTA файл: " + filename + " не загружен");
$('lblFile').innerHTML = "не загружен";
return;
}
ota.fwArray = bytesToHex(data);
addLog("Размер файла: " + (ota.fwArray.length/2).toString(10) + " байт");
if (ota.fwArray.length % 32 !== 0) { // pad last block to 16bytes
var padHex = "ffffffffffffffffffffffffffffffff";
ota.fwArray += padHex.substr(0, 32 - ota.fwArray.length % 32);
}
ota.blockCount = ota.fwArray.length / 32;
addLog("Счетчик: " + ota.blockCount + " блоков");
ota.fwname = filename;
showProgress("OTA файл: " + filename);
if(isConnected)
$('btnStartDFU').disabled = false;
}
var otaErrStrs = [
'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 otaErrStrs[err];
return "Неизвестная ошибка";
}
function parseBlkOTA(value) {
if(value.byteLength < 20) return;
ota.ind = {};
ota.ind.err_flag = value.getUint8(0);
ota.ind.version = value.getUint8(1);
ota.ind.start_flag = value.getUint8(2);
ota.ind.debug_flag = value.getUint8(3);
ota.ind.prg_offset = value.getUint32(4,true);
ota.ind.pkt_index = value.getUint16(8,true);
ota.ind.pkt_total = value.getUint16(10,true);
ota.ind.fw_value = value.getUint32(12,true);
ota.ind.crc32 = value.getUint32(16,true);
//console.log('otablk: '+dump8(value, value.byteLength));
console.log('OTA read: ver: ' + hex(ota.ind.version,2)
+ ', err: ' + ota.ind.err_flag + ' - ' + get_msg_ota_err(ota.ind.err_flag)
+ ', dbg: ' + ota.ind.debug_flag
+ ', start: '+ota.ind.start_flag
+ ', offs: 0x'+hex(ota.ind.prg_offset,8)
+ ', idx: 0x'+hex(ota.ind.pkt_index,4)
+ ', total: 0x'+hex(ota.ind.pkt_total,4)
+ ', crc: 0x'+hex(ota.ind.crc32,8));
}
function updateBegin() {
if (ota.blockCount <= 0) {
showError("Не выбран файл!");
return;
}
setTimeout(function() {
sendCharOTA("00ff").then(_ => { otaCharacteristic.readValue().then(value => {
let cmd = "01ff";
// TODO
// if(ota.ext_flg) {
// cmd += ota.fwArray.substring(16,24); // program_offset
// cmd += ota.fwArray.substring(0,8); // fw_id
// cmd += hex((ota.blockCount >> 8) | ((ota.blockCount & 0xff) << 8),4); // pkt_total
// }
sendCharOTA(cmd).then(_ => { otaCharacteristic.readValue().then(value => {
if(value.byteLength >= 2 && value.getUint8(0) == 0) {
setTimeout(function() {
startTime = new Date().getTime();
sendBlkOTA(0);
}, 50);
} else
showError("Ошибка 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); });
}, 50);
}
function sendLastOTA() {
otaCharacteristic.readValue().then(value => {
if(value.byteLength >= 1 && value.getUint8(0) == 0xff) {
return sendCharOTA("02ff").then(_ => {
let s = "Программирование завершено за " + (new Date().getTime() - startTime) / 1000 + " секунды";
showProgress(s);
addLog(s);
}).catch(function(err) {
updateFail("Ошибка передачи команды перезагрузки после OTA");
});
} else
showError("Ошибка ("+value.getUint8(0)+") на передаче блока "+blockNr+" OTA: "+s);
}).catch(function(err) { updateFail(err); });
}
function sendBlkOTA(blockNr) {
if (blockNr >= ota.blockCount) {
return sendLastOTA();
}
showProgress("Передан блок " + blockNr + " из " + ota.blockCount + ", " + Math.floor(blockNr / (ota.blockCount * 1.0) * 100) + "% успеха, время от старта " + (new Date().getTime() - startTime) / 1000.0 + " сек");
var blockNrString = getHexBLockCount(blockNr);
var blockString = blockNrString + ota.fwArray.substring(blockNr * 32, blockNr * 32 + 32);
var blockCRC = getHexCRC(blockString);
sendCharOTA(blockString + blockCRC).then(_ => {
if (blockNr >= ota.blockCount - 1) {
return sendLastOTA();
}
setTimeout(function() {
if ((blockNr + 1) % 16 == 0) {
otaCharacteristic.readValue().then(value => {
if(value.byteLength >= 20 && value.getUint8(0) == 0)
sendBlkOTA(blockNr + 1);
else {
let s = get_msg_ota_err(value.getUint8(0));
if(s != "OK")
showError("Ошибка ("+value.getUint8(0)+") на передаче блока "+blockNr+" OTA: "+s);
}
}).catch(function(err) { updateFail(err); });
} else
sendBlkOTA(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 sendCharOTA = function(data) {
return new Promise(function(resolve, reject) {
//console.log("OTA send: " + data);
otaCharacteristic.writeValue(hexToBytes(data)).then(function(character) {
resolve("OK");
}).catch(function(err) {
reject("Ошибка при отправке данных");
});
});
}
var sendCharMain = function(data, characteristic) {
return new Promise(function(resolve, reject) {
console.log("Send: " + data);
characteristic.writeValue(hexToBytes(data)).then(function(character) {
resolve("OK");
}).catch(function(err) {
reject("Ошибка при отправке данных");
});
});
}
function readAddr(addr) {
if (cmdCharacteristic) {
let blk = new Uint8Array([0xdb, addr&0xff, (addr>>8)&0xff, (addr>>16)&0xff, (addr>>24)&0xff]);
cmdCharacteristic.writeValue(blk).then(_ => {
startTime = new Date().getTime();
showState("Чтение 16 байт по адресу 0x" + hex(addr,8));
});
}
}
function writeAddr(addr, data) {
if (cmdCharacteristic) {
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);
cmdCharacteristic.writeValue(blk);
showState("Запись " + len + " байт по адресу 0x" + hex(addr,8));
} else {
console.log(data);
showError('Ошибка, длина блока от 1 до 16 байт!');
}
}
}
function writeCmd(data) {
if(cmdCharacteristic) {
len = data.length;
if((len > 0) && (len <= 20)) {
let blk = new Uint8Array(data);
cmdCharacteristic.writeValue(blk);
} else
showError('Ошибка, длина команды от 1 до 20 байт!');
}
}
function readAddress() {
if(cmdCharacteristic) {
let faddr = parseInt($('inpAddr').value, 16);
readAddr(faddr);
}
}
function writeAddress() {
let addr = parseInt($('inpAddr').value, 16);
let data = hexToBytes($('inpData').value);
if(data.length != 0 && data.length <= 16)
writeAddr(addr, data);
else
console.log('Ошибка, длина от 1 до 16 байт!');
}
function sendCommand() {
let data = hexToBytes($('inpCmdData').value);
if(data.length != 0 && data.length <= 20)
writeCmd(data);
else
console.log('Должно быть от 1 до 20 hex байт!');
}
function showConfig() {
/*
if($('inpFlag') != null)
$('inpFlag').value = devCfg.flg; // пока не реализовано
$('inpTxPwr').value = devCfg.tx_power; // 0..0x3f -> -20..+5 dBm нелинейное 0x1f = +0 дБм
$('inpLat').value = (devCfg.connect_latency + 1)*30; // = (connect_latency + 1)*30 ms
$('inpAdvInt').value = devCfg.advertising_interval*62.5; // *62.5 = интервала рекламы в ms
$('inpMeasInt').value = devCfg.measure_interval; // *devCfg.advertising_interval*62.5 = опрос датчика в ms, value минимум = 2 (интервала рекламы)
$('inpAverInt').value = devCfg.averaging_measurements; // запись истории: при 0 - отключена, 1...255 * шаг опроса датчика = интерал записи истории
$('inpBatInt').value = devCfg.batt_interval; // в секундах, минимум 2 секунды, но кратно интервалу рекламы
*/
$('chkCfgNotify').checked = (devCfg.flg & 1) != 0;
$('chkCfgClock').checked = (devCfg.flg & 2) != 0;
$('chkCfgSmiley').checked = (devCfg.flg & 4) != 0;
$('chkCfgTrg').checked = (devCfg.flg & 8) != 0;
let txPwr = 31;
el = $('selTxPwr');
for(let n = 0; n < el.options.length; n++) if(el.options[n].value >= devCfg.tx_power) txPwr = el.options[n].value;
el.value = txPwr;
$('inpLat').value = (devCfg.connect_latency + 1)*30; // = (connect_latency + 1)*30 ms
$('inpAdvInt').value = devCfg.advertising_interval*62.5; // *62.5 = интервала рекламы в ms
$('inpMeasInt').value = devCfg.measure_interval; // *devCfg.advertising_interval*62.5 = опрос датчика в ms, value минимум = 2 (интервала рекламы)
$('inpAverInt').value = devCfg.averaging_measurements; // запись истории: при 0 - отключена, 1...255 * шаг опроса датчика = интерал записи истории
$('inpBatInt').value = devCfg.batt_interval; // в секундах, минимум 2 секунды, но кратно интервалу рекламы
}
function parseBlkCustom(value) {
let len = value.byteLength;
if(len == 0) return;
let blkId = value.getUint8(0);
if((blkId == 0x00) && (len >= 12)) {
// CMD_ID_DEVID Get dev id, version, services
devSrv.revision = value.getUint8(1);
devSrv.hw_version = value.getUint16(2, true);
devSrv.sw_version = value.getUint16(4, true);
devSrv.dev_spec_data = value.getUint16(6, true);
devSrv.services = value.getUint32(8, true);
auxControls(false);
addLog("Device info # hw: " + hex(devSrv.hw_version,4)
+ ", sw: " + hex(devSrv.sw_version,4)
+ ", services: " + hex(devSrv.services,8)
+ ", sd: " + hex(devSrv.dev_spec_data, 4));
} else if(blkId == 0x35){
if(isMemoActive) {
if(len >= 12) {
let cnt = value.getUint16(1, true);
let tc = value.getUint32(3, true);
let tm = value.getInt16(7, true) / 100.0;
let hm = value.getUint16(9, true) / 100.0;
let vb = value.getUint16(11, true);
let dt = new Date(tc*1000);
addLog(((dt.toISOString().slice(0, -1)).replace('T',' ')).replace('.000','')+' # Батарея: '+vb+' мВ , Температура: '+tm+'°C, Влажность: '+hm+'%, Счетчик: '+cnt);
memoCount = cnt;
} else if(len == 3) {
let flg = value.getUint16(1, true);
console.log('Memo End: '+flg);
if(memoCount == 0)
addLog('Пока нет истории!');
isMemoActive = false;
} else if(len == 2) {
addLog('Нет сервиса записи истории!');
isMemoActive = false;
} else {
console.log('blk: ' + dump8(value, value.byteLength));
isMemoActive = false;
}
}
} else if(blkId == 0x33 && len >= 10) {
let count = value.getUint16(1, true);
let temp = value.getInt16(3, true) / 100.0;
let humi = value.getInt16(5, true) / 100.0;
let vbat = value.getUint16(7, true);
let pbat = value.getUint8(9);
let flg = 0;
let rds_count = 0;
let s = 'Температура: ' + temp.toFixed(2) + '°C'
+ ', Влажность: ' + humi.toFixed(2) + '%'
+ ', Vbat: ' + vbat + ' мВ'
+ ', ID: ' + count;
if(len > 12) {
flg = value.getUint8(9);
rds_count = value.getUint32(10, true);
s += ', счетчик срабатываний: '+ rds_count
+ ', флаги: 0x' + hex(flg,2) + ':r' + (flg&1) + '/t' + ((flg>>1)&1);
}
console.log(s);
$('lblTemp').innerHTML = temp + ' °C';
$('lblHumi').innerHTML = humi + ' %RH';
$('lblBat').innerHTML = pbat + ' % (' + vbat / 1000 + ' V)';
if(len > 10) {
flg = value.getUint8(10);
s += ", флаги: 0x" + hex(flg,2);
$('lblTrg').innerHTML = "In: " + (flg&1)
+ ", Out: " + ((flg&2)? 1:0)
+ ", Cmf: " + ((flg&4)? 1:0)
+ ", Te: " + ((flg&16)? 1:0)
+ ", He: " + ((flg&32)? 1:0);
}
console.log(s);
addChartData(temp, humi);
if(isChartEnabled)
chart.updateOptions(
{
file: chartData,
axes: {
y: { 'valueRange': rangeT },
y2: { 'valueRange': rangeH }
}
}
);
getDevTime();
} else if((blkId == 0x25 || blkId == 0x26) && (len > 12)) {
// CMD_ID_CFS Get/Set sensor config
devSens.temp_k = value.getUint32(1, true);
devSens.humi_k = value.getUint32(5, true);
devSens.temp_z = value.getInt16(9, true);
devSens.humi_z = value.getInt16(11, true);
$('inpTempK').value = devSens.temp_k;
$('inpHumK').value = devSens.humi_k;
$('inpTempZ').value = devSens.temp_z;
$('inpHumZ').value = devSens.humi_z;
addLog("Device Sensor "
+ "# Kt: " + devSens.temp_k
+ ", Kh: " + devSens.humi_k
+ ", Zt: " + devSens.temp_z
+ ", Zh: " + devSens.humi_z);
if (len > 17) {
devSens.mid = value.getUint16(13, false);
devSens.vid = value.getUint16(15, false);
devSens.i2c_addr = value.getUint8(17);
s = "???";
if(devSens.mid == 0x5959)
s = "CHT"+hex(devSens.vid,4);
else if(devSens.mid == 0 && devSens.vid == 0xaaaa)
s = "AHT20";
$('lblSensor').innerHTML = s+', I2C адрес: 0x'+hex(devSens.i2c_addr,2);
addLog("Dev sensor # id: "+hex(devSens.mid,4)+"-"+hex(devSens.vid,4)+", i2c_addr: 0x"+hex(devSens.i2c_addr,2));
}
} else if((blkId == 0x55 || blkId == 0x56) && (len > 12)) {
// CMD_ID_CFG Get/Set device config
devCfg.flg = value.getUint32(1, true);
// bit0 - measure notify enable
// bit1 - lcd chow time enable
devCfg.tx_power = value.getUint8(5);
devCfg.advertising_interval = value.getUint8(6);
devCfg.connect_latency = value.getUint8(7);
devCfg.reserved1 = value.getUint8(8);
devCfg.measure_interval = value.getUint8(9);
devCfg.batt_interval = value.getUint8(10);
devCfg.averaging_measurements = value.getUint8(11);
devCfg.reserved2 = value.getUint8(12);
showConfig();
console.log("Dev config # flg: " + hex(devCfg.flg,8)
+ ", tx: " + devCfg.tx_power
+ ", adi: " + devCfg.advertising_interval
+ ", cli: " + devCfg.connect_latency
+" , msi: " + devCfg.measure_interval
+ ", bti: " + devCfg.batt_interval
+ ", avi: " + devCfg.averaging_measurements);
addLog("Строка конфигурации: "+ bytesToHex(value.buffer));
} else if((blkId == 0xdb) && (len > 4)) {
// CMD_ID_MEM_RW Read/Write memory
len -= 4;
let addr = value.getUint32(1,true);
let s = bytesToHex(value.buffer.slice(5), len);
$('inpData').value = s;
addLog("Данные по адресу 0x" + hex(addr,8) + ': ' + s);
showProgress("Считано: " + len + " байт из 0x" + hex(addr,8));
} else if((blkId == 0xda) && (len > 8)) {
let addr = value.getUint32(1,true);
let reg_data = value.getUint32(5,true);
addLog("Регистр по адресу 0x"+ hex(addr,8) + " имеет значение 0x" + hex(reg_data,8));
} else if((blkId == 0x07) && (len > 1)) {
// CMD_ID_SERIAL Get serial string
let s = new TextDecoder("utf-8").decode(value.buffer.slice(1));
addLog("Серийный номер: "+s)
} else if((blkId == 0x06) && (len > 3)) {
// CMD_ID_FLASH_ID Get Flash JEDEC ID
addLog("Flash id: " + bytesToHex(value.buffer.slice(1,4)));
} else if(blkId == 0x60 && len >= 6) {
s = 'LCD data # '+bytesToHex(value.buffer.slice(1));
addLog(s);
} else if(blkId == 0x18 && len >= 1) { // Get/set beacon bkey in EEP
if(len >= 16) {
devkeys.cbindkey = value.buffer.slice(1);
let s = bytesToHex(devkeys.cbindkey,16);
addLog("Bindkey # "+ s);
//if($("cbind_key"))
// $("cbind_key").value = s;
} else {
if(len == 1 && value.getUint8(1) == 0xff)
addLog("Bindkey не запсан!");
else
addAlog("Ошибка чтения Bindkey!");
//if($("cbind_key"))
// $("cbind_key").value = '?';
}
} else if(blkId == 0x23 && len >= 4) {
devTime.cur = value.getUint32(1,true);
console.log('Device Time: 0x' + hex(devTime.cur, 8));
let dt = new Date(devTime.cur * 1000);
let sDateTime = (dt.toISOString().slice(0, -5)).replace('T',' ');
$('lblTime').innerHTML = sDateTime
let time = Date.now() / 1000 - (new Date()).getTimezoneOffset() * 60;
let delta = devTime.cur - time;
console.log('Время на устройстве: ' + sDateTime + ', Уход: ' + Math.round(delta) + ' сек');
if(len >= 8) {
devTime.set = value.getUint32(5,true);
console.log('Последняя установка времени: 0x' + hex(devTime.set,8));
if(devTime.step == 1) {
if(devTime.set > 0x60000000) {
devTime.period = devTime.cur - devTime.set;
devTime.cmp = time;
let odt = new Date(devTime.set * 1000);
addLog('Последняя установка времени: '+(odt.toISOString().slice(0, -5)).replace('T',' '));
console.log('Прошло на устройстве: ' + devTime.period.toFixed(1) + ' секунд');
let rp = devTime.cmp - devTime.set;
let delta = rp - devTime.period;
console.log('Прошло реально: ' + rp.toFixed(1) + ' секунд, разница: ' + delta.toFixed(1) + ' секунд');
if(rp >= 10800) { // 10800
devTime.step = 2;
console.log("Send cmd Get StepTimeSec...");
settingsCharacteristics.writeValue(new Uint8Array([0x24])).then(_ => {
console.log('Get StepTimeSec...');
});
} else {
devTime.step = 0;
addLog('Минимальный перод для расчета ухода часов 3 часа!');
}
} else {
devTime.step = 0;
addLog('Часы необходимо настроить заранее!');
}
}
}
} else if((blkId == 0x44) && (len > 8)) {
if(len != 9) {
devTrig.temp_min = value.getInt16(1, true);
devTrig.temp_max = value.getInt16(3, true);
devTrig.humi_min = value.getInt16(5, true);
devTrig.humi_max = value.getInt16(7, true);
if ( devSrv.services & SERVICE_SCREEN ) {
$("inpTempMin").value = (devTrig.temp_min/100.0).toFixed(2);
$("inpTempMax").value = (devTrig.temp_max/100.0).toFixed(2);
$("inpHumiMin").value = (devTrig.humi_min/100.0).toFixed(2);
$("inpHumiMax").value = (devTrig.humi_max/100.0).toFixed(2);
}
}
let x = 0;
if(len == 10) x = 1;
else if(len >= 18) x = 9;
if(x > 0) {
devTrig.temp_threshold = value.getInt16(x, true);
devTrig.humi_threshold = value.getInt16(x+2, true);
devTrig.temp_hysteresis = value.getInt16(x+4, true);
devTrig.humi_hysteresis = value.getInt16(x+6, true);
devTrig.flg = value.getUint8(x+8);
if( devSrv.services & SERVICE_TH_TRG ) { //
$("chkInvOut").checked = (devTrig.flg & 1) != 0;
$("inpTempThr").value = (devTrig.temp_threshold/100.0).toFixed(2);
$("inpHumiThr").value = (devTrig.humi_threshold/100.0).toFixed(2);
$("inpTempHsr").value = (devTrig.temp_hysteresis/100.0).toFixed(2);
$("inpHumiHsr").value = (devTrig.humi_hysteresis/100.0).toFixed(2);
}
}
addLog("Строка настроек триггера: "+ bytesToHex(value.buffer));
} else if(blkId == 0x20 && len >= 8) {
devcmf.tmp_lo = value.getInt16(1, true); // temp lo
devcmf.tmp_hi = value.getInt16(3, true); // temp hi
devcmf.hm_lo = value.getUint16(5, true); // humi lo
devcmf.hm_hi = value.getUint16(7, true); // humi lo
let s = 'Комфорт Температура: ' + (devcmf.tmp_lo/100.0).toFixed(2) + '..' + (devcmf.tmp_hi/100.0).toFixed(2) + '°C'
+ ', Влажность: ' + (devcmf.hm_lo/100.0).toFixed(2) + '..' + (devcmf.hm_hi/100.0).toFixed(2) + '%';
addLog(s); // UpdCmf();
} else if(blkId == 0x01 && len >= 1) {
devName = TextDecoder("utf-8").decode(value.buffer.slice(1));
$("inpDevName").value = devName;
addLog("Имя устройства: '" + devName + "'");
} else if(blkId == 0x10 && len > 6) {
let mac = new Uint8Array(6);
mac[0] = value.getUint8(6);
mac[1] = value.getUint8(5);
mac[2] = value.getUint8(4);
mac[3] = value.getUint8(3);
mac[4] = value.getUint8(2);
mac[5] = value.getUint8(1);
let mac_txt = bytesToHex(mac);
$("inpDevMAC").value = mac_txt;
addLog("MAC устройства: "+ mac_txt);
} else {
console.log('blk: ' + dump8(value, value.byteLength));
addLog('Ответ на команду (' + hex(blkId,2) + '): ' + bytesToHex(value.buffer.slice(1)));
}
}
function getMemo(num) {
if(cmdCharacteristic != null) {
//addLog("getSensCfg...");
isMemoActive = true;
memoCount = 0;
cmdCharacteristic.writeValue(new Uint8Array([0x35, num&0xff, (num>>8)&0xff])).catch(error => { console.log(error); addLog("getMemo() error!"); });
}
}
function getDevCfg() {
if(cmdCharacteristic != null) {
//addLog("getDevCfg...");
cmdCharacteristic.writeValue(new Uint8Array([0x55])).catch(error => { console.log(error); addLog("getDevCfg() error!") });
}
}
function getSensCfg() {
if(cmdCharacteristic != null) {
//addLog("getSensCfg...");
cmdCharacteristic.writeValue(new Uint8Array([0x25])).catch(error => { console.log(error); addLog("getSensCfg() error!"); });
}
}
function getDevTime() {
if(cmdCharacteristic != null) {
// addLog("Получить время от устройства...");
cmdCharacteristic.writeValue(new Uint8Array([0x23])).then(_ => { console.log('Send GetDevTime ok'); });
}
}
function getDevMAC() {
if(cmdCharacteristic != null) {
cmdCharacteristic.writeValue(new Uint8Array([0x10])).catch(error => { console.log(error); addLog("getMAC() error!"); });
}
}
function getTrgCfg() {
if(cmdCharacteristic != null) {
cmdCharacteristic.writeValue(new Uint8Array([0x44])).catch(error => { console.log(error); addLog("getTrgCfg() error!"); });
}
}
function getDevName() {
if(cmdCharacteristic != null) {
cmdCharacteristic.writeValue(new Uint8Array([0x01])).catch(error => { console.log(error); addLog("getDevName() error!"); });
}
}
function setDevCfg() {
if(cmdCharacteristic != null) {
// addLog("setDevCfg...");
devCfg.flg = parseInt($('inpFlag').value)&0xffffffff; // пока не реализовано
devCfg.tx_power = parseInt($('inpTxPwr').value)&0x3f; // 0..0x3f -> -20..+5 dBm нелинейное 0x1f = +0 дБм
devCfg.flg = ($('chkCfgNotify').checked) ? 1 : 0;
devCfg.flg |= ($('chkCfgClock').checked) ? 2 : 0;
devCfg.flg |= ($('chkCfgSmiley').checked) ? 4 : 0;
devCfg.flg |= ($('chkCfgTrg').checked) ? 8 : 0;
devCfg.tx_power = $('selTxPwr').value & 0x3f; // 0..0x1f -> -20..+5 dBm ? нелинейное 0x1f = +0 дБм
let connect_latency = parseInt($('inpLat').value); // = (connect_latency + 1)*30 ms
if (connect_latency < 0) {
connect_latency = 0;
} else if (connect_latency <= 7680) {
connect_latency = (connect_latency/30) - 1;
} else
connect_latency = 255;
devCfg.connect_latency = connect_latency & 0xff;
let advertising_interval = parseInt($('inpAdvInt').value);
if(advertising_interval <= 62.5)
advertising_interval = 1;
else if(advertising_interval >= 15937.5)
advertising_interval = 255;
else
advertising_interval = advertising_interval / 62.5;
devCfg.advertising_interval = advertising_interval & 0xff;
devCfg.measure_interval = parseInt($('inpMeasInt').value);
if(devCfg.measure_interval < 2) // опрос датчика в интервалах рекламы, value минимум = 2 (интервала рекламы)
devCfg.measure_interval = 2;
else if(devCfg.measure_interval > 255)
devCfg.measure_interval = 255
devCfg.averaging_measurements = parseInt($('inpAverInt').value);
if(devCfg.averaging_measurements < 0) // запись истории: при 0 - отключена, 1...255 * шаг опроса датчика = интерал записи истории
devCfg.averaging_measurements = 0;
if(devCfg.averaging_measurements > 255)
devCfg.averaging_measurements = 255;
devCfg.batt_interval = parseInt($('inpBatInt').value);
if(devCfg.batt_interval < 2) // в секундах, минимум 2 секунды, но кратно интервалу рекламы
devCfg.batt_interval = 2;
if(devCfg.batt_interval > 255)
devCfg.batt_interval = 255;
blk = new Uint8Array([0x55,
devCfg.flg & 0xff, (devCfg.flg>>8) & 0xff, (devCfg.flg>>16) & 0xff, (devCfg.flg>>24) & 0xff,
devCfg.tx_power,
devCfg.advertising_interval,
devCfg.connect_latency,
devCfg.reserved1,
devCfg.measure_interval,
devCfg.batt_interval,
devCfg.averaging_measurements,
devCfg.reserved2
]);
cmdCharacteristic.writeValue(blk).catch(error => { console.log(error); addLog("setDevCfg() error!");});
}
}
function setSensCfg() {
if(cmdCharacteristic != null) {
addLog("setSensCfg...");
devSens.temp_k = parseInt($('inpTempK').value);
devSens.humi_k = parseInt($('inpHumK').value);
devSens.temp_z = parseInt($('inpTempZ').value);
devSens.humi_z = parseInt($('inpHumZ').value);
blk = new Uint8Array([0x55,
devSens.temp_k & 0xff, (devSens.temp_k >> 8) & 0xff, (devSens.temp_k >> 16) & 0xff, (devSens.temp_k >> 24) & 0xff,
devSens.humi_k & 0xff, (devSens.humi_k >> 8) & 0xff, (devSens.humi_k >> 16) & 0xff, (devSens.humi_k >> 24) & 0xff,
devSens.temp_z & 0xff, (devSens.temp_z >> 8) & 0xff,
devSens.humi_z & 0xff, (devSens.humi_z >> 8) & 0xff
]);
cmdCharacteristic.writeValue(new Uint8Array([0x25])).catch(error => { console.log(error); addLog("setSensCfg() error!"); });
}
}
function setDevTime() {
let time = Date.now()/1000;
time -= (new Date()).getTimezoneOffset() * 60;
blk = new Uint8Array(5);
blk[0] = 0x23;
blk[1] = time & 0xff;
blk[2] = (time >> 8) & 0xff;
blk[3] = (time >> 16) & 0xff;
blk[4] = (time >> 24) & 0xff;
addLog("Установка времени на устройстве (" + dump(blk, blk.length) + ")...");
cmdCharacteristic.writeValue(blk).then(_ => {
console.log('Время на устройстве синхронизировано');
});
}
function setDevMAC() {
if(cmdCharacteristic != null) {
let el = $("inpDevMAC").value;
let len = el.length;
if(len == 12) {
let mac = hexToBytes(el);
len = mac.length;
if(len == 6 || len == 8) {
let blk = new Uint8Array(len+2);
blk[0] = 0x10;
blk[1] = mac[5];
blk[2] = mac[4];
blk[3] = mac[3];
blk[4] = mac[2];
blk[5] = mac[1];
blk[6] = mac[0];
if(len == 8) {
blk[7] = mac[7];
blk[8] = mac[6];
}
console.log("Send cmd New MAC ("+dump(blk, blk.length)+")...");
cmdCharacteristic.writeValue(blk).then(_ => {
s = "Передача нового MAC: "+dump(mac, 6);
if(len == 8)
s += " RAND:" +dump(mac.slice(6), 2);
addLog(s+" ok");
});
return;
}
}
addLog("Строка MAC должна быть 6 байт в HEX виде!")
return;
}
}
function setTrgCfg() {
if(cmdCharacteristic != null) {
let len = 0;
if((devSrv.services & 0x00000020) != 0) len = 8;
if((devSrv.services & 0x00004000) != 0) len += 9;
if(len > 0) {
let blk = new Uint8Array(len + 1);
blk[0] = 0x44;
let idx = 1;
if(len != 9) {
devTrig.temp_min = Math.round($("inpTempMin").value * 100);
devTrig.temp_max = Math.round($("inpTempMax").value * 100);
devTrig.humi_min = Math.round($("inpHumiMin").value * 100);
devTrig.humi_max = Math.round($("inpHumiMax").value * 100);
blk[1] = devTrig.temp_min & 0xff;
blk[2] = (devTrig.temp_min >> 8) & 0xff;
blk[3] = devTrig.temp_max & 0xff;
blk[4] = (devTrig.temp_max >> 8) & 0xff;
blk[5] = devTrig.humi_min & 0xff;
blk[6] = (devTrig.humi_min >> 8) & 0xff;
blk[7] = devTrig.humi_max & 0xff;
blk[8] = (devTrig.humi_max >> 8) & 0xff;
idx = 9;
}
if(len > 8) {
devTrig.temp_threshold = Math.round($("inpTempThr").value * 100);
devTrig.humi_threshold = Math.round($("inpHumiThr").value * 100);
devTrig.temp_hysteresis = Math.round($("inpTempHsr").value * 100);
devTrig.humi_hysteresis = Math.round($("inpHumiHsr").value * 100);
devTrig.flg = ($("chkInvOut").checked)? 1: 0;
blk[idx+0] = devTrig.temp_threshold & 0xff;
blk[idx+1] = (devTrig.temp_threshold >> 8) & 0xff;
blk[idx+2] = devTrig.humi_threshold & 0xff;
blk[idx+3] = (devTrig.humi_threshold >> 8) & 0xff;
blk[idx+4] = devTrig.temp_hysteresis & 0xff;
blk[idx+5] = (devTrig.temp_hysteresis >> 8) & 0xff;
blk[idx+6] = devTrig.humi_hysteresis & 0xff;
blk[idx+7] = (devTrig.humi_hysteresis >> 8) & 0xff;
blk[idx+8] = devTrig.flg & 0xff;
}
cmdCharacteristic.writeValue(blk).catch(error => { console.log(error); addLog("setTrgCfg() error!"); });
}
}
}
function setDevName(flg) {
if(cmdCharacteristic != null) {
if(flg) {
let eltxt = $("inpDevName").value;
let len = eltxt.length;
if(len > 1 && len < 20) {
let blk = new Uint8Array(1 + len);
blk[0] = 0x01;
blk.set((new TextEncoder()).encode(eltxt), 1);
console.log(blk);
cmdCharacteristic.writeValue(blk).catch(error => { console.log(error); addLog("setDevName() error!"); });
} else
addLog("Имя устройства должно быть от 1 до 19 символов!");
} else
cmdCharacteristic.writeValue(new Uint8Array([0x01, 0])).catch(error => { console.log(error); addLog("setDevName() error!"); });
}
}
function resetDevCfg() {
if(cmdCharacteristic != null) {
addLog("Restore connection default settings...");
cmdCharacteristic.writeValue(new Uint8Array([0x56])).catch(error => { console.log(error); addLog("resetDevCfg() error!"); });
}
}
function resetSensCfg() {
if(cmdCharacteristic != null) {
addLog("Restore sensor default settings...");
cmdCharacteristic.writeValue(new Uint8Array([0x26])).catch(error => { console.log(error); addLog("resetSensCfg() error!"); });
}
}
function readFile(file) {
var reader = new FileReader();
if (file != null) {
reader.fname = file.name;
reader.readAsArrayBuffer(file);
} else {
$('lblFile').innerHTML = "не загружен";
showError("Файл не загружен");
}
reader.onload = function() {
getFwArray(this.result, this.fname);
if (ota.fwsize > 0) {
// regex = /[^\\]+$/;
// text = fname.match(regex);
$('lblFile').innerHTML = this.fname;
}
}
reader.onerror = function() {
console.log(this.error);
};
}
// var url;
function download(data, path, type) {
var file = new Blob([data], {type: type} );
if (window.navigator.msSaveOrOpenBlob) { // ie10+
window.navigator.msSaveOrOpenBlob(file, path);
} else { // ff, chrome
url = URL.createObjectURL(file);
let link = document.createElement("a");
link.href = path;
link.download = url;
document.body.appendChild(link);
link.click();
setTimeout( function() {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, 0);
URL.revokeObjectURL(url);
}
}
/*
// BUG: Access-Control-Allow-Origin
function download(url, filename) {
fetch(url)
.then(response => response.blob())
.then(blob => {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
})
.catch(console.error);
}
download("https://get.geojs.io/v1/ip/geo.json","geoip.json")
*/
/*
var getFile = new selectFile;
getFile.targets('inpFile','lblFile');
function uploadFile() {
if ( isEmpty($("inpUrl").value) ) {
getFile.simulate()
} else {
let regex = /[^\\]+$/;
// match(regex);
//download(, $("inpUrl").value, "application/octet-stream");
download($("inpUrl").value, "OTA.bin");
// readFile(file);
}
}
*/
function readFlash() {
// if "OK"
$('btnSaveFlash').disabled = false;
}
var chart = null;
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
$(tabName).style.display = "block";
if (!isConnected) $('tabConfig').style.display = "none";
if (evt != 0) {
evt.currentTarget.className += " active";
// console.log(evt.currentTarget.className);
}
if (tabName == 'tabCharts') {
if ((chartData.length > 0) && (chart != null))
chart.updateOptions({'file': chartData});
resizeChart();
isChartEnabled = true;
} else {
isChartEnabled = false;
}
}
function selectConfigTab() {
openTab(0, "tabConfig");
tablinks = document.getElementsByClassName("tablinks");
tablinks[0].className += " active";
}
// Буфер входящих данных
var chart = null;
var chartData = [];
var rslT = 1;
var rslH = 1;
var rangeT = [15, 25]; // y: { valueRange: [15, 25] },
var rangeH = [25, 85]; // y2: { valueRange: [25, 85] }
const arrayColumn = (arr, n) => arr.map(x => x[n]);
function addChartData(temp, humi) {
let dt = new Date();
chartData.push([dt, temp, humi]);
if(chartData.length >= 1000) chartData.shift();
//console.log(arrayColumn(a, 1));
//console.log(Math.min(...arrayColumn(a, 1)));
//console.log(Math.max(...arrayColumn(a, 1)));
let yMin = Math.min(...arrayColumn(chartData, 1));
let yMax = Math.max(...arrayColumn(chartData, 1));
let xMin = Math.floor(yMin / rslT);
let xMax = Math.floor(yMax / rslT) + 1;
rangeT = [xMin*rslT, xMax*rslT];
yMin = Math.min(...arrayColumn(chartData, 2));
yMax = Math.max(...arrayColumn(chartData, 2));
xMin = Math.floor(yMin / rslH);
xMax = Math.floor(yMax / rslH) + 1;
rangeH = [xMin*rslH, xMax*rslH];
//console.log(rangeT, rangeH);
/*********************************
if (chartData.length < 3) return;
if (chart == null) {
chart = new Dygraph(
$("divChart"),
"ny-vs-sf.txt",
{
rollPeriod: 14,
showRoller: true,
customBars: true,
title: 'NYC vs. SF',
ylabel: 'Temperature (F)',
legend: 'always'
}
);
*******************************/
/******************************
chart = new Dygraph(
$("divDygraph"),
"ny-vs-sf.txt", // chartData,
{
//title: "Live data",
//showRangeSelector: true,
//showRoller: true,
//rollPeriod: 1,
//xlabel: 'Time(sec)',
//ylabel: 'Temp(C&deg;)',
//y2label: 'RH(%)',
colors: ['green', 'blue'],
series : { 'RH(%)': { axis: 'y2' } },
labels: ['T(сек)', 'T(°C)', 'RH(%)'],
//divLabels: $('divLabels'),
legend: 'always', // "follow"
digitsAfterDecimal: 3,
rollPeriod: 14,
showRoller: true,
customBars: true,
title: 'NYC vs. SF',
ylabel: 'Temperature (F)',
legend: 'always'
}
);
******************************/
//chart.updateOptions({'file': chartData})
}
function resizeChart() {
if (chart == null) return;
// console.log('resize однако');
var element = $('divChart');
var positionInfo = element.getBoundingClientRect();
// console.log(positionInfo);
// var width = positionInfo.width;
// var height = positionInfo.height;
var width = $('tabCharts').clientWidth;
var height = $('tabCharts').clientHeight;
// console.log(width, height);
if (height < (320 + 100))
height = 320;
else
height -= 100;
if (width < (480 + 40))
width = 480;
else
width -= 40;
chart.resize(width, height);
// console.log(width, height);
}
window.onload = function() {
document.querySelector("#inpFile").addEventListener("change", function() {
var file = this.files[0];
if (file != null) {
text = "не загружен";
readFile(file);
} else {
$('lblFile').innerHTML = "не выбран";
}
}, false);
showState("Не подключено");
disableControls(true);
selectConfigTab();
chart = new Dygraph(
$('divChart'),
[],
{
//title: "Live data",
//showRangeSelector: true,
//showRoller: true,
//rollPeriod: 1,
//xlabel: 'Time(sec)',
//ylabel: 'Temp(C&deg;)',
//y2label: 'RH(%)',
/******************************/
axes: {
y: { valueRange: rangeT },
y2: { valueRange: rangeH }
},
/******************************/
colors: ['green', 'blue'],
series : { 'RH(%)': { axis: 'y2' } },
labels: ['T(сек)', 'T(°C)', 'RH(%)'],
//labelsDiv: $('labelsDiv'),
legend: 'always', // "follow"
digitsAfterDecimal: 2,
}
);
/******************************************
// debug
// let a = [[1, 21, 81], [2, 20, 79], [3, 19, 83]];
// addChartData(21, 81);
// addChartData(20, 79);
// addChartData(19, 83);
for (i = 0; i < 128; i++) {
let t = 20 + (1 - Math.random() * 2);
let h = 70 + (5 - Math.random() * 10);
chartData.push([i, t, h]);
//addChartData(t, h);
}
chart.updateOptions(
{
file: chartData,
axes: {
y: { 'valueRange': rangeT },
y2: { 'valueRange': rangeH }
}
}
);
// chart.updateOptions({'file': chartData});
******************************************/
addEventListener("resize", resizeChart);
};
</script>
<h2>PHY62x2 BTHome <a href="https://github.com/pvvx/THB2"><small>&#9432;</small></a></h2>
<label for="inpNamePrefix">Префикс имени устройств(а)</label>
<input type="text" id="inpNamePrefix" value="" placeholder="THB, BT, TH">
<br>
<button type="button" id="btnConnect" onclick="connect()">Соединение</button>
<button type="button" id="btnDisconnect" onclick="disconnect()" disabled="true">Отключение</button>
<button type="button" id="btnReconnect" onclick="reconnect()" disabled="true">Переподключение</button>
<hr id="hrSensorData" hidden>
<table id="tblSensorData" hidden>
<tr>
<th>Время</th>
<th>Температура</th>
<th>Влажность</th>
<th>Состояние батареи</th>
</tr>
<tr align="center">
<td><label id="lblTime">---/--/-- --:--:--</label></td>
<td><label id="lblTemp">-- °C</label></td>
<td><label id="lblHumi">-- %RH</label></td>
<td><label id="lblBat">-- % (-- V)</label></td>
<td><label id="lblTrg"></label></td>
</tr>
</table>
<hr>
<div class="tab">
<button class="tablinks" onclick="openTab(event, 'tabConfig')">Config</button>
<button class="tablinks" onclick="openTab(event, 'tabOTA')">OTA</button>
<button class="tablinks" onclick="openTab(event, 'tabCharts')">Chart</button>
<button class="tablinks" onclick="openTab(event, 'tabService')">Service</button>
</div>
<div id="tabConfig" class="tabcontent">
<table id="tblChkCfg">
<tr align="left">
<td><label><input type="checkbox" id="chkCfgClock"/>Показывать время</label></td>
<td><label><input type="checkbox" id="chkCfgSmiley"/>Отображать смайлики</label></td>
<td><label><input type="checkbox" id="chkCfgTrg"/>Отображать триггер</label></td>
</tr>
</table>
<table id="tblComfort">
<tr>
<td style="width:75px;"><b>Комфорт</b></td>
<td>Температура от <input size="4" type="text" id="inpTempMin" maxlength="8" title="Минимальная температура">
до <input size="4" type="text" id="inpTempMax" maxlength="8" title="Максимальная температура"> °C,</td>
<td>Влажность от <input size="4" type="text" id="inpHumiMin" maxlength="8" title="Минимальная влажность">
до <input size="4" type="text" id="inpHumiMax" maxlength="8" title="Максимальная влажность"> %RH</td>
</tr>
</table>
<table id="tblTrigger">
<tr>
<td style="width:75px;"><b>Триггер</b></td>
<td>Температура: <input size="4" type="text" id="inpTempThr" maxlength="8" title="Порог температуры"> °C, </td>
<td>Влажность: <input size="4" type="text" id="inpHumiThr" maxlength="8" title="Порог влажности"> %RH, </td>
<td>Гистерезис: <input size="4" type="text" id="inpTempHsr" maxlength="8" title="Гистерезис температуры, если =0 - обработка отключена, если отрицательный - выход включен при температуре менее заданной (нагреватель)"> °C</td>
<td><input size="4" type="text" id="inpHumiHsr" maxlength="8" title="Гистерезис влажности, если =0 - обработка отключена, если отрицательный - выход включен при влажности менее заданной (увлажнитель)"> %RH, </td>
<td><label><input type="checkbox" id="chkInvOut"/>Инверсия вывода</label></td>
</tr>
</table>
<button type="button"id="btnGetTrg" onclick="getTrgCfg()">Прочитать</button>
<button type="button"id="btnSetTrg" onclick="setTrgCfg()">Записать</button>
<hr id="hrPres">
<button type="button" id="btnGetDevTime" onclick="getDevTime()">Время устройства</button>
<button type="button" id="btnSetDevTime" onclick="setDevTime()">Установить время</button>
<hr>
Параметры связи
<table>
<tr>
<th>Send Notification</th>
<th>Tx Power</th>
<th>Conn. Latency</th>
</tr>
<tr align="center">
<td><label><input type="checkbox" id="chkCfgNotify"/>Измерения</label></td>
<td>
<select id="selTxPwr">
<option value="63">+5 дБм</option>
<option value="31" selected>+0 дБм</option>
<option value="23">-5 дБм</option>
<option value="15">-10 дБм</option>
<option value="7">-15 дБм</option>
<option value="0">-20 дБм</option>
</select>
</td>
<td><input size="4" type="text" id="inpLat" maxlength="4" title="Connect latency, Итоговый интервал = 30 * (Connect latency + 1) мс"></td>
</tr>
</table>
Интервалы
<table>
<tr>
<th>Реклама</th>
<th>Измерения</th>
<th>Усреднение</th>
<th>Уровень батареи</th>
</tr>
<tr align="center">
<td><!--- надо отдельно, на ходу, при изменении, выводить расчетное значение inpAdvInt*62.5 в ms --->
<input size="4" type="text" id="inpAdvInt" maxlength="4" title="Интервал BLE рекламы в мс, кратность 62.5 мс"></td>
<td><!--- надо отдельно выводить расчетное значение inpAdvInt*62.5*inpMeasInt в ms --->
<input size="4" type="text" id="inpMeasInt" maxlength="4" title="Опрос датчика в интервалах BLE рекламы, минимум 2 интервала рекламы"></td>
<td><!--- надо отдельно выводить расчетное значение inpAdvInt*62.5*inpMeasInt*inpAverInt в мс, сек --->
<input size="4" type="text" id="inpAverInt" maxlength="4" title="Запись истории: 0 - отключена, 1...255 * шаг опроса датчика = интервал записи истории"></td>
<td>
<input size="4" type="text" id="inpBatInt" maxlength="4" title="В секундах, обрабатывается кратно интервалу BLE рекламы, минимум 2 секунды"></td>
</tr>
</table>
<button type="button"id="btnGetDev" onclick="getDevCfg()">Прочитать</button>
<button type="button"id="btnSetDev" onclick="setDevCfg()">Записать</button>
<button type="button"id="btnRstDev" onclick="resetDevCfg()" title="Восстановить стандартные параметры связи">Восстановить</button>
<hr>
Параметры сенсора&emsp;<b><label id="lblSensor">?</label></b>
<table>
<tr>
<th></th>
<th>Температура</th>
<th>Влажность</th>
</tr>
<tr>
<th></th>
</tr>
<tr>
<td>Поправочные коэффициенты</td>
<td align="center"><input size="8" type="text" id="inpTempK" maxlength="8" title="*0.01 = Коэффициент угла наклона (линейная функция) для расчета температуры"></td>
<td align="center"><input size="8" type="text" id="inpHumK" maxlength="8" title="*0.01 = Коэффициент угла наклона (линейная функция) для расчета влажности"></td>
</tr>
<tr>
<td>Коррекция смещения</td>
<td><input size="8" type="text" id="inpTempZ" maxlength="8" title="*0.01C = Смещение нуля с поправочным значением для расчета температуры"></td>
<td><input size="8" type="text" id="inpHumZ" maxlength="8" title="*0.01% = Смещение нуля с поправочным значением для расчета влажности"></td>
</tr>
</table>
<br>
<button type="button"id="btnGetSens" onclick="getSensCfg()">Прочитать</button>
<button type="button"id="btnSetSens" onclick="setSensCfg()">Записать</button>
<button type="button"id="btnRstSens" onclick="resetSensCfg()" title="Восстановить стандартные параметры сенсора">Восстановить</button>
</div>
<div id="tabOTA" class="tabcontent">
<p>Файл прошивки:
<!--input type="file" hidden accept=".bin" id="inpFile"-->
<input type="file" hidden accept=".bin,application/octet-stream" id="inpFile">
<label id="lblFile">не выбран</label>
<button type="button" onClick="$('inpFile').click()">Выбрать</button>
<div hidden>URL: <input type="text" id="inpUrl"></div>
<p>
<hr>
<button type="button" id="btnStartDFU" onclick="startDFU()" disabled="true" >Старт</button>
</div>
<div id="tabCharts" class="tabcontent">
<div id="divChart"></div>
<!--div id="divLabels"></div-->
<p><a href="GraphMemo.html" target="_blank">Memo Chart</a> - График истории замеров</p>
</div>
<div id="tabService" class="tabcontent">
Чтение и запись памяти:<br>
<table>
<tr>
<td style="width:100px;">Адрес (hex):</td>
<td><button type="button" id="btnReadAddr" style="width:120px;" onclick="readAddress()">Прочитать</button></td>
<td>Данные (hex):</td>
</tr>
<tr>
<td><input size="8" type="text" id="inpAddr" value="11000000" maxlength="8"></td>
<td><button type="button" id="btnWriteAddr" onclick="writeAddress()" style="width:120px;">Записать</button></td>
<td><input size="32" type="text" id="inpData" value="?" maxlength="32"></td>
</tr>
</table>
<hr>
<table>
<tr>
<td style="width:100px;">Команда:</td>
<td><button type="button" id="btnSendCommand" onclick="sendCommand()" style="width:120px;">Выполнить</button></td>
<td><input type="text" id="inpCmdData" value="55" maxlength="16"></td>
</tr>
</table>
<hr>
<table>
<tr>
<td style="width:100px;">MAC Адрес:</td>
<td><button type="button" id="btnGetMAC" onclick="getDevMAC()">Прочитать</button></td>
<td><input type="text" id="inpDevMAC" value="?" maxlength="16"></td>
<td><button type="button" id="btnSetMAC" onclick="setDevMAC()">Записать</button></td>
</tr>
</table>
<hr>
<table>
<tr>
<td style="width:100px;">Имя устройства:</td>
<td><button type="button" id="btnGetName" onclick="getDevName()">Прочитать</button></td>
<td><input type="text" id="inpDevName" value="" maxlength="19"></td>
<td><button type="button" id="btnSetName" onclick="setDevName(true)">Записать</button></td>
<td><button type="button" id="btnRstName" onclick="setDevName(false)">По умолчанию</button></td>
</tr>
</table>
</div>
<hr>
<button type="button" onclick="clearLog()">Очистить лог</button>
<label id="lblStatus"></label>
<br>
<div id="log"></div>
</body>
</html>