From: Thibaut VARÈNE Date: Sun, 3 Oct 2021 11:46:11 +0000 (+0200) Subject: Move source to `src` X-Git-Tag: v2.2~9 X-Git-Url: http://vcs.slashdirt.org/git/?a=commitdiff_plain;h=8f1afb1ca7e34af0a8df0c2a5e39d156dbae19f4;p=sw%2Ftic2json.git Move source to `src` Avoid infinite recursion in `embedded` --- diff --git a/Makefile b/Makefile deleted file mode 100644 index d7dcc98..0000000 --- a/Makefile +++ /dev/null @@ -1,29 +0,0 @@ -MAIN := tic2json -CFLAGS := -Wall -Os -TICVERSIONS := 01 02 - -# don't touch below this line - -TICS := $(addprefix ticv,$(TICVERSIONS)) -TICSDEFS := $(addprefix -DTICV,$(TICVERSIONS)) -LEXSRCS := $(addsuffix .lex.c,$(TICS)) - -CFLAGS += $(TICSDEFS) -DBINNAME='"$(MAIN)"' - -all: $(MAIN) - -%.lex.c: %.l %.tab.h -# The ideal size for the flex buffer is the length of the longest token expected, in bytes, plus a little more. - flex -DYY_BUF_SIZE=128 -P$*yy -o$@ $< - -%.tab.h %.tab.c: %.y - bison -Wno-other -p $*yy -d $< - -tic2json.o: $(addsuffix .tab.h,$(TICS)) - -$(MAIN): $(addsuffix .tab.o,$(TICS)) $(addsuffix .lex.o,$(TICS)) tic.o tic2json.o - -csources: $(LEXSRCS) - -clean: - $(RM) $(MAIN) *.output *.tab.h *.tab.c *.lex.c *.o diff --git a/README.md b/README.md index d929d55..b2d213f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ See LICENSE.md for details ## Building -To build, run `make` +To build, run `cd src; make` **Note:** the build can be adjusted through the top Makefile variables. In particular, it is possible to build support for only specific version(s) of the TIC. diff --git a/embedded/ARM-Mbed/mbed-os-tic2json/tic2json/src b/embedded/ARM-Mbed/mbed-os-tic2json/tic2json/src index 210be6a..b3e266f 120000 --- a/embedded/ARM-Mbed/mbed-os-tic2json/tic2json/src +++ b/embedded/ARM-Mbed/mbed-os-tic2json/tic2json/src @@ -1 +1 @@ -/Users/varenet/NOBACKUP/tic2json \ No newline at end of file +../../../../src \ No newline at end of file diff --git a/embedded/ESP-RTOS/esptic/components/tic2json/src b/embedded/ESP-RTOS/esptic/components/tic2json/src index 3efed95..d753b57 120000 --- a/embedded/ESP-RTOS/esptic/components/tic2json/src +++ b/embedded/ESP-RTOS/esptic/components/tic2json/src @@ -1 +1 @@ -../../../../../ \ No newline at end of file +../../../../../src \ No newline at end of file diff --git a/embedded/Pico/picotic/tic2json/src b/embedded/Pico/picotic/tic2json/src index 11a54ed..b3e266f 120000 --- a/embedded/Pico/picotic/tic2json/src +++ b/embedded/Pico/picotic/tic2json/src @@ -1 +1 @@ -../../../../ \ No newline at end of file +../../../../src \ No newline at end of file diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..d7dcc98 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,29 @@ +MAIN := tic2json +CFLAGS := -Wall -Os +TICVERSIONS := 01 02 + +# don't touch below this line + +TICS := $(addprefix ticv,$(TICVERSIONS)) +TICSDEFS := $(addprefix -DTICV,$(TICVERSIONS)) +LEXSRCS := $(addsuffix .lex.c,$(TICS)) + +CFLAGS += $(TICSDEFS) -DBINNAME='"$(MAIN)"' + +all: $(MAIN) + +%.lex.c: %.l %.tab.h +# The ideal size for the flex buffer is the length of the longest token expected, in bytes, plus a little more. + flex -DYY_BUF_SIZE=128 -P$*yy -o$@ $< + +%.tab.h %.tab.c: %.y + bison -Wno-other -p $*yy -d $< + +tic2json.o: $(addsuffix .tab.h,$(TICS)) + +$(MAIN): $(addsuffix .tab.o,$(TICS)) $(addsuffix .lex.o,$(TICS)) tic.o tic2json.o + +csources: $(LEXSRCS) + +clean: + $(RM) $(MAIN) *.output *.tab.h *.tab.c *.lex.c *.o diff --git a/src/tic.c b/src/tic.c new file mode 100644 index 0000000..016ec00 --- /dev/null +++ b/src/tic.c @@ -0,0 +1,56 @@ +// +// tic.c +// Common routines for TIC parsers +// +// (C) 2021 Thibaut VARENE +// License: GPLv2 - http://www.gnu.org/licenses/gpl-2.0.html +// + +/** + * @file + * Common routines used by the TIC parsers. + * The code making use of the parsers must provide extra functions as mentioned in the header. + */ + +#include +#include + +#include "tic.h" + +bool filter_mode; ///< if true, switch lexers to configuration parsing +bool *etiq_en; ///< when non-NULL, a token-indexed array, where the related token is emitted if the value is true. @note This could be made a bit field if memory is a concern + +void make_field(struct tic_field *field, const struct tic_etiquette *etiq, char *horodate, char *data) +{ + // args come from the bison stack + int base; + + field->horodate = horodate; + memcpy(&field->etiq, etiq, sizeof(field->etiq)); + + switch ((etiq->unittype & 0xF0)) { + case T_STRING: + field->data.s = data; + return; + case T_HEX: + base = 16; + break; + default: + base = 10; + break; + } + field->data.i = (int)strtol(data, NULL, base); + free(data); +} + +void free_field(struct tic_field *field) +{ + free(field->horodate); + switch ((field->etiq.unittype & 0xF0)) { + case T_STRING: + free(field->data.s); + break; + default: + break; + } +} diff --git a/src/tic.h b/src/tic.h new file mode 100644 index 0000000..f01670a --- /dev/null +++ b/src/tic.h @@ -0,0 +1,100 @@ +// +// tic.h +// Interface for TIC parsers +// +// (C) 2021 Thibaut VARENE +// License: GPLv2 - http://www.gnu.org/licenses/gpl-2.0.html +// + +/** + * @file + * Interface for TIC parsers + * Users of this interface must implement the functions declared at the bottom: + * - print_field() + * - frame_sep() + * - frame_err() + */ + +#ifndef tic_h +#define tic_h + +#include +#include +#include + +#ifdef BAREBUILD + #define pr_err(format, ...) /* nothing */ +#else + #define pr_err(format, ...) fprintf(stderr, "ERREUR: " format, ## __VA_ARGS__) +#endif + +/** + * TIC units. + * @warning The code assumes this fits on 4 bits + */ +enum tic_unit { + U_SANS = 0x00, + U_WH, + U_VARH, + U_A, + U_V, + U_KVA, + U_VA, + U_W, + U_MIN, + U_DAL, +}; + +/** + * TIC data types. + * By default everything is an int. + * @warning The code assumes this is packed in the upper 4 bits of a byte: must increment by 0x10. + */ + enum data_type { + T_STRING = 0x10, + T_HEX = 0x20, +}; + +/** Internal parser representation of a TIC etiquette */ +struct tic_etiquette { + uint8_t tok; ///< bison token number + uint8_t unittype; ///< combined unit and type (see @tic_unit @data_type) + const char *label; ///< TIC "etiquette", as an ASCII string + const char *desc; ///< corresponding TIC long description +}; + +/** Internal parser representation of a TIC field (i.e. body of a dataset) */ +struct tic_field { + struct tic_etiquette etiq; ///< the field "etiquette" + union { + char *s; + int i; + } data; ///< the field data, if any + char *horodate; ///< the field horodate, if any +}; + +void make_field(struct tic_field *field, const struct tic_etiquette *etiq, char *horodate, char *data); +void free_field(struct tic_field *field); + +// The following functions must be provided by the output interface + +/** + * Called for each valid dataset. + * Used to print a TIC dataset in the desired output format. + * @param field the dataset to print + */ +void print_field(const struct tic_field *field); + +/** + * Called after each frame, valid or not. + * Used to print a frame separator in the desired output format. + */ +void frame_sep(void); + +/** + * Called whenever a frame error condition occurs. + * When frames or datasets have errors. + */ +void frame_err(void); + +#endif /* tic_h */ diff --git a/src/tic2json.c b/src/tic2json.c new file mode 100644 index 0000000..a540b96 --- /dev/null +++ b/src/tic2json.c @@ -0,0 +1,487 @@ +// +// tic2json.c +// A tool to turn ENEDIS TIC data into pure JSON +// +// (C) 2021 Thibaut VARENE +// License: GPLv2 - http://www.gnu.org/licenses/gpl-2.0.html +// + +/** + * @file + * Outputs as JSON a series of frames formatted as a list of fields or a dictionary. + * - for list mode, fields are { "label": "xxx", "data": "xxx", horodate: "xxx", "desc": "xxx", "unit": "xxx" } + * - for dict mode, the keys are the label, followed by { "data": "xxx", "horodate": "xxx", "desc": "xxx", "unit": "xxx" } + * with horodate optional, unit and data optional and possibly empty and data being either quoted string or number. + * + * Data errors can result in some/all datasets being omitted in the output frame (e.g. invalid datasets or datasets + * that did not pass checksum are not emitted): the JSON root object can then be empty but is still emitted. + * In dictionary mode the parser will report the frame status as "_tvalide" ("trame valide") followed by either 1 + * for a valid frame or 0 for a frame containing errors (including dataset errors). + * + * Output JSON is guaranteed to always be valid for each frame. By default only frames are separated with newlines. + * + * @note: the program can only parse a single version of the TIC within one execution context. + */ + +#include +#include +#include +#ifndef BAREBUILD + #include // getopt + #include +#endif + +#include "tic.h" +#ifdef TICV01 + #include "ticv01.tab.h" + int ticv01yylex_destroy(); +#endif +#ifdef TICV02 + #include "ticv02.tab.h" + int ticv02yylex_destroy(); +#endif + +#include "tic2json.h" + +#define ticprintf(format, ...) printf(format, ## __VA_ARGS__) + +#ifdef BAREBUILD + #warning BAREBUILD currently requires defining only one version of supported TIC and does not provide main() + #ifdef PRINT2BUF + static char * ticbuf; + static size_t ticbufsize, ticbufavail; + static tic2json_framecb_t ticframecb; + + #include + #undef ticprintf + int ticprintf(const char * restrict format, ...) + { + int ret; + va_list args; + + va_start(args, format); + ret = vsnprintf(ticbuf + (ticbufsize - ticbufavail), ticbufavail, format, args); + va_end(args); + + if (ret>= ticbufavail) { + fprintf(stderr, "ERROR: output buffer too small!\n"); + return ticbufavail; + } + else if (ret < 0) + return ret; + + ticbufavail -= ret; + + return (ret); + } + #endif /* PRINT2BUF */ +#endif /* BAREBUILD */ + +#define TIC2JSON_VER "2.1" + +extern bool filter_mode; +extern bool *etiq_en; + +/** Global configuration details */ +static struct { + const char *idtag; + char framedelims[2]; + char fdelim; + int optflags; + unsigned int skipframes, framecount; + bool ferr; +} tp; + +/** TIC units representation strings */ +static const char * tic_units[] = { + [U_SANS] = "", + [U_WH] = "Wh", + [U_VARH] = "VArh", + [U_A] = "A", + [U_V] = "V", + [U_KVA] = "kVA", + [U_VA] = "VA", + [U_W] = "W", + [U_MIN] = "mn", + [U_DAL] = "daL", +}; + +#ifdef TICV02 +static void print_stge_data(int data) +{ + const char sep = (tp.optflags & TIC2JSON_OPT_CRFIELD) ? '\n' : ' '; + uint32_t d = (uint32_t)data; + + + const char *const of[] = { + "fermé", + "ouvert", + }; + + const char *const coupure[] = { + "fermé", + "ouvert sur surpuissance", + "ouvert sur surtension", + "ouvert sur délestage", + "ouvert sur ordre CPL ou Euridis", + "ouvert sur une surchauffe avec une valeur de courant supérieure au courant de commutation maximal", + "ouvert sur une surchauffe avec une valeur de courant inférieure au courant de commutation maximal", + NULL, + }; + + const char *const euridis[] = { + "désactivée", + "activée sans sécurité", + NULL, + "activée avec sécurité", + }; + + const char *const cpl[] = { + "New/Unlock", + "New/Lock", + "Registered", + NULL, + }; + + const char *const tempo[] = { + "Pas d'annonce", + "Bleu", + "Blanc", + "Rouge", + }; + + const char *const pm[] = { + "pas", + "PM1", + "PM2", + "PM3", + }; + + ticprintf("{ " + "\"Contact sec\": \"%s\",%c" + "\"Organe de coupure\": \"%s\",%c" + "\"État du cache-bornes distributeur\": \"%s\",%c" + "\"Surtension sur une des phases\": \"%ssurtension\",%c" + "\"Dépassement de la puissance de référence\": \"%s\",%c" + "\"Fonctionnement producteur/consommateur\": \"%s\",%c" + "\"Sens de l'énergie active\": \"énergie active %s\",%c" + "\"Tarif en cours sur le contrat fourniture\": \"énergie ventilée sur Index %d\",%c" + "\"Tarif en cours sur le contrat distributeur\": \"énergie ventilée sur Index %d\",%c" + "\"Mode dégradé de l'horloge\": \"horloge %s\",%c" + "\"État de la sortie télé-information\": \"mode %s\",%c" + "\"État de la sortie communication Euridis\": \"%s\",%c" + "\"Statut du CPL\": \"%s\",%c" + "\"Synchronisation CPL\": \"compteur%s synchronisé\",%c" + "\"Couleur du jour pour le contrat historique tempo\": \"%s\",%c" + "\"Couleur du lendemain pour le contrat historique tempo\": \"%s\",%c" + "\"Préavis pointes mobiles\": \"%s en cours\",%c" + "\"Pointe mobile\": \"%s en cours\" }%c" + , + of[d & 0x01], sep, + coupure[(d>>1) & 0x07], sep, + of[(d>>4) & 0x01], sep, + (d>>6) & 0x01 ? "" : "pas de ", sep, + (d>>7) & 0x01 ? "dépassement en cours" : "pas de dépassement", sep, + (d>>8) & 0x01 ? "producteur" : "consommateur", sep, + (d>>9) & 0x01 ? "négative" : "positive", sep, + ((d>>10) & 0x0F) + 1, sep, + ((d>>14) & 0x07) + 1, sep, + (d>>16) & 0x01 ? "en mode dégradée" : "correcte", sep, + (d>>17) & 0x01 ? "standard" : "historique", sep, + euridis[(d>>19) & 0x03], sep, + cpl[(d>>21) & 0x03], sep, + (d>>23) & 0x01 ? "" : " non", sep, + tempo[(d>>24) & 0x03], sep, + tempo[(d>>26) & 0x03], sep, + pm[(d>>28) & 0x03], sep, + pm[(d>>30) & 0x03], sep + ); +} +#endif /* TICV02 */ + +void print_field(const struct tic_field *field) +{ + const char fdictout[] = "%c \"%.8s\": { \"data\": "; + const char flistout[] = "%c{ \"label\": \"%.8s\", \"data\": "; + const char *format; + uint8_t type; + + // filters + if (tp.framecount || + ((tp.optflags & TIC2JSON_OPT_MASKZEROES) && (T_STRING != (field->etiq.unittype & 0xF0)) && (0 == field->data.i)) || + (etiq_en && !etiq_en[field->etiq.tok])) + return; + + format = (tp.optflags & TIC2JSON_OPT_DICTOUT) ? fdictout : flistout; + + ticprintf(format, tp.fdelim, field->etiq.label); + switch (field->etiq.unittype & 0x0F) { + case U_SANS: + type = field->etiq.unittype & 0xF0; + if (T_STRING == type) { + ticprintf("\"%s\"", field->data.s ? field->data.s : ""); + break; + } +#ifdef TICV02 + else if ((T_HEX == type) && (tp.optflags & TIC2JSON_OPT_PARSESTGE)) { + // XXX abuse the fact that STGE is the only U_SANS|T_HEX field + print_stge_data(field->data.i); + break; + } +#endif /* TICV02 */ + // fallthrough + default: + ticprintf("%d", field->data.i); + break; + } + +#ifdef TICV02 + if (field->horodate) { + if (tp.optflags & TIC2JSON_OPT_LONGDATE) { + const char *o, *d = field->horodate; + switch (d[0]) { + default: + case ' ': + o = ""; + break; + case 'E': + case 'e': + o = "+02:00"; + break; + case 'H': + case 'h': + o = "+01:00"; + break; + } + ticprintf(", \"horodate\": \"20%.2s-%.2s-%.2sT%.2s:%.2s:%.2s%s\"", d+1, d+3, d+5, d+7, d+9, d+11, o); + } + else + ticprintf(", \"horodate\": \"%s\"", field->horodate); + } +#endif /* TICV02 */ + + if (tp.optflags & TIC2JSON_OPT_DESCFORM) + ticprintf(", \"desc\": \"%s\", \"unit\": \"%s\"", field->etiq.desc, tic_units[(field->etiq.unittype & 0x0F)]); + + if (tp.idtag) + ticprintf(", \"id\": \"%s\"", tp.idtag); + + ticprintf("}%c", (tp.optflags & TIC2JSON_OPT_CRFIELD) ? '\n': ' '); + + tp.fdelim = ','; +} + +void frame_sep(void) +{ + if (!tp.framecount--) { + tp.framecount = tp.skipframes; + if (tp.optflags & TIC2JSON_OPT_DICTOUT) + ticprintf("%c \"_tvalide\": %d", tp.fdelim, !tp.ferr); +#ifdef PRINT2BUF + ticprintf("%c\n", tp.framedelims[1]); + if (ticframecb) + ticframecb(ticbuf, ticbufsize-ticbufavail, !tp.ferr); + ticbufavail = ticbufsize; // reset buffer + ticprintf("%c", tp.framedelims[0]); +#else + ticprintf("%c\n%c", tp.framedelims[1], tp.framedelims[0]); +#endif + } + tp.fdelim = ' '; + tp.ferr = 0; +} + +void frame_err(void) +{ + tp.ferr = 1; +} + +static inline void ticinit(void) +{ + filter_mode = false; + etiq_en = NULL; + + memset(&tp, 0, sizeof(tp)); + tp.framedelims[0] = '['; tp.framedelims[1] = ']'; + tp.fdelim = ' '; +} + +#ifndef BAREBUILD +static void usage(void) +{ + printf( "usage: " BINNAME " {-1|-2} [-dhlnruVz] [-e fichier] [-i id] [-s N]\n" // FIXME -1|-2 always shown +#ifdef TICV01 + " -1\t\t" "Analyse les trames TIC version 01 \"historique\"\n" +#endif +#ifdef TICV02 + " -2\t\t" "Analyse les trames TIC version 02 \"standard\"\n" +#endif + "\n" + " -d\t\t" "Émet les trames sous forme de dictionaire plutà ́t que de liste\n" + " -e fichier\t" "Utilise pour configurer le filtre d'étiquettes\n" + " -h\t\t" "Montre ce message d'aide et quitte\n" + " -i id\t\t" "Ajoute une balise \"id\" avec la valeur à chaque groupe\n" + " -l\t\t" "Ajoute les descriptions longues et les unitées de chaque groupe\n" + " -n\t\t" "Insà ̈re une nouvelle ligne aprà ̈s chaque groupe\n" + " -r\t\t" "Interprà ̈te les horodates en format RFC3339\n" + " -s N\t\t" "Émet une trame toutes les reçues\n" + " -u\t\t" "Décode le registre de statut sous forme de dictionnaire\n" + " -V\t\t" "Montre la version et quitte\n" + " -z\t\t" "Masque les groupes numériques à zéro\n" + "\n" + "Note: le fichier de configuration du filtre d'étiquettes doit commencer par une ligne comportant\n" + "uniquement la séquence de caractà ̈res suivante: `#ticfilter` (sans les apostrophes), suivi à partir de\n" + "la ligne suivante d'un nombre quelconque d'étiquettes TIC séparées par du blanc (espace, nouvelle ligne, etc).\n" + "Seuls les groupes dont les étiquettes sont ainsi listées seront alors émis par le programme.\n" + ); +} + +#ifdef TICV01 +void parse_config_v01(const char *filename); +#endif + +#ifdef TICV02 +void parse_config_v02(const char *filename); +#endif + +int main(int argc, char **argv) +{ + void (*parse_config)(const char *); + int (*yyparse)(void) = NULL; + int (*yylex_destroy)(void) = NULL; + + const char *fconfig = NULL; + int ch; + + ticinit(); + + while ((ch = getopt(argc, argv, "12de:hi:lnrs:uVz")) != -1) { + switch (ch) { +#ifdef TICV01 + case '1': + if (yyparse) + errx(-1, "ERREUR: Une seule version de TIC peut Ãatre analysée à la fois"); + parse_config = parse_config_v01; + yyparse = ticv01yyparse; + yylex_destroy = ticv01yylex_destroy; + break; +#endif +#ifdef TICV02 + case '2': + if (yyparse) + errx(-1, "ERREUR: Une seule version de TIC peut Ãatre analysée à la fois"); + parse_config = parse_config_v02; + yyparse = ticv02yyparse; + yylex_destroy = ticv02yylex_destroy; + break; +#endif + case 'd': + tp.optflags |= TIC2JSON_OPT_DICTOUT; + tp.framedelims[0] = '{'; tp.framedelims[1] = '}'; + break; + case 'e': + fconfig = optarg; + break; + case 'h': + usage(); + return 0; + case 'i': + tp.idtag = optarg; + break; + case 'l': + tp.optflags |= TIC2JSON_OPT_DESCFORM; + break; + case 'n': + tp.optflags |= TIC2JSON_OPT_CRFIELD; + break; + case 'r': + tp.optflags |= TIC2JSON_OPT_LONGDATE; + break; + case 's': + tp.skipframes = (unsigned int)strtol(optarg, NULL, 10); + break; + case 'u': + tp.optflags |= TIC2JSON_OPT_PARSESTGE; + break; + case 'V': + printf( BINNAME " version " TIC2JSON_VER "\n" + "License GPLv2: GNU GPL version 2 .\n" + "Copyright (C) 2021 Thibaut Varà ̈ne.\n"); + return 0; + case 'z': + tp.optflags |= TIC2JSON_OPT_MASKZEROES; + break; + default: + usage(); + exit(-1); + } + } + argc -= optind; + argv += optind; + + if (!yyparse) + errx(-1, "ERREUR: version TIC non spécifiée"); + + if (fconfig) + parse_config(fconfig); + + putchar(tp.framedelims[0]); + yyparse(); + printf("%c\n", tp.framedelims[1]); + yylex_destroy(); + + free(etiq_en); + return 0; +} + +#else /* BAREBUILD */ + +extern FILE *ticv01yyin; +extern FILE *ticv02yyin; + +#ifdef PRINT2BUF +/** + * tic2json_main(), print to buffer variant. + * @param yyin the FILE to read TIC frames from + * @param optflags bitfield for tuning parser behavior + * @param buf an allocated buffer to write JSON data to + * @param size the size of the buffer + * @param an optional callback to call after each printed frame, before the buffer content is overwritten. + */ +void tic2json_main(FILE * yyin, int optflags, char * buf, size_t size, tic2json_framecb_t cb) +#else +void tic2json_main(FILE * yyin, int optflags) +#endif +{ + ticinit(); + tp.optflags = optflags; + + if (tp.optflags & TIC2JSON_OPT_DICTOUT) { + tp.framedelims[0] = '{'; + tp.framedelims[1] = '}'; + } + +#ifdef PRINT2BUF + ticbuf = buf; + ticbufavail = ticbufsize = size; + ticframecb = cb; +#endif + + ticprintf("%c", tp.framedelims[0]); + +#if defined(TICV01) + ticv01yyin = yyin; + ticv01yyparse(); + ticv01yylex_destroy(); +#elif defined(TICV02) + ticv02yyin = yyin; + ticv02yyparse(); + ticv02yylex_destroy(); +#else + fprintf(stderr, "NO TIC VERSION DEFINED!\n"); // avoid utf-8 +#endif + + ticprintf("%c\n", tp.framedelims[1]); +} + +#endif /* !BAREBUILD */ diff --git a/src/tic2json.h b/src/tic2json.h new file mode 100644 index 0000000..afd70f5 --- /dev/null +++ b/src/tic2json.h @@ -0,0 +1,35 @@ +// +// tic2json.h +// +// +// (C) 2021 Thibaut VARENE +// License: GPLv2 - http://www.gnu.org/licenses/gpl-2.0.html +// + +/** + * @file + * exports for tic2json, mainly useful for embedded applications using tic2json_main(). + */ + +#ifndef tic2json_h +#define tic2json_h + +/** enum for optflags bitfield */ +enum { + TIC2JSON_OPT_MASKZEROES = 0x01, + TIC2JSON_OPT_CRFIELD = 0x02, + TIC2JSON_OPT_DESCFORM = 0x04, + TIC2JSON_OPT_DICTOUT = 0x08, + TIC2JSON_OPT_LONGDATE = 0x10, + TIC2JSON_OPT_PARSESTGE = 0x20, +}; + +/** + * TIC frame callback + * @param buf the buffer received from tic2json_main(), filled with JSON data + * @param size the length of JSON data currently in the buffer + * @param valid true if TIC frame is valid, false otherwise + */ +typedef void (*tic2json_framecb_t)(char * buf, size_t size, bool valid); + +#endif /* tic2json_h */ diff --git a/src/ticv01.l b/src/ticv01.l new file mode 100644 index 0000000..a9d3350 --- /dev/null +++ b/src/ticv01.l @@ -0,0 +1,153 @@ +/* +// ticv01.l +// A lexer for ENEDIS TIC version 01 protocol +// +// (C) 2021 Thibaut VARENE +// License: GPLv2 - http://www.gnu.org/licenses/gpl-2.0.html +*/ + +/** + * @file + * Complete lexer for ENEDIS' TIC protocol version 01. + * Supports version 01, a.k.a "historique" as found on ENEDIS' Linky and Bleus meters. + * Ref doc: https://www.enedis.fr/media/2027/download + * + * "le champ « étiquette » ne contient aucun caractà ̈re ayant une valeur égale à celle + * du caractà ̈re-séparateur utilisé pour la trame (caractà ̈re ASCII « espace » ou caractà ̈re + * ASCII « tabulation horizontale » suivant le cas). Par contre, le champ « donnée » + * contenant l’information fournie par le groupe peut, lui, contenir des caractà ̈res ayant + * une valeur égale à celle du caractà ̈re-séparateur utilisé pour la trame." + * + * Mais NOI-CPT_54E indique toutefois: + * "Note : le caractà ̈re séparateur des champs "Horizontal Tab" HT (0x09), en mode standard + * est different du caractà ̈re séparateur "Space" SP (0x20) en mode historique. + * Cette disposition permet d’utiliser le caractà ̈re “Space” pour les données." + * + * On serait donc fondé à penser qu'en mode historique, ni les étiquettes ni les données + * ne contiennent 0x20. Du moins en sortie Linky "historique". Cette hypothà ̈se est donc + * retenue jusqu'à preuve du contraire. + */ + +/* noyywrap disables automatic rewinding for the next file to parse. Since we + always parse only a single string, there's no need to do any wraps. And + using yywrap requires linking with -lfl, which provides the default yywrap + implementation that always returns 1 anyway. */ +%option noyywrap + +/* nounput simplifies the lexer, by removing support for putting a character + back into the input stream. We never use such capability anyway. */ +%option nounput + +/* we never directly read input */ +%option noinput + +/* we don't need the default rule */ +%option nodefault + +/* nounistd suppresses inclusion of the non-ANSI header file unistd.h. + This option is meant to target environments in which unistd.h does not exist. + Be aware that certain options may cause flex to generate code that relies on + functions normally found in unistd.h, (e.g. isatty(), read().) + If you wish to use these functions, you will have to inform your compiler where + to find them. See option-always-interactive. See option-read. */ +%option nounistd + +/* batch means that we'll never use the generated lexer interactively. */ +%option batch + +/* we only process 7-bit input */ +%option 7bit + +/* Enables debug mode. To see the debug messages, one needs to also set + yy_flex_debug to 1, then the debug messages will be printed on stderr. */ +%option nodebug + +%s FILTER +%x DATA + +DATAC [\x21-\x7e] +CHKSUM [\x20-\x5f] +SEP \x20 + +%{ + #include "tic.h" + #include "ticv01.tab.h" + + extern bool filter_mode; + static uint8_t checksum; + + static void crc_calc(void) + { + for (size_t i = 0; i"#"ticfilter\n { return TICFILTER; } +[ \t\n]+ /* ignore whitespace */ + + /* balises */ +<*>\x02 { BEGIN(INITIAL); return TOK_STX; } +<*>\x03 { BEGIN(INITIAL); return TOK_ETX; } +<*>\x04 { BEGIN(INITIAL); return TOK_EOT; } +<*>\x0a { BEGIN(INITIAL); checksum=0; return FIELD_START; } +<*>{SEP} { checksum += (uint8_t)*yytext; BEGIN(DATA); return TOK_SEP; } +<*>{CHKSUM}\x0d { + checksum -= 0x20; // we have one space too many in the checksum: last SEP + checksum = (checksum & 0x3f) + 0x20; + if (checksum == (uint8_t)yytext[0]) return FIELD_OK; + else return FIELD_KO; + } + + + /* etiquettes - mode historique */ +ADCO { crc_calc(); ticv01yylval.label = "ADCO"; return ET_ADCO; } +OPTARIF { crc_calc(); ticv01yylval.label = "OPTARIF"; return ET_OPTARIF; } +ISOUSC { crc_calc(); ticv01yylval.label = "ISOUSC"; return ET_ISOUSC; } +BASE { crc_calc(); ticv01yylval.label = "BASE"; return ET_BASE; } +HCHC { crc_calc(); ticv01yylval.label = "HCHC"; return ET_HCHC; } +HCHP { crc_calc(); ticv01yylval.label = "HCHP"; return ET_HCHP; } +EJPHN { crc_calc(); ticv01yylval.label = "EJPHN"; return ET_EJPHN; } +EJPHPM { crc_calc(); ticv01yylval.label = "EJPHPM"; return ET_EJPHPM; } +BBRHCJB { crc_calc(); ticv01yylval.label = "BBRHCJB"; return ET_BBRHCJB; } +BBRHPJB { crc_calc(); ticv01yylval.label = "BBRHPJB"; return ET_BBRHPJB; } +BBRHCJW { crc_calc(); ticv01yylval.label = "BBRHCJW"; return ET_BBRHCJW; } +BBRHPJW { crc_calc(); ticv01yylval.label = "BBRHPJW"; return ET_BBRHPJW; } +BBRHCJR { crc_calc(); ticv01yylval.label = "BBRHCJR"; return ET_BBRHCJR; } +BBRHPJR { crc_calc(); ticv01yylval.label = "BBRHPJR"; return ET_BBRHPJR; } +PEJP { crc_calc(); ticv01yylval.label = "PEJP"; return ET_PEJP; } +PTEC { crc_calc(); ticv01yylval.label = "PTEC"; return ET_PTEC; } +DEMAIN { crc_calc(); ticv01yylval.label = "DEMAIN"; return ET_DEMAIN; } +IINST { crc_calc(); ticv01yylval.label = "IINST"; return ET_IINST; } +IINST1 { crc_calc(); ticv01yylval.label = "IINST1"; return ET_IINST1; } +IINST2 { crc_calc(); ticv01yylval.label = "IINST2"; return ET_IINST2; } +IINST3 { crc_calc(); ticv01yylval.label = "IINST3"; return ET_IINST3; } +ADPS { crc_calc(); ticv01yylval.label = "ADPS"; return ET_ADPS; } +IMAX { crc_calc(); ticv01yylval.label = "IMAX"; return ET_IMAX; } +IMAX1 { crc_calc(); ticv01yylval.label = "IMAX1"; return ET_IMAX1; } +IMAX2 { crc_calc(); ticv01yylval.label = "IMAX2"; return ET_IMAX2; } +IMAX3 { crc_calc(); ticv01yylval.label = "IMAX3"; return ET_IMAX3; } +PMAX { crc_calc(); ticv01yylval.label = "PMAX"; return ET_PMAX; } +PAPP { crc_calc(); ticv01yylval.label = "PAPP"; return ET_PAPP; } +HHPHC { crc_calc(); ticv01yylval.label = "HHPHC"; return ET_HHPHC; } +MOTDETAT { crc_calc(); ticv01yylval.label = "MOTDETAT"; return ET_MOTDETAT; } +PPOT { crc_calc(); ticv01yylval.label = "PPOT"; return ET_PPOT; } +ADIR1 { crc_calc(); ticv01yylval.label = "ADIR1"; return ET_ADIR1; } +ADIR2 { crc_calc(); ticv01yylval.label = "ADIR2"; return ET_ADIR2; } +ADIR3 { crc_calc(); ticv01yylval.label = "ADIR3"; return ET_ADIR3; } +GAZ { crc_calc(); ticv01yylval.label = "GAZ"; return ET_GAZ; } +AUTRE { crc_calc(); ticv01yylval.label = "AUTRE"; return ET_AUTRE; } + + +{DATAC}+ { crc_calc(); ticv01yylval.text = strdup(yytext); return TOK_DATA; } + +<*>. { if (yy_flex_debug) pr_err("spurious character 0x%02hhx\n", *yytext); return *yytext; } + +<> { yyterminate(); } +%% diff --git a/src/ticv01.y b/src/ticv01.y new file mode 100644 index 0000000..f773c80 --- /dev/null +++ b/src/ticv01.y @@ -0,0 +1,181 @@ +// +// ticv01.y +// A parser for ENEDIS TIC version 01 protocol +// +// (C) 2021 Thibaut VARENE +// License: GPLv2 - http://www.gnu.org/licenses/gpl-2.0.html +// + +/** + * @file + * This parser implements a complete grammar that supports TIC version 01 + * as specified in Enedis-NOI-CPT_02E.pdf version 6. + * + * This parser does not allocate memory, except if a filter configuration is used in + * which case the etiq_en array will be allocated (it's a few hundred bytes). + * A left-recursion grammar has been implemented to keep the memory usage to the bare + * minimum as well. As a tradeoff, valid datasets are always emitted regardless of the + * overall status of the containing frame. + * + * Compteurs supportés: + * - Linky (mode historique) + * - Bleu monophasé (CBEMM / CBEMM ICC) + * - Bleu triphasé (CBETM) + * - Concentrateur de téléreport + * + * Pas de support des compteurs Jaune, Emeraude, PME-PMI ou Saphir. + */ + +%{ +#include +#include "tic.h" + +int ticv01yylex(); +int ticv01yylex_destroy(); +extern FILE *ticv01yyin; +static void yyerror(const char *); + +extern bool filter_mode; +extern bool *etiq_en; +%} + +%union { + char *text; + const char *label; + struct tic_etiquette etiq; + struct tic_field field; +} + +%verbose + +%token TOK_STX TOK_ETX TOK_EOT TOK_SEP +%token FIELD_START FIELD_OK FIELD_KO +%token TICFILTER + +%token TOK_DATA + +%token