## @file OTA_Central_BlueNRG-MS.py
## @brief This script is used as component for implementing the Over-the-Air (OTA) FW upgrade of a BlueNRG-MS device providing the OTA service capability.
## @li The script is implemented using the new BlueNRG GUI language scripts based on the BlueNRG-MS 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 S110 OTA_Central_BlueNRG-MS.py, preliminary steps: configure a BlueNRG-MS Peripheral device with a application providing the OTA service and related characteristics 
##
##  @li On a BlueNRG-MS 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 S111  OTA_Central_BlueNRG-MS.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-MS.py script location and select it
##  @li On BlueNRG GUI, click on  Run Script tab for running the selected script
##  @li BlueNRG-MS 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-MS.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 S112 Supported BlueNRG-MS platforms
##
##  @li BlueNRG-MS development platform (order code: STEVAL-IDB005V1) 
##  @li BlueNRG-MS Daughter Board (order code: STEVAL-IDB005V1D)
##  @li BlueNRG-MS USB dongle (order code: STEVAL-IDB006V1)
## 
##  @section S113 Supported BLE devices and stack versions
##
##  @li BlueNRG-MS stack v7.x
##

## \addtogroup BlueNRG_MS_scripts
## @{
## \see OTA_Central_BlueNRG-MS.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 ------------------------------------------------------------------------------------

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-MS: HW 3.1, FW >= 7.1) 
(hw,fw) = GET_STACK_VERSION()

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

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

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-MS FW 7.x
    elif e.event_code==HCI_LE_META_EVENT and e.get_param('Subevent_Code').val==HCI_LE_ADVERTISING_REPORT_EVENT:

        evtData = e.get_param('Data').get_list()[0] 
        event_type = e.get_param('Event_Type').val[0]

        if (DEBUG_ON == 1):
            PRINT('---------------evtData, event_type')
            PRINT(str(evtData) + str(event_type))

        if ((event_type == 0x04) and (type(evtData) != int) and (containsBTLTAG(evtData))): ## SCAN_RSP: 0x04 
            newAddress = e.get_param('Address').val[0]
            newAddressType = e.get_param('Address_Type').val[0]  

            if (DEBUG_ON == 1):
                PRINT('---------------newAddress, newAddressType')
                PRINT(str(newAddress) + str(newAddressType))
            if not([newAddress,newAddressType,DEVICE_LIST_DEFAULT_NAME] in deviceList):         
                if (DEBUG_ON == 1):
                    PRINT(str(newAddress) + str(newAddressType))
                deviceList.append([newAddress,newAddressType,DEVICE_LIST_DEFAULT_NAME])


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] == 7) and (fw[1] >=1): ## FW 7.x (x>1):  BlueNRG-MS valid stack version 
         eCode = HCI_LE_ADVERTISING_REPORT_EVENT
         while (eCode != ACI_GAP_PROC_COMPLETE_EVENT):
             e = WAIT_EVENT(timeout=30) ##30
             if (e.event_code==HCI_LE_META_EVENT and e.get_param('Subevent_Code').val==HCI_LE_ADVERTISING_REPORT_EVENT):

                 if (e.get_param('Event_Type').val[0] == 0x00):
                    advAddress=e.get_param('Address').val[0]
                    currentItemIndex = 0

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

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

             elif (e.event_code == HCI_VENDOR_EVENT):
                 ##PRINT('---ACI_GAP_PROC_COMPLETE_EVENT')
                 eCode = e.get_param('Ecode').val           

    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
