#!/usr/bin/env python
#
# discovery.py
#
# Discover HPSDR hardware
# 
# Copyright (C) 2015 by G3UKB Bob Cowdery
# 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 2 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, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#    
#  The author can be reached by email at:   
#     bob@bobcowdery.plus.com
#

# Python imports
import sys, os
import socket
import threading
import traceback
import logging
import time

# Library imports
import numpy as np
import zmq
import netifaces

# Application imports
from global_defs import *
from defs import *
from ports import *
import logger
from events import *
import broker
import instance_cache

"""

Discover HPSDR hardware

"""
class Discover:
	
	def __init__(self):
		""" Constructor """
	
		# Here we just create the thread
		self.__discover_thrd = DiscoverThrd()
		
	def start_thrd(self):
		""" Run the thread """
		
		self.__discover_thrd.start()
		
	def terminate(self):
		""" Ask the thread to terminate and wait for it to exit """
		
		self.__discover_thrd.terminate()
		self.__discover_thrd.join()
		
	def reset(self):
		""" Ask the thread to do another discover cycle """
		
		self.__discover_thrd.reset()

class DiscoverThrd (threading.Thread):
	
	def __init__(self):
		""" Constructor """
		
		super(DiscoverThrd, self).__init__()

		self.__logger = logging.getLogger('sdrn.discover')
		self.__msg_callback = instance_cache.getInstance(UI_MSG)
		
		# Set numpy to print in hex for easier debugging
		#np.set_printoptions(formatter={'int':hex})
		
		# Discover request packet
		# 0-3 	sequence no (always set to zero)
		# 4 	command byte (0x02)
		# 5..59	zero
		self.__discovery_request = np.zeros((HPSDR_DISCOVER_RQST_SIZE), dtype=np.ubyte)
		self.__discovery_request[4] = 0x02	# Command discovery
		
		# Discover reply packet
		self.__discovery_reply = np.zeros((HPSDR_DISCOVER_RPLY_SIZE), dtype=np.ubyte)
		
		# Class vars
		self.__terminate = False	# True when terminating
		self.__hpsdr_units = 0		# Count of discovered units
		
		# Wait event
		self.__wait_event = threading.Event()

	def terminate(self):
		""" Terminate the thread """
		
		self.__terminate = True
		self.__wait_event.set()
	
	def reset(self):
		""" Reset the discover cycle """
		
		# Set the event to kick off another discover session
		self.__wait_event.set()
		
	def run(self):
		""" Thread entry point """
		
		#
		# In the thread run action we do the following:
		#	1. Preamble to set up for the communications, then
		#	2. Send a discovery broadcast on each interface in sequence, util
		#	3. We get a least one response and then wait for any further responses on the same interface
		#	4. Each response is published to inform subscribers of discovered HPSDR hardware
		#	5. During 2-4 check for a terminate and if active exit the thread
		#	6. When the scan is complete we wait on a signal. When the signal is active we check for terminate
		#		again and if inactive perform another scan.
		#
		
		# (1) Preamble ====================================================================
		# Create UDP socket
		sock = None
		# Get all IP addresses present on the machine
		# Note this uses netifaces which must be installed for the appropriate python version
		myIps = []
		for interface in netifaces.interfaces():
			if netifaces.AF_INET in netifaces.ifaddresses(interface):
				links = netifaces.ifaddresses(interface)[netifaces.AF_INET]
				for link in links:
					myIps.append(link['addr'])
		# (5) Loop if not terminating
		while not self.__terminate:
			# (2,3) Send broadcast on each IP address, look for response
			for myIp in myIps:
				# Bind UDP socket to any port
				try:
					if sock != None: sock.close()
					sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
					sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
					sock.bind((myIp, 0))
				except Exception as e:
					# Can't bind that interface
					self.__logger.log(logging.INFO, 'Cant bind to %s' % (myIp))
					self.__msg_callback(logging.INFO, 'Cant bind to %s' % (myIp))
					continue
			
				# .split main ping loop
				# We use zmq_poll to wait for activity on the UDP socket, since
				# this function works on non-0MQ file handles.			
				poller = zmq.Poller()
				poller.register(sock, zmq.POLLIN)
			
				# Broadcast discovery packet
				self.__logger.log(logging.INFO, 'Sending discovery broadcast on %s...' % (myIp))
				self.__msg_callback(logging.INFO, 'discover.run', 'Sending discovery broadcast on %s...' % (myIp))
				sock.sendto(self.__discovery_request, 0, ('<broadcast>', HPSDR_BASE_PORT))
				
				loop_count = 3	# Looking for responses
				while True:
					# Wait for any responses for DISCOVER_TIMEOUT ms
					events = dict(poller.poll(DISCOVER_TIMEOUT))
					if sock.fileno() in events:
						# We have a response
						# Read the reply
						self.__discovery_reply, addrinfo = sock.recvfrom(HPSDR_DISCOVER_RPLY_SIZE)
						# (4) Announce we are on-line
						broker.send (NODE_ONLINE_EVNT, None)
						# (4) and we have a reply
						broker.send (NODE_DISC_EVNT, self.__discovery_reply)
						# Bump the discovered units
						self.__hpsdr_units += 1
						# Update ui, logging and transcript
						broker.send(UI_UPDATE_EVNT, (addrinfo[0], self.__mac_to_string(self.__discovery_reply[5:11]), HPSDR_BOARD_TYPES[self.__discovery_reply[11]]))
						self.__logger.log(logging.INFO, 'Found HPSDR hardware at %s:%d' % addrinfo)
						self.__msg_callback(logging.INFO, 'discover.run', 'Found HPSDR hardware at %s:%d' % addrinfo)
					else:
						# Timeout
						if self.__hpsdr_units == 0:
							# No units yet, see if there are more interfaces to try
							break
						else:
							if loop_count <= 0:
								# We are all done
								break
							else:
								loop_count -= 1
				if self.__hpsdr_units > 0 and loop_count <= 0:
					# We are all done
					break
			
			# (6) Reset and wait for activation	
			self.__hpsdr_units = 0
			loop_count = 0
			if sock != None: sock.close()
			sock = None
			# Sit on the wait event
			self.__wait_event.wait()
			self.__wait_event.clear()
			# Clear the UI as we are about to do another scan
			broker.send (UI_CLEAR_EVNT, None)
			
	# PRIVATE ============================================================================
	def __mac_to_string(self, mac_addr):
		""" Convert bytes to a string
		
		Arguments:
			mac_addr	--	6 byte array
			
		"""
		
		return ':'.join('%02x' % b for b in mac_addr)
		
		
		
