en7523ctl

en7523ctl is a Go utility for maintaining the Airoha EN7523 kernel patch series and running a multi-port UART log collector with an SSH console server.

The project has two independent areas:

  • Kernel/OpenWrt migration tools: rebuild, synchronize, and export EN7523 branches as numbered OpenWrt patches.
  • Serial daemon: reconnect to USB serial adapters, keep persistent UART logs, split logs at boot boundaries, identify the board from U-Boot, and expose logs or writable consoles over SSH.

The serial daemon does not require Git. Git is only required by the migration commands.

Features

Kernel and OpenWrt migration

  • Uses the native git executable for clone, fetch, switch, reset, apply, commit, format-patch, and push operations.
  • Applies mailboxes with git am -3 first.
  • Falls back to git apply -p1 --3way and creates a fallback commit when necessary.
  • Exports branches named airoha_en7523_* or airoha_an7523_* into the OpenWrt patch directory.
  • Preserves native git format-patch output, including authorship, trailers, binary diffs, and commit subjects.
  • Supports the stable and testing kernel versions selected by the Airoha OpenWrt target.
  • Handles patch installation across different filesystems. If a direct rename from /tmp to the OpenWrt tree fails with EXDEV, the exporter copies into a temporary file in the destination directory, syncs it, and performs a local atomic rename.

UART daemon and SSH server

  • Supports multiple serial ports.
  • Accepts exact device paths or glob patterns such as /dev/serial/by-id/usb-*_USB_Serial-*.
  • Automatically reconnects after unplug/replug or serial errors.
  • Supports configurable baud rate, data bits, parity, stop bits, timeout, and read-only mode.
  • Maintains an in-memory ring buffer for each UART.
  • Maintains one per-boot log and one global append-only log for each UART.
  • Delays creation of a new log until at least 10 bytes are received or a newline is observed.
  • Provides public-key-only SSH authentication.
  • Supports interactive PTY sessions, CR/LF handling, echo, Backspace, Ctrl-C, Ctrl-D, and Ctrl-U.
  • Allows multiple read-only observers while limiting each UART to one writable SSH console.

Requirements

  • Go 1.23 or newer.
  • Git in PATH for migrate, export, export-new, and sync-kernels.
  • Permission to open the configured TTY devices.
  • systemd for the optional user service.

On Debian and Ubuntu, USB serial devices are commonly assigned to dialout. On Arch Linux they may be assigned to uucp. Check the actual device group with:

stat -c '%A %U %G %n' /dev/ttyUSB0

Add the current account to the required group, then log out and back in:

sudo usermod -aG dialout "$USER"

Build

go mod download
go test ./...
go build -trimpath -o en7523ctl ./cmd/en7523ctl

Install the binary for the current user:

install -Dm0755 en7523ctl "$HOME/.local/bin/en7523ctl"

Ensure ~/.local/bin is in PATH when invoking the binary manually.

Commands

en7523ctl [--config file.json] <command>

migrate | main      migrate kernel/OpenWrt patch branches
export              export main kernel branches to OpenWrt patches
export-new          export backport kernel branches to OpenWrt patches
sync-kernels        synchronize local EN7523 branches from origin
serve               run the serial collector and SSH server
keygen [path]       generate an Ed25519 SSH host key
help                show command help

Examples:

en7523ctl migrate
en7523ctl export
en7523ctl export-new
en7523ctl sync-kernels
en7523ctl serve

Configuration

The following configuration is suitable for the user service included in this repository. Its paths are relative to the user's home directory because the service sets WorkingDirectory=%h.

Create the directories:

mkdir -p "$HOME/.config/en7523ctl"
mkdir -p "$HOME/.local/state/en7523ctl"

Create ~/.config/en7523ctl/config.json:

{
  "migration": {
    "root_dir": "/mnt/Data/airoha_en7523",
    "kernel_repository": "https://sirherobrine23.com.br/airoha_en7523/kernel.git",
    "kernel_repository_upstream": "https://github.com/gregkh/linux.git",
    "main_kernel": "linux",
    "backport_kernel": "linux_backport",
    "openwrt": "openwrt",
    "patcher_dir": "tmp_patcher",
    "branch_prefix": "airoha_en7523",
    "patcher_source": 0,
    "ignore_generic": false,
    "push": false,
    "rewrite_history": false,
    "manual_shell": true
  },
  "daemon": {
    "listen": "0.0.0.0:2222",
    "host_key": ".config/en7523ctl/ssh_host_ed25519_key",
    "authorized_keys": ".ssh/authorized_keys",
    "user": "serial",
    "log_dir": ".local/state/en7523ctl",
    "ring_bytes": 1048576,
    "reconnect_delay": "3s",
    "ports": [
      {
        "name": "en7523",
        "device": "/dev/serial/by-id/usb-*_USB_Serial-*",
        "baud": 115200,
        "data_bits": 8,
        "parity": "none",
        "stop_bits": 1,
        "read_only": false,
        "reboot_split": true,
        "log_file": "{hh}-{mm}_{dd}-{MM}-{yyyy}_en7523_{device}.log",
        "global_log_file": "{name}_global.log",
        "read_timeout": "500ms"
      }
    ]
  }
}

