Embedded Linux ile HMI Geliştirme: Yocto ve Qt ile Özel Arayüz Tasarımı

Amazeng Teknik Ekip
10 dakika okuma
Embedded LinuxHMIYocto ProjectQtQMLEndüstriyel UITouchscreen

Giriş

Endüstriyel HMI sistemlerinde Windows CE ve proprietary çözümlerin hakimiyeti son yıllarda Embedded Linux lehine hızla değişiyor. HMI Endüstriyel Ekran Çözümlerimiz, tam da bu paradigma değişiminin merkezinde: Yocto Project ile özelleştirilmiş Linux dağıtımı ve Qt/QML ile modern arayüz geliştirme.

Bu yazımızda, sıfırdan bir embedded Linux HMI'nın nasıl oluşturulacağını, blog yazımızda anlattığımız Yocto altyapısı üzerine Qt entegrasyonunu ve gerçek dünya uygulamalarını inceliyoruz.

Neden Embedded Linux + Qt?

Geleneksel HMI Çözümlerinin Sınırlamaları

1. Windows CE / Windows Embedded

  • Lisans maliyetleri (cihaz başına $100+)
  • Güvenlik güncellemeleri sonlandı (Windows CE 2018'de EOL)
  • Donanım gereksinimleri yüksek (en az 512MB RAM)

2. Proprietary Çözümler (Siemens WinCC, Rockwell FactoryTalk)

  • Vendor lock-in (üretici bağımlılığı)
  • Sınırlı özelleştirme imkanı
  • Yüksek yazılım maliyeti

3. Arduino/MCU Tabanlı Basit Ekranlar

  • Düşük grafik performansı
  • Sınırlı networking (TCP/IP stack'i zayıf)
  • Karmaşık uygulamalar için yetersiz

Embedded Linux + Qt'nin Avantajları

Açık Kaynak & Ücretsiz: Hiçbir lisans maliyeti yok
Tam Kontrol: İşletim sisteminin her katmanını özelleştirme
Güçlü Grafik: Qt Quick (QML) ile hardware-accelerated UI
Networking: Native TCP/IP, MQTT, Modbus desteği
OTA Updates: A/B partition ile güvenli yazılım güncellemesi
Uzun Ömür: Linux kernel ve Qt, 10+ yıl destek garantisi

Yocto Project: Özelleştirilmiş Linux İmajı

Yocto Nedir?

Yocto Project, embedded cihazlar için özelleştirilmiş Linux dağıtımları oluşturmaya yarayan bir build sistemidir. Standart bir Ubuntu/Debian'dan farkı:

  • Minimal footprint: Sadece ihtiyaç duyulan paketler dahil (~50MB rootfs)
  • Cross-compilation: Geliştirme PC'sinde (x86) ARM işlemciler için derleme
  • Reproducible builds: Her build aynı sonucu verir

HMI İçin Yocto Layer Yapısı

meta-hmi-amazeng/
├── recipes-core/
│   └── images/
│       └── hmi-image.bb          # Ana imaj tarifi
├── recipes-graphics/
│   └── qt5/
│       └── qtbase_%.bbappend     # Qt özelleştirmeleri
├── recipes-connectivity/
│   └── modbus/
│       └── libmodbus_git.bb      # Modbus kütüphanesi
├── recipes-kernel/
│   └── linux/
│       └── linux-%.bbappend      # Touchscreen driver'ları
└── recipes-app/
    └── hmi-app/
        └── hmi-app_git.bb        # HMI uygulaması

Örnek HMI Image Tarifi

# hmi-image.bb
DESCRIPTION = "Amazeng HMI Linux Image"
LICENSE = "MIT"

IMAGE_FEATURES += " \
    splash \
    ssh-server-dropbear \
    hwcodecs \
"

IMAGE_INSTALL = " \
    packagegroup-core-boot \
    qtbase \
    qtdeclarative \
    qtquickcontrols2 \
    qtgraphicaleffects \
    qtsvg \
    qtwebsockets \
    libmodbus \
    hmi-app \
    ca-certificates \
    tzdata \
"

inherit core-image

Qt/QML ile Modern HMI Arayüzü

Qt Quick (QML) vs Qt Widgets

ÖzellikQt Widgets (C++)Qt Quick (QML)
PerformansCPU-basedGPU-accelerated
Geliştirme HızıYavaş (compile)Hızlı (interpreted)
AnimasyonKarmaşıkÇok kolay
TouchscreenMouse event'leriNative touch
Tasarımcı DesteğiQt DesignerQt Design Studio

HMI uygulamaları için Qt Quick (QML) kesinlikle tercih edilmelidir.

Basit HMI Uygulaması: Loadcell Okuyucu

Bir GDT Dijital Transmitter'dan Modbus TCP ile ağırlık okuyup gösteren HMI:

main.qml

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    visible: true
    width: 1024
    height: 600
    title: "GDT Loadcell Monitor"

    // Arka plan gradient
    Rectangle {
        anchors.fill: parent
        gradient: Gradient {
            GradientStop { position: 0.0; color: "#2C3E50" }
            GradientStop { position: 1.0; color: "#34495E" }
        }
    }

    ColumnLayout {
        anchors.centerIn: parent
        spacing: 30

        // Başlık
        Text {
            text: "Tank 1 - Net Ağırlık"
            font.pixelSize: 36
            font.bold: true
            color: "#ECF0F1"
            Layout.alignment: Qt.AlignHCenter
        }

        // Ağırlık göstergesi
        Rectangle {
            width: 600
            height: 200
            color: "#1ABC9C"
            radius: 20

            Text {
                anchors.centerIn: parent
                text: modbusBackend.weight.toFixed(2) + " kg"
                font.pixelSize: 72
                font.bold: true
                color: "white"
            }
        }

        // Kontrol butonları
        RowLayout {
            Layout.alignment: Qt.AlignHCenter
            spacing: 20

            Button {
                text: "TARE"
                font.pixelSize: 24
                implicitWidth: 150
                implicitHeight: 80
                onClicked: modbusBackend.tare()
            }

            Button {
                text: "SIFIRLA"
                font.pixelSize: 24
                implicitWidth: 150
                implicitHeight: 80
                onClicked: modbusBackend.zero()
            }
        }
    }
}

modbus_backend.h (C++)

#ifndef MODBUSBACKEND_H
#define MODBUSBACKEND_H

#include <QObject>
#include <QTimer>
#include <modbus/modbus.h>

class ModbusBackend : public QObject
{
    Q_OBJECT
    Q_PROPERTY(double weight READ weight NOTIFY weightChanged)

public:
    explicit ModbusBackend(QObject *parent = nullptr);
    ~ModbusBackend();

    double weight() const { return m_weight; }

    Q_INVOKABLE void tare();
    Q_INVOKABLE void zero();

signals:
    void weightChanged();

private slots:
    void readModbusData();

private:
    modbus_t *m_modbusCtx;
    QTimer *m_timer;
    double m_weight = 0.0;
};

#endif

modbus_backend.cpp

#include "modbus_backend.h"
#include <QDebug>

ModbusBackend::ModbusBackend(QObject *parent)
    : QObject(parent)
{
    // GDT Modbus TCP bağlantısı (IP: 192.168.1.100, Port: 502)
    m_modbusCtx = modbus_new_tcp("192.168.1.100", 502);

    if (modbus_connect(m_modbusCtx) == -1) {
        qCritical() << "Modbus bağlantısı başarısız:" << modbus_strerror(errno);
        modbus_free(m_modbusCtx);
        return;
    }

    // Her 100ms'de bir veri oku
    m_timer = new QTimer(this);
    connect(m_timer, &QTimer::timeout, this, &ModbusBackend::readModbusData);
    m_timer->start(100);
}

void ModbusBackend::readModbusData()
{
    uint16_t registers[2];

    // GDT holding register 0: Net ağırlık (IEEE 754 float, 2 register)
    if (modbus_read_registers(m_modbusCtx, 0, 2, registers) == 2) {
        // Float'a dönüştür
        union {
            uint32_t u32;
            float f32;
        } converter;

        converter.u32 = (registers[0] << 16) | registers[1];
        m_weight = converter.f32;

        emit weightChanged();
    }
}

void ModbusBackend::tare()
{
    // GDT coil 0: Tare komutu
    modbus_write_bit(m_modbusCtx, 0, 1);
}

void ModbusBackend::zero()
{
    // GDT coil 1: Zero komutu
    modbus_write_bit(m_modbusCtx, 1, 1);
}

Gerçek Dünya Uygulaması: Çoklu Tank İzleme

Senaryo

3 adet süt tankı, her birinde GDT Dijital Transmitter + 4 loadcell. Merkezi bir 10.1" HMI ile tüm tankları izleme.

Mimari

┌──────────────┐    Modbus TCP     ┌──────────────┐
│  Tank 1 GDT  │◄─────────────────►│              │
│ 192.168.1.101│                   │              │
└──────────────┘                   │              │
                                   │   HMI Panel  │
┌──────────────┐    Modbus TCP     │  (10.1 inch) │
│  Tank 2 GDT  │◄─────────────────►│   Qt/QML     │
│ 192.168.1.102│                   │              │
└──────────────┘                   │              │
                                   │              │
┌──────────────┐    Modbus TCP     │              │
│  Tank 3 GDT  │◄─────────────────►│              │
│ 192.168.1.103│                   └──────────────┘
└──────────────┘

QML Layout (3 Tank Grid)

GridLayout {
    columns: 3
    columnSpacing: 20
    rowSpacing: 20

    Repeater {
        model: [
            { name: "Tank 1", ip: "192.168.1.101" },
            { name: "Tank 2", ip: "192.168.1.102" },
            { name: "Tank 3", ip: "192.168.1.103" }
        ]

        TankWidget {
            tankName: modelData.name
            ipAddress: modelData.ip
            Layout.preferredWidth: 300
            Layout.preferredHeight: 400
        }
    }
}

Yocto + Qt Geliştirme İş Akışı

1. Geliştirme Ortamı Kurulumu

# Yocto SDK kurulumu
./poky-sdk-installer.sh

# SDK'yı aktive et
source /opt/poky/environment-setup-cortexa7-poky-linux-gnueabi

# Qt Creator'da cross-compiler ayarla
qmake -query

2. Hızlı Geliştirme Döngüsü

Lokal Test (PC'de):

qmake hmi-app.pro
make
./hmi-app

Hedef Cihazda Test:

# SCP ile HMI paneline kopyala
scp hmi-app [email protected]:/usr/bin/

# SSH ile çalıştır
ssh [email protected]
systemctl stop hmi-app
/usr/bin/hmi-app

3. Yocto Image Build

cd yocto-build
bitbake hmi-image

# SD karta yaz
sudo dd if=tmp/deploy/images/hmi-board/hmi-image.wic of=/dev/sdb bs=4M

OTA (Over-The-Air) Güncelleme Stratejisi

A/B Partition Şeması

/dev/mmcblk0p1: boot
/dev/mmcblk0p2: rootfs-a  (aktif)
/dev/mmcblk0p3: rootfs-b  (yedek)
/dev/mmcblk0p4: data      (kullanıcı verileri)

Güncelleme Süreci

  1. Yeni imaj rootfs-b'ye download edilir
  2. Bootloader flag değiştirilir
  3. Reboot sonrası rootfs-b aktif olur
  4. Uygulama başarılı başlarsa rootfs-b kalıcı olur
  5. Hata varsa, reboot ile rootfs-a'ya geri döner (rollback)

Performans Optimizasyonu

1. Boot Hızı (Target: < 5 saniye)

# hmi-image.bb içinde
IMAGE_FEATURES_remove = "debug-tweaks package-management"
EXTRA_IMAGE_FEATURES = "read-only-rootfs"

2. Qt Quick Animasyon Optimizasyonu

// CPU yerine GPU kullan
layer.enabled: true
layer.effect: ShaderEffect { }

// Gereksiz render'ları önle
visible: opacity > 0
opacity: enabled ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 200 } }

