/* 
 * modbuslog
 *
 * Copyright (C) 2011 Jonathan McCrohan
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

// gcc modbuslog.c -o modbuslog `pkg-config --libs --cflags libmodbus libconfig`

#ifndef VERSION_STRING
#define VERSION_STRING "[undefined version]"
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <sys/select.h>
#include <signal.h>
#include <fcntl.h>

#include <libconfig.h>
#include <modbus.h>

#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <string.h>

#include <syslog.h>

// handle SIGALRM by resetting it
void minute_check(int signum) {
	alarm(60);
}

// get mac address of primary interface eth0
char *mac_address() {
	int s;
	struct ifreq ifr;

	s = socket(PF_INET, SOCK_DGRAM, 0);
	memset(&ifr, 0x00, sizeof(ifr));
	strcpy(ifr.ifr_name, "eth0");
	ioctl(s, SIOCGIFHWADDR, &ifr);
	close(s);

	static char mac_address[12];
	sprintf(mac_address, "%.2X%.2X%.2X%.2X%.2X%.2X",
			(unsigned char) ifr.ifr_hwaddr.sa_data[0],
			(unsigned char) ifr.ifr_hwaddr.sa_data[1],
			(unsigned char) ifr.ifr_hwaddr.sa_data[2],
			(unsigned char) ifr.ifr_hwaddr.sa_data[3],
			(unsigned char) ifr.ifr_hwaddr.sa_data[4],
			(unsigned char) ifr.ifr_hwaddr.sa_data[5]);

	return mac_address;
}

int main(int argc, char *argv[]) {

	int DEBUG = 0;
	int SYSLOG_CONSOLE_OUTPUT = 0;
	int k;

	// check the argv array for strings matching -d
	for (k = 1; k < argc; k++) {
		if (strcmp(argv[k], "-d") == 0) {
			DEBUG = 1;
			SYSLOG_CONSOLE_OUTPUT = LOG_PERROR;
		}
	}

	openlog("modbuslog", SYSLOG_CONSOLE_OUTPUT | LOG_PID | LOG_CONS, LOG_USER);

	syslog(LOG_INFO, "");
	syslog(LOG_INFO, "modbuslog [%s] starting", VERSION_STRING);
	syslog(LOG_INFO, "");

	const char *configfile = "/etc/modbuslog.cfg";
	config_t cfg;
	//config_setting_t *setting;

	const char *modbus_device_address;
	int modbus_baud_rate;
	int modbus_data_bits;
	const char *modbus_parity;
	int modbus_stop_bits;
	int modbus_retry;

	config_init(&cfg);

	// attempt to read config
	// loads entire file to memory and destroys file descriptor
	if (!config_read_file(&cfg, configfile)) {
		fprintf(stderr, "%s:%d - %s\n", config_error_file(&cfg),
				config_error_line(&cfg), config_error_text(&cfg));
		config_destroy(&cfg);
		syslog(LOG_ERR, "Unable to find configfile");
		return -1;
	} else {
		syslog(LOG_INFO, "configfile found successfully");
	}

	// die if core config file options aren't there
	if (!(config_lookup_string(&cfg, "modbus.device", &modbus_device_address)
			&& config_lookup_int(&cfg, "modbus.baud", &modbus_baud_rate)
			&& config_lookup_int(&cfg, "modbus.data_bits", &modbus_data_bits)
			&& config_lookup_string(&cfg, "modbus.parity", &modbus_parity)
			&& config_lookup_int(&cfg, "modbus.stop_bits", &modbus_stop_bits)
			&& config_lookup_int(&cfg, "modbus.retry", &modbus_retry))) {
		syslog(LOG_ERR,
				"Incomplete modbus configuration. Check configuration file");
		closelog();
		return -1;
	}

	syslog(LOG_INFO, "MAC=[%s]", mac_address());
	syslog(LOG_INFO, "Serial Port=[%s]", modbus_device_address);
	syslog(LOG_INFO, "Serial Port Parameters=[%d %d %c %d]", modbus_baud_rate,
			modbus_data_bits, modbus_parity[0], modbus_stop_bits);

	modbus_t *ctx;
	uint16_t tab_reg[4];
	uint8_t byte[8];
	int rc;
	int i;

	// used to force a reading 2 mins after power on
	int firstrun;
	firstrun = 1;

	// SIGALRM used to wake for loop up every 60 secs
	// sleep puts whole thread to sleep which isn't what we want
	// other methods involve CPU spinlocks which are inefficient
	signal(SIGALRM, minute_check);
	alarm(60);

	for (;;) {
		// block until SIGARLM
		select(0, NULL, NULL, NULL, NULL);

		time_t t = time(NULL);
		struct tm unixtime_min_time_t = *gmtime (&t);
		unixtime_min_time_t.tm_sec = 0;
		time_t unixtime_min = mktime(&unixtime_min_time_t);
		//int unixtime = (int) t;
		//printf("%d\n", unixtime);

		config_setting_t *readings;

		readings = config_lookup(&cfg, "reading");

		// find number of required readings
		unsigned int num_readings = config_setting_length(readings);

		int i;

		// cycle through each reading and pull info from config file
		for (i = 0; i < num_readings; ++i) {
			config_setting_t *register_element = config_setting_get_elem(
					readings, i);
			int slaveid;
			config_setting_lookup_int(register_element, "slaveid", &slaveid);
			int intervalvalue;
			config_setting_lookup_int(register_element, "intervalvalue",
					&intervalvalue);
			//printf("%d", intervalvalue);

			int registertype;
			config_setting_lookup_int(register_element, "registertype",
					&registertype);

			int startbyte;
			config_setting_lookup_int(register_element, "startbyte",
					&startbyte);

			int numbytes;
			config_setting_lookup_int(register_element, "numbytes",
					&numbytes);

			const char *intervalunit;
			config_setting_lookup_string(register_element, "intervalunit",
					&intervalunit);

			//printf("%c", intervalunit[0]);

			int intervalduration;

			if (intervalunit[0] == 'h')
				intervalduration = intervalvalue * 3600;
			else
				intervalduration = intervalvalue * 60;

			// if we match the required time for the reading or first run
			if ((unixtime_min % intervalduration == 0) || firstrun) {
				//printf("specified minute.  %d %d\n", unixtime_min,
				//              intervalduration);

				// attempt to create new modbus connection
				ctx = modbus_new_rtu(modbus_device_address, modbus_baud_rate,
						modbus_parity[0], modbus_data_bits, modbus_stop_bits);

				if (ctx == NULL) {
					syslog(LOG_ERR, "Unable to create libmodbus object");
				}

				modbus_set_slave(ctx, slaveid);
				modbus_set_debug(ctx, DEBUG);

				if (modbus_connect(ctx) == -1) {
					syslog(LOG_ERR, "libmodbus: Connection failed: [%s]",
							modbus_strerror(errno));
					modbus_free(ctx);
					// if connection fails, skip rest of loop
					syslog(
							LOG_ERR,
							"Skipping read from slaveid=[%i], registertype=[%i], startbyte=[%i], numbytes=[%i]",
							slaveid, registertype, startbyte, numbytes);
					continue;
				}

				int retry = 0;

				// zero tab_reg before use
				memset(tab_reg, 0, sizeof(tab_reg));

				// convert bytes to registers
				int nummodbusregisters;
				nummodbusregisters = (((numbytes-1)/2)+1);

				int startmodbusregister;
				startmodbusregister = (startbyte/2);
				// handle timeouts and retries
				do {
					switch (registertype) {

					case 3:
						rc = modbus_read_registers(ctx, startmodbusregister,
								nummodbusregisters, tab_reg);
						break;

					case 4:
						rc = modbus_read_input_registers(ctx, startmodbusregister,
								nummodbusregisters, tab_reg);
						break;

					default:
						rc = modbus_read_registers(ctx, startmodbusregister,
								nummodbusregisters, tab_reg);
						break;
					}
					if (rc == -1) {
						syslog(LOG_ERR, "libmodbus error: [%s]",
								modbus_strerror(errno));
						sleep(1);
					}
					retry++;
				} while ((rc == -1) && (retry < (modbus_retry + 1)));

				if (rc == -1){
					// retried, but still no luck
					continue;
				}

				//MODBUS_GET_HIGH_BYTE(data);
				//MODBUS_GET_LOW_BYTE(data);
				//MODBUS_SET_INT16_TO_INT8(tab_int8, index, value);

				// round forward to next midnight
				struct tm midnight = *localtime(&t);
				midnight.tm_hour = 0;
				midnight.tm_min = 0;
				midnight.tm_sec = 0;
				midnight.tm_mday += 1;
				mktime (&midnight);

				char log_filename[50];

				//<mac address>_YYYY_MM_DD_HH_MM_SS.log
				sprintf(log_filename,
						"/var/modbuslog/%s_%04i_%02i_%02i_%02i_%02i_%02i.log",
						mac_address(), midnight.tm_year + 1900,
						midnight.tm_mon + 1, midnight.tm_mday, midnight.tm_hour,
						midnight.tm_min, midnight.tm_sec);

				//printf("%s\n",filename);

				FILE *fp = fopen(log_filename, "r");
				if (fp) {
					fclose(fp);
				} else {
					// file doesn't exist. create it.
					syslog(LOG_NOTICE, "logfile does not exist");
					syslog(LOG_INFO, "creating file: [%s]", log_filename);
					FILE *fp = fopen(log_filename, "w");
					fprintf(
							fp,
							"IntervalID|UTCDate|UTCTime|LOCALDate|LOCALTime|SensorID|RegisterID|Reading\n");
					fclose(fp);
				}

				FILE *filehandle = fopen(log_filename, "a+");

				syslog(LOG_DEBUG, "opening file for append: [%s]",
						log_filename);

				int32_t registervalue = 0;

				int p;

				// zero byte before use
				memset(byte, 0, sizeof(byte));

				// data arrives in words, split into bytes
				for (p = 0; p < nummodbusregisters; p++) {
					MODBUS_SET_INT16_TO_INT8(byte, p * 2, tab_reg[p]);
					//registervalue += tab_reg[i] + tab_reg[i + 1];
				}

				// bitshifting magic
				switch (numbytes) {
				case 1:
					registervalue = (startbyte % 2 == 0) ? byte[0] : byte[1];
					break;
				case 2:
					registervalue = ((byte[0]) << 8) + byte[1];
					break;
				case 4:
					registervalue = ((byte[0]) << 24) + ((byte[1]) << 16)
							+ ((byte[2]) << 8) + byte[3];
					break;
				default:
					registervalue = ((byte[0]) << 8) + byte[1];
					break;
				}

				struct tm utc = *gmtime(&unixtime_min);
				struct tm lc = *localtime(&unixtime_min);

				int intervalid;
				char interval_filename[50];
				sprintf(interval_filename,
						"/var/modbuslog/interval/interval.txt");

				syslog(LOG_DEBUG, "opening interval file: [%s]",
						interval_filename);

				FILE *intervalfile = fopen(interval_filename, "r+");

				if (intervalfile) {
					fclose(intervalfile);
				} else {
					// file doesn't exist. create it.
					syslog(LOG_NOTICE, "interval file does not exist");
					syslog(LOG_INFO, "attempting to create file: [%s]",
							interval_filename);
					intervalfile = fopen(interval_filename, "w");
					fprintf(intervalfile, "0\n");
					fclose(intervalfile);
				}

				// file now exists, try opening again
				intervalfile = fopen(interval_filename, "r+");

				fscanf(intervalfile, "%d", &intervalid);
				//handle 32bit signed overflow
				if (intervalid >= 2147483647) {
					intervalid = 0;
				} else {
					intervalid++;
				}
				rewind(intervalfile);
				fprintf(intervalfile, "%d", intervalid);
				fclose(intervalfile);

				fprintf(
						filehandle,
						"%i|%04i%02i%02i|%02i%02i%02i|%04i%02i%02i|%02i%02i%02i|%i|%i|%i\n",
						intervalid, utc.tm_year + 1900, utc.tm_mon + 1,
						utc.tm_mday, utc.tm_hour, utc.tm_min, utc.tm_sec,
						lc.tm_year + 1900, lc.tm_mon + 1, lc.tm_mday,
						lc.tm_hour, lc.tm_min, lc.tm_sec, slaveid, startbyte,
						registervalue);
				fclose(filehandle);

				syslog(
						LOG_DEBUG,
						"[%i|%04i%02i%02i|%02i%02i%02i|%04i%02i%02i|%02i%02i%02i|%i|%i|%i]\n",
						intervalid, utc.tm_year + 1900, utc.tm_mon + 1,
						utc.tm_mday, utc.tm_hour, utc.tm_min, utc.tm_sec,
						lc.tm_year + 1900, lc.tm_mon + 1, lc.tm_mday,
						lc.tm_hour, lc.tm_min, lc.tm_sec, slaveid, startbyte,
						registervalue);

				modbus_close(ctx);
				modbus_free(ctx);

				sleep(1);
				//return 0;

			}
			//printf("%d ", slaveid);
		}
		// revert to normal timing
		firstrun = 0;
		//printf("%d\n", unixtime_min);
	}
}