Compare commits

...

95 commits
v3.5 ... master

Author SHA1 Message Date
Jakob Haufe
6fb3a64ba2 Fix license in meson.build
- Make license here match LICENSE
- According to meson docs, it should not be an array
2026-01-22 12:41:01 +01:00
aiotter
3af4c5591e Fix redundant output on macOS 2025-08-07 17:18:29 +02:00
John Barbero Unenge
cce94b9d92 Add --complete-profiles to help printout and man pages 2025-06-17 16:30:31 +02:00
ii8
86f48a2fb6 Overhaul Lua API
Lua API moved into a tio library table and names adjusted to Lua stdlib style.
Regex in expect() replaced with Lua patterns so binary data can be handled.
New tio.alwaysecho variable allows enabling and disabling echo to console.
Read and write functions now manage complex retry and timeout logic internally,
giving the user a simple "nil if fail" API like the rest of Lua.
exit() was removed, os.exit() already exists in the Lua standard library.
2025-06-14 15:09:21 +02:00
Martin Lund
381c0b7823 Update codeql to v3 2025-06-14 07:03:17 +02:00
Martin Lund
8f33cff6ea Disable compiler warning on unused result 2025-05-31 19:43:11 +02:00
Keith Barratt
9d00cd3492 Fix device description-Linux
This commit only effects Linux.

The description field of the `device_list`, populated by
`tty_search_for_serial_devices()`, was either incorrect or less than ideal
for CDC ACM virtual com ports. For instance:
    (i) Some devices incorrectly have the description field populated by
    the 'product' property of USB hub they are connected via.
    (ii) Other devices have description fields populated with the interface,
    e.g. CDC, when there is a 'product' property available that would give a
    clearer description.

To solve these issues, we first prioritise searching for the 'product' property
of the device over the 'interface' property. We also look for the
'product' property in an additional directory.
2025-05-30 17:20:06 +02:00
Martin Lund
3e0b2d861d Fix Ubuntu workflow 2025-05-30 17:18:21 +02:00
ii8
a1217af4c6 Fix string truncation bug in scripting api 2025-05-25 21:13:43 +02:00
Martin Lund
58bf5c5008 Update tio man page 2025-05-25 19:46:18 +02:00
Maximilian Seesslen
7e61a34df3 Added timestamp format "epoch-usec"
This timestamp format will print the seconds since epoch along with
subdivision in microseconds.

Example:
 [1748009585.087083] tio v3.9-8-g2fb788f-dirty
 [1748009585.087156] Press ctrl-t q to quit
 [1748009585.087683] Connected to /dev/ttyUSB0
2025-05-23 16:36:39 +02:00
Hideaki Tai
2fb788f817 fix: lua script stops output if it includes null terminate 2025-05-06 17:28:53 +02:00
Martin Lund
f887756a71 meson: Enable compiler warnings on unused result and global shadows 2025-04-29 17:44:08 +02:00
Robert Lipe
5d915134a3 Fix --auto new and --auto latest on MacOS. (redo)
Git is being dumb about
67c071633d This PR is identical to that one and will supercede it.

Fix --auto new and --auto latest on MacOS.

'device_list' was both a global (eww!) and a local inside
tty_search_for_serial_devices(). The local got set and
returned, so it looked sane, but the caller used the global
instead of the return value of the function it had just
called, meaning (global) device_list was NULL while
(ignored, local) device_list held a perfectly lovely
linked list.

Tested:
tio --auto new waits for a new device to appare and connects
tio --latest will connect to the most recently attached device
  which, in most worlds, is the most recently enumerated USB
  device, conveniently skipping all the bluetooth nonsense.
  If the lone USB device is disconnected, it then connects to
  one of those, meaning you really do have to restart tio.
2025-04-29 17:05:24 +02:00
Robert Lipe
7516dff802 Add missing build piece. 2025-04-24 17:55:26 +02:00
Robert Lipe
03ef931fb2 - Implemented getPropertyString(), getDeviceLocation(), tty_search_for_serial_devices()
for MacOS
- Added error handling and memory management
- Improved code readability and consistency
- Updated coding style to match project conventions

- Added robust error checking for CoreFoundation property retrieval
- Implemented more defensive memory allocation and type checking
- Switched to using callout device key for more reliable device discovery
- Added single-line block bracing consistent with project style
- Improved comments and code formatting

- Used `kIOCalloutDeviceKey` instead of `kIODialinDeviceKey` for device path retrieval
- Enhanced type checking for CoreFoundation objects
- Simplified memory management and error handling
- Added additional logging and error reporting

- Verified functionality on MacOS 10.11 and 10.15. Tested with ESP32-P4 and ESP32-BOX

Resolves potential device discovery and memory management issues in the MacOS serial device detection code.
2025-04-24 17:55:26 +02:00
Martin Lund
437881f0ed Update AUTHORS 2025-04-23 08:17:11 +02:00
David Ordnung
c736b1e353 Input ICRCRNL mapping to avoid using INLCRNL with ICRNL 2025-04-23 08:09:22 +02:00
Martin Lund
d682e98a66 codeql: Build using ubuntu-22.04 2025-04-16 10:47:39 +02:00
Martin Lund
bdfe87e1cb Update date 2025-04-13 13:31:06 +02:00
Martin Lund
5c2ced1093 Update NEWS 2025-04-13 13:27:50 +02:00
Martin Lund
f87f470415 Fix pattern matching memory corruption 2025-04-13 13:25:25 +02:00
Martin Lund
2e86718973 Add typos.toml 2025-04-13 08:28:01 +02:00
Martin Lund
013aebcc05 Update NEWS 2025-04-12 09:02:22 +02:00
Martin Lund
b33045189f Update plain text man page 2025-04-12 08:57:25 +02:00
Martin Lund
600c3d7563 Update version date 2025-04-12 08:55:40 +02:00
Martin Lund
ebce2d4ee9 Bump version 2025-04-12 08:55:11 +02:00
Martin Lund
16b7aee42f Update NEWS 2025-04-12 08:54:50 +02:00
Martin Lund
d33e275ca3 Update AUTHORS 2025-03-23 07:04:47 +01:00
Samuel Holland
da4074c9a5 Don't add null characters to the expect buffer
They prevent regexec() from seeing the remainder of the buffer.
2025-03-23 07:02:56 +01:00
Martin Lund
f716d2ccdd Update TODO 2025-03-13 15:44:12 +01:00
V
7567e08227 Disable stdout buffering globally
This makes it possible to pipe output to other programs cleanly.
2025-03-11 20:46:14 +01:00
Martin Lund
6aca9ffee5 Update AUTHORS 2025-03-10 16:17:56 +01:00
Lubov66
f5740dbf31 docs: edited the license date 2025-03-10 16:12:53 +01:00
Lubov66
d163afc6b1 Update README.md 2025-03-10 11:37:32 +01:00
Lubov66
1b60dd1ae7 Update README.md 2025-03-10 11:07:03 +01:00
Martin Lund
795ef28f79 Update TODO 2025-02-25 16:16:24 +01:00
Martin Lund
8e155c9276 Update TODO 2025-02-23 16:26:28 +01:00
Martin Lund
6831ad0eae Fix parsing of timestamp options 2025-02-15 20:27:53 +01:00
Martin Lund
8f7bf2fd2c codeql: Upgrade to upload-artifact@v4 2025-02-08 02:58:02 +01:00
Martin Lund
f389f11669 Update plaintext man page 2025-01-26 16:41:24 +01:00
Martin Lund
37994b3cc5 Add character mapping examples 2025-01-25 15:09:07 +01:00
Jakob Haufe
27f8f2c4e6 Manpage: Fix backslash encoding
Literal backslash needs to be written as \e.
2024-12-01 14:32:56 +01:00
Martin Lund
01e637cdf4 Update NEWS 2024-11-30 12:40:48 +01:00
Martin Lund
1b2a0ea130 Update version date 2024-11-30 12:12:09 +01:00
Martin Lund
b8135ea639 Rename git version to simply version 2024-11-30 11:37:27 +01:00
Martin Lund
c49faa7337 Clean up lua API
Rename modem_send() to send()
Rename send to write()
2024-11-30 11:09:43 +01:00
Martin Lund
4511d74a9e Update AUTHORS 2024-11-07 22:17:26 +01:00
Keith Hill
afd82f7ac4 + 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
2024-11-07 21:45:06 +01:00
Martin Lund
db3f109c7d Zero initialize buffer in read_string() 2024-11-07 18:05:32 +01:00
Martin Lund
ab678e6c88 Use version from git 2024-10-25 19:35:13 +02:00
Martin Lund
330e99381e Fix memory leak in base62_encode() 2024-10-25 18:26:59 +02:00
Martin Lund
7e314b2cc3 Update TODO 2024-10-17 18:53:36 +02:00
Martin Lund
4fb034858a Update AUTHORS 2024-10-15 17:22:34 +02:00
Martin Lund
d494b9d3ac Update README 2024-09-25 20:51:40 +02:00
konosubakonoakua
4034d0ad51 Update readme.md
Update readme.md issue part

Update readme.md issue part
2024-09-25 17:34:06 +02:00
Martin Lund
9fec689117 Fix name declaration conflict with socket send() 2024-09-15 05:57:31 +02:00
Martin Lund
6c4b92270e Add clang-format spec 2024-09-07 09:31:31 +02:00
Martin Lund
a22b270749 Bump version 2024-08-31 09:23:19 +02:00
Martin Lund
9f27ce5899 Update version date 2024-08-31 09:09:34 +02:00
Martin Lund
27f9b9f2e8 Update NEWS 2024-08-31 09:06:28 +02:00
Martin Lund
2e7da862c8 Cleanup 2024-08-24 13:21:41 +02:00
Martin Lund
bb2b4e30b2 Update AUTHORS 2024-08-24 12:37:12 +02:00
Steve Marple
f47467271f Add "epoch" timestamp option
Add an option that prints the timestamp as the number of seconds since
the Unix epoch.
2024-08-24 12:35:30 +02:00
Martin Lund
cdc773100c Update AUTHORS 2024-08-19 20:40:05 +02:00
Tomka Gergely
a3b67d3eb6 The log-directory options is not read from the configuration file. 2024-08-19 20:37:18 +02:00
Martin Lund
ef12ed62df 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.
2024-08-06 20:48:43 +02:00
Martin Lund
2f6b3796f2 Bump version 2024-07-25 00:12:23 +02:00
Martin Lund
6163bc392b Fix socket send call on platforms without MSG_NOSIGNAL
To fix build issue encountered on MacOS Catalina but may apply to other
platforms.
2024-07-20 08:25:49 +02:00
Martin Lund
475bc29cc8 Update plain text man page 2024-07-19 09:39:41 +02:00
Martin Lund
c4f5269c83 Update version date 2024-07-19 09:39:13 +02:00
Martin Lund
13ad59ac12 Update README 2024-07-19 09:37:57 +02:00
Martin Lund
2259244eb2 Update NEWS 2024-07-19 09:27:23 +02:00
Martin Lund
725423c50c 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.
2024-07-19 08:49:49 +02:00
Martin Lund
14963032c3 Update TODO 2024-07-15 20:05:28 +02:00
Martin Lund
e1fe232254 Fix shadow variable 2024-07-13 18:36:03 +02:00
Martin Lund
9cafcbcab5 Update README 2024-07-13 17:17:14 +02:00
Martin Lund
f4076258f1 Update man page 2024-07-13 17:09:26 +02:00
Martin Lund
866b5bcb30 Mention how to list key commands in help output 2024-07-13 16:49:19 +02:00
Martin Lund
289bbfd393 Update AUTHORS 2024-07-13 15:14:24 +02:00
Heinrich Schuchardt
68a64ac554 Print correct 'Done' timestamp for X- and Y-modem transfers
Closes: #268

Call tio_printf() after completing xymodem_send().

Signed-off-by: Heinrich Schuchardt <heinrich.schuchardt@canonical.com>
2024-07-13 15:10:39 +02:00
Martin Lund
0a95da00f1 Typo fixes 2024-07-11 17:00:37 +02:00
Martin Lund
32d97683f8 Update TODO 2024-07-10 13:27:12 +02:00
Martin Lund
c3654486c7 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.
2024-07-10 13:11:28 +02:00
Martin Lund
b5184012c4 Update AUTHORS 2024-07-10 01:48:13 +02:00
Robert Lipe
fc0c6f61d2 Recompute listing_device_name_length_max for MacOS case, too. 2024-07-10 01:46:32 +02:00
Martin Lund
53bb2a6ad1 Fix uptime on MacOS
On MacOS the birth time is apparently not available so we use
modification time instead.
2024-07-09 22:12:42 +02:00
Martin Lund
da9f7a6540 Improve warning upon failing connect
Add device path to warning when connect fails.
2024-07-09 21:06:22 +02:00
Martin Lund
2a1dae6336 Fix crashy search_reset() on macOS 2024-07-09 16:44:10 +02:00
Martin Lund
ada2db0739 Clean up shadow variable 2024-07-02 19:20:59 +02:00
Martin Lund
14ee38a0d9 Clean up readline code 2024-07-02 19:13:14 +02:00
Martin Lund
2c700a90b0 Code cleanup 2024-07-02 19:11:40 +02:00
Martin Lund
da04c2c444 Improve listing of long device names 2024-07-02 17:41:49 +02:00
Martin Lund
5f70b75e90 Fix listing of serial devices on macOS 2024-07-01 23:11:58 +02:00
Martin Lund
02cac07a77 Bump version 2024-06-29 12:33:55 +02:00
38 changed files with 1384 additions and 582 deletions

