ESP32 üzerinde TinyML, ucuz bir Wi-Fi mikrodenetleyicisinin sensör verilerini kendi başına sınıflandırmasını ve yalnızca yanıtı bildirmesini sağlar. Bu eğitimde Python'da küçük bir titreşim sınıflandırıcısı eğitecek, onu int8'e nicemleyecek, Espressif'in TensorFlow Lite Micro portuyla bir ESP32 üzerinde çalıştıracak ve her sınıflandırmayı MQTT üzerinden broker.connectmq.com'a yayınlayacaksınız. Her şey çıplak bir ESP32 kartında çalışır; çünkü firmware, siz gerçek bir ivmeölçer takana kadar sensör sinyalini simüle eder.
Ne İnşa Edeceksiniz
Cihaz 1 kHz'te 256 noktalık bir titreşim penceresi örnekler, üç öznitelik (RMS, tepe faktörü ve sıfır geçiş oranı) hesaplar, küçük bir sinir ağı çalıştırır ve makineyi idle, normal veya fault olarak sınıflandırır. Sınıf her değiştiğinde ve ayrıca her 30 saniyede bir kalp atışı olarak bir JSON sonucu yayınlar ve Last Will ile retained bir online/offline durumu tutar.
| Parça | Seçim |
|---|---|
| Kart | Herhangi bir ESP32 veya ESP32-S3 geliştirme kartı |
| Framework | ESP-IDF 5.x |
| Çıkarım çalışma zamanı | espressif/esp-tflite-micro bileşeni |
| Eğitim | Python 3, TensorFlow 2.x, NumPy |
| MQTT istemcisi | ESP-IDF esp-mqtt |
| Broker | Test için broker.connectmq.com:1883 |
Neden Arduino IDE yerine ESP-IDF? Espressif, esp-tflite-micro'yu güncel upstream TensorFlow Lite Micro ile uyumlu tutar ve ESP32-S3 için optimize edilmiş çekirdekler sunar. Birçok Arduino portu upstream API'nin gerisinde kalır; bu da yaygın bir derleme hatası kaynağıdır. TinyML sizin için yeniyse önce TinyML nedir yazısına göz atın.
Adım 1: Modeli Python'da Eğitin ve Nicemleyin
ESP-IDF projesi için main alt klasörüne sahip bir tinyml_mqtt klasörü oluşturun ve bu eğitim betiğini yanına koyun. Betik etiketli sentetik titreşim pencereleri üretir, küçük bir yoğun ağ eğitir, onu tamamen int8 bir TensorFlow Lite modeline dönüştürür ve firmware'in ihtiyaç duyduğu öznitelik normalizasyon sabitleri dahil olmak üzere main/model_data.cc ve main/model_data.h dosyalarını yazar.
pip install tensorflow numpy
# train_model.py
import numpy as np
import tensorflow as tf
FS, N = 1000, 256
LABELS = ["idle", "normal", "fault"]
rng = np.random.default_rng(42)
def gen(cls):
t = np.arange(N) / FS
if cls == 0: # idle: yalnızca sensör gürültüsü
return 0.05 * rng.standard_normal(N)
x = rng.uniform(0.8, 1.2) * np.sin(2 * np.pi * 50 * t + rng.uniform(0, 2 * np.pi))
x += 0.1 * rng.standard_normal(N)
if cls == 2: # fault: periyodik darbeler
idx = rng.choice(N, size=rng.integers(4, 10), replace=False)
x[idx] += rng.uniform(2, 3, idx.size) * rng.choice([-1, 1], idx.size)
return x
def features(x):
rms = np.sqrt(np.mean(x ** 2))
crest = np.max(np.abs(x)) / (rms + 1e-6)
zcr = np.mean((x[1:] < 0) != (x[:-1] < 0))
return np.array([rms, crest, zcr], dtype=np.float32)
y = rng.integers(0, 3, 6000)
X = np.stack([features(gen(c)) for c in y])
mean, std = X.mean(axis=0), X.std(axis=0)
Xn = ((X - mean) / std).astype(np.float32)
model = tf.keras.Sequential([
tf.keras.Input(shape=(3,)),
tf.keras.layers.Dense(16, activation="relu"),
tf.keras.layers.Dense(3, activation="softmax"),
])
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"])
model.fit(Xn, y, epochs=30, batch_size=32, validation_split=0.2, verbose=2)
def representative_data():
for i in range(300):
yield [Xn[i:i + 1]]
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_data
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_model = converter.convert()
print("model size:", len(tflite_model), "bytes")
hex_bytes = ", ".join(f"0x{b:02x}" for b in tflite_model)
with open("tinyml_mqtt/main/model_data.cc", "w") as f:
f.write('#include "model_data.h"\n\n')
f.write(f"alignas(16) const unsigned char g_model[] = {{{hex_bytes}}};\n")
f.write(f"const unsigned int g_model_len = {len(tflite_model)};\n")
with open("tinyml_mqtt/main/model_data.h", "w") as f:
f.write("#pragma once\n\n")
f.write("extern const unsigned char g_model[];\nextern const unsigned int g_model_len;\n\n")
f.write("constexpr float kFeatMean[3] = {%s};\n" % ", ".join(f"{v:.6f}f" for v in mean))
f.write("constexpr float kFeatStd[3] = {%s};\n" % ", ".join(f"{v:.6f}f" for v in std))
f.write('constexpr const char *kLabels[3] = {"idle", "normal", "fault"};\n')
print("wrote main/model_data.cc and main/model_data.h")
Modelde iki FULLY_CONNECTED katmanı ve bir SOFTMAX bulunur; int8 sürümü yalnızca birkaç kilobayttır. Sentetik sınıflar birbirinden iyi ayrıldığı için doğrulama doğruluğu neredeyse mükemmel olmalıdır. Gerçek verilerle ise zamanınızın çoğunu ağa değil, veri toplamaya ve özniteliklere harcamayı bekleyin.
Neden Tam Int8 Nicemleme
Yukarıdaki dönüştürücü ayarları; ağırlıkları, aktivasyonları, girdisi ve çıktısının tamamı 8 bit tam sayı olan bir model üretir. Bu, ESP32'de üç nedenle önemlidir. Model, kayan nokta sürümünün kabaca dörtte biri boyutundadır, esp-tflite-micro'daki optimize edilmiş çekirdekler int8 için yazılmıştır ve ara tensörler için gereken arena da küçülür. Bedeli ise girdileri, girdi tensöründe saklanan ölçek (scale) ve sıfır noktasını (zero point) kullanarak tam sayı alanına kendinizin dönüştürmesi ve çıktıları geri dönüştürmesidir. Aşağıdaki firmware tam olarak bunu yapar.
Temsili veri kümesi, dönüştürücünün bu ölçekleri seçmek için kullandığı şeydir. Gerçek girdilerin tüm aralığını kapsamıyorsa, aralık dışındaki değerler kırpılır ve Python'da kusursuz görünse bile cihazdaki doğruluk düşer. Gerçek sensör verisine geçtiğinizde, her sınıftan gerçek normalize edilmiş öznitelik vektörlerini representative_data() fonksiyonuna verin.
Adım 2: ESP-IDF Projesini Oluşturun
Proje yapısı:
tinyml_mqtt/
CMakeLists.txt
sdkconfig.defaults
main/
CMakeLists.txt
main.cc
model_data.cc (generated)
model_data.h (generated)
Üst düzey CMakeLists.txt. Eğitimi kısa tutmak için Espressif'in örnek Wi-Fi yardımcısını dahil eder:
cmake_minimum_required(VERSION 3.16)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(tinyml_mqtt)
main/CMakeLists.txt:
idf_component_register(SRCS "main.cc" "model_data.cc" INCLUDE_DIRS ".")
Wi-Fi ağınızla sdkconfig.defaults:
CONFIG_EXAMPLE_CONNECT_WIFI=y
CONFIG_EXAMPLE_WIFI_SSID="your-ssid"
CONFIG_EXAMPLE_WIFI_PASSWORD="your-password"
Ardından proje klasörünün içinden, ESP Component Registry'den çıkarım çalışma zamanını ekleyin:
idf.py set-target esp32s3 # veya esp32
idf.py add-dependency "espressif/esp-tflite-micro"
Adım 3: Çıkarım ve MQTT İçeren Firmware
main/main.cc Wi-Fi ve MQTT'yi kurar, modeli statik bir tensör arenasına yükler ve örnekleme, öznitelik, çıkarım ve yayın döngüsünü çalıştırır. Yüklemeden önce TOPIC_BASE ve client ID değerlerini benzersiz bir şeyle değiştirin.
#include <algorithm>
#include <cmath>
#include <cstdio>
#include "esp_event.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_random.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mqtt_client.h"
#include "nvs_flash.h"
#include "protocol_examples_common.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "model_data.h"
#define TOPIC_BASE "connectmq/demo/esp32-7c41a9/motor1" // bunu benzersiz yapın
#define CLIENT_ID "esp32-tinyml-7c41a9" // bunu benzersiz yapın
static const char *TAG = "tinyml";
static constexpr int kN = 256;
static constexpr float kFs = 1000.0f;
static constexpr int kArenaSize = 8 * 1024;
alignas(16) static uint8_t tensor_arena[kArenaSize];
static esp_mqtt_client_handle_t mqtt = nullptr;
static volatile bool mqtt_ready = false;
static void mqtt_event(void *arg, esp_event_base_t base, int32_t id, void *data) {
if (id == MQTT_EVENT_CONNECTED) {
mqtt_ready = true;
esp_mqtt_client_publish(mqtt, TOPIC_BASE "/status", "online", 0, 1, 1);
ESP_LOGI(TAG, "MQTT connected");
} else if (id == MQTT_EVENT_DISCONNECTED) {
mqtt_ready = false;
ESP_LOGW(TAG, "MQTT disconnected");
}
}
static void start_mqtt() {
esp_mqtt_client_config_t cfg = {};
cfg.broker.address.uri = "mqtt://broker.connectmq.com:1883";
cfg.credentials.client_id = CLIENT_ID;
cfg.session.last_will.topic = TOPIC_BASE "/status";
cfg.session.last_will.msg = "offline";
cfg.session.last_will.qos = 1;
cfg.session.last_will.retain = 1;
mqtt = esp_mqtt_client_init(&cfg);
esp_mqtt_client_register_event(mqtt, MQTT_EVENT_ANY, mqtt_event, nullptr);
esp_mqtt_client_start(mqtt);
}
// ---- simüle edilmiş sensör: gerçek ivmeölçer örnekleriyle değiştirin ----
static float uniform(float a, float b) { return a + (b - a) * (esp_random() / 4294967296.0f); }
static float randn() {
float u1 = (esp_random() + 1.0f) / 4294967297.0f;
float u2 = esp_random() / 4294967296.0f;
return sqrtf(-2.0f * logf(u1)) * cosf(2.0f * M_PI * u2);
}
static void read_window(int scenario, float *x) {
float amp = uniform(0.8f, 1.2f), phase = uniform(0.0f, 2.0f * M_PI);
for (int i = 0; i < kN; i++) {
x[i] = scenario == 0 ? 0.05f * randn()
: amp * sinf(2.0f * M_PI * 50.0f * i / kFs + phase) + 0.1f * randn();
}
if (scenario == 2) {
int impacts = 4 + esp_random() % 6;
for (int k = 0; k < impacts; k++) {
int idx = esp_random() % kN;
x[idx] += uniform(2.0f, 3.0f) * ((esp_random() & 1) ? 1.0f : -1.0f);
}
}
}
static void compute_features(const float *x, float *f) {
float sumsq = 0, peak = 0;
int crossings = 0;
for (int i = 0; i < kN; i++) {
sumsq += x[i] * x[i];
peak = std::max(peak, fabsf(x[i]));
if (i > 0 && ((x[i] < 0) != (x[i - 1] < 0))) crossings++;
}
float rms = sqrtf(sumsq / kN);
f[0] = rms;
f[1] = peak / (rms + 1e-6f);
f[2] = (float)crossings / (kN - 1);
}
extern "C" void app_main(void) {
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());
start_mqtt();
const tflite::Model *model = tflite::GetModel(g_model);
if (model->version() != TFLITE_SCHEMA_VERSION) {
ESP_LOGE(TAG, "schema mismatch");
return;
}
static tflite::MicroMutableOpResolver<4> resolver;
resolver.AddFullyConnected();
resolver.AddSoftmax();
resolver.AddQuantize();
resolver.AddDequantize();
static tflite::MicroInterpreter interpreter(model, resolver, tensor_arena, kArenaSize);
if (interpreter.AllocateTensors() != kTfLiteOk) {
ESP_LOGE(TAG, "AllocateTensors failed: increase kArenaSize");
return;
}
ESP_LOGI(TAG, "arena used: %u bytes", (unsigned)interpreter.arena_used_bytes());
TfLiteTensor *in = interpreter.input(0);
TfLiteTensor *out = interpreter.output(0);
static float window[kN];
int last_label = -1;
int64_t last_sent_us = 0;
for (int n = 0;; n++) {
int scenario = (n / 10) % 3; // demo: makine durumunu her 10 pencerede bir değiştir
read_window(scenario, window);
float feat[3];
compute_features(window, feat);
for (int i = 0; i < 3; i++) {
float norm = (feat[i] - kFeatMean[i]) / kFeatStd[i];
int q = (int)lroundf(norm / in->params.scale) + in->params.zero_point;
in->data.int8[i] = (int8_t)std::min(127, std::max(-128, q));
}
int64_t t0 = esp_timer_get_time();
if (interpreter.Invoke() != kTfLiteOk) {
ESP_LOGE(TAG, "Invoke failed");
continue;
}
int infer_us = (int)(esp_timer_get_time() - t0);
int best = 0;
float best_p = -1;
for (int i = 0; i < 3; i++) {
float p = (out->data.int8[i] - out->params.zero_point) * out->params.scale;
if (p > best_p) { best_p = p; best = i; }
}
ESP_LOGI(TAG, "%s p=%.2f (%d us)", kLabels[best], best_p, infer_us);
int64_t now = esp_timer_get_time();
if (mqtt_ready && (best != last_label || now - last_sent_us > 30000000)) {
char payload[128];
snprintf(payload, sizeof(payload),
"{\"label\":\"%s\",\"p\":%.2f,\"rms\":%.3f,\"us\":%d,\"model\":\"vib-mlp-v1\"}",
kLabels[best], best_p, feat[0], infer_us);
esp_mqtt_client_publish(mqtt, TOPIC_BASE "/inference", payload, 0, 1, 0);
last_label = best;
last_sent_us = now;
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
Derleyin, yükleyin ve monitörü açın:
python train_model.py
cd tinyml_mqtt
idf.py build flash monitor
Adım 4: Sonuçların Gelişini İzleyin
Herhangi bir bilgisayardan önekinize abone olun:
mosquitto_sub -h broker.connectmq.com -p 1883 -i tinyml-viewer-31 \
-t 'connectmq/demo/esp32-7c41a9/#' -v
Retained online durumunu, ardından simüle edilen makine her durum değiştirdiğinde bir çıkarım mesajını ve her 30 saniyede bir kalp atışını göreceksiniz. Firmware saniyede iki pencere değerlendirir ama dakikada yalnızca birkaç mesaj yayınlar; TinyML'in bütün amacı da budur: yerelde hesapla, seyrek bildir. Ücretsiz herkese açık MQTT broker istemci başına saniyede 10 mesaja kadar izin verir, dolayısıyla bu tasarım bolca pay bırakır.
Topic'leri ve Payload'u Tasarlamak
Eğitim, tek bir cihaz öneki altında iki topic kullanır: retained olan ve bir Last Will ile desteklenen status ile sonuçları taşıyan inference. Bu ayrım, sonuçlar olay olarak akarken bir gösterge panelinin cihaz sağlığını anında göstermesini sağlar. Filo büyüdükçe hiyerarşiye daha fazla bağlam ekleyin, örneğin plant1/utilities/compressors/comp-04/inference; böylece bir abone wildcard'larla tek bir makineyi, tek bir hattı veya tüm tesisi seçebilir.
Payload'u kısa ama kendini açıklayan biçimde tutun. model alanı, kablosuz (OTA) bir güncellemeden sonra bir sonucu hangi firmware'in ürettiğini söyler; p abonelerin kendi güven eşiklerini uygulamasına olanak tanır; us ise bir model değişikliğinden sonra çıkarım süresinin ne zaman arttığını ortaya koyar. Davranışı uzaktan ayarlamanız gerekiyorsa, cihazı kalp atışı aralığı gibi ayarlar için bir cmd topic'ine abone edin ve her değeri uygulamadan önce doğrulayın. Özel bir ConnectMQ broker'da her iki topic'i canlı topic gezgininde izleyebilir ve test ederken sayısal alanlar üzerinde bir eşik uyarı kuralı ayarlayabilirsiniz.
Gerçek Sensör Verisine Geçmek
read_window() fonksiyonunu, bir donanım zamanlayıcısı veya sensörün FIFO'su ile sabit bir hızda örneklenen MPU6050 ya da ADXL345 gibi bir ivmeölçerden gelen örneklerle değiştirin. Ardından kaydedilmiş pencerelerle yeniden eğitin: her makine durumundan gerçek pencereleri MQTT veya seri port üzerinden kaydedin, etiketleyin ve aynı öznitelik ve eğitim koduna verin. Sıfır geçiş tanımı dahil olmak üzere öznitelik hesaplamasını Python ve C++'ta birebir aynı tutun; aksi takdirde cihazdaki doğruluk eğitimdekiyle eşleşmez.
Üretime geçtiğinizde TLS ve kimlik bilgilerine geçin: ConnectMQ broker host'unuzla mqtts://broker.connectmq.com:8883 URI'sini kullanın, cfg.credentials.username, cfg.credentials.authentication.password ve verilen client ID'yi ayarlayın ve CA sertifikasını sağlayın (ESP-IDF sertifika paketi Let's Encrypt'i destekler). ESP32 MQTT eğitimi TLS kurulumunu ayrıntılı olarak ele alıyor.
ESP32'de TinyML Sorun Giderme
| Sorun | Neden | Çözüm |
|---|---|---|
AllocateTensors failed |
Arena çok küçük | kArenaSize değerini artırın, ardından arena_used_bytes() artı bir paya kadar düşürün |
Didn't find op for builtin opcode |
Operatör kaydedilmemiş | Resolver'a ekleyin ve şablon sayısını artırın |
| Her zaman tek bir sınıfı tahmin ediyor | Öznitelikler eğitimdeki gibi normalize edilmemiş | kFeatMean/kFeatStd değerlerini ve girdi nicemleme formülünü kontrol edin |
| TFLM başlık dosyalarında derleme hataları | Karışık sürümler veya Arduino portu | ESP-IDF 5.x ile bileşen kayıt deposundan esp-tflite-micro kullanın |
| MQTT hiç bağlanmıyor | Wi-Fi bilgileri veya yinelenen client ID | Monitör günlüğünü kontrol edin ve client ID'yi benzersiz yapın |
Sonraki Adımlar
Bu elle kurulmuş işlem hattını, veri toplamayı ve öznitelik tasarımını otomatikleştiren Edge Impulse MQTT iş akışıyla karşılaştırın veya TensorFlow Lite Micro'ya başlangıç yazısında çalışma zamanının kendisini derinlemesine inceleyin. Yapay zeka ve IoT rehberi, bunun gibi cihazların daha büyük bir AIoT sistemine nasıl yerleştiğini gösteriyor.
ConnectMQ ile Başlayın
TinyML düğümünüz, sonuçlarını alan sistemler kadar faydalıdır. ConnectMQ, TLS, oluşturulan kimlik bilgileri ve çıkarım mesajlarını gerçek zamanlı izlemek için canlı bir topic gezgini sunan yönetilen bir MQTT broker hizmetidir. app.connectmq.com adresinde ücretsiz Serverless planında kredi kartı gerektirmeden bir broker oluşturun ve firmware'i ona yönlendirin.
Sık Sorulan Sorular
TinyML için en iyi ESP32 hangisi?
Olağan seçim ESP32-S3'tür; çünkü vektör komutları esp-tflite-micro'daki sinir ağı çekirdeklerini hızlandırır ve birçok kartta PSRAM bulunur. Orijinal ESP32 de bu eğitimdeki gibi küçük modeller için gayet iyi çalışır.
ESP32'de Arduino IDE ile TinyML yapabilir miyim?
Evet; TensorFlow Lite Micro'yu veya Edge Impulse dışa aktarımlarını içeren Arduino kütüphaneleri aracılığıyla. Kütüphane sürümünün güncel TFLM API'siyle eşleştiğini kontrol edin, çünkü eski portlar farklı yapıcılar ve başlık dosyaları kullanır.
ESP32'deki bir TinyML modeli ne kadar büyük olabilir?
Ağırlıklar için flash'a, tensör arenası için RAM'e bağlıdır. Küçük yoğun ve evrişimli modeller dahili belleğe kolayca sığar; daha büyük görü modelleri genellikle PSRAM'li bir ESP32-S3 gerektirir.
Neden yalnızca değişiklikte yayınlanmalı?
Çıkarım saniyede birçok kez çalışır, ancak sonraki sistemler yalnızca durum değiştiğinde ilgilenir. Değişiklikte yayın artı kalp atışı radyo gücünden ve bant genişliğinden tasarruf sağlar ve sizi broker hız sınırları içinde tutar.



