initial release

This commit is contained in:
Gilles Grandou 2021-08-04 12:24:08 +02:00
parent fe267a81e2
commit bb9da629fb
12 changed files with 883 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
tic2mqtt
*.d
*.o

24
Makefile Normal file
View File

@ -0,0 +1,24 @@
SRCS = tic2mqtt.c tic.c mqtt.c homeassistant.c logger.c
OBJS = $(SRCS:%.c=%.o)
LIBS = -lmosquitto -ljson-c
CFLAGS += -Wall -Werror
DEPFLAGS = -MT $@ -MMD -MP -MF .$*.d
DEPS=$(SRCS:%.c=.%.d)
CFLAGS += $(DEPFLAGS)
all: tic2mqtt
tic2mqtt: $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
clean:
rm -f tic2mqtt $(OBJS) $(DEPS)
$(OBJS): $(MAKEFILE_LIST)
-include $(DEPS)
.PHONY: all clean

View File

@ -5,4 +5,15 @@ tic2mqtt is a MQTT client for Linky Electricity Meters installed by
Enedis in France. Hence the rest of this document is in french.
References
==========
Librement inspiré du code de pmelly93: https://github.com/pmelly93/tic2mqtt
Specification Téléinformation Client - Enedis:
* Linky: https://www.enedis.fr/sites/default/files/Enedis-NOI-CPT_54E.pdf
* Pre-linky: https://www.enedis.fr/sites/default/files/Enedis-NOI-CPT_02E.pdf
Spécification du champ ADS:
https://euridis.org/wp-content/uploads/IdentifiantsEuridisListeCCTTV304A_143027.pdf

112
homeassistant.c Normal file
View File

@ -0,0 +1,112 @@
#include "homeassistant.h"
#include "mqtt.h"
#include "logger.h"
#include <json-c/json.h>
#include <stddef.h>
#include <stdio.h>
typedef struct {
const char *tag; // TIC Tag
const char *state_class; // HA State Class
const char *device_class; // HA Device Class
const char *name; // HA name
const char *unit; // HA unit
const bool last_reset; // HA LastReset present
const char *value_template; // HA Template for value
} ha_config_desc;
static ha_config_desc ha_config_descs[] = {
{ "PAPP", "measurement", "power", "Puissance Apparente", "VA", },
{ "IINST", "measurement", "current", "Intensité Instantanée", "A", },
{ "HCHP", "measurement", "energy", "Energie Heures Pleines", "kWh", true, "{{ (value | float) / 1000 }}" },
{ "HCHC", "measurement", "energy", "Energie Heures Creuses", "kWh", true, "{{ (value | float) / 1000 }}" },
{ "PTEC", NULL, NULL, "Période Tarifaire en cours", NULL, false,
"{% if value == \"TH..\" %}Toutes les Heures"
"{% elif value == \"HC..\" %}Heures Creuses"
"{% elif value == \"HP..\" %}Heures Pleines"
"{% elif value == \"HN..\" %}Heures Normales"
"{% elif value == \"PM..\" %}Heures de Pointe Mobile"
"{% else %}{{value}}"
"{% endif %}" },
{ NULL }
};
/* workaround for libjson-c 0.12 which always escape
* strings while serializing
*/
static void unescape_str(const char *str)
{
const char *s;
char *d;
for (s = str, d = (char*)str; *s; s++, d++) {
if (*s == '\\' && *(s+1) == '/')
s++;
*d = *s;
}
*d = '\0';
}
void ha_config_init(const char *tic_name, struct mosquitto *mosq_tic)
{
ha_config_desc *desc;
// char payload[512];
json_object *device = json_object_new_object();
json_object *dev_ids = json_object_new_array();
json_object_array_add(dev_ids, json_object_new_string(tic_name));
json_object_object_add(device, "identifiers", dev_ids);
json_object_object_add(device, "manufacturer", json_object_new_string("Enedis"));
json_object_object_add(device, "model", json_object_new_string("Linky Monophasé"));
json_object_object_add(device, "name", json_object_new_string(tic_name));
json_object_object_add(device, "sw_version", json_object_new_string("0.0.1"));
for (desc = &ha_config_descs[0]; desc->tag != NULL; desc++) {
char topic[TOPIC_MAXLEN+1];
char vtopic[TOPIC_MAXLEN+1];
char uid[TOPIC_MAXLEN+1];
char name[TOPIC_MAXLEN+1];
snprintf(topic, TOPIC_MAXLEN, "homeassistant/sensor/%s/%s/config", tic_name, desc->tag);
snprintf(vtopic, TOPIC_MAXLEN, "tic2mqtt/%s/%s", tic_name, desc->tag);
snprintf(name, TOPIC_MAXLEN, "%s %s", tic_name, desc->name);
snprintf(uid, TOPIC_MAXLEN, "tic2mqtt_%s_%s", tic_name, desc->tag);
json_object *obj = json_object_new_object();
json_object_object_add(obj, "device", json_object_get(device));
json_object_object_add(obj, "name", json_object_new_string(name));
json_object_object_add(obj, "unique_id", json_object_new_string(uid));
json_object_object_add(obj, "state_topic", json_object_new_string(vtopic));
if (desc->state_class)
json_object_object_add(obj, "state_class", json_object_new_string(desc->state_class));
if (desc->device_class)
json_object_object_add(obj, "device_class", json_object_new_string(desc->device_class));
if (desc->unit)
json_object_object_add(obj, "unit_of_measurement", json_object_new_string(desc->unit));
if (desc->value_template)
json_object_object_add(obj, "value_template", json_object_new_string(desc->value_template));
if (desc->last_reset) {
json_object_object_add(obj, "last_reset_topic", json_object_new_string(vtopic));
json_object_object_add(obj, "last_reset_value_template", json_object_new_string("1970-01-01T00:00:00+00:00"));
}
const char *payload = json_object_to_json_string(obj);
unescape_str(payload);
log_info("%s\n%s\n", topic, payload);
if (mosq_tic) {
int res = mqtt_publish(mosq_tic, topic, NULL, payload, TIC_QOS);
if (res != 0)
log_error("Cannot publish topic %s: %s\n", topic, mqtt_strerror(res));
}
json_object_put(obj);
}
json_object_put(device);
}

