Statistics
| Branch: | Tag: | Revision:

root / host / utils / usrp2_card_burner.py @ 0df49b75

History | View | Annotate | Download (10 KB)

1
#!/usr/bin/env python
2
#
3
# Copyright 2010-2011 Ettus Research LLC
4
#
5
# This program is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
#
18

    
19
import platform
20
import tempfile
21
import subprocess
22
try:
23
    import urllib.request
24
except ImportError:
25
    import urllib
26
    urllib.request = urllib
27
import optparse
28
import math
29
import os
30
import re
31

    
32
########################################################################
33
# constants
34
########################################################################
35
SECTOR_SIZE = 512                 # bytes
36
MAX_FILE_SIZE =  1 * (2**20)      # maximum number of bytes we'll burn to a slot
37

    
38
FPGA_OFFSET = 0                   # offset in flash to fpga image
39
FIRMWARE_OFFSET = 1 * (2**20)     # offset in flash to firmware image
40

    
41
MAX_SD_CARD_SIZE = 2048e6         # bytes (any bigger is sdhc)
42

    
43
########################################################################
44
# helper functions
45
########################################################################
46
def command(*args):
47
    p = subprocess.Popen(
48
        args,
49
        stdout=subprocess.PIPE,
50
        stderr=subprocess.STDOUT,
51
    )
52
    ret = p.wait()
53
    verbose = p.stdout.read().decode()
54
    if ret != 0: raise Exception(verbose)
55
    return verbose
56

    
57
def get_dd_path():
58
    if platform.system() == 'Windows':
59
        dd_path = os.path.join(os.path.dirname(__file__), 'dd.exe')
60
        if os.path.exists(dd_path): return dd_path
61
        dd_path = os.path.join(tempfile.gettempdir(), 'dd.exe')
62
        if not os.path.exists(dd_path):
63
            print('Downloading dd.exe to %s'%dd_path)
64
            dd_bin = urllib.request.urlopen('http://files.ettus.com/dd.exe').read()
65
            open(dd_path, 'wb').write(dd_bin)
66
        return dd_path
67
    return 'dd'
68

    
69
def int_ceil_div(num, den):
70
    return int(math.ceil(float(num)/float(den)))
71

    
72
def get_tmp_file():
73
    tmp = tempfile.mkstemp()
74
    os.close(tmp[0])
75
    return tmp[1]
76

    
77
########################################################################
78
# list possible devices
79
########################################################################
80
def get_raw_device_hints():
81
    ####################################################################
82
    # Platform Windows: parse the output of dd.exe --list
83
    ####################################################################
84
    if platform.system() == 'Windows':
85
        def extract_info_value(info, key):
86
            return info.split(key)[-1].split()[0]
87
        def get_info_list(output):
88
            in_info = False
89
            for line in output.splitlines():
90
                if line.startswith('\\\\'): in_info = True; info = ''
91
                elif in_info and not line.strip(): in_info = False; yield info
92
                if in_info: info += '\n'+line.strip()
93
        def is_info_valid(info):
94
            try:
95
                if 'link to' not in info: return False
96
                #handles two spellings of remov(e)able:
97
                if 'remov' not in info.lower(): return False
98
                if 'size is' in info and int(extract_info_value(info, 'size is')) > MAX_SD_CARD_SIZE: return False
99
            except: return False
100
            return True
101
        def extract_info_name(info):
102
            for key in ('Mounted on', 'link to'):
103
                if key in info: return extract_info_value(info, key)
104
            return info.splitlines()[0].strip()
105

    
106
        return sorted(set(map(extract_info_name, list(filter(is_info_valid, get_info_list(command(get_dd_path(), '--list')))))))
107

    
108
    ####################################################################
109
    # Platform Linux: parse procfs /proc/partitions
110
    ####################################################################
111
    if platform.system() == 'Linux':
112
        devs = list()
113
        for line in command('cat', '/proc/partitions').splitlines():
114
            try:
115
                major, minor, blocks, name = line.split()
116
                if not name[-1].isdigit() and int(minor) == 0: continue
117
                if int(blocks)*1024 > MAX_SD_CARD_SIZE: continue
118
            except: continue
119
            devs.append(os.path.join('/dev', name))
120

    
121
        return sorted(set(devs))
122

    
123
    ####################################################################
124
    # Platform Mac OS X: parse diskutil list and info commands
125
    ####################################################################
126
    if platform.system() == 'Darwin':
127
        devs = [d.split()[0] for d in [l for l in command('diskutil', 'list').splitlines() if l.startswith('/dev')]]
128
        def output_to_info(output):
129
            return dict([list(map(lambda x: x.strip(), pair.lower().split(':'))) for pair in [l for l in output.splitlines() if ':' in l]])
130
        def is_dev_valid(dev):
131
            info = output_to_info(command('diskutil', 'info', dev))
132
            try:
133
                if 'internal' in info and info['internal'] == 'yes': return False
134
                if 'ejectable' in info and info['ejectable'] == 'no': return False
135
                if 'total size' in info:
136
                    size_match = re.match('^.*\((\d+)\s*bytes\).*$', info['total size'])
137
                    if size_match and int(size_match.groups()[0]) > MAX_SD_CARD_SIZE: return False
138
            except: return False
139
            return True
140

    
141
        return sorted(set(filter(is_dev_valid, devs)))
142

    
143
    ####################################################################
144
    # Platform Others:
145
    ####################################################################
146
    return ()
147

    
148
########################################################################
149
# write and verify with dd
150
########################################################################
151
def verify_image(image_file, device_file, offset):
152
    #create a temporary file to store the readback image
153
    tmp_file = get_tmp_file()
154

    
155
    #read the image data
156
    img_data = open(image_file, 'rb').read()
157
    count = int_ceil_div(len(img_data), SECTOR_SIZE)
158

    
159
    #execute a dd subprocess
160
    verbose = command(
161
        get_dd_path(),
162
        "of=%s"%tmp_file,
163
        "if=%s"%device_file,
164
        "skip=%d"%(offset/SECTOR_SIZE),
165
        "bs=%d"%SECTOR_SIZE,
166
        "count=%d"%count,
167
    )
168

    
169
    #verfy the data
170
    tmp_data = open(tmp_file, 'rb').read(len(img_data))
171
    if img_data != tmp_data: return 'Verification Failed:\n%s'%verbose
172
    return 'Verification Passed:\n%s'%verbose
173

    
174
def write_image(image_file, device_file, offset):
175
    #create a temporary file to store the padded image
176
    tmp_file = get_tmp_file()
177

    
178
    #write the padded image data
179
    img_data = open(image_file, 'rb').read()
180
    count = int_ceil_div(len(img_data), SECTOR_SIZE)
181
    pad_len = SECTOR_SIZE*count - len(img_data)
182
    padding = bytes(b'\x00')*pad_len #zero-padding
183
    open(tmp_file, 'wb').write(img_data + padding)
184

    
185
    #execute a dd subprocess
186
    verbose = command(
187
        get_dd_path(),
188
        "if=%s"%tmp_file,
189
        "of=%s"%device_file,
190
        "seek=%d"%(offset/SECTOR_SIZE),
191
        "bs=%d"%SECTOR_SIZE,
192
        "count=%d"%count,
193
    )
194

    
195
    try: #exec the sync command (only works on linux)
196
        if platform.system() == 'Linux': command('sync')
197
    except: pass
198

    
199
    return verbose
200

    
201
def write_and_verify(image_file, device_file, offset):
202
    if os.path.getsize(image_file) > MAX_FILE_SIZE:
203
        raise Exception('Image file larger than %d bytes!'%MAX_FILE_SIZE)
204
    return '%s\n%s'%(
205
        write_image(
206
            image_file=image_file,
207
            device_file=device_file,
208
            offset=offset,
209
        ), verify_image(
210
            image_file=image_file,
211
            device_file=device_file,
212
            offset=offset,
213
        ),
214
    )
215

    
216
def burn_sd_card(dev, fw, fpga):
217
    verbose = ''
218
    if fw: verbose += 'Burn firmware image:\n%s\n'%write_and_verify(
219
        image_file=fw, device_file=dev, offset=FIRMWARE_OFFSET
220
    )
221
    if fpga: verbose += 'Burn fpga image:\n%s\n'%write_and_verify(
222
        image_file=fpga, device_file=dev, offset=FPGA_OFFSET
223
    )
224
    return verbose
225

    
226
########################################################################
227
# command line options
228
########################################################################
229
def get_options():
230
    parser = optparse.OptionParser()
231
    parser.add_option("--dev",  type="string",       help="raw device path",                default='')
232
    parser.add_option("--fw",   type="string",       help="firmware image path (optional)", default='')
233
    parser.add_option("--fpga", type="string",       help="fpga image path (optional)",     default='')
234
    parser.add_option("--list", action="store_true", help="list possible raw devices",      default=False)
235
    parser.add_option("--force", action="store_true", help="override safety check",         default=False)
236
    (options, args) = parser.parse_args()
237

    
238
    return options
239

    
240
########################################################################
241
# main
242
########################################################################
243
if __name__=='__main__':
244
    options = get_options()
245
    device_hints = get_raw_device_hints()
246
    show_listing = options.list
247

    
248
    if not show_listing and not options.force and options.dev and options.dev not in device_hints:
249
        print('The device "%s" was not in the list of possible raw devices.'%options.dev)
250
        print('The card burner application will now exit without burning your card.')
251
        print('To override this safety check, specify the --force option.\n')
252
        show_listing = True
253

    
254
    if show_listing:
255
        print('Possible raw devices:')
256
        print('  ' + '\n  '.join(device_hints))
257
        exit()
258

    
259
    if not options.dev: raise Exception('no raw device path specified')
260
    print(burn_sd_card(dev=options.dev, fw=options.fw, fpga=options.fpga))