Pojďme programovat elektroniku | Letadla | Bosch

Programování elektroniky: Vytiskli jsme obří Mriju a proměnili ji v ovladač leteckého simulátoru

  • Víte co je to digital twin – digitální dvojče?
  • Dnes je to velké téma Průmyslu 4.0 a automatizace
  • V jedno takové dvojče proměníme obří Antonov z 3D tiskárny

Jedním z nejnovějších buzzwordů průmyslu 4.0, digitalizace a internetizace prakticky všeho je digital twin – digitální dvojče.

Tedy počítačová reprezentace nějakého skutečného předmětu z fyzického světa, která se používá pro jeho snadnou simulaci třeba ve virtuálním prostředí. Dnes si to vyzkoušíme v praxi na ikonickém letounu Antonov An-225 Mrija.

Propojené digitální dvojče

Mrija se stala jednou z prvních obětí ruské invaze na Ukrajinu, díky své výjimečnosti ji ale kutilové už před lety a v mnoha variantách přetvarovali do počítačového 3D modelu. Jeden z nich je k dispozici zdarma na webu Sketchfab a poslouží nám jako digitální dvojče, které bude poletovat nad virtuálním Brnem.

586f3c25-1d5f-4397-a758-16acee2e43c4 1dcc50db-8e29-4584-93b1-9c21b2beb067
Digitální Mrija od MaTtea0 ze SketchFabu a vytištěná Mrija z Thingiverse. Dnes oba dva světy propojíme bezdrátovou komunikací

Fyzikou Mrijou bude další model z Thingiverse, který vyrobíme na běžné domácí 3D tiskárně. Bude to macek s rozpětím křídel 44 centimetrů, ale nebojte se, tak velkou tiskárnu nepotřebujete, model se totiž skládá z téměř dvacítky jednotlivých dílů, které složíte dohromady.

Podívejte se na video, jak ovládáme digitální Mriju v PC pomocí té vytištěné, která má vlastní počítač, Wi-Fi a gyroskop:

Do hry zapojíme čip gyroskopu

Takže máme jeden digitální a jeden „skutečný“ Antonov An-225, oba světy ale ještě musíme propojit, aby to celé mělo nějaký smysl. Součástí Mriji z umělé hmoty tak bude box místo podvozku, ve kterém bude schovaná česká prototypovací deska ESP32-LPkit s Wi-Fi mikrokontrolerem ESP32 a konektorem pro připojení jednočlánkového lithiového akumulátoru, destička totiž disponuje i nabíjecím obvodem.

f3d8a2df-c0e9-41a3-9a0d-347f265c907a
Schéma zapojení dnešního projektu

Do schránky ještě zabudujeme jednoduchou šestiosou IMU jednotku MPU-6050 od InvenSense TDK a drobný monochromatický 0,91“ OLED s rozlišením 132×32 pixelů a řadičem SSD1306. Obě součástky komunikují na sběrnici I²C, zapojení tedy bude naprosto jednoduché a znázorňuje jej schéma na obrázku.

Klonění, klopení a bočení

Když schránku zacvakneme do spodní části letounu Mrija a ten čumákem namíříme na monitor počítače, náš firmware po spuštění provede kalibraci gyroskopu a připojí se k předdefinované síti Wi-Fi.

963f5fe0-41a2-4681-8df3-8152c33687b1
Letecké úhly v praxi

Když poté na počítači spustíme skript v prostředí Processing, se kterým jsme pracovali i v předchozím dílu našeho seriálu o programování elektroniky, program se skrze lokální síť spojí s mikrokontrolerem ESP32 a skrze webový komunikační protokol WebSocket začne číst textová data o aktuálním natočení našeho fyzického letounu v prostor, jak jej změřil gyroskop MPU-6050.

  • Natočení okolo podélné osy X říkáme klonění – roll
  • Natočení okolo příčné osy Y říkáme klopení – pitch
  • Natočení okolo svislé osy Z říkáme bočení – yaw

Když nakloníme plastovou Mriju, natočí se i ta digitální v počítači