5
.clang-format Normal file
View file

@ -0,0 +1,5 @@
BasedOnStyle: llvm
IndentWidth: 4
AllowShortFunctionsOnASingleLine: None
KeepEmptyLinesAtTheStartOfBlocks: false
BreakBeforeBraces: Allman

View file

@ -27,7 +27,7 @@ jobs:
# - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners # - https://gh.io/using-larger-runners
# Consider using larger runners for possible analysis time improvements. # Consider using larger runners for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-20.04' }} runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-22.04' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions: permissions:
actions: read actions: read
@ -51,7 +51,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -66,7 +66,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # 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) # If this step fails, then you should remove it and run the build manually (see below)
#- name: Autobuild #- name: Autobuild
# uses: github/codeql-action/autobuild@v2 # uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # 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 # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -78,7 +78,7 @@ jobs:
./.github/workflows/codeql-buildscript.sh ./.github/workflows/codeql-buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"
upload: false upload: false
@ -107,14 +107,14 @@ jobs:
output: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif output: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif
- name: Upload CodeQL results to code scanning - name: Upload CodeQL results to code scanning
uses: github/codeql-action/upload-sarif@v2 uses: github/codeql-action/upload-sarif@v3
with: with:
sarif_file: ${{ steps.step1.outputs.sarif-output }} sarif_file: ${{ steps.step1.outputs.sarif-output }}
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"
- name: Upload CodeQL results as an artifact - name: Upload CodeQL results as an artifact
if: success() || failure() if: success() || failure()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: codeql-results name: codeql-results
path: ${{ steps.step1.outputs.sarif-output }} path: ${{ steps.step1.outputs.sarif-output }}

View file

@ -21,7 +21,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get install -y bash-completion git meson liblua5.2-dev libglib2.0-dev sudo apt update
sudo apt install -y bash-completion git meson liblua5.2-dev libglib2.0-dev
- name: Build - name: Build
run: | run: |

2
.typos.toml Normal file
View file

@ -0,0 +1,2 @@
[default]
extend-ignore-words-re = ["tio"]

11
AUTHORS
View file

@ -55,5 +55,16 @@ Brian <bayuan@purdue.edu>
Davis C <davisclaib@gmail.com> Davis C <davisclaib@gmail.com>
KhazAkar <damianzrb@zohomail.eu> KhazAkar <damianzrb@zohomail.eu>
Eliot Alan Foss <eliotfoss@gmail.com> Eliot Alan Foss <eliotfoss@gmail.com>
Robert Lipe <robertlipe@gpsbabel.org>
Heinrich Schuchardt <heinrich.schuchardt@canonical.com>
Tomka Gergely <tomkatudor@gmail.com>
Steve Marple <stevemarple@googlemail.com>
konosubakonoakua <ailike_meow@qq.com>
Keith Hill <k_hill@unitronlp.com>
Lubov66 <radolevanja@gmail.com>
V <v@anomalous.eu>
Samuel Holland <samuel@sholland.org>
David Ordnung <david.ordnung@googlemail.com>
Thanks to everyone who has contributed to this project. Thanks to everyone who has contributed to this project.

View file

@ -1,4 +1,4 @@
Copyright (c) 2014-2022 Martin Lund <martin.lund@keep-it-simple.com> Copyright (c) 2014-2025 Martin Lund <martin.lund@keep-it-simple.com>
This program is free software; you can redistribute it and/or This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License modify it under the terms of the GNU General Public License

192
NEWS
View file

