From d682e98a66161a58b19d40092e90ec5871c3ad91 Mon Sep 17 00:00:00 2001 From: Martin Lund Date: Wed, 16 Apr 2025 10:47:39 +0200 Subject: [PATCH 01/19] codeql: Build using ubuntu-22.04 --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b96d489..1667c7e 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 From c736b1e35302c7242e01c16dd77572eecadbe6d1 Mon Sep 17 00:00:00 2001 From: David Ordnung Date: Wed, 23 Apr 2025 01:05:12 +0200 Subject: [PATCH 02/19] Input ICRCRNL mapping to avoid using INLCRNL with ICRNL --- man/tio.1.in | 2 ++ man/tio.1.txt | 2 ++ src/bash-completion/tio.in | 2 +- src/options.c | 5 ++++ src/options.h | 1 + src/tty.c | 53 +++++++++++++++++++++++++------------- 6 files changed, 46 insertions(+), 19 deletions(-) diff --git a/man/tio.1.in b/man/tio.1.in index 19c36da..50f6ae8 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -218,6 +218,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" diff --git a/man/tio.1.txt b/man/tio.1.txt index bf44f9d..c37a38c 100644 --- a/man/tio.1.txt +++ b/man/tio.1.txt @@ -168,6 +168,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 diff --git a/src/bash-completion/tio.in b/src/bash-completion/tio.in index d71aceb..daac432 100644 --- a/src/bash-completion/tio.in +++ b/src/bash-completion/tio.in @@ -85,7 +85,7 @@ _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) diff --git a/src/options.c b/src/options.c index 40bcc23..3bdbfef 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, @@ -770,6 +771,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/tty.c b/src/tty.c index c72fd67..a2c917b 100644 --- a/src/tty.c +++ b/src/tty.c @@ -623,16 +623,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 +785,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 +1013,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 @@ -2584,6 +2592,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'); From 437881f0ed220e5a44153c75ef1993a04c0c9205 Mon Sep 17 00:00:00 2001 From: Martin Lund Date: Wed, 23 Apr 2025 08:15:37 +0200 Subject: [PATCH 03/19] Update AUTHORS --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) 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. From 03ef931fb2394f74ce2598fa112aced09cc9342a Mon Sep 17 00:00:00 2001 From: Robert Lipe Date: Thu, 24 Apr 2025 06:49:08 -0500 Subject: [PATCH 04/19] - 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. --- src/tty.c | 269 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 265 insertions(+), 4 deletions(-) diff --git a/src/tty.c b/src/tty.c index a2c917b..6de4af7 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,9 +632,9 @@ void tty_output_mode_set(output_mode_t mode) static void mappings_print(void) { if (option.map_i_cr_nl || option.map_ign_cr || option.map_i_ff_escc || - option.map_i_nl_cr || option.map_i_nl_crnl || option.map_i_cr_crnl || - option.map_o_cr_nl || option.map_o_del_bs || option.map_o_nl_crnl || - option.map_o_ltu || option.map_o_nulbrk || option.map_i_msb2lsb || + option.map_i_nl_cr || option.map_i_nl_crnl || option.map_i_cr_crnl || + option.map_o_cr_nl || option.map_o_del_bs || option.map_o_nl_crnl || + option.map_o_ltu || option.map_o_nulbrk || option.map_i_msb2lsb || option.map_o_ign_cr) { tio_printf(" Mappings:%s%s%s%s%s%s%s%s%s%s%s%s%s", @@ -1841,6 +1850,246 @@ 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) +{ + GList *device_list = NULL; + 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) + { + tio_error_print("No serial devices found"); + return NULL; + } + + /* Sort device list by uptime */ + device_list = g_list_sort(device_list, compare_uptime); + + /* Print header for device listing */ + print_padded("Device", listing_device_name_length_max, ' '); + printf(" TID Uptime [s] Driver Description\n"); + print_padded("", listing_device_name_length_max, '-'); + printf(" ---- -------------- ---------------- --------------------------\n"); + + /* Print sorted device list */ + for (GList *l = device_list; l; l = l->next) + { + device_t *dev = l->data; + printf("%-*s %-4s %14.3f %-16s %s\n", + (int)listing_device_name_length_max, dev->path, + dev->tid ?: "", + dev->uptime, + dev->driver ?: "", + dev->description ?: ""); + } + printf("\n"); + + return device_list; +} + #else GList *tty_search_for_serial_devices(void) @@ -2121,8 +2370,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 } } @@ -2761,4 +3023,3 @@ error_read: error_open: return TIO_ERROR; } - From 7516dff8023c9242ef706ffc40d9f5d45113ed3f Mon Sep 17 00:00:00 2001 From: Robert Lipe Date: Thu, 24 Apr 2025 07:12:57 -0500 Subject: [PATCH 05/19] Add missing build piece. --- src/meson.build | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/meson.build b/src/meson.build index 958d4e9..6919e7e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -47,6 +47,12 @@ tio_dep = [ lua_dep ] +if host_machine.system() == 'darwin' + iokit_dep = dependency('appleframeworks', modules: ['IOKit'], required: true) + corefoundation_dep = dependency('appleframeworks', modules: ['CoreFoundation'], required: true) + tio_dep += [iokit_dep, corefoundation_dep] +endif + tio_c_args = ['-Wno-unused-result'] if enable_setspeed2 From 5d915134a3792d8b9c6e40a1030bc0d9836cca0e Mon Sep 17 00:00:00 2001 From: Robert Lipe Date: Tue, 29 Apr 2025 00:00:42 -0500 Subject: [PATCH 06/19] Fix --auto new and --auto latest on MacOS. (redo) Git is being dumb about https://github.com/tio/tio/commit/67c071633dc6d659fae1e529a85f5990531e1669 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. --- src/tty.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tty.c b/src/tty.c index 6de4af7..c5e6876 100644 --- a/src/tty.c +++ b/src/tty.c @@ -1938,7 +1938,7 @@ char *getDeviceLocation(io_object_t device) // for __APPLE__ GList *tty_search_for_serial_devices(void) { - GList *device_list = NULL; + search_reset(); io_iterator_t iter = IO_OBJECT_NULL; CFMutableDictionaryRef matchingDict = NULL; listing_device_name_length_max = 0; From f887756a7159967e015e3422e9ab04a82db8945f Mon Sep 17 00:00:00 2001 From: Martin Lund Date: Tue, 29 Apr 2025 17:42:22 +0200 Subject: [PATCH 07/19] meson: Enable compiler warnings on unused result and global shadows --- src/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meson.build b/src/meson.build index 6919e7e..b44e9a5 100644 --- a/src/meson.build +++ b/src/meson.build @@ -53,7 +53,7 @@ if host_machine.system() == 'darwin' tio_dep += [iokit_dep, corefoundation_dep] endif -tio_c_args = ['-Wno-unused-result'] +tio_c_args = ['-Wshadow'] if enable_setspeed2 tio_c_args += '-DHAVE_TERMIOS2' From 2fb788f817be131c56d7cd44426f6d64dc42ea7c Mon Sep 17 00:00:00 2001 From: Hideaki Tai Date: Tue, 6 May 2025 15:31:45 +0900 Subject: [PATCH 08/19] fix: lua script stops output if it includes null terminate --- src/script.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/script.c b/src/script.c index b471932..1e46a33 100644 --- a/src/script.c +++ b/src/script.c @@ -187,7 +187,9 @@ static int modem_send(lua_State *L) // lua: send(string) static int write_(lua_State *L) { - const char *string = lua_tostring(L, 1); + size_t len = 0; + const char *string = lua_tolstring(L, 1, &len); + int ret; if (string == NULL) @@ -195,7 +197,7 @@ static int write_(lua_State *L) return 0; } - ret = write(device_fd, string, strlen(string)); + ret = write(device_fd, string, len); fsync(device_fd); // flush these characters now tcdrain(device_fd); //ensure we flushed characters to our device From 7e61a34df342c00f05e323cd4dc0602c19fa64d1 Mon Sep 17 00:00:00 2001 From: Maximilian Seesslen Date: Fri, 23 May 2025 16:14:16 +0200 Subject: [PATCH 09/19] 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 --- man/tio.1.in | 2 ++ man/tio.1.txt | 2 ++ src/bash-completion/tio.in | 2 +- src/configfile.c | 2 +- src/options.c | 8 ++++++++ src/timestamp.c | 13 ++++++++++--- src/timestamp.h | 1 + src/tty.c | 3 +++ 8 files changed, 28 insertions(+), 5 deletions(-) diff --git a/man/tio.1.in b/man/tio.1.in index 50f6ae8..a7cf83f 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 diff --git a/man/tio.1.txt b/man/tio.1.txt index c37a38c..3021890 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 diff --git a/src/bash-completion/tio.in b/src/bash-completion/tio.in index daac432..f510b7e 100644 --- a/src/bash-completion/tio.in +++ b/src/bash-completion/tio.in @@ -89,7 +89,7 @@ _tio() 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/options.c b/src/options.c index 3bdbfef..6aeff88 100644 --- a/src/options.c +++ b/src/options.c @@ -400,6 +400,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; @@ -430,6 +434,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); 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 c5e6876..c38d5dc 100644 --- a/src/tty.c +++ b/src/tty.c @@ -1100,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"); From 58bf5c500886596d9eda188c7be58ec764eb9a13 Mon Sep 17 00:00:00 2001 From: Martin Lund Date: Sun, 25 May 2025 19:46:18 +0200 Subject: [PATCH 10/19] Update tio man page --- man/tio.1.in | 4 ++-- man/tio.1.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/man/tio.1.in b/man/tio.1.in index a7cf83f..9d5ebee 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -447,8 +447,8 @@ Returns 1 on successful match, 0 on timeout, or -1 on error. On successful match it also returns the match string as second return value. .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. +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/man/tio.1.txt b/man/tio.1.txt index 3021890..4363879 100644 --- a/man/tio.1.txt +++ b/man/tio.1.txt @@ -357,7 +357,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. From a1217af4c631345fe6d11280b4192ef24b125326 Mon Sep 17 00:00:00 2001 From: ii8 Date: Sun, 25 May 2025 19:24:11 +0100 Subject: [PATCH 11/19] Fix string truncation bug in scripting api --- src/script.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/script.c b/src/script.c index 1e46a33..8204563 100644 --- a/src/script.c +++ b/src/script.c @@ -332,7 +332,11 @@ error_rs: lua_pushnumber(L, ret); if (buffer != NULL) { - lua_pushstring(L, ret > 0 ? buffer : ""); + if (ret > 0) { + lua_pushlstring(L, buffer, ret); + } else { + lua_pushstring(L, ""); + } free(buffer); } else @@ -400,7 +404,7 @@ static int read_line(lua_State *L) error_rl: lua_pushnumber(L, ret); - lua_pushstring(L, linebuf); + lua_pushlstring(L, linebuf, ret); return 2; } From 3e0b2d861dd8e8c2da70e7494e346dc1d6de006d Mon Sep 17 00:00:00 2001 From: Martin Lund Date: Fri, 30 May 2025 17:18:21 +0200 Subject: [PATCH 12/19] Fix Ubuntu workflow --- .github/workflows/ubuntu.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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: | From 9d00cd3492915792baf1725baaf79e22521bc563 Mon Sep 17 00:00:00 2001 From: Keith Barratt Date: Thu, 29 May 2025 15:57:11 +0100 Subject: [PATCH 13/19] 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. --- src/tty.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/tty.c b/src/tty.c index c38d5dc..22b7eef 100644 --- a/src/tty.c +++ b/src/tty.c @@ -1785,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)); } From 8f33cff6ead809fbf08ff19c2cd720e4de7304d5 Mon Sep 17 00:00:00 2001 From: Martin Lund Date: Sat, 31 May 2025 19:42:08 +0200 Subject: [PATCH 14/19] Disable compiler warning on unused result --- src/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meson.build b/src/meson.build index b44e9a5..05168f7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -53,7 +53,7 @@ if host_machine.system() == 'darwin' tio_dep += [iokit_dep, corefoundation_dep] endif -tio_c_args = ['-Wshadow'] +tio_c_args = ['-Wshadow','-Wno-unused-result'] if enable_setspeed2 tio_c_args += '-DHAVE_TERMIOS2' From 381c0b78236b97a265604801d381587bc204fc5b Mon Sep 17 00:00:00 2001 From: Martin Lund Date: Sat, 14 Jun 2025 07:03:17 +0200 Subject: [PATCH 15/19] Update codeql to v3 --- .github/workflows/codeql.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1667c7e..8a195f4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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}}" From 86f48a2fb689dad56d0e195b4735ef0d4661e8b2 Mon Sep 17 00:00:00 2001 From: ii8 Date: Fri, 13 Jun 2025 15:49:33 +0100 Subject: [PATCH 16/19] 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. --- README.md | 104 ++--- examples/config/config | 2 +- examples/lua/automatic-linux-login.lua | 11 +- examples/lua/control-lines-test.lua | 10 +- examples/lua/read.lua | 24 +- examples/lua/read_line.lua | 28 +- examples/lua/serial-device-search.lua | 2 +- man/tio.1.in | 58 ++- src/script.c | 544 +++++++++---------------- 9 files changed, 318 insertions(+), 465 deletions(-) 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 9d5ebee..eb03ec2 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -433,49 +433,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 "\fBread(size, timeout)" +.IP "\fBtio.read(size, timeout)" Read up to size bytes from serial device. If timeout is 0 or not provided it will wait forever until data is ready to read. -Returns number of bytes read on success, 0 on timeout, or -1 on error. +Returns a string up to size bytes long on success and nil on 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)" +.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. @@ -485,19 +476,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 @@ -726,7 +724,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: @@ -747,7 +745,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 @@ -768,7 +766,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/src/script.c b/src/script.c index 8204563..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, len); 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) - { - if (ret > 0) { - lua_pushlstring(L, buffer, ret); - } else { - lua_pushstring(L, ""); - } - 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_pushlstring(L, linebuf, ret); - 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; @@ -556,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) { @@ -631,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); @@ -647,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; @@ -656,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); From cce94b9d9280415d34575103b0f7f1d783ad1b0c Mon Sep 17 00:00:00 2001 From: John Barbero Unenge Date: Mon, 16 Jun 2025 14:59:24 +0200 Subject: [PATCH 17/19] Add --complete-profiles to help printout and man pages --- man/tio.1.in | 4 ++++ man/tio.1.txt | 4 ++++ src/bash-completion/tio.in | 1 + src/options.c | 1 + 4 files changed, 10 insertions(+) diff --git a/man/tio.1.in b/man/tio.1.in index eb03ec2..eb0338d 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -371,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 diff --git a/man/tio.1.txt b/man/tio.1.txt index 4363879..eeac82c 100644 --- a/man/tio.1.txt +++ b/man/tio.1.txt @@ -289,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. diff --git a/src/bash-completion/tio.in b/src/bash-completion/tio.in index f510b7e..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" diff --git a/src/options.c b/src/options.c index 6aeff88..acff7ce 100644 --- a/src/options.c +++ b/src/options.c @@ -171,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"); From 3af4c5591e0183ea9871654ea4d62254ac23226d Mon Sep 17 00:00:00 2001 From: aiotter Date: Thu, 7 Aug 2025 23:40:39 +0900 Subject: [PATCH 18/19] Fix redundant output on macOS --- src/tty.c | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/tty.c b/src/tty.c index 22b7eef..efda859 100644 --- a/src/tty.c +++ b/src/tty.c @@ -2068,32 +2068,12 @@ GList *tty_search_for_serial_devices(void) /* Check if device list is empty */ if (!device_list) { - tio_error_print("No serial devices found"); return NULL; } /* Sort device list by uptime */ device_list = g_list_sort(device_list, compare_uptime); - /* Print header for device listing */ - print_padded("Device", listing_device_name_length_max, ' '); - printf(" TID Uptime [s] Driver Description\n"); - print_padded("", listing_device_name_length_max, '-'); - printf(" ---- -------------- ---------------- --------------------------\n"); - - /* Print sorted device list */ - for (GList *l = device_list; l; l = l->next) - { - device_t *dev = l->data; - printf("%-*s %-4s %14.3f %-16s %s\n", - (int)listing_device_name_length_max, dev->path, - dev->tid ?: "", - dev->uptime, - dev->driver ?: "", - dev->description ?: ""); - } - printf("\n"); - return device_list; } From 6fb3a64ba234cc255f9637ba938cf0c01e132e4a Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Thu, 22 Jan 2026 08:33:20 +0100 Subject: [PATCH 19/19] Fix license in meson.build - Make license here match LICENSE - According to meson docs, it should not be an array --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' ] )