When running the daemon manually with these relative paths, start it from the home directory:

cd "$HOME"
"$HOME/.local/bin/en7523ctl" \
  --config "$HOME/.config/en7523ctl/config.json" serve

Migration configuration

Field Description
root_dir Parent directory containing the kernel and OpenWrt trees.
main_kernel Main EN7523 kernel repository directory.
backport_kernel Kernel tree used for migration to the selected OpenWrt kernel version.
openwrt OpenWrt source tree directory.
patcher_dir Temporary mailbox directory used during migration.
branch_prefix Base branch prefix, normally airoha_en7523.
patcher_source 0 imports commits from kernel branches; 1 rebuilds branches from OpenWrt patches.
ignore_generic Skip generic and target OpenWrt patch application.
push Force-push migrated branches and tags, synchronize, then export patches.
rewrite_history Run the legacy git filter-branch step. Keep disabled unless explicitly required.
manual_shell Open $SHELL with FILE_PATCH set after automatic patch application fails.

Serial port configuration

Field Description
name Stable logical name used by SSH commands and log templates.
device Exact TTY path or glob pattern. Prefer /dev/serial/by-id/*.
baud Serial baud rate. Defaults to 115200.
data_bits Number of data bits. Defaults to 8.
parity none, odd, or even.
stop_bits One or two stop bits.
read_only Disable writable SSH console access for this UART.
reboot_split Split the per-boot log when an EN7523 boot marker is detected.
log_file Template for the current per-boot log.
global_log_file Append-only file containing every received UART byte.
read_timeout Serial read timeout, for example 500ms.

Log file templates

log_file and global_log_file support these placeholders:

Placeholder Value
{yyyy} Four-digit year.
{yy} Two-digit year.
{MM} Month, 01 through 12.
{dd} Day of month.
{hh} Hour in 24-hour format.
{mm} Minute.
{ss} Second.
{unix} Unix timestamp.
{name} Logical port name.
{device} Physical TTY name initially; U-Boot model for finalized per-boot logs.
{baud} Configured baud rate.

All substituted tokens are sanitized before becoming part of a filename. Slashes, spaces, parentheses, and other unsafe characters are replaced with underscores.

Example template:

"log_file": "{hh}-{mm}_{dd}-{MM}-{yyyy}_en7523_{device}.log"

After detecting:

U-Boot 2026.01-OpenWrt-r35153-918c198643f
Model: MikroTik E60iUGS (hEX S 2025)

it produces a filename similar to:

15-06_29-06-2026_en7523_MikroTik_E60iUGS__hEX_S_2025_.log

Include {ss} or {unix} when multiple boots may occur within the same minute.

Per-boot log splitting

When reboot_split is enabled, the per-boot writer detects a new EN7523 boot at either marker:

Secure key does not exist
EN7523DRAMC

The previous boot log ends immediately before the marker. Bytes starting at the marker are buffered until the daemon observes both:

U-Boot 20...
Model: ...

The daemon then:

  1. derives {device} from the Model: value;
  2. opens the final model-based log path;
  3. writes every buffered byte starting at the boot marker;
  4. continues appending subsequent UART output to that file.

This preserves the DRAM initialization output and all lines printed before the U-Boot model is known.

If the daemon starts after the boot marker has already passed, it initially writes to a physical-device-based path. When the U-Boot banner and model are detected, it relocates or merges the existing content into the model-based path.

If the process stops before a model is detected, the pending boot is saved under the physical TTY identity rather than discarded.

Global UART log

Every port has an independent global log, normally:

"global_log_file": "{name}_global.log"

The global log:

  • contains all UART bytes across every boot;
  • is not split on reboot markers;
  • is not renamed after U-Boot model detection;
  • is opened in append mode;
  • survives TTY reconnects and daemon restarts.

The global log is written before the per-boot log processing, providing a continuous raw history even if model detection or per-boot rotation fails.

Lazy log creation

A newly selected log path is not created immediately. The daemon first buffers incoming bytes and materializes the file only when:

  • at least 10 bytes have been received; or
  • a newline byte (\n) has been received.

When materialized, the entire buffered prefix is written first. If the UART disconnects or the daemon stops with fewer than 10 bytes and no newline, that fragment is treated as noise and no new file is created.

Existing global logs are reopened in append mode immediately, so a short legitimate fragment is not lost after a daemon restart.

SSH access

Generate an SSH host key from the home directory:

cd "$HOME"
"$HOME/.local/bin/en7523ctl" \
  --config "$HOME/.config/en7523ctl/config.json" keygen

The public keys allowed to connect are read from daemon.authorized_keys. The configuration above uses the account's normal ~/.ssh/authorized_keys file.

Connect with the SSH username configured in daemon.user:

ssh -p 2222 serial@localhost

Interactive commands:

help
list
status
tail <name> [bytes]
log <name> [tail-bytes]
global-log <name> [tail-bytes]
all-log <name> [tail-bytes]
attach <name>
console <name>
exit

Command behavior:

  • list and status show connection state, TTY path, byte count, current per-boot log, global log, and last error.
  • tail reads the in-memory ring buffer.
  • log reads the current per-boot log. Supplying a byte count returns only the end of the file.
  • global-log and all-log read the continuous append-only UART history.
  • attach follows new UART output without allowing writes. Press Ctrl-C to detach.
  • console provides bidirectional UART access. Press Ctrl-] to detach.
  • console is rejected for ports configured with read_only: true.
  • Only one writable console may be active for a given UART, while multiple read-only observers are allowed.

Commands can also be executed without an interactive shell:

ssh -p 2222 serial@localhost status
ssh -p 2222 serial@localhost 'tail en7523 65536'
ssh -p 2222 serial@localhost 'log en7523 1048576'
ssh -p 2222 serial@localhost 'global-log en7523 1048576'

Authentication is public-key-only. Password and anonymous authentication are not supported.

Running as a systemd user service

The included user unit runs the daemon as the current login account. It does not create a dedicated system user and does not require root during normal operation.

Install the unit and documentation:

mkdir -p "$HOME/.config/systemd/user"
mkdir -p "$HOME/.local/share/doc/en7523ctl"

install -m0644 systemd/en7523ctl-user.service \
  "$HOME/.config/systemd/user/en7523ctl.service"
install -m0644 README.md \
  "$HOME/.local/share/doc/en7523ctl/README.md"

systemctl --user daemon-reload
systemctl --user enable --now en7523ctl.service

Inspect status and logs:

systemctl --user status en7523ctl.service
journalctl --user -u en7523ctl.service -f

Restart after changing config.json:

systemctl --user restart en7523ctl.service

To keep the user service running after logout and start it during boot, enable lingering once:

sudo loginctl enable-linger "$USER"

Disable it with:

sudo loginctl disable-linger "$USER"

The account must already have permission to open the serial device. A user service cannot bypass TTY ownership or group permissions.

User service hardening

The supplied unit:

  • runs from the user's home directory;
  • restarts after failures;
  • makes the host filesystem read-only through ProtectSystem=strict;
  • keeps the home directory read-only except for ~/.local/state/en7523ctl;
  • does not use PrivateDevices, because the daemon must access /dev/ttyUSB* and /dev/serial/by-id/*;
  • permits only Unix, IPv4, and IPv6 sockets;
  • prevents privilege escalation.

If your systemd version does not support one of the hardening directives in a user unit, remove only the unsupported directive shown in systemctl --user status and keep the remaining restrictions.

Firewall and exposure

The example listens on 0.0.0.0:2222, which exposes the SSH server on every IPv4 interface. Restrict the firewall to trusted management networks, or bind only to localhost:

"listen": "127.0.0.1:2222"

For remote access through an existing SSH host, bind to localhost and use forwarding:

ssh -L 2222:127.0.0.1:2222 user@uart-host
ssh -p 2222 serial@127.0.0.1

Troubleshooting

Permission denied opening the UART

Check ownership and current group membership:

stat -c '%A %U %G %n' /dev/ttyUSB0
id

After adding the user to dialout or uucp, log out and back in. Restarting only the service does not update the groups of an already running user manager. When necessary, terminate the user manager after logging out or reboot the host.

The SSH port is not listening

systemctl --user status en7523ctl.service
journalctl --user -u en7523ctl.service -n 100 --no-pager
ss -lntp | grep 2222

Verify that the host key exists and the authorized-keys file contains at least one valid public key.

Interactive SSH commands do not submit on Enter

Use the current version of the server. It accepts CR, LF, and CRLF in PTY sessions and performs server-side echo. Older versions that only waited for LF appeared to ignore commands from normal SSH clients.

Use the current exporter. It handles EXDEV by copying into the destination filesystem and then performing an atomic local rename. The temporary staging directory and the OpenWrt source tree no longer need to be on the same mount.

No log file is created

This is expected until the UART sends at least 10 bytes or one newline. Check the in-memory state over SSH:

ssh -p 2222 serial@localhost status
ssh -p 2222 serial@localhost 'tail en7523 4096'

Security notes

  • Keep authorized_keys restricted to trusted administrators.
  • Prefer read_only: true when remote UART writes are unnecessary.
  • Bind the SSH server to localhost or a management interface whenever possible.
  • Protect the generated host private key with mode 0600.
  • The writable console command has the same practical impact as physical access to the UART.
S
Description
Go tools do manager uart logs and patchers
Readme
133 KiB
Languages
Go 89.8%
Shell 9.9%
Makefile 0.3%