Podle zjištěných úhlů Processing analogicky nakloní digitální model Mriji ve virtuálním prostředí oblohy pár set metrů nad Brnem. Z vytištěného letounu tedy uděláme jakýsi bezdrátový letecký joystick, který může přenášet data kamkoliv na internet.

Potřebné knihovny pro Arduino

Celý program pro čip ESP32 v Arduinu se nám vejde i s komentářem na necelé dvě stovky řádků, o vše se totiž postará trojice klíčových knihoven:

  • arduinoWebSockets pro práci se stejnojmenným protokolem zejména na čipech ESP8266/ESP32
  • MPU6050_tockn pro práci s kombinovaným gyroskopem MPU-6050 a zjištění úhlu natočení od základní roviny získané úvodní kalibrací
  • Adafruit_SSD1306 pro práci s maličkým OLED displejem
ce888f7f-3a2e-4d02-b793-f2b543e1efce
Sériový výstup našeho programu, pokud bude deska připojená k PC

Potřebné knihovny pro Processing

Na straně Processingu, což je vlastně takové Arduino pro desktopy, pak použijeme tyto dvě knihovny:

52e489be-0bce-48e6-8ad2-34ea0ee4a0c2
Přenos náklonu z fyzické Mriji na tu digitální v prostředí Processing 

Zatímco zvukovou knihovnu doinstalujete vyhledáním z nabídky Sketch – Import Library – Manage Libraries, weboscketovou knihovnu stáhnete z GitHubu a rozbalíte do adresáře s knihovnami pro Processing. Na Windows je to zpravidla složka Processing\libraries v adresáři pro dokumenty uživatele.

Zdrojové kódy

Zdrojové kódy dnešního projektu jsou poměrně jednoduché a pečlivě okomentované, namísto slovní vaty se proto podívejte přímo na ně.

Všechny zdrojové kódy a 3D model letadla najdete na GitHubu našeho seriálu o programování elektroniky.

Zdrojový kód pro Arduino

/*
   Firmware pro Mriju, Arduino a mikrokontroler ESP32
   Po pripojeni napajeni se cip:
   1) Spoji s 128x32 OLED displejem s I2C radicem SSD1306
   2) Spoji s I2C gyroskopem MPU6050
   3) Pripoji se k preddefinovane Wi-Fi
   4) Na TCP portu 80 nastartuje websocketovy server

   Ve smycce bude cekat na pripojeni klienta
   Pokud k tomu dojde, zacne mu posilat uhly natoceni gyroskopu
   jako prosty text ve formatu X.XXX,Y.YYY,Z.ZZZ

   Pouzite externi knihovny pro Arduino:
   1) https://github.com/tockn/MPU6050_tockn
   2) https://github.com/Links2004/arduinoWebSockets
   3) https://github.com/adafruit/Adafruit_SSD1306
*/

#include <WiFi.h>              // Wi-Fi komunikace
#include <ESPmDNS.h>           // LAN MDNS
#include <esp_wifi.h>          // Vynuceni plneho vykonu Wi-Fi
#include <WebSocketsServer.h>  // Websockets
#include <Wire.h>              // I2C
#include <MPU6050_tockn.h>     // Gyroskop MPU6050
#include <Adafruit_SSD1306.h>  // OLED displej

// SSID a heslo Wi-Fi (2GHz),
// ke ktere se bude pripojovat fyzicka Mrija
// Pro jednoduchost natvrdo soucasti firmwaru
const char *ssid = "nazev-wifi-site";
const char *password = "heslo-k-wifi";

// Trida gyroskopu
MPU6050 mpu6050(Wire);

// Trida websocketoveho serveru na TCP portu 80
WebSocketsServer websocket = WebSocketsServer(80);

// Trida SSD1306 I2C OLED displeje 128x32 px
Adafruit_SSD1306 display(128, 32, &Wire, -1);

// Pomocne promenne pro aktualizaci dat z gyroskopu
// a zasilani skrze websocket kazdych x milisekund
uint32_t casOdeslanychDat = 0;
uint32_t prodlevaMeziOdeslanim = 100; // ms

