Posts mit dem Label C werden angezeigt. Alle Posts anzeigen
Posts mit dem Label C werden angezeigt. Alle Posts anzeigen

Dienstag, 27. August 2019

Gästebuch mit Wemos


Als ich mit meiner Freundin zum Geocaching unterwegs war, fiel mir ein das ich ein Programm schreiben wollte, das die Funktion eines Gästebuch ausführt und das auf einem Mikrocontroller. Wer Geocaching kennt, der weis dass man nach einem gefundenen Geocach sich auf einem beiliegenden Zettel eintragen kann. Und genau das will ich als digitale Form abbilden.

Was brauche ich:
  • Wemos
  • SD Shield
  • SD Karte

Anforderung
Für ein Gästebuch, soll ein (Nick)Name und Datum hinterlegt werden. Datum wird Automatisch mit dem Eintrag gesetzt, so das nur der Nickname eingetragen wird. Hier sollte der Wemos als eigenständiger Access-Point funktionieren, so das man sich mit dem Smartphone verbinden kann. Auf der Webseite sollten zwei Textboxen für die Eingabe des Namens und Datum vorhanden sein. Das Speichern erfolgt dann über ein Button. Neben der Eintrage Möglichkeit, soll auch eine Tabelle mit bereits bestehenden Einträgen zusehen sein. Und natürlich sollte der Zugang per Smartphone funktionieren.

Webseite schreiben
Leider kann die Webseite nicht mit Bootstrap aufgebaut werden. Na ja, eigentlich schon, aber das wäre ein eigenes Thema. Diesmal wird die Seite wieder Klassisch und einfach aufgebaut nach Standard HTML Bordmitteln.
Die Webseite wird mit in dem Programmcode eingebettet, so das diese nicht von der SD Karte erst gelesen werden muss. Zudem sollte bei defekter SD Karte trotzdem etwas angezeigt werden, wie eine Fehlermeldung.
Da die Darstellung des Code mit HTML auf dem Blog sich schwierig gestaltet, kann der Programmcode für den Seitenaufbau auf dem Github Repository betrachtet werden [Link zu WebsiteContent].
Die Seite habe ich zuvor ganz normal in HTML geschrieben und das Ergebnis im Browser immer wieder betrachtet. Erst danach kam die Seite in den Programmcode, das jedoch optisch grausig aussieht. Daher habe ich den Teil in einen eigenen Tab bzw. Code-Datei verfrachtet. Am Ende kam fast das selbe Ergebnis wie in den Folgendem Bild zu sehen ist.



Das Aktuelle Datum wird durch ComboBoxen ermöglicht, in dem man das aktuelle Datum und Uhrzeit einrichten kann. Das testen auf dem Windows Phone erfolgt ohne Probleme. Auf dem Iphone jedoch nicht.








SD Karte prüfen
Mit dem Start soll zunähst geprüft werden, ob die SD Karte verfügbar ist. Zuerst wird jedoch geprüft, ob bereits die Datei mit den Gästeeinträgen vorhanden ist. Wenn nicht, dann wird diese angelegt.

 void InitSdCard() {  
  SD.begin(mChipSelect);  
  if(!SD.exists(mGuestList)){  
   Serial.println("Create file...");  
   File dataFile = SD.open(mGuestList, FILE_WRITE);  
   dataFile.close();  
   Serial.println("...finish");  
  }  
  else {  
   Serial.println("the guest list exist");  
  }  
  if(!mCard.init(SPI_HALF_SPEED, mChipSelect)) {     // initialize sd card and check insert sd card  
   Serial.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");  
   Serial.println("\t\tERROR: can not init sd card");  
   Serial.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");  
   return;  
  }  
  if(!mVolume.init(mCard)) {               // initialize sd card  
   Serial.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");  
   Serial.println("\t\tERROR: Could not find FAT16/FAT32 partition.\nMake sure you've formatted the card");  
   Serial.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");  
   return;  
  }  
  Serial.println("SD Card: Exist and partion was found");  
 } 

Wemos zum Access Point machen
Auf der Webseite von Wemos ist es eigentlich schon beschrieben, aber zur Vollständigkeit, nehme ich das hier denoch auf. Um den Wemos zum Access Point zu machen, bedarf dies nur weniger Code zeilen. In den Globalen Variablen muss dann nur die Instanz vom WiFiServer gehalten werden. Für den Zugang wird kein Passwort festgelegt.

 void StartWebserver() {  
  Serial.println("Start access point");  
  WiFi.mode(WIFI_AP);            
  byte setMode = 0;               // 0 = disable, 1 = enable router information  
  wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &setMode);  
  WiFi.softAP("VisitorsBook", "");  
  mServer.begin();                // Start the HTTP Server  
  Serial.print("Ip Address: ");  
  Serial.println(WiFi.softAPIP());// Send the IP address of the ESP8266 to the computer  
 } 

Aufruf der Seite
Mit dem Aufruf der Seite wird zunächst geprüft, ob der Benutzer einen Request sendet. Wenn nicht, wird die Methode zum Speichern umgangen. Den Request nach dem Inhalt zuprüfen, sieht hier etwas kryptisch aus, das jedoch nur schlimm aussieht. Beim auslesen des Datums wird ein Doppelpunkt mit '%3A' ausgefüllt, der sich jedoch einfach mit einem Replace wieder in einen Doppelpunkt gesetzt werden kann. Nach dem aufbereiten des Namens und Datum kann das Ergebnis gespeichert werden.

void printOnWebside() {  
  WiFiClient webclient = mServer.available();  
  if (!webclient) {  
   return;  
  }  
  String request = webclient.readStringUntil('\r');  
  webclient.flush();  
  int indexGuest = request.indexOf("guestname=");  
  if(indexGuest != -1) {  
   int indexEndGuestname = request.indexOf('&');  
   String guestName = request.substring(indexGuest + 10, indexEndGuestname);  
   int indexSetTime = request.indexOf("settime=");  
   int indexEndSettime = request.indexOf(" HTTP");  
   String settime = request.substring(indexSetTime + 8, indexEndSettime);  
   // Example: 2019-08-25T19%3A40  
   // filter technical colon  
   settime.replace("T", " ");  
   settime.replace("%3A", ":");  
   WriteGuest(guestName, settime);  
  }  
  BuildWebside(webclient);  
 } 

