// 1-channel LoRa Gateway for ESP8266 and ESP32
// Copyright (c) 2016-2020 Maarten Westenberg version for ESP8266
//
// 	based on work done by Thomas Telkamp for Raspberry PI 1ch gateway
//	and many others.
//
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the MIT License
// which accompanies this distribution, and is available at
// https://opensource.org/licenses/mit-license.php
//
// NO WARRANTY OF ANY KIND IS PROVIDED
//
// Author: Maarten Westenberg (mw12554@hotmail.com)
//
// _udpSemtech.ino: This file contains the UDP specific code enabling to receive
// and transmit packages/messages to the server usig Semtech protocol.
// ========================================================================================

// Also referred to as Semtech code

#if defined(_UDPROUTER)

//	If _UDPROUTER is defined, _TTNROUTER should NOT be defined. So...
#if defined(_TTNROUTER)
#error "Please make sure that either _UDPROUTER or _TTNROUTER are defined but not both"
#endif

// The following functions ae defined in this module:
// int readUdp(int Packetsize)
// int sendUdp(IPAddress server, int port, uint8_t *msg, uint16_t length)
// bool connectUdp();
// void pullData();
// void sendstat();

// ----------------------------------------------------------------------------
// connectUdp()
//	connect to UDP (which is a local thing, after all UDP 
// connections do not exist.
// Parameters:
//	<None>
// Returns
//	Boollean indicating success or not
// ----------------------------------------------------------------------------
bool connectUdp() 
{

	bool ret = false;
	unsigned int localPort = _LOCUDPPORT;			// To listen to return messages from WiFi
#	if _MONITOR>=1
	if (debug>=1) {
		mPrint("Local UDP port=" + String(localPort));
	}
#	endif //_MONITOR

	if (Udp.begin(localPort) == 1) {
#		if _MONITOR>=1
		if (debug>=1) {
			mPrint("UDP Connection successful");
		}
#		endif //_MONITOR
		ret = true;
	}
	else{
#		if _MONITOR>=1
		if (debug>=0) {
			mPrint("Connection failed");
		}
#		endif //_MONITOR
	}
	return(ret);
}// connectUdp


