Remove tools from the new firmware repository.

This commit is contained in:
Keir Fraser
2021-12-26 11:50:17 +00:00
parent 4b730495f5
commit 554e078967
53 changed files with 23 additions and 6450 deletions

View File

@@ -2,7 +2,7 @@
export FW_MAJOR := 0
export FW_MINOR := 33
TARGETS := all blinky clean dist windist mrproper f1_ocd ocd flash start serial pysetup
TARGETS := all blinky clean dist mrproper f1_ocd ocd flash start serial
.PHONY: $(TARGETS)
ifneq ($(RULES_MK),y)
@@ -19,7 +19,7 @@ VER := v$(FW_MAJOR).$(FW_MINOR)
SUBDIRS += src bootloader blinky_test
all: scripts/greaseweazle/version.py
all:
$(MAKE) -C src -f $(ROOT)/Rules.mk $(PROJ).elf $(PROJ).bin $(PROJ).hex
$(MAKE) bootloader=y -C bootloader -f $(ROOT)/Rules.mk \
Bootloader.elf Bootloader.bin Bootloader.hex
@@ -33,16 +33,11 @@ blinky:
Blinky.elf Blinky.bin Blinky.hex
clean::
rm -rf scripts/greaseweazle/optimised/optimised* scripts/c_ext/build
rm -f *.hex *.upd scripts/greaseweazle/*.pyc
rm -f scripts/greaseweazle/version.py
rm -f *.hex *.upd
find . -name __pycache__ | xargs rm -rf
dist:
rm -rf $(PROJ)-*
mkdir -p $(PROJ)-$(VER)/scripts/greaseweazle/image
mkdir -p $(PROJ)-$(VER)/scripts/greaseweazle/tools
mkdir -p $(PROJ)-$(VER)/scripts/misc
mkdir -p $(PROJ)-$(VER)/hex/alt
$(MAKE) clean
$(MAKE) mcu=stm32f1 all blinky
@@ -51,13 +46,6 @@ dist:
cp -a blinky_test/Blinky.hex $(PROJ)-$(VER)/hex/alt/Blinky_Test-F1-$(VER).hex
cp -a COPYING $(PROJ)-$(VER)/
cp -a README.md $(PROJ)-$(VER)/
cp -a gw $(PROJ)-$(VER)/
cp -a scripts/49-greaseweazle.rules $(PROJ)-$(VER)/scripts/
cp -a scripts/setup.sh $(PROJ)-$(VER)/scripts/
cp -a scripts/gw.py $(PROJ)-$(VER)/scripts/
cp -a scripts/greaseweazle $(PROJ)-$(VER)/scripts
cp -a scripts/c_ext $(PROJ)-$(VER)/scripts
cp -a scripts/misc/*.py $(PROJ)-$(VER)/scripts/misc/
cp -a RELEASE_NOTES $(PROJ)-$(VER)/
$(MAKE) clean
$(MAKE) mcu=stm32f7 all
@@ -72,31 +60,8 @@ dist:
$(MAKE) clean
$(ZIP) $(PROJ)-$(VER).zip $(PROJ)-$(VER)
windist: pysetup
rm -rf $(PROJ)-$(VER) ipf ipf.zip
[ -e $(PROJ)-$(VER).zip ] || \
curl -L https://github.com/keirf/greaseweazle/releases/download/$(VER)/$(PROJ)-$(VER).zip --output $(PROJ)-$(VER).zip
$(UNZIP) $(PROJ)-$(VER).zip
cp -a scripts/setup.py $(PROJ)-$(VER)/scripts
cp -a scripts/greaseweazle/optimised/optimised* $(PROJ)-$(VER)/scripts/greaseweazle/optimised
cd $(PROJ)-$(VER)/scripts && $(PYTHON) setup.py build
cp -a $(PROJ)-$(VER)/scripts/build/exe.win*/* $(PROJ)-$(VER)/
rm -rf $(PROJ)-$(VER)/scripts $(PROJ)-$(VER)/*.py $(PROJ)-$(VER)/gw
curl -L http://softpres.org/_media/files:spsdeclib_5.1_windows.zip --output ipf.zip
$(UNZIP) -oipf ipf.zip
cp -a ipf/capsimg_binary/CAPSImg.dll $(PROJ)-$(VER)/
rm -rf ipf ipf.zip
$(ZIP) $(PROJ)-$(VER)-win.zip $(PROJ)-$(VER)
mrproper: clean
rm -rf $(PROJ)-* ipf ipf.zip
scripts/greaseweazle/version.py: Makefile
echo "major = $(FW_MAJOR)" >$@
echo "minor = $(FW_MINOR)" >>$@
pysetup: scripts/greaseweazle/version.py
PYTHON=$(PYTHON) . ./scripts/setup.sh
rm -rf $(PROJ)-*
BAUD=115200
DEV=/dev/ttyUSB0

View File

@@ -1,23 +1,27 @@
# Greaseweazle
# Greaseweazle: Firmware
*Tools and USB interface for accessing a floppy drive at the raw flux level.*
*Device firmware for accessing a floppy drive at the raw flux level.*
![CI Badge][ci-badge]
![Downloads Badge][downloads-badge]
![Version Badge][version-badge]
This repository contains the Greaseweazle device firmware and its binary
releases. Find the tools repository [here][tools].
### [Purchase a ready-made Greaseweazle][rmb]
### [Download Greaseweazle][Downloads]
### [Read the GitHub Wiki](https://github.com/keirf/greaseweazle/wiki)
### [Read the Greaseweazle Wiki](https://github.com/keirf/greaseweazle/wiki)
### Redistribution
Greaseweazle source code, and all binary releases, are freely redistributable
in any form. Please see the [license](COPYING).
[tools]: https://github.com/keirf/greaseweazle
[rmb]: https://github.com/keirf/greaseweazle/wiki/Ready-Made-Boards
[Downloads]: https://github.com/keirf/greaseweazle/wiki/Downloads
[ci-badge]: https://github.com/keirf/greaseweazle/workflows/CI/badge.svg
[downloads-badge]: https://img.shields.io/github/downloads/keirf/greaseweazle/total
[version-badge]: https://img.shields.io/github/v/release/keirf/greaseweazle
[ci-badge]: https://github.com/keirf/greaseweazle-firmware/workflows/CI/badge.svg
[downloads-badge]: https://img.shields.io/github/downloads/keirf/greaseweazle-firmware/total
[version-badge]: https://img.shields.io/github/v/release/keirf/greaseweazle-firmware

14
gw
View File

@@ -1,14 +0,0 @@
#!/usr/bin/env python3
import sys, os
if sys.version_info < (3,0,0):
print('** FATAL ERROR: Greaseweazle requires Python 3')
sys.exit(1)
# Update the search path and import the real script
sys.path[0] = os.path.join(sys.path[0], "scripts")
import gw
# Execute the real script
gw.main(sys.argv)

View File

@@ -1,23 +0,0 @@
# UDEV Rules for Greaseweazle
#
# To install, type this command in a terminal:
# sudo cp 49-greaseweazle.rules /etc/udev/rules.d/.
#
# After this file is installed, physically unplug and reconnect Greaseweazle.
#
ATTRS{manufacturer}=="Keir Fraser", ATTRS{product}=="Greaseweazle", \
ENV{ID_MM_DEVICE_IGNORE}="1"
ATTRS{manufacturer}=="Keir Fraser", ATTRS{product}=="Greaseweazle", \
ENV{MTP_NO_PROBE}="1"
ATTRS{manufacturer}=="Keir Fraser", ATTRS{product}=="Greaseweazle", \
SUBSYSTEMS=="usb", MODE:="0666"
ATTRS{manufacturer}=="Keir Fraser", ATTRS{product}=="Greaseweazle", \
KERNEL=="ttyACM*", MODE:="0666"
ACTION=="add", \
ATTRS{manufacturer}=="Keir Fraser", ATTRS{product}=="Greaseweazle", \
SYMLINK+="greaseweazle"
#
# If you share your linux system with other users, or just don't like the
# idea of write permission for everybody, you can replace MODE:="0666" with
# OWNER:="yourusername" to create the device owned by you, or with
# GROUP:="somegroupname" and mange access using standard unix groups.

View File

@@ -1,256 +0,0 @@
#define PY_SSIZE_T_CLEAN
#include "Python.h"
#include <stdio.h>
#include <stdint.h>
#define FLUXOP_INDEX 1
#define FLUXOP_SPACE 2
#define FLUXOP_ASTABLE 3
/* bitarray.append(value) */
static PyObject *append_s;
static int bitarray_append(PyObject *bitarray, PyObject *value)
{
PyObject *res = PyObject_CallMethodObjArgs(
bitarray, append_s, value, NULL);
if (res == NULL)
return 0;
Py_DECREF(res);
return 1;
}
/* Like PyList_Append() but steals a reference to @item. */
static int PyList_Append_SR(PyObject *list, PyObject *item)
{
int rc = PyList_Append(list, item);
Py_DECREF(item);
return rc;
}
static PyObject *
flux_to_bitcells(PyObject *self, PyObject *args)
{
/* Parameters */
PyObject *bit_array, *time_array, *revolutions;
PyObject *index_iter, *flux_iter;
double freq, clock_centre, clock_min, clock_max;
double pll_period_adj, pll_phase_adj;
/* Local variables */
PyObject *item;
double clock, new_ticks, ticks, to_index;
int zeros, nbits;
if (!PyArg_ParseTuple(args, "OOOOOdddddd",
&bit_array, &time_array, &revolutions,
&index_iter, &flux_iter,
&freq, &clock_centre, &clock_min, &clock_max,
&pll_period_adj, &pll_phase_adj))
return NULL;
nbits = 0;
ticks = 0.0;
clock = clock_centre;
/* to_index = next(index_iter) */
item = PyIter_Next(index_iter);
to_index = PyFloat_AsDouble(item);
Py_DECREF(item);
if (PyErr_Occurred())
return NULL;
/* for x in flux_iter: */
assert(PyIter_Check(flux_iter));
while ((item = PyIter_Next(flux_iter)) != NULL) {
double x = PyFloat_AsDouble(item);
Py_DECREF(item);
if (PyErr_Occurred())
return NULL;
/* Gather enough ticks to generate at least one bitcell. */
ticks += x / freq;
if (ticks < clock/2)
continue;
/* Clock out zero or more 0s, followed by a 1. */
for (zeros = 0; ; zeros++) {
/* Check if we cross the index mark. */
to_index -= clock;
if (to_index < 0) {
if (PyList_Append_SR(revolutions, PyLong_FromLong(nbits)) < 0)
return NULL;
nbits = 0;
item = PyIter_Next(index_iter);
to_index += PyFloat_AsDouble(item);
Py_DECREF(item);
if (PyErr_Occurred())
return NULL;
}
nbits += 1;
ticks -= clock;
if (PyList_Append_SR(time_array, PyFloat_FromDouble(clock)) < 0)
return NULL;
if (ticks < clock/2) {
if (!bitarray_append(bit_array, Py_True))
return NULL;
break;
}
if (!bitarray_append(bit_array, Py_False))
return NULL;
}
/* PLL: Adjust clock frequency according to phase mismatch. */
if (zeros <= 3) {
/* In sync: adjust clock by a fraction of the phase mismatch. */
clock += ticks * pll_period_adj;
} else {
/* Out of sync: adjust clock towards centre. */
clock += (clock_centre - clock) * pll_period_adj;
}
/* Clamp the clock's adjustment range. */
if (clock < clock_min)
clock = clock_min;
else if (clock > clock_max)
clock = clock_max;
/* PLL: Adjust clock phase according to mismatch. */
new_ticks = ticks * (1.0 - pll_phase_adj);
if (PyList_SetItem(time_array, PyList_Size(time_array)-1,
PyFloat_FromDouble(ticks - new_ticks)) < 0)
return NULL;
ticks = new_ticks;
}
Py_RETURN_NONE;
}
static int _read_28bit(uint8_t *p)
{
int x;
x = (p[0] ) >> 1;
x |= (p[1] & 0xfe) << 6;
x |= (p[2] & 0xfe) << 13;
x |= (p[3] & 0xfe) << 20;
return x;
}
static PyObject *
decode_flux(PyObject *self, PyObject *args)
{
/* Parameters */
Py_buffer bytearray;
PyObject *res = NULL;
/* bytearray buffer */
uint8_t *p;
Py_ssize_t l;
/* Local variables */
PyObject *flux, *index;
long val, ticks, ticks_since_index;
int i, opcode;
if (!PyArg_ParseTuple(args, "y*", &bytearray))
return NULL;
p = bytearray.buf;
l = bytearray.len;
/* assert dat[-1] == 0 */
if ((l == 0) || (p[l-1] != 0)) {
PyErr_SetString(PyExc_ValueError, "Flux is not NUL-terminated");
PyBuffer_Release(&bytearray);
return NULL;
}
/* len(dat) -= 1 */
l -= 1;
/* flux, index = [], [] */
flux = PyList_New(0);
index = PyList_New(0);
/* ticks, ticks_since_index = 0, 0 */
ticks = 0;
ticks_since_index = 0;
while (l != 0) {
i = *p++;
if (i == 255) {
if ((l -= 2) < 0)
goto oos;
opcode = *p++;
switch (opcode) {
case FLUXOP_INDEX:
if ((l -= 4) < 0)
goto oos;
val = _read_28bit(p);
p += 4;
if (PyList_Append_SR(
index, PyLong_FromLong(
ticks_since_index + ticks + val)) < 0)
goto out;
ticks_since_index = -(ticks + val);
break;
case FLUXOP_SPACE:
if ((l -= 4) < 0)
goto oos;
ticks += _read_28bit(p);
p += 4;
break;
default:
PyErr_Format(PyExc_ValueError,
"Bad opcode in flux stream (%d)", opcode);
goto out;
}
} else {
if (i < 250) {
l -= 1;
val = i;
} else {
if ((l -= 2) < 0)
goto oos;
val = 250 + (i - 250) * 255;
val += *p++ - 1;
}
ticks += val;
if (PyList_Append_SR(flux, PyLong_FromLong(ticks)) < 0)
goto out;
ticks_since_index += ticks;
ticks = 0;
}
}
res = Py_BuildValue("OO", flux, index);
out:
PyBuffer_Release(&bytearray);
Py_DECREF(flux);
Py_DECREF(index);
return res;
oos:
PyErr_SetString(PyExc_ValueError, "Unexpected end of flux");
goto out;
}
static PyMethodDef modulefuncs[] = {
{ "flux_to_bitcells", flux_to_bitcells, METH_VARARGS, NULL },
{ "decode_flux", decode_flux, METH_VARARGS, NULL },
{ NULL }
};
static PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT, "optimised", 0, -1, modulefuncs,
};
PyMODINIT_FUNC PyInit_optimised(void)
{
append_s = Py_BuildValue("s", "append");
return PyModule_Create(&moduledef);
}

View File

@@ -1,6 +0,0 @@
from distutils.core import setup, Extension
module1 = Extension('optimised', sources = ['optimised.c'])
setup(name = 'optimised',
ext_modules = [module1])

View File

View File

View File

@@ -1,198 +0,0 @@
# greaseweazle/codec/amiga/amigados.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import struct
import itertools as it
from bitarray import bitarray
from greaseweazle.track import MasterTrack, RawTrack
default_revs = 1.1
sync_bytes = b'\x44\x89\x44\x89'
sync = bitarray(endian='big')
sync.frombytes(sync_bytes)
bad_sector = b'-=[BAD SECTOR]=-' * 32
class AmigaDOS:
time_per_rev = 0.2
def __init__(self, cyl, head):
self.tracknr = cyl*2 + head
self.sector = [None] * self.nsec
self.map = [None] * self.nsec
def summary_string(self):
nsec, nbad = self.nsec, self.nr_missing()
s = "AmigaDOS (%d/%d sectors)" % (nsec - nbad, nsec)
if nbad != 0:
s += " - %d sectors missing" % nbad
return s
# private
def exists(self, sec_id, togo):
return ((self.sector[sec_id] is not None)
or (self.map[self.nsec-togo] is not None))
# private
def add(self, sec_id, togo, label, data):
assert not self.exists(sec_id, togo)
self.sector[sec_id] = label, data
self.map[self.nsec-togo] = sec_id
def has_sec(self, sec_id):
return self.sector[sec_id] is not None
def nr_missing(self):
return len([sec for sec in self.sector if sec is None])
def get_adf_track(self):
tdat = bytearray()
for sec in self.sector:
tdat += sec[1] if sec is not None else bad_sector
return tdat
def set_adf_track(self, tdat):
totsize = self.nsec * 512
if len(tdat) < totsize:
tdat += bytes(totsize - len(tdat))
self.map = list(range(self.nsec))
for sec in self.map:
self.sector[sec] = bytes(16), tdat[sec*512:(sec+1)*512]
return totsize
def flux(self, *args, **kwargs):
return self.raw_track().flux(*args, **kwargs)
def decode_raw(self, track):
raw = RawTrack(clock = self.clock, data = track)
bits, _ = raw.get_all_data()
for offs in bits.itersearch(sync):
if self.nr_missing() == 0:
break
sec = bits[offs:offs+544*16].tobytes()
if len(sec) != 1088:
continue
header = decode(sec[4:12])
format, track, sec_id, togo = tuple(header)
if format != 0xff or track != self.tracknr \
or not(sec_id < self.nsec and 0 < togo <= self.nsec) \
or self.exists(sec_id, togo):
continue
label = decode(sec[12:44])
hsum, = struct.unpack('>I', decode(sec[44:52]))
if hsum != checksum(header + label):
continue
dsum, = struct.unpack('>I', decode(sec[52:60]))
data = decode(sec[60:1084])
gap = decode(sec[1084:1088])
if dsum != checksum(data):
continue;
self.add(sec_id, togo, label, data)
def raw_track(self):
# List of sector IDs missing from the sector map:
missing = iter([x for x in range(self.nsec) if not x in self.map])
# Sector map with the missing entries filled in:
full_map = [next(missing) if x is None else x for x in self.map]
# Post-index track gap.
t = encode(bytes(128 * (self.nsec//11)))
for nr, sec_id in zip(range(self.nsec), full_map):
sector = self.sector[sec_id]
label, data = (bytes(16), bad_sector) if sector is None else sector
header = bytes([0xff, self.tracknr, sec_id, self.nsec-nr])
t += sync_bytes
t += encode(header)
t += encode(label)
t += encode(struct.pack('>I', checksum(header + label)))
t += encode(struct.pack('>I', checksum(data)))
t += encode(data)
t += encode(bytes(2))
# Add the pre-index gap.
tlen = (int((self.time_per_rev / self.clock)) + 31) & ~31
t += bytes(tlen//8-len(t))
track = MasterTrack(
bits = mfm_encode(t),
time_per_rev = 0.2)
track.verify = self
track.verify_revs = default_revs
return track
def verify_track(self, flux):
cyl = self.tracknr // 2
head = self.tracknr & 1
readback_track = self.decode_track(cyl, head, flux)
return (readback_track.nr_missing() == 0
and self.sector == readback_track.sector)
@classmethod
def decode_track(cls, cyl, head, track):
ados = cls(cyl, head)
ados.decode_raw(track)
return ados
class AmigaDOS_DD(AmigaDOS):
nsec = 11
clock = 14/7093790
class AmigaDOS_HD(AmigaDOS):
nsec = 22
clock = AmigaDOS_DD.clock / 2
def mfm_encode(dat):
y = 0
out = bytearray()
for x in dat:
y = (y<<8) | x
if (x & 0xaa) == 0:
y |= ~((y>>1)|(y<<1)) & 0xaaaa
y &= 255
out.append(y)
return bytes(out)
def encode(dat):
return bytes(it.chain(map(lambda x: (x >> 1) & 0x55, dat),
map(lambda x: x & 0x55, dat)))
def decode(dat):
length = len(dat)//2
return bytes(map(lambda x, y: (x << 1 & 0xaa) | (y & 0x55),
it.islice(dat, 0, length),
it.islice(dat, length, None)))
def checksum(dat):
csum = 0
for i in range(0, len(dat), 4):
csum ^= struct.unpack('>I', dat[i:i+4])[0]
return (csum ^ (csum>>1)) & 0x55555555
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,240 +0,0 @@
# greaseweazle/codec/formats.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
from collections import OrderedDict
from greaseweazle.tools import util
class Format:
adf_compatible = False
img_compatible = False
default_trackset = 'c=0-79:h=0-1'
max_trackset = 'c=0-81:h=0-1'
def __init__(self):
self.default_tracks = util.TrackSet(self.default_trackset)
self.max_tracks = util.TrackSet(self.max_trackset)
self.decode_track = self.fmt.decode_track
class Format_Acorn_DFS_SS(Format):
img_compatible = True
default_trackset = 'c=0-39:h=0'
max_trackset = 'c=0-81:h=0'
def __init__(self):
import greaseweazle.codec.ibm.fm as m
self.fmt = m.Acorn_DFS
self.default_revs = m.default_revs
super().__init__()
class Format_Acorn_DFS_DS(Format):
img_compatible = True
default_trackset = 'c=0-39:h=0-1'
max_trackset = 'c=0-81:h=0-1'
def __init__(self):
import greaseweazle.codec.ibm.fm as m
self.fmt = m.Acorn_DFS
self.default_revs = m.default_revs
super().__init__()
class Format_Acorn_ADFS_160(Format):
img_compatible = True
default_trackset = 'c=0-39:h=0'
max_trackset = 'c=0-81:h=0'
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.Acorn_ADFS_640
self.default_revs = m.default_revs
super().__init__()
class Format_Acorn_ADFS_320(Format):
img_compatible = True
default_trackset = 'c=0-79:h=0'
max_trackset = 'c=0-81:h=0'
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.Acorn_ADFS_640
self.default_revs = m.default_revs
super().__init__()
class Format_Acorn_ADFS_640(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.Acorn_ADFS_640
self.default_revs = m.default_revs
super().__init__()
class Format_Acorn_ADFS_800(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.Acorn_ADFS_800
self.default_revs = m.default_revs
super().__init__()
class Format_Acorn_ADFS_1600(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.Acorn_ADFS_1600
self.default_revs = m.default_revs
super().__init__()
class Format_Amiga_AmigaDOS_DD(Format):
adf_compatible = True
def __init__(self):
import greaseweazle.codec.amiga.amigados as m
self.fmt = m.AmigaDOS_DD
self.default_revs = m.default_revs
super().__init__()
class Format_Amiga_AmigaDOS_HD(Format):
adf_compatible = True
def __init__(self):
import greaseweazle.codec.amiga.amigados as m
self.fmt = m.AmigaDOS_HD
self.default_revs = m.default_revs
super().__init__()
class Format_IBM_180(Format):
img_compatible = True
default_trackset = 'c=0-39:h=0'
max_trackset = 'c=0-41:h=0'
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.IBM_MFM_720
self.default_revs = m.default_revs
super().__init__()
class Format_IBM_360(Format):
img_compatible = True
default_trackset = 'c=0-39:h=0-1'
max_trackset = 'c=0-41:h=0-1'
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.IBM_MFM_720
self.default_revs = m.default_revs
super().__init__()
class Format_IBM_720(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.IBM_MFM_720
self.default_revs = m.default_revs
super().__init__()
class Format_IBM_800(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.IBM_MFM_800
self.default_revs = m.default_revs
super().__init__()
class Format_IBM_1440(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.IBM_MFM_1440
self.default_revs = m.default_revs
super().__init__()
class Format_IBM_1200(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.IBM_MFM_1200
self.default_revs = m.default_revs
super().__init__()
class Format_AtariST_360(Format):
img_compatible = True
default_trackset = 'c=0-79:h=0'
max_trackset = 'c=0-81:h=0'
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.AtariST_SS_9SPT
self.default_revs = m.default_revs
super().__init__()
class Format_AtariST_400(Format):
img_compatible = True
default_trackset = 'c=0-79:h=0'
max_trackset = 'c=0-81:h=0'
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.AtariST_10SPT
self.default_revs = m.default_revs
super().__init__()
class Format_AtariST_440(Format):
img_compatible = True
default_trackset = 'c=0-79:h=0'
max_trackset = 'c=0-81:h=0'
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.AtariST_11SPT
self.default_revs = m.default_revs
super().__init__()
class Format_AtariST_720(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.AtariST_DS_9SPT
self.default_revs = m.default_revs
super().__init__()
class Format_AtariST_800(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.AtariST_10SPT
self.default_revs = m.default_revs
super().__init__()
class Format_AtariST_880(Format):
img_compatible = True
def __init__(self):
import greaseweazle.codec.ibm.mfm as m
self.fmt = m.AtariST_11SPT
self.default_revs = m.default_revs
super().__init__()
formats = OrderedDict({
'acorn.dfs.ss': Format_Acorn_DFS_SS,
'acorn.dfs.ds': Format_Acorn_DFS_DS,
'acorn.adfs.160': Format_Acorn_ADFS_160,
'acorn.adfs.320': Format_Acorn_ADFS_320,
'acorn.adfs.640': Format_Acorn_ADFS_640,
'acorn.adfs.800': Format_Acorn_ADFS_800,
'acorn.adfs.1600': Format_Acorn_ADFS_1600,
'amiga.amigados': Format_Amiga_AmigaDOS_DD,
'amiga.amigados_hd': Format_Amiga_AmigaDOS_HD,
'atarist.360': Format_AtariST_360,
'atarist.400': Format_AtariST_400,
'atarist.440': Format_AtariST_440,
'atarist.720': Format_AtariST_720,
'atarist.800': Format_AtariST_800,
'atarist.880': Format_AtariST_880,
'commodore.1581': Format_IBM_800,
'ibm.180': Format_IBM_180,
'ibm.360': Format_IBM_360,
'ibm.720': Format_IBM_720,
'ibm.1200': Format_IBM_1200,
'ibm.1440': Format_IBM_1440,
})
def print_formats(f = None):
s = ''
for k, v in formats.items():
if not f or f(k, v):
if s:
s += '\n'
s += ' ' + k
return s

View File

View File

@@ -1,339 +0,0 @@
# greaseweazle/codec/ibm/fm.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import binascii
import copy, heapq, struct, functools
import itertools as it
from bitarray import bitarray
import crcmod.predefined
from greaseweazle.codec.ibm import mfm
from greaseweazle.track import MasterTrack, RawTrack
default_revs = 2
def sync(dat, clk=0xc7):
x = 0
for i in range(8):
x <<= 1
x |= (clk >> (7-i)) & 1
x <<= 1
x |= (dat >> (7-i)) & 1
return bytes(struct.pack('>H', x))
sync_prefix = bitarray(endian='big')
sync_prefix.frombytes(b'\xaa\xaa' + sync(0xf8))
sync_prefix = sync_prefix[:16+10]
iam_sync_bytes = sync(0xfc, 0xd7)
iam_sync = bitarray(endian='big')
iam_sync.frombytes(b'\xaa\xaa' + iam_sync_bytes)
crc16 = crcmod.predefined.Crc('crc-ccitt-false')
sec_sz = mfm.sec_sz
IDAM = mfm.IDAM
DAM = mfm.DAM
Sector = mfm.Sector
IAM = mfm.IAM
class IBM_FM:
IAM = 0xfc
IDAM = 0xfe
DAM = 0xfb
DDAM = 0xf8
gap_presync = 6
gapbyte = 0xff
def __init__(self, cyl, head):
self.cyl, self.head = cyl, head
self.sectors = []
self.iams = []
def summary_string(self):
nsec, nbad = len(self.sectors), self.nr_missing()
s = "IBM FM (%d/%d sectors)" % (nsec - nbad, nsec)
if nbad != 0:
s += " - %d sectors missing" % nbad
return s
def has_sec(self, sec_id):
return self.sectors[sec_id].crc == 0
def nr_missing(self):
return len(list(filter(lambda x: x.crc != 0, self.sectors)))
def flux(self, *args, **kwargs):
return self.raw_track().flux(*args, **kwargs)
def decode_raw(self, track):
track.cue_at_index()
raw = RawTrack(clock = self.clock, data = track)
bits, _ = raw.get_all_data()
areas = []
idam = None
## 1. Calculate offsets within dump
for offs in bits.itersearch(iam_sync):
offs += 16
areas.append(IAM(offs, offs+1*16))
self.has_iam = True
for offs in bits.itersearch(sync_prefix):
offs += 16
mark = decode(bits[offs:offs+1*16].tobytes())[0]
clock = decode(bits[offs-1:offs+1*16-1].tobytes())[0]
if clock != 0xc7:
continue
if mark == IBM_FM.IDAM:
s, e = offs, offs+7*16
b = decode(bits[s:e].tobytes())
c,h,r,n = struct.unpack(">x4B2x", b)
crc = crc16.new(b).crcValue
if idam is not None:
areas.append(idam)
idam = IDAM(s, e, crc, c=c, h=h, r=r, n=n)
elif mark == IBM_FM.DAM or mark == IBM_FM.DDAM:
if idam is None or idam.end - offs > 1000:
areas.append(DAM(offs, offs+4*16, 0xffff, mark=mark))
else:
sz = 128 << idam.n
s, e = offs, offs+(1+sz+2)*16
b = decode(bits[s:e].tobytes())
crc = crc16.new(b).crcValue
dam = DAM(s, e, crc, mark=mark, data=b[1:-2])
areas.append(Sector(idam, dam))
idam = None
else:
pass #print("Unknown mark %02x" % mark)
if idam is not None:
areas.append(idam)
# Convert to offsets within track
areas.sort(key=lambda x:x.start)
index = iter(raw.revolutions)
p, n = 0, next(index)
for a in areas:
if a.start >= n:
p = n
try:
n = next(index)
except StopIteration:
n = float('inf')
a.delta(p)
areas.sort(key=lambda x:x.start)
# Add to the deduped lists
for a in areas:
match = False
if isinstance(a, IAM):
list = self.iams
elif isinstance(a, Sector):
list = self.sectors
else:
continue
for s in list:
if abs(s.start - a.start) < 1000:
match = True
break
if match and isinstance(a, Sector) and s.crc != 0 and a.crc == 0:
self.sectors = [x for x in self.sectors if x != a]
match = False
if not match:
list.append(a)
def raw_track(self):
areas = heapq.merge(self.iams, self.sectors, key=lambda x:x.start)
t = bytes()
for a in areas:
start = a.start//16 - self.gap_presync
gap = max(start - len(t)//2, 0)
t += encode(bytes([self.gapbyte] * gap))
t += encode(bytes(self.gap_presync))
if isinstance(a, IAM):
t += iam_sync_bytes
elif isinstance(a, Sector):
idam = bytes([self.IDAM,
a.idam.c, a.idam.h, a.idam.r, a.idam.n])
idam += struct.pack('>H', crc16.new(idam).crcValue)
t += sync(idam[0]) + encode(idam[1:])
start = a.dam.start//16 - self.gap_presync
gap = max(start - len(t)//2, 0)
t += encode(bytes([self.gapbyte] * gap))
t += encode(bytes(self.gap_presync))
dam = bytes([a.dam.mark]) + a.dam.data
dam += struct.pack('>H', crc16.new(dam).crcValue)
t += sync(dam[0]) + encode(dam[1:])
# Add the pre-index gap.
tlen = int((self.time_per_rev / self.clock) // 16)
gap = max(tlen - len(t)//2, 0)
t += encode(bytes([self.gapbyte] * gap))
track = MasterTrack(
bits = t,
time_per_rev = self.time_per_rev)
track.verify = self
track.verify_revs = default_revs
return track
class IBM_FM_Formatted(IBM_FM):
gap_4a = 40 # Post-Index
gap_1 = 26 # Post-IAM
gap_2 = 11 # Post-IDAM
def __init__(self, cyl, head):
super().__init__(cyl, head)
self.raw_iams, self.raw_sectors = [], []
def decode_raw(self, track):
iams, sectors = self.iams, self.sectors
self.iams, self.sectors = self.raw_iams, self.raw_sectors
super().decode_raw(track)
self.iams, self.sectors = iams, sectors
for r in self.raw_sectors:
if r.idam.crc != 0:
continue
for s in self.sectors:
if (s.idam.c == r.idam.c and
s.idam.h == r.idam.h and
s.idam.r == r.idam.r and
s.idam.n == r.idam.n):
s.idam.crc = 0
if r.dam.crc == 0 and s.dam.crc != 0:
s.dam.crc = s.crc = 0
s.dam.data = r.dam.data
def set_img_track(self, tdat):
pos = 0
self.sectors.sort(key = lambda x: x.idam.r)
totsize = functools.reduce(lambda x, y: x + (128<<y.idam.n),
self.sectors, 0)
if len(tdat) < totsize:
tdat += bytes(totsize - len(tdat))
for s in self.sectors:
s.crc = s.idam.crc = s.dam.crc = 0
size = 128 << s.idam.n
s.dam.data = tdat[pos:pos+size]
pos += size
self.sectors.sort(key = lambda x: x.start)
return totsize
def get_img_track(self):
tdat = bytearray()
sectors = self.sectors.copy()
sectors.sort(key = lambda x: x.idam.r)
for s in sectors:
tdat += s.dam.data
return tdat
def verify_track(self, flux):
readback_track = IBM_FM_Formatted(self.cyl, self.head)
readback_track.clock = self.clock
readback_track.time_per_rev = self.time_per_rev
for x in self.iams:
readback_track.iams.append(copy.copy(x))
for x in self.sectors:
idam, dam = copy.copy(x.idam), copy.copy(x.dam)
idam.crc, dam.crc = 0xffff, 0xffff
readback_track.sectors.append(Sector(idam, dam))
readback_track.decode_raw(flux)
if readback_track.nr_missing() != 0:
return False
return self.sectors == readback_track.sectors
class IBM_FM_Predefined(IBM_FM_Formatted):
cskew = 0
hskew = 0
interleave = 1
def __init__(self, cyl, head):
super().__init__(cyl, head)
# Create logical sector map in rotational order
sec_map = [-1] * self.nsec
pos = (cyl*self.cskew + head*self.hskew) % self.nsec
for i in range(self.nsec):
while sec_map[pos] != -1:
pos = (pos + 1) % self.nsec
sec_map[pos] = i
pos = (pos + self.interleave) % self.nsec
pos = self.gap_4a
if self.gap_1 is not None:
self.iams = [IAM(pos*16,(pos+1)*16)]
pos += 1 + self.gap_1
for i in range(self.nsec):
pos += self.gap_presync
idam = IDAM(pos*16, (pos+7)*16, 0xffff,
c=cyl, h=head, r=self.id0+sec_map[i], n = self.sz)
pos += 7 + self.gap_2 + self.gap_presync
size = 128 << self.sz
dam = DAM(pos*16, (pos+1+size+2)*16, 0xffff,
mark=self.DAM, data=bytes(size))
self.sectors.append(Sector(idam, dam))
pos += 1 + size + 2 + self.gap_3
@classmethod
def decode_track(cls, cyl, head, track):
mfm = cls(cyl, head)
mfm.decode_raw(track)
return mfm
class Acorn_DFS(IBM_FM_Predefined):
time_per_rev = 0.2
clock = 4e-6
gap_1 = 0 # No IAM
gap_3 = 21
nsec = 10
id0 = 0
sz = 1
cskew = 3
encode_list = []
for x in range(256):
y = 0
for i in range(8):
y <<= 1
y |= 1
y <<= 1
y |= (x >> (7-i)) & 1
encode_list.append(y)
def encode(dat):
out = bytearray()
for x in dat:
out += struct.pack('>H', encode_list[x])
return bytes(out)
decode = mfm.decode
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,486 +0,0 @@
# greaseweazle/codec/ibm/mfm.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import copy, heapq, struct, functools
import itertools as it
from bitarray import bitarray
import crcmod.predefined
from greaseweazle.track import MasterTrack, RawTrack
default_revs = 2
iam_sync_bytes = b'\x52\x24' * 3
iam_sync = bitarray(endian='big')
iam_sync.frombytes(iam_sync_bytes)
sync_bytes = b'\x44\x89' * 3
sync = bitarray(endian='big')
sync.frombytes(sync_bytes)
crc16 = crcmod.predefined.Crc('crc-ccitt-false')
def sec_sz(n):
return 128 << n if n <= 7 else 128 << 8
class TrackArea:
def __init__(self, start, end, crc=None):
self.start = start
self.end = end
self.crc = crc
def delta(self, delta):
self.start -= delta
self.end -= delta
def __eq__(self, x):
return (isinstance(x, type(self))
and self.start == x.start
and self.end == x.end
and self.crc == x.crc)
class IDAM(TrackArea):
def __init__(self, start, end, crc, c, h, r, n):
super().__init__(start, end, crc)
self.c = c
self.h = h
self.r = r
self.n = n
def __str__(self):
return ("IDAM:%6d-%6d c=%02x h=%02x r=%02x n=%02x CRC:%04x"
% (self.start, self.end, self.c, self.h, self.r, self.n,
self.crc))
def __eq__(self, x):
return (super().__eq__(x)
and self.c == x.c and self.h == x.h
and self.r == x.r and self.n == x.n)
def __copy__(self):
return IDAM(self.start, self.end, self.crc,
self.c, self.h, self.r, self.n)
class DAM(TrackArea):
def __init__(self, start, end, crc, mark, data=None):
super().__init__(start, end, crc)
self.mark = mark
self.data = data
def __str__(self):
return "DAM: %6d-%6d mark=%02x" % (self.start, self.end, self.mark)
def __eq__(self, x):
return (super().__eq__(x)
and self.mark == x.mark
and self.data == x.data)
def __copy__(self):
return DAM(self.start, self.end, self.crc, self.mark, self.data)
class Sector(TrackArea):
def __init__(self, idam, dam):
super().__init__(idam.start, dam.end, idam.crc | dam.crc)
self.idam = idam
self.dam = dam
def __str__(self):
s = "Sec: %6d-%6d CRC:%04x\n" % (self.start, self.end, self.crc)
s += " " + str(self.idam) + "\n"
s += " " + str(self.dam)
return s
def delta(self, delta):
super().delta(delta)
self.idam.delta(delta)
self.dam.delta(delta)
def __eq__(self, x):
return (super().__eq__(x)
and self.idam == x.idam
and self.dam == x.dam)
class IAM(TrackArea):
def __str__(self):
return "IAM: %6d-%6d" % (self.start, self.end)
def __copy__(self):
return IAM(self.start, self.end)
class IBM_MFM:
IAM = 0xfc
IDAM = 0xfe
DAM = 0xfb
DDAM = 0xf8
gap_presync = 12
gapbyte = 0x4e
def __init__(self, cyl, head):
self.cyl, self.head = cyl, head
self.sectors = []
self.iams = []
def summary_string(self):
nsec, nbad = len(self.sectors), self.nr_missing()
s = "IBM MFM (%d/%d sectors)" % (nsec - nbad, nsec)
if nbad != 0:
s += " - %d sectors missing" % nbad
return s
def has_sec(self, sec_id):
return self.sectors[sec_id].crc == 0
def nr_missing(self):
return len(list(filter(lambda x: x.crc != 0, self.sectors)))
def flux(self, *args, **kwargs):
return self.raw_track().flux(*args, **kwargs)
def decode_raw(self, track):
track.cue_at_index()
raw = RawTrack(clock = self.clock, data = track)
bits, _ = raw.get_all_data()
areas = []
idam = None
## 1. Calculate offsets within dump
for offs in bits.itersearch(iam_sync):
mark = decode(bits[offs+3*16:offs+4*16].tobytes())[0]
if mark == IBM_MFM.IAM:
areas.append(IAM(offs, offs+4*16))
self.has_iam = True
for offs in bits.itersearch(sync):
mark = decode(bits[offs+3*16:offs+4*16].tobytes())[0]
if mark == IBM_MFM.IDAM:
s, e = offs, offs+10*16
b = decode(bits[s:e].tobytes())
c,h,r,n = struct.unpack(">4x4B2x", b)
crc = crc16.new(b).crcValue
if idam is not None:
areas.append(idam)
idam = IDAM(s, e, crc, c=c, h=h, r=r, n=n)
elif mark == IBM_MFM.DAM or mark == IBM_MFM.DDAM:
if idam is None or idam.end - offs > 1000:
areas.append(DAM(offs, offs+4*16, 0xffff, mark=mark))
else:
sz = 128 << idam.n
s, e = offs, offs+(4+sz+2)*16
b = decode(bits[s:e].tobytes())
crc = crc16.new(b).crcValue
dam = DAM(s, e, crc, mark=mark, data=b[4:-2])
areas.append(Sector(idam, dam))
idam = None
else:
pass #print("Unknown mark %02x" % mark)
if idam is not None:
areas.append(idam)
# Convert to offsets within track
areas.sort(key=lambda x:x.start)
index = iter(raw.revolutions)
p, n = 0, next(index)
for a in areas:
if a.start >= n:
p = n
try:
n = next(index)
except StopIteration:
n = float('inf')
a.delta(p)
areas.sort(key=lambda x:x.start)
# Add to the deduped lists
for a in areas:
match = False
if isinstance(a, IAM):
list = self.iams
elif isinstance(a, Sector):
list = self.sectors
else:
continue
for s in list:
if abs(s.start - a.start) < 1000:
match = True
break
if match and isinstance(a, Sector) and s.crc != 0 and a.crc == 0:
self.sectors = [x for x in self.sectors if x != a]
match = False
if not match:
list.append(a)
def raw_track(self):
areas = heapq.merge(self.iams, self.sectors, key=lambda x:x.start)
t = bytes()
for a in areas:
start = a.start//16 - self.gap_presync
gap = max(start - len(t)//2, 0)
t += encode(bytes([self.gapbyte] * gap))
t += encode(bytes(self.gap_presync))
if isinstance(a, IAM):
t += iam_sync_bytes
t += encode(bytes([self.IAM]))
elif isinstance(a, Sector):
t += sync_bytes
idam = bytes([0xa1, 0xa1, 0xa1, self.IDAM,
a.idam.c, a.idam.h, a.idam.r, a.idam.n])
idam += struct.pack('>H', crc16.new(idam).crcValue)
t += encode(idam[3:])
start = a.dam.start//16 - self.gap_presync
gap = max(start - len(t)//2, 0)
t += encode(bytes([self.gapbyte] * gap))
t += encode(bytes(self.gap_presync))
t += sync_bytes
dam = bytes([0xa1, 0xa1, 0xa1, a.dam.mark]) + a.dam.data
dam += struct.pack('>H', crc16.new(dam).crcValue)
t += encode(dam[3:])
# Add the pre-index gap.
tlen = int((self.time_per_rev / self.clock) // 16)
gap = max(tlen - len(t)//2, 0)
t += encode(bytes([self.gapbyte] * gap))
track = MasterTrack(
bits = mfm_encode(t),
time_per_rev = self.time_per_rev)
track.verify = self
track.verify_revs = default_revs
return track
class IBM_MFM_Formatted(IBM_MFM):
gap_4a = 80 # Post-Index
gap_1 = 50 # Post-IAM
gap_2 = 22 # Post-IDAM
def __init__(self, cyl, head):
super().__init__(cyl, head)
self.raw_iams, self.raw_sectors = [], []
def decode_raw(self, track):
iams, sectors = self.iams, self.sectors
self.iams, self.sectors = self.raw_iams, self.raw_sectors
super().decode_raw(track)
self.iams, self.sectors = iams, sectors
for r in self.raw_sectors:
if r.idam.crc != 0:
continue
for s in self.sectors:
if (s.idam.c == r.idam.c and
s.idam.h == r.idam.h and
s.idam.r == r.idam.r and
s.idam.n == r.idam.n):
s.idam.crc = 0
if r.dam.crc == 0 and s.dam.crc != 0:
s.dam.crc = s.crc = 0
s.dam.data = r.dam.data
def set_img_track(self, tdat):
pos = 0
self.sectors.sort(key = lambda x: x.idam.r)
totsize = functools.reduce(lambda x, y: x + (128<<y.idam.n),
self.sectors, 0)
if len(tdat) < totsize:
tdat += bytes(totsize - len(tdat))
for s in self.sectors:
s.crc = s.idam.crc = s.dam.crc = 0
size = 128 << s.idam.n
s.dam.data = tdat[pos:pos+size]
pos += size
self.sectors.sort(key = lambda x: x.start)
return totsize
def get_img_track(self):
tdat = bytearray()
sectors = self.sectors.copy()
sectors.sort(key = lambda x: x.idam.r)
for s in sectors:
tdat += s.dam.data
return tdat
def verify_track(self, flux):
readback_track = IBM_MFM_Formatted(self.cyl, self.head)
readback_track.clock = self.clock
readback_track.time_per_rev = self.time_per_rev
for x in self.iams:
readback_track.iams.append(copy.copy(x))
for x in self.sectors:
idam, dam = copy.copy(x.idam), copy.copy(x.dam)
idam.crc, dam.crc = 0xffff, 0xffff
readback_track.sectors.append(Sector(idam, dam))
readback_track.decode_raw(flux)
if readback_track.nr_missing() != 0:
return False
return self.sectors == readback_track.sectors
class IBM_MFM_Predefined(IBM_MFM_Formatted):
cskew = 0
hskew = 0
interleave = 1
def __init__(self, cyl, head):
super().__init__(cyl, head)
# Create logical sector map in rotational order
sec_map = [-1] * self.nsec
pos = (cyl*self.cskew + head*self.hskew) % self.nsec
for i in range(self.nsec):
while sec_map[pos] != -1:
pos = (pos + 1) % self.nsec
sec_map[pos] = i
pos = (pos + self.interleave) % self.nsec
pos = self.gap_4a
if self.gap_1 is not None:
self.iams = [IAM(pos*16,(pos+4)*16)]
pos += 4 + self.gap_1
for i in range(self.nsec):
pos += self.gap_presync
idam = IDAM(pos*16, (pos+10)*16, 0xffff,
c=cyl, h=head, r=self.id0+sec_map[i], n = self.sz)
pos += 10 + self.gap_2 + self.gap_presync
size = 128 << self.sz
dam = DAM(pos*16, (pos+4+size+2)*16, 0xffff,
mark=self.DAM, data=bytes(size))
self.sectors.append(Sector(idam, dam))
pos += 4 + size + 2 + self.gap_3
@classmethod
def decode_track(cls, cyl, head, track):
mfm = cls(cyl, head)
mfm.decode_raw(track)
return mfm
class IBM_MFM_720(IBM_MFM_Predefined):
time_per_rev = 0.2
clock = 2e-6
gap_3 = 84 # Post-DAM
nsec = 9
id0 = 1
sz = 2
class IBM_MFM_800(IBM_MFM_720):
gap_3 = 30
nsec = 10
class IBM_MFM_1200(IBM_MFM_720):
time_per_rev = 60/360
clock = 1e-6
nsec = 15
class IBM_MFM_1440(IBM_MFM_720):
clock = 1e-6
nsec = 18
class AtariST_SS_9SPT(IBM_MFM_720):
gap_1 = None
cskew = 2
class AtariST_DS_9SPT(IBM_MFM_720):
gap_1 = None
cskew = 4
hskew = 2
class AtariST_10SPT(IBM_MFM_720):
gap_1 = None
gap_3 = 30
nsec = 10
class AtariST_11SPT(IBM_MFM_720):
clock = 2e-6 * 0.96 # long track
gap_1 = None
gap_3 = 3
nsec = 11
class Acorn_ADFS_640(IBM_MFM_Predefined):
time_per_rev = 0.2
clock = 2e-6
gap_3 = 57
nsec = 16
id0 = 0
sz = 1
class Acorn_ADFS_800(IBM_MFM_Predefined):
time_per_rev = 0.2
clock = 2e-6
gap_3 = 116
nsec = 5
id0 = 0
sz = 3
class Acorn_ADFS_1600(IBM_MFM_Predefined):
time_per_rev = 0.2
clock = 1e-6
gap_3 = 116
nsec = 10
id0 = 0
sz = 3
def mfm_encode(dat):
y = 0
out = bytearray()
for x in dat:
y = (y<<8) | x
if (x & 0xaa) == 0:
y |= ~((y>>1)|(y<<1)) & 0xaaaa
y &= 255
out.append(y)
return bytes(out)
encode_list = []
for x in range(256):
y = 0
for i in range(8):
y <<= 2
y |= (x >> (7-i)) & 1
encode_list.append(y)
def encode(dat):
out = bytearray()
for x in dat:
out += struct.pack('>H', encode_list[x])
return bytes(out)
decode_list = bytearray()
for x in range(0x5555+1):
y = 0
for i in range(16):
if x&(1<<(i*2)):
y |= 1<<i
decode_list.append(y)
def decode(dat):
out = bytearray()
for x,y in zip(dat[::2], dat[1::2]):
out.append(decode_list[((x<<8)|y)&0x5555])
return bytes(out)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,19 +0,0 @@
# greaseweazle/error.py
#
# Error management and reporting.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
class Fatal(Exception):
pass
def check(pred, desc):
if not pred:
raise Fatal(desc)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,147 +0,0 @@
# greaseweazle/flux.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
from greaseweazle import error
class Flux:
def __init__(self, index_list, flux_list, sample_freq, index_cued=True):
self.index_list = index_list
self.list = flux_list
self.sample_freq = sample_freq
self.splice = 0
self.index_cued = index_cued
def __str__(self):
s = "\nFlux: %.2f MHz" % (self.sample_freq*1e-6)
s += ("\n Total: %u samples, %.2fms\n"
% (len(self.list), sum(self.list)*1000/self.sample_freq))
rev = 0
for t in self.index_list:
s += " Revolution %u: %.2fms\n" % (rev, t*1000/self.sample_freq)
rev += 1
return s[:-1]
def summary_string(self):
return ("Raw Flux (%u flux in %.2fms)"
% (len(self.list), sum(self.list)*1000/self.sample_freq))
def cue_at_index(self):
if self.index_cued:
return
# Clip the initial partial revolution.
to_index = self.index_list[0]
for i in range(len(self.list)):
to_index -= self.list[i]
if to_index < 0:
break
if to_index < 0:
self.list = [-to_index] + self.list[i+1:]
else: # we ran out of flux
self.list = []
self.index_list = self.index_list[1:]
self.index_cued = True
def flux_for_writeout(self):
error.check(self.index_cued,
"Cannot write non-index-cued raw flux")
error.check(self.splice == 0 or len(self.index_list) > 1,
"Cannot write single-revolution unaligned raw flux")
splice_at_index = (self.splice == 0)
# Copy the required amount of flux to a fresh list.
flux_list = []
to_index = self.index_list[0]
remain = to_index + self.splice
for f in self.list:
if f > remain:
break
flux_list.append(f)
remain -= f
if splice_at_index:
# Extend with "safe" 4us sample values, to avoid unformatted area
# at end of track if drive motor is a little slow.
four_us = max(self.sample_freq * 4e-6, 1)
if remain > four_us:
flux_list.append(remain)
for i in range(round(to_index/(10*four_us))):
flux_list.append(four_us)
elif remain > 0:
# End the write exactly where specified.
flux_list.append(remain)
return WriteoutFlux(to_index, flux_list, self.sample_freq,
index_cued = True,
terminate_at_index = (self.splice == 0))
def flux(self):
return self
def scale(self, factor):
"""Scale up all flux and index timings by specified factor."""
self.sample_freq /= factor
@property
def ticks_per_rev(self):
"""Mean time between index pulses, in sample ticks"""
index_list = self.index_list
if not self.index_cued:
index_list = index_list[1:]
return sum(index_list) / len(index_list)
@property
def time_per_rev(self):
"""Mean time between index pulses, in seconds (float)"""
return self.ticks_per_rev / self.sample_freq
class WriteoutFlux(Flux):
def __init__(self, ticks_to_index, flux_list, sample_freq,
index_cued, terminate_at_index):
super().__init__([ticks_to_index], flux_list, sample_freq)
self.index_cued = index_cued
self.terminate_at_index = terminate_at_index
def __str__(self):
s = ("\nWriteoutFlux: %.2f MHz, %.2fms to index, %s\n"
" Total: %u samples, %.2fms"
% (self.sample_freq*1e-6,
self.index_list[0]*1000/self.sample_freq,
("Write all", "Terminate at index")[self.terminate_at_index],
len(self.list), sum(self.list)*1000/self.sample_freq))
return s
def flux_for_writeout(self):
return self
@property
def ticks_per_rev(self):
"""Mean time between index pulses, in sample ticks"""
return sum(self.index_list) / len(self.index_list)
# Local variables:
# python-indent: 4
# End:

View File

View File

@@ -1,27 +0,0 @@
# greaseweazle/image/acorn.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
from greaseweazle.image.img import IMG
class SSD(IMG):
default_format = 'acorn.dfs.ss'
class DSD(IMG):
default_format = 'acorn.dfs.ds'
class ADS(IMG):
default_format = 'acorn.adfs.160'
class ADM(IMG):
default_format = 'acorn.adfs.320'
class ADL(IMG):
default_format = 'acorn.adfs.640'
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,97 +0,0 @@
# greaseweazle/image/adf.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
from greaseweazle import error
import greaseweazle.codec.amiga.amigados as amigados
from .image import Image
from greaseweazle.codec import formats
class ADF(Image):
default_format = 'amiga.amigados'
def __init__(self, name, fmt):
self.to_track = dict()
error.check(fmt is not None and fmt.adf_compatible, """\
ADF image requires compatible format specifier
Compatible formats:\n%s"""
% formats.print_formats(lambda k, v: v.adf_compatible))
self.filename = name
self.fmt = fmt
@classmethod
def from_file(cls, name, fmt):
with open(name, "rb") as f:
dat = f.read()
adf = cls(name, fmt)
while True:
nsec = fmt.fmt.nsec
error.check((len(dat) % (2*nsec*512)) == 0, "Bad ADF image length")
ncyl = len(dat) // (2*nsec*512)
if ncyl < 90:
break
error.check(nsec == 11, "Bad ADF image length")
fmt = adf.fmt = formats.Format_Amiga_AmigaDOS_HD()
pos = 0
for t in fmt.max_tracks:
tnr = t.cyl*2 + t.head
ados = fmt.fmt(t.cyl, t.head)
pos += ados.set_adf_track(dat[pos:])
adf.to_track[tnr] = ados
return adf
@classmethod
def to_file(cls, name, fmt=None):
return cls(name, fmt)
def get_track(self, cyl, side):
tnr = cyl * 2 + side
if not tnr in self.to_track:
return None
return self.to_track[tnr].raw_track()
def emit_track(self, cyl, side, track):
tnr = cyl * 2 + side
self.to_track[tnr] = track
def get_image(self):
tdat = bytearray()
ntracks = max(self.to_track, default=0) + 1
for tnr in range(ntracks):
t = self.to_track[tnr] if tnr in self.to_track else None
if t is not None and hasattr(t, 'get_adf_track'):
tdat += t.get_adf_track()
elif tnr < 160:
# Pad empty/damaged tracks.
tdat += amigados.bad_sector * self.fmt.fmt.nsec
else:
# Do not extend past 160 tracks unless there is data.
break
if ntracks < 160:
tdat += amigados.bad_sector * self.fmt.fmt.nsec * (160 - ntracks)
return tdat
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,16 +0,0 @@
# greaseweazle/image/d81.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
from greaseweazle.image.img import IMG
class D81(IMG):
default_format = 'commodore.1581'
sides_swapped = True
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,488 +0,0 @@
# greaseweazle/image/edsk.py
#
# Some of the code here is heavily inspired by Simon Owen's SAMdisk:
# https://simonowen.com/samdisk/
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import binascii, math, struct
import itertools as it
from bitarray import bitarray
from greaseweazle import error
from greaseweazle.codec.ibm import mfm
from greaseweazle.track import MasterTrack, RawTrack
from .image import Image
class SR1:
SUCCESS = 0x00
CANNOT_FIND_ID_ADDRESS = 0x01
WRITE_PROTECT_DETECTED = 0x02
CANNOT_FIND_SECTOR_ID = 0x04
RESERVED1 = 0x08
OVERRUN = 0x10
CRC_ERROR = 0x20
RESERVED2 = 0x40
END_OF_CYLINDER = 0x80
class SR2:
SUCCESS = 0x00
MISSING_ADDRESS_MARK = 0x01
BAD_CYLINDER = 0x02
SCAN_COMMAND_FAILED = 0x04
SCAN_COMMAND_EQUAL = 0x08
WRONG_CYLINDER_DETECTED = 0x10
CRC_ERROR_IN_SECTOR_DATA = 0x20
SECTOR_WITH_DELETED_DATA = 0x40
RESERVED = 0x80
class SectorErrors:
def __init__(self, sr1, sr2):
self.id_crc_error = (sr1 & SR1.CRC_ERROR) != 0
self.data_not_found = (sr2 & SR2.MISSING_ADDRESS_MARK) != 0
self.data_crc_error = (sr2 & SR2.CRC_ERROR_IN_SECTOR_DATA) != 0
self.deleted_dam = (sr2 & SR2.SECTOR_WITH_DELETED_DATA) != 0
if self.data_crc_error:
# uPD765 sets both id and data flags for data CRC errors
self.id_crc_error = False
if (# normal data
(sr1 == SR1.SUCCESS and sr2 == SR2.SUCCESS) or
# deleted data
(sr1 == SR1.SUCCESS and sr2 == SR2.SECTOR_WITH_DELETED_DATA) or
# end of track
(sr1 == SR1.END_OF_CYLINDER and sr2 == SR2.SUCCESS) or
# id crc error
(sr1 == SR1.CRC_ERROR and sr2 == SR2.SUCCESS) or
# normal data crc error
(sr1 == SR1.CRC_ERROR and sr2 == SR2.CRC_ERROR_IN_SECTOR_DATA) or
# deleted data crc error
(sr1 == SR1.CRC_ERROR and sr2 == (SR2.CRC_ERROR_IN_SECTOR_DATA |
SR2.SECTOR_WITH_DELETED_DATA)) or
# data field missing (some FDCs set AM in ST1)
(sr1 == SR1.CANNOT_FIND_ID_ADDRESS
and sr2 == SR2.MISSING_ADDRESS_MARK) or
# data field missing (some FDCs don't)
(sr1 == SR1.SUCCESS and sr2 == SR2.MISSING_ADDRESS_MARK) or
# CHRN mismatch
(sr1 == SR1.CANNOT_FIND_SECTOR_ID and sr2 == SR2.SUCCESS) or
# CHRN mismatch, including wrong cylinder
(sr1 == SR1.CANNOT_FIND_SECTOR_ID
and sr2 == SR2.WRONG_CYLINDER_DETECTED)):
pass
else:
print('Unusual status flags (ST1=%02X ST2=%02X)' % (sr1, sr2))
class EDSKTrack:
gap_presync = 12
gap_4a = 80 # Post-Index
gap_1 = 50 # Post-IAM
gap_2 = 22 # Post-IDAM
gapbyte = 0x4e
def __init__(self):
self.time_per_rev = 0.2
self.clock = 2e-6
self.bits, self.weak, self.bytes = [], [], bytearray()
def raw_track(self):
track = MasterTrack(
bits = self.bits,
time_per_rev = self.time_per_rev,
weak = self.weak)
track.verify = self
track.verify_revs = 1
return track
def _find_sync(self, bits, sync, start):
for offs in bits.itersearch(sync):
if offs >= start:
return offs
return None
def verify_track(self, flux):
flux.cue_at_index()
raw = RawTrack(clock = self.clock, data = flux)
bits, _ = raw.get_all_data()
weak_iter = it.chain(self.weak, [(self.verify_len+1,1)])
weak = next(weak_iter)
# Start checking from the IAM sync
dump_start = self._find_sync(bits, mfm.iam_sync, 0)
self_start = self._find_sync(self.bits, mfm.iam_sync, 0)
# Include the IAM pre-sync header
if dump_start is None:
return False
dump_start -= self.gap_presync * 16
self_start -= self.gap_presync * 16
while self_start is not None and dump_start is not None:
# Find the weak areas immediately before and after the current
# region to be checked.
s,n = None,None
while self_start > weak[0]:
s,n = weak
weak = next(weak_iter)
# If there is a weak area preceding us, move the start point to
# immediately follow the weak area.
if s is not None:
delta = self_start - (s + n + 16)
self_start -= delta
dump_start -= delta
# Truncate the region at the next weak area, or the last sector.
self_end = max(self_start, min(weak[0], self.verify_len+1))
dump_end = dump_start + self_end - self_start
# Extract the corresponding areas from the pristine track and
# from the dump, and check that they match.
if bits[dump_start:dump_end] != self.bits[self_start:self_end]:
return False
# Find the next A1A1A1 sync pattern
dump_start = self._find_sync(bits, mfm.sync, dump_end)
self_start = self._find_sync(self.bits, mfm.sync, self_end)
# Did we verify all regions in the pristine track?
return self_start is None
class EDSK(Image):
read_only = True
def __init__(self):
self.to_track = dict()
# Find all weak ranges in the given sector data copies.
@staticmethod
def find_weak_ranges(dat, size):
orig = dat[:size]
s, w = size, []
# Find first mismatching byte across all copies
for i in range(1, len(dat)//size):
diff = [x^y for x, y in zip(orig, dat[size*i:size*(i+1)])]
weak = [idx for idx, val in enumerate(diff) if val != 0]
if weak:
s = min(s, weak[0])
# Look for runs of filler
i = s
while i < size:
j, x = i, orig[i]
while j < size and orig[j] == x:
j += 1
if j-i >= 16:
w.append((s,i-s))
s = j
i = j
# Append final weak area if any.
if s < size:
w.append((s,size-s))
return w
@staticmethod
def _build_8k_track(sectors):
if len(sectors) != 1:
return None
c,h,r,n,errs,data = sectors[0]
if n != 6:
return None
if errs.id_crc_error or errs.data_not_found or not errs.data_crc_error:
return None
# Magic longtrack value is for Coin-Op Hits. Taken from SAMdisk.
if len(data) > 6307:
data = data[:6307]
track = EDSKTrack()
t = track.bytes
# Post-index gap
t += mfm.encode(bytes([track.gapbyte] * 16))
# IAM
t += mfm.encode(bytes(track.gap_presync))
t += mfm.iam_sync_bytes
t += mfm.encode(bytes([mfm.IBM_MFM.IAM]))
t += mfm.encode(bytes([track.gapbyte] * 16))
# IDAM
t += mfm.encode(bytes(track.gap_presync))
t += mfm.sync_bytes
am = bytes([0xa1, 0xa1, 0xa1, mfm.IBM_MFM.IDAM, c, h, r, n])
crc = mfm.crc16.new(am).crcValue
am += struct.pack('>H', crc)
t += mfm.encode(am[3:])
t += mfm.encode(bytes([track.gapbyte] * track.gap_2))
# DAM
t += mfm.encode(bytes(track.gap_presync))
t += mfm.sync_bytes
dmark = (mfm.IBM_MFM.DDAM if errs.deleted_dam
else mfm.IBM_MFM.DAM)
am = bytes([0xa1, 0xa1, 0xa1, dmark]) + data
t += mfm.encode(am[3:])
return track
@staticmethod
def _build_kbi19_track(sectors):
ids = [0,1,4,7,10,13,16,2,5,8,11,14,17,3,6,9,12,15,18]
if len(sectors) != len(ids):
return None
for s,id in zip(sectors,ids):
c,h,r,n,_,_ = s
if r != id or n != 2:
return None
def addcrc(t,n):
crc = mfm.crc16.new(mfm.decode(t[-n*2:])).crcValue
t += mfm.encode(struct.pack('>H', crc))
track = EDSKTrack()
t = track.bytes
# Post-index gap
t += mfm.encode(bytes([track.gapbyte] * 64))
# IAM
t += mfm.encode(bytes(track.gap_presync))
t += mfm.iam_sync_bytes
t += mfm.encode(bytes([mfm.IBM_MFM.IAM]))
t += mfm.encode(bytes([track.gapbyte] * 50))
for idx, s in enumerate(sectors):
c,h,r,n,errs,data = s
# IDAM
t += mfm.encode(bytes(track.gap_presync))
t += mfm.sync_bytes
t += mfm.encode(bytes([mfm.IBM_MFM.IDAM, c, h, r, n]))
addcrc(t, 8)
if r == 0:
t += mfm.encode(bytes([track.gapbyte] * 17))
t += mfm.encode(b' KBI ')
else:
t += mfm.encode(bytes([track.gapbyte] * 8))
t += mfm.encode(b' KBI ')
t += mfm.encode(bytes([track.gapbyte] * 9))
# DAM
t += mfm.encode(bytes(track.gap_presync))
t += mfm.sync_bytes
dmark = (mfm.IBM_MFM.DDAM if errs.deleted_dam
else mfm.IBM_MFM.DAM)
t += mfm.encode(bytes([dmark]))
if idx%3 != 0:
t += mfm.encode(data[:61])
elif r == 0:
t += mfm.encode(data[:512])
addcrc(t,516)
else:
t += mfm.encode(data[0:0x10e])
addcrc(t,516)
t += mfm.encode(data[0x110:0x187])
addcrc(t,516)
t += mfm.encode(data[0x189:0x200])
addcrc(t,516)
t += mfm.encode(bytes([track.gapbyte] * 80))
return track
@classmethod
def from_file(cls, name):
with open(name, "rb") as f:
dat = f.read()
edsk = cls()
sig, creator, ncyls, nsides, track_sz = struct.unpack(
'<34s14s2BH', dat[:52])
if sig[:8] == b'MV - CPC':
extended = False
elif sig[:16] == b'EXTENDED CPC DSK':
extended = True
else:
raise error.Fatal('Unrecognised CPC DSK file: bad signature')
if extended:
track_sizes = list(dat[52:52+ncyls*nsides])
track_sizes = list(map(lambda x: x*256, track_sizes))
else:
track_sizes = [track_sz] * (ncyls * nsides)
o = 256 # skip disk header and track-size table
for track_size in track_sizes:
if track_size == 0:
continue
sig, cyl, head, sec_sz, nsecs, gap_3, filler = struct.unpack(
'<12s4x2B2x4B', dat[o:o+24])
error.check(sig == b'Track-Info\r\n',
'EDSK: Missing track header')
error.check((cyl, head) not in edsk.to_track,
'EDSK: Track specified twice')
bad_crc_clip_data = False
while True:
track = EDSKTrack()
t = track.bytes
# Post-index gap
t += mfm.encode(bytes([track.gapbyte] * track.gap_4a))
# IAM
t += mfm.encode(bytes(track.gap_presync))
t += mfm.iam_sync_bytes
t += mfm.encode(bytes([mfm.IBM_MFM.IAM]))
t += mfm.encode(bytes([track.gapbyte] * track.gap_1))
sh = dat[o+24:o+24+8*nsecs]
data_pos = o + 256 # skip track header and sector-info table
clippable, ngap3, sectors, idam_included = 0, 0, [], False
while sh:
c, h, r, n, stat1, stat2, data_size = struct.unpack(
'<6BH', sh[:8])
sh = sh[8:]
native_size = mfm.sec_sz(n)
weak = []
errs = SectorErrors(stat1, stat2)
num_copies = 0 if errs.data_not_found else 1
if not extended:
data_size = mfm.sec_sz(sec_sz)
sec_data = dat[data_pos:data_pos+data_size]
data_pos += data_size
if (extended
and data_size > native_size
and errs.data_crc_error
and (data_size % native_size == 0
or data_size == 49152)):
num_copies = (3 if data_size == 49152
else data_size // native_size)
data_size //= num_copies
weak = cls().find_weak_ranges(sec_data, data_size)
sec_data = sec_data[:data_size]
sectors.append((c,h,r,n,errs,sec_data))
# IDAM
if not idam_included:
t += mfm.encode(bytes(track.gap_presync))
t += mfm.sync_bytes
am = bytes([0xa1, 0xa1, 0xa1, mfm.IBM_MFM.IDAM,
c, h, r, n])
crc = mfm.crc16.new(am).crcValue
if errs.id_crc_error:
crc ^= 0x5555
am += struct.pack('>H', crc)
t += mfm.encode(am[3:])
t += mfm.encode(bytes([track.gapbyte] * track.gap_2))
# DAM
gap_included, idam_included = False, False
if errs.id_crc_error or errs.data_not_found:
continue
t += mfm.encode(bytes(track.gap_presync))
t += mfm.sync_bytes
track.weak += [((s+len(t)//2+1)*16, n*16) for s,n in weak]
dmark = (mfm.IBM_MFM.DDAM if errs.deleted_dam
else mfm.IBM_MFM.DAM)
if errs.data_crc_error:
if sh:
# Look for next IDAM
idam = bytes([0]*12 + [0xa1]*3
+ [mfm.IBM_MFM.IDAM])
idx = sec_data.find(idam)
else:
# Last sector: Look for GAP3
idx = sec_data.find(bytes([track.gapbyte]*8))
if idx > 0:
# 2 + gap_3 = CRC + GAP3 (because gap_included)
clippable += data_size - idx + 2 + gap_3
if bad_crc_clip_data:
data_size = idx
sec_data = sec_data[:data_size]
gap_included = True
elif data_size < native_size:
# Pad short data
sec_data += bytes(native_size - data_size)
elif data_size > native_size:
# Clip long data if it includes pre-sync 00 bytes
if (sec_data[-13] != 0
and all([v==0 for v in sec_data[-12:]])):
# Includes next pre-sync: Clip it.
sec_data = sec_data[:-12]
if sh:
# Look for next IDAM
idam = bytes([0]*12 + [0xa1]*3 + [mfm.IBM_MFM.IDAM]
+ list(sh[:4]))
idx = sec_data.find(idam)
if idx > native_size:
# Sector data includes next IDAM. Output it
# here and skip it on next iteration.
t += mfm.encode(bytes([dmark]))
t += mfm.encode(sec_data[:idx+12])
t += mfm.sync_bytes
t += mfm.encode(sec_data[idx+12+3:])
idam_included = True
continue
# Long data includes CRC and GAP
gap_included = True
if gap_included:
t += mfm.encode(bytes([dmark]))
t += mfm.encode(sec_data)
continue
am = bytes([0xa1, 0xa1, 0xa1, dmark]) + sec_data
crc = mfm.crc16.new(am).crcValue
if errs.data_crc_error:
crc ^= 0x5555
am += struct.pack('>H', crc)
t += mfm.encode(am[3:])
if sh:
# GAP3 for all but last sector
t += mfm.encode(bytes([track.gapbyte] * gap_3))
ngap3 += 1
# Special track handlers
special_track = cls()._build_8k_track(sectors)
if special_track is None:
special_track = cls()._build_kbi19_track(sectors)
if special_track is not None:
track = special_track
break
# The track may be too long to fit: Check for overhang.
tracklen = int((track.time_per_rev / track.clock) / 16)
overhang = int(len(t)//2 - tracklen*0.99)
if overhang <= 0:
break
# Some EDSK tracks with Bad CRC contain a raw dump following
# the DAM. This can usually be clipped.
if clippable and not bad_crc_clip_data:
bad_crc_clip_data = True
continue
# Some EDSK images have bogus GAP3 values. Shrink it if
# necessary.
new_gap_3 = -1
if ngap3 != 0:
new_gap_3 = gap_3 - math.ceil(overhang / ngap3)
error.check(new_gap_3 >= 0,
'EDSK: Track %d.%d is too long '
'(%d bits @ GAP3=%d; %d bits @ GAP3=0)'
% (cyl, head, len(t)*8, gap_3,
(len(t)//2-gap_3*ngap3)*16))
#print('EDSK: GAP3 reduced (%d -> %d)' % (gap_3, new_gap_3))
gap_3 = new_gap_3
# Pre-index gap
track.verify_len = len(track.bytes)*8
tracklen = int((track.time_per_rev / track.clock) / 16)
gap = max(40, tracklen - len(t)//2)
track.bytes += mfm.encode(bytes([track.gapbyte] * gap))
# Add the clock buts
track.bits = bitarray(endian='big')
track.bits.frombytes(mfm.mfm_encode(track.bytes))
# Register the track
edsk.to_track[cyl,head] = track
o += track_size
return edsk
def get_track(self, cyl, side):
if (cyl,side) not in self.to_track:
return None
return self.to_track[cyl,side].raw_track()
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,135 +0,0 @@
# greaseweazle/image/hfe.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import struct
from greaseweazle import error
from greaseweazle.track import MasterTrack, RawTrack
from bitarray import bitarray
from .image import Image
class HFE(Image):
def __init__(self):
self.bitrate = 250 # XXX real bitrate?
# Each track is (bitlen, rawbytes).
# rawbytes is a bytes() object in little-endian bit order.
self.to_track = dict()
@classmethod
def from_file(cls, name):
with open(name, "rb") as f:
dat = f.read()
(sig, f_rev, n_cyl, n_side, t_enc, bitrate,
_, _, _, tlut_base) = struct.unpack("<8s4B2H2BH", dat[:20])
error.check(sig != b"HXCHFEV3", "HFEv3 is not supported")
error.check(sig == b"HXCPICFE" and f_rev <= 1, "Not a valid HFE file")
error.check(0 < n_cyl, "HFE: Invalid #cyls")
error.check(0 < n_side < 3, "HFE: Invalid #sides")
error.check(bitrate != 0, "HFE: Invalid bitrate")
hfe = cls()
hfe.bitrate = bitrate
tlut = dat[tlut_base*512:tlut_base*512+n_cyl*4]
for cyl in range(n_cyl):
for side in range(n_side):
offset, length = struct.unpack("<2H", tlut[cyl*4:(cyl+1)*4])
todo = length // 2
tdat = bytes()
while todo:
d_off = offset*512 + side*256
d_nr = 256 if todo > 256 else todo
tdat += dat[d_off:d_off+d_nr]
todo -= d_nr
offset += 1
hfe.to_track[cyl,side] = (len(tdat)*8, tdat)
return hfe
def get_track(self, cyl, side):
if (cyl,side) not in self.to_track:
return None
bitlen, rawbytes = self.to_track[cyl,side]
tdat = bitarray(endian='little')
tdat.frombytes(rawbytes)
track = MasterTrack(
bits = tdat[:bitlen],
time_per_rev = bitlen / (2000*self.bitrate))
return track
def emit_track(self, cyl, side, track):
flux = track.flux()
flux.cue_at_index()
raw = RawTrack(clock = 5e-4 / self.bitrate, data = flux)
bits, _ = raw.get_revolution(0)
bits.bytereverse()
self.to_track[cyl,side] = (len(bits), bits.tobytes())
def get_image(self):
n_side = 1
n_cyl = max(self.to_track.keys(), default=(0), key=lambda x:x[0])[0]
n_cyl += 1
# We dynamically build the Track-LUT and -Data arrays.
tlut = bytearray()
tdat = bytearray()
# Stuff real data into the image.
for i in range(n_cyl):
s0 = self.to_track[i,0] if (i,0) in self.to_track else None
s1 = self.to_track[i,1] if (i,1) in self.to_track else None
if s0 is None and s1 is None:
# Dummy data for empty cylinders. Assumes 300RPM.
nr_bytes = 100 * self.bitrate
tlut += struct.pack("<2H", len(tdat)//512 + 2, nr_bytes)
tdat += bytes([0x88] * (nr_bytes+0x1ff & ~0x1ff))
else:
# At least one side of this cylinder is populated.
if s1 is not None:
n_side = 2
bc = [s0 if s0 is not None else (0,bytes()),
s1 if s1 is not None else (0,bytes())]
nr_bytes = max(len(t[1]) for t in bc)
nr_blocks = (nr_bytes + 0xff) // 0x100
tlut += struct.pack("<2H", len(tdat)//512 + 2, 2 * nr_bytes)
for b in range(nr_blocks):
for t in bc:
slice = t[1][b*256:(b+1)*256]
tdat += slice + bytes([0x88] * (256 - len(slice)))
# Construct the image header.
header = struct.pack("<8s4B2H2BH",
b"HXCPICFE",
0,
n_cyl,
n_side,
0xff, # unknown encoding
self.bitrate,
0, # rpm (unused)
0xff, # unknown interface
1, # rsvd
1) # track list offset
# Pad the header and TLUT to 512-byte blocks.
header += bytes([0xff] * (0x200 - len(header)))
tlut += bytes([0xff] * (0x200 - len(tlut)))
return header + tlut + tdat
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,72 +0,0 @@
# greaseweazle/image/image.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import os
from greaseweazle import error
class Image:
read_only = False
## Context manager for image objects created using .to_file()
def __enter__(self):
self.file = open(self.filename, "wb")
return self
def __exit__(self, type, value, tb):
try:
if type is None:
# No error: Normal writeout.
self.file.write(self.get_image())
finally:
# Always close the file.
self.file.close()
if type is not None:
# An error occurred: We remove the target file.
os.remove(self.filename)
## Default .to_file() constructor
@classmethod
def to_file(cls, name, fmt=None):
error.check(not cls.read_only,
"%s: Cannot create %s image files" % (name, cls.__name__))
obj = cls()
obj.filename = name
obj.fmt = fmt
return obj
# Maximum non-empty cylinder on each head, or -1 if no cylinders exist.
# Returns a list of integers, indexed by head.
def max_cylinder(self):
r = list()
for h in range(2):
for c in range(100, -2, -1):
if c < 0 or self.get_track(c,h) is not None:
r.append(c)
break
return r
## Above methods and class variables can be overridden by subclasses.
## Additionally, subclasses must provide following public interfaces:
## Read support:
# def from_file(cls, name)
# def get_track(self, cyl, side)
## Write support (if not cls.read_only):
# def emit_track(self, cyl, side, track)
## Plus either:
# def get_image(self)
## Or:
# __enter__ / __exit__
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,84 +0,0 @@
# greaseweazle/image/img.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
from greaseweazle import error
from greaseweazle.codec.ibm import mfm
from .image import Image
from greaseweazle.codec import formats
class IMG(Image):
sides_swapped = False
def __init__(self, name, fmt):
self.to_track = dict()
error.check(fmt is not None and fmt.img_compatible, """\
Sector image requires compatible format specifier
Compatible formats:\n%s"""
% formats.print_formats(
lambda k, v: v.img_compatible))
self.filename = name
self.fmt = fmt
@classmethod
def from_file(cls, name, fmt):
with open(name, "rb") as f:
dat = f.read()
img = cls(name, fmt)
pos = 0
for t in fmt.max_tracks:
cyl, head = t.cyl, t.head
if img.sides_swapped:
head ^= 1
track = fmt.fmt(cyl, head)
pos += track.set_img_track(dat[pos:])
img.to_track[cyl,head] = track
return img
@classmethod
def to_file(cls, name, fmt=None):
return cls(name, fmt)
def get_track(self, cyl, side):
if (cyl,side) not in self.to_track:
return None
return self.to_track[cyl,side].raw_track()
def emit_track(self, cyl, side, track):
self.to_track[cyl,side] = track
def get_image(self):
tdat = bytearray()
n_side = 2
n_cyl = max(self.to_track.keys(), default=(0), key=lambda x:x[0])[0]
n_cyl += 1
for cyl in range(n_cyl):
for head in range(n_side):
if self.sides_swapped:
head ^= 1
if (cyl,head) in self.to_track:
tdat += self.to_track[cyl,head].get_img_track()
return tdat
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,338 +0,0 @@
# greaseweazle/image/ipf.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import os, sys
import platform
import ctypes as ct
import itertools as it
from bitarray import bitarray
from greaseweazle.track import MasterTrack, RawTrack
from greaseweazle import error
from .image import Image
class CapsDateTimeExt(ct.Structure):
_pack_ = 1
_fields_ = [
('year', ct.c_uint),
('month', ct.c_uint),
('day', ct.c_uint),
('hour', ct.c_uint),
('min', ct.c_uint),
('sec', ct.c_uint),
('tick', ct.c_uint)]
class CapsImageInfo(ct.Structure):
platform_name = [
"N/A", "Amiga", "Atari ST", "IBM PC", "Amstrad CPC",
"Spectrum", "Sam Coupe", "Archimedes", "C64", "Atari (8-bit)" ]
_pack_ = 1
_fields_ = [
('type', ct.c_uint), # image type
('release', ct.c_uint), # release ID
('revision', ct.c_uint), # release revision ID
('mincylinder', ct.c_uint), # lowest cylinder number
('maxcylinder', ct.c_uint), # highest cylinder number
('minhead', ct.c_uint), # lowest head number
('maxhead', ct.c_uint), # highest head number
('crdt', CapsDateTimeExt), # image creation date.time
('platform', ct.c_uint * 4)] # intended platform(s)
class CapsTrackInfoT2(ct.Structure):
_pack_ = 1
_fields_ = [
('type', ct.c_uint), # track type
('cylinder', ct.c_uint), # cylinder#
('head', ct.c_uint), # head#
('sectorcnt', ct.c_uint), # available sectors
('sectorsize', ct.c_uint), # sector size, unused
('trackbuf', ct.POINTER(ct.c_ubyte)), # track buffer memory
('tracklen', ct.c_uint), # track buffer memory length
('timelen', ct.c_uint), # timing buffer length
('timebuf', ct.POINTER(ct.c_uint)), # timing buffer
('overlap', ct.c_int), # overlap position
('startbit', ct.c_uint), # start position of the decoding
('wseed', ct.c_uint), # weak bit generator data
('weakcnt', ct.c_uint)] # number of weak data areas
class CapsSectorInfo(ct.Structure):
_pack_ = 1
_fields_ = [
('descdatasize', ct.c_uint), # data size in bits from IPF descriptor
('descgapsize', ct.c_uint), # gap size in bits from IPF descriptor
('datasize', ct.c_uint), # data size in bits from decoder
('gapsize', ct.c_uint), # gap size in bits from decoder
('datastart', ct.c_uint), # data start pos in bits from decoder
('gapstart', ct.c_uint), # gap start pos in bits from decoder
('gapsizews0', ct.c_uint), # gap size before write splice
('gapsizews1', ct.c_uint), # gap size after write splice
('gapws0mode', ct.c_uint), # gap size mode before write splice
('gapws1mode', ct.c_uint), # gap size mode after write splice
('celltype', ct.c_uint), # bitcell type
('enctype', ct.c_uint)] # encoder type
class CapsDataInfo(ct.Structure):
_pack_ = 1
_fields_ = [
('type', ct.c_uint), # data type
('start', ct.c_uint), # start position
('size', ct.c_uint)] # size in bits
class DI_LOCK:
INDEX = 1<<0
ALIGN = 1<<1
DENVAR = 1<<2
DENAUTO = 1<<3
DENNOISE = 1<<4
NOISE = 1<<5
NOISEREV = 1<<6
MEMREF = 1<<7
UPDATEFD = 1<<8
TYPE = 1<<9
DENALT = 1<<10
OVLBIT = 1<<11
TRKBIT = 1<<12
NOUPDATE = 1<<13
SETWSEED = 1<<14
def_flags = (DENVAR | UPDATEFD | TYPE | OVLBIT | TRKBIT)
class IPFTrack(MasterTrack):
verify_revs = 2
tolerance = 100
@staticmethod
def strong_data(sector, weak):
"""Return list of sector data areas excluding weak sections."""
def range_next(i):
s,l = next(i)
return s, s+l
weak_tol = 16 # Skip this number of bits after a weak area
weak_iter = it.chain(weak, [(1<<30,1)])
ws,we = -1,-1
sector_iter = iter(sector)
s,e = range_next(sector_iter)
try:
while True:
while we <= s:
ws,we = range_next(weak_iter)
we += weak_tol
if ws < e:
if s < ws:
yield (s,ws-s)
s = we
else:
yield (s,e-s)
s = e
if s >= e:
s,e = range_next(sector_iter)
except StopIteration:
pass
def verify_track(self, flux):
flux.cue_at_index()
raw = RawTrack(clock = self.time_per_rev/len(self.bits), data = flux)
raw_bits, _ = raw.get_all_data()
for s,l in IPFTrack.strong_data(self.sectors, self.weak):
sector = self.bits[s:s+l]
# Search within an area +/- the pre-defined # bitcells tolerance
raw_area = raw_bits[max(self.splice + s - self.tolerance, 0)
: self.splice + s + l + self.tolerance]
# All we care about is at least one match (this is a bit fuzzy)
if next(raw_area.itersearch(sector), None) is None:
return False
return True
class IPF(Image):
read_only = True
def __init__(self):
self.lib = get_libcaps()
def __del__(self):
try:
self.lib.CAPSUnlockAllTracks(self.iid)
self.lib.CAPSUnlockImage(self.iid)
self.lib.CAPSRemImage(self.iid)
del(self.iid)
except AttributeError:
pass
def __str__(self):
pi = self.pi
s = "IPF Image File:"
s += "\n SPS ID: %04d (rev %d)" % (pi.release, pi.revision)
s += "\n Platform: "
nr_platforms = 0
for p in pi.platform:
if p == 0 and nr_platforms != 0:
break
if nr_platforms > 0:
s += ", "
s += pi.platform_name[p]
nr_platforms += 1
s += ("\n Created: %d/%d/%d %02d:%02d:%02d"
% (pi.crdt.year, pi.crdt.month, pi.crdt.day,
pi.crdt.hour, pi.crdt.min, pi.crdt.sec))
s += ("\n Cyls: %d-%d Heads: %d-%d"
% (pi.mincylinder, pi.maxcylinder, pi.minhead, pi.maxhead))
return s
@classmethod
def from_file(cls, name):
ipf = cls()
ipf.iid = ipf.lib.CAPSAddImage()
error.check(ipf.iid >= 0, "Could not create IPF image container")
cname = ct.c_char_p(name.encode())
res = ipf.lib.CAPSLockImage(ipf.iid, cname)
error.check(res == 0, "Could not open IPF image '%s'" % name)
res = ipf.lib.CAPSLoadImage(ipf.iid, DI_LOCK.def_flags)
error.check(res == 0, "Could not load IPF image '%s'" % name)
ipf.pi = CapsImageInfo()
res = ipf.lib.CAPSGetImageInfo(ct.byref(ipf.pi), ipf.iid)
error.check(res == 0, "Could not get info for IPF '%s'" % name)
print(ipf)
return ipf
def get_track(self, cyl, head):
pi = self.pi
if head < pi.minhead or head > pi.maxhead:
return None
if cyl < pi.mincylinder or cyl > pi.maxcylinder:
return None
ti = CapsTrackInfoT2(2)
res = self.lib.CAPSLockTrack(ct.byref(ti), self.iid,
cyl, head, DI_LOCK.def_flags)
error.check(res == 0, "Could not lock IPF track %d.%d" % (cyl, head))
if not ti.trackbuf:
return None # unformatted/empty
carray_type = ct.c_ubyte * ((ti.tracklen+7)//8)
carray = carray_type.from_address(
ct.addressof(ti.trackbuf.contents))
trackbuf = bitarray(endian='big')
trackbuf.frombytes(bytes(carray))
trackbuf = trackbuf[:ti.tracklen]
data = []
for i in range(ti.sectorcnt):
si = CapsSectorInfo()
res = self.lib.CAPSGetInfo(ct.byref(si), self.iid,
cyl, head, 1, i)
error.check(res == 0, "Couldn't get sector info")
# Adjust the range start to be splice- rather than index-relative
data.append(((si.datastart - ti.overlap) % ti.tracklen,
si.datasize))
weak = []
for i in range(ti.weakcnt):
wi = CapsDataInfo()
res = self.lib.CAPSGetInfo(ct.byref(wi), self.iid,
cyl, head, 2, i)
error.check(res == 0, "Couldn't get weak data info")
# Adjust the range start to be splice- rather than index-relative
weak.append(((wi.start - ti.overlap) % ti.tracklen, wi.size))
timebuf = None
if ti.timebuf:
carray_type = ct.c_uint * ti.timelen
carray = carray_type.from_address(
ct.addressof(ti.timebuf.contents))
# Unpack the per-byte timing info into per-bitcell
timebuf = []
for i in carray:
for j in range(8):
timebuf.append(i)
# Pad the timing info with normal cell lengths as necessary
for j in range(len(carray)*8, ti.tracklen):
timebuf.append(1000)
# Clip the timing info, if necessary.
timebuf = timebuf[:ti.tracklen]
# Rotate the track to start at the splice rather than the index.
if ti.overlap:
trackbuf = trackbuf[ti.overlap:] + trackbuf[:ti.overlap]
if timebuf:
timebuf = timebuf[ti.overlap:] + timebuf[:ti.overlap]
# We don't really have access to the bitrate. It depends on RPM.
# So we assume a rotation rate of 300 RPM (5 rev/sec).
rpm = 300
track = IPFTrack(
bits = trackbuf,
time_per_rev = 60/rpm,
bit_ticks = timebuf,
splice = ti.overlap,
weak = weak)
track.verify = track
track.sectors = data
return track
# Open and initialise the CAPS library.
def open_libcaps():
# Get the OS-dependent list of valid CAPS library names.
_names = []
if platform.system() == "Linux":
_names = [ "libcapsimage.so.5", "libcapsimage.so.5.1",
"libcapsimage.so.4", "libcapsimage.so.4.2",
"libcapsimage.so" ]
elif platform.system() == "Darwin":
_names = [ "CAPSImage.framework/CAPSImage" ]
elif platform.system() == "Windows":
_names = [ "CAPSImg_x64.dll", "CAPSImg.dll" ]
# Get the absolute path to the root Greaseweazle folder.
path = os.path.dirname(os.path.abspath(__file__))
for _ in range(3):
path = os.path.join(path, os.pardir)
path = os.path.normpath(path)
# Create a search list of both relative and absolute library names.
names = []
for name in _names:
names.append(name)
names.append(os.path.join(path, name))
# Walk the search list, trying to open the CAPS library.
for name in names:
try:
lib = ct.cdll.LoadLibrary(name)
break
except:
pass
error.check("lib" in locals(), """\
Could not find SPS/CAPS IPF decode library
For installation instructions please read the wiki:
<https://github.com/keirf/Greaseweazle/wiki/IPF-Images>""")
# We have opened the library. Now initialise it.
res = lib.CAPSInit()
error.check(res == 0, "Failure initialising IPF library '%s'" % name)
return lib
# Get a reference to the CAPS library. Open it if necessary.
def get_libcaps():
global libcaps
if not 'libcaps' in globals():
libcaps = open_libcaps()
return libcaps
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,234 +0,0 @@
# greaseweazle/image/kryoflux.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import struct, re, math, os
import itertools as it
from greaseweazle import error
from greaseweazle.flux import Flux
from .image import Image
mck = 18432000 * 73 / 14 / 2
sck = mck / 2
class Op:
Nop1 = 8
Nop2 = 9
Nop3 = 10
Ovl16 = 11
Flux3 = 12
OOB = 13
class OOB:
StreamInfo = 1
Index = 2
StreamEnd = 3
KFInfo = 4
EOF = 13
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()]
@classmethod
def to_file(cls, name, fmt=None):
return cls(name)
@classmethod
def from_file(cls, name):
return cls(name)
def get_track(self, cyl, side):
name = self.basename + '%02d.%d.raw' % (cyl, side)
try:
with open(name, 'rb') as f:
dat = f.read()
except FileNotFoundError:
return None
# Parse the index-pulse stream positions.
index = []
idx = 0
while idx < len(dat):
op = dat[idx]
if op == Op.OOB:
oob_op, oob_sz = struct.unpack('<BH', dat[idx+1:idx+4])
idx += 4
if oob_op == OOB.Index:
pos, = struct.unpack('<I', dat[idx:idx+4])
index.append(pos)
elif oob_op == OOB.EOF:
break
idx += oob_sz
elif op == Op.Nop3 or op == Op.Flux3:
idx += 3
elif op <= 7 or op == Op.Nop2:
idx += 2
else:
idx += 1
# Build the flux and index lists for the Flux object.
flux, flux_list, index_list = [], [], []
val, index_idx, stream_idx, idx = 0, 0, 0, 0
while idx < len(dat):
if index_idx < len(index) and stream_idx >= index[index_idx]:
# We've passed an index marker.
index_list.append(sum(flux))
flux_list += flux
flux = []
index_idx += 1
op = dat[idx]
if op <= 7:
# Flux2
val += (op << 8) + dat[idx+1]
flux.append(val)
val = 0
stream_idx += 2
idx += 2
elif op <= 10:
# Nop1, Nop2, Nop3
nr = op-7
stream_idx += nr
idx += nr
elif op == Op.Ovl16:
# Ovl16
val += 0x10000
stream_idx += 1
idx += 1
elif op == Op.Flux3:
# Flux3
val += (dat[idx+1] << 8) + dat[idx+2]
flux.append(val)
val = 0
stream_idx += 3
idx += 3
elif op == Op.OOB:
# OOB
oob_op, oob_sz = struct.unpack('<BH', dat[idx+1:idx+4])
idx += 4
if oob_op == OOB.StreamInfo or oob_op == OOB.StreamEnd:
pos, = struct.unpack('<I', dat[idx:idx+4])
error.check(pos == stream_idx,
"Out-of-sync during KryoFlux stream read")
elif oob_op == OOB.EOF:
break
idx += oob_sz
else:
# Flux1
val += op
flux.append(val)
val = 0
stream_idx += 1
idx += 1
flux_list += flux
# Crop partial first revolution.
if len(index_list) > 1:
short_index, index_list = index_list[0], index_list[1:]
flux = 0
for i in range(len(flux_list)):
if flux >= short_index:
break
flux += flux_list[i]
flux_list = flux_list[i:]
return Flux(index_list, flux_list, sck)
def emit_track(self, cyl, side, track):
"""Converts @track into a KryoFlux stream file."""
# Check if we should insert an OOB record for the next index mark.
def check_index(prev_flux):
nonlocal index_idx, dat
if index_idx < len(index) and total >= index[index_idx]:
dat += struct.pack('<2BH3I', Op.OOB, OOB.Index, 12,
stream_idx,
round(index[index_idx] - total + prev_flux),
round(index[index_idx]/8))
index_idx += 1
# Emit a resampled flux value to the KryoFlux data stream.
def emit(f):
nonlocal stream_idx, dat, total
while f >= 0x10000:
stream_idx += 1
dat.append(Op.Ovl16)
f -= 0x10000
total += 0x10000
check_index(0x10000)
if f >= 0x800:
stream_idx += 3
dat += struct.pack('>BH', Op.Flux3, f)
elif Op.OOB < f < 0x100:
stream_idx += 1
dat.append(f)
else:
stream_idx += 2
dat += struct.pack('>H', f)
total += f
check_index(f)
flux = track.flux()
# HxC crashes or fails to load non-index-cued stream files.
# So let's give it what it wants.
flux.cue_at_index()
factor = sck / flux.sample_freq
dat = bytearray()
# Start the data stream with a dummy index if our Flux is index cued.
if flux.index_cued:
dat += struct.pack('<2BH3I', Op.OOB, OOB.Index, 12, 0, 0, 0)
# Prefix-sum list of resampled index timings.
index = list(it.accumulate(map(lambda x: x*factor, flux.index_list)))
index_idx = 0
stream_idx, total, rem = 0, 0, 0.0
for x in flux.list:
y = x * factor + rem
f = round(y)
rem = y - f
emit(f)
# We may not have enough flux to get to the final index value.
# Generate a dummy flux just enough to get us there.
if index_idx < len(index):
emit(math.ceil(index[index_idx] - total) + 1)
# A dummy cell so that we definitely have *something* after the
# final OOB.Index, so that all parsers should register the Index.
emit(round(sck*12e-6)) # 12us
# Emit StreamEnd and EOF blocks to terminate the stream.
dat += struct.pack('<2BH2I', Op.OOB, OOB.StreamEnd, 8, stream_idx, 0)
dat += struct.pack('<2BH', Op.OOB, OOB.EOF, 0x0d0d)
name = self.basename + '%02d.%d.raw' % (cyl, side)
with open(name, 'wb') as f:
f.write(dat)
def __enter__(self):
return self
def __exit__(self, type, value, tb):
pass
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,372 +0,0 @@
# greaseweazle/image/scp.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import struct, functools
from greaseweazle import error
from greaseweazle.flux import Flux
from .image import Image
# Names for disktype byte in SCP file header
DiskType = {
'amiga': 0x04,
'c64': 0x00,
'atari800-sd': 0x10,
'atari800-dd': 0x11,
'atari800-ed': 0x12,
'atarist-ss': 0x14,
'atarist-ds': 0x15,
'appleII': 0x20,
'appleIIpro': 0x21,
'apple-400k': 0x24,
'apple-800k': 0x25,
'apple-1m44': 0x26,
'ibmpc-320k': 0x30,
'ibmpc-720k': 0x31,
'ibmpc-1m2': 0x32,
'ibmpc-1m44': 0x33,
'trs80_sssd': 0x40,
'trs80_ssdd': 0x41,
'trs80_dssd': 0x42,
'trs80_dsdd': 0x43,
'ti-99/4a': 0x50,
'roland-d20': 0x60,
'amstrad-cpc': 0x70,
'other-320k': 0x80,
'other-1m2': 0x81,
'other-720k': 0x84,
'other-1m44': 0x85,
'tape-gcr1': 0xe0,
'tape-gcr2': 0xe1,
'tape-mfm': 0xe2,
'hdd-mfm': 0xf0,
'hdd-rll': 0xf1
}
class SCPOpts:
"""legacy_ss: Set to True to generate (incorrect) legacy single-sided
SCP image.
"""
def __init__(self):
self.legacy_ss = False
self._disktype = 0x80 # Other
@property
def disktype(self):
return self._disktype
@disktype.setter
def disktype(self, disktype):
try:
self._disktype = DiskType[disktype.lower()]
except KeyError:
try:
self._disktype = int(disktype, 0)
except ValueError:
raise error.Fatal("Bad SCP disktype: '%s'" % disktype)
class SCPTrack:
def __init__(self, tdh, dat, splice=None):
self.tdh = tdh
self.dat = dat
self.splice = splice
class SCP(Image):
# 40MHz
sample_freq = 40000000
def __init__(self):
self.opts = SCPOpts()
self.nr_revs = None
self.to_track = dict()
self.index_cued = True
def side_count(self):
s = [0,0] # non-empty tracks on each side
for tnr in self.to_track:
s[tnr&1] += 1
return s
@classmethod
def from_file(cls, name):
splices = None
with open(name, "rb") as f:
dat = f.read()
header = struct.unpack("<3s9BI", dat[0:16])
sig, _, disk_type, nr_revs, _, _, flags, _, single_sided, _, _ = header
error.check(sig == b"SCP", "SCP: Bad signature")
index_cued = flags & 1 or nr_revs == 1
# Some tools generate a short TLUT. We handle this by truncating the
# TLUT at the first Track Data Header.
trk_offs = struct.unpack("<168I", dat[16:0x2b0])
for i in range(168):
try:
off = trk_offs[i]
except IndexError:
break
if off == 0 or off >= 0x2b0:
continue
off = off//4 - 4
error.check(off >= 0, "SCP: Bad Track Table")
trk_offs = trk_offs[:off]
# Parse the extension block introduced by github:markusC64/g64conv.
# 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])
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
while end - pos >= 8:
chk_sig, chk_len = struct.unpack('<4sI', dat[pos:pos+8])
pos += 8
# WRSP: WRite SPlice information block.
# Data is comprised of >= 169 32-bit values:
# 0: Flags (currently unused; must be zero)
# N: Write splice/overlap position for track N, in SCP ticks
# (zero if the track is unused)
if chk_sig == b'WRSP' and chk_len >= 169*4:
# Write-splice positions for writing out SCP tracks
# correctly to disk.
splices = struct.unpack('<168I', dat[pos+4:pos+169*4])
pos += chk_len
scp = cls()
scp.nr_revs = nr_revs
if not index_cued:
scp.nr_revs -= 1
for trknr in range(len(trk_offs)):
trk_off = trk_offs[trknr]
if trk_off == 0:
continue
# Parse the SCP track header and extract the flux data.
thdr = dat[trk_off:trk_off+4+12*nr_revs]
sig, tnr = struct.unpack("<3sB", thdr[:4])
error.check(sig == b"TRK", "SCP: Missing track signature")
error.check(tnr == trknr, "SCP: Wrong track number in header")
thdr = thdr[4:] # Remove TRK header
if not index_cued: # Remove first partial revolution
thdr = thdr[12:]
s_off, = struct.unpack("<I", thdr[8:12])
_, e_nr, e_off = struct.unpack("<3I", thdr[-12:])
e_off += e_nr*2
if s_off == e_off:
# FluxEngine creates dummy TDHs for empty tracks.
# Bail on them here.
continue
tdat = dat[trk_off+s_off:trk_off+e_off]
track = SCPTrack(thdr, tdat)
if splices is not None:
track.splice = splices[trknr]
scp.to_track[trknr] = track
s = scp.side_count()
# C64 images with halftracks are genberated by Supercard Pro using
# consecutive track numbers. That needs fixup here for our layout.
# We re-use the legacy-single-sided fixup below.
if (single_sided == 0 and disk_type == 0
and s[1] and s[0]==s[1]+1 and s[0] < 42):
single_sided = 1
print('SCP: Importing C64 image with halftracks')
# Some tools produce (or used to produce) single-sided images using
# consecutive entries in the TLUT. This needs fixing up.
if single_sided and s[0] and s[1]:
new_dict = dict()
for tnr in scp.to_track:
new_dict[tnr*2+single_sided-1] = scp.to_track[tnr]
scp.to_track = new_dict
print('SCP: Imported legacy single-sided image')
return scp
def get_track(self, cyl, side):
tracknr = cyl * 2 + side
if not tracknr in self.to_track:
return None
track = self.to_track[tracknr]
tdh, dat = track.tdh, track.dat
index_list = []
while tdh:
ticks, _, _ = struct.unpack("<3I", tdh[:12])
index_list.append(ticks)
tdh = tdh[12:]
# Decode the SCP flux data into a simple list of flux times.
flux_list = []
val = 0
for i in range(0, len(dat), 2):
x = dat[i]*256 + dat[i+1]
if x == 0:
val += 65536
continue
flux_list.append(val + x)
val = 0
flux = Flux(index_list, flux_list, SCP.sample_freq)
flux.splice = track.splice if track.splice is not None else 0
return flux
def emit_track(self, cyl, side, track):
"""Converts @track into a Supercard Pro Track and appends it to
the current image-in-progress.
"""
flux = track.flux()
# External tools and emulators generally seem to work best (or only)
# with index-cued SCP image files. So let's make sure we give them
# what they want.
flux.cue_at_index()
if not flux.index_cued:
self.index_cued = False
nr_revs = len(flux.index_list)
if not self.nr_revs:
self.nr_revs = nr_revs
else:
assert self.nr_revs == nr_revs
factor = SCP.sample_freq / flux.sample_freq
tdh, dat = bytearray(), bytearray()
len_at_index = rev = 0
to_index = flux.index_list[0]
rem = 0.0
for x in flux.list:
# Does the next flux interval cross the index mark?
while to_index < x:
# Append to the TDH for the previous full revolution
tdh += struct.pack("<III",
round(flux.index_list[rev]*factor),
(len(dat) - len_at_index) // 2,
4 + nr_revs*12 + len_at_index)
# Set up for the next revolution
len_at_index = len(dat)
rev += 1
if rev >= nr_revs:
# We're done: We simply discard any surplus flux samples
self.to_track[cyl*2+side] = SCPTrack(tdh, dat)
return
to_index += flux.index_list[rev]
# Process the current flux sample into SCP "bitcell" format
to_index -= x
y = x * factor + rem
val = round(y)
if (val & 65535) == 0:
val += 1
rem = y - val
while val >= 65536:
dat.append(0)
dat.append(0)
val -= 65536
dat.append(val>>8)
dat.append(val&255)
# Header for last track(s) in case we ran out of flux timings.
while rev < nr_revs:
tdh += struct.pack("<III",
round(flux.index_list[rev]*factor),
(len(dat) - len_at_index) // 2,
4 + nr_revs*12 + len_at_index)
len_at_index = len(dat)
rev += 1
self.to_track[cyl*2+side] = SCPTrack(tdh, dat)
def get_image(self):
# Work out the single-sided byte code
s = self.side_count()
if s[0] and s[1]:
single_sided = 0
elif s[0]:
single_sided = 1
else:
single_sided = 2
to_track = self.to_track
if single_sided and self.opts.legacy_ss:
print('SCP: Generated legacy single-sided image')
to_track = dict()
for tnr in self.to_track:
to_track[tnr//2] = self.to_track[tnr]
ntracks = max(to_track, default=0) + 1
# Generate the TLUT and concatenate all the tracks together.
trk_offs = bytearray()
trk_dat = bytearray()
for tnr in range(ntracks):
if tnr in to_track:
track = to_track[tnr]
trk_offs += struct.pack("<I", 0x2b0 + len(trk_dat))
trk_dat += struct.pack("<3sB", b"TRK", tnr)
trk_dat += track.tdh + track.dat
else:
trk_offs += struct.pack("<I", 0)
error.check(len(trk_offs) <= 0x2a0, "SCP: Too many tracks")
trk_offs += bytes(0x2a0 - len(trk_offs))
# Calculate checksum over all data (except 16-byte image header).
csum = 0
for x in trk_offs:
csum += x
for x in trk_dat:
csum += x
# Generate the image header.
flags = 2 # 96TPI
if self.index_cued:
flags |= 1 # Index-Cued
header = struct.pack("<3s9BI",
b"SCP", # Signature
0, # Version
self.opts.disktype,
self.nr_revs, 0, ntracks-1,
flags,
0, # 16-bit cell width
single_sided,
0, # 25ns capture
csum & 0xffffffff)
# Concatenate it all together and send it back.
return header + trk_offs + trk_dat
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,25 +0,0 @@
# greaseweazle/optimised/__init__.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import os
gw_opt = os.environ.get('GW_OPT')
enabled = gw_opt is None or gw_opt.lower().startswith('y')
if enabled:
try:
from .optimised import *
except ModuleNotFoundError:
enabled = False
print('*** WARNING: Optimised data routines not found: '
'Run scripts/setup.sh')
else:
print('*** WARNING: Optimised data routines disabled (GW_OPT=%s)'
% gw_opt)
# Local variables:
# python-indent: 4
# End:

View File

View File

@@ -1,95 +0,0 @@
# greaseweazle/tools/bandwidth.py
#
# Greaseweazle control script: Measure USB bandwidth.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Report the available USB bandwidth for the Greaseweazle device."
import struct, sys
from timeit import default_timer as timer
from greaseweazle.tools import util
from greaseweazle import usb as USB
def generate_random_buffer(nr, seed):
dat = bytearray()
r = seed
for i in range(nr):
dat.append(r&255)
if r & 1:
r = (r>>1) ^ 0x80000062
else:
r >>= 1
return dat
def measure_bandwidth(usb, args):
print()
print("%19s%-7s/ %-7s/ %-7s" % ("", "Min.", "Mean", "Max."))
seed = 0x12345678
w_nr = r_nr = 1000000
buf = generate_random_buffer(w_nr, seed)
start = timer()
ack = usb.sink_bytes(buf, seed)
end = timer()
av_w_bw = (w_nr * 8) / ((end-start) * 1e6)
min_w_bw, max_w_bw = usb.bw_stats()
print("Write Bandwidth: %8.3f / %8.3f / %8.3f Mbps"
% (min_w_bw, av_w_bw, max_w_bw))
if ack != 0:
print("ERROR: USB write data garbled (Host -> Device)")
return
start = timer()
sbuf = usb.source_bytes(r_nr, seed)
end = timer()
av_r_bw = (r_nr * 8) / ((end-start) * 1e6)
min_r_bw, max_r_bw = usb.bw_stats()
print("Read Bandwidth: %8.3f / %8.3f / %8.3f Mbps"
% (min_r_bw, av_r_bw, max_r_bw))
if sbuf is not None and sbuf != buf:
print("ERROR: USB read data garbled (Device -> Host)")
return
est_min_bw = 0.9 * min(min_r_bw, min_w_bw)
print()
print("Estimated Consistent Min. Bandwidth: %.3f Mbps" % est_min_bw)
max_flux_rate = ((est_min_bw * 0.9) * 1e6) / 8
twobyte_us = 249/72 # Smallest time requiring a 2-byte transmission code
req_min_bw = 16 / twobyte_us # Bandwidth (Mbps) to transmit above time
if req_min_bw > est_min_bw:
print(" -> **WARNING** BELOW REQUIRED MIN.: %.3f Mbps" % req_min_bw)
else:
print(" -> Max. Flux Rate: %.3f Msamples/sec"
% (max_flux_rate / 1e6))
print(" -> Min. Ave. Flux: %.3f us"
% (1e6 / max_flux_rate))
def main(argv):
parser = util.ArgumentParser(usage='%(prog)s [options]')
parser.add_argument("--device", help="greaseweazle device name")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
try:
usb = util.usb_open(args.device)
measure_bandwidth(usb, args)
except USB.CmdError as error:
print("Command Failed: %s" % error)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,64 +0,0 @@
# greaseweazle/tools/clean.py
#
# Greaseweazle control script: Scrub drive heads with a cleaning disk.
#
# Uses a zig-zag pattern, after Dave Dunfield's ImageDisk and
# Phil Pemberton's Magpie/DiscFerret.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Clean a drive in a zig-zag pattern using a cleaning disk."
import sys, time
from greaseweazle.tools import util
from greaseweazle import usb as USB
def seek(cyl, usb, args):
c = min(cyl, args.cyls - 1)
print("%d " % c, end='', flush=True)
usb.seek(c, 0)
def clean(usb, args):
step = max(args.cyls // 8, 2)
for p in range(args.passes):
print('Pass %d: ' % p, end='', flush=True)
for cyl in range(0, args.cyls, step):
seek(cyl + step - 1, usb, args)
time.sleep(args.linger / 1000)
seek(cyl, usb, args)
time.sleep(args.linger / 1000)
print()
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 write (A,B,0,1,2)")
parser.add_argument("--cyls", type=int, default=80,
help="number of drive cylinders")
parser.add_argument("--passes", type=int, default=3,
help="number of passes across the cleaning disk")
parser.add_argument("--linger", type=int, default=100,
help="linger time per step, milliseconds")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
try:
usb = util.usb_open(args.device)
util.with_drive_selected(clean, usb, args)
except USB.CmdError as error:
print("Command Failed: %s" % error)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,66 +0,0 @@
# greaseweazle/tools/delays.py
#
# Greaseweazle control script: Get/Set Delay Timers.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Display (and optionally modify) Greaseweazle \
drive-delay parameters."
import sys
from greaseweazle.tools import util
from greaseweazle import usb as USB
def main(argv):
parser = util.ArgumentParser(usage='%(prog)s [options]')
parser.add_argument("--device", help="greaseweazle device name")
parser.add_argument("--select", type=int,
help="delay after drive select (usecs)")
parser.add_argument("--step", type=int,
help="delay between head steps (usecs)")
parser.add_argument("--settle", type=int,
help="settle delay after seek (msecs)")
parser.add_argument("--motor", type=int,
help="delay after motor on (msecs)")
parser.add_argument("--watchdog", type=int,
help="quiescent time until drives reset (msecs)")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
try:
usb = util.usb_open(args.device)
if args.select:
usb.select_delay = args.select
if args.step:
usb.step_delay = args.step
if args.settle:
usb.seek_settle_delay = args.settle
if args.motor:
usb.motor_delay = args.motor
if args.watchdog:
usb.watchdog_delay = args.watchdog
print("Select Delay: %uus" % usb.select_delay)
print("Step Delay: %uus" % usb.step_delay)
print("Settle Time: %ums" % usb.seek_settle_delay)
print("Motor Delay: %ums" % usb.motor_delay)
print("Watchdog: %ums" % usb.watchdog_delay)
except USB.CmdError as error:
print("Command Failed: %s" % error)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,61 +0,0 @@
# greaseweazle/tools/erase.py
#
# Greaseweazle control script: Erase a Disk.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Erase a disk."
import sys
from greaseweazle.tools import util
from greaseweazle import usb as USB
def erase(usb, args):
# @drive_ticks is the time in Greaseweazle ticks between index pulses.
# We will adjust the flux intervals per track to allow for this.
drive_ticks = usb.read_track(2).ticks_per_rev
for t in args.tracks:
cyl, head = t.cyl, t.head
print("\rErasing Track %u.%u..." % (cyl, head), end="")
usb.seek(t.physical_cyl, head)
usb.erase_track(drive_ticks * 1.1)
print()
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 write (A,B,0,1,2)")
parser.add_argument("--tracks", type=util.TrackSet,
help="which tracks to erase")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
try:
usb = util.usb_open(args.device)
tracks = util.TrackSet('c=0-81:h=0-1')
if args.tracks is not None:
tracks.update_from_trackspec(args.tracks.trackspec)
args.tracks = tracks
print("Erasing %s" % (args.tracks))
util.with_drive_selected(erase, usb, args)
except USB.CmdError as error:
print("Command Failed: %s" % error)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,98 +0,0 @@
# greaseweazle/tools/info.py
#
# Greaseweazle control script: Displat info about tools, firmware, and drive.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Display information about the Greaseweazle setup."
import sys, serial
from greaseweazle.tools import util
from greaseweazle import usb as USB
from greaseweazle import version
model_id = { 1: { 0: 'F1',
1: 'F1 Plus',
2: 'F1 Plus (Unbuffered)' },
4: { 0: 'V4',
1: 'V4 Slim' },
7: { 0: 'F7 v1',
1: 'F7 Plus (Ant Goffart, v1)',
2: 'F7 Lightning',
3: 'F7 v2)',
4: 'F7 Plus (Ant Goffart, v2)',
5: 'F7 Lightning Plus',
6: 'F7 Slim',
7: 'F7 v3 "Thunderbolt"' } }
speed_id = { 0: 'Full Speed (12 Mbit/s)',
1: 'High Speed (480 Mbit/s)' }
def print_info_line(name, value, tab=0):
print(''.ljust(tab) + (name + ':').ljust(12-tab) + value)
def main(argv):
parser = util.ArgumentParser(usage='%(prog)s [options]')
parser.add_argument("--device", help="greaseweazle device name")
parser.add_argument("--bootloader", action="store_true",
help="display bootloader info (F7 only)")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
print_info_line('Host Tools', 'v%d.%d' % (version.major, version.minor))
print('Greaseweazle:')
try:
usb = util.usb_open(args.device, mode_check=False)
except serial.SerialException:
print(' Not found')
sys.exit(0)
mode_switched = (usb.jumperless_update
and usb.update_mode != args.bootloader
and not (usb.update_mode and usb.update_jumpered))
if mode_switched:
usb = util.usb_reopen(usb, args.bootloader)
port = usb.port_info
if port.device:
print_info_line('Device', port.device, tab=2)
try:
model = model_id[usb.hw_model][usb.hw_submodel]
except KeyError:
model = 'Unknown (0x%02X%02X)' % (usb.hw_model, usb.hw_submodel)
print_info_line('Model', model, tab=2)
fwver = 'v%d.%d' % (usb.major, usb.minor)
if usb.update_mode:
fwver += ' (Update Bootloader)'
print_info_line('Firmware', fwver, tab=2)
print_info_line('Serial', port.serial_number if port.serial_number
else 'Unknown', tab=2)
try:
speed = speed_id[usb.usb_speed]
except KeyError:
speed = 'Unknown (0x%02X)' % usb.usb_speed
print_info_line('USB Rate', speed, tab=2)
if mode_switched:
usb = util.usb_reopen(usb, not args.bootloader)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,87 +0,0 @@
# greaseweazle/tools/pin.py
#
# Greaseweazle control script: Set a floppy interface pin to specified level.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Change the setting of a user-modifiable interface pin."
import sys, argparse
from greaseweazle.tools import util
from greaseweazle import usb as USB
def level(letter):
levels = { 'H': True, 'L': False }
if not letter.upper() in levels:
raise argparse.ArgumentTypeError("invalid pin level: '%s'" % letter)
return levels[letter.upper()]
def pin_set(argv):
parser = util.ArgumentParser(usage='%(prog)s [options] pin level')
parser.add_argument("--device", help="greaseweazle device name")
parser.add_argument("pin", type=int, help="pin number")
parser.add_argument("level", type=level, help="pin level (H,L)")
parser.description = description
parser.prog += ' pin set'
args = parser.parse_args(argv[3:])
try:
usb = util.usb_open(args.device)
usb.set_pin(args.pin, args.level)
print("Pin %u is set %s" %
(args.pin, ("Low (0v)", "High (5v)")[args.level]))
except USB.CmdError as error:
print("Command Failed: %s" % error)
def _pin_get(usb, args, **_kwargs):
"""Get the specified pin value.
"""
value = usb.get_pin(args.pin)
print("Pin %u is %s" %
(args.pin, ("Low (0v)", "High (5v)")[value]))
def pin_get(argv):
parser = util.ArgumentParser(usage='%(prog)s [options] pin')
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)")
parser.add_argument("pin", type=int, help="pin number")
parser.description = description
parser.prog += ' pin get'
args = parser.parse_args(argv[3:])
try:
usb = util.usb_open(args.device)
util.with_drive_selected(_pin_get, usb, args, motor=False)
except USB.CmdError as error:
print("Command Failed: %s" % error)
def usage(argv):
print("usage: gw pin get|set [-h] ...")
print(" get|set Get or set a pin")
sys.exit(1)
def main(argv):
if len(argv) < 3:
usage(argv)
if argv[2] == 'get':
pin_get(argv)
elif argv[2] == 'set':
pin_set(argv)
else:
usage(argv)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,175 +0,0 @@
# greaseweazle/tools/read.py
#
# Greaseweazle control script: Read Disk to Image.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Read a disk to the specified image file."
import sys
import importlib
from greaseweazle.tools import util
from greaseweazle import error
from greaseweazle import usb as USB
from greaseweazle.flux import Flux
from greaseweazle.codec import formats
def open_image(args, image_class):
image = image_class.to_file(args.file, args.fmt_cls)
if args.rate is not None:
image.bitrate = args.rate
for opt, val in args.file_opts.items():
error.check(hasattr(image, 'opts') and hasattr(image.opts, opt),
"%s: Invalid file option: %s" % (args.file, opt))
setattr(image.opts, opt, val)
return image
def read_and_normalise(usb, args, revs, ticks=0):
flux = usb.read_track(revs=revs, ticks=ticks)
if args.rpm is not None:
flux.scale((60/args.rpm) / flux.time_per_rev)
return flux
def read_with_retry(usb, args, cyl, head, decoder):
flux = read_and_normalise(usb, args, args.revs, args.ticks)
if decoder is None:
return flux
dat = decoder(cyl, head, flux)
if dat.nr_missing() != 0:
for retry in range(args.retries):
print("T%u.%u: %s - Retrying (%d)"
% (cyl, head, dat.summary_string(), retry+1))
flux = read_and_normalise(usb, args, max(args.revs, 3))
dat.decode_raw(flux)
if dat.nr_missing() == 0:
break
return dat
def print_summary(args, summary):
s = 'Cyl-> '
p = -1
for c in args.tracks.cyls:
s += ' ' if c//10==p else str(c//10)
p = c//10
print(s)
s = 'H. S: '
for c in args.tracks.cyls:
s += str(c%10)
print(s)
tot_sec = good_sec = 0
for head in args.tracks.heads:
nsec = max(summary[x].nsec for x in summary if x[1] == head)
for sec in range(nsec):
print("%d.%2d: " % (head, sec), end="")
for cyl in args.tracks.cyls:
s = summary[cyl,head]
if sec > s.nsec:
print(" ", end="")
else:
tot_sec += 1
if s.has_sec(sec): good_sec += 1
print("." if s.has_sec(sec) else "X", end="")
print()
if tot_sec != 0:
print("Found %d sectors of %d (%d%%)" %
(good_sec, tot_sec, good_sec*100/tot_sec))
def read_to_image(usb, args, image, decoder=None):
"""Reads a floppy disk and dumps it into a new image file.
"""
args.ticks = 0
if isinstance(args.revs, float):
# Measure drive RPM.
# We will adjust the flux intervals per track to allow for this.
args.ticks = int(usb.read_track(2).ticks_per_rev * args.revs)
args.revs = 2
summary = dict()
for t in args.tracks:
cyl, head = t.cyl, t.head
usb.seek(t.physical_cyl, head)
dat = read_with_retry(usb, args, cyl, head, decoder)
s = "T%u.%u: %s" % (cyl, head, dat.summary_string())
if hasattr(dat, 'nr_missing') and dat.nr_missing() != 0:
s += " - Giving up"
print(s)
summary[cyl,head] = dat
image.emit_track(cyl, head, dat)
if decoder is not None:
print_summary(args, summary)
def main(argv):
epilog = "FORMAT options:\n" + formats.print_formats()
parser = util.ArgumentParser(usage='%(prog)s [options] file',
epilog=epilog)
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)")
parser.add_argument("--format", help="disk format")
parser.add_argument("--revs", type=int,
help="number of revolutions to read per track")
parser.add_argument("--tracks", type=util.TrackSet,
help="which tracks to read")
parser.add_argument("--rate", type=int, help="data rate (kbit/s)")
parser.add_argument("--rpm", type=int, help="convert drive speed to RPM")
parser.add_argument("--retries", type=int, default=3,
help="number of retries on decode failure")
parser.add_argument("file", help="output filename")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
args.file, args.file_opts = util.split_opts(args.file)
try:
usb = util.usb_open(args.device)
image_class = util.get_image_class(args.file)
if not args.format and hasattr(image_class, 'default_format'):
args.format = image_class.default_format
decoder, def_tracks, args.fmt_cls = None, None, None
if args.format:
try:
args.fmt_cls = formats.formats[args.format]()
except KeyError as ex:
raise error.Fatal("""\
Unknown format '%s'
Known formats:\n%s"""
% (args.format, formats.print_formats()))
decoder = args.fmt_cls.decode_track
def_tracks = args.fmt_cls.default_tracks
if args.revs is None: args.revs = args.fmt_cls.default_revs
if def_tracks is None:
def_tracks = util.TrackSet('c=0-81:h=0-1')
if args.revs is None: args.revs = 3
if args.tracks is not None:
def_tracks.update_from_trackspec(args.tracks.trackspec)
args.tracks = def_tracks
print(("Reading %s revs=" % args.tracks) + str(args.revs))
with open_image(args, image_class) as image:
util.with_drive_selected(read_to_image, usb, args, image,
decoder=decoder)
except USB.CmdError as err:
print("Command Failed: %s" % err)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,37 +0,0 @@
# greaseweazle/tools/reset.py
#
# Greaseweazle control script: Reset to power-on defaults.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Reset the Greaseweazle device to power-on default state."
import sys
from greaseweazle.tools import util
from greaseweazle import usb as USB
def main(argv):
parser = util.ArgumentParser(usage='%(prog)s [options]')
parser.add_argument("--device", help="greaseweazle device name")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
try:
usb = util.usb_open(args.device)
usb.power_on_reset()
except USB.CmdError as error:
print("Command Failed: %s" % error)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,62 +0,0 @@
# greaseweazle/tools/seek.py
#
# Greaseweazle control script: Seek to specified cylinder.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Seek to the specified cylinder."
import struct, sys
from greaseweazle.tools import util
from greaseweazle import error
from greaseweazle import usb as USB
from greaseweazle.flux import Flux
def seek(usb, args, **_kwargs):
"""Seeks to the cylinder specified in args.
"""
usb.seek(args.cylinder, 0)
def main(argv):
parser = util.ArgumentParser(usage='%(prog)s [options] cylinder')
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)")
parser.add_argument("--force", action="store_true",
help="allow extreme cylinders with no prompt")
parser.add_argument("--motor-on", action="store_true",
help="seek with drive motor activated")
parser.add_argument("cylinder", type=int, help="cylinder to seek")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
try:
struct.pack('b', args.cylinder)
except struct.error:
raise error.Fatal("Cylinder %d out of range" % args.cylinder)
if not 0 <= args.cylinder <= 83 and not args.force:
answer = input("Seek to extreme cylinder %d, Yes/No? " % args.cylinder)
if answer != "Yes":
return
try:
usb = util.usb_open(args.device)
util.with_drive_selected(seek, usb, args, motor=args.motor_on)
except USB.CmdError as err:
print("Command Failed: %s" % err)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,134 +0,0 @@
# greaseweazle/tools/update.py
#
# Greaseweazle control script: Firmware Update.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Update the Greaseweazle device firmware to current version."
import sys, serial, struct, os
import crcmod.predefined
from greaseweazle.tools import util
from greaseweazle import version
from greaseweazle import usb as USB
# update_firmware:
# Updates the Greaseweazle firmware using the specified Update File.
def update_firmware(usb, args):
req_type = b'BL' if args.bootloader else b'GW'
filename = args.file
if filename is None:
# Get the absolute path to the root Greaseweazle folder.
path = os.path.dirname(os.path.abspath(__file__))
for _ in range(3):
path = os.path.join(path, os.pardir)
path = os.path.normpath(path)
filename = os.path.join(path, "Greaseweazle-v%d.%d.upd"
% (version.major, version.minor))
# Read and verify the entire update catalogue.
with open(filename, "rb") as f:
dat = f.read()
if struct.unpack('4s', dat[:4])[0] != b'GWUP':
print('%s: Not a valid UPD file' % (filename))
return
crc32 = crcmod.predefined.Crc('crc-32-mpeg')
crc32.update(dat)
if crc32.crcValue != 0:
print('%s: UPD file is corrupt' % (filename))
return
dat = dat[4:-4]
# Search the catalogue for a match on our Weazle's hardware type.
while dat:
upd_len, hw_model = struct.unpack("<2H", dat[:4])
upd_type, major, minor = struct.unpack("2s2B", dat[upd_len-4:upd_len])
if ((hw_model, upd_type, major, minor)
== (usb.hw_model, req_type, version.major, version.minor)):
# Match: Pull out the embedded update file.
dat = dat[4:upd_len+4]
break
# Skip to the next catalogue entry.
dat = dat[upd_len+4:]
if not dat:
print("%s: F%u v%u.%u %s update not found"
% (filename, usb.hw_model,
version.major, version.minor,
'bootloader' if args.bootloader else 'firmware'))
return
# Check the matching update file's footer.
sig, maj, min, hw_model = struct.unpack("<2s2BH", dat[-8:-2])
if len(dat) & 3 != 0 or sig != req_type or hw_model != usb.hw_model:
print("%s: Bad update file" % (filename))
return
crc16 = crcmod.predefined.Crc('crc-ccitt-false')
crc16.update(dat)
if crc16.crcValue != 0:
print("%s: Bad CRC" % (filename))
return
# Perform the update.
print("Updating %s to v%u.%u..."
% ("Bootloader" if args.bootloader else "Main Firmware", maj, min))
if args.bootloader:
ack = usb.update_bootloader(dat)
if ack != 0:
print("""\
** UPDATE FAILED: Please retry immediately or your Weazle may need
full reflashing via a suitable programming adapter!""")
return
print("Done.")
else:
ack = usb.update_firmware(dat)
if ack != 0:
print("** UPDATE FAILED: Please retry!")
return
print("Done.")
if usb.jumperless_update:
util.usb_reopen(usb, is_update=False)
else:
print("** Disconnect Greaseweazle and remove the Update Jumper")
def main(argv):
parser = util.ArgumentParser(allow_abbrev=False, usage='%(prog)s [options] [file]')
parser.add_argument("file", nargs="?", help="update filename")
parser.add_argument("--device", help="greaseweazle device name")
parser.add_argument("--bootloader", action="store_true",
help="update the bootloader (use with caution!)")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
try:
usb = util.usb_open(args.device, is_update=not args.bootloader)
update_firmware(usb, args)
except USB.CmdError as error:
if error.code == USB.Ack.OutOfSRAM and args.bootloader:
# Special warning for Low-Density F1 devices. The new bootloader
# cannot be fully buffered in the limited RAM available.
print("ERROR: Bootloader update unsupported on this device "
"(insufficient SRAM)")
elif error.code == USB.Ack.OutOfFlash and not args.bootloader:
print("ERROR: New firmware is too large for this device "
"(insufficient Flash memory)")
else:
print("Command Failed: %s" % error)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,347 +0,0 @@
# greaseweazle/tools/util.py
#
# Greaseweazle control script: Utility functions.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import argparse, os, sys, serial, struct, time, re, platform
import importlib
import serial.tools.list_ports
from collections import OrderedDict
from greaseweazle import version
from greaseweazle import error
from greaseweazle import usb as USB
class CmdlineHelpFormatter(argparse.ArgumentDefaultsHelpFormatter,
argparse.RawDescriptionHelpFormatter):
def _get_help_string(self, action):
help = action.help
if '%no_default' in help:
return help.replace('%no_default', '')
if ('%(default)' in help
or action.default is None
or action.default is False
or action.default is argparse.SUPPRESS):
return help
return help + ' (default: %(default)s)'
class ArgumentParser(argparse.ArgumentParser):
def __init__(self, formatter_class=CmdlineHelpFormatter, *args, **kwargs):
return super().__init__(formatter_class=formatter_class,
*args, **kwargs)
def drive_letter(letter):
types = {
'A': (USB.BusType.IBMPC, 0),
'B': (USB.BusType.IBMPC, 1),
'0': (USB.BusType.Shugart, 0),
'1': (USB.BusType.Shugart, 1),
'2': (USB.BusType.Shugart, 2)
}
if not letter.upper() in types:
raise argparse.ArgumentTypeError("invalid drive letter: '%s'" % letter)
return types[letter.upper()]
def range_str(l):
if len(l) == 0:
return '<none>'
p, str = None, ''
for i in l:
if p is not None and i == p[1]+1:
p = p[0], i
continue
if p is not None:
str += ('%d,' % p[0]) if p[0] == p[1] else ('%d-%d,' % p)
p = (i,i)
if p is not None:
str += ('%d' % p[0]) if p[0] == p[1] else ('%d-%d' % p)
return str
class TrackSet:
class TrackIter:
"""Iterate over a TrackSet in physical <cyl,head> order."""
def __init__(self, ts):
l = []
for c in ts.cyls:
for h in ts.heads:
pc = c*ts.step + ts.h_off[h]
l.append((pc, h, c))
l.sort()
self.l = iter(l)
def __next__(self):
self.physical_cyl, self.head, self.cyl = next(self.l)
return self
def __init__(self, trackspec):
self.cyls = list()
self.heads = list()
self.h_off = [0]*2
self.step = 1
self.trackspec = ''
self.update_from_trackspec(trackspec)
def update_from_trackspec(self, trackspec):
"""Update a TrackSet based on a trackspec."""
self.trackspec += trackspec
for x in trackspec.split(':'):
k,v = x.split('=')
if k == 'c':
cyls = [False]*100
for crange in v.split(','):
m = re.match('(\d\d?)(-(\d\d?))?$', crange)
if m is None: raise ValueError()
if m.group(3) is None:
s,e = int(m.group(1)), int(m.group(1))
else:
s,e = int(m.group(1)), int(m.group(3))
for c in range(s, e+1):
cyls[c] = True
self.cyls = []
for c in range(len(cyls)):
if cyls[c]: self.cyls.append(c)
elif k == 'h':
heads = [False]*2
for hrange in v.split(','):
m = re.match('([01])(-([01]))?$', hrange)
if m is None: raise ValueError()
if m.group(3) is None:
s,e = int(m.group(1)), int(m.group(1))
else:
s,e = int(m.group(1)), int(m.group(3))
for h in range(s, e+1):
heads[h] = True
self.heads = []
for h in range(len(heads)):
if heads[h]: self.heads.append(h)
elif re.match('h[01].off$', k):
h = int(re.match('h([01]).off$', k).group(1))
m = re.match('([+-][\d])$', v)
if m is None: raise ValueError()
self.h_off[h] = int(m.group(1))
elif k == 'step':
self.step = int(v)
if self.step <= 0: raise ValueError()
else:
raise ValueError()
def __str__(self):
s = 'c=%s' % range_str(self.cyls)
s += ':h=%s' % range_str(self.heads)
for i in range(len(self.h_off)):
x = self.h_off[i]
if x != 0:
s += ':h%d.off=%s%d' % (i, '+' if x >= 0 else '', x)
if self.step != 1: s += ':step=%d' % self.step
return s
def __iter__(self):
return self.TrackIter(self)
def split_opts(seq):
"""Splits a name from its list of options."""
parts = seq.split('::')
name, opts = parts[0], dict()
for x in map(lambda x: x.split(':'), parts[1:]):
for y in x:
try:
opt, val = y.split('=')
except ValueError:
opt, val = y, True
if opt:
opts[opt] = val
return name, opts
image_types = OrderedDict(
{ '.adf': 'ADF',
'.ads': ('ADS','acorn'),
'.adm': ('ADM','acorn'),
'.adl': ('ADL','acorn'),
'.d81': 'D81',
'.dsd': ('DSD','acorn'),
'.dsk': 'EDSK',
'.hfe': 'HFE',
'.ima': 'IMG',
'.img': 'IMG',
'.ipf': 'IPF',
'.raw': 'KryoFlux',
'.scp': 'SCP',
'.ssd': ('SSD','acorn'),
'.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()]
if isinstance(typespec, tuple):
typename, classname = typespec
else:
typename, classname = typespec, typespec.lower()
mod = importlib.import_module('greaseweazle.image.' + classname)
return mod.__dict__[typename]
def with_drive_selected(fn, usb, args, *_args, **_kwargs):
usb.set_bus_type(args.drive[0])
try:
usb.drive_select(args.drive[1])
usb.drive_motor(args.drive[1], _kwargs.pop('motor', True))
fn(usb, args, *_args, **_kwargs)
except KeyboardInterrupt:
print()
usb.reset()
raise
finally:
usb.drive_motor(args.drive[1], False)
usb.drive_deselect()
def valid_ser_id(ser_id):
return ser_id and ser_id.upper().startswith("GW")
def score_port(x, old_port=None):
score = 0
if x.manufacturer == "Keir Fraser" and x.product == "Greaseweazle":
score = 20
elif x.vid == 0x1209 and x.pid == 0x4d69:
# Our very own properly-assigned PID. Guaranteed to be us.
score = 20
elif x.vid == 0x1209 and x.pid == 0x0001:
# Our old shared Test PID. It's not guaranteed to be us.
score = 10
if score > 0 and valid_ser_id(x.serial_number):
# A valid serial id is a good sign unless this is a reopen, and
# the serials don't match!
if not old_port or not valid_ser_id(old_port.serial_number):
score = 20
elif x.serial_number == old_port.serial_number:
score = 30
else:
score = 0
if old_port and old_port.location:
# If this is a reopen, location field must match. A match is not
# sufficient in itself however, as Windows may supply the same
# location for multiple USB ports (this may be an interaction with
# BitDefender). Hence we do not increase the port's score here.
if not x.location or x.location != old_port.location:
score = 0
return score
def find_port(old_port=None):
best_score, best_port = 0, None
for x in serial.tools.list_ports.comports():
score = score_port(x, old_port)
if score > best_score:
best_score, best_port = score, x
if best_port:
return best_port.device
raise serial.SerialException('Cannot find the Greaseweazle device')
def port_info(devname):
for x in serial.tools.list_ports.comports():
if x.device == devname:
return x
return None
def usb_reopen(usb, is_update):
mode = { False: 1, True: 0 }
try:
usb.switch_fw_mode(mode[is_update])
except (serial.SerialException, struct.error):
# Mac and Linux raise SerialException ("... returned no data")
# Win10 pyserial returns a short read which fails struct.unpack
pass
usb.ser.close()
for i in range(10):
time.sleep(0.5)
try:
devicename = find_port(usb.port_info)
new_ser = serial.Serial(devicename)
except serial.SerialException:
# Device not found
pass
else:
new_usb = USB.Unit(new_ser)
new_usb.port_info = port_info(devicename)
new_usb.jumperless_update = usb.jumperless_update
return new_usb
raise serial.SerialException('Could not reopen port after mode switch')
def print_update_instructions(usb):
print("To perform an Update:")
if not usb.jumperless_update:
print(" - Disconnect from USB")
print(" - Install the Update Jumper at pins %s"
% ("RXI-TXO" if usb.hw_model != 1 else "DCLK-GND"))
print(" - Reconnect to USB")
print(" - Run \"gw update\" to install firmware v%u.%u" %
(version.major, version.minor))
def usb_open(devicename, is_update=False, mode_check=True):
if devicename is None:
devicename = find_port()
usb = USB.Unit(serial.Serial(devicename))
usb.port_info = port_info(devicename)
is_win7 = (platform.system() == 'Windows' and platform.release() == '7')
usb.jumperless_update = ((usb.hw_model, usb.hw_submodel) != (1, 0)
and not is_win7)
if not mode_check:
return usb
if usb.update_mode and not is_update:
if usb.jumperless_update and not usb.update_jumpered:
usb = usb_reopen(usb, is_update)
if not usb.update_mode:
return usb
print("ERROR: Greaseweazle is in Firmware Update Mode")
print(" - The only available action is \"gw update\"")
if usb.update_jumpered:
print(" - For normal operation disconnect from USB and remove "
"the Update Jumper at pins %s"
% ("RXI-TXO" if usb.hw_model != 1 else "DCLK-GND"))
else:
print(" - Main firmware is erased: You *must* perform an update!")
sys.exit(1)
if is_update and not usb.update_mode:
if usb.jumperless_update:
usb = usb_reopen(usb, is_update)
error.check(usb.update_mode, """\
Greaseweazle F7 did not change to Firmware Update Mode as requested.
If the problem persists, install the Update Jumper at pins RXI-TXO.""")
return usb
print("ERROR: Greaseweazle is not in Firmware Update Mode")
print_update_instructions(usb)
sys.exit(1)
if not usb.update_mode and usb.update_needed:
print("ERROR: Greaseweazle firmware v%u.%u is unsupported"
% (usb.major, usb.minor))
print_update_instructions(usb)
sys.exit(1)
return usb
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,212 +0,0 @@
# greaseweazle/tools/write.py
#
# Greaseweazle control script: Write Image to Disk.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
description = "Write a disk from the specified image file."
import sys
from greaseweazle.tools import util
from greaseweazle import error, track
from greaseweazle import usb as USB
from greaseweazle.codec import formats
# Read and parse the image file.
def open_image(args, image_class):
try:
image = image_class.from_file(args.file)
args.raw_image_class = True
except TypeError:
image = image_class.from_file(args.file, args.fmt_cls)
args.raw_image_class = False
return image
# write_from_image:
# Writes the specified image file to floppy disk.
def write_from_image(usb, args, image):
# Measure drive RPM.
# We will adjust the flux intervals per track to allow for this.
drive = usb.read_track(2)
del drive.list
verified_count, not_verified_count = 0, 0
for t in args.tracks:
cyl, head = t.cyl, t.head
track = image.get_track(cyl, head)
if track is None and not args.erase_empty:
continue
print("\r%sing Track %u.%u..." %
("Writ" if track is not None else "Eras", cyl, head),
end="", flush=True)
usb.seek(t.physical_cyl, head)
if track is None:
usb.erase_track(drive.ticks_per_rev * 1.1)
continue
if args.raw_image_class and args.fmt_cls is not None:
track = args.fmt_cls.decode_track(cyl, head, track).raw_track()
if args.precomp is not None:
track.precomp = args.precomp.track_precomp(cyl)
flux = track.flux_for_writeout()
# @factor adjusts flux times for speed variations between the
# read-in and write-out drives.
factor = drive.ticks_per_rev / flux.index_list[0]
# Convert the flux samples to Greaseweazle sample frequency.
rem = 0.0
flux_list = []
for x in flux.list:
y = x * factor + rem
val = round(y)
rem = y - val
flux_list.append(val)
# Encode the flux times for Greaseweazle, and write them out.
verified = False
for retry in range(args.retries+1):
if retry != 0:
print("T%u.%u: Verify Failure - Retry (%d)"
% (cyl, head, retry))
usb.write_track(flux_list = flux_list,
cue_at_index = flux.index_cued,
terminate_at_index = flux.terminate_at_index)
try:
no_verify = args.no_verify or track.verify is None
except AttributeError: # track.verify undefined
no_verify = True
if no_verify:
not_verified_count += 1
verified = True
break
v_revs, v_ticks = track.verify_revs, 0
if isinstance(v_revs, float):
v_ticks = int(drive.ticks_per_rev * v_revs)
v_revs = 2
v_flux = usb.read_track(revs = v_revs, ticks = v_ticks)
v_flux.scale(flux.time_per_rev / drive.time_per_rev)
verified = track.verify.verify_track(v_flux)
if verified:
verified_count += 1
break
if retry == 0:
print()
error.check(verified, "Failed to verify Track %u.%u" % (cyl, head))
print()
if not_verified_count == 0:
print("All tracks verified")
else:
if verified_count == 0:
s = "No tracks verified "
else:
s = ("%d tracks verified; %d tracks *not* verified "
% (verified_count, not_verified_count))
s += ("(Reason: Verify %s)"
% ("unavailable", "disabled")[args.no_verify])
print(s)
class PrecompSpec:
def __str__(self):
s = "Precomp %s" % track.Precomp.TYPESTRING[self.type]
for e in self.list:
s += ", %d-:%dns" % e
return s
def track_precomp(self, cyl):
for c,s in reversed(self.list):
if cyl >= c:
return track.Precomp(self.type, s)
return None
def importspec(self, spec):
self.list = []
self.type = track.Precomp.MFM
for x in spec.split(':'):
k,v = x.split('=')
if k == 'type':
self.type = track.Precomp.TYPESTRING.index(v.upper())
else:
self.list.append((int(k), int(v)))
self.list.sort()
def __init__(self, spec):
try:
self.importspec(spec)
except:
raise ValueError
def main(argv):
epilog = "FORMAT options:\n" + formats.print_formats()
parser = util.ArgumentParser(usage='%(prog)s [options] file',
epilog=epilog)
parser.add_argument("--device", help="greaseweazle device name")
parser.add_argument("--drive", type=util.drive_letter, default='A',
help="drive to write (A,B,0,1,2)")
parser.add_argument("--format", help="disk format")
parser.add_argument("--tracks", type=util.TrackSet,
help="which tracks to write")
parser.add_argument("--erase-empty", action="store_true",
help="erase empty tracks (default: skip)")
parser.add_argument("--no-verify", action="store_true",
help="disable verify")
parser.add_argument("--retries", type=int, default=3,
help="number of retries on verify failure")
parser.add_argument("--precomp", type=PrecompSpec,
help="write precompensation")
parser.add_argument("file", help="input filename")
parser.description = description
parser.prog += ' ' + argv[1]
args = parser.parse_args(argv[2:])
try:
image_class = util.get_image_class(args.file)
if not args.format and hasattr(image_class, 'default_format'):
args.format = image_class.default_format
def_tracks, args.fmt_cls = None, None
if args.format:
try:
args.fmt_cls = formats.formats[args.format]()
except KeyError as ex:
raise error.Fatal("""\
Unknown format '%s'
Known formats:\n%s"""
% (args.format, formats.print_formats()))
def_tracks = args.fmt_cls.default_tracks
if def_tracks is None:
def_tracks = util.TrackSet('c=0-81:h=0-1')
if args.tracks is not None:
def_tracks.update_from_trackspec(args.tracks.trackspec)
args.tracks = def_tracks
usb = util.usb_open(args.device)
image = open_image(args, image_class)
s = str(args.tracks)
if args.precomp is not None:
s += "; %s" % args.precomp
print("Writing %s" % s)
util.with_drive_selected(write_from_image, usb, args, image)
except USB.CmdError as err:
print("Command Failed: %s" % err)
if __name__ == "__main__":
main(sys.argv)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,331 +0,0 @@
# greaseweazle/track.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import binascii
import itertools as it
from bitarray import bitarray
from greaseweazle.flux import Flux, WriteoutFlux
from greaseweazle import optimised
# Precompensation to apply to a MasterTrack for writeout.
class Precomp:
MFM = 0
FM = 1
GCR = 2
TYPESTRING = [ 'MFM', 'FM', 'GCR' ]
def __str__(self):
return "Precomp: %s, %dns" % (Precomp.TYPESTRING[self.type], self.ns)
def __init__(self, type, ns):
self.type = type
self.ns = ns
def apply(self, bits, bit_ticks, scale):
t = self.ns * scale
if self.type == Precomp.MFM:
for i in bits.itersearch(bitarray('10100', endian='big')):
bit_ticks[i+2] -= t
bit_ticks[i+3] += t
for i in bits.itersearch(bitarray('00101', endian='big')):
bit_ticks[i+2] += t
bit_ticks[i+3] -= t
# This is primarily for GCR and FM which permit adjacent 1s (and
# have correspondingly slower bit times). However it may be useful
# for illegal MFM sequences too, especially on Amiga (custom syncwords,
# 4us-bitcell tracks). Normal MFM should not trigger these patterns.
for i in bits.itersearch(bitarray('110', endian='big')):
bit_ticks[i+1] -= t
bit_ticks[i+2] += t
for i in bits.itersearch(bitarray('011', endian='big')):
bit_ticks[i+1] += t
bit_ticks[i+2] -= t
# A pristine representation of a track, from a codec and/or a perfect image.
class MasterTrack:
@property
def bitrate(self):
return len(self.bits) / self.time_per_rev
# bits: Track bitcell data, aligned to the write splice (bitarray or bytes)
# time_per_rev: Time per revolution, in seconds (float)
# bit_ticks: Per-bitcell time values, in unitless 'ticks'
# splice: Location of the track splice, in bitcells, after the index
# weak: List of (start, length) weak ranges
def __init__(self, bits, time_per_rev, bit_ticks=None, splice=0, weak=[]):
if isinstance(bits, bytes):
self.bits = bitarray(endian='big')
self.bits.frombytes(bits)
else:
self.bits = bits
self.time_per_rev = time_per_rev
self.bit_ticks = bit_ticks
self.splice = splice
self.weak = weak
self.precomp = None
self.force_random_weak = True
def __str__(self):
s = "\nMaster Track: splice @ %d\n" % self.splice
s += (" %d bits, %.1f kbit/s"
% (len(self.bits), self.bitrate))
if self.bit_ticks:
s += " (variable)"
s += ("\n %.1f ms / rev (%.1f rpm)"
% (self.time_per_rev * 1000, 60 / self.time_per_rev))
if len(self.weak) > 0:
s += "\n %d weak range" % len(self.weak)
if len(self.weak) > 1: s += "s"
s += ": " + ", ".join(str(n) for _,n in self.weak) + " bits"
#s += str(binascii.hexlify(self.bits.tobytes()))
return s
def flux_for_writeout(self, cue_at_index=True):
return self.flux(for_writeout=True, cue_at_index=cue_at_index)
def flux(self, for_writeout=False, cue_at_index=True):
# We're going to mess with the track data, so take a copy.
bits = self.bits.copy()
bitlen = len(bits)
# Also copy the bit_ticks array (or create a dummy one), and remember
# the total ticks that it contains.
bit_ticks = self.bit_ticks.copy() if self.bit_ticks else [1] * bitlen
ticks_to_index = sum(bit_ticks)
# Weak regions need special processing for correct flux representation.
for s,n in self.weak:
e = s + n
assert 0 < s < e < bitlen
pattern = bitarray(endian="big")
if n < 400 or self.force_random_weak:
# Short weak regions are written with no flux transitions.
# Actually we insert a flux transition every 32 bitcells, else
# we risk triggering Greaseweazle's No Flux Area generator.
pattern.frombytes(b"\x80\x00\x00\x00")
bits[s:e] = (pattern * (n//32+1))[:n]
else:
# Long weak regions we present a fuzzy clock bit in an
# otherwise normal byte (16 bits MFM). The byte may be
# interpreted as
# MFM 0001001010100101 = 12A5 = byte 0x43, or
# MFM 0001001010010101 = 1295 = byte 0x47
pattern.frombytes(b"\x12\xA5")
bits[s:e] = (pattern * (n//16+1))[:n]
for i in range(0, n-10, 16):
x, y = bit_ticks[s+i+10], bit_ticks[s+i+11]
bit_ticks[s+i+10], bit_ticks[s+i+11] = x+y*0.5, y*0.5
# To prevent corrupting a preceding sync word by effectively
# starting the weak region early, we start with a 1 if we just
# clocked out a 0.
bits[s] = not bits[s-1]
# Similarly modify the last bit of the weak region.
bits[e-1] = not(bits[e-2] or bits[e])
if cue_at_index or not for_writeout:
# Rotate data to start at the index.
index = -self.splice % bitlen
if index != 0:
bits = bits[index:] + bits[:index]
bit_ticks = bit_ticks[index:] + bit_ticks[:index]
splice_at_index = index < 4 or bitlen - index < 4
else:
splice_at_index = False
if not for_writeout:
# Do not extend the track for reliable writeout to disk.
pass
elif not cue_at_index:
# We write the track wherever it may fall (uncued).
# We stretch the track with extra header gap bytes, in case the
# drive spins slow and we need more length to create an overlap.
# Thus if the drive spins slow, the track gets a longer header.
pos = 4
# We stretch by 10 percent, which is way more than enough.
rep = bitlen // (10 * 32)
bit_ticks = bit_ticks[pos:pos+32] * rep + bit_ticks[pos:]
bits = bits[pos:pos+32] * rep + bits[pos:]
elif splice_at_index:
# Splice is at the index (or within a few bitcells of it).
# We stretch the track with extra footer gap bytes, in case the
# drive motor spins slower than expected and we need more filler
# to get us to the index pulse (where the write will terminate).
# Thus if the drive spins slow, the track gets a longer footer.
pos = (self.splice - 4) % bitlen
# We stretch by 10 percent, which is way more than enough.
rep = bitlen // (10 * 32)
bit_ticks = bit_ticks[:pos] + bit_ticks[pos-32:pos] * rep
bits = bits[:pos] + bits[pos-32:pos] * rep
else:
# Splice is not at the index. We will write more than one
# revolution, and terminate the second revolution at the splice.
# For the first revolution we repeat the track header *backwards*
# to the very start of the write. This is in case the drive motor
# spins slower than expected and the write ends before the original
# splice position.
# Thus if the drive spins slow, the track gets a longer header.
bit_ticks += bit_ticks[:self.splice-4]
bits += bits[:self.splice-4]
pos = self.splice+4
fill_pattern = bits[pos:pos+32]
while pos >= 32:
pos -= 32
bits[pos:pos+32] = fill_pattern
if for_writeout and self.precomp is not None:
self.precomp.apply(bits, bit_ticks,
ticks_to_index / (self.time_per_rev*1e9))
# Convert the stretched track data into flux.
bit_ticks_i = iter(bit_ticks)
flux_list = []
flux_ticks = 0
for bit in bits:
flux_ticks += next(bit_ticks_i)
if bit:
flux_list.append(flux_ticks)
flux_ticks = 0
if flux_ticks and for_writeout:
flux_list.append(flux_ticks)
# Package up the flux for return.
if for_writeout:
flux = WriteoutFlux(ticks_to_index, flux_list,
ticks_to_index / self.time_per_rev,
index_cued = cue_at_index,
terminate_at_index = splice_at_index)
else:
flux = Flux([ticks_to_index]*2, flux_list*2,
ticks_to_index / self.time_per_rev,
index_cued = True)
flux.splice = sum(bit_ticks[:self.splice])
return flux
# Track data generated from flux.
class RawTrack:
def __init__(self, clock, data):
self.clock = clock
self.clock_max_adj = 0.10
self.pll_period_adj = 0.05
self.pll_phase_adj = 0.60
self.bitarray = bitarray(endian='big')
self.timearray = []
self.revolutions = []
self.import_flux_data(data)
def __str__(self):
s = "\nRaw Track: %d revolutions\n" % len(self.revolutions)
for rev in range(len(self.revolutions)):
b, _ = self.get_revolution(rev)
s += "Revolution %u (%u bits): " % (rev, len(b))
s += str(binascii.hexlify(b.tobytes())) + "\n"
b = self.bitarray[sum(self.revolutions):]
s += "Tail (%u bits): " % (len(b))
s += str(binascii.hexlify(b.tobytes())) + "\n"
return s[:-1]
def get_revolution(self, nr):
start = sum(self.revolutions[:nr])
end = start + self.revolutions[nr]
return self.bitarray[start:end], self.timearray[start:end]
def get_all_data(self):
return self.bitarray, self.timearray
def import_flux_data(self, data):
flux = data.flux()
freq = flux.sample_freq
clock = self.clock
clock_min = self.clock * (1 - self.clock_max_adj)
clock_max = self.clock * (1 + self.clock_max_adj)
index_iter = it.chain(iter(map(lambda x: x/freq, flux.index_list)),
[float('inf')])
# Make sure there's enough time in the flux list to cover all
# revolutions by appending a "large enough" final flux value.
tail = max(0, sum(flux.index_list) - sum(flux.list) + clock*freq*2)
flux_iter = it.chain(flux.list, [tail])
try:
optimised.flux_to_bitcells(
self.bitarray, self.timearray, self.revolutions,
index_iter, flux_iter,
freq, clock, clock_min, clock_max,
self.pll_period_adj, self.pll_phase_adj)
except AttributeError:
flux_to_bitcells(
self.bitarray, self.timearray, self.revolutions,
index_iter, flux_iter,
freq, clock, clock_min, clock_max,
self.pll_period_adj, self.pll_phase_adj)
def flux_to_bitcells(bit_array, time_array, revolutions,
index_iter, flux_iter,
freq, clock_centre, clock_min, clock_max,
pll_period_adj, pll_phase_adj):
nbits = 0
ticks = 0.0
clock = clock_centre
to_index = next(index_iter)
for x in flux_iter:
# Gather enough ticks to generate at least one bitcell.
ticks += x / freq
if ticks < clock/2:
continue
# Clock out zero or more 0s, followed by a 1.
zeros = 0
while True:
# Check if we cross the index mark.
to_index -= clock
if to_index < 0:
revolutions.append(nbits)
nbits = 0
to_index += next(index_iter)
nbits += 1
ticks -= clock
time_array.append(clock)
if ticks >= clock/2:
zeros += 1
bit_array.append(False)
else:
bit_array.append(True)
break
# PLL: Adjust clock frequency according to phase mismatch.
if zeros <= 3:
# In sync: adjust clock by a fraction of the phase mismatch.
clock += ticks * pll_period_adj
else:
# Out of sync: adjust clock towards centre.
clock += (clock_centre - clock) * pll_period_adj
# Clamp the clock's adjustment range.
clock = min(max(clock, clock_min), clock_max)
# PLL: Adjust clock phase according to mismatch.
new_ticks = ticks * (1 - pll_phase_adj)
time_array[-1] += ticks - new_ticks
ticks = new_ticks
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,565 +0,0 @@
# greaseweazle/usb.py
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import struct
import itertools as it
from greaseweazle import version
from greaseweazle import error
from greaseweazle.flux import Flux
from greaseweazle import optimised
EARLIEST_SUPPORTED_FIRMWARE = (0, 25)
## Control-Path command set
class ControlCmd:
ClearComms = 10000
Normal = 9600
## Command set
class Cmd:
GetInfo = 0
Update = 1
Seek = 2
Head = 3
SetParams = 4
GetParams = 5
Motor = 6
ReadFlux = 7
WriteFlux = 8
GetFluxStatus = 9
GetIndexTimes = 10
SwitchFwMode = 11
Select = 12
Deselect = 13
SetBusType = 14
SetPin = 15
Reset = 16
EraseFlux = 17
SourceBytes = 18
SinkBytes = 19
GetPin = 20
TestMode = 21
str = {
GetInfo: "GetInfo",
Update: "Update",
Seek: "Seek",
Head: "Head",
SetParams: "SetParams",
GetParams: "GetParams",
Motor: "Motor",
ReadFlux: "ReadFlux",
WriteFlux: "WriteFlux",
GetFluxStatus: "GetFluxStatus",
GetIndexTimes: "GetIndexTimes",
SwitchFwMode: "SwitchFwMode",
Select: "Select",
Deselect: "Deselect",
SetBusType: "SetBusType",
SetPin: "SetPin",
Reset: "Reset",
EraseFlux: "EraseFlux",
SourceBytes: "SourceBytes",
SinkBytes: "SinkBytes",
GetPin: "GetPin",
TestMode: "TestMode"
}
## Command responses/acknowledgements
class Ack:
Okay = 0
BadCommand = 1
NoIndex = 2
NoTrk0 = 3
FluxOverflow = 4
FluxUnderflow = 5
Wrprot = 6
NoUnit = 7
NoBus = 8
BadUnit = 9
BadPin = 10
BadCylinder = 11
OutOfSRAM = 12
OutOfFlash = 13
str = {
Okay: "Okay",
BadCommand: "Bad Command",
NoIndex: "No Index",
NoTrk0: "Track 0 not found",
FluxOverflow: "Flux Overflow",
FluxUnderflow: "Flux Underflow",
Wrprot: "Disk is Write Protected",
NoUnit: "No drive unit selected",
NoBus: "No bus type (eg. Shugart, IBM/PC) specified",
BadUnit: "Invalid unit number",
BadPin: "Invalid pin",
BadCylinder: "Invalid cylinder",
OutOfSRAM: "Out of SRAM",
OutOfFlash: "Out of Flash"
}
## Cmd.GetInfo indexes
class GetInfo:
Firmware = 0
BandwidthStats = 1
## Cmd.{Get,Set}Params indexes
class Params:
Delays = 0
## Cmd.SetBusType values
class BusType:
Invalid = 0
IBMPC = 1
Shugart = 2
## Flux read stream opcodes, preceded by 0xFF byte
class FluxOp:
Index = 1
Space = 2
Astable = 3
## CmdError: Encapsulates a command acknowledgement.
class CmdError(Exception):
def __init__(self, cmd, code):
self.cmd = cmd
self.code = code
def cmd_str(self):
return Cmd.str.get(self.cmd[0], "UnknownCmd")
def errcode_str(self):
if self.code == Ack.BadCylinder:
s = Ack.str[Ack.BadCylinder]
return s + " %d" % struct.unpack('2Bb', self.cmd)[2]
return Ack.str.get(self.code, "Unknown Error (%u)" % self.code)
def __str__(self):
return "%s: %s" % (self.cmd_str(), self.errcode_str())
class Unit:
## Unit information, instance variables:
## major, minor: Greaseweazle firmware version number
## max_cmd: Maximum Cmd number accepted by this unit
## sample_freq: Resolution of all time values passed to/from this unit
## update_mode: True iff the Greaseweazle unit is in update mode
## Unit(ser):
## Accepts a Pyserial instance for Greaseweazle communications.
def __init__(self, ser):
self.ser = ser
self.reset()
# Copy firmware info to instance variables (see above for definitions).
self._send_cmd(struct.pack("3B", Cmd.GetInfo, 3, GetInfo.Firmware))
x = struct.unpack("<4BI3B21x", self.ser.read(32))
(self.major, self.minor, is_main_firmware,
self.max_cmd, self.sample_freq, self.hw_model,
self.hw_submodel, self.usb_speed) = x
self.version = (self.major, self.minor)
# Old firmware doesn't report HW type but runs on STM32F1 only.
if self.hw_model == 0:
self.hw_model = 1
# Check whether firmware is in update mode: limited command set if so.
self.update_mode = (is_main_firmware == 0)
if self.update_mode:
self.update_jumpered = (self.sample_freq & 1)
del self.sample_freq
return
# We are running main firmware: Check whether an update is needed.
# We can use only the GetInfo command if the firmware is out of date.
self.update_needed = (self.version < EARLIEST_SUPPORTED_FIRMWARE or
self.version > (version.major, version.minor))
if self.update_needed:
return
# Initialise the delay properties with current firmware values.
self._send_cmd(struct.pack("4B", Cmd.GetParams, 4, Params.Delays, 10))
(self._select_delay, self._step_delay,
self._seek_settle_delay, self._motor_delay,
self._watchdog_delay) = struct.unpack("<5H", self.ser.read(10))
## reset:
## Resets communications with Greaseweazle.
def reset(self):
self.ser.reset_output_buffer()
self.ser.baudrate = ControlCmd.ClearComms
self.ser.baudrate = ControlCmd.Normal
self.ser.reset_input_buffer()
self.ser.close()
self.ser.open()
## _send_cmd:
## Send given command byte sequence to Greaseweazle.
## Raise a CmdError if command fails.
def _send_cmd(self, cmd):
self.ser.write(cmd)
(c,r) = struct.unpack("2B", self.ser.read(2))
error.check(c == cmd[0], "Command returned garbage (%02x != %02x)"
% (c, cmd[0]))
if r != 0:
raise CmdError(cmd, r)
## seek:
## Seek the selected drive's heads to the specified track (cyl, head).
def seek(self, cyl, head):
self._send_cmd(struct.pack("2Bb", Cmd.Seek, 3, cyl))
self._send_cmd(struct.pack("3B", Cmd.Head, 3, head))
## set_bus_type:
## Set the floppy bus type.
def set_bus_type(self, type):
self._send_cmd(struct.pack("3B", Cmd.SetBusType, 3, type))
## set_pin:
## Set a pin level.
def set_pin(self, pin, level):
self._send_cmd(struct.pack("4B", Cmd.SetPin, 4, pin, int(level)))
## get_pin:
## Get a pin level.
def get_pin(self, pin):
self._send_cmd(struct.pack("3B", Cmd.GetPin, 3, pin))
v, = struct.unpack("B", self.ser.read(1))
return v
## power_on_reset:
## Re-initialise to power-on defaults.
def power_on_reset(self):
self._send_cmd(struct.pack("2B", Cmd.Reset, 2))
## drive_select:
## Select the specified drive unit.
def drive_select(self, unit):
self._send_cmd(struct.pack("3B", Cmd.Select, 3, unit))
## drive_deselect:
## Deselect currently-selected drive unit (if any).
def drive_deselect(self):
self._send_cmd(struct.pack("2B", Cmd.Deselect, 2))
## drive_motor:
## Turn the specified drive's motor on/off.
def drive_motor(self, unit, state):
self._send_cmd(struct.pack("4B", Cmd.Motor, 4, unit, int(state)))
## switch_fw_mode:
## Switch between update 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):
self._send_cmd(struct.pack("<2BI", Cmd.Update, 6, len(dat)))
self.ser.write(dat)
(ack,) = struct.unpack("B", self.ser.read(1))
return ack
## update_bootloader:
## Update Greaseweazle with the given new bootloader.
def update_bootloader(self, dat):
self._send_cmd(struct.pack("<2B2I", Cmd.Update, 10,
len(dat), 0xdeafbee3))
self.ser.write(dat)
(ack,) = struct.unpack("B", self.ser.read(1))
return ack
## _decode_flux:
## Decode the Greaseweazle data stream into a list of flux samples.
def _decode_flux(self, dat):
flux, index = [], []
assert dat[-1] == 0
dat_i = it.islice(dat, 0, len(dat)-1)
ticks, ticks_since_index = 0, 0
def _read_28bit():
val = (next(dat_i) & 254) >> 1
val += (next(dat_i) & 254) << 6
val += (next(dat_i) & 254) << 13
val += (next(dat_i) & 254) << 20
return val
try:
while True:
i = next(dat_i)
if i == 255:
opcode = next(dat_i)
if opcode == FluxOp.Index:
val = _read_28bit()
index.append(ticks_since_index + ticks + val)
ticks_since_index = -(ticks + val)
elif opcode == FluxOp.Space:
ticks += _read_28bit()
else:
raise error.Fatal("Bad opcode in flux stream (%d)"
% opcode)
else:
if i < 250:
val = i
else:
val = 250 + (i - 250) * 255
val += next(dat_i) - 1
ticks += val
flux.append(ticks)
ticks_since_index += ticks
ticks = 0
except StopIteration:
pass
return flux, index
## _encode_flux:
## Convert the given flux timings into an encoded data stream.
def _encode_flux(self, flux):
nfa_thresh = round(150e-6 * self.sample_freq) # 150us
nfa_period = round(1.25e-6 * self.sample_freq) # 1.25us
dat = bytearray()
def _write_28bit(x):
dat.append(1 | (x<<1) & 255)
dat.append(1 | (x>>6) & 255)
dat.append(1 | (x>>13) & 255)
dat.append(1 | (x>>20) & 255)
# Emit a dummy final flux value. This is never written to disk because
# the write is aborted immediately the final flux is loaded into the
# WDATA timer. The dummy flux is sacrificial, ensuring that the real
# final flux gets written in full.
dummy_flux = round(100e-6 * self.sample_freq)
for val in it.chain(flux, [dummy_flux]):
if val == 0:
pass
elif val < 250:
dat.append(val)
elif val > nfa_thresh:
dat.append(255)
dat.append(FluxOp.Space)
_write_28bit(val)
dat.append(255)
dat.append(FluxOp.Astable)
_write_28bit(nfa_period)
else:
high = (val-250) // 255
if high < 5:
dat.append(250 + high)
dat.append(1 + (val-250) % 255)
else:
dat.append(255)
dat.append(FluxOp.Space)
_write_28bit(val - 249)
dat.append(249)
dat.append(0) # End of Stream
return dat
## _read_track:
## Private helper which issues command requests to Greaseweazle.
def _read_track(self, revs, ticks):
# Request and read all flux timings for this track.
dat = bytearray()
self._send_cmd(struct.pack("<2BIH", Cmd.ReadFlux, 8,
ticks, revs+1))
while True:
dat += self.ser.read(1)
dat += self.ser.read(self.ser.in_waiting)
if dat[-1] == 0:
break
# Check flux status. An exception is raised if there was an error.
self._send_cmd(struct.pack("2B", Cmd.GetFluxStatus, 2))
return dat
## read_track:
## Read and decode flux and index timings for the current track.
def read_track(self, revs, ticks=0, nr_retries=5):
retry = 0
while True:
try:
dat = self._read_track(revs, ticks)
except CmdError as error:
# An error occurred. We may retry on transient overflows.
if error.code == Ack.FluxOverflow and retry < nr_retries:
retry += 1
else:
raise error
else:
# Success!
break
try:
# Decode the flux list and read the index-times list.
flux_list, index_list = optimised.decode_flux(dat)
except AttributeError:
flux_list, index_list = self._decode_flux(dat)
# Success: Return the requested full index-to-index revolutions.
return Flux(index_list, flux_list, self.sample_freq, index_cued=False)
## write_track:
## Write the given flux stream to the current track via Greaseweazle.
def write_track(self, flux_list, terminate_at_index,
cue_at_index=True, nr_retries=5):
# Create encoded data stream.
dat = self._encode_flux(flux_list)
retry = 0
while True:
try:
# Write the flux stream to the track via Greaseweazle.
self._send_cmd(struct.pack("4B", Cmd.WriteFlux, 4,
int(cue_at_index),
int(terminate_at_index)))
self.ser.write(dat)
self.ser.read(1) # Sync with Greaseweazle
self._send_cmd(struct.pack("2B", Cmd.GetFluxStatus, 2))
except CmdError as error:
# An error occurred. We may retry on transient underflows.
if error.code == Ack.FluxUnderflow and retry < nr_retries:
retry += 1
else:
raise error
else:
# Success!
break
## erase_track:
## Erase the current track via Greaseweazle.
def erase_track(self, ticks):
self._send_cmd(struct.pack("<2BI", Cmd.EraseFlux, 6, int(ticks)))
self.ser.read(1) # Sync with Greaseweazle
self._send_cmd(struct.pack("2B", Cmd.GetFluxStatus, 2))
## source_bytes:
## Command Greaseweazle to source 'nr' garbage bytes.
def source_bytes(self, nr, seed):
try:
self._send_cmd(struct.pack("<2B2I", Cmd.SourceBytes, 10, nr, seed))
dat = self.ser.read(nr)
except CmdError as error:
if error.code != Ack.BadCommand:
raise
# Firmware v0.28 and earlier
self._send_cmd(struct.pack("<2BI", Cmd.SourceBytes, 6, nr))
self.ser.read(nr)
dat = None
return dat
## sink_bytes:
## Command Greaseweazle to sink given data buffer.
def sink_bytes(self, dat, seed):
try:
self._send_cmd(struct.pack("<2BII", Cmd.SinkBytes, 10,
len(dat), seed))
except CmdError as error:
if error.code != Ack.BadCommand:
raise
# Firmware v0.28 and earlier
self._send_cmd(struct.pack("<2BI", Cmd.SinkBytes, 6, len(dat)))
self.ser.write(dat)
(ack,) = struct.unpack("B", self.ser.read(1))
return ack
## bw_stats:
## Get min/max bandwidth for previous source/sink command. Mbps (float).
def bw_stats(self):
self._send_cmd(struct.pack("3B", Cmd.GetInfo, 3,
GetInfo.BandwidthStats))
min_bytes, min_usecs, max_bytes, max_usecs = struct.unpack(
"<4I16x", self.ser.read(32))
min_bw = (8 * min_bytes) / min_usecs
max_bw = (8 * max_bytes) / max_usecs
return min_bw, max_bw
##
## Delay-property public getters and setters:
## select_delay: Delay (usec) after asserting drive select
## step_delay: Delay (usec) after issuing a head-step command
## seek_settle_delay: Delay (msec) after completing a head-seek operation
## motor_delay: Delay (msec) after turning on drive spindle motor
## watchdog_delay: Timeout (msec) since last command upon which all
## drives are deselected and spindle motors turned off
##
def _set_delays(self):
self._send_cmd(struct.pack("<3B5H", Cmd.SetParams,
3+5*2, Params.Delays,
self._select_delay, self._step_delay,
self._seek_settle_delay,
self._motor_delay, self._watchdog_delay))
@property
def select_delay(self):
return self._select_delay
@select_delay.setter
def select_delay(self, select_delay):
self._select_delay = select_delay
self._set_delays()
@property
def step_delay(self):
return self._step_delay
@step_delay.setter
def step_delay(self, step_delay):
self._step_delay = step_delay
self._set_delays()
@property
def seek_settle_delay(self):
return self._seek_settle_delay
@seek_settle_delay.setter
def seek_settle_delay(self, seek_settle_delay):
self._seek_settle_delay = seek_settle_delay
self._set_delays()
@property
def motor_delay(self):
return self._motor_delay
@motor_delay.setter
def motor_delay(self, motor_delay):
self._motor_delay = motor_delay
self._set_delays()
@property
def watchdog_delay(self):
return self._watchdog_delay
@watchdog_delay.setter
def watchdog_delay(self, watchdog_delay):
self._watchdog_delay = watchdog_delay
self._set_delays()
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,109 +0,0 @@
#!/usr/bin/env python3
# gw.py
#
# Greaseweazle control script.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# 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
import importlib
from greaseweazle import version
if hasattr(version, 'commit'):
print("""*** TEST/PRE-RELEASE: commit %s
*** Use these tools and firmware ONLY for test and development!!"""
% version.commit)
missing_modules = []
try:
import bitarray
except ImportError:
missing_modules.append("bitarray")
try:
import crcmod
except ImportError:
missing_modules.append("crcmod")
try:
import serial.tools.list_ports
except ImportError:
missing_modules.append("pyserial")
if missing_modules:
print("""\
** Missing Python modules: %s
For installation instructions please read the wiki:
<https://github.com/keirf/Greaseweazle/wiki/Software-Installation>"""
% ', '.join(missing_modules))
sys.exit(1)
actions = [ 'info',
'read',
'write',
'erase',
'clean',
'seek',
'delays',
'update',
'pin',
'reset',
'bandwidth' ]
argv = sys.argv
def usage():
print("Usage: %s [--time] [action] [-h] ..." % (argv[0]))
print(" --time Print elapsed time after action is executed")
print(" -h, --help Show help message for specified action")
print("Actions:")
for a in actions:
mod = importlib.import_module('greaseweazle.tools.' + a)
print(' %-12s%s' % (a, mod.__dict__['description']))
sys.exit(1)
backtrace = False
start_time = None
while len(argv) > 1 and argv[1].startswith('--'):
if argv[1] == '--bt':
backtrace = True
elif argv[1] == '--time':
start_time = time.time()
else:
usage()
argv = [argv[0]] + argv[2:]
if len(argv) < 2 or argv[1] not in actions:
usage()
mod = importlib.import_module('greaseweazle.tools.' + argv[1])
main = mod.__dict__['main']
try:
res = main(argv)
if res is None:
res = 0
except (IndexError, AssertionError, TypeError, KeyError):
raise
except KeyboardInterrupt:
if backtrace: raise
res = 1
except Exception as err:
if backtrace: raise
print("** FATAL ERROR:")
print(err)
res = 1
if start_time is not None:
elapsed = time.time() - start_time
print("Time elapsed: %.2f seconds" % elapsed)
sys.exit(res)
# Local variables:
# python-indent: 4
# End:

View File

@@ -1,42 +0,0 @@
#!/usr/bin/env python3
# Create a public download link for any of my Github Actions artifacts.
# Optionally download the zip file.
# Written & released by Keir Fraser <keir.xen@gmail.com>
import re, sys
import requests
# Latest artifact webpage:
# https://nightly.link/keirf/FlashFloppy/workflows/ci/master
# Github Actions artifact link and corrsponding public link:
# https://github.com/keirf/FlashFloppy/suites/1623687905/artifacts/29859161
# https://nightly.link/keirf/FlashFloppy/actions/artifacts/29859161
def print_usage():
print('artifact.py [-d] <github_artifact_url>')
sys.exit(0)
if not(2 <= len(sys.argv) <= 3):
print_usage()
if len(sys.argv) == 3:
if sys.argv[1] != '-d':
print_usage()
download = True
url = sys.argv[2]
else:
download = False
url = sys.argv[1]
url = re.sub('github.com', 'nightly.link', url)
url = re.sub('suites/\d+', 'actions', url)
url += '.zip'
print(url)
if download:
res = requests.get(url)
content_disposition = res.headers['Content-Disposition']
zipname = re.search('filename=([^ ]+.zip);', content_disposition).group(1)
print('Downloading to: ' + zipname)
with open(zipname, 'wb') as f:
f.write(res.content)

View File

@@ -1,14 +0,0 @@
#!/bin/bash
# Creates a random Amiga ADF, writes the first three cylinders of a disk,
# dumps those cylinders back, and checks against original ADF.
dd if=/dev/urandom of=a.adf bs=512 count=1760
disk-analyse -e 2 a.adf b.adf
disk-analyse a.adf a.scp
./gw write --tracks=c=0-2 a.scp
./gw read --revs=1 --tracks=c=0-2 b.scp
disk-analyse -e 2 b.scp c.adf
diff b.adf c.adf
md5sum b.adf c.adf
rm -f a.adf b.adf c.adf a.scp b.scp

View File

@@ -1,50 +0,0 @@
# ipf_align.py
#
# Align all tracks in an IPF image to the same offset from index mark.
#
# Written & released by Keir Fraser <keir.xen@gmail.com>
#
# This is free and unencumbered software released into the public domain.
# See the file COPYING for more details, or visit <http://unlicense.org>.
import struct, sys, crcmod.predefined
def main(argv):
crc32 = crcmod.predefined.Crc('crc-32')
offset = 1024
if len(argv) == 4:
offset = int(argv[3])
elif len(argv) != 3:
print("%s <input_file> <output_file> [<offset>]" % argv[0])
return
with open(argv[1], "rb") as f:
in_dat = bytearray(f.read())
out_dat = bytearray()
while in_dat:
# Decode the common record header
id, length, crc = struct.unpack(">4s2I", in_dat[:12])
# Consume the record from the input array
record = in_dat[:length]
in_dat = in_dat[length:]
# Check the CRC
record[8:12] = bytes(4)
assert crc == crc32.new(record).crcValue, "CRC mismatch"
# Modify the record as necessary
if id == b'IMGE':
trkbits, = struct.unpack(">I", record[48:52])
if trkbits > offset:
record[32:40] = struct.pack(">2I", offset//8, offset)
# Re-calculate the CRC
record[8:12] = struct.pack(">I", crc32.new(record).crcValue)
# DATA chunk has extra data to copy
if id == b'DATA':
size, bsize, dcrc, datchunk = struct.unpack(">4I", record[12:28])
record += in_dat[:size]
in_dat = in_dat[size:]
# Write the full modified record into the output array
out_dat += record
with open(argv[2], "wb") as f:
f.write(out_dat)
if __name__ == "__main__":
main(sys.argv)

View File

@@ -1,96 +0,0 @@
import struct, sys
import matplotlib.pyplot as plt
NO_DAT = 0
PRINT_DAT = 1
PLOT_DAT = 2
def decode_flux(tdat):
flux, fluxl = 0, []
while tdat:
f, = struct.unpack(">H", tdat[:2])
tdat = tdat[2:]
if f == 0:
flux += 65536
else:
flux += f
fluxl.append(flux / 40)
flux = 0
return fluxl
def dump_track(dat, trk_offs, trknr, show_dat):
print("Track %u:" % trknr)
trk_off = trk_offs[trknr]
if trk_off == 0:
print("Empty")
return
# Parse the SCP track header and extract the flux data.
thdr = dat[trk_off:trk_off+4+12*nr_revs]
sig, tnr, _, _, s_off = struct.unpack("<3sB3I", thdr[:16])
assert sig == b"TRK"
assert tnr == trknr
p_idx, tot = [], 0.0
for i in range(nr_revs):
t,n,off = struct.unpack("<3I", thdr[4+i*12:4+(i+1)*12])
flux = decode_flux(dat[trk_off+off:trk_off+off+n*2])
print("Rev %u: time=%.2fus nr_flux=%u tot_flux=%.2fus"
% (i, t/40, n, sum(flux)))
tot += t/40
p_idx.append(tot)
if not show_dat:
return
_, e_nr, e_off = struct.unpack("<3I", thdr[-12:])
fluxl = decode_flux(dat[trk_off+s_off:trk_off+e_off+e_nr*2])
tot = 0.0
i = 0
px, py = [0], [0]
for x in fluxl:
if show_dat == PRINT_DAT:
bad = ""
if (x < 3.6) or ((x > 4.4) and (x < 5.4)) \
or ((x > 6.6) and (x < 7.2)) or (x > 8.8):
bad = "BAD"
print("%d: %f %s" % (i, x, bad))
elif x < 50:
px.append(tot/1000)
py.append(x)
i += 1
tot += x
print("Total: %uus (%uus per rev)" % (int(tot), tot//nr_revs))
if show_dat == PLOT_DAT:
plt.xlabel("Time (ms)")
plt.ylabel("Flux (us)")
plt.gcf().set_size_inches(12, 8)
plt.axvline(x=0, ymin=0.95, color='r')
for t in p_idx:
plt.axvline(x=t/1000, ymin=0.95, color='r')
plt.scatter(px, py, s=1)
plt.show()
argv = sys.argv
plot = (argv[1] == '--plot')
if plot:
argv = argv[1:]
with open(argv[1], "rb") as f:
dat = f.read()
header = struct.unpack("<3s9BI", dat[0:16])
(sig, _, _, nr_revs, s_trk, e_trk, flags, _, ss, _, _) = header
assert sig == b"SCP"
nr_sides = 1 if ss else 2
trk_offs = struct.unpack("<168I", dat[16:0x2b0])
print("Revolutions: %u" % nr_revs)
if len(argv) == 3:
dump_track(dat, trk_offs, int(argv[2]), PLOT_DAT if plot else PRINT_DAT)
else:
for i in range(s_trk, e_trk+1):
dump_track(dat, trk_offs, i, NO_DAT)

View File

@@ -1,52 +0,0 @@
#!/bin/bash
# Creates a random Amiga ADF, writes the first three cylinders of a disk,
# dumps those cylinders back, and checks against original ADF.
dd if=/dev/urandom of=b.adf bs=512 count=1760
disk-analyse -e 2 b.adf a.adf
rm -f b.adf
# Write and verify ADF, Read ADF
./gw --bt write --tracks="c=0-2" a.adf
./gw --bt read --revs=1 --tracks="c=0-2" b.adf
disk-analyse -e 2 b.adf c.adf
diff a.adf c.adf
md5sum a.adf c.adf
rm -f b.adf c.adf
# Write SCP, Read SCP
disk-analyse a.adf a.scp
./gw --bt write --tracks="c=0-2" a.scp
./gw --bt read --revs=1 --tracks="c=0-2" b.scp
disk-analyse -e 2 b.scp b.adf
diff a.adf b.adf
md5sum a.adf b.adf
rm -f b.adf a.scp b.scp
# Write IPF, Read HFE
disk-analyse a.adf a.ipf
./gw --bt write --tracks="c=0-2" a.ipf
./gw --bt read --revs=1 --tracks="c=0-2" b.hfe
disk-analyse -e 2 b.hfe b.adf
diff a.adf b.adf
md5sum a.adf b.adf
rm -f b.adf a.ipf b.hfe
# Write HFE, Read HFE
disk-analyse a.adf a.hfe
./gw --bt write --tracks="c=0-2" a.hfe
./gw --bt read --revs=1 --tracks="c=0-2" b.hfe
disk-analyse -e 2 b.hfe b.adf
diff a.adf b.adf
md5sum a.adf b.adf
# Read Kryoflux
mkdir a
./gw --bt read --revs=1 --tracks="c=0-2" a/
disk-analyse -e 2 a/ b.adf
diff a.adf b.adf
md5sum a.adf b.adf
rm -f b.adf c.adf a.hfe b.hfe
rm -rf a
rm -f a.adf

View File

@@ -1,63 +0,0 @@
import platform, sys
import serial.tools.list_ports
# MacOS Catalina:
# platform.system == "Darwin"
# Attrs: device, manufacturer, product, vid, pid, location, serial_number
#
# Ubuntu 18.04:
# platform.system == "Linux"
# Attrs: device, manufacturer, product, vid, pid, location, serial_number
#
# Windows 7:
# platform.system, platform.release == "Windows", "7"
# Attrs: device, vid, pid, location, serial_number
# (manufacturer == "InterBiometrics")
#
# Windows 10:
# platform.system, platform.release == "Windows", "10"
# Attrs: device, vid, pid, location, serial_number
# (manufacturer == "Microsoft")
print("platform.system: %s" % platform.system())
print("platform.version: %s" % platform.version())
print("platform.release: %s" % platform.release())
ports = serial.tools.list_ports.comports()
i = 0
for port in ports:
i += 1
print("Port %d:" % i)
if port.device:
print(" device: '%s'" % port.device)
if port.name:
print(" name: '%s'" % port.name)
if port.hwid:
print(" hwid: '%s'" % port.hwid)
if port.manufacturer:
print(" manufacturer: '%s'" % port.manufacturer)
if port.product:
print(" product: '%s'" % port.product)
if port.vid:
print(" vid: %04x" % port.vid)
if port.pid:
print(" pid: %04x" % port.pid)
if port.location:
print(" location: '%s'" % port.location)
if port.serial_number:
print(" serial_number: '%s'" % port.serial_number)
if port.interface:
print(" interface: '%s'" % port.interface)
if len(sys.argv) < 2 or sys.argv[1] != "loop":
sys.exit(0)
# Loop checking that .location is always valid for a Weazle
while True:
for port in serial.tools.list_ports.comports():
if port.vid == 0x1209:
if not port.location:
print("BAD", flush=True)
else:
print(".", end="", flush=True)

View File

@@ -28,7 +28,15 @@
import crcmod.predefined
import re, struct, sys
from greaseweazle import version
class Version:
def __init__(self, major, minor):
self.major, self.minor = major, minor
with open('Makefile', 'r') as f:
l = f.read()
major = int(re.search('FW_MAJOR := (\d+)', l).group(1))
minor = int(re.search('FW_MINOR := (\d+)', l).group(1))
version = Version(major, minor)
name_to_hw_model = { 'stm32f1': 1,
'stm32f7': 7,

View File

@@ -1,19 +0,0 @@
from cx_Freeze import setup, Executable
from greaseweazle import version
buildOptions = dict(
packages = ['greaseweazle'],
excludes = ['tkinter', 'test', 'distutils', 'email'],
include_msvcr = True)
base = 'Console'
executables = [
Executable('gw.py', base=base)
]
setup(name='Greaseweazle',
version = f'{version.major}.{version.minor}',
description = '',
options = dict(build_exe = buildOptions),
executables = executables)

View File

@@ -1,9 +0,0 @@
#!/bin/bash
PYTHON="${PYTHON:-python3}"
if [ ! -d ./scripts/c_ext ]; then
echo "** Please run setup.sh from within the Greaseweazle folder";
echo "** eg: ./scripts/setup.sh";
exit 1;
fi ;
$PYTHON -m pip install --user bitarray crcmod pyserial
(cd ./scripts/c_ext && $PYTHON setup.py install --install-platlib=../greaseweazle/optimised)