Datenspeichern
Das Speichern erfolgt Zeilenweise. Hier wird der Eintrag mit einem Zeilenumbruch in die Datei hinzugefügt. Zuerst hatte ich nur Name und Datum, aber das Parsen wäre relativ aufwendiger geworden. Daher wird die Zeile bereits mit dem Ziel HTML Format eingetragen. (Wegen HTML Inhalt ist der folgende Code nur ein Bild)



















Daten abrufen
Ist etwas lang die Methode. Aber auch nur, weil wieder etwas HTML mit eingeflossen ist, um die Anzahl der gespeicherten Einträge wieder zugeben. Anschließend werden die Gast Einträge aus der Datei Zeilenweise mit dem bereits HTML Anteil abgerufen und hinzugefügt.































Offene Punkte
Das Hübsch machen der Webseite habe ich weggelassen, sowie das Fehlerbehandlung. Denn ein Eintrag kann ohne Namen und Datum erfolgen und führt dazu, dass der Eintrag in der Liste wirklich leer sein kann. Zudem war auch ein RTC Timer angedacht, der jedoch für den ersten Schritt nicht benötigt wird. Im Main Branch habe ich diesen Teil stehen gelassen.
Und dann noch der Fehler, dass das Datum nicht geht, wenn versucht wird vom Iphone ein Request zu senden.


Datenschutz
Die Anmeldung erfolgt anonym, weil hier keine Personenbezogenen Daten erhoben werden. Um genau zu sein, dürfen hier keine Personendaten erhoben werden, wie Email (Macht in diesen Kontext auch kein Sinn), Namen und Geburtsdatum. Für den Eintrag reicht dann der Hinweis:

Mit meinem Eintrag ins Gästebuch erkläre ich mich einverstanden, dass dieser Beitrag mit Datum und Uhrzeit auf der Gästebuch-Seite hier Lokal öffentlich lesbar wird.

Weitere Informationen findet ihr auf der Seite zu "Datenschutz auf Webseiten"

Wie immer das ganze Beispiel auf meinem Github Repository


Sonntag, 21. Juli 2019

Daten in JSON speichern auf dem Wemos


Um seine Daten auf eine SD Karte zu speichern, muss am Anfang klar sein, in welches Format gespeichert werden soll. Das ist dann besonders entscheidend, wenn die Daten wieder weiter verarbeitet werden.
Wenn für den Moment nur ein Wert gespeichert wird, dann reicht Zeilenweise. Bei mehreren Werten müssen die durch ein Zeichen getrennt werden. Das kann dann über Leerzeichen, Semikolon oder Komma sein, bzw. CSV entsprechend. CSV kann prima in Excel betrachtet und weiter verarbeitet werden. Will man die Daten aber weiter verarbeiten, müssen am Ende viele Zeichenketten zu Werten umgewandelt werden. Grundsätzlich ist das nicht verkehrt, aber man muss sich selbst Standards festlegen, um Aufwände gering zu halten.

Was benötigt wird:
  • Arduino oder Wemos
  • SD Shield
  • SD Karte

Vorhaben
Für das Ziel Projekt werden einige Werte aus den Messungen gespeichert. Daher wird ein Daten Objekt benötigt, dass alle Werte enthält. Um dieses nicht im Arbeitsspeicher zu halten, muss der Inhalt persistiert werden und später auf Abruf wieder hergestellt werden.

Datenformat zum Speichern
Die Entscheidung war für mich schon ein eigenes Thema Wert. Neben CSV blicke ich seit langem wieder auf JSON und XML. Nachdem ich ein wenig gesucht habe, kam ich zu dem Entschluss JSON zu verwenden. Für das JSON Format existiert bereits eine fertige Bibliothek.



Was man beachten sollte
Nach einigen Tutorials und veralteten Beispielen, hatte ich die nötigen Informationen zusammen. Zum Schreiben und lesen der Daten können zwei verschiedene Klassen zum Verarbeiten von JSON verwendet werden.
  • StaticJsonDocument
  • DynamicJsonDocument

Für meinen Einsatz reicht der StaticJsonDocument aus. Dieser legt die Daten im Stack ab und verbraucht durch die Eingrenzung auch weniger Arbeitsspeicher. Das ist bedingt durch den Mikrocontroller, vorzugsweise zu verwenden.
Mit DynamicJsonDocument werden die Daten im Heap abgesetzt. Für mein Beispiel konnte ich evaluieren, das nur zwölf Datensätze zuverlässig verarbeitet werden, ohne dass das Programm anhält bzw. abstürzt.

Datenobjekt
Der Zeitpunkt der Aufnahme und zu speichernden Werte werden für den späteren verlauf verwendet. Sekunden habe ich hier nicht einbezogen, da solche Zeitlich genauen Messungen für die Darstellung keinen Nutzen hat. Die Stunden und Minuten werden wiederum verwendet, um später Durchschnittswerte zu berechnen. Der Tag, Monat und Jahr werden ebenfalls für die Durchschnittswerte verwendet.

 class MeasureData {  
  public:  
   int16_t Day;  
   int16_t Month;  
   int16_t Year;  
   int16_t Hour;  
   int16_t Minute;  
   float Temperature;  
   float Humidity;  
   float Pressure;  
 };  


Speichern (Serialisieren)
Bevor die Daten in das JsonObject geschrieben werden können, muss die Größe definiert werden. Im folgenden Programmcode in "JSON_ARRAY_SIZE" der Wert 1 eingetragen, weil hier nur einmal gespeichert wird. Im Github Repository zeige ich dieses mit mehreren Datensätzen.

 void SaveData() {  
  const size_t capacity = JSON_ARRAY_SIZE(1) * JSON_OBJECT_SIZE(8);        
  StaticJsonDocument jsonBuffer;     // set the content size  
  JsonArray data = jsonBuffer.to();   // setup array to write  
  JsonObject measure = data.createNestedObject();  // created to add a new object  
  measure["Day"] = 12;  
  measure["Month"] = 12;  
  measure["Year"] = 2019;  
  measure["Hour"] = 12;  
  measure["Minute"] = 12;  
  measure["Temperature"] = 21.4f;  
  measure["Humidity"] = 50.5f;  
  measure["Pressure"] = 12345.6f;  
  File file = SD.open(mFilename, FILE_WRITE);  
  serializeJson(jsonBuffer, file);       // write json document to file  
  file.close();   
 } 

Öffnen (Deserialisierung)
Genau wie beim Speichern, wird ebenfalls die Kapazität angegeben. Dann kann die eingelesene Datei aus dem JSON Inhalt eingelesen und als JsonObject abgerufen werden.

 void OpenData() {  
  File file = SD.open(mFilename);  
  String content = file.readString();  
  const size_t capacity = JSON_ARRAY_SIZE(1) * JSON_OBJECT_SIZE(8);        
  StaticJsonDocument doc;  
  deserializeJson(doc, content);  
  JsonObject measure = doc[0];  
  Serial.print(" Day:"); Serial.print((int16_t)measure["Day"], DEC);  
  Serial.print(" Month:"); Serial.print((int16_t)measure["Month"], DEC);  
  Serial.print(" Year:"); Serial.print((int16_t)measure["Year"], DEC);  
  Serial.print(" Hour:"); Serial.print((int16_t)measure["Hour"], DEC);  
  Serial.print(" Minute:"); Serial.print((int16_t)measure["Minute"], DEC);  
  Serial.print(" Temperatur:"); Serial.print((float)measure["Temperature"], DEC);  
  Serial.print(" Humidity:"); Serial.print((float)measure["Humidity"], DEC);  
  Serial.print(" Pressure:"); Serial.println((float)measure["Pressure"], DEC);  
 }  


Fazit
Das JSON Format kann ich jeden nahe legen, dieses zu verwenden. Sobald mehrere Daten erfasst und gespeichert werden, ist der Aufwand beim Einlesen nahezu gleich. Das mag anfangs etwas ungewohnt sein. Spätestens wenn ihr Daten aus einer CSV einlesen wollt, das innerhalb des Mikrocontroller Bereiches geschieht, spart ihr eine Menge Zeit für das Entwickeln einer Methode, dass die Daten einließt.

Wie immer ein Beispiel:

Samstag, 7. Juli 2018

Wetterstation und Daten Logger


Immer die Aktuellen Messdaten abzurufen ist schön, aber auf Dauer würde man doch gerne sich ansehen wie sich das Wetter über einen Zeitraum verändert hat. Man könnte natürlich immer zu jeder Stunde nach sehen und sich das in der Excel Tabelle Notieren. Besser wäre der Vorgang, ein Programm zu verwenden das im vordefinierten Intervall die Messdaten automatisch speichert.


Wemos Codebeispiele
Die kurz Beschreibung, wie die Daten auf die SD Karte geschrieben werden, ist auf dem Github zu Wemos zu finden.

Simple übernommen
Ich habe das Codebeispiel übernommen und entsprechende Anpassungen und Erweiterungen eingesetzt. Im wesentlich wird am Anfang das SD Shield initialisiert und geprüft, ob eine SD Karte eingesetzt ist. Über den Seriellen Monitor wird der Typ und die Kapazität der SD Karte abgezeigt. Wenn die Zieldatei noch nicht vorhanden ist beim Speichern, wird die Datei "weather.csv" angelegt und bekommt in die erste Zeile die Header Info zu den Spalten.

Abruf der gespeicherten Daten
Nach dem Initialisieren und der ersten Messungen, werden im Beispiel von mir, alle zehn Sekunden die aktuellen Daten auf die SD Karte gespeichert. Die Daten werden in einer CSV Datei angehängt. Hierbei wird die Datei geöffnet und der neue Datensatz in eine neue Zeile geschrieben. Um später die Datei zu entnehmen, muss die SD Karte aus dem Shield entnommen werden und am PC mit einem SD Karten Slot eingelesen werden.

Problem mit Excel
Öffnet man die Datei in Excel, so kann es sein das Werte als Datum Formatiert werden. Das liegt mitunter an der verwendeten Zeichen für Nachkommastelle. Technisch gesehen ist alles Richtig, nur Excel macht etwas zu viel des Guten.


Statt die Datei per Doppel klick zu öffnen, muss die Datei über Excel aufgerufen werden. Über den Reiter "Daten" und dann Daten abrufen -> Aus Datei -> Aus Text/CSV  kann die Datei importiert werden.


Nach dem Import öffnet sich ein neuer Dialog. In der dritten Combobox wählt ihr Dateitypen nicht ermitteln und anschließend kann dann auf den Button Laden geklickt werden.


Leider kann das hier nur zum Betrachten verwendet werden. Eine Darstellung in einem Diagramm geht hier ebenfalls nicht. Zugegeben empfand ich dies sehr nervig und schrieb hierzu ein einfachen Konverter, der mir die Werte in das Zielformat bringt. In dem vorigen Bild ist auch zu sehen, dass die Nachkommastelle komplett entfernt wurde.

Hat nur eine Aufgabe
Das Tool zum konvertieren der CSV Datei ist ebenfalls auf meinem Repository zu finden. Momentan kann das Tool nur eine Sache und zwar die Zahlen mit einem Punkt als Nachkommastelle umzustellen zu einem Komma. Wenn die Anforderungen steigt, dann werde ich sicherlich mehr daraus machen.


Im Diagramm betrachten
Kommen wir zum eigentlich Ziel. Die gemessenen Daten können in Excel im Einzelnen Betrachtet werden. Mit dem Anlegen einer Diagramm-kurve sieht man wie die Temperatur und Feuchtigkeit zu einander sich verändert haben. Der Luftdruck muss ein separates Diagramm haben, da die Werte im Hektopascal hinterlegt sind.


Immer noch nicht Rund
Das Automatische Aufzeichnen der Messdaten ist schon hilfreich. Aber an den gespeicherten Messungen ist keine Uhrzeit angegeben, so dass eine Zeitliche Orientierung nicht ermittelt werden kann. Und noch besser wäre, die Diagramme ebenfalls im Browser anzuzeigen. Aber dazu komme ich zu einem anderen Zeit.



Donnerstag, 31. Mai 2018

Wetterstation / Gewächshaus mit Webzugriff


Mit der Zeit kommen für das Beispiel Programm einer Wetterstation neue Funktionen hinzu. Somit lässt sich mit dem aktuellen Stand, den Wemos mit dem WLAN verbinden und über einem Browser können die Messdaten abgelesen werden.

Momentan wird das Programm mit dem Wemos in meinem Gewächshaus verwendet. Natürlich ist mir bewusst, dass die Elektronik durch die hohe Luftfeuchtigkeit beschädigt werden kann und ist auch nicht zu empfehlen. Ansonsten müsse die Elektronik mit Plastikspray oder ähnlichen eingesprüht werden.



Webzugriff
Die Implementierung einer Webseite war ein Muss. Für die Realisierung muss man nur wissen, wie man auf eingehende Anfragen reagiert und wie die Webseite zurück gegeben wird. Die Webseite erfüllt ihren Zweck und Zeigt die aktuellen Messdaten. Das Aktualisieren der Messdaten erfolgt durch den erneuten Aufruf des Wemos, bzw. die Seite neu laden.


WLAN
Damit sich der Wemos mit WLAN verbinden kann, muss die SSID (Euer WLAN Name) und das Passwort eingetragen werden. Von den Sicherheitsoptionen sollte mindestens WPA-PSK funktionieren. Die IP Adresse und Gateway müssen bei Bedarf ebenfalls angepasst werden.

Zuletzt
Auf dem Bild mit der Webseite ist ein Vierter Wert aufgeführt. Der Vierte Wert ist der Grund für meinen Test im Gewächshaus und Zeigt einen Messwert von einem Bodensensor an. Aber dazu komme ich dann zu meinem nächsten Blogpost.


Sonntag, 18. März 2018

Analog Anzeige auf dem TFT 1.8" zeichnen


Eine Analog Anzeige ist schon etwas Nostalgisch und haben eigentlich wegen ihrer ungenauen Ablesbarkeit keinen nutzen mehr. Aber das sollte kein Hindernis sein, dennoch solche Analogen Display auf einem Arduino zu Zeichnen. Das man dazu nicht viel machen muss, zeige ich in diesen Post mit einem Code Schnipsel.
  • Arduino UNO / Duemilanove
  • TFT Display 128x160
  • Ggf. TFT Display Shield

Die Bibliothek für TFT hat fast alle Methoden, um das Zeichnen einer Analogen Anzeige zu ermöglich. Damit man nicht zu sehr in Grafik Experimente verfällt, ist es hilfreich sich vorher eine Skizze zu machen. Mit den Rechtecken und Kreisen und einer richtigen Reihenfolge, kann die Grundlage der Analoganzeige gezeichnet werden. Jedoch der Interessante Teil ist, das Animieren der Anzeigenadel.

Anzeigenadel
Der Aufbau der Anzeigenadel ist sehr überschaubar und kommt mit einfacher Schulmathematik aus. Für das Beispiel wird allerdings nur eine Linie gezeichnet. Eine Pfeilspitze würde beim Zeichnen mehr Zeit aufwenden und eine flüssige Darstellung wäre ggf. nicht möglich. Zumindest nicht mit der Bibliothek, da das Pixelzeichnen nicht in ein Backbuffer des TFT geschrieben werden kann.
Später beim Zeichnen eines Wertzustandes, muss der vorige Stand zurück gesetzt werden. Hierbei reicht es die Anzeigenadel mit dem vorigen Wert nochmal mit der Hintergrundfarbe zu Zeichnen.

Code Schnipsel
Der folgende Programmcode Zeichnet nur die Anzeigenadel. Als Eingabewert wird der Pin A0 mit 'analogRead' ausgelesen. Die vollständige Analoganzeige findet ihr wieder auf meinem Github Repository.

 #include <SPI.h>  
 #include <TFT.h>  
 #include <Wire.h>  
 #define CS  10  
 #define DC  9  
 #define RST 8  
 TFT mScreen = TFT(CS, DC, RST);  
 float mValuePointer = 0.0;            
 float mValuePointerLast = 0.0;         
 void setup() {  
  Wire.begin();  
  mScreen.begin();  
  mScreen.background(0, 0, 0);  
  pinMode(A0, INPUT);   
 }  
 void loop() {  
  mValuePointer = ((float)analogRead(A0) / 1023.0) * 100.0;  
  mScreen.stroke(0, 0, 0);  
  renderValuePointer(40, 40, 25, mValuePointerLast);  
  mScreen.stroke(35, 200, 140);  
  renderValuePointer(40, 40, 25, mValuePointer);  
  mValuePointerLast = mValuePointer;  
  delay(10);  
 }  
 void renderValuePointer(int locationX, int locationY, float diffRadius, float val) {  
  float section = 2.0 * 3.14 / 100.0;  
  float sectionStart = section * 5;  
  float center = diffRadius / 2.0;  
  val = val + 50.0;  
  float x = diffRadius * cos(section * val) + diffRadius;  
  float y = diffRadius * sin(section * val) + diffRadius;  
  mScreen.line(locationX + diffRadius, locationY + diffRadius, locationX + x , locationY + y );  
 }  

Wenn ihr das auf eure Umgebung hoch lädt, dann sollte eine Line auf dem Bildschirm zu sehen sein. Das Zappeln ist praktisch gesehen ein Rauschen, wenn kein Pull-Up oder Pull-Down Widerstand gesetzt wurde auf Pin A0.


Kurzes
Wie ihr seht, steckt da nicht viel dahinter und schon schafft man etwas mehr Optik.



Sonntag, 25. Februar 2018

Aufräumen und Codestyle ändern


Irgendwann ist die Schmerzgrenze angekommen, dass der Code wie dieser im Bild geschrieben wurde und einige Schwierigkeiten beim Lesen bereiten. Fehlende Vorzeichen sowie Groß- und Kleinschreibung von Funktionsvariablen erschweren die Übersicht.

Neue Fassade
Der Programmcode den ich für diesen Teil zur Verfügung stelle, funktioniert wie bereits aus 'Von Händlern, Kisten und Münzen (Arduino Esplora, Part8)'. Der wesentliche Unterschied ist, dass der Code zurecht gerückt, größten Teils kommentiert und zusätzliche Trennlinien hinzugekommen sind. Natürlich werden in Zukunft auch alle anderen Beispiele nach diesem Codestyle geschrieben. Durch die Festlegung verschiedener Regeln beim Codeformat, sollte die Lesbarkeit deutlich verbessern.
Im Internet suchte ich einige Codebeispiele und las sie bezüglich dessen Aufbau und Formatierung. Zwar gibt es zu dem Thema bereits einige Codestyles, aber die Anforderung sollte ein Kompromiss für Hobby Entwickler sein.

