Implementasi TinyML untuk Adaptive Speed Limiter pada Sepeda Motor Berdasarkan Pola Perilaku Pengendara
Kata pengantar
TinyML adalah cabang machine learning yang dirancang untuk berjalan langsung di perangkat kecil berdaya rendah (misalnya mikrokontroler seperti ESP32 atau ARM Cortex-M), sehingga memungkinkan analisis data sensor secara real-time tanpa bergantung pada cloud. Dalam konteks sepeda motor, TinyML dapat digunakan untuk mendeteksi pola perilaku pengendara dan menyesuaikan pembatas kecepatan secara adaptif.
Definisi: Tiny Machine Learning (TinyML) adalah penerapan algoritma ML pada perangkat dengan daya sangat rendah dan kapasitas komputasi terbatas.
- Cara kerja:
- Model dilatih di cloud dengan dataset besar.
- Model dioptimalkan (quantization, pruning) agar ringan.
- Model dijalankan di perangkat edge (mikrokontroler/sensor).
- Keunggulan:
- Konsumsi daya rendah → cocok untuk perangkat baterai.
- Latensi minim → keputusan real-time tanpa kirim data ke server.
- Privasi lebih baik → data diproses lokal. Biaya rendah → tidak perlu infrastruktur cloud besar
Terkait hal ini saya menggunakan tinyml ini untuk melatih speed limiter yang dimana ketika speed pada kendaraan melebihi batas maka akan otomatis mengirimkan data ke platform atau cloud dan membunyikan suara alarm.
Berikut adalah gambaran dari PCB nya dari beberapa pin untuk speed limiter ini nanti pada pin j13 yang terhubung langsung ke GPIO34 karena membutuhkan data pwm serta untuk memprediksi dari data serial speednya.
dan berikut adalah kodenya :
/*
Program SPEED LIMITER ADAPTIF dengan TinyML
Model ML memprediksi batas kecepatan aman berdasarkan:
- Suhu mesin
- Kondisi seatbelt
- Kondisi pintu
- Fuel level
- Harsh events (hb/ha/hc)
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <time.h>
#include <Arduino.h>
#include <ESP32Servo.h>
#include <WebServer.h>
#include <Preferences.h>
#include <TensorFlowLite.h> // ✅ TAMBAH: Library TinyML
// ==================== TAMBAH: Include Model TinyML ====================
// Nanti setelah training, file ini di-generate dari model .tflite
#include "speed_limiter_model.h" // Model adaptive speed limiter
// ==================== TAMBAH: TinyML Setup ====================
static tflite::MicroErrorReporter micro_error_reporter;
tflite::ErrorReporter* error_reporter = µ_error_reporter;
const tflite::Model* model = tflite::GetModel(g_model);
static tflite::MicroInterpreter static_interpreter(
model, resolver, tensor_arena, kTensorArenaSize);
TfLiteTensor* input = nullptr;
TfLiteTensor* output = nullptr;
// Memory arena untuk model (sesuaikan ukuran setelah konversi)
constexpr int kTensorArenaSize = 10 * 1024; // 10KB - adjust sesuai model
uint8_t tensor_arena[kTensorArenaSize];
// ==================== KONFIGURASI AWAL (SAMA SEPERTI KODE ANDA) ====================
Preferences preferences;
const char* ntpServer = "time.nist.gov";
const long gmtOffset_sec = 7 * 3600;
const int daylightOffset_sec = 0;
bool shouldStartConfigPortal = false;
WebServer server(80);
WiFiServer servers(81);
// Pin Deklarasi
const int thermistorPin = 32;
#define POT_PIN 34
#define RXD2 16
#define TXD2 17
const int buzz = 33;
#define seat 25
#define doors 21
#define igni 13
// Button
#define hb 5
#define ha 23
#define hc 14
#define sos 22
// Fuel
const int fuelPin = 35;
static const int servoPin = 26;
Servo servo1;
int start, ignitions, door, hbs1, hba1, hbc1, sos1, sensor2, belt;
// Fuel mapping
const int points = 5;
int adcMap[points] = {120, 1200, 2139, 3364, 4096};
float literMap[points] = {0.0, 0.62, 1.21, 1.96, 2.99};
// Filtering
const int samples = 16;
float filtered = 0.0;
const float alpha = 0.1;
String uid;
String data = "";
String datas = "";
int potValue = 0;
float odometer = 2000;
float speed = 0;
// Thermistor constants
const float BETA = 3950;
const float R0 = 10000;
const float Kelvin = 298.15;
const float R_FIXED = 10000;
bool servoMoved = false;
// Status sensor
const int inputPins[] = {seat, doors, hb, ha, hc, sos};
bool status[] = {false, false, false, false, false, false};
// GPS Coordinates (sama seperti kode Anda)
float coordinates[][2] = {
{1.434437, 103.784692}, {1.434652, 103.784134}, {1.4349, 103.783696},
{1.435218, 103.783301}, {1.435578, 103.782941}, {1.436059, 103.782632},
{1.43672, 103.782495}, {1.437389, 103.782495}, {1.437826, 103.782632},
{1.439418, 103.783662}, {1.440825, 103.784692}, {1.441171, 103.785006},
{1.441186, 103.785139}, {1.440826, 103.785624}, {1.440044, 103.787019},
{1.439697, 103.787679}, {1.438873, 103.788657}, {1.438333, 103.788889},
{1.438049, 103.788666}, {1.437749, 103.787799}, {1.437526, 103.787164},
{1.436664, 103.787404}, {1.436029, 103.787387}, {1.435668, 103.786408},
{1.435377, 103.785584}, {1.434879, 103.785344}, {1.434432, 103.785323},
{1.434359, 103.784979}, {1.434445, 103.784688}
};
const int coordinateCount = sizeof(coordinates) / sizeof(coordinates[0]);
int coordinateIndex = 0;
// ==================== TAMBAH: Variabel untuk Adaptive Limiter ====================
float safeSpeedLimit = 120.0; // Default limit, akan di-update oleh model
float lastMLPrediction = 120.0;
unsigned long lastMLInference = 0;
const unsigned long mlInferenceInterval = 500; // Inferensi tiap 500ms
// ==================== SEMUA FUNGSI ASLI (tidak berubah) ====================
// [Fungsi-fungsi Anda tetap sama: startWiFiConfigPortal, connectToWiFi,
// readTemperature, updateStatus, readRawAveraged, readSmoothed,
// adcToLiter, calcPercent, getUnixTime]
// Saya tulis ulang secara ringkas agar kode tidak terlalu panjang
// Fungsi-fungsi di bawah ini SAMA PERSIS dengan kode Anda
void startWiFiConfigPortal() { /* ... kode Anda tetap ... */ }
void connectToWiFi() { /* ... kode Anda tetap ... */ }
float readTemperature() { /* ... kode Anda tetap ... */ }
void updateStatus() { /* ... kode Anda tetap ... */ }
int readRawAveraged() { /* ... kode Anda tetap ... */ }
float readSmoothed() { /* ... kode Anda tetap ... */ }
float adcToLiter(int adc) { /* ... kode Anda tetap ... */ }
float calcPercent(float liter) { /* ... kode Anda tetap ... */ }
unsigned long getUnixTime() { /* ... kode Anda tetap ... */ }
// ==================== TAMBAH: Fungsi Adaptive Speed Limiter dengan TinyML ====================
/**
* Inisialisasi TinyML interpreter
*/
void initTinyML() {
Serial.println("🚀 Inisialisasi TinyML Adaptive Speed Limiter...");
// Setup model
if (model->version() != TFLITE_SCHEMA_VERSION) {
Serial.println("⚠️ Model schema version mismatch!");
return;
}
// Alokasi tensor
static_interpreter.AllocateTensors();
// Dapatkan pointer ke input dan output tensor
input = static_interpreter.input(0);
output = static_interpreter.output(0);
Serial.println("✅ TinyML siap! Model akan memprediksi batas kecepatan aman.");
}
/**
* Prediksi batas kecepatan aman menggunakan TinyML
* Input model: [suhu_mesin, fuel_level, seatbelt_status, door_status, harsh_braking, harsh_accel]
* Output model: safe_speed_limit (km/h)
*/
float predictSafeSpeedLimit(float temp, float fuel, int seatStatus, int doorStatus,
int harshBrake, int harshAccel, int harshCorner) {
// Cek apakah model sudah siap
if (input == nullptr || output == nullptr) {
Serial.println("⚠️ Model belum diinisialisasi, pakai default 120");
return 120.0;
}
// ==================== INPUT TENSOR ====================
// Normalisasi input ke range 0-1 (sesuai training)
// Suhu: 0-120°C → 0-1
input->data.f[0] = constrain(temp / 120.0, 0.0, 1.0);
// Fuel: 0-100% → 0-1
input->data.f[1] = constrain(fuel / 100.0, 0.0, 1.0);
// Seatbelt: 0 (tidak pakai) atau 1 (pakai)
input->data.f[2] = seatStatus ? 1.0 : 0.0;
// Door: 0 (tertutup) atau 1 (terbuka)
input->data.f[3] = doorStatus ? 1.0 : 0.0;
// Harsh braking: 0 atau 1
input->data.f[4] = harshBrake ? 1.0 : 0.0;
// Harsh acceleration: 0 atau 1
input->data.f[5] = harshAccel ? 1.0 : 0.0;
// Harsh cornering: 0 atau 1
input->data.f[6] = harshCorner ? 1.0 : 0.0;
// ==================== INFERENCE ====================
TfLiteStatus invokeStatus = static_interpreter.Invoke();
if (invokeStatus != kTfLiteOk) {
Serial.println("❌ Inference gagal!");
return lastMLPrediction; // Return prediksi terakhir
}
// ==================== OUTPUT TENSOR ====================
// Model output: safe speed limit (km/h) dalam range 40-140
float rawOutput = output->data.f[0];
// Denormalisasi dari 0-1 ke 40-140 km/h
float safeLimit = 40.0 + (rawOutput * 100.0);
// Batasi hasil
safeLimit = constrain(safeLimit, 40.0, 140.0);
// Smoothing perubahan limit (hindari lompatan drastis)
safeLimit = (lastMLPrediction * 0.7) + (safeLimit * 0.3);
lastMLPrediction = safeLimit;
// Logging untuk debugging
Serial.print("🧠 [TinyML] Input: Temp=");
Serial.print(temp, 1);
Serial.print("°C, Fuel=");
Serial.print(fuel, 0);
Serial.print("%, Seat=");
Serial.print(seatStatus);
Serial.print(", Door=");
Serial.print(doorStatus);
Serial.print(", HB=");
Serial.print(harshBrake);
Serial.print(", HA=");
Serial.print(harshAccel);
Serial.print(", HC=");
Serial.print(harshCorner);
Serial.print(" → Safe Limit: ");
Serial.print(safeLimit);
Serial.println(" km/h");
return safeLimit;
}
/**
* Evaluasi apakah kecepatan saat ini melebihi batas aman
* dan kontrol buzzer + throttle
*/
void evaluateSpeedLimiter(float currentSpeed, float safeLimit, int ignitionStatus) {
if (ignitionStatus == 0) {
// Ignition OFF, matikan buzzer
digitalWrite(buzz, HIGH);
return;
}
// Konversi currentSpeed dari persentase throttle ke km/h (adjust sesuai kebutuhan)
// Asumsi: throttle 0-180 → 0-180 km/h
float currentSpeedKMH = currentSpeed; // atau map(currentSpeed, 0, 180, 0, 180)
if (currentSpeedKMH > safeLimit) {
// OVERSPEED! Aktifkan buzzer
digitalWrite(buzz, LOW); // LOW = ON (sesuai kode Anda)
Serial.print("⚠️ SPEED LIMITER ACTIVE! ");
Serial.print(currentSpeedKMH);
Serial.print(" km/h > ");
Serial.print(safeLimit);
Serial.println(" km/h");
} else {
// Aman, matikan buzzer
digitalWrite(buzz, HIGH);
}
}
// ==================== SETUP ====================
void setup() {
Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2);
// Inisialisasi pin (SAMA PERSIS KODE ANDA)
pinMode(buzz, OUTPUT);
digitalWrite(buzz, HIGH);
pinMode(seat, INPUT_PULLUP);
pinMode(doors, INPUT);
pinMode(igni, INPUT_PULLUP);
pinMode(hb, INPUT_PULLUP);
pinMode(ha, INPUT_PULLUP);
pinMode(hc, INPUT_PULLUP);
pinMode(sos, INPUT_PULLUP);
servo1.attach(servoPin);
// WIFI & RFID (SAMA)
preferences.begin("uid_config", false);
preferences.remove("uid");
uid = preferences.getString("uid", "");
preferences.end();
analogSetPinAttenuation(fuelPin, ADC_11db);
analogReadResolution(12);
filtered = analogRead(fuelPin);
// WiFi Connection (SAMA)
connectToWiFi();
if (shouldStartConfigPortal) {
startWiFiConfigPortal();
}
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
server.begin();
servers.begin();
// ==================== TAMBAH: Inisialisasi TinyML ====================
initTinyML();
Serial.println("✅ Adaptive Speed Limiter dengan TinyML siap!");
}
// ==================== LOOP ====================
void loop() {
if (shouldStartConfigPortal) {
server.handleClient();
return;
}
servo1.write(0);
HTTPClient http;
// Baca sensor (SAMA PERSIS KODE ANDA)
float lat = coordinates[coordinateIndex][0];
float lon = coordinates[coordinateIndex][1];
int id = 11220039;
unsigned long timestamp = getUnixTime();
float altitude = 10;
int newValue = analogRead(POT_PIN);
newValue = map(newValue, 0, 4096, 0, 180);
newValue = constrain(newValue, 0, 180);
int speedpersentase = constrain(newValue, 0, 100);
// Baca fuel
int raw = readSmoothed();
float liter = adcToLiter(raw);
float percent = calcPercent(liter);
const int adcMin = 500;
const int adcMax = 3000;
float fuelPercentage = (float)(filtered - adcMin) / (adcMax - adcMin) * 100.0;
int fuelPercentages = constrain(fuelPercentage, 0, 100);
// Baca temperatur
float temps = readTemperature();
// Baca status sensor digital
belt = (digitalRead(seat) == LOW) ? 0 : 1;
door = (digitalRead(doors) == LOW) ? 0 : 1;
hbs1 = (digitalRead(hb) == HIGH) ? 0 : 1;
hba1 = (digitalRead(ha) == HIGH) ? 0 : 1;
hbc1 = (digitalRead(hc) == HIGH) ? 1 : 0;
sos1 = (digitalRead(sos) == HIGH) ? 1 : 0;
// ==================== TAMBAH: Adaptive Speed Limit dengan TinyML ====================
// Jalankan inferensi setiap interval tertentu
unsigned long now = millis();
if (now - lastMLInference >= mlInferenceInterval) {
lastMLInference = now;
// Prediksi batas kecepatan aman berdasarkan kondisi saat ini
safeSpeedLimit = predictSafeSpeedLimit(
temps, // Suhu mesin
percent, // Fuel level (%)
belt, // Seatbelt status
door, // Door status
hbs1, // Harsh braking
hba1, // Harsh acceleration
hbc1 // Harsh cornering
);
}
// ==================== APLIKASI SPEED LIMITER ====================
// Cek apakah ignition ON dan kecepatan melebihi batas aman
if (digitalRead(igni) == LOW || uid.equalsIgnoreCase("79BF900") || uid.equalsIgnoreCase("F9BF900")) {
// Ignition ON
// *** INI PERUBAHAN UTAMA: HAPUS if(newValue >= 120) ***
// *** GANTI DENGAN LOGIKA ADAPTIF ***
if (newValue > safeSpeedLimit) {
digitalWrite(buzz, LOW); // Buzzer ON
Serial.print("🚨 SPEED LIMITER: ");
Serial.print(newValue);
Serial.print(" km/h > ");
Serial.print(safeSpeedLimit);
Serial.println(" km/h (ADAPTIVE LIMIT)");
} else {
digitalWrite(buzz, HIGH); // Buzzer OFF
}
// Servo control (sama seperti kode Anda)
if (!servo1.attached()) {
servo1.attach(servoPin);
delay(50);
}
if (!servoMoved) {
for (int pos = 0; pos <= 120; pos++) {
servo1.write(pos);
delay(10);
}
for (int pos = 120; pos >= 0; pos--) {
servo1.write(pos);
delay(10);
}
servoMoved = true;
}
servo1.write(newValue);
ignitions = 1;
// Build URL untuk HTTP request (SAMA)
String url = "http://telematics.transtrack.id:6055/";
url += "?id=" + String(id);
url += "&speedecm=" + String(newValue);
url += "&odometer=" + String(odometer);
url += "&ignition=" + String(ignitions);
if (newValue > 1 && newValue <= 999) {
url += "&lat=" + String(lat, 6);
url += "&lon=" + String(lon, 6);
}
// TAMBAH: Kirim juga safe speed limit ke server (opsional)
url += "&safe_limit=" + String(safeSpeedLimit);
url += "&seat=" + String(belt);
url += "&temp=" + String(temps);
url += "&door=" + String(door);
url += "×tamp=" + String(timestamp);
url += "&altitude=" + String(altitude, 2);
url += "&hb=" + String(hbs1);
url += "&ha=" + String(hba1);
url += "&hc=" + String(hbc1);
url += "&sos=" + String(sos1);
url += "&fuel=" + String(liter, 1);
url += "&fuellvl=" + String(percent);
} else {
// Ignition OFF (sama persis kode Anda)
digitalWrite(buzz, HIGH);
if (servo1.attached()) {
servo1.write(0);
servo1.detach();
}
servoMoved = false;
ignitions = 0;
String url = "http://telematics.transtrack.id:6055/";
url += "?id=" + String(id);
url += "&ignition=" + String(ignitions);
url += "&odometer=" + String(odometer);
url += "&temp=" + String(temps);
url += "&seat=" + String(belt);
url += "&door=" + String(door);
url += "×tamp=" + String(timestamp);
url += "&altitude=" + String(altitude, 2);
url += "&hb=" + String(hbs1);
url += "&ha=" + String(hba1);
url += "&hc=" + String(hbc1);
url += "&sos=" + String(sos1);
url += "&fuel=" + String(liter, 1);
url += "&fuellvl=" + String(percent);
}
// ==================== NEXTION DISPLAY (SAMA PERSIS KODE ANDA) ====================
// (Kode untuk Serial2 ke Nextion tetap sama)
int suhu = data.toInt();
String nextionTempCmd = "n4.val=" + String(suhu);
Serial2.print(nextionTempCmd);
Serial2.write(0xFF);
Serial2.write(0xFF);
Serial2.write(0xFF);
// ... dan seterusnya (semua kode Nextion Anda tetap)
// ==================== HTTP REQUEST (SAMA) ====================
Serial.print("Meminta URL Target: ");
Serial.println(url);
url.replace(" ", "%20");
http.begin(url);
int httpResponseCode = http.GET();
if (httpResponseCode > 0) {
Serial.print("HTTP Respon kode: ");
Serial.println(httpResponseCode);
} else {
Serial.print("Kode error: ");
Serial.println(httpResponseCode);
}
http.end();
coordinateIndex++;
if (coordinateIndex >= coordinateCount) {
coordinateIndex = 0;
}
delay(1000);
}
untuk adaptive ini setiap speed yang diukur > 120km/h maka akan mengirimkan alert ke sistem