Configuration

  1. hamctl.toml
    1. Path tokens
  2. Process control
  3. The manifest schema
    1. Config formats
    2. Host device pickers
    3. Managed files

hamctl.toml

The server’s own settings live in the home’s hamctl.toml (created by hamctl init):

bind = "0.0.0.0:8080"          # all interfaces; auth protects it
services_dir = "/opt/hamctl/services"   # absolute, so it runs from any cwd
password_hash = "$argon2id$..."         # set via `hamctl set-password`
use_systemd = false            # false = built-in supervisor; true = systemd
config_root = ""               # substituted for {root} in manifest paths
log_root = ""                  # substituted for {log_root} in monitor paths

The file holds the password hash, so hamctl init creates it chmod 600.

Path tokens

Manifests use tokens so one manifest set serves a device and development unchanged:

  • {root}config_root. On a Pi-Star device set config_root = "/etc" so a manifest path like {root}/mmdvmhost resolves to /etc/mmdvmhost; in development point it at a directory of sample configs.
  • {log_root}log_root, plus {date} (UTC YYYY-MM-DD) — used by monitor log paths, e.g. {log_root}/MMDVM-{date}.log. Set log_root to where the daemon logs (on Pi-Star, /var/log/pi-star).
  • {arch} → the host architecture token (amd64/aarch64/…).

Process control

Each service is run either by the built-in supervisor (hamctl spawns it, captures logs to a ring buffer, and auto-restarts on crash) or by systemd (the unit is <service-id>.service) — selected globally by use_systemd. A manifest with no [run] block is controlled purely via systemd.

cleanup lists files the supervisor deletes immediately before every (re)launch — typically a PID/lock file. Some daemons (e.g. the AFRN client) write a PID file and then refuse to start if it already exists; on a crash they leave it behind, so with auto_restart enabled the next launch sees the stale file, aborts with “already running?”, and the daemon wedges in a restart loop. Listing the PID file here breaks that loop. Paths accept the {config}/{root}/{arch} tokens; relative paths resolve against the working directory, absolute paths are used as-is.

The manifest schema

A service is a manifest.toml. The UI renders forms from it; no recompile needed.

[service]
id = "myservice"               # also the systemd unit basename
name = "My Service"
category = "service"           # "os" lists it under "OS Configuration" instead

[run]                          # omit entirely for systemd-only services
binary_glob = "bin/myservice-*"   # {arch} expands to amd64/aarch64/…
args = ["--config", "{config}"]
auto_restart = true
cleanup = ["/run/myservice.pid"]  # files removed before each (re)launch

[[config]]
id = "main"
label = "myservice.conf"
path = "{root}/myservice.conf"
format = "ini"                 # ini | raw | records
  [[config.sections]]
  name = "General"
  # ini_section = ""           # edit a section-less / shared INI section
    [[config.sections.fields]]
    key = "Listen"
    label = "Listen address"
    # type: text | password | number | bool | enum | textarea | path | file

Config formats

  • ini — section/key, edited field-by-field; comments and layout are preserved byte-for-byte and only changed values are rewritten. Bool fields map to literal true_value/false_value (AFRN uses Yes/No; Pi-Star uses 1/0). Set several sections to ini_section = "" to present a flat key=value file as readable groups.
  • records — a delimited table (pipe or whitespace) edited as a responsive, paginated grid; the file’s comment header is kept.
  • raw — opaque text edited in a textarea.

A file-typed field backed by a [[files]] set gets a suggestion list of files that exist in that managed directory; uploads are size-capped and filenames sanitized.

Host device pickers

A field that names a host device can offer the machine’s real devices as suggestions, while still letting you type a value (handy when the device is unplugged or you’re configuring remotely):

[[config.sections.fields]]
key = "OutDevice"
devices = "audio_out"      # serial | audio_in | audio_out
device_prefix = "ALSA:"    # prepended to each suggestion (AFRN audio uses ALSA:)

hamctl enumerates serial ports (/dev/serial/by-id/*, ttyUSB*/ttyACM*/…) and ALSA audio devices (from /proc/asound) and serves them at GET /devices/<kind>; the field becomes an editable dropdown. AFRN’s InDevice/OutDevice and MMDVMHost’s modem Port use this.

Managed files

[[files]]                      # browse / upload / replace / delete via the UI
id = "sounds"
label = "Sounds"
path = "{root}/sounds"
accept = [".wav"]              # allowed extensions; empty = any

See Services for real examples (AFRN, the Pi-Star stack), and Monitoring for the [[monitors]] blocks.