vorhandenes Script mit Countdown ergänzen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
carbonkid
User
Beiträge: 12
Registriert: Mittwoch 8. März 2023, 18:48

Hallo an Alle, hab mich hier neu angemeldet da ich blutiger Anfänger beim programmieren bin.
Ich habe eine DIY CNC-Fräse welche mit einer Steuerungssoftware namens simCNC läuft.
Diese ist vom Hersteller in Python programmiert und man kann sich da mit einem GUI Editor die Oberfläche anpassen und Funktionen über scripte hinzufügen.
Dies hab ich jetzt getan und mir ein script zum Spindelwarmlauf geschrieben:

Code: Alles auswählen

import time
 
RPM = 6000                                              #1.Drehzahl
d.setSpindleSpeed( RPM )
d.setSpindleState(SpindleState.CW_ON)                   #startet die Spindel im Uhrzeigersinn
time.sleep(200)                                         #1.Zeit in Sekunden
print("Warmlauf gestartet")                             #Textausgabe in der Python Konsole der Steuerungssoftware simCNC
 
RPM = 11000                                             #2.Drehzahl
d.setSpindleSpeed( RPM )
time.sleep(200)                                         #2.Zeit
 
RPM = 15000                                             #3.Drehzahl
d.setSpindleSpeed( RPM )
time.sleep(200)                                         #3.Zeit
 
d.setSpindleState(SpindleState.OFF)                     #stoppt die Spindel
 
print("Warmlauf beendet")                               #Textausgabe in der Python Konsole
Dieses funktioniert soweit auch einwandfrei.
Nun hätte ich aber gern das die Gesamtzeit per Countdown runter gezählt wird damit ich sehe wie lang der Warmlauf noch dauert.
Dazu habe ich mir schon Countdown Programme angeschaut, verstehe auch wie die funktionieren kann das aber nicht mit meinem script kombinieren.
Hier ist ja so typischer Counter Code:

Code: Alles auswählen

import time

secs = int(input('enter time in seconds : '))
while secs:
    mins, sec = divmod(secs, 60)
    timef = '{:02d}:{:02d}'.format(mins,sec)
    print(timef,end="\r")
    time.sleep(1)
    secs-=1

Wie sag ich ihm jetzt das er die 3 Zeiten als "input" Zeit nehmen soll?
Würde mich freuen wenn ihr mir da weiterhelfen könnt.

Beste Grüße
Daniel
Gruß Daniel
Benutzeravatar
Dennis89
User
Beiträge: 1155
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

ich glaube in dem Fall würde ich anders vorgehen. Ein 'sleep' legt das Programm ja für die Zeit schlafen. Dann kann da auch kein Countdown runterlaufen außer du startest einen zweiten Thread in dem der Countdown läuft.
Ich würde eine Schleife schreiben, dann beim Start der Spindel würde ich mir die Zeit merken (siehe time.monotonic) und dann in der Schleife regelmäßig die Zeitdifferenz zwischen Startzeit und der aktuellen Zeit messen. Wenn die Differenz die 200 Sekunden überschritten hat, dann kommt die nächste Drehzahl. Gleichzeitig könntest du die Differenz zur Countdownausgabe nutzen.

Vielleicht in diese Richtung:

Code: Alles auswählen

from time import monotonic, sleep

SPEED_TO_TIME = {400: 3, 600: 5}

def main():
    for speed, waiting_time in SPEED_TO_TIME.items():
        print(f'Drehzahl: {speed}')
        timestamp = monotonic()
        while True:
            time_delta = monotonic() - timestamp
            if time_delta >= waiting_time:
                break
            print(f'Noch {waiting_time- time_delta:.1f} Sekunden mit Drehzahl {speed}')
            sleep(0.1)


if __name__ == '__main__':
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
DeaD_EyE
User
Beiträge: 1021
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Da ich mich zurzeit mit dem ESP32 und MicroPython beschäftige, setze ich uasyncio ein.
Oftmals hat man gar keine andere Möglichkeit, da auf manchen Controllern nur ein bis zwei Threads möglich sind. Der ESP32 müsste schätzungsweise 14 Threads können (abhängig vom Speicher).

Jedenfalls hat man oft das Problem, dass man Sachen nebenläufig machen möchte. Es soll z.B. gleichzeitig eine Bewegung und ein Countdown stattfinden.
Das ist prädestiniert für asyncio.

Das Beispiel erfordert Python 3.11. Da auch die ExceptionGroups mit drin sind, kommt es bei Python 3.10 zu einem Syntaxfehler.

Mal ein Beispiel (heißt nicht, dass du es so umsetzen sollst).
Vieles dort ist unnötig, da auch Code zum Testen vorhanden ist.
Wobei, das kann man auch im Hinterkopf behalten, so kann man einfacher Unittests durchführen, ohne echte CNC.
Dann würde ich das aber mit socat nicht machen, sondern Python dazu nutzen.
(im Modul os kann man pty anlegen, hatte aber keine Lust mich jetzt damit auseinanderzusetzen)

Code: Alles auswählen

from __future__ import annotations

import asyncio
from enum import StrEnum
from random import uniform
from typing import Self

from serial_asyncio import open_serial_connection

PROG = [
    "MOVE Z UP",
    "MOVE XY",
    "MOVE Z DOWN",
    "MOVE Z UP",
]


async def handler(sr: asyncio.StreamReader, sw: asyncio.StreamWriter) -> None:
    while True:
        line = (await sr.readline()).decode("ascii")
        print()
        print(f"Serial: {line}")


async def countdown(n) -> None:
    for count in range(n, 0, -1):
        print("\33[2K\r", end="")
        print("Countdown", count, end="", flush=True)
        await asyncio.sleep(1)
    print()


class SpindleState(StrEnum):
    CW_ON = "Clockwise on"
    OFF = "OFF"


class CNC:
    RPM = 6000

    def __init__(self):
        self._error = False
        self._direction = None
        self._speed = None
        # für test
        self._sr: asyncio.StreamReader | None = None
        self._sw: asyncio.StreamWriter | None = None

    async def open(self):
        # verhindern, dass zwei mal geöffnet wird
        if self._sr is not None:
            return

        # zum Testen, da ich keine CNC habe
        self.server = await asyncio.start_server(handler, host="127.0.0.1", port=8000)

        # nur 3 Versuche den lokalen pty zu starten
        for versuch in range(3):
            self.socat = await asyncio.create_subprocess_exec(
                "socat",
                "PTY,link=csTTY1,echo=0,wait-slave",
                "TCP:127.0.0.1:8000,retry=5",
            )
            if self.socat.returncode is not None:
                print(f"Versuch {versuch}: SOCAT ist abgestützt")

        # Die serielle Verbindung
        self._sr, self._sw = await open_serial_connection(url="csTTY1", baudrate=115200)

    async def __aenter__(self) -> Self:
        """
        Der asynchrone Kontextmanager öffnet den seriellen port und
        startet die Aufwärmphase
        """
        await self.open()
        await self._warmup()
        return self

    async def __aexit__(self, exc_type, exc_obj, exc_tb) -> None:
        """
        Der asynchrone Kontextmanager stoppt beim Verlassen
        des Kontexts die Spindel der CNC, sofern die Kommunikation
        funktioniert.
        """

        await self.spindle_state(SpindleState.OFF)
        print("Motor ausgeschaltet")
        # csTTY1 terminieren
        self.socat.terminate()
        self.server.close()
        await self.server.wait_close()

    async def _warmup(self) -> None:
        """
        Aufwärmphase
        """
        # Anstatt asyncio.gather soll nun asyncio.TaskGroup verwendet werden
        # Die TaskGroup stell sicher, dass darin alle Tasks ausgeführt werden
        # und auftretende Fehler werden gesammelt und erhalten.
        async with asyncio.TaskGroup() as tg:
            tg.create_task(self.spindle_speed())
            tg.create_task(self.spindle_state(SpindleState.CW_ON))

        # Ab hier sind alle Tasks in der TaskGroup fertig
        print("Warmlauf gestartet")

        for rpm in (11000, 15000):
            await countdown(10)
            await self.spindle_speed(rpm)

    async def spindle_speed(self, rpm=None) -> int | None:
        """
        Hole oder setze Geschwindigkeit der Spindel.
        """
        if rpm is None:
            return self._speed

        if self._error:
            raise RuntimeError("Kann Spindelstatus nicht setzen.")

        rpm = rpm or self.RPM
        await asyncio.sleep(0)  # Kontrolle abgeben
        print("Setze Geschwindigkeit:", rpm)
        if self._sw:
            self._sw.write(f"SET SPEED {rpm}\r\n".encode("ascii"))
            await self._sw.drain()
        self._speed = rpm
        return rpm

    async def spindle_state(self, direction=None) -> SpindleState | None:
        """
        Hole oder setze den Status der Spindel.
        """
        if direction is None:
            return self._direction

        if self._error:
            raise RuntimeError("Kann Spindelgeschwindigkeit nicht setzen.")

        await asyncio.sleep(0)  # Kontrolle abgeben
        print("Setze Drehrichtung:", direction)
        if self._sw:
            self._sw.write(f"SET DIRECTION {direction}\r\n".encode("ascii"))
            await self._sw.drain()
        self._direction = direction
        return direction

    async def do_command(self, command: str) -> None:
        await asyncio.sleep(0)  # Kontrolle abgeben
        print("Befehl:", command)
        if self._sw:
            self._sw.write(f"{command}\r\n".encode("ascii"))
            await self._sw.drain()
        await asyncio.sleep(uniform(0.5, 5))  # Verzögerung simulieren

    def set_error(self):
        self._error = True


async def run_programm(cnc: CNC, programm: list[str]) -> None:
    for line in programm:
        await asyncio.sleep(0)  # Kontrolle abgeben
        if line == "MOVE Z DOWN":
            cnc.set_error()
            raise TimeoutError("CNC antwortet nicht mehr.")
        await cnc.do_command(line)


async def main():
    async with CNC() as cnc:
        print("Starte Programm ...")
        try:
            await run_programm(cnc, PROG)
        except* TimeoutError:
            print("CNC Antwortet nicht.")

    print("Ende")


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except* RuntimeError:
        print("Laufzeitfehler")

PS: https://www.youtube.com/watch?v=02CLD-42VdI
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
carbonkid
User
Beiträge: 12
Registriert: Mittwoch 8. März 2023, 18:48

Dennis89 hat geschrieben: Mittwoch 8. März 2023, 23:09 Hallo,

ich glaube in dem Fall würde ich anders vorgehen. Ein 'sleep' legt das Programm ja für die Zeit schlafen. Dann kann da auch kein Countdown runterlaufen außer du startest einen zweiten Thread in dem der Countdown läuft.
Ich würde eine Schleife schreiben, dann beim Start der Spindel würde ich mir die Zeit merken (siehe time.monotonic) und dann in der Schleife regelmäßig die Zeitdifferenz zwischen Startzeit und der aktuellen Zeit messen. Wenn die Differenz die 200 Sekunden überschritten hat, dann kommt die nächste Drehzahl. Gleichzeitig könntest du die Differenz zur Countdownausgabe nutzen.

Vielleicht in diese Richtung:

Code: Alles auswählen

from time import monotonic, sleep

SPEED_TO_TIME = {400: 3, 600: 5}

def main():
    for speed, waiting_time in SPEED_TO_TIME.items():
        print(f'Drehzahl: {speed}')
        timestamp = monotonic()
        while True:
            time_delta = monotonic() - timestamp
            if time_delta >= waiting_time:
                break
            print(f'Noch {waiting_time- time_delta:.1f} Sekunden mit Drehzahl {speed}')
            sleep(0.1)


if __name__ == '__main__':
    main()
Grüße
Dennis
Danke. das wäre auch eine Möglichkeit. Ja das mit dem time.sleep scheint das gleich zu sein wie beim Arduino mit delay.
Mal schauen wie und ob ich das irgendwie umgesetzt bekomme.
Gruß Daniel
Benutzeravatar
carbonkid
User
Beiträge: 12
Registriert: Mittwoch 8. März 2023, 18:48

@DeaD_Eye
Sorry aber damit kann ich gar nix anfangen.
Es geht mir ausschließlich das ich bevor ich anfange irgendeinen Job zu fräsen die Spindel X Minuten warm laufen lasse.
Das soll wie in meinem Fall mit drei verschiedenen Drehzahlen und Zeiten ausgeführt werden und um zu sehen wie lange der Warmlauf noch ungefähr dauert sollte die Gesamtzeit countdownmäßig runter zählen.
Gruß Daniel
Benutzeravatar
Dennis89
User
Beiträge: 1155
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

wenn du den Countdown der gesamten Zeit willst, dann vllt so:

Code: Alles auswählen

from time import monotonic, sleep

SPEED_TO_TIME = {400: 3, 600: 5}


def main():
    total_time = sum(SPEED_TO_TIME.values())
    for speed, waiting_time in SPEED_TO_TIME.items():
        timestamp = monotonic()
        while True:
            time_delta = monotonic() - timestamp
            if time_delta >= waiting_time:
                total_time -= waiting_time
                break
            print(
                f"Noch {total_time- time_delta:.1f} Sekunden. Aktuelle Drehzahl: {speed} rpm"
            )
            sleep(0.1)


if __name__ == "__main__":
    main()
Das sollte man schön einbauen können.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
DeaD_EyE
User
Beiträge: 1021
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Hab ja geschrieben, dass Anfänger nichts mit anfangen können.
Gedacht war das so, dass das Programm ein Programm zur CNC sendet.

Wenn du am Ball bleibst, kommst du irgendwann bei asyncio an, spätestens wenn du was mit TCP/UDP machen möchtest.

asyncio ist quasi eine unsichtbare Hauptschleife, die auf Ereignisse reagiert (Paket kommt an, serieller Port bekommt Daten oder geplanter Task ist z.B. fertig).
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
__deets__
User
Beiträge: 14540
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ob das Tool des TE asyncio unterstützt, ist völlig unklar. Würde ich nicht einfach von ausgehen.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1021
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

__deets__ hat geschrieben: Freitag 10. März 2023, 10:54 Ob das Tool des TE asyncio unterstützt, ist völlig unklar. Würde ich nicht einfach von ausgehen.
Garantiert nicht. Würde mich wundern, wenn das so wäre.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
carbonkid
User
Beiträge: 12
Registriert: Mittwoch 8. März 2023, 18:48

@DeaD_EyE
Was hat dein ganzer Beitrag hier eigentlich für einen Sinn?
Gruß Daniel
Benutzeravatar
DeaD_EyE
User
Beiträge: 1021
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Momentan gar keinen, in Zukunft aber schon. Wer Asyncio meidet, wird zukünftig nur die Hälfte mit Python machen können.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Antworten