@ -1,5 +1,151 @@
=== tio v3.9 (2025-04-13) ===
=== tio v3.5 ===
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.
@ -109,7 +255,7 @@ Changes since tio v3.1:
* Fix line ending in --list output * Fix line ending in --list output
* Print location of configuratin file in --list output * Print location of configuration file in --list output
* Fix alignment of profile listing * Fix alignment of profile listing
@ -148,7 +294,7 @@ Changes since tio v3.0:
After including the use of glib we might as well replace inih After including the use of glib we might as well replace inih
with the glib key file parser. with the glib key file parser.
All configuraiton file parsing has been reworked and also the options All configuration file parsing has been reworked and also the options
parsing has been cleaned up, resulting in better and stricter parsing has been cleaned up, resulting in better and stricter
configuration file and option value checks. configuration file and option value checks.
@ -202,9 +348,9 @@ Rui Chen:
* fix: add build patch for `FNM_EXTMATCH` * fix: add build patch for `FNM_EXTMATCH`
* feat: add macos workflow * feat: add macOS workflow
* fix: add macos build patch for `fs_get_creation_time` * fix: add macOS build patch for `fs_get_creation_time`
@ -429,11 +575,11 @@ Changes since tio v2.7:
* Add lua modem_send(file,protocol) * Add lua modem_send(file,protocol)
* Fix xymodem error print outs * Fix xymodem error messages
* Rework x/y-modem transfer command * Rework x/y-modem transfer command
Remove ctrl-t X optin and instead introduce submenu to ctrl-t x option Remove ctrl-t X option and instead introduce submenu to ctrl-t x option
for picking which xmodem protocol to use. for picking which xmodem protocol to use.
* Update README * Update README
@ -442,22 +588,22 @@ Changes since tio v2.7:
* Add independent input and output mode * Add independent input and output mode
Replaces -x, --hexadecimal option with --intput-mode and --output-mode Replaces -x, --hexadecimal option with --input-mode and --output-mode
so it is possible to select hex or normal mode for both input and output so it is possible to select hex or normal mode for both input and output
independently. independently.
To obtain same behaviour as -x, --hexadecimal use the following To obtain same behavior as -x, --hexadecimal use the following
configuration: configuration:
input-mode = hex input-mode = hex
output-mode = hex output-mode = hex
* Fix file descriptor handling on MacOS * Fix file descriptor handling on macOS
* Add tty line configuration script API * Add tty line configuration script API
On some platforms calling high()/low() to switch line states result in On some platforms calling high()/low() to switch line states result in
costly system calls whick makes it impossible to swith two or more tty costly system calls which makes it impossible to switch two or more tty
lines simultaneously. lines simultaneously.
To help solve this timing issue we introduce a tty line state To help solve this timing issue we introduce a tty line state
@ -493,7 +639,7 @@ Changes since tio v2.7:
lines. Script is activated automatically on connect or manually via in lines. Script is activated automatically on connect or manually via in
session key command. session key command.
The Lua scripting feature opens up for many posibilities in the future The Lua scripting feature opens up for many possibilities in the future
such as adding expect like functionality to easily and programatically such as adding expect like functionality to easily and programatically
interact with the connected device. interact with the connected device.
@ -590,7 +736,7 @@ Changes since tio v2.5:
Add --log-append option which makes tio append to any existing log file. Add --log-append option which makes tio append to any existing log file.
This also changes the default behaviour of tio from appending to This also changes the default behavior of tio from appending to
overwriting any existing log file. Now you have to use this new option overwriting any existing log file. Now you have to use this new option
to make tio append. to make tio append.
@ -924,7 +1070,7 @@ Changes since tio v1.46:
Victor Oliveira Victor Oliveira
* add macports install instructions * add MacPorts install instructions
@ -1424,7 +1570,7 @@ Changes since tio v1.35:
* Handle SIGHUP * Handle SIGHUP
Handle SIGHUP so that the registered exit handlers are called to restore Handle SIGHUP so that the registered exit handlers are called to restore
the terminal back to its orignal state. the terminal back to its original state.
* Add color configuration support * Add color configuration support
@ -1480,10 +1626,10 @@ Changes since tio v1.34:
* Add support for configurable timestamp format * Add support for configurable timestamp format
Also changes default timestamp format from ISO8601 to classic 24-hour Also changes default timestamp format from ISO 8601 to classic 24-hour
format as this is assumed to be the format that most users would prefer. format as this is assumed to be the format that most users would prefer.
And reintroduces strict but optional ISO8601 format. And reintroduces strict but optional ISO 8601 format.
This feature allows to easily add more timestamp formats in the future. This feature allows to easily add more timestamp formats in the future.
@ -1604,10 +1750,10 @@ Sylvain LAFRASSE:
attila-v: attila-v:
* Refine timestamps with milliseconds and ISO-8601 format (#129). * Refine timestamps with milliseconds and ISO 8601 format (#129).
* Show milliseconds too in the timestamp (#114) and log file (#124) * Show milliseconds too in the timestamp (#114) and log file (#124)
* Change timestamp format to ISO-8601. * Change timestamp format to ISO 8601.
Yin Fengwei: Yin Fengwei:
@ -1686,7 +1832,7 @@ Lars Kellogg-Stedman:
George Stark: George Stark:
* dont show line state if ioctl failed * don't show line state if ioctl failed
* add serial lines manual control * add serial lines manual control
@ -1699,9 +1845,9 @@ arichi:
Mariusz Midor: Mariusz Midor:
* Newline: handle booth NL and CR * Newline: handle both NL and CR
Flag ONLCRNL expects code \n after press Enter, but on some systems \r is send instead. Flag ONLCRNL expects code \n after press Enter, but on some systems \r is sent instead.
@ -2193,7 +2339,7 @@ Changes since tio v1.11:
To display the total number of bytes transmitted/received simply perform the To display the total number of bytes transmitted/received simply perform the
'ctrl-t s' command sequence. 'ctrl-t s' command sequence.
This feature can be useful when eg. trying to detect non-printable This feature can be useful when e.g. trying to detect non-printable
characters. characters.
* Further simplification of key handling * Further simplification of key handling

133
README.md
View file

@ -3,8 +3,9 @@
# tio - a serial device I/O tool # tio - a serial device I/O tool
[![](https://img.shields.io/github/actions/workflow/status/tio/tio/ubuntu.yml?label=GNU%2FLinux)](https://github.com/tio/tio/actions/workflows/ubuntu.yml) [![](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://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/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/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) [![](https://img.shields.io/repology/repositories/tio)](https://repology.org/project/tio/versions)
@ -74,6 +75,7 @@ when used in combination with [tmux](https://tmux.github.io).
* Configuration file support * Configuration file support
* Support for configuration profiles * Support for configuration profiles
* Activate configuration profiles by name or pattern * 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 of shell command to serial device
* Redirect I/O to UNIX socket or IPv4/v6 network socket * Redirect I/O to UNIX socket or IPv4/v6 network socket
* Useful for scripting or TTY sharing * Useful for scripting or TTY sharing
@ -100,7 +102,7 @@ For more usage details please see the man page documentation
### 3.1 Command-line ### 3.1 Command-line
The command-line interface is straightforward as reflected in the output from The command-line interface is straightforward as reflected in the output from
'tio --help': ```tio --help```:
``` ```
Usage: tio [<options>] <tty-device|profile|tid> Usage: tio [<options>] <tty-device|profile|tid>
@ -126,7 +128,7 @@ Options:
-t, --timestamp Enable line timestamp -t, --timestamp Enable line timestamp
--timestamp-format <format> Set timestamp format (default: 24hour) --timestamp-format <format> Set timestamp format (default: 24hour)
--timestamp-timeout <ms> Set timestamp timeout (default: 200) --timestamp-timeout <ms> Set timestamp timeout (default: 200)
-l, --list List available serial devices -l, --list List available serial devices, TIDs, and profiles
-L, --log Enable log to file -L, --log Enable log to file
--log-file <filename> Set log filename --log-file <filename> Set log filename
--log-directory <path> Set log directory path for automatic named logs --log-directory <path> Set log directory path for automatic named logs
@ -142,11 +144,14 @@ Options:
--script <string> Run script from string --script <string> Run script from string
--script-file <filename> Run script from file --script-file <filename> Run script from file
--script-run once|always|never Run script on connect (default: always) --script-run once|always|never Run script on connect (default: always)
--exec <command> Execute shell command with I/O redirected to device
-v, --version Display version -v, --version Display version
-h, --help Display help -h, --help Display help
Options and profiles may be set via configuration file. Options and profiles may be set via configuration file.
In session you can press ctrl-t ? to list available key commands.
See the man page for more details. See the man page for more details.
``` ```
@ -228,19 +233,24 @@ Connect automatically to latest registered serial device:
$ tio --auto-connect latest $ tio --auto-connect latest
``` ```
It is also possible to use exclude options to affect which serial devices are It is possible to use exclude options to affect which serial devices are
involved in the automatic connection strategy: involved in the automatic connection strategy:
``` ```
$ tio --auto-connect new --exclude-devices "/dev/ttyACM?,/dev/ttyUSB2" $ tio --auto-connect new --exclude-devices "/dev/ttyACM?,/dev/ttyUSB2"
``` ```
Exclude drivers by pattern: And to exclude drivers by pattern:
``` ```
$ tio --auto-connect new --exclude-drivers "cdc_acm,ftdi_sio" $ tio --auto-connect new --exclude-drivers "cdc_acm,ftdi_sio"
``` ```
Note: Pattern matching supports '*' and '?'. Use comma separation to define Note: Pattern matching supports '*' and '?'. Use comma separation to define
multiple patterns. 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: Log to file with autogenerated filename:
``` ```
$ tio --log /dev/ttyUSB0 $ tio --log /dev/ttyUSB0
@ -266,6 +276,11 @@ Redirect I/O to IPv4 network socket on port 4242:
$ tio --socket inet:4242 /dev/ttyUSB0 $ 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: Pipe data to the serial device:
``` ```
$ cat data.bin | tio /dev/ttyUSB0 $ cat data.bin | tio /dev/ttyUSB0
@ -273,12 +288,12 @@ $ cat data.bin | tio /dev/ttyUSB0
Manipulate modem lines on connect: Manipulate modem lines on connect:
``` ```
$ tio --script "set{DTR=high,RTS=low}; msleep(100); set{DTR=toggle,RTS=toggle}" /dev/ttyUSB0 $ tio --script "tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{DTR=toggle,RTS=toggle}" /dev/ttyUSB0
``` ```
Pipe command to serial device and wait for line response within 1 second: Pipe command to serial device and wait for line response within 1 second:
``` ```
$ echo "*IDN?" | tio /dev/ttyACM0 --script "expect('\r\n', 1000)" --mute $ echo "*IDN?" | tio /dev/ttyACM0 --script "tio.expect('\r\n', 1000)" --mute
KORAD KD3305P V4.2 SN:32475045 KORAD KD3305P V4.2 SN:32475045
``` ```
@ -350,12 +365,12 @@ color = 11
[svf2] [svf2]
device = /dev/ttyUSB0 device = /dev/ttyUSB0
baudrate = 9600 baudrate = 9600
script = expect("login: "); send("root\n"); expect("Password: "); send("root\n") script = tio.expect("login: "); tio.write("root\n"); tio.expect("Password: "); tio.write("root\n")
color = 12 color = 12
[esp32] [esp32]
device = /dev/serial/by-id/usb-0403_6014-if00-port0 device = /dev/serial/by-id/usb-0403_6014-if00-port0
script = set{DTR=high,RTS=low}; msleep(100); set{DTR=low,RTS=high}; msleep(100); set{RTS=low} script = tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{DTR=low,RTS=high}; tio.msleep(100); tio.set{RTS=low}
script-run = once script-run = once
color = 13 color = 13
@ -380,62 +395,80 @@ Another more elaborate configuration file example is available [here](examples/c
Tio suppots Lua scripting to easily automate interaction with the tty device. Tio suppots Lua scripting to easily automate interaction with the tty device.
In addition to the Lua API tio makes the following functions available: In addition to the standard Lua API tio makes the following functions
and variables available:
```
expect(string, timeout)
Expect string - waits for string to match or timeout before continueing.
Supports regular expressions. Special characters must be escaped with '\\'.
Timeout is in milliseconds, defaults to 0 meaning it will wait forever.
Returns 1 on successful match, 0 on timeout, or -1 on error. #### `tio.expect(pattern, timeout)`
On successful match it also returns the match string as second return value. Waits for the Lua pattern to match or timeout before continuing.
Timeout is in milliseconds, defaults to 0 meaning it will wait forever.
send(string) Returns the captures from the pattern or `nil` on timeout.
Send string.
Returns number of bytes written on success or -1 on error. #### `tio.read(size, timeout)`
modem_send(file, protocol) Read up to `size` bytes from serial device. If timeout is 0 or not provided it
Send file using x/y-modem protocol. will wait forever until data is ready to read.
Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. Returns a string up to `size` bytes long on success and `nil` on timeout.
tty_search() #### `tio.readline(timeout)`
Search for serial devices.
Returns a table of number indexed tables, one for each serial device Read line from serial device. If timeout is 0 or not provided it will wait
found. Each of these tables contains the serial device information accessible forever until data is ready to read.
via the following string indexed elements "path", "tid", "uptime", "driver",
"description".
Returns nil if no serial devices are found. Returns a string on success and `nil` on timeout. On timeout a partially read
line may be returned as a second return value.
read(size, timeout) #### `tio.write(string)`
Read from serial device. If timeout is 0 or not provided it will wait
forever until data is ready to read.
Returns number of bytes read on success, 0 on timeout, or -1 on error. Write string to serial device.
On success, returns read string as second return value. Returns the `tio` table.
set{line=state, ...} #### `tio.send(file, protocol)`
Set state of one or multiple tty modem lines.
Line can be any of DTR, RTS, CTS, DSR, CD, RI Send file using x/y-modem protocol.
State is high, low, or toggle. Protocol can be any of `XMODEM_1K`, `XMODEM_CRC`, `YMODEM`.
sleep(seconds) #### `tio.ttysearch()`
Sleep for seconds.
msleep(ms) Search for serial devices.
Sleep for miliseconds.
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`.
exit(code)
Exit with exit code.
```
## 4. Installation ## 4. Installation
@ -496,9 +529,13 @@ Note: The meson install steps may differ depending on your specific system.
Getting permission access errors trying to open your serial device? Getting permission access errors trying to open your serial device?
Add your user to the group which allows serial device access. For example, to add your user to the 'dialout' group do: Add your user to the group which allows serial device access permanently. For example, to add your user to the 'dialout' group do:
```bash
sudo usermod -a -G dialout <username>
``` ```
$ sudo usermod -a -G dialout <username> Switch to the "dialout" group, temporary but immediately for this session.
```bash
newgrp dialout
``` ```

22
TODO
View file

@ -1,3 +1,19 @@
* Add release support for arm and x86 binary tarballs
* Support input and input mapping from lua scripts
* Add option to send file raw (no modem protocol)
* Add loopback option
Send received serial input back to output (for testing etc.)
* Add loopback support between two serial ports
Useful for traffic monitoring
* Add mapping feature for printing non-printable characters
* Porting layer to support native win32 builds. * Porting layer to support native win32 builds.
Some of the work that needs to be done: Some of the work that needs to be done:
@ -34,12 +50,6 @@
Already supported in hex output mode. Already supported in hex output mode.
* Advanced line mode
Current line mode only support backspace editing. Would be nice with arrow
key navigation left/right and insert/overwrite support. Also history browsing
pressing up/down.
* Allow tio to connect to socket * Allow tio to connect to socket
After some more consideration I think it makes sense to support connecting to a After some more consideration I think it makes sense to support connecting to a

View file

@ -69,5 +69,10 @@ color = 13
[esp32] [esp32]
device = /dev/ttyUSB0 device = /dev/ttyUSB0
color = 14 color = 14
script = set{DTR=high,RTS=low}; msleep(100); set{DTR=low,RTS=high}; msleep(100); set{RTS=low} script = tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{DTR=low,RTS=high}; tio.msleep(100); tio.set{RTS=low}
script-run = always script-run = always
[buspirate]
device = /dev/ttyACM0
map = INLCRNL,ODELBS
color = 15

View file

@ -13,14 +13,13 @@ local logins = {
}, },
} }
local found, match_str = expect("\\w+- login:", 10) local hostname = tio.expect("^(%g+) login:", 10)
if (1 == found) then if hostname then
local hostname = string.match(match_str, "^%w+")
local login = logins[hostname] local login = logins[hostname]
if (nil ~= login) then if (nil ~= login) then
send(login.username .. "\n") tio.write(login.username .. "\n")
expect("Password:") tio.expect("Password:")
send(login.password .. "\n") tio.write(login.password .. "\n")
else else
io.write("\r\nDon't know login info for " .. hostname .. "\r\n") io.write("\r\nDon't know login info for " .. hostname .. "\r\n")
end end

View file

@ -1,5 +1,5 @@
set{DTR=high, RTS=low} tio.set{DTR=high, RTS=low}
msleep(100) tio.msleep(100)
set{DTR=low, RTS=high} tio.set{DTR=low, RTS=high}
msleep(100) tio.msleep(100)
set{RTS=toggle} tio.set{RTS=toggle}

14
examples/lua/read.lua Normal file
View file

@ -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

View file

@ -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

View file

@ -1,6 +1,6 @@
io.write("Searching... ") io.write("Searching... ")
local device = tty_search() local device = tio.ttysearch()
io.write("done\r\n") io.write("done\r\n")

View file

@ -148,6 +148,10 @@ Set timestamp format to any of the following timestamp formats:
24-hour format relative to previous timestamp 24-hour format relative to previous timestamp
.IP "\fBiso8601" .IP "\fBiso8601"
ISO8601 format ("YYYY-MM-DDThh:mm:ss.sss") ISO8601 format ("YYYY-MM-DDThh:mm:ss.sss")
.IP "\fBepoch"
Seconds since Unix epoch (1970-01-01)
.IP "\fBepoch-usec"
Seconds since Unix epoch (1970-01-01) with subdivision in microseconds
.PP .PP
Default format is \fB24hour\fR Default format is \fB24hour\fR
.RE .RE
@ -216,6 +220,8 @@ Map FF to ESC-c on input
Map NL to CR on input Map NL to CR on input
.IP "\fBINLCRNL" .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" .IP "\fBIMSB2LSB"
Map MSB bit order to LSB on input Map MSB bit order to LSB on input
.IP "\fBOCRNL" .IP "\fBOCRNL"
@ -365,6 +371,10 @@ Default value is "always".
Execute shell command with I/O redirected to device Execute shell command with I/O redirected to device
.TP
.BR "\-\-complete-profiles
Prints profiles (for shell completion)
.TP .TP
.BR \-v ", " \-\-version .BR \-v ", " \-\-version
@ -373,10 +383,10 @@ Display program version.
.BR \-h ", " \-\-help .BR \-h ", " \-\-help
Display help. Display help.
.SH "KEYS" .SH "KEY COMMANDS"
.PP .PP
.TP 16n .TP 16n
In session, all key strokes are forwarded to the serial device except the following key sequence: a prefix key (default: ctrl-t) followed by a command key. These sequences are intercepted as tio commands: In session, all key strokes are forwarded to the serial device except the following key sequence: a prefix key (default: ctrl-t) followed by a command key. These sequences are intercepted as key commands:
.IP "\fBctrl-t ?" .IP "\fBctrl-t ?"
List available key commands List available key commands
.IP "\fBctrl-t b" .IP "\fBctrl-t b"
@ -426,30 +436,41 @@ Send ctrl-t character
.PP .PP
Tio suppots Lua scripting to easily automate interaction with the tty device. Tio suppots Lua scripting to easily automate interaction with the tty device.
In addition to the Lua API tio makes the following functions available: In addition to the standard Lua API tio makes the following functions
and variables available:
.TP 6n .TP 6n
.IP "\fBexpect(string, timeout)" .IP "\fBtio.expect(pattern, timeout)"
Expect string - waits for string to match or timeout before continuing. Waits for the Lua pattern 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. Timeout is in milliseconds, defaults to 0 meaning it will wait forever.
Returns 1 on successful match, 0 on timeout, or -1 on error. Returns the captures from the pattern or nil on timeout.
On successful match it also returns the match string as second return value. .IP "\fBtio.read(size, timeout)"
Read up to size bytes from serial device. If timeout is 0 or not provided it
will wait forever until data is ready to read.
.IP "\fBsend(string)" Returns a string up to size bytes long on success and nil on timeout.
Send string.
Returns number of bytes written on success or -1 on error. .IP "\fBtio.readline(timeout)"
Read line from serial device. If timeout is 0 or not provided it will wait
forever until data is ready to read.
.IP "\fBmodem_send(file, protocol)" Returns a string on success and nil on timeout. On timeout a partially read
line may be returned as a second return value.
.IP "\fBtio.write(string)"
Write string to serial device.
Returns the tio table.
.IP "\fBtio.send(file, protocol)"
Send file using x/y-modem protocol. Send file using x/y-modem protocol.
Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM.
.IP "\fBtty_search()" .IP "\fBtio.ttysearch()"
Search for serial devices. Search for serial devices.
Returns a table of number indexed tables, one for each serial device found. Returns a table of number indexed tables, one for each serial device found.
@ -459,27 +480,26 @@ following string indexed elements "path", "tid", "uptime", "driver",
Returns nil if no serial devices are found. Returns nil if no serial devices are found.
.IP "\fBread(size, timeout)" .IP "\fBtio.set{line=state, ...}"
Read from serial device. If timeout is 0 or not provided it will wait forever
until data is ready to read.
Returns number of bytes read on success, 0 on timeout, or -1 on error.
On success, returns read string as second return value.
.IP "\fBset{line=state, ...}"
Set state of one or multiple tty modem lines. Set state of one or multiple tty modem lines.
Line can be any of DTR, RTS, CTS, DSR, CD, RI Line can be any of DTR, RTS, CTS, DSR, CD, RI
State is high, low, or toggle. State is high, low, or toggle.
.IP "\fBsleep(seconds)" .IP "\fBtio.sleep(seconds)"
Sleep for seconds. Sleep for seconds.
.IP "\fBmsleep(ms)" .IP "\fBtio.msleep(ms)"
Sleep for milliseconds. Sleep for milliseconds.
.IP "\fBexit(code)"
Exit with exit code. .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" .SH "CONFIGURATION FILE"
.PP .PP
@ -581,6 +601,9 @@ Run script on connect
.IP "\fBexec" .IP "\fBexec"
Execute shell command with I/O redirected to device 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 <file>]".
.SH "CONFIGURATION FILE EXAMPLES" .SH "CONFIGURATION FILE EXAMPLES"
@ -705,7 +728,7 @@ expect -i $uart "prompt> "
.TP .TP
It is also possible to use tio's own simpler expect/send script functionality to e.g. automate logins: It is also possible to use tio's own simpler expect/send script functionality to e.g. automate logins:
$ tio --script 'expect("login: "); send("root\\n"); expect("Password: "); send("root\\n")' /dev/ttyUSB0 $ tio --script 'tio.expect("login: "); tio.write("root\\n"); tio.expect("Password: "); tio.write("root\\n")' /dev/ttyUSB0
.TP .TP
Redirect device I/O to network file socket for remote TTY sharing: Redirect device I/O to network file socket for remote TTY sharing:
@ -726,7 +749,7 @@ $ echo "ls -la" | tio /dev/serial/by\-id/usb\-FTDI_TTL232R-3V3_FTGQVXBL\-if00\-p
.TP .TP
Pipe command to serial device and wait for line response within 1 second: Pipe command to serial device and wait for line response within 1 second:
$ echo "*IDN?" | tio /dev/ttyACM0 --script "expect('\\r\\n', 1000)" --mute $ echo "*IDN?" | tio /dev/ttyACM0 --script "tio.expect('\\r\\n', 1000)" --mute
.TP .TP
.TP .TP
@ -734,6 +757,11 @@ Likewise, to pipe data from file to the serial device:
$ cat data.bin | tio /dev/serial/by\-id/usb\-FTDI_TTL232R-3V3_FTGQVXBL\-if00\-port0 $ 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 .TP
Enable RS-485 mode: Enable RS-485 mode:
@ -742,7 +770,7 @@ $ tio --rs-485 --rs-485-config=RTS_ON_SEND=1,RX_DURING_TX /dev/ttyUSB0
.TP .TP
Manipulate DTR and RTS lines upon first connect to reset connected microcontroller: Manipulate DTR and RTS lines upon first connect to reset connected microcontroller:
$ tio --script "set{DTR=high,RTS=low}; msleep(100); set{RTS=toggle}" --script-run once /dev/ttyUSB0 $ tio --script "tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{RTS=toggle}" --script-run once /dev/ttyUSB0
.SH "WEBSITE" .SH "WEBSITE"
.PP .PP

View file

@ -1,4 +1,4 @@
tio(1) User Commands tio(1) tio(1) User Commands tio(1)
NAME NAME
tio - a serial device I/O tool tio - a serial device I/O tool
@ -114,6 +114,10 @@ OPTIONS
iso8601 ISO8601 format ("YYYY-MM-DDThh:mm:ss.sss") 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 Default format is 24hour
--timestamp-timeout <ms> --timestamp-timeout <ms>
@ -166,6 +170,8 @@ OPTIONS
INLCRNL Map NL to CR-NL 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 IMSB2LSB Map MSB bit order to LSB on input
OCRNL Map CR to NL on output OCRNL Map CR to NL on output
@ -216,7 +222,8 @@ OPTIONS
Redirect I/O to 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 multiplexed to the terminal and all connected clients. Any input from clients connected to the socket is sent on the serial port as if entered at the terminal where tio is running (except that ctrl-t sequences are not recognized), and any input from the serial port is multi
plexed to the terminal and all connected clients.
Sockets remain open while the serial port is disconnected, and writes will block. Sockets remain open while the serial port is disconnected, and writes will block.
@ -282,6 +289,10 @@ OPTIONS
Execute shell command with I/O redirected to device Execute shell command with I/O redirected to device
--complete-profiles
Prints profiles (for shell completion)
-v, --version -v, --version
Display program version. Display program version.
@ -290,8 +301,8 @@ OPTIONS
Display help. Display help.
KEYS 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 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:
ctrl-t ? List available key commands ctrl-t ? List available key commands
@ -340,7 +351,7 @@ KEYS
SCRIPT API SCRIPT API
Tio suppots Lua scripting to easily automate interaction with the tty device. Tio suppots Lua scripting to easily automate interaction with the tty device.
In addition to the Lua API tio makes the following functions available: In addition to the standard Lua API tio makes the following functions available:
expect(string, timeout) expect(string, 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. 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.
@ -349,12 +360,26 @@ SCRIPT API
On successful match it also returns the match string as second return value. On successful match it also returns the match string as second return value.
send(string) read(size, timeout)
Send string. 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. Returns number of bytes written on success or -1 on error.
modem_send(file, protocol) send(file, protocol)
Send file using x/y-modem protocol. Send file using x/y-modem protocol.
Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM.
@ -362,17 +387,11 @@ SCRIPT API
tty_search() tty_search()
Search for serial devices. Search for serial devices.
Returns a table of number indexed tables, one for each serial device found. Each of these tables contains the serial device information accessible via the following string indexed elements "path", "tid", "uptime", "driver", "description". Returns a table of number indexed tables, one for each serial device found. Each of these tables contains the serial device information accessible via the following string indexed elements "path", "tid", "uptime", "dri
ver", "description".
Returns nil if no serial devices are found. Returns nil if no serial devices are found.
read(size, timeout)
Read from serial device. If timeout is 0 or not provided it will wait forever until data is ready to read.
Returns number of bytes read on success, 0 on timeout, or -1 on error.
On success, returns read string as second return value.
set{line=state, ...} set{line=state, ...}
Set state of one or multiple tty modem lines. Set state of one or multiple tty modem lines.
@ -476,6 +495,8 @@ CONFIGURATION FILE
exec Execute shell command with I/O redirected to device 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 <file>]".
CONFIGURATION FILE EXAMPLES CONFIGURATION FILE EXAMPLES
To change the default configuration simply set options like so: To change the default configuration simply set options like so:
@ -561,7 +582,7 @@ EXAMPLES
It is also possible to use tio's own simpler expect/send script functionality to e.g. automate logins: It is also possible to use tio's own simpler expect/send script functionality to e.g. automate logins:
$ tio --script 'expect("login: "); send("root\n"); expect("Password: "); send("root\n")' /dev/ttyUSB0 $ tio --script 'expect("login: "); write("root\n"); expect("Password: "); write("root\n")' /dev/ttyUSB0
Redirect device I/O to network file socket for remote TTY sharing: Redirect device I/O to network file socket for remote TTY sharing:
@ -583,6 +604,10 @@ EXAMPLES
$ cat data.bin | tio /dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTGQVXBL-if00-port0 $ 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: Enable RS-485 mode:
$ tio --rs-485 --rs-485-config=RTS_ON_SEND=1,RX_DURING_TX /dev/ttyUSB0 $ tio --rs-485 --rs-485-config=RTS_ON_SEND=1,RX_DURING_TX /dev/ttyUSB0
@ -597,4 +622,4 @@ WEBSITE
AUTHOR AUTHOR
Maintained by Martin Lund <martin.lund@keep-it-simple.com>. Maintained by Martin Lund <martin.lund@keep-it-simple.com>.
tio 3.5 2024-06-15 tio(1) tio 3.9 2025-04-13 tio(1)

View file

@ -1,12 +1,12 @@
project('tio', 'c', project('tio', 'c',
version : '3.5', version : '3.9',
license : [ 'GPL-2'], license : 'GPL-2.0-or-later',
meson_version : '>= 0.53.2', meson_version : '>= 0.53.2',
default_options : [ 'warning_level=2', 'buildtype=release', 'c_std=gnu99' ] default_options : [ 'warning_level=2', 'buildtype=release', 'c_std=gnu99' ]
) )
# The tag date of the project_version(), update when the version bumps. # The tag date of the project_version(), update when the version bumps.
version_date = '2024-06-29' version_date = '2025-04-13'
# Test for dynamic baudrate configuration interface # Test for dynamic baudrate configuration interface
compiler = meson.get_compiler('c') compiler = meson.get_compiler('c')

View file

@ -46,6 +46,7 @@ _tio()
--script-file \ --script-file \
--script-run \ --script-run \
--exec \ --exec \
--complete-profiles \
-v --version \ -v --version \
-h --help" -h --help"
@ -85,11 +86,11 @@ _tio()
return 0 return 0
;; ;;
-m | --map) -m | --map)
COMPREPLY=( $(compgen -W "ICRNL IGNCR INLCR IFFESCC INLCRNL IMSB2LSB OCRNL ODELBS ONLCRNL OLTU ONULBRK OIGNCR" -- ${cur}) ) COMPREPLY=( $(compgen -W "ICRNL IGNCR INLCR IFFESCC INLCRNL ICRCRNL IMSB2LSB OCRNL ODELBS ONLCRNL OLTU ONULBRK OIGNCR" -- ${cur}) )
return 0 return 0
;; ;;
--timestamp-format) --timestamp-format)
COMPREPLY=( $(compgen -W "24hour 24hour-start 24hour-delta iso8601" -- ${cur}) ) COMPREPLY=( $(compgen -W "24hour 24hour-start 24hour-delta iso8601 epoch epoch-usec" -- ${cur}) )
return 0 return 0
;; ;;
-c | --color) -c | --color)

View file

@ -26,18 +26,25 @@
#include <unistd.h> #include <unistd.h>
#include <string.h> #include <string.h>
#include <assert.h> #include <assert.h>
#include <libgen.h>
#include <errno.h> #include <errno.h>
#include <regex.h> #include <regex.h>
#include <glib.h> #include <glib.h>
#include "configfile.h" #include "configfile.h"
#include "timestamp.h"
#include "print.h" #include "print.h"
#include "rs485.h" #include "rs485.h"
#include "misc.h" #include "misc.h"
#define CONFIG_GROUP_NAME_DEFAULT "default" #define CONFIG_GROUP_NAME_DEFAULT "default"
#define CONFIG_GROUP_INCLUDE_PREFIX "include "
#define MAX_LINE_LENGTH 1024
struct config_t config = {}; 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, ...) static void config_get_string(GKeyFile *key_file, gchar *group, gchar *key, char **dest, char *allowed_string, ...)
{ {
(void)dest; (void)dest;
@ -142,7 +149,6 @@ static void config_get_bool(GKeyFile *key_file, gchar *group, gchar *key, bool *
static void config_parse_keys(GKeyFile *key_file, char *group) static void config_parse_keys(GKeyFile *key_file, char *group)
{ {
char *string = NULL; char *string = NULL;
bool boolean = false;
config_get_string(key_file, group, "device", &config.device, 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, "baudrate", &option.baudrate, 0, INT_MAX);
@ -198,21 +204,21 @@ static void config_parse_keys(GKeyFile *key_file, char *group)
g_free((void *)string); g_free((void *)string);
string = NULL; string = NULL;
} }
config_get_bool(key_file, group, "timestamp", &boolean); config_get_bool(key_file, group, "timestamp", (bool*) &option.timestamp);
if (boolean == true) if (option.timestamp != TIMESTAMP_NONE)
{ {
option.timestamp = TIMESTAMP_24HOUR; config_get_string(key_file, group, "timestamp-format", &string, "24hour", "24hour-start", "24hour-delta", "iso8601", "epoch", "epoch-usec", NULL);
} if (string != NULL)
config_get_string(key_file, group, "timestamp-format", &string, "24hour", "24hour-start", "24hour-delta", "iso8601", NULL); {
if (string != NULL) option_parse_timestamp(string, &option.timestamp);
{ g_free((void *)string);
option_parse_timestamp(string, &option.timestamp); string = NULL;
g_free((void *)string); }
string = NULL;
} }
config_get_integer(key_file, group, "timestamp-timeout", &option.timestamp_timeout, 0, INT_MAX); 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_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-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-append", &option.log_append);
config_get_bool(key_file, group, "log-strip", &option.log_strip); config_get_bool(key_file, group, "log-strip", &option.log_strip);
config_get_string(key_file, group, "map", &string, NULL); config_get_string(key_file, group, "map", &string, NULL);
@ -347,9 +353,11 @@ static int config_file_resolve(void)
void config_file_show_profiles(void) void config_file_show_profiles(void)
{ {
GKeyFile *keyfile; GString *config_buffer;
GError *error = NULL; GError *error = NULL;
GKeyFile *keyfile;
// Reset configuration
memset(&config, 0, sizeof(struct config_t)); memset(&config, 0, sizeof(struct config_t));
// Find config file // Find config file
@ -359,13 +367,16 @@ void config_file_show_profiles(void)
return; return;
} }
keyfile = g_key_file_new(); // Load content of configuration file into buffer
config_buffer = g_string_new(NULL);
config_file_load(config.path, config_buffer, false);
if (!g_key_file_load_from_file(keyfile, config.path, G_KEY_FILE_NONE, &error)) // 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)
{ {
tio_error_print("Failure loading file: %s", error->message);
g_error_free(error); g_error_free(error);
return; goto error_load;
} }
// Get all group names // Get all group names
@ -379,11 +390,20 @@ void config_file_show_profiles(void)
{ {
continue; 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]); printf("%s ", group[i]);
} }
g_strfreev(group); g_strfreev(group);
error_load:
g_key_file_free(keyfile); g_key_file_free(keyfile);
g_string_free(config_buffer, TRUE);
} }
static void replace_substring(char *str, const char *substr, const char *replacement) static void replace_substring(char *str, const char *substr, const char *replacement)
@ -408,12 +428,13 @@ static char *match_and_replace(const char *str, const char *pattern, char *devic
assert(pattern != NULL); assert(pattern != NULL);
assert(device != NULL); assert(device != NULL);
char *string = strndup(device, PATH_MAX); char *string = calloc(PATH_MAX, 1);
if (string == NULL) if (string == NULL)
{ {
tio_debug_printf("Failure allocating string memory\n"); tio_debug_printf("Failure allocating string memory\n");
return NULL; return NULL;
} }
strncpy(string, device, PATH_MAX - 1);
/* Find matches of pattern in str. For each match, replace any '%mN' in the /* 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 * copy of the device string with the corresponding match subexpression and
@ -483,6 +504,95 @@ error:
return NULL; 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) void config_file_parse(void)
{ {
// Find config file // Find config file
@ -497,12 +607,18 @@ void config_file_parse(void)
return; return;
} }
GString *config_buffer = g_string_new(NULL);
GKeyFile *keyfile = g_key_file_new(); GKeyFile *keyfile = g_key_file_new();
GList *included_files = NULL;
GError *error = NULL; GError *error = NULL;
if (g_key_file_load_from_file(keyfile, config.path, G_KEY_FILE_NONE, &error) == false) config_file_load(config.path, config_buffer, true);
if (g_key_file_load_from_data(keyfile, config_buffer->str, config_buffer->len, G_KEY_FILE_NONE, &error) == false)
{ {
tio_error_print("Failure loading file %s: %s", config.path, error->message); 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); g_error_free(error);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
@ -574,7 +690,10 @@ void config_file_parse(void)
g_strfreev(group); g_strfreev(group);
} }
// Cleanup
g_key_file_free(keyfile); g_key_file_free(keyfile);
g_string_free(config_buffer, TRUE);
g_list_free_full(included_files, g_free);
atexit(&config_exit); atexit(&config_exit);
} }
@ -614,10 +733,14 @@ void config_list_targets(void)
keyfile = g_key_file_new(); keyfile = g_key_file_new();
if (!g_key_file_load_from_file(keyfile, config.path, G_KEY_FILE_NONE, &error)) 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); g_error_free(error);
return; goto cleanup;
} }
// Get all group names // Get all group names
@ -626,7 +749,7 @@ void config_list_targets(void)
if (num_groups == 0) if (num_groups == 0)
{ {
return; goto cleanup;
} }
printf("\nConfiguration profiles (%s)\n", config.path); printf("\nConfiguration profiles (%s)\n", config.path);
@ -640,6 +763,13 @@ void config_list_targets(void)
{ {
continue; 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]); printf("%-19s ", group[i]);
if (j++ % 4 == 0) if (j++ % 4 == 0)
{ {
@ -652,5 +782,7 @@ void config_list_targets(void)
} }
g_strfreev(group); g_strfreev(group);
cleanup:
g_key_file_free(keyfile); g_key_file_free(keyfile);
g_string_free(config_buffer, TRUE);
} }

View file

@ -19,8 +19,7 @@
* 02110-1301, USA. * 02110-1301, USA.
*/ */
#define __STDC_WANT_LIB_EXT2__ 1 // To access vasprintf #define _GNU_SOURCE // To access vasprintf
#include <stdio.h> #include <stdio.h>
#include "print.h" #include "print.h"

View file

@ -199,7 +199,6 @@ double fs_get_creation_time(const char *path)
double fs_get_creation_time(const char *path) double fs_get_creation_time(const char *path)
{ {
// Use stat on macOS to access creation time
struct stat st; struct stat st;
if (stat(path, &st) != 0) if (stat(path, &st) != 0)
@ -207,7 +206,7 @@ double fs_get_creation_time(const char *path)
return -1; return -1;
} }
return st.st_birthtimespec.tv_sec + st.st_birthtimespec.tv_nsec / 1e9; return st.st_mtimespec.tv_sec + st.st_mtimespec.tv_nsec / 1e9;
} }
#else #else

View file

@ -19,8 +19,7 @@
* 02110-1301, USA. * 02110-1301, USA.
*/ */
#define __STDC_WANT_LIB_EXT2__ 1 // To access vasprintf #define _GNU_SOURCE // To access vasprintf
#include <sys/time.h> #include <sys/time.h>
#include <libgen.h> #include <libgen.h>
#include <errno.h> #include <errno.h>

View file

@ -22,6 +22,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #include <unistd.h>
#include "version.h"
#include "config.h" #include "config.h"
#include "options.h" #include "options.h"
#include "configfile.h" #include "configfile.h"
@ -60,6 +61,10 @@ int main(int argc, char *argv[])
/* Configure tty device */ /* Configure tty device */
tty_configure(); 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 */ /* Configure input terminal */
if (isatty(fileno(stdin))) if (isatty(fileno(stdin)))
{ {
@ -101,7 +106,7 @@ int main(int argc, char *argv[])
error_enter_session_mode(); error_enter_session_mode();
/* Print launch hints */ /* Print launch hints */
tio_printf("tio v%s", VERSION); tio_printf("tio %s", VERSION);
if (interactive_mode) if (interactive_mode)
{ {
tio_printf("Press ctrl-%c q to quit", option.prefix_key); tio_printf("Press ctrl-%c q to quit", option.prefix_key);

View file

@ -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 = configuration_data()
config_h.set_quoted('VERSION', meson.project_version())
config_h.set('BAUDRATE_CASES', baudrate_cases) config_h.set('BAUDRATE_CASES', baudrate_cases)
configure_file(output: 'config.h', configuration: config_h) configure_file(output: 'config.h', configuration: config_h)
@ -21,7 +26,8 @@ tio_sources = [
'xymodem.c', 'xymodem.c',
'script.c', 'script.c',
'fs.c', 'fs.c',
'readline.c' 'readline.c',
version_h
] ]
@ -41,7 +47,13 @@ tio_dep = [
lua_dep lua_dep
] ]
tio_c_args = ['-Wno-unused-result'] if host_machine.system() == 'darwin'
iokit_dep = dependency('appleframeworks', modules: ['IOKit'], required: true)
corefoundation_dep = dependency('appleframeworks', modules: ['CoreFoundation'], required: true)
tio_dep += [iokit_dep, corefoundation_dep]
endif
tio_c_args = ['-Wshadow','-Wno-unused-result']
if enable_setspeed2 if enable_setspeed2
tio_c_args += '-DHAVE_TERMIOS2' tio_c_args += '-DHAVE_TERMIOS2'

View file

@ -121,10 +121,9 @@ unsigned long djb2_hash(const unsigned char *str)
} }
// Function to encode a number to base62 // Function to encode a number to base62
char *base62_encode(unsigned long num) void *base62_encode(unsigned long num, char *output)
{ {
const char base62_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const char base62_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
char *output = (char *) malloc(5); // 4 characters + null terminator
if (output == NULL) if (output == NULL)
{ {
tio_error_print("Memory allocation failed"); tio_error_print("Memory allocation failed");

View file

@ -30,7 +30,7 @@ void delay(long ms);
int ctrl_key_code(unsigned char key); int ctrl_key_code(unsigned char key);
bool regex_match(const char *string, const char *pattern); bool regex_match(const char *string, const char *pattern);
unsigned long djb2_hash(const unsigned char *str); unsigned long djb2_hash(const unsigned char *str);
char *base62_encode(unsigned long num); void *base62_encode(unsigned long num, char *output);
int read_poll(int fd, void *data, size_t len, int timeout); int read_poll(int fd, void *data, size_t len, int timeout);
double get_current_time(void); double get_current_time(void);
bool match_patterns(const char *string, const char *patterns); bool match_patterns(const char *string, const char *patterns);

View file

@ -19,10 +19,13 @@
* 02110-1301, USA. * 02110-1301, USA.
*/ */
#define _GNU_SOURCE // To access vasprintf
#include <assert.h> #include <assert.h>
#include <regex.h> #include <regex.h>
#include <getopt.h> #include <getopt.h>
#include <errno.h> #include <errno.h>
#include "version.h"
#include "config.h" #include "config.h"
#include "misc.h" #include "misc.h"
#include "print.h" #include "print.h"
@ -113,6 +116,7 @@ struct option_t option =
.map_ign_cr = false, .map_ign_cr = false,
.map_i_ff_escc = false, .map_i_ff_escc = false,
.map_i_nl_crnl = false, .map_i_nl_crnl = false,
.map_i_cr_crnl = false,
.map_o_cr_nl = false, .map_o_cr_nl = false,
.map_o_nl_crnl = false, .map_o_nl_crnl = false,
.map_o_del_bs = false, .map_o_del_bs = false,
@ -167,11 +171,14 @@ void option_print_help(char *argv[])
printf(" --script-file <filename> Run script from file\n"); printf(" --script-file <filename> Run script from file\n");
printf(" --script-run once|always|never Run script on connect (default: always)\n"); printf(" --script-run once|always|never Run script on connect (default: always)\n");
printf(" --exec <command> Execute shell command with I/O redirected to device\n"); printf(" --exec <command> 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(" -v, --version Display version\n");
printf(" -h, --help Display help\n"); printf(" -h, --help Display help\n");
printf("\n"); printf("\n");
printf("Options and profiles may be set via configuration file.\n"); printf("Options and profiles may be set via configuration file.\n");
printf("\n"); printf("\n");
printf("In session you can press ctrl-%c ? to list available key commands.\n", option.prefix_key);
printf("\n");
printf("See the man page for more details.\n"); printf("See the man page for more details.\n");
} }
@ -390,6 +397,14 @@ const char* option_timestamp_format_to_string(timestamp_t timestamp)
return "iso8601"; return "iso8601";
break; break;
case TIMESTAMP_EPOCH:
return "epoch";
break;
case TIMESTAMP_EPOCH_USEC:
return "epoch-usec";
break;
default: default:
return "unknown"; return "unknown";
break; break;
@ -416,6 +431,14 @@ void option_parse_timestamp(const char *arg, timestamp_t *timestamp)
{ {
*timestamp = TIMESTAMP_ISO8601; *timestamp = TIMESTAMP_ISO8601;
} }
else if (strcmp(arg, "epoch") == 0)
{
*timestamp = TIMESTAMP_EPOCH;
}
else if (strcmp(arg, "epoch-usec") == 0)
{
*timestamp = TIMESTAMP_EPOCH_USEC;
}
else else
{ {
tio_error_print("Invalid timestamp '%s'", arg); tio_error_print("Invalid timestamp '%s'", arg);
@ -757,6 +780,10 @@ void option_parse_mappings(const char *map)
{ {
option.map_i_nl_crnl = true; option.map_i_nl_crnl = true;
} }
else if (strcmp(token,"ICRCRNL") == 0)
{
option.map_i_cr_crnl = true;
}
else if (strcmp(token, "ONLCRNL") == 0) else if (strcmp(token, "ONLCRNL") == 0)
{ {
option.map_o_nl_crnl = true; option.map_o_nl_crnl = true;
@ -985,7 +1012,10 @@ void options_parse(int argc, char *argv[])
break; break;
case 't': case 't':
option.timestamp = TIMESTAMP_24HOUR; if (option.timestamp == TIMESTAMP_NONE)
{
option.timestamp = TIMESTAMP_24HOUR;
}
break; break;
case OPT_TIMESTAMP_FORMAT: case OPT_TIMESTAMP_FORMAT:
@ -1075,7 +1105,7 @@ void options_parse(int argc, char *argv[])
break; break;
case 'v': case 'v':
printf("tio v%s\n", VERSION); printf("tio %s\n", VERSION);
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
break; break;

View file

@ -98,6 +98,7 @@ struct option_t
bool map_ign_cr; bool map_ign_cr;
bool map_i_ff_escc; bool map_i_ff_escc;
bool map_i_nl_crnl; bool map_i_nl_crnl;
bool map_i_cr_crnl;
bool map_o_cr_nl; bool map_o_cr_nl;
bool map_o_nl_crnl; bool map_o_nl_crnl;
bool map_o_del_bs; bool map_o_del_bs;

View file

@ -87,3 +87,26 @@ void print(const char *format, ...)
print_tainted = true; 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<padding; i++)
{
putchar(pad_char);
}
}
else
{
printf("%s", string);
}
}

View file

@ -140,3 +140,4 @@ void print_normal(char c);
void print_init_ansi_formatting(void); void print_init_ansi_formatting(void);
void tio_printf_array(const char *array); void tio_printf_array(const char *array);
void print_tainted_set(void); void print_tainted_set(void);
void print_padded(char *string, size_t length, char pad_char);

View file

@ -22,17 +22,16 @@
#include "print.h" #include "print.h"
#include "misc.h" #include "misc.h"
#define MAX_LINE_LEN PATH_MAX #define RL_LINE_LENGTH_MAX PATH_MAX
#define MAX_HISTORY 1000 #define RL_HISTORY_MAX 1000
static char line[MAX_LINE_LEN] = {}; static char rl_line[RL_LINE_LENGTH_MAX] = {};
static int history_count = 0; static char *rl_history[RL_HISTORY_MAX];
static int history_index = 0; static int rl_history_count = 0;
static int line_length = 0; static int rl_history_index = 0;
static int cursor_pos = 0; static int rl_line_length = 0;
static int escape = 0; static int rl_cursor_pos = 0;
static int rl_escape = 0;
static char *history[MAX_HISTORY];
static void print_line(const char *string, int cursor_pos) static void print_line(const char *string, int cursor_pos)
{ {
@ -47,58 +46,58 @@ static void print_line(const char *string, int cursor_pos)
void readline_init(void) void readline_init(void)
{ {
history_count = 0; rl_history_count = 0;
history_index = 0; rl_history_index = 0;
for (int i = 0; i < MAX_HISTORY; ++i) for (int i = 0; i < RL_HISTORY_MAX; ++i)
{ {
history[i] = NULL; rl_history[i] = NULL;
} }
line[0] = 0; rl_line[0] = 0;
line_length = 0; rl_line_length = 0;
cursor_pos = 0; rl_cursor_pos = 0;
escape = 0; rl_escape = 0;
} }
char * readline_get(void) char * readline_get(void)
{ {
return line; return rl_line;
} }
static void readline_input_char(char input_char) static void readline_input_char(char input_char)
{ {
if (line_length < MAX_LINE_LEN - 1) if (rl_line_length < RL_LINE_LENGTH_MAX - 1)
{ {
memmove(&line[cursor_pos + 1], &line[cursor_pos], line_length - cursor_pos); memmove(&rl_line[rl_cursor_pos + 1], &rl_line[rl_cursor_pos], rl_line_length - rl_cursor_pos);
line[cursor_pos] = input_char; rl_line[rl_cursor_pos] = input_char;
line_length++; rl_line_length++;
cursor_pos++; rl_cursor_pos++;
line[line_length] = '\0'; rl_line[rl_line_length] = '\0';
print_line(line, cursor_pos); print_line(rl_line, rl_cursor_pos);
} }
escape = 0; rl_escape = 0;
} }
static void readline_input_cr(void) static void readline_input_cr(void)
{ {
if (line_length > 0) if (rl_line_length > 0)
{ {
// Save to history // Save to history
if (history_count < MAX_HISTORY) if (rl_history_count < RL_HISTORY_MAX)
{ {
history[history_count] = strndup(line, line_length); rl_history[rl_history_count] = strndup(rl_line, rl_line_length);
history_count++; rl_history_count++;
} }
else else
{ {
free(history[0]); free(rl_history[0]);
memmove(&history[0], &history[1], (MAX_HISTORY - 1) * sizeof(char*)); memmove(&rl_history[0], &rl_history[1], (RL_HISTORY_MAX - 1) * sizeof(char*));
history[MAX_HISTORY - 1] = strndup(line, line_length); rl_history[RL_HISTORY_MAX - 1] = strndup(rl_line, rl_line_length);
} }
} }
line[line_length] = '\0'; rl_line[rl_line_length] = '\0';
if (option.local_echo == false) if (option.local_echo == false)
{ {
clear_line(); clear_line();
@ -108,54 +107,54 @@ static void readline_input_cr(void)
print("\r\n"); print("\r\n");
} }
line_length = 0; rl_line_length = 0;
cursor_pos = 0; rl_cursor_pos = 0;
history_index = history_count; rl_history_index = rl_history_count;
escape = 0; rl_escape = 0;
} }
static void readline_input_bs(void) static void readline_input_bs(void)
{ {
if (cursor_pos > 0) if (rl_cursor_pos > 0)
{ {
memmove(&line[cursor_pos - 1], &line[cursor_pos], line_length - cursor_pos); memmove(&rl_line[rl_cursor_pos - 1], &rl_line[rl_cursor_pos], rl_line_length - rl_cursor_pos);
line_length--; rl_line_length--;
cursor_pos--; rl_cursor_pos--;
line[line_length] = '\0'; rl_line[rl_line_length] = '\0';
print_line(line, cursor_pos); print_line(rl_line, rl_cursor_pos);
} }
escape = 0; rl_escape = 0;
} }
static void readline_input_escape(void) static void readline_input_escape(void)
{ {
escape = 1; rl_escape = 1;
} }
static void readline_input_left_bracket(void) static void readline_input_left_bracket(void)
{ {
if (escape == 1) if (rl_escape == 1)
{ {
escape = 2; rl_escape = 2;
} }
else else
{ {
escape = 0; rl_escape = 0;
} }
} }
static void readline_input_A(void) static void readline_input_A(void)
{ {
if (escape == 2) if (rl_escape == 2)
{ {
// Up arrow // Up arrow
if (history_index > 0) if (rl_history_index > 0)
{ {
history_index--; rl_history_index--;
strncpy(line, history[history_index], MAX_LINE_LEN-1); strncpy(rl_line, rl_history[rl_history_index], RL_LINE_LENGTH_MAX-1);
line_length = strlen(line); rl_line_length = strlen(rl_line);
cursor_pos = line_length; rl_cursor_pos = rl_line_length;
print_line(line, cursor_pos); print_line(rl_line, rl_cursor_pos);
} }
} }
else else
@ -163,29 +162,29 @@ static void readline_input_A(void)
readline_input_char('A'); readline_input_char('A');
} }
escape = 0; rl_escape = 0;
} }
static void readline_input_B(void) static void readline_input_B(void)
{ {
if (escape == 2) if (rl_escape == 2)
{ {
// Down arrow // Down arrow
if (history_index < history_count - 1) if (rl_history_index < rl_history_count - 1)
{ {
history_index++; rl_history_index++;
strncpy(line, history[history_index], MAX_LINE_LEN-1); strncpy(rl_line, rl_history[rl_history_index], RL_LINE_LENGTH_MAX-1);
line_length = strlen(line); rl_line_length = strlen(rl_line);
cursor_pos = line_length; rl_cursor_pos = rl_line_length;
print_line(line, cursor_pos); print_line(rl_line, rl_cursor_pos);
} }
else if (history_index == history_count - 1) else if (rl_history_index == rl_history_count - 1)
{ {
history_index++; rl_history_index++;
line_length = 0; rl_line_length = 0;
cursor_pos = 0; rl_cursor_pos = 0;
line[line_length] = '\0'; rl_line[rl_line_length] = '\0';
print_line(line, cursor_pos); print_line(rl_line, rl_cursor_pos);
} }
} }
else else
@ -193,17 +192,17 @@ static void readline_input_B(void)
readline_input_char('B'); readline_input_char('B');
} }
escape = 0; rl_escape = 0;
} }
static void readline_input_C(void) static void readline_input_C(void)
{ {
if (escape == 2) if (rl_escape == 2)
{ {
// Right arrow // Right arrow
if (cursor_pos < line_length) if (rl_cursor_pos < rl_line_length)
{ {
cursor_pos++; rl_cursor_pos++;
print("\x1b[C"); print("\x1b[C");
} }
} }
@ -212,17 +211,17 @@ static void readline_input_C(void)
readline_input_char('C'); readline_input_char('C');
} }
escape = 0; rl_escape = 0;
} }
static void readline_input_D(void) static void readline_input_D(void)
{ {
if (escape == 2) if (rl_escape == 2)
{ {
// Left arrow // Left arrow
if (cursor_pos > 0) if (rl_cursor_pos > 0)
{ {
cursor_pos--; rl_cursor_pos--;
print("\b"); print("\b");
} }
} }
@ -231,7 +230,7 @@ static void readline_input_D(void)
readline_input_char('D'); readline_input_char('D');
} }
escape = 0; rl_escape = 0;
} }
void readline_input(char input_char) void readline_input(char input_char)

View file

@ -20,7 +20,6 @@
*/ */
#include <errno.h> #include <errno.h>
#include <regex.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #include <unistd.h>
@ -29,6 +28,7 @@
#include <lauxlib.h> #include <lauxlib.h>
#include <lualib.h> #include <lualib.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <ctype.h>
#include "misc.h" #include "misc.h"
#include "print.h" #include "print.h"
#include "options.h" #include "options.h"
@ -37,27 +37,94 @@
#include "log.h" #include "log.h"
#include "script.h" #include "script.h"
#include "fs.h" #include "fs.h"
#include "timestamp.h"
#include "termios.h"
#define MAX_BUFFER_SIZE 2000 // Maximum size of circular buffer #define MAX_BUFFER_SIZE 2000 // Maximum size of circular buffer
#define READ_LINE_SIZE 4096 // read_line buffer length
static int device_fd; static int device_fd;
static char circular_buffer[MAX_BUFFER_SIZE];
static char match_string[MAX_BUFFER_SIZE];
static int buffer_size = 0;
static char script_init[] = static char script_init[] =
"function set(arg)\n" "tio.set = function(arg)\n"
" local dtr = arg.DTR or -1\n" " local dtr = arg.DTR or -1\n"
" local rts = arg.RTS or -1\n" " local rts = arg.RTS or -1\n"
" local cts = arg.CTS or -1\n" " local cts = arg.CTS or -1\n"
" local dsr = arg.DSR or -1\n" " local dsr = arg.DSR or -1\n"
" local cd = arg.CD or -1\n" " local cd = arg.CD or -1\n"
" local ri = arg.RI or -1\n" " local ri = arg.RI or -1\n"
" line_set(dtr, rts, cts, dsr, cd, ri)\n" " tio.line_set(dtr, rts, cts, dsr, cd, ri)\n"
"end\n"; "end\n"
"tio.expect = function(pattern, timeout)\n"
" local str = ''\n"
" while true do\n"
" local c = tio.read(1, timeout)\n"
" if c then\n"
" str = str .. c\n"
" if string.match(str, pattern) then\n"
" return string.match(str, pattern)\n"
" end\n"
" else\n"
" return nil, str\n"
" end\n"
" end\n"
"end\n"
"tio.alwaysecho = true\n"
"setmetatable(tio, tio)\n";
// lua: sleep(seconds) static bool alwaysecho(lua_State *L)
static int sleep_(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<len; i++)
{
putchar(str[i]);
if (option.log)
log_putc(str[i]);
}
}
return 0;
}
static void maybe_echo(lua_State *L)
{
if (alwaysecho(L))
{
lua_pushcfunction(L, api_echo);
lua_pushvalue(L, -2);
lua_call(L, 1, 0);
}
}
// lua: tio.sleep(seconds)
static int api_sleep(lua_State *L)
{ {
long seconds = lua_tointeger(L, 1); long seconds = lua_tointeger(L, 1);
@ -73,8 +140,8 @@ static int sleep_(lua_State *L)
return 0; return 0;
} }
// lua: msleep(miliseconds) // lua: tio.msleep(miliseconds)
static int msleep(lua_State *L) static int api_msleep(lua_State *L)
{ {
long mseconds = lua_tointeger(L, 1); long mseconds = lua_tointeger(L, 1);
long useconds = mseconds * 1000; long useconds = mseconds * 1000;
@ -90,7 +157,7 @@ static int msleep(lua_State *L)
return 0; return 0;
} }
// lua: line_set(dtr,rts,cts,dsr,cd,ri) // lua: tio.line_set(dtr,rts,cts,dsr,cd,ri)
static int line_set(lua_State *L) static int line_set(lua_State *L)
{ {
tty_line_config_t line_config[6] = { }; tty_line_config_t line_config[6] = { };
@ -144,11 +211,12 @@ static int line_set(lua_State *L)
return 0; return 0;
} }
// lua: modem_send(file, protocol) // lua: tio.send(file, protocol)
static int modem_send(lua_State *L) static int api_send(lua_State *L)
{ {
const char *file = lua_tostring(L, 1); const char *file = luaL_checkstring(L, 1);
int protocol = lua_tointeger(L, 2); int protocol = luaL_checkinteger(L, 2);
int ret;
if (file == NULL) if (file == NULL)
{ {
@ -159,227 +227,138 @@ static int modem_send(lua_State *L)
{ {
case XMODEM_1K: case XMODEM_1K:
tio_printf("Sending file '%s' using XMODEM-1K", file); tio_printf("Sending file '%s' using XMODEM-1K", file);
tio_printf("%s", xymodem_send(device_fd, file, XMODEM_1K) < 0 ? "Aborted" : "Done"); ret = xymodem_send(device_fd, file, XMODEM_1K);
tio_printf("%s", ret < 0 ? "Aborted" : "Done");
break; break;
case XMODEM_CRC: case XMODEM_CRC:
tio_printf("Sending file '%s' using XMODEM-CRC", file); tio_printf("Sending file '%s' using XMODEM-CRC", file);
tio_printf("%s", xymodem_send(device_fd, file, XMODEM_CRC) < 0 ? "Aborted" : "Done"); ret = xymodem_send(device_fd, file, XMODEM_CRC);
break; tio_printf("%s", ret < 0 ? "Aborted" : "Done");
break;
case YMODEM: case YMODEM:
tio_printf("Sending file '%s' using YMODEM", file); tio_printf("Sending file '%s' using YMODEM", file);
tio_printf("%s", xymodem_send(device_fd, file, YMODEM) < 0 ? "Aborted" : "Done"); ret = xymodem_send(device_fd, file, YMODEM);
break; tio_printf("%s", ret < 0 ? "Aborted" : "Done");
break;
} }
return 0; return 0;
} }
// lua: send(string) // lua: tio.write(string)
static int send(lua_State *L) static int api_write(lua_State *L)
{ {
const char *string = lua_tostring(L, 1); size_t len = 0;
int ret; const char *string = luaL_checklstring(L, 1, &len);
ssize_t ret;
int attempts = 100;
if (string == NULL) do {
{ ret = write(device_fd, string, len);
return 0; if (ret < 0)
} return luaL_error(L, "%s", strerror(errno));
ret = write(device_fd, string, strlen(string)); len -= ret;
string += ret;
} while (len > 0 && --attempts);
lua_pushnumber(L, ret); if (len > 0)
return luaL_error(L, "partial write");
fsync(device_fd); // flush these characters now
tcdrain(device_fd); //ensure we flushed characters to our device
lua_getglobal(L, "tio");
return 1; return 1;
} }
// Function to add a character to the circular expect buffer // lua: tio.read(size, timeout)
static void expect_buffer_add(char c) static int api_read(lua_State *L)
{ {
if (buffer_size < MAX_BUFFER_SIZE) int size = luaL_checkinteger(L, 1);
{
circular_buffer[buffer_size++] = c;
}
else
{
// Shift the buffer to accommodate the new character
memmove(circular_buffer, circular_buffer + 1, MAX_BUFFER_SIZE - 1);
circular_buffer[MAX_BUFFER_SIZE - 1] = c;
}
}
// Function to match against the circular expect buffer using regex
static bool match_regex(regex_t *regex)
{
char buffer[MAX_BUFFER_SIZE + 1]; // Temporary buffer for regex matching
const char *s = circular_buffer;
regmatch_t pmatch[1];
regoff_t len;
memcpy(buffer, circular_buffer, buffer_size);
buffer[buffer_size] = '\0'; // Null-terminate the buffer
// Match against the regex
int ret = regexec(regex, buffer, 1, pmatch, 0);
if (!ret)
{
// Match found
len = pmatch[0].rm_eo - pmatch[0].rm_so;
memcpy(match_string, s + pmatch[0].rm_so, len);
match_string[len] = '\0';
return true;
}
else if (ret == REG_NOMATCH)
{
// No match found, do nothing
}
else
{
// Error occurred during matching
tio_error_print("Regex match failed");
}
return false;
}
// lua: ret,string = read_string(size, timeout)
static int read_string(lua_State *L)
{
int size = lua_tointeger(L, 1);
int timeout = lua_tointeger(L, 2); int timeout = lua_tointeger(L, 2);
int ret = 0;
char *buffer = malloc(size);
if (buffer == NULL)
{
ret = -1; // Error
goto error;
}
if (timeout == 0) if (timeout == 0)
{ {
timeout = -1; // Wait forever timeout = -1; // Wait forever
} }
ssize_t bytes_read = read_poll(device_fd, buffer, size, timeout); luaL_Buffer buffer;
if (bytes_read < 0) luaL_buffinit(L, &buffer);
#if LUA_VERSION_NUM >= 502
char *p = luaL_prepbuffsize(&buffer, size);
#else
if (size > LUAL_BUFFERSIZE)
return luaL_error(L, "buffer overflow, max size is: %d", LUAL_BUFFERSIZE);
char *p = luaL_prepbuffer(&buffer);
#endif
ssize_t ret = read_poll(device_fd, p, size, timeout);
if (ret < 0)
return luaL_error(L, "%s", strerror(errno));
luaL_addsize(&buffer, ret);
luaL_pushresult(&buffer);
if (ret == 0)
{ {
ret = -1; // Error // On timeout return nil instead of an empty string
goto error; lua_pop(L, 1);
lua_pushnil(L);
} }
else if (bytes_read == 0) else
{ {
ret = 0; // Timeout maybe_echo(L);
goto error;
} }
for (ssize_t i=0; i<bytes_read; i++) return 1;
{
putchar(buffer[i]);
if (option.log)
{
log_putc(buffer[i]);
}
}
ret = bytes_read;
error:
lua_pushnumber(L, ret);
if (buffer != NULL)
{
lua_pushstring(L, buffer);
free(buffer);
}
return 2;
} }
// lua: expect(string, timeout) // lua: string = tio.readline(timeout)
static int expect(lua_State *L) static int api_readline(lua_State *L) {
{ int timeout = lua_tointeger(L, 1); //ms
const char *string = lua_tostring(L, 1); luaL_Buffer b;
long timeout = lua_tointeger(L, 2); char ch;
regex_t regex;
int ret = 0;
char c;
// Resets buffer to ignore previous `expect` calls
buffer_size = 0;
match_string[0] = '\0';
if ((string == NULL) || (timeout < 0))
{
ret = -1;
goto error;
}
if (timeout == 0) if (timeout == 0)
{ {
// Let poll() wait forever timeout = -1; // Wait forever
timeout = -1;
} }
// Compile the regular expression luaL_buffinit(L, &b);
ret = regcomp(&regex, string, REG_EXTENDED); luaL_prepbuffer(&b);
if (ret) while (true) {
{ int ret = read_poll(device_fd, &ch, 1, timeout);
tio_error_print("Could not compile regex");
ret = -1;
goto error;
}
// Main loop to read and match if (ret < 0)
while (true) return luaL_error(L, "%s", strerror(errno));
{
ssize_t bytes_read = read_poll(device_fd, &c, 1, timeout); if (ret == 0)
if (bytes_read > 0)
{ {
putchar(c); luaL_pushresult(&b);
expect_buffer_add(c); maybe_echo(L);
lua_pushnil(L);
if (option.log) lua_insert(L, -2);
{ return 2;
log_putc(c);
}
// Match against the entire buffer
if (match_regex(&regex))
{
ret = 1;
break;
}
} }
else
if (ch == '\n')
{ {
// Timeout or error luaL_pushresult(&b);
break; maybe_echo(L);
return 1;
} }
luaL_addchar(&b, ch);
} }
// Cleanup
regfree(&regex);
error:
lua_pushnumber(L, ret);
lua_pushstring(L, match_string);
return 2;
} }
// lua: exit(code) // lua: table = tio.ttysearch()
static int exit_(lua_State *L) static int api_ttysearch(lua_State *L)
{
long code = lua_tointeger(L, 1);
exit(code);
return 0;
}
// lua: list = tty_search()
static int tty_search_(lua_State *L)
{ {
UNUSED(L); UNUSED(L);
GList *iter; GList *iter;
@ -445,65 +424,7 @@ static void script_buffer_run(lua_State *L, const char *script_buffer)
} }
} }
static const struct luaL_Reg tio_lib[] = static void script_file_run(lua_State *L, const char *filename)
{
{ "sleep", sleep_},
{ "msleep", msleep},
{ "line_set", line_set},
{ "modem_send", modem_send},
{ "send", send},
{ "read", read_string},
{ "expect", expect},
{ "exit", exit_},
{ "tty_search", tty_search_},
{NULL, NULL}
};
#if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501
/*
** Adapted from Lua 5.2.0 (for backwards compatibility)
*/
static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup)
{
luaL_checkstack(L, nup+1, "too many upvalues");
for (; l->name != NULL; l++) { /* fill the table with given functions */
int i;
lua_pushstring(L, l->name);
for (i = 0; i < nup; i++) /* copy upvalues to the top */
lua_pushvalue(L, -(nup+1));
lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */
lua_settable(L, -(nup + 3));
}
lua_pop(L, nup); /* remove upvalues */
}
#endif
static void script_load(lua_State *L)
{
int error;
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
}
}
int lua_register_tio(lua_State *L)
{
// Register lxi functions
lua_getglobal(L, "_G");
luaL_setfuncs(L, tio_lib, 0);
lua_pop(L, 1);
// Load lua init script
script_load(L);
return 0;
}
void script_file_run(lua_State *L, const char *filename)
{ {
if (strlen(filename) == 0) if (strlen(filename) == 0)
{ {
@ -519,13 +440,39 @@ void script_file_run(lua_State *L, const char *filename)
} }
} }
void script_set_global(lua_State *L, const char *name, long value) 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_pushnumber(L, value);
lua_setglobal(L, name); lua_setglobal(L, name);
} }
void script_set_globals(lua_State *L) static void script_set_globals(lua_State *L)
{ {
script_set_global(L, "toggle", 2); script_set_global(L, "toggle", 2);
script_set_global(L, "high", 1); script_set_global(L, "high", 1);
@ -535,6 +482,14 @@ void script_set_globals(lua_State *L)
script_set_global(L, "YMODEM", YMODEM); 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) void script_run(int fd, const char *script_filename)
{ {
lua_State *L; lua_State *L;
@ -544,8 +499,15 @@ void script_run(int fd, const char *script_filename)
L = luaL_newstate(); L = luaL_newstate();
luaL_openlibs(L); luaL_openlibs(L);
// Bind tio functions #if LUA_VERSION_NUM >= 502
lua_register_tio(L); 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 // Initialize globals
script_set_globals(L); script_set_globals(L);

View file

@ -226,7 +226,11 @@ void socket_configure(void)
exit(EXIT_FAILURE); 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))) if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)))
#endif
{ {
tio_error_printf("Failed to set socket options (%s)", strerror(errno)); tio_error_printf("Failed to set socket options (%s)", strerror(errno));
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
@ -270,7 +274,12 @@ void socket_write(char input_char)
{ {
if (clientfds[i] != -1) if (clientfds[i] != -1)
{ {
#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) if (send(clientfds[i], &input_char, 1, MSG_NOSIGNAL) <= 0)
#endif
{ {
tio_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]); close(clientfds[i]);

View file

@ -29,8 +29,6 @@
#include "options.h" #include "options.h"
#include "timestamp.h" #include "timestamp.h"
#define TIME_STRING_SIZE_MAX 24
char *timestamp_current_time(void) char *timestamp_current_time(void)
{ {
static char time_string[TIME_STRING_SIZE_MAX]; static char time_string[TIME_STRING_SIZE_MAX];
@ -76,16 +74,29 @@ char *timestamp_current_time(void)
tm = localtime(&tv.tv_sec); tm = localtime(&tv.tv_sec);
len = strftime(time_string, sizeof(time_string), "%Y-%m-%dT%H:%M:%S", tm); len = strftime(time_string, sizeof(time_string), "%Y-%m-%dT%H:%M:%S", tm);
break; 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: default:
return NULL; return NULL;
} }
// Append milliseconds to all timestamps // Append millis-/microseconds to all timestamps
if (len) if (len)
{ {
len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%03ld", (long)tv.tv_usec / 1000); if ( option.timestamp == TIMESTAMP_EPOCH_USEC )
{
len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%06ld", (long)tv.tv_usec);
}
else
{
len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%03ld", (long)tv.tv_usec / 1000);
}
} }
// Save previous time value for next run // Save previous time value for next run
tv_previous = tv_now; tv_previous = tv_now;

View file

@ -28,8 +28,12 @@ typedef enum
TIMESTAMP_24HOUR_START, TIMESTAMP_24HOUR_START,
TIMESTAMP_24HOUR_DELTA, TIMESTAMP_24HOUR_DELTA,
TIMESTAMP_ISO8601, TIMESTAMP_ISO8601,
TIMESTAMP_EPOCH,
TIMESTAMP_EPOCH_USEC,
TIMESTAMP_END, TIMESTAMP_END,
} timestamp_t; } timestamp_t;
#define TIME_STRING_SIZE_MAX 24
char *timestamp_current_time(void); char *timestamp_current_time(void);

429
src/tty.c
View file

@ -22,6 +22,16 @@
#if defined(__linux__) #if defined(__linux__)
#include <linux/serial.h> #include <linux/serial.h>
#endif #endif
#if defined(__APPLE__) || defined(__MACH__)
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOBSD.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/serial/IOSerialKeys.h>
#include <IOKit/usb/IOUSBLib.h>
#endif
#include "version.h"
#include "config.h" #include "config.h"
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
@ -172,6 +182,7 @@ static pthread_t thread;
static int pipefd[2]; static int pipefd[2];
static pthread_mutex_t mutex_input_ready = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t mutex_input_ready = PTHREAD_MUTEX_INITIALIZER;
static char line[PATH_MAX]; static char line[PATH_MAX];
static size_t listing_device_name_length_max = 0;
static void optional_local_echo(char c) static void optional_local_echo(char c)
{ {
@ -621,16 +632,18 @@ void tty_output_mode_set(output_mode_t mode)
static void mappings_print(void) static void mappings_print(void)
{ {
if (option.map_i_cr_nl || option.map_ign_cr || option.map_i_ff_escc || 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_o_cr_nl || option.map_i_nl_cr || option.map_i_nl_crnl || option.map_i_cr_crnl ||
option.map_o_del_bs || option.map_o_nl_crnl || option.map_o_ltu || option.map_o_cr_nl || option.map_o_del_bs || option.map_o_nl_crnl ||
option.map_o_nulbrk || option.map_i_msb2lsb || option.map_o_ign_cr) 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", tio_printf(" Mappings:%s%s%s%s%s%s%s%s%s%s%s%s%s",
option.map_i_cr_nl ? " ICRNL" : "", option.map_i_cr_nl ? " ICRNL" : "",
option.map_ign_cr ? " IGNCR" : "", option.map_ign_cr ? " IGNCR" : "",
option.map_i_ff_escc ? " IFFESCC" : "", option.map_i_ff_escc ? " IFFESCC" : "",
option.map_i_nl_cr ? " INLCR" : "", option.map_i_nl_cr ? " INLCR" : "",
option.map_i_nl_crnl ? " INLCRNL" : "", option.map_i_nl_crnl ? " INLCRNL" : "",
option.map_i_cr_crnl ? " ICRCRNL" : "",
option.map_i_msb2lsb ? " IMSB2LSB" : "", option.map_i_msb2lsb ? " IMSB2LSB" : "",
option.map_o_cr_nl ? " OCRNL" : "", option.map_o_cr_nl ? " OCRNL" : "",
option.map_o_del_bs ? " ODELBS" : "", option.map_o_del_bs ? " ODELBS" : "",
@ -711,9 +724,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward)
tio_printf_raw("Enter file name: "); tio_printf_raw("Enter file name: ");
if (tio_readln()) if (tio_readln())
{ {
int ret;
tio_printf("Sending file '%s' ", line); tio_printf("Sending file '%s' ", line);
tio_printf("Press any key to abort transfer"); tio_printf("Press any key to abort transfer");
tio_printf("%s", xymodem_send(device_fd, line, XMODEM_1K) < 0 ? "Aborted" : "Done"); ret = xymodem_send(device_fd, line, XMODEM_1K);
tio_printf("%s", ret < 0 ? "Aborted" : "Done");
} }
break; break;
@ -722,9 +738,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward)
tio_printf_raw("Enter file name: "); tio_printf_raw("Enter file name: ");
if (tio_readln()) if (tio_readln())
{ {
int ret;
tio_printf("Sending file '%s' ", line); tio_printf("Sending file '%s' ", line);
tio_printf("Press any key to abort transfer"); tio_printf("Press any key to abort transfer");
tio_printf("%s", xymodem_send(device_fd, line, XMODEM_CRC) < 0 ? "Aborted" : "Done"); ret = xymodem_send(device_fd, line, XMODEM_CRC);
tio_printf("%s", ret < 0 ? "Aborted" : "Done");
} }
break; break;
@ -733,9 +752,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward)
tio_printf_raw("Enter file name: "); tio_printf_raw("Enter file name: ");
if (tio_readln()) if (tio_readln())
{ {
int ret;
tio_printf("Ready to receiving file '%s' ", line); tio_printf("Ready to receiving file '%s' ", line);
tio_printf("Press any key to abort transfer"); tio_printf("Press any key to abort transfer");
tio_printf("%s", xymodem_receive(device_fd, line, XMODEM_CRC) < 0 ? "Aborted" : "Done"); ret = xymodem_send(device_fd, line, XMODEM_CRC);
tio_printf("%s", ret < 0 ? "Aborted" : "Done");
} }
break; break;
@ -772,30 +794,34 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward)
tio_printf("INLCRNL is %s", option.map_i_nl_crnl ? "set" : "unset"); tio_printf("INLCRNL is %s", option.map_i_nl_crnl ? "set" : "unset");
break; break;
case KEY_5: 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; option.map_i_msb2lsb = !option.map_i_msb2lsb;
tio_printf("IMSB2LSB is %s", option.map_i_msb2lsb ? "set" : "unset"); tio_printf("IMSB2LSB is %s", option.map_i_msb2lsb ? "set" : "unset");
break; break;
case KEY_6: case KEY_7:
option.map_o_cr_nl = !option.map_o_cr_nl; option.map_o_cr_nl = !option.map_o_cr_nl;
tio_printf("OCRNL is %s", option.map_o_cr_nl ? "set" : "unset"); tio_printf("OCRNL is %s", option.map_o_cr_nl ? "set" : "unset");
break; break;
case KEY_7: case KEY_8:
option.map_o_del_bs = !option.map_o_del_bs; option.map_o_del_bs = !option.map_o_del_bs;
tio_printf("ODELBS is %s", option.map_o_del_bs ? "set" : "unset"); tio_printf("ODELBS is %s", option.map_o_del_bs ? "set" : "unset");
break; break;
case KEY_8: case KEY_9:
option.map_o_nl_crnl = !option.map_o_nl_crnl; option.map_o_nl_crnl = !option.map_o_nl_crnl;
tio_printf("ONLCRNL is %s", option.map_o_nl_crnl ? "set" : "unset"); tio_printf("ONLCRNL is %s", option.map_o_nl_crnl ? "set" : "unset");
break; break;
case KEY_9: case KEY_A:
option.map_o_ltu = !option.map_o_ltu; option.map_o_ltu = !option.map_o_ltu;
tio_printf("OLTU is %s", option.map_o_ltu ? "set" : "unset"); tio_printf("OLTU is %s", option.map_o_ltu ? "set" : "unset");
break; break;
case KEY_A: case KEY_B:
option.map_o_nulbrk = !option.map_o_nulbrk; option.map_o_nulbrk = !option.map_o_nulbrk;
tio_printf("ONULBRK is %s", option.map_o_nulbrk ? "set" : "unset"); tio_printf("ONULBRK is %s", option.map_o_nulbrk ? "set" : "unset");
break; break;
case KEY_B: case KEY_C:
option.map_o_ign_cr = !option.map_o_ign_cr; option.map_o_ign_cr = !option.map_o_ign_cr;
tio_printf("OIGNCR is %s", option.map_o_ign_cr ? "set" : "unset"); tio_printf("OIGNCR is %s", option.map_o_ign_cr ? "set" : "unset");
break; break;
@ -996,20 +1022,22 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward)
tio_printf(" (3) INLCR: %s mapping NL to CR on input", tio_printf(" (3) INLCR: %s mapping NL to CR on input",
option.map_i_nl_cr ? "Unset" : "Set"); option.map_i_nl_cr ? "Unset" : "Set");
tio_printf(" (4) INLCRNL: %s mapping NL to CR-NL on input", tio_printf(" (4) INLCRNL: %s mapping NL to CR-NL on input",
option.map_i_nl_cr ? "Unset" : "Set"); option.map_i_nl_crnl ? "Unset" : "Set");
tio_printf(" (5) IMSB2LSB: %s mapping MSB bit order to LSB on input", 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"); option.map_i_msb2lsb ? "Unset" : "Set");
tio_printf(" (6) OCRNL: %s mapping CR to NL on output", tio_printf(" (7) OCRNL: %s mapping CR to NL on output",
option.map_o_cr_nl ? "Unset" : "Set"); option.map_o_cr_nl ? "Unset" : "Set");
tio_printf(" (7) ODELBS: %s mapping DEL to BS on output", tio_printf(" (8) ODELBS: %s mapping DEL to BS on output",
option.map_o_del_bs ? "Unset" : "Set"); option.map_o_del_bs ? "Unset" : "Set");
tio_printf(" (8) ONLCRNL: %s mapping NL to CR-NL on output", tio_printf(" (9) ONLCRNL: %s mapping NL to CR-NL on output",
option.map_o_nl_crnl ? "Unset" : "Set"); option.map_o_nl_crnl ? "Unset" : "Set");
tio_printf(" (9) OLTU: %s mapping lowercase to uppercase on output", tio_printf(" (a) OLTU: %s mapping lowercase to uppercase on output",
option.map_o_ltu ? "Unset" : "Set"); option.map_o_ltu ? "Unset" : "Set");
tio_printf(" (a) ONULBRK: %s mapping NUL to send break signal on output", tio_printf(" (b) ONULBRK: %s mapping NUL to send break signal on output",
option.map_o_nulbrk ? "Unset" : "Set"); option.map_o_nulbrk ? "Unset" : "Set");
tio_printf(" (b) OIGNCR: %s ignoring CR on output", tio_printf(" (c) OIGNCR: %s ignoring CR on output",
option.map_o_ign_cr ? "Unset" : "Set"); option.map_o_ign_cr ? "Unset" : "Set");
// Process next input character as sub command // Process next input character as sub command
@ -1069,6 +1097,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward)
case TIMESTAMP_ISO8601: case TIMESTAMP_ISO8601:
tio_printf("Switched timestamp mode to iso8601"); tio_printf("Switched timestamp mode to iso8601");
break; 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: case TIMESTAMP_END:
option.timestamp = TIMESTAMP_NONE; option.timestamp = TIMESTAMP_NONE;
tio_printf("Switched timestamp mode off"); tio_printf("Switched timestamp mode off");
@ -1077,7 +1111,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward)
break; break;
case KEY_V: case KEY_V:
tio_printf("tio v%s", VERSION); tio_printf("tio %s", VERSION);
break; break;
case KEY_X: case KEY_X:
@ -1093,9 +1127,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward)
tio_printf("Send file with YMODEM"); tio_printf("Send file with YMODEM");
tio_printf_raw("Enter file name: "); tio_printf_raw("Enter file name: ");
if (tio_readln()) { if (tio_readln()) {
int ret;
tio_printf("Sending file '%s' ", line); tio_printf("Sending file '%s' ", line);
tio_printf("Press any key to abort transfer"); tio_printf("Press any key to abort transfer");
tio_printf("%s", xymodem_send(device_fd, line, YMODEM) < 0 ? "Aborted" : "Done"); ret = xymodem_send(device_fd, line, YMODEM);
tio_printf("%s", ret < 0 ? "Aborted" : "Done");
} }
break; break;
@ -1167,10 +1204,6 @@ void stdout_configure(void)
{ {
int status; 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 */ /* Save current stdout settings */
if (tcgetattr(STDOUT_FILENO, &stdout_old) < 0) if (tcgetattr(STDOUT_FILENO, &stdout_old) < 0)
{ {
@ -1407,6 +1440,14 @@ static bool is_serial_device(const char *format, ...)
return false; 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); fd = open(filename, O_RDONLY | O_NONBLOCK | O_NOCTTY);
if (fd == -1) if (fd == -1)
{ {
@ -1590,6 +1631,7 @@ const char* get_serial_port_type(const char* port_name)
const char* get_serial_port_type(const char* port_name) const char* get_serial_port_type(const char* port_name)
{ {
(void)port_name;
return ""; return "";
} }
@ -1619,6 +1661,9 @@ static void search_reset(void)
// Indicate an empty list // Indicate an empty list
device_list = NULL; device_list = NULL;
// Reset max device name length
listing_device_name_length_max = 0;
} }
#if defined(__linux__) #if defined(__linux__)
@ -1708,7 +1753,8 @@ GList *tty_search_for_serial_devices(void)
// Hash remaining string to get unique topology ID // Hash remaining string to get unique topology ID
unsigned long hash = djb2_hash((const unsigned char *)devices_path); unsigned long hash = djb2_hash((const unsigned char *)devices_path);
char *tid = base62_encode(hash); char tid[5];
base62_encode(hash, tid);
free(devices_path); free(devices_path);
// Construct the path to the device's driver symlink // Construct the path to the device's driver symlink
@ -1739,18 +1785,22 @@ GList *tty_search_for_serial_devices(void)
creation_time = fs_get_creation_time(path); creation_time = fs_get_creation_time(path);
double uptime = current_time - creation_time; double uptime = current_time - creation_time;
// Read sysfs files to get best possible description of the driver // Read sysfs files to get best possible description
char description[50] = {}; char description[50] = {};
length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/interface", entry->d_name); length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../product", entry->d_name);
if (length == -1)
{
length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../interface", entry->d_name);
}
if (length == -1) if (length == -1)
{ {
length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../../product", entry->d_name); length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../../product", entry->d_name);
} }
if (length == -1) 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)); snprintf(description, sizeof(description), "%s", get_serial_port_type(path));
} }
@ -1785,6 +1835,12 @@ GList *tty_search_for_serial_devices(void)
// Add device information to device list // Add device information to device list
device_list = g_list_append(device_list, device); 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) if (g_list_length(device_list) == 0)
@ -1801,6 +1857,226 @@ GList *tty_search_for_serial_devices(void)
return device_list; 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 #else
GList *tty_search_for_serial_devices(void) GList *tty_search_for_serial_devices(void)
@ -1858,13 +2134,19 @@ GList *tty_search_for_serial_devices(void)
// Fill in device information // Fill in device information
device->path = g_strdup(path); device->path = g_strdup(path);
device->tid = ""; device->tid = g_strdup("");
device->uptime = uptime; device->uptime = uptime;
device->driver = ""; device->driver = g_strdup("");
device->description = ""; device->description = g_strdup("");
// Add device information to device list // Add device information to device list
device_list = g_list_append(device_list, device); 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) if (g_list_length(device_list) == 0)
@ -1889,8 +2171,14 @@ void list_serial_devices(void)
if (g_list_length(device_list) > 0) if (g_list_length(device_list) > 0)
{ {
printf("Device TID Uptime [s] Driver Description\n"); if (listing_device_name_length_max < 17)
printf("----------------- ---- ------------- ---------------- --------------------------\n"); {
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 // Iterate through the device list
GList *iter; GList *iter;
@ -1899,7 +2187,8 @@ void list_serial_devices(void)
device_t *device = (device_t *) iter->data; device_t *device = (device_t *) iter->data;
// Print device information // Print device information
printf("%-17s %4s %13.3f %-16s %s\n", device->path, device->tid, device->uptime, device->driver, device->description); print_padded(device->path, listing_device_name_length_max, ' ');
printf(" %4s %13.3f %-16s %s\n", device->tid, device->uptime, device->driver, device->description);
} }
printf("\n"); printf("\n");
} }
@ -2068,8 +2357,21 @@ void tty_wait_for_device(void)
} }
else if (status == -1) 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)); tio_error_printf("select() failed (%s)", strerror(errno));
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
#endif
} }
} }
@ -2082,7 +2384,7 @@ void tty_wait_for_device(void)
} }
else if (last_errno != errno) else if (last_errno != errno)
{ {
tio_warning_printf("Could not open tty device (%s)", strerror(errno)); tio_warning_printf("Could not open %s (%s)", device_name, strerror(errno));
tio_printf("Waiting for tty device.."); tio_printf("Waiting for tty device..");
last_errno = errno; last_errno = errno;
} }
@ -2201,11 +2503,17 @@ void forward_to_tty(int fd, char output_char)
{ {
handle_hex_prompt(output_char); handle_hex_prompt(output_char);
} }
else else if (option.input_mode == INPUT_MODE_NORMAL)
{ {
if (option.input_mode == INPUT_MODE_NORMAL) status = tty_write(device_fd, &output_char, 1);
if (status < 0)
{
tio_warning_printf("Could not write to tty device");
}
else
{ {
optional_local_echo(output_char); optional_local_echo(output_char);
tx_total++;
} }
} }
break; break;
@ -2533,6 +2841,15 @@ int tty_connect(void)
do_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)) else if ((input_char == '\f') && (option.map_i_ff_escc) && (!option.map_i_msb2lsb))
{ {
printchar('\e'); printchar('\e');
@ -2612,22 +2929,21 @@ int tty_connect(void)
break; break;
case INPUT_MODE_LINE: case INPUT_MODE_LINE:
switch (input_char) if (input_char == '\r')
{ {
case '\r': // Carriage return // Carriage return
readline_input(input_char); readline_input(input_char);
// Write current line to tty device // Write current line to tty device
char *line = readline_get(); char *rl_line = readline_get();
tty_write(device_fd, line, strlen(line)); tty_write(device_fd, rl_line, strlen(rl_line));
tty_sync(device_fd);
break;
default:
readline_input(input_char);
forward = false;
break;
} }
else
{
readline_input(input_char);
forward = false;
}
break;
default: default:
break; break;
@ -2694,4 +3010,3 @@ error_read:
error_open: error_open:
return TIO_ERROR; return TIO_ERROR;
} }

3
src/version.h.in Normal file
View file

@ -0,0 +1,3 @@
#pragma once
#define VERSION "@VERSION@"