Compare commits

...

17 commits

Author SHA1 Message Date
b1259957f9
Merge remote-tracking branch 'origin/master' into add-apple-floppy-type 2022-03-22 08:26:16 -05:00
2e78af33e7
Add Apple floppy type
With a compatible firmware and electrical interface, this allows the use
of an Apple "Disk II" drive mechanism.

Within the firmware there are a couple of differences:
 * The 'write' waveform is different: a flux reversal is an inversion of
   the write data signal, rather than a pulse
 * the stepper windings are directly controlled (phase 0/1/2/3) rather than
   step and direction
 * it is possible to position to quarter track increments. The ability
   to do so MAY be important for archiving certain copy protected software,
   but since "2 steps = 1 double density track" is an assumption spread
   liberally through greaseweazle host software, the default is
   to step the same as a 5.25" HD drive would
2022-03-22 08:20:38 -05:00
Keir Fraser
cd1a3ccf53 keyoflux: Stricter checking on .raw name patterns
No longer accept a naked folder name in lieu of a full pattern
2022-03-03 15:06:21 +00:00
Keir Fraser
fd19abf153 gw: dedent fatal error exceptions. 2022-03-03 15:05:48 +00:00
Keir Fraser
3050beae64 scp: Open empty SCP image files without crashing 2022-03-03 14:49:40 +00:00
Keir Fraser
afb280fc64 scp: Correctly write empty SCP images without crashing 2022-03-03 13:38:00 +00:00
Keir Fraser
ead5d8e727 scp: Clip #revs to smallest #revs 2022-03-01 08:41:42 +00:00
Keir Fraser
b1e302d312 gw read: Read full revolutions when --raw flag is specified.
Fixes crash in SCP output handler.
2022-02-28 18:31:49 +00:00
Keir Fraser
f88f09cca9 gw update: Clean up and de-fatalise the "already up to date" message. 2022-02-27 19:05:09 +00:00
Keir Fraser
cadb6f1a64 gw: "Update Bootloader" -> "Bootloader"; "Firmware" -> "Main Firmware" 2022-02-27 18:29:59 +00:00
Keir Fraser
abad15d2d7 Update to v0.39 2022-02-27 11:50:04 +00:00
Keir Fraser
a215b53b98 gw: Better error report when trying to set an unsupported bus type 2022-02-26 19:11:44 +00:00
Keir Fraser
093ecd1efa usb: Support new firmware command GetInfo.CurrentDrive.
Use it to detect flippy drives and thus limit the scope of the
track-0 sensing fixup in the seek error path.
2022-02-26 15:57:04 +00:00
Keir Fraser
a7c640fe16
Merge pull request #172 from jepler/atari-810-90kb
Add Atari 90kB format
2022-02-23 07:57:45 -08:00
083bf7af23
Add Atari 90kB format
This is the format used by the Atari 810, commonly used with Atari
800 computers.

I have just a single floppy in this format, and with this patch and
Adafruit's generic GW compatible it almost reads.. but consistently
has errors in certain sectors.

```
T39.0: IBM FM (17/18 sectors) - 1 sectors missing - Giving up
Cyl-> 0         1         2         3
H. S: 0123456789012345678901234567890123456789
0. 0: X.....X.....X.....X.....X.....X.....X...
0. 1: ........................................
0. 2: ........................................
0. 3: .X.....X.....X.....X.....X.....X.....X..
0. 4: ........................................
0. 5: ........................................
0. 6: ..X.....X.....X.....X.....X.....X.....X.
0. 7: ........................................
0. 8: ........................................
0. 9: ...X.....X.....X.....X.....X.....X.....X
0.10: ........................................
0.11: ........................................
0.12: ....X.....X.....XX....X.....X.....X.....
0.13: ........................................
0.14: ........................................
0.15: .....X.....X.....X.....X.....X.....X....
0.16: ........................................
0.17: ........................................
Found 679 sectors of 720 (94%)
```
2022-02-23 09:45:42 -06:00
Keir Fraser
88533b5584
Merge pull request #169 from cincodenada/patch-1
Fix typos in help text
2022-02-23 06:33:50 -08:00
Joel Bradshaw
2f66b9da7a
Fix typos in help text 2022-02-15 20:36:40 -08:00
18 changed files with 139 additions and 56 deletions

View file

@ -1,6 +1,6 @@
export MAJOR := 0
export MINOR := 38
export MINOR := 39
include Rules.mk

View file

@ -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

View file

@ -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,

View file

@ -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):

View file

@ -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)

View file

@ -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,

View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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'

View file

@ -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")

View file

@ -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

View file

@ -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",

View file

@ -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__":

View file

@ -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))

View file

@ -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")

View file

@ -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))

View file

@ -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: