Ασύρματη καταγραφή ποιότητας διαδρομών με ESP32

Ασύρματη καταγραφή ποιότητας διαδρομών με ESP32

Εισαγωγή – Τι είναι το έργο AccessCity;

Το εργαστήριο αυτό αποτελεί μέρος του έργου AccessCity. Στόχος του έργου είναι να μετρήσουμε και να χαρτογραφήσουμε την ποιότητα των διαδρομών με καρότσι στα πεζοδρόμια της πόλης μας, ώστε να καταλάβουμε:

  • πού η μετακίνηση είναι εύκολη και ασφαλής για τους συμπολίτες μας που μετακινούνται με καρότσια λόγω κινητικών προβλημάτων,
  • και πού υπάρχουν προβλήματα (σπασμένα πεζοδρόμια, έντονες δονήσεις, εμπόδια κ.ά.).

Για να το πετύχουμε αυτό, φτιάχνουμε μια μικρή συσκευή που:

  • μπαίνει σε ένα καρότσι,
  • καταγράφει πού κινείται (GPS),
  • και πόσο κουνιέται (αισθητήρας δονήσεων).

Πώς συνδέεται αυτό το εργαστήριο με τα προηγούμενα;

Σε προηγούμενα εργαστήρια (FireBeetle 2 ESP32 – Workshop Guide και σύνδεση και δοκιμή GPS) μάθαμε:

  • να προγραμματίζουμε το ESP32,
  • να χρησιμοποιούμε βιβλιοθήκες,
  • να διαβάζουμε δεδομένα από αισθητήρες,
  • τι είναι τα NMEA μηνύματα,
  • πώς βρίσκουμε δορυφόρους,
  • και πώς παίρνουμε πραγματικές συντεταγμένες και ταχύτητα.

Σε αυτό το εργαστήριο θα ενώσουμε τα δύο αυτά κομμάτια. Θα δοκιμάσουμε:

  • να πάρουμε πραγματικά δοκιμαστικά δεδομένα από το GPS και τον αισθητήρα δονήσεων (LIS2DH),
  • να τα προσαρμόσουμε στις ανάγκες του έργου AccessCity,
  • και να ελέγξουμε αν μπορούμε να τα στείλουμε ασύρματα μέσω Bluetooth Low Energy (BLE) σε ένα κινητό τηλέφωνο.

⚠️ Δεν θα φτιάξουμε ακόμη την τελική εφαρμογή. Θα χρησιμοποιήσουμε ένα απλό BLE terminal στο κινητό για δοκιμές.

Πώς θα δουλεύει η συσκευή μας; (η φιλοσοφία)

Πριν γράψουμε κώδικα, πρέπει να καταλάβουμε πώς θέλουμε να σκέφτεται η συσκευή.

1️⃣ Κατάσταση αναμονής

Όταν ανοίγει:

  • η συσκευή δεν καταγράφει τίποτα,
  • περιμένει μια εντολή από το κινητό μέσω Bluetooth.

2️⃣ Έναρξη καταγραφής (START)

Όταν λάβει την εντολή START ξεκινά η καταγραφή της διαδρομής. Από εκείνη τη στιγμή:

📍 GPS

  • το GPS μάς δίνει νέα θέση περίπου 1 φορά το δευτερόλεπτο.
  • Αυτό είναι αρκετό, γιατί:
    • το καρότσι κινείται αργά,
    • αν παίρναμε πιο συχνά, θα είχαμε πολλά ίδια σημεία χωρίς λόγο,
    • θα γέμιζε άσκοπα η μνήμη.

👉 Άρα: 1 GPS σημείο ανά δευτερόλεπτο είναι ιδανικό.

🌊 Δονήσεις

  • οι δονήσεις αλλάζουν πολύ πιο γρήγορα από το GPS,
  • γι’ αυτό:
    • μέσα σε κάθε δευτερόλεπτο παίρνουμε πολλές μικρές μετρήσεις από τον αισθητήρα LIS2DH,
    • και στο τέλος του δευτερολέπτου τις “συνοψίζουμε” σε έναν αριθμό.

Αυτός ο αριθμός λέει:

«Πόσο κουνήθηκε το καρότσι αυτό το δευτερόλεπτο;»

Συνήθως:

  • μικρός αριθμός → ομαλή διαδρομή
  • μεγάλος αριθμός → κακό έδαφος / έντονες δονήσεις

3️⃣ Τι αποθηκεύουμε τελικά;

Για κάθε δευτερόλεπτο αποθηκεύουμε μία γραμμή δεδομένων:

