Bytestream Library
Table of contents
- Format Specifiers
- Formatting and Parsing
- Bytestream Client
- Usage with DTYP Device Support
- Comparison with StreamDevice
The bytestream library provides scanf-style parsing and printf-style formatting for byte stream device communication. It is a pure Lua library built on top of LPeg and asyn.client.
Format specifiers follow StreamDevice conventions, making it straightforward to translate protocol files into Lua code.
The bytestream library is installed to
lib/<arch>/. CallluaAddModulein your startup script to make it available viarequire.
local bs = require("bytestream")
Format Specifiers
All format specifiers use the syntax %[flags][width][.precision]<specifier>.
Specifiers
| Specifier | Read (match) | Write (format) | Description |
|---|---|---|---|
%s | non-whitespace chars | tostring(v) | String |
%c | any characters | first char (or width chars) | Character |
%d | signed decimal | string.format("%d") | Signed integer |
%u | unsigned decimal | string.format("%u") | Unsigned integer |
%o | octal digits | string.format("%o") | Octal integer |
%x | hex digits (with optional 0x prefix) | string.format("%x") | Hexadecimal integer |
%f | fixed-point float | string.format("%f") | Fixed-point float |
%e | scientific notation | string.format("%e") | Scientific notation |
%g | general float | string.format("%g") | General float |
%E | scientific (uppercase) | string.format("%E") | Uppercase scientific |
%G | general (uppercase) | string.format("%G") | Uppercase general |
%b | binary digits (01) | integer to binary string | Binary integer |
%r | raw N bytes (width = count, default 1) | passthrough | Raw bytes |
%{a\|b\|c} | match enum string, return 0-based index | index to enum string | Enumeration |
%% | literal % | literal % | Escape |
Flags
| Flag | Read effect | Write effect |
|---|---|---|
* | Match but discard (no capture) | Skip conversion, consume no argument |
- | Allow negative sign on %o, %x | Left-align output |
0 | (none) | Zero-pad output |
# | Allow spaces after sign | (reserved) |
? | Lenient: return default on no match | (reserved) |
! | Width is exact, not maximum | (reserved) |
Width and Precision
On the read side, width limits the maximum characters consumed. With the ! flag, width becomes an exact count.
On the write side, width and precision work like string.format:
bs.format("%10d", 42) -- " 42"
bs.format("%-10d", 42) -- "42 "
bs.format("%05d", 42) -- "00042"
bs.format("%.2f", 3.14159) -- "3.14"
bs.format("%08b", 42) -- "00101010"
Enumerations
Enum specifiers use {value0|value1|value2} syntax with 0-based indexing. Longest match is tried first to avoid prefix conflicts.
bs.match("%{off|on|standby}", "standby") -- returns 2
bs.format("%{off|on|standby}", 1) -- returns "on"
Formatting and Parsing
bytestream.match
Parse an input string using a format specifier.
bytestream.match (format, input)
Extracts values from the input string according to the format. Multiple conversions produce multiple return values.
local n = bs.match("%d", "42")
local x, y = bs.match("%d %d", "10 20")
local v, u = bs.match("VOLTS %f %s", "VOLTS 3.14 V")
local val = bs.match("%*s %d", "skip 42")
| Parameter | Type | Description |
|---|---|---|
| format | string | Format specifier with % conversions and literal text. |
| input | string | The string to parse. |
Returns: the extracted values, or nil if the input does not match.
Raises: an error if the format specifier is invalid.
bytestream.format
Format values into a string.
bytestream.format (format, ...)
Produces an output string from the format specifier and values.
local s = bs.format("%d", 42) -- "42"
local s = bs.format("X=%d Y=%d", 10, 20) -- "X=10 Y=20"
local s = bs.format("%{off|on|standby}", 2) -- "standby"
local s = bs.format("%08b", 42) -- "00101010"
| Parameter | Type | Description |
|---|---|---|
| format | string | Format specifier with % conversions and literal text. |
| … | varies | Values to format, consumed left-to-right. |
Returns: the formatted string.
Raises: an error if there are not enough arguments or an enum index is out of range.
bytestream.add_format
Register a custom format specifier.
bytestream.add_format (specifier)
Adds a new format letter for use with match and format. The specifier table must contain an identifier and either or both of read and write.
bs.add_format {
identifier = "B",
read = function(flags)
local lpeg = require("lpeg")
return lpeg.P("true") * lpeg.Cc(true)
+ lpeg.P("false") * lpeg.Cc(false)
end,
write = function(flags)
return function(value)
return value and "true" or "false"
end
end,
}
| Parameter | Type | Description |
|---|---|---|
| specifier | table | Table with identifier (string), read (function), and/or write (function). |
Bytestream Client
bytestream.client
Create a client for structured device I/O.
bytestream.client (portName [, addr])
Wraps an asyn.client with :write() and :read() methods that support format specifiers.
local dev = bs.client("SERIAL1")
dev.OutTerminator = "\n"
dev.InTerminator = "\n"
| Parameter | Type | Description |
|---|---|---|
| portName | string | A registered asyn port name. |
| addr | number | Optional. The asyn address. Default: 0. |
Returns: a bytestream client object.
client:write
Write data to the port.
client:write (data)
client:write (format, ...)
Writes a literal string or formats data using bytestream.format before writing. Returns self for chaining with :read().
dev:write("*RST")
dev:write("SET:VOLT %.3f", 3.300)
local val = dev:write("MEAS?"):read("%f")
| Parameter | Type | Description |
|---|---|---|
| data/format | string | Literal string, or format specifier if additional arguments follow. |
| … | varies | Values to format. |
Returns: the client object (for chaining).
Raises: an error on write failure.
The
OutTerminatoris appended automatically by the asyn port. Do not include terminators in the write data.
client:read
Read and optionally parse data from the port.
client:read ()
client:read (format)
Reads from the port. If a format string is given, parses the response using bytestream.match and returns the extracted values.
local raw = dev:read()
local temp = dev:read("%f")
local v, a = dev:read("%f %f")
local status = dev:write("STAT?"):read("%{off|on|standby}")
| Parameter | Type | Description |
|---|---|---|
| format | string | Optional. Format specifier for parsing. |
Returns: the parsed values, or the raw string if no format given.
Raises: an error if no data is read or the asyn read fails.
client:flush
Flush the input buffer.
client:flush ()
Returns: the client object (for chaining).
Client Properties
Properties delegated to the underlying asyn.client:
| Property | Type | Description |
|---|---|---|
InTerminator | string | Get or set the input end-of-string terminator. |
OutTerminator | string | Get or set the output end-of-string terminator. |
ReadTimeout | number | Get or set the read timeout in seconds (default 1.0). |
WriteTimeout | number | Get or set the write timeout in seconds (default 1.0). |
portName | string | Read-only. The port name. |
addr | number | Read-only. The address. |
Usage with DTYP Device Support
The bytestream library is commonly used in DTYP “lua” callbacks for instrument communication. Errors raised by bytestream are caught by device support and mapped to record alarm states.
Call
luaAddModule("$(LUA)")in your startup script to makerequire("bytestream")available in device support states.
Example: SCPI Temperature Sensor
Each load creates a separate Lua state registered under the port name, allowing multiple instances without collisions.
local db = require("db")
local bs = require("bytestream")
luaRegisterState(PORT)
local client = bs.client(PORT)
client.OutTerminator = "\n"
client.InTerminator = "\n"
db.record("ai", P .. "Temperature") {
DTYP = "lua",
INP = "@" .. PORT .. " read_temp()",
SCAN = "1 second",
EGU = "degC",
PREC = "2",
}
function read_temp()
return client:write("MEAS:TEMP?"):read("%f")
end
Startup:
luaAddModule("$(LUA)")
drvAsynIPPortConfigure("SENSOR1", "192.168.1.100:5025")
luaLoadFile("sensor.lua", {P="dev1:", PORT="SENSOR1"})
Comparison with StreamDevice
| Feature | StreamDevice | bytestream |
|---|---|---|
| Language | Protocol file (.proto) | Lua script |
| Format specifiers | %f, %d, %s, etc. | Same syntax |
| Enum syntax | %{val0\|val1\|val2} | Same syntax |
| Separator handling | Separator field | Literal text in format string |
| Terminators | Terminator field | client.OutTerminator / client.InTerminator |
| I/O direction | out, in protocol commands | client:write(...), client:read(...) |
| Conditional logic | Limited (@init, @mismatch) | Full Lua control flow |