/* Copyright (c) 2015-2018, SCADAmetrics All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the SCADAmetrics nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE SCADAMETRICS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SCADAMETRICS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE */ /* AMR This program collects data from one or more EtherMeters and stores the metering data into a .CSV comma-separated text file. The protocol used is MODBUS/TCP or MODBUS/UDP, which is a connectionless-variant of the MODBUS/TCP protocol. UDP is particularly well-suited for cellular and other wireless networks, as it consumes less network bandwidth. This program is a simple demonstrator; but it can form the basis for more sophisticated variants. The software is provided as-is, and support will only be provided in the context of support for our EtherMeter instrumentation. If you are not a SCADAmetrics EtherMeter customer, please don't contact us with questions about this free software. This program was compiled using Microsoft Visual Studio 2008. The programming language is C++. - Jim Mimlitz, SCADAmetrics */ #include #include #include #include #include #include #pragma comment( lib , "ws2_32.lib" ) //Winsock Library #define BUFLEN 512 // Maximum Buffer Length #define MAX_LINE_LENGTH 256 #define MAX_METERS 512 #define UDP_SOCKET_BLOCKING 0 #define UDP_SOCKET_NONBLOCKING 1 #define TCP_SOCKET_BLOCKING 0 #define TCP_SOCKET_NONBLOCKING 1 #define UINT unsigned int #define ULONG unsigned long #define UCHAR unsigned char typedef struct { char hostname[256] ; char ip_address[128] ; char use_tcp ; UINT tcp_udp_port ; int network_fault ; int em_channel_1_active ; // If meter_1_id==0, then channel_1 NOT active int em_channel_2_active ; // If meter_2_id==0, then channel_2 NOT active ULONG meter_1_id ; ULONG meter_2_id ; ULONG meter_1_unscaled ; ULONG meter_2_unscaled ; double meter_1_scaled ; double meter_2_scaled ; long flow_1_unscaled ; long flow_2_unscaled ; float flow_1_scaled ; float flow_2_scaled ; int meter_1_fault ; int meter_2_fault ; int meter_1_exponent ; int meter_2_exponent ; int meter_1_units ; int meter_2_units ; ULONG meter_1_fault_count ; ULONG meter_2_fault_count ; int iVcc ; float vcc ; ULONG uptime_min ; } MeterStruct ; int Read_EtherMeter( MeterStruct *ms , UINT *message_count ) ; int main( int argc , char *argv[] ) { struct tm *nowtime ; time_t t , daysecs , countdown ; WSADATA wsa; UINT message_count ; int i , n_ethermeters ; char line[MAX_LINE_LENGTH] ; long sample_period_s ; FILE *fp ; char *token , *next_token ; MeterStruct ms[MAX_METERS] ; message_count = 0 ; sample_period_s = 3600L ; // sites.txt file format: // 3600 # SAMPLE_PERIOD_SECS // 1 2 192.168.1.114 502 1 1 # METER_ID_1 , METER_ID_2 , HOSTNAME(OR IP_ADDRESS) , VIRTUAL_PORT , TCP_OR_UDP // 3 0 192.168.1.140 502 1 0 # METER_ID_1 , METER_ID_2 , HOSTNAME(OR IP_ADDRESS) , VIRTUAL_PORT , TCP_OR_UDP fopen_s( &fp , "sites.txt" , "rt" ); if ( fp==NULL ) { printf("\r\nError: Unable to open file 'sites.txt'\r\n"); return(0); } // SAMPLE PERIOD for Meter Readings... sample_period_s = 3600L ; if ( fgets(line,MAX_LINE_LENGTH,fp) != NULL ) { printf("Read: %s",line) ; next_token = NULL ; token = strtok_s(line," ",&next_token) ; sample_period_s = atol( token ) ; printf("Sample Period = %ld Seconds\r\n\n",sample_period_s); } else { printf("\r\nError reading TCP/UDP? from file 'sites.txt'\r\n"); return(0); } n_ethermeters = 0 ; while ( fgets(line,MAX_LINE_LENGTH,fp) != NULL ) { printf("\r\nRead: %s",line) ; next_token = NULL ; token = strtok_s(line," ",&next_token) ; ms[n_ethermeters].meter_1_id = atol( token ) ; ms[n_ethermeters].em_channel_1_active = ( ms[n_ethermeters].meter_1_id > 0 ) ; if ( ms[n_ethermeters].em_channel_1_active ) { printf("Ch.A ID = %ld\r\n",ms[n_ethermeters].meter_1_id) ; } else { printf("Ch.A Inactive\r\n") ; } token = strtok_s(NULL," ",&next_token) ; ms[n_ethermeters].meter_2_id = atol( token ) ; ms[n_ethermeters].em_channel_2_active = ( ms[n_ethermeters].meter_2_id > 0 ) ; if ( ms[n_ethermeters].em_channel_2_active ) { printf("Ch.B ID = %ld\r\n",ms[n_ethermeters].meter_2_id) ; } else { printf("Ch.B Inactive\r\n") ; } token = strtok_s(NULL," ",&next_token) ; strcpy_s( ms[n_ethermeters].hostname , MAX_LINE_LENGTH , token ) ; printf("hostname = %s\r\n",ms[n_ethermeters].hostname) ; token = strtok_s(NULL," ",&next_token) ; ms[n_ethermeters].tcp_udp_port = (UINT)atol ( token ) ; printf("tcp_udp_port = %d\r\n",ms[n_ethermeters].tcp_udp_port ) ; token = strtok_s(NULL," ",&next_token) ; if ( token[0]=='T' || token[0]=='t' ) { ms[n_ethermeters].use_tcp = 1 ; printf("Use TCP Sockets\r\n"); } else if ( token[0]=='U' || token[0]=='u' ) { ms[n_ethermeters].use_tcp = 0 ; printf("Use UDP Sockets\r\n"); } else { printf("\r\nError reading TCP/UDP? for EtherMeter No. %d from file 'sites.txt'\r\n",n_ethermeters); return(0); } if ( token != NULL ) { n_ethermeters++ ; } } fclose(fp); if ( n_ethermeters < 1 ) { printf("\r\nError reading from file 'sites.txt'\r\n"); return(0); } printf("\r\nN EtherMeters = %d\r\n",n_ethermeters); // Initialize EtherMeter Structures... for ( i=0 ; itm_hour*3600L + nowtime->tm_min*60L + nowtime->tm_sec ; countdown = daysecs % sample_period_s ; if ( daysecs % 10L == 0 ) { printf("."); } if ( countdown < 2L ) { // Start a polling cycle... printf("\r\n\nStart Polling Cycle...\r\n") ; // Open .CSV File. // If File Does Not Exist, Create New File. // If File Does Exist, Open File In Append Mode. fp = fopen("meter.csv","r") ; if ( fp==NULL ) { fclose(fp); fp = fopen("meter.csv","wt"); fprintf(fp,"Date , Time , Meter_ID , Network_Fault , Meter_Fault , Meter_Reading , Units , Rate_of_Flow\r\n") ; } else { fclose(fp); fp = fopen("meter.csv","at") ; } for ( i=0 ; itm_year+1900,nowtime->tm_mon+1,nowtime->tm_mday,nowtime->tm_hour,nowtime->tm_min, ms[i].meter_1_id,ms[i].network_fault,ms[i].meter_1_fault,ms[i].meter_1_scaled) ; switch ( ms[i].meter_1_units ) { case 0 : { fprintf(fp,"GAL , ") ; break ; } case 1 : { fprintf(fp,"LITERS , ") ; break ; } case 2 : { fprintf(fp,"CUBIC_FEET , ") ; break ; } case 3 : { fprintf(fp,"CUBIC_METERS , ") ; break ; } case 4 : { fprintf(fp,"LBS , ") ; break ; } case 5 : { fprintf(fp,"KILOGRAMS , ") ; break ; } case 6 : { fprintf(fp,"ACRE_FEET , ") ; break ; } case 7 : { fprintf(fp,"UNITS , ") ; break ; } default : { fprintf(fp,"UNITS , ") ; break ; } } fprintf(fp,"%12.3f\r\n",ms[i].flow_1_scaled); } if ( ms[i].em_channel_2_active ) { fprintf(fp,"%04d/%02d/%02d , %02d:%02d , %04d , %01d , %01d , %15.3f , ", nowtime->tm_year+1900,nowtime->tm_mon+1,nowtime->tm_mday,nowtime->tm_hour,nowtime->tm_min, ms[i].meter_2_id,ms[i].network_fault,ms[i].meter_2_fault,ms[i].meter_2_scaled); switch ( ms[i].meter_2_units ) { case 0 : { fprintf(fp,"GAL , ") ; break ; } case 1 : { fprintf(fp,"LITERS , ") ; break ; } case 2 : { fprintf(fp,"CUBIC_FEET , ") ; break ; } case 3 : { fprintf(fp,"CUBIC_METERS , ") ; break ; } case 4 : { fprintf(fp,"LBS , ") ; break ; } case 5 : { fprintf(fp,"KILOGRAMS , ") ; break ; } case 6 : { fprintf(fp,"ACRE_FEET , ") ; break ; } case 7 : { fprintf(fp,"UNITS , ") ; break ; } default : { fprintf(fp,"UNITS , ") ; break ; } } fprintf(fp,"%12.3f\r\n",ms[i].flow_2_scaled); } } message_count++ ; fclose(fp); Sleep(4000L); } Sleep(1000L) ; } WSACleanup(); return 0; } int Read_EtherMeter( MeterStruct *ms , UINT *message_count ) { char msg_out[BUFLEN] , msg_in[BUFLEN] , ctemp[16] , scaletemp[4] ; struct sockaddr_in si_ethermeter ; int slen , n_modbus_registers , outgoing_length , incoming_length , n_header , n_bytes_expected , iResult , retValue ; SOCKET s ; time_t dwTime , timeout , loop_timeout ; ULONG lMode , lBytesReadable ; char ip[128] ; struct hostent *he; struct in_addr **addr_list; int i ; // int j ; timeout = 5000 ; // Milliseconds ms[0].network_fault = 1 ; slen = sizeof(si_ethermeter) ; retValue = 0 ; outgoing_length = 12 ; n_modbus_registers = 26 ; n_header = 9 ; n_bytes_expected = n_header + n_modbus_registers*2 ; // Formulate Modbus Request... memcpy( &msg_out[0] , &message_count , 2 ) ; // first 2 bytes is message count msg_out[ 2] = 0x00 ; msg_out[ 3] = 0x00 ; msg_out[ 4] = 0x00 ; msg_out[ 5] = 0x06 ; // Request Length (minus header) -- Always 6 msg_out[ 6] = 0x01 ; // Modbus Device ID (EtherMeter ignores this field) msg_out[ 7] = 0x03 ; // Modbus Function 3 (Read Holding Registers) msg_out[ 8] = 0x00 ; msg_out[ 9] = 0x00 ; msg_out[10] = 0x00 ; msg_out[11] = 26 ; // Number of 16-bit registers requested. // Setup IP Address Structure... // Allow Either Hostname (via DNS Lookup) or IP_Address... if ( (he = gethostbyname( ms[0].hostname ) ) == NULL) { //gethostbyname failed printf("gethostbyname failed : %d\r\n" , WSAGetLastError()); printf("Could Not Resolve IP Address of %s\r\n",ms[0].hostname); return 1; } //Cast the h_addr_list to in_addr , since h_addr_list also has the ip address in long format only addr_list = (struct in_addr **) he->h_addr_list; for( i=0 ; addr_list[i] != NULL ; i++) { //Return the first one; strcpy(ip , inet_ntoa(*addr_list[i]) ); } printf("%s resolved to : %s\r\n" , ms[0].hostname , ip ) ; // End DNS Lookup memset( (char *)&si_ethermeter , 0 , sizeof(si_ethermeter) ) ; si_ethermeter.sin_family = AF_INET ; si_ethermeter.sin_port = htons((u_short)ms[0].tcp_udp_port) ; si_ethermeter.sin_addr.S_un.S_addr = inet_addr(ip) ; // Create Socket... if ( ms[0].use_tcp ) { if ( ( s=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == SOCKET_ERROR) { printf("TCP Socket() failed with error code : %d\r\n" , WSAGetLastError()); return(0) ; } } else { if ( ( s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == SOCKET_ERROR) { printf("UDP Socket() failed with error code : %d\r\n" , WSAGetLastError()); return(0) ; } } // Uncomment This Block To Observe The Raw Outgoing Modbus Request Packet... /* printf("\r\nData To Send(%02d): ",(int)outgoing_length); for ( j=0 ; j