diff --git a/.clang-format b/.clang-format index 682ae09..d896a25 100644 --- a/.clang-format +++ b/.clang-format @@ -3,3 +3,7 @@ IndentWidth: 4 AllowShortFunctionsOnASingleLine: None KeepEmptyLinesAtTheStartOfBlocks: false BreakBeforeBraces: Allman +IndentCaseLabels: true +ColumnLimit: 144 +SortIncludes: false +SkipMacroDefinitionBody: true diff --git a/README.md b/README.md index e3ab066..94eaf59 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,9 @@ when used in combination with [tmux](https://tmux.github.io). * Useful for reconnecting when serial device has no serial device by ID * Support for non-standard baud rates * Support for mark and space parity - * X-modem (1K/CRC) and Y-modem file upload + * X-modem (1K/CRC/Checksum) and Y-modem file upload * Support for RS-485 mode + * Support for raw mode and switching by key operations * List available serial devices * By device * Including topology ID, uptime, driver, description @@ -59,7 +60,7 @@ when used in combination with [tmux](https://tmux.github.io). * Switchable independent input and output * Normal mode * Hex mode (output supports variable width) - * Line mode (input only) + * Line mode (input only, it supports line-editing and history) * Timestamp support * Per line in normal output mode * Output timeout timestamps in hex output mode @@ -87,6 +88,7 @@ when used in combination with [tmux](https://tmux.github.io). * Remapping of prefix key * Lua scripting support for automation * Run script manually or automatically at connect (once/always/never) + * Run script and preload functions and variables when tio starts up * Simple expect/send like functionality with support for regular expressions * Manipulate port modem lines (useful for microcontroller reset/boot etc.) * Send files via x/y-modem protocol @@ -116,12 +118,14 @@ Options: -p, --parity odd|even|none|mark|space Parity (default: none) -o, --output-delay Output character delay (default: 0) -O, --output-line-delay Output line delay (default: 0) + --output-line-delay-char cr|lf Output line delay trigger character (default: lf) --line-pulse-duration Set line pulse duration -a, --auto-connect new|latest|direct Automatic connect strategy (default: direct) --exclude-devices Exclude devices by pattern --exclude-drivers Exclude drivers by pattern --exclude-tids Exclude topology IDs by pattern -n, --no-reconnect Do not reconnect + -N, --no-tty-restore Do not restore initial TTY device settings -e, --local-echo Enable local echo --input-mode normal|hex|line Select input mode (default: normal) --output-mode normal|hex|hexN Select output mode (default: normal) @@ -135,16 +139,21 @@ Options: --log-append Append to log file --log-strip Strip control characters and escape sequences -m, --map Map characters + --keymap Set key-script mappings -c, --color 0..255|bold|none|list Colorize tio text (default: bold) -S, --socket Redirect I/O to socket + --raw off|on|on-nodelay Select raw mode for non-interactive use (default: on) + --raw-interactive off|on|on-nodelay Select raw mode for interactive use (default: off) --rs-485 Enable RS-485 mode --rs-485-config Set RS-485 configuration --alert bell|blink|none Alert on connect/disconnect (default: none) --mute Mute tio messages + --script-init-file Set initial script file to run at startup --script Run script from string --script-file Run script from file --script-run once|always|never Run script on connect (default: always) --exec Execute shell command with I/O redirected to device + --complete-profiles Prints profiles (for shell completion) -v, --version Display version -h, --help Display help @@ -312,6 +321,9 @@ ctrl-t ? to list the available key commands. [15:02:53.269] ctrl-t F Flush data I/O buffers [15:02:53.269] ctrl-t g Toggle serial port line [15:02:53.269] ctrl-t i Toggle input mode +[15:02:53.269] ctrl-t j Toggle raw mode for non-interactive use +[15:02:53.269] ctrl-t J Toggle raw mode for interactive use +[15:02:53.269] ctrl-t k Set key-script mappings [15:02:53.269] ctrl-t l Clear screen [15:02:53.269] ctrl-t L Show line states [15:02:53.269] ctrl-t m Change mapping of characters on input or output @@ -329,6 +341,10 @@ ctrl-t ? to list the available key commands. ``` If needed, the prefix key (ctrl-t) can be remapped via configuration file. +And you can also map scripts as user key commands using keymap entries in the configuration file. + +When key commands request line input, you can edit the line and call the history by using the cursor keys and backspace key. +The history is maintained while tio is running. ### 3.3 Configuration file @@ -353,6 +369,9 @@ databits = 8 parity = none stopbits = 1 color = 10 +script-init-file = /home/user/.tio-init-script.lua +# ctrl-t 1 runs setup-script.lua and ctrl-t 9 runs the tio file transfer built-in with xmodem-checksum. +keymap = @1=setup-device.lua @9=!tio.send("firmfile", tio.C.XM_SUM) [rpi3] device = /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 @@ -395,20 +414,28 @@ Another more elaborate configuration file example is available [here](examples/c Tio suppots Lua scripting to easily automate interaction with the tty device. -In addition to the standard Lua API tio makes the following functions -and variables available: - +In addition to the standard Lua API tio makes the functions and variables available +The following are representative. See the man page for the complete list.: #### `tio.expect(pattern, timeout)` Waits for the Lua pattern to match or timeout before continuing. -Timeout is in milliseconds, defaults to 0 meaning it will wait forever. +Timeout is in milliseconds, defaults to tio.C.WAIT_FOREVER(==0) meaning it will wait forever. -Returns the captures from the pattern or `nil` on timeout. +Returns the captures from the pattern and all received data if matched. +Or return nil and all received data if timeout. + +#### `tio.expects(pattern-table, timeout)` + +Waits for any of the multiple Lua pattens to match or timeout before continuing. +Timeout is in milliseconds, defaults to tio.C.WAIT_FOREVER(==0) meaning it will wait forever. + +Returns the index of the matched pattern, the captures from it and all recieved data. +Or return nil, nil and all received data if timeout. #### `tio.read(size, timeout)` -Read up to `size` bytes from serial device. If timeout is 0 or not provided it +Read up to `size` bytes from serial device. If timeout is tio.C.WAIT_FOREVER(==0) or not provided it will wait forever until data is ready to read. Returns a string up to `size` bytes long on success and `nil` on timeout. @@ -423,15 +450,29 @@ line may be returned as a second return value. #### `tio.write(string)` -Write string to serial device. +Write string to serial device without any of input-editing, output-mapping or output-delay. -Returns the `tio` table. +Returns the tio table on success or nil on error. + +#### `tio.twrite(string)` + +Write string to serial device with input-editing, output-mapping and output-delay. + +Returns the tio table on success or nil on error. #### `tio.send(file, protocol)` Send file using x/y-modem protocol. -Protocol can be any of `XMODEM_1K`, `XMODEM_CRC`, `YMODEM`. +Protocol can be any of `XMODEM_1K`, `XMODEM_CRC`, `XMODEM_SUM`, `YMODEM`. +Alternatively, it can be one of tio.C.XM_1K, tio.C.XM_CRC, tio.C.XM_SUM, or tio.C.YM_NORMAL. + +#### `tio.receive(file, protocol)` + +eceive a file using the XMODEM protocol. + +protocol can be one of XMODEM_CRC or XMODEM_SUM. +Alternatively, use tio.C.XM_CRC or tio.C.XM_SUM. #### `tio.ttysearch()` @@ -450,6 +491,7 @@ Set state of one or multiple tty modem lines. Line can be any of `DTR`, `RTS`, `CTS`, `DSR`, `CD`, `RI` State is `high`, `low`, or `toggle`. +Alternatively, it can be one of tio.C.LN_HIGH, tio.C.LN_LOW, tio.C.LN_TOGGLE. #### `tio.sleep(seconds)` @@ -459,6 +501,45 @@ Sleep for seconds. Sleep for milliseconds. +#### `tio.inkey(timeout)` + +Read a key press as a string. + +timeout is specified in milliseconds. +Defaults to tio.C.NOWAIT (==-1), meaning the call returns immediately. +If set to tio.C.WAIT_FOREVER (==0), the function blocks until a key is pressed. + +Returns nil on timeout. + +#### `tio.input(prompt)` + +Display a prompt and read user input until Enter is pressed. +Basic editing is supported (Backspace key). + +If prompt is omitted, no prompt is displayed. +Returns the entered string. + +#### `tio.inputline(prompt)` + +Display a prompt and read a line of input until Enter is pressed. +Supports line editing (cursor keys, Backspace) and command history. + +Returns the entered string. + +#### `tio.set_keymap(keymaps)` + +Add, update, or remove key mappings. + +#### `tio.subcmd_println(fmt, ...)` + +Print a formatted line using sub-command style output +(e.g. [] ). + +#### `tio.subcmd_puts(string)` + +Print string using sub-command style output. +(e.g. [] ). + #### `tio.alwaysecho` A boolean value, defaults to `true`. @@ -530,7 +611,7 @@ Note: The meson install steps may differ depending on your specific system. Getting permission access errors trying to open your serial device? Add your user to the group which allows serial device access permanently. For example, to add your user to the 'dialout' group do: -```bash +ppp```bash sudo usermod -a -G dialout ``` Switch to the "dialout" group, temporary but immediately for this session. diff --git a/docs/interactive-lua-scripting.md b/docs/interactive-lua-scripting.md new file mode 100644 index 0000000..010d513 --- /dev/null +++ b/docs/interactive-lua-scripting.md @@ -0,0 +1,216 @@ +# Interactive Lua Scripting + +## Introduction + +Tio provides an **interactive Lua scripting interface** that allows scripts and Lua commands to be executed while Tio is running. + +In earlier versions, the script interpreter was restarted for each script execution. +The interpreter now **remains active**, preserving its state across executions. + +This makes it possible to: + +* run Lua commands interactively +* reuse functions defined in previously loaded scripts +* experiment with Lua code without restarting the interpreter +* avoid repeated initialization overhead when working with large scripts + +--- + +## Starting Interactive Script Mode + +To start interactive script mode, press: + +**Ctrl-t r** + +Tio displays the script prompt: + +``` +[08:45:46.514] Run Lua script +[08:45:46.514] Enter file name or "!" Lua commands or "@" interpreter directive: +>> +``` + +At this prompt you can: + +| Input | Description | +| ------------ | ---------------------------------------- | +| `filename` | Execute a Lua script file | +| `!commands` | Execute Lua commands | +| `@directive` | Execute an interpreter control directive | + +You can use the cursor keys and backspace key to edit the line and recall command history. +The history is maintained while tio is running. + +--- + +## Running a Script File + +To execute a Lua script file, enter the filename at the prompt. + +Example script (`banner.tio`): + +```lua +function tio.banner() + local banner = [[ + _ _ _ _ __ +| |_ (_) ___ ___ | |_ __ _ _ __ | |_ _ \ \ +| __|| | / _ \ / __|| __|/ _` || '__|| __| (_)_____ | | +| |_ | || (_) | \__ \| |_| (_| || | | |_ _ _|_____|| | + \__||_| \___/ |___/ \__|\__,_||_| \__|(_) (_) | | + /_/ +]] + tio.echo(string.gsub(banner, "\n", "\r\n")) +end + +tio.banner() +``` + +Run the script: + +``` +>> banner.tio +[09:08:13.786] Running script banner.tio + _ _ _ _ __ +| |_ (_) ___ ___ | |_ __ _ _ __ | |_ _ \ \ +| __|| | / _ \ / __|| __|/ _` || '__|| __| (_)_____ | | +| |_ | || (_) | \__ \| |_| (_| || | | |_ _ _|_____|| | + \__||_| \___/ |___/ \__|\__,_||_| \__|(_) (_) | | + /_/ +``` + +The script is loaded and executed immediately. +Any functions defined in the script remain available in the interpreter. + +--- + +## Executing Lua Commands + +Lua commands can be executed directly by prefixing them with `!`. + +This allows you to interact with previously defined functions or variables. + +Example script (`calc_sum.tio`): + +```lua +function tio.calc_sum(t) + local sum = 0 + for _, v in ipairs(t) do + sum = sum + v + end + return sum +end + +function tio.disp_sum(t) + local sum = tio.calc_sum(t) + tio.echo(sum) + tio.echo("\r\n") +end +``` + +Example session: + +``` +>> calc_sum.tio +[09:15:22.372] Running script calc_sum.tio + +<> +>> !t = {1,2,3,4,5,6,7,8,9,10} + +<> +>> !tio.disp_sum(t) +55 +>> !sum = tio.calc_sum(t); tio.echo(sum); tio.echo("\r\n") +55 +``` + +--- + +## Interpreter Directives + +Interpreter behavior can be controlled using directives prefixed with `@`. + +### `@new` + +Restart the script interpreter. + +All interpreter state is cleared and the interpreter is reinitialized. + +If a `script-init-file` is configured, it is executed again. + +--- + +### `@doopt` + +Execute the script specified by the `script` or `script-file` option, if present. + +--- + +### `@nuldo=opt` + +If **Ctrl-t r** is pressed and **Enter** is pressed without entering a command, `@doopt` is executed. + +This is the default behavior for compatibility with Tio v3.9. + +--- + +### `@nuldo=none` + +If **Ctrl-t r** is pressed and **Enter** is pressed without entering a command, no action is performed. + +This helps prevent accidental script execution. + +--- + +### `@repl` + +Start **REPL (Read–Eval–Print Loop)** mode. + +--- + +## REPL Mode + +Enter: + +``` +@repl +``` + +to start REPL mode. + +In REPL mode: + +* Lua commands can be entered continuously +* each line is executed immediately +* multi-line statements can be continued using a trailing `\` +* `@exit` leaves REPL mode + +Example: + +``` +[10:17:30.215] Run Lua script +[10:17:30.215] Enter file name or "!" Lua commands or "@" interpreter directive: +>> @repl +[10:17:31.956] Enter Lua REPL mode (@exit to exit) +-> p=1 +-> for i=1,10 do\ +-> p = p * i\ +-> end +-> print(p, "\r") +3628800 +-> @exit +``` + +--- + +## Summary + +The interactive Lua scripting interface provides: + +* persistent interpreter state +* interactive Lua command execution +* script reuse without interpreter restart +* an integrated REPL environment + +These capabilities make it easier to develop, test, and experiment with Tio scripts while the program is running. + +Enjoy using Tio's Interactive Lua Scripting. diff --git a/examples/config/config b/examples/config/config index 7bd141e..f70b1b1 100644 --- a/examples/config/config +++ b/examples/config/config @@ -17,8 +17,10 @@ stopbits = 1 parity = none output-delay = 0 output-line-delay = 0 +output-line-delay-char = lf auto-connect = direct no-reconnect = false +no-tty-restore = false local-echo = false input-mode = normal output-mode = normal diff --git a/examples/lua/expects.lua b/examples/lua/expects.lua new file mode 100644 index 0000000..9cd72ae --- /dev/null +++ b/examples/lua/expects.lua @@ -0,0 +1,21 @@ +-- +-- example of intaction with AT modem. +-- +tio.write("AT\r") +local matches, all = tio.expect("OK", 1000) +if matches == nil then + tio.echo("no response 1\r\n") + os.exit(0) +end +msleep(200) +tio.read(1000, tio.C.NOWAIT) +tio.write("ATFANTASYCMD\r") +local idx, matches, all = tio.expects({"OK", "ERROR", "BUSY"}, 1000) +if idx == nil then + tio.echo("no response 2\r\n") + os.exit(0) +end + +-- this display 2, ERROR +print(idx, matches[1]) +os.exit(0) diff --git a/examples/lua/tio-script-init.lua b/examples/lua/tio-script-init.lua new file mode 100644 index 0000000..30dfe07 --- /dev/null +++ b/examples/lua/tio-script-init.lua @@ -0,0 +1,41 @@ +-- +-- tio's script-init file +-- +-- Please add following to your tio config file. +-- +-- script-init-file = /<>/.tio-scipt-init.lua +-- +-- note: tio module is already loaded. +-- + +function tio.conv_lf_to_crlf(str) + local new_str = str:gsub("\n", "\r\n") + return new_str +end + +function tio.lprint(...) + local args = {...} + local argN = #args + for i = 1, argN - 1 do + io.write(tio.conv_lf_to_crlf(tostring(args[i]))) + io.write("\t") + end + if argN > 0 then + io.write(tio.conv_lf_to_crlf(tostring(args[argN]))) + end + io.write("\r\n") +end + +function tio.banner() + local banner = [[ + _ _ _ _ __ +| |_ (_) ___ ___ | |_ __ _ _ __ | |_ _ \ \ +| __|| | / _ \ / __|| __|/ _` || '__|| __| (_)_____ | | +| |_ | || (_) | \__ \| |_| (_| || | | |_ _ _|_____|| | + \__||_| \___/ |___/ \__|\__,_||_| \__|(_) (_) | | + /_/]] + + tio.lprint(banner) +end + +tio.banner() diff --git a/examples/lua/twrite.lua b/examples/lua/twrite.lua new file mode 100644 index 0000000..adfe8e0 --- /dev/null +++ b/examples/lua/twrite.lua @@ -0,0 +1,22 @@ +-- +-- tio.twrite() uses output-mapping, output-delay and input-mode. +-- +-- Input-mode HEX is very slow, so it has limited use. +-- +-- This script sends +-- Hello. +-- This is Hex mode. +-- Bye. +-- + +tio.set_input_mode(tio.C.IM_NORMAL) +tio.twrite("Hello.\r\n") + +tio.set_input_mode(tio.C.IM_HEX) +tio.twrite("5468697320697320486578206d6f64652e0d0a") -- "This is Hex mode." + +tio.set_input_mode(tio.C.IM_LINE) +tio.twrite("Bye.\r\n") + +tio.set_input_mode(tio.C.IM_NORMAL) + diff --git a/examples/socket/bidir_cmd_helper.py b/examples/socket/bidir_cmd_helper.py new file mode 100755 index 0000000..6344058 --- /dev/null +++ b/examples/socket/bidir_cmd_helper.py @@ -0,0 +1,94 @@ +#!/usr/bin/python3 + +import sys +import subprocess +import shlex +import os + +# netcat (openbsd) +NC_CMD = "nc" +NC_OPT = "-UN" + +def connect_and_execute_bidirectional_command(socket_path, command): + TIMEOUT_SEC = 600 + + # Sanitize user input (command stays as a string, but avoid passing raw input directly) + safe_cmd = " ".join(shlex.split(command)) + + # Launch nc process (connect to the Unix domain socket) + p_nc = subprocess.Popen( + [NC_CMD, NC_OPT, socket_path], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + text=False, # binary transfer + ) + + # Launch the bidirectional command connected to nc + p_bidir_cmd = subprocess.Popen( + safe_cmd, # keep string command for shell=True + stdin=p_nc.stdout, + stdout=p_nc.stdin, + stderr=None, + shell=True, + text=False, + ) + + # Close unused pipe ends in the parent process to propagate SIGPIPE properly + p_nc.stdout.close() + p_nc.stdin.close() + + try: + p_bidir_cmd.communicate(timeout=TIMEOUT_SEC) + + except subprocess.TimeoutExpired: + # Ensure the command is killed on timeout + p_bidir_cmd.kill() + + except Exception as e: + # Print unexpected exceptions for debugging + print(type(e), file=sys.stderr) + + # Check bidirectional command exit code + if p_bidir_cmd.returncode != 0: + print(f"command exited with {p_bidir_cmd.returncode}", file=sys.stderr) + + # Ensure nc is terminated regardless of exceptions + try: + p_nc.terminate() + p_nc.wait(timeout=5) + except subprocess.TimeoutExpired: + # Force kill if graceful shutdown fails + p_nc.kill() + except Exception as e: + print(type(e), file=sys.stderr) + + # Read and report stderr of nc + nc_stderr = p_nc.stderr.read() + if nc_stderr: + print("nc stderr:", nc_stderr.decode(errors='ignore'), file=sys.stderr) + + return p_bidir_cmd.returncode + + +def main(): + script = os.path.basename(__file__) + usage = f"Usage: {script} " + example = f"Example: {script} /tmp/tio-socket0 \"sz -b -p sample.bin\"" + note = f"Note: Please run \"tio -S \" beforehand." + + if len(sys.argv) != 3: + print(usage, file=sys.stderr) + print(example, file=sys.stderr) + print(note, file=sys.stderr) + return 1 + + socket_path = sys.argv[1] + bidirectional_command = sys.argv[2] + return connect_and_execute_bidirectional_command(socket_path, bidirectional_command) + + +if __name__ == '__main__': + exit_code = main() + sys.exit(exit_code) diff --git a/examples/socket/bidir_cmd_helper.sh b/examples/socket/bidir_cmd_helper.sh new file mode 100755 index 0000000..5459721 --- /dev/null +++ b/examples/socket/bidir_cmd_helper.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +if [ $# -lt 2 ]; then + echo "Usage: $0 " + echo "Example: $0 /tmp/tio-socket0 \"sz -b -p sample.bin\"" + echo "Note: Please run \"tio -S \" beforehand." + exit 1 +fi + +SOCKET_PATH=$1 +BIDIR_CMD=$2 + +socat EXEC:"$BIDIR_CMD" $SOCKET_PATH + +exit $? diff --git a/examples/socket/pexpect-ping.py b/examples/socket/pexpect-ping.py new file mode 100755 index 0000000..4323849 --- /dev/null +++ b/examples/socket/pexpect-ping.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 +# In MSYS2, use /mingw64/bin/python3 +# +# Send a "Ping" and wait for a "Pong". +# Repeat this process. +# + +import pexpect +from pexpect import popen_spawn + +child = pexpect.popen_spawn.PopenSpawn("nc -UN /tmp/tio-socket0") + +cnt = 0 +while True: + try: + child.sendline(f"Ping {cnt:d}") + cnt += 1 + child.expect(r'Pong \d+[\r\n]+', timeout = 10) + except Exception as e: + print(type(e)) + break + diff --git a/examples/socket/pexpect-pong.py b/examples/socket/pexpect-pong.py new file mode 100755 index 0000000..8f93c96 --- /dev/null +++ b/examples/socket/pexpect-pong.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 +# In MSYS2, use /mingw64/bin/python3 +# +# wait for a "Ping" and Send a "Pong" +# Repeat this process. +# + +import pexpect +from pexpect import popen_spawn + +child = pexpect.popen_spawn.PopenSpawn("nc -UN /tmp/tio-socket1") + +cnt = 0 +while True: + try: + child.expect(r'Ping \d+[\r\n]+', timeout = 10) + child.sendline(f"Pong {cnt:d}") + cnt += 1 + except Exception as e: + print(type(e)) + break + diff --git a/man/tio.1.in b/man/tio.1.in index eb0338d..3f969e9 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -51,6 +51,11 @@ Set output delay [ms] inserted between each sent character (default: 0). Set output delay [ms] inserted between each sent line (default: 0). +.TP +.BR " \-\-output\-line\-delay\-char " cr | lf + +Set trigger character of output line delay (default: lf). + .TP .BR " \-\-line\-pulse\-duration " \fI @@ -123,6 +128,14 @@ Do not reconnect. This means that tio will exit if it fails to connect to device or an established connection is lost. +.TP +.BR \-N ", " \-\-no\-tty\-restore + +Do not restore initial TTY device settings. + +This means that tio will exit without trying to restore TTY device +settings that existed when tio was started. + .TP .BR \-e ", " "\-\-local\-echo @@ -205,8 +218,8 @@ Strip control characters and escape sequences from log. .TP .BR \-m ", " "\-\-map " \fI -Map (replace, translate) characters on input to the serial device or output -from the serial device. The following mapping flags are supported: +Map (replace, translate) characters on input from the serial device or output +to the serial device. The following mapping flags are supported: .RS .TP 12n @@ -236,7 +249,7 @@ Map lowercase characters to uppercase on output Map nul (zero) to send break signal on output .IP "\fBOIGNCR" Ignore CR on output -.P +.PP If defining more than one flag, the flags must be comma separated. .RE @@ -251,8 +264,9 @@ In hex input mode bytes can be sent by typing the \fBtwo-character hexadecimal\fR representation of the 1 byte value, e.g.: to send \fI0xA\fR you must type \fI0a\fR or \fI0A\fR. -In line input mode input characters are sent when you press enter. The only -editing feature supported in this mode is backspace. +In line input mode input characters are sent when you press enter. +You can use the cursor keys and backspace key to edit the line and recall command +history. The history is maintained while tio is running. Default value is "normal". @@ -301,12 +315,42 @@ Unix Domain Socket (file) Internet Socket (network) .IP "\fBinet6:" Internet IPv6 Socket (network) -.P +.PP If port is 0 or no port is provided default port 3333 is used. -.P +.PP At present there is a hardcoded limit of 16 clients connected at one time. .RE +.TP +.BR " \-\-raw " off|on|on-nodelay + +nSet raw mode for non-interactive use. +Non-interactive use is Piped-input / Shell command execution / XYMODEM transferring. + +The raw option can be set to one of the following: + +.RS +.TP 20n +.IP "\fBoff" +flow control, character mapping and output delay are enabled +.IP "\fBon" +software flow control and character mapping are disabled; output delay remains enabled +.IP "\fBon-nodelay" +software flow control, character mapping and output delay is disabled +.PP +Default value is "on". +.RE + +.TP +.BR " \-\-raw-interactive " off|on|on-nodelay + +Set raw mode for interactive use. +Interactive use is normal terminal input/output and socket redirection. + +This is useful when transferring files through socket redirection. + +Default value is "off". + .TP .BR " \-\-rs\-485" @@ -330,7 +374,7 @@ Set RTS delay (ms) before sending Set RTS delay (ms) after sending .IP \fBRX_DURING_TX Receive data even while sending data -.P +.PP If defining more than one key or key value pair, they must be comma separated. .RE @@ -349,15 +393,24 @@ Default value is "none". Mute tio messages. +.TP +.BR "\-\-script\-init\-file \fI" + +Run script from file with filename on tio's startup. + +Execution occurs before connecting to the device, and loaded functions and variables are preserved. + +The default is <>, which only loads built\-in functions and variables. + .TP .BR "\-\-script \fI -Run script from string. +Run script from string on connect. .TP .BR "\-\-script\-file \fI -Run script from file with filename. +Run script from file with filename on connect. .TP .BR "\-\-script\-run once|always|never" @@ -371,6 +424,11 @@ Default value is "always". Execute shell command with I/O redirected to device +The standard output and standard error of a shell command are redirected to the device through tio's output filters (with output mapping and output delay enabled), and input from the device is redirected to +the standard input of the shell command. + +If you specify '?' as a shell commands prefix, standard error output will not be redirected. This allows you to use some communication commands such as sz/rz. + .TP .BR "\-\-complete-profiles @@ -403,6 +461,10 @@ Flush data I/O buffers (discard data written but not transmitted and data receiv Toggle serial port line .IP "\fBctrl-t i" Toggle input mode +.IP "\fBctrl-t j" +Toggle raw mode for non-interactive use +.IP "\fBctrl-t J" +Toggle raw mode for interactive use .IP "\fBctrl-t l" Clear screen .IP "\fBctrl-t L" @@ -426,7 +488,7 @@ Toggle line timestamp mode .IP "\fBctrl-t v" Show version .IP "\fBctrl-t x" -Send file using the XMODEM-1K or XMODEM-CRC protocol (prompts for file name and protocol) +Send file using the XMODEM-1K, XMODEM-CRC or XMODEM-SUM protocol (prompts for file name and protocol) .IP "\fBctrl-t y" Send file using the YMODEM protocol (prompts for file name) .IP "\fBctrl-t ctrl-t" @@ -434,7 +496,7 @@ Send ctrl-t character .SH "SCRIPT API" .PP -Tio suppots Lua scripting to easily automate interaction with the tty device. +Tio supports Lua scripting to easily automate interaction with the tty device. In addition to the standard Lua API tio makes the following functions and variables available: @@ -442,33 +504,56 @@ and variables available: .TP 6n .IP "\fBtio.expect(pattern, timeout)" -Waits for the Lua pattern to match or timeout before continuing. -Timeout is in milliseconds, defaults to 0 meaning it will wait forever. +Waits for the Lua pattern to match or timeout before continuing. Special characters must be escaped with '\\' or '%'. +Timeout is in milliseconds, defaults to tio.C.WAIT_FOREVER(==0) meaning it will wait forever. -Returns the captures from the pattern or nil on timeout. +On success, returns the captures from the pattern and all received data. +On timeout, returns nil and all received data. + +.IP "\fBtio.expects(pattern-table, timeout)" +Waits for any of the multiple Lua patterns to match or timeout before continuing. Special characters must be escaped with '\\' or '%'. +Timeout is in milliseconds, defaults to tio.C.WAIT_FOREVER(==0) meaning it will wait forever. + +On success, returns the index of the matched pattern, the captures from it and all received data. +On timeout, returns nil, nil and all received data. .IP "\fBtio.read(size, timeout)" -Read up to size bytes from serial device. If timeout is 0 or not provided it -will wait forever until data is ready to read. +Read up to size bytes from serial device. If timeout is tio.C.WAIT_FOREVER(==0) or not provided it will wait forever until data is ready to read. +If the timeout is tio.C.NOWAIT (==-1), the function immediately reads as much data as possible from the serial device's RX buffer, up to a maximum of bytes, and returns. -Returns a string up to size bytes long on success and nil on timeout. +On success, returns read data as string. Also emits a single timestamp to stdout and log file per options.timestamp and options.log. +On timeout, returns nil and received data. .IP "\fBtio.readline(timeout)" -Read line from serial device. If timeout is 0 or not provided it will wait -forever until data is ready to read. +Read line from serial device. If timeout is tio.C.WAIT_FOREVER(==0) or not provided it will wait forever until data is ready to read. +The line separater is LF (0x0a). -Returns a string on success and nil on timeout. On timeout a partially read -line may be returned as a second return value. +On success, returns received line as string. Also emits a single timestamp to stdout and log file per options.timestamp and options.log. +On timeout, returns nil and received data. .IP "\fBtio.write(string)" -Write string to serial device. +Write string to serial device without input-editing, output-mapping, or output-delay. -Returns the tio table. +Returns the tio table on success or nil on error. + +.IP "\fBtio.twrite(string)" +Write string to serial device with input-editing, output-mapping and output-delay. + +Returns tio table on success or nil on error. .IP "\fBtio.send(file, protocol)" Send file using x/y-modem protocol. -Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. +Protocol can be any of XMODEM_1K, XMODEM_CRC, XMODEM_SUM, YMODEM. +It can alternatively be one of tio.C.XM_1K, tio.C.XM_CRC, tio.C.XM_SUM, tio.C.YM_NORMAL. + +.IP "\fBtio.receive(file, protocol)" +Receive a file using the XMODEM protocol. + +Protocol can be any of XMODEM_CRC or XMODEM_SUM. +It can alternatively be one of tio.C.XM_CRC or tio.C.XM_SUM. + +Returns the tio table on success or nil on error. .IP "\fBtio.ttysearch()" Search for serial devices. @@ -489,9 +574,159 @@ State is high, low, or toggle. .IP "\fBtio.sleep(seconds)" Sleep for seconds. + .IP "\fBtio.msleep(ms)" Sleep for milliseconds. +.IP "\fBtio.send_break()" +Send break signal. +It is equivalent to the key command ctrl-t b. + +.IP "\fBtio.line_get()" +Get state of multiple tty modem lines. +It is equivalent to the key command ctrl-t L. + +Return 6 values DTR, RTS, CTS, DSR, CD, RI. +Each return value is high (==tio.C.LN_HIGH) or low (==tio.C.LN_LOW). + +.IP "\fBtio.set_local_echo(on_off)" +Change the local echo setting. +It is equivalent to the key command ctrl-t e. + +The argument on_off is a boolean value. true means on and false means off. If omitted, it is set to true. + +.IP "\fBtio.set_log(on_off)" +Change the log-file setting. +It is equivalent to the key command ctrl-t f. + +The argument on_off is a boolean value. true means on and false means off. If omitted, it is set to true. + +.IP "\fBtio.flush_data_io_buffer()" +Flush read/write data in I/O buffers. +It is equivalent to the key command ctrl-t F. + +.IP "\fBtio.set_input_mode(input_mode)" +Change the input mode. +It is equivalent to the key command ctrl-t i. + +The argument input_mode is one of tio.C.IM_NORMAL, tio.C.IM_HEX, tio.C.IM_LINE. + +.IP "\fBtio.set_output_mode(output_mode)" +Change the output mode. +It is equivalent to the key command ctrl-t o. + +The argument output_mode is one of tio.C.OM_NORMAL, tio.C.OM_HEX. + +.IP "\fBtio.set_raw_mode(raw_mode)" +Change the raw mode for non-interactive use. +It is equivalent to the key command ctrl-t j. + +The argument raw_mode is one of tio.C.RAW_OFF, tio.C.RAW_ON, tio.C.RAW_ON_NODELAY. + +.IP "\fBtio.set_raw_mode_interactive(raw_mode)" +Change the raw mode for interactive use. +It is equivalent to the key command ctrl-t J. + +The argument raw_mode is one of tio.C.RAW_OFF, tio.C.RAW_ON, tio.C.RAW_ON_NODELAY. + +.IP "\fBtio.set_timestamp_mode(timestamp_mode)" +Change the timestamp mode. +It is equivalent to the key command ctrl-t t. + +The argument timestamp_mode is one of tio.C.TS_OFF, tio.C.TS_24HOUR, tio.C.TS_24HOUR_START, tio.C.TS_24HOUR_DELTA, tio.C.TS_ISO861, tio.C.TS_EPOCH, tio.C.TS_EPOCH_USEC. + +.IP "\fBtio.exec_shell_command(shell_commands)" +Execute /bin/sh -c <>. +Normally, standard output / standard error is forwarded to tio's output filter which do output mapping and output delay. +If the shell commands starts with '?', '?' is removed and standard error is not forwarded. +It is equivalent to the key command ctrl-t R. + +The argument shell_commands is string. + +.IP "\fBtio.get_state()" +Return the main state of tio as a integer. +Return value is one of tio.C.SA_INTERACTIVE, tio.C.SA_STARTING, tio.C.SA_PIPED_INPUT, tio.C.SA_PIPED_INPUT, tio.C.SA_EXEC_SHELL_COMMAND, tio.C.SA_XYMODEM. + +.IP "\fBtio.get_version()" +Return the version of tio as a string. +It is equivalent to the key command ctrl-t v. + +.IP "\fBtio.inkey(timeout)" +Read a key press and return it as a string. + +Timeout is in milliseconds. If timeout is tio.C.WAIT_FOREVER(==0), +the function blocks until a key is pressed. If timeout is +tio.C.NOWAIT (==-1) or not provided, the function returns +immediately. + +Returns the key as a string on success, or nil on timeout. + +.IP "\fBtio.input(prompt)" +Display a prompt and read user input until Enter is pressed. + +Basic line editing is supported (Backspace key). + +If prompt is not provided, no prompt is displayed. + +Returns the entered string. + +.IP "\fBtio.inputline(prompt)" +Display a prompt and read a line of input until Enter is pressed. + +Supports line editing (cursor keys, Backspace) and command history. + +Returns the entered string. + +.IP "\fBtio.set_keymap(keymaps)" +Add, update, or remove key mappings. + +The keymaps argument uses the same syntax as the --keymap option: + + @= + @= + ... + @= + +Each must be either a script filename or an +inline script prefixed with '!'. + +When a mapping is defined, pressing Ctrl-t followed by +executes the corresponding script. + +If a key already has a mapping, it is updated. If + is empty, the mapping is removed. + +User-defined key mappings take precedence over default key bindings, +except for "Ctrl-t q", which is always reserved. + +This function allows key mappings to be modified at runtime after tio +has started. + +.IP "\fBtio.subcmd_println(fmt, ...)" +Print a formatted line using sub-command style output. + +The output format is: + [] + +.IP "\fBtio.subcmd_warning_println(fmt, ...)" +Print a formatted warning line using sub-command style output. + +.IP "\fBtio.subcmd_error_println(fmt, ...)" +Print a formatted error line using sub-command style output. +.IP "\fBtio.subcmd_error_println(fmt, ...)" + +.IP "\fBtio.subcmd_puts(string)" +Print a string using sub-command style output. + +The output format is: + [] + +.IP "\fBtio.subcmd_warning_puts(string)" +Print a warning string using sub-command style output. + +.IP "\fBtio.subcmd_error_puts(string)" +Print an error string using sub-command style output. + .IP "\fBtio.alwaysecho" A boolean value, defaults to true. @@ -501,6 +736,10 @@ will only be returned from the function and not logged or printed. If tio.alwaysecho is set to true, reading functions also emit a single timestamp to stdout and log file per options.timestamp and options.log. +.IP "\fBos.exit(code)" +Exit tio process with exit code (like ctrl-t q). + + .SH "CONFIGURATION FILE" .PP Options can be set via configuration file using the INI format. \fBtio\fR uses @@ -550,6 +789,8 @@ Set parity Set output character delay .IP "\fBoutput-line-delay" Set output line delay +.IP "\fBoutput-line-delay-char" +Set trigger character of output line delay .IP "\fBline-pulse-duration" Set line pulse duration .IP "\fBno-reconnect" @@ -574,6 +815,8 @@ Set timestamp format Set timestamp timeout .IP "\fBmap" Map characters on input or output +.IP "\fBkeymap" +Set key-script mappings .IP "\fBcolor" Colorize tio text using ANSI color code ranging from 0 to 255 .IP "\fBinput-mode" @@ -592,12 +835,14 @@ Set RS-485 configuration Set alert action on connect/disconnect .IP "\fBmute" Mute tio messages +.IP "\fBscript-init-file" +Run script from file on tio's startup .IP "\fBscript" -Run script from string +Run script from string on connect .IP "\fBscript-file" -Run script from file +Run script from file on connect .IP "\fBscript-run" -Run script on connect +Set condition to run script on connect .IP "\fBexec" Execute shell command with I/O redirected to device @@ -772,6 +1017,31 @@ Manipulate DTR and RTS lines upon first connect to reset connected microcontroll $ tio --script "tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{RTS=toggle}" --script-run once /dev/ttyUSB0 +.TP +Manipulate DTR and RTS lines by pressing ctrl-t 1: + +$ tio --keymap '@1=!tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{RTS=toggle}' /dev/ttyUSB0 + +.TP +Send file to device by sz command: + +$ tio --exec '?sz -b file' /dev/ttyUSB0 + +.TP +Receive file from device by rz command: + +$ tio --exec '?rz -b' /dev/ttyUSB0 + +.TP +Send file to device by gkermit command: + +$ tio --exec '?gkermit -XSs file' /dev/ttyUSB0 + +.TP +Receive file from device by gkermit command: + +$ tio --exec '?gkermit -XSr' /dev/ttyUSB0 + .SH "WEBSITE" .PP Visit https://tio.github.io diff --git a/man/tio.1.txt b/man/tio.1.txt index eeac82c..4456ab2 100644 --- a/man/tio.1.txt +++ b/man/tio.1.txt @@ -40,6 +40,10 @@ OPTIONS Set output delay [ms] inserted between each sent line (default: 0). + --output-line-delay-char cr|lf + + Set trigger character of output line delay (default: lf). + --line-pulse-duration Set the pulse duration [ms] of each serial port line using the following key value pair format in the duration field: = @@ -94,6 +98,12 @@ OPTIONS This means that tio will exit if it fails to connect to device or an established connection is lost. + -N, --no-tty-restore + + Do not restore initial TTY device settings. + + This means that tio will exit without trying to restore TTY device settings that existed when tio was started. + -e, --local-echo Enable local echo. @@ -116,7 +126,7 @@ OPTIONS epoch Seconds since Unix epoch (1970-01-01) - epoch-usec Seconds since Unix epoch (1970-01-01) with subdivision microseconds + epoch-usec Seconds since Unix epoch (1970-01-01) with subdivision in microseconds Default format is 24hour @@ -158,7 +168,7 @@ OPTIONS -m, --map - Map (replace, translate) characters on input to the serial device or output from the serial device. The following mapping flags are supported: + Map (replace, translate) characters on input from the serial device or output to the serial device. The following mapping flags are supported: ICRNL Map CR to NL on input (unless IGNCR is set) @@ -188,6 +198,12 @@ OPTIONS If defining more than one flag, the flags must be comma separated. + --keymap + + Specify the mappings as @= @=... @=. + + Script-description is script-filename or '!'script-commands. + --input-mode normal|hex|line Set input mode. @@ -196,7 +212,7 @@ OPTIONS In hex input mode bytes can be sent by typing the two-character hexadecimal representation of the 1 byte value, e.g.: to send 0xA you must type 0a or 0A. - In line input mode input characters are sent when you press enter. The only editing feature supported in this mode is backspace. + In line input mode input characters are sent when you press enter. You can use the cursor keys and backspace key to edit the line and recall command history. The history is maintained while tio is running. Default value is "normal". @@ -239,6 +255,30 @@ OPTIONS At present there is a hardcoded limit of 16 clients connected at one time. + --raw off|on|on-nodelay + + Set raw mode for non-interactive use. + Non-interactive use is Piped-input / Shell command execution / XYMODEM transfering. + + The raw option can be set to one of the following: + + off flow control, character mapping and output delay are enabled + + on software flow control and character mapping are disabled; output delay remains enabled + + on-nodelay software flow control, character mapping and output delay is disabled + + Default value is "on". + + --raw-interactive off|on|on-nodelay + + Set raw mode for interactive use. + Interactive use is normal terminal input/output and socket redirection. + + This is useful when transferring files through socket redirection. + + Default value is "off". + --rs-485 Enable RS-485 mode. @@ -271,13 +311,21 @@ OPTIONS Mute tio messages. + --script-init-file + + Run script from file with filename on tio's startup. + + Execution occurs before connecting to the device, and loaded functions and variables are preserved. + + The default is <>, which only loads built-in functions and variables. + --script - Run script from string. + Run script from string on connect. --script-file - Run script from file with filename. + Run script from file with filename on connect. --script-run once|always|never @@ -289,6 +337,11 @@ OPTIONS Execute shell command with I/O redirected to device + The standard output and standard error of a shell command are redirected to the device through tio's output filters (with output mapping and output delay enabled), and input from the device is redirected to + the standard input of the shell command. + + If you specify '?' as a shell commands prefix, standard error output will not be redirected. This allows you to use some communication commands such as sz/rz. + --complete-profiles Prints profiles (for shell completion) @@ -320,6 +373,10 @@ KEY COMMANDS ctrl-t i Toggle input mode + ctrl-t j Toggle raw mode for non-interactive use + + ctrl-t J Toggle raw mode for interactive use + ctrl-t l Clear screen ctrl-t L Show line states (DTR, RTS, CTS, DSR, DCD, RI) @@ -342,49 +399,70 @@ KEY COMMANDS ctrl-t v Show version - ctrl-t x Send file using the XMODEM-1K or XMODEM-CRC protocol (prompts for file name and protocol) + ctrl-t x Send file using the XMODEM-1K, XMODEM-CRC or XMODEM-SUM protocol (prompts for file name and protocol) ctrl-t y Send file using the YMODEM protocol (prompts for file name) ctrl-t ctrl-t Send ctrl-t character SCRIPT API - Tio suppots Lua scripting to easily automate interaction with the tty device. + Tio supports Lua scripting to easily automate interaction with the tty device. In addition to the standard Lua API tio makes the following functions available: - expect(string, timeout) - Expect string - waits for string to match or timeout before continuing. Supports regular expressions. Special characters must be escaped with '\\'. Timeout is in milliseconds, defaults to 0 meaning it will wait forever. + tio.expect(pattern, timeout) + Waits for the Lua pattern to match or timeout before continuing. Special characters must be escaped with '\\' or '%'. + Timeout is in milliseconds, defaults to tio.C.WAIT_FOREVER(==0) meaning it will wait forever. - Returns 1 on successful match, 0 on timeout, or -1 on error. + On success, returns the captures from the pattern and all received data. + On timeout, returns nil and all received data. - On successful match it also returns the match string as second return value. + tio.expects(pattern-table, timeout) + Waits for any of the multiple Lua patterns to match or timeout before continuing. Special characters must be escaped with '\\' or '%'. + Timeout is in milliseconds, defaults to tio.C.WAIT_FOREVER(==0) meaning it will wait forever. - read(size, timeout) - Read up to size bytes from serial device. If timeout is 0 or not provided it will wait forever until data is ready to read. + On success, returns the index of the matched pattern, the captures from it and all received data. + On timeout, returns nil, nil and all received data. - Returns number of bytes read on success, 0 on timeout, or -1 on error. + tio.read(size, timeout) + Read up to size bytes from serial device. If timeout is tio.C.WAIT_FOREVER(==0) or not provided it will wait forever until data is ready to read. + If the timeout is tio.C.NOWAIT (==-1), the function immediately reads as much data as possible from the serial device's RX buffer, up to a maximum of bytes, and returns. - On success, returns read string as second return value. Also emits a single timestamp to stdout and log file per options.timestamp and options.log. + On success, returns read data as string. Also emits a single timestamp to stdout and log file per options.timestamp and options.log. + On timeout, returns nil and received data. - read_line(timeout) - Read line from serial device. If timeout is 0 or not provided it will wait forever until data is ready to read. + tio.readline(timeout) + Read line from serial device. If timeout is tio.C.WAIT_FOREVER(==0) or not provided it will wait forever until data is ready to read. + The line separater is LF (0x0a). - Returns number of bytes read on success, 0 on timeout, or -1 on error. + On success, returns received line as string. Also emits a single timestamp to stdout and log file per options.timestamp and options.log. + On timeout, returns nil and received data. - On success, returns the string that was read as second return value. Also emits a single timestamp to stdout and log file per options.timestamp and options.log. + tio.write(string) + Write string to serial device without input-editing, output-mapping nor output-delay. - write(string) - Write string to serial device. + Returns the tio table on success or nil on error. - Returns number of bytes written on success or -1 on error. + tio.twrite(string) + Write string to serial device with input-editing, output-mapping and output-delay. - send(file, protocol) + Returns the tio table on success or nil on error. + + tio.send(file, protocol) Send file using x/y-modem protocol. - Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. + Protocol can be any of XMODEM_1K, XMODEM_CRC, XMODEM_SUM, YMODEM. + It can alternatively be one of tio.C.XM_1K, tio.C.XM_CRC, tio.C.XM_SUM, tio.C.YM_NORMAL. - tty_search() + tio.receive(file, protocol) + Receive a file using the XMODEM protocol. + + Protocol can be any of XMODEM_CRC or XMODEM_SUM. + It can alternatively be one of tio.C.XM_CRC or tio.C.XM_SUM. + + Returns the tio table on success or nil on error. + + tio.ttysearch() Search for serial devices. Returns a table of number indexed tables, one for each serial device found. Each of these tables contains the serial device information accessible via the following string indexed elements "path", "tid", "uptime", "dri‐ @@ -392,21 +470,172 @@ SCRIPT API Returns nil if no serial devices are found. - set{line=state, ...} + tio.set{line=state, ...} Set state of one or multiple tty modem lines. Line can be any of DTR, RTS, CTS, DSR, CD, RI - State is high, low, or toggle. + State is high (==tio.C.LN_HIGH), low (==tio.C.LN_LOW), or toggle (==tio.C.LN_TOGGLE). - sleep(seconds) + tio.sleep(seconds) Sleep for seconds. - msleep(ms) + tio.msleep(ms) Sleep for milliseconds. - exit(code) - Exit with exit code. + tio.send_break() + Send break signal. + It is equivalent to the key command ctrl-t b. + + tio.line_get() + Get state of multiple tty modem lines. + It is equivalent to the key command ctrl-t L. + + Return 6 values DTR, RTS, CTS, DSR, CD, RI. + Each return value is high (==tio.C.LN_HIGH) or low (==tio.C.LN_LOW). + + tio.set_local_echo(on_off) + Change the local echo setting. + It is equivalent to the key command ctrl-t e. + + The argument on_off is a boolean value. true means on and false means off. If omitted, it is set to true. + + tio.set_log(on_off) + Change the log-file setting. + It is equivalent to the key command ctrl-t f. + + The argument on_off is a boolean value. true means on and false means off. If omitted, it is set to true. + + tio.flush_data_io_buffer() + Flush read/write data in I/O buffers. + It is equivalent to the key command ctrl-t F. + + tio.set_input_mode(input_mode) + Change the input mode. + It is equivalent to the key command ctrl-t i. + + The argument input_mode is one of tio.C.IM_NORMAL, tio.C.IM_HEX, tio.C.IM_LINE. + + tio.set_output_mode(output_mode) + Change the output mode. + It is equivalent to the key command ctrl-t o. + + The argument output_mode is one of tio.C.OM_NORMAL, tio.C.OM_HEX. + + tio.set_raw_mode(raw_mode) + Change the raw mode for non-interactive use. + It is equivalent to the key command ctrl-t j. + + The argument raw_mode is one of tio.C.RAW_OFF, tio.C.RAW_ON, tio.C.RAW_ON_NODELAY. + + tio.set_raw_mode_interactive(raw_mode) + Change the raw mode for interactive use. + It is equivalent to the key command ctrl-t J. + + The argument raw_mode is one of tio.C.RAW_OFF, tio.C.RAW_ON, tio.C.RAW_ON_NODELAY. + + tio.set_timestamp_mode(timestamp_mode) + Change the timestamp mode. + It is equivalent to the key command ctrl-t t. + + The argument timestamp_mode is one of tio.C.TS_OFF, tio.C.TS_24HOUR, tio.C.TS_24HOUR_START, tio.C.TS_24HOUR_DELTA, tio.C.TS_ISO861, tio.C.TS_EPOCH, tio.C.TS_EPOCH_USEC. + + tio.exec_shell_command(shell_commands) + Execute /bin/sh -c <>. + Normally, standard output / standard error is forwarded to tio's output filter which do output mapping and output delay. + If the shell commands starts with '?', '?' is removed and standard error is not forwarded. + It is equivalent to the key command ctrl-t R. + + The argument shell_commands is string. + + tio.get_state() + Return the main state of tio as a integer. + Return value is one of tio.C.SA_INTERACTIVE, tio.C.SA_STARTING, tio.C.SA_PIPED_INPUT, tio.C.SA_PIPED_INPUT, tio.C.SA_EXEC_SHELL_COMMAND, tio.C.SA_XYMODEM. + + tio.get_version() + Return the version of tio as a string. + It is equivalent to the key command ctrl-t v. + + tio.inkey(timeout) + Read a key press and return it as a string. + + Timeout is in milliseconds. If timeout is tio.C.WAIT_FOREVER(==0), + the function blocks until a key is pressed. If timeout is + tio.C.NOWAIT (==-1) or not provided, the function returns + immediately. + + Returns the key as a string on success, or nil on timeout. + + tio.input(prompt) + Display a prompt and read user input until Enter is pressed. + + Basic line editing is supported (Backspace key). + + If prompt is not provided, no prompt is displayed. + + Returns the entered string. + + tio.inputline(prompt) + Display a prompt and read a line of input until Enter is pressed. + + Supports line editing (cursor keys, Backspace) and command + history. + + Returns the entered string. + + tio.set_keymap(keymaps) + Add, update, or remove key mappings. + + The argument uses the same syntax as the --keymap option: + + @= + @= + ... + @= + + Each must be either a script filename or an inline script prefixed with '!'. + + When a mapping is defined, pressing Ctrl-T followed by executes the corresponding script. + + If a key already has a mapping, it will be updated. If is empty, the mapping is removed. + + User-defined key mappings take precedence over default key bindings, except for "Ctrl-T q", which is always reserved. + + This function can be used to dynamically modify key mappings at runtime after tio has started. + + tio.subcmd_println(fmt, ...) + Print a formatted line using sub-command style output. + + The output format is: + [] + + tio.subcmd_warning_println(fmt, ...) + Print a formatted warning line using sub-command style output. + + tio.subcmd_error_println(fmt, ...) + Print a formatted error line using sub-command style output. + + tio.subcmd_puts(string) + Print a string using sub-command style output. + + The output format is: + [] + + tio.subcmd_warning_puts(string) + Print a warning string using sub-command style output. + + tio.subcmd_error_puts(string) + Print an error string using sub-command style output. + + tio.alwaysecho + A boolean value, defaults to true. + + If tio.alwaysecho is false, the result of tio.read, tio.readline or tio.expect will only be returned from the function and not logged or printed. + If tio.alwaysecho is set to true, reading functions also emit a single timestamp to stdout and log file per options.timestamp and options.log. + + os.exit(code) + Exit tio process with exit code (like ctrl-t q). + CONFIGURATION FILE Options can be set via configuration file using the INI format. tio uses the configuration file first found in the following locations in the order listed: @@ -445,6 +674,8 @@ CONFIGURATION FILE output-line-delay Set output line delay + output-line-delay-char Set trigger character of output line delay + line-pulse-duration Set line pulse duration no-reconnect Do not reconnect @@ -469,6 +700,8 @@ CONFIGURATION FILE map Map characters on input or output + keymap Set key-script mappings + color Colorize tio text using ANSI color code ranging from 0 to 255 input-mode Set input mode @@ -487,11 +720,13 @@ CONFIGURATION FILE mute Mute tio messages - script Run script from string + script-init-file Run script from file on tio's startup - script-file Run script from file + script Run script from string on connect - script-run Run script on connect + script-file Run script from file on connect + + script-run Set condition to run script on connect exec Execute shell command with I/O redirected to device @@ -582,7 +817,7 @@ EXAMPLES It is also possible to use tio's own simpler expect/send script functionality to e.g. automate logins: - $ tio --script 'expect("login: "); write("root\n"); expect("Password: "); write("root\n")' /dev/ttyUSB0 + $ tio --script 'tio.expect("login: "); tio.write("root\n"); tio.expect("Password: "); tio.write("root\n")' /dev/ttyUSB0 Redirect device I/O to network file socket for remote TTY sharing: @@ -614,7 +849,27 @@ EXAMPLES Manipulate DTR and RTS lines upon first connect to reset connected microcontroller: - $ tio --script "set{DTR=high,RTS=low}; msleep(100); set{RTS=toggle}" --script-run once /dev/ttyUSB0 + $ tio --script "tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{RTS=toggle}" --script-run once /dev/ttyUSB0 + + Manipulate DTR and RTS lines by pressing ctrl-t 1: + + $ tio --keymap '@1=!tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{RTS=toggle}' /dev/ttyUSB0 + + Send file to device by sz command: + + $ tio --exec '?sz -b file' /dev/ttyUSB0 + + Receive file from device by rz command: + + $ tio --exec '?rz -b' /dev/ttyUSB0 + + Send file to device by gkermit command: + + $ tio --exec '?gkermit -XSs file' /dev/ttyUSB0 + + Receive file from device by gkermit command: + + $ tio --exec '?gkermit -XSr' /dev/ttyUSB0 WEBSITE Visit https://tio.github.io diff --git a/src/bash-completion/tio.in b/src/bash-completion/tio.in index b3b61cb..3d59d73 100644 --- a/src/bash-completion/tio.in +++ b/src/bash-completion/tio.in @@ -23,6 +23,7 @@ _tio() --exclude-drivers \ --exclude-tids \ -n --no-reconnect \ + -N --no-tty-restore \ -e --local-echo \ -l --log \ --log-file \ diff --git a/src/configfile.c b/src/configfile.c index ca116bf..a8ac75d 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -171,6 +171,13 @@ static void config_parse_keys(GKeyFile *key_file, char *group) config_get_integer(key_file, group, "output-delay", &option.output_delay, 0, INT_MAX); config_get_integer(key_file, group, "output-line-delay", &option.output_line_delay, 0, INT_MAX); + config_get_string(key_file, group, "output-line-delay-char", &string, NULL); + if (string != NULL) + { + option_parse_output_line_delay_char(string); + g_free((void *)string); + string = NULL; + } config_get_string(key_file, group, "line-pulse-duration", &string, NULL); if (string != NULL) { @@ -189,6 +196,7 @@ static void config_parse_keys(GKeyFile *key_file, char *group) config_get_string(key_file, group, "exclude-drivers", &option.exclude_devices, NULL); config_get_string(key_file, group, "exclude-tids", &option.exclude_devices, NULL); config_get_bool(key_file, group, "no-reconnect", &option.no_reconnect); + config_get_bool(key_file, group, "no-tty-restore", &option.no_tty_restore); config_get_bool(key_file, group, "local-echo", &option.local_echo); config_get_string(key_file, group, "input-mode", &string, "normal", "hex", "line", NULL); if (string != NULL) @@ -204,6 +212,20 @@ static void config_parse_keys(GKeyFile *key_file, char *group) g_free((void *)string); string = NULL; } + config_get_string(key_file, group, "raw", &string, NULL); + if (string != NULL) + { + option_parse_raw(string, &option.raw); + g_free((void *)string); + string = NULL; + } + config_get_string(key_file, group, "raw-interactive", &string, NULL); + if (string != NULL) + { + option_parse_raw(string, &option.raw_interactive); + g_free((void *)string); + string = NULL; + } config_get_bool(key_file, group, "timestamp", (bool*) &option.timestamp); if (option.timestamp != TIMESTAMP_NONE) { @@ -228,6 +250,13 @@ static void config_parse_keys(GKeyFile *key_file, char *group) g_free((void *)string); string = NULL; } + config_get_string(key_file, group, "keymap", &string, NULL); + if (string != NULL) + { + option_parse_key_mappings(string); + g_free((void *)string); + string = NULL; + } config_get_string(key_file, group, "color", &string, NULL); if (string != NULL) { @@ -272,6 +301,7 @@ static void config_parse_keys(GKeyFile *key_file, char *group) string = NULL; } config_get_bool(key_file, group, "mute", &option.mute); + config_get_string(key_file, group, "script-init-file", &option.script_init_filename, NULL); config_get_string(key_file, group, "script", &option.script, NULL); config_get_string(key_file, group, "script-file", &option.script_filename, NULL); config_get_string(key_file, group, "script-run", &string, NULL); diff --git a/src/main.c b/src/main.c index 6676bb3..3c2a47a 100644 --- a/src/main.c +++ b/src/main.c @@ -68,7 +68,7 @@ int main(int argc, char *argv[]) /* Configure input terminal */ if (isatty(fileno(stdin))) { - stdin_configure(); + stdin_configure(); } else { @@ -110,7 +110,8 @@ int main(int argc, char *argv[]) if (interactive_mode) { tio_printf("Press ctrl-%c q to quit", option.prefix_key); - } else + } + else { tio_printf("Non-interactive mode enabled"); tio_printf("Press ctrl-c to quit"); @@ -122,6 +123,12 @@ int main(int argc, char *argv[]) socket_configure(); } + /* Script interpreter init */ + script_interp_init(); + + /* Initialize tty module once on program start */ + tty_init(); + /* Spawn input handling into separate thread */ tty_input_thread_create(); diff --git a/src/meson.build b/src/meson.build index 05168f7..d9d206d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -53,6 +53,11 @@ if host_machine.system() == 'darwin' tio_dep += [iokit_dep, corefoundation_dep] endif +if host_machine.system() == 'haiku' + network_dep = meson.get_compiler('c').find_library('network', required:true) + tio_dep += [network_dep] +endif + tio_c_args = ['-Wshadow','-Wno-unused-result'] if enable_setspeed2 diff --git a/src/misc.c b/src/misc.c index bd0a429..62a21f1 100644 --- a/src/misc.c +++ b/src/misc.c @@ -28,6 +28,9 @@ #include #include #include "print.h" +#include "misc.h" + +static pid_t shell_command_pid = 0; void delay(long ms) { @@ -54,6 +57,16 @@ int ctrl_key_code(unsigned char key) return -1; } +int ctrl_key_char(int key_code) +{ + if (key_code >= ('a' & ~0x60) && key_code <= ('z' & ~0x60)) + { + return key_code | 0x60; + } + + return -1; +} + bool regex_match(const char *string, const char *pattern) { regex_t regex; @@ -98,14 +111,53 @@ int read_poll(int fd, void *data, size_t len, int timeout) if (fds.revents & POLLIN) { // Read ready data + // return value should not be 0 return read(fd, data, len); } + else /* if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) */ + { + return -1; + } } /* Timeout */ - return ret; + return 0; } +ssize_t write_poll(int fd, const void *data, size_t len, int timeout) +{ + struct pollfd fds; + ssize_t ret = 0; + + fds.events = POLLOUT; + fds.fd = fd; + + /* Wait data available */ + ret = poll(&fds, 1, timeout); + if (ret < 0) + { + tio_error_print("%s", strerror(errno)); + return ret; + } + else if (ret > 0) + { + if (fds.revents & POLLOUT) + { + // Ready to write + // return value should not be 0 + return write(fd, data, len); + } + else /* if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) */ + { + return -1; + } + } + + /* Timeout */ + return 0; +} + + // Function to calculate djb2 hash of string unsigned long djb2_hash(const unsigned char *str) { @@ -170,6 +222,7 @@ bool match_patterns(const char *string, const char *patterns) pattern = strtok(patterns_copy, ","); while (pattern != NULL) { + // clang-format off // Check if the string matches the current pattern #ifdef FNM_EXTMATCH if (fnmatch(pattern, string, FNM_EXTMATCH) == 0) @@ -180,6 +233,7 @@ bool match_patterns(const char *string, const char *patterns) free(patterns_copy); return true; } + // clang-format on // Move to the next pattern pattern = strtok(NULL, ","); @@ -189,38 +243,76 @@ bool match_patterns(const char *string, const char *patterns) return false; } -// Function that forks subprocess, redirects its stdin and stdout to the +// Function that forks subprocess, redirects its stdout and stderr to the // specified filedescriptor, and runs command. int execute_shell_command(int fd, const char *command) { - pid_t pid; + #define READ_END 0 + #define WRITE_END 1 int status; + int pipefd_c2p[2]; + int pipefd_p2c[2]; + +#if defined(__linux__) + static bool done_once = false; + if (!done_once) + { + atexit(&terminate_shell_command); + done_once = true; + } +#endif + + // Create Pipes + if (pipe(pipefd_c2p) == -1 || pipe(pipefd_p2c) == -1) + { + tio_error_print("pipe() failed (%s)", strerror(errno)); + exit(EXIT_FAILURE); + } // Fork a child process - pid = fork(); - if (pid == -1) + shell_command_pid = fork(); + if (shell_command_pid == -1) { // Error occurred tio_error_print("fork() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); } - else if (pid == 0) + else if (shell_command_pid == 0) { // Child process + close(pipefd_c2p[READ_END]); + close(pipefd_p2c[WRITE_END]); tio_printf("Executing shell command '%s'", command); - // Redirect stdout and stderr to the file descriptor - if (dup2(fd, STDOUT_FILENO) == -1 || dup2(fd, STDERR_FILENO) == -1) + // Redirect stdin and stdout to the parent-pipe + if (dup2(pipefd_c2p[WRITE_END], STDOUT_FILENO) == -1 || + dup2(pipefd_p2c[READ_END], STDIN_FILENO) == -1) { tio_error_print("dup2() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); } + // command prefix '?' excludes stderr from redirection + if (command[0] == '?') + { + command += 1; + } + else + { + if (dup2(pipefd_c2p[WRITE_END], STDERR_FILENO) == -1) + { + tio_error_print("dup2() failed (%s)", strerror(errno)); + exit(EXIT_FAILURE); + } + } + // Execute the shell command execl("/bin/sh", "sh", "-c", command, (char *)NULL); // If execlp() returns, it means an error occurred + close(pipefd_c2p[WRITE_END]); + close(pipefd_p2c[READ_END]); perror("execlp"); tio_error_print("execlp() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); @@ -228,9 +320,69 @@ int execute_shell_command(int fd, const char *command) else { // Parent process + fd_set rdfs; + int maxfd; + char buf[BUFSIZ]; + int bytes; + + close(pipefd_c2p[WRITE_END]); + close(pipefd_p2c[READ_END]); + + while (true) + { + FD_ZERO(&rdfs); + FD_SET(fd, &rdfs); + FD_SET(pipefd_c2p[READ_END], &rdfs); + maxfd = MAX(fd, pipefd_c2p[READ_END]); + + /* Block until input becomes available or timeout */ + status = select(maxfd + 1, &rdfs, NULL, NULL, NULL); + if (status < 0) + { + tio_warning_printf("select() failed(%s)", strerror(errno)); + break; + } + + if (FD_ISSET(fd, &rdfs)) + { + bytes = read(fd, buf, sizeof(buf)); + if (bytes <= 0) + { + tio_warning_printf("Could not read from tty device"); + break; + } + rx_total += bytes; + write(pipefd_p2c[WRITE_END], buf, bytes); + } + + if (FD_ISSET(pipefd_c2p[READ_END], &rdfs)) + { + // Read pipe and transfer to tty device. + bytes = read(pipefd_c2p[READ_END], buf, sizeof(buf)); + if (bytes < 0) + { + tio_warning_printf("Could not write to tty device"); + } + else if (bytes == 0) + { + // Shell command has finished. + break; + } + + if (tty_write(fd, buf, bytes) < 0) + { + tio_warning_printf("Could not write to tty device"); + } + tty_sync(fd); + } + } + + close(pipefd_p2c[WRITE_END]); + close(pipefd_c2p[READ_END]); // Wait for the child process to finish - waitpid(pid, &status, 0); + waitpid(shell_command_pid, &status, 0); + shell_command_pid = 0; if (WIFEXITED(status)) { @@ -243,11 +395,30 @@ int execute_shell_command(int fd, const char *command) return -1; } } - return 0; } +#if defined(__linux__) + +void terminate_shell_command(void) +{ + // If previous shell command pid is remain, terminate it. + if (shell_command_pid != 0) + { + #define PKILL_BUFSIZ 80 + char pkill_buf[PKILL_BUFSIZ] = {0}; + int bytes; + bytes = snprintf(pkill_buf, PKILL_BUFSIZ, "/usr/bin/pkill -P %d", shell_command_pid); + if (bytes > 0 && bytes < PKILL_BUFSIZ) + { + system(pkill_buf); + } + } +} + +#endif + void clear_line() { - print("\r\033[K"); + printf("\r\033[K"); } diff --git a/src/misc.h b/src/misc.h index 08ae5eb..f501e82 100644 --- a/src/misc.h +++ b/src/misc.h @@ -24,15 +24,27 @@ #include #include +#define POLL_NOWAIT (0) +#define POLL_FOREVER (-1) + +#define TOSTRING_(x) #x +#define TOSTR(x) TOSTRING_(x) + #define UNUSED(expr) do { (void)(expr); } while (0) void delay(long ms); int ctrl_key_code(unsigned char key); +int ctrl_key_char(int key_code); bool regex_match(const char *string, const char *pattern); unsigned long djb2_hash(const unsigned char *str); void *base62_encode(unsigned long num, char *output); int read_poll(int fd, void *data, size_t len, int timeout); +ssize_t write_poll(int fd, const void *data, size_t len, int timeout); double get_current_time(void); bool match_patterns(const char *string, const char *patterns); int execute_shell_command(int fd, const char *command); void clear_line(); + +#if defined(__linux__) +void terminate_shell_command(void); +#endif diff --git a/src/options.c b/src/options.c index acff7ce..d115b10 100644 --- a/src/options.c +++ b/src/options.c @@ -44,12 +44,14 @@ enum opt_t OPT_LOG_DIRECTORY, OPT_LOG_STRIP, OPT_LOG_APPEND, + OPT_OUTPUT_LINE_DELAY_CHAR, OPT_LINE_PULSE_DURATION, OPT_RS485, OPT_RS485_CONFIG, OPT_ALERT, OPT_COMPLETE_PROFILES, OPT_MUTE, + OPT_SCRIPT_INIT_FILE, OPT_SCRIPT, OPT_SCRIPT_FILE, OPT_SCRIPT_RUN, @@ -59,8 +61,12 @@ enum opt_t OPT_EXCLUDE_DRIVERS, OPT_EXCLUDE_TIDS, OPT_EXEC, + OPT_RAW, + OPT_RAW_INTERACTIVE, + OPT_KEYMAP, }; +// clang-format off /* Default options */ struct option_t option = { @@ -72,6 +78,7 @@ struct option_t option = .parity = PARITY_NONE, .output_delay = 0, .output_line_delay = 0, + .output_line_delay_char = '\n', .dtr_pulse_duration = 100, .rts_pulse_duration = 100, .cts_pulse_duration = 100, @@ -79,6 +86,7 @@ struct option_t option = .dcd_pulse_duration = 100, .ri_pulse_duration = 100, .no_reconnect = false, + .no_tty_restore = false, .auto_connect = AUTO_CONNECT_DIRECT, .log = false, .log_append = false, @@ -101,6 +109,7 @@ struct option_t option = .rs485_delay_rts_after_send = -1, .alert = ALERT_NONE, .complete_profiles = false, + .script_init_filename = NULL, .script = NULL, .script_filename = NULL, .script_run = SCRIPT_RUN_ALWAYS, @@ -124,7 +133,13 @@ struct option_t option = .map_o_nulbrk = false, .map_i_msb2lsb = false, .map_o_ign_cr = false, + .raw = RAW_ON_DELAY, + .raw_interactive = RAW_OFF, + .keymap = NULL, }; +// clang-format on + +struct keymap_t keymaps[KEYMAP_MAX] = {0}; void option_print_help(char *argv[]) { @@ -142,12 +157,14 @@ void option_print_help(char *argv[]) printf(" -p, --parity odd|even|none|mark|space Parity (default: none)\n"); printf(" -o, --output-delay Output character delay (default: 0)\n"); printf(" -O, --output-line-delay Output line delay (default: 0)\n"); + printf(" --output-line-delay-char cr|lf Output line delay trigger character (default: lf)\n"); printf(" --line-pulse-duration Set line pulse duration\n"); printf(" -a, --auto-connect new|latest|direct Automatic connect strategy (default: direct)\n"); printf(" --exclude-devices Exclude devices by pattern\n"); printf(" --exclude-drivers Exclude drivers by pattern\n"); printf(" --exclude-tids Exclude topology IDs by pattern\n"); printf(" -n, --no-reconnect Do not reconnect\n"); + printf(" -N, --no-tty-restore Do not restore initial TTY device settings\n"); printf(" -e, --local-echo Enable local echo\n"); printf(" --input-mode normal|hex|line Select input mode (default: normal)\n"); printf(" --output-mode normal|hex|hexN Select output mode (default: normal)\n"); @@ -161,12 +178,16 @@ void option_print_help(char *argv[]) printf(" --log-append Append to log file\n"); printf(" --log-strip Strip control characters and escape sequences\n"); printf(" -m, --map Map characters\n"); + printf(" --keymap Set key-script mappings\n"); printf(" -c, --color 0..255|bold|none|list Colorize tio text (default: bold)\n"); printf(" -S, --socket Redirect I/O to socket\n"); + printf(" --raw off|on|on-nodelay Select raw mode for non-interactive use (default: on)\n"); + printf(" --raw-interactive off|on|on-nodelay Select raw mode for interactive use (default: off)\n"); printf(" --rs-485 Enable RS-485 mode\n"); printf(" --rs-485-config Set RS-485 configuration\n"); printf(" --alert bell|blink|none Alert on connect/disconnect (default: none)\n"); printf(" --mute Mute tio messages\n"); + printf(" --script-init-file Set initial script file to run at startup\n"); printf(" --script Run script from string\n"); printf(" --script-file Run script from file\n"); printf(" --script-run once|always|never Run script on connect (default: always)\n"); @@ -446,9 +467,9 @@ void option_parse_timestamp(const char *arg, timestamp_t *timestamp) } } -const char *option_alert_state_to_string(alert_t state) +const char *option_alert_state_to_string(alert_t alert_state) { - switch (state) + switch (alert_state) { case ALERT_NONE: return "none"; @@ -502,6 +523,25 @@ void option_parse_auto_connect(const char *arg, auto_connect_t *auto_connect) } } +void option_parse_output_line_delay_char(const char *arg) +{ + assert(arg != NULL); + + if (strcmp("cr", arg) == 0) + { + option.output_line_delay_char = '\r'; + } + else if (strcmp("lf", arg) == 0) + { + option.output_line_delay_char = '\n'; + } + else + { + tio_error_print("Invalid char '%s'", arg); + exit(EXIT_FAILURE); + } +} + void option_parse_line_pulse_duration(const char *arg) { bool token_found = true; @@ -703,6 +743,44 @@ const char *option_output_mode_to_string(output_mode_t mode) return NULL; } +void option_parse_raw(const char *arg, raw_t *raw) +{ + assert(arg != NULL); + + if (strcmp("off", arg) == 0) + { + *raw = RAW_OFF; + } + else if (strcmp("on", arg) == 0) + { + *raw = RAW_ON_DELAY; + } + else if (strcmp("on-nodelay", arg) == 0) + { + *raw = RAW_ON_NODELAY; + } + else + { + tio_error_print("Invalid raw option '%s'", arg); + exit(EXIT_FAILURE); + } +} + +const char *option_raw_to_string(raw_t raw) +{ + switch (raw) + { + case RAW_OFF: + return "off"; + case RAW_ON_DELAY: + return "on"; + case RAW_ON_NODELAY: + return "on-nodelay"; + } + + return NULL; +} + void option_parse_script_run(const char *arg, script_run_t *script_run) { assert(arg != NULL); @@ -820,27 +898,34 @@ void option_parse_mappings(const char *map) void options_print() { + /* note: negative true/false settings are rephrased as affirmative no/yes. */ tio_printf(" Device: %s", device_name); tio_printf(" Baudrate: %u", option.baudrate); tio_printf(" Databits: %d", option.databits); tio_printf(" Flow: %s", option_flow_to_string(option.flow)); tio_printf(" Stopbits: %d", option.stopbits); tio_printf(" Parity: %s", option_parity_to_string(option.parity)); - tio_printf(" Local echo: %s", option.local_echo ? "true" : "false"); + tio_printf(" Local echo: %s", option.local_echo ? "yes" : "no"); tio_printf(" Timestamp: %s", option_timestamp_format_to_string(option.timestamp)); tio_printf(" Timestamp timeout: %u", option.timestamp_timeout); tio_printf(" Output delay: %d", option.output_delay); tio_printf(" Output line delay: %d", option.output_line_delay); + tio_printf(" Output line delay char: %s", option.output_line_delay_char == '\r' ? "cr" : "lf"); tio_printf(" Automatic connect strategy: %s", option_auto_connect_state_to_string(option.auto_connect)); - tio_printf(" Automatic reconnect: %s", option.no_reconnect ? "true" : "false"); + tio_printf(" Automatic reconnect: %s", option.no_reconnect ? "no" : "yes"); + tio_printf(" TTY device settings restore: %s", option.no_tty_restore ? "no" : "yes"); + // clang-format off tio_printf(" Pulse duration: DTR=%d RTS=%d CTS=%d DSR=%d DCD=%d RI=%d", option.dtr_pulse_duration, option.rts_pulse_duration, option.cts_pulse_duration, option.dsr_pulse_duration, option.dcd_pulse_duration, option.ri_pulse_duration); + // clang-format on tio_printf(" Input mode: %s", option_input_mode_to_string(option.input_mode)); tio_printf(" Output mode: %s", option_output_mode_to_string(option.output_mode)); + tio_printf(" Raw (non-interactive): %s", option_raw_to_string(option.raw)); + tio_printf(" Raw interactive: %s", option_raw_to_string(option.raw_interactive)); tio_printf(" Alert: %s", option_alert_state_to_string(option.alert)); if (option.log) { @@ -856,11 +941,20 @@ void options_print() { tio_printf(" Socket: %s", option.socket); } + if (option.script_init_filename != NULL) + { + tio_printf(" Script init file: %s", option.script_init_filename); + } if (option.script_filename != NULL) { tio_printf(" Script file: %s", option.script_filename); tio_printf(" Script run: %s", script_run_state_to_string(option.script_run)); } + if (option.script != NULL) + { + tio_printf(" Script command: %s", option.script); + tio_printf(" Script run: %s", script_run_state_to_string(option.script_run)); + } } void options_parse(int argc, char *argv[]) @@ -887,8 +981,9 @@ void options_parse(int argc, char *argv[]) option.vt100 = true; } - while (1) + while (true) { + // clang-format off static struct option long_options[] = { {"baudrate", required_argument, 0, 'b' }, @@ -898,12 +993,14 @@ void options_parse(int argc, char *argv[]) {"parity", required_argument, 0, 'p' }, {"output-delay", required_argument, 0, 'o' }, {"output-line-delay" , required_argument, 0, 'O' }, + {"output-line-delay-char", required_argument, 0, OPT_OUTPUT_LINE_DELAY_CHAR}, {"line-pulse-duration", required_argument, 0, OPT_LINE_PULSE_DURATION }, {"auto-connect", required_argument, 0, 'a' }, {"exclude-devices", required_argument, 0, OPT_EXCLUDE_DEVICES }, {"exclude-drivers", required_argument, 0, OPT_EXCLUDE_DRIVERS }, {"exclude-tids", required_argument, 0, OPT_EXCLUDE_TIDS }, {"no-reconnect", no_argument, 0, 'n' }, + {"no-tty-restore", no_argument, 0, 'N' }, {"local-echo", no_argument, 0, 'e' }, {"timestamp", no_argument, 0, 't' }, {"timestamp-format", required_argument, 0, OPT_TIMESTAMP_FORMAT }, @@ -916,13 +1013,17 @@ void options_parse(int argc, char *argv[]) {"log-strip", no_argument, 0, OPT_LOG_STRIP }, {"socket", required_argument, 0, 'S' }, {"map", required_argument, 0, 'm' }, + {"keymap", required_argument, 0, OPT_KEYMAP }, {"color", required_argument, 0, 'c' }, {"input-mode", required_argument, 0, OPT_INPUT_MODE }, {"output-mode", required_argument, 0, OPT_OUTPUT_MODE }, + {"raw", required_argument, 0, OPT_RAW }, + {"raw-interactive", required_argument, 0, OPT_RAW_INTERACTIVE }, {"rs-485", no_argument, 0, OPT_RS485 }, {"rs-485-config", required_argument, 0, OPT_RS485_CONFIG }, {"alert", required_argument, 0, OPT_ALERT }, {"mute", no_argument, 0, OPT_MUTE }, + {"script-init-file", required_argument, 0, OPT_SCRIPT_INIT_FILE }, {"script", required_argument, 0, OPT_SCRIPT }, {"script-file", required_argument, 0, OPT_SCRIPT_FILE }, {"script-run", required_argument, 0, OPT_SCRIPT_RUN }, @@ -932,12 +1033,13 @@ void options_parse(int argc, char *argv[]) {"complete-profiles", no_argument, 0, OPT_COMPLETE_PROFILES }, {0, 0, 0, 0 } }; + // clang-format on /* getopt_long stores the option index here */ int option_index = 0; /* Parse argument using getopt_long */ - c = getopt_long(argc, argv, "b:d:f:s:p:o:O:a:netLlS:m:c:xrvh", long_options, &option_index); + c = getopt_long(argc, argv, "b:d:f:s:p:o:O:a:nNetLlS:m:c:xrvh", long_options, &option_index); /* Detect the end of the options */ if (c == -1) @@ -983,6 +1085,10 @@ void options_parse(int argc, char *argv[]) option_string_to_integer(optarg, &option.output_line_delay, "output line delay", 0, INT_MAX); break; + case OPT_OUTPUT_LINE_DELAY_CHAR: + option_parse_output_line_delay_char(optarg); + break; + case OPT_LINE_PULSE_DURATION: option_parse_line_pulse_duration(optarg); break; @@ -1007,6 +1113,10 @@ void options_parse(int argc, char *argv[]) option.no_reconnect = true; break; + case 'N': + option.no_tty_restore = true; + break; + case 'e': option.local_echo = true; break; @@ -1088,6 +1198,10 @@ void options_parse(int argc, char *argv[]) option.mute = true; break; + case OPT_SCRIPT_INIT_FILE: + option.script_init_filename = optarg; + break; + case OPT_SCRIPT: option.script = optarg; break; @@ -1104,6 +1218,19 @@ void options_parse(int argc, char *argv[]) option.exec = optarg; break; + case OPT_RAW: + option_parse_raw(optarg, &option.raw); + break; + + case OPT_RAW_INTERACTIVE: + option_parse_raw(optarg, &option.raw_interactive); + break; + + case OPT_KEYMAP: + option.keymap = optarg; + option_parse_key_mappings(optarg); + break; + case 'v': printf("tio %s\n", VERSION); exit(EXIT_SUCCESS); @@ -1131,7 +1258,7 @@ void options_parse(int argc, char *argv[]) /* Assume first non-option is the target (tty device, profile, tid) */ if (strcmp(option.target, "")) { - optind++; + optind++; } else if (optind < argc) { @@ -1176,6 +1303,7 @@ void options_parse_final(int argc, char *argv[]) #ifdef __CYGWIN__ unsigned char portnum; char *tty_win; + // clang-format off if ( ((strncmp("COM", option.target, 3) == 0) || (strncmp("com", option.target, 3) == 0) ) && (sscanf(option.target + 3, "%hhu", &portnum) == 1) @@ -1184,5 +1312,177 @@ void options_parse_final(int argc, char *argv[]) asprintf(&tty_win, "/dev/ttyS%hhu", portnum - 1); option.target = tty_win; } + // clang-format on #endif } + +int keymap_set(char *key_str, int key_len, char *func_str, int func_len) +{ + char func_str_r[KEYMAP_FUNC_STR_MAX + 1]; + char *srcp; + int dst_ofs; + int key_ofs; + int empty_idx, matched_idx, idx; + bool found_empty = false; + bool found_matched = false; + bool unset_requested = false; + + if (key_str[key_len] != '\0' || func_str[func_len] != '\0') + { + return -1; + } + + /* key_str should not include spaces */ + key_ofs = 0; + for (key_ofs = 0; key_ofs < key_len; key_ofs++) + { + if (key_str[key_ofs] == ' ') + { + tio_error_printf("Key should not include space"); + return -1; + } + } + + /* check disallowed key_str */ + if (strcmp(key_str, "q") == 0) + { + tio_error_printf("Key %s is immutable", key_str); + return -1; + } + + /* remove prefix spaces and postfix spaces from func_str */ + for (srcp = func_str; *srcp != '\0'; srcp++) + { + if (*srcp != ' ') + { + break; + } + } + strncpy(func_str_r, srcp, KEYMAP_KEY_STR_MAX); + func_str_r[KEYMAP_KEY_STR_MAX] = '\0'; + for (dst_ofs = strlen(func_str_r) - 1; dst_ofs >= 0; dst_ofs--) + { + if (func_str_r[dst_ofs] != ' ') + { + func_str_r[dst_ofs + 1] = '\0'; + break; + } + } + if (dst_ofs < 0) + { + func_str_r[0] = '\0'; + } + + if (strcmp(func_str_r, "nil") == 0 || func_str_r[0] == '\0') + { + unset_requested = true; + } + + /* search for entry which key matched or is empty */ + for (idx = 0; idx < KEYMAP_MAX; idx++) + { + if (found_empty == false && keymaps[idx].key[0] == '\0') + { + empty_idx = idx; + found_empty = true; + } + if (found_matched == false && strcmp(keymaps[idx].key, key_str) == 0) + { + matched_idx = idx; + found_matched = true; + } + if (found_empty && found_matched) + { + break; + } + } + + /* update entry */ + if (unset_requested) + { + if (found_matched) + { + keymaps[matched_idx].key[0] = '\0'; + keymaps[matched_idx].func[0] = '\0'; + } + } + else /* set requested */ + { + if (found_matched) + { + strcpy(keymaps[matched_idx].key, key_str); + strcpy(keymaps[matched_idx].func, func_str); + } + else if (found_empty) + { + strcpy(keymaps[empty_idx].key, key_str); + strcpy(keymaps[empty_idx].func, func_str); + } + else + { + tio_error_printf("Too many keymaps", key_str); + return -1; + } + } + return 0; +} + +void keymaps_print(const char *title, int indent) +{ + int idx; + bool keymap_title_done = false; + + for (idx = 0; idx < KEYMAP_MAX; idx++) + { + if (keymaps[idx].key[0] == '\0') + { + continue; + } + if (!keymap_title_done) + { + if (title[0] != '\0') + { + tio_printf("%s", title); + } + keymap_title_done = true; + } + tio_printf("%*sctrl-%c %s : %s", indent, " ", option.prefix_key, keymaps[idx].key, keymaps[idx].func); + } +} + +void option_parse_key_mappings(const char *keymap) +{ + char key_str[KEYMAP_KEY_STR_MAX + 1]; + char func_str[KEYMAP_FUNC_STR_MAX + 1]; + int key_len, func_len; + char *buffer; + char *cp; + + if (keymap == NULL) + { + return; + } + + /* Parse specified key mappings */ + buffer = strdup(keymap); + cp = strchr(buffer, '@'); + if (cp == NULL) + { + tio_error_print("Can't find keymap top character '@'"); + goto parse_end; + } + + while (sscanf(cp, "@%" TOSTR(KEYMAP_KEY_STR_MAX) "[^=]=%" TOSTR(KEYMAP_FUNC_STR_MAX) "[^@]", key_str, func_str) == 2) + { + key_len = strlen(key_str); + func_len = strlen(func_str); + keymap_set(key_str, key_len, func_str, func_len); + cp = strchr(cp + key_len + func_len + 2, '@'); + if (cp == NULL) + { + break; + } + } + parse_end: + free(buffer); +} diff --git a/src/options.h b/src/options.h index c552217..1ab12ea 100644 --- a/src/options.h +++ b/src/options.h @@ -43,6 +43,13 @@ typedef enum OUTPUT_MODE_END, } output_mode_t; +typedef enum +{ + RAW_OFF, + RAW_ON_DELAY, + RAW_ON_NODELAY, +} raw_t; + /* Options */ struct option_t { @@ -54,6 +61,7 @@ struct option_t parity_t parity; int output_delay; int output_line_delay; + char output_line_delay_char; int dtr_pulse_duration; int rts_pulse_duration; int cts_pulse_duration; @@ -61,6 +69,7 @@ struct option_t int dcd_pulse_duration; int ri_pulse_duration; bool no_reconnect; + bool no_tty_restore; auto_connect_t auto_connect; bool log; bool log_append; @@ -73,6 +82,8 @@ struct option_t int color; input_mode_t input_mode; output_mode_t output_mode; + raw_t raw; + raw_t raw_interactive; char prefix_code; char prefix_key; bool prefix_enabled; @@ -83,6 +94,7 @@ struct option_t int32_t rs485_delay_rts_after_send; alert_t alert; bool complete_profiles; + char *script_init_filename; char *script; char *script_filename; script_run_t script_run; @@ -93,6 +105,7 @@ struct option_t int hex_n_value; bool vt100; char *exec; + char *keymap; bool map_i_nl_cr; bool map_i_cr_nl; bool map_ign_cr; @@ -108,7 +121,18 @@ struct option_t bool map_o_ign_cr; }; +#define KEYMAP_MAX 32 +#define KEYMAP_KEY_STR_MAX 7 +#define KEYMAP_FUNC_STR_MAX 127 + +struct keymap_t +{ + char key[KEYMAP_KEY_STR_MAX + 1]; + char func[KEYMAP_FUNC_STR_MAX + 1]; +}; + extern struct option_t option; +extern struct keymap_t keymaps[KEYMAP_MAX]; void options_print(); void options_parse(int argc, char *argv[]); @@ -121,7 +145,9 @@ void option_parse_parity(const char *arg, parity_t *parity); void option_parse_output_mode(const char *arg, output_mode_t *mode); void option_parse_input_mode(const char *arg, input_mode_t *mode); +void option_parse_raw(const char *arg, raw_t *raw); +void option_parse_output_line_delay_char(const char *arg); void option_parse_line_pulse_duration(const char *arg); void option_parse_script_run(const char *arg, script_run_t *script_run); void option_parse_alert(const char *arg, alert_t *alert); @@ -133,3 +159,8 @@ void option_parse_timestamp(const char *arg, timestamp_t *timestamp); const char* option_timestamp_format_to_string(timestamp_t timestamp); void option_parse_mappings(const char *map); +void option_parse_key_mappings(const char *keymap); + +const char* option_raw_to_string(raw_t raw); + +void keymaps_print(const char *title, int indent); diff --git a/src/readline.c b/src/readline.c index 8176a0b..4696e0c 100644 --- a/src/readline.c +++ b/src/readline.c @@ -19,85 +19,146 @@ * 02110-1301, USA. */ +#include "readline.h" #include "print.h" #include "misc.h" +#include #define RL_LINE_LENGTH_MAX PATH_MAX -#define RL_HISTORY_MAX 1000 -static char rl_line[RL_LINE_LENGTH_MAX] = {}; -static char *rl_history[RL_HISTORY_MAX]; -static int rl_history_count = 0; -static int rl_history_index = 0; -static int rl_line_length = 0; -static int rl_cursor_pos = 0; -static int rl_escape = 0; +typedef struct readline_s +{ + char line[RL_LINE_LENGTH_MAX]; + char *history[RL_HISTORY_MAX]; + char prompt[RL_PROMPT_LENGTH_MAX]; + int prompt_length; + int history_count; + int history_index; + int line_length; + int cursor_pos; + int escape; +} readline_t; -static void print_line(const char *string, int cursor_pos) +void print_prompt(readline_t *rl) { clear_line(); - print("%s", string); - print("\r"); // Move the cursor back to the beginning - for (int i = 0; i < cursor_pos; ++i) + printf("%s", rl->prompt); + printf("\r"); // Move the cursor back to the beginning + for (int i = 0; i < rl->prompt_length; ++i) { - print("\x1b[C"); // Move the cursor right + printf("\x1b[C"); // Move the cursor right } } -void readline_init(void) +void print_line(readline_t *rl) { - rl_history_count = 0; - rl_history_index = 0; + clear_line(); + printf("%s%s", rl->prompt, rl->line); + printf("\r"); // Move the cursor back to the beginning + for (int i = 0; i < rl->prompt_length + rl->cursor_pos; ++i) + { + printf("\x1b[C"); // Move the cursor right + } +} + +readline_t *readline_create(void) +{ + readline_t *rl = malloc(sizeof(readline_t)); + if (rl == NULL) + return NULL; + + readline_reinit(rl); + return rl; +} + +void readline_reinit(readline_t *rl) +{ + assert(rl != NULL); + + rl->prompt[0] = '\0'; + rl->prompt_length = 0; + + rl->history_count = 0; + rl->history_index = 0; for (int i = 0; i < RL_HISTORY_MAX; ++i) { - rl_history[i] = NULL; + rl->history[i] = NULL; } - rl_line[0] = 0; - rl_line_length = 0; - rl_cursor_pos = 0; - rl_escape = 0; + rl->line[0] = 0; + rl->line_length = 0; + rl->cursor_pos = 0; + rl->escape = 0; } -char * readline_get(void) +void readline_set_prompt(readline_t *rl, const char *prompt) { - return rl_line; + strncpy(rl->prompt, prompt, RL_PROMPT_LENGTH_MAX - 1); + rl->prompt[RL_PROMPT_LENGTH_MAX - 1] = '\0'; + rl->prompt_length = strlen(rl->prompt); } -static void readline_input_char(char input_char) +char * readline_get(readline_t *rl) { - if (rl_line_length < RL_LINE_LENGTH_MAX - 1) + assert(rl != NULL); + return rl->line; +} + +void readline_prompt_for_input(readline_t *rl) +{ + assert(rl != NULL); + + rl->line[0] = 0; + rl->line_length = 0; + rl->cursor_pos = 0; + rl->escape = 0; + print_line(rl); +} + +static void readline_input_char(readline_t *rl, char input_char) +{ + assert(rl != NULL); + + if (rl->line_length < RL_LINE_LENGTH_MAX - 1) { - memmove(&rl_line[rl_cursor_pos + 1], &rl_line[rl_cursor_pos], rl_line_length - rl_cursor_pos); - rl_line[rl_cursor_pos] = input_char; - rl_line_length++; - rl_cursor_pos++; - rl_line[rl_line_length] = '\0'; - print_line(rl_line, rl_cursor_pos); + memmove(&rl->line[rl->cursor_pos + 1], &rl->line[rl->cursor_pos], rl->line_length - rl->cursor_pos); + rl->line[rl->cursor_pos] = input_char; + rl->line_length++; + rl->cursor_pos++; + rl->line[rl->line_length] = '\0'; + print_line(rl); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_cr(void) +static void readline_input_cr(readline_t *rl) { - if (rl_line_length > 0) + rl->line[rl->line_length] = '\0'; + + if (rl->line_length > 0) { - // Save to history - if (rl_history_count < RL_HISTORY_MAX) + // Different line only + if (rl->history_count == 0 || + (rl->history_count > 0 && + ! (rl->history[rl->history_count - 1][rl->line_length] == '\0' && + strncmp(rl->history[rl->history_count - 1], rl->line, rl->line_length) == 0)) ) { - rl_history[rl_history_count] = strndup(rl_line, rl_line_length); - rl_history_count++; - } - else - { - free(rl_history[0]); - memmove(&rl_history[0], &rl_history[1], (RL_HISTORY_MAX - 1) * sizeof(char*)); - rl_history[RL_HISTORY_MAX - 1] = strndup(rl_line, rl_line_length); + // Save to history + if (rl->history_count < RL_HISTORY_MAX) + { + rl->history[rl->history_count] = strndup(rl->line, rl->line_length); + rl->history_count++; + } + else + { + free(rl->history[0]); + memmove(&rl->history[0], &rl->history[1], (RL_HISTORY_MAX - 1) * sizeof(char*)); + rl->history[RL_HISTORY_MAX - 1] = strndup(rl->line, rl->line_length); + } } } - rl_line[rl_line_length] = '\0'; if (option.local_echo == false) { clear_line(); @@ -107,170 +168,171 @@ static void readline_input_cr(void) print("\r\n"); } - rl_line_length = 0; - rl_cursor_pos = 0; - rl_history_index = rl_history_count; - rl_escape = 0; + rl->line_length = 0; + rl->cursor_pos = 0; + rl->history_index = rl->history_count; + rl->escape = 0; } -static void readline_input_bs(void) +static void readline_input_bs(readline_t *rl) { - if (rl_cursor_pos > 0) + if (rl->cursor_pos > 0) { - memmove(&rl_line[rl_cursor_pos - 1], &rl_line[rl_cursor_pos], rl_line_length - rl_cursor_pos); - rl_line_length--; - rl_cursor_pos--; - rl_line[rl_line_length] = '\0'; - print_line(rl_line, rl_cursor_pos); + memmove(&rl->line[rl->cursor_pos - 1], &rl->line[rl->cursor_pos], rl->line_length - rl->cursor_pos); + rl->line_length--; + rl->cursor_pos--; + rl->line[rl->line_length] = '\0'; + print_line(rl); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_escape(void) +static void readline_input_escape(readline_t *rl) { - rl_escape = 1; + rl->escape = 1; } -static void readline_input_left_bracket(void) +static void readline_input_left_bracket(readline_t *rl) { - if (rl_escape == 1) + if (rl->escape == 1) { - rl_escape = 2; + rl->escape = 2; } else { - rl_escape = 0; + readline_input_char(rl, '['); + rl->escape = 0; } } -static void readline_input_A(void) +static void readline_input_A(readline_t *rl) { - if (rl_escape == 2) + if (rl->escape == 2) { // Up arrow - if (rl_history_index > 0) + if (rl->history_index > 0) { - rl_history_index--; - strncpy(rl_line, rl_history[rl_history_index], RL_LINE_LENGTH_MAX-1); - rl_line_length = strlen(rl_line); - rl_cursor_pos = rl_line_length; - print_line(rl_line, rl_cursor_pos); + rl->history_index--; + strncpy(rl->line, rl->history[rl->history_index], RL_LINE_LENGTH_MAX-1); + rl->line_length = strlen(rl->line); + rl->cursor_pos = rl->line_length; + print_line(rl); } } else { - readline_input_char('A'); + readline_input_char(rl, 'A'); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_B(void) +static void readline_input_B(readline_t *rl) { - if (rl_escape == 2) + if (rl->escape == 2) { // Down arrow - if (rl_history_index < rl_history_count - 1) + if (rl->history_index < rl->history_count - 1) { - rl_history_index++; - strncpy(rl_line, rl_history[rl_history_index], RL_LINE_LENGTH_MAX-1); - rl_line_length = strlen(rl_line); - rl_cursor_pos = rl_line_length; - print_line(rl_line, rl_cursor_pos); + rl->history_index++; + strncpy(rl->line, rl->history[rl->history_index], RL_LINE_LENGTH_MAX-1); + rl->line_length = strlen(rl->line); + rl->cursor_pos = rl->line_length; + print_line(rl); } - else if (rl_history_index == rl_history_count - 1) + else if (rl->history_index == rl->history_count - 1) { - rl_history_index++; - rl_line_length = 0; - rl_cursor_pos = 0; - rl_line[rl_line_length] = '\0'; - print_line(rl_line, rl_cursor_pos); + rl->history_index++; + rl->line_length = 0; + rl->cursor_pos = 0; + rl->line[rl->line_length] = '\0'; + print_line(rl); } } else { - readline_input_char('B'); + readline_input_char(rl, 'B'); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_C(void) +static void readline_input_C(readline_t *rl) { - if (rl_escape == 2) + if (rl->escape == 2) { // Right arrow - if (rl_cursor_pos < rl_line_length) + if (rl->cursor_pos < rl->line_length) { - rl_cursor_pos++; + rl->cursor_pos++; print("\x1b[C"); } } else { - readline_input_char('C'); + readline_input_char(rl, 'C'); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_D(void) +static void readline_input_D(readline_t *rl) { - if (rl_escape == 2) + if (rl->escape == 2) { // Left arrow - if (rl_cursor_pos > 0) + if (rl->cursor_pos > 0) { - rl_cursor_pos--; + rl->cursor_pos--; print("\b"); } } else { - readline_input_char('D'); + readline_input_char(rl, 'D'); } - rl_escape = 0; + rl->escape = 0; } -void readline_input(char input_char) +void readline_input(readline_t *rl, char input_char) { switch (input_char) { case '\r': // Carriage return - readline_input_cr(); + readline_input_cr(rl); break; case 127: // Backspace - readline_input_bs(); + readline_input_bs(rl); break; case 27: // Escape - readline_input_escape(); + readline_input_escape(rl); break; case '[': - readline_input_left_bracket(); + readline_input_left_bracket(rl); break; case 'A': - readline_input_A(); + readline_input_A(rl); break; case 'B': - readline_input_B(); + readline_input_B(rl); break; case 'C': - readline_input_C(); + readline_input_C(rl); break; case 'D': - readline_input_D(); + readline_input_D(rl); break; default: - readline_input_char(input_char); + readline_input_char(rl, input_char); break; } } diff --git a/src/readline.h b/src/readline.h index 46c3f10..a953258 100644 --- a/src/readline.h +++ b/src/readline.h @@ -21,6 +21,15 @@ #pragma once -void readline_init(void); -void readline_input(char input_char); -char * readline_get(void); +#define RL_HISTORY_MAX 500 +#define RL_PROMPT_LENGTH_MAX 16 + +typedef struct readline_s readline_t; + +readline_t *readline_create(void); +void readline_reinit(readline_t *rl); +void readline_set_prompt(readline_t *rl, const char *prompt); +void readline_prompt_for_input(readline_t *rl); +void readline_input(readline_t *rl, char input_char); +char *readline_get(readline_t *rl); +void print_prompt(readline_t *rl); diff --git a/src/script.c b/src/script.c index b69d55b..d29ef2e 100644 --- a/src/script.c +++ b/src/script.c @@ -29,6 +29,7 @@ #include #include #include +#include #include "misc.h" #include "print.h" #include "options.h" @@ -39,13 +40,24 @@ #include "fs.h" #include "timestamp.h" #include "termios.h" +#include "version.h" #define MAX_BUFFER_SIZE 2000 // Maximum size of circular buffer #define READ_LINE_SIZE 4096 // read_line buffer length -static int device_fd; +static int device_fd = 0; +static lua_State *script_interp = NULL; +// clang-format off static char script_init[] = +"tio.C = {\n" +" EXPECT_CLEANUP_READ_SIZE = 4096,\n" +" WAIT_FOREVER = 0,\n" +" NOWAIT = -1,\n" +"}\n" +"tio.clear_screen = function()\n" +" io.write('\\x1bc')\n" +"end\n" "tio.set = function(arg)\n" " local dtr = arg.DTR or -1\n" " local rts = arg.RTS or -1\n" @@ -58,19 +70,56 @@ static char script_init[] = "tio.expect = function(pattern, timeout)\n" " local str = ''\n" " while true do\n" -" local c = tio.read(1, timeout)\n" -" if c then\n" -" str = str .. c\n" -" if string.match(str, pattern) then\n" -" return string.match(str, pattern)\n" +" local astr = tio.read(tio.C.EXPECT_CLEANUP_READ_SIZE, tio.C.NOWAIT)\n" +" local c = nil\n" +" if astr == nil then\n" +" c = tio.read(1, timeout)\n" +" if c == nil then\n" +" return nil, str\n" " end\n" -" else\n" -" return nil, str\n" +" end\n" +" str = table.concat{str, astr or '', c or ''}\n" +" local captured = { string.match(str, pattern) }\n" +" if #captured > 0 then\n" +" return table.unpack(captured), str\n" " end\n" " end\n" "end\n" +"tio.expects = function(patterns, timeout)\n" +" local str = ''\n" +" if type(patterns) ~= 'table' then\n" +" patterns = { patterns }\n" +" end\n" +" while true do\n" +" local astr = tio.read(tio.C.EXPECT_CLEANUP_READ_SIZE, tio.C.NOWAIT)\n" +" local c = nil\n" +" if astr == nil then\n" +" c = tio.read(1, timeout)\n" +" if c == nil then\n" +" return nil, nil, str\n" +" end\n" +" end\n" +" str = table.concat{str, astr or '', c or ''}\n" +" for idx, pat in ipairs(patterns) do\n" +" local captured = { string.match(str, pat) }\n" +" if #captured > 0 then\n" +" return idx, captured, str\n" +" end\n" +" end\n" +" end\n" +"end\n" +"tio.subcmd_println = function(fmt, ...)\n" +" tio.subcmd_puts(string.format(fmt, select(1, ...)))\n" +"end\n" +"tio.subcmd_warning_println = function(fmt, ...)\n" +" tio.subcmd_warning_puts(fmt, string.format(fmt, select(1, ...)))\n" +"end\n" +"tio.subcmd_error_println = function(fmt, ...)\n" +" tio.subcmd_error_puts(fmt, string.format(fmt, select(1, ...)))\n" +"end\n" "tio.alwaysecho = true\n" "setmetatable(tio, tio)\n"; +// clang-format on static bool alwaysecho(lua_State *L) { @@ -100,7 +149,9 @@ static int api_echo(lua_State *L) log_printf("\n[%s] %s", pTimeStampNow, str); } } - } else { + } + else + { for (size_t i=0; i 0; --len, string++) + { + forward_to_tty(device_fd, *string); + } + tty_sync(device_fd); + + lua_getglobal(L, "tio"); + + return 1; +} + // lua: tio.read(size, timeout) static int api_read(lua_State *L) { int size = luaL_checkinteger(L, 1); - int timeout = lua_tointeger(L, 2); + int timeout = luaL_optinteger(L, 2, 0); // ms, zero value means forever, negative value means nowait. - if (timeout == 0) + if (device_fd == 0) { - timeout = -1; // Wait forever + return luaL_error(L, "tty device not ready"); } + // For C API, the values for forever and nowait are swapped. + int timeout_c; + if (timeout > 0) + timeout_c = timeout; + else if (timeout == 0) + timeout_c = POLL_FOREVER; + else if (timeout < 0) + timeout_c = POLL_NOWAIT; + luaL_Buffer buffer; luaL_buffinit(L, &buffer); @@ -297,7 +483,7 @@ static int api_read(lua_State *L) char *p = luaL_prepbuffer(&buffer); #endif - ssize_t ret = read_poll(device_fd, p, size, timeout); + ssize_t ret = read_poll(device_fd, p, size, timeout_c); if (ret < 0) return luaL_error(L, "%s", strerror(errno)); @@ -319,20 +505,31 @@ static int api_read(lua_State *L) } // lua: string = tio.readline(timeout) -static int api_readline(lua_State *L) { - int timeout = lua_tointeger(L, 1); //ms +static int api_readline(lua_State *L) +{ + int timeout = luaL_optinteger(L, 1, 0); // ms, zero value means forever, negative value means nowait. luaL_Buffer b; char ch; - if (timeout == 0) + if (device_fd == 0) { - timeout = -1; // Wait forever + return luaL_error(L, "tty device not ready"); } + // For C API, the values for forever and nowait are swapped. + int timeout_c; + if (timeout > 0) + timeout_c = timeout; + else if (timeout == 0) + timeout_c = POLL_FOREVER; + else if (timeout < 0) + timeout_c = POLL_NOWAIT; + luaL_buffinit(L, &b); luaL_prepbuffer(&b); - while (true) { - int ret = read_poll(device_fd, &ch, 1, timeout); + while (true) + { + int ret = read_poll(device_fd, &ch, 1, timeout_c); if (ret < 0) return luaL_error(L, "%s", strerror(errno)); @@ -357,6 +554,145 @@ static int api_readline(lua_State *L) { } } +// lua: str = tio.inkey(mseconds) +static int api_inkey(lua_State *L) +{ + extern char inkey_chars[]; + int ret; + int mseconds; + int arg_num = lua_gettop(L); + int arg; + if (arg_num == 0) + { + arg = -1; + } + else + { + arg = lua_tointeger(L, 1); + } + if (arg == 0) + { + mseconds = POLL_FOREVER; + } + else if (arg < 0) + { + mseconds = POLL_NOWAIT; + } + else + { + mseconds = arg; + } + ret = tty_inkey(mseconds); + if (ret == 0) + { + /* Timeout */ + lua_pushnil(L); + return 1; + } + else if (ret < 0) { + return luaL_error(L, "inkey failed"); + } + lua_pushlstring(L, inkey_chars, ret); + return 1; +} + +// lua: str = tio.input(prompt) +static int api_input(lua_State *L) +{ + extern char line[]; + int arg_num = lua_gettop(L); + const char *prompt = ""; + if (arg_num > 0) + { + prompt = luaL_checkstring(L, 1); + } + tty_simple_readln(prompt); + lua_pushstring(L, line); + return 1; +} + +// lua: str = tio.inputline(title_prompt) +static int api_input_line(lua_State *L) +{ + extern char line[]; + int arg_num = lua_gettop(L); + const char *prompt = ""; + if (arg_num > 0) + { + prompt = luaL_checkstring(L, 1); + } + tty_subcmd_readln(prompt); + lua_pushstring(L, line); + return 1; +} + +// lua: api_subcmd_puts(str) +static int api_subcmd_puts(lua_State *L) +{ + int arg_num = lua_gettop(L); + const char *str; + if (arg_num != 1) + { + return luaL_error(L, "arguments error"); + } + str = luaL_checkstring(L, 1); + tio_printf("%s", str); + return 0; +} + +// lua: api_subcmd_warning_puts(str) +static int api_subcmd_warning_puts(lua_State *L) +{ + int arg_num = lua_gettop(L); + const char *str; + if (arg_num != 1) + { + return luaL_error(L, "arguments error"); + } + str = luaL_checkstring(L, 1); + tio_warning_printf("%s", str); + return 0; +} + +// lua: api_subcmd_error_puts(str) +static int api_subcmd_error_puts(lua_State *L) +{ + int arg_num = lua_gettop(L); + const char *str; + if (arg_num != 1) + { + return luaL_error(L, "arguments error"); + } + str = luaL_checkstring(L, 1); + tio_error_printf("%s", str); + return 0; +} + +// lua: true/false, error = tio.set_keymap(keymap_str) +static int api_set_keymap(lua_State *L) +{ + int arg_num = lua_gettop(L); + const char *keymap_str; + if (arg_num != 1) + { + return luaL_error(L, "arguments error"); + } + keymap_str = luaL_checkstring(L, 1); +#if 1 + option_parse_key_mappings(keymap_str); + return 0; +#else + ret = option_parse_key_mappings(keymap_str); + if (ret < 0) + { + return luaL_error(L, "keymap setting failed"); + } + lua_pushboolean(L, true); + return 1; +#endif +} + + // lua: table = tio.ttysearch() static int api_ttysearch(lua_State *L) { @@ -411,6 +747,214 @@ static int api_ttysearch(lua_State *L) return 1; } +// lua: tio.send_break() +static int api_send_break(lua_State *L) +{ + if (device_fd == 0) + { + return luaL_error(L, "tty device not ready"); + } + + tcsendbreak(device_fd, 0); + return 0; +} + +// lua: tio.set_local_echo(boolean local_echo) +static int api_set_local_echo(lua_State *L) +{ + int arg_num = lua_gettop(L); + if (arg_num == 0) + { + option.local_echo = true; + return 0; + } + if ( ! (lua_isboolean(L, 1) || lua_isnil(L, 1)) ) + { + return luaL_error(L, "argument is not boolean"); + } + option.local_echo = lua_toboolean(L, 1); + return 0; +} + +// lua: tio.set_log(boolean log) +static int api_set_log(lua_State *L) +{ + int arg_num = lua_gettop(L); + if (arg_num == 0) + { + option.log = true; + } + else /* arg_num > 0 */ + { + if ( ! (lua_isboolean(L, 1) || lua_isnil(L, 1)) ) + { + return luaL_error(L, "argument is not boolean"); + } + option.log = lua_toboolean(L, 1); + } + + if (option.log) + { + if (log_open(option.log_filename) != 0) + { + option.log = false; + return luaL_error(L, "cant open log file"); + } + } + else + { + log_close(); + } + return 0; +} + +// lua: tio.flush_data_io_buffer() +static int api_flush_data_io_buffer(lua_State *L) +{ + if (device_fd == 0) + { + return luaL_error(L, "tty device not ready"); + } + tcflush(device_fd, TCIOFLUSH); + return 0; +} + +// lua: tio.set_input_mode(tio.C.IM_NORMAL | tio.C.IM_HEX | tio.C.IM_LINE) +static int api_set_input_mode(lua_State *L) +{ + int input_mode = luaL_optinteger(L, 1, INPUT_MODE_NORMAL); + switch (input_mode) + { + case INPUT_MODE_NORMAL: + case INPUT_MODE_HEX: + case INPUT_MODE_LINE: + break; + default: + return luaL_error(L, "invalid input mode"); + } + option.input_mode = input_mode; + return 0; +} + +// lua: tio.set_output_mode(tio.C.OM_NORMAL | tio.C.OM_HEX) +static int api_set_output_mode(lua_State *L) +{ + int output_mode = luaL_optinteger(L, 1, OUTPUT_MODE_NORMAL); + switch (output_mode) + { + case OUTPUT_MODE_NORMAL: + case OUTPUT_MODE_HEX: + break; + default: + return luaL_error(L, "invalid output mode"); + } + option.output_mode = output_mode; + return 0; +} + +// lua: tio.set_raw_mode(tio.C.RAW_OFF | tio.C.RAW_ON | tio.C.RAW_ON_NODELAY) +static int api_set_raw_mode(lua_State *L) +{ + int raw_mode = luaL_optinteger(L, 1, RAW_ON_DELAY); + switch (raw_mode) + { + case RAW_OFF: + case RAW_ON_DELAY: + case RAW_ON_NODELAY: + break; + default: + return luaL_error(L, "invalid raw mode"); + } + option.raw = raw_mode; + if (state != STATE_INTERACTIVE) + { + tty_tcsetattr(device_fd); + } + return 0; +} + +// lua: tio.set_raw_mode_interactive(tio.C.RAW_OFF | tio.C.RAW_ON | tio.C.RAW_ON_NODELAY) +static int api_set_raw_mode_interactive(lua_State *L) +{ + int raw_mode = luaL_optinteger(L, 1, RAW_ON_DELAY); + switch (raw_mode) + { + case RAW_OFF: + case RAW_ON_DELAY: + case RAW_ON_NODELAY: + break; + default: + return luaL_error(L, "invalid raw mode"); + } + option.raw_interactive = raw_mode; + if (state == STATE_INTERACTIVE) + { + tty_tcsetattr(device_fd); + } + return 0; +} + +// lua: tio.set_timestamp_mode(tio.C.TS_NONE | tio.C.TS_24HOUR | ...) +static int api_set_timestamp_mode(lua_State *L) +{ + int timestamp_mode = luaL_optinteger(L, 1, TIMESTAMP_24HOUR); + switch (timestamp_mode) + { + case TIMESTAMP_NONE: + case TIMESTAMP_24HOUR: + case TIMESTAMP_24HOUR_START: + case TIMESTAMP_24HOUR_DELTA: + case TIMESTAMP_ISO8601: + case TIMESTAMP_EPOCH: + case TIMESTAMP_EPOCH_USEC: + break; + default: + return luaL_error(L, "invalid timestamp mode"); + } + option.timestamp = timestamp_mode; + return 0; +} + +// lua: tio.exec_shell_command(string:command) +int api_exec_shell_command(lua_State *L) +{ + const char *command = luaL_checkstring(L, 1); + if (command == NULL) + { + return luaL_error(L, "no command"); + } + if (device_fd == 0) + { + return luaL_error(L, "tty device not ready"); + } + int result; + state_t state_orig = state; + state = STATE_EXEC_SHELL_COMMAND; + tty_tcsetattr(device_fd); + result = execute_shell_command(device_fd, command); + state = state_orig; + tty_tcsetattr(device_fd); + if (result < 0) + { + return luaL_error(L, "command failed."); + } + return 0; +} + +// lua: tio.get_state() +static int api_get_state(lua_State *L) +{ + lua_pushinteger(L, state); + return 1; +} + +// lua: tio.get_version() +static int api_get_version(lua_State *L) +{ + lua_pushstring(L, VERSION); + return 1; +} + static void script_buffer_run(lua_State *L, const char *script_buffer) { int error; @@ -440,19 +984,47 @@ static void script_file_run(lua_State *L, const char *filename) } } +// clang-format off static const struct luaL_Reg tio_lib[] = { { "echo", api_echo}, { "sleep", api_sleep}, { "msleep", api_msleep}, - { "line_set", line_set}, + { "line_set", api_line_set}, + { "line_get", api_line_get}, { "send", api_send}, + { "receive", api_receive}, { "write", api_write}, + { "twrite", api_twrite}, { "read", api_read}, { "readline", api_readline}, { "ttysearch", api_ttysearch}, + + { "send_break", api_send_break}, + { "set_local_echo", api_set_local_echo}, + { "set_log", api_set_log}, + { "flush_data_io_buffer", api_flush_data_io_buffer}, + { "set_input_mode", api_set_input_mode}, + { "set_output_mode", api_set_output_mode}, + { "set_raw_mode", api_set_raw_mode}, + { "set_raw_mode_interactive", api_set_raw_mode_interactive}, + { "set_timestamp_mode", api_set_timestamp_mode}, + { "exec_shell_command", api_exec_shell_command}, + { "get_state", api_get_state}, + { "get_version", api_get_version}, + + { "inkey", api_inkey}, + { "input", api_input}, + { "inputline", api_input_line}, + { "set_keymap", api_set_keymap}, + + { "subcmd_puts", api_subcmd_puts}, + { "subcmd_warning_puts", api_subcmd_warning_puts}, + { "subcmd_error_puts", api_subcmd_error_puts}, + {NULL, NULL} }; +// clang-format on static void script_load(lua_State *L) { @@ -466,22 +1038,66 @@ static void script_load(lua_State *L) } } -static void script_set_global(lua_State *L, const char *name, long value) +static void script_set_global_integer(lua_State *L, const char *name, int value) { - lua_pushnumber(L, value); + lua_pushinteger(L, value); lua_setglobal(L, name); } static void script_set_globals(lua_State *L) { - script_set_global(L, "toggle", 2); - script_set_global(L, "high", 1); - script_set_global(L, "low", 0); - script_set_global(L, "XMODEM_CRC", XMODEM_CRC); - script_set_global(L, "XMODEM_1K", XMODEM_1K); - script_set_global(L, "YMODEM", YMODEM); + script_set_global_integer(L, "toggle", 2); + script_set_global_integer(L, "high", 1); + script_set_global_integer(L, "low", 0); + script_set_global_integer(L, "XMODEM_SUM", XMODEM_SUM); + script_set_global_integer(L, "XMODEM_CRC", XMODEM_CRC); + script_set_global_integer(L, "XMODEM_1K", XMODEM_1K); + script_set_global_integer(L, "YMODEM", YMODEM); } +static void script_set_field_integer(lua_State *L, const char *name, int value) +{ + lua_pushinteger(L, value); + lua_setfield(L, -2, name); +} + +static void script_set_consts(lua_State *L) +{ + lua_getglobal(L, "tio"); + lua_getfield(L, -1, "C"); + + script_set_field_integer(L, "IM_NORMAL", INPUT_MODE_NORMAL); + script_set_field_integer(L, "IM_HEX", INPUT_MODE_HEX); + script_set_field_integer(L, "IM_LINE", INPUT_MODE_LINE); + script_set_field_integer(L, "OM_NORMAL", OUTPUT_MODE_NORMAL); + script_set_field_integer(L, "OM_HEX", OUTPUT_MODE_HEX); + script_set_field_integer(L, "RAW_OFF", RAW_OFF); + script_set_field_integer(L, "RAW_ON", RAW_ON_DELAY); + script_set_field_integer(L, "RAW_ON_NODELAY", RAW_ON_NODELAY); + script_set_field_integer(L, "TS_OFF", TIMESTAMP_NONE); + script_set_field_integer(L, "TS_24HOUR", TIMESTAMP_24HOUR); + script_set_field_integer(L, "TS_24HOUR_START", TIMESTAMP_24HOUR_START); + script_set_field_integer(L, "TS_24HOUR_DELTA", TIMESTAMP_24HOUR_DELTA); + script_set_field_integer(L, "TS_ISO8601", TIMESTAMP_ISO8601); + script_set_field_integer(L, "TS_EPOCH", TIMESTAMP_EPOCH); + script_set_field_integer(L, "TS_EPOCH_USEC", TIMESTAMP_EPOCH_USEC); + script_set_field_integer(L, "LN_TOGGLE", 2); + script_set_field_integer(L, "LN_HIGH", 1); + script_set_field_integer(L, "LN_LOW", 0); + script_set_field_integer(L, "XM_SUM", XMODEM_SUM); + script_set_field_integer(L, "XM_CRC", XMODEM_CRC); + script_set_field_integer(L, "XM_1K", XMODEM_1K); + script_set_field_integer(L, "YM_NORMAL", YMODEM); + script_set_field_integer(L, "SA_INTERACTIVE", STATE_INTERACTIVE); + script_set_field_integer(L, "SA_STARTING", STATE_STARTING); + script_set_field_integer(L, "SA_PIPED_INPUT", STATE_PIPED_INPUT); + script_set_field_integer(L, "SA_EXEC_SHELL_COMMAND", STATE_EXEC_SHELL_COMMAND); + script_set_field_integer(L, "SA_XYMODEM", STATE_XYMODEM); + + lua_pop(L, 2); +} + + #if LUA_VERSION_NUM >= 502 static int luaopen_tio(lua_State *L) { @@ -490,14 +1106,26 @@ static int luaopen_tio(lua_State *L) } #endif -void script_run(int fd, const char *script_filename) +static lua_State *script_interp_new(void) { lua_State *L; - device_fd = fd; + if (script_interp != NULL) { + lua_close(script_interp); + } L = luaL_newstate(); + script_interp = L; + + if (L == NULL) { + tio_error_printf("Can't allocate script buffer"); + return NULL; + } + + lua_gc(L, LUA_GCSTOP); luaL_openlibs(L); + lua_gc(L, LUA_GCRESTART); + lua_gc(L, LUA_GCGEN, 0, 0); #if LUA_VERSION_NUM >= 502 luaL_requiref(L, "tio", luaopen_tio, 1); @@ -511,29 +1139,113 @@ void script_run(int fd, const char *script_filename) // Initialize globals script_set_globals(L); + script_set_consts(L); - if (script_filename != NULL) - { - tio_printf("Running script %s", script_filename); - script_file_run(L, script_filename); + // Execute script-init file + if (option.script_init_filename) { + if (luaL_dofile(L, option.script_init_filename)) { + tio_warning_printf("lua: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } } - else if (option.script_filename != NULL) + + return L; +} + +void script_device_bind(int fd) +{ + device_fd = fd; +} + +void script_device_unbind(void) +{ + device_fd = 0; +} + +void script_do_line(const char *script_line) +{ + assert(script_line != NULL); + assert(script_interp != NULL); + + script_buffer_run(script_interp, script_line); +} + +void script_run(const char *script_filename) +{ + static bool doopt_by_nul = true; + + assert(script_filename != NULL); + assert(script_interp != NULL); + + if (script_filename[0] == '\0') + { + if (doopt_by_nul) + { + script_run_as_specified_by_options(); + } + return; + } + else if (script_filename[0] == '@') + { + if (strcmp(script_filename, "@new") == 0) + { + tio_printf("Restart interpreter"); + script_interp_new(); + } + else if (strcmp(script_filename, "@doopt") == 0) + { + script_run_as_specified_by_options(); + } + else if (strcmp(script_filename, "@nuldo=opt") == 0) + { + doopt_by_nul = true; + } + else if (strcmp(script_filename, "@nuldo=none") == 0) + { + doopt_by_nul = false; + } + else + { + tio_printf("Unknown command"); + } + return; + } + else + { + // if filename starts with '!', do filename's remain parts as lua commands. + tio_printf("Running script %s", script_filename); + if (script_filename[0] == '!') + { + script_buffer_run(script_interp, &script_filename[1]); + } + else + { + script_file_run(script_interp, script_filename); + } + return; + } +} + +void script_run_as_specified_by_options(void) +{ + assert(script_interp != NULL); + + if (option.script_filename != NULL) { tio_printf("Running script %s", option.script_filename); - script_file_run(L, option.script_filename); + script_file_run(script_interp, option.script_filename); + } else if (option.script != NULL) { - tio_printf("Running script"); - script_buffer_run(L, option.script); + tio_printf("Running script !%s", option.script); + script_buffer_run(script_interp, option.script); } - - lua_close(L); } -const char *script_run_state_to_string(script_run_t state) +const char *script_run_state_to_string(script_run_t run_state) { - switch (state) + switch (run_state) { case SCRIPT_RUN_ONCE: return "once"; @@ -545,3 +1257,12 @@ const char *script_run_state_to_string(script_run_t state) return "Unknown"; } } + +void script_interp_init(void) +{ + if (script_interp_new() == NULL) + { + tio_error_printf("Could not start script interpreter."); + exit(EXIT_FAILURE); + } +} diff --git a/src/script.h b/src/script.h index 58ba1e1..c0f14e6 100644 --- a/src/script.h +++ b/src/script.h @@ -29,5 +29,10 @@ typedef enum SCRIPT_RUN_END, } script_run_t; -void script_run(int fd, const char *script_filename); +void script_interp_init(void); +void script_device_bind(int fd); +void script_device_unbind(void); +void script_run(const char *script_filename); +void script_run_as_specified_by_options(void); +void script_do_line(const char *script_line); const char *script_run_state_to_string(script_run_t state); diff --git a/src/tty.c b/src/tty.c index efda859..d5351ea 100644 --- a/src/tty.c +++ b/src/tty.c @@ -117,6 +117,9 @@ #define KEY_SHIFT_F 0x46 #define KEY_G 0x67 #define KEY_I 0x69 +#define KEY_J 0x6A +#define KEY_SHIFT_J 0x4A +#define KEY_K 0x6B #define KEY_L 0x6C #define KEY_SHIFT_L 0x4C #define KEY_M 0x6D @@ -147,6 +150,10 @@ typedef enum SUBCOMMAND_MAP, } sub_command_t; +#define MLINE_MAX 4096 +#define INKEY_CHARS_MAX 16 + +// clang-format off const char random_array[] = { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x28, 0x20, 0x28, 0x0A, 0x20, @@ -160,17 +167,20 @@ const char random_array[] = 0x66, 0x65, 0x65, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6B, 0x21, 0x0A, 0x20, 0x0A, 0x00 }; +// clang-format on bool interactive_mode = true; +state_t state = STATE_STARTING; char key_hit = 0xff; const char* device_name = NULL; GList *device_list = NULL; -static struct termios tio, tio_old, stdout_new, stdout_old, stdin_new, stdin_old; -static unsigned long rx_total = 0, tx_total = 0; +static struct termios tio, tio_raw, tio_old, stdout_new, stdout_old, stdin_new, stdin_old; +unsigned long rx_total = 0, tx_total = 0; static bool connected = false; static bool standard_baudrate = true; +static bool tty_tcsetattr_first = true; static void (*printchar)(char c); static int device_fd; static char hex_chars[2]; @@ -181,8 +191,12 @@ static char *tty_buffer_write_ptr = tty_buffer; static pthread_t thread; static int pipefd[2]; static pthread_mutex_t mutex_input_ready = PTHREAD_MUTEX_INITIALIZER; -static char line[PATH_MAX]; +char line[PATH_MAX], mline[MLINE_MAX]; +char inkey_chars[INKEY_CHARS_MAX]; static size_t listing_device_name_length_max = 0; +static readline_t *readline_ctx = NULL; +static readline_t *subcmd_readline_ctx = NULL; +static readline_t *script_repl_readline_ctx = NULL; static void optional_local_echo(char c) { @@ -212,7 +226,7 @@ inline static bool is_valid_hex(char c) inline static unsigned char char_to_nibble(char c) { - if(c >= '0' && c <= '9') + if (c >= '0' && c <= '9') { return c - '0'; } @@ -230,87 +244,302 @@ inline static unsigned char char_to_nibble(char c) } } +raw_t tty_get_raw_mode(void) +{ + switch (state) + { + case STATE_INTERACTIVE: + return option.raw_interactive; + + case STATE_STARTING: + case STATE_PIPED_INPUT: + case STATE_EXEC_SHELL_COMMAND: + case STATE_XYMODEM: + default: + return option.raw; + } +} + +/* Activate or change port settings */ +int tty_tcsetattr(int fd) +{ + static struct termios tio_cur; + static int baudrate_cur; + + if ( ! connected ) + { + return -1; + } + + int ret = 0; + raw_t raw = tty_get_raw_mode(); + struct termios *tiop = (raw == RAW_OFF) ? &tio : &tio_raw; + + /* If no need to change, no-op and return */ + if (tty_tcsetattr_first == false && + memcmp(&tio_cur, tiop, sizeof(struct termios)) == 0 && + baudrate_cur == option.baudrate) { + tio_debug_printf("same termios. skip tty_tcsetattr."); + return 0; + } + +#if defined(__CYGWIN__) + int line_state; + + /* save line state for buggy tcsetattr */ + if (ioctl(fd, TIOCMGET, &line_state) < 0) + { + tio_warning_printf("Could not get line state (%s)", strerror(errno)); + return -1; + } +#endif + + if (tcsetattr(fd, TCSANOW, tiop) == -1) + { + tio_error_printf_silent("Could not apply port settings (%s)", strerror(errno)); + tty_tcsetattr_first = true; + ret = -1; + goto tcsetattr_error_end; + } + + /* Set arbitrary baudrate (only works on supported platforms) */ + if (!standard_baudrate) + { + if (setspeed(device_fd, option.baudrate) != 0) + { + tio_error_printf_silent("Could not set baudrate speed (%s)", strerror(errno)); + tty_tcsetattr_first = true; + ret = -1; + goto setspeed_error_end; + } + } + + /* Port settings changed successfully */ + memcpy(&tio_cur, tiop, sizeof(tio_cur)); + baudrate_cur = option.baudrate; + tty_tcsetattr_first = false; + + tcsetattr_error_end: + setspeed_error_end: + +#if defined(__CYGWIN__) + /* restore line state for buggy tcsetattr */ + if (option.flow == FLOW_HARD) + { + /* hardware flow control */ + /* touch DTR only, don't touch RTS */ + int tiocm_dtr = TIOCM_DTR; + int action = (line_state & TIOCM_DTR) ? TIOCMBIS /* DTR=LOW */ : TIOCMBIC /* DTR=HIGH */ ; + if (ioctl(fd, action, &tiocm_dtr) < 0) + { + tio_warning_printf("Could not set line state (%s)", strerror(errno)); + ret = -1; + } + } + else + { + /* not hardware flow control */ + /* restore DTR and RTS at the same time */ + if (ioctl(fd, TIOCMSET, &line_state) < 0) + { + tio_warning_printf("Could not set line state (%s)", strerror(errno)); + ret = -1; + } + } +#endif + + return ret; +} + void tty_sync(int fd) { + /* If output_delay is valid, tty_buffer should be already empty. + * So this function doesn't consider output_delay options. */ ssize_t count; + size_t remain = tty_buffer_count; + char *cp = tty_buffer; - while (tty_buffer_count > 0) + while (remain > 0) { - count = write(fd, tty_buffer, tty_buffer_count); + count = write_poll(fd, cp, remain, POLL_FOREVER); if (count < 0) { // Error tio_debug_printf("Write error while flushing tty buffer (%s)", strerror(errno)); break; } - tty_buffer_count -= count; - fsync(fd); - tcdrain(fd); + cp += count; + remain -= count; + + // Update transmit statistics + tx_total += count; + + // Reduce the number of additional writes + if (remain > 0) + { + int estimated_sendtime_us = (int)((int64_t)count * 10 * 1000 * 1000 / option.baudrate); + if (estimated_sendtime_us > 300 * 1000) + usleep(300 * 1000); + else + usleep(estimated_sendtime_us); + } } + fsync(fd); + tcdrain(fd); // Reset tty_buffer_write_ptr = tty_buffer; tty_buffer_count = 0; } -ssize_t tty_write(int fd, const void *buffer, size_t count) +static ssize_t tty_raw_write(int fd) { - ssize_t retval = 0, bytes_written = 0; - size_t i; - - if (option.map_o_ltu) + raw_t raw = tty_get_raw_mode(); + if ((raw == RAW_ON_NODELAY) || + ((raw == RAW_ON_DELAY) && ( ! option.output_delay )) || + ((raw == RAW_OFF) && ( ! (option.output_delay || option.output_line_delay || option.map_o_nulbrk) ))) { - // Convert lower case to upper case - for (i = 0; i= BUFSIZ) { - delay(option.output_line_delay); + status = tty_raw_write(fd); + if (status < 0) + { + return status; + } + tty_sync(fd); } - fsync(fd); - tcdrain(fd); - - if (option.output_delay) - { - delay(option.output_delay); - } + *tty_buffer_write_ptr = *cp; + tty_buffer_write_ptr++; + tty_buffer_count++; } } else { - // Force write of tty buffer if too full - if ((tty_buffer_count + count) > BUFSIZ) + /* not RAW mode */ + for (i = 0; i < count; i++, cp++) { - tty_sync(fd); - } + if (tty_buffer_count >= BUFSIZ) + { + status = tty_raw_write(fd); + if (status < 0) + { + return status; + } + tty_sync(fd); + } - // Copy bytes to tty write buffer - memcpy(tty_buffer_write_ptr, buffer, count); - tty_buffer_write_ptr += count; - tty_buffer_count += count; - bytes_written = count; + /* Map output character */ + char *tp; + int bytes_add; + + bytes_add = -1; /* negative value means "not mapped yet" */ + tp = tty_buffer_write_ptr; + if ((*cp == 127) && (option.map_o_del_bs)) + { + *tp = '\b'; + bytes_add = 1; + } + if ((*cp == '\r') && (option.map_o_cr_nl)) + { + *tp = '\n'; + bytes_add = 1; + } + if ((*cp == '\r') && (option.map_o_ign_cr)) + { + bytes_add = 0; + } + if ((*cp == '\n' || *cp == '\r') && (option.map_o_nl_crnl)) + { + *tp = '\r'; + *(tp + 1) = '\n'; + bytes_add = 2; + } + if (bytes_add < 0) + { + *tp = (option.map_o_ltu) ? toupper(*cp) : *cp; + bytes_add = 1; + } + + if (bytes_add > 0) + { + tty_buffer_write_ptr += bytes_add; + tty_buffer_count += bytes_add; + } + } } - return bytes_written; + status = tty_raw_write(fd); + if (status < 0) + { + return status; + } + return count; } void *tty_stdin_input_thread(void *arg) @@ -331,7 +560,7 @@ void *tty_stdin_input_thread(void *arg) pthread_mutex_unlock(&mutex_input_ready); // Input loop for stdin - while (1) + while (true) { /* Input from stdin ready */ byte_count = read(STDIN_FILENO, input_buffer, BUFSIZ); @@ -360,7 +589,8 @@ void *tty_stdin_input_thread(void *arg) for (int i = 0; i 0) + { + readline_input(ctx, c); + if (c == '\r') break; + } + } + strncpy(line, readline_get(ctx), PATH_MAX - 1); + line[PATH_MAX - 1] = 0; + + return strlen(line); +} + +static int tty_script_repl_readln() +{ + return tty_readln(script_repl_readline_ctx, ""); +} + +int tty_subcmd_readln(const char *title_prompt) +{ + return tty_readln(subcmd_readline_ctx, title_prompt); +} + void tty_output_mode_set(output_mode_t mode) { switch (mode) @@ -631,6 +926,7 @@ void tty_output_mode_set(output_mode_t mode) static void mappings_print(void) { + // clang-format off if (option.map_i_cr_nl || option.map_ign_cr || option.map_i_ff_escc || option.map_i_nl_cr || option.map_i_nl_crnl || option.map_i_cr_crnl || option.map_o_cr_nl || option.map_o_del_bs || option.map_o_nl_crnl || @@ -656,13 +952,58 @@ static void mappings_print(void) { tio_printf(" Mappings: none"); } + // clang-format on +} + +static void handle_script_repl(void) +{ + bool local_echo_bkup = option.local_echo; + int line_len; + int mline_len = 0; + + option.local_echo = true; + tio_printf("Enter Lua REPL mode (@exit to exit)"); + + strcpy(mline, ""); + while (true) + { + tty_script_repl_readln(); + if (strcmp(line, "@exit") == 0) + break; + line_len = strlen(line); + + if (line_len > 0) + { + if (mline_len + line_len + 1 > MLINE_MAX) + { + tio_printf("Too long lines. The size should be lesser then %d bytes", MLINE_MAX); + strcpy(mline, ""); + mline_len = 0; + continue; + } + + strcat(&mline[mline_len], line); + mline_len += line_len; + + if (mline_len > 0 && mline[mline_len - 1] == '\\') + { + mline[mline_len - 1] = '\n'; + continue; + } + } + script_do_line(mline); + strcpy(mline, ""); + mline_len = 0; + } + + option.local_echo = local_echo_bkup; } void handle_command_sequence(char input_char, char *output_char, bool *forward) { char unused_char; bool unused_bool; - int state; + int line_state; static tty_line_mode_t line_mode; static sub_command_t sub_command = SUBCOMMAND_NONE; static char previous_char = 0; @@ -717,12 +1058,14 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) break; case SUBCOMMAND_XMODEM: + state_t state_orig = state; + state = STATE_XYMODEM; + tty_tcsetattr(device_fd); switch (input_char) { case KEY_0: tio_printf("Send file with XMODEM-1K"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tty_subcmd_readln("Enter file name: ")) { int ret; @@ -735,8 +1078,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_1: tio_printf("Send file with XMODEM-CRC"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tty_subcmd_readln("Enter file name: ")) { int ret; @@ -749,14 +1091,39 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_2: tio_printf("Receive file with XMODEM-CRC"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tty_subcmd_readln("Enter file name: ")) { int ret; tio_printf("Ready to receiving file '%s' ", line); tio_printf("Press any key to abort transfer"); - ret = xymodem_send(device_fd, line, XMODEM_CRC); + ret = xymodem_receive(device_fd, line, XMODEM_CRC); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); + } + break; + + case KEY_3: + tio_printf("Send file with XMODEM-SUM"); + if (tty_subcmd_readln("Enter file name: ")) + { + int ret; + + tio_printf("Sending file '%s' ", line); + tio_printf("Press any key to abort transfer"); + ret = xymodem_send(device_fd, line, XMODEM_SUM); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); + } + break; + + case KEY_4: + tio_printf("Receive file with XMODEM-SUM"); + if (tty_subcmd_readln("Enter file name: ")) + { + int ret; + + tio_printf("Ready to receiving file '%s' ", line); + tio_printf("Press any key to abort transfer"); + ret = xymodem_receive(device_fd, line, XMODEM_SUM); tio_printf("%s", ret < 0 ? "Aborted" : "Done"); } break; @@ -765,6 +1132,8 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_error_print("Invalid protocol option"); break; } + state = state_orig; + tty_tcsetattr(device_fd); break; case SUBCOMMAND_MAP: @@ -852,6 +1221,26 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) return; } + // Handle user keymapped commands + for (int idx = 0; idx < KEYMAP_MAX; idx++) + { + if ((input_char >= 0x00) && (input_char <= 0x1f)) + { + int ctrl_key_ch = ctrl_key_char(input_char); + if ((ctrl_key_ch >= 0) && + (strncmp("ctrl-", keymaps[idx].key, 5) == 0) && (keymaps[idx].key[5] == ctrl_key_ch)) + { + script_run(keymaps[idx].func); + goto handle_commands_end; + } + } + else if (input_char == keymaps[idx].key[0] && keymaps[idx].key[1] == '\0') + { + script_run(keymaps[idx].func); + goto handle_commands_end; + } + } + // Handle commands switch (input_char) { @@ -865,6 +1254,9 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf(" ctrl-%c F Flush data I/O buffers", option.prefix_key); tio_printf(" ctrl-%c g Toggle serial port line", option.prefix_key); tio_printf(" ctrl-%c i Toggle input mode", option.prefix_key); + tio_printf(" ctrl-%c j Toggle raw mode for non-interactive use", option.prefix_key); + tio_printf(" ctrl-%c J Toggle raw mode for interactive use", option.prefix_key); + tio_printf(" ctrl-%c k Set key-script mappings", option.prefix_key); tio_printf(" ctrl-%c l Clear screen", option.prefix_key); tio_printf(" ctrl-%c L Show line states", option.prefix_key); tio_printf(" ctrl-%c m Change mapping of characters on input or output", option.prefix_key); @@ -879,21 +1271,22 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf(" ctrl-%c x Send/Receive file via Xmodem", option.prefix_key); tio_printf(" ctrl-%c y Send file via Ymodem", option.prefix_key); tio_printf(" ctrl-%c ctrl-%c Send ctrl-%c character", option.prefix_key, option.prefix_key, option.prefix_key); + keymaps_print("User key commands:", 1); break; case KEY_SHIFT_L: - if (ioctl(device_fd, TIOCMGET, &state) < 0) + if (ioctl(device_fd, TIOCMGET, &line_state) < 0) { tio_warning_printf("Could not get line state (%s)", strerror(errno)); break; } tio_printf("Line states:"); - tio_printf(" DTR: %s", (state & TIOCM_DTR) ? "LOW" : "HIGH"); - tio_printf(" RTS: %s", (state & TIOCM_RTS) ? "LOW" : "HIGH"); - tio_printf(" CTS: %s", (state & TIOCM_CTS) ? "LOW" : "HIGH"); - tio_printf(" DSR: %s", (state & TIOCM_DSR) ? "LOW" : "HIGH"); - tio_printf(" DCD: %s", (state & TIOCM_CD) ? "LOW" : "HIGH"); - tio_printf(" RI : %s", (state & TIOCM_RI) ? "LOW" : "HIGH"); + tio_printf(" DTR: %s", (line_state & TIOCM_DTR) ? "LOW" : "HIGH"); + tio_printf(" RTS: %s", (line_state & TIOCM_RTS) ? "LOW" : "HIGH"); + tio_printf(" CTS: %s", (line_state & TIOCM_CTS) ? "LOW" : "HIGH"); + tio_printf(" DSR: %s", (line_state & TIOCM_DSR) ? "LOW" : "HIGH"); + tio_printf(" DCD: %s", (line_state & TIOCM_CD) ? "LOW" : "HIGH"); + tio_printf(" RI : %s", (line_state & TIOCM_RI) ? "LOW" : "HIGH"); break; case KEY_F: @@ -954,6 +1347,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) rs485_print_config(); } mappings_print(); + keymaps_print(" Keymaps:", 4); break; case KEY_E: @@ -1005,12 +1399,63 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) } break; + case KEY_J: + option.raw += 1; + switch (option.raw) + { + case RAW_ON_DELAY: + tio_printf("Turn on raw mode for non-interactive use"); + break; + case RAW_ON_NODELAY: + tio_printf("Turn on raw-nodelay mode for non-interactive use"); + break; + case RAW_OFF: + default: + option.raw = RAW_OFF; + tio_printf("Turn off raw mode for non-interactive use"); + break; + } + if (state != STATE_INTERACTIVE) + { + tty_tcsetattr(device_fd); + } + break; + + case KEY_SHIFT_J: + option.raw_interactive += 1; + switch (option.raw_interactive) + { + case RAW_ON_DELAY: + tio_printf("Turn on raw mode for interactive use"); + break; + case RAW_ON_NODELAY: + tio_printf("Turn on raw-nodelay mode for interactive use"); + break; + case RAW_OFF: + default: + option.raw_interactive = RAW_OFF; + tio_printf("Turn off raw mode for interactive use"); + break; + } + if (state == STATE_INTERACTIVE) + { + tty_tcsetattr(device_fd); + } + break; + + case KEY_K: + /* Set keymap */ + tty_subcmd_readln("Enter keymap @=|!