// ----------------------------------- DOWN -----------------------------------
//
// readUdp()
// Read DOWN a package from UDP socket, can come from any server
// Messages are received when server responds to gateway requests from LoRa nodes 
// (e.g. JOIN requests etc.) or when server has downstream data.
// We respond only to the server that sents us a message!
//
// Note: So normally we can forget here about codes that do upstream
//
// Parameters:
//	Packetsize: size of the buffer to read, as read by loop() calling function
//
//	Byte 0: 	Contains Protocol Version
//	Byte 1+2:	Contain Token
//	Byte 3:		Contains PULL_RESP or other identifier
//	Byte 4 >	Contains payload (or Gateway EUI 8 bytes first)
//
// Returns:
//	-1 or false if not read
//	Or number of characters read if success
//
// ----------------------------------------------------------------------------
int readUdp(int packetSize)
{ 
	uint8_t buff[32]; 						// General buffer to use for UDP, set to 32
	uint8_t buff_down[RX_BUFF_SIZE];		// Buffer for downstream

	// Make sure we are connected over WiFI
	if (WlanConnect(10) < 0) {				// MMM 200316 Every call contains yield()
#		if _MONITOR>=1
			mPrint("Dwn readUdp:: ERROR connecting to WLAN");
#		endif //_MONITOR
		Udp.flush();
		return(-1);
	}
	
	//yield();								// MMM 200320 Clear buffer in kernel (?)
	
	if (packetSize > RX_BUFF_SIZE) {
#		if _MONITOR>=1
			mPrint("Dwn readUdp:: ERROR package of size: " + String(packetSize));
#		endif //_MONITOR
		Udp.flush();
		return(-1);
	}

	//yield();								// MMM 200406
	
	// We assume here that we know the originator of the message.
	// In practice however this can be any sender!
	if (Udp.read(buff_down, packetSize) < packetSize) {
#		if _MONITOR>=1
			mPrint("Dwn readUdp:: Reading less chars");
#		endif //_MONITOR
		return(-1);
	}

	yield();								// MMM 200406

	// Remote Address should be known
	IPAddress remoteIpNo = Udp.remoteIP();

	// Remote port is either of the remote TTN server or from NTP server (=123)
	
	unsigned int remotePortNo = Udp.remotePort();
	if (remotePortNo == 123) {				// NTP message arriving, not expected
		// This is an NTP message arriving
#		if _MONITOR>=1
		if (debug>=0) {
			mPrint("Dwn readUdp:: NTP msg rcvd");
		}
#		endif //_MONITOR
		gwayConfig.ntpErr++;
		gwayConfig.ntpErrTime = now();
		Udp.flush();						// MMM 200326 Clear buffer when time response arrives (error)
		return(0);
	}
	
	// If it is not NTP it must be a LoRa message for gateway or node
	
	else {
		uint8_t *data = (uint8_t *) ((uint8_t *)buff_down + 4);
		//uint8_t protocol= buff_down[0];
		//uint16_t token= buff_down[2]*256 + buff_down[1];
		uint8_t ident= buff_down[3];

#		if _MONITOR>=1
		if ((debug>=2) && (pdebug & P_TX)) {
			mPrint("Dwn readUdp:: message ident="+String(ident));
		}
#		endif //_MONITOR

		// now parse the message type from the server (if any)
		switch (ident) {


		// This message is used by the gateway to send sensor data UP to server. 
		// As this function is used for downstream only, this option
		// will never be selected but is included as a reference only
		// Para 5.2.1, Semtech Gateway to Server Interface document
		case PKT_PUSH_DATA: 							// 0x00 UP
#			if _MONITOR>=1
			if (debug>=1) {
				mPrint("Dwn PKT_PUSH_DATA:: size "+String(packetSize)+" From "+String(remoteIpNo.toString()));
			}
#			endif //_MONITOR
			//Udp.flush();
		break;


		// This message is sent DOWN by the server to acknowledge receipt of a
		// (sensor) PKT_PUSH_DATA message sent with the code above.
		// Para 5.2.2, Semtech Gateway to Server Interface document
		case PKT_PUSH_ACK:								// 0x01 DOWN
#			if _MONITOR>=1
			if ((debug>=2) && (pdebug & P_TX)) {
				mPrint("Dwn PKT_PUSH_ACK:: size="+String(packetSize)+", From "+String(remoteIpNo.toString())); 
			}
#			endif //_MONITOR
			//Udp.flush();
		break;


		// PULL DATA message
		// This is a request/UP message and will not be executed by this function.
		// We have it here as a description only. 
		//	Para 5.2.3, Semtech Gateway to Server Interface document
		case PKT_PULL_DATA:								// 0x02 UP
#			if _MONITOR>=1
			if ((debug>=1) && (pdebug & P_RX)) {
				mPrint("Dwn readUdp:: PKT_PULL_DATA");
			}
#			endif //_MONITOR
			Udp.flush();					// MMM 200419 Added
		break;


		// PULL_ACK message
		// This is the response to PKT_PULL_DATA message
		// Para 5.2.4, Semtech Gateway to Server Interface document
		// With this ACK, the server confirms the gateway that the route is open
		// for further PULL_RESP messages from the server (to the device)
		// The server sends a PULL_ACK to confirm PULL_DATA receipt, no response is needed
		//
		// Byte 0		contains Protocol Version == 2
		// Byte 1-2		Random Token
		// Byte 3		PULL_ACK ident == 0x04
		//
		case PKT_PULL_ACK:								// 0x04 DOWN
#			if _MONITOR>=1
			if ((debug>=2) && (pdebug & P_TX)) {
				String response="Dwn readUdp PKT_PULL_ACK: micr=";
				printInt(micros(), response);
				response += ", size="+String(packetSize)+" From ";
				printIP(remoteIpNo,'.',response);
				response += ", port " +String(remotePortNo);
				mPrint(response);

				if (debug>=2) {
					Serial.print(F(", data: "));
					for (int i=0; i<packetSize; i++) {
						Serial.print(buff_down[i],HEX);
						Serial.print(':');
					}
					Serial.println();
				}
			}
#			endif //_MONITOR

			
			yield();			
			
			Udp.flush();								// MMM 200419 
		break;



		// This message type is used to confirm OTAA message to the node,
		// but this message format will also be used for other downstream communication
		// It's length shall not exceed 1000 Octets. This is:
		//	RECEIVE_DELAY1		1 s 
		//	RECEIVE_DELAY2		2 s (is RECEIVE_DELAY1+1)
		//	JOIN_ACCEPT_DELAY1	5 s
		//	JOIN_ACCEPT_DELAY2	6 s
		// Para 5.2.5, Semtech Gateway to Server Interface document
		//
		case PKT_PULL_RESP:								// 0x03 DOWN

			// Define when we start with the response to node
#			ifdef _PROFILER
			if ((debug>=1) && (pdebug & P_TX)) {
				mPrint("Dwn PKT_PULL_RESP:: sendPacket: micros="+String(micros() ));
			}
#			endif //_PROFILER

			// Send to the LoRa Node first (timing) and then do reporting to _Monitor
			_state=S_TX;
			sendTime = micros();						// record when we started sending the message

			// Send the package DOWN to the sensor
			// We just read the packet from the Network Server and it is formatted
			// as described in the specs.
			if (sendPacket(data, packetSize-4) < 0) {
#				if _MONITOR>=1
				if (debug>=0) {
					mPrint("Dwn readUdp:: ERROR: PKT_PULL_RESP sendPacket failed");
				}
#				endif //_MONITOR
				Udp.flush();
				return(-1);
			}


			// We need a timeout for this case. In case there does not come an interrupt,
			// then there will not be a TXDONE but probably another CDDONE/CDDETD before
			// we have a timeout in the main program (Keep Alive)

			loraWait(&LoraDown);

			// Set state to transmit
			// Clear interrupt flags and masks
			writeRegister(REG_IRQ_FLAGS_MASK, (uint8_t) 0x00);			// MMM 200407 Reset
			writeRegister(REG_IRQ_FLAGS, (uint8_t) 0xFF);				// reset interrupt flags
			
			// Initiate the transmission of the buffer
			// (We normally react on ALL interrupts if we are in TX state)
			txLoraModem(&LoraDown);

			// wait extra delay out. The delayMicroseconds timer is accurate until 16383 uSec.
#			ifdef _PROFILER
			if ((debug>=1) && (pdebug & P_TX))
			{
				String response = "Dwn PKT_PULL_RESP:: txLoraModem done: ";
				printDwn(&LoraDown, response);
				mPrint(response);
			}
#			endif //_PROFILER

#			if _MONITOR>=1
			if (( debug>=2 ) && ( pdebug & P_TX )) {
				uint8_t flags = readRegister(REG_IRQ_FLAGS);
				uint8_t mask  = readRegister(REG_IRQ_FLAGS_MASK);
				uint8_t intr  = flags & ( ~ mask );

				String response="Dwn txLoraModem fini:: ";
				mStat(intr, response);
				mPrint(response); 

				response = "Dwn readUdp:: PKT_PULL_RESP from IP="+String(remoteIpNo.toString())
					+", micros=" + String(micros())
					+", wait=";
				if (sendTime < micros()) {
					response += String(micros() - sendTime) ;
				}
				else {
					response += "-" + String(sendTime - micros()) ;
				};
				mPrint(response);
			}
#			endif //_MONITOR

			// After filling the buffer we only react on TXDONE interrupt
			// So, more or less start at the "case TXDONE:"  
			_state=S_TXDONE;
			_event=1;										// Or remove the break below
			
			// No break!!


		// This is the response to the PKT_PULL_RESP message by the sensor device
		// it is sent by the gateway UP to the server to confirm the PULL_RESP message.
		//
		case PKT_TX_ACK:									// Message id: 0x05 UP

			if (buff_down[0]== 1) {
#				if _MONITOR>=1
				if ((debug>=1) && (pdebug & P_TX)) {
					mPrint("UP readUdp:: PKT_TX_ACK: protocol version 1");
					data = buff_down + 4;
					data[packetSize] = 0;
				}
#				endif
				break;										// return
			}

#			ifdef _PROFILER
			if ((debug>=1) && (pdebug & P_TX)) {
				mPrint("UP readUDP:: TX_ACK protocol version 2+");
			}
#			endif //_PROFILER


			// Now respond with an PKT_TX_ACK; UP 
			// Byte 3 is 0x05; see para 5.2.6 of spec
			buff[0]= buff_down[0];							// As read from the Network Server
			buff[1]= buff_down[1];							// Token 1
			buff[2]= buff_down[2];							// Token 2
			buff[3]= PKT_TX_ACK;							// ident == 0x05;
			buff[4]= MAC_array[0];
			buff[5]= MAC_array[1];
			buff[6]= MAC_array[2];
			buff[7]= 0xFF;
			buff[8]= 0xFF;
			buff[9]= MAC_array[3];
			buff[10]=MAC_array[4];
			buff[11]=MAC_array[5];
			buff[12]=0;										// Not error means "\0"
			// If it is an error, please look at para 6.1.2

			yield();

			// Only send the PKT_PULL_ACK to the UDP socket that just sent the data!!!
			Udp.beginPacket(remoteIpNo, remotePortNo);
			
			if (Udp.write((unsigned char *)buff, 12) != 12) {
#				if _MONITOR>=1
				if (debug>=0) {
					mPrint("UP readUdp:: ERROR: PKT_PULL_ACK write");
				}
#				endif //_MONITOR
			}
			else {
#				if _MONITOR>=1
				if ((debug>=0) && (pdebug & P_TX)) {
					mPrint("UP readUdp:: PKT_TX_ACK: micros="+String(micros()));
				}
#				endif //_MONITOR
			}

			if (!Udp.endPacket()) {
#				if _MONITOR>=1
				if ((debug>=0) && (pdebug & P_TX)) {
					mPrint("UP readUdp:: PKT_PULL_DATALL: ERROR Udp.endPacket");
				}
#				endif //_MONITOR
			}
			
			yield();

			// ONLY NOW WE START TO MONITOR THE PKT_PULL_RESP MESSAGE.
#			if _MONITOR>=1
			if ((debug>=1) && (pdebug & P_TX)) {
				data = buff_down + 4;
				data[packetSize] = 0;
				mPrint("Dwn readUdp:: PKT_PULL_RESP: size="+String(packetSize)+", data="+String((char *)data)); 
			}
#			endif //_MONITOR

		break;

		default:
#			if _GATEWAYMGT==1
				// For simplicity, we send the first 4 bytes too
				gateway_mgt(packetSize, buff_down);
			else

#			endif
#			if _MONITOR>=1
				mPrint(", ERROR ident not recognized="+String(ident));
#			endif //_MONITOR
		break;
		}
		
#		if _MONITOR>=1
		if (debug>=2) {
			String response= "Dwn readUdp:: ret="+String(packetSize)+", data=";
			for (int i=0; i<packetSize; i++) {
				response+= String(buff_down[i]) + " ";
			}
			mPrint(response); 
		}
#		endif //_MONITOR

		// For downstream messages
		return packetSize;
	}
}//readUdp


