Central Idaho Amateur
Radio Club
     
Addressing Hytera RD-982 Loss of BrandMeister Services in a DHCP Environment
Addressing Hytera RD-982 Loss of BrandMeister Services in a DHCP Environment

This page last updated on 30 June 2017.


A .pdf version of this article can be downloaded by clicking here.


Download a .zip package that contains a .pdf of this article and the GNU free software license by clicking here.


Information on the GNU free software licenses can be found by clicking here.


A copy of the GNU free software license can be downloaded by clicking here.


 
ADDRESSING HYTERA RD-982 LOSS OF
BRANDMEISTER NETWORK SERVICES
IN A DHCP ENVIRONMENT
 
AN IMPLEMENTATION FOR AUTOMATIC RECOVERY
 
30 June 2017
Ray Montagne - W7CIA
 
ISSUE - LOSS OF IP CONNECTIVITY TO A MASTER SERVER

This issue occurs with the BrandMeister Amateur Radio network, and after extensive data collection, appears to be related to expiration of the DHCP lease and assignment of a new WAN / Public IP address to the DSL Modem/Router. It is apparent that the firmware within the RD-982 repeater does not monitor the public IP address. It is not known exactly how the RD-982 firmware behaves during reset / BOOT, but it is known that a reset / BOOT of the RD-982 always results in re-establishing communications between the RD-982 and the BrandMeister network. The root cause of this issue lies in using internet services that use DHCP. Further, those services, a DSL subscription provided by Frontier Communications, have been observed to assign a new address, using DHCP, where network activity is occuring at 5-minute intervals. From an external observational perspective, it is as if Frontier Communications has a 0-time, or very near to zero time DHCP lease expiration time.


PROPOSED SOLUTION

This issue could be prevented if the RD-982 were installed with internet networking services that uses a fixed IP address. In the absence of a fixed IP address, a method or recovery in a DHCP addressing environment would require detecting when the public IP address changes and then trigger a reset / RE-BOOT of the RD-982 to restore network services.

Detection of a change in the public IP address can be performed by issuing an HTTP query to checkip.dyndns.org and comparing the reported public IP address to a previously stored acquisition of the public IP address, then responding to a change in public IP address by issuing an RD-982 reset and storing the new public IP address. Alternate sources of obtaining the public IP address can be had by implementing a PHP web-page and having that page return the value of the HTTP_CLIENT_IP environmental variable (or alternate environmental variable as apporpriate), using the PHP getenv function.


CUSTOMER WORK-AROUND

An embedded microcontroller implements the very recovery described immediately above. An Arduino Pro Mini microcontroller, interfaced to a DeadOn RTC break-out board and a W5500 Ethernet module (which is connected to the same Ethernet HUB as the RD-982 repeater) is used to perform an automated recovery without intervention by the system operator. The RD-982 repeater will be automatically reset under the following circumstances:

Additionally, a DTMF controlled RESET is wire-OR'ed to support remote user reset of the repeater by DTMF command (restricted to a Time Slot 1 channel).

Note that the Customer Programming Software data must be configured to support an external RESET of the RD-982, with the RESET function assigned as an active low signal on GPIO4 (i.e. pin 3 of the 26-pin accessory connector).

BLOCK DIAGRAM

The following block diagram shows the AUTO-RESET circuit as it is implemented in the W7CIA VHF DMR Repeater.


W7CIA DMR REPEATER BLOCK DIAGRAM
SCHEMATIC

These repeater reset facilities are intended to provide sustained connectivity with the BrandMeister network while avoiding having to have direct physical interaction with the repeater to sustain these services, and at minimal cost (e.g. approximately $40).


[REPEATER CONTROLLER INTERFACE SCHEMATIC]

PARTS LIST

For the following parts list, clicking on the image of the part will open the vendor page for that part in a new window or tab. Moving the cursor over the part description may present additional relevant information regarding the part.


Parts List
DESCRIPTION IMAGE COST
ARDUINO PRO MINI Information ARDUINO PRO MIN This board requires the 5V FTDI Breakout board to upload the program to the Arduino Pro Mini. Any Pro series Arduino board will function, including boards with built-in USB support. Arduino Pro Mini image $9.95
DeadOn Real Time Clock Information DeadOn Real Time Clock To avoid damage to the circuit, do not solder with coin battery installed. DeadOn RTC image $19.95
Coin Battery Information Coin Battery To avoid damage to the circuit, do not solder with coin battery installed. DB-15 connector image $1.95
W5500 Ethernet Board Information W5500 Ethernet Board The SparkFun Ethernet2 Shield can be used, but this is a much lower cost option and provides the same function. Ethernet transactions are not secure. A version of the W5500, with hardware SSL support, is avialable (in this case, Google is your friend). Search for W5500 Ethernet Shield S (note: code changes will be required for the W5500S). W5500 Ethernet image ≈ $6.00
2N7000 MOSFET Information 2N7000 MOSFET Any low current switching MOSFET is acceptable. 2N7000 image $0.07
DB-26 Connector (male) Information DB-26 Connector (male) It is recommended that hoods be purchased and installed on the connector. These are available from the same vendor. DB-26 connector image $2.79
DB-25 Connector (male) Information DB-25 Connector (male) It is recommended that hoods be purchased and installed on the connector. These are available from the same vendor. DB-25 connector image $1.36
DB-9 Connector (male) Information DB-9 Connector (male) It is recommended that hoods be purchased and installed on the connector. These are available from the same vendor. DB-9 connector image $0.81

