mirror of
https://github.com/keirf/greaseweazle-firmware.git
synced 2025-10-31 11:06:44 -07:00
Remove tools from the new firmware repository.
This commit is contained in:
43
Makefile
43
Makefile
@@ -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
|
||||
|
||||
16
README.md
16
README.md
@@ -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
14
gw
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
from distutils.core import setup, Extension
|
||||
|
||||
module1 = Extension('optimised', sources = ['optimised.c'])
|
||||
|
||||
setup(name = 'optimised',
|
||||
ext_modules = [module1])
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
109
scripts/gw.py
109
scripts/gw.py
@@ -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:
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user