Worum geht es?

Mit der Lösung, die ich hier beschreibe, kannst Du Kommandos und Skripte vom Rechner aus direkt auf Deinem physischen Agon Light anstoßen. Besonders praktisch, wenn Du in der Entwicklung ständig Programme übertragen und starten willst. Das beschleunigt den Workflow spürbar.

Agon Light

Der Agon Light (bzw. Agon Light 2 oder Console 8) ist ein moderner Retro-Computer auf Basis einer Zilog eZ80 CPU. Um nicht von alten Hardwarekomponenten abhängig zu sein, übernimmt ein ESP32-Mikrocontroller Funktionen wie VGA, Tastatur und Sound.

Datenübertragung mit SD-Karte und Hexload

Programme und Daten liegen auf einer SD-Karte. Um etwas auf den Agon zu bekommen, müsstest Du eigentlich die SD-Karte in Deinen Rechner stecken, die Dateien drauf kopieren und die Karte zurück in den Agon packen.

Das ist umständlich, deshalb gibt es Hexload. Über USB zum ESP32 oder per UART (serielle Schnittstelle) schickst Du Daten direkt vom Rechner an den Agon und kannst sie dort auch gleich ausführen. Dazu kommt Hexload auf den Agon und ein Python-Skript auf den Rechner. Spart eine Menge Zeit, Du entwickelst lokal und überträgst die Programme per Kommando.

Hexload nutzt Du so, wenn Du über vdp (ESP32) überträgst:

hexload vdp filename

Und auf dem Rechner:

python send.py filename /dev/devicename 115200

Falls Du Übertragungsprobleme hast, eventuell DEFAULT_LINE_WAITTIME im Python-Skript anpassen.

Ich arbeite am Mac. Vieles sollte auf Linux vergleichbar sein. Unter Windows passt Du entsprechend an (etwa COM3 statt /dev/...). Wenn der Aufruf über python klemmt (bei mir auf dem Mac mit Homebrew-Python3), versuch es mit python3.

Automation und Fernsteuerung mit agon_automate.py

Mit meinem Skript agon_automate.py schickst Du Kommandos und ASCII-Texte direkt an den Agon, ohne sie über die Tastatur einzutippen. Es nutzt die gleiche Verbindung wie Hexload über vdp und funktioniert wie eine Fernsteuerung.

Voraussetzungen für agon_automate.py

Du brauchst python3, eine USB-Verbindung vom Rechner an den ESP32-USB-Port des Agon und PySerial.

pip install pyserial

Auf dem Agon Light muss SET CONSOLE 1 gesetzt sein. Ich habe das in meiner autoexec.txt hinterlegt, damit es immer aktiv ist:

SET KEYBOARD 1
SET CONSOLE 1
LOAD bbcbasic.bin
RUN

Kurzer Tipp: SET CONSOLE 1 einmal von Hand starten oder den Agon neu starten, damit das Kommando greift. Ohne das landen die Befehle meines Skripts nicht auf dem Agon.

Auf dem Rechner (Mac, Linux oder Windows): Das Skript kannst Du ablegen, wo Du willst. Ich habe agon_automate.py unter ~/bin liegen und .zshrc bzw. .bashrc mit export PATH="$HOME/bin:$PATH" erweitert, sodass alles in bin automatisch gefunden wird.

agon_automate.py

# Title:       agon_automate.py
# Author:      Tom Schimana
# Version:     0.9
# Created:     01/21/2024
# Last update: 01/21/2024

# Description:
# This script automates the process of sending commands and files to devices over a serial connection.
# Designed for versatility, it's suitable for various applications such as transmitting programming code,
# sending text files, or executing general commands.
# Requires 'SET CONSOLE 1' to be enabled on Agon and a USB-to-UART connection to Agon ESP32.
# Optional: hexload (https://github.com/envenomator/agon-hexload) for file transfers.

# Functionalities:
# Direct:
#    Sends individual commands or text directly to the device.
#    Usage: agon_automate.py direct "your text you want to send" [optional: filename]
#    An optional filename can be included and utilized with {FILENAME} as a placeholder.
# Script:
#    Executes a series of predefined commands from a script file.
#    Usage: agon_automate.py script "SCRIPT_PATH" [optional: filename]

