tic2mqtt/tic.c

327 lines
6.8 KiB
C

#include "tic.h"
#include "logger.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fcntl.h>
#include <termios.h>
#include <unistd.h>
#define TIC_FRAME_MAX 2048 // enough for all possible data groups in standard mode
#define TIC_DATA_MAX 128
#define TIC_STX 0x02
#define TIC_ETX 0x03
#define TIC_SGR 0x0a
#define TIC_SEP_STD 0x09
#define TIC_SEP_HIS 0x20
#define TIC_EGR 0x0d
struct tic_info_s {
int fd;
int is_tty;
int mode;
tic_cb_data_f *cb_data;
int frame_len;
char frame[TIC_FRAME_MAX];
};
static void tic_init_tty(int fd, int speed)
{
struct termios termios;
tcgetattr(fd, &termios);
// input and output speed
cfsetispeed(&termios, speed);
cfsetospeed(&termios, speed);
// set input modes:
// - Disable XON/XOFF flow control on input.
// - Do not translate carriage return to newline on input.
// - Enable input parity checking.
// - Strip off eighth bit.
termios.c_iflag &= ~(IXON | IXOFF | IXANY | ICRNL);
termios.c_iflag |= INPCK | ISTRIP;
// set output modes:
// - Disable implementation-defined output processing (raw mode).
termios.c_oflag &= ~OPOST;
// set control modes:
// - Enable receiver.
// - Ignore modem control lines.
// - 7 bit.
// - 1 stop bit.
// - Enable parity generation on output and parity checking for input.
// - Disable RTS/CTS (hardware) flow control.
termios.c_cflag |= CLOCAL | CREAD;
termios.c_cflag &= ~(CSIZE | PARODD | CSTOPB);
termios.c_cflag |= CS7 | PARENB;
termios.c_cflag &= ~CRTSCTS;
// set local modes:
// - Do bot generate signal when the characters INTR, QUIT, SUSP or DSUSP are received.
// - Disable canonical mode.
// - Do not echo input characters.
termios.c_lflag &= ~(ISIG | ICANON | ECHO | ECHOE);
// set special characters:
// - Timeout set to 1 s.
// - Minimum number of characters for noncanonical read set to 1.
termios.c_cc[VTIME] = 10;
termios.c_cc[VMIN] = 0;
tcsetattr(fd, TCSANOW, &termios);
tcflush(fd, TCIFLUSH);
}
/* check received stream
* returns:
* - 0: ok, received at least 20 valid control chars without any
* illegal characters
* - -1: read() error
* - 1: timeout, didn't received enough valid chars in the allowed
* time (2s).
*/
static int tic_check_read(int fd)
{
char c;
int nb_ok = 0;
time_t stamp = time(NULL);
tcflush(fd, TCIFLUSH);
while (nb_ok < 20) {
int ret = read(fd, &c, 1);
if (ret < 0)
return -1;
if (ret == 1) {
switch (c) {
case TIC_STX:
case TIC_ETX:
case TIC_SGR:
case TIC_SEP_STD:
case TIC_EGR:
nb_ok++;
break;
default:
if (c < 0x20 || c > 0x7f)
nb_ok = 0;
}
}
if (difftime(time(NULL), stamp) > 2)
return 1;
}
return 0;
}
tic_info_t* tic_init(const char *fname)
{
tic_info_t* tic = calloc(1, sizeof(tic_info_t));
if (tic == NULL) {
log_error("cannot allocate memory\n");
return NULL;
}
tic->cb_data = NULL;
log_info("opening %s\n", fname);
tic->fd = open(fname, O_RDWR | O_NOCTTY);
if (tic->fd < 0) {
log_error("Cannot open %s: %s\n", fname, strerror(errno));
free(tic);
return NULL;
}
tic->is_tty = isatty(tic->fd);
if (tic->is_tty) {
int ret;
log_debug("trying standard mode\n");
tic_init_tty(tic->fd, B9600);
ret = tic_check_read(tic->fd);
if (ret == 0) {
tic->mode = TIC_MODE_STD;
log_info("TIC standard mode detected\n");
} else {
log_debug("trying historical mode\n");
tic_init_tty(tic->fd, B1200);
ret = tic_check_read(tic->fd);
if (ret == 0) {
tic->mode = TIC_MODE_HIST;
log_info("TIC historical mode detected\n");
} else {
log_error("no valid TIC stream detected\n");
free(tic);
return NULL;
}
}
}
if (tic_read_frame(tic)) {
free(tic);
return NULL;
}
tic_process_frame(tic);
return tic;
}
int tic_mode(tic_info_t *tic)
{
return tic->mode;
}
void tic_set_cb_data(tic_info_t *tic, tic_cb_data_f *func)
{
tic->cb_data = func;
}
int tic_read_frame(tic_info_t *tic)
{
char c;
do {
if (read(tic->fd, &c, 1) <= 0) {
log_error("read error (waiting for STX)\n");
return -1;
}
} while (c != TIC_STX);
for (int i = 0; i < TIC_FRAME_MAX; i++) {
if (read(tic->fd, &c, 1) <= 0) {
log_error("read error (reading frame)\n");
return -1;
}
tic->frame[i] = c;
if (c == TIC_ETX) {
return 0;
}
}
log_error("frame too long\n");
return -1;
}
static inline int twodigits2int(const char *digits)
{
return (digits[0]-'0')*10 + (digits[1]-'0');
}
static time_t tic_horodate_to_time(const char *horodate)
{
if (horodate == NULL)
return (time_t)0L;
// format: "SAAMMJJhhmmss" Saison-year-month-day-hour-minutes-seconds
// 01 3 5 7 9 11
struct tm tm;
tm.tm_sec = twodigits2int(&horodate[11]);
tm.tm_min = twodigits2int(&horodate[9]);
tm.tm_hour = twodigits2int(&horodate[7]);
tm.tm_mday = twodigits2int(&horodate[5]);
tm.tm_mon = twodigits2int(&horodate[3]);
tm.tm_year = twodigits2int(&horodate[1]) + 2000 - 1900;
switch(horodate[0]) {
case 'E': tm.tm_isdst = 1; break;
case 'H': tm.tm_isdst = 0; break;
default: tm.tm_isdst = -1;
}
return mktime(&tm);
}
void tic_process_frame(tic_info_t *tic)
{
char *frame = tic->frame;
for (int i = 0;;) {
int grp_start;
int grp_end;
if (frame[i] == TIC_ETX)
return;
while (frame[i++] != '\n') {
if (i >= TIC_FRAME_MAX)
return;
}
grp_start = i;
while (frame[i++] != '\r') {
if (i >= TIC_FRAME_MAX)
return;
}
grp_end = i - 1;
int grp_end_sum = tic->mode == TIC_MODE_STD ? grp_end - 1 : grp_end - 2;
char sum = 0;
for (int j = grp_start; j < grp_end_sum; j++)
sum += frame[j];
sum = (sum & 0x3f) + 0x20;
if (sum != frame[grp_end - 1]) {
log_warning("checksum error\n");
continue;
}
char sep = tic->mode == TIC_MODE_STD ? '\t' : ' ';
int nsep = 0;
char *p_label = &frame[grp_start];
char *p_horodate;
char *p_data;
for (int j = grp_start; j < grp_end - 1; j++) {
if (frame[j] == sep) {
frame[j] = '\0';
nsep++;
switch(nsep) {
case 1: p_horodate = &frame[j+1]; break;
case 2: p_data = &frame[j+1]; break;
}
}
}
if (nsep > 3 || (nsep == 3 && tic->mode == TIC_MODE_HIST) || nsep < 2) {
log_warning("bad format group\n");
continue;
}
if (nsep == 2) {
p_data = p_horodate;
p_horodate = NULL;
}
if (tic->cb_data) {
time_t date = tic_horodate_to_time(p_horodate);
(*tic->cb_data)(p_label, p_data, date);
}
}
}
void tic_exit(tic_info_t *tic)
{
if (tic) {
close(tic->fd);
free(tic);
}
}