Ist der Arduino Mega zu langsam oder mein code zu ineffizient?

Hallo in die Runde,

ich habe ein Frage,

ich benutze einen Arduino Mega um 5 Schrittmotoren unabhängig voneinander zu steuern. Es handelt sich um eine Vorschubeinheit, deren Rollen unterschiedlich schnell laufen müssen.

Bei den Schrittmotoren handelt es sich um 34HS59-6004S.

Diese sind an einen Schrittmotorentreiber angeschlossen (StepperOnline Digital Stepper Driver DM860I).

Das Netzteil ist ein S-350-60 von Stepper Online. Da ich für die Anwendung ein GUI brauche, habe ich mit TKinter ein GUI für Python geschrieben.

Eigentlich funktioniert auch soweit alles wie es das soll. allerdings gibt es ein Problem. Die Controller sind auf 3200 microsteps eingestellt (tiefer will ich nicht, da ich langsame Geschwindigkeiten fahren muss und bei allen microstepeinstellungen unterhalb die Motoren zu stark vibrieren. Da es sich bei der Anwendung um eine Vorschubeinheit handelt, gebe ich jetzt mal die Geschwindigkeit in m/min an. Wenn ich einen Motor drehen lasse, kann ich ohne Probleme 10 m/min fahren. Schalte ich einen zweiten dazu, der sich ebenfalls mit 10 m/min dreht, fängt sich dieser an zu drehen, der zuerst angeschaltete reduziert dann aber seine Geschwindigkeit, worauf hin beide auf einer Geschwindigkeit laufen, die nicht 10 m/min entspricht.

Ist es Möglich, dass der Arduino für diese Funktion zu langsam ist? Oder liegt es an einer ineffizienten Programmierung meinerseits?

Was für mich dafür spricht, dass der Arduino zu langsam ist, ist dass wenn ich die Anzahl der Microsteps am Treiber runter stelle, die Problematik erst bei höheren Geschwindigkeiten auftaucht. Wenn ich die Anzahl der Microsteps erhöhe, tritt die Problematik bereits bei niedrigeren Geschwindigkeiten auf.

P.s. dass es mit der Stromversorgung zutun hat ist ausgeschlossen, da ich zeitweise nur zwei Motoren auch wirklich angeschlossen hatte. Lasse ich einen Motor auf 10 m/min laufen und schalte einen nicht angeschlossenen mit 10 m/min zu, reduziert sich die Umdrehungszahl ebenfalls obwohl kein weiterer Verbraucher hinzu gekommen ist (und so wie ich das verstehen würde es auch keinen Sinn machen, dass ein Spannungsabfall oder ähnliches dazu führen kann, dass sich ein Schrittmotor langsamer dreht (oder könnte er prinzipiell dann Schritte nicht schaffen und sich langsamer drehen? egal). Zudem werden alle Motoren über ihr eigenes Netzteil betrieben und sind an unterschiedliche Stromquellen angeschlossen.

Hier mein Code:

Arduino:

#include <AccelStepper.h>
#include <MultiStepper.h>

#define NUM_VE 5

float v_ve[NUM_VE] = {0, 0, 0, 0, 0}; // Initial speeds set to 0
float d_ve[NUM_VE]; // Diameter of rollers in meters
int uev_ve[NUM_VE]; // Gear ratios
int stpu_ve[NUM_VE]; // Steps per revolution

const int ena_pins[NUM_VE] = {41, 44, 47, 50, 53};
const int dir_pins[NUM_VE] = {40, 43, 46, 49, 52};
const int pul_pins[NUM_VE] = {39, 42, 45, 48, 51};

AccelStepper steppers[NUM_VE] = {
  AccelStepper(AccelStepper::DRIVER, pul_pins[0], dir_pins[0]),
  AccelStepper(AccelStepper::DRIVER, pul_pins[1], dir_pins[1]),
  AccelStepper(AccelStepper::DRIVER, pul_pins[2], dir_pins[2]),
  AccelStepper(AccelStepper::DRIVER, pul_pins[3], dir_pins[3]),
  AccelStepper(AccelStepper::DRIVER, pul_pins[4], dir_pins[4])
};

MultiStepper multiStepper;

bool running = false;
int direction = 1; // 1 for forward, -1 for reverse

void setup() {
  Serial.begin(115200);  // Updated baud rate to 115200
  for (int i = 0; i < NUM_VE; i++) {
    pinMode(ena_pins[i], OUTPUT);
    digitalWrite(ena_pins[i], LOW); // Enable on startup
    steppers[i].setMaxSpeed(1000); // Set a reasonable max speed
    steppers[i].setAcceleration(800); // Set a reasonable acceleration
    multiStepper.addStepper(steppers[i]);
  }
  setDefaultSettings();
}

void loop() {
  if (Serial.available() > 0) {
    String input = Serial.readStringUntil('\n');
    if (input.length() > 0) {
      if (input.startsWith("M")) {
        int motor_index = input.charAt(1) - '1';
        float speed = input.substring(3).toFloat();
        if (motor_index >= 0 && motor_index < NUM_VE) {
          v_ve[motor_index] = speed;
          updateSpeed(motor_index);
        }
      } else if (input.startsWith("ENABLE")) {
        int motor_index = input.charAt(6) - '1';
        bool enable = input.substring(8) == "true";
        digitalWrite(ena_pins[motor_index], enable ? LOW : HIGH);
      } else if (input == "START") {
        running = true;
        for (int i = 0; i < NUM_VE; i++) {
          updateSpeed(i); // Reinitialize movement when starting
        }
      } else if (input == "STOP") {
        running = false;
        for (int i = 0; i < NUM_VE; i++) {
          steppers[i].stop();
          steppers[i].setCurrentPosition(0); // Reset the current position
        }
      } else if (input.startsWith("DIR:")) {
        direction = (input.substring(4) == "REVERSE") ? -1 : 1;
        for (int i = 0; i < NUM_VE; i++) {
          steppers[i].moveTo(direction * 100000000L);
        }
      } else if (input.startsWith("SETTINGS")) {
        int new_stpu_ve = input.substring(9, input.indexOf(' ', 9)).toInt();
        float new_d_ve = input.substring(input.indexOf(' ', 9) + 1, input.lastIndexOf(' ')).toFloat();
        int new_uev_ve = input.substring(input.lastIndexOf(' ') + 1).toInt();
        
        for (int i = 0; i < NUM_VE; i++) {
          stpu_ve[i] = new_stpu_ve;
          d_ve[i] = new_d_ve;
          uev_ve[i] = new_uev_ve;
        }
      }
    }
  }

  if (running) {
    for (int i = 0; i < NUM_VE; i++) {
      if (steppers[i].distanceToGo() == 0) {
        steppers[i].moveTo(direction * 100000000L); // Keep moving continuously
      }
      steppers[i].run();
    }
  }
}

void updateSpeed(int motor_index) {
  float U_ve = PI * d_ve[motor_index];               // Circumference of the roller
  float um_ve = 1.0 / U_ve;                          // Revolutions per meter
  float sm_ve = um_ve * stpu_ve[motor_index] * uev_ve[motor_index]; // Steps per meter
  float spm_ve = v_ve[motor_index] * sm_ve;          // Steps per minute
  int spsr_ve = int(spm_ve / 60);                    // Steps per second, rounded
  steppers[motor_index].setMaxSpeed(spsr_ve);
  steppers[motor_index].setSpeed(spsr_ve * direction);
  if (running) {
    steppers[motor_index].moveTo(direction * 100000000L); // Move to a large distance for continuous motion
  }
}

void setDefaultSettings() {
  for (int i = 0; i < NUM_VE; i++) {
    d_ve[i] = 0.05;
    uev_ve[i] = 1;
    stpu_ve[i] = 3200;
  }
}

Python:

import customtkinter as ctk
import serial.tools.list_ports
import serial
import threading
import sys
import tkinter as tk


class ConsoleRedirector:
    def __init__(self, text_widget):
        self.text_widget = text_widget

    def write(self, message):
        self.text_widget.insert(tk.END, message)
        self.text_widget.see(tk.END)  # Scroll to the end

    def flush(self):
        pass


class ThreeColumnGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Three Column GUI")
        self.root.geometry("800x400")  # Adjusted window size for extra space

        # Initialize motor direction and settings
        self.motor_direction = "FORWARD"
        self.stpu_ve = 3200  # Default value
        self.d_ve = 0.05  # Default value in meters
        self.uev_ve = 1  # Default value

        # Create a tab view
        self.tabview = ctk.CTkTabview(self.root)
        self.tabview.pack(expand=True, fill="both")

        # Add tabs
        self.tabview.add("Control")
        self.tabview.add("Settings")
        self.tabview.add("Console")

        # Create Control tab content
        self.create_control_tab(self.tabview.tab("Control"))

        # Create Settings tab content
        self.create_settings_tab(self.tabview.tab("Settings"))

        # Create Console tab content
        self.create_console_tab(self.tabview.tab("Console"))

        # Serial connection attributes
        self.serial_connection = None

        # Start periodic check for connection status
        self.check_connection_status()

        # Bind the close event
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def create_control_tab(self, tab):
        # Create three columns
        self.create_column(tab, 0, "Connect")
        self.create_separator(tab, 1)
        self.create_column(tab, 2, "Setup")
        self.create_separator(tab, 3)
        self.create_column(tab, 4, "Control")

        # Configure tab to expand vertically and horizontally
        tab.grid_rowconfigure(0, weight=1)
        for i in [0, 2, 4]:
            tab.grid_columnconfigure(i, weight=1)

    def create_settings_tab(self, tab):
        settings_frame = ctk.CTkFrame(tab)
        settings_frame.pack(pady=20)

        stpu_label = ctk.CTkLabel(settings_frame, text="Select Steps per Revolution (stpu_ve):", font=("Arial", 14))
        stpu_label.pack(pady=5)
        self.stpu_var = ctk.StringVar(value="3200")  # Default value
        stpu_menu = ctk.CTkComboBox(settings_frame, variable=self.stpu_var,
                                    values=["400", "800", "1600", "3200", "6400"])
        stpu_menu.pack(pady=5)

        d_ve_label = ctk.CTkLabel(settings_frame, text="Enter Diameter of Rollers (d_ve) in meters:",
                                  font=("Arial", 14))
        d_ve_label.pack(pady=5)
        self.d_ve_entry = ctk.CTkEntry(settings_frame, width=100, placeholder_text="Diameter (m)")
        self.d_ve_entry.insert(0, "0.05")
        self.d_ve_entry.pack(pady=5)

        uev_ve_label = ctk.CTkLabel(settings_frame, text="Enter Gear Ratio (uev_ve):", font=("Arial", 14))
        uev_ve_label.pack(pady=5)
        self.uev_ve_entry = ctk.CTkEntry(settings_frame, width=100, placeholder_text="Gear Ratio")
        self.uev_ve_entry.insert(0, "1")
        self.uev_ve_entry.pack(pady=5)

        apply_button = ctk.CTkButton(settings_frame, text="Apply", command=self.apply_settings)
        apply_button.pack(pady=5)

    def create_console_tab(self, tab):
        # Create a text box for the console output
        self.console_text = ctk.CTkTextbox(tab, width=750, height=350)
        self.console_text.pack(pady=20)

        # Redirect stdout to the console text box
        sys.stdout = ConsoleRedirector(self.console_text)
        sys.stderr = ConsoleRedirector(self.console_text)

    def apply_settings(self):
        try:
            self.stpu_ve = int(self.stpu_var.get())
            self.d_ve = float(self.d_ve_entry.get())
            self.uev_ve = int(self.uev_ve_entry.get())
            print(f"Settings applied - stpu_ve: {self.stpu_ve}, d_ve: {self.d_ve}, uev_ve: {self.uev_ve}")

            if self.serial_connection and self.serial_connection.is_open:
                command = f"SETTINGS {self.stpu_ve} {self.d_ve} {self.uev_ve}\n"
                self.serial_connection.write(command.encode())
                print(f"Sent command: {command.strip()}")
                self.send_speeds()  # Send speeds after applying settings
        except ValueError:
            print("Invalid input. Please enter valid numbers for stpu_ve, d_ve, and uev_ve.")

    def create_column(self, parent, column_index, header_text):
        column_frame = ctk.CTkFrame(parent, width=200)
        column_frame.grid(row=0, column=column_index, padx=5, pady=5, sticky="nsew")

        # Add header label
        header_label = ctk.CTkLabel(column_frame, text=header_text, font=("Arial", 16, "bold"))
        header_label.pack(pady=10)

        if header_text == "Connect":
            # Create an inner frame to center the dropdown and button
            inner_frame = ctk.CTkFrame(column_frame)
            inner_frame.pack(expand=True)

            # Add dropdown and connect button in the first column
            self.com_var = ctk.StringVar()
            self.com_menu = ctk.CTkComboBox(inner_frame, variable=self.com_var, values=self.get_com_ports())
            self.com_menu.pack(pady=10)

            self.connect_button = ctk.CTkButton(inner_frame, text="Connect", command=self.connect_serial)
            self.connect_button.pack(pady=10)

            # Add connection status light
            self.connection_status = ctk.CTkLabel(inner_frame, text=" ", width=20, height=20, corner_radius=10,
                                                  fg_color="red")
            self.connection_status.pack(pady=10)

        if header_text == "Setup":
            # Create an inner frame to center the switches and text inputs
            inner_frame = ctk.CTkFrame(column_frame)
            inner_frame.pack(expand=True)

            # Add switches and text inputs to enable/disable motors and set speed
            self.motor_switches = []
            self.speed_entries = []
            for i in range(5):
                switch_frame = ctk.CTkFrame(inner_frame)
                switch_frame.pack(pady=5, anchor="w")  # Align to the left

                switch_label_motor = ctk.CTkLabel(switch_frame, text=f"Motor {i + 1}")
                switch_label_motor.pack(side=ctk.LEFT, padx=5)

                switch_label_disable = ctk.CTkLabel(switch_frame, text="Disable")
                switch_label_disable.pack(side=ctk.LEFT, padx=5)

                motor_switch = ctk.CTkSwitch(switch_frame, text="Enable", command=lambda i=i: self.toggle_motor(i))
                motor_switch.pack(side=ctk.LEFT, padx=5)
                motor_switch.select()  # Set all motors to enable on startup
                self.motor_switches.append(motor_switch)

                speed_entry = ctk.CTkEntry(switch_frame, width=100, placeholder_text="Speed (m/min)")
                speed_entry.pack(side=ctk.LEFT, padx=5)
                self.speed_entries.append(speed_entry)

            # Add a frame to hold the Send Speeds button and align it to the right
            button_frame = ctk.CTkFrame(column_frame)
            button_frame.pack(pady=10, anchor="e")

            # Add a button to send all speeds
            self.send_speeds_button = ctk.CTkButton(button_frame, text="Send Speeds", command=self.send_speeds)
            self.send_speeds_button.pack(side=ctk.RIGHT, padx=5)

            # Add a switch to toggle direction
            self.direction_switch = ctk.CTkSwitch(button_frame, text="Forward", onvalue="Forward", offvalue="Backward",
                                                  command=self.toggle_direction)
            self.direction_switch.pack(side=ctk.LEFT, padx=5)
            self.direction_switch.select()  # Set to "Forward" on startup

        if header_text == "Control":
            # Create an inner frame to center the control buttons
            inner_frame = ctk.CTkFrame(column_frame)
            inner_frame.pack(expand=True)

            # Add start/stop switch in the third column
            start_stop_switch = ctk.CTkSwitch(inner_frame, text="Start/Stop", command=self.toggle_start_stop)
            start_stop_switch.pack(pady=10)
            self.start_stop_switch = start_stop_switch

    def create_separator(self, parent, column_index):
        separator = ctk.CTkFrame(parent, fg_color="gray", width=2)
        separator.grid(row=0, column=column_index, sticky="ns")

    def get_com_ports(self):
        """Returns a list of available COM ports"""
        ports = serial.tools.list_ports.comports()
        return [port.device for port in ports]

    def connect_serial(self):
        """Connect to the selected COM port"""
        selected_port = self.com_var.get()
        if selected_port:
            try:
                self.serial_connection = serial.Serial(selected_port, 115200)  # Updated baud rate to 115200
                print(f"Connected to {selected_port}")
                # Send enable command to all motors on startup
                for i in range(5):
                    self.toggle_motor(i)
                # Update connection status light to green
                self.connection_status.configure(fg_color="green")
                # Start a thread to read from the serial port
                self.serial_thread = threading.Thread(target=self.read_serial)
                self.serial_thread.daemon = True
                self.serial_thread.start()
            except serial.SerialException as e:
                print(f"Failed to connect to {selected_port}: {e}")
                # Update connection status light to red
                self.connection_status.configure(fg_color="red")
        else:
            print("No COM port selected")
            # Update connection status light to red
            self.connection_status.configure(fg_color="red")

    def read_serial(self):
        """Read from the serial port and display in the console"""
        while self.serial_connection.is_open:
            try:
                line = self.serial_connection.readline().decode('utf-8').strip()
                if line:
                    print(line)
            except serial.SerialException:
                break

    def toggle_motor(self, motor_index):
        """Toggle motor enable/disable state"""
        if self.serial_connection and self.serial_connection.is_open:
            state = self.motor_switches[motor_index].get()
            enable_str = "true" if state else "false"
            command = f"ENABLE{motor_index + 1} {enable_str}\n"
            self.serial_connection.write(command.encode())
            print(f"Sent command: {command.strip()}")

            # Set speed if enabled
            if state:
                speed = self.speed_entries[motor_index].get()
                if speed:
                    try:
                        speed_value = float(speed)
                        speed_command = f"M{motor_index + 1} {speed_value}\n"
                        self.serial_connection.write(speed_command.encode())
                        print(f"Set speed for Motor {motor_index + 1}: {speed_value} m/min")
                    except ValueError:
                        print(f"Invalid speed value for Motor {motor_index + 1}")
        else:
            print("Serial connection is not open")

    def send_speeds(self):
        """Send speed settings for all motors"""
        if self.serial_connection and self.serial_connection.is_open:
            for i in range(5):
                speed = self.speed_entries[i].get()
                if speed:
                    try:
                        speed_value = float(speed)
                        speed_command = f"M{i + 1} {speed_value}\n"
                        self.serial_connection.write(speed_command.encode())
                        print(f"Set speed for Motor {i + 1}: {speed_value} m/min")
                    except ValueError:
                        print(f"Invalid speed value for Motor {i + 1}")
        else:
            print("Serial connection is not open")

    def toggle_start_stop(self):
        """Toggle start/stop state of all motors"""
        if self.serial_connection and self.serial_connection.is_open:
            if self.start_stop_switch.get():
                self.serial_connection.write(b"START\n")
                print("Sent command: START")
            else:
                self.serial_connection.write(b"STOP\n")
                print("Sent command: STOP")
        else:
            print("Serial connection is not open")

    def toggle_direction(self):
        """Toggle direction of all motors"""
        if self.serial_connection and self.serial_connection.is_open:
            self.motor_direction = "REVERSE" if self.motor_direction == "FORWARD" else "FORWARD"
            command = f"DIR:{self.motor_direction}\n"
            self.serial_connection.write(command.encode())
            direction_text = "Forward" if self.motor_direction == "FORWARD" else "Backward"
            self.direction_switch.configure(text=direction_text)
            print(f"Sent command: {command.strip()}")
        else:
            print("Serial connection is not open")

    def check_connection_status(self):
        """Periodically check the connection status and update the light"""
        if self.serial_connection:
            try:
                self.serial_connection.in_waiting  # Attempt to read
                self.connection_status.configure(fg_color="green")
            except (OSError, serial.SerialException):
                self.connection_status.configure(fg_color="red")
                self.serial_connection = None
        else:
            self.connection_status.configure(fg_color="red")
        # Check again after 1000ms (1 second)
        self.root.after(1000, self.check_connection_status)

    def on_closing(self):
        """Handle the window close event"""
        if self.serial_connection and self.serial_connection.is_open:
            # Disable all motors before closing
            for i in range(5):
                command = f"ENABLE{i + 1} false\n"
                self.serial_connection.write(command.encode())
                print(f"Sent command: {command.strip()}")
        self.root.destroy()


if __name__ == "__main__":
    ctk.set_appearance_mode("light")  # Modes: "System" (default), "Dark", "Light"
    ctk.set_default_color_theme("blue")  # Themes: "blue" (default), "green", "dark-blue"

    root = ctk.CTk()
    gui = ThreeColumnGUI(root)
    root.mainloop()

Beste Grüße

multiStepper.run();

Wieviele Schritte sind das (pro Sekunde)?

Nimm MoBaTools für die Steppersteuerung, das läuft quasi-parallel. Du bast da einen fetten Teil in der Loop, der sicher Probleme macht.

#include <AccelStepper.h>
#include <MultiStepper.h>

was mir da auffällt: warum benötigst du zwei verschiedene Stepper Libs?

Müssten 3395 Schritte pro sek sein

Danke für den Tipp, werds mal probieren. Aber meinst du das müsste grundsätzlich mit dem Mega gehen?

War ein Tipp von einem Kollegen, der meinte dass die multistepper so eingesetzt zügiger arbeiten würden. Ob was dran ist… keine Ahnung ob da was dran ist

Das geht sicher nicht schneller dadurch. Die Multistepper braucht man nur, wenn Stepper mit unterschiedlicher Geschwindigkeit synchron laufen sollen, so dass sie gleichzeitig am Ziel ankommen. Nachteil ist, dass es keine Beschleunigungs und Bremsrampen gibt. Das schränkt die mögliche Geschwindigkeit stark ein.

Wie passt das zusammen?

Die MobaTools können bis zu 6 Stepper mit jeweils max 2500 Steps/sec auf einem Mega ansteuern.

Die Accelstepper schafft auf einem 16MHz AVR bei einem einzelnen Stepper maximal 5000 Steps/Sec - wenn der Prozessor nichts 'gleichzeitig' zu tun hat ( der run() Aufruf braucht ca 200µs wenn ein Schritt fällig ist). Mit jedem weiteren gleichzeitig laufenden Stepper reduziert sich die mögliche Maximalgeschwindigkeit entsprechend.

Ok. Dann würde ich das mit dem multistepper wieder ausbauen.

Der MaxSpeed wird am Anfang einmal auf 1000 Gesetzt, die Geschwindigkeit auf 0. Wenn man ihm dann eine Geschwindigkeit schickt (per GUI) wird die maximale Geschwindigkeit neu gesetzt (s.u. im code). Das hat den Hintergrund, dass die Motoren irgendwann bei Programmstart eventuell in einer bestimmten Geschwindigkeit laufen müssen. Ich hab jetzt erstmal 0 eingegeben weil es mich genervt hat wenn die Dinger gleich los orgeln.

ok. also 2500 ist für meine Anwendung zu wenig, bzw. die 5000 steps/sec mit AccelStepper (aus deinem Post hier drunter. Selbst wenn ich alles aus dem Loop ziehe...

Dann also meine Frage. Welches Board ist in der Lage die Geschwindigkeit zu bringen? Am liebsten wäre mir natürlich etwas das Kompatibel ist mit Arduinocode.

ESP32 ... und MobaTools :slight_smile:

kannst du mir da ein bestimmtes board empfehlen? Die "Bezeichnungsvielfalt" ist ja grausam.

Irgendeines, z.B:

vielen Dank, bestellt. Wäre Arduino GIGA auch dazu in der Lage? Muss noch mehr so ein Gedöns machen und Chefs haben gerne immer Alternativvorschläge :face_with_raised_eyebrow: wenn ich mir die Taktrate anschaue sollte das ja eigentlich auch hinhauen für das 5-fache vom Preis

Klar, aber da musst du dich dann mit FreeRTOS und der Taktgenerierung selber rumschlagen. Oder du nimmst Remora, reißt den Teil mit der Kommunikation mit LinuxCNC raus, läßt nur den Motionplaner drinnen ... tja, ich würd's nicht machen :wink:

Oder du holst ein NUCLEO-F446RE - da gibt's einen GRBL-Port dafür, den kannst du plündern.

Aehm, um auf dein 1. Posting zurückzukommen: Warum nimmst du nicht ESP32+FluidNC+WebGUI ?

in dem raum in dem die Einheit betrieben werden soll ist kein WLAN verfügbar und kann auch nicht installiert werden... Dann wirds mit WebGUI schwierig oder?

Bekommtst Provision vom dem Schrotthändler?
Noch nichts über den Laden gelesen?
Dazu bekommt man ESP32 viel günstiger als bei überteuertem az

1 Like

Klar kriegst du die Teile günstiger, aber das waren die ersten Links die Amazon hochgespült hat. Ist das selbe Zeug wie auf Aliexpress, so what?

Webgui: Na ja, der ESP32 könnte den AP machen. Frage ist halt, wie am Ende die Benutzerinteraktion ausschauen soll. Reicht ein LCD mit ein paar Knöpfen? Bitte auf den Notaus-Schalter nicht vergessen, sonst gibt's Probleme mit der Arbeitssicherheit.