diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b96d489..8a195f4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners # Consider using larger runners for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-20.04' }} + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-22.04' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: actions: read @@ -51,7 +51,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) #- name: Autobuild - # uses: github/codeql-action/autobuild@v2 + # uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -78,7 +78,7 @@ jobs: ./.github/workflows/codeql-buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" upload: false @@ -107,7 +107,7 @@ jobs: output: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif - name: Upload CodeQL results to code scanning - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ${{ steps.step1.outputs.sarif-output }} category: "/language:${{matrix.language}}" diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index a72e3d3..90e1a93 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -21,7 +21,8 @@ jobs: - name: Install dependencies 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 run: | diff --git a/AUTHORS b/AUTHORS index 21f2bad..3754640 100644 --- a/AUTHORS +++ b/AUTHORS @@ -64,5 +64,7 @@ Keith Hill Lubov66 V Samuel Holland +David Ordnung + Thanks to everyone who has contributed to this project. diff --git a/README.md b/README.md index f2c2806..e3ab066 100644 --- a/README.md +++ b/README.md @@ -288,12 +288,12 @@ $ cat data.bin | tio /dev/ttyUSB0 Manipulate modem lines on connect: ``` -$ tio --script "set{DTR=high,RTS=low}; msleep(100); set{DTR=toggle,RTS=toggle}" /dev/ttyUSB0 +$ tio --script "tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{DTR=toggle,RTS=toggle}" /dev/ttyUSB0 ``` Pipe command to serial device and wait for line response within 1 second: ``` -$ echo "*IDN?" | tio /dev/ttyACM0 --script "expect('\r\n', 1000)" --mute +$ echo "*IDN?" | tio /dev/ttyACM0 --script "tio.expect('\r\n', 1000)" --mute KORAD KD3305P V4.2 SN:32475045 ``` @@ -365,12 +365,12 @@ color = 11 [svf2] device = /dev/ttyUSB0 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 [esp32] device = /dev/serial/by-id/usb-0403_6014-if00-port0 -script = set{DTR=high,RTS=low}; msleep(100); set{DTR=low,RTS=high}; msleep(100); set{RTS=low} +script = tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{DTR=low,RTS=high}; tio.msleep(100); tio.set{RTS=low} script-run = once color = 13 @@ -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. -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) - Read from serial device. If timeout is 0 or not provided it will wait - forever until data is ready to read. +Returns the captures from the pattern or `nil` on timeout. - 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) - Read line from serial device. If timeout is 0 or not provided it will - wait forever until data is ready to read. +Returns a string up to `size` bytes long on success and `nil` on timeout. - 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. - Also emits a single timestamp to stdout and log file per options.timestamp - and options.log. +Read line from serial device. If timeout is 0 or not provided it will wait +forever until data is ready to read. -write(string) - Write string to serial device. +Returns a string on success and `nil` on timeout. On timeout a partially read +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) - Send file using x/y-modem protocol. +Write string to serial device. - Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. +Returns the `tio` table. -tty_search() - Search for serial devices. +#### `tio.send(file, protocol)` - 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". +Send file using x/y-modem protocol. - Returns nil if no serial devices are found. +Protocol can be any of `XMODEM_1K`, `XMODEM_CRC`, `YMODEM`. -set{line=state, ...} - Set state of one or multiple tty modem lines. +#### `tio.ttysearch()` - 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) - Sleep for seconds. +Returns `nil` if no serial devices are found. -msleep(ms) - Sleep for miliseconds. +#### `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 diff --git a/examples/config/config b/examples/config/config index 01e84dd..7bd141e 100644 --- a/examples/config/config +++ b/examples/config/config @@ -69,7 +69,7 @@ color = 13 [esp32] device = /dev/ttyUSB0 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 [buspirate] diff --git a/examples/lua/automatic-linux-login.lua b/examples/lua/automatic-linux-login.lua index 428caa1..aa7a990 100644 --- a/examples/lua/automatic-linux-login.lua +++ b/examples/lua/automatic-linux-login.lua @@ -13,14 +13,13 @@ local logins = { }, } -local found, match_str = expect("\\w+- login:", 10) -if (1 == found) then - local hostname = string.match(match_str, "^%w+") +local hostname = tio.expect("^(%g+) login:", 10) +if hostname then local login = logins[hostname] if (nil ~= login) then - write(login.username .. "\n") - expect("Password:") - write(login.password .. "\n") + tio.write(login.username .. "\n") + tio.expect("Password:") + tio.write(login.password .. "\n") else io.write("\r\nDon't know login info for " .. hostname .. "\r\n") end diff --git a/examples/lua/control-lines-test.lua b/examples/lua/control-lines-test.lua index 5b54ab4..55d98b5 100644 --- a/examples/lua/control-lines-test.lua +++ b/examples/lua/control-lines-test.lua @@ -1,5 +1,5 @@ -set{DTR=high, RTS=low} -msleep(100) -set{DTR=low, RTS=high} -msleep(100) -set{RTS=toggle} +tio.set{DTR=high, RTS=low} +tio.msleep(100) +tio.set{DTR=low, RTS=high} +tio.msleep(100) +tio.set{RTS=toggle} diff --git a/examples/lua/read.lua b/examples/lua/read.lua index 6baa032..6452816 100644 --- a/examples/lua/read.lua +++ b/examples/lua/read.lua @@ -1,14 +1,14 @@ -read(1000, 6000) -- initial config -write("\n") -msleep(100) -read(650, 60) -- main menu -write("S") -- S menu -msleep(30) -read(650, 60) -write("t") -- Parallel Value Table -read(650, 60) +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 - msleep(1000) - write("t") - read(650, 50) -- repeat PVT forever + tio.msleep(1000) + tio.write("t") + tio.read(650, 50) -- repeat PVT forever end diff --git a/examples/lua/read_line.lua b/examples/lua/read_line.lua index ef6ec20..a844b48 100644 --- a/examples/lua/read_line.lua +++ b/examples/lua/read_line.lua @@ -1,17 +1,15 @@ -read(1000, 8000) -- read initial config -write("\n") -read(650, 100) -- main menu -write("S") -- S menu -n = 1 -while n > 0 do -- while not empty, read more - n, str = read_line(25) -end +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 - write("t") -- query PV table - msleep(880) - n = 1 - while n > 0 do -- while not empty, read more - n, str = read_line(60) - msleep(60) - end + tio.write("t") -- query PV table + tio.msleep(880) + repeat + str = tio.readline(60) + tio.msleep(60) + until str == nil end diff --git a/examples/lua/serial-device-search.lua b/examples/lua/serial-device-search.lua index 120d650..77d9dbc 100644 --- a/examples/lua/serial-device-search.lua +++ b/examples/lua/serial-device-search.lua @@ -1,6 +1,6 @@ io.write("Searching... ") -local device = tty_search() +local device = tio.ttysearch() io.write("done\r\n") diff --git a/man/tio.1.in b/man/tio.1.in index 19c36da..eb0338d 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -150,6 +150,8 @@ Set timestamp format to any of the following timestamp formats: ISO8601 format ("YYYY-MM-DDThh:mm:ss.sss") .IP "\fBepoch" Seconds since Unix epoch (1970-01-01) +.IP "\fBepoch-usec" +Seconds since Unix epoch (1970-01-01) with subdivision in microseconds .PP Default format is \fB24hour\fR .RE @@ -218,6 +220,8 @@ Map FF to ESC-c on input Map NL to CR on input .IP "\fBINLCRNL" Map NL to CR-NL on input +.IP "\fBICRCRNL" +Map CR to CR-NL on input .IP "\fBIMSB2LSB" Map MSB bit order to LSB on input .IP "\fBOCRNL" @@ -367,6 +371,10 @@ Default value is "always". Execute shell command with I/O redirected to device +.TP +.BR "\-\-complete-profiles + +Prints profiles (for shell completion) .TP .BR \-v ", " \-\-version @@ -429,49 +437,40 @@ Send ctrl-t character Tio suppots Lua scripting to easily automate interaction with the tty device. In addition to the standard Lua API tio makes the following functions -available: +and variables available: .TP 6n -.IP "\fBexpect(string, timeout)" -Expect string - waits for string to match or timeout before continuing. -Supports regular expressions. Special characters must be escaped with '\e\e'. +.IP "\fBtio.expect(pattern, timeout)" +Waits for the Lua pattern to match or timeout before continuing. Timeout is in milliseconds, defaults to 0 meaning it will wait forever. -Returns 1 on successful match, 0 on timeout, or -1 on error. +Returns the captures from the pattern or nil on timeout. -On successful match it also returns the match string as second return value. +.IP "\fBtio.read(size, timeout)" +Read up to size bytes from serial device. If timeout is 0 or not provided it +will wait forever until data is ready to read. -.IP "\fBread(size, timeout)" -Read from serial device. If timeout is 0 or not provided it will wait forever -until data is ready to read. +Returns a string up to size bytes long on success and nil on timeout. -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. - -.IP "\fBread_line(timeout)" +.IP "\fBtio.readline(timeout)" Read line from serial device. If timeout is 0 or not provided it will wait forever until data is ready to read. -Returns 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 -emits a single timestamp to stdout and log file per options.timestamp -and options.log. - -.IP "\fBwrite(string)" +.IP "\fBtio.write(string)" 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. Protocol can be any of XMODEM_1K, XMODEM_CRC, YMODEM. -.IP "\fBtty_search()" +.IP "\fBtio.ttysearch()" Search for serial devices. Returns a table of number indexed tables, one for each serial device found. @@ -481,19 +480,26 @@ following string indexed elements "path", "tid", "uptime", "driver", 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. Line can be any of DTR, RTS, CTS, DSR, CD, RI State is high, low, or toggle. -.IP "\fBsleep(seconds)" +.IP "\fBtio.sleep(seconds)" Sleep for seconds. -.IP "\fBmsleep(ms)" +.IP "\fBtio.msleep(ms)" 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" .PP @@ -722,7 +728,7 @@ expect -i $uart "prompt> " .TP 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 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 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 @@ -764,7 +770,7 @@ $ tio --rs-485 --rs-485-config=RTS_ON_SEND=1,RX_DURING_TX /dev/ttyUSB0 .TP Manipulate DTR and RTS lines upon first connect to reset connected microcontroller: -$ tio --script "set{DTR=high,RTS=low}; msleep(100); set{RTS=toggle}" --script-run once /dev/ttyUSB0 +$ tio --script "tio.set{DTR=high,RTS=low}; tio.msleep(100); tio.set{RTS=toggle}" --script-run once /dev/ttyUSB0 .SH "WEBSITE" .PP diff --git a/man/tio.1.txt b/man/tio.1.txt index bf44f9d..eeac82c 100644 --- a/man/tio.1.txt +++ b/man/tio.1.txt @@ -116,6 +116,8 @@ OPTIONS epoch Seconds since Unix epoch (1970-01-01) + epoch-usec Seconds since Unix epoch (1970-01-01) with subdivision microseconds + Default format is 24hour --timestamp-timeout @@ -168,6 +170,8 @@ OPTIONS INLCRNL Map NL to CR-NL on input + ICRCRNL Map CR to CR-NL on input + IMSB2LSB Map MSB bit order to LSB on input OCRNL Map CR to NL on output @@ -285,6 +289,10 @@ OPTIONS Execute shell command with I/O redirected to device + --complete-profiles + + Prints profiles (for shell completion) + -v, --version Display program version. @@ -353,7 +361,7 @@ SCRIPT API On successful match it also returns the match string as second return value. 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. diff --git a/meson.build b/meson.build index 05f77ce..b95b5c0 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('tio', 'c', version : '3.9', - license : [ 'GPL-2'], + license : 'GPL-2.0-or-later', meson_version : '>= 0.53.2', default_options : [ 'warning_level=2', 'buildtype=release', 'c_std=gnu99' ] ) diff --git a/src/bash-completion/tio.in b/src/bash-completion/tio.in index d71aceb..b3b61cb 100644 --- a/src/bash-completion/tio.in +++ b/src/bash-completion/tio.in @@ -46,6 +46,7 @@ _tio() --script-file \ --script-run \ --exec \ + --complete-profiles \ -v --version \ -h --help" @@ -85,11 +86,11 @@ _tio() return 0 ;; -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 ;; --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 ;; -c | --color) diff --git a/src/configfile.c b/src/configfile.c index 6330151..ca116bf 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -207,7 +207,7 @@ static void config_parse_keys(GKeyFile *key_file, char *group) config_get_bool(key_file, group, "timestamp", (bool*) &option.timestamp); if (option.timestamp != TIMESTAMP_NONE) { - config_get_string(key_file, group, "timestamp-format", &string, "24hour", "24hour-start", "24hour-delta", "iso8601", "epoch", NULL); + config_get_string(key_file, group, "timestamp-format", &string, "24hour", "24hour-start", "24hour-delta", "iso8601", "epoch", "epoch-usec", NULL); if (string != NULL) { option_parse_timestamp(string, &option.timestamp); diff --git a/src/meson.build b/src/meson.build index 958d4e9..05168f7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -47,7 +47,13 @@ tio_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 tio_c_args += '-DHAVE_TERMIOS2' diff --git a/src/options.c b/src/options.c index 40bcc23..acff7ce 100644 --- a/src/options.c +++ b/src/options.c @@ -116,6 +116,7 @@ struct option_t option = .map_ign_cr = false, .map_i_ff_escc = false, .map_i_nl_crnl = false, + .map_i_cr_crnl = false, .map_o_cr_nl = false, .map_o_nl_crnl = false, .map_o_del_bs = false, @@ -170,6 +171,7 @@ void option_print_help(char *argv[]) printf(" --script-file Run script from file\n"); printf(" --script-run once|always|never Run script on connect (default: always)\n"); printf(" --exec Execute shell command with I/O redirected to device\n"); + printf(" --complete-profiles Prints profiles (for shell completion)\n"); printf(" -v, --version Display version\n"); printf(" -h, --help Display help\n"); printf("\n"); @@ -399,6 +401,10 @@ const char* option_timestamp_format_to_string(timestamp_t timestamp) return "epoch"; break; + case TIMESTAMP_EPOCH_USEC: + return "epoch-usec"; + break; + default: return "unknown"; break; @@ -429,6 +435,10 @@ void option_parse_timestamp(const char *arg, timestamp_t *timestamp) { *timestamp = TIMESTAMP_EPOCH; } + else if (strcmp(arg, "epoch-usec") == 0) + { + *timestamp = TIMESTAMP_EPOCH_USEC; + } else { tio_error_print("Invalid timestamp '%s'", arg); @@ -770,6 +780,10 @@ void option_parse_mappings(const char *map) { option.map_i_nl_crnl = true; } + else if (strcmp(token,"ICRCRNL") == 0) + { + option.map_i_cr_crnl = true; + } else if (strcmp(token, "ONLCRNL") == 0) { option.map_o_nl_crnl = true; diff --git a/src/options.h b/src/options.h index dadf133..c552217 100644 --- a/src/options.h +++ b/src/options.h @@ -98,6 +98,7 @@ struct option_t bool map_ign_cr; bool map_i_ff_escc; bool map_i_nl_crnl; + bool map_i_cr_crnl; bool map_o_cr_nl; bool map_o_nl_crnl; bool map_o_del_bs; diff --git a/src/script.c b/src/script.c index b471932..b69d55b 100644 --- a/src/script.c +++ b/src/script.c @@ -20,7 +20,6 @@ */ #include -#include #include #include #include @@ -45,23 +44,87 @@ #define READ_LINE_SIZE 4096 // read_line buffer length static int device_fd; -static char circular_buffer[MAX_BUFFER_SIZE]; -static char match_string[MAX_BUFFER_SIZE]; -static int buffer_size = 0; static char script_init[] = -"function set(arg)\n" +"tio.set = function(arg)\n" " local dtr = arg.DTR or -1\n" " local rts = arg.RTS or -1\n" " local cts = arg.CTS or -1\n" " local dsr = arg.DSR or -1\n" " local cd = arg.CD or -1\n" " local ri = arg.RI or -1\n" -" line_set(dtr, rts, cts, dsr, cd, ri)\n" -"end\n"; +" tio.line_set(dtr, rts, cts, dsr, cd, ri)\n" +"end\n" +"tio.expect = function(pattern, timeout)\n" +" local str = ''\n" +" while true do\n" +" local c = tio.read(1, timeout)\n" +" if c then\n" +" str = str .. c\n" +" if string.match(str, pattern) then\n" +" return string.match(str, pattern)\n" +" end\n" +" else\n" +" return nil, str\n" +" end\n" +" end\n" +"end\n" +"tio.alwaysecho = true\n" +"setmetatable(tio, tio)\n"; -// lua: sleep(seconds) -static int sleep_(lua_State *L) +static bool alwaysecho(lua_State *L) +{ + bool b; + + lua_getglobal(L, "tio"); + lua_getfield(L, -1, "alwaysecho"); + b = lua_toboolean(L, -1); + lua_pop(L, 2); + + return b; +} + +static int api_echo(lua_State *L) +{ + size_t len = 0; + const char *str = luaL_checklstring(L, 1, &len); + + if (option.timestamp) + { + char *pTimeStampNow = timestamp_current_time(); + if (pTimeStampNow) + { + tio_printf("%s", str); + if (option.log) + { + log_printf("\n[%s] %s", pTimeStampNow, str); + } + } + } else { + for (size_t i=0; i 0 && --attempts); + + if (len > 0) + return luaL_error(L, "partial write"); - ret = write(device_fd, string, strlen(string)); fsync(device_fd); // flush these characters now tcdrain(device_fd); //ensure we flushed characters to our device - lua_pushnumber(L, ret); + lua_getglobal(L, "tio"); return 1; } -// Function to add a character to the circular expect buffer -static void expect_buffer_add(char c) +// lua: tio.read(size, timeout) +static int api_read(lua_State *L) { - if (!c) - { - 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= 502 + char *p = luaL_prepbuffsize(&buffer, size); +#else + if (size > LUAL_BUFFERSIZE) + return luaL_error(L, "buffer overflow, max size is: %d", LUAL_BUFFERSIZE); + char *p = luaL_prepbuffer(&buffer); +#endif + + ssize_t ret = read_poll(device_fd, p, size, timeout); + if (ret < 0) + return luaL_error(L, "%s", strerror(errno)); + + luaL_addsize(&buffer, ret); + luaL_pushresult(&buffer); + + if (ret == 0) { - ret = -1; // Error - goto error_rs; - } - else if (bytes_read == 0) - { - ret = 0; // Timeout - goto error_rs; + // On timeout return nil instead of an empty string + lua_pop(L, 1); + lua_pushnil(L); } else { - buffer[bytes_read] = (char)0; + maybe_echo(L); } - echo_buffer(&buffer[0], bytes_read); - 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; + return 1; } -// lua: ret,string = read_line(timeout) -static int read_line(lua_State *L) -{ - static char linebuf[READ_LINE_SIZE]; +// lua: string = tio.readline(timeout) +static int api_readline(lua_State *L) { int timeout = lua_tointeger(L, 1); //ms - int ret = 0; - int read_result = 1; //enable reading input from device + luaL_Buffer b; char ch; - int bytes_read = 0; - linebuf[0] = '\0'; if (timeout == 0) { timeout = -1; // Wait forever } - // loop reading input until a newline seen or timeout - while (true) - { - read_result = 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; - } - } - } - } + luaL_buffinit(L, &b); + luaL_prepbuffer(&b); + while (true) { + int ret = read_poll(device_fd, &ch, 1, timeout); - if (bytes_read) - { - echo_buffer(linebuf, bytes_read); - } - ret = bytes_read; + if (ret < 0) + return luaL_error(L, "%s", strerror(errno)); -error_rl: - lua_pushnumber(L, ret); - lua_pushstring(L, linebuf); - return 2; + if (ret == 0) + { + luaL_pushresult(&b); + maybe_echo(L); + lua_pushnil(L); + lua_insert(L, -2); + return 2; + } + + if (ch == '\n') + { + luaL_pushresult(&b); + maybe_echo(L); + return 1; + } + + luaL_addchar(&b, ch); + } } -// lua: expect(string, timeout) -static int expect(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(®ex, 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(®ex)) - { - ret = 1; - break; - } - } - else - { - // Timeout or error - break; - } - } - - // Cleanup - regfree(®ex); - -error: - lua_pushnumber(L, ret); - lua_pushstring(L, match_string); - return 2; -} - -// lua: exit(code) -static int exit_(lua_State *L) -{ - long code = lua_tointeger(L, 1); - - exit(code); - - return 0; -} - -// lua: list = tty_search() -static int tty_search_(lua_State *L) +// lua: table = tio.ttysearch() +static int api_ttysearch(lua_State *L) { UNUSED(L); GList *iter; @@ -550,66 +424,7 @@ static void script_buffer_run(lua_State *L, const char *script_buffer) } } -static const struct luaL_Reg tio_lib[] = -{ - { "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) +static void script_file_run(lua_State *L, const char *filename) { 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_setglobal(L, name); } -void script_set_globals(lua_State *L) +static void script_set_globals(lua_State *L) { script_set_global(L, "toggle", 2); script_set_global(L, "high", 1); @@ -641,6 +482,14 @@ void script_set_globals(lua_State *L) script_set_global(L, "YMODEM", YMODEM); } +#if LUA_VERSION_NUM >= 502 +static int luaopen_tio(lua_State *L) +{ + luaL_newlib(L, tio_lib); + return 1; +} +#endif + void script_run(int fd, const char *script_filename) { lua_State *L; @@ -650,8 +499,15 @@ void script_run(int fd, const char *script_filename) L = luaL_newstate(); luaL_openlibs(L); - // Bind tio functions - lua_register_tio(L); +#if LUA_VERSION_NUM >= 502 + luaL_requiref(L, "tio", luaopen_tio, 1); +#else + luaL_register(L, "tio", tio_lib); +#endif + lua_pop(L, 1); + + // Load lua init script + script_load(L); // Initialize globals script_set_globals(L); diff --git a/src/timestamp.c b/src/timestamp.c index b5ec306..9273758 100644 --- a/src/timestamp.c +++ b/src/timestamp.c @@ -75,6 +75,7 @@ char *timestamp_current_time(void) len = strftime(time_string, sizeof(time_string), "%Y-%m-%dT%H:%M:%S", tm); break; case TIMESTAMP_EPOCH: + case TIMESTAMP_EPOCH_USEC: // "N.sss" (seconds since Unix epoch, 1970-01-01 00:00:00Z) tv = tv_now; tm = localtime(&tv.tv_sec); @@ -84,12 +85,18 @@ char *timestamp_current_time(void) return NULL; } - // Append milliseconds to all timestamps + // Append millis-/microseconds to all timestamps if (len) { - len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%03ld", (long)tv.tv_usec / 1000); + if ( option.timestamp == TIMESTAMP_EPOCH_USEC ) + { + len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%06ld", (long)tv.tv_usec); + } + else + { + len = snprintf(time_string + len, TIME_STRING_SIZE_MAX - len, ".%03ld", (long)tv.tv_usec / 1000); + } } - // Save previous time value for next run tv_previous = tv_now; diff --git a/src/timestamp.h b/src/timestamp.h index 6a10d98..0544544 100644 --- a/src/timestamp.h +++ b/src/timestamp.h @@ -29,6 +29,7 @@ typedef enum TIMESTAMP_24HOUR_DELTA, TIMESTAMP_ISO8601, TIMESTAMP_EPOCH, + TIMESTAMP_EPOCH_USEC, TIMESTAMP_END, } timestamp_t; diff --git a/src/tty.c b/src/tty.c index c72fd67..efda859 100644 --- a/src/tty.c +++ b/src/tty.c @@ -22,6 +22,15 @@ #if defined(__linux__) #include #endif + +#if defined(__APPLE__) || defined(__MACH__) +#include +#include +#include +#include +#include +#endif + #include "version.h" #include "config.h" #include @@ -623,16 +632,18 @@ void tty_output_mode_set(output_mode_t mode) static void mappings_print(void) { if (option.map_i_cr_nl || option.map_ign_cr || option.map_i_ff_escc || - option.map_i_nl_cr || option.map_i_nl_crnl || option.map_o_cr_nl || - option.map_o_del_bs || option.map_o_nl_crnl || option.map_o_ltu || - option.map_o_nulbrk || option.map_i_msb2lsb || option.map_o_ign_cr) + option.map_i_nl_cr || option.map_i_nl_crnl || option.map_i_cr_crnl || + option.map_o_cr_nl || option.map_o_del_bs || option.map_o_nl_crnl || + option.map_o_ltu || option.map_o_nulbrk || option.map_i_msb2lsb || + option.map_o_ign_cr) { - tio_printf(" Mappings:%s%s%s%s%s%s%s%s%s%s%s%s", + tio_printf(" Mappings:%s%s%s%s%s%s%s%s%s%s%s%s%s", option.map_i_cr_nl ? " ICRNL" : "", option.map_ign_cr ? " IGNCR" : "", option.map_i_ff_escc ? " IFFESCC" : "", option.map_i_nl_cr ? " INLCR" : "", option.map_i_nl_crnl ? " INLCRNL" : "", + option.map_i_cr_crnl ? " ICRCRNL" : "", option.map_i_msb2lsb ? " IMSB2LSB" : "", option.map_o_cr_nl ? " OCRNL" : "", option.map_o_del_bs ? " ODELBS" : "", @@ -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"); break; case KEY_5: + option.map_i_cr_crnl = !option.map_i_cr_crnl; + tio_printf("ICRCRNL is %s", option.map_i_cr_crnl ? "set" : "unset"); + break; + case KEY_6: option.map_i_msb2lsb = !option.map_i_msb2lsb; tio_printf("IMSB2LSB is %s", option.map_i_msb2lsb ? "set" : "unset"); break; - case KEY_6: + case KEY_7: option.map_o_cr_nl = !option.map_o_cr_nl; tio_printf("OCRNL is %s", option.map_o_cr_nl ? "set" : "unset"); break; - case KEY_7: + case KEY_8: option.map_o_del_bs = !option.map_o_del_bs; tio_printf("ODELBS is %s", option.map_o_del_bs ? "set" : "unset"); break; - case KEY_8: + case KEY_9: option.map_o_nl_crnl = !option.map_o_nl_crnl; tio_printf("ONLCRNL is %s", option.map_o_nl_crnl ? "set" : "unset"); break; - case KEY_9: + case KEY_A: option.map_o_ltu = !option.map_o_ltu; tio_printf("OLTU is %s", option.map_o_ltu ? "set" : "unset"); break; - case KEY_A: + case KEY_B: option.map_o_nulbrk = !option.map_o_nulbrk; tio_printf("ONULBRK is %s", option.map_o_nulbrk ? "set" : "unset"); break; - case KEY_B: + case KEY_C: option.map_o_ign_cr = !option.map_o_ign_cr; tio_printf("OIGNCR is %s", option.map_o_ign_cr ? "set" : "unset"); break; @@ -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", option.map_i_nl_cr ? "Unset" : "Set"); tio_printf(" (4) INLCRNL: %s mapping NL to CR-NL on input", - option.map_i_nl_cr ? "Unset" : "Set"); - tio_printf(" (5) IMSB2LSB: %s mapping MSB bit order to LSB on input", + option.map_i_nl_crnl ? "Unset" : "Set"); + tio_printf(" (5) ICRCRNL: %s mapping CR to CR-NL on input", + option.map_i_cr_crnl ? "Unset" : "Set"); + tio_printf(" (6) IMSB2LSB: %s mapping MSB bit order to LSB on input", option.map_i_msb2lsb ? "Unset" : "Set"); - tio_printf(" (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"); - 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"); - 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"); - 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"); - 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"); - 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"); // 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: tio_printf("Switched timestamp mode to epoch"); break; + case TIMESTAMP_EPOCH_USEC: + tio_printf("Switched timestamp mode to epoch with subdivision in microseconds"); + break; case TIMESTAMP_END: option.timestamp = TIMESTAMP_NONE; tio_printf("Switched timestamp mode off"); @@ -1765,18 +1785,22 @@ GList *tty_search_for_serial_devices(void) creation_time = fs_get_creation_time(path); double uptime = current_time - creation_time; - // Read sysfs files to get best possible description of the driver + // Read sysfs files to get best possible description char description[50] = {}; - length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/interface", entry->d_name); - if (length == -1) - { - length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../interface", entry->d_name); - } + length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../product", entry->d_name); if (length == -1) { length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../../product", entry->d_name); } if (length == -1) + { + length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/interface", entry->d_name); + } + if (length == -1) + { + length = fs_read_file_stripped(description, sizeof(description), "/sys/class/tty/%s/device/../interface", entry->d_name); + } + if (length == -1) { snprintf(description, sizeof(description), "%s", get_serial_port_type(path)); } @@ -1833,6 +1857,226 @@ GList *tty_search_for_serial_devices(void) return device_list; } +#elif defined(__APPLE__) || defined(__MACH__) + +char *getPropertyString(io_object_t device, CFStringRef property) +{ + /* Validate inputs */ + if (device == IO_OBJECT_NULL || property == NULL) + { + return NULL; + } + + /* Attempt to get property */ + CFTypeRef valueRef = IORegistryEntryCreateCFProperty( + device, property, kCFAllocatorDefault, 0); + if (!valueRef) + { + return NULL; + } + + /* Ensure it's a CFString */ + if (CFGetTypeID(valueRef) != CFStringGetTypeID()) + { + CFRelease(valueRef); + return NULL; + } + + /* Convert to C string */ + CFIndex length = CFStringGetLength(valueRef); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; + char *result = malloc(maxSize); + + if (!result) + { + CFRelease(valueRef); + return NULL; + } + + bool converted = CFStringGetCString( + (CFStringRef)valueRef, + result, + maxSize, + kCFStringEncodingUTF8 + ); + + CFRelease(valueRef); + + if (!converted) + { + free(result); + return NULL; + } + + return result; +} + +char *getDeviceLocation(io_object_t device) +{ + /* Validate device */ + if (device == IO_OBJECT_NULL) + { + return strdup("Invalid Device"); + } + + /* Attempt to get location */ + io_string_t location = {0}; + kern_return_t result = IORegistryEntryGetLocationInPlane( + device, kIOServicePlane, location); + + if (result != KERN_SUCCESS) + { + return strdup("Unknown Location"); + } + + /* Safely copy location */ + size_t len = strnlen(location, sizeof(io_string_t)); + char *trimmed_location = calloc(1, len + 1); + + if (!trimmed_location) + { + return strdup("Memory Error"); + } + + memcpy(trimmed_location, location, len); + return trimmed_location; +} + +// for __APPLE__ +GList *tty_search_for_serial_devices(void) +{ + search_reset(); + io_iterator_t iter = IO_OBJECT_NULL; + CFMutableDictionaryRef matchingDict = NULL; + listing_device_name_length_max = 0; + + /* Create matching dictionary for serial devices */ + if (!(matchingDict = IOServiceMatching(kIOSerialBSDServiceValue))) + { + tio_error_print("Failed to create matching dictionary for serial devices"); + return NULL; + } + + /* Get matching services */ + kern_return_t kernResult = IOServiceGetMatchingServices( + kIOMainPortDefault, matchingDict, &iter); + matchingDict = NULL; /* Dictionary ownership transferred */ + + if (kernResult != KERN_SUCCESS) + { + tio_error_print("IOServiceGetMatchingServices failed: %d", kernResult); + return NULL; + } + + /* Defensive check for iterator */ + if (iter == IO_OBJECT_NULL) + { + tio_error_print("Invalid device iterator"); + return NULL; + } + + /* Iterate through serial devices and collect information */ + for (io_object_t device; (device = IOIteratorNext(iter));) + { + char *devicePath = NULL, *locationID = NULL; + char *productName = NULL, *vendorName = NULL; + char tid[5] = {0}; + double uptime = 0.0; + + /* Get device path - key determines if we get tty. or cu. */ + //if (!(devicePath = getPropertyString(device, CFSTR(kIODialinDeviceKey)))) + if (!(devicePath = getPropertyString(device, CFSTR(kIOCalloutDeviceKey)))) + { + IOObjectRelease(device); + continue; /* Skip devices without a path */ + } + + /* Update length of longest device name string */ + listing_device_name_length_max = + strlen(devicePath) > listing_device_name_length_max + ? strlen(devicePath) + : listing_device_name_length_max; + + /* Calculate uptime */ + uptime = get_current_time() - fs_get_creation_time(devicePath); + + /* Find USB device (if applicable) */ + io_object_t usbDevice = IO_OBJECT_NULL; + kern_return_t usbResult = IORegistryEntryGetParentEntry( + device, kIOServicePlane, &usbDevice); + + /* Traverse up the device tree to find a USB device */ + while (usbResult == KERN_SUCCESS && + !IOObjectConformsTo(usbDevice, "IOUSBDevice")) + { + io_object_t oldUsbDevice = usbDevice; + usbResult = IORegistryEntryGetParentEntry( + usbDevice, kIOServicePlane, &usbDevice); + IOObjectRelease(oldUsbDevice); + } + + /* If we found a USB device */ + if (usbResult == KERN_SUCCESS) + { + locationID = getDeviceLocation(usbDevice); + + unsigned long hash2 = djb2_hash((const unsigned char *)(locationID ?: "")); + base62_encode(hash2, tid); + + /* Get product and vendor names */ + productName = getPropertyString(usbDevice, CFSTR("USB Product Name")); + vendorName = getPropertyString(usbDevice, CFSTR("USB Vendor Name")); + + IOObjectRelease(usbDevice); + } + + /* Create device structure */ + device_t *device_info = g_new0(device_t, 1); + if (!device_info) + { + tio_error_print("Memory allocation failed for device_info"); + free(devicePath); + free(locationID); + free(productName); + free(vendorName); + IOObjectRelease(device); + continue; + } + + /* Populate device info */ + *device_info = (device_t) { + .path = devicePath, + .tid = g_strdup(tid), + .uptime = uptime, + .driver = g_strdup(vendorName), + .description = g_strdup(productName ?: vendorName ?: "") + }; + + /* Add to device list */ + device_list = g_list_append(device_list, device_info); + + /* Clean up */ + free(locationID); + free(productName); + free(vendorName); + IOObjectRelease(device); + } + + /* Clean up iterator */ + IOObjectRelease(iter); + + /* Check if device list is empty */ + if (!device_list) + { + return NULL; + } + + /* Sort device list by uptime */ + device_list = g_list_sort(device_list, compare_uptime); + + return device_list; +} + #else GList *tty_search_for_serial_devices(void) @@ -2113,8 +2357,21 @@ void tty_wait_for_device(void) } else if (status == -1) { +#if defined(__CYGWIN__) + // Happens when port unpluged + if (errno == EACCES) + { + goto error; + } +#elif defined(__APPLE__) + if (errno == EBADF) + { + break; // tty_disconnect() will be naturally triggered by atexit() + } +#else tio_error_printf("select() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); +#endif } } @@ -2584,6 +2841,15 @@ int tty_connect(void) do_timestamp = true; } } + else if ((input_char == '\r') && (option.map_i_cr_crnl) && (!option.map_i_msb2lsb)) + { + printchar('\r'); + printchar('\n'); + if (option.timestamp) + { + do_timestamp = true; + } + } else if ((input_char == '\f') && (option.map_i_ff_escc) && (!option.map_i_msb2lsb)) { printchar('\e'); @@ -2744,4 +3010,3 @@ error_read: error_open: return TIO_ERROR; } -