diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 418ee82..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 - - 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-buildscript.sh b/.github/workflows/codeql-buildscript.sh new file mode 100644 index 0000000..720bd6b --- /dev/null +++ b/.github/workflows/codeql-buildscript.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +pip3 install meson -U +pip3 install ninja -U +sudo apt-get install -y liblua5.2-dev libglib2.0-dev +meson setup build +meson compile -C build diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..8a195f4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,126 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main", "master" ] + schedule: + - cron: '0 0 * * *' + pull_request: + branches: '*' + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - 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-22.04' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'cpp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: recursive + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + 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. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + queries: security-and-quality + + + # 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@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 + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + - run: | + ./.github/workflows/codeql-buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" + upload: false + id: step1 + + # Filter out rules with low severity or high false positve rate + # Also filter out warnings in third-party code + - name: Filter out unwanted errors and warnings + uses: advanced-security/filter-sarif@v1 + with: + patterns: | + -**:cpp/path-injection + -**:cpp/world-writable-file-creation + -**:cpp/poorly-documented-function + -**:cpp/potentially-dangerous-function + -**:cpp/use-of-goto + -**:cpp/integer-multiplication-cast-to-long + -**:cpp/comparison-with-wider-type + -**:cpp/leap-year/* + -**:cpp/ambiguously-signed-bit-field + -**:cpp/suspicious-pointer-scaling + -**:cpp/suspicious-pointer-scaling-void + -**:cpp/unsigned-comparison-zero + -**/cmake*/Modules/** + input: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif + output: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif + + - name: Upload CodeQL results to code scanning + 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@v4 + with: + name: codeql-results + path: ${{ steps.step1.outputs.sarif-output }} + retention-days: 5 + + - name: Fail if an error is found + run: | + ./.github/workflows/fail_on_error.py \ + ${{ steps.step1.outputs.sarif-output }}/cpp.sarif diff --git a/.github/workflows/fail_on_error.py b/.github/workflows/fail_on_error.py new file mode 100755 index 0000000..2979174 --- /dev/null +++ b/.github/workflows/fail_on_error.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +import json +import sys + +# Return whether SARIF file contains error-level results +def codeql_sarif_contain_error(filename): + with open(filename, 'r') as f: + s = json.load(f) + + for run in s.get('runs', []): + rules_metadata = run['tool']['driver']['rules'] + if not rules_metadata: + rules_metadata = run['tool']['extensions'][0]['rules'] + + for res in run.get('results', []): + if 'ruleIndex' in res: + rule_index = res['ruleIndex'] + elif 'rule' in res and 'index' in res['rule']: + rule_index = res['rule']['index'] + else: + continue + try: + rule_level = rules_metadata[rule_index]['defaultConfiguration']['level'] + except IndexError as e: + print(e, rule_index, len(rules_metadata)) + else: + if rule_level == 'error': + return True + return False + +if __name__ == "__main__": + if codeql_sarif_contain_error(sys.argv[1]): + sys.exit(1) 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/.gitignore b/.gitignore index 1eef64a..40120d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /build /subprojects/libinih *.swp +.cache 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 5795305..3754640 100644 --- a/AUTHORS +++ b/AUTHORS @@ -34,5 +34,37 @@ Yin Fengwei Liam Beguin Peter Collingbourne g0mb4 +ZeroMemoryEx on GitHub +George Joseph +Robert Snell +Rui Chen +Ralph Siemsen +Victor Oliveira +Attila Veghelyi +Vyacheslav Patkov +Bill Hass +Peter van Dijk +Braden Young +Wes Koerber +HiFiPhile +Paul Ruizendaal +Fredrik Svedberg +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/ChangeLog b/ChangeLog deleted file mode 100644 index c1ee251..0000000 --- a/ChangeLog +++ /dev/null @@ -1,1260 +0,0 @@ -=== tio v1.41 === - - - -Changes since tio v1.40: - - * Rename --hex-mode to --hexadecimal - - * Enable buffered writing - - Read block of bytes from input and process same block for output. This - will speed things up by reducing I/O overhead. - - * Enable buffered reading - - Read block of bytes from input and process byte by byte for output. This - will speed things up by reducing I/O overhead. - - * Refactoring - - * Cleanup stdout flushing - - Flushing is not needed since we disabled buffering of stdout. - - * Simplify stdout_configure() code - - * Simplify stdin_configure() code - - * Update man page - - * Update README - - - -Changes since tio v1.39: - - * Add config support for log-strip - - * Add config support for hex-mode - - * Rename --hex to --hex-mode - - * Fix completion for -e, --local-echo - - * Ignore newlines in hex output - - * Fix newline in warning_printf() - - * Fix ansi_printf_raw() in no color mode - - * Enter non-interactive mode when piping to tio - - Add support for a non interactive mode which allows other application to - pipe data to tio which then forwards the data to the connected serial - device. - - Non ineractive means that tio does not react to interactive key commands - in the incoming stream. This allows users to pipe binary data directly - to the connected serial device. - - Example use: - - $ cat commands.txt | tio /dev/ttyUSB0 - - * Also strip backspace from log - - To make log strip feature consistent so that we remove all unprintable - control characters and escape sequences. - - * Socket code cleanup - - * Cleanup man page - - * Rename --log-filename to --log-file - -Yin Fengwei: - - * Allow strip escape sequence characters from log file - - The log without escape key stripped is like: - - ^M[12:47:17] ACRN:\> - ^M[12:47:17] ACRN:\>lasdfjklsdjf - ^M - ^M[12:47:18] Error: Invalid command. - ^M[12:47:19] ACRN:\> - ^M[12:47:26] ACRN:\> - ^M[12:47:26] ACRN:\>sdafkljsdkaljfklsadjflksdjafjsda^H ^H^H... - ^M - ^M[12:47:31] Error: Invalid command. - - After strip escape key, the log is like: - - [12:49:18] ACRN:\> - [12:49:19] ACRN:\> - [12:49:19] ACRN:\>ls - - [12:49:19] Error: Invalid command. - [12:49:19] ACRN:\> - [12:49:19] ACRN:\>dfaslhj - - [12:49:24] Error: Invalid command. - - Beside escape key, it also handle backspace key as well. - - - -Changes since tio v1.38: - - * Improve key command response for local echo and timestamp - - * Fix invalid hex character error message - - * Make sure only matched config section is parsed - - * Add support for "disable" keyword in config file - - * Unify error message formating - - * Cleanup list devices code - - * Fix command-line tty-device|config parsing - - Allow user to add options on both sides of the provided config argument. - - For example: - - $ tio -b 9600 am64-evm -e - - Before, tio only allowed adding arguments after the config argument. - - Implemented as simple as possible by introducing two stage option parsing. - - * Update bash completion - - * Add support for IPv4 and IPv6 network sockets - - Add support for IPv4 and IPv6 network sockets via socket syntax - "inet:" and "inet6:" respectively. - - For example, to listen and redirect serial device I/O to a host bound - IPv4 socket simply do: - - $ tio /dev/ttyUSB0 --socket inet:4444 - - To connect do e.g.: - - $ nc 127.0.0.1 4444 - - Likewise, for IPv6 do: - - $ tio /dev/ttyUSB0 --socket inet6:4444 - - To connect do e.g.: - - $ nc ::1 4444 - - If port is 0 or no port is provided default port 3333 is used. - - * Fix tio deleting unix socket file - - If tio has a unix file socket open, a second tio instance of tio may - delete the socket file. This change fixes so that it will not be deleted - and tio will instead error and complain about conflicting socket file. - - * Rework color option - - Rework the color option to support setting ANSI color code values - ranging from 0..255 or "none" for no color or "list" to print a list of - available ANSI colors codes. - - Also, disables color when piping. - - * Remove print of hex mode status at startup - - * Remove newline option in hex mode - - * Fix configfile memory leaks - - * Remove command-line option inconsistencies - - Optional arguments, as parsed by the getopt_long mechanism, are - inherently inconsistent with how you define required arguments. - - To avoid confusion we decide to avoid this inconsistency by replacing - optional options with additional options with required argmuments. - - * Replace '1' with 'enable' in config files - - * Convert errors to warnings - -g0mb4: - - * Extended hexadecimal mode. - - While in hex mode (ctrl-t h) you can output hexadecimal values. - E.g.: to send 0x0A you have to type 0A (always 2 characters). - - Added option -x, --hex to start in hexadecimal mode. - - Added option --newline-in-hex to interpret newline characters in hex mode. - This is disabled by default, because, in my opinion, hex stream is - fundamentally different from text, so a "new line" is meaningless in this - context. - - - -Changes since tio v1.37: - - * Redirect error messages to stderr - - * Improve help and man page - - * Mention config file in --help - - * Fix running without config file - - * Fix config file error messages - - * Redirect error messages to stderr - - * Add repology packaging status - - * Fix parsing of default settings - - Default configuration file settings were not parsed in case a section - was matched. Now we make sure that the default (unnamed) settings are - always parsed. - - * Append to existing log file (no truncation) - - * Add socket info to show configuration - - * Print socket info at startup - - * Fix socket option parsing - -Peter Collingbourne: - - * Match user input against config section names if pattern matching was unsuccessful. - - This allows for better config file ergonomics if the user has a diverse - set of serial devices as the name does not need to be specified in - the config file twice. - - * Add support for external control via a Unix domain socket. - - This feature allows an external program to inject output into and - listen to input from a serial port via a Unix domain socket (path - specified via the -S/--socket command line flag, or the socket - config file option) while tio is running. This is useful for ad-hoc - scripting of serial port interactions while still permitting manual - control. Since many serial devices (at least on Linux) get confused - when opened by multiple processes, and most commands do not know - how to correctly open a serial device, this allows a more convenient - usage model than directly writing to the device node from an external - program. - - 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. - - Sockets remain open while the serial port is disconnected, and writes - will block. - - Example usage 1 (issue a command): - - echo command | nc -UN /path/to/socket > /dev/null - - Example usage 2 (use the expect command to script an interaction): - - #!/usr/bin/expect -f - - set timeout -1 - log_user 0 - - spawn nc -UN /path/to/socket - set uart $spawn_id - - send -i $uart "command1\n" - expect -i $uart "prompt> " - send -i $uart "command2\n" - expect -i $uart "prompt> " - -lexaone: - - * fix for using option 'log' without 'log-filename' in config file - - - -Changes since tio v1.36: - - * Make libinih a fallback dependency - - This means that in case meson does not find libinih it will - automatically clone libinih and include it in the build. - - The libinih library is reconfigured to be statically built so that no - shared object will be installed. - -Sylvain LAFRASSE: - - * Fix timestamp parsing in INI conf - - * Factorize timestamp parsing to be coherent with command line format in configuration file. - - - -Changes since tio v1.35: - - * Add support for defaults in config file - - If no section name is specified the configuration will be considered the - default one. - - This allows to set e.g. a default color code for sections which do not - configure a color code. - - * Handle SIGHUP - - Handle SIGHUP so that the registered exit handlers are called to restore - the terminal back to its orignal state. - - * Add color configuration support - - * Bypass unused result warnings - - * Force dependency on libinih - - Configuration file support is considered a mandatory feature. - - * Update headers - - * Update AUTHORS - - * Update man page - - * Move string_to_long() to misc.c - - * Update CircleCI config - - * Update tio gif - - * Update README - - * Update LICENSE date - - * Remove redundant COPYING file - -Liam Beguin: - - * Document configuration file options - - * Add support for a configuration file - - * misc: add _unused macro - - Some parameters are expected to be unused. - Add a basic macro to mute these compiler warnings. - - * options: expose string_to_long() - - Expose string_to_long() so that other source files can use it. - - - -Changes since tio v1.34: - - * Add support for automatically generated log filename - - Automatically generate log filename if none is provided. - - The auto generated file name is on the form: - "tio_DEVICE_YYYY-MM-DDTHH:MM:SS.log" - - * Add support for configurable timestamp format - - Also changes default timestamp format from ISO8601 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. - - This feature allows to easily add more timestamp formats in the future. - - * Reintroduce asm-generic/ioctls.h - - It is needed for ppc builds. - - * Add macro hack to workaround older buggy glibc - -Robey Pointer: - - * Add support for high bps on OS X - - - -Changes since tio v1.33: - - * Fix setspeed2 compilation - - * Only apply color formatting when using color option - - To help the color blind who may use custom terminal foreground / - background colors. - - * Update README - - * Add '-c, --color' option - - Allow user to select which ANSI color code to use to colorize the tio - text. To successfully set the color the color code must be in the range - 0..255. - - If color code is negative tio will print all available ANSI colors. - - The default color is changed to bold white to make tio defaults usable - for most users, including color blind users. - - * Fix setspeed2 check - - * Fix meson header check string - - * Reintroduce long timestamp format - - But make the timestamp format RFC3339 compliant instead. The RFC states: - - NOTE: ISO 8601 defines date and time separated by "T". - Applications using this syntax may choose, for the sake of - readability, to specify a full-date and full-time separated by - (say) a space character. - - This way we keep the information specified by ISO 8601 but make it more - human readable which is better for the console output. - - * Update version year - -Sylvain LAFRASSE: - - * Fix TTY device listing on Darwin. (#136) - - * Fix TCGETS2 search on Darwin. - - - -Changes since tio v1.32: - - * Show auto connect status in show configuration - - * Use '#pragma once' in all headers - - * Improve printed output - - Get rid of inconsistencies in the printed output (error printing, - colors, etc.). - - Prepare for user configurable color. - - * Rename option -i to -L - - * Shorten timestamp - - * Shorten timestamp description - - We do not need the date part of the timestamp. It simply takes up too - much precious line space. In case of logging to file, one can easily - conclude the date from the file date information. - - * Replace Travis with circleCI - - * Replace autotools with meson - - To introduce much simpler build configuration which is also easier to - maintain. - - * Add list serial devices feature - - For convenience, add a --list-devices option which lists the available - serial devices. - - * Cleanup: Use dot notation for default options struct - - * Update AUTHORS - - * Add command to show version - - The key sequence ctrl-t v will now show the version of tio. - - * Align format of timestamps - - * Add Sylvain as official co-maintainer - -Sylvain LAFRASSE: - - * Add '-t' option description for time stamping. - - * Add description for time stamping. - - * Resolved tio/tio#84: Added timestamps in log file if enabled. - -attila-v: - - * 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. - -Yin Fengwei: - - * Output newline on stdout with hex print mode - - This is to fix the issue #104. The timestamp will always be - printed at the beginning of line: - - [10:25:56] Switched to hexadecimal mode - 0d 0a 0d [10:25:57] 41 43 52 4e 3a 5c 3e 0d 0a 0d [10:25:58] 41 - - is changed to: - - [12:34:56] 45 72 72 6f 72 3a 20 49 6e 76 61 6c 69 64 20 - [12:34:56] 41 43 52 4e 3a 5c 3e - [12:34:56] 41 43 52 4e 3a 5c 3e - [12:34:57] 41 43 52 4e 3a 5c 3e 6c 73 - -Jakob Haufe: - - * Make comparison POSIX compliant - - String comparison with == is not POSIX compliant and can fail with e.g. - dash. - -Henrik Brix Andersen: - - * Add bash completion of tty devices. - - * Add -t/--timestamp to bash completion script. - -Henner Zeller: - - * Local echo: show character by character even if stdout buffered. - -Björn Stenberg: - - * Show error when failing to open a tty - -Alban Bedel: - - * Fix out of tree builds - - Out of tree builds are currently broken because $(top_srcdir)src/include - is not in the search path. In tree builds are working because autconf add - $(top_builddir)/src/include to the search path for the generated config.h. - As $(top_builddir) and $(top_srcdir) are identical during in tree builds - the search path still end up beeing somehow correct. - - To fix this add -I$(srcdir)/include to the CPPFLAGS in Makefile.am. - -Fabrice Fontaine: - - * src/setspeed2.c: fix redefinition of termio - - Include ioctls.h and termbits.h from asm-generic instead of asm to avoid - build failures. - -Erik Moqvist - - * Exit if output speed cannot be set. - -Lars Kellogg-Stedman: - - * fflush() after putchar() for print_hex and print_normal - - In order for local echo to work properly, we have to either call - fflush(stdout) after every character or just disable line buffering. - This change calls fflush() after putchar(). - - * Disable line buffering in stdout - - In order for local echo to work properly, we have to either call - fflush(stdout) after every character or just disable line buffering. - This change uses setbuf(stdout, NULL) to do the latter. - -George Stark: - - * dont show line state if ioctl failed - - * add serial lines manual control - -arichi: - - * Flush every local echo char - - Flush stdout at every char in case it - happens to be buffered. - -Mariusz Midor: - - * Newline: handle booth NL and CR - - Flag ONLCRNL expects code \n after press Enter, but on some systems \r is send instead. - - - -Changes since tio v1.31: - - * Update AUTHORS - - * Minor code style cleanups - - * Cleanup print macros - - * Flush output - - Make sure output is transmitted immediately by flushing the output. - -Robey Pointer: - - * add optional timestamps - - with "-t" or "C-t T", toggle a timestamp prefix to each line. - -Jakub Wilk: - - * Fix typos - -Sylvain Lafrasse: - - * Added macOS compatibility - - * Made O_NONBLOCK flag to open() call specific to macOS only. - - * Added macOS-related details. - - * Added O_NONBLOCK flag to open() call for macOS (10.13.6) compatibility. - - - -Changes since tio v1.30: - - * Update date - - * Update AUTHORS - -Henner Zeller: - - * Clarify the input/output variable names (No-op change) - - * Organize options the same sequence they are mentioned in cmdline help. - - * Update README. - - * Map CR->NL locally on output instead of using tio.c_oflag |= OCRNL. - - This mostly is intended to have local echo output exactly what is sent - to the remote endpoint. - A nice side-effect is, that it also fixes tty-implementations, that can't - deal with the OCRNL flag on tio.c_oflag. - - * Provide local-echo option. - - Can be switched on with -e on the command line. - Can be toggled with Ctrl t e while program is running. - - * Write to logfile as soon as we have the data, don't buffer. - - Logfiles are important to see what happened, in particular if something - unexpected happened; so we want to make sure that the logfile is flushed - to disk. - - Before this change, the logfile was typically written at the end in - a large chunk as the default (large) buffering applied. Now, characters are - written out ASAP, so it is possible to get a live-view with a - tail -f - - - -Changes since tio v1.29: - - * Update README - - * Update man page and bash completion - - * Update AUTHORS - -qianfan Zhao: - - * ONLCRNL: change the method to map NL to CR-NL - - - -Changes since tio v1.28: - - * Add mapping flags INLCRNL and ODELBS - - The following new mapping flags are added: - - INLCRNL: Map NL to CR-NL on input. - ODELBS: Map DEL to BS on output. - - Flags requested and tested by Jan Ciger (janoc). - - - -Changes since tio v1.27: - - * Update README - - * Update AUTHORS - - * Add snap status to README.md - - * Add README.md to prettify GitHub page - - * Add missing header - -Petr Vaněk: - - * Add missing header file under musl-libc - - Musl's inclusion tree slightly differs from glibc, therefore TCGETS2 is - not reachable through sys/ioctl.h, so asm/ioctls.h needs to be included - too. - -Jakub Wilk: - - * Fix grammar and typos - - - -Changes since tio v1.26: - - * Update man page - - * Add support for setting non-standard baudrates - - Support for non-standard baudrate settings will be automatically enabled - if the termios2 interface is detected available. However, to play it - safe, the old and widely supported termios interface will still be used - when setting standard baudrates. - - * Cleanup - - * Update AUTHORS - - - -Changes since tio v1.25: - - * Reconfigure stdin - - Make stdin behave more raw'ish. In particular, don't - translate CR -> NL on input. - - * Add special character map feature - - Add a --map option which allows mapping special characters, in particular CR and - NL characters which are used in various combinations on various platforms. - - * Cleanup - - * Update AUTHORS - - * Update README - - * Mention website - - * Update man page - - - -Changes since tio v1.24: - - * Fix error applying new stdout settings - - On Fedora 26 tio will quit with the following error message: - - "Error: Could not apply new stdout settings (Invalid argument)" - - In case of Fedora, it turns out that the new stdout settings used are a - bit too aggressive because an empty termios structure is used. To remedy - this we reuse the existing stdout settings and only reconfigure the - specific options we need to make a "raw" stdout configuration. - - * Remove unused pkgconfig in configure - - * Code cleanup - - Remove unused variable. - - - -Changes since tio v1.23: - - * Optimize clear screen command - - Replaced system call with inline ANSI/VT100 clear screen code sequence - - * Fix bash completion installation - - Fixed the configure script to avoid that the bash completion script gets - installed outside of the prefix location. The default install location - is now $prefix/share/bash-completion/completions. - - Use the configure option '--with-bash-completion-dir=PATH' if you need - to install the bash completion script elsewhere. - -Jakub Wilk: - - * Add missing commas in conditional sentences - - - -Changes since tio v1.22: - - * Update copyright headers - -Jakub Wilk: - - * Fix typos - - - -Changes since tio v1.21: - - * Update man page date - - * Update copyright year - - * Code cleanup - - * Update README and man page - - - -Changes since tio v1.20: - - * Add support for hexadecimal mode - - A new key command 'ctrl-t h' is introduced which toggles between - hexadecimal mode and normal mode. When in hexadecimal mode data received - will be printed in hexadecimal. - - * Do not distribute src/bash_completion/tio - - Since the bash completion tio script is now autogenerated from tio.in it - should not be distributed in the tarball. - - * Add missing forward flag - - * Update AUTHORS file - -Adam Borowski: - - * 'ctrl-t b' to send serial break. - -Jakub Wilk: - - * Removed git commit references from ChangeLog - - ChangeLog is primary useful for users who don't have the git repository - at hand. - - Replace git commit references with version numbers; or if the change - only cleans up another change with no release in between, remove the - changelog item completely. - - - -Changes since tio v1.19: - - * Added more error handling of terminal calls - - Also removed duplicate terminal flushing calls. - - * Revert "Added support for non-standard baud rates" - - This reverts a change made in v1.18. - - Reverting because supporting non-standard or arbitrary baud rates is - troublesome because the c library provides no means of doing so and even - if bare metal linux kernel interface is used it will not work on all - Linux kernels version. - - - -Changes since tio v1.18: - - * Rearranged key commands - - Rearranged the key commands: - - ctrl-t c (clear screen) is now - ctrl-t l which is similar to the well known shell ctrl-l - - ctrl-t i (show settings information) is now - ctrl-t c (show configuration) - - Updated man page accordingly. - - * Added "ctrl-t c" key command to clear screen - - - -Changes since tio v1.17: - - * Updated man page - - * Added support for non-standard baud rates - - Only enabled when possible, that is, when the BOTHER definition is - available. - - It is untested but it should work as described here: - https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=683826 - - Some Cypress USB<->serial devices supposedly supports arbitrary speeds. - - * Generate baudrate switch cases based on detection - - Support a single source of baud rate configuration as discussed in - https://github.com/tio/tio/issues/45 . - - To do so, autogeneration of the switch cases which do the baud rate - option value check and configuration/conversion in tty_configure() is - introduced via a single macro. - - Just to be safe, this change also enables configure detection of all - baud rates, including the ones previously assumed supported by most/all - systems (POSIX). - - * Minor cleanup - - * Exit when not a tty device in autoconnect mode - -Jakub Wilk: - - * Added non-standard baud rates that are defined on FreeBSD - - * Capitalized "GitHub" in README - - - -Changes since tio v1.16: - - * Compacted tty_configure() a bit - - * Fixed automatic baud rate enablement - - * Minor cleanups - - * Added autodetection of available baud rates - - Various platforms support different baud rates. - - To avoid adding platform specific handling generic baud rate detection - tests are introduced in the configure script. Successfully detected baud - rates are automatically enabled. This applies to both the C code and the - bash completion script. - - Note: - Baud rates below 57600 are defined by POSIX-1 and supported by most - platforms so only baud rate 57600 and above are tested. - - * Updated bash-completion - - * Fixed printf() format type - - * Added Travis build configuration - -Jakub Wilk: - - * Generated bash completion at configure time - - * Reduce code duplication in baud rate detection - - * Add support for baud rates 200 and 1800 - - * Fixed baudrate type - - - -Changes since tio v1.15: - - * Updated man page - - * Updated README - - * Removed obsolete packaging files - - * Removed use of deprecated bzero() - - - -Changes since tio v1.14: - - * Removed + to remove potential confusion - - * Added input digit checks - - * Fixed license string - - * Introduced tty_configure() - - Moved tty configuration actions to tty_configure() in tty.c. This way - options.c is strictly about parsing options nothing else. - - * Function names cleanup - - * Updated AUTHORS file - - Added Nick who created the new tio package for Arch Linux. - - * Fixed tx/rx counters type - -Jakob Haufe: - - * Include config.h before standard headers - - Large file support was meant to be enabled in v1.11. - This change enables it for real. - - - -Changes since tio v1.13: - - * Fixed tio_printf macro - - * Fixed launch hints - - Fixed launch hints not being printed in no autoconnect mode. - - * Added 'ctrl-t ?' to list available commands - - * Fixed log mechanism - - To avoid echoing only log what is received from tty device. - - * Improved tio output - - Added titles and indentation to commands output for clearer separation - when firing commands repeatedly. - - Also added print of tio version and quit command hint at launch. - - * Cleaned up tio print mechanism - -Jakub Wilk: - - * Fixed grammar - - "allow" is a transitive verb, which requires an object, - so "allow to " is ungrammatical. - - * Fixed typo - - - -Changes since tio v1.12: - - * Fixed some error prints - - * Fixed error printing for no autoconnect mode - - Always print errors but only print silent errors when in no autoconnect - mode. - - * Added key command for showing session settings - - A new key command "ctrl-t i" is added to allow the user to display the - various session settings information (baudrate, databits, log file, etc.). - - This is useful in case you have a running session but have forgotten - what the settings are. - - - -Changes since tio v1.11: - - * Consolidated command key handling - - * Moved delay mechanism into separate function - - * Retired obsolete usleep() - - Replaced with nanosleep() - - * Added simple tx/rx statistics command (ctrl-t s) - - 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 - characters. - - * Further simplification of key handling - - Changed so that the "ctrl-t ctrl-t" sequence is now simply "ctrl-t t" to - send the ctrl-t key code. This is inspired by screen which does similar - to send its command key code (ctrl-a a). - - This change also eases adding new key commands if needed. - - Updated man page accordingly. - - * Cleaned up and simplified key handling - -Jakub Wilk: - - * Insert output delay only if something was output - - - -Changes since tio v1.10: - - * Enabled large file support (LFS) - - Added autotools AC_SYS_LARGEFILE to support 64 bit file size handling. - - * Updated tio title - - - -Changes since tio v1.9: - - * Introduced lock on device file - - Tio will now test for and obtain an advisory lock on the tty device file - to prevent starting multiple sessions on the same tty device. - - * Updated AUTHORS - -Jakub Wilk: - - * Treat EOF on stdin as error - - - -Changes since tio v1.8: - - * Cleanup of error handling - - Introduced consistent way of handling errors and printing error messages. - - Also upgraded some warnings to errors. - - * Updated localtime() error message - - * Cleanup - -Jakub Wilk: - - * Fix error handling for select() - - Previously the error handling code for select() was unreachable. - - * Removed unneeded quotes from AM_CFLAGS - - * Expanded tabs - - * Fixed setting "tainted" - - Set "tainted" if and only if any character was read from the device. - - Ctrl-t is no longer sent to the device on exit, so the trick to avoid - its echo is not necessary. - - Characters read from stdin don't directly affect output, so they - shouldn't enable "tainted". - - * Used \r in color_printf() - - \033[300D is an unusual way to move the cursor back to column 1. - Use straightforward \r instead. - - * Added missing \r\n to warning messages - - \n alone is not enough, because the terminal is in raw mode. - - - -Changes since tio v1.7: - - * Fixed enablement of compiler warnings - - * Fixed log_open() prototype - - * Fixed index error wrt ctrl-t detection - - * Fixed handling of ctrl-t - - Before, when exercising the quit key sequence (ctrl-t + q) the ctrl-t code - (0x14) would be sent. - - This is now fixed so that it is not sent. - - However, in case it is needed to send ctrl-t to the device it is possible by - simply repeating the ctrl-t. - - Meaning, ctrl-t + ctrl-t = ctrl-t sent to device. - - * Improved error handling - - Fixes a memory leak and avoids aggressive busy looping when problems - accessing tty device. - - * Removed redundant log_close() call - - * Enabled compiler warnings - -Jakub Wilk: - - * Stopped copying arguments to fixed-size buffers - - Don't needlessly copy command-line arguments into fixed-size buffers. - - Previously the program crashed if an overlong pathname was provided on - the command line. Also, some systems (such as GNU Hurd) don't define - MAXPATHLEN at all. - - * Added const to log_open() prototype - - * Completed the ^g to ^t transition - - In v1.7 the escape key was changed from ^g to ^t, but some - code and comments still referred to the old key. - - * Used HTTPS for tio.github.io - - * Man page beautification - - * Bumped date in man page - - * Improve man page formatting - - Use regular font for metacharacters such as "[]", "," or "|"; - use italic font for metavariables. - - * Fixed hyphen vs minus vs em-dash confusion in man page - - - prints as hyphen; - \- prints as minus sign; - \em prints as em-dash. - - - -Changes since tio v1.6: - - * Changed escape key from ^g to ^t - - After renaming to "tio" it makes sense to change the escape key - accordingly. Hence, the new escape key is ^t. - - Meaning, in session, its now ctrl-t + q to quit. - -Jakub Wilk: - - * Fixed silly "tio or tio" in man page - - * Fixed typo - 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 new file mode 100644 index 0000000..4289610 --- /dev/null +++ b/NEWS @@ -0,0 +1,2510 @@ +=== tio v3.9 (2025-04-13) === + + + +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` + + + +Changes since tio v2.8: + + * Simplify lua line manipulation API + + Collapses lua high(), low(), toggle(), config_high(), config_low(), + config_apply() into one simple function: + + set{=, ...} + + Line can be any of DTR, RTS, CTS, DSR, CD, RI. + + State is high, low, or toggle. + + Example: + script = set{DTR=high, RTS=low}; msleep(100); set{DTR=low, RTS=high}; msleep(100); set{RTS=low} + + Notice the use of {} instad of () when calling the set function. This is + required to pass parameters by name in lua. + + * Disable DEC Special Graphics at exit if vt100 + + If a vt100 terminal receives the Shift In character '\016' it will + enable the 7 bit DEC Special Graphics character set used for line drawing. + + For most users this can happen due to line noise from the tty device and + will likely mess up your terminal even after tio exits. + + To better handle this we want to make sure that tio disables this mode + by sending the Shift Out character '\017' at exit. + + This mechanism will only activate if environment variable TERM assumes + value "vt100". + + * Add hexN output mode + + Adds support for hexN mode where N is a number in the range 1 to 4096 + which defines how many hex values will be printed before a line break. + + In short, it defines the width of the hex output. + + In this mode, if timestamps are enabled they will be added to each hex + line. + + * Rename sub-config to profile + + Because better naming. + + * Use lua io.write() instead of print() + + io.write() gives better output control as print() is hardcoded to always + print a newline. + + * Add new ways to manage serial devices + + * Rename --list-devices to --list + + * Rename --no-autoconnect to --no-reconnect + + * Switch -l and -L options + + * -l now lists available serial devices + + * -L enables log to file + + * Add option --auto-connect + + * Supported strategies: + + * "new" - Waits to connect first new appearing serial device + + * "latest" - Connects to latest registered serial device + + * "direct" - Connect directly to specified serial device (default) + + * Add options to exclude serial devices from auto connect strategy by + pattern + + * Supported exclude options: + + * --exclude-devices + + Example: '--exclude-devices "/dev/ttyUSB2,/dev/ttyS?"' + + * --exclude-drivers + + Example: '--exclude-drivers "cdc_acm"' + + * --exclude-tids + + Example: '--exclude-tids "yW07,bCC2"' + + * Patterns support '*' and '?' + + * Connect to same port/device combination via unique topology ID (TID) + + * Topology ID is a 4 digit base62 encoded hash of a device topology + string coming from the Linux kernel. This means that whenever you + plug in the same e.g. USB serial port device to the same USB hub + port connected via the exact same hub topology all the way to your + computer, you will get the same unique TID. + + * Useful for stable reconnections when serial device has no serial + device by ID + + * For now, only tested on Linux. + + * Reworked and improved listing of serial devices to show serial devices: + + * By device + + * Including TID, uptime, driver, and description. + + * Sorted by uptime (newest device listed last) + + * By unique topology ID + + * By ID + + * By path + + * Add script interface 'list = tty_search()' for searching for serial + devices. + + * Clean up timestamp enum definition + + * Add missing options to show configuration + + * Update description of mute option + + * Add lua read_string() function + + * Don't forget to log output in lua expect() + + * Generalize automatic login example for Linux + + * Fix log output in hex output mode + + * Add timeout based timestamps in hex output mode + + This change reintroduces timestamping in hex output mode but based on + timeout instead of new lines which made no sense. This means that + timestamps will only be printed when timeout time has elapsed with no + output activity from serial device. + + Adds option --timestamp-timeout for setting the timeout value in + milliseconds. + + Defaults to 200 ms. + + * Improve switched messages + + * Extend lua expect() to also return matched string + + * Add automatic login script example + + * Organize examples directory + + * Introduce basic line input mode + + * Cleanup global variable name shadowing + +Davis C: + + * Updated login example with new expect logic + + * Reset buffer size at start of expect + + * Return 1 when `expect` matches + + + +Changes since tio v2.7: + + * Rework resolve_config_file() + + * Rework line_pulse_duration_option_parse() + + Introduce proper sscanf() checks. + + * Rework rs485_parse_config() + + Introduce proper sscanf() checks. + + * Clean up file descriptor name shadowing + + * Add missing header guard + + * Upgrade inih subproject + + * Remove options --response-wait, --response-timeout + + Remove options and rework input handling so it is possible to do the + same thing but via script which is much more flexible. + + These options were always a bit of a hardcoded solution. With the new + script expect feature we can wait for any type of response. + + For example, pipe command to serial device and wait for line response within 1 second: + + $ echo "*IDN?" | tio /dev/ttyACM0 --script "expect('\r\n', 1000)" --mute + + * Add lua exit(code) + + * Add timeout feature to expect() + + * Add lua expect(string) + + Add simple expect functionality. + + The expect(string) function will wait for input from the tty device and + only return when there is a string match. Regular expressions are + supported. + + Example: + + script = expect('password:'); send('my_password\n') + + * Add lua send(string) + + * Add lua modem_send(file,protocol) + + * Fix xymodem error messages + + * Rework x/y-modem transfer command + + Remove ctrl-t X option and instead introduce submenu to ctrl-t x option + for picking which xmodem protocol to use. + + * Update README + + * Cleanup options + + * Add independent input 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 behavior as -x, --hexadecimal use the following + configuration: + + input-mode = hex + output-mode = hex + + * 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 which makes it impossible to switch two or more tty + lines simultaneously. + + To help solve this timing issue we introduce a tty line state + configuration API which can be used instead of using + high()/low(). + + Using config_low(line) and config_high(line) one can set up a new line + state configuration for multiple lines and then use config_apply() to + finally apply the configuration. This will result in only one system + call to instruct the serial port drive to switch all the configured line + states which should help ensure that the lines are switched + simultaneously. + + Example: + + script = config_high(DTR); config_low(RTS); config_apply() + + * Add ONULBRK mapping flag + + Add ONULBRK mapping to map nul (zero) to send break signal on output. + + This is useful if one needs to e.g. send the break signal to the tty + device when connected via socket. + + * Add --log-directory option + + For specifying directory path in which to save automatically named log + files. + + * Add Lua scripting feature + + Add support for running Lua scripts that can manipulate the tty control + lines. Script is activated automatically on connect or manually via in + session key command. + + 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. + + * Invert line states to reflect true electrical level + + * Add support for disabling prefix key handling + + To disable prefix key input handing simply set prefix-ctrl-key to + 'none'. + + Based on original patch from Sebastian Krahmer. + + * Add meson man pages install option + + Defaults to installing man pages. + +HiFiPhile: + + * Poll on serial port read instead of delay. + + * Add Xmodem-CRC support. + + * CYGWIN: Fix port auto connection. + +Mingjie Shen: + + * Check return values of sscanf() + + Failing to check that a call to 'sscanf' actually writes to an output + variable can lead to unexpected behavior at reading time. + +Jakob Haufe: + + * Support NO_COLOR env variable as per no-color.org + + * Fix troff warning + + .eo/.ec sections seemingly need explicit empty lines using .sp + + Otherwise, troff complains: + + troff::535: warning: expected numeric expression, got '\' + troff::538: warning: expected numeric expression, got '\' + troff::541: warning: expected numeric expression, got '\' + +Fredrik Svedberg: + + * Add map FF to ESC-c on input + + Added map of form feed to ESC-c on input for terminals that + do not clear screen on ^L but do on ESC-c. + +Brian: + + * Add CodeQL Workflow for Code Security Analysis + +Sylvain LAFRASSE: + + * Fix double call of tty_disconnect() on macOS/Darwin. + + + +Changes since tio v2.6: + +Paul Ruizendaal: + + * Add xmodem and ymodem file send support + +HiFiPhile: + + * tty_stdin_input_thread(): write to pipe only if byte_count > 0. + + * Ignore EINTR error. + + * CYGWIN: Add support for "COM*" naming. + +Wes Koerber: + + * chore: reorder log-strip and log-append + + reorder to maintain consistency with documentation + + * chore: update readme, bash completion, man page + + * fix: support --log-append in cli options + + + +Changes since tio v2.5: + + * Remove warning when using pattern option + + * Add --log-append option + + Add --log-append option which makes tio append to any existing log file. + + 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. + + * Update man page + + * Update README + + * Fix line termination for response wait feature + + The response wait feature waited for a line response, a string + terminated with either CR or NL. However, some devices may send a CR and + then their line content and then NL. This means tio will quit before + receiving and printing the line response. To solve this we simply ignore + the CR character and only consider lines terminated with a NL character. + + This should work for all devices as lines are AFAIK always terminated + with either CRNL or a NL. + + * Update tty device listing configuration + + Cleanup and add FreeBSD tty device listing support. + +Braden Young: + + * Move map variables to tty to keep them all in one spot + + * Configure socket mapping flags from tty parsing logic. Remove duplicate parsing logic in socket + + * Support input mapping modes for sockets + +Josh Soref: + + * Various spelling fixes + +Peter van Dijk: + + * avoid "warning: unused parameter" on setspeed stub + + * use right /dev/ path on Haiku + +Bill Hass: + + * Update README with details on snap confinement + + + +Changes since tio v2.4: + + * Update configuration file documentation + + Rename .tiorc to .tioconfig, tiorc to config, etc. + + * Add support for $HOME/.tioconfig + + Replaces what used to be $HOME/.tiorc + + * Fix double prefix key regression + +Vyacheslav Patkov: + + * Better error checking in config file, rename the file + + Accept "true", "enable", "on", "yes", "1" as true values, their + counterparts as false ones. Check integer values for errors and range. + Warn about ignored (e.g. misspelled) options. + + Check getenv() return value for NULL. + + Rename "tiorc" to "config", as it's a static INI file, not an executable + "run commands". + + + +Changes since tio v2.3: + + * Add threaded input handling + + To make tio more responsive to quit and I/O flush key command when main I/O + thread is blocked on output. + + * Fix so that is it possible to quit tio in tio etc. + + Fix regression so that it is possible to send the prefix key code to the + remote tio session without local tio session reacting to same key code + (quitting etc.). + + * Add key command to toggle log on/off + + Add key command 'ctrl-t f' which will toggle log on/off. + + If no log filename has been specified via the 'log-filename' option then + tio will automatically generate a new log filename every time the log + feature is toggled on. Meaning, when toggled multiple times, multiple + log files will be generated. + + However, if a log filename has been specified, tio will only write and + append to that same file. + + + +Changes since tio v2.2: + + * Add mute feature + + This will make tio go fully silent and not print anything. + + * Rename config variable 'tty' to 'device' + + * Deprecate tty config keyword but keep it around for now + + * Update show config + + * Update example tiorc + + + +Changes since tio v2.1: + + * Add shell completion of sub-configuration names + + Does not work with sub configuration names that contains one or more + white spaces. + + * Beautify help + + * Fix error message + + * Simplify configfile implementation + + + +Changes since tio v2.0: + + * Fix output line delay + + Apply output line delay on lines ending with \n. + + On most systems lines ends with \n or \r\n. + + * Do not print timestamps in hex mode + + * Improve input mechanism in hex mode + + Print the 2 character hex code that you input in hex mode but then + delete it before sending. This way it is easier to keep track of what + you are inputting. It basically mimics the ctrl-shift-u input mechanism + that is used to input unicode. + + * Add support for sending prefix character to serial device + + Do so by inputting prefix key twice, e.g. input ctrl-t ctrl-t to send + ctrl-t character to serial device. + + * Clean up indentation + + * Update example tiorc + +Attila Veghelyi: + + * Add bit reverse order feature + + + +Changes since tio v1.47: + + * Handle stale unix socket file + + Delete existing unix socket file if it is tested to be stale, meaning no + one is listening on it. + + * Add visual or audible alert support on connect/disconnect + + The feature is detailed via the following option: + + --alert none|bell|blink + + Set alert action on connect/disconnect. + + It will sound the bell once or blink once on successful connect. + Likewise it will sound the bell twice or blink twice on disconnect. + + Default value is "none" for no alert. + + * Add experimental RS-485 support + + Many modern RS-485 serial devices such as the ones from FTDI already + operate in RS-485 mode by default and will work with tio out of the box. + However, there are some RS-232/485 devices which need to be switched + from e.g. RS-232 to RS-485 mode to operate accordingly on the physical + level. + + This commit implements the switching mechanism and interface required to + enable RS-485 mode. It only works on Linux and with serial devices which + use device drivers that support the Linux RS-485 control interface. + + The RS-485 feature is detailed via the following options: + + --rs-485 Enable RS-485 mode + --rs-485-config Set RS-485 configuration + + Set the RS-485 configuration using the following key or key value pair + format in the configuration field: + + RTS_ON_SEND=value Set logical level (0 or 1) for RTS pin when sending + RTS_AFTER_SEND=value Set logical level (0 or 1) for RTS pin after sending + RTS_DELAY_BEFORE_SEND=value Set RTS delay (ms) before sending + RTS_DELAY_AFTER_SEND=value Set RTS delay (ms) after sending + RX_DURING_TX Receive data even while sending data + + If defining more than one key or key value pair, they must be comma + separated. + + Example use: + + $ tio /dev/ttyUSB0 --rs-485 --rs-r485-config=RTS_DELAY_AFTER_SEND=50,RX_DURING_TX + + * Add line response feature + + Add a simple line response feature to make it possible to send e.g. a + command string to your serial device and easily receive and parse a line + response. + + This is a convenience feature for simple request/response interaction + based on lines. For more advanced interaction the socket feature should + be used instead. + + The line response feature is detailed via the following options: + + -r, --response-wait + + Wait for line response then quit. A line is considered any string ending + with either CR or NL character. If no line is received tio will quit + after response timeout. + + Any tio text is automatically muted when piping a string to tio while in + response mode to make it easy to parse the response. + + --response-timeout + + Set timeout [ms] of line response (default: 100). + + Example: + + Sending a string (SCPI command) to a test instrument (Korad PSU) and + print line response: + + $ echo "*IDN?" | tio /dev/ttyACM0 --response-wait + KORAD KD3305P V4.2 SN:32477045 + + * Fix potential sscanf() overflow + + * Only print version on '--version' + + * Remove duplicate show config entry of DTR pulse duration + + * Remove MacPorts instructions + + Remove instructions for MacPorts because the port has no maintainer and + the port build definition is broken (missing dependency on libinih etc.). + + It is recommended to use brew instead. + +Peter Collingbourne: + + * Ignore SIGPIPE signals + + If the remote end of a socket is closed between when an input character + is received from the serial port and when it is written to the socket, + tio will receive a SIGPIPE signal when writing the character to the + socket, which will terminate the program. To prevent this, ignore the + signal, which will cause write(2) to return -EPIPE, causing tio to close + the socket. + + + +Changes since tio v1.46: + + * Enable log feature when using --log-filename + + No reason to not assume that the user wants to enable log when the + --log-filename is used. This way uses can skip the use of --log to + enable log. + + * Enable line buffering of log + + Replace flushing/writing of log at every log write operation with line + buffering, meaning log will be written line by line to make it more I/O + friendly but still update frequently. + + * Avoid invalid hex character messages when switching hex mode + + * Force flushing of log writes + + * Renamed tty_flush() to tty_sync() + + * Fix sync output to serial port + + Using fsync() on filedescriptors for serial ports can not be relied on. + Add use of tcdrain() to make sure data has been written by the serial + port before proceeding. + + This fixes a problem with tio sometimes not writing piped input data to + the serial port before exiting which results in the pending writes being + cancelled / flushed. + + * Clean up tty_flush() + + * Force frequent sync on tty_flush() + + * Update README + + * Update example tiorc + + * Quit from non-interactive mode using ctrl-c + + When piping to tio it will automatically enter "non-interactive" mode + which means it will not react to any input key sequences but simple read + the input stream and write it to the tty device. + + This also means that ctrl-t q can not be used to quit and so tio would + hang forever when used in non-interactive mode. + + This change allows to send the standard termination signal by pressing + ctrl-c on tio in non-interactive mode to make it quit. + + * Make sure we flush output buffer to tty when piping to tio + + * Do not return false read error when piping to tio + + * Show error message when reading port settings fail + +Victor Oliveira + + * add MacPorts install instructions + + + +Changes since tio v1.45: + + * Rework toggle and pulse feature to support all lines + + Replace existing toggle and pulse key commands with the following + generalized key commands which allows to toggle or pulse all serial port + lines: + + ctrl-t g Toggle serial port line + ctrl-t p Pulse serial port line + + When used, user will be asked which serial line to toggle or pulse. + + Also introduce --line-pulse-duration option for setting specific pulse + duration in milliseconds for each serial line using a key value pair + format. Each key represents a serial line. The following keys are + available: DTR, RTS, CTS, DSR, DCD, RI. + + Example: + + $ tio /dev/ttyUSB0 --line-pulse-duration DTR=200,RTS=300,RI=50 + + Likewise, the pulse duration can also be set via configuration file + using the line-pulse-duration variable: + + line-pulse-duration = DTR=200,RTS=300,RI=50 + + * Upgrade inih wrap to r56 + + * Optimization + + * Add example configuration file + + +Ralph Siemsen: + + * Fix relative timestamps + + Fix the display of relative timestamps. The hack of subtracting 3600 + only works if you happen to be in a time zone that is one hour away from + UTC. When subtracting two time values, the result is an absolute + quantity (interval). These should be displayed as-is; without local time + zone nor daylight saving correction. Hence gmtime() instead of + localtime(). + + + +Changes since tio v1.44: + + * Introduce bold color option + + Introduce "bold" color option which only apply bold color formatting to + existing system color. + + Also make "bold" the default color option. + + Fixes all white issue with black on white tio text. + + * Update README + + * Change 'ctrl-t T' to 'ctrl-t t' for timestamp toggle + + * Add support for remapping prefix key + + Make it possible to remap the prefix key (default: ctrl-t) by setting + the prefix-ctrl-key variable in the configuration file. + + Allowed values are in the range a..z. + + Example, to set the prefix key to ctrl-a simply do: + + prefix-ctrl-key = a + + * Add plaintext man page + +Rui Chen: + + * docs: add homebrew installation note + + * fix macOS build + + * fix compilation error + + + +Changes since tio v1.43: + + * Simplify arbitrary baudrate code + + * Cleanup error printing routines + + Clean up so that only the following error related printing functions are + used: tio_error_printf(), tio_error_printf_silent(), + tio_warning_printf(). + + A session mode switch is introduced for error printing so that it will + print error messages with better formatting depending on in or out of + session. + + * Update README + + * Clean up man page + + * Add support for space parity + + * Rename EOL delay to Output line delay + + * Replace -U,--upcase with mapping flag OLTU + + * Simplify tty_write() + +Robert Snell: + + * Additional commands: EOL delay, lower to upper translation, added mark parity + + Added command line options: + -O, --eol-delay to have a separate delay for end of line + -U, --upper to enable translation of lower case alpha to upper case + + Added ability to set mark parity. + Added ctrl-t U key sequence to allow enable/disable lower case alpha to + upper case during a session. + Updated Man page with command line options, ctrl-t sequences and + configuration file options. + Updated README.md, with above information. + + + +Changes since tio v1.42: + + * Add '24hour-delta' timestamp option + + When enabled this option will timestamp new lines with the time elapsed + since the line before. + + This is a very useful feature to identify which events takes the most + time. + + * Improve description of socket option + + * Rename ChangeLog to NEWS + + * Update README + + * Update man page + +George Joseph: + + * Add Pulse DTR command + + MCUs like the ESP32 can be reset if the serial port DTR line is + pulsed for a short time. You could just type CTRL-t d CTRL-t d + but that's a little awkward since you have to lift your finger + off the CTRL key to type the Ds. Now you can just type CTRL-T D. + + * Added new command "D" to pulse the DTR line. I.E. Toggle its + state twice with a configurable duration between toggles. + + * Added new config/command line option "--dtr-pulse-duration" + to set the duration between the DTR state toggles. The default + is 100ms. + + + +Changes since tio v1.41: + + * Update man page + +ZeroMemoryEx: + + * Handle malloc failure + +Sylvain LAFRASSE: + + * Add missing 'string.h' include. + + + +Changes since tio v1.40: + + * Rename --hex-mode to --hexadecimal + + * Enable buffered writing + + Read block of bytes from input and process same block for output. This + will speed things up by reducing I/O overhead. + + * Enable buffered reading + + Read block of bytes from input and process byte by byte for output. This + will speed things up by reducing I/O overhead. + + * Refactoring + + * Cleanup stdout flushing + + Flushing is not needed since we disabled buffering of stdout. + + * Simplify stdout_configure() code + + * Simplify stdin_configure() code + + * Update man page + + * Update README + + + +Changes since tio v1.39: + + * Add config support for log-strip + + * Add config support for hex-mode + + * Rename --hex to --hex-mode + + * Fix completion for -e, --local-echo + + * Ignore newlines in hex output + + * Fix newline in warning_printf() + + * Fix ansi_printf_raw() in no color mode + + * Enter non-interactive mode when piping to tio + + Add support for a non interactive mode which allows other application to + pipe data to tio which then forwards the data to the connected serial + device. + + Non interactive means that tio does not react to interactive key commands + in the incoming stream. This allows users to pipe binary data directly + to the connected serial device. + + Example use: + + $ cat commands.txt | tio /dev/ttyUSB0 + + * Also strip backspace from log + + To make log strip feature consistent so that we remove all unprintable + control characters and escape sequences. + + * Socket code cleanup + + * Cleanup man page + + * Rename --log-filename to --log-file + +Yin Fengwei: + + * Allow strip escape sequence characters from log file + + The log without escape key stripped is like: + + ^M[12:47:17] ACRN:\> + ^M[12:47:17] ACRN:\>lasdfjklsdjf + ^M + ^M[12:47:18] Error: Invalid command. + ^M[12:47:19] ACRN:\> + ^M[12:47:26] ACRN:\> + ^M[12:47:26] ACRN:\>sdafkljsdkaljfklsadjflksdjafjsda^H ^H^H... + ^M + ^M[12:47:31] Error: Invalid command. + + After strip escape key, the log is like: + + [12:49:18] ACRN:\> + [12:49:19] ACRN:\> + [12:49:19] ACRN:\>ls + + [12:49:19] Error: Invalid command. + [12:49:19] ACRN:\> + [12:49:19] ACRN:\>dfaslhj + + [12:49:24] Error: Invalid command. + + Beside escape key, it also handle backspace key as well. + + + +Changes since tio v1.38: + + * Improve key command response for local echo and timestamp + + * Fix invalid hex character error message + + * Make sure only matched config section is parsed + + * Add support for "disable" keyword in config file + + * Unify error message formating + + * Cleanup list devices code + + * Fix command-line tty-device|config parsing + + Allow user to add options on both sides of the provided config argument. + + For example: + + $ tio -b 9600 am64-evm -e + + Before, tio only allowed adding arguments after the config argument. + + Implemented as simple as possible by introducing two stage option parsing. + + * Update bash completion + + * Add support for IPv4 and IPv6 network sockets + + Add support for IPv4 and IPv6 network sockets via socket syntax + "inet:" and "inet6:" respectively. + + For example, to listen and redirect serial device I/O to a host bound + IPv4 socket simply do: + + $ tio /dev/ttyUSB0 --socket inet:4444 + + To connect do e.g.: + + $ nc 127.0.0.1 4444 + + Likewise, for IPv6 do: + + $ tio /dev/ttyUSB0 --socket inet6:4444 + + To connect do e.g.: + + $ nc ::1 4444 + + If port is 0 or no port is provided default port 3333 is used. + + * Fix tio deleting unix socket file + + If tio has a unix file socket open, a second tio instance of tio may + delete the socket file. This change fixes so that it will not be deleted + and tio will instead error and complain about conflicting socket file. + + * Rework color option + + Rework the color option to support setting ANSI color code values + ranging from 0..255 or "none" for no color or "list" to print a list of + available ANSI colors codes. + + Also, disables color when piping. + + * Remove print of hex mode status at startup + + * Remove newline option in hex mode + + * Fix configfile memory leaks + + * Remove command-line option inconsistencies + + Optional arguments, as parsed by the getopt_long mechanism, are + inherently inconsistent with how you define required arguments. + + To avoid confusion we decide to avoid this inconsistency by replacing + optional options with additional options with required arguments. + + * Replace '1' with 'enable' in config files + + * Convert errors to warnings + +g0mb4: + + * Extended hexadecimal mode. + + While in hex mode (ctrl-t h) you can output hexadecimal values. + E.g.: to send 0x0A you have to type 0A (always 2 characters). + + Added option -x, --hex to start in hexadecimal mode. + + Added option --newline-in-hex to interpret newline characters in hex mode. + This is disabled by default, because, in my opinion, hex stream is + fundamentally different from text, so a "new line" is meaningless in this + context. + + + +Changes since tio v1.37: + + * Redirect error messages to stderr + + * Improve help and man page + + * Mention config file in --help + + * Fix running without config file + + * Fix config file error messages + + * Redirect error messages to stderr + + * Add repology packaging status + + * Fix parsing of default settings + + Default configuration file settings were not parsed in case a section + was matched. Now we make sure that the default (unnamed) settings are + always parsed. + + * Append to existing log file (no truncation) + + * Add socket info to show configuration + + * Print socket info at startup + + * Fix socket option parsing + +Peter Collingbourne: + + * Match user input against config section names if pattern matching was unsuccessful. + + This allows for better config file ergonomics if the user has a diverse + set of serial devices as the name does not need to be specified in + the config file twice. + + * Add support for external control via a Unix domain socket. + + This feature allows an external program to inject output into and + listen to input from a serial port via a Unix domain socket (path + specified via the -S/--socket command line flag, or the socket + config file option) while tio is running. This is useful for ad-hoc + scripting of serial port interactions while still permitting manual + control. Since many serial devices (at least on Linux) get confused + when opened by multiple processes, and most commands do not know + how to correctly open a serial device, this allows a more convenient + usage model than directly writing to the device node from an external + program. + + 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. + + Sockets remain open while the serial port is disconnected, and writes + will block. + + Example usage 1 (issue a command): + + echo command | nc -UN /path/to/socket > /dev/null + + Example usage 2 (use the expect command to script an interaction): + + #!/usr/bin/expect -f + + set timeout -1 + log_user 0 + + spawn nc -UN /path/to/socket + set uart $spawn_id + + send -i $uart "command1\n" + expect -i $uart "prompt> " + send -i $uart "command2\n" + expect -i $uart "prompt> " + +lexaone: + + * fix for using option 'log' without 'log-filename' in config file + + + +Changes since tio v1.36: + + * Make libinih a fallback dependency + + This means that in case meson does not find libinih it will + automatically clone libinih and include it in the build. + + The libinih library is reconfigured to be statically built so that no + shared object will be installed. + +Sylvain LAFRASSE: + + * Fix timestamp parsing in INI conf + + * Factorize timestamp parsing to be coherent with command line format in configuration file. + + + +Changes since tio v1.35: + + * Add support for defaults in config file + + If no section name is specified the configuration will be considered the + default one. + + This allows to set e.g. a default color code for sections which do not + configure a color code. + + * Handle SIGHUP + + Handle SIGHUP so that the registered exit handlers are called to restore + the terminal back to its original state. + + * Add color configuration support + + * Bypass unused result warnings + + * Force dependency on libinih + + Configuration file support is considered a mandatory feature. + + * Update headers + + * Update AUTHORS + + * Update man page + + * Move string_to_long() to misc.c + + * Update CircleCI config + + * Update tio gif + + * Update README + + * Update LICENSE date + + * Remove redundant COPYING file + +Liam Beguin: + + * Document configuration file options + + * Add support for a configuration file + + * misc: add _unused macro + + Some parameters are expected to be unused. + Add a basic macro to mute these compiler warnings. + + * options: expose string_to_long() + + Expose string_to_long() so that other source files can use it. + + + +Changes since tio v1.34: + + * Add support for automatically generated log filename + + Automatically generate log filename if none is provided. + + The auto generated file name is on the form: + "tio_DEVICE_YYYY-MM-DDTHH:MM:SS.log" + + * Add support for configurable timestamp format + + 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 ISO 8601 format. + + This feature allows to easily add more timestamp formats in the future. + + * Reintroduce asm-generic/ioctls.h + + It is needed for ppc builds. + + * Add macro hack to workaround older buggy glibc + +Robey Pointer: + + * Add support for high bps on OS X + + + +Changes since tio v1.33: + + * Fix setspeed2 compilation + + * Only apply color formatting when using color option + + To help the color blind who may use custom terminal foreground / + background colors. + + * Update README + + * Add '-c, --color' option + + Allow user to select which ANSI color code to use to colorize the tio + text. To successfully set the color the color code must be in the range + 0..255. + + If color code is negative tio will print all available ANSI colors. + + The default color is changed to bold white to make tio defaults usable + for most users, including color blind users. + + * Fix setspeed2 check + + * Fix meson header check string + + * Reintroduce long timestamp format + + But make the timestamp format RFC3339 compliant instead. The RFC states: + + NOTE: ISO 8601 defines date and time separated by "T". + Applications using this syntax may choose, for the sake of + readability, to specify a full-date and full-time separated by + (say) a space character. + + This way we keep the information specified by ISO 8601 but make it more + human readable which is better for the console output. + + * Update version year + +Sylvain LAFRASSE: + + * Fix TTY device listing on Darwin. (#136) + + * Fix TCGETS2 search on Darwin. + + + +Changes since tio v1.32: + + * Show auto connect status in show configuration + + * Use '#pragma once' in all headers + + * Improve printed output + + Get rid of inconsistencies in the printed output (error printing, + colors, etc.). + + Prepare for user configurable color. + + * Rename option -i to -L + + * Shorten timestamp + + * Shorten timestamp description + + We do not need the date part of the timestamp. It simply takes up too + much precious line space. In case of logging to file, one can easily + conclude the date from the file date information. + + * Replace Travis with circleCI + + * Replace autotools with meson + + To introduce much simpler build configuration which is also easier to + maintain. + + * Add list serial devices feature + + For convenience, add a --list-devices option which lists the available + serial devices. + + * Cleanup: Use dot notation for default options struct + + * Update AUTHORS + + * Add command to show version + + The key sequence ctrl-t v will now show the version of tio. + + * Align format of timestamps + + * Add Sylvain as official co-maintainer + +Sylvain LAFRASSE: + + * Add '-t' option description for time stamping. + + * Add description for time stamping. + + * Resolved tio/tio#84: Added timestamps in log file if enabled. + +attila-v: + + * 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. + +Yin Fengwei: + + * Output newline on stdout with hex print mode + + This is to fix the issue #104. The timestamp will always be + printed at the beginning of line: + + [10:25:56] Switched to hexadecimal mode + 0d 0a 0d [10:25:57] 41 43 52 4e 3a 5c 3e 0d 0a 0d [10:25:58] 41 + + is changed to: + + [12:34:56] 45 72 72 6f 72 3a 20 49 6e 76 61 6c 69 64 20 + [12:34:56] 41 43 52 4e 3a 5c 3e + [12:34:56] 41 43 52 4e 3a 5c 3e + [12:34:57] 41 43 52 4e 3a 5c 3e 6c 73 + +Jakob Haufe: + + * Make comparison POSIX compliant + + String comparison with == is not POSIX compliant and can fail with e.g. + dash. + +Henrik Brix Andersen: + + * Add bash completion of tty devices. + + * Add -t/--timestamp to bash completion script. + +Henner Zeller: + + * Local echo: show character by character even if stdout buffered. + +Björn Stenberg: + + * Show error when failing to open a tty + +Alban Bedel: + + * Fix out of tree builds + + Out of tree builds are currently broken because $(top_srcdir)src/include + is not in the search path. In tree builds are working because autoconf adds + $(top_builddir)/src/include to the search path for the generated config.h. + As $(top_builddir) and $(top_srcdir) are identical during in tree builds + the search path still end up being somehow correct. + + To fix this add -I$(srcdir)/include to the CPPFLAGS in Makefile.am. + +Fabrice Fontaine: + + * src/setspeed2.c: fix redefinition of termio + + Include ioctls.h and termbits.h from asm-generic instead of asm to avoid + build failures. + +Erik Moqvist + + * Exit if output speed cannot be set. + +Lars Kellogg-Stedman: + + * fflush() after putchar() for print_hex and print_normal + + In order for local echo to work properly, we have to either call + fflush(stdout) after every character or just disable line buffering. + This change calls fflush() after putchar(). + + * Disable line buffering in stdout + + In order for local echo to work properly, we have to either call + fflush(stdout) after every character or just disable line buffering. + This change uses setbuf(stdout, NULL) to do the latter. + +George Stark: + + * don't show line state if ioctl failed + + * add serial lines manual control + +arichi: + + * Flush every local echo char + + Flush stdout at every char in case it + happens to be buffered. + +Mariusz Midor: + + * Newline: handle both NL and CR + + Flag ONLCRNL expects code \n after press Enter, but on some systems \r is sent instead. + + + +Changes since tio v1.31: + + * Update AUTHORS + + * Minor code style cleanups + + * Cleanup print macros + + * Flush output + + Make sure output is transmitted immediately by flushing the output. + +Robey Pointer: + + * add optional timestamps + + with "-t" or "C-t T", toggle a timestamp prefix to each line. + +Jakub Wilk: + + * Fix typos + +Sylvain Lafrasse: + + * Added macOS compatibility + + * Made O_NONBLOCK flag to open() call specific to macOS only. + + * Added macOS-related details. + + * Added O_NONBLOCK flag to open() call for macOS (10.13.6) compatibility. + + + +Changes since tio v1.30: + + * Update date + + * Update AUTHORS + +Henner Zeller: + + * Clarify the input/output variable names (No-op change) + + * Organize options the same sequence they are mentioned in cmdline help. + + * Update README. + + * Map CR->NL locally on output instead of using tio.c_oflag |= OCRNL. + + This mostly is intended to have local echo output exactly what is sent + to the remote endpoint. + A nice side-effect is, that it also fixes tty-implementations, that can't + deal with the OCRNL flag on tio.c_oflag. + + * Provide local-echo option. + + Can be switched on with -e on the command line. + Can be toggled with Ctrl t e while program is running. + + * Write to logfile as soon as we have the data, don't buffer. + + Logfiles are important to see what happened, in particular if something + unexpected happened; so we want to make sure that the logfile is flushed + to disk. + + Before this change, the logfile was typically written at the end in + a large chunk as the default (large) buffering applied. Now, characters are + written out ASAP, so it is possible to get a live-view with a + tail -f + + + +Changes since tio v1.29: + + * Update README + + * Update man page and bash completion + + * Update AUTHORS + +qianfan Zhao: + + * ONLCRNL: change the method to map NL to CR-NL + + + +Changes since tio v1.28: + + * Add mapping flags INLCRNL and ODELBS + + The following new mapping flags are added: + + INLCRNL: Map NL to CR-NL on input. + ODELBS: Map DEL to BS on output. + + Flags requested and tested by Jan Ciger (janoc). + + + +Changes since tio v1.27: + + * Update README + + * Update AUTHORS + + * Add snap status to README.md + + * Add README.md to prettify GitHub page + + * Add missing header + +Petr Vaněk: + + * Add missing header file under musl-libc + + Musl's inclusion tree slightly differs from glibc, therefore TCGETS2 is + not reachable through sys/ioctl.h, so asm/ioctls.h needs to be included + too. + +Jakub Wilk: + + * Fix grammar and typos + + + +Changes since tio v1.26: + + * Update man page + + * Add support for setting non-standard baudrates + + Support for non-standard baudrate settings will be automatically enabled + if the termios2 interface is detected available. However, to play it + safe, the old and widely supported termios interface will still be used + when setting standard baudrates. + + * Cleanup + + * Update AUTHORS + + + +Changes since tio v1.25: + + * Reconfigure stdin + + Make stdin behave more raw'ish. In particular, don't + translate CR -> NL on input. + + * Add special character map feature + + Add a --map option which allows mapping special characters, in particular CR and + NL characters which are used in various combinations on various platforms. + + * Cleanup + + * Update AUTHORS + + * Update README + + * Mention website + + * Update man page + + + +Changes since tio v1.24: + + * Fix error applying new stdout settings + + On Fedora 26 tio will quit with the following error message: + + "Error: Could not apply new stdout settings (Invalid argument)" + + In case of Fedora, it turns out that the new stdout settings used are a + bit too aggressive because an empty termios structure is used. To remedy + this we reuse the existing stdout settings and only reconfigure the + specific options we need to make a "raw" stdout configuration. + + * Remove unused pkgconfig in configure + + * Code cleanup + + Remove unused variable. + + + +Changes since tio v1.23: + + * Optimize clear screen command + + Replaced system call with inline ANSI/VT100 clear screen code sequence + + * Fix bash completion installation + + Fixed the configure script to avoid that the bash completion script gets + installed outside of the prefix location. The default install location + is now $prefix/share/bash-completion/completions. + + Use the configure option '--with-bash-completion-dir=PATH' if you need + to install the bash completion script elsewhere. + +Jakub Wilk: + + * Add missing commas in conditional sentences + + + +Changes since tio v1.22: + + * Update copyright headers + +Jakub Wilk: + + * Fix typos + + + +Changes since tio v1.21: + + * Update man page date + + * Update copyright year + + * Code cleanup + + * Update README and man page + + + +Changes since tio v1.20: + + * Add support for hexadecimal mode + + A new key command 'ctrl-t h' is introduced which toggles between + hexadecimal mode and normal mode. When in hexadecimal mode data received + will be printed in hexadecimal. + + * Do not distribute src/bash_completion/tio + + Since the bash completion tio script is now autogenerated from tio.in it + should not be distributed in the tarball. + + * Add missing forward flag + + * Update AUTHORS file + +Adam Borowski: + + * 'ctrl-t b' to send serial break. + +Jakub Wilk: + + * Removed git commit references from ChangeLog + + ChangeLog is primary useful for users who don't have the git repository + at hand. + + Replace git commit references with version numbers; or if the change + only cleans up another change with no release in between, remove the + changelog item completely. + + + +Changes since tio v1.19: + + * Added more error handling of terminal calls + + Also removed duplicate terminal flushing calls. + + * Revert "Added support for non-standard baud rates" + + This reverts a change made in v1.18. + + Reverting because supporting non-standard or arbitrary baud rates is + troublesome because the c library provides no means of doing so and even + if bare metal linux kernel interface is used it will not work on all + Linux kernels version. + + + +Changes since tio v1.18: + + * Rearranged key commands + + Rearranged the key commands: + + ctrl-t c (clear screen) is now + ctrl-t l which is similar to the well known shell ctrl-l + + ctrl-t i (show settings information) is now + ctrl-t c (show configuration) + + Updated man page accordingly. + + * Added "ctrl-t c" key command to clear screen + + + +Changes since tio v1.17: + + * Updated man page + + * Added support for non-standard baud rates + + Only enabled when possible, that is, when the BOTHER definition is + available. + + It is untested but it should work as described here: + https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=683826 + + Some Cypress USB<->serial devices supposedly supports arbitrary speeds. + + * Generate baudrate switch cases based on detection + + Support a single source of baud rate configuration as discussed in + https://github.com/tio/tio/issues/45 . + + To do so, autogeneration of the switch cases which do the baud rate + option value check and configuration/conversion in tty_configure() is + introduced via a single macro. + + Just to be safe, this change also enables configure detection of all + baud rates, including the ones previously assumed supported by most/all + systems (POSIX). + + * Minor cleanup + + * Exit when not a tty device in autoconnect mode + +Jakub Wilk: + + * Added non-standard baud rates that are defined on FreeBSD + + * Capitalized "GitHub" in README + + + +Changes since tio v1.16: + + * Compacted tty_configure() a bit + + * Fixed automatic baud rate enablement + + * Minor cleanups + + * Added autodetection of available baud rates + + Various platforms support different baud rates. + + To avoid adding platform specific handling generic baud rate detection + tests are introduced in the configure script. Successfully detected baud + rates are automatically enabled. This applies to both the C code and the + bash completion script. + + Note: + Baud rates below 57600 are defined by POSIX-1 and supported by most + platforms so only baud rate 57600 and above are tested. + + * Updated bash-completion + + * Fixed printf() format type + + * Added Travis build configuration + +Jakub Wilk: + + * Generated bash completion at configure time + + * Reduce code duplication in baud rate detection + + * Add support for baud rates 200 and 1800 + + * Fixed baudrate type + + + +Changes since tio v1.15: + + * Updated man page + + * Updated README + + * Removed obsolete packaging files + + * Removed use of deprecated bzero() + + + +Changes since tio v1.14: + + * Removed + to remove potential confusion + + * Added input digit checks + + * Fixed license string + + * Introduced tty_configure() + + Moved tty configuration actions to tty_configure() in tty.c. This way + options.c is strictly about parsing options nothing else. + + * Function names cleanup + + * Updated AUTHORS file + + Added Nick who created the new tio package for Arch Linux. + + * Fixed tx/rx counters type + +Jakob Haufe: + + * Include config.h before standard headers + + Large file support was meant to be enabled in v1.11. + This change enables it for real. + + + +Changes since tio v1.13: + + * Fixed tio_printf macro + + * Fixed launch hints + + Fixed launch hints not being printed in no autoconnect mode. + + * Added 'ctrl-t ?' to list available commands + + * Fixed log mechanism + + To avoid echoing only log what is received from tty device. + + * Improved tio output + + Added titles and indentation to commands output for clearer separation + when firing commands repeatedly. + + Also added print of tio version and quit command hint at launch. + + * Cleaned up tio print mechanism + +Jakub Wilk: + + * Fixed grammar + + "allow" is a transitive verb, which requires an object, + so "allow to " is ungrammatical. + + * Fixed typo + + + +Changes since tio v1.12: + + * Fixed some error prints + + * Fixed error printing for no autoconnect mode + + Always print errors but only print silent errors when in no autoconnect + mode. + + * Added key command for showing session settings + + A new key command "ctrl-t i" is added to allow the user to display the + various session settings information (baudrate, databits, log file, etc.). + + This is useful in case you have a running session but have forgotten + what the settings are. + + + +Changes since tio v1.11: + + * Consolidated command key handling + + * Moved delay mechanism into separate function + + * Retired obsolete usleep() + + Replaced with nanosleep() + + * Added simple tx/rx statistics command (ctrl-t s) + + To display the total number of bytes transmitted/received simply perform the + 'ctrl-t s' command sequence. + + This feature can be useful when e.g. trying to detect non-printable + characters. + + * Further simplification of key handling + + Changed so that the "ctrl-t ctrl-t" sequence is now simply "ctrl-t t" to + send the ctrl-t key code. This is inspired by screen which does similar + to send its command key code (ctrl-a a). + + This change also eases adding new key commands if needed. + + Updated man page accordingly. + + * Cleaned up and simplified key handling + +Jakub Wilk: + + * Insert output delay only if something was output + + + +Changes since tio v1.10: + + * Enabled large file support (LFS) + + Added autotools AC_SYS_LARGEFILE to support 64 bit file size handling. + + * Updated tio title + + + +Changes since tio v1.9: + + * Introduced lock on device file + + Tio will now test for and obtain an advisory lock on the tty device file + to prevent starting multiple sessions on the same tty device. + + * Updated AUTHORS + +Jakub Wilk: + + * Treat EOF on stdin as error + + + +Changes since tio v1.8: + + * Cleanup of error handling + + Introduced consistent way of handling errors and printing error messages. + + Also upgraded some warnings to errors. + + * Updated localtime() error message + + * Cleanup + +Jakub Wilk: + + * Fix error handling for select() + + Previously the error handling code for select() was unreachable. + + * Removed unneeded quotes from AM_CFLAGS + + * Expanded tabs + + * Fixed setting "tainted" + + Set "tainted" if and only if any character was read from the device. + + Ctrl-t is no longer sent to the device on exit, so the trick to avoid + its echo is not necessary. + + Characters read from stdin don't directly affect output, so they + shouldn't enable "tainted". + + * Used \r in color_printf() + + \033[300D is an unusual way to move the cursor back to column 1. + Use straightforward \r instead. + + * Added missing \r\n to warning messages + + \n alone is not enough, because the terminal is in raw mode. + + + +Changes since tio v1.7: + + * Fixed enablement of compiler warnings + + * Fixed log_open() prototype + + * Fixed index error wrt ctrl-t detection + + * Fixed handling of ctrl-t + + Before, when exercising the quit key sequence (ctrl-t + q) the ctrl-t code + (0x14) would be sent. + + This is now fixed so that it is not sent. + + However, in case it is needed to send ctrl-t to the device it is possible by + simply repeating the ctrl-t. + + Meaning, ctrl-t + ctrl-t = ctrl-t sent to device. + + * Improved error handling + + Fixes a memory leak and avoids aggressive busy looping when problems + accessing tty device. + + * Removed redundant log_close() call + + * Enabled compiler warnings + +Jakub Wilk: + + * Stopped copying arguments to fixed-size buffers + + Don't needlessly copy command-line arguments into fixed-size buffers. + + Previously the program crashed if an overlong pathname was provided on + the command line. Also, some systems (such as GNU Hurd) don't define + MAXPATHLEN at all. + + * Added const to log_open() prototype + + * Completed the ^g to ^t transition + + In v1.7 the escape key was changed from ^g to ^t, but some + code and comments still referred to the old key. + + * Used HTTPS for tio.github.io + + * Man page beautification + + * Bumped date in man page + + * Improve man page formatting + + Use regular font for metacharacters such as "[]", "," or "|"; + use italic font for metavariables. + + * Fixed hyphen vs minus vs em-dash confusion in man page + + - prints as hyphen; + \- prints as minus sign; + \em prints as em-dash. + + + +Changes since tio v1.6: + + * Changed escape key from ^g to ^t + + After renaming to "tio" it makes sense to change the escape key + accordingly. Hence, the new escape key is ^t. + + Meaning, in session, its now ctrl-t + q to quit. + +Jakub Wilk: + + * Fixed silly "tio or tio" in man page + + * Fixed typo + diff --git a/README.md b/README.md index b7d2455..e3ab066 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,20 @@ -# tio - a simple serial device I/O tool -[![CircleCI](https://circleci.com/gh/tio/tio/tree/master.svg?style=shield)](https://circleci.com/gh/tio/tio/tree/master) -[![tio](https://snapcraft.io/tio/badge.svg)](https://snapcraft.io/tio) -[![Packaging status](https://repology.org/badge/tiny-repos/tio.svg)](https://repology.org/project/tio/versions) +[![tio](images/tio-icon.png)]() + +# tio - a serial device I/O tool + +[![](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 -tio is a simple 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. +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.