// ----------------------------------- UP -------------------------------------
//
// sendUdp()
// Send UP an UDP/DGRAM message to the MQTT server
// If we send to more than one host (not sure why) then we need to set sockaddr 
// before sending.
// Parameters:
//	IPAddress
//	port
//	msg *
//	length (of msg)
// return values:
//	0: Error
//	1: Success
// ----------------------------------------------------------------------------
int sendUdp(IPAddress server, int port, uint8_t *msg, uint16_t length) 
{
	// Check whether we are conected to Wifi and the internet
	if (WlanConnect(3) < 0) {
#		if _MONITOR>=1
		if ( pdebug & P_MAIN ) {
			mPrint("sendUdp: ERROR not connected to WiFi");
		}
#		endif //_MONITOR						// MMM 200426 We removed yield() 
		Udp.flush();
		return(0);
	}

	//yield();									// MMM 200327 yield not necessary

	//send the update
#	if _MONITOR>=1
	if (( debug>=2 ) && ( pdebug & P_MAIN )) {
		mPrint("sendUdp: WlanConnect connected to="+WiFi.SSID()+". Server IP="+ String(WiFi.localIP().toString()) );
	}
#	endif //_MONITOR

	if (!Udp.beginPacket(server, (int) port)) {
#		if _MONITOR>=1
		if ( debug>=0 ) {
			mPrint("M sendUdp:: ERROR Udp.beginPacket");
		}
#		endif //_MONITOR
		return(0);
	}
	
	yield();									// MMM 200316 May not be necessary

	if (Udp.write((unsigned char *)msg, length) != length) {
#		if _MONITOR>=1
		if ( debug>=0 ) {
			mPrint("sendUdp:: ERROR Udp write");
		}
#		endif //_MONITOR
		Udp.endPacket();						// Close UDP
		return(0);								// Return error
	}
	
	yield();
	
	if (!Udp.endPacket()) {
#	if _MONITOR>=1
		if (debug>=0) {
			mPrint("sendUdp:: ERROR Udp.endPacket");
		}
#	endif //_MONITOR
		return(0);
	}
	return(1);
}//sendUDP




