diff options
| author | Ludvig Gunne Lindström <ludviggunnelindstrom@gmail.com> | 2026-07-17 11:43:24 +0200 |
|---|---|---|
| committer | Ludvig Gunne Lindström <ludviggunnelindstrom@gmail.com> | 2026-11-08 14:15:04 +0100 |
| commit | 039847a29868d335c08fe0a587260dffc71ca5c4 (patch) | |
| tree | e9d775090fef45ca51db576779fb373040d50e6a /mod/serial.c | |
| download | melisma-modules-039847a29868d335c08fe0a587260dffc71ca5c4.tar.gz | |
Diffstat (limited to 'mod/serial.c')
| -rw-r--r-- | mod/serial.c | 511 |
1 files changed, 511 insertions, 0 deletions
diff --git a/mod/serial.c b/mod/serial.c new file mode 100644 index 0000000..a7b51db --- /dev/null +++ b/mod/serial.c @@ -0,0 +1,511 @@ +/* + * melisma - extensible and scriptable audio engine + * Copyright (C) 2026 Ludvig Gunne Lindström + * + * 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 3 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, see <https://www.gnu.org/licenses/>. + */ + +#include <string.h> +#include <assert.h> +#include <stdlib.h> +#include <errno.h> +#include <ctype.h> +#include <unistd.h> +#include <termios.h> +#include <fcntl.h> +#include <lauxlib.h> +#include "melisma.h" +#include "melisma/ring.h" + +#define SERIAL_TYPENAME "serial" +#define DEFAULT_BAUD 9600 +#define DEFAULT_FORMAT "8N1" +#define DEFAULT_ENCODING "cstring" +#define DEFAULT_BUFSIZE 4096 + +struct serial; + +typedef int (*decode_fn)(struct serial*); + +struct conf +{ + char *path; + char *format; + int datasize; + int stopbits; + int parity; + int baud; + int baudflag; + decode_fn decode; + int bufsize; + int callb; +}; + +struct serial +{ + struct conf conf; + int fd; + int synced; + size_t len; + size_t cap; + uint8_t *data; + uint8_t *tmpdata; + ml_ring *ring; + lua_State *state; +}; + +static int cstring(struct serial*); + +static const struct { + decode_fn decode; + const char *name; +} encoding_map[] = { + { cstring, "cstring" }, + { NULL, NULL }, +}; + +static void free_conf(struct conf *c) +{ + free(c->path); + free(c->format); +} + +static void put_byte(struct serial *se, int c) +{ + if (se->len == se->cap) + { + se->cap = se->cap == 0 ? 32 : se->cap*2; + se->data = realloc(se->data,se->cap); + se->tmpdata = realloc(se->tmpdata,se->cap); + } + se->data[se->len++] = c; +} + +static void io_handler(int fd, int events, void *args) +{ + struct serial *se = args; + if (se->conf.decode(se) == 0) + { + if (ml_ring_has_space(se->ring,se->len + sizeof se->len)) + { + ml_ring_write_blocking(se->ring,&se->len,sizeof se->len); + ml_ring_write_blocking(se->ring,se->data,se->len); + } + else + { + fprintf(stderr,"dropped %lu bytes\n",se->len); + } + } + else + { + fprintf(stderr,"failed to decode message\n"); + } + se->len = 0; +} + +static void read_conf(lua_State *s, int tbl, struct conf *c) +{ + int top = lua_gettop(s); + lua_getfield(s,tbl,"path"); + + if (lua_isnil(s,-1)) + { + free_conf(c); + luaL_error(s,"missing 'path' field"); + } + + if (!lua_isstring(s,-1)) + { + free_conf(c); + luaL_error(s,"'path' field is not a string"); + } + + c->path = strdup(lua_tostring(s,-1)); + lua_settop(s,top); + + + lua_getfield(s,tbl,"baud"); + + if (lua_isnil(s,-1)) + { + c->baud = DEFAULT_BAUD; + } + else if (lua_isinteger(s,-1)) + { + c->baud = lua_tointeger(s,-1); + } + else + { + free_conf(c); + luaL_error(s,"'baud' field is not an integer"); + } + + switch (c->baud) + { + case 0: c->baudflag = B0; break; + case 50: c->baudflag = B50; break; + case 75: c->baudflag = B75; break; + case 110: c->baudflag = B110; break; + case 134: c->baudflag = B134; break; + case 150: c->baudflag = B150; break; + case 200: c->baudflag = B200; break; + case 300: c->baudflag = B300; break; + case 600: c->baudflag = B600; break; + case 1200: c->baudflag = B1200; break; + case 1800: c->baudflag = B1800; break; + case 2400: c->baudflag = B2400; break; + case 4800: c->baudflag = B4800; break; + case 9600: c->baudflag = B9600; break; + case 19200: c->baudflag = B19200; break; + case 38400: c->baudflag = B38400; break; + case 57600: c->baudflag = B57600; break; + case 115200: c->baudflag = B115200; break; + case 230400: c->baudflag = B230400; break; + case 460800: c->baudflag = B460800; break; + case 500000: c->baudflag = B500000; break; + case 576000: c->baudflag = B576000; break; + case 921600: c->baudflag = B921600; break; + case 1000000: c->baudflag = B1000000; break; + case 1152000: c->baudflag = B1152000; break; + case 1500000: c->baudflag = B1500000; break; + case 2000000: c->baudflag = B2000000; break; + default: c->baudflag = -1; break; + } + + if (c->baudflag < 0) + { + free_conf(c); + luaL_error(s,"unsupported baud rate %d",c->baud); + } + + lua_settop(s,top); + + const char *format; + lua_getfield(s,tbl,"format"); + + if (lua_isnil(s,-1)) + { + format = DEFAULT_FORMAT; + } + else if (lua_isstring(s,-1)) + { + format = lua_tostring(s,-1); + } + else + { + free_conf(c); + luaL_error(s,"'format' field is not a string"); + } + + c->format = strdup(format); + + if (strlen(format) != 3 || + !strchr("5678",format[0]) || + !strchr("NEOneo",format[1]) || + !strchr("12",format[2])) + { + free_conf(c); + luaL_error(s,"invalid frame format '%s'",format); + } + + c->datasize = format[0] - '0'; + c->parity = toupper(format[1]); + c->stopbits = format[2] - '0'; + + lua_settop(s,top); + + lua_getfield(s,tbl,"encoding"); + const char *encoding = NULL; + + if (lua_isnil(s,-1)) + { + encoding = DEFAULT_ENCODING; + } + else if (lua_isstring(s,-1)) + { + encoding = lua_tostring(s,-1); + } + else + { + free_conf(c); + luaL_error(s,"'encoding' field is not a string"); + } + + for (size_t i = 0;; i++) + { + c->decode = encoding_map[i].decode; + const char *name = encoding_map[i].name; + + if (c->decode == NULL) + { + free_conf(c); + luaL_error(s,"unknown encoding '%s'",encoding); + } + + if (strcmp(name,encoding) == 0) + { + break; + } + } + + lua_getfield(s,tbl,"bufsize"); + + if (lua_isnil(s,-1)) + { + c->bufsize = DEFAULT_BUFSIZE; + } + else if (lua_isinteger(s,-1)) + { + c->bufsize = lua_tointeger(s,-1); + + if (c->bufsize <= 0) + { + luaL_error(s,"invalid bufsize %d",c->bufsize); + } + } + else + { + free_conf(c); + luaL_error(s,"'bufsize' is not an integer"); + } + + lua_getfield(s,tbl,"callback"); + + if (lua_isnil(s,-1)) + { + c->callb = LUA_NOREF; + } + else if (lua_isfunction(s,-1)) + { + c->callb = luaL_ref(s,LUA_REGISTRYINDEX); + } + else + { + free_conf(c); + luaL_error(s,"'callback' is not a function"); + } +} + +static void setup_tty(struct termios *t, struct conf *c) +{ + cfsetispeed(t,c->baudflag); + + switch (c->stopbits) + { + case 1: t->c_cflag &= ~CSTOPB; break; + case 2: t->c_cflag |= CSTOPB; break; + default: assert(0); + } + + switch (c->parity) + { + case 'N': + case 'n': + t->c_cflag &= ~PARENB; + break; + case 'O': + case 'o': + t->c_cflag |= PARENB; + t->c_cflag |= PARODD; + break; + case 'E': + case 'e': + t->c_cflag |= PARENB; + t->c_cflag &= ~PARODD; + break; + default: + assert(0); + } + + t->c_cflag &= ~CSIZE; + + switch (c->datasize) + { + case 5: t->c_cflag |= CS5; break; + case 6: t->c_cflag |= CS6; break; + case 7: t->c_cflag |= CS7; break; + case 8: t->c_cflag |= CS8; break; + default: assert(0); + } + + t->c_cflag |= CREAD|CLOCAL; + t->c_cflag &= ~CRTSCTS; + t->c_lflag &= ~(ICANON|ECHO|ISIG); + t->c_iflag &= ~(IXON|IXOFF|IXANY); + t->c_oflag &= ~OPOST; + t->c_cc[VMIN] = 1; + t->c_cc[VTIME] = 0; +} + +static void proc(const ml_proc_args *args) +{ + struct serial *se = args->userp; + size_t len; + + while (ml_ring_read(se->ring,&len,sizeof len) == sizeof len) + { + ml_ring_read_blocking(se->ring,se->tmpdata,len); + + if (se->conf.callb != LUA_NOREF) + { + lua_pushlstring(se->state,(const char*)se->tmpdata,len); + int arg = luaL_ref(se->state,LUA_REGISTRYINDEX); + ml_schedule_event(0,se->conf.callb,arg); + } + } +} + +static int l_open(lua_State *s) +{ + luaL_checktype(s,1,LUA_TTABLE); + + struct conf conf = {0}; + + read_conf(s,1,&conf); + + int fd = open(conf.path,O_RDONLY|O_NOCTTY); + + if (fd < 0) + { + luaL_error(s,"failed to open %s: %s",conf.path,strerror(errno)); + } + + struct termios tty = {0}; + tcgetattr(fd,&tty); + + setup_tty(&tty,&conf); + + tcsetattr(fd,TCSANOW,&tty); + tcflush(fd,TCIFLUSH); + + struct serial *se = calloc(1,sizeof(*se)); + se->fd = fd; + se->conf = conf; + se->ring = ml_ring_init(conf.bufsize); + se->state = s; + + *(void**) lua_newuserdata(s,sizeof(void**)) = se; + luaL_setmetatable(s,SERIAL_TYPENAME); + + ml_add_io_handler(fd,POLLIN,io_handler,se); + + ml_node *node = ml_create_node(proc,se); + ml_channel *src = ml_add_output(node); + ml_channel *dst = ml_add_input(ml_get_root_node()); + ml_connect(src,dst); + ml_label_node(node,"serial %s",conf.path); + + return 1; +} + +static int read_byte(struct serial *se) +{ + uint8_t c; + + if (read(se->fd,&c,sizeof c) < sizeof c) + { + return -1; + } + + return c; +} + +static int sync_(struct serial *se) +{ + int c; + + if (se->synced) + { + return 0; + } + + while ((c = read_byte(se)) != 0) + { + if (c < 0) + { + return -1; + } + } + + se->synced = 1; + return 0; +} + +static int cstring(struct serial *se) +{ + if (sync_(se) < 0) + { + return -1; + } + + int c; + + while ((c = read_byte(se)) != 0) + { + if (c < 0) + { + se->synced = 0; + return -1; + } + + put_byte(se,c); + } + + return 0; +} + +int luaopen_melisma_serial(lua_State *s) +{ +/*** Read data on serial ports. + + local mserial = require 'melisma.serial' + + mserial.open { + path = '/dev/ttyACM0', + callback = print, + } + +@classmod serial +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Open a serial port. + +`conf` is a table containing the following keys: + +- `path` (**string, required**): Path to serial port (e.g. `/dev/ttyACM0`). +- `callback` (**function, required**): Callback function for handling messages. +- `baud` (**integer**); baud rate (default `9600`). +- `format` (**string**): frame format (default `'8N1'`). +- `encoding` (**string**): method for encoding messages (default `'cstring'`). May be one of: + - `'cstring'`: `NULL`-terminated strings. + +@static +@function open +@tparam table conf +@treturn serial +*/ + lua_pushcfunction(s,l_open); + lua_setfield(s,tbl,"open"); + + luaL_newmetatable(s,SERIAL_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_pushvalue(s,tbl); + return 1; +} |