Folgende Codeformat Vorgaben haben sich bei der Suche und Vergleich als Sinnvoll ergeben.


Beschreibung
Wie der Code aussehen könnte
1
If-Else Abfragen.
Auch wenn man sie in einer Zeile schreiben kann, ist die Form schlecht zu lesen. Auch wenn nur eine Sache behandelt wird.
if(stateValue > 0) {
    doAnything(stateValue);
}
2
Kommentare zu einer Zeile
Nach einigen Vergleichen von Kommentierten Programmcode, empfand ich die rechts neben dem Code stehenden Kommentare als angenehm. Eine Prämisse hat diese Form jedoch. Da keine Auto Funktion vorhanden ist, dass die Kommentare in gleicher Höhe einrückt ist der Vorgang mit etwas Fleiß verbunden.
int16_t stateValue = 0;     // local variable for todo anything
3
Kommentare über einer Funktionsmethode
Scrollt man durch den Code, dann helfen Trennlinien zwischen den Methoden. Wie lang die Trennlinie selbst sein soll, kann soweit bestimmt werden, wie sie für die Sichtbarkeit benötigt wird.
// ============================================
void loop() {
  mCounter++;
}
4
Zusammenhängende Funktionsvariablen
Zusammengenutzte Variablen sollten auch möglich nahe zusammenliegen und auch ggf. eine Beschreibung haben.
// ============================================
// function variables

int16_t mCounter = 0;   // it is only a counter
5
Wertetypen
Ist sicherlich mühseliger zu schreiben, aber gewährleistet das auch mit anderen Compiler dasselbe Ergebnis kommt. Das kann beim Verwenden eines 16Bit oder 32Bit Microcontrollers vorkommen, das beim kompilieren die Wertetypen anders übersetzt. Z.B. würde statt eines 'int16' ein 'int32' eingesetzt. Bei Berechnungen mit größeren Werten kann es da schon zu Rechenfehler kommen.
int8_t value8 = 123;
int16_t value16 = 123;
int32_t value32 = 123;

uint8_t valueU8 = 123;
uint16_t valueU16 = 123;
uint32_t valueU32 = 123;
6
Funktionsvariablen
Obwohl man möglichst versucht keine Globalen Variablen zu verwenden, bleibt es nicht aus. Bisher waren die von der Schreibweise von lokalen und globalen nicht zu unterscheiden.
Ein 'm' davor und der Variablenname groß geschrieben und schon lassen sich funktionsvariablen leichter zuordnen.
int8_t mMyFunctionVariable = 123;

void FunctionAndLocalVariable() {
  int8_t myLocalVariable = 123;
}
7
#define
Um diesen Wert wiederum von den Funktionsvariablen zu unterscheiden, werden diese vollständig groß geschrieben. Bei mehreren Wörter, sollte ein Unterstrich geschrieben werden.
#define MY_DEFINE_VALUE 123

Ordnung ist das halbe Leben
Im Wesentlichen sollten man sich bewusst machen, dass der Programmcode ab einer gewissen Größe immer einer Ordnung folgen sollte. Das Projekt mit dem Arduino Esplora ist noch relativ klein und leicht zu Händeln, da bietet sich die Änderungen der Code Darstellung gut an und hilft auch bei der Selbstreflektion des eigenen geschriebenen Codes. Abgesehen haben wir heute größere Bildschirme und mehr als Ausreichend Festplattenspeicher, so dass kompaktes zusammenrücken und einsparen von Zeichen nicht mehr Zeitgemäß ist.

Der Inhalt zu den Beispiel kann sich mit der Zeit Ändern bzw. erfolgen weitere Codeformat Beispiele.


Reference zu verschieden Codestyle Beschreibungen.

Sonntag, 21. Januar 2018

Motor Treiber für den Rover



Sicherlich hat jemand schon etwas geschrieben, dass die Ansteuern des Motor Shields vereinfacht. Dennoch möchte ich genau die Funktionsmöglichkeiten kennenlernen sowie auch den Schaltplan.

Seit Jahren liegt mein Rover unbenutzt in der Kiste und das möchte ich ändern. Aber damit dieser Betrieben werden kann, wird ein Motor Treiber benötigt. In diesem Fall ist es ein Motor Shield für Arduino. 
  • Arduino UNO / Duemilanove
  • Motor Shield
  • Externe Spannungsversorgung mit dem 9V oder einen zwei Zellen Lipo
  • DAGU Rover 5 Chassis 4WD

Antrieb
Der Rover von DAGU hat vier Motoren die unabhängig voneinander betrieben werden können. Allerdings werden die Gummi Ketten eingesetzt womit die Motoren zur einen Seite immer gleichzeitig laufen müssen.
Leider verfügt der verwendete Arduino nicht Ausreichend Pins für die Interrupt Funktion mit denen sich die vier Encoder vom Rover einlesen ließen. Die Umsetzung für das einlesen der Encoders würde besser mit einem Arduino MKR1000 funktionieren, wenn alle vier Encoder eingelesen werden sollen. Alternativ bei Verwendung mit den Gummi Ketten, reichen zwei Encoders aus und könnte mit einem Arduino Mega eingelesen werden.

Motor Treiber und Motor Shield
Eigentlich existiert für den Rover ein passender Motor Treiber, den ich jedoch nicht finden konnte. Daher kommt ein Motor Shield zum Einsatz, der genau alle Motoren Ansteuern kann. Für die Externe Spannungsversorgung wird ein zwei Zellen Lipo Akku verwendet. Auf dem Shield sollte der Jumper für PWR nicht gesteckt sein, solange der Arduino am USB mit dem PC Verbunden ist.
Die Anschlüsse für den Betrieb der zwei Servos werden aktuell für dieses Beispiel nicht verwendet. Somit bleiben Pin 9 und 10 offen.