SOURCE CODE
/*
    Copyright © 2017
    Raymond B Montagne

    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/>.

    HYTERA RD-982 REPEATER RESET v2.0
    
    THIS MODULE MONITORS THE PUBLIC IP ADDRESS OF THE HYTERA RD-982 REPEATER, AND WILL RESET THE REPEATER IF 
    THE PUBLIC IP ADDRESS CHANGES.  FURTHER, THIS MODULE WILL ALSO MONITOR TIME, BASED ON NETWORK TIME PROTOCOL 
    (NTP), AND WILL PERFORM A DAILY RESET OF THE REPEATER AT 00530 LOCAL TIME.
    
    DEVELOPMENT NOTES
    
    1.	There is an issue with the SparkFunDS3234RTC library, where the 'Category' field 
    	in the 'library.properties' file is not properly set.  The library sources must 
    	be modified to fix this issue.  Change the value of this property from 'Sensor' 
    	to 'Sensors' (i.e. plural) to make the property value conform to the specification 
    	found at <https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification>,
    	and to eliminate the compile time warning thrown by this error.
	
    2.	While the DeadOn RTC documentation for the library indicates that the rtc.autotime () 
    	function will initialize the RTC to the compile time, this is not quite correct.  The 
    	time will be set to the time at which the Arduino IDE was launched.  Therefore, it is 
    	recommended that the Arduino IDE be quit and relaunced, then immediately compile this 
    	code, with the kBUILD_TO_SET_CLOCK constant as defined, and upload this code into 
    	the target Arduino board (I used an Arduino Pro Mini) to set the clock.  Then 
    	recompile this code with the kBUILD_TO_SET_CLOCK constant as undefined, and upload 
    	into the target Arduino board.  The second upload will not set the RTC, but is 
    	necessary so that any subsequent reset of the target Arduino board does not reset 
    	to the RTC to the time at which the Arduino IDE was originally launched.
	
	3.	The SparkFunDS3234RTC library will properly manage the SS pin that is associated with 
		the Real Time Clock device.  No external management of the SS enable state is required. 
		Only the ethernet transactions will require external management of the ethernet SS 
		pin.
	
	4.	The Ethernet2 library must be installed into the Arduino IDE environment.
*/

#include <SPI.h>
#include <SparkFunDS3234RTC.h>
#include <TextFinder.h>
#include <Dhcp.h>
#include <Dns.h>
#include <Ethernet2.h>
#include <EthernetUdp2.h>
#include <util.h>

//#define	kBUILD_TO_DEBUG
#define	kBUILD_FOR_CLOCK
//#define kBUILD_TO_SET_CLOCK
#define kBUILD_TO_SEND_REPORT

const int	kPD382_RESET_ALARM_HOUR			=	 5;
const int	kPD382_RESET_ALARM_MINUTE		=	30;
const int	kPD382_RESET_ALARM_SECOND		=	 0;

const int	kSPI_CLK_PIN					=	13;
const int	kSPI_MISO_PIN					=	12;
const int	kSPI_MOSI_PIN					=	11;
const int	kETHERNET_CS_PIN				=	10;
const int	kINITIALIZE_SWITCH				=	 9;
const int	kRTC_SS_PIN						=	 8;
const int	kRD382_RESET_PIN_ACTIVE_HIGH	=	 3;
const int	kRTC_SQW_PIN					=	 2;

const int	kSPI_DEVICE_DISABLED			=	HIGH;
const int	kSPI_DEVICE_ENABLED				=	LOW;
const int	kRD382_RESET_ASSERTED			=	HIGH;
const int	kRD382_RESET_NEGATED			=	LOW;

const int	kSECONDS_PER_MINUTE				=	60;
const int	kMINUTES_PER_HOUR				=	60;
const int	kHOURS_PER_DAY					=	24;
const int	kFIRST_RESET_MINUTE_OFFSET		=	10;
const int	kIP_POLL_MINUTE_INTERVAL		=	 5;

#ifdef kBUILD_TO_SEND_REPORT					//	{
const char	kREPORT_DOMAIN_URL_STR[]		=	"my_website_address.domain";

//	Set the following to the URI of your web page

const char	kREPORT_PAGE_URI_STR[]			=	"GET /my_uri.php?";
const char	kREPORT_COUNT_STR[]				=	"COUNT=";
const char	kREPORT_KEY_STR[]				=	"&KEY=";
const char	kREPORT_HASHED_KEY_STR[]		=	"my hashed key must match key used by my_uri.php";
const char	kREPORT_IP_STR[]				=	"&IP=";

//	Set the following to numeric DMR-Marc ID of repeater

const char	kREPORT_DMR_MARC_STR[]			=	"&DMR-MARC=311602";
const char	kREPORT_HIDDEN_STR[]			=	"&HIDDEN=";
const char	kREPORT_TYPE_STR[]				=	"&TYPE=";
const char	kREPORT_HTTP_STR[]				=	" HTTP/1.1";
#endif											//	}	kBUILD_TO_SEND_REPORT


