Logging mit Dobot Lua

Robuste Protokollierung mit Lua

Für Qualitätssicherung, Fehleranalyse und nachvollziehbare Prozessdokumentation ist ein gutes Logging unverzichtbar. In diesem Beitrag zeigen wir, wie Sie mit Dobot Lua ein automatisches Logging-System bauen.

Lokale Protokollierung

ⓘ Hinweis:
Dieser Ansatz bietet ein schnelles, robustes Logging ohne zusätzliche Abhängigkeiten. Dies ist ideal für Tests und lokale Protokollierung direkt auf dem Roboter.


Für produktive Umgebungen empfehlen wir jedoch, das Logging an eine externe Stelle wie einen TCP-Server oder zentralen Log-Service auszulagern. Das vorgehen hierzu finden sie weiter unten, unter: Server-Protokollierung
Bei Fragen zur Integration oder Erweiterung unterstützen wir Sie gern per E-Mail unter backoffice@needful-apps.de oder telefonisch unter 0561 2207 6342.

Projektstruktur in DobotStudio Pro

DateiRolleBeschreibung
Global.luaKonfigurationGlobale Werte und (Logging-)Funktionen
src0.luaHauptprogrammRoboteraktionen und Bewegungen

Global.lua

Neben den programmspezifischen Konfigurationen benötigen wir in der Global.lua folgenden Code:

ROBOT_ID = "MG400_001"
LOG_PATH = "/mnt/sdcard/logs/"
MAX_LOG_FILES = 10
LOG_FILE = LOG_PATH .. "robot_log.txt"
LOCK_FILE = LOG_FILE .. ".lock"
MAX_WAIT_MS = 1000

-- Prüft, ob Datei existiert
function file_exists(path)
    local f = io.open(path, "r")
    if f then f:close() return true end
    return false
end

-- Initialisiert Logging: Verzeichnis, .lock entfernen, Rotation
function init_logging()
    os.execute("mkdir -p " .. LOG_PATH)

    -- Alte Lock-Dateien entfernen
    for file in io.popen('ls "' .. LOG_PATH .. '"'):lines() do
        if file:match("%.lock$") then
            os.remove(LOG_PATH .. file)
        end
    end

    -- Logrotation (maximale Anzahl Dateien)
    local files = {}
    for file in io.popen('ls -t "' .. LOG_PATH .. '"'):lines() do
        if not file:match("%.lock$") then
            table.insert(files, file)
        end
    end
    for i = MAX_LOG_FILES + 1, #files do
        os.remove(LOG_PATH .. files[i])
    end
end

-- Loggt eine Nachricht mit Lock-Datei zum Schutz vor parallelem Zugriff
function log(message)
    local waited = 0
    while file_exists(LOCK_FILE) and waited < MAX_WAIT_MS do
        Sleep(10)
        waited = waited + 10
    end

    if file_exists(LOCK_FILE) then
        print("WARNUNG: Logging blockiert durch Lock-Datei – Eintrag übersprungen.")
        return
    end

    -- Lock setzen
    local lock = io.open(LOCK_FILE, "w")
    if lock then lock:write("locked\n") lock:close() end

    -- Schreiben
    local timestamp = os.date("%Y-%m-%d %H:%M:%S")
    local f = io.open(LOG_FILE, "a")
    if f then
        f:write("[" .. timestamp .. "] " .. ROBOT_ID .. " - " .. message .. "\n")
        f:close()
    end

    -- Lock wieder entfernen
    os.remove(LOCK_FILE)
end

ParameterTypBeispielwertBedeutung
ROBOT_IDstring"MG400_001"Eindeutige Kennung des Roboters: Nützlich, wenn mehrere Roboter im Einsatz sind. Wird in jedem Logeintrag mitgeschrieben.
LOG_PATHstring"/mnt/sdcard/logs/"Speicherort für die Logdateien. Muss vorhanden sein oder per Skript erstellt werden. Beachten Sie hierbei, dass der Speicherort, ausgehend vom Roboter (Linux) ist.
MAX_LOG_FILESnumber10Maximale Anzahl an Logdateien, die im Ordner behalten werden. Ältere Dateien werden automatisch gelöscht (Logrotation).
LOG_FILEstring"robot_log.txt"Baut den Pfad zur Log-Datei
LOCK_FILEstring".lock"Baut den Pfad zur Log-Lock-Datei
MAX_WAIT_MSnumber1000Gibt an, wie lange maximal versucht wird, zu warten, bis ein .lock entfernt wird.

src0.lua

-- Muss einmal mit Start des Programms aufgerufen werden
init_logging()

-- Logging kann nun an beliebiger stelle verwendet werden
log("Starte Robotersequenz")

Server-Protokollierung

Für dieses Beispiel benutzen wir den rust-basierten TCP-Logserver von needful-apps.

Überblick

Statt (oder zusätzlich zu) einer lokalen Datei schickt der Roboter seine Meldungen an einen zentralen Log-Server.
Der Log-Server nimmt Logs per TCP an, speichert sie in einer Datenbank und stellt sie zentral bereit:

  • Live-Ansicht im Browser (Web-Dashboard)
  • Filter nach Roboter, Station, Fehlertyp
  • Historie und QS-Unterlagen für alle Zellen
  • Grafana-Anbindung (Loki-kompatible API)
  • Mehrere Datenbanken möglich (SQLite, PostgreSQL, MySQL, Supabase)

Diese Variante eignet sich für:

  • Fertigung mit mehreren Stationen / Robotern
  • Traceability („Welches Teil wurde wann verarbeitet?“)
  • Alarmierung / Monitoring
  • Qualitätsnachweis gegenüber Kunden
needful-apps-dobot-log-server

Konfiguration für Remote-Logging (Global.lua)

Wir ergänzen in Global.lua zusätzliche Konstanten für den Log-Server:

-- Remote Logging Konfiguration
REMOTE_LOG_ENABLED       = true                -- zentralen Log-Server benutzen?
REMOTE_LOG_HOST          = "192.168.10.50"     -- IP/Hostname des Log-Servers
REMOTE_LOG_PORT          = 9090                -- TCP-Port des Log-Servers
REMOTE_CLIENT_ID         = "MG400_001"         -- eindeutige Kennung dieses Roboters
REMOTE_API_KEY           = "secure-random-key" -- API-Key aus dem Admin-Panel des Log-Servers
REMOTE_SOCKET_TIMEOUT_MS = 200                 -- max. Verbindungswartezeit in Millisekunden

Wichtig:

  • Diese Werte sind pro Roboter unterschiedlich.
  • REMOTE_LOG_ENABLED kann auf false gesetzt werden, wenn der Roboter offline getestet wird.

TCP-Verbindung zum Log-Server herstellen

Konzeptionelle Hilfsfunktion (vereinfacht im Lua-Stil mit Socket-API):

function connect_log_server()
    if not REMOTE_LOG_ENABLED then
        return nil, "remote disabled"
    end

    local socket = require("socket")  -- abhängig von Firmware / Umgebung
    local tcp = assert(socket.tcp())

    -- Kurzer Timeout, Roboter darf nicht hängen
    tcp:settimeout(REMOTE_SOCKET_TIMEOUT_MS / 1000.0)

    local ok, err = tcp:connect(REMOTE_LOG_HOST, REMOTE_LOG_PORT)
    if not ok then
        print("WARNUNG: Konnte Log-Server nicht erreichen: " .. tostring(err))
        return nil, err
    end

    -- 1. Auth schicken
    local auth_payload = string.format(
        '{"client_id":"%s","api_key":"%s"}\n',
        REMOTE_CLIENT_ID,
        REMOTE_API_KEY
    )
    tcp:send(auth_payload)

    -- 2. Antwort lesen (optional, nicht blockierend kritisch)
    local resp, recv_err = tcp:receive("*l")
    -- Wenn resp nil ist, ignorieren wir das still. Roboterlauf geht vor Komfort.

    return tcp, nil
end

Hinweis:
Falls die eingesetzte Roboter-Firmware keine direkten TCP-Sockets zulässt, übernimmt diese Rolle ein kleines Edge-Gerät, das seriell/Modbus vom Roboter Nachrichten bekommt und dann an den Log-Server weiterleitet. Die Log-Server-Seite bleibt identisch.

Remote-Log senden

Diese Funktion schickt einen einzelnen Eintrag an den Server.
Sie ist bewusst fehlertolerant: Wenn der Server nicht erreichbar ist, läuft der Roboter einfach weiter.

-- level: "INFO", "WARN", "ERROR", ...
-- message: Freitext
-- meta: optionale Tabelle { key = value, ... } mit Zusatzinformationen
function log_remote(level, message, meta)
    if not REMOTE_LOG_ENABLED then
        return
    end

    local ok, err = pcall(function()
        local tcp, cerr = connect_log_server()
        if not tcp then
            -- Server nicht erreichbar -> wir geben auf, aber stoppen nicht die Produktion
            return
        end

        -- Metadaten als einfaches JSON bauen (ohne Sonderzeichen-Escaping)
        local meta_json = "{}"
        if meta ~= nil then
            local parts = {}
            for k,v in pairs(meta) do
                table.insert(parts,
                    string.format('"%s":"%s"', tostring(k), tostring(v))
                )
            end
            meta_json = "{" .. table.concat(parts, ",") .. "}"
        end

        local timestamp = os.date("%Y-%m-%dT%H:%M:%S")

        local payload = string.format(
            '{"level":"%s","message":"%s","metadata":%s,"robot_id":"%s","ts":"%s"}\n',
            level,
            tostring(message),
            meta_json,
            ROBOT_ID,
            timestamp
        )

        tcp:send(payload)

        -- optional: kurzes Ack lesen, aber nicht hart blockieren
        tcp:settimeout(0.05)
        tcp:receive("*l")

        tcp:close()
    end)

    if not ok then
        print("WARNUNG: Remote-Logging fehlgeschlagen: " .. tostring(err))
    end
end

Wichtig:

  • Die Funktion blockiert den Roboter nicht dauerhaft.
    Im schlimmsten Fall wird der Eintrag einfach nicht gesendet.
  • Jeder Eintrag enthält robot_id, level, message, metadata, ts.
    Genau diese Felder tauchen später im Dashboard und in Grafana auf.

Verwendung in src0.lua (Remote-Logging)

-- Beispielhafter Produktionsablauf

-- Lokale Initialisierung nur nötig, wenn auch lokal geloggt wird
init_logging()

-- Schrittstart dokumentieren
log_remote("INFO", "Starte Robotersequenz", {job="init", step="boot"})

-- Bauteil greifen
log_remote("INFO", "Greife Bauteil", {part_id="GPU-4711", station="pick"})

-- Warnung melden (z. B. Vakuumdruck zu niedrig)
log_remote("WARN", "Vakuumdruck niedrig", {vacuum_kpa="-38.2"})

-- Fehlerfall
log_remote("ERROR", "Bauteil nicht korrekt ausgerichtet", {part_id="GPU-4711"})

Diese Logs erscheinen dann zentral:

  • im Web-Dashboard des Log-Servers (Port 8080),
  • und in Grafana über die Loki-Datasource (Filter z. B. {client_id="MG400_001"} oder {level="ERROR"}).

Was der Log-Server bietet

Kurz zusammengefasst (aus Sicht der Fertigung / QS):

  • Zentrale Sicht für alle Roboter
    Alle Einträge landen an einer Stelle.
  • Live-Ansicht
    Neue Logs erscheinen in Echtzeit im Browser (WebSocket).
  • Filterbarkeit
    Nach Roboter, Level (WARN/ERROR), Station, Werkstück-ID.
  • Grafana-Integration (Loki-kompatibel)
    • Zeiträume vergleichen
    • Fehlerhäufigkeit als Graph
    • Alarme definieren
  • Datenbank-Backends
    • SQLite für Test / PoC
    • PostgreSQL/MySQL für Produktion
    • Supabase für managed Cloud-Varianten
  • Sicherheit durch API-Key
    Jeder Roboter bekommt eine eigene client_id + api_key. Kann zentral gesperrt werden.

Mehr davon