BetterOne Console Server User Manual
Updated: October 13, 2026
For customers deploying and using this product. Chinese edition. Development, packaging, and licensing protocol details are maintained in the internal documentation.
Open System → Documentation & legal information for this manual and legal documents. The login page also links to the user agreement and privacy policy. Reading pages open in a new tab without requiring login or Internet access and offer Chinese / English switching.
Installation and first login
Deploy the release for your Windows, Linux, or Raspberry Pi host, start the executable, and verify serial drivers and access permissions. On Linux, grant the service account serial access. On Windows, check that the COM port appears in Device Manager.
Release targets are Windows x64 (amd64) and Linux amd64, arm64, and armv7 (32-bit ARM, GOARM=7). Choose a Raspberry Pi release by OS architecture, not board model alone. There is no Windows x86 installer. Only IPv4 is currently supported.
Web/API defaults to 0.0.0.0:80; browse to http://<server-IPv4>/. The actual address follows http_host, http_port, and startup logs. An occupied port or insufficient permission can prevent startup.
A new database creates only an uninitialized admin account, with no default password and no engineer account. Open the Web page and set and confirm a password of at least eight characters that is neither letters-only nor digits-only. Initialization succeeds once; existing accounts and passwords are preserved. Before initialization, empty-password access, Web/SSH login, and administrator operations are unavailable. Administrators create other accounts.
Administrators manage accounts, API keys, licensing, and complete configuration; engineers can perform daily serial/protocol configuration.
For automated initialization, query GET /api/auth/initialization for required, then submit password and confirm_password to POST /api/auth/initialize. Matching compliant passwords return 204; an already initialized database returns 409 without resetting a password. Log in normally afterward. --init-config generates configuration and keys only; it does not set the administrator password.
Installation options
- Windows service: run
betterone-cs_<version>_Windows_x86_64_setup.exeas administrator. WinSW registers and startsBetterOneConsoleServer. The executable is in Program Files; configuration, data, and logs are in ProgramData. - Linux deb/rpm: install the package matching your architecture. It uses the
betterone-console-serveraccount and enables/restarts systemd. A normal user unable to bind port 80 can use port 8080. - Portable ZIP/tar.gz: extract, run
betterone-cs --init-config, inspect settings, then runbetterone-cs. Windows uses.exe; Linux commands in the current directory need./. Default mode isportable, withconfig/anddata/beside the executable.
Installer upgrades restart the service and disconnect serial sessions. Installers do not move old data from other locations automatically. Stop and back up first. Uninstalling preserves configuration, database, license, keys, installation identity, and logs; it is not a factory reset.
Service management and logs
Windows administrator PowerShell:
Get-Service BetterOneConsoleServer
Restart-Service BetterOneConsoleServer
Stop-Service BetterOneConsoleServer
Get-Content "$env:ProgramData\BetterOne\ConsoleServer\logs\betterone-console-service.err.log" -Tail 50
Linux:
sudo systemctl status betterone-console-server
sudo systemctl restart betterone-console-server
sudo systemctl stop betterone-console-server
journalctl -u betterone-console-server.service -n 50
Startup configuration file: and data directory: messages show actual paths. Check them and restart status when an edited setting appears ineffective.
What initialization does
Four operations occur at different times and preserve existing content:
| When | Action |
|---|---|
Configuration initialization: --init-config or the start of normal startup |
Create directories and test data-directory writability with a temporary file. Generate settings.toml (0600), defaults for Web address, SSH key, license.txt, license server, and runtime.db; generate secret_key, api_key_pepper, and device_uuid, also saving UUID to a machine-level file. --init-config exits here, without database creation or listeners. deb/rpm installation uses this command. |
| First normal startup | Open/create SQLite runtime.db in WAL mode, create tables and defaults for 48 logical consoles, and create passwordless pending admin. No engineer/default password is created. Incomplete default console configuration may be rewritten; existing complete data is preserved. |
| First SSH listener startup | Generate missing Ed25519 ssh_host_key with permissions 0600; do not replace an existing key. |
| First Web password setup | If the database has only passwordless admin, show setup, require two matching compliant passwords, and allow success only once. Later calls return 409. Return to login without automatically logging in. |
Default console configuration:
| Setting | Default |
|---|---|
| Serial | 115200, 8 data bits, 1 stop bit, no parity, no flow control, no binding |
| Connections | 1 |
| Protocols | SSH and WebSocket enabled; TCP and Telnet disabled |
| TCP | 127.0.0.1:5001–5048 |
| SSH | 0.0.0.0:6001–6048 |
| Telnet | 127.0.0.1:7001–7048 |
| WebSocket | Shared Web port, path /console/<number> |
Use --factory-reset to remove the database and repeat database/password initialization. Initialization does not contact the licensing server automatically, create API keys or engineers, or bind hardware. Without a license, capacity is two ports.
Set the password promptly: before setup, anyone able to reach the page can set it first. deb/rpm automatically starts listening on port 80. On an untrusted network, first restrict http_host to 127.0.0.1 or use --addr, then open access after setup.
Factory reset
Factory reset is available only in a local interactive terminal, not the Web interface. “Clear all serial configurations” resets serial configuration only.
betterone-cs --factory-reset
When the device goes to someone else, add --new-ssh-host-key to replace the SSH host key as well:
betterone-cs --factory-reset --new-ssh-host-key
The old key is deleted and the service generates a new one when it starts, so a copy of the old key the previous user may still have (in a backup, for example) can no longer be used to impersonate the device. SSH clients that connected before warn that the host key changed until the old entry is removed from known_hosts. Leave it out when you are only starting over yourself.
Stop the service and back up first. The command temporarily stages the database for rollback during the operation, then deletes that stage on success; it is not a lasting backup.
- Check whether a program responds at the configured Web address; refuse to proceed while the service is running.
- Display what will be deleted/retained and a backup warning.
- Require the exact uppercase word
RESET. Any other input aborts without changes. - Preflight permissions and file types, stage the database in the same directory, write the reset marker and rotate runtime secrets, then remove the staged files. On failure it attempts to restore the old database. An interrupted stage blocks the next startup rather than silently creating an empty database.
It deletes runtime.db, runtime.db-wal, and runtime.db-shm: users/passwords, API keys, audits, and all 48 consoles' notes, bindings, serial parameters, connection limits, protocol switches, and listener addresses.
Once the database part has succeeded, it also deletes, where present:
| File | Content | Afterwards |
|---|---|---|
web-settings.toml in the data directory |
Web address, port, HTTPS switch and certificate settings changed on the web page | The values in settings.toml apply again; the command shows the resulting address and port |
web-tls.pem (data directory, and a copy older versions left in the configuration directory) |
Uploaded HTTPS certificate and private key | Upload again when HTTPS is needed |
web-tls-self-signed.pem |
Self-signed HTTPS certificate and key | A new one is generated when self-signed HTTPS is enabled again |
Windows: ProgramData\BetterOne\ConsoleServer\logs\betterone-console-service*.log |
Service logs with user names, source addresses and actions | Written anew once the service starts |
Files that cannot be deleted (for example an old copy under root-owned /etc) are listed for manual removal; the database part is complete regardless.
On Linux the service logs to the system journal, which cannot be cleared for one service only. To clear it as well, run sudo journalctl --rotate && sudo journalctl --vacuum-time=1s; this removes the history of every service.
| Retained | Explanation |
|---|---|
settings.toml |
Keep the addresses and other base settings written in it (changes made on the web page are cleared, see above). Factory reset rotates secret_key and api_key_pepper in DataDir/runtime-identity.toml, invalidating old sessions and keys. |
license.txt |
Bound to this device; deletion would require activation again. |
| SSH host key | Avoid host-key-change warnings; replaced only with --new-ssh-host-key (see above). |
device_uuid in configuration or writable runtime identity, plus machine file when available |
Preserve device identity, especially without hardware identity. |
Restart the service. It creates a database owned by its own account and records factory-reset with the OS user (under sudo, the person who ran sudo, e.g. alice (sudo, as betterone-console-server)) and local origin. Then promptly set a new administrator password.
The command requires an interactive terminal and refuses scripts, pipes, or service execution. It operates only on an existing database; use the service's -user / -system / -portable, --config-dir, and --data-dir. On Linux the command runs as the data-directory owner (the service account): for deb/rpm installations, stop the service and run sudo betterone-cs -system --factory-reset; started as root, it switches to that account before it reads or writes anything, so every file it leaves belongs to the service. Without sudo, run runuser -u betterone-console-server -- betterone-cs -system --factory-reset as root. Other ordinary users are refused. If a .runtime.db.factory-reset-stage-* directory remains after an interruption, inspect the old database and previous-runtime-identity.toml inside it and restore both as needed before removing the stage; after copying or moving files as root, give them back with sudo chown -R betterone-console-server: /var/lib/betterone-console-server, or the service cannot open them. Other directories are untouched.
Forgotten administrator password
If another administrator is available, log in with that account and reset the affected user in Users. Old sessions expire and the Web interface requires another password change at next login.
Otherwise run locally with the same paths/mode as the service:
betterone-cs --reset-admin-password
The command shows the database path and prompts twice without echoing the password. Require eight or more characters, neither letters-only nor digits-only. It resets built-in admin, reenables it if disabled, invalidates its sessions, and sets the password-change flag. It exits without starting the service; the service may remain running.
Each reset records reset-admin-password, the OS user (under sudo, the person who ran sudo), and local origin in audits and logs, never the password. Pipes, scripts, and noninteractive sessions are refused; no password command-line or file option is provided. Local access is required.
| Installation | Command |
|---|---|
| Linux deb/rpm | sudo betterone-cs -system --reset-admin-password (it switches to the service account by itself; without sudo, runuser -u betterone-console-server -- betterone-cs -system --reset-admin-password as root) |
| Portable | ./betterone-cs -portable --reset-admin-password in the program directory |
| User mode | betterone-cs -user --reset-admin-password as the usual user |
Wrong paths produce “database not found” instead of creating another database. On Linux, the command must run as the data-directory owner (normally the service account); it checks before generating any runtime file. Windows service installations need an administrator terminal and the service's actual system-mode paths; the service runs as LocalService, with directory access for LocalService, SYSTEM, and Administrators.
A channel without an interactive terminal cannot use this command. Deleting the database loses all console configuration, users, keys, and audits and is not a password-recovery method.
Directories and startup options
The configuration file is settings.toml. The configuration template has Chinese comments describing settings/defaults; it is not loaded automatically. Generate local configuration with --init-config, then copy only settings you need to change. Do not overwrite existing keys or UUID. Omitted fields use program defaults.
The template ships in archive roots, Windows installation directories, and /usr/share/doc/betterone-console-server/settings.example.toml on Linux. It does not replace runtime configuration.
| Mode | Linux configuration / data | Windows configuration / data |
|---|---|---|
-user |
$XDG_CONFIG_HOME/betterone-console-server / $XDG_DATA_HOME/betterone-console-server, default ~/.config/betterone-console-server / ~/.local/share/betterone-console-server |
LocalAppData Known Folder: BetterOne\ConsoleServer\config / BetterOne\ConsoleServer\data |
-system |
/etc/betterone-console-server / /var/lib/betterone-console-server |
ProgramData Known Folder: BetterOne\ConsoleServer\config / BetterOne\ConsoleServer\data |
Default portable, -portable |
config/ / data/ beside executable |
config\ / data\ beside executable |
Choose only one of -user, -system, or -portable; double-dash forms also work. Without a mode flag, NBC_MODE applies; otherwise portable is the default.
Linux ignores relative XDG values; Windows resolves Known Folders through the OS API. Nonempty --config, --config-dir, --data-dir, -user / -system / -portable override NBC_CONFIG_FILE, NBC_CONFIG_DIR, NBC_DATA_DIR, NBC_MODE, then mode defaults. An explicit configuration file is independent of ConfigDir; otherwise use <ConfigDir>/settings.toml. Relative CLI paths resolve once against startup cwd.
Relative database, SSH key, and license paths resolve against DataDir, with defaults runtime.db, ssh_host_key, license.txt; absolute paths stay unchanged. Restart after startup-setting changes.
For a new deployment run betterone-cs -user --init-config, inspect configuration, then start normally. This creates initial configuration/missing keys only, not database/users/SSH key/listeners. An existing base configuration is not rewritten; missing generated secrets or UUID are kept in DataDir/runtime-identity.toml. The base may be read-only, but DataDir must be writable for SQLite/WAL and runtime state.
On Linux, do not run the server itself as root on the packaged data (sudo betterone-cs -system): the files it writes would belong to root and lock the service out, so the program refuses; use systemctl, or run it as the service account. Linux normal users usually cannot bind port 80: use 8080 or configured capability. Packaged systemd uses the dedicated account with CAP_NET_BIND_SERVICE; configuration directory root:betterone-console-server/0750, file 0640, and data directory owned by the service/0700. Windows WinSW uses LocalService with restricted ProgramData ACLs. Customers need no Go, Inno Setup, or WinSW development tools.
Configuration file
Only overridden fields need be written. Explicit values, including allowed empty strings, 0, and false, take precedence. Omitting license_server uses its default; license_server = "" disables online licensing; an explicitly saved address remains in use. Restart after changes.
| Setting | Default | Purpose |
|---|---|---|
http_host, http_port |
0.0.0.0, 80 |
Shared Web, REST, and WebSocket address |
websocket_allowed_origins |
Empty | Additional allowed host[:port] origins |
https_enabled, https_host, https_port |
false, 0.0.0.0, 443 |
Independent HTTPS switch and listener; HTTP redirects to HTTPS when enabled. Ports must differ. |
tls_cert_file, tls_key_file |
Empty | HTTPS certificate/key; omit to use a self-signed certificate. Relative to ConfigDir. |
max_connections_per_ip |
128 |
Combined SSH/Telnet/Raw TCP connections per source across consoles; excess closes immediately, 0 means unlimited. NAT/proxy users share the source allowance. |
secure_cookies |
false |
Secure cookies behind TLS proxy; automatic with built-in HTTPS |
ssh_host_key |
ssh_host_key |
Relative to DataDir |
license_file |
license.txt |
Shared activation/renewal license, relative to DataDir |
license_server |
https://license.betterone.top:8443 |
Online licensing; explicit empty disables |
license_portal_url |
https://license.betterone.top:8443/#devices |
Browser device-management link, no admin API calls |
license_offline_url |
https://license.betterone.top:8443/#offline |
Browser “Offline activation / renewal” page; detects activation requests (.request.txt), renewal requests (.renewal.txt), and license files (.license.txt) by content |
database_dsn |
runtime.db |
SQLite settings, users, keys, and audits; relative to DataDir |
secret_key |
Generated | Session encryption/authentication key |
api_key_pepper |
Generated | API key verification key |
device_uuid |
Generated | UUID v4 also saved in machine-level file |
The three browser URL settings are independent of license_server. Current implementation uses the listed defaults when omitted; explicit empty values hide links. Restart after editing them.
Session key: secret_key
Sessions use AES-256-GCM to encrypt/authenticate user ID, session version, and expiry in cookies. Web and WebSocket login depend on this 64-character hexadecimal (32-byte) key. Replacing/losing it invalidates all sessions, not users/configuration. Disclosure allows forged cookies, including administrator impersonation.
API key verification: api_key_pepper
The database stores HMAC-SHA256 verification data rather than plaintext API keys; this 64-character hexadecimal key is the HMAC key. Plaintext API keys are returned only once. Losing/replacing the pepper invalidates all existing keys irrecoverably; recreate keys and update clients. Pepper disclosure alone is not a login credential but weakens protection if the database is also disclosed.
Storage and backups
Restrict configuration access to the service user (program-created files use 0600); do not publish it in repositories, tickets, or chats. Diagnostics report only whether these keys are set. Back up keys with the corresponding database. Keep them during migration. Suspected disclosure: stop and remove the affected value from both settings.toml and DataDir/runtime-identity.toml if present; deleting only the base value may leave the runtime value in effect. Restart to generate a replacement, accepting session/key invalidation. --init-config fills missing values without changing existing ones.
Upgrading an old deployment
The new default names are a breaking change: there is no automatic migration or fallback. If settings.toml or runtime.db is absent, the program ignores any old nbc_settings.toml or nbc.db in the same directory and creates fresh configuration or database files; it does not delete the old files. To retain old deployment data, stop the service and back up the configuration, database/WAL/SHM, SSH private key, license.txt, and old executable before migrating manually. Rename the old nbc_settings.toml to settings.toml in ConfigDir and the old nbc.db to runtime.db in DataDir. Make sure SQLite has shut down cleanly; if WAL/SHM files remain, use a consistent SQLite backup instead of moving only the main database. Check custom --config and database_dsn paths separately.
Keep secret_key, api_key_pepper, and the SSH host key. If the old TOML explicitly sets database_dsn = "nbc.db", change it to "runtime.db"; explicit paths are otherwise still honored. Check other relative paths, including data/nbc.db and data/ssh_host_key, and verify the license path. Keep backups until the startup paths and original data have been checked.
Logs go to stdout/stderr: journald for systemd, terminal for portable, and WinSW captured/rotated files for Windows service. /run is a systemd runtime directory; no PID file is currently written.
Serial binding and communication settings
Bind a console by device name, USB physical slot, or USB VID/PID/serial identity. These are mutually exclusive. Duplicate name, slot, or USB device bindings are rejected; different methods targeting one physical device can still conflict when opening it.
Defaults are 115200 8N1 without flow control. Allowed baud-rate input is 300–4,000,000; actual support depends on chipset, driver, and target. Match the device and test before production. Open handles may retain old settings after a save; new settings apply at next open. Check active_serial_config for active settings.
Each console's shared client limit across protocols is 1–256, default 1; 0 and -1 unlimited modes are no longer accepted. Invalid new values are rejected; no automatic migration of old unlimited configurations is provided. Clients share one stream and must coordinate commands.
Three binding methods
Setting one method clears the others. A request providing multiple nonempty methods is rejected.
| Device name | USB slot | USB identity | |
|---|---|---|---|
| Fields | device |
slot |
vid, pid, serial_number |
| Example | COM11, /dev/ttyUSB0 |
00.1400@2.3 |
0403, 6015, D30J42Y6A |
| Identifies | Terminal name | Current serial port at a physical position | Particular adapter regardless of position |
| Name changes | Binding may fail | Unaffected | Unaffected |
| Replace adapter in same socket | Depends on name | No change needed | Rebind |
| Move socket/cable | Depends on name | Rebind | Unaffected |
| Applicable equipment | Any serial device | USB serial | USB serial with unique serial number |
Device name (device)
Use for motherboard ports, virtual com0com/PTY ports, multiport devices where other methods are ambiguous, or stable single-adapter deployments. Names must remain stable and not be held by another program. Windows COM numbers can be assigned in Device Manager. Linux /dev/ttyUSBn enumeration may change: use udev or /dev/serial/by-path/ and /dev/serial/by-id/ names when needed. Multiple adapters with unfixed names risk binding the wrong equipment.
USB physical slot (slot)
Use for fixed rack cabling or adapters without unique serial numbers. A replacement at the same slot needs no change. Windows and Linux share slot syntax. Host hardware, motherboard, expansion cards, hub model/order, and sockets must remain stable. Firmware changes can also alter slots; recheck afterward. Multiport devices need an interface suffix for ports other than the first. Linux must expose readable sysfs; containers/WSL may have no slots. This method does not apply to non-USB ports.
USB identity (vid, pid, serial_number)
Use to follow a particular adapter across sockets/hubs. All three fields are required, the serial number must be unique, and one identity must resolve to one port. Adapters without serial numbers cannot use it; duplicated serials or shared multiport identities are ambiguous. Replacement adapters require rebinding.
Use usb_vid, usb_pid, and usb_serial exactly as enumerated. Windows FTDI values may include a channel letter (D30J42Y6A) while Linux uses D30J42Y6; FTDI identity configurations are not portable between these platforms. If go.bug.st/serial metadata enumeration fails, identity fields may be empty, temporarily blocking identity resolution while name/slot bindings remain usable.
Choosing a method
- Non-USB or ambiguous multiport equipment: device name.
- Fixed cabling with potentially replaced adapters: slot, subject to topology stability.
- Stable adapters with changing positions and unique serials: identity.
- Inspect
GET /api/system/serial-portsfor names, slots, and USB identifiers before choosing.
Missing/ambiguous bindings enter serial fault/retry handling and eventually close sessions on timeout. Status distinguishes unavailable device/USB position and multiple matches.
Slot syntax
00.1C00.0000@1.4:2
Before @ is the PCI root bus (two hexadecimal digits) followed by PCI device/function segments (four hexadecimal digits). After @ is the decimal USB port chain, followed by optional USB interface number :2. Omitted interface selects the first/minimum interface. Hex is case-insensitive and displayed uppercase; USB port numbers are 1–255.
Examples: a hub on motherboard port 2, adapter on hub port 3: 00.1400@2.3; third FT4232H interface: 00.1400@2.3:2. Motherboard/PCI serial ports may have no @, for example 00.1C00.0000, using a suffix for multiple ports ordered by address. Device-tree platforms use dt.<platform-device>@..., such as dt.3f980000.usb@1.2.
A slot is a system-reported location, not a serial number or terminal name. Resolution finds the current COM/tty name there. Physical case labels do not necessarily correspond one-to-one: USB 3 sockets expose USB 2/3 logical positions, internal devices consume numbers, and Raspberry Pi 3B/3B+ sockets sit behind an onboard hub.
Slot stability
Slots avoid changing USB bus numbers and Windows root-hub indices. PCI root bus numbering comes from firmware. With unchanged hardware it is normally stable, but BIOS changes, PCI/USB settings, speed fallback, or different hub internals can alter it and cause missing or wrong bindings.
Recheck every slot after changes. Linux relies on sysfs; WSL/containers/permissions may leave slots empty. usbipd-forwarded WSL ports do not reflect physical topology. Windows uses LocationPaths and hub queries independently of COM numbers. Virtual non-USB/non-PCI ports have no slots. Use genuine cables/chipsets.
Multiport devices
FT2232H/FT4232H, CP2105, CH348, and similar devices share a location and need interface suffixes, such as 00.1400@2.3:0 and :1. Without a suffix, the lowest interface is selected. Firmware interface numbers normally remain stable across reconnects/restarts. Multiple ports under one interface (such as some MCS7840/CH348 devices) require physical validation of ordering; stable device-name/udev bindings are an alternative. Cards containing an internal hub and separate single-port chips have distinct extended port chains.
Choosing cables and chipsets
Buy genuine cables, adapters, and powered hubs from reputable suppliers, including genuine FTDI, Silicon Labs, Prolific, or WCH products. Use externally powered hubs for multiple adapters.
Counterfeit/unknown products may lack unique serial numbers, be rejected or rewritten by drivers, lose/corrupt data, disconnect, use inaccurate baud rates, lack power/isolation/protection, or mix incompatible 3.3 V, 5 V, and RS-232 levels. Users bear resulting identification, wrong-binding, data-loss, interruption, and equipment-damage risks. The Software cannot guarantee operation on them. Inspect USB identifiers in Physical Ports when receiving new adapters.
Expanding through hubs and replacing them
Each extra hub adds a .port segment:
| Connection | Slot | Windows LocationPaths ending |
|---|---|---|
| Direct adapter | 00.1400@2 |
...#USB(2) |
| Hub port 3 | 00.1400@2.3 |
...#USB(2)#USB(3) |
| Cascaded hub | 00.1400@2.3.1 |
...#USB(2)#USB(3)#USB(1) |
Hub hardware numbering is normally stable across reconnects and power cycles. Keep the same host socket, hub model, and cascade order. Do not infer slots from printed case numbers: a seven-port hub may contain two cascaded four-port chips. A case port 5 might be 00.1400@2.4.2, not @2.5. Read actual slots. USB 3 hubs contain separate USB 2/3 hubs; serial adapters usually use the USB 2 side, but changing host speed/controller may change paths. Prefer powered hubs, especially for more than four adapters; inadequate power causes repeated enumeration even if slots stay unchanged.
Example: seven-port hub A at 00.1400@2 exposes ports 1–3 at @2.1, @2.2, @2.3 and ports 4–7 at @2.4.1 through @2.4.4. A different hub B may expose ports 1–4 at @2.1.1 through @2.1.4, then ports 5–7 at @2.2, @2.3, @2.4.
Replacing A with B and copying physical placements can leave console 1's @2.1 missing while console 2's @2.2 silently connects to the equipment now on case port 5. Wrong equipment without an error is the most dangerous outcome.
- Prefer identical replacement hubs at the same host socket.
- If the model changes, keep affected consoles inactive, insert adapters individually, read every new slot, and update bindings.
- Test each console against its intended equipment.
- Unique-serial adapters can use identity binding to avoid hub/socket dependence.
- Label each hub socket with its slot and console number.
Serial binding API
PATCH /api/consoles/{id}/serial requires admin or engineer. It is a partial update: at least one field, omitted fields unchanged; null and unknown fields rejected.
{"device":"COM11"}
{"slot":"00.1400@2.3"}
{"vid":"0403","pid":"6015","serial_number":"D30J42Y6A"}
VID/PID are four hexadecimal digits, saved uppercase. Serial number cannot be blank. All identity fields must be sent together, even when changing one. A nonempty method clears others; multiple nonempty methods are rejected. Empty strings clear a method, including all three identity fields:
{"vid":"","pid":"","serial_number":""}
To change parameters while retaining binding, omit binding fields:
{"baudrate":115200}
| HTTP | Meaning |
|---|---|
| 401 | Not authenticated |
| 403 | Insufficient role |
| 404 | Console missing |
| 409 | Device/slot already bound; response identifies the console |
| 422 | Invalid/incomplete identity, multiple binding methods, malformed VID/PID, or parameter outside range; detail explains |
Serial discovery returns port_name, slot, usb_vid, usb_pid, usb_serial. Console serial objects return saved binding fields; unused methods are empty strings. Status exposes configured_device, configured_slot, configured_vid, configured_pid, configured_serial_number, and actual resolved_device. Existing handles retain their current settings; the next connect/reconnect uses the new binding.
Identity binding details
Identity follows the adapter; slot follows the socket. Duplicate identities cannot bind multiple consoles, with case-insensitive serial comparison. The database adds serial_vid, serial_pid, and serial_number columns automatically to old databases without changing existing settings.
Identity cannot cover every device: Windows FTDI channel suffixes differ from Linux; failed metadata enumeration can leave all identity fields empty for that scan; missing/duplicate serials and multiport identities are unusable or ambiguous. Windows temporary instance IDs containing & are not USB serial numbers. Multiple matches are refused and candidates logged rather than selected arbitrarily; an ambiguity message may use the existing text configured USB slot matches more than one port.
Temporarily disabling a console
Admin/engineer can pause a licensed console:
PATCH /api/consoles/1/enabled
Content-Type: application/json
{"enabled": false}
Set true to resume. The boolean is required; missing, null, or unknown fields produce 422 validation errors.
Pause disconnects bridged sessions and rejects new TCP/Telnet/SSH/WebSocket forwarding; the owner closes the serial port after in-flight work ends. Settings and network listener ports remain. Pause is in memory only, cleared by service restart, not by configuration reload or license changes. Lists/details show enabled=false, disabled_reason="manually_disabled"; status shows disabled/manually_disabled. Configuration remains editable. Repeated requests are idempotent and audited. Unlicensed consoles return 409; this cannot bypass licensing.
Components remaining active
| Component | Paused behavior |
|---|---|
| Physical serial port | Cancel/close after pending work; no reopen |
| Bridged clients | Disconnected |
| TCP/Telnet listeners | Retain bound ports; new bridge registration refused; Telnet may negotiate first |
| SSH listener | Retain port; handshake/authentication/session creation may complete, but serial bridging is refused |
| Shared WebSocket service | Continues for other consoles; rejects this console |
| Console owner | Remains waiting without new opens |
| Management APIs | Retain/query/edit configuration |
Unbridged SSH handshakes and sessions are not necessarily closed by pause; normal connection and timeout limits apply. Resuming does not open hardware until a client enters bridging. Disable individual TCP/Telnet/SSH protocols to release their ports. Disabling one console's WebSocket does not stop the shared service. Schema is available at /api/openapi.json and /api/docs.
Protocols and network protection
| Protocol | Default | Authentication/encryption |
|---|---|---|
| SSH | Enabled, 6000 + console number |
Enabled local user credentials; encrypted |
| WebSocket | Enabled, shared Web port and /console/<number> |
Supported credentials; default ws:// is plaintext |
| Raw TCP | Disabled, 127.0.0.1, 5000 + number |
No application authentication; plaintext |
| Telnet | Disabled, 127.0.0.1, 7000 + number |
No application authentication; plaintext |
SSH/WebSocket default to 0.0.0.0 (all local IPv4 addresses); clients use a real host address. Remote TCP/Telnet requires an explicit listening address and firewall protection. Defaults apply to new/reset configuration, not existing saved values. SSH example: ssh -p 6001 admin@<server-IPv4>.
WebSocket supports X-API-Key, HTTP Basic, or the nbc_session cookie, in that precedence order. Invalid earlier credentials do not fall back. Cookie access is restricted to admin/engineer and valid origins. Browser terminals use cookies directly: the former one-time ticket and POST /api/consoles/{id}/websocket-ticket were removed. Web login does not authenticate TCP/Telnet. Some configuration/status GET endpoints allow anonymous reads and need network controls.
WebSocket behavior:
- No independent WebSocket port; the former port 8001 and its
/healthare removed. UseGET /api/system/health. The shared WebSocket listener's address follows Web settings;PATCH /api/listeners/{id}cannot change it (409). Legacy saved WebSocket host/port fields remain for compatibility and do not allocate or conflict with ports. - Browser
Originmust match requestHost(hostname/port) or a configured allowed origin. Nonbrowser clients without Origin still require credentials but skip browser origin checking. - Proxies should preserve Host and forward WebSocket upgrades. Configure additional
host[:port]entries inwebsocket_allowed_origins, with*wildcard support; restart after changes. - Server ping interval is 30 seconds; no reply within 15 seconds closes the connection and frees capacity.
websocket_allowed_origins = ["console.example.com", "console.example.com:8443"]
| Protocol | Refusal feedback |
|---|---|
| SSH | Shell shows maximum connections reached or other text after authentication; handshake overload closes before text and is logged |
| Telnet | [bridge] maximum connections reached or other text, then close |
| WebSocket | Terminal text and close 1013 for capacity or 1008 for other refusals, with a reason |
| Raw TCP | Close without refusal text to avoid corrupting device byte protocols; inspect logs |
Authentication precedes WebSocket console checks. Unauthenticated requests receive 401 without learning console existence. Authenticated browsers may complete upgrade then receive refusal text for nonexistent/disabled/unlicensed/paused consoles because browser scripts cannot read handshake error bodies. Nonbrowser clients receive HTTP 404/403/409.
Resource protection:
- SSH allows 16 concurrent unauthenticated handshakes separately from each console's authenticated 1–256 limit. Excess handshakes close immediately; authenticated users may log in but receive capacity text at shell startup and close after about 30 seconds. Handshake timeout is 10 seconds; up to eight sessions per connection, with 30 seconds to start each. PTY requests do not extend it. Started shells follow shared console limits.
- Telnet negotiation writes time out after five seconds; empty/invalid or over-1024-byte subnegotiations close the connection.
- HTTP/WebSocket holds up to 4096 connections, queuing excess. Headers are limited to 64 KB and must arrive within five seconds. Each browser terminal uses one connection.
- Up to 512 ordinary Web requests run concurrently (excluding terminal WebSockets); excess returns 503 with
Retry-After: 1rather than queuing. - Login/initialization source limits allow a burst of 30, then one request per two seconds; excess returns 429. Proxy users share a source address.
X-Forwarded-Foris not trusted; account lockout supplies stronger password protection. - Combined SSH/Telnet/Raw TCP connections have a global 4096 cap and queue excess; per-console limits still apply.
- No systemd
MemoryMaxis set; configure a drop-in if your deployment needs it. - Web bind failures include reasons/remedies. WebSocket authentication/pause/license failures are checked before upgrading, subject to browser refusal feedback above.
- Closing/cancelling a client cancels its blocked serial enqueue. A write queue still full after 30 seconds terminates that client. These limits do not replace firewalls and deployment-wide memory/connection budgets.
Prefer SSH and restrict listening/firewall scope. For encrypted HTTP/WS use built-in HTTPS or a TLS proxy. Serial bytes may contain equipment passwords; proxy TLS protects only connections passing through that proxy.
Enabling HTTPS
Administrators can select a PEM certificate and matching private key under Network → HTTPS and certificate, then enable HTTPS. To avoid sending a private key in cleartext, uploads are allowed only from an HTTPS page or localhost on the server itself. For headless devices managed over remote HTTP, leave the certificate files empty, click “Enable HTTPS” and confirm using a self-signed certificate, save, restart the service over SSH or with a service manager, then return over HTTPS to upload the production certificate. Alternatively, transfer certificate files securely over SSH and configure them on the server. If a reverse proxy already provides HTTPS, built-in HTTPS is usually unnecessary. Enabling or disabling HTTPS takes effect after a restart; saving does not interrupt existing connections. UI-managed certificates are stored as web-tls.pem in the data directory; restrict read access to this file. Replacing a valid managed certificate takes effect for new connections without a restart.
If you have no certificate, leave the files empty, click “Enable HTTPS” and confirm using a self-signed certificate. An invalid uploaded certificate/key pair also selects a self-signed certificate, clearly indicated in the UI. It is stored as web-tls-self-signed.pem in the data directory and reused after restart. Browsers will warn that it is untrusted; use a trusted certificate for public access. If configured certificate files are unusable at startup, HTTPS starts with this self-signed fallback rather than reverting to HTTP. The original files are not overwritten.
The administrator-owned configuration is read-only to the service. Web UI changes are stored in DataDir/web-settings.toml and override only the corresponding Web settings at startup; the original configuration remains unchanged. To restore its Web settings, stop the service, back up and move aside this override file, then restart. The license, local release markers, managed certificates, and any missing generated identity values are also persisted under DataDir.
HTTP and HTTPS have independent listen ports. When HTTPS is enabled, HTTP redirects to HTTPS. Set the HTTPS options and, optionally, a PEM certificate and key, then restart:
http_port = 80
https_enabled = true
https_host = "0.0.0.0"
https_port = 443
tls_cert_file = "server.crt" # Relative to configuration directory, or absolute
tls_key_file = "server.key"
With https_enabled = false, only HTTP is served. When enabled, HTTP and HTTPS must use different ports. If the certificate pair is missing or invalid, startup uses a persistent self-signed HTTPS fallback rather than reverting to HTTP. HTTPS uses minimum TLS 1.2 and HTTP/1.1 for WebSockets. HTTP requests redirect to the HTTPS port. Browser terminals use wss://; cookies become Secure.
HTTP and HTTPS start independently. If one listener cannot bind or exits while running, the error is logged and the other keeps serving. When HTTPS is unavailable, a running HTTP listener serves the application directly instead of redirecting to a dead HTTPS endpoint. If neither Web listener can start, server startup fails. If only HTTP is available, fix the HTTPS failure before sending sensitive data over an untrusted network.
Existing configurations without https_enabled but with a certificate pair retain their original single-port HTTPS behaviour until the HTTPS settings are saved again in the web UI.
Replacing certificate files does not need a restart: new connections use the new valid certificate, invalid replacements are logged and the old one remains. Existing connections are unaffected. Restrict private-key reads.
HTTPS covers the Web port only. SSH is already encrypted; Telnet/TCP remain plaintext. For TLS termination by Nginx/Caddy, keep application HTTP and set secure_cookies = true. Diagnostics show TLS and certificate status.
Login protection
Web login, WebSocket Basic, and SSH password authentication share protections:
- Five consecutive username failures lead to a 30-second lock starting at the sixth failed attempt; further failures double it up to five minutes. Correct passwords are refused while locked. Web returns 429; SSH/WebSocket report authentication failure. Success or 15 minutes without failures clears state.
- Unknown usernames count too; usernames are case-insensitive.
- At most two expensive password checks run concurrently; queued checks waiting over three seconds are rejected with 429.
- Password checks do not hold configuration locks. A failed last-login timestamp write logs an error without blocking successful login.
- Lock state is in memory and clears on restart; API keys are unaffected. Wait, or use local password recovery and restart if necessary.
Listener status and manual retry
Individual console listener bind/key failures or recoverable serve panics mark that listener failed without stopping others. The management HTTP service is essential; its startup/runtime failure remains fatal.
Admin/engineer GET /api/listeners returns all 145 entries, including protocol-disabled and unlicensed items. Saved configuration does not establish that a listener is running. Existing ID/protocol/console/host/port fields remain, with:
| Runtime field | Meaning |
|---|---|
state |
running, failed, stopped, disabled, license_blocked, or unavailable |
last_error |
String, empty when no error; cleared on successful retry |
retry_allowed |
Boolean |
Running does not guarantee hardware or authentication. Paused console listeners may remain running. WebSocket is one shared physical listener with console_number=null, not separately retried per console.
# Replace the key and choose listener_id from the list.
curl -sS -H 'X-API-Key: YOUR_API_KEY' http://127.0.0.1:80/api/listeners
curl -i -X POST -H 'X-API-Key: YOUR_API_KEY' http://127.0.0.1:80/api/listeners/1/retry
Retry uses latest persisted settings and changes only the requested physical listener, without changing configuration. Running listeners return idempotently without restarts/disconnects. Unknown IDs return 404; disabled/blocked/nonretryable listeners 409; unsupported runtime capability 503. An attempted bind that fails still returns HTTP 200:
{
"listener_id": 1,
"protocol": "tcp",
"console_number": 1,
"host": "127.0.0.1",
"port": 5001,
"runtime": {
"state": "failed",
"last_error": "listen tcp 127.0.0.1:5001: bind: address already in use",
"retry_allowed": true
}
}
Handle non-200 request errors first, then inspect runtime.state; only running with an empty error proves listener recovery. Resolve port/address/key problems before retrying. PATCH with the same host does not retry; use POST. Configuration reload/license application can attempt enabled listeners again; there is no periodic listener retry. Restart flags do not represent listener failures. License runtime_error reports the most recent license-application failure, not current live listener status. Network provides status/retry UI. Retry audits metadata, not terminal data.
Configuration import, defaults, and backups
Admin PUT /api/configuration/ imports all 48 console configurations. Check duplicate devices/slots, serial settings, and listener conflicts and back up first. Obtain the list through GET /api/consoles; consult /api/docs for shape.
Database save is transactional, followed by runtime reload that may change limits, next-open settings, and listeners and interrupt clients. Runtime reload is not a single atomic switch; reload failures are not returned through the save response. Success proves saving, not full listener application. Check listener/status/logs, resolve failures, retry individually, and test real connections. Restart flags do not prove complete application.
POST /api/configuration/restore-defaults resets console defaults and attempts reload, without resetting users, API keys, or licensing. Web “Clear all serial configurations” is an admin-only System danger-zone action for 48 consoles' notes, bindings, parameters, limits, protocols, and listener addresses. It does not clear users, keys, audits, licenses, startup configuration/UUID, or SSH key. Restart for complete application; the operation is irreversible, so export first. Use CLI factory reset to clear accounts/audits too.
For a complete backup, stop and copy SQLite, configuration, SSH key, and license, including existing WAL/SHM or using a reliable consistent SQLite backup. A console export excludes accounts, API keys, audits, and startup keys. Restore the corresponding file set while stopped, restrict permissions, then start and verify. Wrong pepper invalidates API keys, wrong session key invalidates cookies, changed SSH key triggers client warnings. Databases/backups contain sensitive authentication data and must not be public configuration attachments.
License application and installation
Free licensing allows consoles 1–2. Commercial tiers sold directly are 8P, 16P, and 48P; other tiers are not sold directly. Issued license capacity governs:
| Tier | Maximum logical serial ports | Console range |
|---|---|---|
| 8P | 8 | 1–8 |
| 16P | 16 | 1–16 |
| 48P | 48 | 1–48 |
Capacity is a total range, not added ports or arbitrary choices among 48. Startup creates all 48 bridges/owners; unlicensed consoles keep configuration but reject forwarding, do not open hardware, and do not start independent TCP/Telnet/SSH listeners. Shared WebSocket checks authorization per console. Even licensed hardware opens only with clients. Capacity is not listener count or client count.
Three ways to obtain a license
| Method | Procedure |
|---|---|
| Online | Admin POST /api/system/license/activate with {"code":"ACT-23456-789AB-CDEFG-HRZQE"}; sends code, hashes, hostname/platform/version, verifies/saves and applies the returned file. Repeat activation of the same device does not consume another seat. |
| Offline | Generate a one-use request, upload from a connected computer, download/import the signed file. The device clock does not block activation; created_at text participates in nonce validation and must not change. |
| Existing file | Import original text via POST /api/system/license/import-file, use compatible envelope PUT /api/system/license, or stop and place the file at license_file. |
Activation confirmation explains binding and uploaded hashes/platform, and that incorrect activation may be revoked on the website within 72 hours; other transfers need the seller. Do not store activation codes on the device; renewal does not need them.
File format and size
License/request files are UTF-8 JSON text without BOM. Current local export names are cs-YYMMDDHHmmss.license.txt and cs-YYMMDDHHmmss.request.txt using local generation time. Activation/renewal use the same file; internal storage defaults to license.txt.
Preserve server-issued original text; do not reformat or reserialize. Changing payload/signature invalidates verification. Limit is 8192 bytes; oversized imports return 413/422 depending on validation route, and oversized local files are invalid. Web selects .txt; legacy .lic/.token selection is not supported by the current UI. Content/type is checked after signature verification. For automation preserving original text, send the file string to the import-file endpoint; the legacy envelope PUT remains compatible.
License server
license_server accepts host:port or full HTTP(S) URLs and defaults to https://license.betterone.top:8443. Explicit empty disables online activation/checks; omitted settings use defaults and explicit existing addresses are not replaced. Restart to apply edits. Unreachable/malformed server responses return 502; missing configuration returns 503.
Device identity
License device_id uses SHA-256 hashes with product salt/type, not raw values, except administrator diagnostics:
| Field | Source | Required presence |
|---|---|---|
hardware |
SMBIOS motherboard UUID (Windows), DMI product_uuid (Linux), CPU serial fallback on applicable ARM boards |
May be absent |
install |
Windows MachineGuid or Linux /etc/machine-id |
May be absent |
generated |
UUID v4 saved in TOML device_uuid and independent machine file |
Required |
Requests also include informational platform and device_name; neither affects matching. MAC addresses are not used. Hardware UUID must be a valid 36-character UUID, not all zero/F, repeated-byte, or known placeholder (for example 03000200-0400-0500-0006-000700080009). CPU serial must be 8–32 hexadecimal characters and not repeated-character. Installation ID, after removing hyphens/braces, must be 32 hex characters, not repeated-character or uninitialized. Generated UUID must be lowercase v4; invalid values refuse startup.
Linux product_uuid often needs root. Packaged systemd copies it before startup to /run/betterone-console-server/product_uuid for the service. Manually running as an unprivileged user may omit hardware identity.
If a license has hardware identity, only hardware matches: reinstall/lost configuration does not matter; another board or unreadable hardware fails, without fallback. Otherwise installation and generated identities must both match. Without hardware and installation identity activation is impossible; generated-only identity is insufficient. Cloned installation templates with separately generated UUIDs therefore do not share a license.
Machine UUID files:
| Platform | Path |
|---|---|
| Linux | /var/lib/betterone-console-server/device_uuid |
| Windows | %ProgramData%\BetterOne\ConsoleServer\device_uuid |
Startup uses an existing UUID; when the machine-level file differs, it wins and the value is retained in writable DataDir/runtime-identity.toml instead of rewriting administrator-owned base configuration. If the machine file cannot be written, the base or runtime identity file retains the UUID. Invalid machine UUID refuses startup. NBC_DEVICE_UUID_FILE can override its path; off disables that copy for special deployments/tests. Uninstall preserves it. Removing all copies generates a new identity; OS reinstall may lose them, so devices without hardware identity need reactivation. Whole-system cloning copies all identities and cannot be distinguished locally; server limits help prevent repeat activation. Contact the seller for migration.
Installation and immediate application
Import-file verifies type=license, signature, device and downgrade confirmation, atomically saves, and coordinates runtime immediately with restart_required=false. Upgrades start enabled protocols on newly licensed consoles; downgrades disconnect out-of-range sessions, stop serial ports, and close their independent listeners. Shared WebSocket stays with per-console checks. Manual pauses remain. Invalid files/save failures preserve old licensing/runtime.
Optional admin-only runtime_error means the file installed but some listeners failed to apply, not that installation failed or a restart is required. It reports the last license application, not live retry updates. Inspect GET /api/listeners, resolve errors, and POST retry or reload/reimport. No periodic retry is scheduled.
Verification chooses the file's kid. Only the built-in key table is trusted; keys are never fetched from the network. A file with an unknown kid is rejected (reason unsupported_key) until you upgrade to a release that includes the new key.
Maintenance and upgrades
The license is perpetual. Coverage uses:
coverage_end = license.maintenance_until
version_allowed = version_build_date <= coverage_end
This is an offline comparison against build date, not the current clock. Covered versions remain usable after maintenance expiry. A later version runs as free 2P, while covered earlier versions remain fully licensed. Configuration for higher consoles is retained; those consoles cannot forward/open/listen. Status becomes update_not_covered, retaining valid license metadata; importing a covering license restores capacity without restart.
For example, coverage through 2027-10-01 includes that day's build but not 2027-10-02, even with a 48P license.
Migration, reinstall, and mistaken activation
The Software does not release/unbind/deactivate devices. Seat release is a website/seller process; existing offline files remain usable after release, but cannot obtain new coverage.
| Situation | Action |
|---|---|
| Incorrect activation within 72 hours | Buyer verifies code plus order/email on Devices and revokes, subject to limits |
| Later, limits exhausted, or missing order registration | Contact seller for administrative release |
Then activate normally on the new device. License summary provides public portal_url; hide empty links. Seats-full and device-released messages guide buyers to the same page when configured.
Per-device maintenance
Each device starts its own maintenance at first activation; renewal extends that device only. Use its signed file for display/checks, never assume all devices under a code share an expiry.
License status for scripts and interfaces
state |
Meaning |
|---|---|
free |
No installed license, normal two-port edition |
licensed |
Valid license covering this version |
update_not_covered |
Valid license but this build is outside maintenance; current capacity is free while older covered builds retain licensed capacity |
invalid |
Installed file unusable; admin reason distinguishes device_mismatch, signature_invalid, unsupported_key, file_too_large, unreadable, license_invalid, device_unavailable |
“Activation and renewal” uses one input to detect ACT / RNW codes and select the operation. Activated devices can still enter a new ACT code to replace their license. An unconfigured server disables online operations; the corresponding offline request can still be downloaded. Request download and license import buttons appear below the shared input. Confirm before replacing capacity.
edition is display-only; max_ports is currently usable capacity; licensed_ports is purchased capacity; actions.activate/renew reflects online availability. Deprecated valid is false only for invalid state. Detailed reason, licensed capacity, actions and metadata are administrator-only; anonymous summaries expose state/edition/capacity/build date.
Checking renewal
“Check renewal” sends stored file and identity to /v1/license/refresh; no activation code or repeat-activation delay is needed. The new file preserves identity/license ID/ports and updates coverage/issuance. Replace the same license only when coverage is later. Released devices cannot obtain new files but can continue covered versions with the old one.
Offline renewal
When the device cannot go online, finish on a connected computer through the website's “Offline activation / renewal” page. It takes a single file and recognises its type automatically; no renewal code is typed on the web page.
- Prepare the file and copy it to a connected computer:
- Bought a renewal code: enter the RNW code in “Activation and renewal” and click “Create renewal request” (name ends with
.renewal.txt). After an online connection failure, you can also confirm the prompt to generate an offline request. - The seller already renewed in the back office: expand the offline renewal section within “Activation and renewal” and export this device's license file (name ends with
.license.txt). This retrieves the latest license from the server without redeeming a renewal code.
- Bought a renewal code: enter the RNW code in “Activation and renewal” and click “Create renewal request” (name ends with
- Upload it on the “Offline activation / renewal” page (
/#offline; the old/#renewopens the same page). - Download the new
.license.txt, copy back, and import original text/file. - Repeat independently for each device.
Import verifies the local device and never regresses the same license's date. Different licenses can replace it, with confirmation if purchased capacity falls. Cancellation leaves the old file unchanged. Compare purchased capacity, not free fallback capacity. Files are limited to 8192 bytes; choosing one does not install automatically.
Admin export is GET /api/system/license/export without caching; import:
{"file":"<original file text>","allow_downgrade":false}
Set true only after explicit confirmation and resend the original. No new date means no celebration; successful renewal/activation retains their fireworks/confetti feedback.
Redeeming renewal codes
ACT activation and RNW renewal prefixes precede 20 characters: 16 random and four check characters. Allowed alphabet is 23456789ABCDEFGHJKMNPQRSTUVWXYZ; normalization ignores case, hyphens, spaces, and tabs. Omitted prefixes are identified by validating both checksums. Wrong-kind/invalid codes are rejected locally before requests.
Redemption uses the stored license/identity at /v1/renew, renews one device, and requires code capacity at least purchased capacity. The new signed file is automatically imported. Redemption cannot be undone or transferred.
Admin local request:
POST /api/system/license/redeem-renewal
Content-Type: application/json
{"renewal_code":"RNW-23456-789AB-CDEFG-H3G4C"}
Retries are safe: already_redeemed=true means no duplicate years/use deduction. Codes are not stored. Higher-capacity renewal codes require confirmation, then resend the original with confirm_ports:true; cancelling does not use the code. Equal capacity needs no confirmation; insufficient capacity is rejected without use. Higher capacity gives no extra ports or benefits and renews for the same duration as a matching code.
Maintenance may extend no later than today plus five years. renewal_exceeds_limit means wait and try later; the code remains unused. Server natural-month/month-end rules determine dates; clients do not predict future expiry.
Online errors
| HTTP | Meaning |
|---|---|
| 400/403/409/422/429 | Upstream refusal retained with structured code, including seats_full, code_voided, code_expired, device_released, invalid_license, rate_limited |
| 422 | Invalid code; upstream invalid_code (404) mapped locally to 422; invalid signature/device |
| 409 | Renewal requested without a license |
| 502 | Unreachable server or malformed reply |
| 503 | Missing license_server |
The server limits repeated failures: five failures per code/IP start a three-minute lock, escalating under server rules. Unknown and otherwise invalid activation codes both use invalid_code; do not try to distinguish them.
Before upgrading
Check coverage and stop/back up executable, database, configuration, SSH key, and license. Renew/get the latest covering file before using a later version. Verify compatibility before reverting and restore old backups if needed. Users choosing old versions after maintenance ends bear resulting defect/security/compatibility risks, subject to nonexcludable legal liability and separately agreed support duties. Device identity changes need developer/seller verification; there is no automatic migration.
Serial status messages
With a configured binding, failed opens/disconnections retain client sessions during reconnect. Input during failure is discarded, not buffered. Missing binding does not enter continuous retry. Sessions close if recovery exceeds 300 seconds.
| Trigger | Exact client text |
|---|---|
| First failure/disconnect | \r\n[serial] device unavailable; input will be discarded while reconnecting\r\n |
| Continued failure | Same, at most once per 30 seconds |
| Recovery | \r\n[serial] device connected\r\n |
| 300-second timeout | \r\n[serial] reconnect timeout; closing session\r\n, then close |
An initial successful open does not print recovery text. These texts come from the server, not the equipment, and enter all client streams, including Raw TCP/WebSocket binary protocols. Clients must recognize them.
Listener status diagnoses network binds/serving; console status diagnoses serial hardware/read/write/reconnect. Running listeners do not guarantee hardware. Listener retries do not repair drivers/cables/busy devices or restart already-running listeners. Automatic serial reconnect is separate from manual listener retry.
Permission and busy-port failures
| Message | Meaning |
|---|---|
serial device access denied |
Service account lacks serial access, typically Linux |
serial device is busy |
Another program owns the device |
serial device is unavailable |
Missing/unplugged device, wrong name, or missing driver |
Linux
Devices typically use root:dialout/0660. Packages add the service to dialout or RPM uucp. Remaining issues include Raspberry Pi onboard login console (root:tty and held by getty), missing groups, manually run users without membership, or restrictive udev rules.
ls -l /dev/ttyUSB0
id betterone-console-server
journalctl -u betterone-console-server -n 50
sudo fuser /dev/ttyUSB0
Add the service with sudo usermod -aG dialout betterone-console-server, then restart it. For an interactive user, add to the group then log out/in. Raspberry Pi onboard serial: raspi-config → Interface Options → Serial Port, disable login shell and enable hardware, then reboot. If dialout/uucp is absent, use the actual device group. Fix udev ownership/mode and reload/trigger/replug. Stop competing tools/getty after identifying them.
Avoid chmod 666: it is temporary across reconnects and grants every user serial access.
Windows
COM ports normally have no Linux-style group restriction. “Access denied” generally means already opened, displayed as busy. Close PuTTY, SecureCRT, vendor tools, or other program instances; one program can hold the COM port at a time. For unavailable ports inspect Device Manager → Ports, driver warnings, and configured COM number; install genuine vendor drivers/replug or use slot/identity binding. Corporate group-policy blocks require administrator intervention.
Serial fault handling and hardware acceptance
Recoverable driver panics become errors with logged stacks. Nil handles/invalid I/O counts are rejected. Partial writes continue; reads containing valid bytes plus an error forward bytes first. Empty immediate reads use cancellable waits; retry waits respect the remaining window. Each open has a separate identity so old reads/queued writes cannot enter new connections. Cancelled remainder is not replayed; bytes already delivered to hardware cannot be recalled.
Native Windows/Linux I/O uses a default five-second write timeout and 250 ms bridge read timeout; ordinary read timeouts are not faults. CancelIO() stops new operations and requests cancellation, not buffer clearing or proof of physical release. Owners wait for active work before closing. Windows confirms overlapped completion before releasing events/buffers, retains failed handle closes for retry, and marks Released only after all owned OS handles close. Linux uses nonblocking poll/cancellation, a 100 ms cancellation fallback, releases exclusivity, and closes the raw fd once without retrying a numeric fd after EINTR.
Failed opens may still leave handles needing cleanup. Before a new open, old handles must be confirmed released; retained handles are isolated and cleanup retried by one owner timer, even without clients, while paused, or after bridge close. Control APIs returning, idle/disabled state, context expiry, or cancellation cannot force a broken driver to release within an absolute deadline. Driver/device maintenance or service/system restart may be needed. Drain waits for an empty output queue, not guaranteed hardware shift-register completion; Break is cancellable. No protection is promised against OOM, fatal runtime, or OS failures.
Physical hardware acceptance remains incomplete. Prior recorded checks: WSL Ubuntu Linux serial PTY suite passed three runs; actual opener/bridge integration passed ten runs with reads/writes, idle checks, teardown/release/reopen, client departure/pause/license revocation, initialization cleanup, and descriptor checks. Windows syscall mock checks passed. These did not enable race detection in that round and are not physical USB-driver or native Linux release acceptance. Reproduction is in serial documentation.
Before production, test real equipment with an independent healthy console and logs:
- Unplug/replug during traffic; verify messages, retries, recovery, and no replay of discarded/queued input.
- Missing/busy bindings; verify deadline closure/no extra opens and recovery after release.
- Read/write faults; valid trailing read bytes forwarded and partial writes completed without replaying cancelled commands.
- Slow nonreading clients; healthy clients/consoles continue. Disconnect/log the slow client at a 64 KiB/512-frame queue or ten-second write timeout rather than silently dropping bytes. SSH disconnection ends other sessions on that underlying connection.
- Pause/license/close during continuous writes with injected cancellation/close failures; do not equate API return to release. Never reopen before confirmed cleanup. Verify Windows retained handles/retry and Linux exclusivity/fd safety. Arrange maintenance/restart for permanently unresponsive drivers rather than assuming automatic recovery.
Console status API
Authenticated GET /api/consoles/{conf_id}/status, IDs 1–48, uses session cookie or API key; unauthenticated returns 401.
| Field | Meaning |
|---|---|
state, message |
idle/connecting/connected/retry/error/disabled plus explanatory text |
serial_release_pending |
Always boolean; physical close/abort pending or failed cleanup retaining handles; no new open before release |
serial_stalled_operation, serial_stalled_for_seconds |
Earliest operation incomplete for 30 seconds; empty/null without warning. Indicates suspected unresponsiveness, not proof of permanent hang; clears on completion, not hidden by licensing. |
connection_count, max_connections |
Combined sessions and saved 1–256 limit |
configured_device, configured_slot |
Latest saved binding |
resolved_device, active_serial_config |
Actual current device/configuration; empty/null if unavailable |
connected_for_seconds |
Whole seconds connected, otherwise null |
last_error, last_error_ago_seconds |
Controlled error/age, overwritten by newer errors, cleared to empty/null on success |
retry_count |
Scheduled waits in this client-session cycle, retained after last departure and reset at next first client |
retry_elapsed_seconds |
Retry-window age while retrying/reconnecting, otherwise null |
retry_in_seconds |
Rounded-up remaining wait only while retry waiting |
Durations use server Go monotonic time, not client clocks or subsequent clock adjustments. Elapsed values round down, remaining waits up, minimum zero. Null means not applicable. Polling values are advisory. Former connected_at/last_error_at/retry_started_at/next_retry_at timestamps are removed; audits retain absolute UTC.
Release-pending is independent of listener status and disabled/idle/control responses. Last client departure closes hardware on demand, retaining a failed last error until later success. Detailed raw driver errors/paths stay in logs. Saved settings may differ from active handles; USB slot active config may have empty device, so use resolved_device. Next connect uses saved settings.
License-blocked state is disabled/license_limit with zero connections/retries, no active handle information, and null durations; pending release remains independently reported. No per-protocol client counts, byte totals, or dropped-frame statistics are currently exposed.
User management
Functions and roles
Admin can list/create/edit/delete users and reset passwords. Engineer cannot manage users and can change only their own password. Both authenticated roles can change their own password by supplying the current one. No password hashes are returned in lists.
Creating users
Usernames must be nonempty and unique; roles are admin/engineer. Passwords are at least eight characters, not letters-only/digits-only. New users default enabled and carry must_change_password. The Web interface enforces the initial change; API/SSH do not universally block other operations on that flag. First initialized admin chose its password and is not flagged again.
Editing users
Admin may edit nonbuilt-in usernames, roles, and enabled state. Changes invalidate existing sessions. At least one enabled administrator must remain: the last one cannot be demoted, disabled, or deleted.
Built-in admin protection
Built-in admin cannot be renamed/deleted; its password can change/reset. It can be disabled/demoted only while another enabled administrator remains.
Password changes and resets
Self-change requires current password and invalidates all old sessions, including the current one. Admin reset needs neither target nor administrator current password, only two matching compliant new passwords; it invalidates target sessions and flags next Web login for change.
Deletion
Delete other users permanently; built-in admin and last enabled administrator are protected. Deletion invalidates accounts/sessions and cannot be undone. Prefer disabling for a temporary block.
User APIs
All user-management APIs require admin. /api/docs assets are embedded and work without external Internet access.
| Method | Purpose |
|---|---|
GET /api/users |
List |
POST /api/users |
Create |
PATCH /api/users/{id} |
Edit name/role/enabled |
DELETE /api/users/{id} |
Delete |
PUT /api/users/{id}/password |
Reset without current password |
SSH public keys
Besides the password, users can sign in to the SSH consoles with an SSH public key, which suits scripts and automation: no password in the script and nothing to guess. Both work at the same time; adding a key does not turn the password off.
Your own keys: in the web interface, open the user menu (top right) → "My SSH keys", paste the whole line of a .pub file (such as id_ed25519.pub), optionally give it a name, and add it. To create a key on your computer: ssh-keygen -t ed25519.
Keys of other users: administrators use "SSH keys" in that user's row on the Users page.
Connecting: as with the password, for example ssh -p 6001 alice@<server IPv4>; the client uses the private key by itself.
Rules:
- ed25519, ECDSA (including hardware security keys,
sk-types) and RSA of at least 2048 bits are accepted; DSA, certificates and lines with options such asfrom=orcommand=are not. - Up to 10 keys per user; the same key cannot be added twice for one user.
- Paste only the public key (
.pub). Private keys (starting with-----BEGIN ... PRIVATE KEY-----) are refused; never give them to anyone. - Keys do not sign in while the account is disabled, deleted or must change its password (a new user, or a password just reset). New users sign in to the web interface once and change the initial password first.
- Deleting a key or disabling the account refuses new connections; SSH sessions already open stay open until disconnected on the Ports page.
- The list shows each key's fingerprint (as
ssh-keygen -lf id_ed25519.pubprints it) and when it was last used. - A sign-in with a key writes an
ssh-loginaudit event whose target names the key's fingerprint; adding and deleting keys are audited too. - API keys belong to no user and cannot have SSH keys of their own.
- A factory reset removes all users and with them their keys.
| Method | Purpose |
|---|---|
GET /api/auth/me/ssh-keys |
The signed-in user's keys |
POST /api/auth/me/ssh-keys |
Add a key for yourself: {"public_key": "...", "comment": "optional name"} |
DELETE /api/auth/me/ssh-keys/{key_id} |
Delete one of your keys |
GET /api/users/{id}/ssh-keys |
Admin: list a user's keys |
POST /api/users/{id}/ssh-keys |
Admin: add a key for a user |
DELETE /api/users/{id}/ssh-keys/{key_id} |
Admin: delete a user's key |
API key management
All management requires admin. Create with unique client_name and admin/engineer role. Save returned plaintext api_key immediately: it appears only once and cannot be retrieved later.
Editing, rotating, and deleting
Only role/enabled are editable; client_name/key are immutable. PATCH only fields to change:
{"role":"engineer","enabled":false}
For renaming, rotation, or lost plaintext, create a new uniquely named key, update clients, then delete the old one. Deletion is irreversible; disabling temporarily prevents new authentication requests.
| Method | Purpose |
|---|---|
GET /api/api-keys |
List with masked hashes, no plaintext |
POST /api/api-keys |
Create, plaintext once |
PATCH /api/api-keys/{id} |
Role/enabled |
DELETE /api/api-keys/{id} |
Permanently delete |
Changing the Web/API address
Web/REST/WebSocket share http_host/http_port, default 0.0.0.0:80. Admin can GET /api/system/web-address or PUT the same path:
The response includes settings_source (base or data). Administrators can confirm and call DELETE /api/system/web-settings to clear the UI-saved HTTP/HTTPS overrides; the base settings apply after restart and the address may change.
{"host":"0.0.0.0","port":8080}
Every change is audited. Admin/engineer can query /api/system/ipv4-addresses for active interface names, IPv4/prefix, loopback and link_local flags. 0.0.0.0 means all addresses but is not a listed interface. Clients usually choose nonloopback, nonlink-local addresses.
Restart required: saving changes only configuration. running is current, saved is next-start address, restart_required=true; system service_restart_required also reflects it. Saving the running address clears the flag.
Host must be local IPv4 or all-addresses (422 otherwise). Port is 1–65535 excluding 5001–5048, 6001–6048, 7001–7048 reserved console ranges, even if disabled. A changed port is test-bound immediately; conflicts/permission return 409. A host-only change does not test-bind because the current server owns the port.
Only the two Web address settings change in the data-directory override; the base configuration and its comments/secrets remain untouched. A temporary-file rename replaces the override atomically. --addr overrides saved settings at every startup: command_line_override=true, restart flag false; remove it to use saved values. If a change makes the service unreachable, start once with --addr 127.0.0.1:8000, then correct the saved override. Validation cannot guarantee network/firewall reachability.
System diagnostics for support
Admin: System → Support diagnostics → Generate report. Confirm the privacy warning before collection; cancellation collects nothing. Review, copy/download text, and send to support yourself. Admin POST /api/system/diagnostics returns full text; engineers/anonymous users are refused.
Report body is always English, independent of UI language. Report ID and X-Request-ID identify its audit entry, enabling attribution to creator/time/source if disclosed.
Do not send it with your license file. Raw identity plus a license could help someone reuse it on another host. Reports are unredacted, include identifying and network information/logs, and are for troubleshooting only. No automatic upload occurs; send only to contacted support, not publicly. Passwords, API keys, secret_key, pepper, and SSH private key are excluded.
| Section | Information |
|---|---|
| Program/system | Version/build/platform/Go/OS, CPUs, uptime, hostname |
| Resources | Instantaneous physical memory/available including reclaimable cache, swap, Go allocated/heap memory (not process resident memory), Linux 1/5/15-minute load; container values may describe host, not limits |
| Files/configuration | Executable/cwd/user, paths and file existence/size/time, writable data directory, Web address, allowed origins, license server; secret keys show set/unset only |
| License | Edition, ID, coverage, identity hashes, verification errors |
| Raw identity | Motherboard UUID, CPU serial, installation/generated IDs and hashes; rejection/read-failure reasons and hardware_source_used. Only this report exposes raw values. |
| Listeners | Running/failed listeners and errors; disabled/blocked counts |
| Consoles 1–48 | Licensing/enabled, bindings, serial settings, listeners, counts, state/errors |
| Discovered ports | Names, slots, VID/PID/serial identifiers |
| Recent logs | About 300 in-memory lines, cleared on restart |
data_dir_disk reports free/total disk space. Failed collection of one section shows its reason without preventing other sections. Each successful/refused generation is audited as create-system-diagnostics with actor/source, not report content. The page itself contains no report data until requested.
Connection logs
TCP/Telnet/SSH/WebSocket connections are application logs only, not database/audit entries. The process writes stdout/stderr, not its own rotating log files; use systemd/journald, service management, or redirection for retention.
console 3 TCP: client 192.0.2.10:51234 connected (1 of 4 connections)
console 3 SSH: client 192.0.2.10:51240 (user admin) connected (2 of 4 connections)
console 3 SSH: client 192.0.2.10:51240 (user admin) disconnected after 12m3s
console 3 TCP: client 192.0.2.11:40022 refused: maximum connections reached
Logs show console/protocol/IP/port, authenticated SSH/WebSocket username, current/configured counts, duration, and abnormal-close reason. TCP/Telnet have no username. Refusals include maximum capacity, disabled console, shutdown, and SSH login failures; early WebSocket refusals are logged too.
Same IP/reason repeats are collapsed over 30 seconds with omitted counts. SSH failure consolidation is by IP, independent of usernames. Client-supplied strings are control-character stripped and length limited to prevent forged log lines. No terminal payload is recorded.
These logs can enter diagnostics' memory tail, exposing client IP/usernames to support. SSH authentication is additionally audited as ssh-login. Deployers control retention/access to identifying logs.
Sensitive-operation audits
SQLite audit_events records initial admin setup, Web login success/failure/logout, password changes/resets, user/key management, license import, configuration updates/default reset, console changes/disconnections, and SSH password authentication. Current SSH callback does not supply source IP, so that field is empty for those entries.
Records contain UTC time, operation, actor/role, actual source IP, request ID, target and result; Web results success/failure/denied include HTTP status. Proxy addresses may be recorded; untrusted forwarding headers are not used. Passwords, cookies, plaintext keys, license text/signatures, request/response bodies, terminal payloads, and before/after field differences are excluded. Normal reads/polls and former ticket issuance do not write audit entries.
Admin queries GET /api/audit-events?limit=50 newest ID first; page with before=<last-ID>, for example ?limit=50&before=123. Limit 1–200, default 50; before default 0. Use session/admin API key. Current Nuxt Logs supports queries; old deployed assets may not, while API queries remain available.
Sensitive operations synchronously append using SQLite WAL. Hourly cleanup when new events arrive retains at most 100,000 records from 90 days, with temporary excess between cleanups. Retention follows host clock. No individual edit/delete API exists; OS database-file access can still modify records.
Audit-write failure preserves the operation's result, logs event/request ID, and adds Web X-Audit-Status: failed. Sensitive-operation responses include generated X-Request-ID. Business changes and audits are separate commits, so a crash can leave a completed operation without an audit. Failed logins can produce frequent writes; audit recording itself introduces no additional login throttling.