NEWS
Bluetooth LE Türsensor mit Puck.js
-
Mein aktuelles Miniprojekt ist jetzt vollendet, daher wollte ich es mal vorstellen.
Um die Beleuchtung in meiner Abstellkammer (die ich gerne mal vergesse auszuschalten) zu automatisieren, habe ich mir mit https://www.espruino.com/PuckJS und https://www.amazon.de/gp/product/B00F0BTTBU einen (fast) kabellosen Türsensor gebaut. Wie das ganze aussieht, sieht man hier:
Die Reed-Kontakte haben den Vorteil, dass sie sowohl NC (normal geschlossen) als auch NO (normal geöffnet) betrieben werden können. Bei Verwendung von NO ist der interne Kontakt geöffnet bei angelegtem Magneten, d.h. es fließt im Normalfall kein Strom.
Das Kabel für NO (braun) ist am Puck an Pin D2 angeschlossen, das gemeinsame Kabel (weiß) an Pin D1.
Der Code auf dem Puck wird nur ausgeführt, wenn ein Schaltvorgang erkannt wird, sodass er möglichst wenig Strom verbraucht (ca. 25µA im geschlossenen Zustand laut Hersteller).
Über den Stromverbrauch im offenen Zustand habe ich noch keine Info. Evtl. sollte man hier die DAC-Funktion des Pins verwenden, um den Kontakt mit geringerer Spannung zu betreiben, oder den NC-Kontakt mitverwenden und im Code intelligent zwischen den Inputs wechseln.
! ````
NRF.setScanResponse([
//0x02,
//0x01,0x05, // flags
5, // length (including type byte)
0x09, // name
'T', 'u', 'e', 'r'
]);
! function send(value) {
NRF.setAdvertising({
0xEFA1: [value]
}, {showName: false});
}
! // set D2 to output, D1 to input with pulldown
pinMode(D2, "output");
digitalWrite(D2, 1);
pinMode(D1, "input_pulldown");
! // watch the input for changes
let status = null;
setWatch(function(e) {
if (e.state && status != "open") {
// magnet open
status = "open";
console.log(status);
digitalPulse(LED2, 1, 500);
send(1);
} else if (!e.state && status != "closed") {
// magnet closed
status = "closed";
console.log(status);
digitalPulse(LED1, 1, 500);
send(0);
}
}, D1, {
repeat: true,
debounce: 150
});Details zum Code im Spoiler: >! Der erste Block gibt dem Puck einen kurzen Namen und kann angepasst werden, sollte aber kurz bleiben, damit das Advertising noch funktioniert (die Datenmenge ist stark begrenzt). >! In der Funktion send wird der aktuelle Zustand des Kontakts (0 oder 1) auf dem Service 0xEFA1 bekanntgegeben. Die ID kann angepasst werden (sollte laut Spec eigentlich eine UUID sein, das ist mir zuhause aber egal), und muss im Script auf dem ioBroker ebenfalls eingetragen werden. >! Pin D2 wird als output festgelegt und auf den Status 1 gesetzt (d.h. hier liegen 3,3V an). >! Pin D1 ist ein input mit Pulldown-Widerstand, sodass bei geöffnetem Kontakt (Tür zu) der Sensorwert zu 0 definiert ist und nicht springt. >! Im Folgenden wird der Input-Pin überwacht und bei Änderungen kurz geblinkt (grün bei offener Tür, rot bei geschlossener) sowie die Bluetooth-Daten aktualisiert. Auf dem ioBroker läuft ein Skript, das per Bluetooth auf Advertising-Daten lauscht und diese als States anlegt: >! ```` // Hier die BLE-Services eintragen, für die States angelegt werden sollen. // Als HEX-strings, in Kleinbuchstaben und ohne führendes 0x const services = [ "efa1" // puck.js magnetometer proximity detection ]; >! // ========================= >! function updateState(stateId, value, ack) { const val = getState(stateId).val; if (val == null) { createState(stateId, value); } else if (val != value) { setState(stateId, value, ack); } } >! const noble = require("noble"); >! const onDiscover = (p) => { if (!(p && p.advertisement && p.advertisement.serviceData)) return; const stateId_name = `BLE.${p.address}.name`; createState(`BLE.${p.address}.name`, p.advertisement.localName); for (let entry of p.advertisement.serviceData) { const uuid = entry.uuid; let data = entry.data; if (data.type === "Buffer") { data = Buffer.from(data.data); } if (data.length === 1) { // single byte data = data[0]; } else { // not supported yet continue; } updateState(`BLE.${p.address}.${uuid}`, data, true); } }; >! let isScanning = false; function startScanning() { if (isScanning) return; noble.on("discover", onDiscover); noble.startScanning(services, true); isScanning = true; } function stopScanning() { if (!isScanning) return; noble.removeAllListeners("discover"); noble.stopScanning(); isScanning = false; } >! noble.on("stateChange", (state) => { switch (state) { case "poweredOn": startScanning(); break; case "poweredOff": stopScanning(); break; } }); if (noble.state === "poweredOn") startScanning(); >! onStop((callback) => { stopScanning(); noble.removeAllListeners("stateChange"); if (callback) callback(); }, 2000);
Sollte auf dem Puck der Code des BLE-Service geändert worden sein, muss das hier bei services eingetragen werden.
-
Kleines Update:
Ich habe mich nochmal mit der Firmware und der Verkabelung beschäftigt.
Neue Verkabelung:
NC (grau) => D29 (output)
NO (braun) => D2 (output)
COM (weiß) => D1 (input)
Um dauerhaften Stromfluss zu verhindern, liegt Spannung jetzt entweder an NC (D29) oder NO (D2) an. Der Pin wird so ausgewählt, dass kein Strom fließt.
Wenn sich der Zustand des Magnetkontakts ändert, fließt kurzzeitig Strom. Der Puck reagiert das und vertauscht die Polung.
Neue Firmware für den Puck:
! ````
NRF.setScanResponse([
//0x02,
//0x01,0x05, // flags
5, // length (including type byte)
0x09, // name
'T', 'u', 'e', 'r'
]);
! const advertisingData = {
0xEFA1: [0], // magnet sensor
0x180F: [Puck.getBatteryPercentage()], // battery
};
! function send(value) {
advertisingData[0xEFA1] = [value];
NRF.setAdvertising(advertisingData, {showName: false});
}
! // ^^^ BLUETOOTH ^^^
// =================================
// vvv SENSOR CODE vvv
! const pinNC = D29;
const pinNO = D2;
const pinCOM = D1;
! const HIGH = 1;
const LOW = 0;
! let circuit = "NO";
let rawState = null;
let bluetoothState = null; // the state reported over bluetooth
! function writeNO() {
digitalWrite(pinNO, HIGH);
digitalWrite(pinNC, LOW);
circuit = "NO";
console.log("using NO circuit");
}
function writeNC() {
digitalWrite(pinNC, HIGH);
digitalWrite(pinNO, LOW);
circuit = "NC";
console.log("using NC circuit");
}
! function parseState(valCOM) {
valCOM = valCOM || digitalRead(pinCOM) === 1;
if (circuit === "NO") {
// when the NO circuit is used, valCOM is true when the door is open
rawState = valCOM ? "open" : "closed";
} else {
// when the NC circuit is used, valCOM is true when the door is closed
rawState = valCOM ? "closed" : "open";
}
! return rawState;
}
! function isConducting() {
console.log(checking for conductance: circuit=${circuit}, rawState=${rawState}
);
if (circuit === "NO" /* NO / && rawState === "open" / open /) return true;
if (circuit === "NC" / NC / && rawState === "closed" / closed /) return true;
return false;
}
! function switchCircuit() {
console.log("switching circuit");
if (circuit === "NO") { // NO
writeNC();
} else { // NC
writeNO();
}
}
! // set NC and NO pin to output, COM pin to input with pulldown
pinMode(pinNC, "output");
pinMode(pinNO, "output");
pinMode(pinCOM, "input_pulldown");
! function onChange(e) {
// parse the state depending on the current circuit;
parseState(e.state);
console.log("pin state changed: " + rawState);
// switch circuit if neccessary
if (isConducting()) switchCircuit();
! // do the switch stuff
if (rawState == "open" && bluetoothState != "open") {
// magnet open
bluetoothState = "open";
console.log(bluetoothState);
digitalPulse(LED2, 1, 500);
send(1);
} else if (rawState == "closed" && bluetoothState != "closed") {
// magnet closed
bluetoothState = "closed";
console.log(bluetoothState);
digitalPulse(LED1, 1, 500);
send(0);
}
}
! // watch the input for changes
setWatch(onChange, pinCOM, {
repeat: true,
debounce: 150
});
// also update the battery level every hour
setInterval(() => {
advertisingData[0x180F] = [Puck.getBatteryPercentage()];
send();
}, 36001000);
! // initially try to close the NO circuit
writeNO();
onChange({
state: digitalRead(pinCOM) === 1
});