Steuerung des Motor Shields
Für die Ansteuerung der Ausgänge M1, M2, M3 und M4 auf dem Motor Shield, werden insgesamt acht Pins benötigt. Zwei mehr, wenn man die Servo Ausgänge mit zählt, die jedoch nur weitergeleitete Pins sind.
Der Ausgang M1 besteht aus zwei Ausgängen die in diesem Beispiel immer mit M1_A und M1_B Bezeichnet werden. Daraus schließt sich, dass insgesamt acht Ausgänge geschaltet werden. Alle Ausgänge können jedoch mit einem PWM reguliert werden, aber es stehen nur vier zu Verfügung. Daher steht z.B. für M1 ein PWM Ausgang zur Verfügung. Aber rechnet man dies wieder hoch, kommen wir auf zwölf benötigte Pins.
Auf dem Shield wird ein 74HC595 Shiftregister verwendet, der selbst acht schaltbare Ausgänge hat. Dieser wird vom Arduino mit vier Pins beschaltet. Grundsätzlich reichen drei Pins, um einen Shiftregister anzusteuern. Der Vierte jedoch ermöglicht das explizite Abschalten aller Ausgänge.


Programmcode
Im folgenden Code Beispiel sind die wesentlichen Methoden aufgeführt. Geschaltet werden die Ausgänge M3 und M4 und maximalen PWM Signal Ausgang. Den gesamten Programmcode mit Kommentaren findet ihr wieder auf dem Github Repository.

 #define PIN_595_LATCH 12       // Pin connected to ST_CP of 74HC595  
 #define PIN_595_CLOCK 4        // Pin connected to SH_CP of 74HC595  
 #define PIN_595_DATA 8        // Pin connected to DS of 74HC595  
 #define PIN_595_SHIFT_REG_EN 7    // enable the Shiftregister  
 #define PIN_OUTPUT_M1 11       // pwm pin to control M1 output  
 #define PIN_OUTPUT_M2 3        // pwm pin to control M2 output  
 #define PIN_OUTPUT_M3 6        // pwm pin to control M3 output  
 #define PIN_OUTPUT_M4 5        // pwm pin to control M4 output  
 void setup() {  
  MotorShieldInitialize();  
 }  
 void loop() {  
  int motorsOn = 33; // schaltet M3 A und M4 A ein  
  int speedValue = 255; // Der Wert kann von 0 bis 255 gesetzt werden  
  SetRunMotors(motorsOn, speedValue, speedValue);  
  digitalWrite(PIN_595_SHIFT_REG_EN, LOW);  
  delay(2000);  
  digitalWrite(PIN_595_SHIFT_REG_EN, HIGH);  
  delay(1000);  
 }  
 void MotorShieldInitialize() {  
  pinMode(PIN_OUTPUT_M1, OUTPUT);  
  pinMode(PIN_OUTPUT_M2, OUTPUT);  
  pinMode(PIN_OUTPUT_M3, OUTPUT);  
  pinMode(PIN_OUTPUT_M4, OUTPUT);  
  pinMode(PIN_595_LATCH, OUTPUT);  
  pinMode(PIN_595_CLOCK, OUTPUT);  
  pinMode(PIN_595_DATA, OUTPUT);  
  pinMode(PIN_595_SHIFT_REG_EN, OUTPUT);  
  digitalWrite(PIN_595_LATCH, LOW);  
  digitalWrite(PIN_595_CLOCK, LOW);  
  digitalWrite(PIN_595_DATA, LOW);  
  digitalWrite(PIN_595_SHIFT_REG_EN, LOW);  
 }  
 void SetRunMotors(int motorsOn, int speedValueLeft, int speedValueRight) {  
  SetOutputValue(speedValueLeft, speedValueRight);  
  digitalWrite(PIN_595_LATCH, LOW);  
  shiftOut(PIN_595_DATA, PIN_595_CLOCK, MSBFIRST, motorsOn); //GetShiftValue(m));  
  digitalWrite(PIN_595_LATCH, HIGH);  
 }  
 void SetOutputValue(int speedValueLeft, int speedValueRight) {  
  analogWrite(PIN_OUTPUT_M1, speedValueRight); // right  
  analogWrite(PIN_OUTPUT_M2, speedValueRight);  
  analogWrite(PIN_OUTPUT_M3, speedValueLeft); // left  
  analogWrite(PIN_OUTPUT_M4, speedValueLeft);  
 }  

Steuerung
Das Beispiel stellt wieder eine Grundlage für weiteres. Der Spaß geht natürlich am besten weiter, wenn zur Steuerung des Rovers Sensoren eingesetzt werden oder über Funk die Steuerbefehle übermittelt werden.

Das selbe in .NET
Ein Codebeispiel mit C# und .NET Micro Framework beschreibe ich im Post "Motor Treiber für den Rover (.NETMF)"



Mittwoch, 17. Januar 2018

Zeichnen auf dem TFT mit dem XBox Controller

 
Im ersten Beispiel wurde beschrieben, wie man die Daten von einem Xbox Controller zu dem Arduino sendet und dabei einen Servo ansteuerte. Nachteil an der Code Konstellation war, dass nur ein Byte Wert übermittelt wurde und auch sehr instabil lief. Mit ein paar Anpassungen lässt sich dies einfach beheben. (siehe: Servo mit dem Xbox Controller steuern)

Hinweis
Das folgende Beispiel ist vom Aufbau relativ simple gehalten, so dass bei der Ausgabe Kompromisse eingegangen werden, wie z.B. die Daten Qualität über die Serielle Schnittstelle.

Mehr Daten Senden
Das MonoGame Projekt muss nur in der Update Methode angepasst werden. Zum einen wird geprüft, ob noch Bytes geschrieben werden. Dann können die Eingabewerte vom Xbox Controller eingelesen werden und über die Serielle Verbindung versendet werden.

 protected override void Update(GameTime gameTime)  
 {  
   GamePadState state = GamePad.GetState(PlayerIndex.One);  
   if (state.Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))  
   {  
     this.Exit();  
   }  
   if (this._serialPort.BytesToWrite == 0)  
   {  
     byte stickValueX = (byte)((state.ThumbSticks.Left.X + 1) * 90);  
     byte stickValueY = (byte)((state.ThumbSticks.Left.Y + 1) * 90);  
     byte buttonValuePaint = (byte)(state.Buttons.A == ButtonState.Pressed ? 10 : 0);  
     byte buttonValueReset = (byte)(state.Buttons.B == ButtonState.Pressed ? 10 : 0);  
     this._serialPort.Write(new byte[] { stickValueX, stickValueY, buttonValuePaint, buttonValueReset }, 0, 4);  
     this._serialPort.WriteLine(string.Empty);  
   }  
   Task.Delay(5);  
   base.Update(gameTime);  
 }  

Mehr Daten Empfangen
Die Lösung ist simple. Um mehrere Bytes einzulesen, müssen diese über einen Schleifen Vorgang vom Serial gelesen werden. Die Byte Werte werden dann zunächst in eine lokales Byte Array zugewiesen. Anschließend kann noch geprüft werden ob alle Bytes gelesen wurden. Die ersten zwei Werte sollten zumindest da sein, ansonsten kann der zu zeichnende Punkt (eigentlich ein Kreuz) nicht bewegt werden.

 bool ReadReceivedBytes() {  
  byte received[4];  
  int index = 0;  
  while(Serial.available() &gt; 0) {  
   if(index < 5) {  
    received[index] = Serial.read();  
   }  
   index++;  
  }  
  if(index < 4) {  
   return false;  
  }  
  mReceivedStickX = received[0];  
  mReceivedStickY = received[1];  
  mReceivedButtonA = received[2];  
  mReceivedButtonB = received[3];  
  return true;  
 }  

Ausgabe und Fehler
Die Ausgabe erfolgt diesmal nicht an einem Servo, sondern an einem TFT Display mit einer Auflösung von 160x128 Pixeln, der auch für den Arduino Esplora ebenfalls verwendet wird.
Die Übertragung ist nicht immer Fehlerfrei, womit für genauere Ergebnisse eine Fehlerbehandlung notwendig wird. Aber das wird in einem anderen Blog Post beschrieben. :)



Sonntag, 10. Dezember 2017

Von Händlern, Kisten und Münzen (Arduino Esplora, Part 8)


Ok, so richtiger Handel wird hier nicht stattfinden. Dafür reicht der Speicher nicht. Oder? In erster Linie sollen nur Grund Funktionen Umgesetzt werden. Ziel wird sein, wenn die Figur vor dem Händler oder einer Kiste steht, dann sollte sich der Inhalt Zeigen. Anschließend kann ein Objekt Ausgewählt und in die eigene Tasche übertragen werden. Leider passt das nicht alles in einen Blogpost, so dass der Inhalt mit der Waren Anzeige in einem späteren Post kommt.

Anforderung
Beim Händler können Gegenstände erworben werden und diese in Kisten abgelegt werden. Das erfordert einige mehr Programmcodezeilen und daher muss an der Stelle wieder eine neue Seite  mit dem Namen 'TraderComponent' angelegt werden.


Am Anfang werden die Werte für Händler und Kisten hinterlegt, die später über den Flashspeicher abgerufen werden. Die Münzen werden hier ebenfalls abgelegt als Funktionsvariable, wird aber erst in einen späteren Post weiter behandelt. (im Folgender Programmcode sind Kommentare und Bilddaten gekürzt, ggf. schaut ihr am besten in die Github Sourcen)

 // # Coins, im Besitz  
 int16_t coins = 25;  
 int16_t lastStateCoins = 0;  
 // # Common Text   
 // Begruessungstext (Sollte immer verschieden sein.)  
 const PROGMEM char traderStartText[] = "Hallo, was darf ich ihnen verkaufen?";  
 // Wenn zu wenig Muenzen zum Kaufen da sind  
 const PROGMEM char traderNotEnough[] = "Du hast nicht genug Muenzen.";  
 // Frage zum Kauf  
 const PROGMEM char traderYouWantToBuy[] = "Kaufen?";  
 // # Common Sprite  
 // Bild vom Handler / Die Farbe des Shirts, kann veraendert werden.  
 const PROGMEM byte traderSpriteFrontMen[160] = { … };  
 const PROGMEM byte traderSpriteFrontWomen[160] = { … };  
 const PROGMEM byte boxSpriteFront[100] = { … };  
 const PROGMEM byte coinSpiteIcon[49] = { … };  
 // # TRADER  
 // temp Variablen zum zwischen laden.  
 char traderName[1];  
 char traderdescription[1];  
 byte traderItems[4];  
 // '0' bedeutet immer nicht belegt.  
 // #######################################  
 // ID 1  
 // Name des Handlers  
 const PROGMEM char trader01Name[5] = "Surie";  
 // Kurze Beschreibung  
 const PROGMEM char trader01Description[11] = "Verkaeferin";  
 // Dinge zum verkauf  
 const PROGMEM byte trader01Items[4] = { 2, 0, 0, 0 }; // 2 = Kamera  
 // 0 = Taschenplaetze werden wie angegeben befullt.  
 // Stellen werden Stellenweise in Bit herausgenommen  
 byte trader01ItemsClear = 0;  
 // # Box  
 // '0' bedeutet immer nicht belegt.  
 // #######################################  
 // ID 1  
 // Name des Handlers  
 const PROGMEM char box01Name[11] = "Meine Kiste";  
 // Kurze Beschreibung  
 const PROGMEM char box01Description[25] = "Dinge die man so braucht.";  
 // Dinge zum verkauf  
 const PROGMEM byte box01Items[4] = { 3, 0, 0, 0 }; // 3 = Foto  
 void memCopyItems(byte arrayContent[], byte traderItemsClear) {  
   if(traderItemsClear == 128) {  
   traderItemsClear-= 128;  
   traderItems[0] = 0;  
  }  
  else { traderItems[0] = pgm_read_byte_near(arrayContent + 0); }  
  if(traderItemsClear &gt;= 64) {  
   traderItemsClear-= 64;  
   traderItems[1] = 0;  
  }  
  else { traderItems[1] = pgm_read_byte_near(arrayContent + 1); }  
  if(traderItemsClear &gt;= 32) {  
   traderItemsClear-= 32;  
   traderItems[2] = 0;  
  }  
  else { traderItems[2] = pgm_read_byte_near(arrayContent + 2); }  
  if(traderItemsClear &gt;= 16) {  
   traderItemsClear-= 16;  
   traderItems[3] = 0;  
  }  
  else { traderItems[3] = pgm_read_byte_near(arrayContent + 3); }  
 }  
 void drawTrader(int16_t traderId, int16_t positionX, int16_t positionY) {  
  if(!mapFigureRerender) {  
   return;  
  }  
  mapFigureRerender = false;  
  switch(traderId) {  
   case(1): { // Farbe des Haenderls/in  
    spriteHairColor1 = 0xEEEC; spriteHairColor2 = 0xE662; // hell Braun 1, hell braun 2  
    spriteShirtColor1 = 0xD69A; spriteShirtColor2 = 0xB596; // hell grau, grau  
    spritePantsColor1 = 0x0418; spritePantsColor2 = 0x0312; // Blau 1, blau  
    memCopy(traderSpriteFrontWomen);             // sprite einer Weiblichen figur  
    memCopyItems(trader01Items, trader01ItemsClear);     // Taschen Inhalt  
    break;  
   }  
   case(2): { // Farbe des Haenderls/in  
    spriteHairColor1 = 0xD615; spriteHairColor2 = 0xBD30; // hell Braun 1, hell braun 2  
    spriteShirtColor1 = 0xD69A; spriteShirtColor2 = 0xB596; // hell grau, grau  
    spritePantsColor1 = 0x0418; spritePantsColor2 = 0x0312; // Blau 1, blau  
    memCopy(traderSpriteFrontMen);  
    break;  
   }  
   default: { break; }  
  }  
  drawTile(positionX, positionY, 10, 16, tempArray, false);  
 }  
 void drawBox(int16_t boxId, int16_t positionX, int16_t positionY) {  
  switch(boxId) {  
   case(1): {  
    boxColor = 0xDCFE;  
    break;  
   }  
   default: { break; }  
  }  
  memCopy(boxSpriteFront);  
  drawTile(positionX, positionY, 10, 10, tempArray, false);  
 }  
 void drawCoinsStatus(bool redraw) {  
  if(coins != lastStateCoins || redraw) {  
   EsploraTFT.fillRect(2, 2, 30, 9, mapNumberToColor(1));  
   memCopy(coinSpiteIcon);  
   drawTile(3, 3, 7, 7, tempArray, false);  
   writeValue(12, 3, coins, false);  
   lastStateCoins = coins;  
  }  
 }  

