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() > 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.



Samstag, 25. November 2017

Konfigurierbarkeit des Feuchtigkeitssensor HTU21D


Wenn man im Internet sucht, finden sich viele Beispiele zur Programmierung einiger Sensoren, die ich ebenfalls selbst verwende. Die meisten Codeschnipsel funktionieren auch auf dem Wemos. Dennoch muss noch etwas herum probiert werden, um bestimmte Schwierigkeiten anzugehen, damit auch das erwartete Ergebnis kommt.

HTU21D, HTU21, SHT21
Wird ein Sensor gelesen bekommt man nach diesem Vorgang einen Rohwert, der dann in einen für uns bekannten und lesbaren Wert umgerechnet wird. Wir können dies ohne weiteres nach Datenblatt tun oder einen fertigen Beispiel Code verwenden.
Ich wollte meinen Programmcode mit anderen Beispielen Vergleichen, auf Grund einer Konfigurierbarkeit des Sensors. Leider war dazu auf Anhieb nichts zu finden, womit ein Grund bestand sich damit selbst auseinander zu setzten.
In diesem Fall ist es der Feuchtigkeit Sensor HTU21D und ist auch unter HTU21 oder SHT21 zu finden (Nicht ganz sicher, ob alle dieselben sind). Abgesehen Technischer Unterschiede, können alle drei mit dem gleichen Programmcode angesteuert werden. Vor ein paar Jahren hatte ich bereits ein Beispiel Code mit C# und .NET Micro Framework geschrieben, in dem ich damals keine Implementierung der Einstellmöglichkeiten vornahm.

Für den Sensor sind drei Register verfügbar, mit denen die Einstellungen abgefragt, festgelegt oder zurückgesetzt werden können.
  • Write user register 0xE6
  • Read user register 0xE7
  • Soft Reset 0xFE
Grundeinstellung
Wird der Sensor nur gestartet und gelesen, dann bestehen die Grundeinstellungen. Diese sollten im Normalfall reichen. Bei Speziellen Anwendungen könnten dieses Lesevorgänge zu lange dauern. Mit dem Kompromiss der Genauigkeit, kann der Lesevorgang für die Feuchtigkeit von 18ms auf 3ms gesengt werden. Anstatt der 12bit, werden jedoch nur 8bit verwendet.

Genauigkeit
Zeit
12bit
18ms
11bit
9ms
10bit
5ms
8bit
3ms

Abruf und Zuweisen der Einstellung
Im Grunde ist dies so simpel wie der Abruf der Sensor Daten. Für die Einstellwerte ist jedoch etwas umdenken erforderlich, da diese in einem Byte verpackt sind. Ein Byte besteht aus 8Bit und verfügt damit so acht Schalter für bestimmte Einstellungen. Zum Nachvollziehen habe ich aus dem Datenblatt die Tabelle mit den zugehörigen Bit Schaltern abgebildet.


Einstellen und Auswerten
Mit dem 'Read user register' 0xE7 kann die aktuelle Einstellung abgerufen werden. Das kann hilfreich sein, um die geschrieben Einstellungen gegen zu prüfen.

 void Htu21ReadUserRegister() {  
  Wire.beginTransmission(HTU21D_ADDRESS);  
  Wire.write(command);              
  Wire.endTransmission();  
  Wire.requestFrom(HTU21D_ADDRESS, 1);   
  while(Wire.available() < 1) { }  
  byte regCon = Wire.read();  
  Serial.print("\tRegister Result: ");   
  Serial.println(regCon, BIN);         
 }  

Das folgende Beispiel beschreibt die Einstellung 'RH 11Bit / Temp 11Bit', 'On-Chip Header' aus und 'Disable OTP Reload' aus.

 void Htu21WriteRegisterSetting() {  
  byte command = 0xC0;  // RH 11bit / 11Bit Temp  
  command |= 0x01;     // disable otp reload  
  Wire.beginTransmission(HTU21D_ADDRESS);  
  Wire.write(0xE6);       // command for write register command  
  Wire.write(command);  // write the finish command  
  Wire.endTransmission();  
 }  

War das Schreiben der Einstellung erfolgreich, sollte mit der Methode 'Htu21ReadUserRegister()' das Binäre Ergebnis '1100 0001' ausgegeben werden.

Beispiel Projekt Wetterstation
Auf dem Github Repository stelle ich diesmal das kleine Projekt zur Verfügung, in dem ich bereits den HTU21 und den BMP180 Sensor verwende. Dort sind einige Methoden etwas weiter ausgeführt und zum Teil Kommentiert. Soweit ich immer Zeit habe, erweitere ich das Projekt mit Möglichst überschaubaren Funktionen.

Samstag, 18. November 2017

Servo mit dem Xbox Controller steuern


Das ansteuern eines Servos über einen Xbox One Controller ist simpel umzusetzen. Für dieses Beispiel wird folgendes verwendet:

  • MonoGame
  • Xbox Controller
  • Arduino UNO oder vergleichbar
  • Servo
  • Motor Shield
  • Externe Batterie

Nach der Installation von MonoGame sind in Visual Studio mehre Vorlagen verfügbar. Benötigt wird das Template 'MonoGame Windows Project', das im folgenden Bild als erstes in der Liste erscheint.


Programmcode mit MonoGame
Sobald das Projekt angelegt würde, könnt ihr die Game1.cs Datei öffnen. Für die Verbindung zum Arduino wird die Klasse SerialPort verwendet. Dazu sollte vorher bekannt sein, welcher COM Port bei euch der Arduino verwendet. Die Baudrate von 115200 ist die maximale Geschwindigkeit, die zuverlässig funktioniert. Die restlichen Parameter sind die Default Werte der Seriellen Verbindung zum Arduino (siehe Programmcode).
Die überschriebenen Methoden 'Initialization()', 'LoadContent()' und 'Draw()' werden für das Beispiel nicht verwendet und können entfernt werden.
Mit der Update Methode wird der Xbox Controller eingelesen. Der Input Wert vom Stick kommt dann aus dem GamePadState unter '.ThumbSticks.Left.X'. Der Float Wert geht von -1 bis +1 und wird deshalb zunächst um plus eins dass Offset verschoben. Anschießend wird der Float Wert in ein Byte Wert transformiert, dass dann anschließend an den Serial Port geschrieben wird.
Damit die Anwendung nicht zu viele Bytes versendet, wird am Ende 50 Millisekunden gewartet. Ansonsten läuft der Buffer im Arduino über.

GraphicsDeviceManager _graphics;  
 private SerialPort _serialPort;  
 public Game1()  
 {  
   _graphics = new GraphicsDeviceManager(this);  
   // Verwendete COM Port muss ggf. angepasst werden.  
   this._serialPort = new SerialPort("COM5", 115200, Parity.None, 8, StopBits.One);  
   this._serialPort.Open();  
 }  
 protected override void UnloadContent()  
 {  
   this._serialPort.DiscardOutBuffer();  
   this._serialPort.Close();  
 }  
 protected override void Update(GameTime gameTime)  
 {  
   GamePadState state = GamePad.GetState(PlayerIndex.One);  
   byte stickValue = (byte)((state.ThumbSticks.Left.X + 1) * 90);  
   this._serialPort.Write(new byte[] { stickValue }, 0, 1);  
   Task.Delay(50).Wait();  
 }  

Arduino als Empfänger
Der Arduino muss nun die empfangenden Bytes vom PC entgegen nehmen und daraus ein PWM Signal erzeugen das zur Steuerung des Servos verwendet wird. Weiters habe ich im Programmcode beschrieben.

 #include <Servo.h>  
 Servo servo;  
 int servoPin = 9;  
 int receivedValue;  
 void setup() {  
  // Muss die selbe Baudrate haben,   
  // wie in der MonoGame Anwendung.  
  Serial.begin(115200);  
  // die meisten Servos arbeiten mit einem   
  // HIGH Signal von 1ms bis 2ms.  
  servo.attach(servoPin, 1000, 2000);  
 }  
 void loop() {  
  if( Serial.available() > 0){  
   receivedValue = Serial.read();  
   // Den eingelesenen Wert Abschneiden,  
   // falls der maximale Stellwert überschritten wird.  
   if(receivedValue > 180) {  
    receivedValue = 180;  
   }  
   else if(receivedValue < 0) {  
    receivedValue = 0;  
   }  
   // alle weiteren eingegangenen bytes werden verworfen  
   Serial.flush();  
  }  
  // Aktuellen Einstellwert festlegen.  
  servo.write(receivedValue);  
  delay(20);  
 }  