8
homeassistant.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef __HOME_ASSISTANT_H__
#define __HOME_ASSISTANT_H__
#include "mosquitto.h"
extern void ha_config_init(const char *tic_name, struct mosquitto *mosq_tic);
#endif

32
logger.c Normal file
View File

@ -0,0 +1,32 @@
#include "logger.h"
#include <stdio.h>
#include <stdarg.h>
static char* log_level_str[] = {
[LOG_DEBUG] = "DEBUG",
[LOG_INFO] = "INFO",
[LOG_WARNING] = "WARNING",
[LOG_ERROR] = "ERROR",
};
static int log_level;
void set_loglevel(int level)
{
log_level = level;
}
void logger(int priority, const char *fmt, ...)
{
if (priority < log_level)
return;
printf("%s: ", log_level_str[priority]);
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
}

17
logger.h Normal file
View File

@ -0,0 +1,17 @@
#ifndef __LOGGER_H__
#define __LOGGER_H__
#define LOG_DEBUG 1
#define LOG_INFO 2
#define LOG_WARNING 3
#define LOG_ERROR 4
#define log_debug(...) logger(LOG_DEBUG, __VA_ARGS__)
#define log_info(...) logger(LOG_INFO, __VA_ARGS__)
#define log_warning(...) logger(LOG_WARNING, __VA_ARGS__)
#define log_error(...) logger(LOG_ERROR, __VA_ARGS__)
extern void set_loglevel(int level);
extern void logger(int priority, const char *fmt, ...);
#endif

91
mqtt.c Normal file
View File