enum {
	kRESET_TYPE__SCHEDULED = 1,
	kRESET_TYPE__PUBLIC_IP,
	kRESET_TYPE__POWER_FAIL
} RESET_TYPES;

typedef struct {
	IPAddress			public_ip_address;
    IPAddress			ip_address;
    unsigned long int	currentTime;
    unsigned int		millisTimer;
    unsigned int		secondsTimer;
    unsigned int		minutesTimer;
	unsigned int		second;
	unsigned int		minute;
	unsigned int		hour;
	unsigned int		day;					//  Sunday = 1, Monday = 2, ..., Saturday = 7
	unsigned int		date;					//  1 through {28,29,30,31}
	unsigned int		month;
	unsigned int		year;
	unsigned int		alarmTimeSecond;
	unsigned int		alarmTimeMinute;
	unsigned int		alarmTimeHour;
	unsigned int		alarmTimeDay;
	unsigned int		alarmTimeDate;
	unsigned int		lastSecond;
	unsigned int		previousMinute;
	unsigned int		resetCount;
	unsigned int		resetType;
	unsigned int		next_ip_poll_minute;
	boolean				ip_poll_is_armed;
	boolean				resetAlarmFired;
} GLOBALS;

const int	kSERVER_PORT								=	  80;

const int	kMILLIS_ONE_SECOND_DELAY					=	1000;
const int	kSECONDS_ONE_MINUTE_DELAY					=	  60;
const int	kMINUTES_TEN_MINUTE_DELAY					=	  10;

const int	kCONNECT_SUCCESS							=	   1;
const int	kCONNECT_TIMED_OUT							=	  -1;
const int	kCONNECT_INVALID_SERVER						=	  -2;
const int	kCONNECT_TRUNCATED							=	  -3;
const int	kCONNECT_INVALID_RESPONSE					=	  -4;

//	The least significant bit of the most significant byte of the MAC address must be 
//	cleared to 0 for non-xxxx MAC address selection of Multi-Cast.  The second least 
//	significant bit of the most significant byte of the MAC address must be set to 1 to 
//	indicate that the MAC address is self-administered.  With the MAC address residing 
//	on a LAN, with a minimum number of devices, the concatenation of the ascii values 
//	use in this module's MAC address spells out "RD-982".  An examination of the MAC 
//	address of each device attached to the LAN should be performed prior to installation 
//	of this device onto the LAN.  If a MAC address conflict exists, the value of the MAC
//	address of this device should be adjusted to eliminate the MAC address conflict. 

const int	kMAC_UNICAST_MULTICAST_BIT_ADDRESS			=	0;
const int	kMAC_UNICAST								=	0;
const int	kMAC_UNICAST_AND_MASK						=	~( 1 << kMAC_UNICAST_MULTICAST_BIT_ADDRESS );
const int	kMAC_LOCAL_ADMIN_BIT_ADDRESS				=	1;
const int	kMAC_LOCAL_ADMIN							=	1;

//	The following MAC address is set to the ascii representation of 'RD-982' 
//	(i.e. 52.44.2D.39.38.32), which must be unique on your local area network.

const int	kMAC_LOCAL_ADMIN_OR_MASK					=	( kMAC_LOCAL_ADMIN << kMAC_LOCAL_ADMIN_BIT_ADDRESS );
const int	kMAC_ADDRESS_MSB							=	'R' & kMAC_UNICAST_AND_MASK | kMAC_LOCAL_ADMIN_OR_MASK;
const byte	mac_address[]								=	{kMAC_ADDRESS_MSB, 'D', '-', '9', '8', '2'};

const char	server_domain_name[]						=	"checkip.dyndns.org";
const char	server_page_name[]							=	"/";
const char	ip_search_str[]								=	"IP Address: ";

const int	kNUM_SEARCH_ELEMENTS						=	4;
const int	kETHERNET_TIMEOUT							=	5;
const int	kIP_QUERY_MINIMUM_SIZE						=	12;
const int	kIP_QUERY_TIMEOUT							=	300;
const int	kNOTIFICATION_RETRY_COUNT_SEED				=	10;

EthernetClient	ethernetClient;
TextFinder		textFinder ( ethernetClient, kETHERNET_TIMEOUT );

GLOBALS		globals;

//  --------------------------------------------------------------------------------

void reset_rd982 ( int reset_type )
{
	globals.resetCount++;
	globals.resetType = reset_type;
	digitalWrite ( kRD382_RESET_PIN_ACTIVE_HIGH, kRD382_RESET_ASSERTED );
#ifdef kBUILD_TO_DEBUG							//	{
	Serial.println ();
	Serial.println ( "ASSERT Hytera RD-982 RESET" );
#endif											//	}	kBUILD_TO_DEBUG
	delay ( 500 );
	digitalWrite ( kRD382_RESET_PIN_ACTIVE_HIGH, kRD382_RESET_NEGATED );
	
#ifdef kBUILD_TO_SEND_REPORT					//	{
	send_reset_report ( reset_type );
#endif											//	}	kBUILD_TO_SEND_REPORT
}

//  --------------------------------------------------------------------------------
 
void get_current_time ( void )
{
	rtc.update ();

	globals.second = rtc.second ();
	globals.minute = rtc.minute ();
	globals.hour = rtc.hour ();
	globals.day = rtc.day ();
	globals.date = rtc.date ();
	globals.month = rtc.month ();
	globals.year = rtc.year ();
}

//  --------------------------------------------------------------------------------

 void setNormalResetTime ()
 {
    globals.alarmTimeHour = kPD382_RESET_ALARM_HOUR;
    globals.alarmTimeMinute = kPD382_RESET_ALARM_MINUTE;
    globals.alarmTimeSecond = kPD382_RESET_ALARM_SECOND;
 }

//  --------------------------------------------------------------------------------

 boolean isNormalResetTime ()
 {
 	boolean		result = false;
 	
 	if ( ( kPD382_RESET_ALARM_HOUR == globals.alarmTimeHour ) &&
 		 ( kPD382_RESET_ALARM_MINUTE == globals.alarmTimeMinute ) && 
 		 ( kPD382_RESET_ALARM_SECOND == globals.alarmTimeSecond ) )
 	{
 		result = true;
 	}
    
    return result;
 }

//  --------------------------------------------------------------------------------

void	display_time ( boolean displayAlarmTime )
{
#ifdef kBUILD_TO_DEBUG							//	{
	Serial.print ( globals.hour );
	Serial.print ( ":" );
	Serial.print ( globals.minute );
	Serial.print ( ":" );
	Serial.print ( globals.second );
	if ( displayAlarmTime )
	{
		Serial.print ( ", next SCHEDULED or POWER UP RESET will occur at " );
		Serial.print ( globals.alarmTimeHour );
		Serial.print ( ":" );
		Serial.print ( globals.alarmTimeMinute );
		Serial.print ( ":" );
		Serial.print ( globals.alarmTimeSecond );
	}
	Serial.println ( "" );
#endif											//	}	kBUILD_TO_DEBUG
}

//  --------------------------------------------------------------------------------
//	Returns TRUE if the next time at which an public IP address should be determined 
//	has expired.

boolean ip_update_timer_fired ( void )
{
	boolean				result = false;
	
	if ( globals.next_ip_poll_minute == globals.minute )
	{
		if ( globals.ip_poll_is_armed )
		{
			globals.ip_poll_is_armed = false;
			result = true;
		}
	}
	else
	{
		globals.ip_poll_is_armed = true;
	}
	
	return result;
}

#ifdef kBUILD_TO_SEND_REPORT					//	{
//  --------------------------------------------------------------------------------
//	Hytera RD-982 repeater notifications are posted with $_GET and take the following format:
//	
//	where:
//		COUNT		The number of RESET events that have been reported since power-up
//		KEY			A hashed validating key (not fully secure)
//		IP			The conventional string expression of a public IP address that is assigned to the repeater
//		DMR-MARC	The DMR-MARC ID of the repeater
//		HIDDEN		A zero-length string used to defeat BOTs
//		TYPE		A numeric value indicating the cause of the reset, where:
//					1 = daily timed reset
//					2 = a change in the public IP address o the repeater
//					3 = primary power was restored after a primary power failure
//		
//	Notifications require thse 6 keys.  Notification will not occur if there are less than or more than 6 keys.
//	The order of the keys within the URL are unimportant.
//
//	The hashed KEY uses the BCRYPT method of hashing a password in PHP.  The target web-page uses the 
//	PHP password_verify () function to validate the hashed KEY against the page password access.  If 
//	validation passes, an email message is sent to the page encoded target email address.  While this 
//	method is not fully secure, field data checking of the URL by the web-page provides additional 
//	protection against unauthorized notifications.  The hash is not constructed in this code module, 
//	rather it is generated in a PHP environment and simply copy and pasted into this code.
//
//	The reporting URL addresses a PHP web-page, which will parse the $_GET parameters and generate a 
//	data restricted structured email message that is addressed to a fixed email recipient address.  
//	Further, the $_GET input is only accepted from a locale that is within the United States of America.
//	This implementation avoids using SMTP, or relying on other intermediate email services, and results 
//	in a very simple code implementation to provide reset event notification.

