What’s it all about?
With the setup described here, you kick off commands and scripts on your physical Agon Light straight from your computer. Especially handy during development when you constantly need to transfer and run programs. It speeds up the whole workflow.
Agon Light
The Agon Light (or Agon Light 2, or Console 8) is a modern retro-computer based on a Zilog eZ80 CPU. To avoid depending on old hardware, an ESP32 microcontroller handles things like VGA, keyboard and sound.
Data Transfer with SD Card and Hexload
Programs and data live on an SD card. To push something onto the Agon you would normally pull the SD card, copy the files onto it on your computer, and put it back into the Agon.
That is tedious, which is why Hexload exists. Over USB to the ESP32 or via UART (serial port) you transfer data straight from your computer to the Agon and can even run it there immediately. You install Hexload on the Agon and a Python script on your computer. Saves a lot of time, and lets you develop locally and ship the programs over with a single command.
To use Hexload when you transfer via vdp (ESP32):
hexload vdp filename
And on your computer:
python send.py filename /dev/devicename 115200
If you run into transfer issues, try adjusting DEFAULT_LINE_WAITTIME in the Python script.
I work on a Mac. Most of it should be similar on Linux. On Windows you’ll need the usual adjustments (for example COM3 instead of /dev/...). If calling python fails (on a Mac with Homebrew Python3 for example), try python3.
Automation and Remote Control with agon_automate.py
With my script agon_automate.py you send commands and ASCII text directly to the Agon, without typing them in on its keyboard. It uses the same connection as Hexload over vdp and works like a remote control.
Requirements for agon_automate.py
You need python3, a USB connection from your computer to the Agon’s ESP32 USB port, and PySerial.
pip install pyserial
On the Agon Light, SET CONSOLE 1 has to be active. I have that in my autoexec.txt so it’s always on:
SET KEYBOARD 1
SET CONSOLE 1
LOAD bbcbasic.bin
RUN
Quick tip: run SET CONSOLE 1 manually once or reboot the Agon so the command kicks in. Without it, the characters from my script never reach the Agon.
On your computer (Mac, Linux or Windows), put the script wherever you like. I keep agon_automate.py in ~/bin and added export PATH="$HOME/bin:$PATH" to .zshrc or .bashrc, so everything in bin is picked up automatically.
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)
Configuration
A few values in the Python code need to match your environment.
Required:
DEVICE: path to the port used to talk to the Agon Light. On Windows for exampleCOM3, on Linux or Mac/dev/tty.... I assume you already use Hexload, so this is the same device you use there for the vdp connection.
Optional:
BAUD: baud rate. I’d leave it.CHAR_DELAY: delay between single characters. Only bump it up if characters get dropped.COMMAND_DELAY: delay after each line. A command needs time to run, otherwise the next characters get eaten. I’d leave it and useWAIT(see below) when needed.DEBUG: with1you see on your terminal what’s being sent to the Agon.
Using agon_automate.py
Direct Mode
For quick, single commands you want to send to the Agon Light. Handy for one-off edits. For example, when you’re in Basic and just want to change one line of code:
python agon_automate.py direct "10 PRINT \"HELLO WORLD\""
The catch with Direct Mode is the shell itself. Hence the extra escaping in the example, otherwise the shell mangles the characters.
Most of the time you’ll use Direct Mode for simple things like starting a program. Kick off Hexload on the Agon side:
python agon_automate.py direct "hexload vdp helloword.bas"
You can also send more than one line separated by \n:
python agon_automate.py direct "10 PRINT \"HELLO WORLD\"\n20 GOTO 10"
Direct Mode is limited because the shell eventually gives up on special characters or long strings.
Script Mode
That’s where Script Mode comes in, for longer sequences with several commands. Good for recurring tasks or longer automations.
My standard automation script agonbasic.script leaves BBC Basic on the Agon Light, starts Hexload, runs send.py locally, reloads Basic after the transfer and runs the transferred BASIC program. The script assumes you’re in BBC Basic. And you need Hexload. I leave BBC Basic first because Hexload tends to cause trouble when called from inside Basic, at least in my setup.
agonbasic.script (the name is up to you):
*BYE
hexload vdp {FILENAME}
{COMMAND python ~/bin/send.py {FILENAME} {DEVICE}}{WAIT 2}
load /bbcbasic.bin
run
LOAD "{FILENAME}"
{WAIT 2}
RUN
I call it like this:
python agon_automate.py script agonbasic.script helloagon.bas
The first parameter is script for the mode, then the name of the script holding the commands, and the last one is the filename used for {FILENAME}.
The commands in curly braces are the actual automation magic:
{COMMAND}: runs a local program on your computer. You can pass extra parameters. In the script above we callsend.pywith the filename, the device and then wait 2 seconds.{WAIT}: useful when something on the Agon takes a moment, for example loading a bigger program. Without a parameter it’s one second (which is the default delay between lines anyway), with{WAIT 2}it’s two seconds.{FILENAME}: the filename passed in, the last argument afterscriptin the call above (so in the examplehelloagon.bas).{DEVICE}: placeholder for theDEVICEvariable from the Python script.
That’s how you push small automations to the Agon. Not limited to BASIC of course. The only real limit: you can only transmit things you could also type on the keyboard. As a pure transfer tool it’s not the right choice, there is no error checking. That’s what Hexload is for. But the combination of my script and Hexload covers a lot of ground.
What about neovim or VIM?
How to use neovim or vim as a small IDE for the Agon Light is covered in a separate post: A Mini-IDE for the Agon Light using Neovim or Vim.
With that you write or edit BBC Basic programs in vim or neovim, transfer them with one command, start them on the Agon and can even push individual lines across.