Hier die von uns verwendeten LoRa Skripte auf der Home und Remote Seite.
Beide Seiten sind sowohl Sender, wie auch Empfänger.
Auf beiden Seiten werden im Shelly virtuelle Komponenten genutzt, als Mittler zwischen der LoRa-Funk-Welt und den Shelly Aktionen auf die physikalischen Shelly-Funktionen im Heimnetz.
Das versprach Flexibilität in der Anwendung, weil über LoRa beliebige Texte transportiert werden, die dann je nach definiertem Text, für jede Menge variabler Funktionen als Trigger dienen können, -- über ein einzelnes Text Ein- oder Ausgabefeld.
Es werden einfach die Texte in den Aktionen definiert, die gesendet werden, - oder die als Trigger für eine Aktion fungieren.
Im Beispiel geht es um die Fernalarmierung einer Statusänderung am Remote Shelly.
Durch den Home Shelly kann dann wiederum eine Aktion am Remote Shelly getriggert werden.
Home Shelly:
virtual Components:
button:200 , button:201 -- zur Steuerung einer Remote Komponente;
text:200 -- zur Darstellung empfangener (remote Shelly) Textnachricht auf dem Home Shelly
(z.B. Eingangs-Status remote Shelly)
number:200 , number:201 -- zur Darstellung des Packet transmission fail
// #### comms-LoRa-Home.js ####
// #### Shelly local input ####
let btnOff = Virtual.getHandle("button:200");
btnOff.on("single_push", function(ev) {
console.log("Button OFF");
sendPacket(PACKET_TYPE_COMMAND, "turn_alarm_off");
});
let btnOn = Virtual.getHandle("button:201");
btnOn.on("single_push", function(ev) {
console.log("Button ON");
sendPacket(PACKET_TYPE_COMMAND, "turn_alarm_on");
});
// #### Shelly local output ####
function onReceiveStatus(statusText) {
let alarm = Virtual.getHandle("text:200");
alarm.setValue(statusText);
}
function onReceiveCommand(commandText) {
/* no-op */
}
// #### display count of unconfirmed transmissions ####
function onUpdatePaketsWaiting(count) {
let number = Virtual.getHandle("number:200");
number.setValue(count);
}
function onUpdatePacketsLost(count) {
let number = Virtual.getHandle("number:201");
number.setValue(count);
}
// #### Start of common script ####
const MAX_RETRIES = 8;
const MIN_RETRY_INTERVAL = 10000;
const MAX_RETRY_INTERVAL = 20000;
const MAX_CONCURRENT_CALLS = 4;
// #### encryption - decryption functions ####
const KEY_RAW = "<put your encryption key here>";
function keyFromHex(hex) {
const buffer = new ArrayBuffer(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
buffer[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return buffer;
}
const KEY = keyFromHex(KEY_RAW);
const AES_MODE = "ECB";
function encrypt(plaintext, key) {
//console.log("Encrypting: ", plaintext);
function stringToBlockBuffer(string, blocksize) {
let size = string.length;
const lastBlock = string.length % blocksize;
if (lastBlock > 0) {
size += blocksize - lastBlock;
}
const buffer = new ArrayBuffer(size);
let i = 0;
for (; i < string.length; i++) {
buffer[i] = string.charCodeAt(i);
}
for (; i < size; i++) {
buffer[i] = 0;
}
return buffer;
}
return AES.encrypt(stringToBlockBuffer(plaintext, 16), key, { mode: AES_MODE });
}
function decrypt(cyphertext, key) {
function bufferToString(buffer) {
let string = '';
for (let i = 0; i < buffer.length; i++) {
const code = buffer[i];
if (code == 0) continue;
string += String.fromCharCode(buffer[i]);
}
return string;
}
const buffer = AES.decrypt(cyphertext, key, { mode: 'ECB' });
if (!buffer || buffer.byteLength === 0) {
console.log('Could not decrypt message');
return;
}
return bufferToString(buffer);
}
function numberToHex(n, size) {
let string = n.toString(16);
while (string.length < size) {
string = '0' + string;
}
return string.slice(-size);
}
function checksum(text, size) {
let checksum = 0;
for (let i = 0; i < text.length; i++) {
checksum ^= text.charCodeAt(i);
}
return numberToHex(checksum , size);
}
// #### packet tracking functions ####
const CHECKSUM_SIZE = 4;
const PACKET_TYPE_SIZE = 1;
const PACKET_ID_SIZE = 2;
const PACKET_TYPE_STATUS = "S";
const PACKET_TYPE_COMMAND = "C";
const PACKET_TYPE_ACK = "A";
var checkAcknowledgementsTimer = undefined;
function generatePacket(type, id, text) {
let packet = type + id + text;
packet = checksum(packet, CHECKSUM_SIZE) + packet;
return packet;
}
let nextPacketId = 0;
var receivedAcknowledgments = [ ];
var awaitsAcknowledgment = { };
function sendPacket(type, text) {
const id = numberToHex(nextPacketId++, PACKET_ID_SIZE);
const packet = generatePacket(type, id, text);
awaitsAcknowledgment[id] = {
"packet": packet,
"retries": 0,
};
loraSend(packet);
updatePacketsWaiting();
setCheckAcknowledgementsTimer();
}
function sendAcknowledgment(id) {
const packet = generatePacket(PACKET_TYPE_ACK, id, '');
loraSend(packet);
}
function receivePacket(packet) {
if (packet.length < CHECKSUM_SIZE + PACKET_TYPE_SIZE + PACKET_ID_SIZE) {
console.log("Error: Packet too short");
return;
}
const actualChecksum = packet.slice(0, CHECKSUM_SIZE);
const afterChecksum = packet.slice(CHECKSUM_SIZE);
const expectedChecksum = checksum(afterChecksum, CHECKSUM_SIZE);
if (actualChecksum !== expectedChecksum) {
console.log("Error: Checksum mismatch");
return;
}
const packetType = afterChecksum.slice(0, PACKET_TYPE_SIZE);
const packetId = afterChecksum.slice(PACKET_TYPE_SIZE, PACKET_TYPE_SIZE + PACKET_ID_SIZE);
const text = afterChecksum.slice(PACKET_TYPE_SIZE + PACKET_ID_SIZE);
console.log("Received {type:"+packetType+",id:"+packetId+",text:'"+text+"'}");
switch (packetType) {
case PACKET_TYPE_STATUS:
onReceiveStatus(text);
break;
case PACKET_TYPE_COMMAND:
onReceiveCommand(text);
break;
case PACKET_TYPE_ACK:
console.log("Received acknowledgment for packet " + packetId);
receivedAcknowledgments.push(packetId);
delete awaitsAcknowledgment[packetId];
updatePacketsWaiting();
setCheckAcknowledgementsTimer();
return;
default:
console.log("Error: Unknown packet type");
return
}
sendAcknowledgment(packetId);
}
// #### LoRa send- receive functions ####
var concurrentCalls = 0;
function loraSend(packet) {
console.log("Sending: ", packet);
const encoded = btoa(encrypt(packet, KEY));
if (concurrentCalls >= MAX_CONCURRENT_CALLS) {
console.log("Error: Too many concurrent calls, skipping ...");
return;
}
concurrentCalls++;
Shelly.call(
'Lora.SendBytes',
{ id: 100, data: encoded },
function (_, err_code, err_msg) {
if (err_code !== 0) {
console.log('Error:', err_code, err_msg);
}
concurrentCalls--;
}
);
}
function loraReceive(data) {
const packet = decrypt(atob(data), KEY);
if (!packet) {
return;
}
console.log("Received: ", packet);
receivePacket(packet);
}
Shelly.addEventHandler(function (event) {
if (
typeof event !== 'object' ||
event.name !== 'lora' ||
!event.info ||
!event.info.data
) {
return;
}
loraReceive(event.info.data);
});
var totalPacketsLost = 0;
function updatePacketsWaiting() {
onUpdatePaketsWaiting(Object.keys(awaitsAcknowledgment).length);
}
function updatePacketsLost() {
updatePacketsWaiting();
onUpdatePacketsLost(totalPacketsLost);
}
function checkAcknowledgements() {
for (const id of receivedAcknowledgments) {
delete awaitsAcknowledgment[id];
}
receivedAcknowledgments = [];
const waiting = Object.keys(awaitsAcknowledgment);
updatePacketsWaiting();
if (waiting.length === 0) {
console.log("No packets waiting");
return;
}
for (const id of waiting) {
const entry = awaitsAcknowledgment[id];
if (entry.retries >= MAX_RETRIES) {
console.log("Error: Packet " + id + " not acknowledged after " + MAX_RETRIES + " retries");
delete awaitsAcknowledgment[id];
totalPacketsLost++;
onUpdatePacketsLost(totalPacketsLost);
continue;
}
console.log("Retrying packet " + id + ", retries left: " + (MAX_RETRIES - entry.retries));
entry.retries++;
loraSend(entry.packet);
anyRetry = true;
}
setCheckAcknowledgementsTimer();
}
function setCheckAcknowledgementsTimer() {
if (checkAcknowledgementsTimer) {
return;
}
const interval = MIN_RETRY_INTERVAL + Math.random() * (MAX_RETRY_INTERVAL - MIN_RETRY_INTERVAL);
console.log("Setting timer to check acknowledgments in " + interval + " ms");
checkAcknowledgementsTimer = Timer.set(interval, false, function() {
checkAcknowledgementsTimer = undefined;
checkAcknowledgements();
});
}
Alles anzeigen
Remote Shelly:
virtual Components:
text:200 , text:201 -- zur Darstellung von zu sendenden (remote Shelly) oder empfangenen (Home Shelly) Textnachricht ;
(z.B. senden des remote Shelly Eingangs-Status ; Empfang des Kommandos vom Home Shelly)
number:200 , number:201 -- zur Darstellung des Packet transmission fail
// #### comms-Lora-Remote.js ####
// #### Shelly local input ####
let alarm = Virtual.getHandle("text:200");
alarm.on("change", function(ev) {
console.log("Alarm changed: ", ev.value);
sendPacket(PACKET_TYPE_STATUS, ev.value);
});
// #### Shelly local output ####
function onReceiveStatus(statusText) {
/* no-op */
}
function onReceiveCommand(commandText) {
let command = Virtual.getHandle("text:201");
command.setValue(commandText);
}
// #### display count of unconfirmed transmissions ####
function onUpdatePaketsWaiting(count) {
let number = Virtual.getHandle("number:200");
number.setValue(count);
}
function onUpdatePacketsLost(count) {
let number = Virtual.getHandle("number:201");
number.setValue(count);
}
// #### Start of common script ####
const MAX_RETRIES = 100;
const MIN_RETRY_INTERVAL = 10000;
const MAX_RETRY_INTERVAL = 20000;
const MAX_CONCURRENT_CALLS = 4;
// #### encryption - decryption functions ####
const KEY_RAW = "<put your encryption key here>";
function keyFromHex(hex) {
const buffer = new ArrayBuffer(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
buffer[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return buffer;
}
const KEY = keyFromHex(KEY_RAW);
const AES_MODE = "ECB";
function encrypt(plaintext, key) {
//console.log("Encrypting: ", plaintext);
function stringToBlockBuffer(string, blocksize) {
let size = string.length;
const lastBlock = string.length % blocksize;
if (lastBlock > 0) {
size += blocksize - lastBlock;
}
const buffer = new ArrayBuffer(size);
let i = 0;
for (; i < string.length; i++) {
buffer[i] = string.charCodeAt(i);
}
for (; i < size; i++) {
buffer[i] = 0;
}
return buffer;
}
return AES.encrypt(stringToBlockBuffer(plaintext, 16), key, { mode: AES_MODE });
}
function decrypt(cyphertext, key) {
function bufferToString(buffer) {
let string = '';
for (let i = 0; i < buffer.length; i++) {
const code = buffer[i];
if (code == 0) continue;
string += String.fromCharCode(buffer[i]);
}
return string;
}
const buffer = AES.decrypt(cyphertext, key, { mode: 'ECB' });
if (!buffer || buffer.byteLength === 0) {
console.log('Could not decrypt message');
return;
}
return bufferToString(buffer);
}
function numberToHex(n, size) {
let string = n.toString(16);
while (string.length < size) {
string = '0' + string;
}
return string.slice(-size);
}
function checksum(text, size) {
let checksum = 0;
for (let i = 0; i < text.length; i++) {
checksum ^= text.charCodeAt(i);
}
return numberToHex(checksum , size);
}
// #### packet tracking functions ####
const CHECKSUM_SIZE = 4;
const PACKET_TYPE_SIZE = 1;
const PACKET_ID_SIZE = 2;
const PACKET_TYPE_STATUS = "S";
const PACKET_TYPE_COMMAND = "C";
const PACKET_TYPE_ACK = "A";
var checkAcknowledgementsTimer = undefined;
function generatePacket(type, id, text) {
let packet = type + id + text;
packet = checksum(packet, CHECKSUM_SIZE) + packet;
return packet;
}
let nextPacketId = 0;
var receivedAcknowledgments = [ ];
var awaitsAcknowledgment = { };
function sendPacket(type, text) {
const id = numberToHex(nextPacketId++, PACKET_ID_SIZE);
const packet = generatePacket(type, id, text);
awaitsAcknowledgment[id] = {
"packet": packet,
"retries": 0,
};
loraSend(packet);
updatePacketsWaiting();
setCheckAcknowledgementsTimer();
}
function sendAcknowledgment(id) {
const packet = generatePacket(PACKET_TYPE_ACK, id, '');
loraSend(packet);
}
function receivePacket(packet) {
if (packet.length < CHECKSUM_SIZE + PACKET_TYPE_SIZE + PACKET_ID_SIZE) {
console.log("Error: Packet too short");
return;
}
const actualChecksum = packet.slice(0, CHECKSUM_SIZE);
const afterChecksum = packet.slice(CHECKSUM_SIZE);
const expectedChecksum = checksum(afterChecksum, CHECKSUM_SIZE);
if (actualChecksum !== expectedChecksum) {
console.log("Error: Checksum mismatch");
return;
}
const packetType = afterChecksum.slice(0, PACKET_TYPE_SIZE);
const packetId = afterChecksum.slice(PACKET_TYPE_SIZE, PACKET_TYPE_SIZE + PACKET_ID_SIZE);
const text = afterChecksum.slice(PACKET_TYPE_SIZE + PACKET_ID_SIZE);
console.log("Received {type:"+packetType+",id:"+packetId+",text:'"+text+"'}");
switch (packetType) {
case PACKET_TYPE_STATUS:
onReceiveStatus(text);
break;
case PACKET_TYPE_COMMAND:
onReceiveCommand(text);
break;
case PACKET_TYPE_ACK:
console.log("Received acknowledgment for packet " + packetId);
receivedAcknowledgments.push(packetId);
delete awaitsAcknowledgment[packetId];
updatePacketsWaiting();
setCheckAcknowledgementsTimer();
return;
default:
console.log("Error: Unknown packet type");
return
}
sendAcknowledgment(packetId);
}
// #### LoRa send- receive functions ####
var concurrentCalls = 0;
function loraSend(packet) {
console.log("Sending: ", packet);
const encoded = btoa(encrypt(packet, KEY));
if (concurrentCalls >= MAX_CONCURRENT_CALLS) {
console.log("Error: Too many concurrent calls, skipping ...");
return;
}
concurrentCalls++;
Shelly.call(
'Lora.SendBytes',
{ id: 100, data: encoded },
function (_, err_code, err_msg) {
if (err_code !== 0) {
console.log('Error:', err_code, err_msg);
}
concurrentCalls--;
}
);
}
function loraReceive(data) {
const packet = decrypt(atob(data), KEY);
if (!packet) {
return;
}
console.log("Received: ", packet);
receivePacket(packet);
}
Shelly.addEventHandler(function (event) {
if (
typeof event !== 'object' ||
event.name !== 'lora' ||
!event.info ||
!event.info.data
) {
return;
}
loraReceive(event.info.data);
});
var totalPacketsLost = 0;
function updatePacketsWaiting() {
onUpdatePaketsWaiting(Object.keys(awaitsAcknowledgment).length);
}
function updatePacketsLost() {
updatePacketsWaiting();
onUpdatePacketsLost(totalPacketsLost);
}
function checkAcknowledgements() {
for (const id of receivedAcknowledgments) {
delete awaitsAcknowledgment[id];
}
receivedAcknowledgments = [];
const waiting = Object.keys(awaitsAcknowledgment);
updatePacketsWaiting();
if (waiting.length === 0) {
console.log("No packets waiting");
return;
}
for (const id of waiting) {
const entry = awaitsAcknowledgment[id];
if (entry.retries >= MAX_RETRIES) {
console.log("Error: Packet " + id + " not acknowledged after " + MAX_RETRIES + " retries");
delete awaitsAcknowledgment[id];
totalPacketsLost++;
onUpdatePacketsLost(totalPacketsLost);
continue;
}
console.log("Retrying packet " + id + ", retries left: " + (MAX_RETRIES - entry.retries));
entry.retries++;
loraSend(entry.packet);
anyRetry = true;
}
setCheckAcknowledgementsTimer();
}
function setCheckAcknowledgementsTimer() {
if (checkAcknowledgementsTimer) {
return;
}
const interval = MIN_RETRY_INTERVAL + Math.random() * (MAX_RETRY_INTERVAL - MIN_RETRY_INTERVAL);
console.log("Setting timer to check acknowledgments in " + interval + " ms");
checkAcknowledgementsTimer = Timer.set(interval, false, function() {
checkAcknowledgementsTimer = undefined;
checkAcknowledgements();
});
}
Alles anzeigen
Für die Encryption muss bitte jeder seinen eigenen persönlichen encryption key definieren (lange Zeichen+Ziffernfolge), und an der Skriptstelle KEY_RAW zwischen den Gänsefüßchen einfügen.
Wir hoffen, dass dies hilfreich ist für neue LoRa Projekte.
Mit Reichweiten-Test bin ich noch nicht weitergekommen.
Die bisherigen Ergebnisse waren ernüchternd, und eher im Range von 500m (und nicht 5km).