@ -0,0 +1,91 @@
#include "mqtt.h"
#include "logger.h"
#include <mosquitto.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
static void mosq_log_callback(struct mosquitto *mosq, void *userdata, int level, const char *str)
{
switch (level) {
case MOSQ_LOG_WARNING: log_warning("%s", str); break;
case MOSQ_LOG_ERR: log_error("%s", str); break;
}
}
struct mosquitto *mqtt_open(const char *host, int port, int keepalive)
{
struct mosquitto *mosq;
int res;
mosquitto_lib_init();
/* Create MQTT client. */
mosq = mosquitto_new(NULL, 1, NULL);
if (mosq == NULL) {
log_error("Cannot create mosquitto client: %s", strerror(errno));
return NULL;
}
mosquitto_log_callback_set(mosq, mosq_log_callback);
/* Connect to broker. */
res = mosquitto_connect(mosq, host, port, keepalive);
if (res != MOSQ_ERR_SUCCESS) {
log_error("Unable to connect to MQTT broker %s:%d: %s", host, port, mosquitto_strerror(res));
return NULL;
}
/* Start network loop. */
res = mosquitto_loop_start(mosq);
if (res != MOSQ_ERR_SUCCESS) {
log_error("Unable to start loop: %s", mosquitto_strerror(res));
return NULL;
}
return mosq;
}
void mqtt_close(struct mosquitto *mosq)
{
mosquitto_loop_stop(mosq, 1);
mosquitto_disconnect(mosq);
mosquitto_destroy(mosq);
mosquitto_lib_cleanup();
}
int mqtt_publish(struct mosquitto *mosq, const char *topic_prefix, const char *topic_suffix, const void *payload, int qos)
{
char topic[TOPIC_MAXLEN + 1];
int res;
if (topic_prefix != NULL) {
if (topic_suffix != NULL) {
sprintf(topic, "%s%s", topic_prefix, topic_suffix);
} else {
sprintf(topic, "%s", topic_prefix);
}
} else {
if (topic_suffix != NULL) {
sprintf(topic, "%s", topic_suffix);
} else {
return -1;
}
}
res = mosquitto_publish(mosq, NULL, topic, strlen(payload), payload, qos, 1);
if (res != 0)
log_error("Cannot publish topic %s: %s\n", topic, mosquitto_strerror(res));
return res;
}
const char *mqtt_strerror(int err)
{
return mosquitto_strerror(err);
}

13
mqtt.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef __MQTT_H__
#define __MQTT_H__
#define TOPIC_MAXLEN 255
#define TIC_QOS 0
extern struct mosquitto *mqtt_open(const char *host, int port, int keepalive);
extern void mqtt_close(struct mosquitto *mosq);
extern int mqtt_publish(struct mosquitto *mosq, const char *topic_prefix, const char *topic_suffix, const void *payload, int qos);
extern const char *mqtt_strerror(int err);
#endif

326
tic.c Normal file
View File

@ -0,0 +1,326 @@
#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);
}
}

19
tic.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef __TIC_H__
#define __TIC_H__
#include <time.h>
#define TIC_MODE_HIST 0
#define TIC_MODE_STD 1
typedef struct tic_info_s tic_info_t;
typedef void (tic_cb_data_f)(const char *label, const char *data, time_t date);
extern tic_info_t* tic_init(const char *fname);
extern void tic_set_cb_data(tic_info_t *tic, tic_cb_data_f *func);
extern int tic_mode(tic_info_t *tic);
extern int tic_read_frame(tic_info_t *tic);
extern void tic_process_frame(tic_info_t *tic);
extern void tic_exit(tic_info_t *tic);
#endif

227
tic2mqtt.c Normal file
View File

