/*
Damon Hart-Davis licenses this file to you
under the Apache Licence, Version 2.0 (the "Licence");
you may not use this file except in compliance
with the Licence. You may obtain a copy of the Licence at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the Licence is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the Licence for the
specific language governing permissions and limitations
under the Licence.

Author(s) / Copyright (s): Damon Hart-Davis 2016
*/

//static const char *Id = "$Id: mbaccess.cpp 36084 2020-08-21 13:42:13Z dhd $";

/*
MODBUS access routines/wrapper.
*/

#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "mbaccess.h"


// Default (USB) wired serial connection to SS-MPPT-15L.
const char *DEFAULT_MODBUS_DEV = "/dev/serial/by-id/usb-FTDI_UT232R_FTXIEVAK-if00-port0";
//const char *DEFAULT_MODBUS_DEV = "/dev/ttyUSB0";

// Get process-scope cached MODBUS context/connection to SS-MPPT-15L.
// Flushes any pending input.
// Returns NULL in case of failure.
static modbus_t * const getCachedContext()
    {
    static modbus_t * const ctx = modbus_new_rtu(DEFAULT_MODBUS_DEV, 9600, 'N', 8, 2);
    if(NULL == ctx)
        {
        fprintf(stderr, "MODBUS: unable to create libmodbus context for device '%s'.\n", DEFAULT_MODBUS_DEV);
        return(NULL); // Failed.
        }

    // First time only, set slave and connect to avoid multiple open() calls.
    static bool inited;
    if(!inited)
        {
        // Set the SS-MPPT-15L as the device to talk to.
        modbus_set_slave(ctx, SUNSAVERMPPT);
        // Attempt to connect; return NULL if failed.
        if(-1 == modbus_connect(ctx))
            {
            fprintf(stderr, "MODBUS: connection failed: %s\n", modbus_strerror(errno));
            return(NULL);
            }
        // Allow time for power to stabilise, etc.
        // (Our circs with RS232 interface powered over USB...)
        // 750ms seems enough to avoid initial read timeout (500ms not).
        usleep(750000ul);
        inited = true;
        }

    // Pause/flush to allow things to settle and previous responses to clear.
    // 10ms is ~10 character times at 9600 bps;
    // typical single-register read is 7 or 8 chars.
    do { usleep(10000ul); } while(0 < modbus_flush(ctx));

    return(ctx);
    }

// Read single SS-MPPT-15L register into uint16_t over MODBUS; true if success.
// Potentially somewhat inefficient nominally setting up and tearing down
// connection for each read, but self-contained.
// Can automatically retry the read after a pause.
bool getMODBUSReg(const uint16_t addr, uint16_t &value, const int retries)
    {
    bool result = false;
    for(int t = retries; !result && (--t >= 0); )
        {
        modbus_t * const ctx = getCachedContext();
        if(NULL == ctx)
            {
            fprintf(stderr, "MODBUS: unable to use cached libmodbus context\n");
            return(false);
            }

        // Request specified register.
        const int n = modbus_read_input_registers(ctx, addr, 1, &value);
        if(1 == n) { result = true; }
        else
            {
            fprintf(stderr, "MODBUS: bad read: %s\n", modbus_strerror(errno));
            sleep(1);
            }
        }

    if(!result) { fprintf(stderr, "Giving up.\n"); }
    return(result);
    }



// Exclusive lock file to serialise all device, incl MODBUS, and log, access.
// Behaviour is meant to approximate that of procmail lockfile.
// This process is only intended to need to hold a lock for at most ~1s.
const char LOCK_FILE[] = "/var/lock/powermng.lock";

// Acquire exclusive access to devices and logs (~1s max) else exit() w/err.
// If retry is true will retry indefinitely with sleeps.
// Also disables SIGINT to try to allow uninterrupted use of devs/logs.
void lockAcquire(const bool retry)
    {
    for( ; ; sleep(LOCK_SUSPEND_S + 2))
        {
        // Attempt to create read-only lock file.
        const int lockfd0 = open(LOCK_FILE, O_CREAT | O_EXCL | O_RDONLY, 0444);
        if(lockfd0 >= 0) { close(lockfd0); }
        else
            {
            fprintf(stderr, "Initial attempt to acquire lock failed: %s\n", LOCK_FILE);
            struct stat statbuf;
            if(0 != stat(LOCK_FILE, &statbuf))
                {
                fprintf(stderr, "Cannot stat lock: %s\n", LOCK_FILE);
                if(retry) { continue; }
                exit(2);
                }
            struct timeval tv;
            gettimeofday(&tv, NULL);
            if(tv.tv_sec - statbuf.st_mtime > LOCK_STALE_S)
                {
                fprintf(stderr, "Removing stale lock, sleeping: %s\n", LOCK_FILE);
                unlink(LOCK_FILE);
                sleep(LOCK_SUSPEND_S);
                const int lockfdr = open(LOCK_FILE, O_CREAT | O_EXCL | O_RDONLY, 0444);
                if(lockfdr >= 0) { close(lockfdr); }
                else
                    {
                    fprintf(stderr, "Attempt to re-acquire lock failed: %s\n", LOCK_FILE);
                    if(retry) { continue; }
                    exit(3);
                    }
                }
            }

        // Acquired lock!
        break;
        }

    // Once the lock is acquired, try to run without interruption.
    signal(SIGINT, SIG_IGN);
    }

// Release exclusive access to devices and logs else exit() w/err.
// Also reenables SIGINT.
void lockRelease()
    {
    // RELEASE MUTEX
    unlink(LOCK_FILE);
    // Allow SIGINT again.
    signal(SIGINT, SIG_DFL);
    }
