Εισαγωγή – Τι είναι το έργο 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
- Συνδέσου στο
ACCESSCITY-ESP32 - Πήγαινε έξω για GPS fix
- Στείλε:
STATUS
START
- Περπάτησε / κούνησε τη συσκευή
- Στείλε:
FINISH
- Πρέπει να εμφανιστεί:
#LOG_BEGIN
t,lat,lon,speed,vi
...
#LOG_END
🟥 Troubleshooting
🟥 Δεν βρίσκει GPS
✔ Πηγαίνουμε έξω
✔ Περιμένουμε 1–2 λεπτά
✔ Ελέγχουμε baud (9600)
🟥 Δεν συνδέεται BLE
✔ Bluetooth ON
✔ Reset ESP32
✔ Ξανασκανάρισμα
🟦 Τελικό σημείωμα
«Σε αυτό το εργαστήριο:
✓ ενώσαμε GPS και αισθητήρα δονήσεων
✓ σχεδιάσαμε σωστή δειγματοληψία δεδομένων
✓ στείλαμε πραγματικές καταγραφές ασύρματα στο κινητό