void send_reset_report ( int reset_type )
{
	String			serverString = kREPORT_DOMAIN_URL_STR;
	String			queryString = "";
	unsigned int	retry_count = kNOTIFICATION_RETRY_COUNT_SEED;
	boolean			public_ip_address_is_zero = true;
	boolean			connected = false;
	
#ifdef kBUILD_TO_DEBUG
	Serial.println ( "==========" );
	Serial.println ( "Will send reset report query" );
#endif

	for ( int index = 0; index < ( sizeof ( globals.public_ip_address ) / sizeof ( globals.public_ip_address[0] ) ); index++ )
	{
		if ( 0 != globals.public_ip_address[index] )
		{
			public_ip_address_is_zero = false;
		}
	}
	
	if ( !public_ip_address_is_zero )
	{
		queryString.concat ( kREPORT_PAGE_URI_STR );
		queryString.concat ( kREPORT_COUNT_STR );
		queryString.concat ( globals.resetCount );
		queryString.concat ( kREPORT_KEY_STR );
		queryString.concat ( kREPORT_HASHED_KEY_STR );
		queryString.concat ( kREPORT_IP_STR );
		queryString.concat ( globals.public_ip_address[0] );
		queryString.concat ( "." );
		queryString.concat ( globals.public_ip_address[1] );
		queryString.concat ( "." );
		queryString.concat ( globals.public_ip_address[2] );
		queryString.concat ( "." );
		queryString.concat ( globals.public_ip_address[3] );
		queryString.concat ( kREPORT_HIDDEN_STR );
		queryString.concat ( kREPORT_DMR_MARC_STR );
		queryString.concat ( kREPORT_TYPE_STR );
		queryString.concat ( reset_type );
		queryString.concat ( kREPORT_HTTP_STR );
		
		ethernet_conditional_stop ( false );
		
		while ( !connected && ( 0 != retry_count ) )
		{
			if ( kCONNECT_SUCCESS == ethernetClient.connect ( (const char*)serverString.c_str(), 80 ) )
			{
				connected = true;
#ifdef kBUILD_TO_DEBUG
				Serial.print ( "Connected to " );
				Serial.print ( serverString.c_str() );
				Serial.println ( ", sending GET to store event to database table." );
#endif
				ethernetClient.println ( queryString.c_str () );
				ethernetClient.print ( "Host: " );
				ethernetClient.println ( serverString.c_str() );
				ethernetClient.println ( "Connection: close" );
				ethernetClient.println ();
				ethernetClient.println ();
			}
			else
			{
#ifdef kBUILD_TO_DEBUG
				Serial.print ( ( kNOTIFICATION_RETRY_COUNT_SEED + 1 ) - retry_count );
				Serial.print ( "ERROR: Could not connect to " );
				Serial.println ( serverString.c_str() );
#endif
				if ( 0 != retry_count )
				{
					retry_count--;
					delay ( 5000 );
				}
			}

			ethernet_conditional_stop ( false );
		}

		ethernet_conditional_stop ( false );
	}
	
	display_time ( true );
#ifdef kBUILD_TO_DEBUG
	Serial.println ( "Did send reset report query" );
#endif
}

#endif											//	}	kBUILD_TO_SEND_REPORT

//  --------------------------------------------------------------------------------

void clear_public_ip_address ( void )
{
	for ( int index = 0; index < ( sizeof ( globals.public_ip_address ) / sizeof ( globals.public_ip_address[0] ) ); index++ )
	{
		globals.public_ip_address[0] = 0;
	}
}

//  --------------------------------------------------------------------------------

void display_local_ip_address ( void )
{
#ifdef kBUILD_TO_DEBUG							//	{
	Serial.print ( "Local IP:  " );
	Serial.print ( globals.ip_address[0] );
	Serial.print ( "." );
	Serial.print ( globals.ip_address[1] );
	Serial.print ( "." );
	Serial.print ( globals.ip_address[2] );
	Serial.print ( "." );
	Serial.println ( globals.ip_address[3] );
#endif											//	}	kBUILD_TO_DEBUG
}

//  --------------------------------------------------------------------------------

void display_public_ip_address ( void )
{
#ifdef kBUILD_TO_DEBUG							//	{
	Serial.print ( "Public IP: " );
	Serial.print ( globals.public_ip_address[0] );
	Serial.print ( "." );
	Serial.print ( globals.public_ip_address[1] );
	Serial.print ( "." );
	Serial.print ( globals.public_ip_address[2] );
	Serial.print ( "." );
	Serial.println ( globals.public_ip_address[3] );
#endif											//	}	kBUILD_TO_DEBUG
}

//  --------------------------------------------------------------------------------

void display_mac_address ( void )
{
#ifdef kBUILD_TO_DEBUG							//	{
	Serial.print ( "MAC:       " );
	Serial.print ( mac_address[0], HEX );
	Serial.print ( "." );
	Serial.print ( mac_address[1], HEX );
	Serial.print ( "." );
	Serial.print ( mac_address[2], HEX );
	Serial.print ( "." );
	Serial.print ( mac_address[3], HEX );
	Serial.print ( "." );
	Serial.print ( mac_address[4], HEX );
	Serial.print ( "." );
	Serial.println ( mac_address[5], HEX );
#endif											//	}	kBUILD_TO_DEBUG
}

//  --------------------------------------------------------------------------------

void set_ip_pol_timer ( void )
{
	globals.next_ip_poll_minute = globals.minute + kIP_POLL_MINUTE_INTERVAL;
	if ( kMINUTES_PER_HOUR <= globals.next_ip_poll_minute )
	{
		globals.next_ip_poll_minute -= kMINUTES_PER_HOUR;
	}
#ifdef kBUILD_TO_DEBUG							//	{
	Serial.print ( "Next IP poll scheduled at minute " );
	Serial.println ( globals.next_ip_poll_minute );
#endif											//	}	kBUILD_TO_DEBUG
}

//  --------------------------------------------------------------------------------
//	It has been observed that there are cases where ethernetClient.connect() returns 
//	true but ethernetClient.connected() returns false.  In order to enable a retry 
//	mechanism, the force_stop variable will issue a flush() and stop(), regardless of
//	the state returned by ethernetClient.connected.  Note that in cases where 
//	ethernetClient.connect() returns true and ethernetClient.connected() returns 
//	false, no data is obtained after issuing a GET query, necessitating a retry 
//	mechanism to ensure that the public IP address can be obtained.

void	ethernet_conditional_stop ( boolean force_stop )
{
	if ( ethernetClient.connected () || force_stop )
	{
#ifdef kBUILD_TO_DEBUG							//	{
		Serial.println ( "Closing connection." );
#endif											//	}	kBUILD_TO_DEBUG
		ethernetClient.flush ();
		ethernetClient.stop ();
	}
}

