Compare commits

...

19 commits
v3.9 ... 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
22 changed files with 676 additions and 503 deletions

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,7 +107,7 @@ 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}}"

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

View file

@ -64,5 +64,7 @@ Keith Hill <k_hill@unitronlp.com>
Lubov66 <radolevanja@gmail.com> Lubov66 <radolevanja@gmail.com>
V <v@anomalous.eu> V <v@anomalous.eu>
Samuel Holland <samuel@sholland.org> 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.

104
README.md
View file

@ -288,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
``` ```
@ -365,12 +365,12 @@ color = 11
[svf2] [svf2]
device = /dev/ttyUSB0 device = /dev/ttyUSB0
baudrate = 9600 baudrate = 9600
script = expect("login: "); write("root\n"); expect("Password: "); write("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
@ -395,72 +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.
read(size, timeout) Returns the captures from the pattern or `nil` on 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. #### `tio.read(size, timeout)`
On success, returns read string as second return value. Read up to `size` bytes from serial device. If timeout is 0 or not provided it
will wait forever until data is ready to read.
read_line(timeout) Returns a string up to `size` bytes long on success and `nil` on 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. #### `tio.readline(timeout)`
On success, returns the string that was read as second return value. Read line from serial device. If timeout is 0 or not provided it will wait
Also emits a single timestamp to stdout and log file per options.timestamp forever until data is ready to read.
and options.log.
write(string) Returns a string on success and `nil` on timeout. On timeout a partially read
Write string to serial device. line may be returned as a second return value.
Returns number of bytes written on success or -1 on error. #### `tio.write(string)`
send(file, protocol) Write string to serial device.
Send file using x/y-modem protocol.
Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. Returns the `tio` table.
tty_search() #### `tio.send(file, protocol)`
Search for serial devices.
Returns a table of number indexed tables, one for each serial device Send file using x/y-modem protocol.
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. Protocol can be any of `XMODEM_1K`, `XMODEM_CRC`, `YMODEM`.
set{line=state, ...} #### `tio.ttysearch()`
Set state of one or multiple tty modem lines.
Line can be any of DTR, RTS, CTS, DSR, CD, RI Search for serial devices.
State is high, low, or toggle. 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".
sleep(seconds) Returns `nil` if no serial devices are found.
Sleep for seconds.
msleep(ms) #### `tio.set{line=state, ...}`
Sleep for miliseconds. 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

View file

@ -69,7 +69,7 @@ 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] [buspirate]

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
write(login.username .. "\n") tio.write(login.username .. "\n")
expect("Password:") tio.expect("Password:")
write(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}

View file

@ -1,14 +1,14 @@
read(1000, 6000) -- initial config tio.read(1000, 6000) -- initial config
write("\n") tio.write("\n")
msleep(100) tio.msleep(100)
read(650, 60) -- main menu tio.read(650, 60) -- main menu
write("S") -- S menu tio.write("S") -- S menu
msleep(30) tio.msleep(30)
read(650, 60) tio.read(650, 60)
write("t") -- Parallel Value Table tio.write("t") -- Parallel Value Table
read(650, 60) tio.read(650, 60)
while true do while true do
msleep(1000) tio.msleep(1000)
write("t") tio.write("t")
read(650, 50) -- repeat PVT forever tio.read(650, 50) -- repeat PVT forever
end end

View file

@ -1,17 +1,15 @@
read(1000, 8000) -- read initial config tio.read(1000, 8000) -- read initial config
write("\n") tio.write("\n")
read(650, 100) -- main menu tio.read(650, 100) -- main menu
write("S") -- S menu tio.write("S") -- S menu
n = 1 repeat
while n > 0 do -- while not empty, read more str = tio.readline(25)
n, str = read_line(25) until str == nil
end
while true do while true do
write("t") -- query PV table tio.write("t") -- query PV table
msleep(880) tio.msleep(880)
n = 1 repeat
while n > 0 do -- while not empty, read more str = tio.readline(60)
n, str = read_line(60) tio.msleep(60)
msleep(60) until str == nil
end
end 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

@ -150,6 +150,8 @@ Set timestamp format to any of the following timestamp formats:
ISO8601 format ("YYYY-MM-DDThh:mm:ss.sss") ISO8601 format ("YYYY-MM-DDThh:mm:ss.sss")
.IP "\fBepoch" .IP "\fBepoch"
Seconds since Unix epoch (1970-01-01) 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
@ -218,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"
@ -367,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
@ -429,49 +437,40 @@ Send ctrl-t character
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 standard Lua API tio makes the following functions In addition to the standard Lua API tio makes the following functions
available: 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 '\e\e'.
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 "\fBread(size, timeout)" Returns a string up to size bytes long on success and nil on timeout.
Read from serial device. If timeout is 0 or not provided it will wait forever
until data is ready to read.
Returns number of bytes read on success, 0 on timeout, or -1 on error. .IP "\fBtio.readline(timeout)"
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.
.IP "\fBread_line(timeout)"
Read line from serial device. If timeout is 0 or not provided it will wait Read line from serial device. If timeout is 0 or not provided it will wait
forever until data is ready to read. forever until data is ready to read.
Returns number of bytes read on success, 0 on timeout, or -1 on error. Returns a string on success and nil on timeout. On timeout a partially read
line may be returned as a second return value.
On success, returns the string that was read as second return value. Also .IP "\fBtio.write(string)"
emits a single timestamp to stdout and log file per options.timestamp
and options.log.
.IP "\fBwrite(string)"
Write string to serial device. Write string to serial device.
Returns number of bytes written on success or -1 on error. Returns the tio table.
.IP "\fBsend(file, protocol)" .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.
@ -481,19 +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 "\fBset{line=state, ...}" .IP "\fBtio.set{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
@ -722,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: "); write("root\\n"); expect("Password: "); write("root\\n")' /dev/ttyUSB0 $ tio --script 'tio.expect("login: "); tio.write("root\\n"); tio.expect("Password: "); tio.write("root\\n")' /dev/ttyUSB0
.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:
@ -743,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
@ -764,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

@ -116,6 +116,8 @@ OPTIONS
epoch Seconds since Unix epoch (1970-01-01) 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>
@ -168,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
@ -285,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.
@ -353,7 +361,7 @@ 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.
read(size, timeout) read(size, timeout)
Read from serial device. If timeout is 0 or not provided it will wait forever until data is ready to read. Read up to size bytes from serial device. If timeout is 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. Returns number of bytes read on success, 0 on timeout, or -1 on error.

View file

@ -1,6 +1,6 @@
project('tio', 'c', project('tio', 'c',
version : '3.9', 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' ]
) )

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