# Special Commands and Placeholders:
# - {COMMAND}: Executes an external shell command.
#   Example: {COMMAND python3 ~/bin/send.py {FILENAME} {DEVICE}}
# - {WAIT}: Pauses execution for a specified number of seconds.
#   Example: {WAIT 2}
# - {FILENAME}: Replaced with the optional filename argument provided in the command.
# - {DEVICE}: Placeholder for the device address, replaced with the configured DEVICE variable.
# - \n: In the 'direct' method, this is replaced with a carriage return ('\r')
#   to signify a new line on the device.

# Configuration Variables
DEVICE = "/dev/cu.usbserial-02B1CCDF"  # Serial device address for USB to ESP32 connection
BAUD = 115200                           # Baud rate for serial communication
CHAR_DELAY = 0.03                       # Delay between each character sent via serial
DEBUG = 1                               # Set to 1 to enable debug output, 0 to disable
COMMAND_DELAY = 0.5                     # Delay after sending each command line or after a carriage return

import subprocess
import os
import time
import sys

try:
    import serial
except ImportError:
    print("Missing module: pyserial. Please install it using 'pip install pyserial'.")
    sys.exit(1)

def debug_print(message):
    if DEBUG:
        print(message)

def send_command(command, file_name=None, interpret_escapes=False):
    command = command.replace("{FILENAME}", file_name if file_name else "")
    command = command.replace("{DEVICE}", DEVICE)
    if interpret_escapes:
        command = command.replace('\\n', '\r')

    debug_print(f"Sending command: {command}")
    parts = command.split("{")
    for part in parts:
        if "WAIT" in part:
            wait_time = float(part.split()[1].strip("}"))
            debug_print(f"Waiting for {wait_time} seconds")
            time.sleep(wait_time)
        elif "COMMAND" in part:
            execute_external_command(part, file_name)
        else:
            send_partial_command(part + '\r')

def execute_external_command(part, file_name):
    cmd = part.split('COMMAND')[1].strip("}").strip()
    cmd = cmd.replace("{FILENAME}", file_name if file_name else "").replace("{DEVICE}", DEVICE)
    cmd_parts = [os.path.expanduser(p) for p in cmd.split()]
    try:
        subprocess.run(cmd_parts, check=True)
        time.sleep(COMMAND_DELAY)
    except (subprocess.CalledProcessError, FileNotFoundError) as e:
        debug_print(f"Error executing command: {e}")

def send_partial_command(part):
    with serial.Serial(DEVICE, BAUD, timeout=1) as ser:
        for char in part:
            ser.write(char.encode())
            time.sleep(CHAR_DELAY)
        time.sleep(COMMAND_DELAY)

def process_command_line(command_line, file_name=None):
    send_command(command_line, file_name, interpret_escapes=True)

def process_script_file(script_path, file_name=None):
    with open(script_path, 'r') as file:
        for line in file:
            line = line.replace('\n', '\r\n')
            process_command_line(line.strip(), file_name)

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: {} [direct|script] argument [filename]".format(sys.argv[0]))
        sys.exit(1)

    method, argument = sys.argv[1], sys.argv[2]
    file_name = sys.argv[3] if len(sys.argv) > 3 else None

    if method == "direct":
        send_command(argument, file_name, interpret_escapes=True)
    elif method == "script":
        process_script_file(argument, file_name)
    else:
        print("Invalid method. Use 'direct' or 'script'.")
        sys.exit(1)

Konfiguration

Ein paar Werte im Python-Code musst Du an Deine Umgebung anpassen.

Zwingend:

  • DEVICE: Pfad zum Port für die Kommunikation mit dem Agon Light. Unter Windows etwa COM3, unter Linux oder Mac /dev/tty.... Ich gehe davon aus, dass Du Hexload schon nutzt, also ist es dasselbe Device wie dort für die vdp-Verbindung.

Optional:

  • BAUD: Baudrate. Würde ich so lassen.
  • CHAR_DELAY: Wartezeit zwischen einzelnen Zeichen. Nur erhöhen, wenn Zeichen verloren gehen.
  • COMMAND_DELAY: Wartezeit nach jeder Zeile. Ein Kommando braucht Rechenzeit, sonst gehen die nächsten Zeichen unter. Würde ich so lassen und bei Bedarf mit WAIT (siehe unten) arbeiten.
  • DEBUG: Steht er auf 1, siehst Du im Terminal was an den Agon geht.

agon_automate.py nutzen

Direct Mode

Für einzelne, kurze Kommandos an den Agon Light. Praktisch für schnelle, einmalige Eingaben. Zum Beispiel, wenn Du in BASIC nur eine Zeile ändern willst:

python agon_automate.py direct "10 PRINT \"HELLO WORLD\""

Der Haken am Direct Mode ist die Kommandozeile selbst. Deshalb die zusätzlichen Sonderzeichen im Beispiel, sonst interpretiert die Shell das falsch.

Meistens nimmst Du Direct Mode für einfache Dinge wie einen Programmstart. Hexload auf der Agon-Seite starten:

python agon_automate.py direct "hexload vdp helloword.bas"

Du kannst auch mehrere Zeilen mit \n trennen:

python agon_automate.py direct "10 PRINT \"HELLO WORLD\"\n20 GOTO 10"

Entsprechend ist der Direct Mode begrenzt, weil die Kommandozeile bei Sonderzeichen und Länge irgendwann streikt.

Script Mode

Deshalb gibt es den Script Mode für längere Abläufe mit mehreren Kommandos. Ideal für wiederkehrende Aufgaben oder längere Automationen.

Mein Standard-Automationsskript agonbasic.script beendet BBC Basic auf dem Agon Light, startet Hexload, ruft send.py lokal auf, lädt nach der Übertragung BASIC erneut und startet das übertragene BASIC-Programm. Das Skript setzt voraus, dass Du in BBC Basic bist. Und Du brauchst Hexload. Ich beende BBC Basic deswegen, weil Hexload bei mir aus BASIC heraus gerne Probleme macht.

agonbasic.script (Name ist natürlich frei):

*BYE
hexload vdp {FILENAME}
{COMMAND python ~/bin/send.py {FILENAME} {DEVICE}}{WAIT 2}
load /bbcbasic.bin
run
LOAD "{FILENAME}"
{WAIT 2}
RUN

Aufruf bei mir:

python agon_automate.py script agonbasic.script helloagon.bas

Erster Parameter ist script für den Modus, dann der Name des Skripts mit den Kommandos und am Ende der Dateiname für {FILENAME}.

Die Kommandos in geschweiften Klammern sind die eigentliche Automatisierungs-Logik:

  • {COMMAND}: Startet ein lokales Programm auf dem Rechner. Du kannst dort weitere Parameter mitgeben. Im Skript oben rufen wir send.py auf und übergeben Dateiname, Device und warten 2 Sekunden.
  • {WAIT}: Praktisch, wenn ein Befehl auf dem Agon länger braucht, etwa beim Laden eines großen Programms. Ohne Parameter ist es eine Sekunde (das ist aber eh der Standard nach jeder Zeile), mit {WAIT 2} entsprechend zwei Sekunden.
  • {FILENAME}: Hier kommt der Dateiname rein, also beim Aufruf der letzte Parameter nach script (in unserem Beispiel helloagon.bas).
  • {DEVICE}: Platzhalter für die DEVICE-Variable aus dem Python-Skript.

So bekommst Du schnell kleine Automationen auf den Agon. Natürlich nicht auf BASIC beschränkt. Einzige Einschränkung: Du kannst nur übertragen, was sich auch per Tastatur eingeben lässt. Als reines Übertragungstool taugt es nicht, es gibt keine Fehlerprüfung. Dafür ist Hexload gebaut. Die Kombination aus meinem Skript und Hexload macht aber schon ziemlich viel möglich.

Und mit neovim oder VIM?

Wie Du neovim oder vim als kleine IDE für den Agon Light nutzt, habe ich in einem eigenen Beitrag beschrieben: Mini-IDE für den Agon Light mit Neovim oder Vim.

Damit schreibst und änderst Du BBC-Basic-Programme direkt in vim oder neovim, überträgst sie mit einem Befehl, startest sie auf dem Agon und kannst sogar einzelne Zeilen gezielt nachschieben.