tio/src/script.c

585 lines
13 KiB
C

/*
* tio - a serial device I/O tool
*
* Copyright (c) 2014-2024 Martin Lund
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#include <errno.h>
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <lauxlib.h>
#include <lualib.h>
#include <sys/ioctl.h>
#include "misc.h"
#include "print.h"
#include "options.h"
#include "tty.h"
#include "xymodem.h"
#include "log.h"
#include "script.h"
#include "fs.h"
#define MAX_BUFFER_SIZE 2000 // Maximum size of circular buffer
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"
" 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";
// lua: sleep(seconds)
static int sleep_(lua_State *L)
{
long seconds = lua_tointeger(L, 1);
if (seconds < 0)
{
return 0;
}
tio_printf("Sleeping %ld seconds", seconds);
sleep(seconds);
return 0;
}
// lua: msleep(miliseconds)
static int msleep(lua_State *L)
{
long mseconds = lua_tointeger(L, 1);
long useconds = mseconds * 1000;
if (useconds < 0)
{
return 0;
}
tio_printf("Sleeping %ld ms", mseconds);
usleep(useconds);
return 0;
}
// lua: line_set(dtr,rts,cts,dsr,cd,ri)
static int line_set(lua_State *L)
{
tty_line_config_t line_config[6] = { };
int dtr = lua_tointeger(L, 1);
int rts = lua_tointeger(L, 2);
int cts = lua_tointeger(L, 3);
int dsr = lua_tointeger(L, 4);
int cd = lua_tointeger(L, 5);
int ri = lua_tointeger(L, 6);
if (dtr != -1)
{
line_config[0].mask = TIOCM_DTR;
line_config[0].value = dtr;
line_config[0].reserved = true;
}
if (rts != -1)
{
line_config[1].mask = TIOCM_RTS;
line_config[1].value = rts;
line_config[1].reserved = true;
}
if (cts != -1)
{
line_config[2].mask = TIOCM_CTS;
line_config[2].value = cts;
line_config[2].reserved = true;
}
if (dsr != -1)
{
line_config[3].mask = TIOCM_DSR;
line_config[3].value = dsr;
line_config[3].reserved = true;
}
if (cd != -1)
{
line_config[4].mask = TIOCM_CD;
line_config[4].value = cd;
line_config[4].reserved = true;
}
if (ri != -1)
{
line_config[5].mask = TIOCM_RI;
line_config[5].value = ri;
line_config[5].reserved = true;
}
tty_line_set(device_fd, line_config);
return 0;
}
// lua: modem_send(file, protocol)
static int modem_send(lua_State *L)
{
const char *file = lua_tostring(L, 1);
int protocol = lua_tointeger(L, 2);
if (file == NULL)
{
return 0;
}
switch (protocol)
{
case XMODEM_1K:
tio_printf("Sending file '%s' using XMODEM-1K", file);
tio_printf("%s", xymodem_send(device_fd, file, XMODEM_1K) < 0 ? "Aborted" : "Done");
break;
case XMODEM_CRC:
tio_printf("Sending file '%s' using XMODEM-CRC", file);
tio_printf("%s", xymodem_send(device_fd, file, XMODEM_CRC) < 0 ? "Aborted" : "Done");
break;
case YMODEM:
tio_printf("Sending file '%s' using YMODEM", file);
tio_printf("%s", xymodem_send(device_fd, file, YMODEM) < 0 ? "Aborted" : "Done");
break;
}
return 0;
}
// lua: send(string)
static int send(lua_State *L)
{
const char *string = lua_tostring(L, 1);
int ret;
if (string == NULL)
{
return 0;
}
ret = write(device_fd, string, strlen(string));
lua_pushnumber(L, ret);
return 1;
}
// Function to add a character to the circular expect buffer
static void expect_buffer_add(char c)
{
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;
}
// lua: ret,string = read_string(size, timeout)
static int read_string(lua_State *L)
{
int size = lua_tointeger(L, 1);
int timeout = lua_tointeger(L, 2);
int ret = 0;
char *buffer = malloc(size);
if (buffer == NULL)
{
ret = -1; // Error
goto error;
}
if (timeout == 0)
{
timeout = -1; // Wait forever
}
ssize_t bytes_read = read_poll(device_fd, buffer, size, timeout);
if (bytes_read < 0)
{
ret = -1; // Error
goto error;
}
else if (bytes_read == 0)
{
ret = 0; // Timeout
goto error;
}
for (ssize_t i=0; i<bytes_read; i++)
{
putchar(buffer[i]);
if (option.log)
{
log_putc(buffer[i]);
}
}
ret = bytes_read;
error:
lua_pushnumber(L, ret);
if (buffer != NULL)
{
lua_pushstring(L, buffer);
free(buffer);
}
return 2;
}
// lua: expect(string, timeout)
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(&regex, string, REG_EXTENDED);
if (ret)
{
tio_error_print("Could not compile regex");
ret = -1;
goto error;
}
// Main loop to read and match
while (true)
{
ssize_t bytes_read = read_poll(device_fd, &c, 1, timeout);
if (bytes_read > 0)
{
putchar(c);
expect_buffer_add(c);
if (option.log)
{
log_putc(c);
}
// Match against the entire buffer
if (match_regex(&regex))
{
ret = 1;
break;
}
}
else
{
// Timeout or error
break;
}
}
// Cleanup
regfree(&regex);
error:
lua_pushnumber(L, ret);
lua_pushstring(L, match_string);
return 2;
}
// lua: exit(code)
static int exit_(lua_State *L)
{
long code = lua_tointeger(L, 1);
exit(code);
return 0;
}
// lua: list = tty_search()
static int tty_search_(lua_State *L)
{
UNUSED(L);
GList *iter;
int i = 1;
GList *device_list = tty_search_for_serial_devices();
if (device_list == NULL)
{
return 0;
}
// Create a new table
lua_newtable(L);
// Iterate through found devices
for (iter = device_list; iter != NULL; iter = g_list_next(iter))
{
device_t *device = (device_t *) iter->data;
// Create a new sub-table for each serial device
lua_newtable(L);
// Add elements to the table
lua_pushstring(L, "path");
lua_pushstring(L, device->path);
lua_settable(L, -3);
lua_pushstring(L, "tid");
lua_pushstring(L, device->tid);
lua_settable(L, -3);
lua_pushstring(L, "uptime");
lua_pushnumber(L, device->uptime);
lua_settable(L, -3);
lua_pushstring(L, "driver");
lua_pushstring(L, device->driver);
lua_settable(L, -3);
lua_pushstring(L, "description");
lua_pushstring(L, device->description);
lua_settable(L, -3);
// Set the sub-table as a row in the main table
lua_rawseti(L, -2, i++);
}
// Return table
return 1;
}
static void script_buffer_run(lua_State *L, const char *script_buffer)
{
int error;
error = luaL_loadbuffer(L, script_buffer, strlen(script_buffer), "tio") ||
lua_pcall(L, 0, 0, 0);
if (error)
{
tio_warning_printf("lua: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); /* Pop error message from the stack */
}
}
static const struct luaL_Reg tio_lib[] =
{
{ "sleep", sleep_},
{ "msleep", msleep},
{ "line_set", line_set},
{ "modem_send", modem_send},
{ "send", send},
{ "read", read_string},
{ "expect", expect},
{ "exit", exit_},
{ "tty_search", tty_search_},
{NULL, NULL}
};
#if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501
/*
** Adapted from Lua 5.2.0 (for backwards compatibility)
*/
static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup)
{
luaL_checkstack(L, nup+1, "too many upvalues");
for (; l->name != NULL; l++) { /* fill the table with given functions */
int i;
lua_pushstring(L, l->name);
for (i = 0; i < nup; i++) /* copy upvalues to the top */
lua_pushvalue(L, -(nup+1));
lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */
lua_settable(L, -(nup + 3));
}
lua_pop(L, nup); /* remove upvalues */
}
#endif
static void script_load(lua_State *L)
{
int error;
error = luaL_loadbuffer(L, script_init, strlen(script_init), "tio") || lua_pcall(L, 0, 0, 0);
if (error)
{
tio_error_print("%s\n", lua_tostring(L, -1));
lua_pop(L, 1); // Pop error message from the stack
}
}
int lua_register_tio(lua_State *L)
{
// Register lxi functions
lua_getglobal(L, "_G");
luaL_setfuncs(L, tio_lib, 0);
lua_pop(L, 1);
// Load lua init script
script_load(L);
return 0;
}
void script_file_run(lua_State *L, const char *filename)
{
if (strlen(filename) == 0)
{
tio_warning_printf("Missing script filename\n");
return;
}
if (luaL_dofile(L, filename))
{
tio_warning_printf("lua: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); /* pop error message from the stack */
return;
}
}
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)
{
script_set_global(L, "toggle", 2);
script_set_global(L, "high", 1);
script_set_global(L, "low", 0);
script_set_global(L, "XMODEM_CRC", XMODEM_CRC);
script_set_global(L, "XMODEM_1K", XMODEM_1K);
script_set_global(L, "YMODEM", YMODEM);
}
void script_run(int fd, const char *script_filename)
{
lua_State *L;
device_fd = fd;
L = luaL_newstate();
luaL_openlibs(L);
// Bind tio functions
lua_register_tio(L);
// Initialize globals
script_set_globals(L);
if (script_filename != NULL)
{
tio_printf("Running script %s", script_filename);
script_file_run(L, script_filename);
}
else if (option.script_filename != NULL)
{
tio_printf("Running script %s", option.script_filename);
script_file_run(L, option.script_filename);
}
else if (option.script != NULL)
{
tio_printf("Running script");
script_buffer_run(L, option.script);
}
lua_close(L);
}
const char *script_run_state_to_string(script_run_t state)
{
switch (state)
{
case SCRIPT_RUN_ONCE:
return "once";
case SCRIPT_RUN_ALWAYS:
return "always";
case SCRIPT_RUN_NEVER:
return "never";
default:
return "Unknown";
}
}