@ -207,7 +207,7 @@ static void config_parse_keys(GKeyFile *key_file, char *group)
config_get_bool(key_file, group, "timestamp", (bool*) &option.timestamp); config_get_bool(key_file, group, "timestamp", (bool*) &option.timestamp);
if (option.timestamp != TIMESTAMP_NONE) if (option.timestamp != TIMESTAMP_NONE)
{ {
config_get_string(key_file, group, "timestamp-format", &string, "24hour", "24hour-start", "24hour-delta", "iso8601", "epoch", NULL); config_get_string(key_file, group, "timestamp-format", &string, "24hour", "24hour-start", "24hour-delta", "iso8601", "epoch", "epoch-usec", NULL);
if (string != NULL) if (string != NULL)
{ {
option_parse_timestamp(string, &option.timestamp); option_parse_timestamp(string, &option.timestamp);

View file

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

@ -116,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,
@ -170,6 +171,7 @@ 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");
@ -399,6 +401,10 @@ const char* option_timestamp_format_to_string(timestamp_t timestamp)
return "epoch"; return "epoch";
break; break;
case TIMESTAMP_EPOCH_USEC:
return "epoch-usec";
break;
default: default:
return "unknown"; return "unknown";
break; break;
@ -429,6 +435,10 @@ void option_parse_timestamp(const char *arg, timestamp_t *timestamp)
{ {
*timestamp = TIMESTAMP_EPOCH; *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);
@ -770,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;

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

@ -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>
@ -45,23 +44,87 @@
#define READ_LINE_SIZE 4096 // read_line buffer length #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);
@ -77,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;
@ -94,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] = { };
@ -148,11 +211,11 @@ 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; int ret;
if (file == NULL) if (file == NULL)
@ -184,307 +247,118 @@ static int modem_send(lua_State *L)
return 0; return 0;
} }
// lua: send(string) // lua: tio.write(string)
static int write_(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));
len -= ret;
string += ret;
} while (len > 0 && --attempts);
if (len > 0)
return luaL_error(L, "partial write");
ret = write(device_fd, string, strlen(string));
fsync(device_fd); // flush these characters now fsync(device_fd); // flush these characters now
tcdrain(device_fd); //ensure we flushed characters to our device tcdrain(device_fd); //ensure we flushed characters to our device
lua_pushnumber(L, ret); 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 (!c) int size = luaL_checkinteger(L, 1);
{
return;
}
if (buffer_size < MAX_BUFFER_SIZE)
{
circular_buffer[buffer_size++] = c;
}
else
{
// Shift the buffer to accommodate the new character
memmove(circular_buffer, circular_buffer + 1, MAX_BUFFER_SIZE - 1);
circular_buffer[MAX_BUFFER_SIZE - 1] = c;
}
}
// Function to match against the circular expect buffer using regex
static bool match_regex(regex_t *regex)
{
char buffer[MAX_BUFFER_SIZE + 1]; // Temporary buffer for regex matching
const char *s = circular_buffer;
regmatch_t pmatch[1];
regoff_t len;
memcpy(buffer, circular_buffer, buffer_size);
buffer[buffer_size] = '\0'; // Null-terminate the buffer
// Match against the regex
int ret = regexec(regex, buffer, 1, pmatch, 0);
if (!ret)
{
// Match found
len = pmatch[0].rm_eo - pmatch[0].rm_so;
memcpy(match_string, s + pmatch[0].rm_so, len);
match_string[len] = '\0';
return true;
}
else if (ret == REG_NOMATCH)
{
// No match found, do nothing
}
else
{
// Error occurred during matching
tio_error_print("Regex match failed");
}
return false;
}
// Function to echo a buffer to stdout and to the log
// per the option.timestamp and option.log settings
static void echo_buffer(char buffer[], ssize_t len)
{
if (option.timestamp)
{
char *pTimeStampNow;
pTimeStampNow = timestamp_current_time();
if (pTimeStampNow)
{
tio_printf("%s", buffer); //does timestamps for us
if (option.log)
{
log_printf("\n[%s] %s", pTimeStampNow, buffer);
}
}
} else {
for (ssize_t i=0; i<len; i++)
{
putchar(buffer[i]);
if (option.log)
{
log_putc(buffer[i]);
}
}
}
}
// lua: ret,string = read(size, timeout)
static int read_string(lua_State *L)
{
int size = lua_tointeger(L, 1) + 1; //plus one for null-terminated string
int timeout = lua_tointeger(L, 2); int timeout = lua_tointeger(L, 2);
int ret = 0;
char *buffer = calloc(1, size);
if (buffer == NULL)
{
ret = -1; // Error
goto error_rs;
}
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_rs; lua_pop(L, 1);
} lua_pushnil(L);
else if (bytes_read == 0)
{
ret = 0; // Timeout
goto error_rs;
} }
else else
{ {
buffer[bytes_read] = (char)0; maybe_echo(L);
} }
echo_buffer(&buffer[0], bytes_read); return 1;
ret = bytes_read;
error_rs:
lua_pushnumber(L, ret);
if (buffer != NULL)
{
lua_pushstring(L, ret > 0 ? buffer : "");
free(buffer);
}
else
{
lua_pushstring(L, ""); // give empty string to caller
}
return 2;
} }
// lua: ret,string = read_line(timeout) // lua: string = tio.readline(timeout)
static int read_line(lua_State *L) static int api_readline(lua_State *L) {
{
static char linebuf[READ_LINE_SIZE];
int timeout = lua_tointeger(L, 1); //ms int timeout = lua_tointeger(L, 1); //ms
int ret = 0; luaL_Buffer b;
int read_result = 1; //enable reading input from device
char ch; char ch;
int bytes_read = 0;
linebuf[0] = '\0';
if (timeout == 0) if (timeout == 0)
{ {
timeout = -1; // Wait forever timeout = -1; // Wait forever
} }
// loop reading input until a newline seen or timeout luaL_buffinit(L, &b);
while (true) luaL_prepbuffer(&b);
{ while (true) {
read_result = read_poll(device_fd, &ch, 1, timeout); int ret = read_poll(device_fd, &ch, 1, timeout);
if (read_result < 0)
{
ret = -1; // Error
linebuf[bytes_read] = '\0';
goto error_rl;
}
else if (!read_result)
{
// Timeout returns a non-empty linebuf as a 'line'
ret = bytes_read;
linebuf[bytes_read] = '\0';
break;
}
else // we got a character, so handle it
{
if (ch == '\n')
{
linebuf[bytes_read] = '\0';
break;
}
else if (bytes_read <= (READ_LINE_SIZE-2))
{
if (isprint(ch)) // store all printable chars
{
linebuf[bytes_read++] = ch;
}
}
}
}
if (bytes_read) if (ret < 0)
{ return luaL_error(L, "%s", strerror(errno));
echo_buffer(linebuf, bytes_read);
}
ret = bytes_read;
error_rl: if (ret == 0)
lua_pushnumber(L, ret); {
lua_pushstring(L, linebuf); luaL_pushresult(&b);
return 2; maybe_echo(L);
lua_pushnil(L);
lua_insert(L, -2);
return 2;
}
if (ch == '\n')
{
luaL_pushresult(&b);
maybe_echo(L);
return 1;
}
luaL_addchar(&b, ch);
}
} }
// lua: expect(string, timeout) // lua: table = tio.ttysearch()
static int expect(lua_State *L) static int api_ttysearch(lua_State *L)
{
const char *string = lua_tostring(L, 1);
long timeout = lua_tointeger(L, 2);
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)
{
// Let poll() wait forever
timeout = -1;
}
// Compile the regular expression
ret = regcomp(&regex, string, REG_EXTENDED);
if (ret)
{
tio_error_print("Could not compile regex");
ret = -1;
goto error;
}
// Main loop to read and match
while (true)
{
ssize_t bytes_read = read_poll(device_fd, &c, 1, timeout);
if (bytes_read > 0)
{
putchar(c);
expect_buffer_add(c);
if (option.log)
{
log_putc(c);
}
// Match against the entire buffer
if (match_regex(&regex))
{
ret = 1;
break;
}
}
else
{
// Timeout or error
break;
}
}
// Cleanup
regfree(&regex);
error:
lua_pushnumber(L, ret);
lua_pushstring(L, match_string);
return 2;
}
// lua: exit(code)
static int exit_(lua_State *L)
{
long code = lua_tointeger(L, 1);
exit(code);
return 0;
}
// lua: list = tty_search()
static int tty_search_(lua_State *L)
{ {
UNUSED(L); UNUSED(L);
GList *iter; GList *iter;
@ -550,66 +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},
{ "send", modem_send},
{ "write", write_},
{ "read", read_string},
{ "read_line", read_line},
{ "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)
{ {
@ -625,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);
@ -641,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;
@ -650,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

@ -75,6 +75,7 @@ char *timestamp_current_time(void)
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:
case TIMESTAMP_EPOCH_USEC:
// "N.sss" (seconds since Unix epoch, 1970-01-01 00:00:00Z) // "N.sss" (seconds since Unix epoch, 1970-01-01 00:00:00Z)
tv = tv_now; tv = tv_now;
tm = localtime(&tv.tv_sec); tm = localtime(&tv.tv_sec);
@ -84,12 +85,18 @@ char *timestamp_current_time(void)
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

@ -29,6 +29,7 @@ typedef enum
TIMESTAMP_24HOUR_DELTA, TIMESTAMP_24HOUR_DELTA,
TIMESTAMP_ISO8601, TIMESTAMP_ISO8601,
TIMESTAMP_EPOCH, TIMESTAMP_EPOCH,
TIMESTAMP_EPOCH_USEC,
TIMESTAMP_END, TIMESTAMP_END,
} timestamp_t; } timestamp_t;

315
src/tty.c
View file

@ -22,6 +22,15 @@
#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 "version.h"
#include "config.h" #include "config.h"
#include <stdarg.h> #include <stdarg.h>
@ -623,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" : "",
@ -783,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;
@ -1007,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
@ -1083,6 +1100,9 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward)
case TIMESTAMP_EPOCH: case TIMESTAMP_EPOCH:
tio_printf("Switched timestamp mode to epoch"); tio_printf("Switched timestamp mode to epoch");
break; 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");
@ -1765,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));
} }
@ -1833,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)
@ -2113,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
} }
} }
@ -2584,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');
@ -2744,4 +3010,3 @@ error_read:
error_open: error_open:
return TIO_ERROR; return TIO_ERROR;
} }