Was nun
Im Grunde ist die Ausführung simpel und das Schreiben dieses Post hat da deutlich mehr Zeit benötig. :D
Aber was kann man nun mit dem Wissen anstellen? Nun, man kann stattdessen einer  Kabelgebundenen Ausführung, auf eine Bluetooth Funkverbindung wechseln. Damit ließe sich dann ein RC Model ansteuern. Mag etwas Umständlich sein, die Fernsteuerung gegen etwas eigenes gebasteltes zu ersetzen, aber darin liegt der Spaß.

Zur Vollständigkeit wieder ein Link zu dem Github Repository und dem gesamten Beispiel Programmcode.

Nachtrag
Leider funktioniert dieses einfache Beispiel nur in dieser Konstellation. Sobald versucht wird, zwei Bytes zu versenden, treten Probleme auf mit der Seriellen Verbindung.
Wie man das behebt, beschreibe ich mit dem Post "Zeichnen auf dem TFT mit dem XBox Controller"





Sonntag, 5. November 2017

Unterdruck und Höllentemperatur bei falschen Valuetype (Wemos)


Wenn die Ergebnisse nicht den Erwartungen entsprechen, dann ist mit Sicherheit etwas falsch. Das geschah diesmal mit dem Wemos D1 Mini. Einen bereits fertiges Code Beispiel für das Auslesen eines BMP085 Sensors mit einem Arduino, verwendete ich diesmal auf dem Wemos. Nach dem hochladen zeigten sich die nicht erwartenden Ergebnisse. Zumindest war offensichtlich, dass in meiner Wohnung keine 119 Grad Celsius herrschten und bei einem Luftdruck von 4000 Pascal wäre ich sicherlich an Sauerstoffmangel oder kochendem Blut auseinander gegangen. Also musste was an der Berechnung nicht stimmen.


Behoben
Der Fehler ließ sich relativ schnell beheben. Die Verwendeten ValueTypes int und unsigned int wurden ersetzt durch int16_t und unt16_t.

Aber warum
Ein ValueType INT ist immer das gleiche, solange die Variable als INT definiert wird auf einem System. Das eine System ist die Arduino Plattform mit dem 8Bit Mikrocontroller. Der Wemos verwendet wiederum einen 32Bit Mikrocontroller. Das sollte als erstes Auffallen und schnell sollte klar sein, dass etwas mit den Typen etwas nicht stimmt und folglich nicht mit den richtigen Werten rechnet. Eigentlich ist das von Compiler abhängig, welcher Type aus einem INT angelegt wird.
Der Folgende Code macht die Größe eines INT sichtbar, sobald ihr das auf der Ziel Plattform ausführt.

 void setup() {  
  Serial.begin(9600);  
  Serial.print("int: ");   
  Serial.println(sizeof(int), DEC);  
  Serial.print("unsigned int: ");   
  Serial.println(sizeof(unsigned int), DEC);  
  Serial.print("int16_t: ");   
  Serial.println(sizeof(int16_t), DEC);  
  Serial.print("uint16_t: ");   
  Serial.println(sizeof(uint16_t), DEC);  
 }  

Wie man sieht, wird auf dem Wemos kein 16Bit INT, sondern 32Bit INTEGER angelegt

Arduino Nano
Wemos D1 Mini



Aber Moment mal. Damit wäre eine Berechnung auf dem Wemos genauer und der Rechenfehler dürfte erst gar nicht auftreten. Sieht man sich allerdings die Funktionsinhalte an, dann sind mehrere Stellen auffällig, wo die Werte durch Byteshifting hoch oder runter gerechnet werden. Das nur passt wiederum nur mit 16 Bit. Andernfalls muss die Funktion geändert werden.

