util.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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, re
  10. import importlib
  11. import serial.tools.list_ports
  12. from greaseweazle import version
  13. from greaseweazle import error
  14. from greaseweazle import usb as USB
  15. class CmdlineHelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
  16. def _get_help_string(self, action):
  17. help = action.help
  18. if '%no_default' in help:
  19. return help.replace('%no_default', '')
  20. if ('%(default)' in help
  21. or action.default is None
  22. or action.default is False
  23. or action.default is argparse.SUPPRESS):
  24. return help
  25. return help + ' (default: %(default)s)'
  26. class ArgumentParser(argparse.ArgumentParser):
  27. def __init__(self, formatter_class=CmdlineHelpFormatter, *args, **kwargs):
  28. return super().__init__(formatter_class=formatter_class,
  29. *args, **kwargs)
  30. def drive_letter(letter):
  31. types = {
  32. 'A': (USB.BusType.IBMPC, 0),
  33. 'B': (USB.BusType.IBMPC, 1),
  34. '0': (USB.BusType.Shugart, 0),
  35. '1': (USB.BusType.Shugart, 1),
  36. '2': (USB.BusType.Shugart, 2)
  37. }
  38. if not letter.upper() in types:
  39. raise argparse.ArgumentTypeError("invalid drive letter: '%s'" % letter)
  40. return types[letter.upper()]
  41. def range_str(s, e):
  42. str = "%d" % s
  43. if s != e: str += "-%d" % e
  44. return str
  45. class TrackSet:
  46. class TrackIter:
  47. def __init__(self, ts):
  48. l = []
  49. for c in range(ts.cyl[0], ts.cyl[1]+1):
  50. for s in range(ts.side[0], ts.side[1]+1):
  51. pc = (c, c*2)[ts.double_step]
  52. pc += ts.side_off[0] if s == ts.side[0] else ts.side_off[1]
  53. l.append((pc, s, c))
  54. l.sort()
  55. self.l = iter(l)
  56. def __next__(self):
  57. self.physical_cyl, self.side, self.cyl = next(self.l)
  58. return self
  59. def __init__(self):
  60. self.cyl = (0,79)
  61. self.side = (0,1)
  62. self.double_step = False
  63. def __str__(self):
  64. a, b = self.cyl
  65. s = 'c=%d' % a
  66. if a != b: s += '-%d' % b
  67. if self.double_step: s += 'x2'
  68. a, c = self.side
  69. b, d = self.side_off
  70. s += ':s=%d' % a
  71. if b != 0: s += '[%s%d]' % ('+' if b >= 0 else '', b)
  72. if a != c:
  73. s += '-%d' % c
  74. if d != 0: s += '[%s%d]' % ('+' if d >= 0 else '', d)
  75. return s
  76. def __iter__(self):
  77. return self.TrackIter(self)
  78. def trackset(tracks):
  79. ts = TrackSet()
  80. for x in tracks.split(':'):
  81. k,v = x.split('=')
  82. if k == 'c':
  83. m = re.match('(\d+)(-(\d+))?(x2)?$', v)
  84. if m is None: raise ValueError()
  85. if m.group(3) is None:
  86. ts.cyl = int(m.group(1)), int(m.group(1))
  87. else:
  88. ts.cyl = int(m.group(1)), int(m.group(3))
  89. if m.group(4) is not None:
  90. ts.double_step = True
  91. elif k == 's':
  92. m = re.match('(?P<s>\d+)(\[(?P<s_off>[-+]\d)\])?'
  93. '(-(?P<e>\d+)(\[(?P<e_off>[-+]\d)\])?)?$', v)
  94. if m is None: raise ValueError()
  95. s, e = m.group('s'), m.group('e')
  96. ts.side = int(s), int(e if e is not None else s)
  97. s, e = m.group('s_off'), m.group('e_off')
  98. s = int(s) if s is not None else 0
  99. e = int(e) if e is not None else 0
  100. ts.side_off = s,e
  101. return ts
  102. def split_opts(seq):
  103. """Splits a name from its list of options."""
  104. parts = seq.split('::')
  105. name, opts = parts[0], dict()
  106. for x in map(lambda x: x.split(':'), parts[1:]):
  107. for y in x:
  108. try:
  109. opt, val = y.split('=')
  110. except ValueError:
  111. opt, val = y, True
  112. if opt:
  113. opts[opt] = val
  114. return name, opts
  115. def get_image_class(name):
  116. image_types = { '.adf': 'ADF',
  117. '.scp': 'SCP',
  118. '.hfe': 'HFE',
  119. '.ipf': 'IPF',
  120. '.raw': 'KryoFlux' }
  121. if os.path.isdir(name):
  122. typename = 'KryoFlux'
  123. else:
  124. _, ext = os.path.splitext(name)
  125. error.check(ext.lower() in image_types,
  126. "%s: Unrecognised file suffix '%s'" % (name, ext))
  127. typename = image_types[ext.lower()]
  128. mod = importlib.import_module('greaseweazle.image.' + typename.lower())
  129. return mod.__dict__[typename]
  130. def with_drive_selected(fn, usb, args, *_args, **_kwargs):
  131. usb.set_bus_type(args.drive[0])
  132. try:
  133. usb.drive_select(args.drive[1])
  134. usb.drive_motor(args.drive[1], _kwargs.pop('motor', True))
  135. fn(usb, args, *_args, **_kwargs)
  136. except KeyboardInterrupt:
  137. print()
  138. usb.reset()
  139. usb.ser.close()
  140. usb.ser.open()
  141. raise
  142. finally:
  143. usb.drive_motor(args.drive[1], False)
  144. usb.drive_deselect()
  145. def valid_ser_id(ser_id):
  146. return ser_id and ser_id.upper().startswith("GW")
  147. def score_port(x, old_port=None):
  148. score = 0
  149. if x.manufacturer == "Keir Fraser" and x.product == "Greaseweazle":
  150. score = 20
  151. elif x.vid == 0x1209 and x.pid == 0x4d69:
  152. # Our very own properly-assigned PID. Guaranteed to be us.
  153. score = 20
  154. elif x.vid == 0x1209 and x.pid == 0x0001:
  155. # Our old shared Test PID. It's not guaranteed to be us.
  156. score = 10
  157. if score > 0 and valid_ser_id(x.serial_number):
  158. # A valid serial id is a good sign unless this is a reopen, and
  159. # the serials don't match!
  160. if not old_port or not valid_ser_id(old_port.serial_number):
  161. score = 20
  162. elif x.serial_number == old_port.serial_number:
  163. score = 30
  164. else:
  165. score = 0
  166. if old_port and old_port.location:
  167. # If this is a reopen, location field must match. A match is not
  168. # sufficient in itself however, as Windows may supply the same
  169. # location for multiple USB ports (this may be an interaction with
  170. # BitDefender). Hence we do not increase the port's score here.
  171. if not x.location or x.location != old_port.location:
  172. score = 0
  173. return score
  174. def find_port(old_port=None):
  175. best_score, best_port = 0, None
  176. for x in serial.tools.list_ports.comports():
  177. score = score_port(x, old_port)
  178. if score > best_score:
  179. best_score, best_port = score, x
  180. if best_port:
  181. return best_port.device
  182. raise serial.SerialException('Cannot find the Greaseweazle device')
  183. def port_info(devname):
  184. for x in serial.tools.list_ports.comports():
  185. if x.device == devname:
  186. return x
  187. return None
  188. def usb_reopen(usb, is_update):
  189. mode = { False: 1, True: 0 }
  190. try:
  191. usb.switch_fw_mode(mode[is_update])
  192. except (serial.SerialException, struct.error):
  193. # Mac and Linux raise SerialException ("... returned no data")
  194. # Win10 pyserial returns a short read which fails struct.unpack
  195. pass
  196. usb.ser.close()
  197. for i in range(10):
  198. time.sleep(0.5)
  199. try:
  200. devicename = find_port(usb.port_info)
  201. new_ser = serial.Serial(devicename)
  202. except serial.SerialException:
  203. # Device not found
  204. pass
  205. else:
  206. new_usb = USB.Unit(new_ser)
  207. new_usb.port_info = port_info(devicename)
  208. return new_usb
  209. raise serial.SerialException('Could not reopen port after mode switch')
  210. def usb_open(devicename, is_update=False, mode_check=True):
  211. if devicename is None:
  212. devicename = find_port()
  213. usb = USB.Unit(serial.Serial(devicename))
  214. usb.port_info = port_info(devicename)
  215. if not mode_check:
  216. return usb
  217. if usb.update_mode and not is_update:
  218. if usb.hw_model == 7 and not usb.update_jumpered:
  219. usb = usb_reopen(usb, is_update)
  220. if not usb.update_mode:
  221. return usb
  222. print("Greaseweazle is in Firmware Update Mode:")
  223. print(" The only available action is \"update\" of main firmware")
  224. if usb.update_jumpered:
  225. print(" Remove the Update Jumper for normal operation")
  226. else:
  227. print(" Main firmware is erased: You *must* perform an update!")
  228. sys.exit(1)
  229. if is_update and not usb.update_mode:
  230. if usb.hw_model == 7:
  231. usb = usb_reopen(usb, is_update)
  232. error.check(usb.update_mode, """\
  233. Greaseweazle F7 did not change to Firmware Update Mode as requested.
  234. If the problem persists, install the Update Jumper (across RX/TX).""")
  235. return usb
  236. print("Greaseweazle is in Normal Mode:")
  237. print(" To \"update\" you must install the Update Jumper")
  238. sys.exit(1)
  239. if not usb.update_mode and usb.update_needed:
  240. print("Firmware is out of date: Require v%u.%u"
  241. % (version.major, version.minor))
  242. if usb.hw_model == 7:
  243. print("Run \"update <update_file>\"")
  244. else:
  245. print("Install the Update Jumper and \"update <update_file>\"")
  246. sys.exit(1)
  247. return usb
  248. # Local variables:
  249. # python-indent: 4
  250. # End: