## @file OTA_Central_BlueNRG.py
## @brief This script is used as component for implementing the Over-the-Air (OTA) FW upgrade of a BlueNRG device providing the OTA service capability.
## @li The script is implemented using the new BlueNRG GUI language scripts based on the BlueNRG ACI APIs. The BlueNRG GUI with script engine is delivered within BlueNRG DK 1.9.0 SW package and coming next versions.
## @li The script can also be run using the BlueNRG script launcher standalone utility available with BlueNRG DK 1.9.0 SW package and coming next versions.
## @li A PC running Windows Vista/7 operating systems (64 and 32 bit version) or Windows XP operating systems (32 bit edition), with available USB ports is required for installing the BlueNRG, BlueNRG-MS DK SW package.
##
## <!-- Copyright 2015 by STMicroelectronics.  All rights reserved.       *80*-->
##
##  @section S100 OTA_Central_BlueNRG.py, preliminary steps: configure a BlueNRG Peripheral device with a application providing the OTA service and related characteristics 
##
##  @li On a BlueNRG device load an application image providing the OTA service (reference example application are BLE_Chat and SensorDemo applications with OTA service configuration).
##  @li Make sure application is on discoverable mode and it offers the OTA service capability (refer to Application Note AN4486 for more detailed description)
##
##  @section S101  OTA_Central_BlueNRG.py script execution steps through BlueNRG GUI Script Engine
##
##  @li Open BlueNRG GUI available within BlueNRG DK 1.9.0 SW package (and coming next versions)
##  @li Connect a BlueNRG-MS platform to a PC USB port
##  @li Put the BlueNRG-MS platform in DFU mode and download the "C:\Program Files (x86)\STMicroelectronics\BlueNRG DK 1.x.0\Firmware\STM32L1_prebuilt_images\BlueNRG_VCOM_1_x.hex" using the BlueNRG GUI, Tools, Flash Motherboard FW... utility
##  @li On BlueNRG GUI, open the USB Port associated to the plugged BlueNRG-MS platform
##  @li On BlueNRG GUI, select the Scripts window 
##  @li On BlueNRG GUI, Scripts window, Scripts Engine section, click on ... tab and browse to the OTA_Central_BlueNRG.py script location and select it
##  @li On BlueNRG GUI, click on  Run Script tab for running the selected script
##  @li BlueNRG  device Slave_A enters in discovery mode.
##  @li BlueNRG OTA central device searches for Peripheral devices offering the OTA service and it allows user to select the identified device (by pressing the related index key on keyboard)
##  @li Then user is asked to select a binary application image (*.bin file) built with the proper base address (based on the peer device available free flash memory layout) and to start the FW upgrade process
##  @li When the FW upgrade process is completed a specific message is displayed
##  @li NOTE: OTA_Central_BlueNRG.py can also be run using the BlueNRG script launcher standalone utility (refer to the related doxygen documentation for instrunctions about how to use it)
##
##  @section S102 Supported BlueNRG-MS platforms
##
##  @li BlueNRG development platform (order code: STEVAL-IDB002V1) 
##  @li BlueNRG USB dongle (order code: STEVAL-IDB003V1)
## 
##  @section S103 Supported BLE devices and stack versions
##
##  @li BlueNRG stack v6.4
##

## \addtogroup BlueNRG_scripts
## @{
## \see OTA_Central_BlueNRG.py for documentation.
## 
## @} 

## @cond DOXYGEN_SHOULD_SKIP_THIS

## ------------------------------------------ OTA.py script utilities ---------------------------------------------------------------
_progress_val = -1

def progress(value, total):        
    global _progress_val
        
    new_progress_val = value*100/total
    if new_progress_val != _progress_val:
        _progress_val = new_progress_val
        PRINT(str(_progress_val) +'%')
    