Zur Vollständigkeit der Beispielcode mit den geänderten ValueTypes. In diesem Fall reichten die Änderungen für INT aus und kann nun für Arduino oder Wemos verwendet werden.

 #include <Wire.h>  
 #define BMP085_ADDRESS 0x77 // I2C address of BMP085  
 const unsigned char OSS = 0; // Oversampling Setting 
// Calibration values
int16_t ac1, ac2, ac3, b1, b2, mb, mc, md;
uint16_t ac4, ac5, ac6;
long b5;

void setup() {
  Serial.begin(115200);
  Wire.begin();
  bmp085Calibration();
}

void loop() {
  uint16_t rawValueUt = bmp085ReadUT();  // MUST be called first
  float temperature = bmp085GetTemperature(rawValueUt);
  uint16_t rawValueUp = bmp085ReadUP();
  float pressure = bmp085GetPressure(rawValueUp);
  float atm = pressure / 101325; // "standard atmosphere"
  float altitude = calcAltitude(pressure); //Uncompensated caculation - in Meters 

  Serial.print("Temperature: "); Serial.print(temperature, 1); Serial.println(" deg C");
  Serial.print("Pressure: "); Serial.print(pressure, 0); Serial.println(" Pa");
  Serial.print("Standard Atmosphere: "); Serial.println(atm, 4);
  Serial.print("Altitude: "); Serial.print(altitude, 2); Serial.println(" M");
  Serial.println();
  delay(2000);
}