@ -0,0 +1,227 @@
#include "logger.h"
#include "tic.h"
#include "mqtt.h"
#include "homeassistant.h"
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define TIC2MQTT_VERSION "1.0.1"
#define DEFAULT_TTY "/dev/ttyUSB0"
#define DEFAULT_PORT 1883
#define DEFAULT_KEEPALIVE 60
#define DEFAULT_TIC_NAME "Linky"
static tic_info_t *tic_info = NULL;
struct tag_desc {
const char *tag; // Name of tag.
const int len; // Length of data.
time_t stamp; // last time received
char *data; // Last data received.
};
static struct tag_desc tag_descs[] =
{
{ "ADCO", 12 },
{ "OPTARIF", 4 },
{ "ISOUSC", 2 },
{ "BASE", 9 },
{ "HCHC", 9 },
{ "HCHP", 9 },
{ "EJPHN", 9 },
{ "EJPHPM", 9 },
{ "BBRHCJB", 9 },
{ "BBRHPJB", 9 },
{ "BBRHCJW", 9 },
{ "BBRHPJW", 9 },
{ "BBRHCJR", 9 },
{ "BBRHPJR", 9 },
{ "PEJP", 2 },
{ "PTEC", 4 },
{ "DEMAIN", 4 },
{ "IINST", 3 },
{ "ADPS", 3 },
{ "IMAX", 3 },
{ "PAPP", 5 },
{ "HHPHC", 1 },
{ "MOTDETAT", 6 },
{ NULL, 0 } /* End of table marker. */
};
static struct mosquitto *mosq_tic = NULL;
static int log_level = LOG_WARNING;
static int ha_config = 0;
char *tic_name = DEFAULT_TIC_NAME;
static void tic2mqtt_process_group(const char *tag, const char *data, time_t date)
{
struct tag_desc *ptag_desc;
for (ptag_desc = tag_descs; ptag_desc->tag != NULL; ptag_desc++) {
if (strcmp(ptag_desc->tag, tag) == 0) {
if (ptag_desc->data == NULL) {
ptag_desc->data = calloc(1, ptag_desc->len + 1);
if (ptag_desc->data == NULL) {
log_error("Cannot alloc data for tag %s: %s\n", tag, strerror(errno));
}
}
time_t stamp = time(NULL);
double elapsed = difftime(stamp, ptag_desc->stamp);
if (elapsed >= 60 || strcmp(ptag_desc->data, data)) {
ptag_desc->stamp = stamp;
strncpy(ptag_desc->data, data, ptag_desc->len);
char topic[TOPIC_MAXLEN + 1];
snprintf(topic, TOPIC_MAXLEN, "tic2mqtt/%s/%s", tic_name, tag);
log_info("%ld %s %s\n", stamp, topic, data);
if (mosq_tic) {
int res = mqtt_publish(mosq_tic, topic, NULL, data, TIC_QOS);
if (res != 0)
log_error("Cannot publish topic %s: %s\n", topic, mqtt_strerror(res));
}
}
return; // No more processing.
}
}
}
static void sighandler(int signum)
{
log_info("Catch signal #%d (%s)\n", signum, strsignal(signum));
exit(EXIT_SUCCESS);
}
static void cleanup(void)
{
tic_exit(tic_info);
if (mosq_tic)
mqtt_close(mosq_tic);
}
static void usage(const char *progname)
{
fprintf(stderr, "Usage: %s [-Hv] [-a] [-t tty] [-n name] [-h host] [-p port] [-k keepalive]\n", progname);
}
static void set_progname(char *argv0)
{
char *p;
if ((p = strrchr(argv0, '/')) != NULL)
strcpy(argv0, p + 1); // argv[0] contains a slash.
}
int main(int argc, char *argv[])
{
int opt;
const char *tty = DEFAULT_TTY;
const char *host = NULL;
int port = DEFAULT_PORT;
int keepalive = DEFAULT_KEEPALIVE;
set_progname(argv[0]);
/* Decode options. */
opterr = 1;
while ((opt = getopt(argc, argv, "vdat:n:h:p:k:H")) != -1) {
switch (opt) {
case 'v':
log_level = LOG_INFO;
break;
case 'd':
log_level = LOG_DEBUG;
break;
case 'a':
ha_config = 1;
break;
case 't':
tty = optarg;
break;
case 'n':
tic_name = optarg;
break;
case 'h':
host = optarg;
break;
case 'p':
port = atoi(optarg);
break;
case 'k':
keepalive = atoi(optarg);
break;
case 'H':
printf("version " TIC2MQTT_VERSION "\n");
usage(argv[0]);
exit(EXIT_SUCCESS);
break;
default:
usage(argv[0]);
exit(EXIT_FAILURE);
break;
}
}
atexit(cleanup);
signal(SIGINT, sighandler);
signal(SIGQUIT, sighandler);
signal(SIGTERM, sighandler);
signal(SIGHUP, sighandler);
if (host) {
mosq_tic = mqtt_open(host, port, keepalive);
if (mosq_tic == NULL)
return EXIT_FAILURE;
}
tic_info = tic_init(tty);
if (tic_info == NULL)
return EXIT_FAILURE;
if (ha_config)
ha_config_init(tic_name, mosq_tic);
tic_set_cb_data(tic_info, tic2mqtt_process_group);
for (;;) {
if (tic_read_frame(tic_info) < 0)
return EXIT_FAILURE;
tic_process_frame(tic_info);
}
return EXIT_SUCCESS;
}