Wie kompiliert man MicroPython vom Quellcode selbst?

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

heute mal wieder was passendes zum Forum, ich würde euch gerne mal den Zwischenstand meines Programms zur Schweißmaschine zeigen. Geschrieben wurde es in MicroPython mit dieser Version. Die benötige ich wegen dem Counter und MCPWM, das @__deets__ eingebaut hat.

Ich habe mal ein paar Tests gemacht und so weit ich das überblicken konnte funktioniert der „Hauptteil“ auch. Da es absolut nicht ausgeschlossen ist, dass meine Programmstruktur falsch oder nicht besonders sinnvoll ist und da ich sicherlich auch einige schlechte Codezeilen geschrieben habe, würde ich euch gerne mal drüber schauen lassen.
Die Ausgabe über das Display fehlt noch, da habe ich noch keinen wirklichen Plan wie ich das machen soll, dazu später mehr.

Ich habe neulich ein Bild der Maschine hier gepostet, aber hier habe ich das ganze noch mal schematisch dargestellt:
https://www.dropbox.com/s/oa7i93isf0skc ... 3.png?dl=0

Erst mal die Funktionsbeschreibung in Textform:

- Maschine einschalten:

- Keine Aktion bevor nicht mit „Start“ bestätigt wird
- Referenzfahrt der Maschine
- Abfrage der Endlagen, wenn die linke Endlage nicht aktiv ist, dreht der Schwenkmotor bis
bis sie aktiv ist. Motor hält an, Pneumatikzylinder fährt aus. Kurze Pause um sicher zu
stellen dass der Zylinder ausgefahren ist.
- Abfrage des Näherungsschalters. Ist er nicht aktiv, drehen der Schweissachse bis der Näherungsschalter das Loch in der Scheibe gefunden hat. Da die Scheibe nicht direkt auf der
Schweissachse ist, kann der Benutzer durch erneutes „Start“ drücken den Motor eine weitere
Umdrehung drehen lassen um die Startposition festzulegen. Wird innerhalb von 5s „Start“
nicht gedrückt, wird der Encoder auf 0 gesetzt
- Maschinenreferenzfahrt abgeschlossen
- Status LED an


- „Start“-Taster betätigt:

- Status LED ausfahren
- Schweißen darf nur starten wenn die Endlage betätigt ist
Wenn Simulations-Modus Schalter auf 1 steht, darf das Schweißgerät nicht angehen
- Wenn Simulations-Modus auf 0 steht Schweißgerät einblenden
- Warten bis eingestellte Verweilzeit abgelaufen ist
- Abhängig von der belegten Endlage die Schweßachse in entsprechende Richtung drehen
- Wenn eingestellter Drehwinkel erreicht ist, Schweißachse und Schweißgerät stoppen
- Schweißachse auf Ausgangsposition zurück fahren.
Wenn Automatik-Modus auf 1 steht, Zylinder einfahren, kurz warten, schwenken bis Endlage erreicht wird, anderer Pneumatikzylinder ausfahren, kurz warten, Schweißprozess wiederholen, nach Schweißprozess auf Ausgangsstellung zurück schwenken. Status LED an.
Wenn Automatik-Modus auf 0 steht, Status LED an.
- Während des Schweißprozesses muss der Endlagenschalter immer abgefragt werden. Wenn er nicht mehr aktiv ist, Schweißprozess abbrechen.


- „Schwenk“-Taster bestätigt:

- Pneumatikzylinder einfahren.
- Schwenkmotor dreht auf Endlagen
- Pneumatikzylinder ausfahren
- Kurz warten
- Status LED an


- „Geschwindigkeit“-Poti:

- Gibt die Drehzahl der Schweißachse vor.
- Einheit ist Prozent
- Soll während des Schweißens änderbar sein


- „Winkel“-Poti:

- Gibt den Drehwinkel vor um die sich die Schweißachse drehen soll
- Einheit in Grad
- Soll während des Schweißens änderbar sein


- „Zeit“-Poti:

- Gibt die Verweilzeit vor, in der der Schweißbrenner an ist, die Schweißachse aber noch steht
- Einheit in Sekunden
- Soll während des Wartens änderbar sein


- Fehler Endlage verloren

- Schweißen abbrechen
- Keine weiteren Aktionen anwählbar
- Mit „Start“-Taster erneute Referenzfahrt bestätigen.
- Sicherstellen das die Pneumatikzylinder eingefahren sind
- Start Referenzfahrt


Ich habe noch nie eine Ablaufsteuerung geschrieben und weis gar nicht wie man das sinnvoll aufbauen könnte. Bei mir steckt jetzt alles in einer Klasse und das sieht so aus:

Code: Alles auswählen

from time import sleep, ticks_diff, ticks_us

from esp32 import MCPWM, PCNT
from LargeCounter import LargeCounter
from machine import ADC, Pin

ENCODER_PINS = [2, 15]

REFERENCE_SWITCH_PIN = 4
END_POSITION_PINS = {"left": 18, "right": 19}