Der Händler oder Händlerin sollten für die Kollisionsabfrage den selben Raum einnehmen, wie die eigene Spielfigur. Damit dies funktioniert und der Händler nicht wie ein Karten Block (Kachelgröße) registriert wird, ist eine kleine Erweiterung an der Methode "CanEnterArea" mit "checkCollideOther" notwendig. Etwas abwegig ist die Abfrage der Position, weil diese wiederum über das Byte Array der Karte weiterhin abgefragt wird. Dafür habe ich eine relativ simple Lösung (ggf. in den Github Source schauen)

boolean checkCollideOther(boolean resultColide, int positionX, int positionY) {
  // anderes bewegbares objekt
  if(resultColide) {

    int overlap = 4;
    resultColide = checkCollide(positionX, positionY, mapFigurePositionX + (overlap / 2), mapFigurePositionY + (overlap), 10 - overlap, 16 - (overlap * 2));

    // zum testen Fenster oeffnen
    showWindow = !resultColide;
    menueNavigation = showWindow;
  }

  return resultColide;
}

Message Box
Der Text bekommt sein Platz in einem eigenen Fenster Bereich. Für diese Funktion wird ebenfalls eine weiter Seite angelegt mit dem Namen "WindowComponent". Das Fenster (MessageBox) wird angezeigt, sobald man mit seiner gesteuerten Figur in den Kollisionsradius des Händlers kommt.
Solange der Dialog offen ist, sollte die Figur nicht mehr bewegbar sein und mit dem Joystick kann nur noch in den Taschenplätzen Navigiert werden. Nachdem der Spieler die Schließen-Option Auswählt, verschwindet das Fenster und die Figur sollte sich wieder frei bewegen können.
Was im folgenden Code nicht zu sehen ist, ist die Ausführung des Schließen der MessageBox über den Button 2 bzw. Switch 2.

// Legt ein Fenster in den Vordergrund
bool lastStateShowWindow = false;
bool windowHasRendered = false;

void drawWindow(bool rightSide) {
  if(lastStateShowWindow != showWindow && !showWindow) {
    lastStateShowWindow = showWindow;
    drawStack(true);
  }

  lastStateShowWindow = showWindow;
  
  if(!showWindow) {
    windowHasRendered = false;
    return;
  }

  if(windowHasRendered) {
    return;
  }
  
  // Mitte des Bildschirm schreiben
  int sizeX = 100; int sizeY = 40;
  int winPosX = (EsploraTFT.width() / 2) - (sizeX / 2);
  int winPosY = (EsploraTFT.height() / 2) - (sizeY / 2);

  EsploraTFT.fillRect(winPosX, winPosY, sizeX, sizeY, mapNumberToColor(0));
  EsploraTFT.drawRect(winPosX, winPosY, sizeX, sizeY, mapNumberToColor(18));
  EsploraTFT.drawRect(winPosX + 2, winPosY + 2, sizeX - 4, sizeY - 4, mapNumberToColor(18));

  // Text schreiben
  writeText(winPosX + 5, winPosY + 5, "Hallo!");
  writeText(winPosX + 5, winPosY + 28, "Schliessen [2]");
   windowHasRendered = true;
}


Das Stehenbleiben der Figur muss wiederum auf der Hauptseite festgelegt werden. Dazu muss die Funktion für das Laufen erweitert werden, damit die Figur sich erst nach der Option "Schließen" bewegen kann. Zudem müssen alle Inhalte nach dem Schließen neu gerendert werden mit der  Methode 'drawStack'.

void loop() {
  …  
  // Wenn sich X oder Y Position unterscheiden, dann den zu bewegenden Punkt neu zeichnen.
  if(!menueNavigation && lastPosX != lastPosXtemp || lastPosY != lastPosYtemp) {

    drawStack(false);
  }
  else if(menueNavigation) {
    menueNavigateWithDelay();
  }

  drawWindow(lastPosX > EsploraTFT.width() / 2);
  drawCoinsStatus(false);
}

Der Dialog ist noch nicht ganz fertig. Die Taschenplätze sollten mit dem Joystick erreichbar sein. Das fehlt derzeitig auch für den Rucksack. Dies würde jedoch den Rahmen des Posts sprengen und kommt daher im übernächsten. Für den nächsten Part wird der Programmcode dringend aufgeräumt, auf dass ich näher eingehen will.



Ameisen Simulation und andere Dinge

Wer lange sich schon mit C# beschäftig, hat sicherlich schon mal was von AntMe gelesen oder gehört. Diese Idee hatte ich aufgegriffen und mi...