// ----------------------------------------------------------------------------
// pullData()
// Send UDP periodic Pull_DATA message UP to server to keepalive the connection
// and to invite the server to send downstream messages when these are available
// *2, par. 5.2
//	- Protocol Version (1 byte)
//	- Random Token (2 bytes)
//	- PULL_DATA identifier (1 byte) = 0x02
//	- Gateway unique identifier (8 bytes) = MAC address
// ----------------------------------------------------------------------------
void pullData()
{
    uint8_t pullDataReq[12]; 								// status report as a JSON object
    int pullIndex=0;
	
	uint8_t token_h = (uint8_t)rand(); 						// random token
    uint8_t token_l = (uint8_t)rand();						// random token
	
    // pre-fill the data buffer with fixed fields
    pullDataReq[0]  = PROTOCOL_VERSION;						// 0x01
    pullDataReq[1]  = token_h;
    pullDataReq[2]  = token_l;
    pullDataReq[3]  = PKT_PULL_DATA;						// 0x02
	// READ MAC ADDRESS OF ESP8266, and return unique Gateway ID consisting of MAC address and 2bytes 0xFF
    pullDataReq[4]  = MAC_array[0];
    pullDataReq[5]  = MAC_array[1];
    pullDataReq[6]  = MAC_array[2];
    pullDataReq[7]  = 0xFF;
    pullDataReq[8]  = 0xFF;
    pullDataReq[9]  = MAC_array[3];
    pullDataReq[10] = MAC_array[4];
    pullDataReq[11] = MAC_array[5];
    //pullDataReq[12] = 0/00; 								// add string terminator, for safety
	
    pullIndex = 12;											// 12-byte header
	
    //send the update
	
	uint8_t *pullPtr;
	pullPtr = pullDataReq,
#ifdef _TTNSERVER
    sendUdp(ttnServer, _TTNPORT, pullDataReq, pullIndex);
	yield();
#endif

#	if _MONITOR>=1
	if (pullPtr != pullDataReq) {
		mPrint("pullPtr != pullDatReq");
	}
#	endif //_MONITOR

#ifdef _THINGSERVER
	sendUdp(thingServer, _THINGPORT, pullDataReq, pullIndex);
#endif

#if _MONITOR>=1
    if ((debug>=2) && (pdebug & P_MAIN)) {
		yield();
		mPrint("M PKT_PULL_DATA request, len=" + String(pullIndex) );
		for (int i=0; i<pullIndex; i++) {
			Serial.print(pullDataReq[i],HEX);				// debug: display JSON stat
			Serial.print(':');
		}
		Serial.println();
	}
#endif // _MONITOR

	return;
	
} // pullData()


