Erstelle IoT4School 2.0 Blöcke - esdkrwl/IoT4School GitHub Wiki
Diese Anleitung beschreibt die Vorgehensweise für die Implementation neuer Module in Form von Google Blockly Blöcken.
Voraussetzung dieser Anleitung ist die bereits durchgeführte Vorbereitung des Raspberry Pis mithilfe des vorkonfigurierten
Raspberry Pi Images oder nach der im Wiki verfügbaren Anleitung.
IoT4School 2.0 verwendet BIPES - eine blockbasierte, integrierte Plattform für eingebettete Systeme.
BIPES selbst stellt eine Dokumentation zum Erstellen neuer Blöcke zur Verfügung, die hier abgerufen werden kann.
Diese Anleitung ist daher als Ergänzung zu verstehen, die auf die Besonderheiten bei der Implementierung neuer Sensoren/Aktoren für IoT4School 2.0 abzielt.
Der Prozess der Blockerstellung und Implementierung geschieht in drei Schritten:
- Blockdefinition: Hier wird das Aussehen (Farbe, Form, Ein- und Ausgänge) der Blöcke definiert
- Implementierung der Blocklogik: Definieren des Codes, der bei der Benutzung des Blocks generiert werden soll
- Blockeinbindung: Hinzufügen der erstellten Blöcke zur BIPES-Toolbox
Wurden nicht nur Blöcke für bestehende Module (Sensoren / Aktoren) entwickelt, sondern neue Sensoren / Aktoren mithilfe der Datei module_Blueprint.py
aus dem Repository erstellt, ist noch folgender Schritt notwendig:
- Anpassung der Datei
cb_handler.py
Zur Veranschaulichung erfolgt dieser Prozess anhand eines Beispiels, der Implementierung eines Motor-Aktors.
Die Blockdefinition kann in JavaScript oder im JSON-Format erfolgen.
Bisherige Blockdefinitionen sind in JavaScript verfasst und sind auf dem Raspberry Pi
unter /var/www/html/BIPES/ui/core/block_definitions.js
zu finden.
Die IoT4School-spezifischen Blöcke tragen den Präfix iot_
und können gerne als Referenz für
die Implementation weiterer Blöcke verwendet werden.
Das Aussehen der Blöcke des Motor-Aktors wurde mithilfe von Google Block Factory definiert.
Die vollständige Blockdefinition für den Motor-Aktor sowie für weitere Sensoren / Aktoren
stehen hier im Repository in Form von .xml
-Dateien zur Verfügung.
Diese können über den Import Button eingebunden werden:
Namenskonvention der Blöcke ist iot_modulname_funktion.
Neben dem Initialisierungs-Block wurden noch folgende, weitere Funktions-Blöcke für den Motor-Aktor erstellt:
- iot_motor_set_enabled (Block / Vorschau)
- iot_motor_set_disabled (Block / Vorschau)
- iot_motor_toggle (Block / Vorschau)
- iot_motor_set_speed (Block / Vorschau)
- iot_motor_brake (Block / Vorschau)
Implementiert wurden die Funktionen ab Zeile 148 der selbst entwickelten Motor Klasse aus dem [module_Motor.py][]-Skript.
Diese kann gerne als Grundlage für die Entwicklung weiterer Module genommen werden.
(Im selben Verzeichnis steht hierfür außerdem das [module_Blueprint.py][]-Skript zur Verfügung.)
Schließlich wird pro Modul noch eine typisierte Zugriffsfunktion (Getter) erstellt:
Diese Funktion wird bei der Implementierung der Blocklogik eine gesonderte Rolle spielen.
Desweiteren sind, unter anderem auch durch die Typisierung der Module, folgende Dinge bei der Blockerstellung zu beachten:
- Initialisierungsblock: Der Bild-Pfad
/iot_img/motor.png
verweist auf dem Raspberry Pi zum Pfad/var/www/iot_img/motor.png
- Bei Implementierung neuer Module sollte ein Bild auf dieses Verzeichnis kopiert werden
- Bei Abfrage der Modul-ID sollten
output type
des Getters mit demvalue input
übereinstimmen (in diesem Fall "motor") - Für alle anderen
value inputs
sollten, sofern möglich, bestimmte Typen hinterlegt werden (Number, String, ...) - Umlaute wie ä,ö,ü werden aufgrund der ANSI-Kodierung der
block_definitions.js
nicht richtig dargestellt- Diese sollten daher vermieden oder an die ANSI-Zeichenkodierung angepasst werden
Nach Fertigstellung der Blöcke in der Google Block Factory können Sie den Code aus dem Bereich Block Definition
kopieren.
Dieser sollte, in Form von Javascript-Code, am Ende der /var/www/html/BIPES/ui/core/block_definitions.js
angehängt werden.
Im letzten Schritt müssen für die Blockdefinition jeweils noch eine Zeile aus dem Initialisierungsblock und dem Getter Block angepasst werden:
- Initialisierungsblock:
.appendField(new Blockly.FieldVariable(null), "NAME");
wird zu.appendField(new Blockly.FieldVariable("", null, ["motor"], "motor"), "NAME");
- Getter:
.appendField(new Blockly.FieldVariable("item"), "NAME")
wird zu.appendField(new Blockly.FieldVariable("", null, ["motor"], "motor"), "NAME")
Je nach Modultyp muss motor
durch einen anderen, selbst gewählten Typ ersetzt werden.
Dieser Typ muss ebenfalls mit dem output type
des Getters sowie dem value input
bei der Abfrage der Modul-IDs übereinstimmen.
Für Informationen zu weiteren Anpassungsmöglichkeiten, besuchen Sie die offizielle Google Blockly Dokumentationsseite.
Da BIPES sich auf die Verwendung von Python stützt, muss die Blocklogik als Python Entwurfsmuster exportiert werden.
Wurde für die Erstellung der Blockdefinitionen Block Factory von Google verwendet,
können Sie den Programm-Code aus dem Bereich Generator Stubs
kopieren (Achtung: Python auswählen!)
und am Ende von /var/www/html/BIPES/ui/core/generator_stubs.js
einfügen.
Initialisierungsblock:
Blockly.Python['iot_motor_init'] = function(block) {
Blockly.Python.definitions_['import_Motor'] = 'from module_Motor import Motor';
var variable_name = Blockly.Python.nameDB_.getName(block.getFieldValue('NAME'), Blockly.Variables.NAME_TYPE);
var value_i2c = Blockly.Python.valueToCode(block, 'i2c', Blockly.Python.ORDER_ATOMIC).replace('(','').replace(')','');
var value_scl = Blockly.Python.valueToCode(block, 'scl', Blockly.Python.ORDER_ATOMIC).replace('(','').replace(')','');
var value_sda = Blockly.Python.valueToCode(block, 'sda', Blockly.Python.ORDER_ATOMIC).replace('(','').replace(')','');
var code = variable_name + ' = Motor(' + '\'' + variable_name + '\'' + ', ' + value_scl + ', ' + value_sda + ', ' + value_i2c + ', network_manager.mqtt_client, network_manager.device_id)\n';
return code;
};
Verglichen zum Entwurfsmuster wurden folgende Anpassungen vorgenommen:
-
.replace('(','').replace(')','')
wurde für alle Angaben der PIN bzw. I2C-Adresse gesetzt, um überflüssige Klammern zu entfernen - Mit
Blockly.Python.definitions_['import_PIR'] = 'from module_Motor import Motor';
kann ein Import Statement im generierten Code gesetzt werden - Die Variable
code
dient der Initialisierung der Klasse. Dies kann je nach verwendetem Modul in der Implementation abweichen - Hierzu können Sie sich die anderen, entwickelten Module anschauen, die beispielsweise bei der Initialisierung eine PIN-Angabe benötigen
Als besondere Funktion wird hier beispielhaft die Funktion set_speed
aufgeführt, da diese einen optionalen Parameter hat, der geprüft werden muss:
Blockly.Python['iot_motor_set_speed'] = function(block) {
var value_motor_module_id = Blockly.Python.valueToCode(block, 'motor_module_id', Blockly.Python.ORDER_ATOMIC);
var value_speed = Blockly.Python.valueToCode(block, 'speed', Blockly.Python.ORDER_ATOMIC);
var value_motor_index = Blockly.Python.valueToCode(block, 'motor_index', Blockly.Python.ORDER_ATOMIC);
if (value_motor_index == '') {
var code = value_motor_module_id + '.set_speed(' + value_speed + ')\n';
} else {
var code = value_motor_module_id + '.set_speed(' + value_speed + ',' + value_motor_index + ')\n';
}
return code;
};
Hier wurde eine if-Abfrage implementiert, die je nach Vorhandensein des Motor-Indexes den generierten Code anpasst.
So könnte, unter der Annahme, dass der Anwendende die Variable i und eine Geschwindigkeit von 10 gewählt hat, der generierte Code folgendermaßen aussehen:
i.set_speed(10);
Bei Angabe eines Motor-Indexes (z.B. 0) sieht der Code hingegen so aus:
i.set_speed(10, 0);
Die anderen Funktionsblöcke lassen sich analog programmieren.
Der Getter-Block sieht bei allen implementierten Blöcken gleich aus:
Blockly.Python['iot_motor_get_module_id'] = function(block) {
var variable_name = Blockly.Python.nameDB_.getName(block.getFieldValue('NAME'), Blockly.Variables.NAME_TYPE);
var code = variable_name;
return [code, Blockly.Python.ORDER_ATOMIC];
};
Hier wurde lediglich die Operatorenpriorität von ORDER_NONE
auf ORDER_ATOMIC
gesetzt und code
gibt den Variablen-Namen aus.
Für Informationen zur Bedeutung der Operatorenpriorität, siehe: Operator Precedence.
Wurde die Logik aller Blöcke implementiert, müssen noch Anpassungen an dem, in der selben Datei vorhandenen, iot_setup-Block vorgenommen werden.
for(var i=0;i<blocks.length; i++) {
switch(blocks[i].type)
{
case 'iot_rgb_led_init':
case 'iot_smart_button_init':
case 'iot_buzzer_init':
case 'iot_dht_init':
case 'iot_sht_init':
case 'iot_pir_init':
case 'iot_motor_init':
var variable_name = (Blockly.Python.nameDB_.getName(blocks[i].getFieldValue('NAME'), Blockly.Variables.NAME_TYPE));
code += Blockly.Python.INDENT + 'dict[' + variable_name + '] = ' + variable_name + '\n';
break;
}
}
Hier muss noch mit case 'iot_motor_init':
der Initialisierungsblock als Fall hinzugefügt werden, damit die Programmlogik den neuen Block berücksichtigt.
Wenn ein spezielles Handling im Hauptloop gewünscht ist (z.B. wiederholtes Abfragen eines PIN-Zustandes),
muss ein entsprechender Fall in der darauffolgenden for-Schleife definiert werden.
Als nächstes sind Anpassungen an der Datei /var/www/html/BIPES/ui/core/code.js
notwendig, damit die Modul-IDs wie folgt in der Toolbox auftauchen:
Hierfür gibt es in der code.js
eine Funktion createFlyout
, zu der folgender, weiterer Fall hinzugefügt werden muss:
case 'motor':
var blockText = '<block type="iot_motor_get_module_id"><field name="NAME" variabletype="motor">';
blockText += variableModelList[i].name;
blockText += '</field></block>';
var block = Blockly.Xml.textToDom(blockText);
xmlList.push(block);
break;
Das Hinzufügen dieses Abschnittes geschieht unter Veränderung des Fallnamens, Blocktyps und Variabletyps bei den anderen Modulen analog.
Um Blöcke robuster gegen Falscheingaben zu machen oder um Schüler*innnen bzw. Studierenden eine Hilfestellung beim Programmieren zu geben,
können die Eingaben validiert werden:
Diese Blocklogik wird ergänzend in /var/www/html/BIPES/ui/core/block_definitions.js
für die einzelnen Blöcke vorgenommen.
Als Beispiel dient erneut die Funktion set_speed
, die nun um eine Blockvalidierung sowie einer Werte-Validierung erweitert wurde:
Blockly.Python['iot_motor_set_speed'] = function(block) {
var value_motor_module_id = Blockly.Python.valueToCode(block, 'motor_module_id', Blockly.Python.ORDER_ATOMIC);
var value_speed = Blockly.Python.valueToCode(block, 'speed', Blockly.Python.ORDER_ATOMIC);
var value_motor_index = Blockly.Python.valueToCode(block, 'motor_index', Blockly.Python.ORDER_ATOMIC);
// Blockvalidierung, ob alle benötigten Input-Felder gesetzt/gefüllt sind
if (!this.getInputTargetBlock('motor_module_id') || !this.getInputTargetBlock('speed')) {
block.setWarningText("Nicht alle benötigten Werte ausgefüllt!");
} else {
block.setWarningText();
}
// Validierung Werte der Input-Felder
// Wenn Wert kleiner als -100, dann wird Zahl auf -100 gesetzt
if (value_speed < '-100') {
this.getInputTargetBlock('speed').setFieldValue(-100, 'NUM');
//console.log(firstChild);
// Wenn Wert größer als 100, dann wird Zahl auf 100 gesetzt
} else if (value_speed > 100) {
this.getInputTargetBlock('speed').setFieldValue(100, 'NUM');
// Wenn kein Integer, dann werden die Nachkommastellen entfernt
} else if (!Number.isInteger(value_speed)) {
this.getInputTargetBlock('speed').setFieldValue(Math.trunc(value_speed), 'NUM');
}
if (value_motor_index == '') {
var code = value_motor_module_id + '.set_speed(' + value_speed + ')\n';
} else {
var code = value_motor_module_id + '.set_speed(' + value_speed + ', ' + value_motor_index + ')\n';
}
return code;
};
Entscheidend ist, dass bei der Funktion getInputTargetBlock
der zu prüfende Blockname (z.B. speed
) übergeben wird, nicht die Variable value_speed.
Der Blockname speed
wurde als value input
bei der Blockdefinition in Google Block Factory angegeben.
Die Variable wird für die Prüfung der eingegebenen Werte verwendet.
Damit dem Anwendenden die programmierten Blöcke angezeigt werden, müssen diese noch in die Toolbox eingebunden werden.
Da die Hardware Grundlage für IoT4School das Entwicklungsboard Wemos D1 mini ist, welcher mit dem ESP8266-Chipsatz ausgestattet ist,
werden Anpassungen an der Datei /var/www/html/BIPES/ui/toolbox/esp8266.xml
vorgenommen.
Dies ist für die Geräteauswahl innerhalb von BIPES entscheidend, da in der esp8266.xml
eingebundene Blöcke nicht sichtbar sind,
wenn beispielsweise der ESP32 als Gerät ausgewählt wird.
Die Blockanbindung wird anhand des Initialisierungsblocks gezeigt:
<category name="Motor" colour="185">
<block type="iot_motor_init">
</block>
</category>
Entweder wird eine neue Kategorie (wie im Code zu sehen) erstellt oder der Block-Typ wird in eine vorhandene Kategorie eingebunden.
Darauf zu achten ist, dass der Name des Block-Typs exakt mit den Überschriften dieses Blocks aus block_definitions.js
und generator_stubs.js
übereinstimmen.
Die Gleichheit zwischen block_definitions.js
und generator_stubs.js
ist durch die Verwendung von Google Block Factory gegeben.
Nach Einbindung sollte der Block in der Toolbox auftauchen:
"Shadow Values" helfen dem Anwendenden dabei auf den ersten Blick zu sehen, die Einbindung welcher Blöcke/Eingabe welcher Werte für die korrekte Funktion des Blockes nötig sind.
Anhand des Initialisierungsblocks iot_motor_init
wird die zusätzliche Einbindung der "Shadow Values" veranschaulicht:
<category name="Motor" colour="185">
<block type="iot_motor_init">
<value name="i2c">
<shadow type="math_number">
<field name="NUM">0x30</field>
</shadow>
</value>
<value name="scl">
<shadow type="pinout">
<field name="PIN">5</field>
</shadow>
</value>
<value name="sda">
<shadow type="pinout">
<field name="PIN">4</field>
</shadow>
</value>
</block>
</category>
-
value name
= Name der Variable, die mit einem "Shadow Value" versehen werden soll -
shadow type
= Überschrift des Blocks, der als "Shadow Value" dienen soll -
field name
= Feld, welches für das "Shadowing" aus demshadow type
genommen werden soll
So sieht es für die Anwendenden aus:
Wurden Blöcke für neue Module (Sensoren/Aktoren) entwickelt, jedoch keine Anpassungen an der Datei cb_handler.py
vorgenommen, erscheint, beim Empfang einer Nachricht für das Modul, folgende Nachricht:
Öffnen Sie die Datei cb_handler.py
aus dem heruntergeladenen Repository und editieren Sie die Funkion module_callback
, indem Sie eine Abfrage nach dem vorliegenden Muster ergänzen:
Transferieren Sie die aktualisierte Datei auf Ihren Wemos D1 mini Mikrocontroller.
Um weitere Informationen zur Aktualisierung Ihres Wemos D1 mini zu erhalten, schauen Sie hier: Wemos D1 Mini Ersteinrichtung und Konfiguration.