χρόνος, latitude, longitude, ταχύτητα, δείκτης δόνησης

Έτσι, όλη η διαδρομή γίνεται ένα μικρό “ημερολόγιο”.

4️⃣ Παύση και τερματισμός

  • PAUSE → η συσκευή σταματά προσωρινά να γράφει
  • CONTINUE → συνεχίζει από εκεί που σταμάτησε
  • FINISH / STOP → σταματά οριστικά την καταγραφή και στέλνει όλα τα δεδομένα στο κινητό μέσω Bluetooth.

📤 Τα δεδομένα στέλνονται σε μορφή κειμένου (CSV), ώστε να μπορούμε να τα δούμε εύκολα και να τα επεξεργαστούμε αργότερα στην εφαρμογή.

Υπενθύμιση συνδέσεων

🔌 Αισθητήρας δονήσεων LIS2DH (I2C)

  • VCC → 3.3V
  • GND → GND
  • SDA → SDA
  • SCL → SCL

🔌 GPS L76K

  • VCC → 3.3V (ή 5V, αν το module το υποστηρίζει)
  • GND → GND
  • TX (GPS) → RX2 του ESP32
  • RX (GPS) → TX2 του ESP32

🟦 Mission 1: Σύνδεση ESP32 με BLE Terminal

🎯 Στόχος

Να ελέγξουμε ότι:

  • το ESP32 εμφανίζεται στο κινητό,
  • γίνεται σύνδεση,
  • και λαμβάνονται απλές εντολές.

📌 Κώδικας BLE δοκιμής

/*
  Mission 1: BLE σύνδεση (test)

  Στόχος:
  1) Το ESP32 να εμφανίζεται στο κινητό ως BLE συσκευή με όνομα "ACCESSCITY-ESP32"
  2) Όταν το κινητό συνδεθεί, το ESP32 να στέλνει μήνυμα "BOOT:OK" κάθε 1 δευτερόλεπτο
  Θέλουμε απλά να ελέγξουμε ότι η ασύρματη σύνδεση BLE δουλεύει.
*/

// Η βασική βιβλιοθήκη Arduino
#include <Arduino.h>

// Βιβλιοθήκες για Bluetooth Low Energy (BLE) στο ESP32
#include <BLEDevice.h>   // αρχικοποίηση BLE συσκευής
#include <BLEServer.h>   // BLE server (το ESP32 παίζει τον ρόλο server)
#include <BLEUtils.h>    // βοηθητικά εργαλεία BLE
#include <BLE2902.h>     // descriptor για να μπορεί το κινητό να παίρνει notifications

/*
  UUIDs (μοναδικοί κωδικοί) για BLE υπηρεσία και χαρακτηριστικά.
  Χρησιμοποιούμε μια πολύ γνωστή “λογική UART” πάνω από BLE:
  - SERVICE: η υπηρεσία μας
  - RX: εκεί γράφει το κινητό (θα το χρησιμοποιήσουμε από Mission 10)
  - TX: από εκεί στέλνει το ESP32 προς το κινητό (notifications)
  Τα συγκεκριμένα UUIDs συχνά δουλεύουν με BLE terminals.
*/
static const char* SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E";
static const char* RX_UUID      = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"; // phone -> ESP (WRITE)
static const char* TX_UUID      = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"; // ESP -> phone (NOTIFY)

// Δείκτης (pointer) στο χαρακτηριστικό που θα στέλνει μηνύματα προς το κινητό
BLECharacteristic* txChar;

// Μεταβλητή που μας λέει αν υπάρχει ενεργή σύνδεση με κινητό
bool connected = false;

/*
  Συνάρτηση: sendLine
  Στέλνει ένα μήνυμα (κειμενάκι) στο κινητό μέσω BLE notification.
  Αν δεν είμαστε συνδεδεμένοι, δεν κάνει τίποτα.
*/
void sendLine(const String& s) {
  if (!connected) return;             // αν δεν υπάρχει σύνδεση, σταμάτα
  txChar->setValue(s.c_str());        // γράφουμε το κείμενο στο TX characteristic
  txChar->notify();                   // το στέλνουμε στο κινητό ως notification
}

/*
  Callbacks για τον server:
  Αυτές οι συναρτήσεις καλούνται αυτόματα όταν:
  - συνδεθεί κινητό
  - αποσυνδεθεί κινητό
*/
class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer*) override {
    connected = true;                 // ενημερώνουμε ότι υπάρχει σύνδεση
  }

  void onDisconnect(BLEServer*) override {
    connected = false;                // ενημερώνουμε ότι δεν υπάρχει σύνδεση
  }
};

void setup() {
  // Ανοίγουμε την Serial για να βλέπουμε μηνύματα στο PC (debug)
  Serial.begin(115200);

  /*
    1) Δίνουμε όνομα στη BLE συσκευή.
    Αυτό θα φαίνεται όταν κάνουμε scan στο κινητό.
  */
  BLEDevice::init("ACCESSCITY-ESP32");

  /*
    2) Δημιουργούμε BLE server.
    Το ESP32 είναι ο "server" και το κινητό είναι ο "client".
  */
  BLEServer* server = BLEDevice::createServer();

  // Βάζουμε τα callbacks ώστε να ξέρουμε πότε συνδέθηκε/αποσυνδέθηκε κινητό
  server->setCallbacks(new ServerCallbacks());

  /*
    3) Δημιουργούμε BLE Service.
    Η "υπηρεσία" είναι σαν ένας φάκελος που περιέχει τα χαρακτηριστικά (RX/TX).
  */
  BLEService* service = server->createService(SERVICE_UUID);

  /*
    4) Δημιουργούμε το TX characteristic (ESP -> phone).
    PROPERTY_NOTIFY = επιτρέπει να στέλνουμε notifications στο κινητό.
  */
  txChar = service->createCharacteristic(
    TX_UUID,
    BLECharacteristic::PROPERTY_NOTIFY
  );

  /*
    5) Προσθέτουμε BLE2902 descriptor.
    Αυτό είναι σημαντικό γιατί πολλά κινητά/terminals το χρειάζονται
    για να ενεργοποιήσουν notifications.
  */
  txChar->addDescriptor(new BLE2902());

  /*
    6) Ξεκινάμε την υπηρεσία.
  */
  service->start();

  /*
    7) Ξεκινάμε διαφήμιση (advertising).
    Αυτό σημαίνει ότι το ESP32 "φωνάζει" στο χώρο:
    "Είμαι εδώ! Μπορείς να συνδεθείς!"
  */
  BLEDevice::getAdvertising()->start();

  Serial.println("BLE ready! Αναζήτησε στο κινητό: ACCESSCITY-ESP32");
}

void loop() {
  /*
    Αν υπάρχει σύνδεση με κινητό, στέλνουμε μήνυμα κάθε 1 δευτερόλεπτο.
    Έτσι βλέπουμε στο BLE terminal ότι:
    - συνδεθήκαμε
    - και το ESP32 μπορεί να στέλνει δεδομένα.
  */
  if (connected) {
    sendLine("BOOT:OK");
    delay(1000); // 1 δευτερόλεπτο
  } else {
    // Αν δεν είμαστε συνδεδεμένοι, δεν χρειάζεται να κάνουμε κάτι.
    delay(50);
  }
}

✅ Δοκιμή

  • Κάνουμε Upload στο ESP32
  • Ανοίγουμε BLE Terminal στο κινητό
  • Scan → βρίσκουμε ACCESSCITY-ESP32 → Connect
  • Πρέπει να εμφανίζεται κάθε 1 sec: BOOT:OK

🟦 Mission 2 – Πλήρης καταγραφή GPS & δονήσεων με BLE

/*
  Mission 2 – AccessCity Logger

  Τι κάνει αυτό το πρόγραμμα:

  - Το ESP32 λειτουργεί σαν BLE server
  - Δέχεται εντολές από κινητό:
      STATUS, START, PAUSE, CONTINUE, FINISH
  - Όταν γράφει:
      • παίρνει GPS δεδομένα (1 φορά το δευτερόλεπτο)
      • μετρά πολλές δονήσεις μέσα σε κάθε δευτερόλεπτο
      • βγάζει έναν "δείκτη δόνησης" (μέσος όρος)
  - Αποθηκεύει κάθε δευτερόλεπτο:
      χρόνος, latitude, longitude, ταχύτητα, δείκτης δόνησης
  - Στο FINISH στέλνει όλα τα δεδομένα στο κινητό (CSV)
*/

// =======================================================
// ΒΑΣΙΚΕΣ ΒΙΒΛΙΟΘΗΚΕΣ
// =======================================================

#include <Arduino.h>        // βασική βιβλιοθήκη Arduino
#include <Wire.h>           // I2C επικοινωνία
#include <DFRobot_LIS2DH.h> // αισθητήρας επιτάχυνσης LIS2DH
#include <TinyGPSPlus.h>    // επεξεργασία GPS δεδομένων

