Compare commits
17 commits
master
...
add-apple-
| Author | SHA1 | Date | |
|---|---|---|---|
| b1259957f9 | |||
| 2e78af33e7 | |||
|
|
cd1a3ccf53 | ||
|
|
fd19abf153 | ||
|
|
3050beae64 | ||
|
|
afb280fc64 | ||
|
|
ead5d8e727 | ||
|
|
b1e302d312 | ||
|
|
f88f09cca9 | ||
|
|
cadb6f1a64 | ||
|
|
abad15d2d7 | ||
|
|
a215b53b98 | ||
|
|
093ecd1efa | ||
|
|
a7c640fe16 | ||
| 083bf7af23 | |||
|
|
88533b5584 | ||
|
|
2f66b9da7a |
18 changed files with 139 additions and 56 deletions
2
Makefile
2
Makefile
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
export MAJOR := 0
|
||||
export MINOR := 38
|
||||
export MINOR := 39
|
||||
|
||||
include Rules.mk
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
** Keir Fraser <keir.xen@gmail.com>
|
||||
************************************
|
||||
|
||||
** v0.39 - 27 February 2022
|
||||
- Fix crashes in the USB.seek error path
|
||||
- Support Atari 90kB format (atari.90)
|
||||
|
||||
** v0.38 - 28 January 2022
|
||||
- gw rpm: Print 3 decimal places, and print summary stats
|
||||
- gw read,convert: Rename --rpm to --adjust-speed
|
||||
|
|
|
|||
|
|
@ -151,6 +151,16 @@ class Format_IBM_1200(Format):
|
|||
self.default_revs = m.default_revs
|
||||
super().__init__()
|
||||
|
||||
class Format_Atari_90(Format):
|
||||
img_compatible = True
|
||||
default_trackset = 'c=0-39:h=0:step=2'
|
||||
max_trackset = 'c=0-41:h=0:step=2'
|
||||
def __init__(self):
|
||||
import greaseweazle.codec.ibm.fm as m
|
||||
self.fmt = m.Atari_90
|
||||
self.default_revs = m.default_revs
|
||||
super().__init__()
|
||||
|
||||
class Format_AtariST_360(Format):
|
||||
img_compatible = True
|
||||
default_trackset = 'c=0-79:h=0'
|
||||
|
|
@ -226,6 +236,7 @@ formats = OrderedDict({
|
|||
'acorn.adfs.1600': Format_Acorn_ADFS_1600,
|
||||
'amiga.amigados': Format_Amiga_AmigaDOS_DD,
|
||||
'amiga.amigados_hd': Format_Amiga_AmigaDOS_HD,
|
||||
'atari.90': Format_Atari_90,
|
||||
'atarist.360': Format_AtariST_360,
|
||||
'atarist.400': Format_AtariST_400,
|
||||
'atarist.440': Format_AtariST_440,
|
||||
|
|
|
|||
|
|
@ -320,6 +320,16 @@ class Acorn_DFS(IBM_FM_Predefined):
|
|||
sz = 1
|
||||
cskew = 3
|
||||
|
||||
class Atari_90(IBM_FM_Predefined):
|
||||
time_per_rev = 0.2
|
||||
clock = 4e-6
|
||||
|
||||
gap_1 = 6
|
||||
gap_3 = 17
|
||||
nsec = 18
|
||||
id0 = 0
|
||||
sz = 0
|
||||
cskew = 3
|
||||
|
||||
encode_list = []
|
||||
for x in range(256):
|
||||
|
|
|
|||
|
|
@ -33,11 +33,14 @@ class OOB:
|
|||
class KryoFlux(Image):
|
||||
|
||||
def __init__(self, name):
|
||||
if os.path.isdir(name):
|
||||
self.basename = os.path.join(name, '')
|
||||
else:
|
||||
m = re.search("(\d{2}.[01])?.raw$", name)
|
||||
self.basename = name[:m.start()]
|
||||
m = re.search("\d{2}.[01].raw$", name, flags=re.IGNORECASE)
|
||||
error.check(
|
||||
m is not None,
|
||||
'''\
|
||||
Bad Kryoflux image name pattern '%s'
|
||||
Name pattern must be path/to/nameNN.N.raw (N is a digit)'''
|
||||
% name)
|
||||
self.basename = name[:m.start()]
|
||||
|
||||
|
||||
@classmethod
|
||||
|
|
@ -48,6 +51,9 @@ class KryoFlux(Image):
|
|||
|
||||
@classmethod
|
||||
def from_file(cls, name):
|
||||
# Check that the specified raw file actually exists.
|
||||
with open(name, 'rb') as _:
|
||||
pass
|
||||
return cls(name)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,9 @@ class SCP(Image):
|
|||
# b'EXTS', length, <length bytes: Extension Area>
|
||||
# Extension Area contains consecutive chunks of the form:
|
||||
# ID, length, <length bytes: ID-specific data>
|
||||
ext_sig, ext_len = struct.unpack('<4sI', dat[0x2b0:0x2b8])
|
||||
ext_sig, ext_len = None, 0
|
||||
if len(dat) >= 0x2b8:
|
||||
ext_sig, ext_len = struct.unpack('<4sI', dat[0x2b0:0x2b8])
|
||||
min_tdh = min(filter(lambda x: x != 0, trk_offs), default=0)
|
||||
if ext_sig == b'EXTS' and 0x2b8 + ext_len <= min_tdh:
|
||||
pos, end = 0x2b8, 0x2b8 + ext_len
|
||||
|
|
@ -263,8 +265,8 @@ class SCP(Image):
|
|||
if not self.nr_revs:
|
||||
self.nr_revs = nr_revs
|
||||
else:
|
||||
assert self.nr_revs == nr_revs
|
||||
|
||||
self.nr_revs = min(self.nr_revs, nr_revs)
|
||||
|
||||
factor = SCP.sample_freq / flux.sample_freq
|
||||
|
||||
tdh, dat = bytearray(), bytearray()
|
||||
|
|
@ -361,11 +363,12 @@ class SCP(Image):
|
|||
flags = 2 # 96TPI
|
||||
if self.index_cued:
|
||||
flags |= 1 # Index-Cued
|
||||
nr_revs = self.nr_revs if self.nr_revs is not None else 0
|
||||
header = struct.pack("<3s9BI",
|
||||
b"SCP", # Signature
|
||||
0, # Version
|
||||
self.opts.disktype,
|
||||
self.nr_revs, 0, ntracks-1,
|
||||
nr_revs, 0, ntracks-1,
|
||||
flags,
|
||||
0, # 16-bit cell width
|
||||
single_sided,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def main(argv):
|
|||
parser = util.ArgumentParser(usage='%(prog)s [options]')
|
||||
parser.add_argument("--device", help="device name (COM/serial port)")
|
||||
parser.add_argument("--drive", type=util.drive_letter, default='A',
|
||||
help="drive to write (A,B,0,1,2)")
|
||||
help="drive to read (A,B,0,1,2,APPLE2,APPLE2_QUARTERTRACK)")
|
||||
parser.add_argument("--cyls", type=int, default=80, metavar="N",
|
||||
help="number of drive cylinders")
|
||||
parser.add_argument("--passes", type=int, default=3, metavar="N",
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def main(argv):
|
|||
epilog=epilog)
|
||||
parser.add_argument("--device", help="device name (COM/serial port)")
|
||||
parser.add_argument("--drive", type=util.drive_letter, default='A',
|
||||
help="drive to write (A,B,0,1,2)")
|
||||
help="drive to read (A,B,0,1,2,APPLE2,APPLE2_QUARTERTRACK)")
|
||||
parser.add_argument("--tracks", type=util.TrackSet, metavar="TSPEC",
|
||||
help="which tracks to erase")
|
||||
parser.add_argument("--hfreq", action="store_true",
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ def main(argv):
|
|||
|
||||
fwver = 'v%d.%d' % (usb.major, usb.minor)
|
||||
if usb.update_mode:
|
||||
fwver += ' (Update Bootloader)'
|
||||
fwver += ' (Bootloader)'
|
||||
print_info_line('Firmware', fwver, tab=2)
|
||||
|
||||
print_info_line('Serial', port.serial_number if port.serial_number
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ def pin_get(argv):
|
|||
parser = util.ArgumentParser(usage='%(prog)s [options] pin')
|
||||
parser.add_argument("--device", help="device name (COM/serial port)")
|
||||
parser.add_argument("--drive", type=util.drive_letter, default='A',
|
||||
help="drive to read (A,B,0,1,2)")
|
||||
help="drive to read (A,B,0,1,2,APPLE2,APPLE2_QUARTERTRACK)")
|
||||
parser.add_argument("pin", type=int, help="pin number")
|
||||
parser.description = description
|
||||
parser.prog += ' pin get'
|
||||
|
|
|
|||
|
|
@ -130,12 +130,16 @@ def read_to_image(usb, args, image, decoder=None):
|
|||
args.drive_ticks_per_rev = args.fake_index * usb.sample_freq
|
||||
|
||||
if isinstance(args.revs, float):
|
||||
# Measure drive RPM.
|
||||
# We will adjust the flux intervals per track to allow for this.
|
||||
if args.drive_ticks_per_rev is None:
|
||||
args.drive_ticks_per_rev = usb.read_track(2).ticks_per_rev
|
||||
args.ticks = int(args.drive_ticks_per_rev * args.revs)
|
||||
args.revs = 2
|
||||
if args.raw:
|
||||
# If dumping raw flux we want full index-to-index revolutions.
|
||||
args.revs = 2
|
||||
else:
|
||||
# Measure drive RPM.
|
||||
# We will adjust the flux intervals per track to allow for this.
|
||||
if args.drive_ticks_per_rev is None:
|
||||
args.drive_ticks_per_rev = usb.read_track(2).ticks_per_rev
|
||||
args.ticks = int(args.drive_ticks_per_rev * args.revs)
|
||||
args.revs = 2
|
||||
|
||||
summary = dict()
|
||||
|
||||
|
|
@ -158,7 +162,7 @@ def main(argv):
|
|||
epilog=epilog)
|
||||
parser.add_argument("--device", help="device name (COM/serial port)")
|
||||
parser.add_argument("--drive", type=util.drive_letter, default='A',
|
||||
help="drive to read (A,B,0,1,2)")
|
||||
help="drive to read (A,B,0,1,2,APPLE2,APPLE2_QUARTERTRACK)")
|
||||
parser.add_argument("--format", help="disk format (output is converted unless --raw)")
|
||||
parser.add_argument("--revs", type=int, metavar="N",
|
||||
help="number of revolutions to read per track")
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ def main(argv):
|
|||
parser = util.ArgumentParser(usage='%(prog)s [options]')
|
||||
parser.add_argument("--device", help="greaseweazle device name")
|
||||
parser.add_argument("--drive", type=util.drive_letter, default='A',
|
||||
help="drive to read (A,B,0,1,2)")
|
||||
help="drive to read (A,B,0,1,2,APPLE2,APPLE2_QUARTERTRACK)")
|
||||
parser.add_argument("--nr", type=int, default=1, metavar="N",
|
||||
help="number of iterations")
|
||||
parser.description = description
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def main(argv):
|
|||
parser = util.ArgumentParser(usage='%(prog)s [options] cylinder')
|
||||
parser.add_argument("--device", help="device name (COM/serial port)")
|
||||
parser.add_argument("--drive", type=util.drive_letter, default='A',
|
||||
help="drive to read (A,B,0,1,2)")
|
||||
help="drive to read (A,B,0,1,2,APPLE2,APPLE2_QUARTERTRACK)")
|
||||
parser.add_argument("--force", action="store_true",
|
||||
help="allow extreme cylinders with no prompt")
|
||||
parser.add_argument("--motor-on", action="store_true",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
description = "Update the Greaseweazle device firmware to current version."
|
||||
|
||||
import requests, zipfile, io, re
|
||||
import sys, serial, struct, os
|
||||
import sys, serial, struct, os, textwrap
|
||||
import crcmod.predefined
|
||||
|
||||
from greaseweazle.tools import util
|
||||
|
|
@ -18,6 +18,9 @@ from greaseweazle import error
|
|||
from greaseweazle import version
|
||||
from greaseweazle import usb as USB
|
||||
|
||||
class SkipUpdate(Exception):
|
||||
pass
|
||||
|
||||
def update_firmware(usb, dat, args):
|
||||
'''Updates the device firmware using the specified Update File.'''
|
||||
|
||||
|
|
@ -30,7 +33,7 @@ def update_firmware(usb, dat, args):
|
|||
return
|
||||
print("Done.")
|
||||
else:
|
||||
ack = usb.update_firmware(dat)
|
||||
ack = usb.update_main_firmware(dat)
|
||||
if ack != 0:
|
||||
print("** UPDATE FAILED: Please retry!")
|
||||
return
|
||||
|
|
@ -133,9 +136,10 @@ def main(argv):
|
|||
if usb.version >= dat_version:
|
||||
if usb.update_mode and usb.can_mode_switch:
|
||||
usb = util.usb_reopen(usb, is_update=False)
|
||||
raise error.Fatal('Device is running v%d.%d (>= v%d.%d). '
|
||||
'Use --force to update anyway.'
|
||||
% (usb.version + dat_version))
|
||||
raise SkipUpdate(
|
||||
'''\
|
||||
Device is already running v%d.%d.
|
||||
Use --force to update anyway.''' % usb.version)
|
||||
usb = util.usb_mode_check(usb, is_update=not args.bootloader)
|
||||
update_firmware(usb, dat, args)
|
||||
if usb.update_mode and usb.can_mode_switch:
|
||||
|
|
@ -151,6 +155,9 @@ def main(argv):
|
|||
"(insufficient Flash memory)")
|
||||
else:
|
||||
print("Command Failed: %s" % err)
|
||||
except SkipUpdate as exc:
|
||||
print("** SKIPPING UPDATE:")
|
||||
print(textwrap.dedent(str(exc)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ TSPEC: Colon-separated list of:
|
|||
h=SET :: Set of heads (sides) to access
|
||||
step=[0-9] :: # physical head steps between cylinders
|
||||
hswap :: Swap physical drive heads
|
||||
h[01].off=[+-][0-9] :: Physical cylkinder offsets per head
|
||||
h[01].off=[+-][0-9] :: Physical cylinder offsets per head
|
||||
SET is a comma-separated list of integers and integer ranges
|
||||
eg. 'c=0-7,9-12:h=0-1'
|
||||
e.g. 'c=0-7,9-12:h=0-1'
|
||||
"""
|
||||
|
||||
# Returns time period in seconds (float)
|
||||
|
|
@ -78,7 +78,9 @@ def drive_letter(letter):
|
|||
'B': (USB.BusType.IBMPC, 1),
|
||||
'0': (USB.BusType.Shugart, 0),
|
||||
'1': (USB.BusType.Shugart, 1),
|
||||
'2': (USB.BusType.Shugart, 2)
|
||||
'2': (USB.BusType.Shugart, 2),
|
||||
'APPLE2': (USB.BusType.Apple2, 0),
|
||||
'APPLE2_QUARTERTRACK': (USB.BusType.Apple2QuarterTrack, 0),
|
||||
}
|
||||
if not letter.upper() in types:
|
||||
raise argparse.ArgumentTypeError("invalid drive letter: '%s'" % letter)
|
||||
|
|
@ -227,16 +229,13 @@ image_types = OrderedDict(
|
|||
'.st' : 'IMG' })
|
||||
|
||||
def get_image_class(name):
|
||||
if os.path.isdir(name):
|
||||
typespec = 'KryoFlux'
|
||||
else:
|
||||
_, ext = os.path.splitext(name)
|
||||
error.check(ext.lower() in image_types,
|
||||
"""\
|
||||
%s: Unrecognised file suffix '%s'
|
||||
Known suffixes: %s"""
|
||||
% (name, ext, ', '.join(image_types)))
|
||||
typespec = image_types[ext.lower()]
|
||||
_, ext = os.path.splitext(name)
|
||||
error.check(ext.lower() in image_types,
|
||||
"""\
|
||||
%s: Unrecognised file suffix '%s'
|
||||
Known suffixes: %s"""
|
||||
% (name, ext, ', '.join(image_types)))
|
||||
typespec = image_types[ext.lower()]
|
||||
if isinstance(typespec, tuple):
|
||||
typename, classname = typespec
|
||||
else:
|
||||
|
|
@ -246,7 +245,12 @@ Known suffixes: %s"""
|
|||
|
||||
|
||||
def with_drive_selected(fn, usb, args, *_args, **_kwargs):
|
||||
usb.set_bus_type(args.drive[0])
|
||||
try:
|
||||
usb.set_bus_type(args.drive[0].value)
|
||||
except USB.CmdError as err:
|
||||
if err.code == USB.Ack.BadCommand:
|
||||
raise error.Fatal("Device does not support " + str(args.drive[0]))
|
||||
raise
|
||||
try:
|
||||
usb.drive_select(args.drive[1])
|
||||
usb.drive_motor(args.drive[1], _kwargs.pop('motor', True))
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ def main(argv):
|
|||
epilog=epilog)
|
||||
parser.add_argument("--device", help="device name (COM/serial port)")
|
||||
parser.add_argument("--drive", type=util.drive_letter, default='A',
|
||||
help="drive to write (A,B,0,1,2)")
|
||||
help="drive to read (A,B,0,1,2,APPLE2,APPLE2_QUARTERTRACK)")
|
||||
parser.add_argument("--format", help="disk format")
|
||||
parser.add_argument("--tracks", type=util.TrackSet, metavar="TSPEC",
|
||||
help="which tracks to write")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import struct
|
||||
import itertools as it
|
||||
from enum import Enum
|
||||
from greaseweazle import version
|
||||
from greaseweazle import error
|
||||
from greaseweazle.flux import Flux
|
||||
|
|
@ -111,6 +112,7 @@ class Ack:
|
|||
class GetInfo:
|
||||
Firmware = 0
|
||||
BandwidthStats = 1
|
||||
CurrentDrive = 7
|
||||
|
||||
|
||||
## Cmd.{Get,Set}Params indexes
|
||||
|
|
@ -119,10 +121,12 @@ class Params:
|
|||
|
||||
|
||||
## Cmd.SetBusType values
|
||||
class BusType:
|
||||
Invalid = 0
|
||||
IBMPC = 1
|
||||
Shugart = 2
|
||||
class BusType(Enum):
|
||||
Invalid = 0
|
||||
IBMPC = 1
|
||||
Shugart = 2
|
||||
Apple2 = 3
|
||||
Apple2QuarterTrack = 4
|
||||
|
||||
|
||||
## Flux read stream opcodes, preceded by 0xFF byte
|
||||
|
|
@ -132,6 +136,28 @@ class FluxOp:
|
|||
Astable = 3
|
||||
|
||||
|
||||
## Cmd.GetInfo DriveInfo result
|
||||
class DriveInfo:
|
||||
|
||||
FLAG_CYL_VALID = 1
|
||||
FLAG_MOTOR_ON = 2
|
||||
FLAG_IS_FLIPPY = 4
|
||||
|
||||
def __init__(self, rsp):
|
||||
flags, cyl = struct.unpack("<Ii24x", rsp)
|
||||
self.cyl = cyl if (flags & self.FLAG_CYL_VALID) != 0 else None
|
||||
self.motor_on = (flags & self.FLAG_MOTOR_ON) != 0
|
||||
self.is_flippy = (flags & self.FLAG_IS_FLIPPY) != 0
|
||||
|
||||
def __str__(self):
|
||||
s = "Cyl: " + ("Unknown" if self.cyl is None else str(self.cyl))
|
||||
if self.motor_on:
|
||||
s += "; Motor-On"
|
||||
if self.is_flippy:
|
||||
s += "; Is-Flippy"
|
||||
return s
|
||||
|
||||
|
||||
## CmdError: Encapsulates a command acknowledgement.
|
||||
class CmdError(Exception):
|
||||
|
||||
|
|
@ -216,6 +242,11 @@ class Unit:
|
|||
raise CmdError(cmd, r)
|
||||
|
||||
|
||||
def get_current_drive_info(self):
|
||||
self._send_cmd(struct.pack("3B", Cmd.GetInfo, 3, GetInfo.CurrentDrive))
|
||||
return DriveInfo(self.ser.read(32))
|
||||
|
||||
|
||||
## seek:
|
||||
## Seek the selected drive's heads to the specified track (cyl, head).
|
||||
def seek(self, cyl, head):
|
||||
|
|
@ -227,10 +258,13 @@ class Unit:
|
|||
# from cylinder -1. We can check this by attempting a fake outward
|
||||
# step, which is exactly NoClickStep's purpose.
|
||||
try:
|
||||
self._send_cmd(struct.pack("2B", Cmd.NoClickStep, 2))
|
||||
info = self.get_current_drive_info()
|
||||
if info.is_flippy:
|
||||
self._send_cmd(struct.pack("2B", Cmd.NoClickStep, 2))
|
||||
except CmdError:
|
||||
# NoClickStep is "best effort" and we're on a likely error
|
||||
# path anyway. Let it fail silently.
|
||||
# GetInfo.CurrentDrive is unsupported by older firmwares.
|
||||
# NoClickStep is "best effort". We're on a likely error
|
||||
# path anyway, so let them fail silently.
|
||||
pass
|
||||
trk0 = not self.get_pin(26) # now re-sample /TRK0
|
||||
error.check(cyl < 0 or (cyl == 0) == trk0,
|
||||
|
|
@ -284,14 +318,14 @@ class Unit:
|
|||
|
||||
|
||||
## switch_fw_mode:
|
||||
## Switch between update bootloader and main firmware.
|
||||
## Switch between bootloader and main firmware.
|
||||
def switch_fw_mode(self, mode):
|
||||
self._send_cmd(struct.pack("3B", Cmd.SwitchFwMode, 3, int(mode)))
|
||||
|
||||
|
||||
## update_firmware:
|
||||
## Update Greaseweazle to the given new firmware.
|
||||
def update_firmware(self, dat):
|
||||
## update_main_firmware:
|
||||
## Update Greaseweazle with the given new main firmware.
|
||||
def update_main_firmware(self, dat):
|
||||
self._send_cmd(struct.pack("<2BI", Cmd.Update, 6, len(dat)))
|
||||
self.ser.write(dat)
|
||||
(ack,) = struct.unpack("B", self.ser.read(1))
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
# This is free and unencumbered software released into the public domain.
|
||||
# See the file COPYING for more details, or visit <http://unlicense.org>.
|
||||
|
||||
import sys, time, struct
|
||||
import sys, time, struct, textwrap
|
||||
import importlib
|
||||
|
||||
# Put all logging/printing on stderr. This keeps stdout clean for future use.
|
||||
|
|
@ -116,7 +116,7 @@ except KeyboardInterrupt:
|
|||
except Exception as err:
|
||||
if backtrace: raise
|
||||
print("** FATAL ERROR:")
|
||||
print(err)
|
||||
print(textwrap.dedent(str(err)))
|
||||
res = 1
|
||||
|
||||
if start_time is not None:
|
||||
|
|
|
|||
Loading…
Reference in a new issue