Eccelerators.Cli is a transport-independent command-line interface for FPGA
applications written in Livt. It provides bounded terminal input, editing,
parsing, prompts, atomic output writes, and explicit backpressure. Applications
remain responsible for the physical byte transport and static command dispatch.
The current package version is 1.1.0 and depends on Livt.IO 1.2.0-dev.
- 64-byte bounded command lines
- up to eight whitespace-separated arguments
- CR, LF, and CRLF line endings
- backspace and delete editing
- optional local echo
- scheduled
>prompts - 64-byte circular output FIFO
- atomic byte-array and CRLF line writes
- lossless input and output backpressure
- no dynamic allocation or command registration
- optional free-text lines with raw byte access
All capacities and commands are fixed at synthesis time.
The parser and output queue each use a 64-element DistributedRam<byte, 64>
from Livt.IO. Their length/count metadata guards unwritten cells; neither relies
on zero-filled startup. CompactCli uses the fixed asynchronous AsynchronousDistributedRam8x64
specialization with explicit combinational port wiring and boolean writeEnable. Storage style is a synthesis
hint, not a guarantee of physical allocation. The current RAM APIs are verified
against workspace snapshots with make test-workspace-memory in the livt
integration repository; dependency version pins remain unchanged.
For resource-constrained applications, CompactCli provides the same core
terminal contract with one 64-byte distributed-RAM line buffer and a
single-byte backpressured output slot. It deliberately omits tokenized
arguments, prompts, and the output FIFO; applications stream their
own text and perform static exact-command dispatch.
Add the package to a Livt project with:
[dependencies]
"Eccelerators.Cli" = "1.1.0"Import its public API with:
using Eccelerators.Cli
Cliis the application-facing facade.CliLineEditortracks bounded input length and line-ending state.CliParserstores a command snapshot and token spans.CliOutputqueues echo, responses, errors, and prompts atomically.CompactCliprovides bounded free-text input and streaming output with a smaller hardware footprint.
See DESIGN.md for ownership, data flow, and timing contracts.
An application owns one Cli and one byte transport. Its continuous process
services prompts, retries pending commands, drains output only after the
transport accepts a byte, and accepts input only when the CLI has capacity. The
transport operations below are pseudocode and must be replaced by the target
application's UART or other byte transport:
process Main()
{
this.cli.Service()
if (this.cli.HasCommand() == true) {
var completed: bool = this.DispatchCommand()
if (completed == true) { this.cli.CompleteCommand() }
}
if (this.cli.HasOutput() == true && transportCanWrite == true) {
var value: byte = this.cli.PeekOutput()
if (transportWrite(value) == true) { this.cli.ConsumeOutput() }
}
if (transportHasData == true && this.cli.CanAcceptByte() == true) {
this.cli.AcceptByte(transportRead())
}
}
The transport must not discard an input byte when CanAcceptByte() is false.
Likewise, call ConsumeOutput() only after the transport accepted the byte from
PeekOutput(). These two rules preserve data during backpressure.
With Livt.IO 1.2.0-dev, use BufferedUart or RtsCtsBufferedUart for the
scheduled byte transport. Uart is the low-level signal interface. An application
that owns cli: Cli and a buffered uart can perform the handoff as follows:
if (this.cli.HasOutput()) {
var value = this.cli.PeekOutput()
if (this.uart.TryTransmit(value)) {
this.cli.ConsumeOutput()
}
}
if (this.cli.CanAcceptByte()) {
var value: byte
if (this.uart.TryReceive(value)) {
this.cli.AcceptByte(value)
}
}
Use one transport owner so no other caller changes CLI input capacity between the check and acceptance. UART receive removes the byte only on success; UART transmit success means FIFO acceptance, not completion on the wire. A rejected transmit leaves CLI output queued for retry. Hardware receive storage is bounded; use RTS/CTS and a cooperating peer when input must pause during command handling.
CompactCli is suited to applications such as the TinyStories showcase where
every non-command line is application data. A transport process accepts bytes
when CanAcceptByte() is true, waits for HasLine(), and calls CompleteLine()
after dispatch. CR, LF, CRLF, backspace/delete, printable input, and overflow
recovery are handled internally. EnableEcho() adds backpressured printable,
line-ending, and editing echo; DisableEcho() restores silent input.
Exact command matching uses a fixed-width argument so synthesis does not need dynamic command storage. The expected command is left-aligned and padded to 16 bytes:
var helpCommand: byte[16] = ['/' as byte, 'h' as byte, 'e' as byte, 'l' as byte,
'p' as byte, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
if (this.cli.CommandEquals(helpCommand, 5)) {
// Stream the help response.
}
Output is explicitly backpressured. TryWriteByte(value) returns false when its
one-byte slot is full, leaving the pending byte unchanged. Retry rejected writes
later; the transport reads PeekOutput() and calls ConsumeOutput() only after
the UART or other sink accepts that byte.
Commands are statically dispatched by the application that owns Cli:
var status = "status".Encode()
var isStatus: bool = this.cli.CommandEquals(status)
if (isStatus == true) {
var message = "OK".Encode()
var written: bool = this.cli.TryWriteLine(message)
return written
}
Returning false keeps the command pending so the application can retry after
the output transport drains.
Input and lifecycle:
Service()CanAcceptByte()andAcceptByte(value)HasCommand()andCompleteCommand()EnableFreeText(),DisableFreeText(),GetLineLength(), andGetLineByte(index)Reset()
Compact input and lifecycle:
CanAcceptByte()andAcceptByte(value)HasLine(),GetLineLength(),GetLineByte(index), andCompleteLine()EnableEcho()andDisableEcho()HasOverflow()andReset()CommandEquals(expected, expectedLength)TryWriteByte(value),HasOutput(),PeekOutput(), andConsumeOutput()
Parsing:
CommandEquals(expected)andGetCommandLength()GetArgumentCount()GetArgumentLength(index)andGetArgumentByte(index, offset)ArgumentEquals(index, expected)
Output:
CanWrite(length)TryWriteByte(value),TryWrite(data), andTryWriteLine(data)TryWriteArgumentsLine()HasOutput(),PeekOutput(), andConsumeOutput()GetOutputCount()
- Command lines contain at most 64 printable bytes.
- At most eight arguments are retained; excess arguments reject the line.
- Free-text mode retains the complete 64-byte line and allows excess argument words to reach the application; only the first eight remain available through the argument API.
- Parsing is case-sensitive.
- Spaces and tabs separate arguments; quoting and escaping are not implemented.
- The output FIFO holds 64 bytes, so one atomic
TryWriteLine()payload can be at most 62 bytes when the FIFO is empty. - Applications must drain output while receiving echoed input.
- Dynamic command registration is intentionally outside the hardware model.
livt validate
livt testThe test suite covers complete FIFO ordering and wraparound, atomic writes, line
endings, editing, overflow recovery, parsing, argument limits, prompts, enabled
and disabled echo, input backpressure, command completion, compact exact-command
matching, compact echo and streaming-output backpressure, and buffered UART
acceptance/retry plus serial loopback reception with Livt.IO 1.2.0-dev.
MIT. See LICENSE.