// =======================================================
// BLE ΒΙΒΛΙΟΘΗΚΕΣ
// =======================================================

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// =======================================================
// BLE UUIDs (UART-like επικοινωνία)
// =======================================================

static const char* SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E";
static const char* RX_UUID      = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"; // κινητό -> ESP
static const char* TX_UUID      = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"; // ESP -> κινητό

BLECharacteristic* txChar;   // από εδώ στέλνουμε δεδομένα
bool connected = false;      // αν είμαστε συνδεδεμένοι με κινητό
String rxBuffer;             // buffer για εντολές BLE

// =======================================================
// ΚΑΤΑΣΤΑΣΕΙΣ ΣΥΣΤΗΜΑΤΟΣ
// =======================================================

enum State {
  IDLE,        // αναμονή
  RECORDING,   // καταγραφή
  PAUSED,      // παύση
  SENDING      // αποστολή δεδομένων
};

State state = IDLE;

// =======================================================
// GPS (L76K)
// =======================================================

static const int GPS_RX_PIN = 16; // GPS TX -> ESP32 RX2
static const int GPS_TX_PIN = 17; // GPS RX -> ESP32 TX2 (προαιρετικό)

HardwareSerial gpsSerial(2);  // UART2
TinyGPSPlus gps;

bool gpsHasFix = false;
double gpsLat = 0.0;
double gpsLon = 0.0;
double gpsSpeed = 0.0; // m/s

// =======================================================
// LIS2DH (αισθητήρας δονήσεων)
// =======================================================

DFRobot_LIS2DH_I2C acce;

// =======================================================
// ΑΠΟΘΗΚΕΥΣΗ ΔΕΔΟΜΕΝΩΝ
// =======================================================

static const int MAX_RECORDS = 1200; // ~20 λεπτά (1 record/sec)
String records[MAX_RECORDS];
int recordCount = 0;

// =======================================================
// ΧΡΟΝΙΣΜΟΣ
// =======================================================

unsigned long lastSecond = 0;

// =======================================================
// ΒΟΗΘΗΤΙΚΗ ΣΥΝΑΡΤΗΣΗ BLE ΑΠΟΣΤΟΛΗΣ
// =======================================================

void sendLine(const String& s) {
  if (!connected) return;
  txChar->setValue(s.c_str());
  txChar->notify();
  delay(5); // μικρή καθυστέρηση για σταθερότητα
}

// =======================================================
// ΥΠΟΛΟΓΙΣΜΟΣ ΔΕΙΚΤΗ ΔΟΝΗΣΗΣ (1 δευτερόλεπτο)
// =======================================================

float calculateVibrationIndex() {
  const int samples = 50;   // πόσες μετρήσεις θα πάρουμε
  const int delayMs = 20;   // 50 x 20ms = 1000ms

  float sum = 0;

  for (int i = 0; i < samples; i++) {
    // Διαβάζουμε τους 3 άξονες
    float x = abs(acce.readAccX());
    float y = abs(acce.readAccY());
    float z = abs(acce.readAccZ());

    // Απλός δείκτης: άθροισμα απόλυτων τιμών
    sum += (x + y + z);
    delay(delayMs);
  }

  // Επιστρέφουμε τον μέσο όρο
  return sum / samples;
}

// =======================================================
// ΕΝΗΜΕΡΩΣΗ GPS
// =======================================================

void updateGPS() {
  while (gpsSerial.available()) {
    gps.encode(gpsSerial.read());
  }

  if (gps.location.isValid() && gps.location.isUpdated()) {
    gpsHasFix = true;
    gpsLat = gps.location.lat();
    gpsLon = gps.location.lng();
  }

  if (gps.speed.isValid() && gps.speed.isUpdated()) {
    gpsSpeed = gps.speed.mps();
  }
}

// =======================================================
// ΔΙΑΧΕΙΡΙΣΗ ΕΝΤΟΛΩΝ BLE
// =======================================================

void handleCommand(String cmd) {
  cmd.trim();
  cmd.toUpperCase();

  if (cmd == "STATUS") {
    sendLine("STATUS:state=" + String(state) +
             ",records=" + String(recordCount) +
             ",gpsFix=" + String(gpsHasFix));
  }

  else if (cmd == "START") {
    if (!gpsHasFix) {
      sendLine("ERR:NO_GPS_FIX");
      return;
    }
    state = RECORDING;
    lastSecond = 0;
    sendLine("ACK:START");
  }

  else if (cmd == "PAUSE") {
    if (state == RECORDING) {
      state = PAUSED;
      sendLine("ACK:PAUSE");
    }
  }

  else if (cmd == "CONTINUE") {
    if (state == PAUSED) {
      state = RECORDING;
      sendLine("ACK:CONTINUE");
    }
  }

  else if (cmd == "FINISH") {
    if (state == RECORDING || state == PAUSED) {
      state = SENDING;
      sendLine("ACK:FINISH");
    }
  }

  else {
    sendLine("ERR:UNKNOWN_CMD");
  }
}