AUTOMATIC_BUTTON_PIN = 35
SIMULATE_BUTTON_PIN = 33
START_BUTTON_PIN = 25
ROTATE_BUTTON_PIN = 21
TURN_SWITCH_PINS = {"wait_for_start": 34, "speed": 36, "welding_angle": 39}

STATUS_LED_PIN = 32
WELDING_MACHINE_PIN = 26

PNEUMATIC_CYLINDER_PINS = {"left": 22, "right": 27}
PWM_PINS = {"welding_axis": [5, 16], "rotation_axis": [17, 23]}

ENCODER_RESOLUTION = 5000
# Welding-angle in °, speed in %, time before start welding in s
MAX_VALUES_TO_TRANSLATE_DIGITAL_VALUE = [450, 100, 5]
POSITION_MACHINE_START = "left"
RATIO = 96 / 41


class WeldingControl:
    def __init__(self):
        self.turn_switches = {
            actuator: ADC(Pin(TURN_SWITCH_PINS[actuator]))
            for actuator in TURN_SWITCH_PINS
        }
        for turn_switch, pin in self.turn_switches.items():
            pin.atten(self.turn_switches[turn_switch].ATTN_11DB)
        self.start = Pin(START_BUTTON_PIN, Pin.IN, Pin.PULL_DOWN)
        self.rotate_workpiece = Pin(ROTATE_BUTTON_PIN, Pin.IN, Pin.PULL_DOWN)
        self.simulate_mode = Pin(SIMULATE_BUTTON_PIN, Pin.IN, Pin.PULL_DOWN)
        self.automatic_mode = Pin(AUTOMATIC_BUTTON_PIN, Pin.IN, Pin.PULL_DOWN)
        self.welding_axis = MCPWM(0)
        self.welding_axis.bind(
            Pin(PWM_PINS["welding_axis"][0]), Pin(PWM_PINS["welding_axis"][1])
        )
        self.turning_axis = MCPWM(3)
        self.turning_axis.bind(
            Pin(PWM_PINS["rotation_axis"][0]), Pin(PWM_PINS["rotation_axis"][1])
        )
        self.welding_machine = Pin(WELDING_MACHINE_PIN, Pin.OUT)
        self.cylinders = {
            position: Pin(pin, Pin.OUT)
            for position, pin in PNEUMATIC_CYLINDER_PINS.items()
        }
        self.status_led = Pin(STATUS_LED_PIN, Pin.OUT)
        self.machine_start_reference = Pin(
            POSITION_MACHINE_START, Pin.IN, Pin.PULL_DOWN
        )
        self.end_positions = {
            position: Pin(END_POSITION_PINS[position], Pin.IN)
            for position in END_POSITION_PINS
        }
        encoder = PCNT(0, Pin(ENCODER_PINS[0]), Pin(ENCODER_PINS[1]))
        self.counter = LargeCounter(encoder)

        try:
            self.active_end_position = [
                position
                for position in self.end_positions
                if not self.end_positions[position].value()
            ][0]
        except IndexError:
            self.active_end_position = None
        self.welding_axis.duty(0)
        self.status_led.off()
        for cylinder in self.cylinders.values():
            cylinder.off()
        sleep(1)
        self.wait_for_initial()

    @property
    def actually_welding_position(self):
        # position in angle
        return (
            self.direction * 360 * self.counter.counter() // ENCODER_RESOLUTION // RATIO
        )

    @property
    def direction(self):
        return 1 if self.active_end_position == POSITION_MACHINE_START else -1

    @property
    def time_before_start(self):
        return self.translate_measure_value(
            self.turn_switches["wait_for_start"].read(),
            MAX_VALUES_TO_TRANSLATE_DIGITAL_VALUE[2],
        )

    @property
    def welding_angle(self):
        return self.translate_measure_value(
            self.turn_switches["welding_angle"].read(),
            MAX_VALUES_TO_TRANSLATE_DIGITAL_VALUE[0],
        )

    @property
    def welding_speed(self):
        return self.translate_measure_value(
            self.turn_switches["speed"].read(),
            MAX_VALUES_TO_TRANSLATE_DIGITAL_VALUE[1] * self.direction,
        )

    @staticmethod
    def translate_measure_value(value, reference_value):
        # 4095 = max. value ADC.read()
        if reference_value > 10:
            return reference_value - (reference_value * value // 4095)
        else:
            return round(reference_value - (reference_value * value / 4095), 1)

    def check_machine_position(self):
        self.turning_axis.duty(50)
        if not self.end_positions[POSITION_MACHINE_START].value():
            self.turning_axis.duty(0)
            self.active_end_position = POSITION_MACHINE_START
            return True

    def control_cylinder(self, state, position):
        if state == "out":
            self.cylinders[position].on()
        else:
            self.cylinders[position].off()

    def control_welding_process(self):
        self.status_led.off()
        self.do_welding()
        self.stop_welding()
        if self.automatic_mode.value():
            self.control_workpiece_rotation()
            sleep(1)
            self.do_welding()
            self.stop_welding()
            self.control_workpiece_rotation()
        self.status_led.on()

    def control_workpiece_rotation(self):
        self.control_cylinder("in", self.active_end_position)
        sleep(1)
        self.rotate()
        sleep(1)
        self.control_cylinder("out", self.active_end_position)

    def do_welding(self):
        if self.end_positions[self.active_end_position].value():
            self.stop_welding()
            self.set_back_to_initial_mode()
            return
        if not self.simulate_mode.value():
            self.welding_machine.on()
        start = ticks_us()
        while True:
            if self.end_positions[self.active_end_position].value():
                self.stop_welding()
                self.set_back_to_initial_mode()
                break
            if ticks_diff(ticks_us(), start) > self.time_before_start * 1e6:
                break
            sleep(0.01)
        self.welding_axis.duty(self.welding_speed)
        while True:
            if self.end_positions[self.active_end_position].value():
                self.stop_welding()
                self.set_back_to_initial_mode()
                break
            if self.actually_welding_position >= self.welding_angle:
                break
            self.welding_axis.duty(self.welding_speed)
            sleep(0.01)

    def initial_machine(self):
        if self.active_end_position is None:
            self.control_cylinder("in", "left")
            self.control_cylinder("in", "right")
            sleep(1)
            while True:
                if self.check_machine_position():
                    sleep(0.5)
                    self.control_cylinder("out", self.active_end_position)
                    sleep(1)
                    break
                sleep(0.1)
        else:
            self.control_cylinder("out", self.active_end_position)
        while not self.initial_welding_axis():
            pass

    def initial_welding_axis(self):
        initial_status = None
        self.welding_axis.duty(100)
        while True:
            if self.machine_start_reference.value():
                self.welding_axis.duty(0)
                timestamp = ticks_us()
                while True:
                    if ticks_diff(ticks_us(), timestamp) >= 5e6:
                        initial_status = True
                        break
                    if self.start.value():
                        self.welding_axis.duty(100)
                        sleep(0.5)
                        initial_status = False
                        break
                if initial_status:
                    self.counter.reset()
                    return True
            sleep(0.01)

    def monitor_user_input(self):
        while True:
            if self.simulate_mode.value():
                self.welding_machine.off()
            if self.start.value():
                self.switching_start_stop()
            if self.rotate_workpiece.value() and self.status_led.value():
                self.status_led.off()
                self.control_workpiece_rotation()
                sleep(1)
                self.status_led.on()
            sleep(0.1)

    def rotate(self):
        for end_position in self.end_positions:
            if self.active_end_position != end_position:
                break
        self.turning_axis.duty(
            50 if self.active_end_position == POSITION_MACHINE_START else -50
        )
        sleep(1)
        while True:
            if not self.end_positions[end_position].value():
                self.turning_axis.duty(0)
                self.active_end_position = end_position
                break
            sleep(0.1)

    def set_back_to_initial_mode(self):
        self.status_led.off()
        self.cylinders[self.active_end_position].off()
        self.active_end_position = None
        self.wait_for_initial()

    def stop_welding(self):
        self.welding_axis.duty(0)
        self.welding_machine.off()
        sleep(1)
        if self.actually_welding_position != 360:
            self.welding_axis.duty(-60 * self.direction)
            while True:
                if self.actually_welding_position in [360, 0]:
                    self.welding_axis.duty(0)
                    break
        self.counter.reset()

    def switching_start_stop(self):
        if not self.status_led.value():
            self.stop_welding()
            self.status_led.on()
        else:
            self.control_welding_process()

    def wait_for_initial(self):
        while True:
            if self.start.value():
                self.initial_machine()
                self.status_led.on()
                break
            sleep(0.1)
        self.monitor_user_input()


def main():
    WeldingControl()


if __name__ == "__main__":
    main()
Ich habe hier mal eine Diskussion mit verfolgt, in der es darum ging ob man immer eine Insanz einer Klasse erstellen soll oder nicht. Da bin ich aber mangels Verständnis irgendwann ausgestiegen und da ich gerade mit der Insanz nichts machen würde, habe ich die Klasse direkt aufgerufen. Die ganzen 'sleep' 's die höher wie 0.1s sind habe ich drin, damit die Zylinder Zeit haben aus und einzufahren, den solange die sich bewegen soll auch sonst nichts passieren.

Das Display will ich nur aktualisieren wenn sich ein Wert geändert hat. Also die drei Einstellwerte mit den Potis. Eventuell kommen noch ein zwei Hinweise dazu, zum Beispiel "Für Referenzfahrt Start drücken" oder so.
Macht es Sinn, dafür eine eigene Klasse zu schreiben und für die Aktualisierung und Abfrage einen Thread zu starten?

Wie sortiert man so ein Programm eigentlich? Ich habe mal mit dem Eigenschaften angefangen und die Funktionen alphabetisch.

Wer das jetzt bis hier her alles gelesen hat, vielen Dank dafür schon mal!
Falls ihr mal Zeit und Lust habt würde ich mich über jede Kritik und Verbesserung freuen.

Viele Grüße
Dennis

Edit: Die Endpositionen muss ich mit 'not' abfragen, da die 0 liefer wenn sie betätigt sind. Das muss ich auf dem Steckbrett noch ändern.
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Du erzeugst ja eine Instanz Deiner Klasse, Du brauchst aber diese nicht, weil Deine __init__ zu viel macht.
wait_for_initial gehört Sa nicht rein, und monitor_user_input nicht in wait_for_initial.

Code: Alles auswählen

...
    def wait_for_initial(self): 
        while not self.start.value():
            sleep(.1)

def main():
    welding_control = WeldingControl()
    welding_control.wait_for_initial()
    weldung_control.initial_machine()
    welding_control.status_led.on()
    welding_control.monitor_user_input()
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

vielen Dank für die Antwort und sorry für die späte Reaktion von mir.
Ich kann das gar nicht begründen, wieso ich die Aufrufe in die __init__ geschrieben habe. Das werde ich so ändern und die Schleife in 'wait_for_initial' auch.

Anfang nächster Woche will ich den Code für die Displayaktualisierung einbauen. Würde das mit einer eigenen Klasse, so wie oben beschrieben, Sinn machen? Damit könnte ich mir immer die letzten Werte merken und müsste das Display nur bei Bedarf aktualisieren und ich denke der Code sieht dann verständlicher aus. Ich habe das Gefühl, dass wenn ich mir die letzten Werte mit der 'WeldingControl'-Klasse merke, dann wird das zu chaotisch und unübersichtlich oder was meint ihr?


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Grundsaetzlich ist eine Klasse immer besser, wenn sie einen "single concern" hat. Darum bietet sich sowas an.
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Abend,

danke für die Antwort. Bis jetzt hat es mir leider noch nicht gereicht damit anzufangen, aber ich halte euch natürlich auf dem laufenden. Zumindest im Kopf habe ich das Konzept schon mal :D

Viele Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Abend,

endlich bin ich dazu gekommen die benötigten Bilder für das Display zu ändern. Vielleicht erinnert sich der ein oder andere noch daran, dass mir das Nextion-Display zu langsam war und meine Visualisierung der Drehregler nicht flüssig lief. Ich habe jetzt pro Drehregler 25 Bilder und diese haben im Nextion-Editor fortlaufende ID's bekommen, mit denen ich die Bilder aufrufe und ihrem Platz zuweise. Ich habe mich dazu entschieden, das ich keine transparenten Bilder mehr nehme, sonder dass das jedes Bild immer den entsprechendem Hintergrundausschnitt hat. (Wie "damals" von @__blackjack__ hier vorgeschlagen).

Wenn ich die Maschine starte, dann sollen erst mal die Drehregler ausgelesen werden und auf dem Display sollen die richtigen Werte dargestellt werden. Das Auslesen der Regler gibt mir gleich den "richtigen" Wert, Drehzahl gibt mir einen Wert von 0-100%, Zeit 0-5s und Winkel 0 - 450°. Damit will ich sagen, die spätere Display-Klasse muss sich nicht um das umrechnen der ADC-Werte kümmern, sondern bekommt die Werte, die sie anzeigen soll. Aber sie muss aus den Werten die ID des Bildes berechnen und je nach dem um welchen Regler es geht muss die richtige Position hinzugefügt werden.

Ich habe mir gerade überlegt wie ich das sinnvoll aufbauen könnte, damit es auch gut lesbar ist und habe folgenden Test-Code geschrieben:

Code: Alles auswählen

NUMBER_OF_PICTURES = 25
CONTROLLER_TO_PICTURE_SETTINGS = {
        'speed': {"add_to_picture_number": 0, "max_value": 100, "start_picture_number": 1, "position": 0},
        'time': {"add_to_picture_number": 25, "max_value": 5, "start_picture_number": 26, "position": 267},
        'angle': {"add_to_picture_number": 50, "max_value": 450, "start_picture_number": 51, "position": 534}
    }


def translate_to_picture_number(factors, value):
    picture_number = factors["add_to_picture_number"] + (NUMBER_OF_PICTURES * value // factors["max_value"])
    return picture_number if picture_number > factors["start_picture_number"] - 1 else factors["start_picture_number"]


def main():
    # speed_value, time_value, angle_value
    values = [0, 2, 30]
    for value, factors in zip(values, CONTROLLER_TO_PICTURE_SETTINGS.values()):
        picture_number = translate_to_picture_number(factors, value)
        print(f'Picturenumber is: {picture_number}')
        print(f'Picture place on: {factors["position"]}')


if __name__ == '__main__':
    main()

'values' sind die Werte, die auf dem Display stehen sollen. Ich könnte mir vorstellen, dass ich beim regelmäßigen abfragen der Drehregler, die Ergebnisse auch in einer Liste sammle und dann einmal ein Displayupdate mit den Werten mache.
Die Abfrage im 'return'-Teil von 'translate_to_picture_number' habe ich drin, weil wenn der Wert des Drehreglers 0 ist, dann würde bei 'time' zum Beispiel das Bild mit der ID 25 angezeigt werden und dass Bild gehört noch zu 'speed'.

Meine Frage, ist das mit dem Dictonary im Dictonary so schön gelöst oder soll ich da anders ran gehen? Bin gerade leider krank und kann mir hier nur theoretische Testprogramme schreiben und es nicht live testen. Aber die Ausgabe sieht für mich mal logisch aus.

Vielen Dank und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Abend,

meine Display-Klasse sieht jetzt so aus:

Code: Alles auswählen

from Nextion import Nextion


NUMBER_OF_PICTURES = 25
CONTROLLER_TO_PICTURE_SETTINGS = {
    "speed": {
        "add_to_picture_number": 0,
        "max_value": 100,
        "start_picture_number": 1,
        "position": 0,
    },
    "time": {
        "add_to_picture_number": 25,
        "max_value": 5,
        "start_picture_number": 26,
        "position": 267,
    },
    "angle": {
        "add_to_picture_number": 50,
        "max_value": 450,
        "start_picture_number": 51,
        "position": 534,
    },
}


class Display:
    def __init__(self, tx_pin, rx_pin):
        self.display = Nextion(tx_pin, rx_pin, 9600)
        self.last_values = [None, None, None]

    def update_controller(self, values):
        for index, (value, factors, controller) in enumerate(
                zip(
                values,
                CONTROLLER_TO_PICTURE_SETTINGS.values(),
                CONTROLLER_TO_PICTURE_SETTINGS.keys()
                )
        ):
            if value != self.last_values[index]:
                picture_number = translate_to_picture_number(factors, value)
                print('value: {} {}'.format(values[index], factors))
                print('picture nr: {}'.format(picture_number))
                print("pic {},230,{}".format(factors["position"], picture_number))
                self.display.cmd(
                    "pic {},230,{}".format(factors["position"], picture_number)
                )
                self.display.cmd('{}_value.txt="{}"'.format(controller, value))
                self.last_values[index] = value

    def show_info_text(self, text):
        self.display.cmd('info.txt="{}"'.format(text))


def translate_to_picture_number(factors, value):
    picture_number = factors["add_to_picture_number"] + (
            NUMBER_OF_PICTURES * value // factors["max_value"]
    )
    return (
        picture_number
        if picture_number > factors["start_picture_number"] - 1
        else factors["start_picture_number"]
    )
Das lief auch am PC unter Python 3.9 wunderbar, allerdings hat mir MicroPython einen Strich durch die Rechnung gemacht. Die berechneten Bildnummern stimmen hinten und vorne nicht, womöglich weil das Dictionary nicht in der Reihenfolge abgearbeitet wird, in der ich es erstellt habe? Kann ich an meinem Konzept trotzdem noch irgendwie "sinnvoll" festhalten oder muss ich mir was anderes überlegen?

Ich könnte die Werte, die an die Klasse übergeben werden auch als Dictionary übergeben, dann hätte ich die Schlüssel und damit einen eindeutigen Zugriff.

Vielen Dank und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Indizes zu benutzen, versucht man eigentlich zu vermeiden. Du hast doch einen schönen Key! Das löst auch das Problem mit der Reihenfolge!
Warum benutzt Du den nicht? Statt keys und values getrennt abzufragen, benutzt man items.

Code: Alles auswählen

from Nextion import Nextion


NUMBER_OF_PICTURES = 25
CONTROLLER_TO_PICTURE_SETTINGS = {
    "speed": {
        "add_to_picture_number": 0,
        "max_value": 100,
        "start_picture_number": 1,
        "position": 0,
    },
    "time": {
        "add_to_picture_number": 25,
        "max_value": 5,
        "start_picture_number": 26,
        "position": 267,
    },
    "angle": {
        "add_to_picture_number": 50,
        "max_value": 450,
        "start_picture_number": 51,
        "position": 534,
    },
}


class Display:
    def __init__(self, tx_pin, rx_pin):
        self.display = Nextion(tx_pin, rx_pin, 9600)
        self.last_values = {}

    def update_controller(self, values):
        for controller, factors in CONTROLLER_TO_PICTURE_SETTINGS.items():
            value = values[controller]
            if value != self.last_values.get(controller):
                picture_number = translate_to_picture_number(factors, value)
                print('value: {} {}'.format(value, factors))
                print('picture nr: {}'.format(picture_number))
                print("pic {},230,{}".format(factors["position"], picture_number))
                self.display.cmd(
                    "pic {},230,{}".format(factors["position"], picture_number)
                )
                self.display.cmd('{}_value.txt="{}"'.format(controller, value))
                self.last_values[controller] = value

    def show_info_text(self, text):
        self.display.cmd('info.txt="{}"'.format(text))


def translate_to_picture_number(factors, value):
    picture_number = factors["add_to_picture_number"] + (
            NUMBER_OF_PICTURES * value // factors["max_value"]
    )
    return max(picture_number, factors["start_picture_number"])
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Vielen Dank der Code funktioniert sehr gut.

Die Keys hatte ich in meinem Code noch nicht, da habe ich die Werte noch als Liste übergeben. Mein Code, der so hätte werden sollen wie deiner, wurde gestern Abend sehr aufwendig. Vermutlich hätte ich noch einmal auf einem "leeren" Blatt anfangen sollen.

Ich kann zwar jetzt schön meine Regler darstellen und es sieht auch schön aus wie die sich mit Position des Drehgebers ändern, aber wenn es damit getan wäre, dann wärs ja nicht mein Projekt.
"In" den Regler will ich den Wert schreiben und weil ich mit 25 Bilder nur grob den Wert darstellen kann, wollte ich den tatsächlichen noch in Zahlen auf das Display schreiben. Aber da überschreibt wohl der Text das Bild.
Jetzt muss ich das Layout wohl noch ändern und schauen das ich die Werte vielleicht unterhalb des Reglers schreibe und die Beschreibung vielleicht in den Regler. Naja mal sehen.

Ich halte euch auf dem laufenden.

Viele Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo an diesem wunderschönen Sonntag,

Schaut euch doch bitte den Grund meiner guten Laune an

Das Problem war, das wenn ich im Nextion-Editor der den Text an einer bestimmten Stell vordefiniere oder auch sonst irgendwelche Elemente, dann wird jedes Element in einer eigenen Ebene definiert. Wird dann ein Element zur Laufzeit geändert, dann kann es vorkommen das Elemente in anderen Ebenen im GRAM überschrieben werden. (So die Erklärung aus einem Nextion-Forum).

Ich habe jetzt (man erkennt es noch etwas, wird noch ausgebessert) Bilder mit Hintergrundausschnitten von den Reglern gemacht und an der Stelle an der der Wert steht, habe ich einen transparenten Ausschnitt gemacht. Das habe ich gemacht, da beim Bildwechsel der Wert sonst kurz geblinkt hat, weil erst das neue Bild kommt und dann der Text wieder angezeigt werden muss. Damit das ganze in einer Ebene abläuft habe ich im Editor keine Elemente mehr vordefiniert. Die Bilder lade ich direkt an die gewünschte Position und mit 'xstr' schreibe ich die Werte an die gewünschte Stelle.

Code: Alles auswählen

from Nextion import Nextion

NUMBER_OF_PICTURES = 25
CONTROLLER_TO_PICTURE_SETTINGS = {
    "speed": {
        "add_to_picture_number": 0,
        "max_value": 100,
        "start_picture_number": 1,
        "picture_position": 0,
        "text_position": 80
    },
    "time": {
        "add_to_picture_number": 25,
        "max_value": 5,
        "start_picture_number": 26,
        "picture_position": 267,
        "text_position": 340
    },
    "angle": {
        "add_to_picture_number": 50,
        "max_value": 550,
        "start_picture_number": 51,
        "picture_position": 534,
        "text_position": 605
    },
}


class Display:
    def __init__(self, tx_pin, rx_pin):
        self.display = Nextion(tx_pin, rx_pin, 9600)
        self.last_values = {}
        self.last_picture_number = {}

    def update_controller(self, values):
        for controller, factors in CONTROLLER_TO_PICTURE_SETTINGS.items():
            value = values[controller]
            if value != self.last_values.get(controller):
                picture_number = translate_to_picture_number(factors, value)
                if picture_number != self.last_picture_number.get(controller):
                    self.display.cmd(
                        "pic {},230,{}".format(factors["picture_position"], picture_number)
                    )
                    self.last_picture_number[controller] = picture_number
                self.display.cmd('xstr {},300,114,60,1,65535,0,1,1,0,"{}"'.format(factors["text_position"], value))
                self.last_values[controller] = value

    def show_info_text(self, text):
        self.display.cmd('xstr 8,167,770,43,2,65535,0,1,1,0,"{}"'.format(text))


def translate_to_picture_number(factors, value):
    picture_number = factors["add_to_picture_number"] + (
            NUMBER_OF_PICTURES * value // factors["max_value"]
    )
    return max(int(picture_number), factors["start_picture_number"])
Man erkennt es deutlich im Video, dass die Werte nicht konstant sind. An den Stellen (bei 0 und ~82) habe ich bewusst gefilmt ohne den Potentiometer zu berühren und man sieht zum einen ein schwanken um den Wert 82 und der Startwert erreicht die 0 gar nicht, sondern schwankt da um die 2 rum.
Meine Überlegungen dazu:
- es ist alles auf einem Steckbrett mit vielleicht nicht so guten Kontakten
- es sind billige Potentiometer
- ich lese den Wert des ADC einmal aus und verarbeite ihn gleich. Ich könnte ihn auch 10 mal lesen und den Mittelwert verarbeiten und/oder eine Entprell-Zeit softwareseitig einbauen

Angeschlossen habe ich die so, nur das jeder Poti seinen eigenen Widerstand hat, das ist im Schaltplan noch falsch. Jeder Poti hat einen 100 Ohm Widerstand in der Zuleitung.

Aktuell wird das so ausgelesen (Codeausschnitte):

Code: Alles auswählen

TURN_SWITCH_PINS = {"wait_for_start": 34, "speed": 36, "welding_angle": 39}
# Welding-angle in °, speed in %, time before start welding in s
MAX_VALUES_TO_TRANSLATE_DIGITAL_VALUE = [450, 100, 5]


class WeldingControl:
    def __init__(self):
        self.turn_switches = {
            actuator: ADC(Pin(TURN_SWITCH_PINS[actuator]))
            for actuator in TURN_SWITCH_PINS
        }
        for turn_switch, pin in self.turn_switches.items():
            pin.atten(self.turn_switches[turn_switch].ATTN_11DB)


    @property
    def time_before_start(self):
        return self.translate_measure_value(
            self.turn_switches["wait_for_start"].read(),
            MAX_VALUES_TO_TRANSLATE_DIGITAL_VALUE[2],
        )

    @property
    def welding_angle(self):
        return self.translate_measure_value(
            self.turn_switches["welding_angle"].read(),
            MAX_VALUES_TO_TRANSLATE_DIGITAL_VALUE[0],
        )

    @property
    def welding_speed(self):
        return self.translate_measure_value(
            self.turn_switches["speed"].read(),
            MAX_VALUES_TO_TRANSLATE_DIGITAL_VALUE[1] * self.direction,
        )

    @staticmethod
    def translate_measure_value(value, reference_value):
        # 4095 = max. value ADC.read()
        if reference_value > 10:
            return reference_value - (reference_value * value // 4095)
        else:
            return round(reference_value - (reference_value * value / 4095), 1)
Wo würdet ihr als erstes mit der Optimierung anfangen? Habe ich vielleicht noch eine Möglichkeit vergessen?

Vielen Dank und viele Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Leider hat der ESP32 hundsbeschissene ADCs. Ich habe damit auch gerade zu kaempfen, und selbst mit einem MPC3008 habe ich Probleme, wobei das wahrscheinlich eher an dem NTC und meinem Aufbau liegt.

Du kannst das probieren mit dem mitteln, aber es wird fuerchte ich nur so mittelgeil werden. Kannst du ja mal mit einem Testprogramm ausprobieren, wie lange du mitteln musst, bist du stabile Werte bekommst (einfach mit print ausgeben oder so, statt alles gleich zu versuchen einzubauen).

Alternativ stattdessen eben einen neuen ADC (es gibt zB auch I2C-basierte mit 4 Kanaelen oder so). Oder die schon existierende PCNT-Anbindung benutzen, und Encoder nehmen. Die sind zwar endlos, aber den Anschlag kann man ja auch simulieren.
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die schnelle Antwort.

Ich sollte mal aufhören, alle Bauteile in die Werkstatt zu tragen. Ich habe zwar noch einen ESP hier, aber keinen Poti. Ich teste das mit dem mitteln nächste Woche dann mal.
Nach ADCs habe ich gerade auch mal etwas gesucht, da gibt es ja einige mit 4 Kanälen. Der ADS1115 hätte 16 Bit (das ist die Auflösung, wenn ich es richtig geleseen habe) und man kriegt den schon für ein paar Euro. Das wäre mir ehrlich gesagt lieber, wie Anschläge für einen Encoder zu bauen.
Einen Encoder habe ich zwar hier und mit dem habe ich mal einen Code getestet, den ich mit deiner (__deets__) Hilfe geschrieben habe. Das war unabhängig vom jetzt verwendeten PCNT, hat aber auch super funktioniert. Aber das ist leider kein absoluter Encoder, da müsse ich die Benutzer zwingen bei Systemstart die Encoder auf "0" zu stellen.

Naja ich teste erst mal das Mitteln und arbeite mich so wieder Schritt für Schritt durch. Wir haben hier ja erst 231 Beiträge geschrieben, da ist sicherlich noch Platz für meine Versuche :mrgreen:

Schönen Rest-Sonntag noch
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich verstehe nicht, was du mit Encoder auf 0 stellen meinst. Noch was mit den Anschlaegen. Das schoene (und schlechte) am relativen Encoder ist doch, dass man den immer relativ zu einer frei gewaehlten Basis benutzen kann. Bei Systemstart *ist* der Wert immer 0. Und Anschlaege meinte, dass du natuerlich den Encoder weiter drehen kannst, aber einfach nicht weiter zaehlst. In beide Richtungen. Man "kurbelt" also quasi. Ja, manchmal in bestimmten Anwendungen wuerde man sich einen physikalischen Anschlag wuenschen. Aber zwingend ist der oft eben nicht.

Der ADS ist ok, 16 Bit sind aber viel zu viel. Kannst du einlesen, aber dann einfach die unteren 4 wegwerfen.
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Ja genau, in die Richtung habe ich noch nicht gedacht. Ohne feste Anschläge und wenn ich nur bis zu einem bestimmten Bereich zähle, dann macht das wieder Sinn.
Ich verstehe nicht, was du mit Encoder auf 0 stellen meinst.
Dabei habe ich mir den Encoder mit zwei Anschlägen vorgestellt, das ich den nur in einem Bereich drehen kann. (So wie beim Poti auch) Wenn der Encoder dann auf einer Stellung steht und Strom vom System genommen wird, dann wäre beim Neustart die Position verloren, wenn es kein absoluter Encoder wäre. Damit ich vom linken Anschlag wieder bis zum rechten von 0 bis 100 zählen kann, müsste man den ja auf eine definierte Position stellen. So war das gemeint.

Es würde auch eine 12-Bit Variante geben: ADS1015. Ich hätte jetzt aus dem Bauchgefühl und nach dem Motto, "Mehr ist besser" die 16 Bit gewählt. Wie macht sich das bemerkbar, dass das zu viele sind oder wie muss ich vorgehen um die benötige Auflösung zu ermitteln?

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

16 Bit messen dir in den unteren Bits einfach nur rauschen. Sonst nix. Um sowas wirklich nutzen zu können, muss man viel Aufwand treiben. Mit 10 Bit musst du bereits wenige Millivolt, mit 16 0.05mV - das geht dir völlig im Rauschteppich unter.
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Okay, vielen Dank.
Wenn das Mitteln wie befürchtet nicht so toll wird, dann versuche ich es mal mit einem 12 Bit ADC oder den Encoder. Vernutlich aber erst der ADC, weil für 3 Encoder benötige ich zumindest 6 Pins, einer für das Signal und einer um zu erkennen in welche Richtung gedreht wird, soviele habe ich nicht mehr frei. Also theoretisch schon noch, es ist noc GPIO 0, 1, 3 und 14 frei. Mit 0 und 14 haben einen PWM-Zustand während des booten und 1 und 3 sind die vorbelegten RX- und TX-Pins, die kann ich auch nicht nach belieben nutzen.

Aber ich lasse mir das dann nochmal durch den Kopf gehen.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Abend,

hm ich weis jetzt nicht ob 20 Messungen üblich, viel oder wenig sind, aber wenn ich daraus einen Mittelwert bilde bleiben die meisten Werte stabil, aber die 0 wird zum Beispiel nicht erreicht. Der maximale Wert denn der ADC liest sind 4095 und wenn der Poti auf Anschlag steht kommt dieser nur selten zum Vorschein, so dass der Mittelwert immer vom max. bzw. min. Wert abweicht.
Ich bestelle nachher mal den ADS1015 mit 12 Bit und schaue mir dass dann noch einmal an.

Viele Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist nicht verwunderlich - der ESP ADC geht nicht rail to rail, vor allem nicht gegen 0. Der ist für “normale” ADC Anwendungen im Grunde ungeeignet IMHO. Gedacht ist er für Touch Interfaces oder sowas.

Das kann man beeinflussen indem man um das Poti Widerstände packt, was zwar die Dynamik beschränkt, aber den Wertebereich in die Mitte verschiebt.
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Okay, dann kann ich mir das für die Zukunft gleich merken. :)

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

der ADC ist bestellt und mein Programm hat meine bis jetzt durchgeführten Tests bestanden. Den ESP habe ich bis jetzt immer über USB und den Laptop mit Strom versorgt und das Programm auch so gestartet.
Bevor ich jetzt anfange alles auf eine Platine zu löten, wollte ich mal noch ein zwei Personen die Maschine bedienen lassen um zu sehen ob es verständlich ist und ob vielleicht noch Fehler auftreten. Damit das realistischer ist, will ich den Laptop nicht mehr mit im Spiel haben und da kommt leider mal wieder ein Problem auf.
Der ESP startet mit eigener Stromversorgung seine 'main.py' nicht. Wenn ich das Display erst mit Strom versorge, nach dem der ESP kurze Zeit Strom hatte, dann läuft alles. Also irgendwie muss der ESP beim Starten ziemlich viel Strom ziehen?

Mein Netzteil (RS-75-24) liefert 24V und 3,2A. Für das Display wird eine Stromversorgung von 5V und 1A empfohlen. Dafür verwende ich diesen Stepdown, der liefert 5V und 1A.
Für den ESP habe ich jetzt einen zweiten Stepdown eingesetzt, der gleiche wie für das Display. Dadurch müssten doch beiden Geräten 1A zur Verfügung stehen und die übrigen 1,2A des Netzteil kann sich der Rest teilen.
Zum ESP-Board findet man viele Angaben was den Strom angeht. Mit Wifi kann er wohl bis 500mA benötigen. Meine 'boot.py' ist komplett leer, also kein Wifi oder ähnliches aktiviert. Aber auch 500mA sind ja nur die Hälfte, von dem was er zur Verfügung hat.
Die 5V habe ich an dem Pin, der mit "5V" beschriftet ist, angeschlossen.

Ich hab auch schon alle anderen Stromverbraucher getrennt, so das nur die zwei Stepdowns am Netzteil angeschlossen sind, aber das änderte auch nichts.

Was übersehe ich denn da? 🤯

Vielen Dank und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Antworten