//  --------------------------------------------------------------------------------

boolean	get_public_ip_address ( boolean issue_reset )
{
	unsigned long int	availableTimer = 0;
	unsigned int		retry_count = kNOTIFICATION_RETRY_COUNT_SEED;
	byte				public_ip_address[] = {0,0,0,0};
	int					availableDataSize;
	boolean				found_ip_address = false;
	boolean				reset_occurred = false;
	boolean				values_matched = true;
	boolean				all_zero = true;
	
#ifdef kBUILD_TO_DEBUG							//	{
	Serial.println ( "----------" );
#endif											//	}	kBUILD_TO_DEBUG
	ethernet_conditional_stop ( false );
	
	while ( !found_ip_address && ( 0 != retry_count ) )
	{
#ifdef kBUILD_TO_DEBUG							//	{
			Serial.print ( "Connect to " );
			Serial.println ( server_domain_name );
#endif											//	}	kBUILD_TO_DEBUG
		if ( kCONNECT_SUCCESS == ethernetClient.connect ( (const char*)server_domain_name, 80 ) )
		{
#ifdef kBUILD_TO_DEBUG							//	{
			Serial.print ( "Successful connect to " );
			Serial.println ( server_domain_name );
			
			if ( !ethernetClient.connected () )
			{
				Serial.print ( "ERROR: Not connected!" );
			}
#endif											//	}	kBUILD_TO_DEBUG
			ethernetClient.print ( "GET " );
			ethernetClient.print ( server_page_name );
			ethernetClient.println ( " " );
			
			availableTimer = kIP_QUERY_TIMEOUT;
			availableDataSize = ethernetClient.available ();
			do {
				delay ( 100 );
				availableDataSize = ethernetClient.available ();
				
				if ( 0 < availableTimer )
				{
					availableTimer--;
				}
			} while ( ( kIP_QUERY_MINIMUM_SIZE > availableDataSize ) & ( 0 != availableTimer ) );
			
			if ( 0 != availableDataSize )
			{
				if ( textFinder.find ( (char*)ip_search_str ) )
				{
					values_matched = true;
					all_zero = true;
					for ( int index = 0; index < ( sizeof ( public_ip_address ) / sizeof ( public_ip_address[0] ) ); index++ )
					{
						public_ip_address[index] = textFinder.getValue ();
						
						if ( globals.public_ip_address[index] != public_ip_address[index] )
						{
							values_matched = false;
							globals.public_ip_address[index] = public_ip_address[index];
						}
						if ( 0 != public_ip_address[index] )
						{
							all_zero = false;
						}
					}
					
					ethernet_conditional_stop ( false );
					if ( values_matched || !all_zero )
					{
						found_ip_address = true;
					}
					
#ifdef kBUILD_TO_DEBUG							//	{
					Serial.print ( "values_matched: " );
					Serial.print ( values_matched );
					Serial.print ( ", all_zero: " );
					Serial.print ( all_zero );
					Serial.print ( ", issue_reset: " );
					Serial.print ( issue_reset );
					Serial.print ( ", found_ip_address: " );
					Serial.println ( found_ip_address );
#endif											//	}	kBUILD_TO_DEBUG
					display_public_ip_address ();
					if ( !values_matched && !all_zero )
					{
						if ( issue_reset )
						{
							reset_rd982 ( kRESET_TYPE__PUBLIC_IP );
							reset_occurred = true;
						}
						else
						{
							display_time ( true );
						}
					}
					else
					{
						display_time ( true );
					}
				}
			}
			else
			{
#ifdef kBUILD_TO_DEBUG							//	{
				Serial.print ( "ERROR: Timed out with no data found.  RETRY: " );
				Serial.println ( ( kNOTIFICATION_RETRY_COUNT_SEED + 1 ) - retry_count );
#endif											//	}	kBUILD_TO_DEBUG
			}
		}
		else
		{
#ifdef kBUILD_TO_DEBUG							//	{
			Serial.print ( "ERROR: Could not connect to " );
			Serial.println ( server_domain_name );
#endif											//	}	kBUILD_TO_DEBUG
		}

		ethernet_conditional_stop ( true );

		if ( 0 != retry_count )
		{
			retry_count--;
			if ( !found_ip_address )
			{
				delay ( 1000 );
			}
		}
	}

	set_ip_pol_timer ();

#ifdef kBUILD_TO_DEBUG							//	{
	Serial.println ();
#endif											//	}	kBUILD_TO_DEBUG
}

//  --------------------------------------------------------------------------------