## Check if LL contains the expected OTA_SCAN_RSP (OTA Service UUID)
def containsBTLTAG(LL):
    OTA_SCAN_RSP = [0x11,0x06,0x8a,0x97,0xf7,0xc0,0x85,0x06,0x11,0xe3,0xba,0xa7,0x08,0x00,0x20,0x0c,0x9a,0x66]
    idx=0

    while True:
        if ((idx + len(OTA_SCAN_RSP)) <= len(LL)):

            if (LL[idx] == (len(OTA_SCAN_RSP) - 1)):

                if (LL[idx:idx+len(OTA_SCAN_RSP)] == OTA_SCAN_RSP):

                    return True

            idx+=(LL[idx]+1)    
        else:
            return False

## Discovery the OTA characteristics handles
def getBtlCharHandle(otaBtlHandle, CharUUID): 

     ACI_GATT_DISC_CHAR_BY_UUID(Connection_Handle=otaBtlHandle,
                                  UUID_Type=0x02,
                                  UUID_128=CharUUID)

     evt = WAIT_EVENT(HCI_VENDOR_EVENT,Ecode=ACI_GATT_DISC_READ_CHAR_BY_UUID_RESP_EVENT)

     respData = evt.get_param('Attribute_Handle').val 
     evt = WAIT_EVENT(HCI_VENDOR_EVENT,Ecode=ACI_GATT_PROC_COMPLETE_EVENT)

     return respData ##  characteristic attribute handle

## ------------------------------------------ OTA.py script ------------------------------------------------------------------------------------
EVENT_TYPE_ADV_IND = 0x00
EVENT_TYPE_SCAN_RSP = 0x04
SERVICE_UUID_LENGHT = 0x12

DEVICE_LIST_DEFAULT_NAME = '(NO-NAME)'
DEBUG_ON = 0 ##0
master_slave_address= 0x123456789AAA

HW_RESET()
SET_MODE(2) 
SET_PUBLIC_ADDRESS(0x123456789CCC)
ACI_HAL_SET_TX_POWER_LEVEL(En_High_Power=1,PA_Level = 4)

if  ACI_GATT_INIT()!=0x00:
    ERROR('ACI_GATT_INIT FAILED')

Status,_,_,_=  ACI_GAP_INIT(Role=CENTRAL)
if (Status != 0x00):
    ERROR('ACI_GAP_INIT CALL FAILED')

## Get BLE HW, SW version (This script supports only BlueNRG: HW 3.0, FW 6.4)
(hw,fw) = GET_STACK_VERSION()

PRINT('HW version: ' + str(hw)+ '; SW version: '+str(fw))

if (fw[0] == 6) and (fw[1] != 4):
    ERROR('BLUENRG FW version ' + str(fw) + ' is not supported from this script. Only BlueNRG FW 6.4 is supported')
elif (fw[0] == 7):
    ERROR('BLUENRG-MS FW 7.x is not supported from this script (only BlueNRG)') 

deviceList = []
currentPeerAddress = 0

###  First GAP General Discovery Procedure to identify SCAN RESPONSE with OTA Service 128 bits UUID: it stored related device ADV addresses
Status = ACI_GAP_START_GENERAL_DISCOVERY_PROC(LE_Scan_Interval=0x4000, 
                                     LE_Scan_Window=0x4000,
                                     Own_Address_Type=0x00,
                                     Filter_Duplicates=0x00)

if (Status != 0x00):
    ERROR('ACI_GAP_START_GENERAL_DISCOVERY_PROC CALL FAILED') ## Print error message and stop script execution

#Search bootloader slave candidates
PRINT('Searching for bootloader targets..........')

while (True): 
    
    e = WAIT_EVENT(timeout=30) 

    if e.event_code==HCI_VENDOR_EVENT and e.get_param('Ecode').val==ACI_GAP_PROC_COMPLETE_EVENT:
        break ## exit condition from while True

    ## BlueNRG FW 6.4
    elif e.event_code==HCI_VENDOR_EVENT and e.get_param('Ecode').val==ACI_GAP_DEVICE_FOUND_EVENT:
         if (e.get_param('Event_Type').val == EVENT_TYPE_SCAN_RSP):

             if (DEBUG_ON == 1):
                print "SCAN_RSP"

             service_uuid_128bits_lenght =  e.get_param('Length_Data').val
             if (service_uuid_128bits_lenght == SERVICE_UUID_LENGHT):
                 service_uuid_128bits = e.get_param('Data').get_list()
                 if (DEBUG_ON == 1):
                     for i in range (service_uuid_128bits_lenght):
                         PRINT(str(hex(service_uuid_128bits[i])))

                 if containsBTLTAG(service_uuid_128bits):
                     scan_rsp_device_address_type= e.get_param('Address_Type').val
                   
                     device_address = e.get_param('Address').val
                     if (DEBUG_ON == 1):
                         PRINT('scan_rsp_device_address_type ' +  str(hex(scan_rsp_device_address_type)))

                         PRINT('[' + str(hex(device_address)) + ', ' + str(scan_rsp_device_address_type) + ', ' + str(DEVICE_LIST_DEFAULT_NAME) +']')

                     if not([device_address,scan_rsp_device_address_type,DEVICE_LIST_DEFAULT_NAME] in deviceList):
                         deviceList.append([device_address,scan_rsp_device_address_type,DEVICE_LIST_DEFAULT_NAME]) #Latest



if (DEBUG_ON == 1):
    PRINT('-------- SCAN_RSP results:')
    PRINT(str(deviceList))       

if (len(deviceList) > 0) : 
    #look at advertisements in order to get application names
    PRINT('Checking application names...')

    # go ahead to search for device names#
    # Set parameters to scan for advertisements only  

    ### Second GAP General Discovery Procedure for looking for device names of stored devices with OTA Service 128 bits UUID: it stores related ADV device name
    Status = ACI_GAP_START_GENERAL_DISCOVERY_PROC(LE_Scan_Interval=0x4000, 
                                         LE_Scan_Window=0x4000,
                                         Own_Address_Type=0x00,
                                         Filter_Duplicates=0x00)

    if (Status != 0x00):
        ERROR('ACI_GAP_START_GENERAL_DISCOVERY_PROC CALL FAILED') ## Print error message and stop script execution

    
    if (fw[0] == 6) and (fw[1] == 4): ## BlueNRG 6.4
        eCode = ACI_GAP_DEVICE_FOUND_EVENT

        while (eCode != ACI_GAP_PROC_COMPLETE_EVENT):
            e = WAIT_EVENT(HCI_VENDOR_EVENT, timeout=30)
            eCode = e.get_param('Ecode').val
            if (eCode == ACI_GAP_DEVICE_FOUND_EVENT):
                ## CHECK FOR ADV_IND
                if  (e.get_param('Event_Type').val ==  EVENT_TYPE_ADV_IND):
                    if (DEBUG_ON == 1):
                        PRINT('ADV_IND')
                    adv_ind_device_address_type  = e.get_param('Address_Type').val

                    device_address =  e.get_param('Address').val 
                    currentItemIndex = 0

                    if (device_address in [x[0] for x in deviceList]) :
                        deviceListIndex = ([x[0] for x in deviceList].index(device_address))
                        if (deviceList[deviceListIndex][2] == DEVICE_LIST_DEFAULT_NAME) :

                            adv_ind_device_name=GET_NAME(e.get_param('Data').val) 
                            deviceList[deviceListIndex][2] = adv_ind_device_name

    else: 
        ERROR('Wrong FW version (it is not supported from this script)')
                    
if (len(deviceList) == 0):
    currentPeerAddress = 0
    ERROR('No devices running OTA bootloader service were found') 

else: 
    PRINT('Available OTA peer device candidates:')
    count =0
    for elem in deviceList:
        PRINT(str(count) + ': ' + '[' + str(hex(elem[0])) + ', ' + str(elem[1]) + ', ' + elem[2] + ']')
        count +=1

index = GET_CHAR('Select one index of the available OTA peer devices. Press related key: [0 - ' + str(len(deviceList)-1) + ']')

if (index == None):
    ERROR("No index has been selected")
num = int(index) ## GET_CHAR returns the inserted character

INFO('Press OK for connecting to peer device!  Address: ' +str(hex(deviceList[num][0])) + ', Address type: ' + str(deviceList[num][1]) + ', Name: ' + str(deviceList[num][2]))

### Connect to selected the device with OTA service 
currentPeerAddress = deviceList[num][0]
currentPeerAddressType = deviceList[num][1]
PRINT('Selected device: ' + str(hex(deviceList[num][0])))

Status = ACI_GAP_CREATE_CONNECTION(LE_Scan_Interval=0x4000,
                                   LE_Scan_Window=0x4000, 
                                   Peer_Address_Type=currentPeerAddressType, 
                                   Peer_Address=currentPeerAddress,
                                   Own_Address_Type=0, 
                                   Conn_Interval_Min=0x0006,
                                   Conn_Interval_Max=0x0006,
                                   Conn_Latency=0,   
                                   Supervision_Timeout=0x0C80,
                                   Minimum_CE_Length=0x10, 
                                   Maximum_CE_Length=0xFFFF)
       
if Status !=0x00:
    ERROR('ACI_GAP_CREATE_CONNECTION CALL FAILED') 

e= WAIT_EVENT(HCI_LE_META_EVENT,timeout=30,Subevent_Code=HCI_LE_CONNECTION_COMPLETE_EVENT)
if e.get_param('Status').val==0x00:
    otaBtlHandle = (e.get_param('Connection_Handle').val)
    if (otaBtlHandle == None) or (otaBtlHandle > 0x0EFF ) :
          currentPeerAddress = 0
          ERROR('Connection handle out of range')
         
    else :
        #PRINT('Connection_Handle: ' + str(hex(otaBtlHandle)))
        currentDeviceIndex = num 


### Read OTA service characteristics handles
BTLCharHandle1=getBtlCharHandle(otaBtlHandle,0x122e8cc0850811e3baa70800200c9a66)
if (DEBUG_ON == 1):
    PRINT('BTL char handle1: ' + hex(BTLCharHandle1))
BTLCharHandle2=getBtlCharHandle(otaBtlHandle,0x210f99f0850811e3baa70800200c9a66)
if (DEBUG_ON == 1):
    PRINT('BTL char handle2: ' + hex(BTLCharHandle2))
BTLCharHandle3=getBtlCharHandle(otaBtlHandle,0x2691aa80850811e3baa70800200c9a66)
if (DEBUG_ON == 1):
    PRINT('BTL char handle3: ' + hex(BTLCharHandle3))
BTLCharHandle4=getBtlCharHandle(otaBtlHandle,0x2bdc5760850811e3baa70800200c9a66)
if (DEBUG_ON == 1):
    PRINT('BTL char handle4: ' + hex(BTLCharHandle4))

### Read free memory information from related OTA characteristic 
Status = ACI_GATT_READ_CHAR_VALUE(Connection_Handle=otaBtlHandle,
                                          Attr_Handle=BTLCharHandle1+1)
if (Status != 0x00):
    ERROR('ACI_GATT_READ_CHAR_VALUE(Attr_Handle=' + hex(BTLCharHandle1+1) + ' CALL FAILED')
evt = WAIT_EVENT(0xFF,2,Ecode=ACI_ATT_READ_RESP_EVENT) 

notifData = (evt.get_param('Attribute_Value').get_list()) 

# Store peer free flash range: [LB, UB]
flash_LB=(notifData[0]<<24)+(notifData[1]<<16)+(notifData[2]<<8)+notifData[3]  
flash_UB=(notifData[4]<<24)+(notifData[5]<<16)+(notifData[6]<<8)+notifData[7] 

## Set base address 
base_address = flash_LB

### Peer Device Flash Update with new selected FW image 

filename = GET_FILE(msg = 'Add new image file path with required base address: ' + str(hex(base_address)))
PRINT('Selected filename: ' + filename)
 
INFO('Press OK for updating Peer Device FW from address ' +hex(flash_LB))

try: 
    f = open(filename,'rb')
    try: 

        fw_image16 = []
        #we loop to upload image in advance and count for size rather than working with openfile and relying on os.path.getsize(filename)
        cnt=0
        imageWord16b = 0
        while True:
           byte = f.read(1) 
           if byte != "":
               imageWord16b += (ord(byte) << ((cnt%16)*8))
               if ((cnt+1)%16 == 0) :
                   fw_image16.append(imageWord16b)
                   imageWord16b = 0
               cnt+=1

           else:
               if ((cnt%16)!=0) :
                   fw_image16.append(imageWord16b)

               if (DEBUG_ON == 1):
                   PRINT('image size is: ' + str(cnt))
                   PRINT('16 byte wise size: ' + str(len(fw_image16)))

               if ((base_address < flash_LB) or ((base_address+cnt) > flash_UB) or ((base_address%512)!=0)):
                  
                   ERROR('Image above free memory range or base address is not 512 byte aligned')
    
               break
    finally:      
        f.close()       

    btlBoundaries = (base_address<<40) + (cnt << 8) + 3

    retries = 0
   
    ACI_GATT_WRITE_CHAR_VALUE(Connection_Handle=otaBtlHandle,
                              Attr_Handle=BTLCharHandle2+1,
                              Attribute_Val_Length=0x09,
                              Attribute_Val=btlBoundaries)
    evt = WAIT_EVENT(0xFF,Ecode=ACI_GATT_PROC_COMPLETE_EVENT)

    ACI_GATT_READ_CHAR_VALUE(Connection_Handle=otaBtlHandle,
                                     Attr_Handle=BTLCharHandle2+1)

    evt = WAIT_EVENT(0xFF,Ecode=ACI_ATT_READ_RESP_EVENT)
    notifData = evt.get_param('Attribute_Value').get_list()  
    verifData = 0 
    notifData.reverse()

    for elem in (notifData[:len(notifData)]) : 

        verifData<<=8
        verifData += elem

    if (btlBoundaries != verifData) :
      retries += 1
      if (retries > 10) :
          ERROR('Cannot write parameters for OTA image transfer :')
    elif (DEBUG_ON == 1):
      PRINT('OTA image transfer parameters written')

    ACI_GATT_WRITE_CHAR_DESC(Connection_Handle=otaBtlHandle,
                                     Attr_Handle= BTLCharHandle4+2,
                                     Attribute_Val_Length=0x01,
                                     Attribute_Val=0x01)

    evt = WAIT_EVENT(0xFF,Ecode=ACI_GATT_NOTIFICATION_EVENT)
    notifData = evt.get_param('Attribute_Value').get_list() 

    OTA_ACK_EVERY = 1
    characSize = 16
    roundedImgSize = (len(fw_image16) * characSize) 
    SeqNum = 0
    retriesForChesumError = 0
    retriesForSequenceError = 0
    retriesForMissedNotification = 0
    retriesOnSPI = 0
    consecutiveRetries = 0

    while ((SeqNum * characSize) < roundedImgSize):
        checksum = 0

        prevSeqNum = SeqNum
        needsACK =  (((SeqNum+1)%(OTA_ACK_EVERY)) == 0) or (((SeqNum+1) * characSize) == roundedImgSize) 
        ImgPayload = (SeqNum<<144) + ((int(needsACK))<<136) + (fw_image16[SeqNum] << 8)
        for k in range(19):
            checksum ^= ((ImgPayload >> (8*(k+1))) & 0xFF)

        CheckedImgPayload = ImgPayload + checksum
        if (DEBUG_ON == 1): 
            PRINT('***********************************************')
            PRINT('SeqNum: ' + str(hex(SeqNum)))
            PRINT('needs Ack: ' + str(hex(needsACK)))
            PRINT('payload: ' + str(hex(CheckedImgPayload)))
            PRINT('checksum: ' + str(hex(checksum)))
        while True:
            if (prevSeqNum == SeqNum):
               consecutiveRetries+=1
            else :
               consecutiveRetries=0

            evt = ACI_GATT_WRITE_WITHOUT_RESP(Connection_Handle=otaBtlHandle,
                                                  Attr_Handle=BTLCharHandle3+1,
                                                  Attribute_Val_Length=0x0014,
                                                  Attribute_Val= CheckedImgPayload)
            if evt != 0:
                retriesOnSPI+=1
                if (DEBUG_ON == 1):
                    PRINT('Retry on SPI') #Happens when HCI_COMMAND_COMPLETE_EVENT command returns back with Status not 0 (Success)
                if (OTA_ACK_EVERY > 1) :
                  #Prevent SPI retries occurring too often (?)  
                  SLEEP(0.1) 
            else:
                if (retries >= (len(fw_image16)/3)):
                    ERROR('Too many errors, aborting OTA bootloading..')
                else:
                    break

        prevSeqNum=SeqNum
        if needsACK:
            evt = WAIT_EVENT(0xFF,1,continueOnEvtMiss=True,Ecode=ACI_GATT_NOTIFICATION_EVENT)
            if (evt!=None):  
                notifData = evt.get_param('Attribute_Value').get_list()  
                index = 0

                nextExpectedCharBlock = (notifData[index] + (notifData[index+1]<<8))  
                if (notifData[index+2] == 0xFF):
                    ERROR('FLASH WRITE FAILED ON TARGET DEVICE!')
                elif (notifData[index+2] == 0x3C) :
                    ERROR('FLASH VERIFY FAILED ON TARGET DEVICE!')
                elif (notifData[index+2] == 0x0F):
                    if (DEBUG_ON == 1):
                        PRINT('checksum error retrying last packet')
                    SeqNum = nextExpectedCharBlock
                    retriesForChesumError+=1
                elif (notifData[index+2] == 0xF0):
                    if (DEBUG_ON == 1):
                        PRINT('sequencing error retrying last packet')
                    SeqNum = nextExpectedCharBlock
                    retriesForSequenceError+=1
                else:
                    #by protocol (SeqNum + 1) expected as notification number 
                    SeqNum = nextExpectedCharBlock
                    progress(min((SeqNum * 16),cnt),cnt)
            else:
                if (DEBUG_ON == 1):
                    PRINT('Missed notification, retrying last sequence')
                SeqNum-=(OTA_ACK_EVERY - 1)
                retriesForMissedNotification+=1
        else:
            SeqNum+=1 

    if (DEBUG_ON == 1):
        PRINT('******************* STATS: **************************')
        PRINT('retriesForChesumError: ' + retriesForChesumError)
        PRINT('retriesForSequenceError: ' + retriesForSequenceError)
        PRINT('retriesForMissedNotification: ' + retriesForMissedNotification)
        PRINT('retriesOnSPI: ' + retriesOnSPI)

    if len(deviceList) > 0 :
        currentPeerAddress = 0
        deviceList.pop(currentDeviceIndex)
      
    ret = ACI_GAP_TERMINATE(Connection_Handle=otaBtlHandle, Reason=0x13)
    if (ret != 0):
        PRINT('GAP TERMINATE: FAIL')

    e = WAIT_EVENT(HCI_DISCONNECTION_COMPLETE_EVENT)
    if e.get_param('Connection_Handle').val != otaBtlHandle:
         ERROR('Connection handle mismatch on connection closure')

    PRINT('FW UPGRADE COMPLETED')

except Exception as e:
    PRINT('BTL procedure warning !!!' + str(e))
    
     

## @endcond
