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