util.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. # greaseweazle/tools/util.py
  2. #
  3. # Greaseweazle control script: Utility functions.
  4. #
  5. # Written & released by Keir Fraser <keir.xen@gmail.com>
  6. #
  7. # This is free and unencumbered software released into the public domain.
  8. # See the file COPYING for more details, or visit <http://unlicense.org>.
  9. import argparse, os, sys, serial, struct, time
  10. import serial.tools.list_ports
  11. from greaseweazle import version
  12. from greaseweazle import error
  13. from greaseweazle import usb as USB
  14. from greaseweazle.image.scp import SCP
  15. from greaseweazle.image.hfe import HFE
  16. from greaseweazle.image.ipf import IPF
  17. class CmdlineHelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
  18. def _get_help_string(self, action):
  19. help = action.help
  20. if '%no_default' in help:
  21. return help.replace('%no_default', '')
  22. if ('%(default)' in help
  23. or action.default is None
  24. or action.default is False
  25. or action.default is argparse.SUPPRESS):
  26. return help
  27. return help + ' (default: %(default)s)'
  28. class ArgumentParser(argparse.ArgumentParser):
  29. def __init__(self, formatter_class=CmdlineHelpFormatter, *args, **kwargs):
  30. return super().__init__(formatter_class=formatter_class,
  31. *args, **kwargs)
  32. def drive_letter(letter):
  33. types = {
  34. 'A': (USB.BusType.IBMPC, 0),
  35. 'B': (USB.BusType.IBMPC, 1),
  36. '0': (USB.BusType.Shugart, 0),
  37. '1': (USB.BusType.Shugart, 1),
  38. '2': (USB.BusType.Shugart, 2)
  39. }
  40. if not letter.upper() in types:
  41. raise argparse.ArgumentTypeError("invalid drive letter: '%s'" % letter)
  42. return types[letter.upper()]
  43. def split_opts(seq):
  44. """Splits a name from its list of options."""
  45. parts = seq.split('::')
  46. name, opts = parts[0], dict()
  47. for x in map(lambda x: x.split(':'), parts[1:]):
  48. for y in x:
  49. try:
  50. opt, val = y.split('=')
  51. except ValueError:
  52. opt, val = y, True
  53. if opt:
  54. opts[opt] = val
  55. return name, opts
  56. def get_image_class(name):
  57. image_types = { '.scp': SCP, '.hfe': HFE, '.ipf': IPF }
  58. _, ext = os.path.splitext(name)
  59. error.check(ext.lower() in image_types,
  60. "%s: Unrecognised file suffix '%s'" % (name, ext))
  61. return image_types[ext.lower()]
  62. def with_drive_selected(fn, usb, args, *_args, **_kwargs):
  63. usb.set_bus_type(args.drive[0])
  64. try:
  65. usb.drive_select(args.drive[1])
  66. usb.drive_motor(args.drive[1], True)
  67. fn(usb, args, *_args, **_kwargs)
  68. except KeyboardInterrupt:
  69. print()
  70. usb.reset()
  71. usb.ser.close()
  72. usb.ser.open()
  73. raise
  74. finally:
  75. usb.drive_motor(args.drive[1], False)
  76. usb.drive_deselect()
  77. def valid_ser_id(ser_id):
  78. return ser_id and ser_id.upper().startswith("GW")
  79. def score_port(x, old_port=None):
  80. score = 0
  81. if x.manufacturer == "Keir Fraser" and x.product == "Greaseweazle":
  82. score = 20
  83. elif x.vid == 0x1209 and x.pid == 0x4d69:
  84. # Our very own properly-assigned PID. Guaranteed to be us.
  85. score = 20
  86. elif x.vid == 0x1209 and x.pid == 0x0001:
  87. # Our old shared Test PID. It's not guaranteed to be us.
  88. score = 10
  89. if score > 0 and valid_ser_id(x.serial_number):
  90. # A valid serial id is a good sign unless this is a reopen, and
  91. # the serials don't match!
  92. if not old_port or not valid_ser_id(old_port.serial_number):
  93. score = 20
  94. elif x.serial_number == old_port.serial_number:
  95. score = 30
  96. else:
  97. score = 0
  98. if old_port and old_port.location:
  99. # If this is a reopen, location field must match. A match is not
  100. # sufficient in itself however, as Windows may supply the same
  101. # location for multiple USB ports (this may be an interaction with
  102. # BitDefender). Hence we do not increase the port's score here.
  103. if not x.location or x.location != old_port.location:
  104. score = 0
  105. return score
  106. def find_port(old_port=None):
  107. best_score, best_port = 0, None
  108. for x in serial.tools.list_ports.comports():
  109. score = score_port(x, old_port)
  110. if score > best_score:
  111. best_score, best_port = score, x
  112. if best_port:
  113. return best_port.device
  114. raise serial.SerialException('Could not auto-probe Greaseweazle device')
  115. def port_info(devname):
  116. for x in serial.tools.list_ports.comports():
  117. if x.device == devname:
  118. return x
  119. return None
  120. def usb_reopen(usb, is_update):
  121. mode = { False: 1, True: 0 }
  122. try:
  123. usb.switch_fw_mode(mode[is_update])
  124. except (serial.SerialException, struct.error):
  125. # Mac and Linux raise SerialException ("... returned no data")
  126. # Win10 pyserial returns a short read which fails struct.unpack
  127. pass
  128. usb.ser.close()
  129. for i in range(10):
  130. time.sleep(0.5)
  131. try:
  132. devicename = find_port(usb.port_info)
  133. new_ser = serial.Serial(devicename)
  134. except serial.SerialException:
  135. # Device not found
  136. pass
  137. else:
  138. new_usb = USB.Unit(new_ser)
  139. new_usb.port_info = port_info(devicename)
  140. return new_usb
  141. raise serial.SerialException('Could not reopen port after mode switch')
  142. def usb_open(devicename, is_update=False, mode_check=True):
  143. if devicename is None:
  144. devicename = find_port()
  145. usb = USB.Unit(serial.Serial(devicename))
  146. usb.port_info = port_info(devicename)
  147. if not mode_check:
  148. return usb
  149. print("** %s v%u.%u [F%u], Host Tools v%u.%u"
  150. % (("Greaseweazle", "Bootloader")[usb.update_mode],
  151. usb.major, usb.minor, usb.hw_model,
  152. version.major, version.minor))
  153. if usb.update_mode and not is_update:
  154. if usb.hw_model == 7 and not usb.update_jumpered:
  155. usb = usb_reopen(usb, is_update)
  156. if not usb.update_mode:
  157. return usb
  158. print("Greaseweazle is in Firmware Update Mode:")
  159. print(" The only available action is \"update\" of main firmware")
  160. if usb.update_jumpered:
  161. print(" Remove the Update Jumper for normal operation")
  162. else:
  163. print(" Main firmware is erased: You *must* perform an update!")
  164. sys.exit(1)
  165. if is_update and not usb.update_mode:
  166. if usb.hw_model == 7:
  167. usb = usb_reopen(usb, is_update)
  168. error.check(usb.update_mode, """\
  169. Greaseweazle F7 did not change to Firmware Update Mode as requested.
  170. If the problem persists, install the Update Jumper (across RX/TX).""")
  171. return usb
  172. print("Greaseweazle is in Normal Mode:")
  173. print(" To \"update\" you must install the Update Jumper")
  174. sys.exit(1)
  175. if not usb.update_mode and usb.update_needed:
  176. print("Firmware is out of date: Require v%u.%u"
  177. % (version.major, version.minor))
  178. if usb.hw_model == 7:
  179. print("Run \"update <update_file>\"")
  180. else:
  181. print("Install the Update Jumper and \"update <update_file>\"")
  182. sys.exit(1)
  183. return usb
  184. # Local variables:
  185. # python-indent: 4
  186. # End: