aboutsummaryrefslogtreecommitdiff
path: root/mod/serial.c
diff options
context:
space:
mode:
authorLudvig Gunne Lindström <ludviggunnelindstrom@gmail.com>2026-07-17 11:43:24 +0200
committerLudvig Gunne Lindström <ludviggunnelindstrom@gmail.com>2026-11-08 14:15:04 +0100
commit039847a29868d335c08fe0a587260dffc71ca5c4 (patch)
treee9d775090fef45ca51db576779fb373040d50e6a /mod/serial.c
downloadmelisma-modules-main.tar.gz
Diffstat (limited to 'mod/serial.c')
-rw-r--r--mod/serial.c511
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;
+}