THB2/bthome_phy6222/web/PHY62x2BTHome.html
2024-01-27 21:26:47 +03:00

1403 lines
42 KiB
HTML
Raw 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">
<style type="text/css">
/* basic sytles */
body {
font-family: Arial, 'Open Sans', sans-serif;
color: #204056;
}
h1 {
font-size: 28px;
font-weight: 400;
text-align: center;
margin-top: 12px;
margin-bottom: 18px;
}
hr {
height: 10px;
border: 0;
box-shadow: 0 10px 10px -10px #8c8b8b inset;
}
span#info {
font-style: italic;
}
.button, [type='button'] {
background-color: #1a73e8;
border: none;
border-radius: 4px;
color: white;
padding: 8px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 6px 6px;
cursor: pointer;
box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f;
}
.button, [type='button'].ok {
background-color: #4CAF50; /* Green */
color: white;
border-color: #4CAF50;
}
.button, [type='button'].danger {
background-color: #f44336; /* Red */
color: white;
border-color: #f44336;
}
.button, [type='button']:disabled {
cursor: not-allowed;
opacity: 0.6;
}
input[type="checkbox"] + label {
margin-right: 8px;
}
div#div_v {
height:400px;
margin-top: 16px;
margin-bottom: 16px;
}
div#labdiv {
margin-top: 16px;
margin-bottom: 16px;
}
div#log {
padding: 12px;
font-style: italic;
font-size: 16px;
}
div#MAC {
font-style: smaller;
margin: 8px;
}
div#txtStatus {
font-style: italic;
font-size: 16px;
text-align: center;
background-color: #eef6fc;
padding-top: 5px;
padding-bottom: 5px;
margin-top: 8px;
}
div#tempHumiData{
text-align: center;
background-color: #eef6fc;
padding-top: 5px;
padding-bottom: 5px;
}
input {
padding: 4px;
margin: 4px;
}
select {
padding: 4px;
}
/* menu */
.navbar {
width: 95%;
/* box-shadow: 0 1px 4px rgb(146 161 176 / 15%);*/
position: absolute;
top: 0;
}
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 62px;
}
.navbar .menu-items {
display: flex;
}
.navbar .nav-container li {
list-style: none;
}
.navbar .nav-container a {
font-size: 1.0rem;
font-weight: 400;
}
.navbar .nav-container a:hover{
font-weight: bolder;
}
.nav-container {
display: block;
position: relative;
height: 60px;
}
.nav-container .checkbox {
position: absolute;
display: block;
height: 32px;
width: 32px;
top: 20px;
left: 20px;
z-index: 5;
opacity: 0;
cursor: pointer;
}
.nav-container .hamburger-lines {
display: block;
height: 26px;
width: 32px;
position: absolute;
top: 17px;
left: 20px;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.nav-container .hamburger-lines .line {
display: block;
height: 4px;
width: 100%;
border-radius: 10px;
background: #0e2431;
}
.nav-container .hamburger-lines .line1 {
transform-origin: 0% 0%;
transition: transform 0.4s ease-in-out;
}
.nav-container .hamburger-lines .line2 {
transition: transform 0.2s ease-in-out;
}
.nav-container .hamburger-lines .line3 {
transform-origin: 0% 100%;
transition: transform 0.4s ease-in-out;
}
.navbar .menu-items {
position: relative;
padding-top: 55px;
box-shadow: 5px 3px 13px 0px rgb(204 204 204 / 80%);
min-height: 100vh;
width: 60%;
transform: translate(-150%);
display: flex;
flex-direction: column;
transition: transform 0.5s ease-in-out;
text-align: center;
z-index: 1;
background: white;
}
.navbar .menu-items li {
margin-bottom: 12px;
font-size: 1.2rem;
font-weight: 800;
}
.nav-container input[type="checkbox"]:checked ~ .menu-items {
transform: translateX(0);
}
.nav-container input[type="checkbox"]:checked ~ .hamburger-lines .line1 {
transform: rotate(45deg);
}
.nav-container input[type="checkbox"]:checked ~ .hamburger-lines .line2 {
transform: scaleY(0);
}
.nav-container input[type="checkbox"]:checked ~ .hamburger-lines .line3 {
transform: rotate(-45deg);
}
.nav-container input[type="checkbox"]:checked ~ .logo{
display: none;
}
.shadowbox {
width: 15em;
border: 1px solid #333;
box-shadow: 8px 8px 5px #444;
padding: 8px 12px;
background-image: linear-gradient(180deg, #fff, #ddd 40%, #ccc);
}
.shadowprogress {
width: 15em;
border: 1px solid #333;
/* box-shadow: 8px 8px 5px #444; */
padding: 8px 12px;
/* background-image: linear-gradient(180deg, #fff, #ddd 40%, #ccc); */
}
.shadowerror {
color: red;
width: 15em;
border: 1px solid #333;
box-shadow: 8px 8px 5px #444;
padding: 8px 12px;
background-image: linear-gradient(180deg, #fff, #ddd 40%, #ccc);
}
/* Style the tab */
.tab {
overflow: hidden;
border: 1px solid #ccc;
background-color: #f1f1f1;
}
/* Style the buttons inside the tab */
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
font-size: 17px;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
}
/* Create an active/current tablink class */
.tab button.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
display: none;
padding: 6px 12px;
border: 1px solid #ccc;
border-top: none;
}
</style>
<title>PHY62x2-BTHome v0.2</title>
</head>
<body>
<script>
//BLE values
const FLASH_SIZE = 0x80000;
var bluetoothDevice, gattServer, otaCharacteristic, cmdCharacteristic, infoService;
var devinfo = {};
var devsrs = {};
var devcfg = {};
var devcfs = {};
var startTime = 0,
isConnected = false;
var ota = {
fwArray: null,
fwname: "",
fwsize: 0,
fwmaxsize: 196608,
ext_flg: false,
blockCount: 0,
program_offset: 0x11010000
};
//Connection values
var connectRetries = 0;
var $ = function(id) { return document.getElementById(id);}
function resetVariables() {
gattServer = null;
mainService = null;
otaCharacteristic = null;
cmdCharacteristic = null;
$('btnDisconnect').disabled = true;
$('btnReadAddr').disabled = true;
$('btnStartDFU').disabled = true;
$('btnSendData').disabled = true;
$('btnSendCommand').disabled = true;
}
function handleError(text) {
showError(text);
resetVariables();
if (connectRetries < 5) {
connectRetries++;
addLog("Переподключение " + connectRetries + " из " + 5);
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();
resetVariables();
$('btnReconnect').disabled = true;
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;
}
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;
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 => OtaBlkParse(event.target.value));
}).then(_ => {
return otaCharacteristic.readValue();
}).then(value => {
if(value.byteLength > 1)
addLog("OTA ver: "+ hex(value.getUint8(1),2));
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;
return mainService.getCharacteristic(0xfff4);
}).then(characteristic => {
console.log("Найдена CMD Characteristic");
cmdCharacteristic = characteristic;
return cmdCharacteristic.addEventListener('characteristicvaluechanged', event => customBlkParse(event.target.value));
}).then(_ => {
return cmdCharacteristic.startNotifications();
}).then(_ => {
return cmdCharacteristic.readValue();
}).then(value => {
if(value.byteLength >= 10)
if(value.getUint8(0) != 0)
addLog("Странный ответ устройства!");
otaCharacteristic = null;
return linkOta();
}).then(_ => {
showState("Устройство подключено.");
isConnected = true;
// $('btnConnect').disabled = true;
$('btnDisconnect').disabled = false;
$('btnReconnect').disabled = false;
$('btnSendCommand').disabled = false;
$('btnReadAddr').disabled = false;
$('btnSendData').disabled = false;
if (otaCharacteristic != null && ota.fwArray != null)
$('btnStartDFU').disabled = false;
selectConfigTab();
$('tabConfig').style.display = "block";
}).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 onDisconnected() {
isConnected = false;
resetVariables();
showState('Устройство отключено.');
$('btnConnect').disabled = false;
$('tabConfig').style.display = "none";
}
function startDFU() {
addLog("Старт программирования...");
updateBegin();
}
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 showProgress(text) {
$("lblStatus").className = "shadowprogress";
$("lblStatus").innerHTML = text;
}
function clearLog() {
$("log").innerHTML = "";
}
function showError(text) {
// console.log("Status: " + status);
let s = "Ошибка: " + text;
$("lblStatus").className = "shadowerror";
$("lblStatus").innerHTML = s;
addLog(text);
}
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 testOTAFirmware(data) {
let fsize = data.byteLength;
console.log("File size = 0x"+ fsize.toString(16));
if (fsize < 20)
return "Неправильный размер двоичной прошивки PHY6 OTA!";
if(fsize > ota.fwmaxsize)
return "Размер прошивки более " + (fwmaxsize/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 = testOTAFirmware(data);
if(s != "ok") {
$('btnStartDFU').disabled = true;
addLog(s);
ota.blockCount = 0;
ota.fwArray = null;
fwname = "";
alert(s);
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;
if(isConnected)
$('btnStartDFU').disabled = 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;
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() {
otaCharSend("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
// }
otaCharSend(cmd).then(_ => { otaCharacteristic.readValue().then(value => {
if(value.byteLength >= 2 && value.getUint8(0) == 0) {
setTimeout(function() {
startTime = new Date().getTime();
sendOTAblock(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) {
let s = "Программирование завершено за " + (new Date().getTime() - startTime) / 1000 + " секунды";
showProgress(s);
addLog(s);
/* Сброс - отключен для теста
var data = "02ff";
return otaCharSend(data).then(_ => {
addLog("Программирование завершено за " + (new Date().getTime() - startTime) / 1000 + " секунды");
}).catch(function(err) {
updateFail(err);
});
*/
} else
showError("Ошибка ("+value.getUint8(0)+") на передаче блока "+blockNr+" OTA: "+s);
}).catch(function(err) { updateFail(err); });
}
function sendOTAblock(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);
otaCharSend(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)
sendOTAblock(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
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) {
//console.log("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) {
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);
showState('Ошибка, длина блока от 1 до 16 байт!');
}
}
}
function infoReadAddr() {
if(cmdCharacteristic) {
let faddr = parseInt($('inputAddr').value, 16);
readAddr(faddr);
}
}
function writeCmd(data) {
if(cmdCharacteristic) {
len = data.length;
if(len != 0 && len <= 20) {
let blk = new Uint8Array(data);
cmdCharacteristic.writeValue(blk);
} else
showState('Ошибка, длина команды от 1 до 20 байт!');
}
}
function sendData() {
let addr = parseInt($('inputAddr').value, 16);
let data = hexToBytes($('inputData').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 customBlkParse(value) {
let len = value.byteLength;
if(len == 0) return;
let blkId = value.getUint8(0);
if((blkId == 0) && (len > 11)) {
// CMD_ID_DEVID Get dev id, version, services
devsrs.revision = value.getUint8(1);
devsrs.hw_version = value.getUint16(2, true);
devsrs.sw_version = value.getUint16(4, true);
devsrs.dev_spec_data = value.getUint16(6, true);
devsrs.services = value.getUint32(8, true);
addLog("Dev info # hw: "+hex(devsrs.hw_version,4)+", sw: "+hex(devsrs.sw_version,4)+", services: "+hex(devsrs.services,8)+", sd: "+hex(devsrs.dev_spec_data, 4));
} else if(blkId == 0x35){
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);
console.log(((dt.toISOString().slice(0, -1)).replace('T',' ')).replace('.000','')+' # Батарея: '+vb+' мВ , Температура: '+tm+'°C, Влажность: '+hm+'%, Счетчик: '+cnt);
} else if(len == 3) {
let flg = value.getUint16(1, true);
console.log('Последний блок: '+flg);
flg_memo_act = false;
} else if(len == 2) {
addLog('Нет сервиса записи истории!');
} else
console.log('blk: ' + dump8(value, value.byteLength));
} else if((blkId == 0x25 || blkId == 0x26) && (len > 12)) {
// CMD_ID_CFS Get/Set sensor config
devcfs.temp_k = value.getUint32(1, true);
devcfs.humi_k = value.getUint32(5, true);
devcfs.temp_z = value.getInt16(9, true);
devcfs.humi_z = value.getInt16(11, true);
$('inputTempK').value = devcfs.temp_k;
$('inputHumK').value = devcfs.humi_k;
$('inputTempZ').value = devcfs.temp_z;
$('inputHumZ').value = devcfs.humi_z;
addLog("Dev sensor # tk: "+devcfs.temp_k+", hk: "+devcfs.humi_k+", tz: "+devcfs.temp_z+", hz: "+devcfs.humi_z);
if (len > 17) {
devcfs.mid = value.getUint16(13, false);
devcfs.vid = value.getUint16(15, false);
devcfs.i2c_addr = value.getUint8(17);
s = "???";
if(devcfs.mid == 0x5959)
s = "CHT"+hex(devcfs.vid,4);
else if(devcfs.mid == 0 && devcfs.vid == 0xaaaa)
s = "AHT20";
$('txtSensor').innerHTML = s+', I2C адрес: 0x'+hex(devcfs.i2c_addr,2);
addLog("Dev sensor # id: "+hex(devcfs.mid,4)+"-"+hex(devcfs.vid,4)+", i2c_addr: 0x"+hex(devcfs.i2c_addr,2));
}
} else if((blkId == 0x55 || blkId == 0x56) && (len > 12)) {
// CMD_ID_CFG Get/Set device config
devcfg.flg = value.getUint32(1, true);
devcfg.rf_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);
$('inputFlag').value = devcfg.flg; // пока нет
$('inputTxPwr').value = devcfg.rf_tx_power; // 0..0x3f -> -20..+5 dBm нелинейное 0x1f = +0 дБм
$('inputLat').value = (devcfg.connect_latency + 1)*30; // = (connect_latency + 1)*30 ms
$('inputAdvInt').value = devcfg.advertising_interval*62.5; // *62.5 = интервала реклам в ms
$('inputMeasInt').value = devcfg.measure_interval; // *devcfg.advertising_interval*62.5 = опрос датчика в ms, value минимум = 2 (интервала рекламы)
$('inputAverInt').value = devcfg.averaging_measurements; // запись истории: при 0 - отключена, 1...255 * шаг опроса датчика = интерал записи истории
$('inputBatInt').value = devcfg.batt_interval; // в секундах, минимум 2 секунды, но кратно интервалу рекламы
addLog("Dev config # flg: "+hex(devcfg.flg,8)+", tx: "+devcfg.rf_tx_power+", adi: "+devcfg.advertising_interval+", cly: "+devcfg.connect_latency+", msi: "+devcfg.measure_interval+", bti: "+devcfg.batt_interval+", avi: "+devcfg.averaging_measurements);
} else if((blkId == 0xdb || blkId == 0xda) && (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);
$('inputData').value = s;
addLog(hex(addr,8) + ':' + s);
showProgress("Считано: " + len + " байт из 0x" + hex(addr,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("Dev serial # "+s)
} else if((blkId == 0x06) && (len > 3)) {
// CMD_ID_FLASH_ID Get Flash JEDEC ID
addLog("Dev flash # id: " + bytesToHex(value.buffer.slice(1,4)));
} else {
console.log('blk: ' + dump8(value, value.byteLength));
addLog('Ответ на команду (' + hex(blkId,2) + '): ' + bytesToHex(value.buffer.slice(1)));
}
}
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 SetDevCfg() {
if(cmdCharacteristic != null) {
// addLog("SetDevCfg...");
devcfg.flg = parseInt($('inputFlag').value)&0xffffffff; // пока нет
devcfg.rf_tx_power = parseInt($('inputTxPwr').value)&0x3f; // 0..0x3f -> -20..+5 dBm нелинейное 0x1f = +0 дБм
let connect_latency = parseInt($('inputLat').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($('inputAdvInt').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($('inputMeasInt').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($('inputAverInt').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($('inputBatInt').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.rf_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...");
devcfs.temp_k = parseInt($('inputTempK').value);
devcfs.humi_k = parseInt($('inputHumK').value);
devcfs.temp_z = parseInt($('inputTempZ').value);
devcfs.humi_z = parseInt($('inputHumZ').value);
blk = new Uint8Array([0x55,
devcfs.temp_k & 0xff, (devcfs.temp_k >> 8) & 0xff, (devcfs.temp_k >> 16) & 0xff, (devcfs.temp_k >> 24) & 0xff,
devcfs.humi_k & 0xff, (devcfs.humi_k >> 8) & 0xff, (devcfs.humi_k >> 16) & 0xff, (devcfs.humi_k >> 24) & 0xff,
devcfs.temp_z & 0xff, (devcfs.temp_z >> 8) & 0xff,
devcfs.humi_z & 0xff, (devcfs.humi_z >> 8) & 0xff
]);
cmdCharacteristic.writeValue(new Uint8Array([0x25])).catch(error => { console.log(error); addLog("SetSensCfg error!"); });
}
}
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);
}
}
const selectFile = function() {
let regex = /[^\\]+$/
this.choose,
this.selected
this.msg = str => {
let prefix = '[selectFile]\n\nError: '
return alert(prefix+str)
}
this.check = () => {
if (this.choose && this.selected != null) {
let choose = $(this.choose),
selected = $(this.selected)
choose.addEventListener('change',() => {
if (choose.value != '') {
selected.innerHTML = choose.value.match(regex);
readFile(choose.files[0]);
}
})
} else {
this.msg('Targets not set.')
}
}
selectFile.prototype.targets = (trigger, filetext) => {
this.choose = trigger
this.selected = filetext
}
selectFile.prototype.simulate = () => {
if (this.choose != null) {
let choose = $(this.choose)
if (typeof choose != 'undefined') {
choose.click()
this.check()
} else {
this.msg('Could not find element '+this.choose)
}
} else {
this.msg('Targets not set.')
}
}
};
function readFile(file) {
var reader = new FileReader();
if (file != null) {
reader.fname = file.name;
reader.readAsArrayBuffer(file);
} else {
showError("Файл не загружен");
}
reader.onload = function() {
getFwArray(this.result, this.fname);
}
reader.onerror = function() {
console.log(this.error);
};
}
var getFile = new selectFile;
getFile.targets('inpFile','lblFile');
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);
}
}
function selectConfigTab() {
openTab(0, "tabConfig");
tablinks = document.getElementsByClassName("tablinks");
tablinks[0].className += " active";
}
window.onload = (event) => {
showState("Нет подключения" );
selectConfigTab();
};
</script>
<h2>PHY62x2-BTHome <a href="https://github.com/pvvx/THB2"><small>&#9432;</small></a></h2>
<label for="inpNamePrefix">Префикс названия устройств(а)</label><br>
<input type="text" id="inpNamePrefix" value="" placeholder="THB, BT">
<hr>
<label id="lblStatus">Состояние: загрузка страницы</label>
<br><br><hr>
<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>
<br><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, 'tabFlash')">Flash</button>
<button class="tablinks" onclick="openTab(event, 'tabService')">Service</button>
</div>
<div id="tabConfig" class="tabcontent">
Параметры связи<br>
<!--
Флаг: <input size="8" type="text" id="inputFlag" maxlength="8">
Tx Power: <input size="8" type="text" id="inputTxPwr" maxlength="8">
Conn. Latency: <input size="8" type="text" id="inputLat" maxlength="8">
<br>
Интервал рекламы: <input size="8" type="text" id="inputAdvInt" maxlength="8">
Интервал измерений: <input size="8" type="text" id="inputMeasInt" maxlength="8">
Интервал опроса батареи: <input size="8" type="text" id="inputMeasBat" maxlength="8">
Интервал усреднения: <input size="8" type="text" id="inputAverInt" maxlength="8">
<br> -->
<table>
<tr>
<th>Флаг</th>
<th>Tx Power</th>
<th>Conn. Latency</th>
</tr>
<tr>
<td><input size="8" type="text" id="inputFlag" maxlength="8"></td>
<td><input size="4" type="text" id="inputTxPwr" maxlength="4"></td>
<td><input size="4" type="text" id="inputLat" maxlength="4"></td>
</tr>
</table>
Интервалы
<table>
<tr>
<th>Реклама</th>
<th>Измерения</th>
<th>Усреднение</th>
<th>Уровень батареи</th>
</tr>
<tr>
<td><input size="4" type="text" id="inputAdvInt" maxlength="4"></td>
<td><input size="4" type="text" id="inputMeasInt" maxlength="4"></td>
<td><input size="4" type="text" id="inputAverInt" maxlength="4"></td>
<td><input size="4" type="text" id="inputBatInt" maxlength="4"></td>
</tr>
</table>
<button type="button"id="btnGetDev" onclick="GetDevCfg()" >Прочитать</button>
<button type="button"id="btnSetDev" onclick="SetDevCfg()" >Записать</button>
<hr>
<table>
<tr>
<th>Параметры сенсора:</th>
<th>Температура</th>
<th>Влажность</th>
</tr>
<tr>
<th><div id="txtSensor">?</div></th>
</tr>
<tr>
<td>Поправочные коэффициенты</td>
<td><input size="8" type="text" id="inputTempK" maxlength="8"></td>
<td><input size="8" type="text" id="inputHumK" maxlength="8"></td>
</tr>
<tr>
<td>Коррекция смещения</td>
<td><input size="8" type="text" id="inputTempZ" maxlength="8"></td>
<td><input size="8" type="text" id="inputHumZ" maxlength="8"></td>
</tr>
</table>
<br>
<!--
Поправочные коэффициенты:
Температуры: <input size="8" type="text" id="inputTempK" maxlength="8">
Влажности:
<br>
Коррекция смещения:
Температуры: <input size="8" type="text" id="inputTempZ" maxlength="8">
Влажности: <input size="8" type="text" id="inputHumZ" maxlength="8">
<br>
Параметры I2C<br>
Vendor: <input size="4" type="text" id="inputI2CVen" maxlength="4">
Model: <input size="4" type="text" id="inputI2CMod" maxlength="4">
Address: <input size="2" type="text" id="inputI2CAddr" maxlength="2">
<br> -->
<button type="button"id="btnGetSens" onclick="GetSensCfg()" >Прочитать</button>
<button type="button"id="btnSetSens" onclick="SetSensCfg()" >Записать</button>
</div>
<div id="tabOTA" class="tabcontent">
<p>Файл прошивки:
<input type=file hidden id=inpFile>
<label id=lblFile>не выбран</label>
<button type=button onClick=getFile.simulate()>Выбрать</button>
<p>
<hr>
<button type="button"id="btnStartDFU" onclick="startDFU()" disabled="true" >Старт программирования</button>
</div>
<div id="tabFlash" class="tabcontent">
<p>
<button type="button" id="btnReadFF" onclick="startReadFF()" disabled="true">Прочитать</button>
<button type="button" id="btnSave" disabled="true">Сохранить в файл</button>
<p>
</div>
<div id="tabService" class="tabcontent">
Чтение и запись памяти:<br>
Адрес (hex): <input size="8" type="text" id="inputAddr" value="11000000" maxlength="8">
<button type="button" id="btnReadAddr" onclick="infoReadAddr()" disabled="true">Прочитать</button>
Данные (hex): <input size="32" type="text" id="inputData" value="?" maxlength="32">
<button type="button" id="btnSendData" onclick="sendData()" disabled="true">Записать</button><hr>
<button type="button" id="btnSendCommand" onclick="sendCommand()" disabled="true">Команда</button>
<input type="text" id="inpCmdData" value="55" size="40" maxlength="40"><hr>
</div>
<hr>
<button type="button" onclick="clearLog()">Очистить лог</button><br>
<div id="log"></div>
</body>
</html>