1#!/usr/bin/python2.5
2#
3# Copyright 2009 Olivier Gillet.
4#
5# Author: Olivier Gillet (ol.gillet@gmail.com)
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
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#
20# Hex2SysEx utility
21
22"""Hex2SysEx utility.
23
24usage:
25  python hex2sysex.py \
26    [--page_size 128] \
27    [--delay 250] \
28    [--syx] \
29    [--output_file path_to/firmware.mid] \
30    path_to/firmware.hex
31"""
32
33import logging
34import optparse
35import os
36import struct
37import sys
38
39# Allows the code to be run from the project root directory
40sys.path.append('.')
41
42from tools.midi import midifile
43from tools.hexfile import hexfile
44
45
46def CreateMidifile(
47    input_file_name,
48    data,
49    output_file,
50    options):
51  size = len(data)
52  page_size = options.page_size
53  delay = options.delay
54  _, input_file_name = os.path.split(input_file_name)
55  comments = [
56      'Warning: contains OS data!',
57      'Created from %(input_file_name)s' % locals(),
58      'Size: %(size)d' % locals(),
59      'Page size: %(page_size)d' % locals(),
60      'Delay: %(delay)d ms' % locals()]
61  m = midifile.Writer()
62  if options.write_comments:
63    for comment in comments:
64      m.AddTrack().AddEvent(0, midifile.TextEvent(comment))
65  t = m.AddTrack()
66  t.AddEvent(0, midifile.TempoEvent(120.0))
67  page_size *= 2  # Converts from words to bytes
68  # The first SysEx block must not start at 0! Sequencers like Logic play the
69  # first SysEx block everytime stop/play is pressed.
70  time = 1
71  syx_data = []
72  for i in xrange(0, size, page_size):
73    block = ''.join(map(chr, data[i:i+page_size]))
74    padding = page_size - len(block)
75    block += '\x00' * padding
76    mfr_id = options.manufacturer_id if not \
77        options.force_obsolete_manufacturer_id else '\x00\x20\x77'
78    event = midifile.SysExEvent(
79        mfr_id,
80        struct.pack('>h', options.device_id),
81        options.update_command + midifile.Nibblize(block))
82    t.AddEvent(time, event)
83    syx_data.append(event.raw_message)
84    # ms -> s -> beats -> ticks
85    time += int(delay / 1000.0 / 0.5 * 96)
86  event = midifile.SysExEvent(
87      mfr_id,
88      struct.pack('>h', options.device_id),
89      options.reset_command)
90  t.AddEvent(time, event)
91  syx_data.append(event.raw_message)
92
93  f = file(output_file, 'wb')
94  if options.syx:
95    f.write(''.join(syx_data))
96  else:
97    m.Write(f, format=1)
98  f.close()
99
100
101if __name__ == '__main__':
102  parser = optparse.OptionParser()
103  parser.add_option(
104      '-p',
105      '--page_size',
106      dest='page_size',
107      type='int',
108      default=128,
109      help='Flash page size in words')
110  parser.add_option(
111      '-d',
112      '--delay',
113      dest='delay',
114      type='int',
115      default=250,
116      help='Delay between pages in milliseconds')
117  parser.add_option(
118      '-o',
119      '--output_file',
120      dest='output_file',
121      default=None,
122      help='Write output file to FILE',
123      metavar='FILE')
124  parser.add_option(
125      '-m',
126      '--manufacturer_id',
127      dest='manufacturer_id',
128      default='\x00\x21\x02',
129      help='Manufacturer ID to use in SysEx message')
130  parser.add_option(
131      '-b',
132      '--obsolete_manufacturer_id',
133      dest='force_obsolete_manufacturer_id',
134      default=False,
135      action='store_true',
136      help='Force the use of the manufacturer ID used in early products')
137  parser.add_option(
138      '-v',
139      '--device_id',
140      dest='device_id',
141      type='int',
142      default=2,
143      help='Device ID to use in SysEx message')
144  parser.add_option(
145      '-u',
146      '--update_command',
147      dest='update_command',
148      default='\x7e\x00',
149      help='OS update SysEx command')
150  parser.add_option(
151      '-r',
152      '--reset_command',
153      dest='reset_command',
154      default='\x7f\x00',
155      help='Post-OS update reset SysEx command')
156  parser.add_option(
157      '-s',
158      '--syx',
159      dest='syx',
160      action='store_true',
161      default=False,
162      help='Produces a .syx file instead of a MIDI file')
163  parser.add_option(
164      '-c',
165      '--comments',
166      dest='write_comments',
167      action='store_true',
168      default=False,
169      help='Store additional technical gibberish')
170
171  options, args = parser.parse_args()
172  if len(args) != 1:
173    logging.fatal('Specify one, and only one firmware .hex file!')
174    sys.exit(1)
175
176  if args[0].endswith('.bin'):
177    data = map(ord, file(args[0], 'rb').read())
178  else:
179    data = hexfile.LoadHexFile(file(args[0]))
180    if not data:
181      logging.fatal('Error while loading .hex file')
182      sys.exit(2)
183
184  output_file = options.output_file
185  if not output_file:
186    if '.hex' in args[0]:
187      output_file = args[0].replace('.hex', '.mid')
188    else:
189      output_file = args[0] + '.mid'
190
191  CreateMidifile(
192      args[0],
193      data,
194      output_file,
195      options)
196