//MODBUS DATALOGGER //T.A.Mitchell Feb 2018 //LIBRARIES #include //LCD Library #include //SPI Bus for SD Card #include //Simple SD card library #include //I2C Comms for RTC on A4 and A5 #include //RTC #include //Modbus RTU //DEFINITIONS to insert into code at compile time (so end up in flash memory not RAM) #define I2CDS1307Add 0x68 //I2C address for the RTC //LCD Pins #define pin_RS 8 //arduino pin wired to LCD RS #define pin_EN 9 //arduino pin wired to LCD EN #define pin_d4 4 //arduino pin wired to LCD d4 #define pin_d5 5 //arduino pin wired to LCD d5 #define pin_d6 6 //arduino pin wired to LCD d7 #define pin_d7 7 //arduino pin wired to LCD d8 #define pin_BL 3 //arduino pin wired to LCD backlight circuit //Indicator LED Pins #define pin_led_G 15 //Green LED on pin 15 (= A1) - Normal Operating Mode Indicator LED (flash) #define pin_led_Y 16 //Yellow LED on pin 16 (= A2) - Logging Error LED (ON),Card Activity LED (flash) #define pin_led_R 17 //Red LED on pin 17 (= A3) - Initalisation Error LED (ON), Set Clock (Flash) //SD Card Select (SPI bus) #define SD_CS 10 //Set ChipSelect Pin - use 10 as SD.h reserves this pin anyhow //Pushbuttons //The pushbuttons present 5V to the analogue pin via a resistor //0 to 5V is mapped to 0 to 1023 when the pin is read using analogRead() #define pin_Button 0 //Analogue pin connected to buttons #define btnRightHighThresh 50 //Value under this implies right (File) button (measures as 0) #define btnUpHighThresh 250 //Value under this implies up button (Item +) (measures as 144) #define btnDownHighThresh 450 //Value under this implies down (Item -) button (measures as 342) #define btnLeftHighThresh 650 //Value under this implies left (backlight) button (measures as 511) #define btnSelectHighThresh 850 //Value under this implies select (Go/Stop) button (measures as 731) //Button names: #define btnRIGHT 0 #define btnUP 1 #define btnDOWN 2 #define btnLEFT 3 #define btnSELECT 4 #define btnNONE 5 //Modbus #define timeout 1000 //Maximum time for slave to respond (ms) #define polling 200 //Maximum scan rate of master to allow slave to return to idle (ms) #define retry_count 10 //Maximum retries if slave returns response timeout or error #define TxEnablePin 2 //Pin to set RS485 interface to transmit or receive (set pin high to Transmit, sets DE and RE on RS485 PCB high. DE must be high to transmit and RE low to receive) //Misc #define LongIntMax 4294967295 //Maximum value of a long integer, used in timer check in case value has reset to zero in the interval #define Clock_Int 100 //Interval to check RTC (ms) to trigger next log to file. Should happen several times a second #define WriteLED_Int 500 //Interval to keep yellow LED on after writing file (ms). Should be long enough to see it, e.g. 500ms #define Def_BL_Timeout 30000 //Default Backlight timeout in ms, e.g. 30,000ms #define LCD_UD_Int 900 //LCD Update Interval (ms). Should happen just over once a second so clock seconds increment nicely #define But_Int 500 //Maximum button press interval (ms), stops multiple triggering of button command actions due to fast running of code. 500ms is suitable, short enough not to be noticeable by user, but long enough to debounce #define Msg_Delay 5000 //Delay (ms) after certain LCD messages before proceeding to next action that might write to the LCD, e.g. 5,000ms //GLOBAL VARIABLES unsigned int LogInterval = 1; //Logging interval in seconds (overwritten with value from config file) byte QtyRegs = 0; //Number of registers defined in config file to be read byte newFiles; //How often to create a new datafile, value is the number of digits in the name (without extension) of file to create each time, minus 1 (1 for yearly, 3 for monthly, 5 for daily and 7 for hourly) byte lcd_key = btnNONE; //Button number pressed by user, see function read_LCD_buttons int adc_key_in = 0; //Analogue value of button pin char logger_status; //Logger Status - G=Go, S=Stop, E=Card Error, M-Modbus Error, H=Synch to hour (persistent), h=Synch to hour (not on subsequent resets) char DisableStop; //Stop logging using pushbutton only available if 'N', note if not 'N' synch to hour can be overridden by presing button to start logging byte disp_reg = 0; //Register to display on LCD (zero based) - changeable using buttons byte FileN = 99; //File Number that can be incremented by user //Timer Variables unsigned long previousMillis; //Old timer value used to trigger RTC check unsigned long writeMillis; //Timer value at last card write - used to keep yellow LED on for a short period after write unsigned long Prev_LCD_UD = 0; //Timer at last LCD update unsigned int Secs = 65534; //Elapsed seconds (read from RTC) since values last logged, large value ensures logging starts immediately byte OldSecs; //Previous seconds value read from RTC - to detect change in RTC seconds to trigger logging on an interval defined in seconds byte OldHour; //Used for synch to midnight start - only start logging when hour increments //LCD Backlight Timer unsigned long oldBLTime=0; //Time backlight last turned on unsigned long BL_Timeout = 0; //Backlight timeout in ms //Previous button Press unsigned long OldButTime = 0; //Time of last button press //Modbus Error Flags byte MErrorMin = 1; //MODBUS error condition, set to zero (no error) whenever modbus reading successful for all registers and reset to 1 (error) after each write to file //MODBUS data structures #define TOTAL_NO_OF_REGISTERS 10 //Max number of registers to log, used to allocate memory, same as total packets as all packets are sized for one register // Create an array of Packets to be configured Packet packets[TOTAL_NO_OF_REGISTERS]; //Create an array to hold returned data in its raw form of unsigned integers unsigned int holdingRegs[TOTAL_NO_OF_REGISTERS]; byte RegOffset = 0; //Bugfix for values being returned in wrong array subscript in holdingRegs. Determined by testing for each baud and parity setting. 0 if no adjustment, 1 adds one to subscript and highest number becomes zero //INITIALISE DEVICES LiquidCrystal lcd( pin_RS, pin_EN, pin_d4, pin_d5, pin_d6, pin_d7); //Initialise LCD HCRTC HCRTC; //Initialise RTC library //Setup function reads or creates a config file //Sets up the serial port as modbus slave //Updates the RTC if the config file requests this (then writes to config file to stop future RTC update on reset) void setup() { //Local variables for config unsigned long serBaud; //Baud Rate byte serParity; //Parity N/O/E = None/Odd/Even byte serStop; //1 or 2 byte serConfig; //Value to set Data bits, parity and stop bits byte dataRead; //Raw data from file unsigned int slaveID, slaveReg, lineStartPos, x = 0; //Parameters to read slave ID and registers from file byte func, invalidEntry = 0, commaCount = 0; //Various variables used to read register parameters from file byte rtcData[6]; //Store elements of date and time to writte to RTC to set it byte i; //Counter to loop over characters char BaudStr[7]; //Reads baud rate as series of character digits char tempStr[6]; //Reads log rate, file number and register number as series of character digits char timeStr[3]; //Reads in the two digits of each time variable unsigned int logRate; //Log rate in raw state read from file // set up the LCD's number of columns and rows: lcd.begin(16, 2); // Initialise backlight BL_Timeout = Def_BL_Timeout; //Backlight timeout to default digitalWrite(pin_BL, LOW); //Ensures pullup disabled to avoid damage due to backlight circuit bad design pinMode(pin_BL, INPUT); //Set Backlight On //Initialise LED Pins pinMode(pin_led_G, OUTPUT); pinMode(pin_led_Y, OUTPUT); pinMode(pin_led_R, OUTPUT); //Initialise RS485 Pin pinMode(TxEnablePin, OUTPUT); digitalWrite(TxEnablePin,LOW);//Receive 485 //Initialise SD card pin pinMode(SD_CS, OUTPUT); digitalWrite(SD_CS, HIGH); //Initialise RTC HCRTC.RTCRead(I2CDS1307Add); //Open up the SD Card if (!SD.begin(SD_CS)) //If fail to initialise SD comms trigger error condition and message { lcd.print(F("SD Card Error")); digitalWrite(pin_led_R, HIGH); //RED LED ON //Exit setup return; } else //card is present { //Does the config file exist? if (!SD.exists("modlog.cfg")) //If config file missing create an example and trigger error condition and message { //If no config file, create am example SdFile::dateTimeCallback(dateTime); //Set datestamp for file File dataFile = SD.open("modlog.cfg", FILE_WRITE); //create example file if (dataFile) { //Create example //F() stores strings in flash memory rather than RAM //Example for ABB B21 meter, 9600,8O1, 5 minute logging, new file daily, log all 4 registers of active import (20480-20483), 2 registers of L1-N voltage (23296-23297), //2 registers of L1 Current (23308-23309), L1 frequency (23340) dataFile.println(F( "Update Time (N/Y): N\r\n" "New Time (YYMMDDHHMMSS): 180212130000\r\n" "Baud: 9600\r\n" "Parity (O/E/N): O\r\n" "Stop Bits (1/2): 1\r\n" "Log Rate (1-65535 S): 300\r\n" "New File Rate (N/H/D/M/Y): D\r\n" "Starting File Number (00-99): 99\r\n" "Initial State (G/S/s/H/h): G\r\n" "Disable Stop (Y/N): N\r\n" "Registers to Log (Slave ID,Type (C/S/I/H),Reg No):\r\n" "6,H,20480\r\n" "6,H,20481\r\n" "6,H,20482\r\n" "6,H,20483\r\n" "6,H,23296\r\n" "6,H,23297\r\n" "6,H,23308\r\n" "6,H,23309\r\n" "6,H,23340\r\n")); dataFile.close(); //close file lcd.print(F("Config file not")); lcd.setCursor(0,1); //LCD Column (0-15), row (0-1) //Move to new line lcd.print(F("found, created")); digitalWrite(pin_led_R, HIGH); //RED LED ON //Exit setup return; } else //Failed to create a config file { lcd.print(F("Config file not")); lcd.setCursor(0,1); //LCD Column (0-15), row (0-1) //Move to new line lcd.print(F("found, failed")); digitalWrite(pin_led_R, HIGH); //RED LED ON //Exit setup return; } } else //Config file is present, read it { File dataFile = SD.open("modlog.cfg", FILE_WRITE); //open file, leave datestamp as is if (dataFile) { dataFile.seek(0);//go to start of file dataFile.seek(findNext(dataFile,':') + 1);//Find the first parameter if (dataFile.peek() == 'Y') //Update RTC specified (Peek reads without changing current position in file) { dataFile.write('N'); //Change to N so clock not set again on next reset dataFile.seek(findNext(dataFile,':') + 1); //Loop over Year (5), Month (4),Date (3), Hour (2), Minute (1), Second (0) for(i=5; i<255; i--) { timeStr[0] = dataFile.read(); timeStr[1] = dataFile.read(); rtcData[i] = atoi(timeStr); } //Set RTC //Display message to press button to set clock lcd.print(F("Press Go/Stop to")); lcd.setCursor(0,1); lcd.print(F("set clock")); //Wait for keypress do { adc_key_in = analogRead(pin_Button); //Read button } while ((adc_key_in > btnSelectHighThresh) || (adc_key_in < btnLeftHighThresh)); //Keep looping until value is in the range for the Select button //Set clock HCRTC.RTCWrite(I2CDS1307Add, rtcData[5], rtcData[4], rtcData[3], rtcData[2], rtcData[1], rtcData[0], 1); //Set Clock, last parameter is weekday and not used delay(100); //Delay to let the clock be set //Message displaying new time HCRTC.RTCRead(I2CDS1307Add); //Read RTC lcd.clear(); lcd.print(F("Clock set to:")); lcd.setCursor(0,1); //LCD Column (0-15), row (0-1) //Move to new line lcd.print(HCRTC.GetDateString()); //Display date lcd.setCursor(5,1); //LCD Column 0 (0-15), row 0 (0-1) lcd.print(" "); //Erase year lcd.setCursor(6,1); //LCD Column 0 (0-15), row 0 (0-1) lcd.print(HCRTC.GetTimeString()); //Display time blinkLED(pin_led_R,1); //blink LED to signify time updated delay(Msg_Delay); //Pause so can read LCD diagnostic message } else //don't need to update time { dataFile.seek(findNext(dataFile,':') + 1); //Move to clock set value, but will not be used } //Baud rate (read as multiple chars) dataFile.seek(findNext(dataFile,':') + 1); i=0; do { dataRead = dataFile.read(); BaudStr[i] = dataRead; i++; } while ((dataRead != 13) && (i<7)); //read up to 6 chars or carriage return serBaud = atol(BaudStr); //can be over 65535 so use atol (ascii to long) instead of atoi (ascii to int) //Parity dataFile.seek(findNext(dataFile,':') + 1); serParity = dataFile.read(); //Stop Bits dataFile.seek(findNext(dataFile,':') + 1); serStop = dataFile.read(); //Set serConfig from constants switch (serParity) { case 'N': if(serStop == '1') { serConfig = SERIAL_8N1; //This breaks the MODBUS RTU standard but is used by some devices } else //2 stop bits by default { serConfig = SERIAL_8N2; } break; case 'E': if(serStop == '2') { serConfig = SERIAL_8E2; //This breaks the MODBUS RTU standard } else //1 stop bit by default { serConfig = SERIAL_8E1; } break; default: //Odd Parity if(serStop == '2') { serConfig = SERIAL_8O2; //This breaks the MODBUS RTU standard } else //1 stop bit by default { serConfig = SERIAL_8O1; } break; } //Set the RegOffset bugfix variable depending on selected comms parameters //Note: 8E2 not tested, no adjustment made if ((serBaud == 19200) || (serBaud == 115200) || (serConfig == SERIAL_8N2) || (serConfig == SERIAL_8E1)) { RegOffset = 1; } //Log Rate (read as multiple chars) dataFile.seek(findNext(dataFile,':') + 1); i=0; do { dataRead = dataFile.read(); tempStr[i] = dataRead; i++; } while ((dataRead != 13) && (i<7)); logRate = atoi(tempStr); //set the logging interval LogInterval = ((unsigned int)logRate); //Create new files Hourly, Daily, Monthly or Yearly 0r Never dataFile.seek(findNext(dataFile,':') + 1); switch (dataFile.read()) { case 'H': newFiles = 7; break; case 'D': newFiles = 5; break; case 'M': newFiles = 3; break; case 'Y': newFiles = 1; break; default: //Always use modlog00.csv newFiles = 0; } //Initial file number (if not creating new file based on time) dataFile.seek(findNext(dataFile,':') + 1); memset(tempStr, 0, sizeof(tempStr)); //Clear the temporary string tempStr[0] = dataFile.read();//First digit tempStr[1] = dataFile.read();//Second digit FileN = atoi(tempStr); //Convert to integer //Initial State (S/s/G/M/m) dataFile.seek(findNext(dataFile,':') + 1); switch (dataFile.peek()) { case 'h': dataFile.write('G'); //Non persistent synch to hour - write to file to start immediately after subsequent reset logger_status = 'H'; //Use synch to hour to start logging break; case 's': dataFile.write('G'); //Non persistent stop - write to file to start immediately after subsequent reset logger_status = 'S'; //Use synch to hour to start logging break; default: //Any other value logger_status = dataFile.read(); //Read value from file and assign directly break; } //Disable Stop (Y/N) dataFile.seek(findNext(dataFile,':') + 1); DisableStop = dataFile.read(); //Registers to log dataFile.seek(findNext(dataFile,':') + 1); do //scan the remaining data in the file to see if there are 2 commas on each line { lineStartPos = dataFile.position(); //save this position, so can return here commaCount = 0; //reset the count do { dataRead = dataFile.read(); if (dataRead == ',') { commaCount++;//count up the commas } } while ((dataRead != 255) && (dataRead != 13)); //keep going until end of file or carriage return if (commaCount == 2) //if it's got 2 commas assume a valid slaveid, type and register { dataFile.seek(lineStartPos); i=0; memset(tempStr, 0, sizeof(tempStr)); //Clear the temporary string do { dataRead = dataFile.read(); tempStr[i] = dataRead; i++; } while ((dataRead != ',') && (i<7)); slaveID = atoi(tempStr); //slave id dataRead = dataFile.read(); //this should be the register type switch (dataRead) { case 'C': func = READ_COIL_STATUS; break; case 'S': func = READ_INPUT_STATUS; break; case 'I': func = READ_INPUT_REGISTERS; break; default: //H - Holding registers func = READ_HOLDING_REGISTERS; break; } dataFile.read(); //move to the next char i=0; memset(tempStr, 0, sizeof(tempStr)); //Clear the temporary string do { dataRead = dataFile.read(); tempStr[i] = dataRead; i++; } while ((dataRead != 13) && (i<7)); dataFile.read(); //move to the next char slaveReg = atoi(tempStr); //get the register number // Initialize packets (pointer to packet, slave device ID, function code constant, address to read (zero based),number of registers to read,subscript of first returned value in holdingRegs array //x is the zero based subscript value modbus_construct(&packets[x], slaveID, func, slaveReg, 1, x); x++; } else //not two commas on line, assume end of file { invalidEntry = 1; } } while((!invalidEntry) && (x 1 SECOND digitalWrite(pin_led_G, !digitalRead(pin_led_G)); //Toggle Green LED to flash it //Synch to hour start check if(logger_status == 'H') //Synch to hour mode, waiting for hour to change { rtcData[2] = HCRTC.GetHour(); if(rtcData[2] != OldHour) //Change status to Go if hour has changed on the RTC { logger_status = 'G'; } } } } if ((Secs >= LogInterval) && (logger_status != 'S') && (logger_status != 'H'))//time to log, and logging is on, and not waiting for new hour (note logger_status could be G, or E or M if in error condition) { Secs = 0; //Reset seconds counter if(newFiles != 0) //Need to assemble filename as it changes periodically { //Retrieve each part of date and time as a numeric value (byte data type) rtcData[0] = HCRTC.GetSecond(); //Seconds rtcData[1] = HCRTC.GetMinute(); //Minutes rtcData[2] = HCRTC.GetHour(); //Hours rtcData[3] = HCRTC.GetDay(); //Date rtcData[4] = HCRTC.GetMonth(); //Month rtcData[5] = HCRTC.GetYear(); //Year //Create the filename from the time in the form YYMMDDHH.csv //dependent on the config file whether hourly, daily, monthly or yearly //Omit elements that don't change with the new file interval selected byte j=0; //j is character number in the filename (zero based, 0..7 for the date part) //loop over parts of the date and time as stored in rtcData //Start with year, then month...seconds, stop when counter resets to max value of datatype //Only add the digits as required by new file interval for (byte i=5; i<255; i--) //For each part of date { if(j < newFiles) //Only add required elements { //Add the two digits to the filename: all parts of date and time are 1 or 2 digits fileName[j] = rtcData[i] / 10 + 0x30; //get first digit by integer division by 10 and convert to ascii code of digit by adding 30 j++; fileName[j] = rtcData[i] % 10 + 0x30; //get second digit by remainder when divided by 10 and convert to ascii code of digit by adding 30 j++; } } //add file extension fileName[j+0] = '.'; fileName[j+1] = 'c'; fileName[j+2] = 's'; fileName[j+3] = 'v'; fileName[j+4] = 0; //end the string } else { //Fixed filename: add file number fileName[6] = FileN / 10 + 0x30; //get first digit by integer division by 10 and convert to ascii code of digit by adding 30 fileName[7] = FileN % 10 + 0x30; //get second digit by remainder when divided by 10 and convert to ascii code of digit by adding 30 } //Open the file SdFile::dateTimeCallback(dateTime); //Set datestamp for file File dataFile = SD.open(fileName, FILE_WRITE); //create it if it doesn't exist if(dataFile) { digitalWrite(pin_led_Y, HIGH); //Turn on Yellow LED while writing file writeMillis = millis(); //Store timer to keep yellow LED on for a while after finished write //Insert Date and Time into file dataFile.print(HCRTC.GetDateString()); dataFile.print(" "); // space between DATE and TIME dataFile.print(HCRTC.GetTimeString()); dataFile.print(","); // comma after time and date //Write error flag dataFile.print(MErrorMin,DEC); dataFile.print(","); //log master registers, which are continuously read in background //Should just be able to use disp_reg to get correct value as it is zero based, but have to add 1 to it and use [0] for last item if (RegOffset == 1) //Bugfix adjusting registers { for (byte i=1; i interval)) { return 1; } else if ((current_time < datum) && (((LongIntMax - datum) + (current_time)) <= interval)) { return 0; } else if ((current_time - datum) > interval) { return 1; } else { return 0; } } //Function to read value of button input and return button name void read_LCD_buttons() { if(Interval_Check(OldButTime, But_Int)) { //Minimum interval between keypresses to avoid multiple triggers // read the value from the sensor, Measured values ~ 50-100 less than these adc_key_in = analogRead(pin_Button); if (adc_key_in > btnSelectHighThresh) { lcd_key = btnNONE; // The 1st option for speed reasons since it will be the most likely result } else if (adc_key_in < btnRightHighThresh) { lcd_key = btnRIGHT; } else if (adc_key_in < btnUpHighThresh) { lcd_key = btnUP; } else if (adc_key_in < btnDownHighThresh) { lcd_key = btnDOWN; } else if (adc_key_in < btnLeftHighThresh) { lcd_key = btnLEFT; } else if (adc_key_in < btnSelectHighThresh) { lcd_key = btnSELECT; } else { lcd_key = btnNONE; } //If button pressed, turn backlight on and force display refresh on next loop if (lcd_key != btnNONE) { OldButTime = millis(); //Store time of this button press for debounce //Turn backlight on pinMode(pin_BL, INPUT); //Reset timeout oldBLTime = millis(); //Force LCD refresh ASAP - using oldBLTime as just set to millis, saves creating another variable if(oldBLTime > LCD_UD_Int) { //Setting previous update time to zero will trigger Prev_LCD_UD = 0; } else { Prev_LCD_UD = LongIntMax - LCD_UD_Int; } } } else //Insufficient time since previous button action: ignore button press { lcd_key = btnNONE; } } //Function to flash LED as diagnostic indicator void blinkLED(byte led, byte ontimes) { byte x = 0; while(x < ontimes) { digitalWrite(led,HIGH); delay(200); digitalWrite(led,LOW); delay(200); x++; } delay(400); } //Function to find the next instance of a character in the sd card file unsigned int findNext(File df,byte CharToFind) { byte dataRead; do { dataRead = df.read(); } while ((dataRead != 255) && (dataRead != CharToFind)); //end of file or character position return df.position(); } //Callback Function to set timestamp for files void dateTime(uint16_t* ddate, uint16_t* ttime) { HCRTC.RTCRead(I2CDS1307Add); //Update RTC data // return date using FAT_DATE macro to format fields *ddate = FAT_DATE((2000 + HCRTC.GetYear()), HCRTC.GetMonth(), HCRTC.GetDay()); // return time using FAT_TIME macro to format fields *ttime = FAT_TIME(HCRTC.GetHour(), HCRTC.GetMinute(), HCRTC.GetSecond()); }