// ----------------------------------------------------------------------------
// sendstat()
// Send UP periodic status message to server even when we do not receive any
// data. 
// Parameters:
//	- <none>
// ----------------------------------------------------------------------------
void sendstat()
{

    uint8_t status_report[STATUS_SIZE]; 					// status report as a JSON object
    char stat_timestamp[32];								// 
	char clat[10]={0};
	char clon[10]={0};

    int stat_index=0;
	uint8_t token_h   = (uint8_t)rand(); 					// random token
    uint8_t token_l   = (uint8_t)rand();					// random token
	
    // pre-fill the data buffer with fixed fields
    status_report[0]  = PROTOCOL_VERSION;					// 0x01
	status_report[1]  = token_h;
    status_report[2]  = token_l;
    status_report[3]  = PKT_PUSH_DATA;						// 0x00
	
	// READ MAC ADDRESS OF ESP8266, and return unique Gateway ID consisting of MAC address and 2bytes 0xFF
    status_report[4]  = MAC_array[0];
    status_report[5]  = MAC_array[1];
    status_report[6]  = MAC_array[2];
    status_report[7]  = 0xFF;
    status_report[8]  = 0xFF;
    status_report[9]  = MAC_array[3];
    status_report[10] = MAC_array[4];
    status_report[11] = MAC_array[5];

    stat_index = 12;										// 12-byte header
	
	// XXX Using CET as the current timezone. Change to your timezone	
	sprintf(stat_timestamp, "%04d-%02d-%02d %02d:%02d:%02d CET", year(),month(),day(),hour(),minute(),second());
	//sprintf(stat_timestamp, "%04d-%02d-%02dT%02d:%02d:%02d.00000000Z", year(),month(),day(),hour(),minute(),second());

	ftoa(lat,clat,5);										// Convert lat to char array with 5 decimals
	ftoa(lon,clon,5);										// As IDE CANNOT prints floats

	// Build the Status message in JSON format, XXX Split this one up...
	yield();

    int j = snprintf((char *)(status_report + stat_index), STATUS_SIZE-stat_index, 
		"{\"stat\":{\"time\":\"%s\",\"lati\":%s,\"long\":%s,\"alti\":%i,\"rxnb\":%u,\"rxok\":%u,\"rxfw\":%u,\"ackr\":%u.0,\"dwnb\":%u,\"txnb\":%u,\"pfrm\":\"%s\",\"mail\":\"%s\",\"desc\":\"%s\"}}", 
		stat_timestamp, clat, clon, (int)alt, statc.msg_ttl, statc.msg_ok, statc.msg_down, 0, 0, 0, platform, email, description);

	yield();												// Give way to the internal housekeeping of the ESP8266

    stat_index += j;
    status_report[stat_index] = 0; 							// add string terminator, for safety

#	if _MONITOR>=1
    if (( debug>=2 ) && ( pdebug & P_MAIN )) {
		mPrint("M stat update: <"+String(stat_index)+"> "+String((char *)(status_report+12)) );
	}
#	endif //_MONITOR

	if (stat_index > STATUS_SIZE) {
#		if _MONITOR>=1
			mPrint("sendstat:: ERROR buffer too big");
#		endif //_MONITOR
		return;
	}

    //send the update
#	ifdef _TTNSERVER
		sendUdp(ttnServer, _TTNPORT, status_report, stat_index);
#	endif

#	ifdef _THINGSERVER
		yield();
		sendUdp(thingServer, _THINGPORT, status_report, stat_index);
#	endif
	return;

} // sendstat()


#endif //_UDPROUTER