3. Modbus Polling Optimizasyonu

// Her değişken için farklı update rate
QTimer *fastTimer = new QTimer(this);  // 100ms - ağırlık
QTimer *slowTimer = new QTimer(this);  // 1000ms - sıcaklık

Bulut Entegrasyonu: HMI + AWS IoT

HMI paneli aynı zamanda bir IoT gateway olarak da çalışabilir. Cloud & IoT Veri Toplama çözümümüzde, HMI:

  1. Modbus RTU/TCP ile ZMA ve GDT cihazlarından veri toplar
  2. MQTT ile AWS IoT Core'a gönderir
  3. Hem lokal arayüzde gösterir, hem de Grafana dashboard'da

Qt MQTT Client Örneği:

#include <QtMqtt/QMqttClient>

QMqttClient *mqttClient = new QMqttClient(this);
mqttClient->setHostname("xxxxx.iot.eu-west-1.amazonaws.com");
mqttClient->setPort(8883);
mqttClient->connectToHost();

// GDT'den okunan veriyi publish et
QByteArray payload = QString("{\"tank_id\":1,\"weight\":%1}").arg(weight).toUtf8();
mqttClient->publish("amazeng/tank/data", payload);

Sonuç ve Öneriler

Embedded Linux + Qt kombinasyonu, modern endüstriyel HMI'lar için en esnek ve güçlü çözümdür. HMI Endüstriyel Ekran Çözümlerimiz, bu teknoloji stack'i üzerine inşa edilmiştir ve:

✅ Proprietary çözümlerden %70 daha ekonomiktir
✅ Tam özelleştirme özgürlüğü sunar
✅ 10+ yıl uzun ömürlü destek garantisi
✅ Cloud ve IoT entegrasyonuna hazır

İlgili Kaynaklar