diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 43e399a..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/2.0/configuration-reference -version: 2.1 - -# Define a job to be invoked later in a workflow. -# See: https://circleci.com/docs/2.0/configuration-reference/#jobs -jobs: - build-tio: - # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. - # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor - docker: - - image: cimg/base:edge - # Add steps to the job - # See: https://circleci.com/docs/2.0/configuration-reference/#steps - steps: - - checkout - - run: sudo apt-get -qq update - - run: sudo apt-get install -y bash-completion git meson libinih-dev liblua5.2-dev libglib2.0-dev - - run: git clone https://github.com/tio/tio.git - - run: cd tio && meson build --prefix $HOME/test/tio && ninja -C build install - -# Invoke jobs via workflows -# See: https://circleci.com/docs/2.0/configuration-reference/#workflows -workflows: - build-tio-workflow: - jobs: - - build-tio diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..682ae09 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +BasedOnStyle: llvm +IndentWidth: 4 +AllowShortFunctionsOnASingleLine: None +KeepEmptyLinesAtTheStartOfBlocks: false +BreakBeforeBraces: Allman diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8164922..8a195f4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,8 +12,8 @@ name: "CodeQL" on: - # push: - # branches: [ "main", "master" ] + push: + branches: [ "main", "master" ] schedule: - cron: '0 0 * * *' pull_request: @@ -27,7 +27,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners # Consider using larger runners for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-20.04' }} + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-22.04' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: actions: read @@ -51,7 +51,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) #- name: Autobuild - # uses: github/codeql-action/autobuild@v2 + # uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -78,7 +78,7 @@ jobs: ./.github/workflows/codeql-buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" upload: false @@ -107,14 +107,14 @@ jobs: output: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif - name: Upload CodeQL results to code scanning - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ${{ steps.step1.outputs.sarif-output }} category: "/language:${{matrix.language}}" - name: Upload CodeQL results as an artifact if: success() || failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: codeql-results path: ${{ steps.step1.outputs.sarif-output }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000..8f54070 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,30 @@ +name: MacOS build + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + brew install meson ninja lua + + - name: Build + run: | + meson setup build + meson compile -C build --verbose + meson install -C build diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml new file mode 100644 index 0000000..90e1a93 --- /dev/null +++ b/.github/workflows/ubuntu.yml @@ -0,0 +1,31 @@ +name: Ubuntu build + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y bash-completion git meson liblua5.2-dev libglib2.0-dev + + - name: Build + run: | + meson setup build --prefix $HOME/opt/tio + meson compile -C build --verbose + meson install -C build diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..11f019f --- /dev/null +++ b/.typos.toml @@ -0,0 +1,2 @@ +[default] +extend-ignore-words-re = ["tio"] diff --git a/AUTHORS b/AUTHORS index 9aaace3..3754640 100644 --- a/AUTHORS +++ b/AUTHORS @@ -53,5 +53,18 @@ Sebastian Mingjie Shen Brian Davis C +KhazAkar +Eliot Alan Foss +Robert Lipe +Heinrich Schuchardt +Tomka Gergely +Steve Marple +konosubakonoakua +Keith Hill +Lubov66 +V +Samuel Holland +David Ordnung + Thanks to everyone who has contributed to this project. diff --git a/LICENSE b/LICENSE index a793eef..d5fa60d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2022 Martin Lund +Copyright (c) 2014-2025 Martin Lund This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/NEWS b/NEWS index 8a4739e..4289610 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,356 @@ +=== tio v3.9 (2025-04-13) === -=== tio v3.0 === + + +Changes since tio v3.8 (2024-11-30): + + * Fix parsing of timestamp options + + * codeql: Upgrade to upload-artifact@v4 + + * Update plaintext man page + + * Add character mapping examples + + * Fix pattern matching memory corruption + +Samuel Holland: + + * Don't add null characters to the expect buffer + + They prevent regexec() from seeing the remainder of the buffer. + +V: + + * Disable stdout buffering globally + + This makes it possible to pipe output to other programs cleanly. + +Lubov66: + + * docs: edited the license date + +Jakob Haufe: + + * Manpage: Fix backslash encoding + + Literal backslash needs to be written as \e. + + + +Changes since tio v3.7 (2024-08-31): + + * Rename git version to simply version + + * Clean up lua API + + Rename modem_send() to send() + Rename send to write() + + * Zero initialize buffer in read_string() + + * Use version from git + + * Fix memory leak in base62_encode() + + * Fix name declaration conflict with socket send() + + * Add clang-format spec + +Keith Hill: + + + Add system timestamps to lua read() and new lua read_line() per global options + + Add missing timestamp-format epoch + + Update send_ to use fsync and tcdrain like normal tty_sync does + + Rework read_line to save partial line at timeout + + Simplified read_line to reduce cyclomatic complexity + + renamed example files read.lua and read_line.lua + + moved #define READ_LINE_SIZE to top of file + + renamed g_linebuf to linebuf, and moved it into read_line as a static variable + + + +Changes since tio v3.6 (2024-07-19): + + * Remove unnecessary sync in line input mode + + This caused a problem for some highly timing sensitive modem read-eval-print + loops because the input line and line termination characters (cr/nl) would be + shifted out on the UART with too big delay inbetween because of two + syncs. + + * Fix socket send call on platforms without MSG_NOSIGNAL + + To fix build issue encountered on MacOS Catalina but may apply to other + platforms. + +Steve Marple: + + * Add "epoch" timestamp option + + Add an option that prints the timestamp as the number of seconds since + the Unix epoch. + +Tomka Gergely: + + * The log-directory options is not read from the configuration file. + + + +Changes since tio v3.5 (2024-06-29): + + * Add configuration file include directive + + To include the contents of another configuration file simply do e.g.: + + [include raspberrypi.conf] + + Also, included file can include other files which can include other + files etc. + + This feature is useful for managing many configuration files and sharing + configuration files with others. + + * Mention how to list key commands in help output + + * Fix hex output mode when using normal input mode + + In this combination of modes the input character was not forwarded to + the tty device. This fix makes sure it is forwarded. + + * Fix uptime on MacOS + + On MacOS the birth time is apparently not available so we use + modification time instead. + + * Improve warning upon failing connect + + Add device path to warning when connect fails. + + * Fix crashy search_reset() on macOS + + * Clean up shadow variable + + * Clean up readline code + + * Improve listing of long device names + + * Fix listing of serial devices on macOS + +Heinrich Schuchardt: + + * Print correct 'Done' timestamp for X- and Y-modem transfers + + Call tio_printf() after completing xymodem_send(). + +Robert Lipe: + + * Recompute listing_device_name_length_max for MacOS case, too. + + + +Changes since tio v3.4: + + * Clarify input and output direction of map flags + + * Rename mapping flag MSB2LSB to IMSB2LSB + + This is the correct naming since we are changing the input bit order on + input from the serial device. + + * Add OIGNCR mapping flag + + Ignores CR on output to serial device. + + * Fix line input mode ignoring characters ABCD + + * Fix tainted print + +Jakob Haufe: + + * Fix typos + + + +Changes since tio v3.3: + + * Update configuration output + + * Clean up script run interaction text + + * Fix unbounded writes + + * Add history and editing feature to line input mode + + Use up and down arrow keys to navigate history. + + Use left and right arrow keys to move cursor back and forth. + + We try mimic the behaviour of GNU readline which we can not use because + we also need to react to key commands. + + * Reuse socket address + + To avoid having to wait for socket timeout when restarting server. + + * Fix line input mode + + Fix so that ABCD are no longer ignored. + + * Make sure ICRNL, IGNCR, INLCR take effect + + * Include correct header for poll() + + * Add group write permission to xymodem received file + + * Fix missing open() flags in xymodem_receive() + +Vyacheslav Patkov: + + * Show current mappings in the configuration printout + + * Use "ctrl-t m" to change mappings interactively + + * Prompt for Lua script or shell command in interactive session + +Eliot Alan Foss: + + * Added support to receive XMODEM-CRC files from the connected serial port. + + + +Changes since tio v3.2: + + * Force destructive backspace when using local echo + + Only takes effect in normal output mode. + + * Fix local-echo in configuration file + + * Clean up includes + + * Force socket write operation to ignore any signals + + * Man page cleanup + + + +Changes since tio v3.1: + + * Do not print error when using --list with broken config file + + * Clean up completion script + + * Add option '--exec ' for running shell command + + Runs shell command with I/O redirected to device. + + * Make sure all error output is directed to stderr + + * Fix shadow variables + + * Update man page + + * Fix build on older GNU/Linux systems without statx + + * Fix line ending in --list output + + * Print location of configuration file in --list output + + * Fix alignment of profile listing + + + +Changes since tio v3.0: + + * Improve --list feature on non-linux platform + + * List available profiles in --list output + + * Always message when saving log file + + * Add support for using TID as device in config file + + * Fix use of invalid flag with regexec() + + * Fix potential buffer overflow in match_and_replace() + + * Fix profile autocompletion + + * Remove inih dependency from CI builds + + * Replace use of stat() with fstat() + + For better security. + + * Fix hexN output mode + + * Update pattern matching example + + * Fix submenu response when invalid key hit + + * Replace inih with glib key file parser + + After including the use of glib we might as well replace inih + with the glib key file parser. + + All configuration file parsing has been reworked and also the options + parsing has been cleaned up, resulting in better and stricter + configuration file and option value checks. + + Compared to old, configuration files now requires any default + configurations to be put in a group/section named [default]. + + Configuration file keywords such as "enable", "disable", "on", + "off", "yes", "no", "0", "1" have been retired. Now only "true" and + "false" apply to boolean configuration options. This is done to simplify + things and avoid any confusion. + + The pattern option feature has been reworked so now the user can now + access the full match string and any matching subexpression using the + %mN syntax. + + For example: + + [usb devices] + pattern = usb([0-9]*) + device = /dev/ttyUSB%m1 + + Then when using tio: + $ tio usb12 + + %m0 = 'usb12' // Full match string + %m1 = 12 // First match subexpression + + Which results in device = /dev/ttyUSB12 + + * Remove CircleCI + + Replaced with github workflow CI. + + * Add github workflow for Ubuntu build + + * Enable extended pattern matching + + So that the exclude options can also work as include using special + pattern syntax. + + For example, to only include /dev/ttyUSB* devices simply do: + + $ tio --exclude-devices=!(/dev/ttyUSB*) --list + + See the man page of fnmatch() for all available extended pattern + options. + + * Update lua read() description + +Rui Chen: + + * fix: add build patch for `FNM_EXTMATCH` + + * feat: add macOS workflow + + * fix: add macOS build patch for `fs_get_creation_time` @@ -224,11 +575,11 @@ Changes since tio v2.7: * Add lua modem_send(file,protocol) - * Fix xymodem error print outs + * Fix xymodem error messages * Rework x/y-modem transfer command - Remove ctrl-t X optin and instead introduce submenu to ctrl-t x option + Remove ctrl-t X option and instead introduce submenu to ctrl-t x option for picking which xmodem protocol to use. * Update README @@ -237,22 +588,22 @@ Changes since tio v2.7: * Add independent input and output mode - Replaces -x, --hexadecimal option with --intput-mode and --output-mode + Replaces -x, --hexadecimal option with --input-mode and --output-mode so it is possible to select hex or normal mode for both input and output independently. - To obtain same behaviour as -x, --hexadecimal use the following + To obtain same behavior as -x, --hexadecimal use the following configuration: input-mode = hex output-mode = hex - * Fix file descriptor handling on MacOS + * Fix file descriptor handling on macOS * Add tty line configuration script API On some platforms calling high()/low() to switch line states result in - costly system calls whick makes it impossible to swith two or more tty + costly system calls which makes it impossible to switch two or more tty lines simultaneously. To help solve this timing issue we introduce a tty line state @@ -288,7 +639,7 @@ Changes since tio v2.7: lines. Script is activated automatically on connect or manually via in session key command. - The Lua scripting feature opens up for many posibilities in the future + The Lua scripting feature opens up for many possibilities in the future such as adding expect like functionality to easily and programatically interact with the connected device. @@ -385,7 +736,7 @@ Changes since tio v2.5: Add --log-append option which makes tio append to any existing log file. - This also changes the default behaviour of tio from appending to + This also changes the default behavior of tio from appending to overwriting any existing log file. Now you have to use this new option to make tio append. @@ -719,7 +1070,7 @@ Changes since tio v1.46: Victor Oliveira - * add macports install instructions + * add MacPorts install instructions @@ -1219,7 +1570,7 @@ Changes since tio v1.35: * Handle SIGHUP Handle SIGHUP so that the registered exit handlers are called to restore - the terminal back to its orignal state. + the terminal back to its original state. * Add color configuration support @@ -1275,10 +1626,10 @@ Changes since tio v1.34: * Add support for configurable timestamp format - Also changes default timestamp format from ISO8601 to classic 24-hour + Also changes default timestamp format from ISO 8601 to classic 24-hour format as this is assumed to be the format that most users would prefer. - And reintroduces strict but optional ISO8601 format. + And reintroduces strict but optional ISO 8601 format. This feature allows to easily add more timestamp formats in the future. @@ -1399,10 +1750,10 @@ Sylvain LAFRASSE: attila-v: - * Refine timestamps with milliseconds and ISO-8601 format (#129). + * Refine timestamps with milliseconds and ISO 8601 format (#129). * Show milliseconds too in the timestamp (#114) and log file (#124) - * Change timestamp format to ISO-8601. + * Change timestamp format to ISO 8601. Yin Fengwei: @@ -1481,7 +1832,7 @@ Lars Kellogg-Stedman: George Stark: - * dont show line state if ioctl failed + * don't show line state if ioctl failed * add serial lines manual control @@ -1494,9 +1845,9 @@ arichi: Mariusz Midor: - * Newline: handle booth NL and CR + * Newline: handle both NL and CR - Flag ONLCRNL expects code \n after press Enter, but on some systems \r is send instead. + Flag ONLCRNL expects code \n after press Enter, but on some systems \r is sent instead. @@ -1988,7 +2339,7 @@ Changes since tio v1.11: To display the total number of bytes transmitted/received simply perform the 'ctrl-t s' command sequence. - This feature can be useful when eg. trying to detect non-printable + This feature can be useful when e.g. trying to detect non-printable characters. * Further simplification of key handling diff --git a/README.md b/README.md index 8ba76cd..e3ab066 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,12 @@ # tio - a serial device I/O tool -[![](https://img.shields.io/circleci/build/github/tio/tio)](https://circleci.com/github/tio/tio/tree/master) +[![](https://img.shields.io/github/actions/workflow/status/tio/tio/ubuntu.yml?label=Ubuntu)](https://github.com/tio/tio/actions/workflows/ubuntu.yml) +[![](https://img.shields.io/github/actions/workflow/status/tio/tio/macos.yml?label=MacOS)](https://github.com/tio/tio/actions/workflows/macos.yml) +[![](https://github.com/tio/tio/actions/workflows/codeql.yml/badge.svg)](https://github.com/tio/tio/actions/workflows/codeql.yml) +[![](https://img.shields.io/codefactor/grade/github/tio/tio)](https://www.codefactor.io/repository/github/tio/tio) [![](https://img.shields.io/github/v/release/tio/tio?sort=semver)](https://github.com/tio/tio/releases) [![](https://img.shields.io/repology/repositories/tio)](https://repology.org/project/tio/versions) - ## 1. Introduction @@ -32,14 +34,15 @@ when used in combination with [tmux](https://tmux.github.io). * Easily connect to serial TTY devices * Sensible defaults (115200 8n1) - * Support for non-standard baud rates - * Support for mark and space parity * Automatic connection management + * Automatic detection of serial ports * Automatic reconnect * Automatically connect to first new appearing serial device * Automatically connect to latest registered serial device * Connect to same port/device combination via unique topology ID (TID) * 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 * Support for RS-485 mode * List available serial devices @@ -72,6 +75,8 @@ when used in combination with [tmux](https://tmux.github.io). * Configuration file support * Support for configuration profiles * Activate configuration profiles by name or pattern + * Support for including other configuration files + * Redirect I/O of shell command to serial device * Redirect I/O to UNIX socket or IPv4/v6 network socket * Useful for scripting or TTY sharing * Pipe input and/or output @@ -87,7 +92,7 @@ when used in combination with [tmux](https://tmux.github.io). * Send files via x/y-modem protocol * Search for serial devices * Man page documentation - * Plays nicely with [tmux](https://tmux.github.io) + * Plays nicely with [tmux](https://tmux.github.io) and similar terminal multiplexers ## 3. Usage @@ -97,7 +102,7 @@ For more usage details please see the man page documentation ### 3.1 Command-line The command-line interface is straightforward as reflected in the output from -'tio --help': +```tio --help```: ``` Usage: tio [] @@ -123,7 +128,7 @@ Options: -t, --timestamp Enable line timestamp --timestamp-format Set timestamp format (default: 24hour) --timestamp-timeout Set timestamp timeout (default: 200) - -l, --list List available serial devices + -l, --list List available serial devices, TIDs, and profiles -L, --log Enable log to file --log-file Set log filename --log-directory Set log directory path for automatic named logs @@ -139,11 +144,14 @@ Options: --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 -v, --version Display version -h, --help Display help Options and profiles may be set via configuration file. +In session you can press ctrl-t ? to list available key commands. + See the man page for more details. ``` @@ -191,6 +199,11 @@ By-path /dev/serial/by-path/pci-0000:00:14.0-usb-0:6.4:1.0-port0 /dev/serial/by-path/pci-0000:00:14.0-usbv2-0:6.3:1.2 /dev/serial/by-path/pci-0000:00:14.0-usb-0:6.3:1.2 + +Configuration profiles (/home/lundmar/.config/tio/config) +-------------------------------------------------------------------------------- +rpi3 stm32 esp32 am64-evm +imx8mp-evk nucleo-h743zi2 usb-devices ``` It is recommended to connect serial TTY devices by ID: @@ -206,8 +219,9 @@ topology ID (TID): $ tio bCC2 ``` Note: The TID is unique and will stay the same as long as your USB serial port -device plugs into the same USB topology (same ports, same hubs, etc.). This way -it is possible for tio to successfully reconnect to the same device. +device plugs into the same USB topology (same ports, same hubs, same +connections, etc.). This way it is possible for tio to successfully reconnect +to the same device. Connect automatically to first new appearing serial device: ``` @@ -219,19 +233,24 @@ Connect automatically to latest registered serial device: $ tio --auto-connect latest ``` -It is also possible to use exclude options to affect which serial devices are +It is possible to use exclude options to affect which serial devices are involved in the automatic connection strategy: ``` $ tio --auto-connect new --exclude-devices "/dev/ttyACM?,/dev/ttyUSB2" ``` -Exclude drivers by pattern: +And to exclude drivers by pattern: ``` $ tio --auto-connect new --exclude-drivers "cdc_acm,ftdi_sio" ``` Note: Pattern matching supports '*' and '?'. Use comma separation to define multiple patterns. +To include drivers by specific pattern simply negate the exclude option: +``` +$ tio --auto-connect new --exclude-drivers !("cp2102") +``` + Log to file with autogenerated filename: ``` $ tio --log /dev/ttyUSB0 @@ -257,6 +276,11 @@ Redirect I/O to IPv4 network socket on port 4242: $ tio --socket inet:4242 /dev/ttyUSB0 ``` +Map NL to CR-NL on input from device and DEL to BS on output to device: +``` +$ tio --map INLCRNL,ODELBS /dev/ttyUSB0 +``` + Pipe data to the serial device: ``` $ cat data.bin | tio /dev/ttyUSB0 @@ -264,12 +288,12 @@ $ cat data.bin | tio /dev/ttyUSB0 Manipulate modem lines on connect: ``` -$ tio --script "set{DTR=high,RTS=low}; msleep(100); set{DTR=toggle,RTS=toggle}" /dev/ttyUSB0 +$ tio --script "tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{DTR=toggle,RTS=toggle}" /dev/ttyUSB0 ``` Pipe command to serial device and wait for line response within 1 second: ``` -$ echo "*IDN?" | tio /dev/ttyACM0 --script "expect('\r\n', 1000)" --mute +$ echo "*IDN?" | tio /dev/ttyACM0 --script "tio.expect('\r\n', 1000)" --mute KORAD KD3305P V4.2 SN:32475045 ``` @@ -290,14 +314,14 @@ ctrl-t ? to list the available key commands. [15:02:53.269] ctrl-t i Toggle input mode [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 Toggle MSB to LSB bit order +[15:02:53.269] ctrl-t m Change mapping of characters on input or output [15:02:53.269] ctrl-t o Toggle output mode [15:02:53.269] ctrl-t p Pulse serial port line [15:02:53.269] ctrl-t q Quit [15:02:53.269] ctrl-t r Run script +[15:02:53.269] ctrl-t R Execute shell command with I/O redirected to device [15:02:53.269] ctrl-t s Show statistics [15:02:53.269] ctrl-t t Toggle line timestamp mode -[15:02:53.269] ctrl-t U Toggle conversion to uppercase on output [15:02:53.269] ctrl-t v Show version [15:02:53.269] ctrl-t x Send file via Xmodem [15:02:53.269] ctrl-t y Send file via Ymodem @@ -306,66 +330,7 @@ ctrl-t ? to list the available key commands. If needed, the prefix key (ctrl-t) can be remapped via configuration file. -### 3.3 Lua script API - -Tio suppots Lua scripting to easily automate interaction with the tty device. - -In addition to the Lua API tio makes the following functions available: - -``` -expect(string, timeout) - Expect string - waits for string to match or timeout before continueing. - Supports regular expressions. Special characters must be escaped with '\\'. - Timeout is in milliseconds, defaults to 0 meaning it will wait forever. - - Returns 1 on successful match, 0 on timeout, or -1 on error. - - On successful match it also returns the match string as second return value. - -send(string) - Send string. - - Returns number of bytes written on success or -1 on error. - -modem_send(file, protocol) - Send file using x/y-modem protocol. - - Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. - -tty_search() - 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", "driver", - "description". - - Returns nil if no serial devices are found. - -read(size, timeout) - Read from serial device. If timeout is 0 or not provided it will wait - forever until data is ready to read. - - Returns number of bytes read on success, 0 on timeout, or -1 on error. - -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. - -sleep(seconds) - Sleep for seconds. - -msleep(ms) - Sleep for miliseconds. - -exit(code) - Exit with exit code. -``` - -### 3.4 Configuration file +### 3.3 Configuration file Options can be set via the configuration file first found in any of the following locations in the order listed: @@ -377,13 +342,13 @@ The configuration file supports profiles using named sections which can be activated via the command-line by name or pattern. A profile specifies which TTY device to connect to and other options. -### 3.4.1 Examples +### 3.3.1 Example Example configuration file: ``` -# Defaults -baudrate = 9600 +[default] +baudrate = 115200 databits = 8 parity = none stopbits = 1 @@ -391,27 +356,27 @@ color = 10 [rpi3] device = /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 -baudrate = 115200 -no-reconnect = enable -log = enable +no-reconnect = true +log = true log-file = rpi3.log line-pulse-duration = DTR=200,RTS=150 color = 11 [svf2] device = /dev/ttyUSB0 -script = expect("login: "); send("root\n"); expect("Password: "); send("root\n") +baudrate = 9600 +script = tio.expect("login: "); tio.write("root\n"); tio.expect("Password: "); tio.write("root\n") color = 12 [esp32] device = /dev/serial/by-id/usb-0403_6014-if00-port0 -script = set{DTR=high,RTS=low}; msleep(100); set{DTR=low,RTS=high}; msleep(100); set{RTS=low} +script = tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{DTR=low,RTS=high}; tio.msleep(100); tio.set{RTS=low} script-run = once color = 13 -[usb devices] -pattern = usb([0-9]*) -device = /dev/ttyUSB%s +[usb-devices] +pattern = ^usb([0-9]*) +device = /dev/ttyUSB%m1 color = 14 ``` @@ -426,6 +391,85 @@ $ tio usb12 Another more elaborate configuration file example is available [here](examples/config/config). +### 3.4 Lua script API + +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: + + +#### `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. + +Returns the captures from the pattern or `nil` on timeout. + +#### `tio.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. + +Returns a string up to `size` bytes long on success and `nil` on timeout. + +#### `tio.readline(timeout)` + +Read line from serial device. If timeout is 0 or not provided it will wait +forever until data is ready to read. + +Returns a string on success and `nil` on timeout. On timeout a partially read +line may be returned as a second return value. + +#### `tio.write(string)` + +Write string to serial device. + +Returns the `tio` table. + +#### `tio.send(file, protocol)` + +Send file using x/y-modem protocol. + +Protocol can be any of `XMODEM_1K`, `XMODEM_CRC`, `YMODEM`. + +#### `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", "driver", +"description". + +Returns `nil` if no serial devices are found. + +#### `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`. + +#### `tio.sleep(seconds)` + +Sleep for seconds. + +#### `tio.msleep(ms)` + +Sleep for milliseconds. + +#### `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`. + + ## 4. Installation ### 4.1 Installation using package manager (Linux) @@ -464,6 +508,12 @@ $ pacman -S tio The latest source releases can be found [here](https://github.com/tio/tio/releases). +Before running the install steps make sure you have glib and lua libraries installed. For example: + +``` +$ sudo apt install libglib2.0-dev liblua5.2-dev +``` + Install steps: ``` $ meson setup build @@ -479,9 +529,13 @@ 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. For example, to add your user to the 'dialout' group do: +Add your user to the group which allows serial device access permanently. For example, to add your user to the 'dialout' group do: +```bash +sudo usermod -a -G dialout ``` -$ sudo usermod -a -G dialout +Switch to the "dialout" group, temporary but immediately for this session. +```bash +newgrp dialout ``` diff --git a/TODO b/TODO index bf36c62..6036c7b 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,40 @@ + * Add release support for arm and x86 binary tarballs + + * Support input and input mapping from lua scripts + + * Add option to send file raw (no modem protocol) + + * Add loopback option + + Send received serial input back to output (for testing etc.) + + * Add loopback support between two serial ports + + Useful for traffic monitoring + + * Add mapping feature for printing non-printable characters + + * Porting layer to support native win32 builds. + + Some of the work that needs to be done: + + All posix functions need to be platform independent, go though file by file: + + termios.h + unistd.h has very limited functions + ENV different in config_file_resolve + errno + sys/ioctl.h + sys/poll.h + socket, may need a new thread + Serial, RS485, character mapping + Communication pipe + + Port enumerate, all devices of the same type have the same name (eg. USB + Serial Device for ttyACM) -> which makes regex not meaningful (kind of a + good thing since libtre in Mingw has too much dependencies makes binary too + big) + * Support traditional hex output format such as: 00000000 74 65 73 74 20 74 65 73 74 20 74 65 73 74 20 74 |test test test t| @@ -13,22 +50,6 @@ Already supported in hex output mode. - * Advanced line mode - - Current line mode only support backspace editing. Would be nice with arrow - key navigation left/right and insert/overwrite support. Also history browsing - pressing up/down. - - * Support for running external command - - Add key command e.g. 'ctrl-t r' which prompts user to run external command. - The command will be run in a process which stdin/stdout is redirected to the - serial port. - - This is the first step towards maybe also adding automatic support for - x/y/zmodem data transfer protocols by calling external programs such as - rb/sb, rx/sx, rz/sz, etc. - * Allow tio to connect to socket After some more consideration I think it makes sense to support connecting to a diff --git a/examples/config/config b/examples/config/config index 4e0de75..7bd141e 100644 --- a/examples/config/config +++ b/examples/config/config @@ -9,28 +9,28 @@ # $HOME/.config/tio/config # $HOME/.tioconfig -# Defaults +[default] baudrate = 115200 databits = 8 flow = none stopbits = 1 parity = none -prefix-ctrl-key = t output-delay = 0 output-line-delay = 0 auto-connect = direct -no-reconnect = disable +no-reconnect = false +local-echo = false input-mode = normal output-mode = normal -timestamp = disable -log = disable -log-append = disable -log-strip = disable -local-echo = disable +timestamp = false +log = false +log-append = false +log-strip = false color = bold -rs-485 = disable +rs-485 = false alert = none script-run = always +prefix-ctrl-key = t # Configuration profiles @@ -50,25 +50,29 @@ color = 10 [tincan] baudrate = 9600 device = /dev/serial/by-id/usb-TinCanTools_Flyswatter2_FS20000-if00-port0 -log = enable +log = true log-file = tincan.log -log-strip = enable +log-strip = true color = 11 -[usb] -pattern = usb([0-9]*) -device = /dev/ttyUSB%s +[usb-devices] +pattern = ^usb([0-9]*) +device = /dev/ttyUSB%m1 color = 12 [rs-485-device] device = /dev/ttyUSB0 -rs-485 = enable +rs-485 = true rs-485-config = RTS_ON_SEND=1,RTS_AFTER_SEND=1,RTS_DELAY_BEFORE_SEND=60,RTS_DELAY_AFTER_SEND=80,RX_DURING_TX color = 13 [esp32] device = /dev/ttyUSB0 color = 14 -script = high(DTR); low(RTS); msleep(100); low(DTR); high(RTS); msleep(100); low(RTS) +script = tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{DTR=low,RTS=high}; tio.msleep(100); tio.set{RTS=low} script-run = always +[buspirate] +device = /dev/ttyACM0 +map = INLCRNL,ODELBS +color = 15 diff --git a/examples/lua/automatic-linux-login.lua b/examples/lua/automatic-linux-login.lua index a287b40..aa7a990 100644 --- a/examples/lua/automatic-linux-login.lua +++ b/examples/lua/automatic-linux-login.lua @@ -13,14 +13,13 @@ local logins = { }, } -local found, match_str = expect("\\w+- login:", 10) -if (1 == found) then - local hostname = string.match(match_str, "^%w+") +local hostname = tio.expect("^(%g+) login:", 10) +if hostname then local login = logins[hostname] if (nil ~= login) then - send(login.username .. "\n") - expect("Password:") - send(login.password .. "\n") + tio.write(login.username .. "\n") + tio.expect("Password:") + tio.write(login.password .. "\n") else io.write("\r\nDon't know login info for " .. hostname .. "\r\n") end diff --git a/examples/lua/control-lines-test.lua b/examples/lua/control-lines-test.lua index 5b54ab4..55d98b5 100644 --- a/examples/lua/control-lines-test.lua +++ b/examples/lua/control-lines-test.lua @@ -1,5 +1,5 @@ -set{DTR=high, RTS=low} -msleep(100) -set{DTR=low, RTS=high} -msleep(100) -set{RTS=toggle} +tio.set{DTR=high, RTS=low} +tio.msleep(100) +tio.set{DTR=low, RTS=high} +tio.msleep(100) +tio.set{RTS=toggle} diff --git a/examples/lua/read.lua b/examples/lua/read.lua new file mode 100644 index 0000000..6452816 --- /dev/null +++ b/examples/lua/read.lua @@ -0,0 +1,14 @@ +tio.read(1000, 6000) -- initial config +tio.write("\n") +tio.msleep(100) +tio.read(650, 60) -- main menu +tio.write("S") -- S menu +tio.msleep(30) +tio.read(650, 60) +tio.write("t") -- Parallel Value Table +tio.read(650, 60) +while true do + tio.msleep(1000) + tio.write("t") + tio.read(650, 50) -- repeat PVT forever +end diff --git a/examples/lua/read_line.lua b/examples/lua/read_line.lua new file mode 100644 index 0000000..a844b48 --- /dev/null +++ b/examples/lua/read_line.lua @@ -0,0 +1,15 @@ +tio.read(1000, 8000) -- read initial config +tio.write("\n") +tio.read(650, 100) -- main menu +tio.write("S") -- S menu +repeat + str = tio.readline(25) +until str == nil +while true do + tio.write("t") -- query PV table + tio.msleep(880) + repeat + str = tio.readline(60) + tio.msleep(60) + until str == nil +end diff --git a/examples/lua/serial-device-search.lua b/examples/lua/serial-device-search.lua index 120d650..77d9dbc 100644 --- a/examples/lua/serial-device-search.lua +++ b/examples/lua/serial-device-search.lua @@ -1,6 +1,6 @@ io.write("Searching... ") -local device = tty_search() +local device = tio.ttysearch() io.write("done\r\n") diff --git a/man/tio.1.in b/man/tio.1.in index b305d07..eb0338d 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -148,6 +148,10 @@ Set timestamp format to any of the following timestamp formats: 24-hour format relative to previous timestamp .IP "\fBiso8601" ISO8601 format ("YYYY-MM-DDThh:mm:ss.sss") +.IP "\fBepoch" +Seconds since Unix epoch (1970-01-01) +.IP "\fBepoch-usec" +Seconds since Unix epoch (1970-01-01) with subdivision in microseconds .PP Default format is \fB24hour\fR .RE @@ -165,7 +169,7 @@ Default value is 200. .TP .BR \-l ", " \-\-list -List available serial devices. +List available targets (serial devices, TIDs, configuration profiles). .TP .BR \-L ", " \-\-log @@ -173,7 +177,8 @@ List available serial devices. Enable log to file. The log file will be automatically named using the following format -tio_DEVICE_YYYY-MM-DDTHH:MM:SS.log. +tio_TARGET_YYYY-MM-DDTHH:MM:SS.log. Target being the command line target such +as tty-device, tid, or configuration profile. The filename can be manually set using the \-\-log-file option. @@ -200,8 +205,8 @@ Strip control characters and escape sequences from log. .TP .BR \-m ", " "\-\-map " \fI -Map (replace, translate) characters on input or output. The following mapping -flags are supported: +Map (replace, translate) characters on input to the serial device or output +from the serial device. The following mapping flags are supported: .RS .TP 12n @@ -215,6 +220,10 @@ Map FF to ESC-c on input Map NL to CR on input .IP "\fBINLCRNL" Map NL to CR-NL on input +.IP "\fBICRCRNL" +Map CR to CR-NL on input +.IP "\fBIMSB2LSB" +Map MSB bit order to LSB on input .IP "\fBOCRNL" Map CR to NL on output .IP "\fBODELBS" @@ -225,8 +234,8 @@ Map NL to CR-NL on output Map lowercase characters to uppercase on output .IP "\fBONULBRK" Map nul (zero) to send break signal on output -.IP "\fBMSB2LSB" -Map MSB bit order to LSB on output +.IP "\fBOIGNCR" +Ignore CR on output .P If defining more than one flag, the flags must be comma separated. .RE @@ -357,6 +366,15 @@ Run script on connect once, always, or never. Default value is "always". +.TP +.BR "\-\-exec \fI + +Execute shell command with I/O redirected to device + +.TP +.BR "\-\-complete-profiles + +Prints profiles (for shell completion) .TP .BR \-v ", " \-\-version @@ -365,10 +383,10 @@ Display program version. .BR \-h ", " \-\-help Display help. -.SH "KEYS" +.SH "KEY COMMANDS" .PP .TP 16n -In session, all key strokes are forwarded to the serial device except the following key sequence: a prefix key (default: ctrl-t) followed by a command key. These sequences are intercepted as tio commands: +In session, all key strokes are forwarded to the serial device except the following key sequence: a prefix key (default: ctrl-t) followed by a command key. These sequences are intercepted as key commands: .IP "\fBctrl-t ?" List available key commands .IP "\fBctrl-t b" @@ -390,7 +408,7 @@ Clear screen .IP "\fBctrl-t L" Show line states (DTR, RTS, CTS, DSR, DCD, RI) .IP "\fBctrl-t m" -Toggle MSB to LSB bit order +Change mapping of characters on input or output .IP "\fBctrl-t o" Toggle output mode .IP "\fBctrl-t p" @@ -399,12 +417,12 @@ Pulse serial port line Quit .IP "\fBctrl-t r" Run script +.IP "\fBctrl-t R" +Execute shell command with I/O redirected to device .IP "\fBctrl-t s" Show TX/RX statistics .IP "\fBctrl-t t" Toggle line timestamp mode -.IP "\fBctrl-t U" -Toggle conversion to uppercase on output .IP "\fBctrl-t v" Show version .IP "\fBctrl-t x" @@ -418,30 +436,41 @@ Send ctrl-t character .PP Tio suppots Lua scripting to easily automate interaction with the tty device. -In addition to the Lua API tio makes the following functions available: +In addition to the standard Lua API tio makes the following functions +and variables available: .TP 6n -.IP "\fBexpect(string, timeout)" -Expect string - waits for string to match or timeout before continueing. -Supports regular expressions. Special characters must be escaped with '\\\\'. +.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. -Returns 1 on successful match, 0 on timeout, or -1 on error. +Returns the captures from the pattern or nil on timeout. -On successful match it also returns the match string as second return value. +.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. -.IP "\fBsend(string)" -Send string. +Returns a string up to size bytes long on success and nil on timeout. -Returns number of bytes written on success or -1 on error. +.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. -.IP "\fBmodem_send(file, protocol)" +Returns a string on success and nil on timeout. On timeout a partially read +line may be returned as a second return value. + +.IP "\fBtio.write(string)" +Write string to serial device. + +Returns the tio table. + +.IP "\fBtio.send(file, protocol)" Send file using x/y-modem protocol. Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. -.IP "\fBtty_search()" +.IP "\fBtio.ttysearch()" Search for serial devices. Returns a table of number indexed tables, one for each serial device found. @@ -451,25 +480,26 @@ following string indexed elements "path", "tid", "uptime", "driver", Returns nil if no serial devices are found. -.IP "\fBread(size, timeout)" -Read from serial device. If timeout is 0 or not provided it will wait forever -until data is ready to read. - -Returns number of bytes read on success, 0 on timeout, or -1 on error. - -.IP "\fBset{line=state, ...}" +.IP "\fBtio.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. -.IP "\fBsleep(seconds)" +.IP "\fBtio.sleep(seconds)" Sleep for seconds. -.IP "\fBmsleep(ms)" -Sleep for miliseconds. -.IP "\fBexit(code)" -Exit with exit code. +.IP "\fBtio.msleep(ms)" +Sleep for milliseconds. + +.IP "\fBtio.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. .SH "CONFIGURATION FILE" .PP @@ -568,6 +598,12 @@ Run script from string Run script from file .IP "\fBscript-run" Run script on connect +.IP "\fBexec" +Execute shell command with I/O redirected to device + +.PP +It is possible to include the content of other configuration files using the +include directive like so: "[include ]". .SH "CONFIGURATION FILE EXAMPLES" @@ -577,7 +613,7 @@ To change the default configuration simply set options like so: .RS .nf .eo -# Defaults +[default] baudrate = 9600 databits = 8 parity = none @@ -613,15 +649,14 @@ Which is equivalent to: $ tio -b 115200 -c 11 /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 .TP -A configuration profile can also be activated by its pattern which supports -regular expressions: +A configuration profile can also be activated by its pattern which supports regular expressions: .RS .nf .eo -[usb device] -pattern = usb([0-9]*) -device = /dev/ttyUSB%s +[usb-devices] +pattern = ^usb([0-9]*) +device = /dev/ttyUSB%m1 baudrate = 115200 .ec .fi @@ -633,13 +668,12 @@ Activate the configuration profile by pattern match: $ tio usb12 .TP -Which is equivalent to: +Which becomes equivalent to: $ tio -b 115200 /dev/ttyUSB12 .TP -It is also possible to combine use of configuration profile and command-line -options. For example: +It is also possible to combine use of configuration profile and command-line options. For example: $ tio -l -t usb12 @@ -692,9 +726,9 @@ expect -i $uart "prompt> " .RE .TP -It is also possible to use the expect/send script functionality to e.g. automate logins: +It is also possible to use tio's own simpler expect/send script functionality to e.g. automate logins: -$ tio --script 'expect("login: "); send("root\\n"); expect("Password: "); send("root\\n")' /dev/ttyUSB0 +$ tio --script 'tio.expect("login: "); tio.write("root\\n"); tio.expect("Password: "); tio.write("root\\n")' /dev/ttyUSB0 .TP Redirect device I/O to network file socket for remote TTY sharing: @@ -715,7 +749,7 @@ $ echo "ls -la" | tio /dev/serial/by\-id/usb\-FTDI_TTL232R-3V3_FTGQVXBL\-if00\-p .TP Pipe command to serial device and wait for line response within 1 second: -$ echo "*IDN?" | tio /dev/ttyACM0 --script "expect('\\r\\n', 1000)" --mute +$ echo "*IDN?" | tio /dev/ttyACM0 --script "tio.expect('\\r\\n', 1000)" --mute .TP .TP @@ -723,6 +757,11 @@ Likewise, to pipe data from file to the serial device: $ cat data.bin | tio /dev/serial/by\-id/usb\-FTDI_TTL232R-3V3_FTGQVXBL\-if00\-port0 +.TP +Map NL to CR-NL on input from device and DEL to BS on output to device: + +$ tio --map INLCRNL,ODELBS /dev/ttyUSB0 + .TP Enable RS-485 mode: @@ -731,12 +770,7 @@ $ tio --rs-485 --rs-485-config=RTS_ON_SEND=1,RX_DURING_TX /dev/ttyUSB0 .TP 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 - -.TP -Automatically log in to connected OS: - -$ tio --script "expect('password:'); send('my_password\\n')" /dev/ttyUSB0 +$ tio --script "tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{RTS=toggle}" --script-run once /dev/ttyUSB0 .SH "WEBSITE" .PP diff --git a/man/tio.1.txt b/man/tio.1.txt index a6851db..eeac82c 100644 --- a/man/tio.1.txt +++ b/man/tio.1.txt @@ -1,10 +1,10 @@ -tio(1) User Commands tio(1) +tio(1) User Commands tio(1) NAME tio - a serial device I/O tool SYNOPSIS - tio [] + tio [] DESCRIPTION tio is a serial device tool which features a straightforward command-line and configuration file interface to easily connect to serial TTY devices for basic I/O operations. @@ -114,6 +114,10 @@ OPTIONS iso8601 ISO8601 format ("YYYY-MM-DDThh:mm:ss.sss") + epoch Seconds since Unix epoch (1970-01-01) + + epoch-usec Seconds since Unix epoch (1970-01-01) with subdivision microseconds + Default format is 24hour --timestamp-timeout @@ -126,13 +130,13 @@ OPTIONS -l, --list - List available serial devices. + List available targets (serial devices, TIDs, configuration profiles). -L, --log Enable log to file. - The log file will be automatically named using the following format tio_DEVICE_YYYY-MM-DDTHH:MM:SS.log. + The log file will be automatically named using the following format tio_TARGET_YYYY-MM-DDTHH:MM:SS.log. Target being the command line target such as tty-device, tid, or configuration profile. The filename can be manually set using the --log-file option. @@ -154,7 +158,7 @@ OPTIONS -m, --map - Map (replace, translate) characters on input or output. The following mapping flags are supported: + Map (replace, translate) characters on input to the serial device or output from the serial device. The following mapping flags are supported: ICRNL Map CR to NL on input (unless IGNCR is set) @@ -166,6 +170,10 @@ OPTIONS INLCRNL Map NL to CR-NL on input + ICRCRNL Map CR to CR-NL on input + + IMSB2LSB Map MSB bit order to LSB on input + OCRNL Map CR to NL on output ODELBS Map DEL to BS on output @@ -176,7 +184,7 @@ OPTIONS ONULBRK Map nul (zero) to send break signal on output - MSB2LSB Map MSB bit order to LSB on output + OIGNCR Ignore CR on output If defining more than one flag, the flags must be comma separated. @@ -192,9 +200,13 @@ OPTIONS Default value is "normal". - --output-mode normal|hex + --output-mode normal|hex|hexN - Set output mode. In hex mode each incoming byte is printed out as a 1 byte hex value. + Set output mode. + + In hex mode each incoming byte is printed out as a 1 byte hex value. + + In hexN mode, N is a number less than or equal to 4096 which defines how many hex values will be printed before a line break. Default value is "normal". @@ -210,7 +222,8 @@ OPTIONS Redirect I/O to socket. - Any input from clients connected to the socket is sent on the serial port as if entered at the terminal where tio is running (except that ctrl-t sequences are not recognized), and any input from the serial port is multiplexed to the terminal and all connected clients. + Any input from clients connected to the socket is sent on the serial port as if entered at the terminal where tio is running (except that ctrl-t sequences are not recognized), and any input from the serial port is multi‐ + plexed to the terminal and all connected clients. Sockets remain open while the serial port is disconnected, and writes will block. @@ -272,6 +285,14 @@ OPTIONS Default value is "always". + --exec + + Execute shell command with I/O redirected to device + + --complete-profiles + + Prints profiles (for shell completion) + -v, --version Display program version. @@ -280,8 +301,8 @@ OPTIONS Display help. -KEYS - In session, all key strokes are forwarded to the serial device except the following key sequence: a prefix key (default: ctrl-t) followed by a command key. These sequences are intercepted as tio commands: +KEY COMMANDS + In session, all key strokes are forwarded to the serial device except the following key sequence: a prefix key (default: ctrl-t) followed by a command key. These sequences are intercepted as key commands: ctrl-t ? List available key commands @@ -303,7 +324,7 @@ KEYS ctrl-t L Show line states (DTR, RTS, CTS, DSR, DCD, RI) - ctrl-t m Toggle MSB to LSB bit order + ctrl-t m Change mapping of characters on input or output ctrl-t o Toggle output mode @@ -313,12 +334,12 @@ KEYS ctrl-t r Run script + ctrl-t R Execute shell command with I/O redirected to device + ctrl-t s Show TX/RX statistics ctrl-t t Toggle line timestamp mode - ctrl-t U Toggle conversion to uppercase on output - ctrl-t v Show version ctrl-t x Send file using the XMODEM-1K or XMODEM-CRC protocol (prompts for file name and protocol) @@ -330,19 +351,35 @@ KEYS SCRIPT API Tio suppots Lua scripting to easily automate interaction with the tty device. - In addition to the Lua API tio makes the following functions available: + 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 continueing. Supports regular expressions. Special characters must be escaped with '\\'. Timeout is in milliseconds, defaults to 0 meaning it will wait forever. + 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. - Returns 1 on successful match, 0 on timeout, or -1 on invalid regular expression. + Returns 1 on successful match, 0 on timeout, or -1 on error. On successful match it also returns the match string as second return value. - send(string) - Send string. + 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. - modem_send(file, protocol) + Returns number of bytes read on success, 0 on timeout, or -1 on error. + + 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. + + 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. + + Returns number of bytes read on success, 0 on timeout, or -1 on error. + + 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. + + write(string) + Write string to serial device. + + Returns number of bytes written on success or -1 on error. + + send(file, protocol) Send file using x/y-modem protocol. Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. @@ -350,38 +387,26 @@ SCRIPT API tty_search() 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", "driver", "description". + 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‐ + ver", "description". Returns nil if no serial devices are found. - exit(code) - Exit with exit code. + set{line=state, ...} + Set state of one or multiple tty modem lines. - high(line) - Set tty line high. + Line can be any of DTR, RTS, CTS, DSR, CD, RI - low(line) - Set tty line low. - - toggle(line) - Toggle the tty line. + State is high, low, or toggle. sleep(seconds) Sleep for seconds. msleep(ms) - Sleep for miliseconds. + Sleep for milliseconds. - config_high(line) - Set tty line state configuration to high. - - config_low(line) - Set tty line state configuration to low. - - apply_config() - Apply tty line state configuration. Using the line state configuration API instead of high()/low() will help to make the lines physically switch as simultaneously as possible. This may solve timing issues on some platforms. - - Note: Line can be any of DTR, RTS, CTS, DSR, CD, RI + exit(code) + Exit with exit code. 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: @@ -392,9 +417,9 @@ CONFIGURATION FILE $HOME/.tioconfig - Labels can be used to group settings into named sub-configurations which can be activated from the command-line when starting tio. + Labels can be used to group settings into named configuration profiles which can be activated from the command-line when starting tio. - tio will try to match the user input to a sub-configuration by name or by pattern to get the TTY device and other options. + tio will try to match the user input to a configuration profile by name or by pattern to get the TTY device and other options. Options without any label change the default options. @@ -468,10 +493,14 @@ CONFIGURATION FILE script-run Run script on connect + exec Execute shell command with I/O redirected to device + + It is possible to include the content of other configuration files using the include directive like so: "[include ]". + CONFIGURATION FILE EXAMPLES To change the default configuration simply set options like so: - # Defaults + [default] baudrate = 9600 databits = 8 parity = none @@ -479,14 +508,14 @@ CONFIGURATION FILE EXAMPLES color = 10 line-pulse-duration = DTR=200,RTS=400 - Named sub-configurations can be added via labels: + Named configuration profiles can be added via labels: [rpi3] device = /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 baudrate = 115200 color = 11 - Activate the sub-configuration by name: + Activate the configuration profile by name: $ tio rpi3 @@ -494,22 +523,22 @@ CONFIGURATION FILE EXAMPLES $ tio -b 115200 -c 11 /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 - A sub-configuration can also be activated by its pattern which supports regular expressions: + A configuration profile can also be activated by its pattern which supports regular expressions: - [usb device] - pattern = usb([0-9]*) - device = /dev/ttyUSB%s + [usb-devices] + pattern = ^usb([0-9]*) + device = /dev/ttyUSB%m1 baudrate = 115200 - Activate the sub-configuration by pattern match: + Activate the configuration profile by pattern match: $ tio usb12 - Which is equivalent to: + Which becomes equivalent to: $ tio -b 115200 /dev/ttyUSB12 - It is also possible to combine use of sub-configuration and command-line options. For example: + It is also possible to combine use of configuration profile and command-line options. For example: $ tio -l -t usb12 @@ -551,6 +580,10 @@ EXAMPLES send -i $uart "ls -la\n" expect -i $uart "prompt> " + 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 + Redirect device I/O to network file socket for remote TTY sharing: $ tio --socket inet:4444 /dev/ttyUSB0 @@ -571,17 +604,17 @@ EXAMPLES $ cat data.bin | tio /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 + Map NL to CR-NL on input from device and DEL to BS on output to device: + + $ tio --map INLCRNL,ODELBS /dev/ttyUSB0 + Enable RS-485 mode: $ tio --rs-485 --rs-485-config=RTS_ON_SEND=1,RX_DURING_TX /dev/ttyUSB0 Manipulate DTR and RTS lines upon first connect to reset connected microcontroller: - $ tio --script "high(DTR); low(RTS); msleep(100); toggle(DTR)" --script-run once /dev/ttyUSB0 - - Automatically log in to connected OS: - - $ tio --script "expect('password:'); send('my_password\n')" /dev/ttyUSB0 + $ tio --script "set{DTR=high,RTS=low}; msleep(100); set{RTS=toggle}" --script-run once /dev/ttyUSB0 WEBSITE Visit https://tio.github.io @@ -589,4 +622,4 @@ WEBSITE AUTHOR Maintained by Martin Lund . -tio 2.9 2024-04-14 tio(1) +tio 3.9 2025-04-13 tio(1) diff --git a/meson.build b/meson.build index 79c8a67..b95b5c0 100644 --- a/meson.build +++ b/meson.build @@ -1,12 +1,12 @@ project('tio', 'c', - version : '3.0', - license : [ 'GPL-2'], + version : '3.9', + license : 'GPL-2.0-or-later', meson_version : '>= 0.53.2', default_options : [ 'warning_level=2', 'buildtype=release', 'c_std=gnu99' ] ) # The tag date of the project_version(), update when the version bumps. -version_date = '2024-04-14' +version_date = '2025-04-13' # Test for dynamic baudrate configuration interface compiler = meson.get_compiler('c') diff --git a/src/alert.c b/src/alert.c index e2f90a4..23a728e 100644 --- a/src/alert.c +++ b/src/alert.c @@ -19,38 +19,10 @@ * 02110-1301, USA. */ -#include "alert.h" -#include "config.h" #include -#include #include -#include -#include "error.h" -#include "print.h" #include "options.h" - -alert_t alert_option_parse(const char *arg) -{ - alert_t alert = option.alert; // Default - - if (arg != NULL) - { - if (strcmp(arg, "none") == 0) - { - return ALERT_NONE; - } - else if (strcmp(arg, "bell") == 0) - { - return ALERT_BELL; - } - else if (strcmp(arg, "blink") == 0) - { - return ALERT_BLINK; - } - } - - return alert; -} +#include "alert.h" void blink_background(void) { @@ -109,18 +81,3 @@ void alert_disconnect(void) break; } } - -const char *alert_state_to_string(alert_t state) -{ - switch (state) - { - case ALERT_NONE: - return "none"; - case ALERT_BELL: - return "bell"; - case ALERT_BLINK: - return "blink"; - default: - return "Unknown"; - } -} diff --git a/src/alert.h b/src/alert.h index d7b66f5..91258c9 100644 --- a/src/alert.h +++ b/src/alert.h @@ -29,7 +29,5 @@ typedef enum ALERT_END, } alert_t; -alert_t alert_option_parse(const char *arg); void alert_connect(void); void alert_disconnect(void); -const char *alert_state_to_string(alert_t state); diff --git a/src/bash-completion/tio.in b/src/bash-completion/tio.in index 75a207c..b3b61cb 100644 --- a/src/bash-completion/tio.in +++ b/src/bash-completion/tio.in @@ -45,6 +45,8 @@ _tio() --script \ --script-file \ --script-run \ + --exec \ + --complete-profiles \ -v --version \ -h --help" @@ -79,60 +81,16 @@ _tio() COMPREPLY=( $(compgen -W "1 10 100" -- ${cur}) ) return 0 ;; - --line-pulse-duration) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; -a | --auto-connect) COMPREPLY=( $(compgen -W "new latest none" -- ${cur}) ) return 0 ;; - -n | --no-reconnect) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - -e | --local-echo) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - -l | --log) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - --log-file) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - --log-directory) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - --log-append) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - --log-strip) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; -m | --map) - COMPREPLY=( $(compgen -W "ICRNL IGNCR INLCR IFFESCC INLCRNL OCRNL ODELBS ONLCRNL MSB2LSB" -- ${cur}) ) - return 0 - ;; - -t | --timestamp) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + COMPREPLY=( $(compgen -W "ICRNL IGNCR INLCR IFFESCC INLCRNL ICRCRNL IMSB2LSB OCRNL ODELBS ONLCRNL OLTU ONULBRK OIGNCR" -- ${cur}) ) return 0 ;; --timestamp-format) - COMPREPLY=( $(compgen -W "24hour 24hour-start 24hour-delta iso8601" -- ${cur}) ) - return 0 - ;; - --timestamp-timeout) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - -L | --list) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + COMPREPLY=( $(compgen -W "24hour 24hour-start 24hour-delta iso8601 epoch epoch-usec" -- ${cur}) ) return 0 ;; -c | --color) @@ -151,10 +109,6 @@ _tio() COMPREPLY=( $(compgen -W "normal hex" -- ${cur}) ) return 0 ;; - --rs-485) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; --rs-485-config) COMPREPLY=( $(compgen -W "RTS_ON_SEND RTS_AFTER_SEND RTS_DELAY_BEFORE_SEND RTS_DELAY_AFTER_SEND RX_DURING_TX" -- ${cur}) ) return 0 @@ -163,30 +117,10 @@ _tio() COMPREPLY=( $(compgen -W "none bell blink" -- ${cur}) ) return 0 ;; - --mute) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - --script) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - --script-file) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; --script-run) COMPREPLY=( $(compgen -W "once always never" -- ${cur}) ) return 0 ;; - -v | --version) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - -h | --help) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; *) ;; esac diff --git a/src/configfile.c b/src/configfile.c index 0357b53..ca116bf 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -21,530 +21,768 @@ */ #define _GNU_SOURCE - -#include "config.h" -#include -#include -#include -#include -#include #include -#include -#include -#include -#include +#include #include +#include +#include +#include +#include #include -#include -#include "options.h" +#include #include "configfile.h" -#include "misc.h" -#include "options.h" -#include "error.h" +#include "timestamp.h" #include "print.h" #include "rs485.h" -#include "timestamp.h" -#include "alert.h" +#include "misc.h" -struct config_t +#define CONFIG_GROUP_NAME_DEFAULT "default" +#define CONFIG_GROUP_INCLUDE_PREFIX "include " +#define MAX_LINE_LENGTH 1024 + +struct config_t config = {}; + +static void config_file_load(const char *filename, GString *buffer, bool test); +static void config_file_process(const char *filename, GString *buffer, GList **included_files, bool test); + +static void config_get_string(GKeyFile *key_file, gchar *group, gchar *key, char **dest, char *allowed_string, ...) { - const char *user; + (void)dest; + GError *error = NULL; + bool mismatch = true; - char *path; - char *section_name; - char *match; - - char *target; - char *flow; - char *parity; - char *log_filename; - char *socket; - char *map; - char *script; - char *script_filename; - bool script_run; - char *exclude_devices; - char *exclude_drivers; - char *exclude_tids; -}; - -static struct config_t c; - -static int get_match(const char *input, const char *pattern, char **match) -{ - int ret; - int len = 0; - regex_t re; - regmatch_t m[2]; - char err[128]; - - /* compile a regex with the pattern */ - ret = regcomp(&re, pattern, REG_EXTENDED); - if (ret) + gchar *string = g_key_file_get_string(key_file, group, key, &error); + if (error != NULL) { - regerror(ret, &re, err, sizeof(err)); - tio_error_printf("Regex failure: %s", err); - return ret; - } - - /* try to match on input */ - ret = regexec(&re, input, 2, m, 0); - if (!ret) - { - len = m[1].rm_eo - m[1].rm_so; - } - - regfree(&re); - - if (len) - { - asprintf(match, "%s", &input[m[1].rm_so]); - } - - return len; -} - -static bool read_boolean(const char *value, const char *name) -{ - const char *true_values[] = { "true", "enable", "on", "yes", "1", NULL }; - const char *false_values[] = { "false", "disable", "off", "no", "0", NULL }; - - for (int i = 0; true_values[i] != NULL; i++) - if (strcmp(value, true_values[i]) == 0) - return true; - - for (int i = 0; false_values[i] != NULL; i++) - if (strcmp(value, false_values[i]) == 0) - return false; - - tio_error_printf("Invalid value '%s' for option '%s' in configuration file", - value, name); - exit(EXIT_FAILURE); -} - -static long read_integer(const char *value, const char *name, long min_value, long max_value) -{ - errno = 0; - char *endptr; - long result = strtol(value, &endptr, 10); - - if (errno || endptr == value || *endptr != '\0' || result < min_value || result > max_value) - { - tio_error_printf("Invalid value '%s' for option '%s' in configuration file", - value, name); + if (error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND) + { + // Key not found - ignore key + g_error_free(error); + return; + } + tio_error_print("%s: %s", config.path, error->message); + g_error_free(error); exit(EXIT_FAILURE); } - return result; + va_list args; + const char* current_arg = allowed_string; + va_start(args, allowed_string); + + if (current_arg == NULL) + { + mismatch = false; + } + + // Iterate through variable arguments + while (current_arg != NULL) + { + if (strcmp(string, current_arg) == 0) + { + mismatch = false; + break; + } + current_arg = va_arg(args, const char *); + } + + if (mismatch) + { + tio_error_print("%s: Invalid %s value '%s' in %s profile", config.path, key, string, group); + exit(EXIT_FAILURE); + } + + va_end(args); + + *dest = string; } -/** - * data_handler() - walk config file to load parameters matching user input - * - * INIH handler used to get all parameters from a given section - */ -static int data_handler(void *user, const char *section, const char *name, - const char *value) +static void config_get_integer(GKeyFile *key_file, gchar *group, gchar *key, int *dest, int min, int max) { - UNUSED(user); + (void)dest; + GError *error = NULL; - // If section matches current section being parsed - if (!strcmp(section, c.section_name)) + int value = g_key_file_get_integer(key_file, group, key, &error); + if (error != NULL) { - // Set configuration parameter if found - if (!strcmp(name, "device") || !strcmp(name, "tty")) + if (error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND) { - asprintf(&c.target, value, c.match); - option.target = c.target; + // Key not found - ignore key + g_error_free(error); + return; } - else if (!strcmp(name, "baudrate")) - { - option.baudrate = read_integer(value, name, 0, LONG_MAX); - } - else if (!strcmp(name, "databits")) - { - option.databits = read_integer(value, name, 5, 8); - } - else if (!strcmp(name, "flow")) - { - asprintf(&c.flow, "%s", value); - option.flow = c.flow; - } - else if (!strcmp(name, "stopbits")) - { - option.stopbits = read_integer(value, name, 1, 2); - } - else if (!strcmp(name, "parity")) - { - asprintf(&c.parity, "%s", value); - option.parity = c.parity; - } - else if (!strcmp(name, "output-delay")) - { - option.output_delay = read_integer(value, name, 0, LONG_MAX); - } - else if (!strcmp(name, "output-line-delay")) - { - option.output_line_delay = read_integer(value, name, 0, LONG_MAX); - } - else if (!strcmp(name, "line-pulse-duration")) - { - line_pulse_duration_option_parse(value); - } - else if (!strcmp(name, "no-reconnect")) - { - option.no_reconnect = read_boolean(value, name); - } - else if (!strcmp(name, "auto-connect")) - { - option.auto_connect = auto_connect_option_parse(value); - } - else if (!strcmp(name, "log")) - { - option.log = read_boolean(value, name); - } - else if (!strcmp(name, "log-file")) - { - asprintf(&c.log_filename, "%s", value); - option.log_filename = c.log_filename; - } - else if (!strcmp(name, "log-append")) - { - option.log_append = read_boolean(value, name); - } - else if (!strcmp(name, "log-strip")) - { - option.log_strip = read_boolean(value, name); - } - else if (!strcmp(name, "local-echo")) - { - option.local_echo = read_boolean(value, name); - } - else if (!strcmp(name, "input-mode")) - { - option.input_mode = input_mode_option_parse(value); - } - else if (!strcmp(name, "output-mode")) - { - option.output_mode = output_mode_option_parse(value); - } - else if (!strcmp(name, "timestamp")) - { - option.timestamp = read_boolean(value, name) ? - TIMESTAMP_24HOUR : TIMESTAMP_NONE; - } - else if (!strcmp(name, "timestamp-format")) - { - option.timestamp = timestamp_option_parse(value); - } - else if (!strcmp(name, "timestamp-timeout")) - { - option.timestamp_timeout = read_integer(value, name, 0, LONG_MAX); - } - else if (!strcmp(name, "map")) - { - asprintf(&c.map, "%s", value); - option.map = c.map; - } - else if (!strcmp(name, "color")) - { - if (!strcmp(value, "list")) - { - // Ignore - return 0; - } - else if (!strcmp(value, "none")) - { - option.color = -1; // No color - return 0; - } - else if (!strcmp(value, "bold")) - { - option.color = 256; // Bold - return 0; - } + tio_error_print("%s: %s", config.path, error->message); + g_error_free(error); + exit(EXIT_FAILURE); + } - option.color = atoi(value); - if ((option.color < 0) || (option.color > 255)) - { - option.color = -1; // No color - } - } - else if (!strcmp(name, "socket")) + if ((value < min) || (value > max)) + { + tio_error_print("%s: Invalid %s value '%d' in %s profile", config.path, key, value, group); + exit(EXIT_FAILURE); + } + + *dest = value; +} + +static void config_get_bool(GKeyFile *key_file, gchar *group, gchar *key, bool *dest) +{ + (void)dest; + GError *error = NULL; + + bool value = g_key_file_get_boolean(key_file, group, key, &error); + if (error != NULL) + { + if (error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND) { - asprintf(&c.socket, "%s", value); - option.socket = c.socket; + // Key not found - ignore key + g_error_free(error); + return; } - else if (!strcmp(name, "prefix-ctrl-key")) + tio_error_print("%s: %s", config.path, error->message); + g_error_free(error); + exit(EXIT_FAILURE); + } + + *dest = value; +} + +static void config_parse_keys(GKeyFile *key_file, char *group) +{ + char *string = NULL; + + config_get_string(key_file, group, "device", &config.device, NULL); + config_get_integer(key_file, group, "baudrate", &option.baudrate, 0, INT_MAX); + config_get_integer(key_file, group, "databits", &option.databits, 5, 8); + config_get_string(key_file, group, "flow", &string, "none", "hard", "soft", NULL); + if (string != NULL) + { + option_parse_flow(string, &option.flow); + g_free((void *)string); + string = NULL; + } + config_get_integer(key_file, group, "stopbits", &option.stopbits, 1, 2); + config_get_string(key_file, group, "parity", &string, "odd", "even", "none", "mark", "space", NULL); + if (string != NULL) + { + option_parse_parity(string, &option.parity); + g_free((void *)string); + string = NULL; + } + + 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, "line-pulse-duration", &string, NULL); + if (string != NULL) + { + option_parse_line_pulse_duration(string); + g_free((void *)string); + string = NULL; + } + config_get_string(key_file, group, "auto-connect", &string, "new", "latest", "direct", NULL); + if (string != NULL) + { + option_parse_auto_connect(string, &option.auto_connect); + g_free((void *)string); + string = NULL; + } + config_get_string(key_file, group, "exclude-devices", &option.exclude_devices, NULL); + 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, "local-echo", &option.local_echo); + config_get_string(key_file, group, "input-mode", &string, "normal", "hex", "line", NULL); + if (string != NULL) + { + option_parse_input_mode(string, &option.input_mode); + g_free((void *)string); + string = NULL; + } + config_get_string(key_file, group, "output-mode", &string, NULL); + if (string != NULL) + { + option_parse_output_mode(string, &option.output_mode); + g_free((void *)string); + string = NULL; + } + config_get_bool(key_file, group, "timestamp", (bool*) &option.timestamp); + if (option.timestamp != TIMESTAMP_NONE) + { + config_get_string(key_file, group, "timestamp-format", &string, "24hour", "24hour-start", "24hour-delta", "iso8601", "epoch", "epoch-usec", NULL); + if (string != NULL) { - if (!strcmp(value, "none")) - { - option.prefix_enabled = false; - } - else if (ctrl_key_code(value[0]) > 0) - { - option.prefix_code = ctrl_key_code(value[0]); - option.prefix_key = value[0]; - } + option_parse_timestamp(string, &option.timestamp); + g_free((void *)string); + string = NULL; } - else if (!strcmp(name, "rs-485")) + } + config_get_integer(key_file, group, "timestamp-timeout", &option.timestamp_timeout, 0, INT_MAX); + config_get_bool(key_file, group, "log", &option.log); + config_get_string(key_file, group, "log-file", &option.log_filename, NULL); + config_get_string(key_file, group, "log-directory", &option.log_directory, NULL); + config_get_bool(key_file, group, "log-append", &option.log_append); + config_get_bool(key_file, group, "log-strip", &option.log_strip); + config_get_string(key_file, group, "map", &string, NULL); + if (string != NULL) + { + option_parse_mappings(string); + g_free((void *)string); + string = NULL; + } + config_get_string(key_file, group, "color", &string, NULL); + if (string != NULL) + { + if (strcmp(string, "list") == 0) { - option.rs485 = read_boolean(value, name); + // Ignore } - else if (!strcmp(name, "rs-485-config")) + else if (strcmp(string, "none") == 0) { - rs485_parse_config(value); + option.color = -1; // No color } - else if (!strcmp(name, "alert")) + else if (strcmp(string, "bold") == 0) { - option.alert = alert_option_parse(value); - } - else if (!strcmp(name, "mute")) - { - option.mute = read_boolean(value, name); - } - else if (!strcmp(name, "pattern")) - { - // Do nothing - } - else if (!strcmp(name, "script")) - { - asprintf(&c.script, "%s", value); - option.script = c.script; - } - else if (!strcmp(name, "script-file")) - { - asprintf(&c.script_filename, "%s", value); - option.script_filename = c.script_filename; - } - else if (!strcmp(name, "script-run")) - { - option.script_run = script_run_option_parse(value); - } - else if (!strcmp(name, "exclude-devices")) - { - c.exclude_devices = strdup(value); - option.exclude_devices = c.exclude_devices; - } - else if (!strcmp(name, "exclude-drivers")) - { - c.exclude_drivers = strdup(value); - option.exclude_drivers = c.exclude_drivers; - } - else if (!strcmp(name, "exclude-tids")) - { - c.exclude_tids = strdup(value); - option.exclude_tids = c.exclude_tids; + option.color = 256; // Bold } else { - tio_warning_printf("Unknown option '%s' in configuration file, ignored", name); + option.color = atoi(string); + if ((option.color < 0) || (option.color > 255)) + { + tio_error_print("%s: Invalid color value in %s profile", config.path, group); + exit(EXIT_FAILURE); + } } + g_free((void *)string); + string = NULL; + } + config_get_string(key_file, group, "socket", &option.socket, NULL); + config_get_bool(key_file, group, "rs-485", &option.rs485); + config_get_string(key_file, group, "rs-385-config", &string, NULL); + if (string != NULL) + { + rs485_parse_config(string); + g_free((void *)string); + string = NULL; + } + config_get_string(key_file, group, "alert", &string, "bell", "blink", "none", NULL); + if (string != NULL) + { + option_parse_alert(string, &option.alert); + g_free((void *)string); + string = NULL; + } + config_get_bool(key_file, group, "mute", &option.mute); + 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); + if (string != NULL) + { + option_parse_script_run(string, &option.script_run); + g_free((void *)string); + string = NULL; + } + config_get_string(key_file, group, "exec", &option.exec, NULL); + config_get_string(key_file, group, "prefix-ctrl-key", &string, NULL); + if (string != NULL) + { + if (strcmp(string, "none") == 0) + { + option.prefix_enabled = false; + } + else if (strlen(string) >= 2) + { + tio_error_print("%s: Invalid prefix-ctrl-key value in %s profile", config.path, group); + exit(EXIT_FAILURE); + } + else if (ctrl_key_code(string[0]) > 0) + { + option.prefix_enabled = true; + option.prefix_code = ctrl_key_code(string[0]); + option.prefix_key = string[0]; + } + else + { + tio_error_print("%s: Invalid prefix-ctrl-key value in %s profile", config.path, group); + exit(EXIT_FAILURE); + } + g_free((void *)string); + string = NULL; } - - return 0; } -/** - * section_pattern_search_handler() - walk config file to find section matching user input - * - * INIH handler used to resolve the section matching the user's input. - * This will look for the pattern element of each section and try to match it - * with the user input. - */ -static int section_pattern_search_handler(void *user, const char *section, const char *varname, - const char *varval) -{ - UNUSED(user); - - if (strcmp(varname, "pattern")) - return 0; - - if (!strcmp(varval, c.user)) - { - /* pattern matches as plain text */ - asprintf(&c.section_name, "%s", section); - } - else if (get_match(c.user, varval, &c.match) > 0) - { - /* pattern matches as regex */ - asprintf(&c.section_name, "%s", section); - } - - return 0; -} - -/** - * section_pattern_search_handler() - walk config file to find section matching user input - * - * INIH handler used to resolve the section matching the user's input. - * This will try to match the user input against a section with the name of the user input. - */ -static int section_name_search_handler(void *user, const char *section, const char *varname, - const char *varval) -{ - UNUSED(user); - UNUSED(varname); - UNUSED(varval); - - if (!strcmp(section, c.user)) - { - /* section name matches as plain text */ - asprintf(&c.section_name, "%s", section); - } - - return 0; -} - -static int section_name_print_handler(void *user, const char *section, const char *varname, - const char *varval) -{ - UNUSED(user); - UNUSED(varname); - UNUSED(varval); - - static char *section_previous = ""; - - if (strcmp(section, section_previous) != 0) - { - printf("%s ", section); - section_previous = strdup(section); - } - - return 0; -} - -static int resolve_config_file(void) +static int config_file_resolve(void) { char *xdg = getenv("XDG_CONFIG_HOME"); if (xdg) { - if (asprintf(&c.path, "%s/tio/config", xdg) != -1) + if (asprintf(&config.path, "%s/tio/config", xdg) != -1) { - if (access(c.path, F_OK) == 0) + if (access(config.path, F_OK) == 0) { return 0; } - free(c.path); + free(config.path); } } char *home = getenv("HOME"); if (home) { - if (asprintf(&c.path, "%s/.config/tio/config", home) != -1) + if (asprintf(&config.path, "%s/.config/tio/config", home) != -1) { - if (access(c.path, F_OK) == 0) + if (access(config.path, F_OK) == 0) { return 0; } - free(c.path); + free(config.path); } - if (asprintf(&c.path, "%s/.tioconfig", home) != -1) + if (asprintf(&config.path, "%s/.tioconfig", home) != -1) { - if (access(c.path, F_OK) == 0) + if (access(config.path, F_OK) == 0) { return 0; } - free(c.path); + free(config.path); } } - c.path = NULL; + config.path = NULL; return -EINVAL; } void config_file_show_profiles(void) { - memset(&c, 0, sizeof(struct config_t)); + GString *config_buffer; + GError *error = NULL; + GKeyFile *keyfile; + + // Reset configuration + memset(&config, 0, sizeof(struct config_t)); // Find config file - if (resolve_config_file() != 0) + if (config_file_resolve() != 0) { // None found - stop parsing return; } - ini_parse(c.path, section_name_print_handler, NULL); + // Load content of configuration file into buffer + config_buffer = g_string_new(NULL); + config_file_load(config.path, config_buffer, false); + + // Load configuration + keyfile = g_key_file_new(); + if (g_key_file_load_from_data(keyfile, config_buffer->str, config_buffer->len, G_KEY_FILE_NONE, &error) == false) + { + g_error_free(error); + goto error_load; + } + + // Get all group names + gsize num_groups; + gchar **group = g_key_file_get_groups(keyfile, &num_groups); + + for (gsize i = 0; i < num_groups; i++) + { + // Skip default group + if (strcmp(group[i], CONFIG_GROUP_NAME_DEFAULT) == 0) + { + continue; + } + + // Skip group with include directive + if (strncmp(group[i], CONFIG_GROUP_INCLUDE_PREFIX, strlen(CONFIG_GROUP_INCLUDE_PREFIX)) == 0) + { + continue; + } + + printf("%s ", group[i]); + } + + g_strfreev(group); +error_load: + g_key_file_free(keyfile); + g_string_free(config_buffer, TRUE); +} + +static void replace_substring(char *str, const char *substr, const char *replacement) +{ + char *pos = strstr(str, substr); + if (pos != NULL) + { + int substrLen = strlen(substr); + int replacementLen = strlen(replacement); + memmove(pos + replacementLen, pos + substrLen, strlen(pos + substrLen) + 1); + memcpy(pos, replacement, replacementLen); + } +} + +static char *match_and_replace(const char *str, const char *pattern, char *device) +{ + char replacement_str[PATH_MAX] = {}; + char m_key[14] = {}; + regex_t regex; + + assert(str != NULL); + assert(pattern != NULL); + assert(device != NULL); + + char *string = calloc(PATH_MAX, 1); + if (string == NULL) + { + tio_debug_printf("Failure allocating string memory\n"); + return NULL; + } + strncpy(string, device, PATH_MAX - 1); + + /* Find matches of pattern in str. For each match, replace any '%mN' in the + * copy of the device string with the corresponding match subexpression and + * return the new formed device string. + * + * Note: %m0 = Full match expression. + * %m1 = First subexpression + * %m2 = Second subexpression + * %m3 = etc.. + */ + + if (regcomp(®ex, pattern, REG_EXTENDED) != 0) + { + // Failure to compile regular expression + tio_error_print("Failure compiling regular expression '%s'\n", pattern); + exit(EXIT_FAILURE); + } + + regmatch_t matches[regex.re_nsub + 1]; + int status = regexec(®ex, str, regex.re_nsub + 1, matches, 0); + if (status == 0) + { + tio_debug_printf("Full match: "); + int j = 0; + for (int i = matches[0].rm_so; i < matches[0].rm_eo; i++) + { + tio_debug_printf_raw("%c", str[i]); + replacement_str[j++] = str[i]; + } + replacement_str[j] = '\0'; + replace_substring(string, "%m0", replacement_str); + tio_debug_printf_raw("\n"); + + for (int i = 1; i < ((int)regex.re_nsub + 1) && matches[i].rm_so != -1; i++) + { + tio_debug_printf("Subexpression %d match: ", i); + int k = 0; + for (int l = matches[i].rm_so; l < matches[i].rm_eo; l++) + { + tio_debug_printf_raw("%c", str[l]); + replacement_str[k++] = str[l]; + } + replacement_str[k] = '\0'; + sprintf(m_key, "%%m%d", i); + replace_substring(string, m_key, replacement_str); + tio_debug_printf_raw("\n"); + } + } + else if (status == REG_NOMATCH) + { + tio_debug_printf("No regex match\n"); + goto error; + } + else + { + char error_message[100]; + regerror(status, ®ex, error_message, sizeof(error_message)); + tio_debug_printf("Regex match failed: %s", error_message); + goto error; + } + + regfree(®ex); + return string; + +error: + regfree(®ex); + return NULL; +} + +static void config_file_process_line(const char *line, GString *buffer, GList **included_files, bool test) +{ + if (strncmp(line, "[include ", 9) == 0 && line[strlen(line) - 2] == ']') + { + char include_filename[MAX_LINE_LENGTH]; + + // Construct the format string safely + char format_string[50]; + snprintf(format_string, sizeof(format_string), "[include %%%ds]", MAX_LINE_LENGTH - 1); + + int ret = sscanf(line, format_string, include_filename); + if (ret != 1) + { + return; + } + + // Remove the trailing ']' character + include_filename[strlen(include_filename) - 1] = '\0'; + + if (g_list_find_custom(*included_files, include_filename, (GCompareFunc)strcmp) != NULL) + { + // Already included, avoid recursion + return; + } + + // Add to included files list + *included_files = g_list_append(*included_files, g_strdup(include_filename)); + + // Process the included file + config_file_process(include_filename, buffer, included_files, test); + } + else + { + // Normal line, add to buffer + g_string_append(buffer, line); + } +} + +static void config_file_process(const char *filename, GString *buffer, GList **included_files, bool test) +{ + if (test) + { + // Test that configuration file can be parsed + + GError *error = NULL; + GKeyFile *keyfile = g_key_file_new(); + + if (g_key_file_load_from_file(keyfile, filename, G_KEY_FILE_NONE, &error) == false) + { + tio_error_print("Failure loading file %s: %s", filename, error->message); + g_key_file_free(keyfile); + g_error_free(error); + exit(EXIT_FAILURE); + } + } + + FILE *file = fopen(filename, "r"); + if (file) + { + char line[MAX_LINE_LENGTH]; + while (fgets(line, sizeof(line), file)) + { + config_file_process_line(line, buffer, included_files, test); + } + fclose(file); + } +} + +static void config_file_load(const char *filename, GString *buffer, bool test) +{ + char current_dir[PATH_MAX] = "."; + char *config_file_dir = dirname(strdup(config.path)); + GList *included_files = NULL; + + getcwd(current_dir, PATH_MAX); + + // Change to the directory of the configuration file + chdir(config_file_dir); + + config_file_process(filename, buffer, &included_files, test); + + // Restore current directory + chdir(current_dir); + + // Free memory + g_list_free_full(included_files, g_free); + free(config_file_dir); } void config_file_parse(void) { - int ret; - - memset(&c, 0, sizeof(struct config_t)); - // Find config file - if (resolve_config_file() != 0) + if (config_file_resolve() != 0) { // None found - stop parsing return; } - // Set user input which may be tty device or profile or tid - c.user = option.target; - - if (!c.user) + if (option.target == NULL) { return; } - // Parse default (unnamed) settings - asprintf(&c.section_name, "%s", ""); - ret = ini_parse(c.path, data_handler, NULL); - if (ret < 0) + GString *config_buffer = g_string_new(NULL); + GKeyFile *keyfile = g_key_file_new(); + GList *included_files = NULL; + GError *error = NULL; + + config_file_load(config.path, config_buffer, true); + + if (g_key_file_load_from_data(keyfile, config_buffer->str, config_buffer->len, G_KEY_FILE_NONE, &error) == false) { - tio_error_printf("Unable to parse configuration file (%d)", ret); + tio_error_print("Failure loading file %s: %s", config.path, error->message); + g_string_free(config_buffer, TRUE); + g_key_file_free(keyfile); + g_error_free(error); exit(EXIT_FAILURE); } - free(c.section_name); - c.section_name = NULL; - // Find matching section - ret = ini_parse(c.path, section_pattern_search_handler, NULL); - if (!c.section_name) + // Parse default group/section + if (g_key_file_has_group(keyfile, CONFIG_GROUP_NAME_DEFAULT)) { - ret = ini_parse(c.path, section_name_search_handler, NULL); - if (!c.section_name) + config_parse_keys(keyfile, CONFIG_GROUP_NAME_DEFAULT); + } + + // Parse target + if (g_key_file_has_group(keyfile, option.target)) + { + config.active_group = strdup(option.target); + config_parse_keys(keyfile, option.target); + } + else + { + // Find group by pattern + gsize num_groups; + gchar **group = g_key_file_get_groups(keyfile, &num_groups); + + for (gsize i = 0; i < num_groups; i++) { - tio_debug_printf("Unable to match user input to configuration section (%d)", ret); - return; + // Skip default group + if (strcmp(group[i], CONFIG_GROUP_NAME_DEFAULT) == 0) + { + continue; + } + + // Lookup 'pattern' key + gchar *pattern = g_key_file_get_string(keyfile, group[i], "pattern", &error); + if (error != NULL) + { + g_error_free(error); + error = NULL; + continue; + } + + // Lookup 'device' key + gchar *device = g_key_file_get_string(keyfile, group[i], "device", &error); + if (error != NULL) + { + g_error_free(error); + error = NULL; + continue; + } + + // Match pattern against target and replace any sub expression + // matches (%mN) in device string and return resulting string + // representing the new pattern based string. + config.device = match_and_replace(option.target, pattern, device); + if (config.device != NULL) + { + // Match found - save device + device = strdup(config.device); + + // Parse found group (may replace config.device) + config_parse_keys(keyfile, group[i]); + + // Update configuration + config.active_group = strdup(group[i]); + config.device = device; // Restore new device string + + break; + } } + + g_strfreev(group); } - // Parse settings of found section (profile) - ret = ini_parse(c.path, data_handler, NULL); - if (ret < 0) - { - tio_error_printf("Unable to parse configuration file (%d)", ret); - exit(EXIT_FAILURE); - } + // Cleanup + g_key_file_free(keyfile); + g_string_free(config_buffer, TRUE); + g_list_free_full(included_files, g_free); atexit(&config_exit); } void config_exit(void) { - free(c.target); - free(c.flow); - free(c.parity); - free(c.log_filename); - free(c.map); - - free(c.match); - free(c.section_name); - free(c.path); + free(config.active_group); + free(config.path); + free(config.device); } void config_file_print(void) { - if (c.path != NULL) + if (config.path != NULL) { - tio_printf(" Active configuration file: %s", c.path); - if (c.section_name != NULL) + tio_printf(" Active configuration file: %s", config.path); + if (config.active_group != NULL) { - tio_printf(" Active configuration profile: %s", c.section_name); + tio_printf(" Active configuration profile: %s", config.active_group); } } } + +void config_list_targets(void) +{ + memset(&config, 0, sizeof(struct config_t)); + + // Find config file + if (config_file_resolve() != 0) + { + // None found + return; + } + + GKeyFile *keyfile; + GError *error = NULL; + + keyfile = g_key_file_new(); + + GString *config_buffer = g_string_new(NULL); + + config_file_load(config.path, config_buffer, false); + + if (g_key_file_load_from_data(keyfile, config_buffer->str, config_buffer->len, G_KEY_FILE_NONE, &error) == false) + { + g_error_free(error); + goto cleanup; + } + + // Get all group names + gsize num_groups; + gchar **group = g_key_file_get_groups(keyfile, &num_groups); + + if (num_groups == 0) + { + goto cleanup; + } + + printf("\nConfiguration profiles (%s)\n", config.path); + printf("--------------------------------------------------------------------------------\n"); + + int j = 1; + for (gsize i = 0; i < num_groups; i++) + { + // Skip default group + if (strcmp(group[i], CONFIG_GROUP_NAME_DEFAULT) == 0) + { + continue; + } + + // Skip group with include directive + if (strncmp(group[i], CONFIG_GROUP_INCLUDE_PREFIX, strlen(CONFIG_GROUP_INCLUDE_PREFIX)) == 0) + { + continue; + } + + printf("%-19s ", group[i]); + if (j++ % 4 == 0) + { + putchar('\n'); + } + } + if ((j-1) % 4 != 0) + { + putchar('\n'); + } + + g_strfreev(group); +cleanup: + g_key_file_free(keyfile); + g_string_free(config_buffer, TRUE); +} diff --git a/src/configfile.h b/src/configfile.h index 554df3c..2cb9f15 100644 --- a/src/configfile.h +++ b/src/configfile.h @@ -22,7 +22,17 @@ #pragma once +struct config_t +{ + char *path; + char *active_group; + char *device; +}; + +extern struct config_t config; + void config_file_print(void); void config_file_parse(void); void config_exit(void); void config_file_show_profiles(void); +void config_list_targets(void); diff --git a/src/error.c b/src/error.c index 9cff900..9f6bac2 100644 --- a/src/error.c +++ b/src/error.c @@ -19,22 +19,18 @@ * 02110-1301, USA. */ -#define __STDC_WANT_LIB_EXT2__ 1 // To access vasprintf - -#include "config.h" +#define _GNU_SOURCE // To access vasprintf #include -#include -#include -#include -#include -#include -#include "options.h" #include "print.h" -#include "error.h" -#include "timestamp.h" static char error[2][1000]; static bool in_session = false; +bool error_normal = true; + +void switch_error_output_mode(void) +{ + error_normal = false; +} void error_enter_session_mode(void) { diff --git a/src/error.h b/src/error.h index ed81ba3..9a257fc 100644 --- a/src/error.h +++ b/src/error.h @@ -21,6 +21,10 @@ #pragma once +#include + +extern bool error_normal; + #define TIO_SUCCESS 0 #define TIO_ERROR 1 @@ -28,3 +32,4 @@ void tio_error_printf(const char *format, ...); void tio_error_printf_silent(const char *format, ...); void error_exit(void); void error_enter_session_mode(void); +void switch_error_output_mode(void); diff --git a/src/fs.c b/src/fs.c index fa989d4..740d9a0 100644 --- a/src/fs.c +++ b/src/fs.c @@ -32,7 +32,7 @@ #include #include #include -#include +#include #include #include "error.h" #include "print.h" @@ -150,14 +150,7 @@ char* fs_search_directory(const char *dir_path, const char *dirname) // If it's a directory, check if it's the one we're looking for if (strcmp(entry->d_name, dirname) == 0) { - char* result = malloc(strlen(path) + 1); - if (result == NULL) - { - // Error allocating memory - closedir(dir); - return NULL; - } - strcpy(result, path); + char *result = strndup(path, PATH_MAX); closedir(dir); return result; } @@ -178,22 +171,20 @@ char* fs_search_directory(const char *dir_path, const char *dirname) return NULL; } +#if defined(__linux__) && defined(STATX_BTIME) + // Function to return creation time of file double fs_get_creation_time(const char *path) { struct statx stx; - int fd = open(path, O_RDONLY); if (fd == -1) { - // Error return -1; } - int ret = statx(fd, "", AT_EMPTY_PATH, STATX_ALL, &stx); - if (ret == -1) + if (statx(fd, "", AT_EMPTY_PATH, STATX_ALL, &stx) != 0) { - // Error close(fd); return -1; } @@ -203,3 +194,33 @@ double fs_get_creation_time(const char *path) return stx.stx_btime.tv_sec + stx.stx_btime.tv_nsec / 1e9; } + +#elif defined(__APPLE__) || defined(__MACH__) + +double fs_get_creation_time(const char *path) +{ + struct stat st; + + if (stat(path, &st) != 0) + { + return -1; + } + + return st.st_mtimespec.tv_sec + st.st_mtimespec.tv_nsec / 1e9; +} + +#else + +double fs_get_creation_time(const char *path) +{ + struct stat st; + + if (stat(path, &st) != 0) + { + return -1; + } + + return (double) st.st_ctime; +} + +#endif diff --git a/src/log.c b/src/log.c index 0a2df34..4131617 100644 --- a/src/log.c +++ b/src/log.c @@ -19,20 +19,11 @@ * 02110-1301, USA. */ -#define __STDC_WANT_LIB_EXT2__ 1 // To access vasprintf - -#include "config.h" -#include -#include -#include -#include -#include -#include +#define _GNU_SOURCE // To access vasprintf #include #include -#include "options.h" +#include #include "print.h" -#include "error.h" #include "fs.h" #define IS_ESC_CSI_INTERMEDIATE_CHAR(c) ((c >= 0x20) && (c <= 0x3F)) @@ -75,7 +66,7 @@ int log_open(const char *filename) // If using 'new' or 'latest' autoconnect strategy we simply use strategy // name to name autogenerated log name as device names may vary asprintf(&automatic_filename, "tio_%s_%s.log", - auto_connect_state_to_string(option.auto_connect), + option_auto_connect_state_to_string(option.auto_connect), date_time()); } @@ -231,6 +222,7 @@ void log_close(void) if (fp != NULL) { fclose(fp); + tio_printf("Saved log to file %s", log_filename); fp = NULL; log_filename = NULL; } @@ -240,7 +232,6 @@ void log_exit(void) { if ((option.log) && (log_filename != NULL)) { - tio_printf("Saved log to file %s", log_filename); log_close(); } } diff --git a/src/main.c b/src/main.c index 269995e..6676bb3 100644 --- a/src/main.c +++ b/src/main.c @@ -19,10 +19,11 @@ * 02110-1301, USA. */ -#include "config.h" #include #include #include +#include "version.h" +#include "config.h" #include "options.h" #include "configfile.h" #include "tty.h" @@ -60,6 +61,10 @@ int main(int argc, char *argv[]) /* Configure tty device */ tty_configure(); + /* Disable line buffering in stdout. This is necessary if we + * want things like local echo to work correctly. */ + setvbuf(stdout, NULL, _IONBF, 0); + /* Configure input terminal */ if (isatty(fileno(stdin))) { @@ -71,6 +76,9 @@ int main(int argc, char *argv[]) interactive_mode = false; } + /* Switch error output format */ + switch_error_output_mode(); + /* Configure output terminal */ if (isatty(fileno(stdout))) { @@ -98,7 +106,7 @@ int main(int argc, char *argv[]) error_enter_session_mode(); /* Print launch hints */ - tio_printf("tio v%s", VERSION); + tio_printf("tio %s", VERSION); if (interactive_mode) { tio_printf("Press ctrl-%c q to quit", option.prefix_key); diff --git a/src/meson.build b/src/meson.build index 297777e..05168f7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,5 +1,10 @@ +# Generate version header +version_h = vcs_tag(command : ['git', 'describe', '--tags', '--always', '--dirty'], + input : 'version.h.in', + output :'version.h', + replace_string:'@VERSION@') + config_h = configuration_data() -config_h.set_quoted('VERSION', meson.project_version()) config_h.set('BAUDRATE_CASES', baudrate_cases) configure_file(output: 'config.h', configuration: config_h) @@ -20,7 +25,9 @@ tio_sources = [ 'alert.c', 'xymodem.c', 'script.c', - 'fs.c' + 'fs.c', + 'readline.c', + version_h ] @@ -37,13 +44,16 @@ endif tio_dep = [ dependency('threads', required: true), dependency('glib-2.0', required: true), - dependency('inih', required: true, - fallback : ['libinih', 'inih_dep'], - default_options: ['default_library=static', 'distro_install=false']), lua_dep ] -tio_c_args = ['-Wno-unused-result'] +if host_machine.system() == 'darwin' + iokit_dep = dependency('appleframeworks', modules: ['IOKit'], required: true) + corefoundation_dep = dependency('appleframeworks', modules: ['CoreFoundation'], required: true) + tio_dep += [iokit_dep, corefoundation_dep] +endif + +tio_c_args = ['-Wshadow','-Wno-unused-result'] if enable_setspeed2 tio_c_args += '-DHAVE_TERMIOS2' diff --git a/src/misc.c b/src/misc.c index 1d1acea..bd0a429 100644 --- a/src/misc.c +++ b/src/misc.c @@ -19,24 +19,15 @@ * 02110-1301, USA. */ -#include "config.h" -#include -#include -#include -#include -#include -#include +#define _GNU_SOURCE // For FNM_EXTMATCH #include #include -#include -#include -#include -#include -#include +#include +#include #include -#include "error.h" +#include +#include #include "print.h" -#include "options.h" void delay(long ms) { @@ -53,22 +44,6 @@ void delay(long ms) nanosleep(&ts, NULL); } -long string_to_long(char *string) -{ - long result; - char *end_token; - - errno = 0; - result = strtol(string, &end_token, 10); - if ((errno != 0) || (*end_token != 0)) - { - printf("Error: Invalid digit\n"); - exit(EXIT_FAILURE); - } - - return result; -} - int ctrl_key_code(unsigned char key) { if ((key >= 'a') && (key <= 'z')) @@ -146,10 +121,9 @@ unsigned long djb2_hash(const unsigned char *str) } // Function to encode a number to base62 -char *base62_encode(unsigned long num) +void *base62_encode(unsigned long num, char *output) { const char base62_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - char *output = (char *) malloc(5); // 4 characters + null terminator if (output == NULL) { tio_error_print("Memory allocation failed"); @@ -197,7 +171,11 @@ bool match_patterns(const char *string, const char *patterns) while (pattern != NULL) { // Check if the string matches the current pattern - if (fnmatch(pattern, string, 0) == 0) + #ifdef FNM_EXTMATCH + if (fnmatch(pattern, string, FNM_EXTMATCH) == 0) + #else + if (fnmatch(pattern, string, 0) == 0) + #endif { free(patterns_copy); return true; @@ -210,3 +188,66 @@ bool match_patterns(const char *string, const char *patterns) free(patterns_copy); return false; } + +// Function that forks subprocess, redirects its stdin and stdout to the +// specified filedescriptor, and runs command. +int execute_shell_command(int fd, const char *command) +{ + pid_t pid; + int status; + + // Fork a child process + pid = fork(); + if (pid == -1) + { + // Error occurred + tio_error_print("fork() failed (%s)", strerror(errno)); + exit(EXIT_FAILURE); + } + else if (pid == 0) + { + // Child process + + 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) + { + 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 + perror("execlp"); + tio_error_print("execlp() failed (%s)", strerror(errno)); + exit(EXIT_FAILURE); + } + else + { + // Parent process + + // Wait for the child process to finish + waitpid(pid, &status, 0); + + if (WIFEXITED(status)) + { + tio_printf("Command exited with status %d", WEXITSTATUS(status)); + return WEXITSTATUS(status); + } + else + { + tio_error_printf("Child process exited abnormally\n"); + return -1; + } + } + + return 0; +} + +void clear_line() +{ + print("\r\033[K"); +} diff --git a/src/misc.h b/src/misc.h index 25be622..08ae5eb 100644 --- a/src/misc.h +++ b/src/misc.h @@ -27,13 +27,12 @@ #define UNUSED(expr) do { (void)(expr); } while (0) void delay(long ms); -long string_to_long(char *string); int ctrl_key_code(unsigned char key); -void alert_connect(void); -void alert_disconnect(void); bool regex_match(const char *string, const char *pattern); unsigned long djb2_hash(const unsigned char *str); -char *base62_encode(unsigned long num); +void *base62_encode(unsigned long num, char *output); int read_poll(int fd, 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(); diff --git a/src/options.c b/src/options.c index 65082b6..acff7ce 100644 --- a/src/options.c +++ b/src/options.c @@ -19,28 +19,19 @@ * 02110-1301, USA. */ -#include "config.h" +#define _GNU_SOURCE // To access vasprintf + +#include #include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include "options.h" -#include "error.h" +#include +#include "version.h" +#include "config.h" #include "misc.h" #include "print.h" -#include "tty.h" #include "rs485.h" -#include "timestamp.h" -#include "alert.h" #include "log.h" -#include "script.h" +#include "configfile.h" #define HEX_N_VALUE_MAX 4096 @@ -67,6 +58,7 @@ enum opt_t OPT_EXCLUDE_DEVICES, OPT_EXCLUDE_DRIVERS, OPT_EXCLUDE_TIDS, + OPT_EXEC, }; /* Default options */ @@ -75,9 +67,9 @@ struct option_t option = .target = "", .baudrate = 115200, .databits = 8, - .flow = "none", + .flow = FLOW_NONE, .stopbits = 1, - .parity = "none", + .parity = PARITY_NONE, .output_delay = 0, .output_line_delay = 0, .dtr_pulse_duration = 100, @@ -96,7 +88,6 @@ struct option_t option = .local_echo = false, .timestamp = TIMESTAMP_NONE, .socket = NULL, - .map = "", .color = 256, // Bold .input_mode = INPUT_MODE_NORMAL, .output_mode = OUTPUT_MODE_NORMAL, @@ -119,9 +110,23 @@ struct option_t option = .exclude_tids = NULL, .hex_n_value = 0, .vt100 = false, + .exec = NULL, + .map_i_nl_cr = false, + .map_i_cr_nl = false, + .map_ign_cr = false, + .map_i_ff_escc = false, + .map_i_nl_crnl = false, + .map_i_cr_crnl = false, + .map_o_cr_nl = false, + .map_o_nl_crnl = false, + .map_o_del_bs = false, + .map_o_ltu = false, + .map_o_nulbrk = false, + .map_i_msb2lsb = false, + .map_o_ign_cr = false, }; -void print_help(char *argv[]) +void option_print_help(char *argv[]) { UNUSED(argv); @@ -149,7 +154,7 @@ void print_help(char *argv[]) printf(" -t, --timestamp Enable line timestamp\n"); printf(" --timestamp-format Set timestamp format (default: 24hour)\n"); printf(" --timestamp-timeout Set timestamp timeout (default: 200)\n"); - printf(" -l, --list List available serial devices\n"); + printf(" -l, --list List available serial devices, TIDs, and profiles\n"); printf(" -L, --log Enable log to file\n"); printf(" --log-file Set log filename\n"); printf(" --log-directory Set log directory path for automatic named logs\n"); @@ -165,15 +170,298 @@ void print_help(char *argv[]) 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"); + printf(" --exec Execute shell command with I/O redirected to device\n"); + printf(" --complete-profiles Prints profiles (for shell completion)\n"); printf(" -v, --version Display version\n"); printf(" -h, --help Display help\n"); printf("\n"); printf("Options and profiles may be set via configuration file.\n"); printf("\n"); + printf("In session you can press ctrl-%c ? to list available key commands.\n", option.prefix_key); + printf("\n"); printf("See the man page for more details.\n"); } -const char *auto_connect_state_to_string(auto_connect_t strategy) +int option_string_to_integer(const char *string, int *value, const char *desc, int min, int max) +{ + int val; + char *end_token; + + errno = 0; + val = strtol(string, &end_token, 10); + if ((errno != 0) || (*end_token != 0)) + { + if (desc != NULL) + { + tio_error_print("Invalid %s '%s'", desc, string); + exit(EXIT_FAILURE); + } + else + { + tio_error_print("Invalid digit '%s'", string); + exit(EXIT_FAILURE); + } + return -1; + } + else + { + if ((val < min) || (val > max)) + { + if (desc != NULL) + { + tio_error_print("Invalid %s '%s'", desc, string); + exit(EXIT_FAILURE); + } + else + { + tio_error_print("Invalid digit '%s'", string); + exit(EXIT_FAILURE); + } + return -1; + } + else + { + *value = val; + } + } + + return 0; +} + +void option_parse_flow(const char *arg, flow_t *flow) +{ + assert(arg != NULL); + + /* Parse flow control */ + if (strcmp("hard", arg) == 0) + { + *flow = FLOW_HARD; + } + else if (strcmp("soft", arg) == 0) + { + *flow = FLOW_SOFT; + } + else if (strcmp("none", arg) == 0) + { + *flow = FLOW_NONE; + } + else + { + tio_error_print("Invalid flow control '%s'", arg); + exit(EXIT_FAILURE); + } +} + +const char *option_flow_to_string(flow_t flow) +{ + switch (flow) + { + case FLOW_NONE: + return "none"; + case FLOW_HARD: + return "hard"; + case FLOW_SOFT: + return "soft"; + default: + return "unknown"; + } +} + +void option_parse_parity(const char *arg, parity_t *parity) +{ + assert(arg != NULL); + + if (strcmp("none", arg) == 0) + { + *parity = PARITY_NONE; + } + else if (strcmp("odd", arg) == 0) + { + *parity = PARITY_ODD; + } + else if (strcmp("even", arg) == 0) + { + *parity = PARITY_EVEN; + } + else if (strcmp("mark", arg) == 0) + { + *parity = PARITY_MARK; + } + else if (strcmp("space", arg) == 0) + { + *parity = PARITY_SPACE; + } + else + { + tio_error_print("Invalid parity '%s'", arg); + exit(EXIT_FAILURE); + } +} + +const char *option_parity_to_string(parity_t parity) +{ + switch (parity) + { + case PARITY_NONE: + return "none"; + case PARITY_ODD: + return "odd"; + case PARITY_EVEN: + return "even"; + case PARITY_MARK: + return "mark"; + case PARITY_SPACE: + return "space"; + default: + return "unknown"; + } +} + +void option_parse_color(const char *arg, int *color) +{ + int value; + + assert(arg != NULL); + + if (strcmp(optarg, "list") == 0) + { + // Print available color codes + printf("Available color codes:\n"); + for (int i=0; i<=255; i++) + { + printf(" \e[1;38;5;%dmThis is color code %d\e[0m\n", i, i); + } + exit(EXIT_SUCCESS); + } + else if (strcmp(arg, "none") == 0) + { + *color = -1; // No color + } + else if (strcmp(arg, "bold") == 0) + { + *color = 256; // Bold + } + else + { + if (option_string_to_integer(arg, &value, "color code", 0, 255) == 0) + { + *color = value; + } + } +} + +void option_parse_alert(const char *arg, alert_t *alert) +{ + assert(arg != NULL); + + if (strcmp(arg, "none") == 0) + { + *alert = ALERT_NONE; + } + else if (strcmp(arg, "bell") == 0) + { + *alert = ALERT_BELL; + } + else if (strcmp(arg, "blink") == 0) + { + *alert = ALERT_BLINK; + } + else + { + tio_error_print("Invalid alert '%s'", arg); + exit(EXIT_FAILURE); + } +} + +const char* option_timestamp_format_to_string(timestamp_t timestamp) +{ + switch (timestamp) + { + case TIMESTAMP_NONE: + return "none"; + break; + + case TIMESTAMP_24HOUR: + return "24hour"; + break; + + case TIMESTAMP_24HOUR_START: + return "24hour-start"; + break; + + case TIMESTAMP_24HOUR_DELTA: + return "24hour-delta"; + break; + + case TIMESTAMP_ISO8601: + return "iso8601"; + break; + + case TIMESTAMP_EPOCH: + return "epoch"; + break; + + case TIMESTAMP_EPOCH_USEC: + return "epoch-usec"; + break; + + default: + return "unknown"; + break; + } +} + +void option_parse_timestamp(const char *arg, timestamp_t *timestamp) +{ + assert(arg != NULL); + + if (strcmp(arg, "24hour") == 0) + { + *timestamp = TIMESTAMP_24HOUR; + } + else if (strcmp(arg, "24hour-start") == 0) + { + *timestamp = TIMESTAMP_24HOUR_START; + } + else if (strcmp(arg, "24hour-delta") == 0) + { + *timestamp = TIMESTAMP_24HOUR_DELTA; + } + else if (strcmp(arg, "iso8601") == 0) + { + *timestamp = TIMESTAMP_ISO8601; + } + else if (strcmp(arg, "epoch") == 0) + { + *timestamp = TIMESTAMP_EPOCH; + } + else if (strcmp(arg, "epoch-usec") == 0) + { + *timestamp = TIMESTAMP_EPOCH_USEC; + } + else + { + tio_error_print("Invalid timestamp '%s'", arg); + exit(EXIT_FAILURE); + } +} + +const char *option_alert_state_to_string(alert_t state) +{ + switch (state) + { + case ALERT_NONE: + return "none"; + case ALERT_BELL: + return "bell"; + case ALERT_BLINK: + return "blink"; + default: + return "unknown"; + } +} + +const char *option_auto_connect_state_to_string(auto_connect_t strategy) { switch (strategy) { @@ -184,39 +472,44 @@ const char *auto_connect_state_to_string(auto_connect_t strategy) case AUTO_CONNECT_LATEST: return "latest"; default: - return "Unknown"; + return "unknown"; } } -auto_connect_t auto_connect_option_parse(const char *arg) +void option_parse_auto_connect(const char *arg, auto_connect_t *auto_connect) { - auto_connect_t auto_connect = option.auto_connect; // Default + assert(arg != NULL); if (arg != NULL) { if (strcmp(arg, "direct") == 0) { - return AUTO_CONNECT_DIRECT; + *auto_connect = AUTO_CONNECT_DIRECT; } else if (strcmp(arg, "new") == 0) { - return AUTO_CONNECT_NEW; + *auto_connect = AUTO_CONNECT_NEW; } else if (strcmp(arg, "latest") == 0) { - return AUTO_CONNECT_LATEST; + *auto_connect = AUTO_CONNECT_LATEST; + } + else + { + tio_error_print("Invalid auto-connect strategy '%s'", arg); + exit(EXIT_FAILURE); } } - - return auto_connect; } -void line_pulse_duration_option_parse(const char *arg) +void option_parse_line_pulse_duration(const char *arg) { bool token_found = true; char *token = NULL; char *buffer = strdup(arg); + assert(arg != NULL); + while (token_found == true) { if (token == NULL) @@ -259,6 +552,11 @@ void line_pulse_duration_option_parse(const char *arg) { option.ri_pulse_duration = value; } + else + { + tio_error_print("Invalid line '%s'", keyname); + exit(EXIT_FAILURE); + } } else { @@ -274,7 +572,7 @@ void line_pulse_duration_option_parse(const char *arg) } // Function to parse the input string -int parse_hexN_string(const char *input_string) +int option_parse_hexN_string(const char *input_string) { regmatch_t match[2]; // One for entire match, one for the optional N int n_value = 0; @@ -285,7 +583,7 @@ int parse_hexN_string(const char *input_string) ret = regcomp(®ex, "^hex([0-9]+)?$", REG_EXTENDED); if (ret) { - tio_error_printf("Could not compile regex\n"); + tio_error_print("Could not compile regex"); exit(EXIT_FAILURE); } @@ -319,7 +617,7 @@ int parse_hexN_string(const char *input_string) { char msgbuf[100]; regerror(ret, ®ex, msgbuf, sizeof(msgbuf)); - tio_error_printf("Regex match failed: %s\n", msgbuf); + tio_error_print("Regex match failed: %s", msgbuf); exit(EXIT_FAILURE); } @@ -328,48 +626,52 @@ int parse_hexN_string(const char *input_string) return n_value; } -input_mode_t input_mode_option_parse(const char *arg) +void option_parse_input_mode(const char *arg, input_mode_t *mode) { + assert(arg != NULL); + if (strcmp("normal", arg) == 0) { - return INPUT_MODE_NORMAL; + *mode = INPUT_MODE_NORMAL; } else if (strcmp("hex", arg) == 0) { - return INPUT_MODE_HEX; + *mode = INPUT_MODE_HEX; } else if (strcmp("line", arg) == 0) { - return INPUT_MODE_LINE; + *mode = INPUT_MODE_LINE; } else { - tio_error_printf("Invalid input mode option"); + tio_error_print("Invalid input mode '%s'", arg); exit(EXIT_FAILURE); } } -output_mode_t output_mode_option_parse(const char *arg) +void option_parse_output_mode(const char *arg, output_mode_t *mode) { int n = 0; + assert(arg != NULL); + if (strcmp("normal", arg) == 0) { - return OUTPUT_MODE_NORMAL; + *mode = OUTPUT_MODE_NORMAL; } - else if ((n = parse_hexN_string(arg)) != -1) + else if ((n = option_parse_hexN_string(arg)) != -1) { option.hex_n_value = n; - return OUTPUT_MODE_HEX; + *mode = OUTPUT_MODE_HEX; } else { - tio_error_printf("Invalid output mode option"); + tio_error_print("Invalid output mode '%s'", arg); exit(EXIT_FAILURE); } } -const char *input_mode_by_string(input_mode_t mode) +const char *option_input_mode_to_string(input_mode_t mode) { switch (mode) { @@ -386,7 +688,7 @@ const char *input_mode_by_string(input_mode_t mode) return NULL; } -const char *output_mode_by_string(output_mode_t mode) +const char *option_output_mode_to_string(output_mode_t mode) { switch (mode) { @@ -401,55 +703,145 @@ const char *output_mode_by_string(output_mode_t mode) return NULL; } -script_run_t script_run_option_parse(const char *arg) +void option_parse_script_run(const char *arg, script_run_t *script_run) { + assert(arg != NULL); + if (strcmp("once", arg) == 0) { - return SCRIPT_RUN_ONCE; + *script_run = SCRIPT_RUN_ONCE; } else if (strcmp("always", arg) == 0) { - return SCRIPT_RUN_ALWAYS; + *script_run = SCRIPT_RUN_ALWAYS; } else if (strcmp("never", arg) == 0) { - return SCRIPT_RUN_NEVER; + *script_run = SCRIPT_RUN_NEVER; } else { - tio_error_printf("Invalid script run option"); + tio_error_print("Invalid script run option '%s'", arg); exit(EXIT_FAILURE); } } +void option_parse_mappings(const char *map) +{ + bool token_found = true; + char *token = NULL; + char *buffer; + + if (map == NULL) + { + return; + } + + /* Parse any specified input or output mappings */ + buffer = strdup(map); + while (token_found == true) + { + if (token == NULL) + { + token = strtok(buffer,","); + } + else + { + token = strtok(NULL, ","); + } + + if (token != NULL) + { + if (strcmp(token,"INLCR") == 0) + { + option.map_i_nl_cr = true; + } + else if (strcmp(token,"IGNCR") == 0) + { + option.map_ign_cr = true; + } + else if (strcmp(token,"ICRNL") == 0) + { + option.map_i_cr_nl = true; + } + else if (strcmp(token,"OCRNL") == 0) + { + option.map_o_cr_nl = true; + } + else if (strcmp(token,"ODELBS") == 0) + { + option.map_o_del_bs = true; + } + else if (strcmp(token,"IFFESCC") == 0) + { + option.map_i_ff_escc = true; + } + else if (strcmp(token,"INLCRNL") == 0) + { + option.map_i_nl_crnl = true; + } + else if (strcmp(token,"ICRCRNL") == 0) + { + option.map_i_cr_crnl = true; + } + else if (strcmp(token, "ONLCRNL") == 0) + { + option.map_o_nl_crnl = true; + } + else if (strcmp(token, "OLTU") == 0) + { + option.map_o_ltu = true; + } + else if (strcmp(token, "ONULBRK") == 0) + { + option.map_o_nulbrk = true; + } + else if (strcmp(token, "OIGNCR") == 0) + { + option.map_o_ign_cr = true; + } + else if (strcmp(token, "IMSB2LSB") == 0) + { + option.map_i_msb2lsb = true; + } + else + { + printf("Error: Unknown mapping flag %s\n", token); + exit(EXIT_FAILURE); + } + } + else + { + token_found = false; + } + } + free(buffer); +} + void options_print() { tio_printf(" Device: %s", device_name); tio_printf(" Baudrate: %u", option.baudrate); tio_printf(" Databits: %d", option.databits); - tio_printf(" Flow: %s", option.flow); + tio_printf(" Flow: %s", option_flow_to_string(option.flow)); tio_printf(" Stopbits: %d", option.stopbits); - tio_printf(" Parity: %s", option.parity); - tio_printf(" Local echo: %s", option.local_echo ? "enabled" : "disabled"); - tio_printf(" Timestamp: %s", timestamp_state_to_string(option.timestamp)); + tio_printf(" Parity: %s", option_parity_to_string(option.parity)); + tio_printf(" Local echo: %s", option.local_echo ? "true" : "false"); + 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(" Automatic connect strategy: %s", auto_connect_state_to_string(option.auto_connect)); - tio_printf(" Automatic reconnect: %s", option.no_reconnect ? "disabled" : "enabled"); + 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(" 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); - tio_printf(" Input mode: %s", input_mode_by_string(option.input_mode)); - tio_printf(" Output mode: %s", output_mode_by_string(option.output_mode)); - tio_printf(" Alert: %s", alert_state_to_string(option.alert)); - if (option.map[0] != 0) - { - tio_printf(" Map flags: %s", option.map); - } + 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(" Alert: %s", option_alert_state_to_string(option.alert)); if (option.log) { tio_printf(" Log file: %s", log_get_filename()); @@ -457,8 +849,8 @@ void options_print() { tio_printf(" Log file directory: %s", option.log_directory); } - tio_printf(" Log append: %s", option.log_append ? "enabled" : "disabled"); - tio_printf(" Log strip: %s", option.log_strip ? "enabled" : "disabled"); + tio_printf(" Log append: %s", option.log_append ? "true" : "false"); + tio_printf(" Log strip: %s", option.log_strip ? "true" : "false"); } if (option.socket) { @@ -477,7 +869,7 @@ void options_parse(int argc, char *argv[]) if (argc == 1) { - print_help(argv); + option_print_help(argv); exit(EXIT_SUCCESS); } @@ -534,6 +926,7 @@ void options_parse(int argc, char *argv[]) {"script", required_argument, 0, OPT_SCRIPT }, {"script-file", required_argument, 0, OPT_SCRIPT_FILE }, {"script-run", required_argument, 0, OPT_SCRIPT_RUN }, + {"exec", required_argument, 0, OPT_EXEC }, {"version", no_argument, 0, 'v' }, {"help", no_argument, 0, 'h' }, {"complete-profiles", no_argument, 0, OPT_COMPLETE_PROFILES }, @@ -563,39 +956,39 @@ void options_parse(int argc, char *argv[]) break; case 'b': - option.baudrate = string_to_long(optarg); + option_string_to_integer(optarg, &option.baudrate, "baudrate", 0, INT_MAX); break; case 'd': - option.databits = string_to_long(optarg); + option_string_to_integer(optarg, &option.databits, "databits", 5, 8); break; case 'f': - option.flow = optarg; + option_parse_flow(optarg, &option.flow); break; case 's': - option.stopbits = string_to_long(optarg); + option_string_to_integer(optarg, &option.stopbits, "stopbits", 1, 2); break; case 'p': - option.parity = optarg; + option_parse_parity(optarg, &option.parity); break; case 'o': - option.output_delay = string_to_long(optarg); + option_string_to_integer(optarg, &option.output_delay, "output delay", 0, INT_MAX); break; case 'O': - option.output_line_delay = string_to_long(optarg); + option_string_to_integer(optarg, &option.output_line_delay, "output line delay", 0, INT_MAX); break; case OPT_LINE_PULSE_DURATION: - line_pulse_duration_option_parse(optarg); + option_parse_line_pulse_duration(optarg); break; case 'a': - option.auto_connect = auto_connect_option_parse(optarg); + option_parse_auto_connect(optarg, &option.auto_connect); break; case OPT_EXCLUDE_DEVICES: @@ -619,15 +1012,18 @@ void options_parse(int argc, char *argv[]) break; case 't': - option.timestamp = TIMESTAMP_24HOUR; + if (option.timestamp == TIMESTAMP_NONE) + { + option.timestamp = TIMESTAMP_24HOUR; + } break; case OPT_TIMESTAMP_FORMAT: - option.timestamp = timestamp_option_parse(optarg); + option_parse_timestamp(optarg, &option.timestamp); break; case OPT_TIMESTAMP_TIMEOUT: - option.timestamp_timeout = string_to_long(optarg); + option_string_to_integer(optarg, &option.timestamp_timeout, "timestamp timeout", 0, INT_MAX); break; case 'L': @@ -636,6 +1032,7 @@ void options_parse(int argc, char *argv[]) case 'l': list_serial_devices(); + config_list_targets(); exit(EXIT_SUCCESS); break; @@ -660,45 +1057,19 @@ void options_parse(int argc, char *argv[]) break; case 'm': - option.map = optarg; + option_parse_mappings(optarg); break; case 'c': - if (!strcmp(optarg, "list")) - { - // Print available color codes - printf("Available color codes:\n"); - for (int i=0; i<=255; i++) - { - printf(" \e[1;38;5;%dmThis is color code %d\e[0m\n", i, i); - } - exit(EXIT_SUCCESS); - } - else if (!strcmp(optarg, "none")) - { - option.color = -1; // No color - break; - } - else if (!strcmp(optarg, "bold")) - { - option.color = 256; // Bold - break; - } - - option.color = string_to_long(optarg); - if ((option.color < 0) || (option.color > 255)) - { - tio_error_printf("Invalid color code"); - exit(EXIT_FAILURE); - } + option_parse_color(optarg, &option.color); break; case OPT_INPUT_MODE: - option.input_mode = input_mode_option_parse(optarg); + option_parse_input_mode(optarg, &option.input_mode); break; case OPT_OUTPUT_MODE: - option.output_mode = output_mode_option_parse(optarg); + option_parse_output_mode(optarg, &option.output_mode); break; case OPT_RS485: @@ -710,7 +1081,7 @@ void options_parse(int argc, char *argv[]) break; case OPT_ALERT: - option.alert = alert_option_parse(optarg); + option_parse_alert(optarg, &option.alert); break; case OPT_MUTE: @@ -726,16 +1097,20 @@ void options_parse(int argc, char *argv[]) break; case OPT_SCRIPT_RUN: - option.script_run = script_run_option_parse(optarg); + option_parse_script_run(optarg, &option.script_run); + break; + + case OPT_EXEC: + option.exec = optarg; break; case 'v': - printf("tio v%s\n", VERSION); + printf("tio %s\n", VERSION); exit(EXIT_SUCCESS); break; case 'h': - print_help(argv); + option_print_help(argv); exit(EXIT_SUCCESS); break; @@ -775,7 +1150,7 @@ void options_parse(int argc, char *argv[]) if (strlen(option.target) == 0) { - tio_error_printf("Missing tty device, profile or topology ID"); + tio_error_print("Missing tty device, profile or topology ID"); exit(EXIT_FAILURE); } @@ -794,9 +1169,6 @@ void options_parse(int argc, char *argv[]) void options_parse_final(int argc, char *argv[]) { - /* Preserve target which may have been set by configuration file */ - const char *target = option.target; - /* Do 2nd pass to override settings set by configuration file */ optind = 1; // Reset option index to restart scanning of argv options_parse(argc, argv); @@ -804,16 +1176,13 @@ void options_parse_final(int argc, char *argv[]) #ifdef __CYGWIN__ unsigned char portnum; char *tty_win; - if ( ((strncmp("COM", target, 3) == 0) - || (strncmp("com", target, 3) == 0) ) - && (sscanf(target + 3, "%hhu", &portnum) == 1) - && (portnum > 0) ) + if ( ((strncmp("COM", option.target, 3) == 0) + || (strncmp("com", option.target, 3) == 0) ) + && (sscanf(option.target + 3, "%hhu", &portnum) == 1) + && (portnum > 0) ) { asprintf(&tty_win, "/dev/ttyS%hhu", portnum - 1); - target = tty_win; + option.target = tty_win; } #endif - - /* Restore target */ - option.target = target; } diff --git a/src/options.h b/src/options.h index 016978d..c552217 100644 --- a/src/options.h +++ b/src/options.h @@ -23,9 +23,6 @@ #include #include -#include -#include -#include #include "script.h" #include "timestamp.h" #include "alert.h" @@ -49,20 +46,20 @@ typedef enum /* Options */ struct option_t { - const char *target; - unsigned int baudrate; + char *target; + int baudrate; int databits; - char *flow; + flow_t flow; int stopbits; - char *parity; + parity_t parity; int output_delay; int output_line_delay; - unsigned int dtr_pulse_duration; - unsigned int rts_pulse_duration; - unsigned int cts_pulse_duration; - unsigned int dsr_pulse_duration; - unsigned int dcd_pulse_duration; - unsigned int ri_pulse_duration; + int dtr_pulse_duration; + int rts_pulse_duration; + int cts_pulse_duration; + int dsr_pulse_duration; + int dcd_pulse_duration; + int ri_pulse_duration; bool no_reconnect; auto_connect_t auto_connect; bool log; @@ -70,15 +67,14 @@ struct option_t bool log_strip; bool local_echo; timestamp_t timestamp; - const char *log_filename; - const char *log_directory; - const char *map; - const char *socket; + char *log_filename; + char *log_directory; + char *socket; int color; input_mode_t input_mode; output_mode_t output_mode; - unsigned char prefix_code; - unsigned char prefix_key; + char prefix_code; + char prefix_key; bool prefix_enabled; bool mute; bool rs485; @@ -87,15 +83,29 @@ struct option_t int32_t rs485_delay_rts_after_send; alert_t alert; bool complete_profiles; - const char *script; - const char *script_filename; + char *script; + char *script_filename; script_run_t script_run; - unsigned int timestamp_timeout; - const char *exclude_devices; - const char *exclude_drivers; - const char *exclude_tids; + int timestamp_timeout; + char *exclude_devices; + char *exclude_drivers; + char *exclude_tids; int hex_n_value; bool vt100; + char *exec; + bool map_i_nl_cr; + bool map_i_cr_nl; + bool map_ign_cr; + bool map_i_ff_escc; + bool map_i_nl_crnl; + bool map_i_cr_crnl; + bool map_o_cr_nl; + bool map_o_nl_crnl; + bool map_o_del_bs; + bool map_o_ltu; + bool map_o_nulbrk; + bool map_i_msb2lsb; + bool map_o_ign_cr; }; extern struct option_t option; @@ -104,10 +114,22 @@ void options_print(); void options_parse(int argc, char *argv[]); void options_parse_final(int argc, char *argv[]); -void line_pulse_duration_option_parse(const char *arg); -script_run_t script_run_option_parse(const char *arg); +int option_string_to_integer(const char *string, int *value, const char *desc, int min, int max); -input_mode_t input_mode_option_parse(const char *arg); -output_mode_t output_mode_option_parse(const char *arg); -auto_connect_t auto_connect_option_parse(const char *arg); -const char *auto_connect_state_to_string(auto_connect_t strategy); +void option_parse_flow(const char *arg, flow_t *flow); +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_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); + +void option_parse_auto_connect(const char *arg, auto_connect_t *auto_connect); +const char *option_auto_connect_state_to_string(auto_connect_t strategy); + +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); diff --git a/src/print.c b/src/print.c index 04d17da..06f7e73 100644 --- a/src/print.c +++ b/src/print.c @@ -19,11 +19,6 @@ * 02110-1301, USA. */ -#include -#include -#include -#include -#include "options.h" #include "print.h" bool print_tainted = false; @@ -80,3 +75,38 @@ void print_tainted_set() { print_tainted = true; } + +void print(const char *format, ...) +{ + va_list args; + + va_start(args, format); + vprintf(format, args); + fflush(stdout); + va_end(args); + + print_tainted = true; +} + +void print_padded(char *string, size_t length, char pad_char) +{ + size_t padding = 0; + size_t string_length = 0; + size_t i; + + string_length = strlen(string); + + if (string_length < length) + { + padding += length - string_length; + printf("%s", string); + for (i=0; i #include -#include "misc.h" #include "error.h" #include "options.h" #include "timestamp.h" @@ -87,10 +86,18 @@ extern char ansi_format[]; { \ if (print_tainted) \ putchar('\n'); \ - if (option.color < 0) \ - fprintf (stdout, "\r[%s] Error: " format "\r\n", timestamp_current_time(), ## args); \ - else \ - ansi_printf("[%s] Error: " format, timestamp_current_time(), ## args); \ + if (option.color < 0) { \ + if (error_normal) \ + fprintf (stderr, "Error: " format "\n", ## args); \ + else \ + fprintf (stderr, "\r[%s] Error: " format "\r\n", timestamp_current_time(), ## args); \ + } \ + else { \ + if (error_normal) \ + { ansi_error_printf("Error: " format, ## args); }\ + else \ + { ansi_error_printf("[%s] Error: " format, timestamp_current_time(), ## args); }\ + } \ print_tainted = false; \ } \ } @@ -127,8 +134,10 @@ extern char ansi_format[]; #define tio_debug_printf_raw(format, args...) #endif +void print(const char *format, ...); void print_hex(char c); void print_normal(char c); void print_init_ansi_formatting(void); void tio_printf_array(const char *array); void print_tainted_set(void); +void print_padded(char *string, size_t length, char pad_char); diff --git a/src/readline.c b/src/readline.c new file mode 100644 index 0000000..8176a0b --- /dev/null +++ b/src/readline.c @@ -0,0 +1,276 @@ +/* + * tio - a serial device I/O tool + * + * Copyright (c) 2014-2024 Martin Lund + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include "print.h" +#include "misc.h" + +#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; + +static void print_line(const char *string, int cursor_pos) +{ + clear_line(); + print("%s", string); + print("\r"); // Move the cursor back to the beginning + for (int i = 0; i < cursor_pos; ++i) + { + print("\x1b[C"); // Move the cursor right + } +} + +void readline_init(void) +{ + rl_history_count = 0; + rl_history_index = 0; + + for (int i = 0; i < RL_HISTORY_MAX; ++i) + { + rl_history[i] = NULL; + } + + rl_line[0] = 0; + rl_line_length = 0; + rl_cursor_pos = 0; + rl_escape = 0; +} + +char * readline_get(void) +{ + return rl_line; +} + +static void readline_input_char(char input_char) +{ + 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); + } + rl_escape = 0; +} + +static void readline_input_cr(void) +{ + if (rl_line_length > 0) + { + // 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(); + } + else + { + print("\r\n"); + } + + rl_line_length = 0; + rl_cursor_pos = 0; + rl_history_index = rl_history_count; + rl_escape = 0; +} + +static void readline_input_bs(void) +{ + 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); + } + rl_escape = 0; +} + +static void readline_input_escape(void) +{ + rl_escape = 1; +} + +static void readline_input_left_bracket(void) +{ + if (rl_escape == 1) + { + rl_escape = 2; + } + else + { + rl_escape = 0; + } +} + +static void readline_input_A(void) +{ + if (rl_escape == 2) + { + // Up arrow + 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); + } + } + else + { + readline_input_char('A'); + } + + rl_escape = 0; +} + +static void readline_input_B(void) +{ + if (rl_escape == 2) + { + // Down arrow + 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); + } + 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); + } + } + else + { + readline_input_char('B'); + } + + rl_escape = 0; +} + +static void readline_input_C(void) +{ + if (rl_escape == 2) + { + // Right arrow + if (rl_cursor_pos < rl_line_length) + { + rl_cursor_pos++; + print("\x1b[C"); + } + } + else + { + readline_input_char('C'); + } + + rl_escape = 0; +} + +static void readline_input_D(void) +{ + if (rl_escape == 2) + { + // Left arrow + if (rl_cursor_pos > 0) + { + rl_cursor_pos--; + print("\b"); + } + } + else + { + readline_input_char('D'); + } + + rl_escape = 0; +} + +void readline_input(char input_char) +{ + switch (input_char) + { + case '\r': // Carriage return + readline_input_cr(); + break; + + case 127: // Backspace + readline_input_bs(); + break; + + case 27: // Escape + readline_input_escape(); + break; + + case '[': + readline_input_left_bracket(); + break; + + case 'A': + readline_input_A(); + break; + + case 'B': + readline_input_B(); + break; + + case 'C': + readline_input_C(); + break; + + case 'D': + readline_input_D(); + break; + + default: + readline_input_char(input_char); + break; + } +} diff --git a/src/readline.h b/src/readline.h new file mode 100644 index 0000000..46c3f10 --- /dev/null +++ b/src/readline.h @@ -0,0 +1,26 @@ +/* + * tio - a serial device I/O tool + * + * Copyright (c) 2014-2024 Martin Lund + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#pragma once + +void readline_init(void); +void readline_input(char input_char); +char * readline_get(void); diff --git a/src/rs485.c b/src/rs485.c index c1b06db..3c9280c 100644 --- a/src/rs485.c +++ b/src/rs485.c @@ -19,14 +19,13 @@ * 02110-1301, USA. */ -#include -#include +#include #include +#include #include -#include #include "options.h" #include "print.h" -#include "error.h" +#include "misc.h" #ifdef HAVE_RS485 @@ -128,7 +127,7 @@ void rs485_print_config(void) tio_printf(" RTS_AFTER_SEND: %s", (rs485_config.flags & SER_RS485_RTS_AFTER_SEND) ? "high" : "low"); tio_printf(" RTS_DELAY_BEFORE_SEND = %d", rs485_config.delay_rts_before_send); tio_printf(" RTS_DELAY_AFTER_SEND = %d", rs485_config.delay_rts_after_send); - tio_printf(" RX_DURING_TX: %s", (rs485_config.flags & SER_RS485_RX_DURING_TX) ? "enabled" : "disabled"); + tio_printf(" RX_DURING_TX: %s", (rs485_config.flags & SER_RS485_RX_DURING_TX) ? "true" : "false"); } int rs485_mode_enable(int fd) diff --git a/src/script.c b/src/script.c index f425a1f..b69d55b 100644 --- a/src/script.c +++ b/src/script.c @@ -20,7 +20,6 @@ */ #include -#include #include #include #include @@ -29,6 +28,7 @@ #include #include #include +#include #include "misc.h" #include "print.h" #include "options.h" @@ -37,27 +37,94 @@ #include "log.h" #include "script.h" #include "fs.h" +#include "timestamp.h" +#include "termios.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 char circular_buffer[MAX_BUFFER_SIZE]; -static char match_string[MAX_BUFFER_SIZE]; -static int buffer_size = 0; static char script_init[] = -"function set(arg)\n" +"tio.set = function(arg)\n" " local dtr = arg.DTR or -1\n" " local rts = arg.RTS or -1\n" " local cts = arg.CTS or -1\n" " local dsr = arg.DSR or -1\n" " local cd = arg.CD or -1\n" " local ri = arg.RI or -1\n" -" line_set(dtr, rts, cts, dsr, cd, ri)\n" -"end\n"; +" tio.line_set(dtr, rts, cts, dsr, cd, ri)\n" +"end\n" +"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" +" end\n" +" else\n" +" return nil, str\n" +" end\n" +" end\n" +"end\n" +"tio.alwaysecho = true\n" +"setmetatable(tio, tio)\n"; -// lua: sleep(seconds) -static int sleep_(lua_State *L) +static bool alwaysecho(lua_State *L) +{ + bool b; + + lua_getglobal(L, "tio"); + lua_getfield(L, -1, "alwaysecho"); + b = lua_toboolean(L, -1); + lua_pop(L, 2); + + return b; +} + +static int api_echo(lua_State *L) +{ + size_t len = 0; + const char *str = luaL_checklstring(L, 1, &len); + + if (option.timestamp) + { + char *pTimeStampNow = timestamp_current_time(); + if (pTimeStampNow) + { + tio_printf("%s", str); + if (option.log) + { + log_printf("\n[%s] %s", pTimeStampNow, str); + } + } + } else { + for (size_t i=0; i 0 && --attempts); - lua_pushnumber(L, ret); + if (len > 0) + return luaL_error(L, "partial write"); + + fsync(device_fd); // flush these characters now + tcdrain(device_fd); //ensure we flushed characters to our device + + lua_getglobal(L, "tio"); return 1; } -// Function to add a character to the circular expect buffer -static void expect_buffer_add(char c) +// lua: tio.read(size, timeout) +static int api_read(lua_State *L) { - if (buffer_size < MAX_BUFFER_SIZE) - { - circular_buffer[buffer_size++] = c; - } - else - { - // Shift the buffer to accommodate the new character - memmove(circular_buffer, circular_buffer + 1, MAX_BUFFER_SIZE - 1); - circular_buffer[MAX_BUFFER_SIZE - 1] = c; - } -} - -// Function to match against the circular expect buffer using regex -static bool match_regex(regex_t *regex) -{ - char buffer[MAX_BUFFER_SIZE + 1]; // Temporary buffer for regex matching - const char *s = circular_buffer; - regmatch_t pmatch[1]; - regoff_t len; - - memcpy(buffer, circular_buffer, buffer_size); - buffer[buffer_size] = '\0'; // Null-terminate the buffer - - // Match against the regex - int ret = regexec(regex, buffer, 1, pmatch, 0); - if (!ret) - { - // Match found - len = pmatch[0].rm_eo - pmatch[0].rm_so; - memcpy(match_string, s + pmatch[0].rm_so, len); - match_string[len] = '\0'; - - return true; - } - else if (ret == REG_NOMATCH) - { - // No match found, do nothing - } - else - { - // Error occurred during matching - tio_error_print("Regex match failed"); - } - - return false; -} - -// lua: ret,string = read_string(size, timeout) -static int read_string(lua_State *L) -{ - int size = lua_tointeger(L, 1); + int size = luaL_checkinteger(L, 1); int timeout = lua_tointeger(L, 2); - int ret = 0; - - char *buffer = malloc(size); - if (buffer == NULL) - { - ret = -1; // Error - goto error; - } if (timeout == 0) { timeout = -1; // Wait forever } - ssize_t bytes_read = read_poll(device_fd, buffer, size, timeout); - if (bytes_read < 0) + luaL_Buffer buffer; + luaL_buffinit(L, &buffer); + +#if LUA_VERSION_NUM >= 502 + char *p = luaL_prepbuffsize(&buffer, size); +#else + if (size > LUAL_BUFFERSIZE) + return luaL_error(L, "buffer overflow, max size is: %d", LUAL_BUFFERSIZE); + char *p = luaL_prepbuffer(&buffer); +#endif + + ssize_t ret = read_poll(device_fd, p, size, timeout); + if (ret < 0) + return luaL_error(L, "%s", strerror(errno)); + + luaL_addsize(&buffer, ret); + luaL_pushresult(&buffer); + + if (ret == 0) { - ret = -1; // Error - goto error; + // On timeout return nil instead of an empty string + lua_pop(L, 1); + lua_pushnil(L); } - else if (bytes_read == 0) + else { - ret = 0; // Timeout - goto error; + maybe_echo(L); } - for (ssize_t i=0; i 0) + if (ret < 0) + return luaL_error(L, "%s", strerror(errno)); + + if (ret == 0) { - putchar(c); - expect_buffer_add(c); - - if (option.log) - { - log_putc(c); - } - - // Match against the entire buffer - if (match_regex(®ex)) - { - ret = 1; - break; - } + luaL_pushresult(&b); + maybe_echo(L); + lua_pushnil(L); + lua_insert(L, -2); + return 2; } - else + + if (ch == '\n') { - // Timeout or error - break; + luaL_pushresult(&b); + maybe_echo(L); + return 1; } + + luaL_addchar(&b, ch); } - - // Cleanup - regfree(®ex); - -error: - lua_pushnumber(L, ret); - lua_pushstring(L, match_string); - return 2; } -// lua: exit(code) -static int exit_(lua_State *L) -{ - long code = lua_tointeger(L, 1); - - exit(code); - - return 0; -} - -// lua: list = tty_search() -static int tty_search_(lua_State *L) +// lua: table = tio.ttysearch() +static int api_ttysearch(lua_State *L) { UNUSED(L); GList *iter; @@ -445,39 +424,36 @@ static void script_buffer_run(lua_State *L, const char *script_buffer) } } +static void script_file_run(lua_State *L, const char *filename) +{ + if (strlen(filename) == 0) + { + tio_warning_printf("Missing script filename\n"); + return; + } + + if (luaL_dofile(L, filename)) + { + tio_warning_printf("lua: %s", lua_tostring(L, -1)); + lua_pop(L, 1); /* pop error message from the stack */ + return; + } +} + static const struct luaL_Reg tio_lib[] = { - { "sleep", sleep_}, - { "msleep", msleep}, + { "echo", api_echo}, + { "sleep", api_sleep}, + { "msleep", api_msleep}, { "line_set", line_set}, - { "modem_send", modem_send}, - { "send", send}, - { "read", read_string}, - { "expect", expect}, - { "exit", exit_}, - { "tty_search", tty_search_}, + { "send", api_send}, + { "write", api_write}, + { "read", api_read}, + { "readline", api_readline}, + { "ttysearch", api_ttysearch}, {NULL, NULL} }; -#if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501 -/* -** Adapted from Lua 5.2.0 (for backwards compatibility) -*/ -static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) -{ - luaL_checkstack(L, nup+1, "too many upvalues"); - for (; l->name != NULL; l++) { /* fill the table with given functions */ - int i; - lua_pushstring(L, l->name); - for (i = 0; i < nup; i++) /* copy upvalues to the top */ - lua_pushvalue(L, -(nup+1)); - lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ - lua_settable(L, -(nup + 3)); - } - lua_pop(L, nup); /* remove upvalues */ -} -#endif - static void script_load(lua_State *L) { int error; @@ -490,42 +466,13 @@ static void script_load(lua_State *L) } } -int lua_register_tio(lua_State *L) -{ - // Register lxi functions - lua_getglobal(L, "_G"); - luaL_setfuncs(L, tio_lib, 0); - lua_pop(L, 1); - - // Load lua init script - script_load(L); - - return 0; -} - -void script_file_run(lua_State *L, const char *filename) -{ - if (strlen(filename) == 0) - { - tio_warning_printf("Missing script filename\n"); - return; - } - - if (luaL_dofile(L, filename)) - { - tio_warning_printf("lua: %s\n", lua_tostring(L, -1)); - lua_pop(L, 1); /* pop error message from the stack */ - return; - } -} - -void script_set_global(lua_State *L, const char *name, long value) +static void script_set_global(lua_State *L, const char *name, long value) { lua_pushnumber(L, value); lua_setglobal(L, name); } -void script_set_globals(lua_State *L) +static void script_set_globals(lua_State *L) { script_set_global(L, "toggle", 2); script_set_global(L, "high", 1); @@ -535,7 +482,15 @@ void script_set_globals(lua_State *L) script_set_global(L, "YMODEM", YMODEM); } -void script_run(int fd) +#if LUA_VERSION_NUM >= 502 +static int luaopen_tio(lua_State *L) +{ + luaL_newlib(L, tio_lib); + return 1; +} +#endif + +void script_run(int fd, const char *script_filename) { lua_State *L; @@ -544,13 +499,25 @@ void script_run(int fd) L = luaL_newstate(); luaL_openlibs(L); - // Bind tio functions - lua_register_tio(L); +#if LUA_VERSION_NUM >= 502 + luaL_requiref(L, "tio", luaopen_tio, 1); +#else + luaL_register(L, "tio", tio_lib); +#endif + lua_pop(L, 1); + + // Load lua init script + script_load(L); // Initialize globals script_set_globals(L); - if (option.script_filename != NULL) + if (script_filename != NULL) + { + tio_printf("Running script %s", script_filename); + script_file_run(L, script_filename); + } + else if (option.script_filename != NULL) { tio_printf("Running script %s", option.script_filename); script_file_run(L, option.script_filename); diff --git a/src/script.h b/src/script.h index d6df204..58ba1e1 100644 --- a/src/script.h +++ b/src/script.h @@ -29,5 +29,5 @@ typedef enum SCRIPT_RUN_END, } script_run_t; -void script_run(int fd); +void script_run(int fd, const char *script_filename); const char *script_run_state_to_string(script_run_t state); diff --git a/src/socket.c b/src/socket.c index 3500f97..e51f937 100644 --- a/src/socket.c +++ b/src/socket.c @@ -124,6 +124,7 @@ void socket_configure(void) struct sockaddr_in6 sockaddr_inet6 = {}; struct sockaddr *sockaddr_p; socklen_t socklen; + int optval; /* Parse socket string */ @@ -225,6 +226,16 @@ void socket_configure(void) exit(EXIT_FAILURE); } +#if defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL) + if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_NOSIGPIPE, &optval, sizeof(optval))) +#else + if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))) +#endif + { + tio_error_printf("Failed to set socket options (%s)", strerror(errno)); + exit(EXIT_FAILURE); + } + /* Bind */ if (bind(sockfd, sockaddr_p, socklen) < 0) { @@ -263,7 +274,12 @@ void socket_write(char input_char) { if (clientfds[i] != -1) { - if (write(clientfds[i], &input_char, 1) <= 0) + +#if defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL) + if (send(clientfds[i], &input_char, 1, 0) <= 0) +#else + if (send(clientfds[i], &input_char, 1, MSG_NOSIGNAL) <= 0) +#endif { tio_error_printf_silent("Failed to write to socket (%s)", strerror(errno)); close(clientfds[i]); @@ -343,20 +359,20 @@ bool socket_handle_input(fd_set *rdfs, char *output_char) } /* If INLCR is set, a received NL character shall be translated into a CR character */ - if (*output_char == '\n' && map_i_nl_cr) + if (*output_char == '\n' && option.map_i_nl_cr) { *output_char = '\r'; } else if (*output_char == '\r') { /* If IGNCR is set, a received CR character shall be ignored (not read). */ - if (map_ign_cr) + if (option.map_ign_cr) { return false; } /* If IGNCR is not set and ICRNL is set, a received CR character shall be translated into an NL character. */ - if (map_i_cr_nl) + if (option.map_i_cr_nl) { *output_char = '\n'; } diff --git a/src/timestamp.c b/src/timestamp.c index 19ea70e..9273758 100644 --- a/src/timestamp.c +++ b/src/timestamp.c @@ -22,8 +22,6 @@ #include "config.h" #include #include -#include -#include #include #include #include "error.h" @@ -31,8 +29,6 @@ #include "options.h" #include "timestamp.h" -#define TIME_STRING_SIZE_MAX 24 - char *timestamp_current_time(void) { static char time_string[TIME_STRING_SIZE_MAX]; @@ -78,75 +74,31 @@ char *timestamp_current_time(void) tm = localtime(&tv.tv_sec); len = strftime(time_string, sizeof(time_string), "%Y-%m-%dT%H:%M:%S", tm); break; + case TIMESTAMP_EPOCH: + case TIMESTAMP_EPOCH_USEC: + // "N.sss" (seconds since Unix epoch, 1970-01-01 00:00:00Z) + tv = tv_now; + tm = localtime(&tv.tv_sec); + len = strftime(time_string, sizeof(time_string), "%s", tm); + break; default: return NULL; } - // Append milliseconds to all timestamps + // Append millis-/microseconds to all timestamps if (len) { - len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%03ld", (long)tv.tv_usec / 1000); + if ( option.timestamp == TIMESTAMP_EPOCH_USEC ) + { + len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%06ld", (long)tv.tv_usec); + } + else + { + len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%03ld", (long)tv.tv_usec / 1000); + } } - // Save previous time value for next run tv_previous = tv_now; return (len < TIME_STRING_SIZE_MAX) ? time_string : NULL; } - -const char* timestamp_state_to_string(timestamp_t timestamp) -{ - switch (timestamp) - { - case TIMESTAMP_NONE: - return "disabled"; - break; - - case TIMESTAMP_24HOUR: - return "24hour"; - break; - - case TIMESTAMP_24HOUR_START: - return "24hour-start"; - break; - - case TIMESTAMP_24HOUR_DELTA: - return "24hour-delta"; - break; - - case TIMESTAMP_ISO8601: - return "iso8601"; - break; - - default: - return "unknown"; - break; - } -} - -timestamp_t timestamp_option_parse(const char *arg) -{ - timestamp_t timestamp = TIMESTAMP_24HOUR; // Default - - if (arg != NULL) - { - if (strcmp(arg, "24hour") == 0) - { - return TIMESTAMP_24HOUR; - } - else if (strcmp(arg, "24hour-start") == 0) - { - return TIMESTAMP_24HOUR_START; - } - else if (strcmp(arg, "24hour-delta") == 0) - { - return TIMESTAMP_24HOUR_DELTA; - } - else if (strcmp(arg, "iso8601") == 0) - { - return TIMESTAMP_ISO8601; - } - } - - return timestamp; -} diff --git a/src/timestamp.h b/src/timestamp.h index e36660a..0544544 100644 --- a/src/timestamp.h +++ b/src/timestamp.h @@ -28,9 +28,12 @@ typedef enum TIMESTAMP_24HOUR_START, TIMESTAMP_24HOUR_DELTA, TIMESTAMP_ISO8601, + TIMESTAMP_EPOCH, + TIMESTAMP_EPOCH_USEC, TIMESTAMP_END, } timestamp_t; +#define TIME_STRING_SIZE_MAX 24 + char *timestamp_current_time(void); -const char* timestamp_state_to_string(timestamp_t timestamp); -timestamp_t timestamp_option_parse(const char *arg); + diff --git a/src/tty.c b/src/tty.c index c8f7d7b..efda859 100644 --- a/src/tty.c +++ b/src/tty.c @@ -22,6 +22,16 @@ #if defined(__linux__) #include #endif + +#if defined(__APPLE__) || defined(__MACH__) +#include +#include +#include +#include +#include +#endif + +#include "version.h" #include "config.h" #include #include @@ -64,43 +74,42 @@ #include "script.h" #include "xymodem.h" #include "fs.h" +#include "readline.h" /* tty device listing configuration */ #if defined(__linux__) -#define PATH_SERIAL_DEVICES "/dev/serial/by-id/" -#define PATH_SERIAL_DEVICES_BY_PATH "/dev/serial/by-path/" -#define PREFIX_TTY_DEVICES "" +#define PATH_SERIAL_DEVICES "/dev" +#define PATH_SERIAL_DEVICES_BY_ID "/dev/serial/by-id" +#define PATH_SERIAL_DEVICES_BY_PATH "/dev/serial/by-path" #elif defined(__FreeBSD__) -#define PATH_SERIAL_DEVICES "/dev/" -#define PREFIX_TTY_DEVICES "cua" +#define PATH_SERIAL_DEVICES "/dev" #elif defined(__APPLE__) -#define PATH_SERIAL_DEVICES "/dev/" -#define PREFIX_TTY_DEVICES "tty." +#define PATH_SERIAL_DEVICES "/dev" #elif defined(__CYGWIN__) -#define PATH_SERIAL_DEVICES "/dev/" -#define PREFIX_TTY_DEVICES "ttyS" +#define PATH_SERIAL_DEVICES "/dev" #elif defined(__HAIKU__) -#define PATH_SERIAL_DEVICES "/dev/ports/" -#define PREFIX_TTY_DEVICES "" +#define PATH_SERIAL_DEVICES "/dev/ports" #else -#define PATH_SERIAL_DEVICES "/dev/" -#define PREFIX_TTY_DEVICES "tty" +#define PATH_SERIAL_DEVICES "/dev" #endif #ifndef CMSPAR #define CMSPAR 010000000000 #endif -#define LINE_SIZE_MAX 1000 - #define KEY_0 0x30 #define KEY_1 0x31 #define KEY_2 0x32 #define KEY_3 0x33 #define KEY_4 0x34 #define KEY_5 0x35 +#define KEY_6 0x36 +#define KEY_7 0x37 +#define KEY_8 0x38 +#define KEY_9 0x39 #define KEY_QUESTION 0x3f +#define KEY_A 0x61 #define KEY_B 0x62 #define KEY_C 0x63 #define KEY_E 0x65 @@ -115,9 +124,9 @@ #define KEY_P 0x70 #define KEY_Q 0x71 #define KEY_R 0x72 +#define KEY_SHIFT_R 0x52 #define KEY_S 0x73 #define KEY_T 0x74 -#define KEY_U 0x55 #define KEY_V 0x76 #define KEY_X 0x78 #define KEY_Y 0x79 @@ -135,6 +144,7 @@ typedef enum SUBCOMMAND_LINE_TOGGLE, SUBCOMMAND_LINE_PULSE, SUBCOMMAND_XMODEM, + SUBCOMMAND_MAP, } sub_command_t; const char random_array[] = @@ -152,28 +162,17 @@ const char random_array[] = }; bool interactive_mode = true; -bool map_i_nl_cr = false; -bool map_i_cr_nl = false; -bool map_ign_cr = false; char key_hit = 0xff; -const char* device_name; +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 bool connected = false; static bool standard_baudrate = true; -static void (*print)(char c); +static void (*printchar)(char c); static int device_fd; -static bool map_i_ff_escc = false; -static bool map_i_nl_crnl = false; -static bool map_o_cr_nl = false; -static bool map_o_nl_crnl = false; -static bool map_o_del_bs = false; -static bool map_o_ltu = false; -static bool map_o_nulbrk = false; -static bool map_o_msblsb = false; static char hex_chars[2]; static unsigned char hex_char_index = 0; static char tty_buffer[BUFSIZ*2]; @@ -182,7 +181,8 @@ 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[LINE_SIZE_MAX]; +static char line[PATH_MAX]; +static size_t listing_device_name_length_max = 0; static void optional_local_echo(char c) { @@ -191,7 +191,13 @@ static void optional_local_echo(char c) return; } - print(c); + printchar(c); + + if ((option.output_mode == OUTPUT_MODE_NORMAL) && (c == 127)) + { + // Force destructive backspace + printf("\b \b"); + } if (option.log) { @@ -252,7 +258,7 @@ ssize_t tty_write(int fd, const void *buffer, size_t count) ssize_t retval = 0, bytes_written = 0; size_t i; - if (map_o_ltu) + if (option.map_o_ltu) { // Convert lower case to upper case for (i = 0; i 0) { if (*p == 0x08 || *p == 0x7f) { - if (p > line ) + if (p > line) { write(STDOUT_FILENO, "\b \b", 3); p--; @@ -611,11 +617,11 @@ void tty_output_mode_set(output_mode_t mode) switch (mode) { case OUTPUT_MODE_NORMAL: - print = print_normal; + printchar = print_normal; break; case OUTPUT_MODE_HEX: - print = print_hex; + printchar = print_hex; break; case OUTPUT_MODE_END: @@ -623,6 +629,35 @@ void tty_output_mode_set(output_mode_t mode) } } +static void mappings_print(void) +{ + 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 || + option.map_o_ltu || option.map_o_nulbrk || option.map_i_msb2lsb || + option.map_o_ign_cr) + { + tio_printf(" Mappings:%s%s%s%s%s%s%s%s%s%s%s%s%s", + option.map_i_cr_nl ? " ICRNL" : "", + option.map_ign_cr ? " IGNCR" : "", + option.map_i_ff_escc ? " IFFESCC" : "", + option.map_i_nl_cr ? " INLCR" : "", + option.map_i_nl_crnl ? " INLCRNL" : "", + option.map_i_cr_crnl ? " ICRCRNL" : "", + option.map_i_msb2lsb ? " IMSB2LSB" : "", + option.map_o_cr_nl ? " OCRNL" : "", + option.map_o_del_bs ? " ODELBS" : "", + option.map_o_nl_crnl ? " ONLCRNL" : "", + option.map_o_ltu ? " OLTU" : "", + option.map_o_nulbrk ? " ONULBRK" : "", + option.map_o_ign_cr ? " OIGNCR" : ""); + } + else + { + tio_printf(" Mappings: none"); + } +} + void handle_command_sequence(char input_char, char *output_char, bool *forward) { char unused_char; @@ -676,7 +711,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tty_line_poke(device_fd, TIOCM_RI, line_mode, option.ri_pulse_duration); break; default: - tio_warning_printf("Invalid line number"); + tio_error_print("Invalid line number"); break; } break; @@ -689,9 +724,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf_raw("Enter file name: "); if (tio_readln()) { + int ret; + tio_printf("Sending file '%s' ", line); tio_printf("Press any key to abort transfer"); - tio_printf("%s", xymodem_send(device_fd, line, XMODEM_CRC) < 0 ? "Aborted" : "Done"); + ret = xymodem_send(device_fd, line, XMODEM_1K); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); } break; @@ -700,11 +738,96 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf_raw("Enter file name: "); if (tio_readln()) { + int ret; + tio_printf("Sending file '%s' ", line); tio_printf("Press any key to abort transfer"); - tio_printf("%s", xymodem_send(device_fd, line, XMODEM_CRC) < 0 ? "Aborted" : "Done"); + ret = xymodem_send(device_fd, line, XMODEM_CRC); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); } break; + + case KEY_2: + tio_printf("Receive file with XMODEM-CRC"); + tio_printf_raw("Enter file name: "); + if (tio_readln()) + { + 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); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); + } + break; + + default: + tio_error_print("Invalid protocol option"); + break; + } + break; + + case SUBCOMMAND_MAP: + switch (input_char) + { + case KEY_0: + option.map_i_cr_nl = !option.map_i_cr_nl; + tty_reconfigure(); + tio_printf("ICRNL is %s", option.map_i_cr_nl ? "set" : "unset"); + break; + case KEY_1: + option.map_ign_cr = !option.map_ign_cr; + tty_reconfigure(); + tio_printf("IGNCR is %s", option.map_ign_cr ? "set" : "unset"); + break; + case KEY_2: + option.map_i_ff_escc = !option.map_i_ff_escc; + tio_printf("IFFESCC is %s", option.map_i_ff_escc ? "set" : "unset"); + break; + case KEY_3: + option.map_i_nl_cr = !option.map_i_nl_cr; + tty_reconfigure(); + tio_printf("INLCR is %s", option.map_i_nl_cr ? "set" : "unset"); + break; + case KEY_4: + option.map_i_nl_crnl = !option.map_i_nl_crnl; + tio_printf("INLCRNL is %s", option.map_i_nl_crnl ? "set" : "unset"); + break; + case KEY_5: + option.map_i_cr_crnl = !option.map_i_cr_crnl; + tio_printf("ICRCRNL is %s", option.map_i_cr_crnl ? "set" : "unset"); + break; + case KEY_6: + option.map_i_msb2lsb = !option.map_i_msb2lsb; + tio_printf("IMSB2LSB is %s", option.map_i_msb2lsb ? "set" : "unset"); + break; + case KEY_7: + option.map_o_cr_nl = !option.map_o_cr_nl; + tio_printf("OCRNL is %s", option.map_o_cr_nl ? "set" : "unset"); + break; + case KEY_8: + option.map_o_del_bs = !option.map_o_del_bs; + tio_printf("ODELBS is %s", option.map_o_del_bs ? "set" : "unset"); + break; + case KEY_9: + option.map_o_nl_crnl = !option.map_o_nl_crnl; + tio_printf("ONLCRNL is %s", option.map_o_nl_crnl ? "set" : "unset"); + break; + case KEY_A: + option.map_o_ltu = !option.map_o_ltu; + tio_printf("OLTU is %s", option.map_o_ltu ? "set" : "unset"); + break; + case KEY_B: + option.map_o_nulbrk = !option.map_o_nulbrk; + tio_printf("ONULBRK is %s", option.map_o_nulbrk ? "set" : "unset"); + break; + case KEY_C: + option.map_o_ign_cr = !option.map_o_ign_cr; + tio_printf("OIGNCR is %s", option.map_o_ign_cr ? "set" : "unset"); + break; + default: + tio_error_print("Invalid input"); + break; } break; } @@ -744,16 +867,16 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf(" ctrl-%c i Toggle input mode", 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 Toggle MSB to LSB bit order", option.prefix_key); + tio_printf(" ctrl-%c m Change mapping of characters on input or output", option.prefix_key); tio_printf(" ctrl-%c o Toggle output mode", option.prefix_key); tio_printf(" ctrl-%c p Pulse serial port line", option.prefix_key); tio_printf(" ctrl-%c q Quit", option.prefix_key); tio_printf(" ctrl-%c r Run script", option.prefix_key); + tio_printf(" ctrl-%c R Execute shell command with I/O redirected to device", option.prefix_key); tio_printf(" ctrl-%c s Show statistics", option.prefix_key); tio_printf(" ctrl-%c t Toggle line timestamp mode", option.prefix_key); - tio_printf(" ctrl-%c U Toggle conversion to uppercase on output", option.prefix_key); tio_printf(" ctrl-%c v Show version", option.prefix_key); - tio_printf(" ctrl-%c x Send file via Xmodem", option.prefix_key); + 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); break; @@ -824,12 +947,13 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_C: tio_printf("Configuration:"); - options_print(); config_file_print(); + options_print(); if (option.rs485) { rs485_print_config(); } + mappings_print(); break; case KEY_E: @@ -887,17 +1011,37 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) break; case KEY_M: - /* Toggle bit order */ - if (!map_o_msblsb) - { - map_o_msblsb = true; - tio_printf("Switched to reverse bit order"); - } - else - { - map_o_msblsb = false; - tio_printf("Switched to normal bit order"); - } + /* Change mapping of characters on input or output */ + tio_printf("Please enter which mapping to set or unset:"); + tio_printf(" (0) ICRNL: %s mapping CR to NL on input (unless IGNCR is set)", + option.map_i_cr_nl ? "Unset" : "Set"); + tio_printf(" (1) IGNCR: %s ignoring CR on input", + option.map_ign_cr ? "Unset" : "Set"); + tio_printf(" (2) IFFESCC: %s mapping FF to ESC-c on input", + option.map_i_ff_escc ? "Unset" : "Set"); + tio_printf(" (3) INLCR: %s mapping NL to CR on input", + option.map_i_nl_cr ? "Unset" : "Set"); + tio_printf(" (4) INLCRNL: %s mapping NL to CR-NL on input", + option.map_i_nl_crnl ? "Unset" : "Set"); + tio_printf(" (5) ICRCRNL: %s mapping CR to CR-NL on input", + option.map_i_cr_crnl ? "Unset" : "Set"); + tio_printf(" (6) IMSB2LSB: %s mapping MSB bit order to LSB on input", + option.map_i_msb2lsb ? "Unset" : "Set"); + tio_printf(" (7) OCRNL: %s mapping CR to NL on output", + option.map_o_cr_nl ? "Unset" : "Set"); + tio_printf(" (8) ODELBS: %s mapping DEL to BS on output", + option.map_o_del_bs ? "Unset" : "Set"); + tio_printf(" (9) ONLCRNL: %s mapping NL to CR-NL on output", + option.map_o_nl_crnl ? "Unset" : "Set"); + tio_printf(" (a) OLTU: %s mapping lowercase to uppercase on output", + option.map_o_ltu ? "Unset" : "Set"); + tio_printf(" (b) ONULBRK: %s mapping NUL to send break signal on output", + option.map_o_nulbrk ? "Unset" : "Set"); + tio_printf(" (c) OIGNCR: %s ignoring CR on output", + option.map_o_ign_cr ? "Unset" : "Set"); + + // Process next input character as sub command + sub_command = SUBCOMMAND_MAP; break; case KEY_Q: @@ -906,7 +1050,26 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_R: /* Run script */ - script_run(device_fd); + tio_printf("Run Lua script") + tio_printf_raw("Enter file name: "); + if (tio_readln()) + { + clear_line(); + script_run(device_fd, line); + } + else + { + clear_line(); + script_run(device_fd, NULL); + } + break; + + case KEY_SHIFT_R: + /* Execute shell command */ + tio_printf("Execute shell command with I/O redirected to device"); + tio_printf_raw("Enter command: "); + if (tio_readln()) + execute_shell_command(device_fd, line); break; case KEY_S: @@ -934,6 +1097,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case TIMESTAMP_ISO8601: tio_printf("Switched timestamp mode to iso8601"); break; + case TIMESTAMP_EPOCH: + tio_printf("Switched timestamp mode to epoch"); + break; + case TIMESTAMP_EPOCH_USEC: + tio_printf("Switched timestamp mode to epoch with subdivision in microseconds"); + break; case TIMESTAMP_END: option.timestamp = TIMESTAMP_NONE; tio_printf("Switched timestamp mode off"); @@ -941,18 +1110,15 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) } break; - case KEY_U: - map_o_ltu = !map_o_ltu; - break; - case KEY_V: - tio_printf("tio v%s", VERSION); + tio_printf("tio %s", VERSION); break; case KEY_X: tio_printf("Please enter which X modem protocol to use:"); - tio_printf(" (0) XMODEM-1K"); - tio_printf(" (1) XMODEM-CRC"); + tio_printf(" (0) XMODEM-1K send"); + tio_printf(" (1) XMODEM-CRC send"); + tio_printf(" (2) XMODEM-CRC receive"); // Process next input character as sub command sub_command = SUBCOMMAND_XMODEM; break; @@ -961,9 +1127,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf("Send file with YMODEM"); tio_printf_raw("Enter file name: "); if (tio_readln()) { + int ret; + tio_printf("Sending file '%s' ", line); tio_printf("Press any key to abort transfer"); - tio_printf("%s", xymodem_send(device_fd, line, YMODEM) < 0 ? "Aborted" : "Done"); + ret = xymodem_send(device_fd, line, YMODEM); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); } break; @@ -1035,10 +1204,6 @@ void stdout_configure(void) { int status; - /* Disable line buffering in stdout. This is necessary if we - * want things like local echo to work correctly. */ - setvbuf(stdout, NULL, _IONBF, 0); - /* Save current stdout settings */ if (tcgetattr(STDOUT_FILENO, &stdout_old) < 0) { @@ -1071,7 +1236,7 @@ void stdout_configure(void) } /* At start use normal print function */ - print = print_normal; + printchar = print_normal; /* Make sure we restore old stdout settings on exit */ atexit(&stdout_restore); @@ -1079,9 +1244,6 @@ void stdout_configure(void) void tty_configure(void) { - bool token_found = true; - char *token = NULL; - char *buffer; int status; speed_t baudrate; @@ -1154,25 +1316,26 @@ void tty_configure(void) } /* Set flow control */ - if (strcmp("hard", option.flow) == 0) + switch (option.flow) { - tio.c_cflag |= CRTSCTS; - tio.c_iflag &= ~(IXON | IXOFF | IXANY); - } - else if (strcmp("soft", option.flow) == 0) - { - tio.c_cflag &= ~CRTSCTS; - tio.c_iflag |= IXON | IXOFF; - } - else if (strcmp("none", option.flow) == 0) - { - tio.c_cflag &= ~CRTSCTS; - tio.c_iflag &= ~(IXON | IXOFF | IXANY); - } - else - { - tio_error_printf("Invalid flow control"); - exit(EXIT_FAILURE); + case FLOW_NONE: + tio.c_cflag &= ~CRTSCTS; + tio.c_iflag &= ~(IXON | IXOFF | IXANY); + break; + + case FLOW_HARD: + tio.c_cflag |= CRTSCTS; + tio.c_iflag &= ~(IXON | IXOFF | IXANY); + break; + + case FLOW_SOFT: + tio.c_cflag &= ~CRTSCTS; + tio.c_iflag |= IXON | IXOFF; + break; + + default: + tio_error_printf("Invalid flow control"); + exit(EXIT_FAILURE); } /* Set stopbits */ @@ -1190,36 +1353,37 @@ void tty_configure(void) } /* Set parity */ - if (strcmp("odd", option.parity) == 0) + switch (option.parity) { - tio.c_cflag |= PARENB; - tio.c_cflag |= PARODD; - } - else if (strcmp("even", option.parity) == 0) - { - tio.c_cflag |= PARENB; - tio.c_cflag &= ~PARODD; - } - else if (strcmp("none", option.parity) == 0) - { - tio.c_cflag &= ~PARENB; - } - else if ( strcmp("mark", option.parity) == 0) - { - tio.c_cflag |= PARENB; - tio.c_cflag |= PARODD; - tio.c_cflag |= CMSPAR; - } - else if ( strcmp("space", option.parity) == 0) - { - tio.c_cflag |= PARENB; - tio.c_cflag &= ~PARODD; - tio.c_cflag |= CMSPAR; - } - else - { - tio_error_printf("Invalid parity"); - exit(EXIT_FAILURE); + case PARITY_NONE: + tio.c_cflag &= ~PARENB; + break; + + case PARITY_ODD: + tio.c_cflag |= PARENB; + tio.c_cflag |= PARODD; + break; + + case PARITY_EVEN: + tio.c_cflag |= PARENB; + tio.c_cflag &= ~PARODD; + break; + + case PARITY_MARK: + tio.c_cflag |= PARENB; + tio.c_cflag |= PARODD; + tio.c_cflag |= CMSPAR; + break; + + case PARITY_SPACE: + tio.c_cflag |= PARENB; + tio.c_cflag &= ~PARODD; + tio.c_cflag |= CMSPAR; + break; + + default: + tio_error_printf("Invalid parity"); + exit(EXIT_FAILURE); } /* Control, input, output, local modes for tty device */ @@ -1231,80 +1395,30 @@ void tty_configure(void) tio.c_cc[VTIME] = 0; // Inter-character timer unused tio.c_cc[VMIN] = 1; // Blocking read until 1 character received - /* Configure any specified input or output mappings */ - buffer = strdup(option.map); - while (token_found == true) + /* Configure input mappings */ + if (option.map_i_nl_cr) { - if (token == NULL) - { - token = strtok(buffer,","); - } - else - { - token = strtok(NULL, ","); - } - - if (token != NULL) - { - if (strcmp(token,"INLCR") == 0) - { - tio.c_iflag |= INLCR; - map_i_nl_cr = true; - } - else if (strcmp(token,"IGNCR") == 0) - { - tio.c_iflag |= IGNCR; - map_ign_cr = true; - } - else if (strcmp(token,"ICRNL") == 0) - { - tio.c_iflag |= ICRNL; - map_i_cr_nl = true; - } - else if (strcmp(token,"OCRNL") == 0) - { - map_o_cr_nl = true; - } - else if (strcmp(token,"ODELBS") == 0) - { - map_o_del_bs = true; - } - else if (strcmp(token,"IFFESCC") == 0) - { - map_i_ff_escc = true; - } - else if (strcmp(token,"INLCRNL") == 0) - { - map_i_nl_crnl = true; - } - else if (strcmp(token, "ONLCRNL") == 0) - { - map_o_nl_crnl = true; - } - else if (strcmp(token, "OLTU") == 0) - { - map_o_ltu = true; - } - else if (strcmp(token, "ONULBRK") == 0) - { - map_o_nulbrk = true; - } - else if (strcmp(token, "MSB2LSB") == 0) - { - map_o_msblsb = true; - } - else - { - printf("Error: Unknown mapping flag %s\n", token); - exit(EXIT_FAILURE); - } - } - else - { - token_found = false; - } + tio.c_iflag |= INLCR; + } + if (option.map_ign_cr) + { + tio.c_iflag |= IGNCR; + } + if (option.map_i_cr_nl) + { + tio.c_iflag |= ICRNL; + } +} + +void tty_reconfigure(void) +{ + tty_configure(); + + if (connected) + { + /* Activate new port settings */ + tcsetattr(device_fd, TCSANOW, &tio); } - free(buffer); } static bool is_serial_device(const char *format, ...) @@ -1326,7 +1440,21 @@ static bool is_serial_device(const char *format, ...) return false; } - if (stat(filename, &st) != 0) +#if defined(__APPLE__) + // Make sure device name is on the form /dev/cu.* or /dev/tty.* + if ((strncmp(filename, "/dev/cu.", 8) != 0) && (strncmp(filename, "/dev/tty.", 9) != 0)) + { + return false; + } +#endif + + fd = open(filename, O_RDONLY | O_NONBLOCK | O_NOCTTY); + if (fd == -1) + { + return false; + } + + if (fstat(fd, &st) == -1) { return false; } @@ -1337,12 +1465,6 @@ static bool is_serial_device(const char *format, ...) return false; } - fd = open(filename, O_RDONLY | O_NONBLOCK | O_NOCTTY); - if (fd == -1) - { - return false; - } - // Make sure it is a tty status = isatty(fd); if (status == 0) @@ -1369,7 +1491,8 @@ error: static void list_serial_devices_by_id(void) { - DIR *d = opendir(PATH_SERIAL_DEVICES); +#ifdef PATH_SERIAL_DEVICES_BY_ID + DIR *d = opendir(PATH_SERIAL_DEVICES_BY_ID); if (d) { struct dirent *dir; @@ -1381,23 +1504,20 @@ static void list_serial_devices_by_id(void) { if ((strcmp(dir->d_name, ".")) && (strcmp(dir->d_name, ".."))) { - if (!strncmp(dir->d_name, PREFIX_TTY_DEVICES, sizeof(PREFIX_TTY_DEVICES) - 1)) + if (is_serial_device("%s/%s", PATH_SERIAL_DEVICES_BY_ID, dir->d_name)) { - if (is_serial_device("%s%s", PATH_SERIAL_DEVICES, dir->d_name)) - { - printf("%s%s\n", PATH_SERIAL_DEVICES, dir->d_name); - } + printf("%s/%s\n", PATH_SERIAL_DEVICES_BY_ID, dir->d_name); } } } closedir(d); } +#endif } static void list_serial_devices_by_path(void) { #ifdef PATH_SERIAL_DEVICES_BY_PATH - DIR *d = opendir(PATH_SERIAL_DEVICES_BY_PATH); if (d) { @@ -1410,12 +1530,9 @@ static void list_serial_devices_by_path(void) { if ((strcmp(dir->d_name, ".")) && (strcmp(dir->d_name, ".."))) { - if (!strncmp(dir->d_name, "", sizeof("") - 1)) + if (is_serial_device("%s/%s", PATH_SERIAL_DEVICES_BY_PATH, dir->d_name)) { - if (is_serial_device("%s%s", PATH_SERIAL_DEVICES_BY_PATH, dir->d_name)) - { - printf("%s%s\n", PATH_SERIAL_DEVICES_BY_PATH, dir->d_name); - } + printf("%s/%s\n", PATH_SERIAL_DEVICES_BY_PATH, dir->d_name); } } } @@ -1514,6 +1631,7 @@ const char* get_serial_port_type(const char* port_name) const char* get_serial_port_type(const char* port_name) { + (void)port_name; return ""; } @@ -1543,8 +1661,13 @@ static void search_reset(void) // Indicate an empty list device_list = NULL; + + // Reset max device name length + listing_device_name_length_max = 0; } +#if defined(__linux__) + GList *tty_search_for_serial_devices(void) { DIR *dir; @@ -1560,7 +1683,6 @@ GList *tty_search_for_serial_devices(void) dir = opendir("/sys/class/tty"); if (!dir) { - // Error return NULL; } @@ -1631,7 +1753,8 @@ GList *tty_search_for_serial_devices(void) // Hash remaining string to get unique topology ID unsigned long hash = djb2_hash((const unsigned char *)devices_path); - char *tid = base62_encode(hash); + char tid[5]; + base62_encode(hash, tid); free(devices_path); // Construct the path to the device's driver symlink @@ -1662,18 +1785,22 @@ GList *tty_search_for_serial_devices(void) creation_time = fs_get_creation_time(path); double uptime = current_time - creation_time; - // Read sysfs files to get best possible description of the driver + // Read sysfs files to get best possible description char description[50] = {}; - length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/interface", entry->d_name); - if (length == -1) - { - length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../interface", entry->d_name); - } + length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../product", entry->d_name); if (length == -1) { length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../../product", entry->d_name); } if (length == -1) + { + length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/interface", entry->d_name); + } + if (length == -1) + { + length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../interface", entry->d_name); + } + if (length == -1) { snprintf(description, sizeof(description), "%s", get_serial_port_type(path)); } @@ -1708,6 +1835,12 @@ GList *tty_search_for_serial_devices(void) // Add device information to device list device_list = g_list_append(device_list, device); + + // Update length of longest device name string + if (strlen(device->path) > listing_device_name_length_max) + { + listing_device_name_length_max = strlen(device->path); + } } if (g_list_length(device_list) == 0) @@ -1724,14 +1857,328 @@ GList *tty_search_for_serial_devices(void) return device_list; } +#elif defined(__APPLE__) || defined(__MACH__) + +char *getPropertyString(io_object_t device, CFStringRef property) +{ + /* Validate inputs */ + if (device == IO_OBJECT_NULL || property == NULL) + { + return NULL; + } + + /* Attempt to get property */ + CFTypeRef valueRef = IORegistryEntryCreateCFProperty( + device, property, kCFAllocatorDefault, 0); + if (!valueRef) + { + return NULL; + } + + /* Ensure it's a CFString */ + if (CFGetTypeID(valueRef) != CFStringGetTypeID()) + { + CFRelease(valueRef); + return NULL; + } + + /* Convert to C string */ + CFIndex length = CFStringGetLength(valueRef); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; + char *result = malloc(maxSize); + + if (!result) + { + CFRelease(valueRef); + return NULL; + } + + bool converted = CFStringGetCString( + (CFStringRef)valueRef, + result, + maxSize, + kCFStringEncodingUTF8 + ); + + CFRelease(valueRef); + + if (!converted) + { + free(result); + return NULL; + } + + return result; +} + +char *getDeviceLocation(io_object_t device) +{ + /* Validate device */ + if (device == IO_OBJECT_NULL) + { + return strdup("Invalid Device"); + } + + /* Attempt to get location */ + io_string_t location = {0}; + kern_return_t result = IORegistryEntryGetLocationInPlane( + device, kIOServicePlane, location); + + if (result != KERN_SUCCESS) + { + return strdup("Unknown Location"); + } + + /* Safely copy location */ + size_t len = strnlen(location, sizeof(io_string_t)); + char *trimmed_location = calloc(1, len + 1); + + if (!trimmed_location) + { + return strdup("Memory Error"); + } + + memcpy(trimmed_location, location, len); + return trimmed_location; +} + +// for __APPLE__ +GList *tty_search_for_serial_devices(void) +{ + search_reset(); + io_iterator_t iter = IO_OBJECT_NULL; + CFMutableDictionaryRef matchingDict = NULL; + listing_device_name_length_max = 0; + + /* Create matching dictionary for serial devices */ + if (!(matchingDict = IOServiceMatching(kIOSerialBSDServiceValue))) + { + tio_error_print("Failed to create matching dictionary for serial devices"); + return NULL; + } + + /* Get matching services */ + kern_return_t kernResult = IOServiceGetMatchingServices( + kIOMainPortDefault, matchingDict, &iter); + matchingDict = NULL; /* Dictionary ownership transferred */ + + if (kernResult != KERN_SUCCESS) + { + tio_error_print("IOServiceGetMatchingServices failed: %d", kernResult); + return NULL; + } + + /* Defensive check for iterator */ + if (iter == IO_OBJECT_NULL) + { + tio_error_print("Invalid device iterator"); + return NULL; + } + + /* Iterate through serial devices and collect information */ + for (io_object_t device; (device = IOIteratorNext(iter));) + { + char *devicePath = NULL, *locationID = NULL; + char *productName = NULL, *vendorName = NULL; + char tid[5] = {0}; + double uptime = 0.0; + + /* Get device path - key determines if we get tty. or cu. */ + //if (!(devicePath = getPropertyString(device, CFSTR(kIODialinDeviceKey)))) + if (!(devicePath = getPropertyString(device, CFSTR(kIOCalloutDeviceKey)))) + { + IOObjectRelease(device); + continue; /* Skip devices without a path */ + } + + /* Update length of longest device name string */ + listing_device_name_length_max = + strlen(devicePath) > listing_device_name_length_max + ? strlen(devicePath) + : listing_device_name_length_max; + + /* Calculate uptime */ + uptime = get_current_time() - fs_get_creation_time(devicePath); + + /* Find USB device (if applicable) */ + io_object_t usbDevice = IO_OBJECT_NULL; + kern_return_t usbResult = IORegistryEntryGetParentEntry( + device, kIOServicePlane, &usbDevice); + + /* Traverse up the device tree to find a USB device */ + while (usbResult == KERN_SUCCESS && + !IOObjectConformsTo(usbDevice, "IOUSBDevice")) + { + io_object_t oldUsbDevice = usbDevice; + usbResult = IORegistryEntryGetParentEntry( + usbDevice, kIOServicePlane, &usbDevice); + IOObjectRelease(oldUsbDevice); + } + + /* If we found a USB device */ + if (usbResult == KERN_SUCCESS) + { + locationID = getDeviceLocation(usbDevice); + + unsigned long hash2 = djb2_hash((const unsigned char *)(locationID ?: "")); + base62_encode(hash2, tid); + + /* Get product and vendor names */ + productName = getPropertyString(usbDevice, CFSTR("USB Product Name")); + vendorName = getPropertyString(usbDevice, CFSTR("USB Vendor Name")); + + IOObjectRelease(usbDevice); + } + + /* Create device structure */ + device_t *device_info = g_new0(device_t, 1); + if (!device_info) + { + tio_error_print("Memory allocation failed for device_info"); + free(devicePath); + free(locationID); + free(productName); + free(vendorName); + IOObjectRelease(device); + continue; + } + + /* Populate device info */ + *device_info = (device_t) { + .path = devicePath, + .tid = g_strdup(tid), + .uptime = uptime, + .driver = g_strdup(vendorName), + .description = g_strdup(productName ?: vendorName ?: "") + }; + + /* Add to device list */ + device_list = g_list_append(device_list, device_info); + + /* Clean up */ + free(locationID); + free(productName); + free(vendorName); + IOObjectRelease(device); + } + + /* Clean up iterator */ + IOObjectRelease(iter); + + /* Check if device list is empty */ + if (!device_list) + { + return NULL; + } + + /* Sort device list by uptime */ + device_list = g_list_sort(device_list, compare_uptime); + + return device_list; +} + +#else + +GList *tty_search_for_serial_devices(void) +{ + DIR *dir; + char path[PATH_MAX] = {}; + double current_time, creation_time; + + search_reset(); + + // Open the directory containing serial devices + dir = opendir(PATH_SERIAL_DEVICES); + if (!dir) + { + return NULL; + } + + current_time = get_current_time(); + + // Iterate through each device in the subsystem directory + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) + { + // Skip . and .. entries + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + { + continue; + } + + // Construct the path to the TTY device file + snprintf(path, sizeof(path), PATH_SERIAL_DEVICES "/%s", entry->d_name); + + // Skip non serial devices + if (is_serial_device(path) == false) + { + continue; + } + + // Calculate uptime + creation_time = fs_get_creation_time(path); + double uptime = current_time - creation_time; + + // Do not add devices excluded by exclude patterns + if (match_patterns(path, option.exclude_devices)) + { + continue; + } + + // Allocate new device item for device list + device_t *device = g_new0(device_t, 1); + if (device == NULL) + { + continue; + } + + // Fill in device information + device->path = g_strdup(path); + device->tid = g_strdup(""); + device->uptime = uptime; + device->driver = g_strdup(""); + device->description = g_strdup(""); + + // Add device information to device list + device_list = g_list_append(device_list, device); + + // Update length of longest device name string + if (strlen(device->path) > listing_device_name_length_max) + { + listing_device_name_length_max = strlen(device->path); + } + } + + if (g_list_length(device_list) == 0) + { + // Return NULL if no serial devices found + return NULL; + } + + // Sort device list device with respect to uptime + device_list = g_list_sort(device_list, compare_uptime); + + closedir(dir); + + return device_list; +} + +#endif + void list_serial_devices(void) { tty_search_for_serial_devices(); if (g_list_length(device_list) > 0) { - printf("Device TID Uptime [s] Driver Description\n"); - printf("----------------- ---- ------------- ---------------- --------------------------\n"); + if (listing_device_name_length_max < 17) + { + listing_device_name_length_max = 17; + } + print_padded("Device", listing_device_name_length_max, ' '); + printf(" TID Uptime [s] Driver Description\n"); + print_padded("", listing_device_name_length_max, '-'); + printf(" ---- ------------- ---------------- --------------------------\n"); // Iterate through the device list GList *iter; @@ -1740,7 +2187,8 @@ void list_serial_devices(void) device_t *device = (device_t *) iter->data; // Print device information - printf("%-17s %4s %13.3f %-16s %s\n", device->path, device->tid, device->uptime, device->driver, device->description); + print_padded(device->path, listing_device_name_length_max, ' '); + printf(" %4s %13.3f %-16s %s\n", device->tid, device->uptime, device->driver, device->description); } printf("\n"); } @@ -1809,7 +2257,19 @@ void tty_search(void) return; case AUTO_CONNECT_DIRECT: - if (strlen(option.target) == TOPOLOGY_ID_SIZE) + if (config.device != NULL) + { + // Prioritize any device result of the configuration file first + // Meaning a pattern or section/group have been matched the cmdline target. + device_name = config.device; + } + else + { + // Fallback to use the target direcly + device_name = option.target; + } + + if (strlen(device_name) == TOPOLOGY_ID_SIZE) { // Potential topology ID detected -> trigger device search tty_search_for_serial_devices(); @@ -1819,7 +2279,7 @@ void tty_search(void) { device = (device_t *) iter->data; - if (strcmp(device->tid, option.target) == 0) + if (strcmp(device->tid, device_name) == 0) { // Topology ID match found -> use corresponding device name device_name = device->path; @@ -1828,9 +2288,6 @@ void tty_search(void) } } } - - // Fallback to using tty device provided via cmdline target - device_name = option.target; break; default: @@ -1900,8 +2357,21 @@ void tty_wait_for_device(void) } else if (status == -1) { +#if defined(__CYGWIN__) + // Happens when port unpluged + if (errno == EACCES) + { + goto error; + } +#elif defined(__APPLE__) + if (errno == EBADF) + { + break; // tty_disconnect() will be naturally triggered by atexit() + } +#else tio_error_printf("select() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); +#endif } } @@ -1914,7 +2384,7 @@ void tty_wait_for_device(void) } else if (last_errno != errno) { - tio_warning_printf("Could not open tty device (%s)", strerror(errno)); + tio_warning_printf("Could not open %s (%s)", device_name, strerror(errno)); tio_printf("Waiting for tty device.."); last_errno = errno; } @@ -1964,17 +2434,21 @@ void forward_to_tty(int fd, char output_char) int status; /* Map output character */ - if ((output_char == 127) && (map_o_del_bs)) + if ((output_char == 127) && (option.map_o_del_bs)) { output_char = '\b'; } - if ((output_char == '\r') && (map_o_cr_nl)) + if ((output_char == '\r') && (option.map_o_cr_nl)) { output_char = '\n'; } + if ((output_char == '\r') && (option.map_o_ign_cr)) + { + return; + } /* Map newline character */ - if ((output_char == '\n' || output_char == '\r') && (map_o_nl_crnl)) + if ((output_char == '\n' || output_char == '\r') && (option.map_o_nl_crnl)) { const char *crlf = "\r\n"; @@ -2000,8 +2474,12 @@ void forward_to_tty(int fd, char output_char) else { /* Send output to tty device */ - optional_local_echo(output_char); - if ((output_char == 0) && (map_o_nulbrk)) + if (option.input_mode != INPUT_MODE_LINE) + { + optional_local_echo(output_char); + } + + if ((output_char == 0) && (option.map_o_nulbrk)) { status = tcsendbreak(fd, 0); } @@ -2009,6 +2487,7 @@ void forward_to_tty(int fd, char output_char) { status = tty_write(fd, &output_char, 1); } + if (status < 0) { tio_warning_printf("Could not write to tty device"); @@ -2024,11 +2503,17 @@ void forward_to_tty(int fd, char output_char) { handle_hex_prompt(output_char); } - else + else if (option.input_mode == INPUT_MODE_NORMAL) { - if (option.input_mode == INPUT_MODE_NORMAL) + status = tty_write(device_fd, &output_char, 1); + if (status < 0) + { + tio_warning_printf("Could not write to tty device"); + } + else { optional_local_echo(output_char); + tx_total++; } } break; @@ -2045,13 +2530,10 @@ int tty_connect(void) int maxfd; /* Maximum file descriptor used */ char input_char, output_char; char input_buffer[BUFSIZ] = {}; - char line_buffer[BUFSIZ] = {}; static bool first = true; int status; bool do_timestamp = false; char* now = NULL; - unsigned int line_index = 0; - static char previous_char[2] = {}; struct timeval tval_before = {}, tval_now, tval_result; /* Open tty device */ @@ -2175,7 +2657,7 @@ int tty_connect(void) /* Manage script activation */ if (option.script_run != SCRIPT_RUN_NEVER) { - script_run(device_fd); + script_run(device_fd, NULL); if (option.script_run == SCRIPT_RUN_ONCE) { @@ -2189,6 +2671,15 @@ int tty_connect(void) exit(EXIT_SUCCESS); } + if (option.exec != NULL) + { + status = execute_shell_command(device_fd, option.exec); + exit(status); + } + + // Initialize readline like history + readline_init(); + /* Input loop */ while (true) { @@ -2305,6 +2796,7 @@ int tty_connect(void) if (first_) { // Do nothing + first_ = false; } else { @@ -2315,7 +2807,6 @@ int tty_connect(void) { log_putc('\n'); } - first_ = false; } } } @@ -2324,13 +2815,13 @@ int tty_connect(void) break; default: - tio_error_printf("Unknown outut mode"); + tio_error_printf("Unknown output mode"); exit(EXIT_FAILURE); break; } /* Convert MSB to LSB bit order */ - if (map_o_msblsb) + if (option.map_i_msb2lsb) { char ch = input_char; input_char = 0; @@ -2341,24 +2832,33 @@ int tty_connect(void) } /* Map input character */ - if ((input_char == '\n') && (map_i_nl_crnl) && (!map_o_msblsb)) + if ((input_char == '\n') && (option.map_i_nl_crnl) && (!option.map_i_msb2lsb)) { - print('\r'); - print('\n'); + printchar('\r'); + printchar('\n'); if (option.timestamp) { do_timestamp = true; } } - else if ((input_char == '\f') && (map_i_ff_escc) && (!map_o_msblsb)) + else if ((input_char == '\r') && (option.map_i_cr_crnl) && (!option.map_i_msb2lsb)) { - print('\e'); - print('c'); + printchar('\r'); + printchar('\n'); + if (option.timestamp) + { + do_timestamp = true; + } + } + else if ((input_char == '\f') && (option.map_i_ff_escc) && (!option.map_i_msb2lsb)) + { + printchar('\e'); + printchar('c'); } else { /* Print received tty character to stdout */ - print(input_char); + printchar(input_char); } /* Write to log */ @@ -2429,95 +2929,20 @@ int tty_connect(void) break; case INPUT_MODE_LINE: - switch (input_char) + if (input_char == '\r') { - case 27: // Escape - forward = false; - break; + // Carriage return + readline_input(input_char); - case '[': - if (previous_char[0] == 27) - { - forward = false; - } - break; - - case 'A': - case 'B': - case 'C': - case 'D': - if ((previous_char[1] == 27) && (previous_char[0] == '[')) - { - // Handle arrow keys - switch (input_char) - { - case 'A': // Up arrow - // Ignore - break; - case 'B': // Down arrow - // Ignore - break; - case 'C': // Right arrow - // Ignore - break; - case 'D': // Left arrow - // Ignore - break; - } - forward = false; - } - break; - - case '\b': - case 127: // Backspace - if (line_index) - { - printf("\b \b"); // Destructive backspace - line_index--; - } - forward = false; - break; - - case '\r': // Carriage return - if (option.local_echo == false) - { - // Delete line - int i = line_index; - while (i--) - { - printf("\b \b"); // Destructive backspace - } - } - else - { - // Preserve line, go to next line - putchar('\r'); - putchar('\n'); - } - - // Write buffered line to tty device - tty_write(device_fd, line_buffer, line_index); - tty_sync(device_fd); - line_index = 0; - break; - - default: - if (line_index < BUFSIZ) - { - putchar(input_char); - print_tainted_set(); - line_buffer[line_index++] = input_char; - } - else - { - tio_error_print("Input exceeds maximum line length. Truncating."); - } - forward = false; + // Write current line to tty device + char *rl_line = readline_get(); + tty_write(device_fd, rl_line, strlen(rl_line)); + } + else + { + readline_input(input_char); + forward = false; } - - // Save 2 latest stdin input characters - previous_char[1] = previous_char[0]; - previous_char[0] = input_char; break; default: @@ -2585,4 +3010,3 @@ error_read: error_open: return TIO_ERROR; } - diff --git a/src/tty.h b/src/tty.h index b142034..39c64af 100644 --- a/src/tty.h +++ b/src/tty.h @@ -29,6 +29,22 @@ #define TOPOLOGY_ID_SIZE 4 +typedef enum +{ + FLOW_NONE, + FLOW_HARD, + FLOW_SOFT, +} flow_t; + +typedef enum +{ + PARITY_NONE, + PARITY_ODD, + PARITY_EVEN, + PARITY_MARK, + PARITY_SPACE, +} parity_t; + typedef enum { AUTO_CONNECT_DIRECT, @@ -55,13 +71,11 @@ typedef struct extern const char *device_name; extern bool interactive_mode; -extern bool map_i_nl_cr; -extern bool map_i_cr_nl; -extern bool map_ign_cr; void stdout_configure(void); void stdin_configure(void); void tty_configure(void); +void tty_reconfigure(void); int tty_connect(void); void tty_wait_for_device(void); void list_serial_devices(void); diff --git a/src/version.h.in b/src/version.h.in new file mode 100644 index 0000000..459952d --- /dev/null +++ b/src/version.h.in @@ -0,0 +1,3 @@ +#pragma once + +#define VERSION "@VERSION@" diff --git a/src/xymodem.c b/src/xymodem.c index 83ac6dc..6bc3da3 100644 --- a/src/xymodem.c +++ b/src/xymodem.c @@ -7,30 +7,36 @@ * */ -#include -#include -#include -#include +#include #include #include -#include -#include +#include #include #include -#include -#include +#include #include "xymodem.h" #include "print.h" +#include "misc.h" #define SOH 0x01 #define STX 0x02 #define ACK 0x06 #define NAK 0x15 #define CAN 0x18 -#define EOT "\004" +#define EOT 0x04 + +#define SOH_STR "\001" +#define ACK_STR "\006" +#define NAK_STR "\025" +#define CAN_STR "\030" +#define EOT_STR "\004" #define OK 0 #define ERR (-1) +#define ERR_FATAL (-2) +#define USER_CAN (-5) + +#define RX_IGNORE 5 #define min(a, b) ((a) < (b) ? (a) : (b)) @@ -166,7 +172,7 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) while (seq) { if (key_hit) return ERR; - if (write(sio, EOT, 1) < 0) { + if (write(sio, EOT_STR, 1) < 0) { tio_error_print("Write EOT to serial failed"); return ERR; } @@ -285,7 +291,7 @@ static int xmodem(int sio, const void *data, size_t len) while (1) { if (key_hit) return ERR; - if (write(sio, EOT, 1) < 0) { + if (write(sio, EOT_STR, 1) < 0) { tio_error_print("Write EOT to serial failed"); return ERR; } @@ -306,6 +312,345 @@ static int xmodem(int sio, const void *data, size_t len) return 0; /* not reached */ } +int start_receive(int sio) +{ + int rc; + struct pollfd fds; + fds.events = POLLIN; + fds.fd = sio; + for (int n = 0; n < 20; n++) + { + /* Send the 'C' byte until the sender of the file responds with + something. The start character will be sent once a second for a number of + seconds. If nothing is received in that time then return false to indicate + that the transfer did not start. */ + rc = write(sio, "C", 1); + if (rc < 0) { + if (errno == EWOULDBLOCK) { + usleep(1000); + continue; + } + tio_error_print("Write packet to serial failed"); + return ERR; + } + /* Wait until data is available */ + rc = poll(&fds, 1, 3000); + if (rc < 0) + { + tio_error_print("%s", strerror(errno)); + return rc; + } + else if (rc > 0) + { + if (fds.revents & POLLIN) + { + return rc; + } + } + if (key_hit) + return USER_CAN; + } + return rc; +} + +uint16_t update_CRC(uint16_t crc, char data_char) +{ + uint8_t data = data_char; + crc = crc ^ ((uint16_t)data << 8); + for (int ix = 0; (ix < 8); ix++) + { + if (crc & 0x8000) + { + crc = (crc << 1) ^ 0x1021; + } + else + { + crc <<= 1; + } + } + return crc; +} + +int receive_packet(int sio, struct xpacket packet, int fd) +{ + char rxSeq1, rxSeq2 = 0; + char resp = 0; + uint16_t calcCrc = 0; + uint16_t rxCrc = 0; + int rc; + + struct pollfd fds; + fds.events = POLLIN; + fds.fd = sio; + + /* Read seq bytes*/ + rc = read_poll(sio, &rxSeq1, 1, 3000); + if (rc == 0) { + tio_error_print("Timeout waiting for first seq byte"); + return ERR; + } else if (rc < 0) { + tio_error_print("Error reading first seq byte") + return ERR_FATAL; + } + rc = read_poll(sio, &rxSeq2, 1, 3000); + if (rc == 0) { + tio_error_print("Timeout waiting for second seq byte"); + return ERR; + } else if (rc < 0) { + tio_error_print("Error reading second seq byte") + return ERR_FATAL; + } + if (key_hit) + return USER_CAN; + + /* Read packet Data */ + for (unsigned ix = 0; (ix < sizeof(packet.data)); ix++) + { + rc = read_poll(sio, &resp, 1, 3000); + /* If the read times out or fails then fail this packet. */ + if (rc == 0) + { + tio_error_print("Timeout waiting for next packet char"); + rc = write(sio, CAN_STR, 1); + if (rc < 0) { + tio_error_print("Write cancel packet to serial failed"); + return ERR_FATAL; + } + return ERR; + } else if (rc < 0) { + tio_error_print("Error reading next packet char") + rc = write(sio, CAN_STR, 1); + if (rc < 0) { + tio_error_print("Write cancel packet to serial failed"); + } + return ERR_FATAL; + } + packet.data[ix] = (uint8_t) resp; + calcCrc = update_CRC(calcCrc, resp); + if (key_hit) + return USER_CAN; + } + + /* Read CRC */ + rc = read_poll(sio, &resp, 1, 3000); + if (rc == 0) { + tio_error_print("Timeout waiting for first CRC byte"); + return ERR; + } else if (rc < 0) { + tio_error_print("Error reading first CRC byte") + return ERR_FATAL; + } + + uint8_t uresp = resp; + uint16_t uresp16 = uresp; + rxCrc = uresp16 << 8; + + rc = read_poll(sio, &resp, 1, 3000); + if (rc == 0) { + tio_error_print("Timeout waiting for second CRC byte"); + return ERR; + } else if (rc < 0) { + tio_error_print("Error reading second CRC byte") + return ERR_FATAL; + } + + uresp = resp; + uresp16 = uresp; + rxCrc |= uresp16; + + if (key_hit) + return USER_CAN; + + /* At this point in the code, there should not be anything in the receive buffer + because the sender has just sent a complete packet and is waiting on a response. */ + rc = poll(&fds, 1, 10); + if (rc < 0) + { + tio_error_print("%s", strerror(errno)); + tio_error_print("Poll check error after packet finish"); + rc = write(sio, CAN_STR, 1); + if (rc < 0) { + tio_error_print("Write cancel packet to serial failed"); + } + return ERR_FATAL; + } + else if (rc > 0) + { + if (fds.revents & POLLIN) + { + tio_error_print("RX sync error"); + char dummy = 0; + /* Drain buffer */ + while (read_poll(sio, &dummy, 1, 100) > 0) {} + return ERR; + } + } + + uint8_t tester = 0xff; + uint8_t seq1 = rxSeq1; + uint8_t seq2 = rxSeq2; + + if ((calcCrc == rxCrc) && (seq1 == packet.seq - 1) && ((seq1 ^ seq2) == tester)) + { + /* Resend of previously processed packet. */ + rc = write(sio, ACK_STR, 1); + if (rc < 0) { + tio_error_print("Write acknowlegdement packet to serial failed"); + return ERR_FATAL; + } + return RX_IGNORE; + } + else if ((calcCrc != rxCrc) || (seq1 != packet.seq) || ((seq1 ^ seq2) != tester)) + { + /* Fail if the CRC or sequence number is not correct or if the two received + sequence numbers are not the complement of one another. */ + tio_error_print("Bad CRC or sequence number"); + tio_debug_printf("CRC read: %u", rxCrc); + tio_debug_printf("CRC calculated: %u", calcCrc); + tio_debug_printf("Seq read: %hhu", rxSeq1); + tio_debug_printf("Seq should be: %hhu", packet.seq); + tio_debug_printf("inv seq: %hhu", rxSeq2); + return ERR; + } + else + { + /* The data is good. Process the packet then ACK it to the sender. */ + rc = write(fd, packet.data, sizeof(packet.data)); + if (rc < 0) + { + tio_error_print("Problem writing to file"); + rc = write(sio, CAN_STR, 1); + if (rc < 0) { + tio_error_print("Write cancel packet to serial failed"); + } + return ERR_FATAL; + } + rc = write(sio, ACK_STR, 1); + if (rc < 0) + { + tio_error_print("Write acknowlegdement packet to serial failed"); + return ERR_FATAL; + } + } + + return OK; +} + +int xmodem_receive(int sio, int fd) +{ + struct xpacket packet; + char resp = 0; + int rc; + bool complete = false; + char status; + + /* Drain pending characters from serial line.*/ + while(1) { + if (key_hit) + return -1; + rc = read_poll(sio, &resp, 1, 50); + if (rc == 0) { + if (resp == CAN) return ERR; + break; + } + else if (rc < 0) { + if (rc != USER_CAN) { + tio_error_print("Read sync from serial failed"); + } + return ERR; + } + } + + /* Always work with 128b packets */ + packet.seq = 1; + packet.type = SOH; + + /* Start Receive*/ + rc = start_receive(sio); + if (rc == 0) + { + tio_error_print("Timeout waiting for transfer to start"); + return ERR; + } else if (rc < 0) { + tio_error_print("Error starting XMODEM receive"); + return ERR; + } + + while (!complete) { + /* Poll for 1 new byte for 3 seconds */ + rc = read_poll(sio, &resp, 1, 3000); + if (rc == 0) { + tio_error_print("Timeout waiting for start of next packet"); + return ERR; + } else if (rc < 0) { + tio_error_print("Error reading start of next packet") + return ERR; + } + if (key_hit) + return USER_CAN; + + switch(resp) + { + case SOH: + /* Start of a packet */ + rc = receive_packet(sio, packet, fd); + if (rc == OK) { + packet.seq++; + status = '.'; + } else if (rc == ERR) { + rc = write(sio, NAK_STR, 1); + if (rc < 0) { + tio_error_print("Writing not acknowledge packet to serial failed"); + return ERR; + } + status = 'N'; + } else if (rc == ERR_FATAL) { + tio_error_print("Receive cancelled due to fatal error"); + return ERR; + } else if (rc == USER_CAN) { + rc = write(sio, CAN_STR, 1); + if (rc < 0) { + tio_error_print("Writing cancel to serial failed"); + return ERR; + } + return USER_CAN; + } else if (rc == RX_IGNORE) { + status = ':'; + } + break; + + case EOT: + /* End of Transfer */ + rc = write(sio, ACK_STR, 1); + if (rc < 0) + { + tio_error_print("Write acknowlegdement packet to serial failed"); + return ERR; + } + complete = true; + status = '\0'; + write(STDOUT_FILENO, "|\r\n", 3); + break; + + case CAN: + /* Cancel from sender */ + tio_error_print("Transmission cancelled from sender"); + return ERR; + break; + + default: + tio_error_print("Unexpected character received waiting for next packet"); + return ERR; + break; + } + + + /* Update "progress bar" */ + write(STDOUT_FILENO, &status, 1); + } + return OK; +} + int xymodem_send(int sio, const char *filename, modem_mode_t mode) { size_t len; @@ -343,7 +688,7 @@ int xymodem_send(int sio, const char *filename, modem_mode_t mode) rc = -1; if (strlen(filename) > 977) break; /* hdr block overrun */ - p = stpcpy(hdr, filename) + 1; + p = stpncpy(hdr, filename, 1024) + 1; p += sprintf(p, "%ld %lo %o", len, stat.st_mtime, stat.st_mode); if (xmodem_1k(sio, hdr, p - hdr, 0) < 0) break; /* hdr with metadata */ @@ -360,3 +705,35 @@ int xymodem_send(int sio, const char *filename, modem_mode_t mode) close(fd); return rc; } + +int xymodem_receive(int sio, const char *filename, modem_mode_t mode) +{ + int rc, fd; + + /* Create new file */ + fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0664); + if (fd < 0) { + tio_error_print("Could not open file"); + return ERR; + } + + /* Do transfer */ + key_hit = 0; + if (mode == XMODEM_1K) { + tio_error_print("Not supported"); + rc = -1; + } + else if (mode == XMODEM_CRC) { + rc = xmodem_receive(sio, fd); + } + else { + tio_error_print("Not supported"); + rc = -1; + } + key_hit = 0xff; + + /* Flush serial and release resources */ + tcflush(sio, TCIOFLUSH); + close(fd); + return rc; +} diff --git a/src/xymodem.h b/src/xymodem.h index ef7f9a3..1b46cd7 100644 --- a/src/xymodem.h +++ b/src/xymodem.h @@ -30,3 +30,5 @@ typedef enum { extern char key_hit; int xymodem_send(int sio, const char *filename, modem_mode_t mode); + +int xymodem_receive(int sio, const char *filename, modem_mode_t mode); diff --git a/subprojects/libinih.wrap b/subprojects/libinih.wrap deleted file mode 100644 index fc5f690..0000000 --- a/subprojects/libinih.wrap +++ /dev/null @@ -1,4 +0,0 @@ -[wrap-git] -directory=libinih -url=https://github.com/benhoyt/inih.git -revision=r58