// Funkce setup se spusti na zacatku programu
void setup() {
  // Nastartovani seriove linky
  Serial.begin(115200);
  delay(1000);

  // Nastartuj sbernici I2C
  Wire.begin();

  // Nastartuj displej
  Serial.print("Startuji displej na I2C adrese 0x3C... ");
  if (display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("OK");
  }
  else Serial.println("CHYBA");

  // Nastartuj gyroskop
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.clearDisplay();
  display.setCursor(0, 0);
  display.print("Gyroskop... ");
  display.display();
  Serial.print("Startuji gyroskop MPU6050... ");
  mpu6050.begin(); // Knihovna pri startu nevraci zadnou hodnotu a nelze overit, jestli spojeni funguje
  Serial.println("OK");
  display.println("OK");
  display.display();
  delay(2000);

  // Nastartuj Wi-Fi
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.clearDisplay();
  display.setCursor(0, 0);
  display.print("Wifi...");
  display.display();
  Serial.print("Startuji Wi-Fi...");
  WiFi.mode(WIFI_STA);
  // Vynuceni vysokeho vykonu WI-FI, ale tim padem i vyssi spotreby
  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv415esp_wifi_set_ps14wifi_ps_type_t
  esp_wifi_set_ps(WIFI_PS_NONE);
  WiFi.begin(ssid, password);
  // Konfigurace MDNS
  // Na podporovanych platformach bude
  // Mrija dostupna i na LAN domene mrija.local
  MDNS.begin("mrija");
  MDNS.addService("http", "tcp", 80);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" OK");
  Serial.printf("IP: %s\r\n", WiFi.localIP().toString().c_str());
  display.println(" OK");
  display.display();
  delay(2000);

  // Namir Mriju cumakem k displeji
  // Provedu kalibraci gyroskopu
  // Natoceni cumakem k displeji je klicove,
  // jinak bude Mrija na monitoru natocena
  // pod spatnym uhlem v rovine (osa Z/boceni)
  display.clearDisplay();
  display.setCursor(0, 0);
  display.println("Namir Mriju");
  display.println("k displeji");
  display.display();
  Serial.println("Namir zarizeni v rovine na displej");
  delay(5000);
  display.clearDisplay();
  display.setCursor(0, 0);
  display.print("Kalibruji... ");
  display.display();
  Serial.println("Kalibruji gyroskop... ");
  mpu6050.calcGyroOffsets(true);
  display.print("OK");
  display.display();
  Serial.println("");
  delay(2000);

  // Nastartuj websocketovy server na stanovenem TCP portu
  // Pri udalosti zavola funkci websocketUdalost
  websocket.begin();
  websocket.onEvent(websocketUdalost);

  // Konfigurace je hotova,
  // a tak vypiseme an displej IP adresu Mriji
  // Pote se uz spusti smycka loop
  display.clearDisplay();
  display.setCursor(0, 0);
  display.println("IP adresa letadla:");
  display.println(WiFi.localIP().toString());
  display.display();
}

// Funkce loop se opakuje stale dokola
void loop() {
  // Zpracuj ulohy websocketoveho serveru
  websocket.loop();
  // Pokud se pripojil nejaky websocketovy klient
  // a zaroven uplynulo 100 ms od posledniho odeslani dat,
  // ziskej nove hodnoty z gyroskopu a odesli je vsem
  // pripojenym klientum a pro kontrolu vypis do seriove linky
  if (websocket.connectedClients(false) > 0 && millis() - casOdeslanychDat > prodlevaMeziOdeslanim) {
    casOdeslanychDat = millis();
    mpu6050.update();
    char zprava[100];
    sprintf(zprava, "%f,%f,%f", mpu6050.getAngleX(), mpu6050.getAngleY(), mpu6050.getAngleZ());
    websocket.broadcastTXT(zprava, strlen(zprava));
    Serial.println(zprava);
  }
}

// Tuto funkci zavola websocketovy server, pokud se neco stane
// Registruji udalost pripojeni/odpojeni klientu a prijem textovych dat
void websocketUdalost(uint8_t id, WStype_t typ, uint8_t *data, size_t length) {
  IPAddress ip = websocket.remoteIP(id);
  switch (typ) {
    // Klient se odpojil
    case WStype_DISCONNECTED:
      Serial.printf("Klient %u (%s) se odpojil\r\n", id, ip.toString().c_str());
      break;
    // Klient se pripojil, vypisu jeho IP adresu
    case WStype_CONNECTED:
      Serial.printf("Klient %u se pripojil z IP adresy %s\r\n", id, ip.toString().c_str());
      websocket.sendTXT(id, "#Mrija Websocket Server te vita!");
      break;
    // Textova ASCII data od klienta
    // Jen je vypisu do seriove linky, mohli bychom je ale pouzit
    // treba pro konfiguraci
    case WStype_TEXT:
      Serial.printf("Zprava od klienta %u (%s): %s\n", id, ip.toString().c_str(), data);
      break;
  }
}

Zdrojový kód pro Processing

// https://processing.org/reference/libraries/sound/index.html
import processing.sound.*;

// https://github.com/alexandrainst/processing_websockets
import websockets.*;

// IP adresa a TCP port našeho letadla, tedy Wi-Fi čipu ESP32
String ipAdresaLetadla = "172.17.16.131";
int tcpPortLetadla = 80;

// Proměnné pro souřadncie kamery, obrázku pozadí a 3D modelu letadla
float x,y,z;
PImage pozadi;
PShape letadlo;

// Proměnné s úhly z gyroskopu
float xGyro = 0;
float yGyro = 0;
float zGyro = 0;

// Souřadncie letadla ve scéně
float pX = 0.0;
float pY = 4300;

// Soubor se zvukovou nahrávkou letadla
SoundFile file;

// Pomocné proměnné pro stav linky a její statistiku
boolean dataOk = false;
boolean spojeniOk = false;
int dataPocet = 0;

// Objekt websocketového klientu
WebsocketClient websocket;

// Otáčením kolečka myši přiblížíme, nebo vzdálíme kameru
// a tedy i model letadla
void mouseWheel(MouseEvent event) {
  z += event.getCount();
}

// Funkce setup se zpracuje hned na začátku
// Analogie funkce setup ze světa Arduino
void setup() {
  surface.setTitle("Mrija Simulátor 2022");
  surface.setResizable(false);
  // Vytvoříme 3D scénu v okně s rozměry 1280x720 pixelů
  size(1280, 720, P3D);
  // Souřadnice kamery
  x = width/2;
  y = height/2;
  // Ve výchozím stavu bude kamera hluboko ve scéně
  // Na začátku ji postupně posuneme dozadu,
  // čímž vytvoříme animaci přilétající Mriji
  z = 600;
  // Nastavíme obrázek pozadi okna,
  // nahrajeme 3D model Mriji a začneme přehrávat zvuk motorů
  pozadi = loadImage("brno.jpg");
  letadlo = loadShape("mrija.obj");
  file = new SoundFile(this, "motory.mp3");
  file.play();
  
  // Otevřeme spojení s websocketovým serverem
  // na stanovené IP adrese a TCP portu
  try{
    websocket = new WebsocketClient(this, "ws://" + ipAdresaLetadla + ":" + tcpPortLetadla);
  }
  catch(Exception e){
    dataOk = false;
    println("Nemohu dekodovat uhly X, Y, Z ");
  } 
}

// Funkce draw stále dokola překresluje okno
// Je to tedy analogie funkce loop ze světa Arduino
void draw() {
  // Aktualizujeme nadpis okna, ve kterém vypisujeme FPS
  surface.setTitle("Mrija Simulátor 2022");
  // Pokud nehraje stopa s hlukem motorů, začni ji přehrávat
  if(!file.isPlaying()) file.play();
  // Jako pozadí okna nastav fotografii Brna
  background(pozadi);
  // Nastav pohled kamery na souřadnice X, Y, Z
  translate(x,y,z);
  
  // Pokud je hloubka (z) modelu větší než 0,
  // je model skrytý vpředu mimo záběr, a tak
  // jej začnu vysunovat do záběru. Vznikne animace
  // úvodního příletu letadla do středu obrazovky
  if(z > 0){ 
    z--;   
  }
  
  // Natoč scénu podle aktuální hodnoty X, Y, Z
  // z gyroskopu. Stupně přervedeme na radiány
  rotateX(radians(xGyro));
  rotateY(radians(zGyro));
  rotateZ(radians(yGyro));
  
  // Tvorba komplexního modrého bodového světla
  // Osvit modelu zhruba ze zadního pravého rohu
  // podobně jako na podkladové fotografii
  pointLight(3, 177, 252,  0,  -500,  600);
  pointLight(3, 177, 252,  0,  500,  600);
  pointLight(3, 177, 252,  1000,  0,  0);
  
  // Měřítko modelu
  scale(0.02);
  // vykreslení modelu na souřadnice X, Y
  shape(letadlo, pX, pY);
  
  // 2D vrstva s textovými poisky
  camera();
  hint(DISABLE_DEPTH_TEST);
  noLights();
  textMode(MODEL);
  textSize(20);
  text("Leť bezpečně, jsi nad Brnem!", 10, 30);
  textSize(15);
  text("Naklonění okolo osy Y (klopení, pitch): " + (xGyro) + "°" , 10, 60);
  text("Naklonění okolo osy X (klonění, roll): " + (yGyro) + "°" , 10, 80);
  text("Otáčení okolo osy Z (bočení, yaw): " + (zGyro) + "°" , 10, 100);
  text("Letadlo X: " + (pX), 10, 120);
  text("Letadlo Y: " + (pY), 10, 140);
  text("Kamera X: " + (x), 10, 160);
  text("Kamera Y: " + (y), 10, 180);
  text("Kamera Z: " + (z), 10, 200);
  
  // Barevné indikátory v pravém horním rohu
  // Websocket spojení zelená/červená
  stroke(0,0,0);
  if(spojeniOk)fill(0,255,0);
  else fill(255,0,0);
  rect(width-140, 20, 15, 15);
  fill(0,0,0);
  text(ipAdresaLetadla, width-110, 32);
  
  // Korektní data čevená/zelená
  //Celkový počet přijatých úhlů X, Y, Z
  if(dataOk)fill(0,255,0);
  else fill(255,0,0);
  rect(width-140, 45, 15, 15);
  fill(0,0,0);
  text(dataPocet, width-110, 57);
  
  hint(ENABLE_DEPTH_TEST);
}

// Tato funkce se bude volat, jakmile z WebSocketu dorazí nějaká textová data
void webSocketEvent(String data){
  if(data != null){
    // Zprávy se znakem # na začátku neobsahují souřadnice
    if(data.charAt(0) == '#'){
      data = data.substring(1);
      println(data);
      // Touto zprávou websocketový server přivítá každého nového klienta
      // Jedná se tedy o ověření, že spojení funguje 
      if(data.equals("Mrija Websocket Server te vita!")) spojeniOk = true;
    }
    // Pokud zpráva neobsahuje na začátku znak #,
    // musí to být trojice úhlů X, Y a Z ve formátu:
    // XX.XXXX,YY.YYYY,Z.ZZZZ
    // Dekódujeme tyto hodnoty a uložíme do proměnných
    else{
      String[] casti = split(data, ',');
      try{
      xGyro = -1 * Float.parseFloat(casti[0]);
      yGyro = Float.parseFloat(casti[1]);
      zGyro = Float.parseFloat(casti[2]);
      dataOk = true;
      dataPocet++;
      }
      catch(Exception e){
        dataOk = false;
        println("Nemohu dekódovat úhly X, Y, Z ");
      }
    }
  }
}
Diskuze (4) Další článek: Google kope další hrobeček. Kvůli pandemii po šesti letech ukončí spolujízdu v aplikaci Waze

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , ,