├── compat.py ├── unpacker.py ├── scan.py ├── gen_dat.c ├── wizard.py ├── README.md ├── LICENSE ├── application.mk ├── dfu.py └── intelhex.py /compat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | if sys.version_info[0] >= 3: 6 | def asbytes(s): 7 | if isinstance(s, bytes): 8 | return s 9 | return s.encode('latin1') 10 | def asstr(s): 11 | if isinstance(s, str): 12 | return s 13 | return s.decode('latin1') 14 | else: 15 | asbytes = str 16 | asstr = str 17 | 18 | -------------------------------------------------------------------------------- /unpacker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os.path 4 | import zipfile 5 | import tempfile 6 | import random 7 | import string 8 | import shutil 9 | 10 | from os.path import basename 11 | 12 | class Unpacker(object): 13 | 14 | #-------------------------------------------------------------------------- 15 | # 16 | #-------------------------------------------------------------------------- 17 | def unzip(self, zip_src, unzip_dir): 18 | try: 19 | zip = zipfile.ZipFile(r'{0}'.format(zip_src)) 20 | zip.extractall(r'{0}'.format(unzip_dir)) 21 | except: 22 | return False 23 | 24 | return True 25 | 26 | #-------------------------------------------------------------------------- 27 | # 28 | #-------------------------------------------------------------------------- 29 | def entropy(self, length): 30 | return ''.join(random.choice(string.lowercase) for i in range (length)) 31 | 32 | 33 | #-------------------------------------------------------------------------- 34 | # 35 | #-------------------------------------------------------------------------- 36 | def unpack_zipfile(self, zipfile): 37 | 38 | if not os.path.isfile(zipfile): 39 | raise Exception("Error: zipfile, not found!") 40 | 41 | # Create unique working direction into which the zip file is expanded 42 | 43 | self.unzip_dir = "{0}/{1}_{2}".format(tempfile.gettempdir(), 44 | os.path.splitext(basename(zipfile))[0], 45 | self.entropy(6)) 46 | 47 | print("unzip_dir: {0}".format(self.unzip_dir)) 48 | 49 | if self.unzip(zipfile, self.unzip_dir) == False: 50 | raise Exception("unzip failed") 51 | 52 | # Check that "application.dat" exist in directory. 53 | 54 | datfile = "{0}/{1}".format(self.unzip_dir, "application.dat") 55 | if not os.path.isfile(datfile): 56 | raise Exception("No DAT file found") 57 | 58 | # Check that "application.[hex|bin]" exists in directory. 59 | 60 | hexfile = "{0}/{1}".format(self.unzip_dir, "application.hex") 61 | if not os.path.isfile(hexfile): 62 | hexfile = "{0}/{1}".format(self.unzip_dir, "application.bin") 63 | if not os.path.isfile(hexfile): 64 | raise Exception("No HEX or BIN file found") 65 | 66 | #print("hex: {0}".format(hexfile)) 67 | #print("dat: {0}".format(datfile)) 68 | 69 | return hexfile, datfile 70 | 71 | #-------------------------------------------------------------------------- 72 | # 73 | #-------------------------------------------------------------------------- 74 | def delete(self): 75 | # delete self.unzip_dir and its contents 76 | shutil.rmtree(self.unzip_dir) 77 | 78 | 79 | -------------------------------------------------------------------------------- /scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #------------------------------------------------------------------------------ 4 | # Device scan 5 | #------------------------------------------------------------------------------ 6 | 7 | from subprocess import call 8 | 9 | import pexpect 10 | import signal 11 | 12 | #------------------------------------------------------------------------------ 13 | # Bluetooth LE scan for advertising peripheral devices 14 | #------------------------------------------------------------------------------ 15 | class HciTool: 16 | 17 | def __init__( self, advert_name ): 18 | self.advert_name = advert_name 19 | return 20 | 21 | def scan( self ): 22 | 23 | try: 24 | self.hcitool = pexpect.spawn('hcitool lescan') 25 | #self.hcitool.logfile = sys.stdout 26 | index = self.hcitool.expect(['LE Scan ...'], 1) 27 | except pexpect.EOF: 28 | self.hcitool.terminate(force=True) 29 | return [] 30 | except Exception as err: 31 | print "scan: exception: {0}".format(sys.exc_info()[0]) 32 | self.hcitool.terminate(force=True) 33 | return [] 34 | 35 | if index != 0: 36 | print "scan: failed" 37 | self.hcitool.terminate(force=True) 38 | return [] 39 | 40 | list = [] 41 | for dummy in range(0, 2): 42 | try: 43 | list.append(self.hcitool.readline()) 44 | except pexpect.TIMEOUT: 45 | break 46 | 47 | if list == []: 48 | return [] 49 | 50 | # Eliminate duplicate items in list 51 | list = set(list) 52 | 53 | # Remove non self.advert_name units 54 | if self.advert_name != None: 55 | list = [item for item in list if self.advert_name in item] 56 | 57 | # remove empty items from list 58 | while '\r\n' in list: 59 | list.remove('\r\n') 60 | 61 | # Strip newline from items in list 62 | list = [item.strip() for item in list] 63 | list = list[0:2] 64 | 65 | # Close pexpect (release device for subsequent use) 66 | self.hcitool.terminate(force=True) 67 | 68 | return list 69 | 70 | #------------------------------------------------------------------------------ 71 | # 72 | #------------------------------------------------------------------------------ 73 | class Scan: 74 | 75 | def __init__( self, advert_name ): 76 | self.advert_name = advert_name 77 | return 78 | 79 | def scan(self): 80 | 81 | scan_list = [] 82 | 83 | try: 84 | hcitool = HciTool(self.advert_name) 85 | scan_list = hcitool.scan() 86 | 87 | except KeyboardInterrupt: 88 | # On Cntl-C 89 | pass; 90 | except pexpect.TIMEOUT: 91 | print "scan: pexpect.TIMEOUT" 92 | pass 93 | except Exception as e: 94 | print "scan: exception: {0} ".format(sys.exc_info()[0]) 95 | pass 96 | 97 | 98 | return scan_list 99 | 100 | #------------------------------------------------------------------------------ 101 | # 102 | #------------------------------------------------------------------------------ 103 | if __name__ == '__main__': 104 | 105 | # Do not litter the world with broken .pyc files. 106 | sys.dont_write_bytecode = True 107 | 108 | #scanner = Scan("DfuTarg") # specific advertisement name 109 | scanner = Scan(None) # any advertising name 110 | 111 | scanner.scan() 112 | 113 | print "scan complete" 114 | -------------------------------------------------------------------------------- /gen_dat.c: -------------------------------------------------------------------------------- 1 | /* 2 | * gen_dat.c -- generate a *.dat file from a *.bin file 3 | * 4 | * Compile: gcc gen_dat.c -o gen_dat 5 | * NOTE: move executable to app's gcc directory. 6 | */ 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | /* 14 | * Minimalistic INIT file structure 15 | * See Nordic docs for INIT (*.dat) file format details. 16 | */ 17 | typedef struct { 18 | uint16_t device_type; 19 | uint16_t device_rev; 20 | uint32_t app_version; 21 | uint16_t softdevice_len; 22 | uint16_t softdevice_1; 23 | uint16_t softdevice_2; 24 | uint16_t crc; 25 | } dfu_init_t; 26 | 27 | static dfu_init_t dfu_init = { 28 | .device_type = 0xffff, 29 | .device_rev = 0xffff, 30 | .app_version = 0xffffffff, 31 | .softdevice_len = 0x0002, 32 | .softdevice_1 = 0x005a, // SoftDevice 7.1 33 | .softdevice_2 = 0x0064, // SoftDevice 8.0 34 | .crc = 0x0000, 35 | }; 36 | 37 | /* 38 | * This CRC code was liberated from the Nordic SDK. 39 | * That way it exactly matched the algrithm used by the bootloader. 40 | */ 41 | uint16_t crc16_compute(const uint8_t * p_data, 42 | uint32_t size, 43 | const uint16_t * p_crc) 44 | { 45 | uint32_t i; 46 | uint16_t crc = (p_crc == NULL) ? 0xffff : *p_crc; 47 | 48 | for (i = 0; i < size; i++) { 49 | crc = (unsigned char)(crc >> 8) | (crc << 8); 50 | crc ^= p_data[i]; 51 | crc ^= (unsigned char)(crc & 0xff) >> 4; 52 | crc ^= (crc << 8) << 4; 53 | crc ^= ((crc & 0xff) << 4) << 1; 54 | } 55 | return crc; 56 | } 57 | 58 | /* 59 | * Given an input bin file, generate a matching dat file. 60 | */ 61 | int main(int argc, char* argv[]) 62 | { 63 | char * binfilename = NULL; 64 | char * datfilename = NULL; 65 | 66 | FILE * binfile = NULL; 67 | FILE * datfile = NULL; 68 | 69 | size_t binsize = 0; 70 | uint8_t * bindata = NULL; 71 | uint16_t crc = 0; 72 | 73 | /* 74 | * USAGE: gen_dat bin-filename dat-filename 75 | */ 76 | if (argc < 3) 77 | return -1; 78 | 79 | binfilename = argv[1]; 80 | datfilename = argv[2]; 81 | 82 | /* Open bin file for reads. */ 83 | binfile = fopen(binfilename, "r"); 84 | if (binfile == NULL) { 85 | fprintf(stderr, "bin file open failed: %s\n", strerror(errno)); 86 | return -1; 87 | } 88 | 89 | /* Open dat file for writes. */ 90 | datfile = fopen(datfilename, "wb"); 91 | if (datfile == NULL) { 92 | fprintf(stderr, "dat file open failed: %s\n", strerror(errno)); 93 | fclose(binfile); 94 | return -1; 95 | } 96 | 97 | /* Detrmine size of bin file in bytes. */ 98 | fseek(binfile, 0L, SEEK_END); 99 | binsize = ftell(binfile); 100 | fseek(binfile, 0L, SEEK_SET); 101 | 102 | if (binsize == 0) { 103 | fprintf(stderr, "bin file size determination failed"); 104 | fclose(datfile); 105 | fclose(binfile); 106 | return -1; 107 | } 108 | 109 | /* Allocate buffer to hold all of bin file. */ 110 | bindata = (uint8_t*) malloc(binsize); 111 | if (bindata == NULL) { 112 | fprintf(stderr, "malloc failed"); 113 | fclose(datfile); 114 | fclose(binfile); 115 | return -1; 116 | } 117 | 118 | /* Fill buffer with bin file data. */ 119 | fread(bindata, binsize, 1, binfile); 120 | 121 | /* Compute CRC-16. */ 122 | dfu_init.crc = crc16_compute(bindata, binsize, 0); 123 | 124 | /* 125 | * Write INIT file 126 | */ 127 | fwrite(&dfu_init, sizeof(dfu_init_t), 1, datfile); 128 | 129 | /* 130 | * Close all files 131 | */ 132 | fclose(datfile); 133 | fclose(binfile); 134 | 135 | return 0; 136 | } -------------------------------------------------------------------------------- /wizard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #------------------------------------------------------------------------------ 4 | # Graphical User Interface for DFU Server 5 | #------------------------------------------------------------------------------ 6 | 7 | import os 8 | 9 | from Tkinter import * 10 | 11 | import ttk 12 | import tkMessageBox 13 | 14 | import tkFileDialog 15 | from tkFileDialog import askopenfilename 16 | 17 | from scan import Scan 18 | from dfu2 import * 19 | 20 | #------------------------------------------------------------------------------ 21 | # 22 | #------------------------------------------------------------------------------ 23 | class Application(Frame): 24 | 25 | def __init__(self, master): 26 | Frame.__init__(self, master) 27 | self.file = None 28 | self.addr = None 29 | self.device = None 30 | self.grid() 31 | self.create_widgets() 32 | 33 | def create_widgets(self): 34 | 35 | self.frame1 = Frame(self) 36 | self.frame1['relief'] = RIDGE 37 | self.frame1.grid(row=0, column=0, pady=3, sticky=N) 38 | 39 | self.button1 = Button(self.frame1) 40 | self.button1["width"] = 20 41 | self.button1["text"] = "Select application.zip file" 42 | self.button1["command"] = self.selectFile 43 | self.button1.grid(row=0, column=0, sticky=N) 44 | 45 | self.text1 = Label(self.frame1) 46 | self.text1["background"] = "white" 47 | self.text1["width"] = 30 48 | self.text1["text"] = "none" 49 | self.text1.grid(row=1, column=0, pady=5, sticky=N) 50 | 51 | self.frame2 = Frame(self) 52 | self.frame2.grid(row=1, rowspan=2, sticky=N) 53 | 54 | self.button2 = Button(self.frame2) 55 | self.button2["width"] = 20 56 | self.button2["text"] = "Scan for target devices" 57 | self.button2["command"] = self.get_device_name 58 | self.button2.grid(row=0, column=0, sticky=N) 59 | 60 | self.scrollbar2 = Scrollbar(self.frame2) 61 | self.scrollbar2.grid(row=2, column=0, sticky=N) 62 | 63 | self.listbox2 = Listbox(self.frame2) 64 | self.listbox2['height'] = 5 65 | self.listbox2.bind("", self.device_selected) 66 | self.listbox2.grid(row=2, column=0, sticky=N) 67 | 68 | self.scrollbar2.config(command=self.listbox2.yview) 69 | self.listbox2.config(yscrollcommand=self.scrollbar2.set) 70 | 71 | self.text2 = Label(self.frame2) 72 | self.text2["text"] = "Double-click to select" 73 | self.text2.grid(row=3, column=0, sticky=N) 74 | 75 | self.frame3 = Frame(self) 76 | self.frame3.grid(row=7, column=0, rowspan=2, pady=5, sticky=N) 77 | 78 | self.progress3 = ttk.Progressbar(self.frame3) 79 | self.progress3['orient'] = 'horizontal' 80 | self.progress3['length'] = 250 81 | self.progress3['mode'] = 'determinate' 82 | self.progress3.grid(row=0, column=0, sticky=N) 83 | 84 | self.text3 = Label(self.frame3) 85 | self.text3["text"] = "Download progress" 86 | self.text3.grid(row=3, column=0, sticky=N+W+E) 87 | #self.text3.pack(side=BOTTOM, fill="x") 88 | 89 | def selectFile(self): 90 | self.file = askopenfilename(filetypes=[('Zip files', '*.zip')]) 91 | filename = os.path.basename(self.file) 92 | self.text1["text"] = filename 93 | 94 | def get_device_name(self): 95 | scanner = Scan(None) 96 | targets = scanner.scan() 97 | self.listbox2.delete(0, END) 98 | 99 | if targets: 100 | for target in targets: 101 | index = targets.index(target) 102 | addr = targets[index][:17] 103 | self.listbox2.insert("end", addr) 104 | 105 | def device_selected(self, event): 106 | widget = event.widget 107 | selected = widget.curselection() 108 | self.addr = widget.get(selected[0]) 109 | 110 | if self.addr and self.file: 111 | print "addr: {0}".format(self.addr) 112 | print "file: {0}".format(self.file) 113 | 114 | # dfu_server("-z {0} -a {1}".format(self.file, self.addr)) 115 | 116 | else: 117 | tkMessageBox.showwarning("Error", "Missing application file") 118 | 119 | return 120 | 121 | 122 | #------------------------------------------------------------------------------ 123 | # 124 | #------------------------------------------------------------------------------ 125 | def main(): 126 | 127 | root = Tk() 128 | root.title("DFU Server") 129 | root.configure(bg='lightgrey') 130 | root.geometry("250x235") 131 | 132 | app = Application(root) 133 | 134 | root.mainloop() 135 | 136 | #------------------------------------------------------------------------------ 137 | # 138 | #------------------------------------------------------------------------------ 139 | if __name__ == '__main__': 140 | 141 | # Do not litter the world with broken .pyc files. 142 | sys.dont_write_bytecode = True 143 | 144 | main() 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python nRF51 DFU Server 2 | ============================ 3 | 4 | A python script for bluez gatttool using pexpect to achive Device Firmware Updates (DFU) to the nRF51. 5 | The host system is assumed to be some flavor of Linux, and was developed with the Raspberry-Pi as the primary hosting system. 6 | 7 | **NOTE:** 8 | This is probably not a beginner's project. 9 | Peripheral firmware updating is a complex process, requiring several critical development support steps, not covered here, before the *dfu.py* utility can be used. 10 | 11 | It is assumed that your peripheral firmware has been build to Nordic's SDK8.x + SoftDevice 8.x 12 | The target peripheral firmware should also include some variation of Nordic's DFU support. 13 | 14 | How you get the target periheral to enter DFU-mode (e.g. advertizing *DfuTarg*) is not handled here. 15 | It is assumed you can trigger your peripheral to enter the bootloader; either by a hardware switch or application-trigger. 16 | 17 | The *dfu.py* utility comes into play only after the peripheral is executing the bootloader. 18 | 19 | System: 20 | * Raspberry Pi - RaspberryPi B+ or later (tested with RaspPi B+) 21 | * Bluetooth 4.0 USB dongle - (tested with "UTechSmart" and "Plugable" BLE dongles) 22 | * Linux raspian - kernel 3.12.22 or later 23 | * bluez - 5.4 or later 24 | 25 | See [here](https://learn.adafruit.com/pibeacon-ibeacon-with-a-raspberry-pi/setting-up-the-pi "BlueZ build") for details on building bluez. 26 | 27 | This project assumes you are developing on a Linux/Unix or OSX system and deploying to a Raspberry Pi (Raspian) system. Development on Windows systems should be OK, but it hasn't been (nor will be) tested. 28 | 29 | Prerequisite 30 | ------------ 31 | 32 | sudo pip install pexpect 33 | sudo pip install intelhex 34 | 35 | Firmware Build Requirement 36 | -------------------------- 37 | * Your nRF51 firmware build method will produce either a firmware hex or bin file named *application.hex* or *application.bin*. This naming convention is per Nordics DFU specification, which is use by this DFU server as well as the Android Master Control Panel DFU, and iOS DFU app. 38 | * Your nRF51 firmware build method will produce an Init file (aka *application.dat*). Again, this is per Nordic's naming conventions. 39 | 40 | The *gen_dat* Utility 41 | --------------------- 42 | The gen_dat utility will read your build method's hex file and produce a dat file. The utility is written the C-language, but should be easy to rebuild: just follow the directions at the top of the source file. Ideally, you would incorporate the gen_dat utility into your build system so that your build method will generate the dat file for each build. 43 | 44 | Below is a snippet showing how you might use the gen_dat utility in a makefile. The *application.mk* file shows a more complete example. This makefile example shows how the gen_dat and zip files are integrated into the build process. It is an example, and you must customize it to your requirements. 45 | 46 | GENZIP := zip 47 | GENDAT := ./gen_dat 48 | 49 | # Create .dat file from the .bin file 50 | gendat: 51 | @echo Preparing: application.dat 52 | $(NO_ECHO)$(GENDAT) $(OUTPUT_BINARY_DIRECTORY)/application.bin $(OUTPUT_BINARY_DIRECTORY)/application.dat 53 | 54 | # Create .zip file from .bin and .dat files 55 | genzip: 56 | @echo Preparing: $(OUTPUT_NAME).zip 57 | -@$(GENZIP) -j $(OUTPUT_BINARY_DIRECTORY)/application.zip $(OUTPUT_BINARY_DIRECTORY)/application.bin $(OUTPUT_BINARY_DIRECTORY)/application.dat 58 | 59 | 60 | Usage 61 | ----- 62 | There are two ways to speicify firmware files for this OTA-DFU server. Either by specifying both the file with the dat file, or more easily by the zip file, which contains both the hex and dat files. 63 | The new "zip file" form is encouraged by Nordic, but the older hex+dat file methods should still work. 64 | 65 | 66 | Usage Examples 67 | -------------- 68 | 69 | > sudo ./dfu.py -f ~/application.hex -d ~/application.dat -a EF:FF:D2:92:9C:2A 70 | 71 | or 72 | 73 | > sudo ./dfu.py -z ~/application.zip -a EF:FF:D2:92:9C:2A 74 | 75 | To figure out the address of DfuTarg do a 'hcitool lescan' - 76 | 77 | $ sudo hcitool -i hci0 lescan 78 | LE Scan ... 79 | CD:E3:4A:47:1C:E4 DfuTarg 80 | CD:E3:4A:47:1C:E4 (unknown) 81 | 82 | 83 | Example of *dfu.py* Output 84 | ------------------------ 85 | 86 | pi@raspberrypi ~/src/ota-dfu/ $ sudo ./dfu.py -z application_debug_1435008894.zip -a EF:FF:D2:92:9C:2A 87 | DFU Server start 88 | unzip_dir: /tmp/application_debug_1435008894_nzjesh 89 | input_setup 90 | bin array size: 72352 91 | scan_and_connect 92 | dfu_send_image 93 | [0, 0, 0, 0, 0, 0, 0, 0, 160, 26, 1, 0] 94 | Sending hex file size 95 | oper: RESPONSE, proc: START, status: SUCCESS 96 | dfu_send_info 97 | PKT_RCPT: 200 98 | PKT_RCPT: 400 99 | PKT_RCPT: 600 100 | PKT_RCPT: 800 101 | PKT_RCPT: 1000 102 | PKT_RCPT: 1200 103 | PKT_RCPT: 1400 104 | PKT_RCPT: 1600 105 | PKT_RCPT: 1800 106 | PKT_RCPT: 2000 107 | PKT_RCPT: 2200 108 | PKT_RCPT: 2400 109 | PKT_RCPT: 2600 110 | PKT_RCPT: 2800 111 | PKT_RCPT: 3000 112 | ... 113 | ... 114 | ... 115 | PKT_RCPT: 69800 116 | PKT_RCPT: 70000 117 | PKT_RCPT: 70200 118 | PKT_RCPT: 70400 119 | PKT_RCPT: 70600 120 | PKT_RCPT: 70800 121 | PKT_RCPT: 71000 122 | PKT_RCPT: 71200 123 | PKT_RCPT: 71400 124 | PKT_RCPT: 71600 125 | PKT_RCPT: 71800 126 | PKT_RCPT: 72000 127 | PKT_RCPT: 72200 128 | State timeout 129 | DFU Server done 130 | 131 | **NOTE:** 132 | The final "State timeout" is due to the target peripheral rebooting, as expected, and the disconnect not getting back soon enough. 133 | This is benign: the update should have been successful and the peripheral should have restarted and run the new firmware. 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Callender-Consulting.com 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /application.mk: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # Application firmware build 3 | # This is a template makefile -- modify to your application requirements 4 | #------------------------------------------------------------------------------ 5 | 6 | #------------------------------------------------------------------------------ 7 | # Selectable build options 8 | #------------------------------------------------------------------------------ 9 | 10 | TARGET_BOARD ?= BOARD_PCA10028 11 | BLE_DFU_APP_SUPPORT ?= "yes" 12 | DBGLOG_SUPPORT ?= "yes" 13 | 14 | #------------------------------------------------------------------------------ 15 | # Define relative paths to SDK components 16 | #------------------------------------------------------------------------------ 17 | 18 | SDK_BASE = ../../../.. 19 | COMPONENTS = $(SDK_BASE)/components 20 | TEMPLATE_PATH = $(COMPONENTS)/toolchain/gcc 21 | 22 | # 23 | # Output products must be named application.* [*.hex | *.dat] in order 24 | # for the *.zip file to be constructed to be acceptable to the Nordic 25 | # DFU apps (nRF_Toolbox and MCP). 26 | # When we incorporate DFU support in the Pillsy app, we can relax this 27 | # requirement, as we can set the filespec format and standards. 28 | # 29 | OUTPUT_NAME = application 30 | VERSION_NAME = $(OUTPUT_NAME)_$(BUILD_TYPE)_$(TIMESTAMP) 31 | 32 | #------------------------------------------------------------------------------ 33 | # Proceed cautiously beyond this point. Little should change. 34 | #------------------------------------------------------------------------------ 35 | 36 | export OUTPUT_NAME 37 | export GNU_INSTALL_ROOT 38 | 39 | MAKEFILE_NAME := $(MAKEFILE_LIST) 40 | MAKEFILE_DIR := $(dir $(MAKEFILE_NAME) ) 41 | 42 | ifeq ($(OS),Windows_NT) 43 | include $(TEMPLATE_PATH)/Makefile.windows 44 | else 45 | include $(TEMPLATE_PATH)/Makefile.posix 46 | endif 47 | 48 | # echo suspend 49 | ifeq ("$(VERBOSE)","1") 50 | NO_ECHO := 51 | else 52 | NO_ECHO := @ 53 | endif 54 | 55 | ifeq ($(MAKECMDGOALS),debug) 56 | BUILD_TYPE := debug 57 | else 58 | BUILD_TYPE := release 59 | DBGLOG_SUPPORT := "no" 60 | endif 61 | 62 | # Toolchain commands 63 | CC := "$(GNU_INSTALL_ROOT)/bin/$(GNU_PREFIX)-gcc" 64 | AS := "$(GNU_INSTALL_ROOT)/bin/$(GNU_PREFIX)-as" 65 | AR := "$(GNU_INSTALL_ROOT)/bin/$(GNU_PREFIX)-ar" -r 66 | LD := "$(GNU_INSTALL_ROOT)/bin/$(GNU_PREFIX)-ld" 67 | NM := "$(GNU_INSTALL_ROOT)/bin/$(GNU_PREFIX)-nm" 68 | OBJDUMP := "$(GNU_INSTALL_ROOT)/bin/$(GNU_PREFIX)-objdump" 69 | OBJCOPY := "$(GNU_INSTALL_ROOT)/bin/$(GNU_PREFIX)-objcopy" 70 | SIZE := "$(GNU_INSTALL_ROOT)/bin/$(GNU_PREFIX)-size" 71 | MK := mkdir 72 | RM := rm -rf 73 | CP := cp 74 | GENDAT := ./gen_dat 75 | GENZIP := zip 76 | 77 | # function for removing duplicates in a list 78 | remduplicates = $(strip $(if $1,$(firstword $1) $(call remduplicates,$(filter-out $(firstword $1),$1)))) 79 | 80 | # source common to all targets -- add application specific files below 81 | 82 | C_SOURCE_FILES += ../main.c 83 | C_SOURCE_FILES += ../printf.c 84 | C_SOURCE_FILES += ../hard_fault_handler.c 85 | 86 | ifeq ($(DBGLOG_SUPPORT), "yes") 87 | CFLAGS += -D DBGLOG_SUPPORT=1 88 | C_SOURCE_FILES += ../uart.c 89 | endif 90 | 91 | ifeq ($(BLE_DFU_APP_SUPPORT), "yes") 92 | CFLAGS += -D BLE_DFU_APP_SUPPORT 93 | C_SOURCE_FILES += ../dfu_trigger/ble_dfu.c 94 | C_SOURCE_FILES += ../dfu_trigger/bootloader_util_gcc.c 95 | C_SOURCE_FILES += ../dfu_trigger/dfu_app_handler.c 96 | INC_PATHS += -I../dfu_trigger 97 | LINKER_SCRIPT = ./application_dfu.ld 98 | else 99 | LINKER_SCRIPT = ./application.ld 100 | endif 101 | 102 | C_SOURCE_FILES += $(COMPONENTS)/libraries/button/app_button.c 103 | C_SOURCE_FILES += $(COMPONENTS)/libraries/timer/app_timer.c 104 | C_SOURCE_FILES += $(COMPONENTS)/libraries/gpiote/app_gpiote.c 105 | C_SOURCE_FILES += $(COMPONENTS)/libraries/scheduler/app_scheduler.c 106 | C_SOURCE_FILES += $(COMPONENTS)/drivers_nrf/pstorage/pstorage.c 107 | C_SOURCE_FILES += $(COMPONENTS)/drivers_nrf/spi_master/spi_master.c 108 | C_SOURCE_FILES += $(COMPONENTS)/drivers_nrf/twi_master/twi_hw_master.c 109 | C_SOURCE_FILES += $(COMPONENTS)/drivers_nrf/hal/nrf_delay.c 110 | C_SOURCE_FILES += $(COMPONENTS)/softdevice/common/softdevice_handler/softdevice_handler.c 111 | C_SOURCE_FILES += $(COMPONENTS)/ble/ble_debug_assert_handler/ble_debug_assert_handler.c 112 | C_SOURCE_FILES += $(COMPONENTS)/ble/ble_radio_notification/ble_radio_notification.c 113 | C_SOURCE_FILES += $(COMPONENTS)/ble/ble_services/ble_dis/ble_dis.c 114 | C_SOURCE_FILES += $(COMPONENTS)/ble/ble_services/ble_bas/ble_bas.c 115 | C_SOURCE_FILES += $(COMPONENTS)/ble/device_manager/device_manager_peripheral.c 116 | C_SOURCE_FILES += $(COMPONENTS)/ble/common/ble_conn_params.c 117 | C_SOURCE_FILES += $(COMPONENTS)/ble/common/ble_advdata.c 118 | C_SOURCE_FILES += $(COMPONENTS)/ble/common/ble_srv_common.c 119 | C_SOURCE_FILES += $(COMPONENTS)/toolchain/system_nrf51.c 120 | 121 | # assembly files common to all targets 122 | ASM_SOURCE_FILES += $(COMPONENTS)/toolchain/gcc/gcc_startup_nrf51.s 123 | 124 | # includes common to all targets 125 | INC_PATHS += -I../ 126 | INC_PATHS += -I$(COMPONENTS)/toolchain/gcc 127 | INC_PATHS += -I$(COMPONENTS)/toolchain 128 | INC_PATHS += -I$(COMPONENTS)/device 129 | INC_PATHS += -I$(COMPONENTS)/libraries/button 130 | INC_PATHS += -I$(COMPONENTS)/ble/device_manager 131 | INC_PATHS += -I$(COMPONENTS)/ble/ble_services/ble_dis 132 | INC_PATHS += -I$(COMPONENTS)/ble/ble_services/ble_bas 133 | INC_PATHS += -I$(COMPONENTS)/softdevice/s110/headers 134 | INC_PATHS += -I$(COMPONENTS)/ble/common 135 | INC_PATHS += -I$(COMPONENTS)/ble/ble_error_log 136 | INC_PATHS += -I$(COMPONENTS)/drivers_nrf/ble_flash 137 | INC_PATHS += -I$(COMPONENTS)/drivers_nrf/spi_master 138 | INC_PATHS += -I$(COMPONENTS)/drivers_nrf/twi_master 139 | INC_PATHS += -I$(COMPONENTS)/ble/ble_radio_notification 140 | INC_PATHS += -I$(COMPONENTS)/ble/ble_debug_assert_handler 141 | INC_PATHS += -I$(COMPONENTS)/libraries/timer 142 | INC_PATHS += -I$(COMPONENTS)/libraries/gpiote 143 | INC_PATHS += -I$(COMPONENTS)/libraries/sensorsim 144 | INC_PATHS += -I$(COMPONENTS)/drivers_nrf/hal 145 | INC_PATHS += -I$(COMPONENTS)/softdevice/common/softdevice_handler 146 | INC_PATHS += -I$(COMPONENTS)/libraries/scheduler 147 | INC_PATHS += -I$(COMPONENTS)/libraries/util 148 | INC_PATHS += -I$(COMPONENTS)/drivers_nrf/pstorage 149 | INC_PATHS += -I$(COMPONENTS)/drivers_nrf/pstorage/config 150 | INC_PATHS += -I$(COMPONENTS)/libraries/util 151 | INC_PATHS += -I$(COMPONENTS)/ble/device_manager 152 | INC_PATHS += -I$(COMPONENTS)/ble/device_manager/config 153 | INC_PATHS += -I$(COMPONENTS)/libraries/trace 154 | 155 | OBJECT_DIRECTORY = _build 156 | LISTING_DIRECTORY = $(OBJECT_DIRECTORY) 157 | OUTPUT_BINARY_DIRECTORY = $(OBJECT_DIRECTORY) 158 | 159 | # Sorting removes duplicates 160 | BUILD_DIRECTORIES := $(sort $(OBJECT_DIRECTORY) $(OUTPUT_BINARY_DIRECTORY) $(LISTING_DIRECTORY) ) 161 | 162 | ifeq ($(BUILD_TYPE),debug) 163 | DEBUG_FLAGS += -D DEBUG -g -O0 164 | else 165 | DEBUG_FLAGS += -D NDEBUG -O3 166 | endif 167 | 168 | # flags common to all targets 169 | #CFLAGS += -save-temps 170 | CFLAGS += $(DEBUG_FLAGS) 171 | CFLAGS += -D NRF51 172 | CFLAGS += -D BLE_STACK_SUPPORT_REQD 173 | CFLAGS += -D S110 174 | CFLAGS += -D SOFTDEVICE_PRESENT 175 | CFLAGS += -D $(TARGET_BOARD) 176 | CFLAGS += -D SPI_MASTER_0_ENABLE 177 | 178 | CFLAGS += -mcpu=cortex-m0 179 | CFLAGS += -mthumb -mabi=aapcs --std=gnu99 180 | CFLAGS += -Wall -Werror 181 | CFLAGS += -Wa,-adhln 182 | CFLAGS += -mfloat-abi=soft 183 | # keep every function in separate section. This will allow linker to dump unused functions 184 | CFLAGS += -ffunction-sections 185 | CFLAGS += -fdata-sections -fno-strict-aliasing 186 | CFLAGS += -fno-builtin 187 | 188 | # keep every function in separate section. This will allow linker to dump unused functions 189 | LDFLAGS += -Xlinker -Map=$(LISTING_DIRECTORY)/$(OUTPUT_NAME).map 190 | LDFLAGS += -mthumb -mabi=aapcs -L $(TEMPLATE_PATH) -T$(LINKER_SCRIPT) 191 | LDFLAGS += -mcpu=cortex-m0 192 | LDFLAGS += $(DEBUG_FLAGS) 193 | # let linker to dump unused sections 194 | LDFLAGS += -Wl,--gc-sections 195 | # use newlib in nano version 196 | LDFLAGS += --specs=nano.specs -lc -lnosys 197 | 198 | # Assembler flags 199 | ASMFLAGS += $(DEBUG_FLAGS) 200 | ASMFLAGS += -x assembler-with-cpp 201 | ASMFLAGS += -D NRF51 202 | ASMFLAGS += -D BLE_STACK_SUPPORT_REQD 203 | ASMFLAGS += -D S110 204 | ASMFLAGS += -D SOFTDEVICE_PRESENT 205 | ASMFLAGS += -D $(TARGET_BOARD) 206 | 207 | C_SOURCE_FILE_NAMES = $(notdir $(C_SOURCE_FILES)) 208 | C_PATHS = $(call remduplicates, $(dir $(C_SOURCE_FILES) ) ) 209 | C_OBJECTS = $(addprefix $(OBJECT_DIRECTORY)/, $(C_SOURCE_FILE_NAMES:.c=.o) ) 210 | 211 | ASM_SOURCE_FILE_NAMES = $(notdir $(ASM_SOURCE_FILES)) 212 | ASM_PATHS = $(call remduplicates, $(dir $(ASM_SOURCE_FILES) )) 213 | ASM_OBJECTS = $(addprefix $(OBJECT_DIRECTORY)/, $(ASM_SOURCE_FILE_NAMES:.s=.o) ) 214 | 215 | TOOLCHAIN_BASE = $(basename $(notdir $(GNU_INSTALL_ROOT))) 216 | 217 | TIMESTAMP = $(shell date +'%s') 218 | 219 | vpath %.c $(C_PATHS) 220 | vpath %.s $(ASM_PATHS) 221 | 222 | OBJECTS = $(C_OBJECTS) $(ASM_OBJECTS) 223 | 224 | all: $(BUILD_DIRECTORIES) $(OBJECTS) 225 | @echo Linking target: $(OUTPUT_NAME).elf 226 | $(NO_ECHO)$(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf 227 | $(NO_ECHO)$(MAKE) -f $(MAKEFILE_NAME) -C $(MAKEFILE_DIR) -e finalize 228 | 229 | $(NO_ECHO)$(CP) $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).hex $(OUTPUT_BINARY_DIRECTORY)/$(VERSION_NAME).hex 230 | $(NO_ECHO)$(CP) $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).dat $(OUTPUT_BINARY_DIRECTORY)/$(VERSION_NAME).dat 231 | $(NO_ECHO)$(CP) $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).zip $(OUTPUT_BINARY_DIRECTORY)/$(VERSION_NAME).zip 232 | 233 | @echo "*****************************************************" 234 | @echo "build project: $(OUTPUT_NAME)" 235 | @echo "build type: $(BUILD_TYPE)" 236 | @echo "build with: $(TOOLCHAIN_BASE)" 237 | @echo "build target: $(TARGET_BOARD)" 238 | @echo "build options --" 239 | @echo " DBGLOG_SUPPORT $(DBGLOG_SUPPORT)" 240 | @echo " BLE_DFU_APP_SUPPORT $(BLE_DFU_APP_SUPPORT)" 241 | @echo "build products --" 242 | @echo " $(OUTPUT_NAME).elf" 243 | @echo " $(OUTPUT_NAME).hex" 244 | @echo " $(OUTPUT_NAME).dat" 245 | @echo " $(OUTPUT_NAME).zip" 246 | @echo "build versioning --" 247 | @echo " $(VERSION_NAME).hex" 248 | @echo " $(VERSION_NAME).dat" 249 | @echo " $(VERSION_NAME).zip" 250 | @echo "*****************************************************" 251 | 252 | debug : all 253 | 254 | release : all 255 | 256 | # Create build directories 257 | $(BUILD_DIRECTORIES): 258 | echo $(MAKEFILE_NAME) 259 | $(MK) $@ 260 | 261 | # Create objects from C SRC files 262 | $(OBJECT_DIRECTORY)/%.o: %.c 263 | @echo Compiling file: $(notdir $<) 264 | $(NO_ECHO)$(CC) $(CFLAGS) $(INC_PATHS) \ 265 | -c $< -o $@ > $(OUTPUT_BINARY_DIRECTORY)/$*.lst 266 | 267 | # Assemble files 268 | $(OBJECT_DIRECTORY)/%.o: %.s 269 | @echo Compiling file: $(notdir $<) 270 | $(NO_ECHO)$(CC) $(ASMFLAGS) $(INC_PATHS) -c -o $@ $< 271 | 272 | # Link 273 | $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf: $(BUILD_DIRECTORIES) $(OBJECTS) 274 | @echo Linking target: $(OUTPUT_NAME).elf 275 | $(NO_ECHO)$(CC) $(LDFLAGS) $(OBJECTS) $(LIBS) -o $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf 276 | 277 | # Create binary .bin file from the .elf file 278 | $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).bin: $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf 279 | @echo Preparing: $(OUTPUT_NAME).bin 280 | $(NO_ECHO)$(OBJCOPY) -O binary $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).bin 281 | 282 | # Create binary .hex file from the .elf file 283 | $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).hex: $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf 284 | @echo Preparing: $(OUTPUT_NAME).hex 285 | $(NO_ECHO)$(OBJCOPY) -O ihex $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).hex 286 | 287 | finalize: genbin genhex gendat genzip echosize 288 | 289 | genbin: 290 | @echo Preparing: $(OUTPUT_NAME).bin 291 | $(NO_ECHO)$(OBJCOPY) -O binary $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).bin 292 | 293 | # Create binary .hex file from the .elf file 294 | genhex: 295 | @echo Preparing: $(OUTPUT_NAME).hex 296 | $(NO_ECHO)$(OBJCOPY) -O ihex $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).hex 297 | 298 | # Create .dat file from the .bin file 299 | gendat: 300 | @echo Preparing: $(OUTPUT_NAME).dat 301 | $(NO_ECHO)$(GENDAT) $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).bin $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).dat 302 | 303 | # Create .zip file from the .bin + .dat files 304 | genzip: 305 | @echo Preparing: $(OUTPUT_NAME).zip 306 | -@$(GENZIP) -j $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).zip $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).bin $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).dat 307 | 308 | echosize: 309 | -@echo "" 310 | $(NO_ECHO)$(SIZE) $(OUTPUT_BINARY_DIRECTORY)/$(OUTPUT_NAME).elf 311 | -@echo "" 312 | 313 | clean: 314 | $(RM) $(BUILD_DIRECTORIES) 315 | 316 | cleanobj: 317 | $(RM) $(BUILD_DIRECTORIES)/*.o 318 | 319 | -------------------------------------------------------------------------------- /dfu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------------------ 3 | # DFU Server for Nordic nRF51 based systems. 4 | # Conforms to nRF51_SDK 8.0 BLE_DFU requirements. 5 | #------------------------------------------------------------------------------ 6 | import os 7 | import sys 8 | import pexpect 9 | import optparse 10 | import time 11 | 12 | from intelhex import IntelHex 13 | from array import array 14 | from unpacker import Unpacker 15 | 16 | # DFU Opcodes 17 | class Commands: 18 | START_DFU = 1 19 | INITIALIZE_DFU = 2 20 | RECEIVE_FIRMWARE_IMAGE = 3 21 | VALIDATE_FIRMWARE_IMAGE = 4 22 | ACTIVATE_FIRMWARE_AND_RESET = 5 23 | SYSTEM_RESET = 6 24 | PKT_RCPT_NOTIF_REQ = 8 25 | 26 | # DFU Procedures values 27 | DFU_proc_to_str = { 28 | "01" : "START", 29 | "02" : "INIT", 30 | "03" : "RECEIVE_APP", 31 | "04" : "VALIDATE", 32 | "08" : "PKT_RCPT_REQ", 33 | } 34 | 35 | # DFU Operations values 36 | DFU_oper_to_str = { 37 | "01" : "START_DFU", 38 | "02" : "RECEIVE_INIT", 39 | "03" : "RECEIVE_FW", 40 | "04" : "VALIDATE", 41 | "05" : "ACTIVATE_N_RESET", 42 | "06" : "SYS_RESET", 43 | "07" : "IMAGE_SIZE_REQ", 44 | "08" : "PKT_RCPT_REQ", 45 | "10" : "RESPONSE", 46 | "11" : "PKT_RCPT_NOTIF", 47 | } 48 | 49 | # DFU Status values 50 | DFU_status_to_str = { 51 | "01" : "SUCCESS", 52 | "02" : "INVALID_STATE", 53 | "03" : "NOT_SUPPORTED", 54 | "04" : "DATA_SIZE", 55 | "05" : "CRC_ERROR", 56 | "06" : "OPER_FAILED", 57 | } 58 | 59 | #------------------------------------------------------------------------------ 60 | # Convert a number into an array of 4 bytes (LSB). 61 | # This has been modified to prepend 8 zero bytes per the new DFU spec. 62 | #------------------------------------------------------------------------------ 63 | def convert_uint32_to_array(value): 64 | return [0,0,0,0,0,0,0,0, 65 | (value >> 0 & 0xFF), 66 | (value >> 8 & 0xFF), 67 | (value >> 16 & 0xFF), 68 | (value >> 24 & 0xFF) 69 | ] 70 | 71 | #------------------------------------------------------------------------------ 72 | # Convert a number into an array of 2 bytes (LSB). 73 | #------------------------------------------------------------------------------ 74 | def convert_uint16_to_array(value): 75 | return [ 76 | (value >> 0 & 0xFF), 77 | (value >> 8 & 0xFF) 78 | ] 79 | 80 | #------------------------------------------------------------------------------ 81 | # 82 | #------------------------------------------------------------------------------ 83 | def convert_array_to_hex_string(arr): 84 | hex_str = "" 85 | for val in arr: 86 | if val > 255: 87 | raise Exception("Value is greater than it is possible to represent with one byte") 88 | hex_str += "%02x" % val 89 | 90 | return hex_str 91 | 92 | #------------------------------------------------------------------------------ 93 | # Define the BleDfuServer class 94 | #------------------------------------------------------------------------------ 95 | class BleDfuServer(object): 96 | 97 | #-------------------------------------------------------------------------- 98 | # Adjust these handle values to your peripheral device requirements. 99 | #-------------------------------------------------------------------------- 100 | ctrlpt_handle = 0x19 101 | ctrlpt_cccd_handle = 0x1a 102 | data_handle = 0x17 103 | 104 | pkt_receipt_interval = 10 105 | pkt_payload_size = 20 106 | 107 | #-------------------------------------------------------------------------- 108 | # 109 | #-------------------------------------------------------------------------- 110 | def __init__(self, target_mac, hexfile_path, datfile_path): 111 | 112 | self.hexfile_path = hexfile_path 113 | self.datfile_path = datfile_path 114 | 115 | self.ble_conn = pexpect.spawn("gatttool -b '%s' -t random --interactive" % target_mac) 116 | 117 | # remove next line comment for pexpect detail tracing. 118 | #self.ble_conn.logfile = sys.stdout 119 | 120 | #-------------------------------------------------------------------------- 121 | # Connect to peripheral device. 122 | #-------------------------------------------------------------------------- 123 | def scan_and_connect(self): 124 | 125 | print "scan_and_connect" 126 | 127 | try: 128 | self.ble_conn.expect('\[LE\]>', timeout=10) 129 | except pexpect.TIMEOUT, e: 130 | print "Connect timeout" 131 | 132 | self.ble_conn.sendline('connect') 133 | 134 | try: 135 | res = self.ble_conn.expect('\[CON\].*>', timeout=10) 136 | except pexpect.TIMEOUT, e: 137 | print "Connect timeout" 138 | 139 | #-------------------------------------------------------------------------- 140 | # Wait for notification to arrive. 141 | # Example format: "Notification handle = 0x0019 value: 10 01 01" 142 | #-------------------------------------------------------------------------- 143 | def _dfu_wait_for_notify(self): 144 | 145 | while True: 146 | #print "dfu_wait_for_notify" 147 | 148 | if not self.ble_conn.isalive(): 149 | print "connection not alive" 150 | return None 151 | 152 | try: 153 | index = self.ble_conn.expect('Notification handle = .*? \r\n', timeout=30) 154 | 155 | except pexpect.TIMEOUT: 156 | # 157 | # The gatttool does not report link-lost directly. 158 | # The only way found to detect it is monitoring the prompt '[CON]' 159 | # and if it goes to '[ ]' this indicates the connection has 160 | # been broken. 161 | # In order to get a updated prompt string, issue an empty 162 | # sendline(''). If it contains the '[ ]' string, then 163 | # raise an exception. Otherwise, if not a link-lost condition, 164 | # continue to wait. 165 | # 166 | self.ble_conn.sendline('') 167 | string = self.ble_conn.before 168 | if '[ ]' in string: 169 | print 'Connection lost! {0}.{1}'.format(name, os.getpid()) 170 | raise Exception('Connection Lost') 171 | return None 172 | 173 | if index == 0: 174 | after = self.ble_conn.after 175 | hxstr = after.split()[3:] 176 | handle = long(float.fromhex(hxstr[0])) 177 | return hxstr[2:] 178 | 179 | else: 180 | print "unexpeced index: {0}".format(index) 181 | return None 182 | 183 | #-------------------------------------------------------------------------- 184 | # Parse notification status results 185 | #-------------------------------------------------------------------------- 186 | def _dfu_parse_notify(self, notify): 187 | 188 | if len(notify) < 3: 189 | print "notify data length error" 190 | return None 191 | 192 | dfu_oper = notify[0] 193 | oper_str = DFU_oper_to_str[dfu_oper] 194 | 195 | if oper_str == "RESPONSE": 196 | 197 | dfu_process = notify[1] 198 | dfu_status = notify[2] 199 | 200 | process_str = DFU_proc_to_str[dfu_process] 201 | status_str = DFU_status_to_str[dfu_status] 202 | 203 | print "oper: {0}, proc: {1}, status: {2}".format(oper_str, process_str, status_str) 204 | 205 | if oper_str == "RESPONSE" and status_str == "SUCCESS": 206 | return "OK" 207 | else: 208 | return "FAIL" 209 | 210 | if oper_str == "PKT_RCPT_NOTIF": 211 | 212 | byte1 = int(notify[4], 16) 213 | byte2 = int(notify[3], 16) 214 | byte3 = int(notify[2], 16) 215 | byte4 = int(notify[1], 16) 216 | 217 | receipt = 0 218 | receipt = receipt + (byte1 << 24) 219 | receipt = receipt + (byte2 << 16) 220 | receipt = receipt + (byte3 << 8) 221 | receipt = receipt + (byte4 << 0) 222 | 223 | print "PKT_RCPT: {0:8}".format(receipt) 224 | 225 | return "OK" 226 | 227 | 228 | #-------------------------------------------------------------------------- 229 | # Send two bytes: command + option 230 | #-------------------------------------------------------------------------- 231 | def _dfu_state_set(self, opcode): 232 | self.ble_conn.sendline('char-write-req 0x%04x %04x' % (self.ctrlpt_handle, opcode)) 233 | 234 | # Verify that command was successfully written 235 | try: 236 | res = self.ble_conn.expect('.* Characteristic value was written successfully', timeout=10) 237 | except pexpect.TIMEOUT, e: 238 | print "State timeout" 239 | 240 | #-------------------------------------------------------------------------- 241 | # Send one byte: command 242 | #-------------------------------------------------------------------------- 243 | def _dfu_state_set_byte(self, opcode): 244 | self.ble_conn.sendline('char-write-req 0x%04x %02x' % (self.ctrlpt_handle, opcode)) 245 | 246 | # Verify that command was successfully written 247 | try: 248 | res = self.ble_conn.expect('.* Characteristic value was written successfully', timeout=10) 249 | except pexpect.TIMEOUT, e: 250 | print "State timeout" 251 | 252 | #-------------------------------------------------------------------------- 253 | # Send 3 bytes: PKT_RCPT_NOTIF_REQ with interval of 10 (0x0a) 254 | #-------------------------------------------------------------------------- 255 | def _dfu_pkt_rcpt_notif_req(self): 256 | 257 | opcode = 0x080000 258 | opcode = opcode + (self.pkt_receipt_interval << 8) 259 | 260 | self.ble_conn.sendline('char-write-req 0x%04x %06x' % (self.ctrlpt_handle, opcode)) 261 | 262 | # Verify that command was successfully written 263 | try: 264 | res = self.ble_conn.expect('.* Characteristic value was written successfully', timeout=10) 265 | except pexpect.TIMEOUT, e: 266 | print "Send PKT_RCPT_NOTIF_REQ timeout" 267 | 268 | #-------------------------------------------------------------------------- 269 | # Send an array of bytes: request mode 270 | #-------------------------------------------------------------------------- 271 | def _dfu_data_send_req(self, data_arr): 272 | hex_str = convert_array_to_hex_string(data_arr) 273 | #print hex_str 274 | self.ble_conn.sendline('char-write-req 0x%04x %s' % (self.data_handle, hex_str)) 275 | 276 | # Verify that data was successfully written 277 | try: 278 | res = self.ble_conn.expect('.* Characteristic value was written successfully', timeout=10) 279 | except pexpect.TIMEOUT, e: 280 | print "Data timeout" 281 | 282 | #-------------------------------------------------------------------------- 283 | # Send an array of bytes: command mode 284 | #-------------------------------------------------------------------------- 285 | def _dfu_data_send_cmd(self, data_arr): 286 | hex_str = convert_array_to_hex_string(data_arr) 287 | #print hex_str 288 | self.ble_conn.sendline('char-write-cmd 0x%04x %s' % (self.data_handle, hex_str)) 289 | 290 | #-------------------------------------------------------------------------- 291 | # Enable DFU Control Point CCCD (Notifications) 292 | #-------------------------------------------------------------------------- 293 | def _dfu_enable_cccd(self): 294 | cccd_enable_value_array_lsb = convert_uint16_to_array(0x0001) 295 | cccd_enable_value_hex_string = convert_array_to_hex_string(cccd_enable_value_array_lsb) 296 | self.ble_conn.sendline('char-write-req 0x%04x %s' % (self.ctrlpt_cccd_handle, cccd_enable_value_hex_string)) 297 | 298 | # Verify that CCCD was successfully written 299 | try: 300 | res = self.ble_conn.expect('.* Characteristic value was written successfully', timeout=10) 301 | except pexpect.TIMEOUT, e: 302 | print "CCCD timeout" 303 | 304 | #-------------------------------------------------------------------------- 305 | # Send the Init info (*.dat file contents) to peripheral device. 306 | #-------------------------------------------------------------------------- 307 | def _dfu_send_init(self): 308 | 309 | print "dfu_send_info" 310 | 311 | # Open the DAT file and create array of its contents 312 | bin_array = array('B', open(self.datfile_path, 'rb').read()) 313 | 314 | # Transmit Init info 315 | self._dfu_data_send_req(bin_array) 316 | 317 | #-------------------------------------------------------------------------- 318 | # Initialize: 319 | # Hex: read and convert hexfile into bin_array 320 | # Bin: read binfile into bin_array 321 | #-------------------------------------------------------------------------- 322 | def input_setup(self): 323 | 324 | print "input_setup" 325 | 326 | if self.hexfile_path == None: 327 | raise Exception("input invalid") 328 | 329 | name, extent = os.path.splitext(self.hexfile_path) 330 | 331 | if extent == ".bin": 332 | self.bin_array = array('B', open(self.hexfile_path, 'rb').read()) 333 | self.hex_size = len(self.bin_array) 334 | print "bin array size: ", self.hex_size 335 | return 336 | 337 | if extent == ".hex": 338 | intelhex = IntelHex(self.hexfile_path) 339 | self.bin_array = intelhex.tobinarray() 340 | self.hex_size = len(self.bin_array) 341 | print "bin array size: ", self.hex_size 342 | return 343 | 344 | raise Exception("input invalid") 345 | 346 | #-------------------------------------------------------------------------- 347 | # Send the binary firmware image to peripheral device. 348 | #-------------------------------------------------------------------------- 349 | def dfu_send_image(self): 350 | 351 | print "dfu_send_image" 352 | 353 | # Enable Notifications 354 | self._dfu_enable_cccd() 355 | 356 | # Send 'START DFU' + Application Command 357 | self._dfu_state_set(0x0104) 358 | 359 | # Transmit binary image size 360 | hex_size_array_lsb = convert_uint32_to_array(len(self.bin_array)) 361 | 362 | print hex_size_array_lsb 363 | self._dfu_data_send_req(hex_size_array_lsb) 364 | print "Sending hex file size" 365 | 366 | # Send 'INIT DFU' Command 367 | self._dfu_state_set(0x0200) 368 | 369 | # Wait for INIT DFU notification (indicates flash erase completed) 370 | notify = self._dfu_wait_for_notify() 371 | 372 | # Check the notify status. 373 | dfu_status = self._dfu_parse_notify(notify) 374 | if dfu_status != "OK": 375 | raise Exception("bad notification status") 376 | 377 | # Transmit the Init image (DAT). 378 | self._dfu_send_init() 379 | 380 | # Send 'INIT DFU' + Complete Command 381 | self._dfu_state_set(0x0201) 382 | 383 | # Send packet receipt notification interval (currently 10) 384 | self._dfu_pkt_rcpt_notif_req() 385 | 386 | # Send 'RECEIVE FIRMWARE IMAGE' command to set DFU in firmware receive state. 387 | self._dfu_state_set_byte(Commands.RECEIVE_FIRMWARE_IMAGE) 388 | 389 | ''' 390 | Send bin_array contents as as series of packets (burst mode). 391 | Each segment is pkt_payload_size bytes long. 392 | For every pkt_receipt_interval sends, wait for notification. 393 | ''' 394 | segment_count = 1 395 | for i in range(0, self.hex_size, self.pkt_payload_size): 396 | 397 | segment = self.bin_array[i:i + self.pkt_payload_size] 398 | self._dfu_data_send_cmd(segment) 399 | 400 | #print "segment #", segment_count 401 | 402 | if (segment_count % self.pkt_receipt_interval) == 0: 403 | notify = self._dfu_wait_for_notify() 404 | 405 | if notify == None: 406 | raise Exception("no notification received") 407 | 408 | dfu_status = self._dfu_parse_notify(notify) 409 | 410 | if dfu_status == None or dfu_status != "OK": 411 | raise Exception("bad notification status") 412 | 413 | segment_count += 1 414 | 415 | # Send Validate Command 416 | self._dfu_state_set_byte(Commands.VALIDATE_FIRMWARE_IMAGE) 417 | 418 | # Wait a bit for copy on the peer to be finished 419 | time.sleep(1) 420 | 421 | # Send Activate and Reset Command 422 | self._dfu_state_set_byte(Commands.ACTIVATE_FIRMWARE_AND_RESET) 423 | 424 | 425 | #-------------------------------------------------------------------------- 426 | # Disconnect from peer device if not done already and clean up. 427 | #-------------------------------------------------------------------------- 428 | def disconnect(self): 429 | self.ble_conn.sendline('exit') 430 | self.ble_conn.close() 431 | 432 | #------------------------------------------------------------------------------ 433 | # 434 | #------------------------------------------------------------------------------ 435 | def main(): 436 | 437 | print "DFU Server start" 438 | 439 | try: 440 | parser = optparse.OptionParser(usage='%prog -f -a \n\nExample:\n\tdfu.py -f application.hex -f application.dat -a cd:e3:4a:47:1c:e4', 441 | version='0.5') 442 | 443 | parser.add_option('-a', '--address', 444 | action='store', 445 | dest="address", 446 | type="string", 447 | default=None, 448 | help='DFU target address.' 449 | ) 450 | 451 | parser.add_option('-f', '--file', 452 | action='store', 453 | dest="hexfile", 454 | type="string", 455 | default=None, 456 | help='hex file to be uploaded.' 457 | ) 458 | 459 | parser.add_option('-d', '--dat', 460 | action='store', 461 | dest="datfile", 462 | type="string", 463 | default=None, 464 | help='dat file to be uploaded.' 465 | ) 466 | 467 | parser.add_option('-z', '--zip', 468 | action='store', 469 | dest="zipfile", 470 | type="string", 471 | default=None, 472 | help='zip file to be used.' 473 | ) 474 | 475 | options, args = parser.parse_args() 476 | 477 | except Exception, e: 478 | print e 479 | print "For help use --help" 480 | sys.exit(2) 481 | 482 | try: 483 | 484 | ''' Validate input parameters ''' 485 | 486 | if not options.address: 487 | parser.print_help() 488 | exit(2) 489 | 490 | unpacker = None 491 | hexfile = None 492 | datfile = None 493 | 494 | if options.zipfile != None: 495 | 496 | if (options.hexfile != None) or (options.datfile != None): 497 | print "Conflicting input directives" 498 | exit(2) 499 | 500 | unpacker = Unpacker() 501 | 502 | hexfile, datfile = unpacker.unpack_zipfile(options.zipfile) 503 | 504 | else: 505 | if (not options.hexfile) or (not options.datfile): 506 | parser.print_help() 507 | exit(2) 508 | 509 | if not os.path.isfile(options.hexfile): 510 | print "Error: Hex file doesn't exist" 511 | exit(2) 512 | 513 | if not os.path.isfile(options.datfile): 514 | print "Error: DAT file doesn't exist" 515 | exit(2) 516 | 517 | hexfile = options.hexfile 518 | datfile = options.datfile 519 | 520 | 521 | ''' Start of Device Firmware Update processing ''' 522 | 523 | ble_dfu = BleDfuServer(options.address.upper(), hexfile, datfile) 524 | 525 | # Initialize inputs 526 | ble_dfu.input_setup() 527 | 528 | # Connect to peer device. 529 | ble_dfu.scan_and_connect() 530 | 531 | # Transmit the hex image to peer device. 532 | ble_dfu.dfu_send_image() 533 | 534 | # Wait to receive the disconnect event from peripheral device. 535 | time.sleep(1) 536 | 537 | # Disconnect from peer device if not done already and clean up. 538 | ble_dfu.disconnect() 539 | 540 | except Exception, e: 541 | print e 542 | pass 543 | 544 | except: 545 | pass 546 | 547 | # If Unpacker for zipfile used then delete Unpacker 548 | if unpacker != None: 549 | unpacker.delete() 550 | 551 | print "DFU Server done" 552 | 553 | #------------------------------------------------------------------------------ 554 | # 555 | #------------------------------------------------------------------------------ 556 | if __name__ == '__main__': 557 | 558 | # Do not litter the world with broken .pyc files. 559 | sys.dont_write_bytecode = True 560 | 561 | main() 562 | 563 | -------------------------------------------------------------------------------- /intelhex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2005-2013, Alexander Belchenko 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, 7 | # with or without modification, are permitted provided 8 | # that the following conditions are met: 9 | # 10 | # * Redistributions of source code must retain 11 | # the above copyright notice, this list of conditions 12 | # and the following disclaimer. 13 | # * Redistributions in binary form must reproduce 14 | # the above copyright notice, this list of conditions 15 | # and the following disclaimer in the documentation 16 | # and/or other materials provided with the distribution. 17 | # * Neither the name of the author nor the names 18 | # of its contributors may be used to endorse 19 | # or promote products derived from this software 20 | # without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 24 | # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 25 | # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 | # IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 28 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 29 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 30 | # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 31 | # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 34 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | 36 | '''Intel HEX file format reader and converter. 37 | 38 | @author Alexander Belchenko (alexander dot belchenko at gmail dot com) 39 | @version 1.5 40 | ''' 41 | 42 | 43 | __docformat__ = "javadoc" 44 | 45 | 46 | from array import array 47 | from binascii import hexlify, unhexlify 48 | from bisect import bisect_right 49 | import os 50 | import sys 51 | 52 | from compat import asbytes, asstr 53 | 54 | 55 | class _DeprecatedParam(object): 56 | pass 57 | 58 | _DEPRECATED = _DeprecatedParam() 59 | 60 | 61 | class IntelHex(object): 62 | ''' Intel HEX file reader. ''' 63 | 64 | def __init__(self, source=None): 65 | ''' Constructor. If source specified, object will be initialized 66 | with the contents of source. Otherwise the object will be empty. 67 | 68 | @param source source for initialization 69 | (file name of HEX file, file object, addr dict or 70 | other IntelHex object) 71 | ''' 72 | # public members 73 | self.padding = 0x0FF 74 | # Start Address 75 | self.start_addr = None 76 | 77 | # private members 78 | self._buf = {} 79 | self._offset = 0 80 | 81 | if source is not None: 82 | if isinstance(source, basestring) or getattr(source, "read", None): 83 | # load hex file 84 | self.loadhex(source) 85 | elif isinstance(source, dict): 86 | self.fromdict(source) 87 | elif isinstance(source, IntelHex): 88 | self.padding = source.padding 89 | if source.start_addr: 90 | self.start_addr = source.start_addr.copy() 91 | self._buf = source._buf.copy() 92 | else: 93 | raise ValueError("source: bad initializer type") 94 | 95 | def _decode_record(self, s, line=0): 96 | '''Decode one record of HEX file. 97 | 98 | @param s line with HEX record. 99 | @param line line number (for error messages). 100 | 101 | @raise EndOfFile if EOF record encountered. 102 | ''' 103 | s = s.rstrip('\r\n') 104 | if not s: 105 | return # empty line 106 | 107 | if s[0] == ':': 108 | try: 109 | bin = array('B', unhexlify(asbytes(s[1:]))) 110 | except (TypeError, ValueError): 111 | # this might be raised by unhexlify when odd hexascii digits 112 | raise HexRecordError(line=line) 113 | length = len(bin) 114 | if length < 5: 115 | raise HexRecordError(line=line) 116 | else: 117 | raise HexRecordError(line=line) 118 | 119 | record_length = bin[0] 120 | if length != (5 + record_length): 121 | raise RecordLengthError(line=line) 122 | 123 | addr = bin[1]*256 + bin[2] 124 | 125 | record_type = bin[3] 126 | if not (0 <= record_type <= 5): 127 | raise RecordTypeError(line=line) 128 | 129 | crc = sum(bin) 130 | crc &= 0x0FF 131 | if crc != 0: 132 | raise RecordChecksumError(line=line) 133 | 134 | if record_type == 0: 135 | # data record 136 | addr += self._offset 137 | for i in xrange(4, 4+record_length): 138 | if not self._buf.get(addr, None) is None: 139 | raise AddressOverlapError(address=addr, line=line) 140 | self._buf[addr] = bin[i] 141 | addr += 1 # FIXME: addr should be wrapped 142 | # BUT after 02 record (at 64K boundary) 143 | # and after 04 record (at 4G boundary) 144 | 145 | elif record_type == 1: 146 | # end of file record 147 | if record_length != 0: 148 | raise EOFRecordError(line=line) 149 | raise _EndOfFile 150 | 151 | elif record_type == 2: 152 | # Extended 8086 Segment Record 153 | if record_length != 2 or addr != 0: 154 | raise ExtendedSegmentAddressRecordError(line=line) 155 | self._offset = (bin[4]*256 + bin[5]) * 16 156 | 157 | elif record_type == 4: 158 | # Extended Linear Address Record 159 | if record_length != 2 or addr != 0: 160 | raise ExtendedLinearAddressRecordError(line=line) 161 | self._offset = (bin[4]*256 + bin[5]) * 65536 162 | 163 | elif record_type == 3: 164 | # Start Segment Address Record 165 | if record_length != 4 or addr != 0: 166 | raise StartSegmentAddressRecordError(line=line) 167 | if self.start_addr: 168 | raise DuplicateStartAddressRecordError(line=line) 169 | self.start_addr = {'CS': bin[4]*256 + bin[5], 170 | 'IP': bin[6]*256 + bin[7], 171 | } 172 | 173 | elif record_type == 5: 174 | # Start Linear Address Record 175 | if record_length != 4 or addr != 0: 176 | raise StartLinearAddressRecordError(line=line) 177 | if self.start_addr: 178 | raise DuplicateStartAddressRecordError(line=line) 179 | self.start_addr = {'EIP': (bin[4]*16777216 + 180 | bin[5]*65536 + 181 | bin[6]*256 + 182 | bin[7]), 183 | } 184 | 185 | def loadhex(self, fobj): 186 | """Load hex file into internal buffer. This is not necessary 187 | if object was initialized with source set. This will overwrite 188 | addresses if object was already initialized. 189 | 190 | @param fobj file name or file-like object 191 | """ 192 | if getattr(fobj, "read", None) is None: 193 | fobj = open(fobj, "r") 194 | fclose = fobj.close 195 | else: 196 | fclose = None 197 | 198 | self._offset = 0 199 | line = 0 200 | 201 | try: 202 | decode = self._decode_record 203 | try: 204 | for s in fobj: 205 | line += 1 206 | decode(s, line) 207 | except _EndOfFile: 208 | pass 209 | finally: 210 | if fclose: 211 | fclose() 212 | 213 | def loadbin(self, fobj, offset=0): 214 | """Load bin file into internal buffer. Not needed if source set in 215 | constructor. This will overwrite addresses without warning 216 | if object was already initialized. 217 | 218 | @param fobj file name or file-like object 219 | @param offset starting address offset 220 | """ 221 | fread = getattr(fobj, "read", None) 222 | if fread is None: 223 | f = open(fobj, "rb") 224 | fread = f.read 225 | fclose = f.close 226 | else: 227 | fclose = None 228 | 229 | try: 230 | self.frombytes(array('B', asbytes(fread())), offset=offset) 231 | finally: 232 | if fclose: 233 | fclose() 234 | 235 | def loadfile(self, fobj, format): 236 | """Load data file into internal buffer. Preferred wrapper over 237 | loadbin or loadhex. 238 | 239 | @param fobj file name or file-like object 240 | @param format file format ("hex" or "bin") 241 | """ 242 | if format == "hex": 243 | self.loadhex(fobj) 244 | elif format == "bin": 245 | self.loadbin(fobj) 246 | else: 247 | raise ValueError('format should be either "hex" or "bin";' 248 | ' got %r instead' % format) 249 | 250 | # alias (to be consistent with method tofile) 251 | fromfile = loadfile 252 | 253 | def fromdict(self, dikt): 254 | """Load data from dictionary. Dictionary should contain int keys 255 | representing addresses. Values should be the data to be stored in 256 | those addresses in unsigned char form (i.e. not strings). 257 | The dictionary may contain the key, ``start_addr`` 258 | to indicate the starting address of the data as described in README. 259 | 260 | The contents of the dict will be merged with this object and will 261 | overwrite any conflicts. This function is not necessary if the 262 | object was initialized with source specified. 263 | """ 264 | s = dikt.copy() 265 | start_addr = s.get('start_addr') 266 | if start_addr is not None: 267 | del s['start_addr'] 268 | for k in s.keys(): 269 | if type(k) not in (int, long) or k < 0: 270 | raise ValueError('Source dictionary should have only int keys') 271 | self._buf.update(s) 272 | if start_addr is not None: 273 | self.start_addr = start_addr 274 | 275 | def frombytes(self, bytes, offset=0): 276 | """Load data from array or list of bytes. 277 | Similar to loadbin() method but works directly with iterable bytes. 278 | """ 279 | for b in bytes: 280 | self._buf[offset] = b 281 | offset += 1 282 | 283 | def _get_start_end(self, start=None, end=None, size=None): 284 | """Return default values for start and end if they are None. 285 | If this IntelHex object is empty then it's error to 286 | invoke this method with both start and end as None. 287 | """ 288 | if (start,end) == (None,None) and self._buf == {}: 289 | raise EmptyIntelHexError 290 | if size is not None: 291 | if None not in (start, end): 292 | raise ValueError("tobinarray: you can't use start,end and size" 293 | " arguments in the same time") 294 | if (start, end) == (None, None): 295 | start = self.minaddr() 296 | if start is not None: 297 | end = start + size - 1 298 | else: 299 | start = end - size + 1 300 | if start < 0: 301 | raise ValueError("tobinarray: invalid size (%d) " 302 | "for given end address (%d)" % (size,end)) 303 | else: 304 | if start is None: 305 | start = self.minaddr() 306 | if end is None: 307 | end = self.maxaddr() 308 | if start > end: 309 | start, end = end, start 310 | return start, end 311 | 312 | def tobinarray(self, start=None, end=None, pad=_DEPRECATED, size=None): 313 | ''' Convert this object to binary form as array. If start and end 314 | unspecified, they will be inferred from the data. 315 | @param start start address of output bytes. 316 | @param end end address of output bytes (inclusive). 317 | @param pad [DEPRECATED PARAMETER, please use self.padding instead] 318 | fill empty spaces with this value 319 | (if pad is None then this method uses self.padding). 320 | @param size size of the block, used with start or end parameter. 321 | @return array of unsigned char data. 322 | ''' 323 | if not isinstance(pad, _DeprecatedParam): 324 | print "IntelHex.tobinarray: 'pad' parameter is deprecated." 325 | if pad is not None: 326 | print "Please, use IntelHex.padding attribute instead." 327 | else: 328 | print "Please, don't pass it explicitly." 329 | print "Use syntax like this: ih.tobinarray(start=xxx, end=yyy, size=zzz)" 330 | else: 331 | pad = None 332 | return self._tobinarray_really(start, end, pad, size) 333 | 334 | def _tobinarray_really(self, start, end, pad, size): 335 | if pad is None: 336 | pad = self.padding 337 | 338 | bin = array('B') 339 | 340 | if self._buf == {} and None in (start, end): 341 | return bin 342 | 343 | if size is not None and size <= 0: 344 | raise ValueError("tobinarray: wrong value for size") 345 | 346 | start, end = self._get_start_end(start, end, size) 347 | 348 | for i in xrange(start, end+1): 349 | bin.append(self._buf.get(i, pad)) 350 | 351 | return bin 352 | 353 | def tobinstr(self, start=None, end=None, pad=_DEPRECATED, size=None): 354 | ''' Convert to binary form and return as a string. 355 | @param start start address of output bytes. 356 | @param end end address of output bytes (inclusive). 357 | @param pad [DEPRECATED PARAMETER, please use self.padding instead] 358 | fill empty spaces with this value 359 | (if pad is None then this method uses self.padding). 360 | @param size size of the block, used with start or end parameter. 361 | @return string of binary data. 362 | ''' 363 | if not isinstance(pad, _DeprecatedParam): 364 | print "IntelHex.tobinstr: 'pad' parameter is deprecated." 365 | if pad is not None: 366 | print "Please, use IntelHex.padding attribute instead." 367 | else: 368 | print "Please, don't pass it explicitly." 369 | print "Use syntax like this: ih.tobinstr(start=xxx, end=yyy, size=zzz)" 370 | else: 371 | pad = None 372 | return self._tobinstr_really(start, end, pad, size) 373 | 374 | def _tobinstr_really(self, start, end, pad, size): 375 | return asstr(self._tobinarray_really(start, end, pad, size).tostring()) 376 | 377 | def tobinfile(self, fobj, start=None, end=None, pad=_DEPRECATED, size=None): 378 | '''Convert to binary and write to file. 379 | 380 | @param fobj file name or file object for writing output bytes. 381 | @param start start address of output bytes. 382 | @param end end address of output bytes (inclusive). 383 | @param pad [DEPRECATED PARAMETER, please use self.padding instead] 384 | fill empty spaces with this value 385 | (if pad is None then this method uses self.padding). 386 | @param size size of the block, used with start or end parameter. 387 | ''' 388 | if not isinstance(pad, _DeprecatedParam): 389 | print "IntelHex.tobinfile: 'pad' parameter is deprecated." 390 | if pad is not None: 391 | print "Please, use IntelHex.padding attribute instead." 392 | else: 393 | print "Please, don't pass it explicitly." 394 | print "Use syntax like this: ih.tobinfile(start=xxx, end=yyy, size=zzz)" 395 | else: 396 | pad = None 397 | if getattr(fobj, "write", None) is None: 398 | fobj = open(fobj, "wb") 399 | close_fd = True 400 | else: 401 | close_fd = False 402 | 403 | fobj.write(self._tobinstr_really(start, end, pad, size)) 404 | 405 | if close_fd: 406 | fobj.close() 407 | 408 | def todict(self): 409 | '''Convert to python dictionary. 410 | 411 | @return dict suitable for initializing another IntelHex object. 412 | ''' 413 | r = {} 414 | r.update(self._buf) 415 | if self.start_addr: 416 | r['start_addr'] = self.start_addr 417 | return r 418 | 419 | def addresses(self): 420 | '''Returns all used addresses in sorted order. 421 | @return list of occupied data addresses in sorted order. 422 | ''' 423 | aa = self._buf.keys() 424 | aa.sort() 425 | return aa 426 | 427 | def minaddr(self): 428 | '''Get minimal address of HEX content. 429 | @return minimal address or None if no data 430 | ''' 431 | aa = self._buf.keys() 432 | if aa == []: 433 | return None 434 | else: 435 | return min(aa) 436 | 437 | def maxaddr(self): 438 | '''Get maximal address of HEX content. 439 | @return maximal address or None if no data 440 | ''' 441 | aa = self._buf.keys() 442 | if aa == []: 443 | return None 444 | else: 445 | return max(aa) 446 | 447 | def __getitem__(self, addr): 448 | ''' Get requested byte from address. 449 | @param addr address of byte. 450 | @return byte if address exists in HEX file, or self.padding 451 | if no data found. 452 | ''' 453 | t = type(addr) 454 | if t in (int, long): 455 | if addr < 0: 456 | raise TypeError('Address should be >= 0.') 457 | return self._buf.get(addr, self.padding) 458 | elif t == slice: 459 | addresses = self._buf.keys() 460 | ih = IntelHex() 461 | if addresses: 462 | addresses.sort() 463 | start = addr.start or addresses[0] 464 | stop = addr.stop or (addresses[-1]+1) 465 | step = addr.step or 1 466 | for i in xrange(start, stop, step): 467 | x = self._buf.get(i) 468 | if x is not None: 469 | ih[i] = x 470 | return ih 471 | else: 472 | raise TypeError('Address has unsupported type: %s' % t) 473 | 474 | def __setitem__(self, addr, byte): 475 | """Set byte at address.""" 476 | t = type(addr) 477 | if t in (int, long): 478 | if addr < 0: 479 | raise TypeError('Address should be >= 0.') 480 | self._buf[addr] = byte 481 | elif t == slice: 482 | if not isinstance(byte, (list, tuple)): 483 | raise ValueError('Slice operation expects sequence of bytes') 484 | start = addr.start 485 | stop = addr.stop 486 | step = addr.step or 1 487 | if None not in (start, stop): 488 | ra = range(start, stop, step) 489 | if len(ra) != len(byte): 490 | raise ValueError('Length of bytes sequence does not match ' 491 | 'address range') 492 | elif (start, stop) == (None, None): 493 | raise TypeError('Unsupported address range') 494 | elif start is None: 495 | start = stop - len(byte) 496 | elif stop is None: 497 | stop = start + len(byte) 498 | if start < 0: 499 | raise TypeError('start address cannot be negative') 500 | if stop < 0: 501 | raise TypeError('stop address cannot be negative') 502 | j = 0 503 | for i in xrange(start, stop, step): 504 | self._buf[i] = byte[j] 505 | j += 1 506 | else: 507 | raise TypeError('Address has unsupported type: %s' % t) 508 | 509 | def __delitem__(self, addr): 510 | """Delete byte at address.""" 511 | t = type(addr) 512 | if t in (int, long): 513 | if addr < 0: 514 | raise TypeError('Address should be >= 0.') 515 | del self._buf[addr] 516 | elif t == slice: 517 | addresses = self._buf.keys() 518 | if addresses: 519 | addresses.sort() 520 | start = addr.start or addresses[0] 521 | stop = addr.stop or (addresses[-1]+1) 522 | step = addr.step or 1 523 | for i in xrange(start, stop, step): 524 | x = self._buf.get(i) 525 | if x is not None: 526 | del self._buf[i] 527 | else: 528 | raise TypeError('Address has unsupported type: %s' % t) 529 | 530 | def __len__(self): 531 | """Return count of bytes with real values.""" 532 | return len(self._buf.keys()) 533 | 534 | def write_hex_file(self, f, write_start_addr=True): 535 | """Write data to file f in HEX format. 536 | 537 | @param f filename or file-like object for writing 538 | @param write_start_addr enable or disable writing start address 539 | record to file (enabled by default). 540 | If there is no start address in obj, nothing 541 | will be written regardless of this setting. 542 | """ 543 | fwrite = getattr(f, "write", None) 544 | if fwrite: 545 | fobj = f 546 | fclose = None 547 | else: 548 | fobj = open(f, 'w') 549 | fwrite = fobj.write 550 | fclose = fobj.close 551 | 552 | # Translation table for uppercasing hex ascii string. 553 | # timeit shows that using hexstr.translate(table) 554 | # is faster than hexstr.upper(): 555 | # 0.452ms vs. 0.652ms (translate vs. upper) 556 | if sys.version_info[0] >= 3: 557 | table = bytes(range(256)).upper() 558 | else: 559 | table = ''.join(chr(i).upper() for i in range(256)) 560 | 561 | 562 | 563 | # start address record if any 564 | if self.start_addr and write_start_addr: 565 | keys = self.start_addr.keys() 566 | keys.sort() 567 | bin = array('B', asbytes('\0'*9)) 568 | if keys == ['CS','IP']: 569 | # Start Segment Address Record 570 | bin[0] = 4 # reclen 571 | bin[1] = 0 # offset msb 572 | bin[2] = 0 # offset lsb 573 | bin[3] = 3 # rectyp 574 | cs = self.start_addr['CS'] 575 | bin[4] = (cs >> 8) & 0x0FF 576 | bin[5] = cs & 0x0FF 577 | ip = self.start_addr['IP'] 578 | bin[6] = (ip >> 8) & 0x0FF 579 | bin[7] = ip & 0x0FF 580 | bin[8] = (-sum(bin)) & 0x0FF # chksum 581 | fwrite(':' + 582 | asstr(hexlify(bin.tostring()).translate(table)) + 583 | '\n') 584 | elif keys == ['EIP']: 585 | # Start Linear Address Record 586 | bin[0] = 4 # reclen 587 | bin[1] = 0 # offset msb 588 | bin[2] = 0 # offset lsb 589 | bin[3] = 5 # rectyp 590 | eip = self.start_addr['EIP'] 591 | bin[4] = (eip >> 24) & 0x0FF 592 | bin[5] = (eip >> 16) & 0x0FF 593 | bin[6] = (eip >> 8) & 0x0FF 594 | bin[7] = eip & 0x0FF 595 | bin[8] = (-sum(bin)) & 0x0FF # chksum 596 | fwrite(':' + 597 | asstr(hexlify(bin.tostring()).translate(table)) + 598 | '\n') 599 | else: 600 | if fclose: 601 | fclose() 602 | raise InvalidStartAddressValueError(start_addr=self.start_addr) 603 | 604 | # data 605 | addresses = self._buf.keys() 606 | addresses.sort() 607 | addr_len = len(addresses) 608 | if addr_len: 609 | minaddr = addresses[0] 610 | maxaddr = addresses[-1] 611 | 612 | if maxaddr > 65535: 613 | need_offset_record = True 614 | else: 615 | need_offset_record = False 616 | high_ofs = 0 617 | 618 | cur_addr = minaddr 619 | cur_ix = 0 620 | 621 | while cur_addr <= maxaddr: 622 | if need_offset_record: 623 | bin = array('B', asbytes('\0'*7)) 624 | bin[0] = 2 # reclen 625 | bin[1] = 0 # offset msb 626 | bin[2] = 0 # offset lsb 627 | bin[3] = 4 # rectyp 628 | high_ofs = int(cur_addr>>16) 629 | b = divmod(high_ofs, 256) 630 | bin[4] = b[0] # msb of high_ofs 631 | bin[5] = b[1] # lsb of high_ofs 632 | bin[6] = (-sum(bin)) & 0x0FF # chksum 633 | fwrite(':' + 634 | asstr(hexlify(bin.tostring()).translate(table)) + 635 | '\n') 636 | 637 | while True: 638 | # produce one record 639 | low_addr = cur_addr & 0x0FFFF 640 | # chain_len off by 1 641 | chain_len = min(15, 65535-low_addr, maxaddr-cur_addr) 642 | 643 | # search continuous chain 644 | stop_addr = cur_addr + chain_len 645 | if chain_len: 646 | ix = bisect_right(addresses, stop_addr, 647 | cur_ix, 648 | min(cur_ix+chain_len+1, addr_len)) 649 | chain_len = ix - cur_ix # real chain_len 650 | # there could be small holes in the chain 651 | # but we will catch them by try-except later 652 | # so for big continuous files we will work 653 | # at maximum possible speed 654 | else: 655 | chain_len = 1 # real chain_len 656 | 657 | bin = array('B', asbytes('\0'*(5+chain_len))) 658 | b = divmod(low_addr, 256) 659 | bin[1] = b[0] # msb of low_addr 660 | bin[2] = b[1] # lsb of low_addr 661 | bin[3] = 0 # rectype 662 | try: # if there is small holes we'll catch them 663 | for i in range(chain_len): 664 | bin[4+i] = self._buf[cur_addr+i] 665 | except KeyError: 666 | # we catch a hole so we should shrink the chain 667 | chain_len = i 668 | bin = bin[:5+i] 669 | bin[0] = chain_len 670 | bin[4+chain_len] = (-sum(bin)) & 0x0FF # chksum 671 | fwrite(':' + 672 | asstr(hexlify(bin.tostring()).translate(table)) + 673 | '\n') 674 | 675 | # adjust cur_addr/cur_ix 676 | cur_ix += chain_len 677 | if cur_ix < addr_len: 678 | cur_addr = addresses[cur_ix] 679 | else: 680 | cur_addr = maxaddr + 1 681 | break 682 | high_addr = int(cur_addr>>16) 683 | if high_addr > high_ofs: 684 | break 685 | 686 | # end-of-file record 687 | fwrite(":00000001FF\n") 688 | if fclose: 689 | fclose() 690 | 691 | def tofile(self, fobj, format): 692 | """Write data to hex or bin file. Preferred method over tobin or tohex. 693 | 694 | @param fobj file name or file-like object 695 | @param format file format ("hex" or "bin") 696 | """ 697 | if format == 'hex': 698 | self.write_hex_file(fobj) 699 | elif format == 'bin': 700 | self.tobinfile(fobj) 701 | else: 702 | raise ValueError('format should be either "hex" or "bin";' 703 | ' got %r instead' % format) 704 | 705 | def gets(self, addr, length): 706 | """Get string of bytes from given address. If any entries are blank 707 | from addr through addr+length, a NotEnoughDataError exception will 708 | be raised. Padding is not used.""" 709 | a = array('B', asbytes('\0'*length)) 710 | try: 711 | for i in xrange(length): 712 | a[i] = self._buf[addr+i] 713 | except KeyError: 714 | raise NotEnoughDataError(address=addr, length=length) 715 | return asstr(a.tostring()) 716 | 717 | def puts(self, addr, s): 718 | """Put string of bytes at given address. Will overwrite any previous 719 | entries. 720 | """ 721 | a = array('B', asbytes(s)) 722 | for i in xrange(len(a)): 723 | self._buf[addr+i] = a[i] 724 | 725 | def getsz(self, addr): 726 | """Get zero-terminated string from given address. Will raise 727 | NotEnoughDataError exception if a hole is encountered before a 0. 728 | """ 729 | i = 0 730 | try: 731 | while True: 732 | if self._buf[addr+i] == 0: 733 | break 734 | i += 1 735 | except KeyError: 736 | raise NotEnoughDataError(msg=('Bad access at 0x%X: ' 737 | 'not enough data to read zero-terminated string') % addr) 738 | return self.gets(addr, i) 739 | 740 | def putsz(self, addr, s): 741 | """Put string in object at addr and append terminating zero at end.""" 742 | self.puts(addr, s) 743 | self._buf[addr+len(s)] = 0 744 | 745 | def dump(self, tofile=None): 746 | """Dump object content to specified file object or to stdout if None. 747 | Format is a hexdump with some header information at the beginning, 748 | addresses on the left, and data on right. 749 | 750 | @param tofile file-like object to dump to 751 | """ 752 | 753 | if tofile is None: 754 | tofile = sys.stdout 755 | # start addr possibly 756 | if self.start_addr is not None: 757 | cs = self.start_addr.get('CS') 758 | ip = self.start_addr.get('IP') 759 | eip = self.start_addr.get('EIP') 760 | if eip is not None and cs is None and ip is None: 761 | tofile.write('EIP = 0x%08X\n' % eip) 762 | elif eip is None and cs is not None and ip is not None: 763 | tofile.write('CS = 0x%04X, IP = 0x%04X\n' % (cs, ip)) 764 | else: 765 | tofile.write('start_addr = %r\n' % start_addr) 766 | # actual data 767 | addresses = self._buf.keys() 768 | if addresses: 769 | addresses.sort() 770 | minaddr = addresses[0] 771 | maxaddr = addresses[-1] 772 | startaddr = int(minaddr>>4)*16 773 | endaddr = int((maxaddr>>4)+1)*16 774 | maxdigits = max(len(str(endaddr)), 4) 775 | templa = '%%0%dX' % maxdigits 776 | range16 = range(16) 777 | for i in xrange(startaddr, endaddr, 16): 778 | tofile.write(templa % i) 779 | tofile.write(' ') 780 | s = [] 781 | for j in range16: 782 | x = self._buf.get(i+j) 783 | if x is not None: 784 | tofile.write(' %02X' % x) 785 | if 32 <= x < 127: # GNU less does not like 0x7F (128 decimal) so we'd better show it as dot 786 | s.append(chr(x)) 787 | else: 788 | s.append('.') 789 | else: 790 | tofile.write(' --') 791 | s.append(' ') 792 | tofile.write(' |' + ''.join(s) + '|\n') 793 | 794 | def merge(self, other, overlap='error'): 795 | """Merge content of other IntelHex object into current object (self). 796 | @param other other IntelHex object. 797 | @param overlap action on overlap of data or starting addr: 798 | - error: raising OverlapError; 799 | - ignore: ignore other data and keep current data 800 | in overlapping region; 801 | - replace: replace data with other data 802 | in overlapping region. 803 | 804 | @raise TypeError if other is not instance of IntelHex 805 | @raise ValueError if other is the same object as self 806 | (it can't merge itself) 807 | @raise ValueError if overlap argument has incorrect value 808 | @raise AddressOverlapError on overlapped data 809 | """ 810 | # check args 811 | if not isinstance(other, IntelHex): 812 | raise TypeError('other should be IntelHex object') 813 | if other is self: 814 | raise ValueError("Can't merge itself") 815 | if overlap not in ('error', 'ignore', 'replace'): 816 | raise ValueError("overlap argument should be either " 817 | "'error', 'ignore' or 'replace'") 818 | # merge data 819 | this_buf = self._buf 820 | other_buf = other._buf 821 | for i in other_buf: 822 | if i in this_buf: 823 | if overlap == 'error': 824 | raise AddressOverlapError( 825 | 'Data overlapped at address 0x%X' % i) 826 | elif overlap == 'ignore': 827 | continue 828 | this_buf[i] = other_buf[i] 829 | # merge start_addr 830 | if self.start_addr != other.start_addr: 831 | if self.start_addr is None: # set start addr from other 832 | self.start_addr = other.start_addr 833 | elif other.start_addr is None: # keep existing start addr 834 | pass 835 | else: # conflict 836 | if overlap == 'error': 837 | raise AddressOverlapError( 838 | 'Starting addresses are different') 839 | elif overlap == 'replace': 840 | self.start_addr = other.start_addr 841 | #/IntelHex 842 | 843 | 844 | class IntelHex16bit(IntelHex): 845 | """Access to data as 16-bit words. Intended to use with Microchip HEX files.""" 846 | 847 | def __init__(self, source=None): 848 | """Construct class from HEX file 849 | or from instance of ordinary IntelHex class. If IntelHex object 850 | is passed as source, the original IntelHex object should not be used 851 | again because this class will alter it. This class leaves padding 852 | alone unless it was precisely 0xFF. In that instance it is sign 853 | extended to 0xFFFF. 854 | 855 | @param source file name of HEX file or file object 856 | or instance of ordinary IntelHex class. 857 | Will also accept dictionary from todict method. 858 | """ 859 | if isinstance(source, IntelHex): 860 | # from ihex8 861 | self.padding = source.padding 862 | self.start_addr = source.start_addr 863 | # private members 864 | self._buf = source._buf 865 | self._offset = source._offset 866 | elif isinstance(source, dict): 867 | raise IntelHexError("IntelHex16bit does not support initialization from dictionary yet.\n" 868 | "Patches are welcome.") 869 | else: 870 | IntelHex.__init__(self, source) 871 | 872 | if self.padding == 0x0FF: 873 | self.padding = 0x0FFFF 874 | 875 | def __getitem__(self, addr16): 876 | """Get 16-bit word from address. 877 | Raise error if only one byte from the pair is set. 878 | We assume a Little Endian interpretation of the hex file. 879 | 880 | @param addr16 address of word (addr8 = 2 * addr16). 881 | @return word if bytes exists in HEX file, or self.padding 882 | if no data found. 883 | """ 884 | addr1 = addr16 * 2 885 | addr2 = addr1 + 1 886 | byte1 = self._buf.get(addr1, None) 887 | byte2 = self._buf.get(addr2, None) 888 | 889 | if byte1 != None and byte2 != None: 890 | return byte1 | (byte2 << 8) # low endian 891 | 892 | if byte1 == None and byte2 == None: 893 | return self.padding 894 | 895 | raise BadAccess16bit(address=addr16) 896 | 897 | def __setitem__(self, addr16, word): 898 | """Sets the address at addr16 to word assuming Little Endian mode. 899 | """ 900 | addr_byte = addr16 * 2 901 | b = divmod(word, 256) 902 | self._buf[addr_byte] = b[1] 903 | self._buf[addr_byte+1] = b[0] 904 | 905 | def minaddr(self): 906 | '''Get minimal address of HEX content in 16-bit mode. 907 | 908 | @return minimal address used in this object 909 | ''' 910 | aa = self._buf.keys() 911 | if aa == []: 912 | return 0 913 | else: 914 | return min(aa)>>1 915 | 916 | def maxaddr(self): 917 | '''Get maximal address of HEX content in 16-bit mode. 918 | 919 | @return maximal address used in this object 920 | ''' 921 | aa = self._buf.keys() 922 | if aa == []: 923 | return 0 924 | else: 925 | return max(aa)>>1 926 | 927 | def tobinarray(self, start=None, end=None, size=None): 928 | '''Convert this object to binary form as array (of 2-bytes word data). 929 | If start and end unspecified, they will be inferred from the data. 930 | @param start start address of output data. 931 | @param end end address of output data (inclusive). 932 | @param size size of the block (number of words), 933 | used with start or end parameter. 934 | @return array of unsigned short (uint16_t) data. 935 | ''' 936 | bin = array('H') 937 | 938 | if self._buf == {} and None in (start, end): 939 | return bin 940 | 941 | if size is not None and size <= 0: 942 | raise ValueError("tobinarray: wrong value for size") 943 | 944 | start, end = self._get_start_end(start, end, size) 945 | 946 | for addr in xrange(start, end+1): 947 | bin.append(self[addr]) 948 | 949 | return bin 950 | 951 | 952 | #/class IntelHex16bit 953 | 954 | 955 | def hex2bin(fin, fout, start=None, end=None, size=None, pad=None): 956 | """Hex-to-Bin convertor engine. 957 | @return 0 if all OK 958 | 959 | @param fin input hex file (filename or file-like object) 960 | @param fout output bin file (filename or file-like object) 961 | @param start start of address range (optional) 962 | @param end end of address range (inclusive; optional) 963 | @param size size of resulting file (in bytes) (optional) 964 | @param pad padding byte (optional) 965 | """ 966 | try: 967 | h = IntelHex(fin) 968 | except HexReaderError, e: 969 | txt = "ERROR: bad HEX file: %s" % str(e) 970 | print(txt) 971 | return 1 972 | 973 | # start, end, size 974 | if size != None and size != 0: 975 | if end == None: 976 | if start == None: 977 | start = h.minaddr() 978 | end = start + size - 1 979 | else: 980 | if (end+1) >= size: 981 | start = end + 1 - size 982 | else: 983 | start = 0 984 | 985 | try: 986 | if pad is not None: 987 | # using .padding attribute rather than pad argument to function call 988 | h.padding = pad 989 | h.tobinfile(fout, start, end) 990 | except IOError, e: 991 | txt = "ERROR: Could not write to file: %s: %s" % (fout, str(e)) 992 | print(txt) 993 | return 1 994 | 995 | return 0 996 | #/def hex2bin 997 | 998 | 999 | def bin2hex(fin, fout, offset=0): 1000 | """Simple bin-to-hex convertor. 1001 | @return 0 if all OK 1002 | 1003 | @param fin input bin file (filename or file-like object) 1004 | @param fout output hex file (filename or file-like object) 1005 | @param offset starting address offset for loading bin 1006 | """ 1007 | h = IntelHex() 1008 | try: 1009 | h.loadbin(fin, offset) 1010 | except IOError, e: 1011 | txt = 'ERROR: unable to load bin file:', str(e) 1012 | print(txt) 1013 | return 1 1014 | 1015 | try: 1016 | h.tofile(fout, format='hex') 1017 | except IOError, e: 1018 | txt = "ERROR: Could not write to file: %s: %s" % (fout, str(e)) 1019 | print(txt) 1020 | return 1 1021 | 1022 | return 0 1023 | #/def bin2hex 1024 | 1025 | 1026 | def diff_dumps(ih1, ih2, tofile=None, name1="a", name2="b", n_context=3): 1027 | """Diff 2 IntelHex objects and produce unified diff output for their 1028 | hex dumps. 1029 | 1030 | @param ih1 first IntelHex object to compare 1031 | @param ih2 second IntelHex object to compare 1032 | @param tofile file-like object to write output 1033 | @param name1 name of the first hex file to show in the diff header 1034 | @param name2 name of the first hex file to show in the diff header 1035 | @param n_context number of context lines in the unidiff output 1036 | """ 1037 | def prepare_lines(ih): 1038 | from cStringIO import StringIO 1039 | sio = StringIO() 1040 | ih.dump(sio) 1041 | dump = sio.getvalue() 1042 | lines = dump.splitlines() 1043 | return lines 1044 | a = prepare_lines(ih1) 1045 | b = prepare_lines(ih2) 1046 | import difflib 1047 | result = list(difflib.unified_diff(a, b, fromfile=name1, tofile=name2, n=n_context, lineterm='')) 1048 | if tofile is None: 1049 | tofile = sys.stdout 1050 | output = '\n'.join(result)+'\n' 1051 | tofile.write(output) 1052 | 1053 | 1054 | class Record(object): 1055 | """Helper methods to build valid ihex records.""" 1056 | 1057 | def _from_bytes(bytes): 1058 | """Takes a list of bytes, computes the checksum, and outputs the entire 1059 | record as a string. bytes should be the hex record without the colon 1060 | or final checksum. 1061 | 1062 | @param bytes list of byte values so far to pack into record. 1063 | @return String representation of one HEX record 1064 | """ 1065 | assert len(bytes) >= 4 1066 | # calculate checksum 1067 | s = (-sum(bytes)) & 0x0FF 1068 | bin = array('B', bytes + [s]) 1069 | return ':' + asstr(hexlify(bin.tostring())).upper() 1070 | _from_bytes = staticmethod(_from_bytes) 1071 | 1072 | def data(offset, bytes): 1073 | """Return Data record. This constructs the full record, including 1074 | the length information, the record type (0x00), the 1075 | checksum, and the offset. 1076 | 1077 | @param offset load offset of first byte. 1078 | @param bytes list of byte values to pack into record. 1079 | 1080 | @return String representation of one HEX record 1081 | """ 1082 | assert 0 <= offset < 65536 1083 | assert 0 < len(bytes) < 256 1084 | b = [len(bytes), (offset>>8)&0x0FF, offset&0x0FF, 0x00] + bytes 1085 | return Record._from_bytes(b) 1086 | data = staticmethod(data) 1087 | 1088 | def eof(): 1089 | """Return End of File record as a string. 1090 | @return String representation of Intel Hex EOF record 1091 | """ 1092 | return ':00000001FF' 1093 | eof = staticmethod(eof) 1094 | 1095 | def extended_segment_address(usba): 1096 | """Return Extended Segment Address Record. 1097 | @param usba Upper Segment Base Address. 1098 | 1099 | @return String representation of Intel Hex USBA record. 1100 | """ 1101 | b = [2, 0, 0, 0x02, (usba>>8)&0x0FF, usba&0x0FF] 1102 | return Record._from_bytes(b) 1103 | extended_segment_address = staticmethod(extended_segment_address) 1104 | 1105 | def start_segment_address(cs, ip): 1106 | """Return Start Segment Address Record. 1107 | @param cs 16-bit value for CS register. 1108 | @param ip 16-bit value for IP register. 1109 | 1110 | @return String representation of Intel Hex SSA record. 1111 | """ 1112 | b = [4, 0, 0, 0x03, (cs>>8)&0x0FF, cs&0x0FF, 1113 | (ip>>8)&0x0FF, ip&0x0FF] 1114 | return Record._from_bytes(b) 1115 | start_segment_address = staticmethod(start_segment_address) 1116 | 1117 | def extended_linear_address(ulba): 1118 | """Return Extended Linear Address Record. 1119 | @param ulba Upper Linear Base Address. 1120 | 1121 | @return String representation of Intel Hex ELA record. 1122 | """ 1123 | b = [2, 0, 0, 0x04, (ulba>>8)&0x0FF, ulba&0x0FF] 1124 | return Record._from_bytes(b) 1125 | extended_linear_address = staticmethod(extended_linear_address) 1126 | 1127 | def start_linear_address(eip): 1128 | """Return Start Linear Address Record. 1129 | @param eip 32-bit linear address for the EIP register. 1130 | 1131 | @return String representation of Intel Hex SLA record. 1132 | """ 1133 | b = [4, 0, 0, 0x05, (eip>>24)&0x0FF, (eip>>16)&0x0FF, 1134 | (eip>>8)&0x0FF, eip&0x0FF] 1135 | return Record._from_bytes(b) 1136 | start_linear_address = staticmethod(start_linear_address) 1137 | 1138 | 1139 | class _BadFileNotation(Exception): 1140 | """Special error class to use with _get_file_and_addr_range.""" 1141 | pass 1142 | 1143 | def _get_file_and_addr_range(s, _support_drive_letter=None): 1144 | """Special method for hexmerge.py script to split file notation 1145 | into 3 parts: (filename, start, end) 1146 | 1147 | @raise _BadFileNotation when string cannot be safely split. 1148 | """ 1149 | if _support_drive_letter is None: 1150 | _support_drive_letter = (os.name == 'nt') 1151 | drive = '' 1152 | if _support_drive_letter: 1153 | if s[1:2] == ':' and s[0].upper() in ''.join([chr(i) for i in range(ord('A'), ord('Z')+1)]): 1154 | drive = s[:2] 1155 | s = s[2:] 1156 | parts = s.split(':') 1157 | n = len(parts) 1158 | if n == 1: 1159 | fname = parts[0] 1160 | fstart = None 1161 | fend = None 1162 | elif n != 3: 1163 | raise _BadFileNotation 1164 | else: 1165 | fname = parts[0] 1166 | def ascii_hex_to_int(ascii): 1167 | if ascii is not None: 1168 | try: 1169 | return int(ascii, 16) 1170 | except ValueError: 1171 | raise _BadFileNotation 1172 | return ascii 1173 | fstart = ascii_hex_to_int(parts[1] or None) 1174 | fend = ascii_hex_to_int(parts[2] or None) 1175 | return drive+fname, fstart, fend 1176 | 1177 | 1178 | ## 1179 | # IntelHex Errors Hierarchy: 1180 | # 1181 | # IntelHexError - basic error 1182 | # HexReaderError - general hex reader error 1183 | # AddressOverlapError - data for the same address overlap 1184 | # HexRecordError - hex record decoder base error 1185 | # RecordLengthError - record has invalid length 1186 | # RecordTypeError - record has invalid type (RECTYP) 1187 | # RecordChecksumError - record checksum mismatch 1188 | # EOFRecordError - invalid EOF record (type 01) 1189 | # ExtendedAddressRecordError - extended address record base error 1190 | # ExtendedSegmentAddressRecordError - invalid extended segment address record (type 02) 1191 | # ExtendedLinearAddressRecordError - invalid extended linear address record (type 04) 1192 | # StartAddressRecordError - start address record base error 1193 | # StartSegmentAddressRecordError - invalid start segment address record (type 03) 1194 | # StartLinearAddressRecordError - invalid start linear address record (type 05) 1195 | # DuplicateStartAddressRecordError - start address record appears twice 1196 | # InvalidStartAddressValueError - invalid value of start addr record 1197 | # _EndOfFile - it's not real error, used internally by hex reader as signal that EOF record found 1198 | # BadAccess16bit - not enough data to read 16 bit value (deprecated, see NotEnoughDataError) 1199 | # NotEnoughDataError - not enough data to read N contiguous bytes 1200 | # EmptyIntelHexError - requested operation cannot be performed with empty object 1201 | 1202 | class IntelHexError(Exception): 1203 | '''Base Exception class for IntelHex module''' 1204 | 1205 | _fmt = 'IntelHex base error' #: format string 1206 | 1207 | def __init__(self, msg=None, **kw): 1208 | """Initialize the Exception with the given message. 1209 | """ 1210 | self.msg = msg 1211 | for key, value in kw.items(): 1212 | setattr(self, key, value) 1213 | 1214 | def __str__(self): 1215 | """Return the message in this Exception.""" 1216 | if self.msg: 1217 | return self.msg 1218 | try: 1219 | return self._fmt % self.__dict__ 1220 | except (NameError, ValueError, KeyError), e: 1221 | return 'Unprintable exception %s: %s' \ 1222 | % (repr(e), str(e)) 1223 | 1224 | class _EndOfFile(IntelHexError): 1225 | """Used for internal needs only.""" 1226 | _fmt = 'EOF record reached -- signal to stop read file' 1227 | 1228 | class HexReaderError(IntelHexError): 1229 | _fmt = 'Hex reader base error' 1230 | 1231 | class AddressOverlapError(HexReaderError): 1232 | _fmt = 'Hex file has data overlap at address 0x%(address)X on line %(line)d' 1233 | 1234 | # class NotAHexFileError was removed in trunk.revno.54 because it's not used 1235 | 1236 | 1237 | class HexRecordError(HexReaderError): 1238 | _fmt = 'Hex file contains invalid record at line %(line)d' 1239 | 1240 | 1241 | class RecordLengthError(HexRecordError): 1242 | _fmt = 'Record at line %(line)d has invalid length' 1243 | 1244 | class RecordTypeError(HexRecordError): 1245 | _fmt = 'Record at line %(line)d has invalid record type' 1246 | 1247 | class RecordChecksumError(HexRecordError): 1248 | _fmt = 'Record at line %(line)d has invalid checksum' 1249 | 1250 | class EOFRecordError(HexRecordError): 1251 | _fmt = 'File has invalid End-of-File record' 1252 | 1253 | 1254 | class ExtendedAddressRecordError(HexRecordError): 1255 | _fmt = 'Base class for extended address exceptions' 1256 | 1257 | class ExtendedSegmentAddressRecordError(ExtendedAddressRecordError): 1258 | _fmt = 'Invalid Extended Segment Address Record at line %(line)d' 1259 | 1260 | class ExtendedLinearAddressRecordError(ExtendedAddressRecordError): 1261 | _fmt = 'Invalid Extended Linear Address Record at line %(line)d' 1262 | 1263 | 1264 | class StartAddressRecordError(HexRecordError): 1265 | _fmt = 'Base class for start address exceptions' 1266 | 1267 | class StartSegmentAddressRecordError(StartAddressRecordError): 1268 | _fmt = 'Invalid Start Segment Address Record at line %(line)d' 1269 | 1270 | class StartLinearAddressRecordError(StartAddressRecordError): 1271 | _fmt = 'Invalid Start Linear Address Record at line %(line)d' 1272 | 1273 | class DuplicateStartAddressRecordError(StartAddressRecordError): 1274 | _fmt = 'Start Address Record appears twice at line %(line)d' 1275 | 1276 | class InvalidStartAddressValueError(StartAddressRecordError): 1277 | _fmt = 'Invalid start address value: %(start_addr)s' 1278 | 1279 | 1280 | class NotEnoughDataError(IntelHexError): 1281 | _fmt = ('Bad access at 0x%(address)X: ' 1282 | 'not enough data to read %(length)d contiguous bytes') 1283 | 1284 | class BadAccess16bit(NotEnoughDataError): 1285 | _fmt = 'Bad access at 0x%(address)X: not enough data to read 16 bit value' 1286 | 1287 | class EmptyIntelHexError(IntelHexError): 1288 | _fmt = "Requested operation cannot be executed with empty object" 1289 | --------------------------------------------------------------------------------