void bmp085Calibration(){
  ac1 = bmp085ReadInt(0xAA); Serial.print("ac1"); Serial.print(ac1, DEC);
  ac2 = bmp085ReadInt(0xAC); Serial.print(" ac2"); Serial.print(ac2, DEC);
  ac3 = bmp085ReadInt(0xAE); Serial.print(" ac3"); Serial.print(ac3, DEC);
  ac4 = bmp085ReadInt(0xB0); Serial.print(" ac4"); Serial.print(ac4, DEC);
  ac5 = bmp085ReadInt(0xB2); Serial.print(" ac5"); Serial.print(ac5, DEC);
  ac6 = bmp085ReadInt(0xB4); Serial.print(" ac6"); Serial.print(ac6, DEC);
  b1 = bmp085ReadInt(0xB6); Serial.print(" b1"); Serial.print(b1, DEC);
  b2 = bmp085ReadInt(0xB8); Serial.print(" b2"); Serial.print(b2, DEC);
  mb = bmp085ReadInt(0xBA); Serial.print(" mb"); Serial.print(mb, DEC);
  mc = bmp085ReadInt(0xBC); Serial.print(" mc"); Serial.print(mc, DEC);
  md = bmp085ReadInt(0xBE); Serial.print(" md"); Serial.print(md, DEC); 
  Serial.println();
}
// Calculate temperature in deg C
float bmp085GetTemperature(uint16_t ut){
  long x1 = (((long)ut - (long)ac6)*(long)ac5) >> 15;
  long x2 = ((long)mc << 11)/(x1 + md);
  b5 = x1 + x2;

  return (float)((b5 + 8)>>4) / 10.0;
}
// calculate pressure from raw value
long bmp085GetPressure(unsigned long up){

  long b6 = b5 - 4000;
  // Calculate B3
  long x1 = (b2 * (b6 * b6)>>12)>>11;
  long x2 = (ac2 * b6)>>11;
  long x3 = x1 + x2;
  long b3 = (((((long)ac1)*4 + x3)<>2;

  // Calculate B4
  x1 = (ac3 * b6)>>13;
  x2 = (b1 * ((b6 * b6)>>12))>>16;
  x3 = ((x1 + x2) + 2)>>2;
  unsigned long b4 = (ac4 * (unsigned long)(x3 + 32768))>>15;
  
  unsigned long b7 = ((unsigned long)(up - b3) * (50000>>OSS));

  long p;
  if (b7 < 0x80000000)
    p = (b7<<1 b4="" else="" p="(b7" x1="(p">>8) * (p>>8);
  x1 = (x1 * 3038)>>16;
  x2 = (-7357 * p)>>16;
  p += (x1 + x2 + 3791)>>4;

  return p;
}
// Read 1 byte from the BMP085 at 'address'
char bmp085Read(unsigned char address){
  Wire.beginTransmission(BMP085_ADDRESS);
  Wire.write(address);
  Wire.endTransmission();
  Wire.requestFrom(BMP085_ADDRESS, 1);
  while(!Wire.available()) {}
  return Wire.read();
}
// read two byte 
int16_t bmp085ReadInt(unsigned char address){
  Wire.beginTransmission(BMP085_ADDRESS);
  Wire.write(address);
  Wire.endTransmission();

  Wire.requestFrom(BMP085_ADDRESS, 2);
  while(Wire.available()<2 0x34="" 0xf4="" 0xf6="" 0xf7="" 0xf8="" 16="" 4.5ms="" 8="" a="" and="" at="" bmp085readint="" bmp085readup="" bmp085readut="" char="" conversion="" delay="" dependent="" for="" int16_t="" into="" least="" long="" lsb="" msb="" on="" oss="" oversampling="" pressure="" raw="" read="" reading="" register="" request="" return="" setting="" temperature="" the="" time="" uint16_t="" uncompensated="" unsigned="" up="(((unsigned" value="" w="" wait="" wire.begintransmission="" wire.endtransmission="" wire.write="" write="" x2e="" x34="" xf4="" xf6="" xlsb="">> (8-OSS);
  Serial.print("Pressure Raw Value: "); Serial.print(up, DEC); Serial.println();
  return up;
}
// calculate altitude by pressure value
float calcAltitude(float pressure){
  float alti = pow((pressure/101325),(1/5.25588));
  return (1 - alti) /0.0000225577;
}


Sonntag, 29. Oktober 2017

Ich packe in meinen Rucksack (Arduino Esplora, Part 7)


Was wäre ein Abenteuer ohne einen Rucksack, in dem man seine Gefundenen Gegenstände einsammeln kann. Um diese Funktion Übersichtlich zu halten, wird der Rucksack sechs Plätze haben. Im Vorfeld muss festgelegt werden, wie zunächst die Informationen im Rucksack gehalten werden. Auch hier wird weiterhin eine Datenbanklose Lösung erzielt. Die Gegenstände müssen als Abstrakt betrachtet werden, so dass diese auf wesentliche Informationen eingeschränkt wird.

Ein wichtiger Punkt wird sein, die Funktionsvariablen entsprechend zu kommentieren. Das wird später hilfreich sein, die Informationen auch wieder zu zuordnen.

Ein Objekt sollte Grundlegende Eigenschaften haben:

  • Name
  • Bild (ein 16x16 Pixel Sprite)
  • Beschreibung (sollte nur für bestimmte Gegenstände verwendet werden)
  • Verwendungszweck

Damit der Gegenstand Zugeordnet werden kann, ist zusätzlich eine Identifikationsnummer erforderlich oder auch kurz ID. Diese wird z.B. für den Rucksack Funktion verwendet. Allerdings muss die ID Nummer nicht als Funktionsvariable angelegt werden und steht nur als Kommentar zu den verwendeten Daten.

 // ID 01  
 // Name  
 const PROGMEM char itemKey01[10] = "Schluessel";  
 // Icon / Bild  
 const PROGMEM byte itemKey01Icon[256] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,10,10,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,10,0,0,10,10,10,10,10,10,10,10,10,10,10,0,0,10,0,0,10,10,0,10,0,0,0,0,10,0,0,0,0,10,0,0,10,0,0,0,0,0,0,0,10,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,10,10,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };  
 // Beschreibung  
 const PROGMEM char itemKey01Description[] = "Oeffnet eine Box";  
 // Verwendungszweck Id =&gt; kombinierte funktions abruf fur position und verknuepfte Box mit der selben Id  
 const PROGMEM uint16_t itemKey01Usage = 1;  
 // #######################################  
 // ID 02  
 // Name  
 const PROGMEM char itemCamera[6] = "Kamera";  
 // Icon / Bild  
 const PROGMEM byte itemCameraIcon[256] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,9,9,9,9,9,9,1,0,0,0,0,0,1,1,1,1,9,19,19,19,19,9,1,1,1,1,0,1,9,9,9,9,9,9,1,1,9,9,9,9,9,9,1,1,9,9,9,9,1,1,11,11,1,1,9,19,19,9,1,1,9,9,9,9,1,11,11,11,11,1,9,19,19,9,1,1,9,9,9,1,11,11,11,11,11,11,1,9,9,9,1,1,9,9,9,1,11,11,11,11,11,11,1,9,9,9,1,1,9,9,9,9,1,11,11,11,11,1,9,9,9,9,1,1,9,9,9,9,1,1,11,11,1,1,9,9,9,9,1,1,9,9,9,9,9,9,1,1,9,9,9,9,9,9,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };  
 // Beschreibung  
 const PROGMEM char itemCameraDescription[] = "Mach ein paar Fotos!";  
 // Verwendungszweck  
 const PROGMEM uint16_t itemCameraUsage = 2;  

Der Name ist klar, Bild muss sein und Beschreibung zu einem Objekt ist auch hilfreich. Aber wie sieht der Einsatz für die Eigenschaft 'Verwendungszweck' aus? Im Programmcode wird dort eine Nummer stehen. Hier kommt die Kollisionsabfrage ins Spiel.

Ein Fallbeispiel
Die Figur hat den Gegenstand 'Schlüssel' und kann damit eine Bestimmte Tür öffnen. Über die Kollisionsabfrage wird geprüft, ob das Hindernis eine Tür ist. Wenn ja, dann wird der Rucksack nach einem Objekt abgefragt, dass dem Verwendungszweck entspricht.


Weiteres zum Verwendungszweck, kommt im späteren Abschnitt und bleiben zunächst bei der Umsetzung Gegenstände einzusammeln.

Der Rucksack
Als erstes sollten die Taschenplätze im Unteren Bildschirm Bereich abgebildet werden. Im Aus übersichtlichen Gründen wird im Programmcode eine weitere Seite (Tab) angelegt mit dem Namen 'BackpackComponent'. Für die Anforderungen kommen einige Funktionen hinzu, um ein Item in den Rucksack zu schreiben, abzurufen oder zu entfernen.

// Grundeinstellung des Rucksackes
#define backpackPlacesCount 6
uint16_t backPlaces[backpackPlacesCount] = { 0, 0, 0, 0, 0, 0 };
byte tempIcon[256];

// … Item Objekte …

// Kopiert das array aus dem flash in den Arbeitsspeicher
void setItemIconToTemp(byte icon[]) {
  for(int index = 0; index < 256; index++) {
    tempIcon[index] = pgm_read_byte_near(icon + index);
  }
}

// Pruefen ob das Item bereits vorhanden ist
boolean isItemInBackback(uint16_t itemId) {

  for(byte index = 0; index < 6; index++) {
    if(backPlaces[index] == itemId) { return true; }
  }
  return false;
}

// Legt das Item in die Tasche ab und Zeichnet es in einen offen Taschenplatz
bool setItemToBackpack(uint16_t itemId) {

  if(isItemInBackback(itemId)) { return false; }
  
  // id ablegen in ersten freien Taschenplatz
  byte place = 0;
  for(byte index = 0; index < sizeof(backPlaces); index++) {
    if(backPlaces[index] == 0) {
      backPlaces[index] = itemId;
      place = index;
      break;
    }
  }
  
  byte relationPlaceX = 0;
  byte relationPlaceY = 0;
  setItemRelationPlace(place, &relationPlaceX, &relationPlaceY);

  // Abruf des Icon zu dem Item
  boolean isArrayCopy = true;
  switch(itemId) {
    case(1): { setItemIconToTemp(itemKey01Icon);  break; } // Schluessel
    case(2): { setItemIconToTemp(itemCameraIcon); break; } // Fotoapparat
    case(3): { setItemIconToTemp(itemPhoto01Icon); break; } // Foto
    default: { isArrayCopy = false; break; } // Nicht belegt, darf aber auch nicht eintreten
  }

  if(isArrayCopy) { drawTile(relationPlaceX, relationPlaceY, mapTileSize, mapTileSize, tempIcon, false); }
  else { EsploraTFT.fillRect(relationPlaceX, relationPlaceY, mapTileSize, mapTileSize, 0xF800); }
  
  // einen Rahmen darueber zeichnen
  EsploraTFT.drawRect(relationPlaceX, relationPlaceY, mapTileSize, mapTileSize, mapNumberToColor(12));

  return true;
}

// Holt die anfangs Position des Taschenplatzes das auf dem Bildschirm gerendert werden soll.
void setItemRelationPlace(byte place, byte* relationPlaceX, byte* relationPlaceY) {

  if(place == 0 || place == 1 || place == 2) { *relationPlaceY = 96; }  // erste Zeile
  else if(place == 3 || place == 4 || place == 5) { *relationPlaceY = 112; }  // zweite Zeile

  if(place == 0 || place == 3) { *relationPlaceX = 0; } // erste Spalte 
  else if(place == 1 || place == 4) { *relationPlaceX = 16; } // zweite Spalte
  else if(place == 2 || place == 5) { *relationPlaceX = 32; } // dritte Spalte
}

// Prueft die Karten Id mit einem Objekt aus dem Rucksack.
bool getItemToUsed(int16_t mapUsageId) {

  int16_t itemId = 0;

  // hole itemId aus der Karteneigenschaft ab.
  if(mapUsageId == mapBarrierUsageDoor01) {
    itemId = 1; // Id des zu verwendenden Schlussels
  }

  // pruefe die Tasche, ob das Item vorhanden ist und dann aus dem inventar nehmen
  for(byte index = 0; index < sizeof(backPlaces); index++) {

    //       Item einmalig verwenden
    if(itemId != 0 && backPlaces[index] == itemId) {

      // Verwendungszweck
      if(mapUsageId == mapBarrierUsageDoor01) {

        mapBarrierDoorIsOpen = true;
        backPlaces[index] = 0; // aus dem Inventar entfernen
      }
    }
  }
  if(mapUsageId == mapBarrierUsageDoor01 && mapBarrierDoorIsOpen == true) {
    return true;
  }
   return false;
}

Die Tasche ist nun da. Jetzt fehlt noch das Einsammeln, dass mit Hilfe der Kollisionsabfrage ermöglicht. Bisher wurden nur die Werte für Begehbar und Wand geprüft. Auf der Karte kommt nun ein weiterer Wert hinzu, das für ein einzusammelndes Objekt steht. Damit wir diese Stelle wiedererkennen, muss auch das Rendern der Karte noch angepasst werden.

 ...
void renderMap(int positionX, int positionY, boolean renderAll) {

  // zum probieren wird zunächst ein Grid gerendert.
  byte index = 0;
  for(byte y = 0; y < mapTileCountY; y++) {
    for(byte x = 0; x < mapTileCountX; x++) {

      if(((positionX >= (int)(x * mapTileSize) - (int)mapTileSize && positionX <= (int)(x + 1) * (int)mapTileSize && 
          positionY >= (int)(y * mapTileSize) - (int)mapTileSize && positionY <= (int)(y + 1) * (int)mapTileSize)) || 
          renderAll) {
            byte bTile = pgm_read_byte_near(mapContent + index);

            // TODO: Kartenspezifische abhangigkeit, 
            //       Eigenschaften andern sich mit Kartenwechsel
            if(bTile == 2 && mapKeyIsGet) { bTile = 0; }
            if(bTile == 5 && mapBarrierDoorIsOpen) { bTile = 0; }
           
           renderMapTile(x, y, bTile);
      }
      index++;
    }
  }
}
...

Momentan werden die zwei Werte noch direkt in der Funktion 'renderMap' aufgerufen. Die ergänzende Ausführung ist Simple. Solange sich noch die Objekte an ihren Stellen befinden, werden die Kacheln in der vorgesehenden Farbe eingefärbt. Die Funktion 'renderMapTile' benötigt daher weitere 'case´s'.

...
void renderMapTile(byte x, byte y, byte mapSegment) {
  
  byte mapTileColorNumber = 0;
  switch(mapSegment) {
    case(1): { mapTileColorNumber = 10; break; }
    case(2): { mapTileColorNumber = 12; break; }
    case(5): { mapTileColorNumber = 13; break; }
    default: { mapTileColorNumber = 15; break; }
  }

  EsploraTFT.fillRect(x * mapTileSize, y * mapTileSize, mapTileSize, mapTileSize, mapNumberToColor(mapTileColorNumber));
}
...

Einsammeln und Verwenden
Die Kacheln, an dem eine Tür oder ein Schlüssel liegt, erfüllen zwei Eigenschaften. Die Kachel ist weiterhin begehbar und hat ein Objekt. Wurde das Objekt aufgenommen, wird jedoch im Karten Array der Wert nicht auf '0' gesetzt. Denn die Karte wird immer aus dem Flashspeicher geladen und kann nur gelesen werden. Deshalb werden neue Funktionsvariablen angelegt die den Status der Kachel wiedergeben. Das wird bereits in der Funktion 'renderMap' erledigt. Später erfüllen die Variablen auch für andere Karten dieselbe Rolle. Die Information wird jedoch für die Karte hinfällig, wenn sie verlassen wird. Aber dazu in einen späteren Post.

Die Kollisionsabfrage 'checkCollideNeighbor' wurde erweitert, um den Wert '2' und '5'. Die Werte '3' und '4' werden jetzt noch nicht verwendet, sollen aber später die selbe Eigenschaft haben, wie der Wert '2'. Der folgende Vorgang prüft ähnlich wie bei einer Kollision mit einer Wand. Allerdings wird hier nach einem Objekt geprüft, dass in der zu betretenden Kachel vorhanden ist.

 ...
  if(bTile == 2) {

    resultColide = checkCollide(positionX, positionY, mapOffsetX, mapOffsetY);

    // Abruf des Objektes, dass zu der Karte gehoert an der Position.
    if(!resultColide) {
      if(setItemToBackpack(1)) {
        mapKeyIsGet = true;
      }
      // nicht blockieren
      resultColide = true;
    }
  }
...

Der Wert '5' benötigt ein anderes Vorgehen, hält sich jedoch ebenfalls simpel. Auch hier wird vorher abgefragt, ob ein Hindernis besteht. Wenn nicht, dann prüfe ob die Tür offen ist oder der Schlüssel die Tür öffnet. In diesem Fall verschwindet der braune Block.

...
  if(bTile == 5) {

    resultColide = checkCollide(positionX, positionY, mapOffsetX, mapOffsetY);

    // Uebergabewert des Verwendungswecks > Tuer oeffnen.
    // kollision aufheben
    if(!resultColide) {
      
      // ID 1 ist der Schlüssel und entscheidet,
      // ob die Tuer sich oeffen laest.
      resultColide = getItemToUsed(1); 
    }
  }
...

Animationslos verschwindet die Tür. Hier färbt sich die braune Kachel in hell grün (sieht leider mehr grau aus), sowie die anderen Kacheln die begehbar sind.
So dass sollte Inhaltlich vom Blogpost reichen. Das Thema ist länger geworden als vorgesehen und dabei habe ich einiges noch gekürzt. Alles weiter sowie Kommentar Beschreibungen sind in den Sourcen eingetragen, die ich wieder auf Github hoch geladen habe.


Lange noch nicht fertig
Dass die Grundfunktionen noch nicht reichen, dürfte klar sein und viele würden lieber ein Schwert ziehen und Monster bekämpfen. Aber, wie bereits ein weiser Mann Sprach: "Wie soll das Schwert richtig geschwungen werden, wenn das nicht mal mit einem Stock geht".

Nächster Post: Von Händler, Kisten und Münzen (Arduino Esplora, Part 8)


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...