@@ -16,85 +22,280 @@ devices for basic I/O operations. ### 1.1 Motivation -To make a simpler serial device tool for talking with serial TTY devices with +To make a simpler serial device tool for working with serial TTY devices with less focus on classic terminal/modem features and more focus on the needs of embedded developers and hackers. -tio was originally created to replace +tio was originally created as an alternative to [screen](https://www.gnu.org/software/screen) for connecting to serial devices when used in combination with [tmux](https://tmux.github.io). ## 2. Features * Easily connect to serial TTY devices - * Automatic connect - * Support for arbitrary baud rates + * Sensible defaults (115200 8n1) + * 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 + * By device + * Including topology ID, uptime, driver, description + * Sorted by uptime (newest device listed last) + * By ID + * By path * Show RX/TX statistics * Toggle serial lines + * Pulse serial lines with configurable pulse duration * Local echo support - * Remap special characters (nl, cr-nl, bs, etc.) - * Line timestamps + * Remapping of characters (nl, cr-nl, bs, lowercase to uppercase, etc.) + * Switchable independent input and output + * Normal mode + * Hex mode (output supports variable width) + * Line mode (input only) + * Timestamp support + * Per line in normal output mode + * Output timeout timestamps in hex output mode * Support for delayed output - * Hexadecimal mode + * Per character + * Per line * Log to file - * Autogeneration of log filename + * Automatic naming of log file (default) + * Configurable directory for saving automatic named log files + * Manual naming of log file + * Overwrite (default) or append to log file + * Strip control characters and escape sequences * Configuration file support - * Activate sub-configurations by name or pattern - * Redirect I/O to socket for scripting or TTY sharing + * 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 - * Bash completion - * Color support + * Bash completion on options, serial device names, and profile names + * Configurable tio message text color + * Supports NO_COLOR env variable as per [no-color.org](https://no-color.org) + * Visual or audible alert on connect/disconnect + * Remapping of prefix key + * Lua scripting support for automation + * Run script manually or automatically at connect (once/always/never) + * Simple expect/send like functionality with support for regular expressions + * Manipulate port modem lines (useful for microcontroller reset/boot etc.) + * Send files via x/y-modem protocol + * Search for serial devices * Man page documentation + * Plays nicely with [tmux](https://tmux.github.io) and similar terminal multiplexers ## 3. Usage +For more usage details please see the man page documentation +[here](https://raw.githubusercontent.com/tio/tio/master/man/tio.1.txt). + ### 3.1 Command-line The command-line interface is straightforward as reflected in the output from -'tio --help': +```tio --help```: ``` - Usage: tio [] +Usage: tio [] - Connect to tty-device directly or via sub-configuration. +Connect to TTY device directly or via configuration profile or topology ID. - Options: - -b, --baudrate Baud rate (default: 115200) - -d, --databits 5|6|7|8 Data bits (default: 8) - -f, --flow hard|soft|none Flow control (default: none) - -s, --stopbits 1|2 Stop bits (default: 1) - -p, --parity odd|even|none Parity (default: none) - -o, --output-delay Character output delay (default: 0) - -n, --no-autoconnect Disable automatic connect - -e, --local-echo Enable local echo - -t, --timestamp Enable line timestamp - --timestamp-format Set timestamp format (default: 24hour) - -L, --list-devices List available serial devices - -l, --log Enable log to file - --log-file Set log filename - --log-strip Strip control characters and escape sequences - -m, --map Map special characters - -c, --color 0..255|none|list Colorize tio text (default: 15) - -S, --socket Redirect I/O to socket - -x, --hexadecimal Enable hexadecimal mode - -v, --version Display version - -h, --help Display help +Options: + -b, --baudrate Baud rate (default: 115200) + -d, --databits 5|6|7|8 Data bits (default: 8) + -f, --flow hard|soft|none Flow control (default: none) + -s, --stopbits 1|2 Stop bits (default: 1) + -p, --parity odd|even|none|mark|space Parity (default: none) + -o, --output-delay Output character delay (default: 0) + -O, --output-line-delay Output line delay (default: 0) + --line-pulse-duration Set line pulse duration + -a, --auto-connect new|latest|direct Automatic connect strategy (default: direct) + --exclude-devices Exclude devices by pattern + --exclude-drivers Exclude drivers by pattern + --exclude-tids Exclude topology IDs by pattern + -n, --no-reconnect Do not reconnect + -e, --local-echo Enable local echo + --input-mode normal|hex|line Select input mode (default: normal) + --output-mode normal|hex|hexN Select output mode (default: normal) + -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, TIDs, and profiles + -L, --log Enable log to file + --log-file Set log filename + --log-directory Set log directory path for automatic named logs + --log-append Append to log file + --log-strip Strip control characters and escape sequences + -m, --map Map characters + -c, --color 0..255|bold|none|list Colorize tio text (default: bold) + -S, --socket Redirect I/O to socket + --rs-485 Enable RS-485 mode + --rs-485-config Set RS-485 configuration + --alert bell|blink|none Alert on connect/disconnect (default: none) + --mute Mute tio messages + --script 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 sub-configurations may be set via configuration file. +Options and profiles may be set via configuration file. - In session, press ctrl-t q to quit. +In session you can press ctrl-t ? to list available key commands. - See the man page for more details. +See the man page for more details. ``` -By default tio automatically connects to the provided TTY device if present. -If the device is not present, it will wait for it to appear and then connect. -If the connection is lost (eg. device is unplugged), it will wait for the -device to reappear and then reconnect. However, if the `--no-autoconnect` -option is provided, tio will exit if the device is not present or an -established connection is lost. +By default tio automatically connects to the provided TTY device. If the device +is not present, tio will wait for it to appear and then connect. If the +connection is lost (e.g. device is unplugged), it will wait for the device to +reappear and then reconnect. However, if the `--no-reconnect` option is +provided, tio will exit if the device is not present or an established +connection is lost. -tio features full bash autocompletion. +#### 3.1.1 Examples + +Typical use is without options: +``` +$ tio /dev/ttyUSB0 +``` + +Which corresponds to the commonly used default options: +``` +$ tio --baudrate 115200 --databits 8 --flow none --stopbits 1 --parity none /dev/ttyUSB0 +``` + +List available serial devices: +``` +$ tio --list +Device TID Uptime [s] Driver Description +----------------- ---- ------------- ---------------- -------------------------- +/dev/ttyS4 BaaB 19526.576 port 16550A UART +/dev/ttyS5 eV0Z 19525.845 port 16550A UART +/dev/ttyUSB1 bCC2 1023.274 ftdi_sio TTL232R-3V3 +/dev/ttyUSB0 SPpw 978.527 ftdi_sio TTL232RG-VREG3V3 +/dev/ttyACM0 i5q4 2.079 cdc_acm ST-Link VCP Ctrl + +By-id +-------------------------------------------------------------------------------- +/dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTCHUV56-if00-port0 +/dev/serial/by-id/usb-FTDI_TTL232RG-VREG3V3_FT1NELUB-if00-port0 +/dev/serial/by-id/usb-STMicroelectronics_STLINK-V3_004900343438510234313939-if02 + +By-path +-------------------------------------------------------------------------------- +/dev/serial/by-path/pci-0000:00:14.0-usb-0:8.1.3.1.4:1.0-port0 +/dev/serial/by-path/pci-0000:00:14.0-usbv2-0:8.1.3.1.4:1.0-port0 +/dev/serial/by-path/pci-0000:00:14.0-usbv2-0:6.4:1.0-port0 +/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: +``` +$ tio /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTCHUV56-if00-port0 +``` +Note: Using serial devices by ID helps ensure that tio automatically reconnects +to the same serial device when reattached, even when it enumerates differently. + +If no serial device by ID is available it is recommended to connect via +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, 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: +``` +$ tio --auto-connect new +``` + +Connect automatically to latest registered serial device: +``` +$ tio --auto-connect latest +``` + +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" +``` + +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 +``` + +Log to file with specific filename: +``` +$ tio --log --log-file my-log.txt +``` + +Enable ISO8601 timestamps per line: +``` +$ tio --timestamp --timestamp-format iso8601 /dev/ttyUSB0 +``` + +Output to hex with width 16: +``` +$ tio --output-mode hex16 /dev/ttyUSB0 +``` + +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 +``` + +Manipulate modem lines on connect: +``` +$ 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 "tio.expect('\r\n', 1000)" --mute +KORAD KD3305P V4.2 SN:32475045 +``` ### 3.2 Key commands @@ -102,112 +303,246 @@ Various in session key commands are supported. When tio is started, press ctrl-t ? to list the available key commands. ``` -[20:19:12.040] Key commands: -[20:19:12.040] ctrl-t ? List available key commands -[20:19:12.040] ctrl-t b Send break -[20:19:12.040] ctrl-t c Show configuration -[20:19:12.040] ctrl-t d Toggle DTR line -[20:19:12.040] ctrl-t e Toggle local echo mode -[20:19:12.040] ctrl-t h Toggle hexadecimal mode -[20:19:12.040] ctrl-t l Clear screen -[20:19:12.040] ctrl-t L Show line states -[20:19:12.040] ctrl-t q Quit -[20:19:12.040] ctrl-t r Toggle RTS line -[20:19:12.041] ctrl-t s Show statistics -[20:19:12.041] ctrl-t t Send ctrl-t key code -[20:19:12.041] ctrl-t T Toggle line timestamp mode -[20:19:12.041] ctrl-t v Show version +[15:02:53.269] Key commands: +[15:02:53.269] ctrl-t ? List available key commands +[15:02:53.269] ctrl-t b Send break +[15:02:53.269] ctrl-t c Show configuration +[15:02:53.269] ctrl-t e Toggle local echo mode +[15:02:53.269] ctrl-t f Toggle log to file +[15:02:53.269] ctrl-t F Flush data I/O buffers +[15:02:53.269] ctrl-t g Toggle serial port line +[15:02:53.269] ctrl-t i Toggle input mode +[15:02:53.269] ctrl-t l Clear screen +[15:02:53.269] ctrl-t L Show line states +[15:02:53.269] ctrl-t m Change mapping of characters on input or output +[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 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 +[15:02:53.269] ctrl-t ctrl-t Send ctrl-t character ``` +If needed, the prefix key (ctrl-t) can be remapped via 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: - - $XDG_CONFIG_HOME/tio/tiorc - - $HOME/.config/tio/tiorc - - $HOME/.tiorc + - $XDG_CONFIG_HOME/tio/config + - $HOME/.config/tio/config + - $HOME/.tioconfig -The configuration file supports sub-configurations using named sections which can -be activated via the command-line by name or pattern. +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.3.1 Example Example configuration file: ``` -# Defaults +[default] baudrate = 115200 databits = 8 parity = none stopbits = 1 color = 10 -[ftdi] -tty = /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 +[rpi3] +device = /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 +no-reconnect = true +log = true +log-file = rpi3.log +line-pulse-duration = DTR=200,RTS=150 +color = 11 + +[svf2] +device = /dev/ttyUSB0 baudrate = 9600 -no-autoconnect = enable -log = enable -log-file = ftdi.log +script = tio.expect("login: "); tio.write("root\n"); tio.expect("Password: "); tio.write("root\n") color = 12 -[usb devices] -pattern = usb([0-9]*) -tty = /dev/ttyUSB%s +[esp32] +device = /dev/serial/by-id/usb-0403_6014-if00-port0 +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%m1 +color = 14 ``` -To use a specific sub-configuration by name simply start tio like so: +To use a specific profile by name simply start tio like so: ``` -$ tio ftdi +$ tio rpi3 ``` Or by pattern match: ``` $ 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 +### 4.1 Installation using package manager (Linux) Packages for various GNU/Linux distributions are available. Please consult your package manager tool to find and install tio. If you would like to see tio included in your favorite distribution, please -reach out to their package maintainers team. +reach out to its package maintainers team. -### 4.2 Installation using snap +### 4.2 Installation using snap (Linux) Install latest stable version: ``` - $ snap install tio -``` -Install bleeding edge: -``` - $ snap install tio --edge +$ snap install tio --classic ``` -### 4.3 Installation from source +Note: Classic confinement is currently required due to limitations of the snapcraft framework. +See [Issue #187](https://github.com/tio/tio/issues/187) for discussion. + +### 4.3 Installation using brew (MacOS, Linux) + +If you have [brew](http://brew.sh) installed: +``` +$ brew install tio +``` + +### 4.4 Installation using MSYS2 (Windows) + +If you have [MSYS2](https://www.msys2.org) installed: +``` +$ pacman -S tio +``` + +### 4.5 Installation from source 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 build - $ meson compile -C build - $ meson install -C build +$ meson setup build +$ meson compile -C build +$ meson install -C build ``` See meson\_options.txt for tio specific build options. -Note: Please do no try to install from source if you are not familiar with -how to build stuff using meson. +Note: The meson install steps may differ depending on your specific system. + +### 4.6 Known issues + +Getting permission access errors trying to open your serial device? + +Add your user to the group which allows serial device access permanently. For example, to add your user to the 'dialout' group do: +```bash +sudo usermod -a -G dialout +``` +Switch to the "dialout" group, temporary but immediately for this session. +```bash +newgrp dialout +``` ## 5. Contributing -tio is open source. If you want to help out with the project please feel free -to join in. - -All contributions (bug reports, code, doc, ideas, etc.) are welcome. +This is an open source project - all contributions (bug reports, code, doc, +ideas, etc.) are welcome. Please use the github issue tracker and pull request features. @@ -234,6 +569,6 @@ tio is GPLv2+. See LICENSE file for more details. ## 9. Authors -Created by Martin Lund \ +Maintained by Martin Lund \ See the AUTHORS file for full list of contributors. diff --git a/TODO b/TODO index 563ceea..6036c7b 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,111 @@ + * Add release support for arm and x86 binary tarballs - * Improve error/warning messaging when parsing configuration file + * 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| + 00000010 65 73 74 64 66 0a 61 0a 66 61 0a 66 0a 61 73 66 |estdf.a.fa.f.asf| + 00000020 64 61 64 73 66 61 73 66 64 61 73 64 66 61 64 73 |dadsfasfdasdfads| + 00000030 66 0a 61 73 64 66 61 64 73 66 61 73 64 66 61 73 |f.asdfadsfasdfas| + 00000040 64 66 0a 61 73 64 66 61 64 73 66 61 73 64 66 61 |df.asdfadsfasdfa| + 00000050 73 64 66 66 64 61 73 64 66 0a 0a 31 32 33 31 0a |sdffdasdf..1231.| + 00000060 65 32 31 64 73 77 65 64 0a 0a |e21dswed..| + 0000006a + + * Add support for activity based time stamping in normal output mode + + Already supported in hex output mode. + + * Allow tio to connect to socket + + After some more consideration I think it makes sense to support connecting to a + socket as that will make tio be able to both serve a serial port via a socket + and connect to it - it will be an end to end solution. In short we will be able + to do the following: + + Host serial port on socket (existing feature): + $ tio --socket unix:/tmp/tio-socket-0 /dev/ttyUSB0 + + Connect to same socket (new feature): + $ tio unix:/tmp/tio-socket-0 + + Besides a bit of refactoring the following required changes spring to mind: + + * Socket mode and type of socket should be activated via device name prefix. For example: + * UNIX socket: tio unix: + * TCPv4 socket: tio inet:: + * TCPv6 socket: tio inet6:: + * If no port number defined default to 3333 + * Mapping flags INLCR, IGNCR, ICRNL needs implementation for socket mode + * Error messages should just say "device" instead of "tty device" etc. + * Remove other tty'isms (tty_write() should be device_write() etc.) + * In session key commands that do not work in socket mode should either not be listed or print an error messages if used. + * All non-tty features should continue work (auto-connect etc.) + * Shell completion script update + * Man page update + + + * Split I/O feature + + Allow to split input and output so that it is possible to manage these + independently. + + The general idea is to redirect the output stream on the socket port number + specified but then redirect the input stream on the same port number + 1. + + Example: + + $ tio /dev/ttyUSB0 --socket inet:4444,split-io + + Will result in output stream being hosted on port 4444 and input stream + hosted on port 4445. + + For file sockets something similar can be arranged: + + $ tio /dev/ttyUSB0 --socket unix:/tmp/tio-socket-0,split-io + + Will result in output stream being hosted via /tmp/tio-socket-0 and input + stream hosted via /tmp/tio-socket-0_input + + * Websocket support + + Extend the socket feature to redirect serial I/O to websocket on e.g. port + 1234 like so: + + $ tio --socket ws:1234 + + Use libwesockets to implement feature. diff --git a/examples/config/config b/examples/config/config new file mode 100644 index 0000000..7bd141e --- /dev/null +++ b/examples/config/config @@ -0,0 +1,78 @@ +############################### +# tio - https://tio.github.io # +############################### + +# Example tio configuration file +# +# Place file in any of the following locations: +# $XDG_CONFIG_HOME/tio/config +# $HOME/.config/tio/config +# $HOME/.tioconfig + +[default] +baudrate = 115200 +databits = 8 +flow = none +stopbits = 1 +parity = none +output-delay = 0 +output-line-delay = 0 +auto-connect = direct +no-reconnect = false +local-echo = false +input-mode = normal +output-mode = normal +timestamp = false +log = false +log-append = false +log-strip = false +color = bold +rs-485 = false +alert = none +script-run = always +prefix-ctrl-key = t + +# Configuration profiles + +[rpi3] +baudrate = 115200 +device = /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A6009HU3-if00-port0 +socket = unix:/tmp/tio-socket-0 +color = 9 + +[am64-evm] +baudrate = 115200 +device = /dev/serial/by-id/usb-Silicon_Labs_CP2105_Dual_USB_to_UART_Bridge_Controller_01093176-if01-port0 +line-pulse-duration = DTR=200,RTS=300,RI=50 +alert = bell +color = 10 + +[tincan] +baudrate = 9600 +device = /dev/serial/by-id/usb-TinCanTools_Flyswatter2_FS20000-if00-port0 +log = true +log-file = tincan.log +log-strip = true +color = 11 + +[usb-devices] +pattern = ^usb([0-9]*) +device = /dev/ttyUSB%m1 +color = 12 + +[rs-485-device] +device = /dev/ttyUSB0 +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 = 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 new file mode 100644 index 0000000..aa7a990 --- /dev/null +++ b/examples/lua/automatic-linux-login.lua @@ -0,0 +1,28 @@ +local logins = { + ["foo"] = { + username = "foouser", + password = "foopass", + }, + ["bar"] = { + username = "baruser", + password = "barpass", + }, + ["baz"] = { + username = "bazuser", + password = "bazpass", + }, +} + +local hostname = tio.expect("^(%g+) login:", 10) +if hostname then + local login = logins[hostname] + if (nil ~= login) then + 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 +else + io.write("\r\nDidn't find a login prompt\r\n") +end diff --git a/examples/lua/control-lines-test.lua b/examples/lua/control-lines-test.lua new file mode 100644 index 0000000..55d98b5 --- /dev/null +++ b/examples/lua/control-lines-test.lua @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..77d9dbc --- /dev/null +++ b/examples/lua/serial-device-search.lua @@ -0,0 +1,13 @@ +io.write("Searching... ") + +local device = tio.ttysearch() + +io.write("done\r\n") + +for i in ipairs(device) do + io.write("\r\n" .. device[i]["path"] .. "\r\n") + io.write(" tid = " .. device[i]["tid"] .. "\r\n") + io.write(" uptime = " .. device[i]["uptime"] .. "\r\n") + io.write(" driver = " .. device[i]["driver"] .. "\r\n") + io.write(" description = " .. device[i]["description"] .. "\r\n") +end diff --git a/images/tio-demo.gif b/images/tio-demo.gif index 5106a28..7e12336 100644 Binary files a/images/tio-demo.gif and b/images/tio-demo.gif differ diff --git a/images/tio-icon.png b/images/tio-icon.png new file mode 100644 index 0000000..d95d449 Binary files /dev/null and b/images/tio-icon.png differ diff --git a/man/tio.1.in b/man/tio.1.in index 20c34d9..eb0338d 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -1,17 +1,16 @@ .TH "tio" "1" "@version_date@" "tio @version@" "User Commands" .SH "NAME" -tio \- a simple serial device I/O tool +tio \- a serial device I/O tool .SH "SYNOPSIS" .PP .B tio -.RI "[" "] " "" +.RI "[" "] " "" .SH "DESCRIPTION" .PP -.B tio -is a simple serial device tool which features a straightforward command-line +\fBtio\fR 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. @@ -22,35 +21,107 @@ basic I/O operations. Set baud rate [bps] (default: 115200). .TP -.BR \-d ", " "\-\-databits 5" | 6 | 7 | 8 +.BR \-d ", " "\-\-databits " 5 | 6 | 7 | 8 Set data bits (default: 8). .TP -.BR \-f ", " "\-\-flow hard" | soft | none +.BR \-f ", " "\-\-flow " hard | soft | none Set flow control (default: none). .TP -.BR \-s ", " "\-\-stopbits 1" | 2 +.BR \-s ", " "\-\-stopbits " 1 | 2 Set stop bits (default: 1). .TP -.BR \-p ", " "\-\-parity odd" | even | none +.BR \-p ", " "\-\-parity " odd | even | none | mark | space Set parity (default: none). + +Note: With \fBmark\fR parity the parity bit is always 0. With \fBspace\fR +parity the parity bit is always 1. Not all platforms support \fBmark\fR and +\fBspace\fR parity. + .TP .BR \-o ", " "\-\-output\-delay " \fI Set output delay [ms] inserted between each sent character (default: 0). + .TP -.BR \-n ", " \-\-no\-autoconnect +.BR \-O ", " "\-\-output\-line\-delay " \fI -Disable automatic connect. +Set output delay [ms] inserted between each sent line (default: 0). -By default tio automatically connects to the provided device if present. If the device is not present, it will wait for it to appear and then connect. If the connection is lost (eg. device disconnects), it will wait for the device to reappear and then reconnect. +.TP +.BR " \-\-line\-pulse\-duration " \fI -However, if the -.B \-\-no\-autoconnect -option is provided, tio will exit if the device is not present or an established connection is lost. +Set the pulse duration [ms] of each serial port line using the following key +value pair format in the duration field: = + +Each key represents a serial line. The following keys are available: + +.RS +.TP 8n +.IP \fBDTR +Data Terminal Ready +.IP \fBRTS +Request To Send +.IP \fBCTS +Clear To Send +.IP \fBDSR +Data Set Ready +.IP \fBDCD +Data Carrier Detect +.IP \fBRI +Ring Indicator +.P +If defining more than one key value pair, the pairs must be comma separated. + +The default pulse duration for each line is 100 ms. +.RE + +.TP +.BR "\-a, \-\-auto\-connect new|latest|direct" + +Automatically connect to serial device according to one of the following +strategies: + +.RS +.TP 10n +.IP "\fBnew" +Automatically connect to first new appearing serial device. +.IP "\fBlatest" +Automatically connect to latest registered serial device. +.IP "\fBdirect" +Connect directly to specified TTY device. +.P +All the listed strategies automatically reconnects according to strategy if +device is not available or connection is lost. +.P +Default value is "direct". +.RE + +.TP +.BR " \-\-exclude\-devices \fI" + +Exclude devices by pattern ('*' and '?' supported). + +.TP +.BR " \-\-exclude\-drivers \fI" + +Exclude drivers by pattern ('*' and '?' supported). + +.TP +.BR " \-\-exclude\-tids \fI" + +Exclude topology IDs by pattern ('*' and '?' supported). + +.TP +.BR \-n ", " \-\-no\-reconnect + +Do not reconnect. + +This means that tio will exit if it fails to connect to device or an +established connection is lost. .TP .BR \-e ", " "\-\-local\-echo @@ -63,37 +134,69 @@ Enable local echo. Enable line timestamp. .TP -.BR " \-\-timestamp-format \fI +.BR " \-\-timestamp\-format \fI" Set timestamp format to any of the following timestamp formats: .RS .TP 16n + .IP "\fB24hour" 24-hour format ("hh:mm:ss.sss") .IP "\fB24hour-start" 24-hour format relative to start time +.IP "\fB24hour-delta" +24-hour format relative to previous timestamp .IP "\fBiso8601" ISO8601 format ("YYYY-MM-DDThh:mm:ss.sss") -.P -Default format is -.B 24hour +.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 .TP -.BR \-L ", " \-\-list\-devices +.BR " \-\-timestamp\-timeout \fI" -List available serial devices. +Set timestamp timeout value in milliseconds. + +This value only takes effect in hex output mode where timestamps are only +printed after elapsed timeout time of no output activity from tty device. + +Default value is 200. .TP -.BR \-l ", " \-\-log +.BR \-l ", " \-\-list -Enable log to file. If no filename is provided the filename will be automatically generated. +List available targets (serial devices, TIDs, configuration profiles). .TP -.BR " \-\-log-file \fI +.BR \-L ", " \-\-log + +Enable log to file. + +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. + +.TP +.BR " \-\-log\-file \fI Set log filename. +.TP +.BR " \-\-log\-directory \fI + +Set log directory path in which to save automatically named log files. + +.TP +.BR " \-\-log\-append + +Append to log file. + .TP .BR " \-\-log-strip @@ -102,47 +205,89 @@ Strip control characters and escape sequences from log. .TP .BR \-m ", " "\-\-map " \fI -Map (replace, translate) special 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 .IP "\fBICRNL" -Map CR to NL on input (unless IGNCR is set). +Map CR to NL on input (unless IGNCR is set) .IP "\fBIGNCR" -Ignore CR on input. +Ignore CR on input +.IP "\fBIFFESCC" +Map FF to ESC-c on input .IP "\fBINLCR" -Map NL to CR on input. +Map NL to CR on input .IP "\fBINLCRNL" -Map NL to CR-NL on input. +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. +Map CR to NL on output .IP "\fBODELBS" -Map DEL to BS on output. +Map DEL to BS on output .IP "\fBONLCRNL" -Map NL to CR-NL on output. +Map NL to CR-NL on output +.IP "\fBOLTU" +Map lowercase characters to uppercase on output +.IP "\fBONULBRK" +Map nul (zero) to send break signal on output +.IP "\fBOIGNCR" +Ignore CR on output .P If defining more than one flag, the flags must be comma separated. .RE .TP -.BR \-x ", " \-\-hexadecimal +.BR " \-\-input\-mode " normal|hex|line -Enable hexadecimal mode. +Set input mode. + +In normal mode input characters are sent immediately as they are typed. + +In hex input mode bytes can be sent by typing the \fBtwo-character +hexadecimal\fR representation of the 1 byte value, e.g.: to send \fI0xA\fR you +must type \fI0a\fR or \fI0A\fR. + +In line input mode input characters are sent when you press enter. The only +editing feature supported in this mode is backspace. + +Default value is "normal". .TP -.BR \-c ", " "\-\-color " \fI0..255|none|list +.BR " \-\-output\-mode " normal|hex|hexN -Colorize tio text using ANSI color code value ranging from 0 to 255 or use "none" for no color. +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". + +.TP +.BR \-c ", " "\-\-color " 0..255|bold|none|list + +Colorize tio text using ANSI color code value ranging from 0 to 255 or use +"none" for no color or use "bold" to apply bold formatting to existing system +color. Use "list" to print a list of available ANSI color codes. -Default value is 15. +Default value is "bold". .TP .BR \-S ", " "\-\-socket \fI\fR\fB -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 -.B ctrl-t -sequences are not recognized), and any input from the serial port is multiplexed to the terminal and all connected clients. +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 \fBctrl-t\fR sequences +are not recognized), and any input from the serial port is multiplexed to the +terminal and all connected clients. Sockets remain open while the serial port is disconnected, and writes will block. @@ -162,6 +307,74 @@ If port is 0 or no port is provided default port 3333 is used. At present there is a hardcoded limit of 16 clients connected at one time. .RE +.TP +.BR " \-\-rs\-485" + +Enable RS-485 mode. + +.TP +.BR " \-\-rs\-485\-config " \fI + +Set the RS-485 configuration using the following key or key value pair format in +the configuration field: + +.RS +.TP 30n +.IP \fBRTS_ON_SEND=value +Set logical level (0 or 1) for RTS pin when sending +.IP \fBRTS_AFTER_SEND=value +Set logical level (0 or 1) for RTS pin after sending +.IP \fBRTS_DELAY_BEFORE_SEND=value +Set RTS delay (ms) before sending +.IP \fBRTS_DELAY_AFTER_SEND=value +Set RTS delay (ms) after sending +.IP \fBRX_DURING_TX +Receive data even while sending data +.P +If defining more than one key or key value pair, they must be comma separated. +.RE + +.TP +.BR "\-\-alert none|bell|blink" + +Set alert action on connect/disconnect. + +It will sound the bell once or blink once on successful connect. Likewise it +will sound the bell twice or blink twice on disconnect. + +Default value is "none". + +.TP +.BR "\-\-mute" + +Mute tio messages. + +.TP +.BR "\-\-script \fI + +Run script from string. + +.TP +.BR "\-\-script\-file \fI + +Run script from file with filename. + +.TP +.BR "\-\-script\-run once|always|never" + +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 @@ -170,10 +383,10 @@ Display program version. .BR \-h ", " \-\-help Display help. -.SH "KEYS" +.SH "KEY COMMANDS" .PP .TP 16n -In session, the following key 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" @@ -182,63 +395,147 @@ Send serial break (triggers SysRq on Linux, etc.) Show configuration (baudrate, databits, etc.) .IP "\fBctrl-t e" Toggle local echo mode -.IP "\fBctrl-t h" -Toggle hexadecimal mode +.IP "\fBctrl-t f" +Toggle log to file +.IP "\fBctrl-t F" +Flush data I/O buffers (discard data written but not transmitted and data received but not read) +.IP "\fBctrl-t g" +Toggle serial port line +.IP "\fBctrl-t i" +Toggle input mode .IP "\fBctrl-t l" Clear screen +.IP "\fBctrl-t L" +Show line states (DTR, RTS, CTS, DSR, DCD, RI) +.IP "\fBctrl-t m" +Change mapping of characters on input or output +.IP "\fBctrl-t o" +Toggle output mode +.IP "\fBctrl-t p" +Pulse serial port line .IP "\fBctrl-t q" 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" -Send ctrl-t key code -.IP "\fBctrl-t L" -Show line states (DTR, RTS, CTS, DSR, DCD, RI) -.IP "\fBctrl-t d" -Toggle DTR -.IP "\fBctrl-t r" -Toggle RTS +Toggle line timestamp mode .IP "\fBctrl-t v" Show version +.IP "\fBctrl-t x" +Send file using the XMODEM-1K or XMODEM-CRC protocol (prompts for file name and protocol) +.IP "\fBctrl-t y" +Send file using the YMODEM protocol (prompts for file name) +.IP "\fBctrl-t ctrl-t" +Send ctrl-t character -.SH "HEXADECIMAL MODE" -.TP -In hexadecimal mode each incoming byte is printed out as a hexadecimal value. -.TP -Bytes can be sent in this mode by typing the \fBtwo-character hexadecimal\fR representation of the value, e.g.: to send \fI0xA\fR you must type \fI0a\fR or \fI0A\fR. +.SH "SCRIPT API" +.PP +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: + +.TP 6n + +.IP "\fBtio.expect(pattern, timeout)" +Waits for the Lua pattern to match or timeout before continuing. +Timeout is in milliseconds, defaults to 0 meaning it will wait forever. + +Returns the captures from the pattern or nil on timeout. + +.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. + +Returns a string up to size bytes long on success and nil on timeout. + +.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. + +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 "\fBtio.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. + +.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 "\fBtio.sleep(seconds)" +Sleep for seconds. +.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 -.TP 16n -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: -.PP -.I $XDG_CONFIG_HOME/tio/tiorc -.PP -.I $HOME/.config/tio/tiorc -.PP -.I $HOME/.tiorc +Options can be set via configuration file using the INI format. \fBtio\fR uses +the configuration file first found in the following locations in the order +listed: -.TP -Labels can be used to group settings into named sub-configurations which can be activated from the command-line when starting tio. +.PP +.I $XDG_CONFIG_HOME/tio/config +.PP +.I $HOME/.config/tio/config +.PP +.I $HOME/.tioconfig -.TP -.TP -tio will try to match the user input to a sub-configuration by name or by pattern to get the tty and other options. +.PP +Labels can be used to group settings into named configuration profiles which +can be activated from the command-line when starting tio. -.TP +.PP +\fBtio\fR will try to match the user input to a configuration profile by name or by +pattern to get the TTY device and other options. + +.PP Options without any label change the default options. -.TP -Any options set via command-line will override options set in the configuraqtion file. +.PP +Any options set via command-line will override options set in the configuration file. -.TP +.PP The following configuration file options are available: -.TP 20n +.TP 25n .IP "\fBpattern" Pattern matching user input. This pattern can be an extended regular expression with a single group. -.IP "\fBtty" -tty device to open. If it contains a "%s" it is substituted with the first group match. +.IP "\fBdevice" +TTY device to open. If it contains a "%s" it is substituted with the first group match. .IP "\fBbaudrate" Set baud rate .IP "\fBdatabits" @@ -250,13 +547,21 @@ Set stop bits .IP "\fBparity" Set parity .IP "\fBoutput-delay" -Set output delay -.IP "\fBno-autoconnect" -Disable automatic connect +Set output character delay +.IP "\fBoutput-line-delay" +Set output line delay +.IP "\fBline-pulse-duration" +Set line pulse duration +.IP "\fBno-reconnect" +Do not reconnect .IP "\fBlog" Enable log to file .IP "\fBlog-file" Set log filename +.IP "\fBlog-directory" +Set log directory path in which to save automatically named log files. +.IP "\fBlog-append" +Append to log file .IP "\fBlog-strip" Enable strip of control and escape sequences from log .IP "\fBlocal-echo" @@ -265,14 +570,40 @@ Enable local echo Enable line timestamp .IP "\fBtimestamp-format" Set timestamp format +.IP "\fBtimestamp-timeout" +Set timestamp timeout .IP "\fBmap" -Map special characters on input or output +Map characters on input or output .IP "\fBcolor" Colorize tio text using ANSI color code ranging from 0 to 255 -.IP "\fBhexadecimal" -Enable hexadecimal mode +.IP "\fBinput-mode" +Set input mode +.IP "\fBoutput-mode" +Set output mode .IP "\fBsocket" Set socket to redirect I/O to +.IP "\fBprefix-ctrl-key" +Set prefix ctrl key (a..z or 'none', default: t) +.IP "\fBrs-485" +Enable RS-485 mode +.IP "\fBrs-485-config" +Set RS-485 configuration +.IP "\fBalert" +Set alert action on connect/disconnect +.IP "\fBmute" +Mute tio messages +.IP "\fBscript" +Run script from string +.IP "\fBscript-file" +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" @@ -282,34 +613,35 @@ To change the default configuration simply set options like so: .RS .nf .eo -# Defaults -baudrate = 115200 +[default] +baudrate = 9600 databits = 8 parity = none stopbits = 1 color = 10 +line-pulse-duration = DTR=200,RTS=400 .ec .fi .RE .TP -Named sub-configurations can be added via labels: +Named configuration profiles can be added via labels: .RS .nf .eo -[ftdi] +[rpi3] +device = /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 baudrate = 115200 -tty = /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 color = 11 .ec .fi .RE .TP -Activate the sub-configuration by name: +Activate the configuration profile by name: -$ tio ftdi +$ tio rpi3 .TP Which is equivalent to: @@ -317,59 +649,59 @@ Which is equivalent to: $ tio -b 115200 -c 11 /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 .TP -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: .RS .nf .eo -[usb device] -pattern = usb([0-9]*) +[usb-devices] +pattern = ^usb([0-9]*) +device = /dev/ttyUSB%m1 baudrate = 115200 -tty = /dev/ttyUSB%s .ec .fi .RE .TP -Activate the sub-configuration by pattern match: +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 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 .SH "EXAMPLES" .TP -Typical use is without options. For example: +Typical use is without options: $ tio /dev/ttyUSB0 .TP -Which corresponds to the commonly used options: +Which corresponds to the commonly used default options: $ tio \-b 115200 \-d 8 \-f none \-s 1 \-p none /dev/ttyUSB0 .TP -It is recommended to connect serial tty devices by ID. For example: +It is recommended to connect serial TTY devices by ID: $ tio /dev/serial/by\-id/usb\-FTDI_TTL232R-3V3_FTGQVXBL\-if00\-port0 .PP Using serial devices by ID ensures that tio automatically reconnects to the -correct serial device if the device is disconnected and then reconnected. +correct serial device if it is disconnected and then reconnected. .TP -Redirect serial device I/O to Unix file socket. For example: +Redirect serial device I/O to Unix file socket for scripting: -$ tio -S unix:/tmp/tmux-socket0 /dev/ttyUSB0 +$ tio -S unix:/tmp/tio-socket0 /dev/ttyUSB0 .TP -Then, to issue a command via the file socket simply do for example: +Then, to issue a command via the file socket simply do: -$ echo "ls -lah" | nc -UN /tmp/tmux-socket0 > /dev/null +$ echo "ls -la" | nc -UN /tmp/tio-socket0 > /dev/null .TP Or use the expect command to script an interaction: @@ -378,25 +710,67 @@ Or use the expect command to script an interaction: .nf .eo #!/usr/bin/expect -f - +.sp set timeout -1 log_user 0 - -spawn nc -UN /path/to/socket +.sp +spawn nc -UN /tmp/tio-socket0 set uart $spawn_id - -send -i $uart "command1\n" +.sp +send -i $uart "date\n" expect -i $uart "prompt> " -send -i $uart "command2\n" +send -i $uart "ls -la\n" expect -i $uart "prompt> " .ec .fi .RE .TP -It is also possible to pipe data or command to the serial device. For example: +It is also possible to use tio's own simpler expect/send script functionality to e.g. automate logins: -$ echo "ls -lah" | tio /dev/serial/by\-id/usb\-FTDI_TTL232R-3V3_FTGQVXBL\-if00\-port0 +$ 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: + +$ tio --socket inet:4444 /dev/ttyUSB0 + +.TP + +Then, use netcat to connect to the shared TTY session over network (assuming tio is hosted on IP 10.0.0.42): + +$ nc -N 10.0.0.42 4444 + +.TP +Pipe command to the serial device: + +$ echo "ls -la" | tio /dev/serial/by\-id/usb\-FTDI_TTL232R-3V3_FTGQVXBL\-if00\-port0 + +.TP +Pipe command to serial device and wait for line response within 1 second: + +$ echo "*IDN?" | tio /dev/ttyACM0 --script "tio.expect('\\r\\n', 1000)" --mute +.TP + +.TP +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: + +$ 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 "tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{RTS=toggle}" --script-run once /dev/ttyUSB0 .SH "WEBSITE" .PP @@ -404,4 +778,4 @@ Visit https://tio.github.io .SH "AUTHOR" .PP -Created by Martin Lund . +Maintained by Martin Lund . diff --git a/man/tio.1.txt b/man/tio.1.txt new file mode 100644 index 0000000..eeac82c --- /dev/null +++ b/man/tio.1.txt @@ -0,0 +1,625 @@ +tio(1) User Commands tio(1) + +NAME + tio - a serial device I/O tool + +SYNOPSIS + 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. + +OPTIONS + -b, --baudrate + + Set baud rate [bps] (default: 115200). + + -d, --databits 5|6|7|8 + + Set data bits (default: 8). + + -f, --flow hard|soft|none + + Set flow control (default: none). + + -s, --stopbits 1|2 + + Set stop bits (default: 1). + + -p, --parity odd|even|none|mark|space + + Set parity (default: none). + + Note: With mark parity the parity bit is always 0. With space parity the parity bit is always 1. Not all platforms support mark and space parity. + + -o, --output-delay + + Set output delay [ms] inserted between each sent character (default: 0). + + -O, --output-line-delay + + Set output delay [ms] inserted between each sent line (default: 0). + + --line-pulse-duration + + Set the pulse duration [ms] of each serial port line using the following key value pair format in the duration field: = + + Each key represents a serial line. The following keys are available: + + DTR Data Terminal Ready + + RTS Request To Send + + CTS Clear To Send + + DSR Data Set Ready + + DCD Data Carrier Detect + + RI Ring Indicator + + If defining more than one key value pair, the pairs must be comma separated. + + The default pulse duration for each line is 100 ms. + + -a, --auto-connect new|latest|direct + + Automatically connect to serial device according to one of the following strategies: + + new Automatically connect to first new appearing serial device. + + latest Automatically connect to latest registered serial device. + + direct Connect directly to specified TTY device. + + All the listed strategies automatically reconnects according to strategy if device is not available or connection is lost. + + Default value is "direct". + + --exclude-devices + + Exclude devices by pattern ('*' and '?' supported). + + --exclude-drivers + + Exclude drivers by pattern ('*' and '?' supported). + + --exclude-tids + + Exclude topology IDs by pattern ('*' and '?' supported). + + -n, --no-reconnect + + Do not reconnect. + + This means that tio will exit if it fails to connect to device or an established connection is lost. + + -e, --local-echo + + Enable local echo. + + -t, --timestamp + + Enable line timestamp. + + --timestamp-format + + Set timestamp format to any of the following timestamp formats: + + 24hour 24-hour format ("hh:mm:ss.sss") + + 24hour-start 24-hour format relative to start time + + 24hour-delta 24-hour format relative to previous timestamp + + 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 + + Set timestamp timeout value in milliseconds. + + This value only takes effect in hex output mode where timestamps are only printed after elapsed timeout time of no output activity from tty device. + + Default value is 200. + + -l, --list + + 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_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. + + --log-file + + Set log filename. + + --log-directory + + Set log directory path in which to save automatically named log files. + + --log-append + + Append to log file. + + --log-strip + + Strip control characters and escape sequences from log. + + -m, --map + + 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) + + IGNCR Ignore CR on input + + IFFESCC Map FF to ESC-c on input + + INLCR Map NL to CR on input + + 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 + + ONLCRNL Map NL to CR-NL on output + + OLTU Map lowercase characters to uppercase on output + + ONULBRK Map nul (zero) to send break signal on output + + OIGNCR Ignore CR on output + + If defining more than one flag, the flags must be comma separated. + + --input-mode normal|hex|line + + Set input mode. + + In normal mode input characters are sent immediately as they are typed. + + In hex input mode bytes can be sent by typing the two-character hexadecimal representation of the 1 byte value, e.g.: to send 0xA you must type 0a or 0A. + + In line input mode input characters are sent when you press enter. The only editing feature supported in this mode is backspace. + + Default value is "normal". + + --output-mode normal|hex|hexN + + 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". + + -c, --color 0..255|bold|none|list + + Colorize tio text using ANSI color code value ranging from 0 to 255 or use "none" for no color or use "bold" to apply bold formatting to existing system color. + + Use "list" to print a list of available ANSI color codes. + + Default value is "bold". + + -S, --socket + + 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 multi‐ + plexed to the terminal and all connected clients. + + Sockets remain open while the serial port is disconnected, and writes will block. + + Various socket types are supported using the following prefixes in the socket field: + + unix: Unix Domain Socket (file) + + inet: Internet Socket (network) + + inet6: Internet IPv6 Socket (network) + + If port is 0 or no port is provided default port 3333 is used. + + At present there is a hardcoded limit of 16 clients connected at one time. + + --rs-485 + + Enable RS-485 mode. + + --rs-485-config + + Set the RS-485 configuration using the following key or key value pair format in the configuration field: + + RTS_ON_SEND=value Set logical level (0 or 1) for RTS pin when sending + + RTS_AFTER_SEND=value Set logical level (0 or 1) for RTS pin after sending + + RTS_DELAY_BEFORE_SEND=value Set RTS delay (ms) before sending + + RTS_DELAY_AFTER_SEND=value Set RTS delay (ms) after sending + + RX_DURING_TX Receive data even while sending data + + If defining more than one key or key value pair, they must be comma separated. + + --alert none|bell|blink + + Set alert action on connect/disconnect. + + It will sound the bell once or blink once on successful connect. Likewise it will sound the bell twice or blink twice on disconnect. + + Default value is "none". + + --mute + + Mute tio messages. + + --script + + Run script from string. + + --script-file + + Run script from file with filename. + + --script-run once|always|never + + Run script on connect once, always, or never. + + 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. + + -h, --help + + Display help. + +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 + + ctrl-t b Send serial break (triggers SysRq on Linux, etc.) + + ctrl-t c Show configuration (baudrate, databits, etc.) + + ctrl-t e Toggle local echo mode + + ctrl-t f Toggle log to file + + ctrl-t F Flush data I/O buffers (discard data written but not transmitted and data received but not read) + + ctrl-t g Toggle serial port line + + ctrl-t i Toggle input mode + + ctrl-t l Clear screen + + ctrl-t L Show line states (DTR, RTS, CTS, DSR, DCD, RI) + + ctrl-t m Change mapping of characters on input or output + + ctrl-t o Toggle output mode + + ctrl-t p Pulse serial port line + + ctrl-t q Quit + + 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 v Show version + + ctrl-t x Send file using the XMODEM-1K or XMODEM-CRC protocol (prompts for file name and protocol) + + ctrl-t y Send file using the YMODEM protocol (prompts for file name) + + ctrl-t ctrl-t Send ctrl-t character + +SCRIPT API + Tio suppots Lua scripting to easily automate interaction with the tty device. + + In addition to the standard Lua API tio makes the following functions available: + + expect(string, timeout) + Expect string - waits for string to match or timeout before continuing. Supports regular expressions. Special characters must be escaped with '\\'. Timeout is in milliseconds, defaults to 0 meaning it will wait forever. + + 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. + + 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 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. + + 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", "dri‐ + ver", "description". + + Returns nil if no serial devices are found. + + 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 milliseconds. + + 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: + + $XDG_CONFIG_HOME/tio/config + + $HOME/.config/tio/config + + $HOME/.tioconfig + + 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 configuration profile by name or by pattern to get the TTY device and other options. + + Options without any label change the default options. + + Any options set via command-line will override options set in the configuration file. + + The following configuration file options are available: + + pattern Pattern matching user input. This pattern can be an extended regular expression with a single group. + + device TTY device to open. If it contains a "%s" it is substituted with the first group match. + + baudrate Set baud rate + + databits Set data bits + + flow Set flow control + + stopbits Set stop bits + + parity Set parity + + output-delay Set output character delay + + output-line-delay Set output line delay + + line-pulse-duration Set line pulse duration + + no-reconnect Do not reconnect + + log Enable log to file + + log-file Set log filename + + log-directory Set log directory path in which to save automatically named log files. + + log-append Append to log file + + log-strip Enable strip of control and escape sequences from log + + local-echo Enable local echo + + timestamp Enable line timestamp + + timestamp-format Set timestamp format + + timestamp-timeout Set timestamp timeout + + map Map characters on input or output + + color Colorize tio text using ANSI color code ranging from 0 to 255 + + input-mode Set input mode + + output-mode Set output mode + + socket Set socket to redirect I/O to + + prefix-ctrl-key Set prefix ctrl key (a..z or 'none', default: t) + + rs-485 Enable RS-485 mode + + rs-485-config Set RS-485 configuration + + alert Set alert action on connect/disconnect + + mute Mute tio messages + + script Run script from string + + script-file Run script from 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: + + [default] + baudrate = 9600 + databits = 8 + parity = none + stopbits = 1 + color = 10 + line-pulse-duration = DTR=200,RTS=400 + + 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 configuration profile by name: + + $ tio rpi3 + + Which is equivalent to: + + $ tio -b 115200 -c 11 /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 + + A configuration profile can also be activated by its pattern which supports regular expressions: + + [usb-devices] + pattern = ^usb([0-9]*) + device = /dev/ttyUSB%m1 + baudrate = 115200 + + Activate the configuration profile by pattern match: + + $ tio usb12 + + Which becomes equivalent to: + + $ tio -b 115200 /dev/ttyUSB12 + + It is also possible to combine use of configuration profile and command-line options. For example: + + $ tio -l -t usb12 + +EXAMPLES + Typical use is without options: + + $ tio /dev/ttyUSB0 + + Which corresponds to the commonly used default options: + + $ tio -b 115200 -d 8 -f none -s 1 -p none /dev/ttyUSB0 + + It is recommended to connect serial TTY devices by ID: + + $ tio /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 + + Using serial devices by ID ensures that tio automatically reconnects to the correct serial device if it is disconnected and then reconnected. + + Redirect serial device I/O to Unix file socket for scripting: + + $ tio -S unix:/tmp/tio-socket0 /dev/ttyUSB0 + + Then, to issue a command via the file socket simply do: + + $ echo "ls -la" | nc -UN /tmp/tio-socket0 > /dev/null + + Or use the expect command to script an interaction: + + #!/usr/bin/expect -f + + set timeout -1 + log_user 0 + + spawn nc -UN /tmp/tio-socket0 + set uart $spawn_id + + send -i $uart "date\n" + expect -i $uart "prompt> " + 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 + + Then, use netcat to connect to the shared TTY session over network (assuming tio is hosted on IP 10.0.0.42): + + $ nc -N 10.0.0.42 4444 + + Pipe command to the serial device: + + $ echo "ls -la" | tio /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 + + Pipe command to serial device and wait for line response within 1 second: + + $ echo "*IDN?" | tio /dev/ttyACM0 --script "expect('\r\n', 1000)" --mute + + 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 + + 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 "set{DTR=high,RTS=low}; msleep(100); set{RTS=toggle}" --script-run once /dev/ttyUSB0 + +WEBSITE + Visit https://tio.github.io + +AUTHOR + Maintained by Martin Lund . + +tio 3.9 2025-04-13 tio(1) diff --git a/meson.build b/meson.build index 0266f9b..b95b5c0 100644 --- a/meson.build +++ b/meson.build @@ -1,12 +1,12 @@ project('tio', 'c', - version : '1.41', - 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 = '2022-07-04' +version_date = '2025-04-13' # Test for dynamic baudrate configuration interface compiler = meson.get_compiler('c') @@ -71,5 +71,17 @@ foreach rate : test_baudrates endif endforeach +# Test for RS-485 support on Linux +enable_rs485 = false +if host_machine.system() == 'linux' + if compiler.check_header('linux/serial.h') + enable_rs485 = compiler.has_header_symbol('sys/ioctl.h', 'TIOCSRS485') + endif +endif + subdir('src') -subdir('man') + +install_man_pages = get_option('install_man_pages') +if install_man_pages + subdir('man') +endif diff --git a/meson_options.txt b/meson_options.txt index ddb4225..51b7ca0 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,6 @@ option('bashcompletiondir', type : 'string', description : 'Directory for bash completion scripts ["no" disables]') +option('install_man_pages', + type : 'boolean', value: true, + description : 'Install man pages') diff --git a/src/alert.c b/src/alert.c new file mode 100644 index 0000000..23a728e --- /dev/null +++ b/src/alert.c @@ -0,0 +1,83 @@ +/* + * tio - a serial device I/O tool + * + * Copyright (c) 2014-2022 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 +#include +#include "options.h" +#include "alert.h" + +void blink_background(void) +{ + // Turn on reverse video + printf("\e[?5h"); + fflush(stdout); + + usleep(200*1000); + + // Turn on normal video + printf("\e[?5l"); + fflush(stdout); +} + +void sound_bell(void) +{ + // Audio bell + printf("\a"); + fflush(stdout); +} + +void alert_connect(void) +{ + switch (option.alert) + { + case ALERT_NONE: + break; + case ALERT_BELL: + sound_bell(); + break; + case ALERT_BLINK: + blink_background(); + break; + default: + break; + } +} + +void alert_disconnect(void) +{ + switch (option.alert) + { + case ALERT_NONE: + break; + case ALERT_BELL: + sound_bell(); + usleep(200*1000); + sound_bell(); + break; + case ALERT_BLINK: + blink_background(); + usleep(200*1000); + blink_background(); + break; + default: + break; + } +} diff --git a/src/alert.h b/src/alert.h new file mode 100644 index 0000000..91258c9 --- /dev/null +++ b/src/alert.h @@ -0,0 +1,33 @@ +/* + * tio - a serial device I/O tool + * + * Copyright (c) 2014-2022 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 + +typedef enum +{ + ALERT_NONE, + ALERT_BELL, + ALERT_BLINK, + ALERT_END, +} alert_t; + +void alert_connect(void); +void alert_disconnect(void); diff --git a/src/bash-completion/tio.in b/src/bash-completion/tio.in index 1710ecc..b3b61cb 100644 --- a/src/bash-completion/tio.in +++ b/src/bash-completion/tio.in @@ -16,18 +16,37 @@ _tio() -s --stopbits \ -p --parity \ -o --output-delay \ - -n --no-autoconnect \ + -o --output-line-delay \ + --line-pulse-duration \ + -a --auto-connect \ + --exclude-devices \ + --exclude-drivers \ + --exclude-tids \ + -n --no-reconnect \ -e --local-echo \ -l --log \ --log-file \ + --log-directory \ + --log-append \ --log-strip \ -m --map \ -t --timestamp \ --timestamp-format \ - -L --list-devices \ + --timestamp-timeout \ + -L --list \ -c --color \ -S --socket \ - -x --hexadecimal \ + --input-mode \ + --output-mode \ + --rs-485 \ + --rs-485-config \ + --alert \ + --mute \ + --script \ + --script-file \ + --script-run \ + --exec \ + --complete-profiles \ -v --version \ -h --help" @@ -55,43 +74,23 @@ _tio() return 0 ;; -o | --output-delay) - COMPREPLY=( $(compgen -W "0 1 10 100" -- ${cur}) ) + COMPREPLY=( $(compgen -W "1 10 100" -- ${cur}) ) return 0 ;; - -n | --no-autoconnect) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + -O | --output-line-delay) + COMPREPLY=( $(compgen -W "1 10 100" -- ${cur}) ) return 0 ;; - -e | --local-echo) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - -l | --log) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - -l | --log-file) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - ;; - -l | --log-strip) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + -a | --auto-connect) + COMPREPLY=( $(compgen -W "new latest none" -- ${cur}) ) return 0 ;; -m | --map) - COMPREPLY=( $(compgen -W "ICRNL IGNCR INLCR INLCRNL OCRNL ODELBS ONLCRNL" -- ${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 iso8601" -- ${cur}) ) - return 0 - ;; - -L | --list-devices) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + COMPREPLY=( $(compgen -W "24hour 24hour-start 24hour-delta iso8601 epoch epoch-usec" -- ${cur}) ) return 0 ;; -c | --color) @@ -102,16 +101,24 @@ _tio() COMPREPLY=( $(compgen -W "unix: inet: inet6:" -- ${cur}) ) return 0 ;; - -x | --hexadecimal) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + --input-mode) + COMPREPLY=( $(compgen -W "normal hex line" -- ${cur}) ) return 0 ;; - -v | --version) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + --output-mode) + COMPREPLY=( $(compgen -W "normal hex" -- ${cur}) ) return 0 ;; - -h | --help) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + --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 + ;; + --alert) + COMPREPLY=( $(compgen -W "none bell blink" -- ${cur}) ) + return 0 + ;; + --script-run) + COMPREPLY=( $(compgen -W "once always never" -- ${cur}) ) return 0 ;; *) @@ -124,12 +131,14 @@ _tio() ;; esac + profiles="`tio --complete-profiles`" + if [ -d /dev/serial/by-id ]; then ttys=$(printf '%s\n' /dev/tty* /dev/serial/by-id/*) else ttys=$(printf '%s\n' /dev/tty*) fi - COMPREPLY=( $(compgen -W "${ttys}" -- ${cur}) ) + COMPREPLY=( $(compgen -W "${ttys} ${profiles}" -- ${cur}) ) return 0 } diff --git a/src/configfile.c b/src/configfile.c index 4acd4e9..ca116bf 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2020-2022 Liam Beguin * Copyright (c) 2022 Martin Lund @@ -21,379 +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 "misc.h" -static struct config_t *c; +#define CONFIG_GROUP_NAME_DEFAULT "default" +#define CONFIG_GROUP_INCLUDE_PREFIX "include " +#define MAX_LINE_LENGTH 1024 -static int get_match(const char *input, const char *pattern, char **match) +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, ...) { - int ret; - int len = 0; - regex_t re; - regmatch_t m[2]; - char err[128]; + (void)dest; + GError *error = NULL; + bool mismatch = true; - /* 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)); - fprintf(stderr, "regex error: %s", err); - return ret; + 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); } - /* try to match on input */ - ret = regexec(&re, input, 2, m, 0); - if (!ret) + va_list args; + const char* current_arg = allowed_string; + va_start(args, allowed_string); + + if (current_arg == NULL) { - len = m[1].rm_eo - m[1].rm_so; + mismatch = false; } - regfree(&re); - - if (len) + // Iterate through variable arguments + while (current_arg != NULL) { - asprintf(match, "%s", &input[m[1].rm_so]); + if (strcmp(string, current_arg) == 0) + { + mismatch = false; + break; + } + current_arg = va_arg(args, const char *); } - return len; + 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, "tty")) + if (error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND) { - asprintf(&c->tty, value, c->match); - option.tty_device = c->tty; + // Key not found - ignore key + g_error_free(error); + return; } - else if (!strcmp(name, "baudrate")) - { - option.baudrate = string_to_long((char *)value); - } - else if (!strcmp(name, "databits")) - { - option.databits = atoi(value); - } - else if (!strcmp(name, "flow")) - { - asprintf(&c->flow, "%s", value); - option.flow = c->flow; - } - else if (!strcmp(name, "stopbits")) - { - option.stopbits = atoi(value); - } - else if (!strcmp(name, "parity")) - { - asprintf(&c->parity, "%s", value); - option.parity = c->parity; - } - else if (!strcmp(name, "output-delay")) - { - option.output_delay = atoi(value); - } - else if (!strcmp(name, "no-autoconnect")) - { - if (!strcmp(value, "enable")) - { - option.no_autoconnect = true; - } - else if (!strcmp(value, "disable")) - { - option.no_autoconnect = false; - } - } - else if (!strcmp(name, "log")) - { - if (!strcmp(value, "enable")) - { - option.log = true; - } - else if (!strcmp(value, "disable")) - { - option.log = false; - } - } - else if (!strcmp(name, "log-file")) - { - asprintf(&c->log_filename, "%s", value); - option.log_filename = c->log_filename; - } - else if (!strcmp(name, "log-strip")) - { - if (!strcmp(value, "enable")) - { - option.log_strip = true; - } - else if (!strcmp(value, "disable")) - { - option.log_strip = false; - } - } - else if (!strcmp(name, "local-echo")) - { - if (!strcmp(value, "enable")) - { - option.local_echo = true; - } - else if (!strcmp(value, "disable")) - { - option.local_echo = false; - } - } - else if (!strcmp(name, "hexadecimal")) - { - if (!strcmp(value, "enable")) - { - option.hex_mode = true; - } - else if (!strcmp(value, "disable")) - { - option.hex_mode = false; - } - } - else if (!strcmp(name, "timestamp")) - { - if (!strcmp(value, "enable")) - { - option.timestamp = TIMESTAMP_24HOUR; - } - else if (!strcmp(value, "disable")) - { - option.timestamp = TIMESTAMP_NONE; - } - } - else if (!strcmp(name, "timestamp-format")) - { - option.timestamp = timestamp_option_parse(value); - } - 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; - } + tio_error_print("%s: %s", config.path, error->message); + g_error_free(error); + exit(EXIT_FAILURE); + } - if (!strcmp(value, "none")) - { - option.color = -1; // No color - return 0; - } + if ((value < min) || (value > max)) + { + tio_error_print("%s: Invalid %s value '%d' in %s profile", config.path, key, value, group); + exit(EXIT_FAILURE); + } - option.color = atoi(value); + *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) + { + // 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); + } + + *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) + { + option_parse_timestamp(string, &option.timestamp); + g_free((void *)string); + string = NULL; + } + } + 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) + { + // Ignore + } + else if (strcmp(string, "none") == 0) + { + option.color = -1; // No color + } + else if (strcmp(string, "bold") == 0) + { + option.color = 256; // Bold + } + else + { + option.color = atoi(string); if ((option.color < 0) || (option.color > 255)) { - option.color = -1; // No color + tio_error_print("%s: Invalid color value in %s profile", config.path, group); + exit(EXIT_FAILURE); } } - else if (!strcmp(name, "socket")) + 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) { - asprintf(&c->socket, "%s", value); - option.socket = c->socket; + 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; + } +} + +static int config_file_resolve(void) +{ + char *xdg = getenv("XDG_CONFIG_HOME"); + if (xdg) + { + if (asprintf(&config.path, "%s/tio/config", xdg) != -1) + { + if (access(config.path, F_OK) == 0) + { + return 0; + } + free(config.path); } } - 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)) + char *home = getenv("HOME"); + if (home) { - /* 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); + if (asprintf(&config.path, "%s/.config/tio/config", home) != -1) + { + if (access(config.path, F_OK) == 0) + { + return 0; + } + free(config.path); + } + + if (asprintf(&config.path, "%s/.tioconfig", home) != -1) + { + if (access(config.path, F_OK) == 0) + { + return 0; + } + free(config.path); + } } - 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 resolve_config_file(void) -{ - asprintf(&c->path, "%s/tio/tiorc", getenv("XDG_CONFIG_HOME")); - if (!access(c->path, F_OK)) - { - return 0; - } - - free(c->path); - - asprintf(&c->path, "%s/.config/tio/tiorc", getenv("HOME")); - if (!access(c->path, F_OK)) - { - return 0; - } - - free(c->path); - - asprintf(&c->path, "%s/.tiorc", getenv("HOME")); - if (!access(c->path, F_OK)) - { - return 0; - } - - free(c->path); - - c->path = NULL; - + config.path = NULL; return -EINVAL; } -void config_file_parse(void) +void config_file_show_profiles(void) { - int ret; + GString *config_buffer; + GError *error = NULL; + GKeyFile *keyfile; - c = malloc(sizeof(struct config_t)); - memset(c, 0, sizeof(struct config_t)); + // 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; } - // Set user input which may be tty device or sub config - c->user = option.tty_device; + // Load content of configuration file into buffer + config_buffer = g_string_new(NULL); + config_file_load(config.path, config_buffer, false); - if (!c->user) + // 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) +{ + // Find config file + if (config_file_resolve() != 0) + { + // None found - stop parsing + return; + } + + 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) { - fprintf(stderr, "Error: 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++) { - 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 (sub config) - ret = ini_parse(c->path, data_handler, NULL); - if (ret < 0) - { - fprintf(stderr, "Error: 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->tty); - free(c->flow); - free(c->parity); - free(c->log_filename); - free(c->map); - - free(c->match); - free(c->section_name); - free(c->path); - - free(c); + 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(" Path: %s", c->path); - if (c->section_name != NULL) + tio_printf(" Active configuration file: %s", config.path); + if (config.active_group != NULL) { - tio_printf(" Active sub-configuration: %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 8fbea07..2cb9f15 100644 --- a/src/configfile.h +++ b/src/configfile.h @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2020 Liam Beguin * Copyright (c) 2022 Martin Lund @@ -24,20 +24,15 @@ struct config_t { - const char *user; - - char *path; - char *section_name; - char *match; - - char *tty; - char *flow; - char *parity; - char *log_filename; - char *socket; - char *map; + 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 8e25a8c..9f6bac2 100644 --- a/src/error.c +++ b/src/error.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -19,27 +19,79 @@ * 02110-1301, USA. */ -#include "config.h" +#define _GNU_SOURCE // To access vasprintf #include -#include -#include -#include -#include "options.h" #include "print.h" -#include "error.h" -char error[2][1000]; +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) +{ + in_session = true; +} + +void error_printf_(const char *format, ...) +{ + va_list args; + char *line; + + va_start(args, format); + vasprintf(&line, format, args); + + if (in_session) + { + if (print_tainted) + { + putchar('\n'); + } + ansi_error_printf("[%s] %s", timestamp_current_time(), line); + } + else + { + fprintf(stderr, "%s\n", line); + } + + va_end(args); + + print_tainted = false; + free(line); +} + +void tio_error_printf(const char *format, ...) +{ + va_list args; + + va_start(args, format); + vsnprintf(error[0], 1000, format, args); + va_end(args); +} + +void tio_error_printf_silent(const char *format, ...) +{ + va_list args; + + va_start(args, format); + vsnprintf(error[1], 1000, format, args); + va_end(args); +} void error_exit(void) { - if (error[0][0] != 0) - { - /* Print error */ - tio_error_printf("Error: %s", error[0]); - } - else if ((error[1][0] != 0) && (option.no_autoconnect)) - { - /* Print silent error */ - tio_error_printf("Error: %s", error[1]); - } + if (error[0][0] != 0) + { + /* Print error */ + error_printf_("Error: %s", error[0]); + } + else if ((error[1][0] != 0) && (option.no_reconnect)) + { + /* Print silent error */ + error_printf_("Error: %s", error[1]); + } } diff --git a/src/error.h b/src/error.h index a7c9d45..9a257fc 100644 --- a/src/error.h +++ b/src/error.h @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -21,9 +21,15 @@ #pragma once +#include + +extern bool error_normal; + #define TIO_SUCCESS 0 #define TIO_ERROR 1 -extern char error[2][1000]; - +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 new file mode 100644 index 0000000..740d9a0 --- /dev/null +++ b/src/fs.c @@ -0,0 +1,226 @@ +/* + * 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. + */ + +#define _GNU_SOURCE // For statx() +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "error.h" +#include "print.h" +#include "options.h" + +bool fs_dir_exists(const char *path) +{ + struct stat st; + + if (stat(path, &st) != 0) + { + return false; + } + else if (!S_ISDIR(st.st_mode)) + { + return false; + } + + return true; +} + +// Function to read the content of a file but stripped of newline +ssize_t fs_read_file_stripped(char *buf, size_t bufsiz, const char *format, ...) +{ + char filename[PATH_MAX]; + int bytes_printed = 0; + va_list args; + + va_start(args, format); + bytes_printed = vsnprintf(filename, sizeof(filename), format, args); + va_end(args); + + if (bytes_printed < 0) + { + return -1; + } + + FILE *file = fopen(filename, "r"); + if (!file) + { + return -1; + } + ssize_t length = fread(buf, 1, bufsiz - 1, file); + if (length == -1) + { + fclose(file); + return -1; + } + + // Strip any newline + buf[strcspn(buf, "\n")] = 0; + buf[length] = '\0'; // Make sure to null-terminate the string + + fclose(file); + + return length; +} + +bool fs_file_exists(const char *format, ...) +{ + char filename[PATH_MAX]; + int bytes_printed = 0; + struct stat st; + va_list args; + + va_start(args, format); + bytes_printed = vsnprintf(filename, sizeof(filename), format, args); + va_end(args); + + if (bytes_printed < 0) + { + return false; + } + + return stat(filename, &st) == 0; +} + +char* fs_search_directory(const char *dir_path, const char *dirname) +{ + struct dirent *entry; + char path[PATH_MAX]; + struct stat st; + DIR *dir; + + if ((dir = opendir(dir_path)) == NULL) + { + // Error opening directory + return NULL; + } + + while ((entry = readdir(dir)) != NULL) + { + snprintf(path, PATH_MAX, "%s/%s", dir_path, entry->d_name); + + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + { + continue; + } + + if (lstat(path, &st) == -1) + { + // Error getting directory status + closedir(dir); + return NULL; + } + + if (S_ISLNK(st.st_mode)) + { + // Skip symbolic links + continue; + } + + if (S_ISDIR(st.st_mode)) + { + // If it's a directory, check if it's the one we're looking for + if (strcmp(entry->d_name, dirname) == 0) + { + char *result = strndup(path, PATH_MAX); + closedir(dir); + return result; + } + else + { + // Recursively search within directories + char* result = fs_search_directory(path, dirname); + if (result != NULL) + { + closedir(dir); + return result; + } + } + } + } + + closedir(dir); + 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) + { + return -1; + } + + if (statx(fd, "", AT_EMPTY_PATH, STATX_ALL, &stx) != 0) + { + close(fd); + return -1; + } + + // Close the file + close(fd); + + 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/fs.h b/src/fs.h new file mode 100644 index 0000000..fa78f97 --- /dev/null +++ b/src/fs.h @@ -0,0 +1,31 @@ +/* + * 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 + +#include +#include + +bool fs_dir_exists(const char *path); +bool fs_file_exists(const char *format, ...); +char* fs_search_directory(const char *dir_path, const char *dirname); +ssize_t fs_read_file_stripped(char *buf, size_t bufsiz, const char *format, ...); +double fs_get_creation_time(const char *path); diff --git a/src/log.c b/src/log.c index b1577ec..4131617 100644 --- a/src/log.c +++ b/src/log.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -19,28 +19,20 @@ * 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)) #define IS_ESC_END_CHAR(c) ((c >= 0x30) && (c <= 0x7E)) #define IS_CTRL_CHAR(c) ((c >= 0x00) && (c <= 0x1F)) -static FILE *fp; -static bool log_error = false; +static FILE *fp = NULL; static char file_buffer[BUFSIZ]; +static const char *log_filename = NULL; static char *date_time(void) { @@ -56,28 +48,68 @@ static char *date_time(void) return date_time_string; } -void log_open(const char *filename) +int log_open(const char *filename) { - static char automatic_filename[400]; + char *automatic_filename; + char *dir_plus_automatic_filename; if (filename == NULL) { - // Generate filename if none provided ("tio_DEVICE_YYYY-MM-DDTHH:MM:SS.log") - sprintf(automatic_filename, "tio_%s_%s.log", basename((char *)option.tty_device), date_time()); - filename = automatic_filename; - option.log_filename = automatic_filename; + // Generate filename if none provided + if (option.auto_connect == AUTO_CONNECT_DIRECT) + { + // File name format ("tio_TARGET_YYYY-MM-DDTHH:MM:SS.log") + asprintf(&automatic_filename, "tio_%s_%s.log", basename((char *)option.target), date_time()); + } + else + { + // 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", + option_auto_connect_state_to_string(option.auto_connect), + date_time()); + } + + if (option.log_directory != NULL) + { + if (fs_dir_exists(option.log_directory) == false) + { + tio_error_printf("Log directory not found"); + exit(EXIT_FAILURE); + } + + asprintf(&dir_plus_automatic_filename, "%s/%s", option.log_directory, automatic_filename); + filename = dir_plus_automatic_filename; + } + else + { + filename = automatic_filename; + } } - // Open log file in append write mode - fp = fopen(filename, "a+"); + log_filename = filename; + + // Open log file + if (option.log_append) + { + // Append to existing log file + fp = fopen(filename, "a+"); + } + else + { + // Truncate existing log file + fp = fopen(filename, "w+"); + } if (fp == NULL) { - log_error = true; - exit(EXIT_FAILURE); + tio_warning_printf("Could not open log file %s (%s)", filename, strerror(errno)); + return -1; } - // Enable full buffering - setvbuf(fp, file_buffer, _IOFBF, BUFSIZ); + // Enable line buffering + setvbuf(fp, file_buffer, _IOLBF, BUFSIZ); + + return 0; } bool log_strip(char c) @@ -141,6 +173,11 @@ bool log_strip(char c) void log_printf(const char *format, ...) { + if (fp == NULL) + { + return; + } + char *line; va_list(args); @@ -155,11 +192,20 @@ void log_printf(const char *format, ...) void log_putc(char c) { - if (fp != NULL) + if (fp == NULL) + { + return; + } + + if (option.output_mode == OUTPUT_MODE_HEX) + { + fprintf(fp, "%02x ", (unsigned char) c); + } + else { if (option.log_strip) { - if (!log_strip(c)) + if (log_strip(c) == false) { fputc(c, fp); } @@ -176,22 +222,21 @@ void log_close(void) if (fp != NULL) { fclose(fp); + tio_printf("Saved log to file %s", log_filename); + fp = NULL; + log_filename = NULL; } } void log_exit(void) { - if (option.log) + if ((option.log) && (log_filename != NULL)) { log_close(); } - - if (log_error) - { - error_printf("Could not open log file %s (%s)", option.log_filename, strerror(errno)); - } - else if (option.log) - { - tio_printf("Saved log to file %s", option.log_filename); - } +} + +const char *log_get_filename(void) +{ + return log_filename; } diff --git a/src/log.h b/src/log.h index d2a2188..d95d9f3 100644 --- a/src/log.h +++ b/src/log.h @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -21,8 +21,9 @@ #pragma once -void log_open(const char *filename); +int log_open(const char *filename); void log_printf(const char *format, ...); void log_putc(char c); void log_close(void); void log_exit(void); +const char * log_get_filename(void); diff --git a/src/main.c b/src/main.c index 519e8d9..6676bb3 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -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" @@ -45,6 +46,12 @@ int main(int argc, char *argv[]) /* Parse command-line options (1st pass) */ options_parse(argc, argv); + if (option.complete_profiles) + { + config_file_show_profiles(); + return status; + } + /* Parse configuration file */ config_file_parse(); @@ -54,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))) { @@ -65,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))) { @@ -88,11 +102,18 @@ int main(int argc, char *argv[]) /* Initialize ANSI text formatting (colors etc.) */ print_init_ansi_formatting(); + /* Change error printing mode */ + 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-t q to quit"); + tio_printf("Press ctrl-%c q to quit", option.prefix_key); + } else + { + tio_printf("Non-interactive mode enabled"); + tio_printf("Press ctrl-c to quit"); } /* Open socket */ @@ -101,9 +122,16 @@ int main(int argc, char *argv[]) socket_configure(); } + /* Spawn input handling into separate thread */ + tty_input_thread_create(); + + /* Wait for input to be ready */ + tty_input_thread_wait_ready(); + /* Connect to tty device */ - if ((option.no_autoconnect) || (!interactive_mode)) + if (option.no_reconnect) { + tty_search(); status = tty_connect(); } else @@ -112,7 +140,7 @@ int main(int argc, char *argv[]) while (true) { tty_wait_for_device(); - status = tty_connect(); + tty_connect(); } } diff --git a/src/meson.build b/src/meson.build index 38d300a..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) @@ -13,25 +18,55 @@ tio_sources = [ 'print.c', 'configfile.c', 'signals.c', - 'socket.c' + 'socket.c', + 'setspeed.c', + 'rs485.c', + 'timestamp.c', + 'alert.c', + 'xymodem.c', + 'script.c', + 'fs.c', + 'readline.c', + version_h ] -tio_dep = dependency('inih', required: true, - fallback : ['libinih', 'inih_dep'], - default_options: ['default_library=static', 'distro_install=false']) -tio_c_args = ['-Wno-unused-result'] +foreach name: ['lua-5.4', 'lua-5.3', 'lua-5.2', 'lua-5.1', 'lua'] + lua_dep = dependency(name, version: '>=5.1', required: false) + if lua_dep.found() + break + endif +endforeach +if not lua_dep.found() + error('Lua could not be found!') +endif + +tio_dep = [ + dependency('threads', required: true), + dependency('glib-2.0', required: true), + lua_dep +] + +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_sources += 'setspeed2.c' tio_c_args += '-DHAVE_TERMIOS2' endif if enable_iossiospeed - tio_sources += 'iossiospeed.c' tio_c_args += '-DHAVE_IOSSIOSPEED' endif +if enable_rs485 + tio_c_args += '-DHAVE_RS485' +endif + executable('tio', tio_sources, c_args: tio_c_args, diff --git a/src/misc.c b/src/misc.c index b4555ea..bd0a429 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -19,92 +19,235 @@ * 02110-1301, USA. */ -#include "config.h" -#include -#include +#define _GNU_SOURCE // For FNM_EXTMATCH +#include #include -#include -#include +#include +#include +#include +#include #include -#include "error.h" #include "print.h" -#include "options.h" - -#define TIME_STRING_SIZE_MAX 24 - -char *current_time(void) -{ - static char time_string[TIME_STRING_SIZE_MAX]; - static struct timeval tv_start; - static bool first = true; - struct tm *tm; - struct timeval tv; - size_t len; - - gettimeofday(&tv, NULL); - - if (first) - { - tv_start = tv; - first = false; - } - - // Add formatted timestap - switch (option.timestamp) - { - case TIMESTAMP_NONE: - case TIMESTAMP_24HOUR: - // "hh:mm:ss.sss" (24 hour format) - tm = localtime(&tv.tv_sec); - len = strftime(time_string, sizeof(time_string), "%H:%M:%S", tm); - break; - case TIMESTAMP_24HOUR_START: - // "hh:mm:ss.sss" (24 hour format relative to start time) - timersub(&tv, &tv_start, &tv); - tv.tv_sec -= 3600; // Why is this needed?? - tm = localtime(&tv.tv_sec); - len = strftime(time_string, sizeof(time_string), "%H:%M:%S", tm); - break; - case TIMESTAMP_ISO8601: - // "YYYY-MM-DDThh:mm:ss.sss" (ISO-8601) - tm = localtime(&tv.tv_sec); - len = strftime(time_string, sizeof(time_string), "%Y-%m-%dT%H:%M:%S", tm); - break; - default: - return NULL; - } - - // Append milliseconds to all timestamps - if (len) - { - len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%03ld", (long)tv.tv_usec / 1000); - } - - return (len < TIME_STRING_SIZE_MAX) ? time_string : NULL; -} void delay(long ms) { struct timespec ts; + if (ms <= 0) + { + return; + } + ts.tv_sec = ms / 1000; ts.tv_nsec = (ms % 1000) * 1000000; nanosleep(&ts, NULL); } -long string_to_long(char *string) +int ctrl_key_code(unsigned char key) { - long result; - char *end_token; - - errno = 0; - result = strtol(string, &end_token, 10); - if ((errno != 0) || (*end_token != 0)) + if ((key >= 'a') && (key <= 'z')) { - printf("Error: Invalid digit\n"); + return key & ~0x60; + } + + return -1; +} + +bool regex_match(const char *string, const char *pattern) +{ + regex_t regex; + int status; + + if (regcomp(®ex, pattern, REG_EXTENDED | REG_NOSUB) != 0) + { + // No match + return false; + } + + status = regexec(®ex, string, (size_t) 0, NULL, 0); + regfree(®ex); + + if (status != 0) + { + // No match + return false; + } + + // Match + return true; +} + +int read_poll(int fd, void *data, size_t len, int timeout) +{ + struct pollfd fds; + int ret = 0; + + fds.events = POLLIN; + fds.fd = fd; + + /* Wait data available */ + ret = poll(&fds, 1, timeout); + if (ret < 0) + { + tio_error_print("%s", strerror(errno)); + return ret; + } + else if (ret > 0) + { + if (fds.revents & POLLIN) + { + // Read ready data + return read(fd, data, len); + } + } + + /* Timeout */ + return ret; +} + +// Function to calculate djb2 hash of string +unsigned long djb2_hash(const unsigned char *str) +{ + unsigned long hash = 5381; + int c; + + while ((c = *str++)) + { + hash = ((hash << 5) + hash) + c; // hash * 33 + c + } + + return hash; +} + +// Function to encode a number to base62 +void *base62_encode(unsigned long num, char *output) +{ + const char base62_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + if (output == NULL) + { + tio_error_print("Memory allocation failed"); exit(EXIT_FAILURE); } - return result; + for (int i = 0; i < 4; ++i) + { + output[i] = base62_chars[num % 62]; + num /= 62; + } + output[4] = '\0'; + + return output; +} + +// Function to return current time +double get_current_time(void) +{ + struct timespec current_time_ts; + + if (clock_gettime(CLOCK_REALTIME, ¤t_time_ts) == -1) + { + // Error + return -1; + } + + return current_time_ts.tv_sec + current_time_ts.tv_nsec / 1e9; +} + +bool match_patterns(const char *string, const char *patterns) +{ + char *pattern; + char *patterns_copy; + + if ((string == NULL) || (patterns == NULL)) + { + return false; + } + + patterns_copy = strdup(patterns); + + // Tokenize the patterns string using strtok + pattern = strtok(patterns_copy, ","); + while (pattern != NULL) + { + // Check if the string matches the current pattern + #ifdef FNM_EXTMATCH + if (fnmatch(pattern, string, FNM_EXTMATCH) == 0) + #else + if (fnmatch(pattern, string, 0) == 0) + #endif + { + free(patterns_copy); + return true; + } + + // Move to the next pattern + pattern = strtok(NULL, ","); + } + + 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 734cd9b..08ae5eb 100644 --- a/src/misc.h +++ b/src/misc.h @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -21,8 +21,18 @@ #pragma once +#include +#include + #define UNUSED(expr) do { (void)(expr); } while (0) -char * current_time(void); void delay(long ms); -long string_to_long(char *string); +int ctrl_key_code(unsigned char key); +bool regex_match(const char *string, const char *pattern); +unsigned long djb2_hash(const unsigned char *str); +void *base62_encode(unsigned long num, char *output); +int read_poll(int fd, void *data, size_t len, int timeout); +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 85782c9..acff7ce 100644 --- a/src/options.c +++ b/src/options.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -19,89 +19,361 @@ * 02110-1301, USA. */ -#include "config.h" -#include -#include -#include -#include -#include -#include -#include +#define _GNU_SOURCE // To access vasprintf + +#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 "log.h" +#include "configfile.h" + +#define HEX_N_VALUE_MAX 4096 enum opt_t { OPT_NONE, OPT_TIMESTAMP_FORMAT, + OPT_TIMESTAMP_TIMEOUT, OPT_LOG_FILE, + OPT_LOG_DIRECTORY, OPT_LOG_STRIP, + OPT_LOG_APPEND, + OPT_LINE_PULSE_DURATION, + OPT_RS485, + OPT_RS485_CONFIG, + OPT_ALERT, + OPT_COMPLETE_PROFILES, + OPT_MUTE, + OPT_SCRIPT, + OPT_SCRIPT_FILE, + OPT_SCRIPT_RUN, + OPT_INPUT_MODE, + OPT_OUTPUT_MODE, + OPT_EXCLUDE_DEVICES, + OPT_EXCLUDE_DRIVERS, + OPT_EXCLUDE_TIDS, + OPT_EXEC, }; /* Default options */ struct option_t option = { - .tty_device = "", + .target = "", .baudrate = 115200, .databits = 8, - .flow = "none", + .flow = FLOW_NONE, .stopbits = 1, - .parity = "none", + .parity = PARITY_NONE, .output_delay = 0, - .no_autoconnect = false, + .output_line_delay = 0, + .dtr_pulse_duration = 100, + .rts_pulse_duration = 100, + .cts_pulse_duration = 100, + .dsr_pulse_duration = 100, + .dcd_pulse_duration = 100, + .ri_pulse_duration = 100, + .no_reconnect = false, + .auto_connect = AUTO_CONNECT_DIRECT, .log = false, + .log_append = false, .log_filename = NULL, + .log_directory = NULL, .log_strip = false, .local_echo = false, .timestamp = TIMESTAMP_NONE, .socket = NULL, - .map = "", - .color = 15, - .hex_mode = false, + .color = 256, // Bold + .input_mode = INPUT_MODE_NORMAL, + .output_mode = OUTPUT_MODE_NORMAL, + .prefix_code = 20, // ctrl-t + .prefix_key = 't', + .prefix_enabled = true, + .mute = false, + .rs485 = false, + .rs485_config_flags = 0, + .rs485_delay_rts_before_send = -1, + .rs485_delay_rts_after_send = -1, + .alert = ALERT_NONE, + .complete_profiles = false, + .script = NULL, + .script_filename = NULL, + .script_run = SCRIPT_RUN_ALWAYS, + .timestamp_timeout = 200, + .exclude_devices = NULL, + .exclude_drivers = NULL, + .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[]) { - printf("Usage: %s [] \n", argv[0]); + UNUSED(argv); + + printf("Usage: tio [] \n"); printf("\n"); - printf("Connect to tty-device directly or via sub-configuration.\n"); + printf("Connect to TTY device directly or via configuration profile or topology ID.\n"); printf("\n"); printf("Options:\n"); - printf(" -b, --baudrate Baud rate (default: 115200)\n"); - printf(" -d, --databits 5|6|7|8 Data bits (default: 8)\n"); - printf(" -f, --flow hard|soft|none Flow control (default: none)\n"); - printf(" -s, --stopbits 1|2 Stop bits (default: 1)\n"); - printf(" -p, --parity odd|even|none Parity (default: none)\n"); - printf(" -o, --output-delay Output delay (default: 0)\n"); - printf(" -n, --no-autoconnect Disable automatic connect\n"); - printf(" -e, --local-echo Enable local echo\n"); - printf(" -t, --timestamp Enable line timestamp\n"); - printf(" --timestamp-format Set timestamp format (default: 24hour)\n"); - printf(" -L, --list-devices List available serial devices\n"); - printf(" -l, --log Enable log to file\n"); - printf(" --log-file Set log filename\n"); - printf(" --log-strip Strip control characters and escape sequences\n"); - printf(" -m, --map Map special characters\n"); - printf(" -c, --color 0..255|none|list Colorize tio text (default: 15)\n"); - printf(" -S, --socket Redirect I/O to socket\n"); - printf(" -x, --hexadecimal Enable hexadecimal mode\n"); - printf(" -v, --version Display version\n"); - printf(" -h, --help Display help\n"); + printf(" -b, --baudrate Baud rate (default: 115200)\n"); + printf(" -d, --databits 5|6|7|8 Data bits (default: 8)\n"); + printf(" -f, --flow hard|soft|none Flow control (default: none)\n"); + printf(" -s, --stopbits 1|2 Stop bits (default: 1)\n"); + printf(" -p, --parity odd|even|none|mark|space Parity (default: none)\n"); + printf(" -o, --output-delay Output character delay (default: 0)\n"); + printf(" -O, --output-line-delay Output line delay (default: 0)\n"); + printf(" --line-pulse-duration Set line pulse duration\n"); + printf(" -a, --auto-connect new|latest|direct Automatic connect strategy (default: direct)\n"); + printf(" --exclude-devices Exclude devices by pattern\n"); + printf(" --exclude-drivers Exclude drivers by pattern\n"); + printf(" --exclude-tids Exclude topology IDs by pattern\n"); + printf(" -n, --no-reconnect Do not reconnect\n"); + printf(" -e, --local-echo Enable local echo\n"); + printf(" --input-mode normal|hex|line Select input mode (default: normal)\n"); + printf(" --output-mode normal|hex|hexN Select output mode (default: normal)\n"); + 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, 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"); + printf(" --log-append Append to log file\n"); + printf(" --log-strip Strip control characters and escape sequences\n"); + printf(" -m, --map Map characters\n"); + printf(" -c, --color 0..255|bold|none|list Colorize tio text (default: bold)\n"); + printf(" -S, --socket Redirect I/O to socket\n"); + printf(" --rs-485 Enable RS-485 mode\n"); + printf(" --rs-485-config Set RS-485 configuration\n"); + printf(" --alert bell|blink|none Alert on connect/disconnect (default: none)\n"); + printf(" --mute Mute tio messages\n"); + printf(" --script 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 sub-configurations may be set via configuration file.\n"); + printf("Options and profiles may be set via configuration file.\n"); printf("\n"); - printf("In session, press ctrl-t q to quit.\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* timestamp_token(enum timestamp_t timestamp) +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) { @@ -117,57 +389,478 @@ const char* timestamp_token(enum timestamp_t timestamp) 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; } } -enum timestamp_t timestamp_option_parse(const char *arg) +void option_parse_timestamp(const char *arg, timestamp_t *timestamp) { - enum timestamp_t timestamp = TIMESTAMP_24HOUR; // Default - + 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) + { + case AUTO_CONNECT_DIRECT: + return "direct"; + case AUTO_CONNECT_NEW: + return "new"; + case AUTO_CONNECT_LATEST: + return "latest"; + default: + return "unknown"; + } +} + +void option_parse_auto_connect(const char *arg, auto_connect_t *auto_connect) +{ + assert(arg != NULL); + if (arg != NULL) { - if (strcmp(arg, "24hour-start") == 0) + if (strcmp(arg, "direct") == 0) { - return TIMESTAMP_24HOUR_START; + *auto_connect = AUTO_CONNECT_DIRECT; } - else if (strcmp(arg, "iso8601") == 0) + else if (strcmp(arg, "new") == 0) { - return TIMESTAMP_ISO8601; + *auto_connect = AUTO_CONNECT_NEW; + } + else if (strcmp(arg, "latest") == 0) + { + *auto_connect = AUTO_CONNECT_LATEST; } else { - warning_printf("Unknown timestamp type, falling back to '24hour' default format"); + tio_error_print("Invalid auto-connect strategy '%s'", arg); + exit(EXIT_FAILURE); } } +} - return timestamp; +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) + { + token = strtok(buffer,","); + } + else + { + token = strtok(NULL, ","); + } + + if (token != NULL) + { + char keyname[11]; + unsigned int value; + + if (sscanf(token, "%10[^=]=%d", keyname, &value) == 2) + { + if (!strcmp(keyname, "DTR")) + { + option.dtr_pulse_duration = value; + } + else if (!strcmp(keyname, "RTS")) + { + option.rts_pulse_duration = value; + } + else if (!strcmp(keyname, "CTS")) + { + option.cts_pulse_duration = value; + } + else if (!strcmp(keyname, "DSR")) + { + option.dsr_pulse_duration = value; + } + else if (!strcmp(keyname, "DCD")) + { + option.dcd_pulse_duration = value; + } + else if (!strcmp(keyname, "RI")) + { + option.ri_pulse_duration = value; + } + else + { + tio_error_print("Invalid line '%s'", keyname); + exit(EXIT_FAILURE); + } + } + else + { + token_found = false; + } + } + else + { + token_found = false; + } + } + free(buffer); +} + +// Function to parse the 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; + regex_t regex; + int ret; + + // Compile the regular expression to match "hex" and optionally capture N + ret = regcomp(®ex, "^hex([0-9]+)?$", REG_EXTENDED); + if (ret) + { + tio_error_print("Could not compile regex"); + exit(EXIT_FAILURE); + } + + // Execute the regular expression + ret = regexec(®ex, input_string, 2, match, 0); + if (!ret) + { + // If there is a match, extract the N value if present + if (match[1].rm_so != -1) + { + char n_value_str[32]; // Assume max 32 digits for the numerical value + strncpy(n_value_str, input_string + match[1].rm_so, match[1].rm_eo - match[1].rm_so); + n_value_str[match[1].rm_eo - match[1].rm_so] = '\0'; // Null-terminate the string + n_value = atoi(n_value_str); + + if ((n_value > HEX_N_VALUE_MAX) || (n_value == 0)) + { + n_value = -1; + } + } + else + { + n_value = 0; + } + } + else if (ret == REG_NOMATCH) + { + n_value = -1; + } + else + { + char msgbuf[100]; + regerror(ret, ®ex, msgbuf, sizeof(msgbuf)); + tio_error_print("Regex match failed: %s", msgbuf); + exit(EXIT_FAILURE); + } + + regfree(®ex); + + return n_value; +} + +void option_parse_input_mode(const char *arg, input_mode_t *mode) +{ + assert(arg != NULL); + + if (strcmp("normal", arg) == 0) + { + *mode = INPUT_MODE_NORMAL; + } + else if (strcmp("hex", arg) == 0) + { + *mode = INPUT_MODE_HEX; + } + else if (strcmp("line", arg) == 0) + { + *mode = INPUT_MODE_LINE; + } + else + { + tio_error_print("Invalid input mode '%s'", arg); + exit(EXIT_FAILURE); + } +} + +void option_parse_output_mode(const char *arg, output_mode_t *mode) +{ + int n = 0; + + assert(arg != NULL); + + if (strcmp("normal", arg) == 0) + { + *mode = OUTPUT_MODE_NORMAL; + } + else if ((n = option_parse_hexN_string(arg)) != -1) + { + option.hex_n_value = n; + *mode = OUTPUT_MODE_HEX; + } + else + { + tio_error_print("Invalid output mode '%s'", arg); + exit(EXIT_FAILURE); + } +} + +const char *option_input_mode_to_string(input_mode_t mode) +{ + switch (mode) + { + case INPUT_MODE_NORMAL: + return "normal"; + case INPUT_MODE_HEX: + return "hex"; + case INPUT_MODE_LINE: + return "line"; + case INPUT_MODE_END: + break; + } + + return NULL; +} + +const char *option_output_mode_to_string(output_mode_t mode) +{ + switch (mode) + { + case OUTPUT_MODE_NORMAL: + return "normal"; + case OUTPUT_MODE_HEX: + return "hex"; + case OUTPUT_MODE_END: + break; + } + + return NULL; +} + +void option_parse_script_run(const char *arg, script_run_t *script_run) +{ + assert(arg != NULL); + + if (strcmp("once", arg) == 0) + { + *script_run = SCRIPT_RUN_ONCE; + } + else if (strcmp("always", arg) == 0) + { + *script_run = SCRIPT_RUN_ALWAYS; + } + else if (strcmp("never", arg) == 0) + { + *script_run = SCRIPT_RUN_NEVER; + } + else + { + 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(" TTY device: %s", option.tty_device); + 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(" Timestamps: %s", timestamp_token(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(" Auto connect: %s", option.no_autoconnect ? "disabled" : "enabled"); - if (option.map[0] != 0) - tio_printf(" Map flags: %s", option.map); + tio_printf(" Output line delay: %d", option.output_line_delay); + 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", 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", option.log_filename); + { + tio_printf(" Log file: %s", log_get_filename()); + if (option.log_directory != NULL) + { + tio_printf(" Log file directory: %s", option.log_directory); + } + tio_printf(" Log append: %s", option.log_append ? "true" : "false"); + tio_printf(" Log strip: %s", option.log_strip ? "true" : "false"); + } if (option.socket) + { tio_printf(" Socket: %s", option.socket); + } + if (option.script_filename != NULL) + { + tio_printf(" Script file: %s", option.script_filename); + tio_printf(" Script run: %s", script_run_state_to_string(option.script_run)); + } } void options_parse(int argc, char *argv[]) @@ -176,42 +869,75 @@ void options_parse(int argc, char *argv[]) if (argc == 1) { - print_help(argv); + option_print_help(argv); exit(EXIT_SUCCESS); } + // Support no-color.org informal spec + char *no_color = getenv("NO_COLOR"); + if (no_color != NULL && no_color[0] != '\0') + { + option.color = -1; + } + + // Check for vt100 terminal + char *term = getenv("TERM"); + if ((term != NULL) && (!strcmp(term, "vt100"))) + { + option.vt100 = true; + } + while (1) { static struct option long_options[] = { - {"baudrate", required_argument, 0, 'b' }, - {"databits", required_argument, 0, 'd' }, - {"flow", required_argument, 0, 'f' }, - {"stopbits", required_argument, 0, 's' }, - {"parity", required_argument, 0, 'p' }, - {"output-delay", required_argument, 0, 'o' }, - {"no-autoconnect", no_argument, 0, 'n' }, - {"local-echo", no_argument, 0, 'e' }, - {"timestamp", no_argument, 0, 't' }, - {"timestamp-format", required_argument, 0, OPT_TIMESTAMP_FORMAT }, - {"list-devices", no_argument, 0, 'L' }, - {"log", no_argument, 0, 'l' }, - {"log-file", required_argument, 0, OPT_LOG_FILE }, - {"log-strip", no_argument, 0, OPT_LOG_STRIP }, - {"socket", required_argument, 0, 'S' }, - {"map", required_argument, 0, 'm' }, - {"color", required_argument, 0, 'c' }, - {"hexadecimal", no_argument, 0, 'x' }, - {"version", no_argument, 0, 'v' }, - {"help", no_argument, 0, 'h' }, - {0, 0, 0, 0 } + {"baudrate", required_argument, 0, 'b' }, + {"databits", required_argument, 0, 'd' }, + {"flow", required_argument, 0, 'f' }, + {"stopbits", required_argument, 0, 's' }, + {"parity", required_argument, 0, 'p' }, + {"output-delay", required_argument, 0, 'o' }, + {"output-line-delay" , required_argument, 0, 'O' }, + {"line-pulse-duration", required_argument, 0, OPT_LINE_PULSE_DURATION }, + {"auto-connect", required_argument, 0, 'a' }, + {"exclude-devices", required_argument, 0, OPT_EXCLUDE_DEVICES }, + {"exclude-drivers", required_argument, 0, OPT_EXCLUDE_DRIVERS }, + {"exclude-tids", required_argument, 0, OPT_EXCLUDE_TIDS }, + {"no-reconnect", no_argument, 0, 'n' }, + {"local-echo", no_argument, 0, 'e' }, + {"timestamp", no_argument, 0, 't' }, + {"timestamp-format", required_argument, 0, OPT_TIMESTAMP_FORMAT }, + {"timestamp-timeout", required_argument, 0, OPT_TIMESTAMP_TIMEOUT }, + {"list", no_argument, 0, 'l' }, + {"log", no_argument, 0, 'L' }, + {"log-file", required_argument, 0, OPT_LOG_FILE }, + {"log-directory", required_argument, 0, OPT_LOG_DIRECTORY }, + {"log-append", no_argument, 0, OPT_LOG_APPEND }, + {"log-strip", no_argument, 0, OPT_LOG_STRIP }, + {"socket", required_argument, 0, 'S' }, + {"map", required_argument, 0, 'm' }, + {"color", required_argument, 0, 'c' }, + {"input-mode", required_argument, 0, OPT_INPUT_MODE }, + {"output-mode", required_argument, 0, OPT_OUTPUT_MODE }, + {"rs-485", no_argument, 0, OPT_RS485 }, + {"rs-485-config", required_argument, 0, OPT_RS485_CONFIG }, + {"alert", required_argument, 0, OPT_ALERT }, + {"mute", no_argument, 0, OPT_MUTE }, + {"script", 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 }, + {0, 0, 0, 0 } }; /* getopt_long stores the option index here */ int option_index = 0; /* Parse argument using getopt_long */ - c = getopt_long(argc, argv, "b:d:f:s:p:o:netLlS:m:c:xvh", long_options, &option_index); + c = getopt_long(argc, argv, "b:d:f:s:p:o:O:a:netLlS:m:c:xrvh", long_options, &option_index); /* Detect the end of the options */ if (c == -1) @@ -230,31 +956,55 @@ 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_string_to_integer(optarg, &option.output_line_delay, "output line delay", 0, INT_MAX); + break; + + case OPT_LINE_PULSE_DURATION: + option_parse_line_pulse_duration(optarg); + break; + + case 'a': + option_parse_auto_connect(optarg, &option.auto_connect); + break; + + case OPT_EXCLUDE_DEVICES: + option.exclude_devices = optarg; + break; + + case OPT_EXCLUDE_DRIVERS: + option.exclude_drivers = optarg; + break; + + case OPT_EXCLUDE_TIDS: + option.exclude_tids = optarg; break; case 'n': - option.no_autoconnect = true; + option.no_reconnect = true; break; case 'e': @@ -262,83 +1012,112 @@ 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_string_to_integer(optarg, &option.timestamp_timeout, "timestamp timeout", 0, INT_MAX); break; case 'L': - list_serial_devices(); - exit(EXIT_SUCCESS); + option.log = true; break; case 'l': - option.log = true; + list_serial_devices(); + config_list_targets(); + exit(EXIT_SUCCESS); break; case OPT_LOG_FILE: option.log_filename = optarg; break; + case OPT_LOG_DIRECTORY: + option.log_directory = optarg; + break; + case OPT_LOG_STRIP: option.log_strip = true; break; + case OPT_LOG_APPEND: + option.log_append = true; + break; + case 'S': option.socket = optarg; 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); - } - - if (!strcmp(optarg, "none")) - { - option.color = -1; - break; - } - - option.color = string_to_long(optarg); - if ((option.color < 0) || (option.color > 255)) - { - printf("Error: Invalid color code\n"); - exit(EXIT_FAILURE); - } + option_parse_color(optarg, &option.color); break; - case 'x': - option.hex_mode = true; + case OPT_INPUT_MODE: + option_parse_input_mode(optarg, &option.input_mode); + break; + + case OPT_OUTPUT_MODE: + option_parse_output_mode(optarg, &option.output_mode); + break; + + case OPT_RS485: + option.rs485 = true; + break; + + case OPT_RS485_CONFIG: + rs485_parse_config(optarg); + break; + + case OPT_ALERT: + option_parse_alert(optarg, &option.alert); + break; + + case OPT_MUTE: + option.mute = true; + break; + + case OPT_SCRIPT: + option.script = optarg; + break; + + case OPT_SCRIPT_FILE: + option.script_filename = optarg; + break; + + case OPT_SCRIPT_RUN: + 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("Copyright (c) 2014-2022 Martin Lund\n"); - printf("\n"); - printf("License GPLv2+: GNU GPL version 2 or later .\n"); - printf("This is free software: you are free to change and redistribute it.\n"); - printf("There is NO WARRANTY, to the extent permitted by law.\n"); + printf("tio %s\n", VERSION); exit(EXIT_SUCCESS); break; case 'h': - print_help(argv); + option_print_help(argv); exit(EXIT_SUCCESS); break; + case OPT_COMPLETE_PROFILES: + option.complete_profiles = true; + break; + case '?': /* getopt_long already printed an error message */ exit(EXIT_FAILURE); @@ -349,38 +1128,61 @@ void options_parse(int argc, char *argv[]) } } - /* Assume first non-option is the tty device name */ - if (strcmp(option.tty_device, "")) - optind++; - else if (optind < argc) - option.tty_device = argv[optind++]; - - if (strlen(option.tty_device) == 0) + /* Assume first non-option is the target (tty device, profile, tid) */ + if (strcmp(option.target, "")) { - printf("Error: Missing device or config name\n"); + optind++; + } + else if (optind < argc) + { + option.target = argv[optind++]; + } + + if (option.complete_profiles) + { + return; + } + + if (option.auto_connect != AUTO_CONNECT_DIRECT) + { + return; + } + + if (strlen(option.target) == 0) + { + tio_error_print("Missing tty device, profile or topology ID"); exit(EXIT_FAILURE); } - /* Print any remaining command line arguments (unknown options) */ + /* Print any remaining command line arguments as unknown */ if (optind < argc) { - printf("Error: Unknown argument "); + fprintf(stderr, "Error: Unknown argument "); while (optind < argc) - printf("%s ", argv[optind++]); - printf("\n"); + { + fprintf(stderr, "%s ", argv[optind++]); + } + fprintf(stderr, "\n"); exit(EXIT_FAILURE); } } void options_parse_final(int argc, char *argv[]) { - /* Preserve tty device which may have been set by configuration file */ - const char *tty_device = option.tty_device; - /* Do 2nd pass to override settings set by configuration file */ optind = 1; // Reset option index to restart scanning of argv options_parse(argc, argv); - /* Restore tty device */ - option.tty_device = tty_device; +#ifdef __CYGWIN__ + unsigned char portnum; + char *tty_win; + 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); + option.target = tty_win; + } +#endif } diff --git a/src/options.h b/src/options.h index cc905bf..c552217 100644 --- a/src/options.h +++ b/src/options.h @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -21,43 +21,91 @@ #pragma once +#include #include -#include -#include -#include +#include "script.h" +#include "timestamp.h" +#include "alert.h" +#include "tty.h" -enum timestamp_t +typedef enum { - TIMESTAMP_NONE, - TIMESTAMP_24HOUR, - TIMESTAMP_24HOUR_START, - TIMESTAMP_ISO8601, - TIMESTAMP_END, -}; + INPUT_MODE_NORMAL, + INPUT_MODE_HEX, + INPUT_MODE_LINE, + INPUT_MODE_END, +} input_mode_t; -const char* timestamp_token(enum timestamp_t timestamp); -enum timestamp_t timestamp_option_parse(const char *arg); +typedef enum +{ + OUTPUT_MODE_NORMAL, + OUTPUT_MODE_HEX, + OUTPUT_MODE_END, +} output_mode_t; /* Options */ struct option_t { - const char *tty_device; - unsigned int baudrate; + char *target; + int baudrate; int databits; - char *flow; + flow_t flow; int stopbits; - char *parity; + parity_t parity; int output_delay; - bool no_autoconnect; + int output_line_delay; + 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; + bool log_append; bool log_strip; bool local_echo; - enum timestamp_t timestamp; - const char *log_filename; - const char *map; - const char *socket; + timestamp_t timestamp; + char *log_filename; + char *log_directory; + char *socket; int color; - bool hex_mode; + input_mode_t input_mode; + output_mode_t output_mode; + char prefix_code; + char prefix_key; + bool prefix_enabled; + bool mute; + bool rs485; + uint32_t rs485_config_flags; + int32_t rs485_delay_rts_before_send; + int32_t rs485_delay_rts_after_send; + alert_t alert; + bool complete_profiles; + char *script; + char *script_filename; + script_run_t script_run; + 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; @@ -65,3 +113,23 @@ extern struct option_t option; void options_print(); void options_parse(int argc, char *argv[]); void options_parse_final(int argc, char *argv[]); + +int option_string_to_integer(const char *string, int *value, const char *desc, int min, int max); + +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 68e663c..06f7e73 100644 --- a/src/print.c +++ b/src/print.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -19,9 +19,6 @@ * 02110-1301, USA. */ -#include -#include -#include "options.h" #include "print.h" bool print_tainted = false; @@ -29,16 +26,87 @@ char ansi_format[30]; void print_hex(char c) { - printf("%02x ", (unsigned char) c); + print_tainted = true; + printf("%02x ", (unsigned char) c); } void print_normal(char c) { - putchar(c); + print_tainted = true; + putchar(c); } void print_init_ansi_formatting() { - // Set bold text with user defined ANSI color - sprintf(ansi_format, "\e[1;38;5;%dm", option.color); + if (option.color == 256) + { + // Set bold text with no color changes + sprintf(ansi_format, "\e[1m"); + } + else + { + // Set bold text with user defined ANSI color + sprintf(ansi_format, "\e[1;38;5;%dm", option.color); + } +} + +void tio_printf_array(const char *array) +{ + int i = 0, j = 0; + + tio_printf(""); + + while (array[i]) + { + if (array[i] == '\n') + { + const char *line = &array[j]; + char *line_copy = strndup(line, i-j); + tio_printf_raw("%s\r", line_copy); + free(line_copy); + j = i; + } + i++; + } + tio_printf(""); +} + +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" extern bool print_tainted; extern char ansi_format[]; @@ -33,71 +34,110 @@ extern char ansi_format[]; #define ansi_printf(format, args...) \ { \ - if (option.color < 0) \ - fprintf (stdout, "\r" format "\r\n", ## args); \ - else \ - fprintf (stdout, "\r%s" format ANSI_RESET "\r\n", ansi_format, ## args); \ + if (!option.mute) \ + { \ + if (option.color < 0) \ + fprintf (stdout, "\r" format "\r\n", ## args); \ + else \ + fprintf (stdout, "\r%s" format ANSI_RESET "\r\n", ansi_format, ## args); \ + } \ } #define ansi_error_printf(format, args...) \ { \ - if (option.color < 0) \ - fprintf (stdout, "\r" format "\r\n", ## args); \ - else \ - fprintf (stderr, "\r%s" format ANSI_RESET "\r\n", ansi_format, ## args); \ - fflush(stderr); \ + if (!option.mute) \ + { \ + if (option.color < 0) \ + fprintf (stderr, "\r" format "\r\n", ## args); \ + else \ + fprintf (stderr, "\r%s" format ANSI_RESET "\r\n", ansi_format, ## args); \ + fflush(stderr); \ + } \ } #define ansi_printf_raw(format, args...) \ { \ - if (option.color < 0) \ - fprintf (stdout, format, ## args); \ - else \ - fprintf (stdout, "%s" format ANSI_RESET, ansi_format, ## args); \ + if (!option.mute) \ + { \ + if (option.color < 0) \ + fprintf (stdout, format, ## args); \ + else \ + fprintf (stdout, "%s" format ANSI_RESET, ansi_format, ## args); \ + } \ } -#define warning_printf(format, args...) \ +#define tio_warning_printf(format, args...) \ { \ - if (print_tainted) \ - putchar('\n'); \ - if (option.color < 0) \ - fprintf (stdout, "\r[%s] Warning: " format "\r\n", current_time(), ## args); \ - else \ - ansi_printf("[%s] Warning: " format, current_time(), ## args); \ + if (!option.mute) \ + { \ + if (print_tainted) \ + putchar('\n'); \ + if (option.color < 0) \ + fprintf (stdout, "\r[%s] Warning: " format "\r\n", timestamp_current_time(), ## args); \ + else \ + ansi_printf("[%s] Warning: " format, timestamp_current_time(), ## args); \ + print_tainted = false; \ + } \ +} + +#define tio_error_print(format, args...) \ +{ \ + if (!option.mute) \ + { \ + if (print_tainted) \ + putchar('\n'); \ + 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; \ + } \ } #define tio_printf(format, args...) \ { \ - if (print_tainted) \ - putchar('\n'); \ - ansi_printf("[%s] " format, current_time(), ## args); \ - print_tainted = false; \ + if (!option.mute) \ + { \ + if (print_tainted) \ + putchar('\n'); \ + ansi_printf("[%s] " format, timestamp_current_time(), ## args); \ + print_tainted = false; \ + } \ } -#define tio_error_printf(format, args...) \ +#define tio_printf_raw(format, args...) \ { \ - if (print_tainted) \ - putchar('\n'); \ - ansi_error_printf("[%s] " format, current_time(), ## args); \ - print_tainted = false; \ + if (!option.mute) \ + { \ + if (print_tainted) \ + putchar('\n'); \ + ansi_printf_raw("[%s] " format, timestamp_current_time(), ## args); \ + print_tainted = false; \ + } \ } -#define error_printf(format, args...) \ - snprintf(error[0], 1000, format, ## args); - -#define error_printf_silent(format, args...) \ - snprintf(error[1], 1000, format, ## args); - #ifdef DEBUG -#define debug_printf(format, args...) \ - fprintf (stdout, "[debug] " format, ## args) -#define debug_printf_raw(format, args...) \ - fprintf (stdout, "" format, ## args) +#define tio_debug_printf(format, args...) \ + fprintf(stdout, "[debug] " format, ## args) +#define tio_debug_printf_raw(format, args...) \ + fprintf(stdout, "" format, ## args) #else -#define debug_printf(format, args...) -#define debug_printf_raw(format, args...) +#define tio_debug_printf(format, args...) +#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/iossiospeed.c b/src/readline.h similarity index 76% rename from src/iossiospeed.c rename to src/readline.h index 8fa0d5f..46c3f10 100644 --- a/src/iossiospeed.c +++ b/src/readline.h @@ -1,7 +1,7 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * - * Copyright (c) 2017 Martin Lund + * 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 @@ -19,10 +19,8 @@ * 02110-1301, USA. */ -#include -#include +#pragma once -int iossiospeed(int fd, int baudrate) -{ - return ioctl(fd, IOSSIOSPEED, (char *)&baudrate); -} +void readline_init(void); +void readline_input(char input_char); +char * readline_get(void); diff --git a/src/rs485.c b/src/rs485.c new file mode 100644 index 0000000..3c9280c --- /dev/null +++ b/src/rs485.c @@ -0,0 +1,210 @@ +/* + * tio - a serial device I/O tool + * + * Copyright (c) 2022 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 +#include +#include +#include +#include "options.h" +#include "print.h" +#include "misc.h" + +#ifdef HAVE_RS485 + +#include + +static struct serial_rs485 rs485_config_saved; +static struct serial_rs485 rs485_config; +static bool rs485_config_written = false; + +void rs485_parse_config(const char *arg) +{ + bool token_found = true; + char *token = NULL; + char *buffer = strdup(arg); + + while (token_found == true) + { + if (token == NULL) + { + token = strtok(buffer,","); + } + else + { + token = strtok(NULL, ","); + } + + if (token != NULL) + { + char keyname[31]; + unsigned int value; + int match_count; + + match_count = sscanf(token, "%30[^=]=%d", keyname, &value); + + if (match_count == 2) + { + if (!strcmp(keyname, "RTS_ON_SEND")) + { + if (value) + { + /* Set logical level for RTS pin equal to 1 when sending */ + option.rs485_config_flags |= SER_RS485_RTS_ON_SEND; + } + else + { + /* Set logical level for RTS pin equal to 0 when sending */ + option.rs485_config_flags &= ~(SER_RS485_RTS_ON_SEND); + } + } + else if (!strcmp(keyname, "RTS_AFTER_SEND")) + { + if (value) + { + /* Set logical level for RTS pin equal to 1 after sending */ + option.rs485_config_flags |= SER_RS485_RTS_AFTER_SEND; + } + else + { + /* Set logical level for RTS pin equal to 0 after sending */ + option.rs485_config_flags &= ~(SER_RS485_RTS_AFTER_SEND); + } + } + else if (!strcmp(keyname, "RTS_DELAY_BEFORE_SEND")) + { + /* Set RTS delay before send */ + option.rs485_delay_rts_before_send = value; + } + else if (!strcmp(keyname, "RTS_DELAY_AFTER_SEND")) + { + /* Set RTS delay after send */ + option.rs485_delay_rts_after_send = value; + } + } + else if (match_count == 1) + { + if (!strcmp(keyname, "RX_DURING_TX")) + { + /* Receive data even while sending data */ + option.rs485_config_flags |= SER_RS485_RX_DURING_TX; + } + } + else + { + token_found = false; + } + } + else + { + token_found = false; + } + } + free(buffer); +} + +void rs485_print_config(void) +{ + tio_printf(" RS-485 Configuration:"); + tio_printf(" RTS_ON_SEND: %s", (rs485_config.flags & SER_RS485_RTS_ON_SEND) ? "high" : "low"); + 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) ? "true" : "false"); +} + +int rs485_mode_enable(int fd) +{ + /* Save existing RS-485 configuration */ + ioctl (fd, TIOCGRS485, &rs485_config_saved); + + /* Prepare new RS-485 configuration */ + rs485_config.flags = SER_RS485_ENABLED; + rs485_config.flags |= option.rs485_config_flags; + + if (option.rs485_delay_rts_before_send > 0) + { + rs485_config.delay_rts_before_send = option.rs485_delay_rts_before_send; + } + else + { + rs485_config.delay_rts_before_send = rs485_config_saved.delay_rts_before_send; + } + + if (option.rs485_delay_rts_after_send > 0) + { + rs485_config.delay_rts_after_send = option.rs485_delay_rts_after_send; + } + else + { + rs485_config.delay_rts_after_send = rs485_config_saved.delay_rts_after_send; + } + + /* Write new RS-485 configuration */ + if (ioctl(fd, TIOCSRS485, &rs485_config) < 0) + { + tio_warning_printf("RS-485 mode is not supported by your device (%s)", strerror(errno)); + return -1; + } + + rs485_config_written = true; + + return 0; +} + +void rs485_mode_restore(int fd) +{ + if (rs485_config_written) + { + /* Write saved RS-485 configuration */ + if (ioctl(fd, TIOCSRS485, &rs485_config_saved) < 0) + { + tio_warning_printf("TIOCGRS485 ioctl failed (%s)", strerror(errno)); + } + } +} + +#else + +void rs485_parse_config(const char *arg) +{ + UNUSED(arg); + return; +} + +void rs485_print_config(void) +{ + return; +} + +int rs485_mode_enable(int fd) +{ + UNUSED(fd); + tio_error_printf("RS485 mode is not supported on your system"); + exit(EXIT_FAILURE); +} + +void rs485_mode_restore(int fd) +{ + UNUSED(fd); + return; +} + +#endif diff --git a/src/rs485.h b/src/rs485.h new file mode 100644 index 0000000..ed075ab --- /dev/null +++ b/src/rs485.h @@ -0,0 +1,27 @@ +/* + * tio - a serial device I/O tool + * + * Copyright (c) 2022 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 rs485_parse_config(const char *arg); +int rs485_mode_enable(int fd); +void rs485_mode_restore(int fd); +void rs485_print_config(void); diff --git a/src/script.c b/src/script.c new file mode 100644 index 0000000..b69d55b --- /dev/null +++ b/src/script.c @@ -0,0 +1,547 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "misc.h" +#include "print.h" +#include "options.h" +#include "tty.h" +#include "xymodem.h" +#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 script_init[] = +"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" +" 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"; + +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); + + 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; +} + +// lua: tio.read(size, timeout) +static int api_read(lua_State *L) +{ + int size = luaL_checkinteger(L, 1); + int timeout = lua_tointeger(L, 2); + + if (timeout == 0) + { + timeout = -1; // Wait forever + } + + 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) + { + // On timeout return nil instead of an empty string + lua_pop(L, 1); + lua_pushnil(L); + } + else + { + maybe_echo(L); + } + + return 1; +} + +// lua: string = tio.readline(timeout) +static int api_readline(lua_State *L) { + int timeout = lua_tointeger(L, 1); //ms + luaL_Buffer b; + char ch; + + if (timeout == 0) + { + timeout = -1; // Wait forever + } + + luaL_buffinit(L, &b); + luaL_prepbuffer(&b); + while (true) { + int ret = read_poll(device_fd, &ch, 1, timeout); + + if (ret < 0) + return luaL_error(L, "%s", strerror(errno)); + + if (ret == 0) + { + luaL_pushresult(&b); + maybe_echo(L); + lua_pushnil(L); + lua_insert(L, -2); + return 2; + } + + if (ch == '\n') + { + luaL_pushresult(&b); + maybe_echo(L); + return 1; + } + + luaL_addchar(&b, ch); + } +} + +// lua: table = tio.ttysearch() +static int api_ttysearch(lua_State *L) +{ + UNUSED(L); + GList *iter; + int i = 1; + + GList *device_list = tty_search_for_serial_devices(); + + if (device_list == NULL) + { + return 0; + } + + // Create a new table + lua_newtable(L); + + // Iterate through found devices + for (iter = device_list; iter != NULL; iter = g_list_next(iter)) + { + device_t *device = (device_t *) iter->data; + + // Create a new sub-table for each serial device + lua_newtable(L); + + // Add elements to the table + lua_pushstring(L, "path"); + lua_pushstring(L, device->path); + lua_settable(L, -3); + + lua_pushstring(L, "tid"); + lua_pushstring(L, device->tid); + lua_settable(L, -3); + + lua_pushstring(L, "uptime"); + lua_pushnumber(L, device->uptime); + lua_settable(L, -3); + + lua_pushstring(L, "driver"); + lua_pushstring(L, device->driver); + lua_settable(L, -3); + + lua_pushstring(L, "description"); + lua_pushstring(L, device->description); + lua_settable(L, -3); + + // Set the sub-table as a row in the main table + lua_rawseti(L, -2, i++); + } + + // Return table + return 1; +} + +static void script_buffer_run(lua_State *L, const char *script_buffer) +{ + int error; + + error = luaL_loadbuffer(L, script_buffer, strlen(script_buffer), "tio") || + lua_pcall(L, 0, 0, 0); + if (error) + { + tio_warning_printf("lua: %s\n", lua_tostring(L, -1)); + lua_pop(L, 1); /* Pop error message from the stack */ + } +} + +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[] = +{ + { "echo", api_echo}, + { "sleep", api_sleep}, + { "msleep", api_msleep}, + { "line_set", line_set}, + { "send", api_send}, + { "write", api_write}, + { "read", api_read}, + { "readline", api_readline}, + { "ttysearch", api_ttysearch}, + {NULL, NULL} +}; + +static void script_load(lua_State *L) +{ + int error; + + error = luaL_loadbuffer(L, script_init, strlen(script_init), "tio") || lua_pcall(L, 0, 0, 0); + if (error) + { + tio_error_print("%s\n", lua_tostring(L, -1)); + lua_pop(L, 1); // Pop error message from the stack + } +} + +static void script_set_global(lua_State *L, const char *name, long value) +{ + lua_pushnumber(L, value); + lua_setglobal(L, name); +} + +static void script_set_globals(lua_State *L) +{ + script_set_global(L, "toggle", 2); + script_set_global(L, "high", 1); + script_set_global(L, "low", 0); + script_set_global(L, "XMODEM_CRC", XMODEM_CRC); + script_set_global(L, "XMODEM_1K", XMODEM_1K); + script_set_global(L, "YMODEM", YMODEM); +} + +#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; + + device_fd = fd; + + L = luaL_newstate(); + luaL_openlibs(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 (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); + } + else if (option.script != NULL) + { + tio_printf("Running script"); + script_buffer_run(L, option.script); + } + + lua_close(L); +} + +const char *script_run_state_to_string(script_run_t state) +{ + switch (state) + { + case SCRIPT_RUN_ONCE: + return "once"; + case SCRIPT_RUN_ALWAYS: + return "always"; + case SCRIPT_RUN_NEVER: + return "never"; + default: + return "Unknown"; + } +} diff --git a/src/script.h b/src/script.h new file mode 100644 index 0000000..58ba1e1 --- /dev/null +++ b/src/script.h @@ -0,0 +1,33 @@ +/* + * 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 + +typedef enum +{ + SCRIPT_RUN_ONCE, + SCRIPT_RUN_ALWAYS, + SCRIPT_RUN_NEVER, + SCRIPT_RUN_END, +} script_run_t; + +void script_run(int fd, const char *script_filename); +const char *script_run_state_to_string(script_run_t state); diff --git a/src/setspeed2.c b/src/setspeed.c similarity index 71% rename from src/setspeed2.c rename to src/setspeed.c index 64ecb4a..8ed54c4 100644 --- a/src/setspeed2.c +++ b/src/setspeed.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2017-2022 Martin Lund * @@ -19,13 +19,24 @@ * 02110-1301, USA. */ +#include + +#ifdef HAVE_TERMIOS2 #define termios asmtermios #include #undef termios #include #include -int setspeed2(int fd, int baudrate) +#elif HAVE_IOSSIOSPEED +#include +#include +#endif + +#include "misc.h" + +#ifdef HAVE_TERMIOS2 +int setspeed(int fd, int baudrate) { struct termios2 tio; int status; @@ -42,3 +53,20 @@ int setspeed2(int fd, int baudrate) return status; } + +#elif HAVE_IOSSIOSPEED +int setspeed(int fd, int baudrate) +{ + return ioctl(fd, IOSSIOSPEED, (char *)&baudrate); +} + +#else +int setspeed(int fd, int baudrate) +{ + UNUSED(fd); + UNUSED(baudrate); + + errno = EINVAL; + return -1; +} +#endif diff --git a/src/setspeed.h b/src/setspeed.h new file mode 100644 index 0000000..feef305 --- /dev/null +++ b/src/setspeed.h @@ -0,0 +1,24 @@ +/* + * tio - a serial device I/O tool + * + * Copyright (c) 2022 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 + +int setspeed(int fd, int baudrate); diff --git a/src/signals.c b/src/signals.c index 364de8b..95a6062 100644 --- a/src/signals.c +++ b/src/signals.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2022 Martin Lund * @@ -27,15 +27,25 @@ #include "error.h" #include "print.h" #include "misc.h" +#include "tty.h" static void signal_handler(int signum) { - UNUSED(signum); - tio_printf("Received hangup signal!"); + switch (signum) + { + case SIGHUP: + tio_printf("Received SIGHUP signal!"); + break; + case SIGINT: + tio_printf("Received SIGINT signal!"); + break; + } exit(EXIT_FAILURE); } void signal_handlers_install(void) { signal(SIGHUP, signal_handler); + signal(SIGINT, signal_handler); + signal(SIGPIPE, SIG_IGN); } diff --git a/src/signals.h b/src/signals.h index 8d8d70f..68c4e76 100644 --- a/src/signals.h +++ b/src/signals.h @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2022 Martin Lund * diff --git a/src/socket.c b/src/socket.c index ed09ff7..e51f937 100644 --- a/src/socket.c +++ b/src/socket.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * Copyright (c) 2022 Google LLC @@ -28,10 +28,12 @@ #include #include #include +#include #include "socket.h" #include "options.h" #include "print.h" +#include "tty.h" #define MAX_SOCKET_CLIENTS 16 #define SOCKET_PORT_DEFAULT 3333 @@ -50,23 +52,23 @@ static const char *socket_filename(void) static int socket_inet_port(void) { /* skip 'inet:' */ - int port_number = atoi(option.socket + 5); - if (port_number == 0) + int port = atoi(option.socket + 5); + if (port == 0) { - port_number = SOCKET_PORT_DEFAULT; + port = SOCKET_PORT_DEFAULT; } - return port_number; + return port; } static int socket_inet6_port(void) { /* skip 'inet6:' */ - int port_number = atoi(option.socket + 6); - if (port_number == 0) + int port = atoi(option.socket + 6); + if (port == 0) { - port_number = SOCKET_PORT_DEFAULT; + port = SOCKET_PORT_DEFAULT; } - return port_number; + return port; } static void socket_exit(void) @@ -77,6 +79,44 @@ static void socket_exit(void) } } +static bool socket_stale(const char *path) +{ + struct sockaddr_un addr; + bool stale = false; + int sfd; + + /* Test if socket file exists */ + if (access(path, F_OK) == 0) + { + /* Create test socket */ + sfd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sfd < 0) + { + tio_warning_printf("Failure opening socket (%s)", strerror(errno)); + return false; + } + + /* Prepare address */ + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + + /* Perform connect to test if socket is active */ + if (connect(sockfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1) + { + if (errno == ECONNREFUSED) + { + // No one is listening on socket file + stale = true; + } + } + + /* Cleanup */ + close(sockfd); + } + + return stale; +} + void socket_configure(void) { struct sockaddr_un sockaddr_unix = {}; @@ -84,6 +124,7 @@ void socket_configure(void) struct sockaddr_in6 sockaddr_inet6 = {}; struct sockaddr *sockaddr_p; socklen_t socklen; + int optval; /* Parse socket string */ @@ -93,13 +134,13 @@ void socket_configure(void) if (strlen(socket_filename()) == 0) { - error_printf("Missing socket filename"); + tio_error_printf("Missing socket filename"); exit(EXIT_FAILURE); } if (strlen(socket_filename()) > sizeof(sockaddr_unix.sun_path) - 1) { - error_printf("Socket file path %s too long", option.socket); + tio_error_printf("Socket file path %s too long", option.socket); exit(EXIT_FAILURE); } } @@ -112,7 +153,7 @@ void socket_configure(void) if (port_number < 0) { - error_printf("Invalid port number: %d", port_number); + tio_error_printf("Invalid port number: %d", port_number); exit(EXIT_FAILURE); } } @@ -125,17 +166,17 @@ void socket_configure(void) if (port_number < 0) { - error_printf("Invalid port number: %d", port_number); + tio_error_printf("Invalid port number: %d", port_number); exit(EXIT_FAILURE); } } if (socket_family == AF_UNSPEC) { - error_printf("%s: Invalid socket scheme, must be prefixed with 'unix:', 'inet:', or 'inet6:'", option.socket); + tio_error_printf("%s: Invalid socket scheme, must be prefixed with 'unix:', 'inet:', or 'inet6:'", option.socket); exit(EXIT_FAILURE); } - + /* Configure socket */ switch (socket_family) @@ -145,6 +186,14 @@ void socket_configure(void) strncpy(sockaddr_unix.sun_path, socket_filename(), sizeof(sockaddr_unix.sun_path) - 1); sockaddr_p = (struct sockaddr *) &sockaddr_unix; socklen = sizeof(sockaddr_unix); + + /* Test for stale unix socket file */ + if (socket_stale(socket_filename())) + { + tio_printf("Cleaning up old socket file"); + unlink(socket_filename()); + } + break; case AF_INET: @@ -164,7 +213,7 @@ void socket_configure(void) break; default: - error_printf("Invalid socket family (%d)", socket_family); + tio_error_printf("Invalid socket family (%d)", socket_family); exit(EXIT_FAILURE); break; } @@ -173,21 +222,31 @@ void socket_configure(void) sockfd = socket(socket_family, SOCK_STREAM, 0); if (sockfd < 0) { - error_printf("Failed to create socket (%s)", strerror(errno)); + tio_error_printf("Failed to create socket (%s)", strerror(errno)); + 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) { - error_printf("Failed to bind to socket (%s)", strerror(errno)); + tio_error_printf("Failed to bind to socket (%s)", strerror(errno)); exit(EXIT_FAILURE); } /* Listen */ if (listen(sockfd, MAX_SOCKET_CLIENTS) < 0) { - error_printf("Failed to listen on socket (%s)", strerror(errno)); + tio_error_printf("Failed to listen on socket (%s)", strerror(errno)); exit(EXIT_FAILURE); } @@ -215,9 +274,14 @@ 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 { - error_printf_silent("Failed to write to socket (%s)", strerror(errno)); + tio_error_printf_silent("Failed to write to socket (%s)", strerror(errno)); close(clientfds[i]); clientfds[i] = -1; } @@ -288,16 +352,31 @@ bool socket_handle_input(fd_set *rdfs, char *output_char) } if (status < 0) { - error_printf_silent("Failed to read from socket (%s)", strerror(errno)); + tio_error_printf_silent("Failed to read from socket (%s)", strerror(errno)); close(clientfds[i]); clientfds[i] = -1; continue; } - /* match the behavior of a terminal in raw mode */ - if (*output_char == '\n') + + /* If INLCR is set, a received NL character shall be translated into a CR character */ + 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 (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 (option.map_i_cr_nl) + { + *output_char = '\n'; + } + } return true; } } diff --git a/src/socket.h b/src/socket.h index 2caffaf..523f6c4 100644 --- a/src/socket.h +++ b/src/socket.h @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * Copyright (c) 2022 Google LLC diff --git a/src/timestamp.c b/src/timestamp.c new file mode 100644 index 0000000..9273758 --- /dev/null +++ b/src/timestamp.c @@ -0,0 +1,104 @@ +/* + * tio - a serial device I/O tool + * + * Copyright (c) 2014-2022 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 "config.h" +#include +#include +#include +#include +#include "error.h" +#include "print.h" +#include "options.h" +#include "timestamp.h" + +char *timestamp_current_time(void) +{ + static char time_string[TIME_STRING_SIZE_MAX]; + static struct timeval tv, tv_now, tv_start, tv_previous; + static bool first = true; + struct tm *tm; + size_t len; + + // Get current time value + gettimeofday(&tv_now, NULL); + + if (first) + { + tv_start = tv_now; + first = false; + } + + // Add formatted timestamp + switch (option.timestamp) + { + case TIMESTAMP_NONE: + case TIMESTAMP_24HOUR: + // "hh:mm:ss.sss" (24 hour format) + tv = tv_now; + tm = localtime(&tv.tv_sec); + len = strftime(time_string, sizeof(time_string), "%H:%M:%S", tm); + break; + case TIMESTAMP_24HOUR_START: + // "hh:mm:ss.sss" (24 hour format relative to start time) + timersub(&tv_now, &tv_start, &tv); + tm = gmtime(&tv.tv_sec); + len = strftime(time_string, sizeof(time_string), "%H:%M:%S", tm); + break; + case TIMESTAMP_24HOUR_DELTA: + // "hh:mm:ss.sss" (24 hour format relative to previous time stamp) + timersub(&tv_now, &tv_previous, &tv); + tm = gmtime(&tv.tv_sec); + len = strftime(time_string, sizeof(time_string), "%H:%M:%S", tm); + break; + case TIMESTAMP_ISO8601: + // "YYYY-MM-DDThh:mm:ss.sss" (ISO-8601) + tv = tv_now; + 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 millis-/microseconds to all timestamps + if (len) + { + 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; +} diff --git a/src/timestamp.h b/src/timestamp.h new file mode 100644 index 0000000..0544544 --- /dev/null +++ b/src/timestamp.h @@ -0,0 +1,39 @@ +/* + * tio - a serial device I/O tool + * + * Copyright (c) 2014-2022 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 + +typedef enum +{ + TIMESTAMP_NONE, + TIMESTAMP_24HOUR, + 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); + diff --git a/src/tty.c b/src/tty.c index f1efa69..efda859 100644 --- a/src/tty.c +++ b/src/tty.c @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -19,11 +19,27 @@ * 02110-1301, USA. */ +#if defined(__linux__) +#include +#endif + +#if defined(__APPLE__) || defined(__MACH__) +#include +#include +#include +#include +#include +#endif + +#include "version.h" #include "config.h" +#include #include #include +#include #include #include +#include #include #include #include @@ -39,6 +55,8 @@ #include #include #include +#include +#include #include "config.h" #include "configfile.h" #include "tty.h" @@ -48,39 +66,123 @@ #include "log.h" #include "error.h" #include "socket.h" +#include "setspeed.h" +#include "rs485.h" +#include "alert.h" +#include "timestamp.h" +#include "misc.h" +#include "script.h" +#include "xymodem.h" +#include "fs.h" +#include "readline.h" -#ifdef HAVE_TERMIOS2 -extern int setspeed2(int fd, int baudrate); -#endif +/* tty device listing configuration */ -#ifdef HAVE_IOSSIOSPEED -extern int iossiospeed(int fd, int baudrate); -#endif - -#ifdef __APPLE__ -#define PATH_SERIAL_DEVICES "/dev/" +#if defined(__linux__) +#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" +#elif defined(__APPLE__) +#define PATH_SERIAL_DEVICES "/dev" +#elif defined(__CYGWIN__) +#define PATH_SERIAL_DEVICES "/dev" +#elif defined(__HAIKU__) +#define PATH_SERIAL_DEVICES "/dev/ports" #else -#define PATH_SERIAL_DEVICES "/dev/serial/by-id/" +#define PATH_SERIAL_DEVICES "/dev" #endif +#ifndef CMSPAR +#define CMSPAR 010000000000 +#endif + +#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 +#define KEY_F 0x66 +#define KEY_SHIFT_F 0x46 +#define KEY_G 0x67 +#define KEY_I 0x69 +#define KEY_L 0x6C +#define KEY_SHIFT_L 0x4C +#define KEY_M 0x6D +#define KEY_O 0x6F +#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_V 0x76 +#define KEY_X 0x78 +#define KEY_Y 0x79 +#define KEY_Z 0x7a + +typedef enum +{ + LINE_TOGGLE, + LINE_PULSE +} tty_line_mode_t; + +typedef enum +{ + SUBCOMMAND_NONE, + SUBCOMMAND_LINE_TOGGLE, + SUBCOMMAND_LINE_PULSE, + SUBCOMMAND_XMODEM, + SUBCOMMAND_MAP, +} sub_command_t; + +const char random_array[] = +{ +0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x28, 0x20, 0x28, 0x0A, 0x20, +0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x29, 0x20, 0x29, 0x0A, 0x20, +0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, +0x2E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x20, 0x20, +0x20, 0x20, 0x20, 0x7C, 0x5D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, +0x5C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2F, 0x0A, 0x20, 0x20, 0x20, 0x20, +0x20, 0x20, 0x20, 0x20, 0x60, 0x2D, 0x2D, 0x2D, 0x2D, 0x27, 0x0A, 0x0A, 0x54, +0x69, 0x6D, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x61, 0x20, 0x63, 0x6F, 0x66, +0x66, 0x65, 0x65, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6B, 0x21, 0x0A, 0x20, 0x0A, +0x00 +}; + bool interactive_mode = true; +char key_hit = 0xff; + +const char* device_name = NULL; +GList *device_list = NULL; static struct termios tio, tio_old, stdout_new, stdout_old, stdin_new, stdin_old; static unsigned long rx_total = 0, tx_total = 0; static bool connected = false; -static bool print_mode = NORMAL; static bool standard_baudrate = true; -static void (*print)(char c); -static int fd; -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 void (*printchar)(char c); +static int device_fd; static char hex_chars[2]; static unsigned char hex_char_index = 0; static char tty_buffer[BUFSIZ*2]; static size_t tty_buffer_count = 0; static char *tty_buffer_write_ptr = tty_buffer; +static pthread_t thread; +static int pipefd[2]; +static pthread_mutex_t mutex_input_ready = PTHREAD_MUTEX_INITIALIZER; +static char line[PATH_MAX]; +static size_t listing_device_name_length_max = 0; static void optional_local_echo(char c) { @@ -88,7 +190,15 @@ 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) { log_putc(c); @@ -120,22 +230,23 @@ inline static unsigned char char_to_nibble(char c) } } -void tty_flush(int fd) +void tty_sync(int fd) { ssize_t count; - do + while (tty_buffer_count > 0) { count = write(fd, tty_buffer, tty_buffer_count); if (count < 0) { // Error - debug_printf("Write error while flushing tty buffer (%s)", strerror(errno)); + tio_debug_printf("Write error while flushing tty buffer (%s)", strerror(errno)); break; } tty_buffer_count -= count; + fsync(fd); + tcdrain(fd); } - while (tty_buffer_count > 0); // Reset tty_buffer_write_ptr = tty_buffer; @@ -144,31 +255,52 @@ void tty_flush(int fd) ssize_t tty_write(int fd, const void *buffer, size_t count) { - ssize_t bytes_written = 0; + ssize_t retval = 0, bytes_written = 0; + size_t i; - if (option.output_delay) + if (option.map_o_ltu) + { + // Convert lower case to upper case + for (i = 0; i BUFSIZ) { - tty_flush(fd); + tty_sync(fd); } // Copy bytes to tty write buffer @@ -181,21 +313,145 @@ ssize_t tty_write(int fd, const void *buffer, size_t count) return bytes_written; } -static void output_hex(char c) +void *tty_stdin_input_thread(void *arg) +{ + UNUSED(arg); + char input_buffer[BUFSIZ]; + ssize_t byte_count; + ssize_t bytes_written; + + // Create FIFO pipe + if (pipe(pipefd) == -1) + { + tio_error_printf("Failed to create pipe"); + exit(EXIT_FAILURE); + } + + // Signal that input pipe is ready + pthread_mutex_unlock(&mutex_input_ready); + + // Input loop for stdin + while (1) + { + /* Input from stdin ready */ + byte_count = read(STDIN_FILENO, input_buffer, BUFSIZ); + if (byte_count < 0) + { + /* No error actually occurred */ + if (errno == EINTR) + { + continue; + } + tio_warning_printf("Could not read from stdin (%s)", strerror(errno)); + } + else if (byte_count == 0) + { + // Close write end to signal EOF in read end + close(pipefd[1]); + pthread_exit(0); + } + + if (interactive_mode) + { + static char previous_char = 0; + char input_char; + + // Process quit and flush key command + for (int i = 0; i 0) + { + bytes_written = write(pipefd[1], input_buffer, byte_count); + if (bytes_written < 0) + { + tio_warning_printf("Could not write to pipe (%s)", strerror(errno)); + break; + } + byte_count -= bytes_written; + } + } + + pthread_exit(0); +} + +void tty_input_thread_create(void) +{ + pthread_mutex_lock(&mutex_input_ready); + + if (pthread_create(&thread, NULL, tty_stdin_input_thread, NULL) != 0) { + tio_error_printf("pthread_create() error"); + exit(1); + } +} + +void tty_input_thread_wait_ready(void) +{ + pthread_mutex_lock(&mutex_input_ready); +} + +static void handle_hex_prompt(char c) { hex_chars[hex_char_index++] = c; + printf("%c", c); + print_tainted_set(); + if (hex_char_index == 2) { + usleep(100*1000); + if (option.local_echo == false) + { + printf("\b \b"); + printf("\b \b"); + } + else + { + printf(" "); + } + unsigned char hex_value = char_to_nibble(hex_chars[0]) << 4 | (char_to_nibble(hex_chars[1]) & 0x0F); hex_char_index = 0; - optional_local_echo(hex_value); - - ssize_t status = tty_write(fd, &hex_value, 1); + ssize_t status = tty_write(device_fd, &hex_value, 1); if (status < 0) { - warning_printf("Could not write to tty device"); + tio_warning_printf("Could not write to tty device"); } else { @@ -204,100 +460,500 @@ static void output_hex(char c) } } -static void toggle_line(const char *line_name, int mask) +static const char *tty_line_name(int mask) +{ + switch (mask) + { + case TIOCM_DTR: + return "DTR"; + case TIOCM_RTS: + return "RTS"; + case TIOCM_CTS: + return "CTS"; + case TIOCM_DSR: + return "DSR"; + case TIOCM_CD: + return "CD"; + case TIOCM_RI: + return "RI"; + default: + return NULL; + } +} + +void tty_line_set(int fd, tty_line_config_t line_config[]) +{ + static int state; + int i = 0; + + if (ioctl(fd, TIOCMGET, &state) < 0) + { + tio_warning_printf("Could not get line state (%s)", strerror(errno)); + return; + } + + for (i=0; i<6; i++) + { + if (line_config[i].reserved) + { + if (line_config[i].value == 0) + { + // Low + state |= line_config[i].mask; + tio_printf("Setting %s to LOW", tty_line_name(line_config[i].mask)); + } + else if (line_config[i].value == 1) + { + // High + state &= ~line_config[i].mask; + tio_printf("Setting %s to HIGH", tty_line_name(line_config[i].mask)); + } + else if (line_config[i].value == 2) + { + // Toggle + state ^= line_config[i].mask; + + if (state & line_config[i].mask) + { + tio_printf("Setting %s to LOW", tty_line_name(line_config[i].mask)); + } + else + { + tio_printf("Setting %s to HIGH", tty_line_name(line_config[i].mask)); + } + } + } + } + + if (ioctl(fd, TIOCMSET, &state) < 0) + { + tio_warning_printf("Could not set line state (%s)", strerror(errno)); + } +} + +void tty_line_toggle(int fd, int mask) { int state; if (ioctl(fd, TIOCMGET, &state) < 0) { - warning_printf("Could not get line state (%s)", strerror(errno)); + tio_warning_printf("Could not get line state (%s)", strerror(errno)); + return; + } + + if (state & mask) + { + state &= ~mask; + tio_printf("Setting %s to HIGH", tty_line_name(mask)); } else { - if (state & mask) - { - state &= ~mask; - tio_printf("set %s to LOW", line_name); - } - else - { - state |= mask; - tio_printf("set %s to HIGH", line_name); - } - if (ioctl(fd, TIOCMSET, &state) < 0) - warning_printf("Could not set line state (%s)", strerror(errno)); + state |= mask; + tio_printf("Setting %s to LOW", tty_line_name(mask)); + } + + if (ioctl(fd, TIOCMSET, &state) < 0) + { + tio_warning_printf("Could not set line state (%s)", strerror(errno)); } } -void handle_command_sequence(char input_char, char previous_char, char *output_char, bool *forward) +static void tty_line_pulse(int fd, int mask, unsigned int duration) +{ + tty_line_toggle(fd, mask); + + if (duration > 0) + { + tio_printf("Waiting %d ms", duration); + delay(duration); + } + + tty_line_toggle(fd, mask); +} + +static void tty_line_poke(int fd, int mask, tty_line_mode_t mode, unsigned int duration) +{ + switch (mode) + { + case LINE_TOGGLE: + tty_line_toggle(fd, mask); + break; + + case LINE_PULSE: + tty_line_pulse(fd, mask, duration); + break; + } +} + +static int tio_readln(void) +{ + char *p = line; + + /* Read line, accept BS and DEL as rubout characters */ + for (p = line ; p < &line[PATH_MAX-1]; ) + { + if (read(pipefd[0], p, 1) > 0) + { + if (*p == 0x08 || *p == 0x7f) + { + if (p > line) + { + write(STDOUT_FILENO, "\b \b", 3); + p--; + } + continue; + } + write(STDOUT_FILENO, p, 1); + if (*p == '\r') break; + p++; + } + } + *p = 0; + return (p - line); +} + +void tty_output_mode_set(output_mode_t mode) +{ + switch (mode) + { + case OUTPUT_MODE_NORMAL: + printchar = print_normal; + break; + + case OUTPUT_MODE_HEX: + printchar = print_hex; + break; + + case OUTPUT_MODE_END: + break; + } +} + +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; bool unused_bool; int state; + static tty_line_mode_t line_mode; + static sub_command_t sub_command = SUBCOMMAND_NONE; + static char previous_char = 0; /* Ignore unused arguments */ if (output_char == NULL) + { output_char = &unused_char; + } if (forward == NULL) + { forward = &unused_bool; + } + + // Handle sub commands + if (sub_command) + { + *forward = false; + + switch (sub_command) + { + case SUBCOMMAND_NONE: + break; + + case SUBCOMMAND_LINE_TOGGLE: + case SUBCOMMAND_LINE_PULSE: + switch (input_char) + { + case KEY_0: + tty_line_poke(device_fd, TIOCM_DTR, line_mode, option.dtr_pulse_duration); + break; + case KEY_1: + tty_line_poke(device_fd, TIOCM_RTS, line_mode, option.rts_pulse_duration); + break; + case KEY_2: + tty_line_poke(device_fd, TIOCM_CTS, line_mode, option.cts_pulse_duration); + break; + case KEY_3: + tty_line_poke(device_fd, TIOCM_DSR, line_mode, option.dsr_pulse_duration); + break; + case KEY_4: + tty_line_poke(device_fd, TIOCM_CD, line_mode, option.dcd_pulse_duration); + break; + case KEY_5: + tty_line_poke(device_fd, TIOCM_RI, line_mode, option.ri_pulse_duration); + break; + default: + tio_error_print("Invalid line number"); + break; + } + break; + + case SUBCOMMAND_XMODEM: + switch (input_char) + { + case KEY_0: + tio_printf("Send file with XMODEM-1K"); + 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"); + ret = xymodem_send(device_fd, line, XMODEM_1K); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); + } + break; + + case KEY_1: + tio_printf("Send file with XMODEM-CRC"); + 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"); + 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; + } + + sub_command = SUBCOMMAND_NONE; + return; + } /* Handle escape key commands */ - if (previous_char == KEY_CTRL_T) + if (option.prefix_enabled && previous_char == option.prefix_code) { /* Do not forward input char to output by default */ *forward = false; + /* Handle special double prefix key input case */ + if (input_char == option.prefix_code) + { + /* Forward prefix character to tty */ + *output_char = option.prefix_code; + *forward = true; + previous_char = 0; + return; + } + + // Handle commands switch (input_char) { case KEY_QUESTION: tio_printf("Key commands:"); - tio_printf(" ctrl-t ? List available key commands"); - tio_printf(" ctrl-t b Send break"); - tio_printf(" ctrl-t c Show configuration"); - tio_printf(" ctrl-t d Toggle DTR line"); - tio_printf(" ctrl-t e Toggle local echo mode"); - tio_printf(" ctrl-t h Toggle hexadecimal mode"); - tio_printf(" ctrl-t l Clear screen"); - tio_printf(" ctrl-t L Show line states"); - tio_printf(" ctrl-t q Quit"); - tio_printf(" ctrl-t r Toggle RTS line"); - tio_printf(" ctrl-t s Show statistics"); - tio_printf(" ctrl-t t Send ctrl-t key code"); - tio_printf(" ctrl-t T Toggle line timestamp mode"); - tio_printf(" ctrl-t v Show version"); + tio_printf(" ctrl-%c ? List available key commands", option.prefix_key); + tio_printf(" ctrl-%c b Send break", option.prefix_key); + tio_printf(" ctrl-%c c Show configuration", option.prefix_key); + tio_printf(" ctrl-%c e Toggle local echo mode", option.prefix_key); + tio_printf(" ctrl-%c f Toggle log to file", option.prefix_key); + tio_printf(" ctrl-%c F Flush data I/O buffers", option.prefix_key); + tio_printf(" ctrl-%c g Toggle serial port line", option.prefix_key); + tio_printf(" ctrl-%c i Toggle input mode", option.prefix_key); + tio_printf(" ctrl-%c l Clear screen", option.prefix_key); + tio_printf(" ctrl-%c L Show line states", option.prefix_key); + tio_printf(" ctrl-%c m Change mapping of characters on input or output", option.prefix_key); + 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 v Show version", 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; case KEY_SHIFT_L: - if (ioctl(fd, TIOCMGET, &state) < 0) + if (ioctl(device_fd, TIOCMGET, &state) < 0) { - warning_printf("Could not get line state (%s)", strerror(errno)); + tio_warning_printf("Could not get line state (%s)", strerror(errno)); break; } tio_printf("Line states:"); - tio_printf(" DTR: %s", (state & TIOCM_DTR) ? "HIGH" : "LOW"); - tio_printf(" RTS: %s", (state & TIOCM_RTS) ? "HIGH" : "LOW"); - tio_printf(" CTS: %s", (state & TIOCM_CTS) ? "HIGH" : "LOW"); - tio_printf(" DSR: %s", (state & TIOCM_DSR) ? "HIGH" : "LOW"); - tio_printf(" DCD: %s", (state & TIOCM_CD) ? "HIGH" : "LOW"); - tio_printf(" RI : %s", (state & TIOCM_RI) ? "HIGH" : "LOW"); - break; - case KEY_D: - toggle_line("DTR", TIOCM_DTR); + tio_printf(" DTR: %s", (state & TIOCM_DTR) ? "LOW" : "HIGH"); + tio_printf(" RTS: %s", (state & TIOCM_RTS) ? "LOW" : "HIGH"); + tio_printf(" CTS: %s", (state & TIOCM_CTS) ? "LOW" : "HIGH"); + tio_printf(" DSR: %s", (state & TIOCM_DSR) ? "LOW" : "HIGH"); + tio_printf(" DCD: %s", (state & TIOCM_CD) ? "LOW" : "HIGH"); + tio_printf(" RI : %s", (state & TIOCM_RI) ? "LOW" : "HIGH"); break; - case KEY_R: - toggle_line("RTS", TIOCM_RTS); + case KEY_F: + if (option.log) + { + log_close(); + option.log = false; + } + else + { + if (log_open(option.log_filename) == 0) + { + option.log = true; + } + } + tio_printf("Switched log to file %s", option.log ? "on" : "off"); + break; + + case KEY_SHIFT_F: + break; + + case KEY_G: + tio_printf("Please enter which serial line number to toggle:"); + tio_printf("(0) DTR"); + tio_printf("(1) RTS"); + tio_printf("(2) CTS"); + tio_printf("(3) DSR"); + tio_printf("(4) DCD"); + tio_printf("(5) RI"); + line_mode = LINE_TOGGLE; + // Process next input character as sub command + sub_command = SUBCOMMAND_LINE_TOGGLE; + break; + + case KEY_P: + tio_printf("Please enter which serial line number to pulse:"); + tio_printf("(0) DTR"); + tio_printf("(1) RTS"); + tio_printf("(2) CTS"); + tio_printf("(3) DSR"); + tio_printf("(4) DCD"); + tio_printf("(5) RI"); + line_mode = LINE_PULSE; + // Process next input character as sub command + sub_command = SUBCOMMAND_LINE_PULSE; break; case KEY_B: - tcsendbreak(fd, 0); + tcsendbreak(device_fd, 0); break; case KEY_C: tio_printf("Configuration:"); config_file_print(); options_print(); + if (option.rs485) + { + rs485_print_config(); + } + mappings_print(); break; case KEY_E: @@ -305,19 +961,47 @@ void handle_command_sequence(char input_char, char previous_char, char *output_c tio_printf("Switched local echo %s", option.local_echo ? "on" : "off"); break; - case KEY_H: - /* Toggle hexadecimal printing mode */ - if (print_mode == NORMAL) + case KEY_I: + option.input_mode += 1; + switch (option.input_mode) { - print = print_hex; - print_mode = HEX; - tio_printf("Switched to hexadecimal mode"); + case INPUT_MODE_NORMAL: + break; + + case INPUT_MODE_HEX: + option.input_mode = INPUT_MODE_HEX; + tio_printf("Switched input mode to hex"); + break; + + case INPUT_MODE_LINE: + option.input_mode = INPUT_MODE_LINE; + tio_printf("Switched input mode to line"); + break; + + case INPUT_MODE_END: + option.input_mode = INPUT_MODE_NORMAL; + tio_printf("Switched input mode to normal"); + break; } - else + break; + + case KEY_O: + option.output_mode += 1; + switch (option.output_mode) { - print = print_normal; - print_mode = NORMAL; - tio_printf("Switched to normal mode"); + case OUTPUT_MODE_NORMAL: + break; + + case OUTPUT_MODE_HEX: + tty_output_mode_set(OUTPUT_MODE_HEX); + tio_printf("Switched output mode to hex"); + break; + + case OUTPUT_MODE_END: + option.output_mode = OUTPUT_MODE_NORMAL; + tty_output_mode_set(OUTPUT_MODE_NORMAL); + tio_printf("Switched output mode to normal"); + break; } break; @@ -326,10 +1010,68 @@ void handle_command_sequence(char input_char, char previous_char, char *output_c printf("\033c"); break; + case KEY_M: + /* 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: /* Exit upon ctrl-t q sequence */ exit(EXIT_SUCCESS); + case KEY_R: + /* Run script */ + 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: /* Show tx/rx statistics upon ctrl-t s sequence */ tio_printf("Statistics:"); @@ -338,35 +1080,64 @@ void handle_command_sequence(char input_char, char previous_char, char *output_c break; case KEY_T: - /* Send ctrl-t key code upon ctrl-t t sequence */ - *output_char = KEY_CTRL_T; - *forward = true; - break; - - case KEY_SHIFT_T: option.timestamp += 1; switch (option.timestamp) { case TIMESTAMP_NONE: break; case TIMESTAMP_24HOUR: - tio_printf("Switched to 24hour timestamp mode"); + tio_printf("Switched timestamp mode to 24hour"); break; case TIMESTAMP_24HOUR_START: - tio_printf("Switched to 24hour-start timestamp mode"); + tio_printf("Switched timestamp mode to 24hour-start"); + break; + case TIMESTAMP_24HOUR_DELTA: + tio_printf("Switched timestamp mode to 24hour-delta"); break; case TIMESTAMP_ISO8601: - tio_printf("Switched to iso8601 timestamp mode"); + 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 off"); + tio_printf("Switched timestamp mode off"); break; } 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 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; + + case KEY_Y: + 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"); + ret = xymodem_send(device_fd, line, YMODEM); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); + } + break; + + case KEY_Z: + tio_printf_array(random_array); break; default: @@ -374,6 +1145,8 @@ void handle_command_sequence(char input_char, char previous_char, char *output_c break; } } + + previous_char = input_char; } void stdin_restore(void) @@ -388,7 +1161,7 @@ void stdin_configure(void) /* Save current stdin settings */ if (tcgetattr(STDIN_FILENO, &stdin_old) < 0) { - error_printf("Saving current stdin settings failed"); + tio_error_printf("Saving current stdin settings failed"); exit(EXIT_FAILURE); } @@ -406,7 +1179,7 @@ void stdin_configure(void) status = tcsetattr(STDIN_FILENO, TCSANOW, &stdin_new); if (status == -1) { - error_printf("Could not apply new stdin settings (%s)", strerror(errno)); + tio_error_printf("Could not apply new stdin settings (%s)", strerror(errno)); exit(EXIT_FAILURE); } @@ -417,20 +1190,24 @@ void stdin_configure(void) void stdout_restore(void) { tcsetattr(STDOUT_FILENO, TCSANOW, &stdout_old); + + // If terminal is vt100 + if (option.vt100) + { + // Disable DEC Special Graphics character set just in case it was randomly + // enabled by noise from serial device. + putchar('\017'); + } } 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) { - error_printf("Saving current stdio settings failed"); + tio_error_printf("Saving current stdio settings failed"); exit(EXIT_FAILURE); } @@ -440,6 +1217,12 @@ void stdout_configure(void) /* Reconfigure stdout (RAW configuration) */ cfmakeraw(&stdout_new); + /* Allow ^C / SIGINT (to allow termination when piping to tio) */ + if (!interactive_mode) + { + stdout_new.c_lflag |= ISIG; + } + /* Control characters */ stdout_new.c_cc[VTIME] = 0; /* Inter-character timer unused */ stdout_new.c_cc[VMIN] = 1; /* Blocking read until 1 character received */ @@ -448,12 +1231,12 @@ void stdout_configure(void) status = tcsetattr(STDOUT_FILENO, TCSANOW, &stdout_new); if (status == -1) { - error_printf("Could not apply new stdout settings (%s)", strerror(errno)); + tio_error_printf("Could not apply new stdout settings (%s)", strerror(errno)); exit(EXIT_FAILURE); } /* At start use normal print function */ - print = print_normal; + printchar = print_normal; /* Make sure we restore old stdout settings on exit */ atexit(&stdout_restore); @@ -461,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; @@ -490,7 +1270,7 @@ void tty_configure(void) standard_baudrate = false; break; #else - error_printf("Invalid baud rate"); + tio_error_printf("Invalid baud rate"); exit(EXIT_FAILURE); #endif } @@ -501,7 +1281,7 @@ void tty_configure(void) status = cfsetispeed(&tio, baudrate); if (status == -1) { - error_printf("Could not configure input speed (%s)", strerror(errno)); + tio_error_printf("Could not configure input speed (%s)", strerror(errno)); exit(EXIT_FAILURE); } @@ -509,7 +1289,7 @@ void tty_configure(void) status = cfsetospeed(&tio, baudrate); if (status == -1) { - error_printf("Could not configure output speed (%s)", strerror(errno)); + tio_error_printf("Could not configure output speed (%s)", strerror(errno)); exit(EXIT_FAILURE); } } @@ -531,30 +1311,31 @@ void tty_configure(void) tio.c_cflag |= CS8; break; default: - error_printf("Invalid data bits"); + tio_error_printf("Invalid data bits"); exit(EXIT_FAILURE); } /* 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 - { - 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 */ @@ -567,29 +1348,42 @@ void tty_configure(void) tio.c_cflag |= CSTOPB; break; default: - error_printf("Invalid stop bits"); + tio_error_printf("Invalid stop bits"); exit(EXIT_FAILURE); } /* 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 - { - 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 */ @@ -601,61 +1395,906 @@ 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) + 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); + } +} + +static bool is_serial_device(const char *format, ...) +{ + char filename[PATH_MAX]; + struct winsize ws; + int bytes_printed; + int status = true; + struct stat st; + va_list args; + int fd = -1; + + va_start(args, format); + bytes_printed = vsnprintf(filename, sizeof(filename), format, args); + va_end(args); + + if (bytes_printed < 0) + { + return false; + } + +#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; + } + + // Make sure it is a character device + if ((st.st_mode & S_IFMT) != S_IFCHR) + { + return false; + } + + // Make sure it is a tty + status = isatty(fd); + if (status == 0) + { + goto error; + } + + // Serial devices do not have rows and columns + status = ioctl(fd, TIOCGWINSZ, &ws); + if (status == 0) + { + status = true; + if (ws.ws_row && ws.ws_col) { - token = strtok(buffer,","); + status = false; + goto error; } - else + } + +error: + close(fd); + return status; +} + +static void list_serial_devices_by_id(void) +{ +#ifdef PATH_SERIAL_DEVICES_BY_ID + DIR *d = opendir(PATH_SERIAL_DEVICES_BY_ID); + if (d) + { + struct dirent *dir; + + printf("By-id\n"); + printf("--------------------------------------------------------------------------------\n"); + + while ((dir = readdir(d)) != NULL) { - token = strtok(NULL, ","); + if ((strcmp(dir->d_name, ".")) && (strcmp(dir->d_name, ".."))) + { + if (is_serial_device("%s/%s", PATH_SERIAL_DEVICES_BY_ID, 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) + { + struct dirent *dir; + + printf("\nBy-path\n"); + printf("--------------------------------------------------------------------------------\n"); + + while ((dir = readdir(d)) != NULL) + { + if ((strcmp(dir->d_name, ".")) && (strcmp(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); + } + } + } + closedir(d); + } +#endif +} + +static gint compare_uptime(gconstpointer a, gconstpointer b) +{ + device_t *device_a = (device_t *) a; + device_t *device_b = (device_t *) b; + + // Make sure we end up with device with smallest uptime last in list + if (device_a->uptime > device_b->uptime) + return -1; + else if (device_a->uptime < device_b->uptime) + return 1; + else + return 0; +} + +#if defined(__linux__) + +// Function to get serial port type as a string +const char* get_serial_port_type(const char* port_name) +{ + int fd; + static struct serial_struct serial_info; + + // Open the serial port + fd = open(port_name, O_RDWR); + if (fd == -1) + { + return ""; + } + + // Get serial port information + if (ioctl(fd, TIOCGSERIAL, &serial_info) == -1) + { + close(fd); + return ""; + } + + // Close the serial port + close(fd); + + // Return the serial port type as a string + switch (serial_info.type) + { + case PORT_UNKNOWN: + return "Unknown"; + + case PORT_8250: + return "8250 UART"; + + case PORT_16450: + return "16450 UART"; + + case PORT_16550: + return "16550 UART"; + + case PORT_16550A: + return "16550A UART"; + + case PORT_16650: + return "16650 UART"; + + case PORT_16650V2: + return "16650V2 UART"; + + case PORT_16750: + return "16750 UART"; + + case PORT_STARTECH: + return "Startech UART"; + + case PORT_16850: + return "16850 UART"; + + case PORT_16C950: + return "16C950 UART"; + + case PORT_16654: + return "16654 UART"; + + case PORT_RSA: + return "RSA UART"; + + default: + return ""; + } +} + +#else + +const char* get_serial_port_type(const char* port_name) +{ + (void)port_name; + return ""; +} + +#endif + +static void search_reset(void) +{ + GList *iter; + + if (g_list_length(device_list) == 0) + { + return; + } + + // Free data of all list elements + for (iter = device_list; iter != NULL; iter = g_list_next(iter)) + { + device_t *device = (device_t *) iter->data; + g_free(device->tid); + g_free(device->path); + g_free(device->driver); + g_free(device->description); + } + + // Free all list elements + g_list_free_full(device_list, g_free); + + // 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; + char path[PATH_MAX] = {}; + char device_path[PATH_MAX] = {}; + char driver_path[PATH_MAX] = {}; + double current_time, creation_time; + ssize_t length; + + search_reset(); + + // Open the sysfs directory for the tty subsystem + dir = opendir("/sys/class/tty"); + 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; } - if (token != NULL) + // Skip non serial devices + if (is_serial_device("/dev/%s", entry->d_name) == false) { - if (strcmp(token,"INLCR") == 0) + continue; + } + + // Construct the path to the device's device symlink + snprintf(path, sizeof(path), "/sys/class/tty/%s/device", entry->d_name); + + // Read the device symlink to get the device path + // Example symlinks: + // /sys/class/tty/ttyUSB0/device -> ../../../ttyUSB0 + // /sys/class/tty/ttyACM0/device -> ../../../3-6.4:1.2 + length = readlink(path, device_path, sizeof(device_path) - 1); + if (length == -1) + { + continue; + } + + // Null-terminate the string + device_path[length] = '\0'; + + // Extract last part of device path (string after last '/') + // Example resulting device_name: + // "ttyUSB0" + // "3-6.4:1.2" + char *last_part = strrchr(device_path, '/'); + last_part++; // Move past the '/' + + // Find that part in /sys/devices and return first result string + // Example devices_path: + // "/sys/devices/pci0000:00/0000:00:14.0/usb3/3-6/3-6.3/3-6.3:1.0/ttyUSB0" + // "/sys/devices/pci0000:00/0000:00:14.0/usb3/3-6/3-6.4/3-6.4:1.2" + char *devices_path = fs_search_directory("/sys/devices", last_part); + if (devices_path == NULL) + { + continue; + } + + // Remove last part if it contains device short name (e.g ttyUSB0) + // Example resulting devices_path: + // "/sys/devices/pci0000:00/0000:00:14.0/usb3/3-6/3-6.3/3-6.3:1.0" + // "/sys/devices/pci0000:00/0000:00:14.0/usb3/3-6/3-6.4/3-6.4:1.2" + last_part = strrchr(devices_path, '/'); + last_part++; + if (strcmp(last_part, entry->d_name) == 0) + { + // Remove last part (string after last '/') + char *slash = strrchr(devices_path, '/'); + int index = (int) (slash - devices_path); + devices_path[index] = '\0'; + } + + // Hash remaining string to get unique topology ID + unsigned long hash = djb2_hash((const unsigned char *)devices_path); + char tid[5]; + base62_encode(hash, tid); + free(devices_path); + + // Construct the path to the device's driver symlink + snprintf(path, sizeof(path), "/sys/class/tty/%s/device/driver", entry->d_name); + + // Read the symlink to get the driver's path + length = readlink(path, driver_path, sizeof(driver_path) - 1); + if (length == -1) + { + continue; + } + + // Null-terminate the string + driver_path[length] = '\0'; + + // Extract the driver name from the path + char *driver = strrchr(driver_path, '/'); + if (driver == NULL) + { + continue; + } + driver++; // Move past the last '/' + + // Construct the path to the TTY device file + snprintf(path, sizeof(path), "/dev/%s", entry->d_name); + + // Calculate uptime + creation_time = fs_get_creation_time(path); + double uptime = current_time - creation_time; + + // Read sysfs files to get best possible description + char description[50] = {}; + 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)); + } + + // Do not add devices excluded by exclude patterns + if (match_patterns(path, option.exclude_devices)) + { + continue; + } + if (match_patterns(driver, option.exclude_drivers)) + { + continue; + } + if (match_patterns(tid, option.exclude_tids)) + { + 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(tid); + device->uptime = uptime; + device->driver = g_strdup(driver); + device->description = g_strdup(description); + + // 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; +} + +#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) + { + 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; + for (iter = device_list; iter != NULL; iter = g_list_next(iter)) + { + device_t *device = (device_t *) iter->data; + + // Print device information + 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"); + } + + list_serial_devices_by_id(); + list_serial_devices_by_path(); +} + +void tty_search(void) +{ + GList *iter; + device_t *device = NULL; + double uptime_minimum = 0; + bool no_new = true; + + switch (option.auto_connect) + { + case AUTO_CONNECT_NEW: + tty_search_for_serial_devices(); + + // Save smallest uptime + if (g_list_length(device_list) > 0) { - tio.c_iflag |= INLCR; + // Get latest registered device (smallest uptime) + GList *last = g_list_last(device_list); + device = last->data; + uptime_minimum = device->uptime; } - else if (strcmp(token,"IGNCR") == 0) + + tio_printf("Waiting for tty device.."); + + while (no_new) { - tio.c_iflag |= IGNCR; + tty_search_for_serial_devices(); + + // Iterate through the device list generated by search + for (iter = device_list; iter != NULL; iter = g_list_next(iter)) + { + device = (device_t *) iter->data; + + // Find first new device + if (device->uptime < uptime_minimum) + { + // Match found -> update device + device_name = device->path; + no_new = false; + break; + } + } + if (no_new) + { + usleep(500*1000); // Sleep 0.5 s + } } - else if (strcmp(token,"ICRNL") == 0) + return; + + case AUTO_CONNECT_LATEST: + tty_search_for_serial_devices(); + if (g_list_length(device_list) > 0) { - tio.c_iflag |= ICRNL; + // Get latest registered device (smallest uptime) + GList *last = g_list_last(device_list); + device = last->data; + device_name = device->path; } - else if (strcmp(token,"OCRNL") == 0) + return; + + case AUTO_CONNECT_DIRECT: + if (config.device != NULL) { - map_o_cr_nl = true; - } - else if (strcmp(token,"ODELBS") == 0) - { - map_o_del_bs = true; - } - else if (strcmp(token,"INLCRNL") == 0) - { - map_i_nl_crnl = true; - } - else if (strcmp(token, "ONLCRNL") == 0) - { - map_o_nl_crnl = true; + // 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 { - printf("Error: Unknown mapping flag %s\n", token); - exit(EXIT_FAILURE); + // Fallback to use the target direcly + device_name = option.target; } - } - else - { - token_found = false; - } + + if (strlen(device_name) == TOPOLOGY_ID_SIZE) + { + // Potential topology ID detected -> trigger device search + tty_search_for_serial_devices(); + + // Iterate through the device list generated by search + for (iter = device_list; iter != NULL; iter = g_list_next(iter)) + { + device = (device_t *) iter->data; + + if (strcmp(device->tid, device_name) == 0) + { + // Topology ID match found -> use corresponding device name + device_name = device->path; + + return; + } + } + } + break; + + default: + // Should never be reached + tio_printf("Unknown connection strategy"); + exit(EXIT_FAILURE); } - free(buffer); } void tty_wait_for_device(void) @@ -664,62 +2303,80 @@ void tty_wait_for_device(void) int status; int maxfd; struct timeval tv; - static char input_char, previous_char = 0; + static char input_char; static bool first = true; static int last_errno = 0; /* Loop until device pops up */ while (true) { - if (first) - { - /* Don't wait first time */ - tv.tv_sec = 0; - tv.tv_usec = 1; - first = false; - } - else - { - /* Wait up to 1 second */ - tv.tv_sec = 1; - tv.tv_usec = 0; - } + tty_search(); - FD_ZERO(&rdfs); - FD_SET(STDIN_FILENO, &rdfs); - maxfd = MAX(STDIN_FILENO, socket_add_fds(&rdfs, false)); - - /* Block until input becomes available or timeout */ - status = select(maxfd + 1, &rdfs, NULL, NULL, &tv); - if (status > 0) + if (interactive_mode) { - if (FD_ISSET(STDIN_FILENO, &rdfs)) + /* In interactive mode, while waiting for tty device, we need to + * read from stdin to react on input key commands. */ + if (first) { - /* Input from stdin ready */ - - /* Read one character */ - status = read(STDIN_FILENO, &input_char, 1); - if (status <= 0) - { - error_printf("Could not read from stdin"); - exit(EXIT_FAILURE); - } - - /* Handle commands */ - handle_command_sequence(input_char, previous_char, NULL, NULL); - - previous_char = input_char; + /* Don't wait first time */ + tv.tv_sec = 0; + tv.tv_usec = 1; + first = false; + } + else + { + /* Wait up to 1 second for input */ + tv.tv_sec = 1; + tv.tv_usec = 0; + } + + FD_ZERO(&rdfs); + FD_SET(pipefd[0], &rdfs); + maxfd = MAX(pipefd[0], socket_add_fds(&rdfs, false)); + + /* Block until input becomes available or timeout */ + status = select(maxfd + 1, &rdfs, NULL, NULL, &tv); + if (status > 0) + { + if (FD_ISSET(pipefd[0], &rdfs)) + { + /* Input from stdin ready */ + + /* Read one character */ + status = read(pipefd[0], &input_char, 1); + if (status <= 0) + { + tio_error_printf("Could not read from stdin"); + exit(EXIT_FAILURE); + } + + /* Handle commands */ + handle_command_sequence(input_char, NULL, NULL); + } + socket_handle_input(&rdfs, NULL); + } + 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 } - socket_handle_input(&rdfs, NULL); - } - else if (status == -1) - { - error_printf("select() failed (%s)", strerror(errno)); - exit(EXIT_FAILURE); } /* Test for accessible device file */ - status = access(option.tty_device, R_OK); + status = access(device_name, R_OK); if (status == 0) { last_errno = 0; @@ -727,10 +2384,18 @@ void tty_wait_for_device(void) } else if (last_errno != errno) { - 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; } + + if (!interactive_mode) + { + /* In non-interactive mode we do not need to handle input key + * commands so we simply sleep 1 second between checking for + * presence of tty device */ + sleep(1); + } } } @@ -739,15 +2404,24 @@ void tty_disconnect(void) if (connected) { tio_printf("Disconnected"); - flock(fd, LOCK_UN); - close(fd); + flock(device_fd, LOCK_UN); + close(device_fd); connected = false; + + /* Fire alert action */ + alert_disconnect(); } } void tty_restore(void) { - tcsetattr(fd, TCSANOW, &tio_old); + tcsetattr(device_fd, TCSANOW, &tio_old); + + if (option.rs485) + { + /* Restore original RS-485 mode */ + rs485_mode_restore(device_fd); + } if (connected) { @@ -760,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"; @@ -779,29 +2457,69 @@ void forward_to_tty(int fd, char output_char) status = tty_write(fd, crlf, 2); if (status < 0) { - warning_printf("Could not write to tty device"); + tio_warning_printf("Could not write to tty device"); } tx_total += 2; } else { - if (print_mode == HEX) + switch (option.output_mode) { - output_hex(output_char); - } - else - { - /* Send output to tty device */ - optional_local_echo(output_char); - status = tty_write(fd, &output_char, 1); - if (status < 0) - { - warning_printf("Could not write to tty device"); - } + case OUTPUT_MODE_NORMAL: + if (option.input_mode == INPUT_MODE_HEX) + { + handle_hex_prompt(output_char); + } + else + { + /* Send output to tty device */ + if (option.input_mode != INPUT_MODE_LINE) + { + optional_local_echo(output_char); + } - /* Update transmit statistics */ - tx_total++; + if ((output_char == 0) && (option.map_o_nulbrk)) + { + status = tcsendbreak(fd, 0); + } + else + { + status = tty_write(fd, &output_char, 1); + } + + if (status < 0) + { + tio_warning_printf("Could not write to tty device"); + } + + /* Update transmit statistics */ + tx_total++; + } + break; + + case OUTPUT_MODE_HEX: + if (option.input_mode == INPUT_MODE_HEX) + { + handle_hex_prompt(output_char); + } + else 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; + + case OUTPUT_MODE_END: + break; } } } @@ -811,76 +2529,77 @@ int tty_connect(void) fd_set rdfs; /* Read file descriptor set */ int maxfd; /* Maximum file descriptor used */ char input_char, output_char; - char input_buffer[BUFSIZ]; - static char previous_char = 0; + char input_buffer[BUFSIZ] = {}; static bool first = true; int status; - bool next_timestamp = false; + bool do_timestamp = false; char* now = NULL; + struct timeval tval_before = {}, tval_now, tval_result; /* Open tty device */ - fd = open(option.tty_device, O_RDWR | O_NOCTTY | O_NONBLOCK); - if (fd < 0) + device_fd = open(device_name, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (device_fd < 0) { - error_printf_silent("Could not open tty device (%s)", strerror(errno)); + tio_error_printf_silent("Could not open tty device (%s)", strerror(errno)); goto error_open; } /* Make sure device is of tty type */ - if (!isatty(fd)) + if (!isatty(device_fd)) { - error_printf("Not a tty device"); + tio_error_printf("Not a tty device"); exit(EXIT_FAILURE);; } /* Lock device file */ - status = flock(fd, LOCK_EX | LOCK_NB); + status = flock(device_fd, LOCK_EX | LOCK_NB); if ((status == -1) && (errno == EWOULDBLOCK)) { - error_printf("Device file is locked by another process"); + tio_error_printf("Device file is locked by another process"); exit(EXIT_FAILURE); } /* Flush stale I/O data (if any) */ - tcflush(fd, TCIOFLUSH); + tcflush(device_fd, TCIOFLUSH); /* Print connect status */ - tio_printf("Connected"); + tio_printf("Connected to %s", device_name); connected = true; print_tainted = false; + /* Fire alert action */ + alert_connect(); + if (option.timestamp) { - next_timestamp = true; + do_timestamp = true; } /* Manage print output mode */ - if (option.hex_mode) - { - print = print_hex; - print_mode = HEX; - } - else - { - print = print_normal; - print_mode = NORMAL; - } + tty_output_mode_set(option.output_mode); /* Save current port settings */ - if (tcgetattr(fd, &tio_old) < 0) + if (tcgetattr(device_fd, &tio_old) < 0) { + tio_error_printf_silent("Could not get port settings (%s)", strerror(errno)); goto error_tcgetattr; } #ifdef HAVE_IOSSIOSPEED if (!standard_baudrate) { - /* OS X wants these fields left alone. We'll set baudrate with iossiospeed below. */ + /* OS X wants these fields left alone before setting arbitrary baud rate */ tio.c_ispeed = tio_old.c_ispeed; tio.c_ospeed = tio_old.c_ospeed; } #endif + /* Manage RS-485 mode */ + if (option.rs485) + { + rs485_mode_enable(device_fd); + } + /* Make sure we restore tty settings on exit */ if (first) { @@ -889,42 +2608,86 @@ int tty_connect(void) } /* Activate new port settings */ - status = tcsetattr(fd, TCSANOW, &tio); + status = tcsetattr(device_fd, TCSANOW, &tio); if (status == -1) { - error_printf_silent("Could not apply port settings (%s)", strerror(errno)); + tio_error_printf_silent("Could not apply port settings (%s)", strerror(errno)); goto error_tcsetattr; } -#ifdef HAVE_TERMIOS2 + /* Set arbitrary baudrate (only works on supported platforms) */ if (!standard_baudrate) { - if (setspeed2(fd, option.baudrate) != 0) + if (setspeed(device_fd, option.baudrate) != 0) { - error_printf_silent("Could not set baudrate speed (%s)", strerror(errno)); + tio_error_printf_silent("Could not set baudrate speed (%s)", strerror(errno)); goto error_setspeed; } } -#endif -#ifdef HAVE_IOSSIOSPEED - if (!standard_baudrate) + /* If stdin is a pipe forward all input to tty device */ + if (interactive_mode == false) { - if (iossiospeed(fd, option.baudrate) != 0) + while (true) { - error_printf_silent("Could not set baudrate speed (%s)", strerror(errno)); - goto error_setspeed; + int ret = read(pipefd[0], &input_char, 1); + if (ret < 0) + { + tio_error_printf("Could not read from pipe (%s)", strerror(errno)); + exit(EXIT_FAILURE); + } + else if (ret > 0) + { + // Forward to tty device + ret = write(device_fd, &input_char, 1); + if (ret < 0) + { + tio_error_printf("Could not write to serial device (%s)", strerror(errno)); + exit(EXIT_FAILURE); + } + } + else + { + // EOF - finished forwarding + break; + } } } -#endif + + /* Manage script activation */ + if (option.script_run != SCRIPT_RUN_NEVER) + { + script_run(device_fd, NULL); + + if (option.script_run == SCRIPT_RUN_ONCE) + { + option.script_run = SCRIPT_RUN_NEVER; + } + } + + // Exit if piped input + if (interactive_mode == false) + { + 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) { FD_ZERO(&rdfs); - FD_SET(fd, &rdfs); - FD_SET(STDIN_FILENO, &rdfs); - maxfd = MAX(fd, STDIN_FILENO); + FD_SET(device_fd, &rdfs); + FD_SET(pipefd[0], &rdfs); + + maxfd = MAX(device_fd, pipefd[0]); maxfd = MAX(maxfd, socket_add_fds(&rdfs, true)); /* Block until input becomes available */ @@ -932,54 +2695,170 @@ int tty_connect(void) if (status > 0) { bool forward = false; - if (FD_ISSET(fd, &rdfs)) + if (FD_ISSET(device_fd, &rdfs)) { + /*******************************/ /* Input from tty device ready */ - ssize_t bytes_read = read(fd, input_buffer, BUFSIZ); + /*******************************/ + + ssize_t bytes_read = read(device_fd, input_buffer, BUFSIZ); if (bytes_read <= 0) { /* Error reading - device is likely unplugged */ - error_printf_silent("Could not read from tty device"); + tio_error_printf_silent("Could not read from tty device"); goto error_read; } /* Update receive statistics */ rx_total += bytes_read; + // Manage timeout based timestamping in hex mode + if ((option.output_mode == OUTPUT_MODE_HEX) && (option.hex_n_value == 0)) + { + if (option.timestamp != TIMESTAMP_NONE) + { + gettimeofday(&tval_now, NULL); + timersub(&tval_now, &tval_before, &tval_result); + if ((tval_result.tv_sec * 1000 + tval_result.tv_usec / 1000) > option.timestamp_timeout) + { + now = timestamp_current_time(); + if (now) + { + ansi_printf_raw("\r\n[%s] ", now); + if (option.log) + { + log_printf("\r\n[%s] ", now); + } + do_timestamp = false; + } + } + tval_before = tval_now; + } + } + /* Process input byte by byte */ for (int i=0; i 0) + { + static bool first_ = true; + if ((count % option.hex_n_value) == 0) + { + if (option.timestamp != TIMESTAMP_NONE) + { + now = timestamp_current_time(); + if (first_) + { + ansi_printf_raw("[%s] ", now); + if (option.log) + { + log_printf("[%s] ", now); + } + first_ = false; + } + else + { + ansi_printf_raw("\r\n[%s] ", now); + if (option.log) + { + log_printf("\n[%s] ", now); + } + } + } + else + { + if (first_) + { + // Do nothing + first_ = false; + } + else + { + putchar('\r'); + putchar('\n'); + + if (option.log) + { + log_putc('\n'); + } + } + } + } + } + count++; + break; + + default: + tio_error_printf("Unknown output mode"); + exit(EXIT_FAILURE); + break; + } + + /* Convert MSB to LSB bit order */ + if (option.map_i_msb2lsb) + { + char ch = input_char; + input_char = 0; + for (int j = 0; j < 8; ++j) + { + input_char |= ((1 << j) & ch) ? (1 << (7 - j)) : 0; } } /* Map input character */ - if ((input_char == '\n') && (map_i_nl_crnl)) + if ((input_char == '\n') && (option.map_i_nl_crnl) && (!option.map_i_msb2lsb)) { - print('\r'); - print('\n'); + printchar('\r'); + printchar('\n'); if (option.timestamp) { - next_timestamp = true; + do_timestamp = true; } } + else if ((input_char == '\r') && (option.map_i_cr_crnl) && (!option.map_i_msb2lsb)) + { + 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 */ @@ -994,19 +2873,28 @@ int tty_connect(void) if (input_char == '\n' && option.timestamp) { - next_timestamp = true; + do_timestamp = true; } } } - else if (FD_ISSET(STDIN_FILENO, &rdfs)) + else if (FD_ISSET(pipefd[0], &rdfs)) { + /**************************/ /* Input from stdin ready */ - ssize_t bytes_read = read(STDIN_FILENO, input_buffer, BUFSIZ); - if (bytes_read <= 0) + /**************************/ + + ssize_t bytes_read = read(pipefd[0], input_buffer, BUFSIZ); + if (bytes_read < 0) { - error_printf_silent("Could not read from stdin"); + tio_error_printf_silent("Could not read from stdin (%s)", strerror(errno)); goto error_read; } + else if (bytes_read == 0) + { + /* Reached EOF (when piping to stdin, never reached) */ + tty_sync(device_fd); + exit(EXIT_SUCCESS); + } /* Process input byte by byte */ for (int i=0; id_name, ".")) && (strcmp(dir->d_name, ".."))) - { -#ifdef __APPLE__ -#define TTY_DEVICES_PREFIX "tty." - if (!strncmp(dir->d_name, TTY_DEVICES_PREFIX, sizeof(TTY_DEVICES_PREFIX) - 1)) -#endif - printf("%s%s\n", PATH_SERIAL_DEVICES, dir->d_name); - } - } - closedir(d); - } -} diff --git a/src/tty.h b/src/tty.h index 339dce9..39c64af 100644 --- a/src/tty.h +++ b/src/tty.h @@ -1,5 +1,5 @@ /* - * tio - a simple serial terminal I/O tool + * tio - a serial device I/O tool * * Copyright (c) 2014-2022 Martin Lund * @@ -22,31 +22,65 @@ #pragma once #include +#include -#define KEY_QUESTION 0x3f -#define KEY_B 0x62 -#define KEY_C 0x63 -#define KEY_E 0x65 -#define KEY_H 0x68 -#define KEY_L 0x6C -#define KEY_Q 0x71 -#define KEY_S 0x73 -#define KEY_T 0x74 -#define KEY_SHIFT_T 0x54 -#define KEY_CTRL_T 0x14 -#define KEY_V 0x76 -#define KEY_D 0x64 -#define KEY_R 0x72 -#define KEY_SHIFT_L 0x4C +#define LINE_HIGH true +#define LINE_LOW false -#define NORMAL 0 -#define HEX 1 +#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, + AUTO_CONNECT_NEW, + AUTO_CONNECT_LATEST, + AUTO_CONNECT_END, +} auto_connect_t; + +typedef struct +{ + char *tid; + double uptime; + char *path; + char *driver; + char *description; +} device_t; + +typedef struct +{ + int mask; + int value; + bool reserved; +} tty_line_config_t; + +extern const char *device_name; extern bool interactive_mode; 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); +void tty_input_thread_create(void); +void tty_input_thread_wait_ready(void); +void tty_line_set(int fd, tty_line_config_t line_config[]); +void tty_search(void); +GList *tty_search_for_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 new file mode 100644 index 0000000..6bc3da3 --- /dev/null +++ b/src/xymodem.c @@ -0,0 +1,739 @@ +/* + * Minimalistic implementation of the xmodem-1k and ymodem sender protocol. + * https://en.wikipedia.org/wiki/XMODEM + * https://en.wikipedia.org/wiki/YMODEM + * + * SPDX-License-Identifier: GPL-2.0-or-later OR MIT-0 + * + */ + +#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 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)) + +struct xpacket_1k { + uint8_t type; + uint8_t seq; + uint8_t nseq; + uint8_t data[1024]; + uint8_t crc_hi; + uint8_t crc_lo; +} __attribute__((packed)); + +struct xpacket { + uint8_t type; + uint8_t seq; + uint8_t nseq; + uint8_t data[128]; + uint8_t crc_hi; + uint8_t crc_lo; +} __attribute__((packed)); + +/* See https://en.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks */ +static uint16_t crc16(const uint8_t *data, uint16_t size) +{ + uint16_t crc, s; + + for (crc = 0; size > 0; size--) { + s = *data++ ^ (crc >> 8); + s ^= (s >> 4); + crc = (crc << 8) ^ s ^ (s << 5) ^ (s << 12); + } + return crc; +} + +static int xmodem_1k(int sio, const void *data, size_t len, int seq) +{ + struct xpacket_1k packet; + const uint8_t *buf = data; + char resp = 0; + int rc, crc; + + /* Drain pending characters from serial line. Insist on the + * last drained character being 'C'. + */ + while(1) { + if (key_hit) + return -1; + rc = read_poll(sio, &resp, 1, 50); + if (rc == 0) { + if (resp == 'C') break; + if (resp == CAN) return ERR; + continue; + } + else if (rc < 0) { + tio_error_print("Read sync from serial failed"); + return ERR; + } + } + + /* Always work with 1K packets */ + packet.seq = seq; + packet.type = STX; + + while (len) { + size_t sz, z = 0; + char *from, status; + + /* Build next packet, pad with 0 to full seq */ + z = min(len, sizeof(packet.data)); + memcpy(packet.data, buf, z); + memset(packet.data + z, 0, sizeof(packet.data) - z); + crc = crc16(packet.data, sizeof(packet.data)); + packet.crc_hi = crc >> 8; + packet.crc_lo = crc; + packet.nseq = 0xff - packet.seq; + + /* Send packet */ + from = (char *) &packet; + sz = sizeof(packet); + while (sz) { + if (key_hit) + return ERR; + if ((rc = write(sio, from, sz)) < 0 ) { + if (errno == EWOULDBLOCK) { + usleep(1000); + continue; + } + tio_error_print("Write packet to serial failed"); + return ERR; + } + from += rc; + sz -= rc; + } + + /* Clear response */ + resp = 0; + + /* 'lrzsz' does not ACK ymodem's fin packet */ + if (seq == 0 && packet.data[0] == 0) resp = ACK; + + /* Read receiver response, timeout 1 s */ + for(int n=0; n < 20; n++) { + if (key_hit) + return ERR; + rc = read_poll(sio, &resp, 1, 50); + if (rc < 0) { + tio_error_print("Read ack/nak from serial failed"); + return ERR; + } else if(rc > 0) { + break; + } + } + + /* Update "progress bar" */ + switch (resp) { + case NAK: status = 'N'; break; + case ACK: status = '.'; break; + case 'C': status = 'C'; break; + case CAN: status = '!'; return ERR; + default: status = '?'; + } + write(STDOUT_FILENO, &status, 1); + + /* Move to next block after ACK */ + if (resp == ACK) { + packet.seq++; + len -= z; + buf += z; + } + } + + /* Send EOT at 1 Hz until ACK or CAN received */ + while (seq) { + if (key_hit) + return ERR; + if (write(sio, EOT_STR, 1) < 0) { + tio_error_print("Write EOT to serial failed"); + return ERR; + } + write(STDOUT_FILENO, "|", 1); + /* 1s timeout */ + rc = read_poll(sio, &resp, 1, 1000); + if (rc < 0) { + tio_error_print("Read from serial failed"); + return ERR; + } else if(rc == 0) { + continue; + } + if (resp == ACK || resp == CAN) { + write(STDOUT_FILENO, "\r\n", 2); + return (resp == ACK) ? OK : ERR; + } + } + return 0; /* not reached */ +} + +static int xmodem(int sio, const void *data, size_t len) +{ + struct xpacket packet; + const uint8_t *buf = data; + char resp = 0; + int rc, crc; + + /* Drain pending characters from serial line. Insist on the + * last drained character being 'C'. + */ + while(1) { + if (key_hit) + return -1; + rc = read_poll(sio, &resp, 1, 50); + if (rc == 0) { + if (resp == 'C') break; + if (resp == CAN) return ERR; + continue; + } + else if (rc < 0) { + tio_error_print("Read sync from serial failed"); + return ERR; + } + } + + /* Always work with 128b packets */ + packet.seq = 1; + packet.type = SOH; + + while (len) { + size_t sz, z = 0; + char *from, status; + + /* Build next packet, pad with 0 to full seq */ + z = min(len, sizeof(packet.data)); + memcpy(packet.data, buf, z); + memset(packet.data + z, 0, sizeof(packet.data) - z); + crc = crc16(packet.data, sizeof(packet.data)); + packet.crc_hi = crc >> 8; + packet.crc_lo = crc; + packet.nseq = 0xff - packet.seq; + + /* Send packet */ + from = (char *) &packet; + sz = sizeof(packet); + while (sz) { + if (key_hit) + return ERR; + if ((rc = write(sio, from, sz)) < 0 ) { + if (errno == EWOULDBLOCK) { + usleep(1000); + continue; + } + tio_error_print("Write packet to serial failed"); + return ERR; + } + from += rc; + sz -= rc; + } + + /* Clear response */ + resp = 0; + + /* Read receiver response, timeout 1 s */ + for(int n=0; n < 20; n++) { + if (key_hit) + return ERR; + rc = read_poll(sio, &resp, 1, 50); + if (rc < 0) { + tio_error_print("Read ack/nak from serial failed"); + return ERR; + } else if(rc > 0) { + break; + } + } + + /* Update "progress bar" */ + switch (resp) { + case NAK: status = 'N'; break; + case ACK: status = '.'; break; + case 'C': status = 'C'; break; + case CAN: status = '!'; return ERR; + default: status = '?'; + } + write(STDOUT_FILENO, &status, 1); + + /* Move to next block after ACK */ + if (resp == ACK) { + packet.seq++; + len -= z; + buf += z; + } + } + + /* Send EOT at 1 Hz until ACK or CAN received */ + while (1) { + if (key_hit) + return ERR; + if (write(sio, EOT_STR, 1) < 0) { + tio_error_print("Write EOT to serial failed"); + return ERR; + } + write(STDOUT_FILENO, "|", 1); + /* 1s timeout */ + rc = read_poll(sio, &resp, 1, 1000); + if (rc < 0) { + tio_error_print("Read from serial failed"); + return ERR; + } else if(rc == 0) { + continue; + } + if (resp == ACK || resp == CAN) { + write(STDOUT_FILENO, "\r\n", 2); + return (resp == ACK) ? OK : ERR; + } + } + 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; + int rc, fd; + struct stat stat; + const uint8_t *buf; + + /* Open file, map into memory */ + fd = open(filename, O_RDONLY); + if (fd < 0) { + tio_error_print("Could not open file"); + return ERR; + } + fstat(fd, &stat); + len = stat.st_size; + buf = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0); + if (!buf) { + close(fd); + tio_error_print("Could not mmap file"); + return ERR; + } + + /* Do transfer */ + key_hit = 0; + if (mode == XMODEM_1K) { + rc = xmodem_1k(sio, buf, len, 1); + } + else if (mode == XMODEM_CRC) { + rc = xmodem(sio, buf, len); + } + else { + /* Ymodem: hdr + file + fin */ + while(1) { + char hdr[1024], *p; + + rc = -1; + if (strlen(filename) > 977) break; /* hdr block overrun */ + 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 */ + if (xmodem_1k(sio, buf, len, 1) < 0) break; /* xmodem file */ + if (xmodem_1k(sio, "", 1, 0) < 0) break; /* empty hdr = fin */ + rc = 0; break; + } + } + key_hit = 0xff; + + /* Flush serial and release resources */ + tcflush(sio, TCIOFLUSH); + munmap((void *)buf, len); + 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 new file mode 100644 index 0000000..1b46cd7 --- /dev/null +++ b/src/xymodem.h @@ -0,0 +1,34 @@ +/* + * 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 + +typedef enum { + XMODEM_1K, + XMODEM_CRC, + YMODEM, +} modem_mode_t; + +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 c9d1760..0000000 --- a/subprojects/libinih.wrap +++ /dev/null @@ -1,4 +0,0 @@ -[wrap-git] -directory=libinih -url=https://github.com/benhoyt/inih.git -revision=r55