void setup ()
{
	unsigned long int		dhcp_address = 0;
	
#ifdef kBUILD_TO_DEBUG							//	{
	Serial.begin ( 115200 );
#endif											//	}	kBUILD_TO_DEBUG
	
	globals.resetAlarmFired = false;
	clear_public_ip_address ();
	
	pinMode ( kRD382_RESET_PIN_ACTIVE_HIGH, OUTPUT );  
	pinMode ( kETHERNET_CS_PIN, OUTPUT );
	
	digitalWrite ( kRD382_RESET_PIN_ACTIVE_HIGH, kRD382_RESET_NEGATED );
	digitalWrite ( kETHERNET_CS_PIN, kSPI_DEVICE_DISABLED );
	
	rtc.begin ( kRTC_SS_PIN );
	rtc.set24Hour ();

	display_mac_address ();
	if ( 0 == Ethernet.begin ( (byte*)mac_address ) )
	{
#ifdef kBUILD_TO_DEBUG							//	{
		Serial.println ( "ERROR: Unable to start Ethernet!" );
#endif											//	}	kBUILD_TO_DEBUG
		while ( true ) {};
	}
	
	globals.ip_address = Ethernet.localIP ();
	display_local_ip_address ();
	display_public_ip_address ();
	
#ifdef kBUILD_FOR_CLOCK							//	{		
#ifdef kBUILD_TO_SET_CLOCK						//	{
	rtc.autoTime ();
#endif											//	}	kBUILD_TO_SET_CLOCK

	get_current_time ();
	globals.alarmTimeDate	= globals.date;
	globals.alarmTimeDay	= globals.day;
	globals.alarmTimeHour	= globals.hour;
	globals.alarmTimeMinute	= globals.minute;
	globals.alarmTimeSecond	= 0;
	display_time ( false );
	
	//  The first alarm, after power on RESET, will schedule an RD-982 RESET 
	//	several minutes after this code runs.  The purpose of this is to allow 
	//	RD-982 internet services (i.e. DSL Modem initialization) to become available, 
	//	and then issue a RESET to the RD-982 so that a DHCP address will be acquired 
	//	and a connection to BrandMeister network services will then be available.  
	//	This will result in a short-term drop out of local repeat services 
	//	approximately 10-minutes after power is restored following a power outage.  

	globals.alarmTimeMinute = globals.alarmTimeMinute + kFIRST_RESET_MINUTE_OFFSET;
	if ( kMINUTES_PER_HOUR <= globals.alarmTimeMinute )
	{
		globals.alarmTimeMinute -= kMINUTES_PER_HOUR;
		globals.alarmTimeHour++;
		if ( kHOURS_PER_DAY <= globals.alarmTimeHour )	
		{
			globals.alarmTimeHour -= kHOURS_PER_DAY;
		}
	}
#endif											//	}	kBUILD_FOR_CLOCK

	get_public_ip_address ( false );
}

//  --------------------------------------------------------------------------------

void loop ()
{
    unsigned long int	currentTime = millis ();
	boolean				normal_reset;
	boolean				reset_occurred = false;
	
    if ( globals.currentTime != currentTime )
    {
    	globals.currentTime = currentTime;
    	
		if ( ip_update_timer_fired () )
		{
			reset_occurred = get_public_ip_address ( true );
		}

#ifdef kBUILD_FOR_CLOCK							//	{		
		if ( !reset_occurred )
		{
			get_current_time ();

			if ( globals.lastSecond != globals.second )
			{
				globals.lastSecond = globals.second;

				if ( globals.alarmTimeHour == globals.hour && 
					 globals.alarmTimeMinute == globals.minute )
				{
					if ( !globals.resetAlarmFired )
					{
						normal_reset = isNormalResetTime ();
						setNormalResetTime ();
						reset_rd982 ( normal_reset ? kRESET_TYPE__SCHEDULED : kRESET_TYPE__POWER_FAIL );
						globals.resetAlarmFired = true;
					}
				}
				else
				{
					globals.resetAlarmFired = false;
				}
			}
		}
#endif											//	}	kBUILD_FOR_CLOCK
		
		Ethernet.maintain ();
    }
}

The above Arduino source code obfuscates some database interface information. Reset events that are generated by the Arduino device are posted to a MySQL database, and are available for viewing on-line. Optionally, the same data can be sent as an email notification and/or text message notification to the repeater system operator.

SOURCE CODE VALIDATION

The following image shows the RD-982 Reset module appearing on the local area network.


RD-982 IP Reset Device Info

All three reset scenarios have been encountered during testing, and issuing of a RESET appears to be performed properly. With IP polling occuring at 5-minute intervals, the act of polling does not appear to result in renewal of the DHCP lease with Frontier Communications DSL, and this observation further justifies the need for detection of IP address assignment for the purpose of maintaining or restoring BrandMeister network connectivity.

A debug build of the above code will log messages to the Arduino IDE Serial Monitor. A typical log output is shown below. This log sample includes real networking errors that occurred while attempting to resolve the public IP address and the recovery from those errors, which employs a retry method.

MAC:       52.44.2D.39.38.32
Local IP:  192.168.254.27
Public IP: 0.0.0.0
----------
Connect to checkip.dyndns.org
ERROR: Timed out with no data found.  RETRY: 1
Closing connection.
Connect to checkip.dyndns.org
ERROR: Timed out with no data found.  RETRY: 2
Closing connection.
ERROR: Could not connect to checkip.dyndns.org
Closing connection.
Connect to checkip.dyndns.org
Closing connection.
values_matched: 0, all_zero: 0, issue_reset: 0, found_ip_address: 1
Public IP: 192.183.167.10
12:47:56, next SCHEDULED or POWER UP RESET will occur at 12:57:0
Closing connection.
Next IP poll scheduled at minute 52

