/* * 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 . */ #include #include #include #include #include #include #include #include #include #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; }