// =======================================================
// BLE CALLBACKS
// =======================================================

class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer*) override { connected = true; }
  void onDisconnect(BLEServer*) override { connected = false; }
};

class RxCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic* ch) override {
    std::string value = ch->getValue();
    for (char c : value) {
      if (c == '\n') {
        handleCommand(rxBuffer);
        rxBuffer = "";
      } else if (c != '\r') {
        rxBuffer += c;
      }
    }
  }
};

// =======================================================
// SETUP
// =======================================================

void setup() {
  Serial.begin(115200);

  // --- LIS2DH ---
  Wire.begin();
  while (!acce.begin()) {
    Serial.println("Δεν βρέθηκε ο LIS2DH...");
    delay(1000);
  }
  acce.setRange(DFRobot_LIS2DH::e16_g);

  // --- GPS ---
  gpsSerial.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);

  // --- BLE ---
  BLEDevice::init("ACCESSCITY-ESP32");
  BLEServer* server = BLEDevice::createServer();
  server->setCallbacks(new ServerCallbacks());

  BLEService* service = server->createService(SERVICE_UUID);

  txChar = service->createCharacteristic(
    TX_UUID,
    BLECharacteristic::PROPERTY_NOTIFY
  );
  txChar->addDescriptor(new BLE2902());

  BLECharacteristic* rxChar = service->createCharacteristic(
    RX_UUID,
    BLECharacteristic::PROPERTY_WRITE
  );
  rxChar->setCallbacks(new RxCallbacks());

  service->start();
  BLEDevice::getAdvertising()->start();

  sendLine("BOOT:OK");
}

// =======================================================
// LOOP
// =======================================================

void loop() {
  updateGPS();

  // --- ΚΑΤΑΓΡΑΦΗ ---
  if (state == RECORDING) {
    if (millis() - lastSecond >= 1000) {
      lastSecond = millis();

      float vi = calculateVibrationIndex();

      if (recordCount < MAX_RECORDS) {
        String line = String(millis() / 1000) + "," +
                      String(gpsLat, 6) + "," +
                      String(gpsLon, 6) + "," +
                      String(gpsSpeed, 2) + "," +
                      String(vi, 2);

        records[recordCount++] = line;
        sendLine("REC:" + String(recordCount));
      }
    }
  }

  // --- ΑΠΟΣΤΟΛΗ ΔΕΔΟΜΕΝΩΝ ---
  if (state == SENDING) {
    sendLine("#LOG_BEGIN");
    sendLine("t,lat,lon,speed,vi");

    for (int i = 0; i < recordCount; i++) {
      sendLine(records[i]);
    }

    sendLine("#LOG_END");
    state = IDLE;
  }

  delay(5);
}

📲 Δοκιμές με BLE Terminal

  1. Συνδέσου στο ACCESSCITY-ESP32
  2. Πήγαινε έξω για GPS fix
  3. Στείλε:
STATUS
START
  1. Περπάτησε / κούνησε τη συσκευή
  2. Στείλε:
FINISH
  1. Πρέπει να εμφανιστεί:
#LOG_BEGIN
t,lat,lon,speed,vi
...
#LOG_END

🟥 Troubleshooting

🟥 Δεν βρίσκει GPS

✔ Πηγαίνουμε έξω
✔ Περιμένουμε 1–2 λεπτά
✔ Ελέγχουμε baud (9600)

🟥 Δεν συνδέεται BLE

✔ Bluetooth ON
✔ Reset ESP32
✔ Ξανασκανάρισμα

🟦 Τελικό σημείωμα

«Σε αυτό το εργαστήριο:
✓ ενώσαμε GPS και αισθητήρα δονήσεων
✓ σχεδιάσαμε σωστή δειγματοληψία δεδομένων
✓ στείλαμε πραγματικές καταγραφές ασύρματα στο κινητό

Σχόλια

Δεν υπάρχουν ακόμη σχόλια. Γιατί δεν ξεκινάτε τη συζήτηση;

Αφήστε μια απάντηση