----------
Connect to checkip.dyndns.org
ERROR: Timed out with no data found.  RETRY: 1
Closing connection.
Connect to checkip.dyndns.org
ERROR: Timed out with no data found.  RETRY: 2
Closing connection.
Connect to checkip.dyndns.org
Closing connection.
values_matched: 1, all_zero: 0, issue_reset: 1, found_ip_address: 1
Public IP: 192.183.167.10
12:52:0, next SCHEDULED or POWER UP RESET will occur at 12:57:0
Closing connection.
Next IP poll scheduled at minute 57


ASSERT Hytera RD-982 RESET
==========
Will send reset report query
Connected to ciarc.org, sending GET to store event to database table.
Closing connection.
12:57:0, next SCHEDULED or POWER UP RESET will occur at 5:30:0
Did send reset report query
----------
Connect to checkip.dyndns.org
ERROR: Timed out with no data found.  RETRY: 1
Closing connection.
ERROR: Could not connect to checkip.dyndns.org
Closing connection.
Connect to checkip.dyndns.org
Closing connection.
values_matched: 1, all_zero: 0, issue_reset: 1, found_ip_address: 1
Public IP: 192.183.167.10
12:57:0, next SCHEDULED or POWER UP RESET will occur at 5:30:0
Closing connection.
Next IP poll scheduled at minute 2

----------
Connect to checkip.dyndns.org
Closing connection.
values_matched: 1, all_zero: 0, issue_reset: 1, found_ip_address: 1
Public IP: 192.183.167.10
13:2:0, next SCHEDULED or POWER UP RESET will occur at 5:30:0
Closing connection.
Next IP poll scheduled at minute 7

----------
ERROR: Could not connect to checkip.dyndns.org
Closing connection.
Connect to checkip.dyndns.org
Closing connection.
values_matched: 1, all_zero: 0, issue_reset: 1, found_ip_address: 1
Public IP: 192.183.167.10
13:7:0, next SCHEDULED or POWER UP RESET will occur at 5:30:0
Closing connection.
Next IP poll scheduled at minute 12

----------
Connect to checkip.dyndns.org
ERROR: Timed out with no data found.  RETRY: 1
Closing connection.
Connect to checkip.dyndns.org
ERROR: Timed out with no data found.  RETRY: 2
Closing connection.
Connect to checkip.dyndns.org
ERROR: Timed out with no data found.  RETRY: 3
Closing connection.
Connect to checkip.dyndns.org
Closing connection.
values_matched: 1, all_zero: 0, issue_reset: 1, found_ip_address: 1
Public IP: 192.183.167.10
13:12:0, next SCHEDULED or POWER UP RESET will occur at 5:30:0
Closing connection.
Next IP poll scheduled at minute 17

LOGGED RESET EVENTS AND OBSERVATIONS

Hytera RD-982 RESET events for the W7CIA (311602) DMR Repeater are shown in the table below, with the most recent RESET event appearing at the top of the table.


DMR REPEATER RESET LOG
RESET Event Date & TimeDMR Marc IDCall SignRESET Event Type
Monday November 13, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Sunday November 12, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Saturday November 11, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Friday November 10, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Thursday November 9, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Wednesday November 8, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Tuesday November 7, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Monday November 6, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Sunday November 5, 2017 2238 UTC311602W7CIAPRIMARY POWER RESTORED
Sunday November 5, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Saturday November 4, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Friday November 3, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Thursday November 2, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Wednesday November 1, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Tuesday October 31, 2017 1821 UTC311602W7CIAPRIMARY POWER RESTORED
Wednesday October 11, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Tuesday October 10, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Monday October 9, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Sunday October 8, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Saturday October 7, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Friday October 6, 2017 1718 UTC311602W7CIAIP ADDRESS CHANGED
Friday October 6, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Thursday October 5, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Wednesday October 4, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT
Tuesday October 3, 2017 1130 UTC311602W7CIADAILY SCHEDULED EVENT

The RESET event table contains no data for the N7IBC (311603) DMR Repeater. This is indicative that the IP reset module has not yet been installed on the N7IBC (311603) DMR Repeater.

The RESET event table contains no data for the KC7MCC (311604) DMR Repeater. This is indicative that the IP reset module has not yet been installed on the KC7MCC (311604) DMR Repeater.


On occasion, with version A7.06.04.000 firmware installed in the RD-982 repeater, a Low Forward Power alarm has been observed. This alarm occurs rarely, but when it does occur, all repeater operation is inhibited. Any attempt to input a manual reset from DTMF command on a time-slot 1 channel will result in a denial tone and an inability to invoke the DTMF command. Should this occur, the daily scheduled reset will provide recovery within 24-hours. For this reason, a control receiver is recommended to enable remote manual reset in the event that the repeater is in an alarm condition that prevents normal operation.


Prior to installling this module on the W7CIA / 311602 repeater, changes to the public IP were observed to occur at a rate of approximately once every 2.5 days. The frequency of these events appears to have decreased, as is evident in the reset log above. The current working theory is that the act of optaining the public IP address may be holding off expiration of the DHCP lease and that changes to the public IP address are occuring related to router maintenance tasks being performed by the internet service provider.


The original content for this article is posted at:

www.ciarc.org/projects/hytera_rd982_ip_reset.php.

Updates to this article will be posted at the original address.


Information on the GNU free software licenses can be found by clicking here.


A copy of the GNU free software license can be downloaded by clicking here.