diskstack 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diskstack/__init__.py +5 -0
- diskstack/_vendor/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/COPYING +24 -0
- diskstack/_vendor/greaseweazle/__init__.py +2 -0
- diskstack/_vendor/greaseweazle/codec/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/amiga/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/amiga/amigados.py +221 -0
- diskstack/_vendor/greaseweazle/codec/apple2/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/apple2/apple2_gcr.py +223 -0
- diskstack/_vendor/greaseweazle/codec/bitcell.py +117 -0
- diskstack/_vendor/greaseweazle/codec/codec.py +362 -0
- diskstack/_vendor/greaseweazle/codec/commodore/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/commodore/c64_gcr.py +229 -0
- diskstack/_vendor/greaseweazle/codec/datageneral/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/datageneral/datageneral.py +211 -0
- diskstack/_vendor/greaseweazle/codec/hp/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/hp/hp_mmfm.py +258 -0
- diskstack/_vendor/greaseweazle/codec/ibm/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/ibm/ibm.py +1005 -0
- diskstack/_vendor/greaseweazle/codec/macintosh/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/macintosh/mac_gcr.py +238 -0
- diskstack/_vendor/greaseweazle/codec/micropolis/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/micropolis/micropolis.py +233 -0
- diskstack/_vendor/greaseweazle/codec/northstar/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/codec/northstar/northstar.py +210 -0
- diskstack/_vendor/greaseweazle/data/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/data/diskdefs.cfg +81 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_acorn.cfg +119 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_akai.cfg +27 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_amiga.cfg +17 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_apple2.cfg +31 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_apricot.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_atari.cfg +33 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_atarist.cfg +76 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_coco.cfg +98 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_commodore.cfg +127 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_datageneral.cfg +8 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_dec.cfg +37 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_dragon.cfg +61 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_eagle.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_ensoniq.cfg +39 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_epson.cfg +95 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_gem.cfg +15 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_hp.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_ibm.cfg +140 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_kaypro.cfg +58 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_luxor.cfg +96 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_mac.cfg +71 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_micropolis.cfg +73 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_mm1.cfg +69 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_msx.cfg +45 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_northstar.cfg +37 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_occ1.cfg +23 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_olivetti.cfg +18 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_pc98.cfg +69 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_raw.cfg +25 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_rm.cfg +98 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_sci.cfg +15 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_sega.cfg +13 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_sharp.cfg +13 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_thomson.cfg +78 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_tsc.cfg +81 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_xerox.cfg +59 -0
- diskstack/_vendor/greaseweazle/data/diskdefs_zx.cfg +245 -0
- diskstack/_vendor/greaseweazle/error.py +19 -0
- diskstack/_vendor/greaseweazle/flux.py +315 -0
- diskstack/_vendor/greaseweazle/image/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/image/a2r.py +133 -0
- diskstack/_vendor/greaseweazle/image/acorn.py +28 -0
- diskstack/_vendor/greaseweazle/image/adf.py +15 -0
- diskstack/_vendor/greaseweazle/image/apple2.py +18 -0
- diskstack/_vendor/greaseweazle/image/apridisk.py +107 -0
- diskstack/_vendor/greaseweazle/image/caps.py +464 -0
- diskstack/_vendor/greaseweazle/image/d64.py +51 -0
- diskstack/_vendor/greaseweazle/image/d81.py +28 -0
- diskstack/_vendor/greaseweazle/image/d88.py +211 -0
- diskstack/_vendor/greaseweazle/image/dcp.py +45 -0
- diskstack/_vendor/greaseweazle/image/dim.py +53 -0
- diskstack/_vendor/greaseweazle/image/dmk.py +166 -0
- diskstack/_vendor/greaseweazle/image/dsk.py +31 -0
- diskstack/_vendor/greaseweazle/image/edsk.py +591 -0
- diskstack/_vendor/greaseweazle/image/fd.py +23 -0
- diskstack/_vendor/greaseweazle/image/fdi.py +41 -0
- diskstack/_vendor/greaseweazle/image/hdm.py +15 -0
- diskstack/_vendor/greaseweazle/image/hfe.py +742 -0
- diskstack/_vendor/greaseweazle/image/image.py +130 -0
- diskstack/_vendor/greaseweazle/image/imd.py +201 -0
- diskstack/_vendor/greaseweazle/image/img.py +111 -0
- diskstack/_vendor/greaseweazle/image/kryoflux.py +304 -0
- diskstack/_vendor/greaseweazle/image/mgt.py +15 -0
- diskstack/_vendor/greaseweazle/image/msa.py +153 -0
- diskstack/_vendor/greaseweazle/image/nfd.py +113 -0
- diskstack/_vendor/greaseweazle/image/nsi.py +47 -0
- diskstack/_vendor/greaseweazle/image/scp.py +471 -0
- diskstack/_vendor/greaseweazle/image/sf7.py +15 -0
- diskstack/_vendor/greaseweazle/image/sharp2d.py +15 -0
- diskstack/_vendor/greaseweazle/image/td0.py +141 -0
- diskstack/_vendor/greaseweazle/image/xdf.py +15 -0
- diskstack/_vendor/greaseweazle/optimised/__init__.py +22 -0
- diskstack/_vendor/greaseweazle/tools/__init__.py +0 -0
- diskstack/_vendor/greaseweazle/tools/util.py +133 -0
- diskstack/_vendor/greaseweazle/track.py +494 -0
- diskstack/cache.py +210 -0
- diskstack/candidates.py +396 -0
- diskstack/cli.py +348 -0
- diskstack/errors.py +5 -0
- diskstack/filler.py +29 -0
- diskstack/formats.py +381 -0
- diskstack/parallel.py +73 -0
- diskstack/report.py +353 -0
- diskstack/stack.py +317 -0
- diskstack-1.0.0.dist-info/METADATA +329 -0
- diskstack-1.0.0.dist-info/RECORD +119 -0
- diskstack-1.0.0.dist-info/WHEEL +5 -0
- diskstack-1.0.0.dist-info/entry_points.txt +2 -0
- diskstack-1.0.0.dist-info/licenses/LICENSE +21 -0
- diskstack-1.0.0.dist-info/licenses/NOTICE +55 -0
- diskstack-1.0.0.dist-info/licenses/diskstack/_vendor/greaseweazle/COPYING +24 -0
- diskstack-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Extracted from greaseweazle/tools/util.py by tools/vendor.py.
|
|
2
|
+
# The rest of that module drives the USB hardware and needs pyserial.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
import re
|
|
6
|
+
import itertools as it
|
|
7
|
+
from diskstack._vendor.greaseweazle import error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def columnify(strings, columns=80, sep=2):
|
|
11
|
+
max_len = max(len(s) for s in strings) + sep
|
|
12
|
+
per_row = max(1, columns // max_len)
|
|
13
|
+
return '\n'.join(map(lambda row: (f'{{:{max_len}}}'*per_row).format(*row),
|
|
14
|
+
it.zip_longest(*[iter(strings)]*per_row,
|
|
15
|
+
fillvalue='')))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def range_str(l):
|
|
19
|
+
if len(l) == 0:
|
|
20
|
+
return '<none>'
|
|
21
|
+
p, str = None, ''
|
|
22
|
+
for i in l:
|
|
23
|
+
if p is not None and i == p[1]+1:
|
|
24
|
+
p = p[0], i
|
|
25
|
+
continue
|
|
26
|
+
if p is not None:
|
|
27
|
+
str += ('%d,' % p[0]) if p[0] == p[1] else ('%d-%d,' % p)
|
|
28
|
+
p = (i,i)
|
|
29
|
+
if p is not None:
|
|
30
|
+
str += ('%d' % p[0]) if p[0] == p[1] else ('%d-%d' % p)
|
|
31
|
+
return str
|
|
32
|
+
|
|
33
|
+
class TrackSet:
|
|
34
|
+
|
|
35
|
+
class TrackIter:
|
|
36
|
+
"""Iterate over a TrackSet in physical <cyl,head> order."""
|
|
37
|
+
def __init__(self, ts):
|
|
38
|
+
l = []
|
|
39
|
+
for c in ts.cyls:
|
|
40
|
+
for h in ts.heads:
|
|
41
|
+
pc, ph = ts.ch_to_pch(c, h)
|
|
42
|
+
l.append((pc, ph, c, h))
|
|
43
|
+
l.sort()
|
|
44
|
+
self.l = iter(l)
|
|
45
|
+
def __iter__(self):
|
|
46
|
+
return self
|
|
47
|
+
def __next__(self):
|
|
48
|
+
(self.physical_cyl, self.physical_head,
|
|
49
|
+
self.cyl, self.head) = next(self.l)
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __init__(self, trackspec):
|
|
53
|
+
self.cyls = list()
|
|
54
|
+
self.heads = list()
|
|
55
|
+
self.h_off = [0]*2
|
|
56
|
+
self.step = 1
|
|
57
|
+
self.hswap = False
|
|
58
|
+
self.trackspec = ''
|
|
59
|
+
self.update_from_trackspec(trackspec)
|
|
60
|
+
|
|
61
|
+
def ch_to_pch(self, c, h):
|
|
62
|
+
pc = c//-self.step if self.step < 0 else c*self.step
|
|
63
|
+
pc += self.h_off[h]
|
|
64
|
+
ph = 1-h if self.hswap else h
|
|
65
|
+
return pc, ph
|
|
66
|
+
|
|
67
|
+
def update_from_trackspec(self, trackspec):
|
|
68
|
+
"""Update a TrackSet based on a trackspec."""
|
|
69
|
+
self.trackspec += trackspec
|
|
70
|
+
for x in trackspec.split(':'):
|
|
71
|
+
if x == 'hswap':
|
|
72
|
+
self.hswap = True
|
|
73
|
+
continue
|
|
74
|
+
k,v = x.split('=')
|
|
75
|
+
if k == 'c':
|
|
76
|
+
cyls = set()
|
|
77
|
+
for crange in v.split(','):
|
|
78
|
+
m = re.match(r'(\d+)(-(\d+)(/(\d+))?)?$', crange)
|
|
79
|
+
if m is None: raise ValueError()
|
|
80
|
+
if m.group(3) is None:
|
|
81
|
+
s,e,step = int(m.group(1)), int(m.group(1)), 1
|
|
82
|
+
else:
|
|
83
|
+
s,e,step = int(m.group(1)), int(m.group(3)), 1
|
|
84
|
+
if m.group(5) is not None:
|
|
85
|
+
step = int(m.group(5))
|
|
86
|
+
for c in range(s, e+1, step):
|
|
87
|
+
cyls.add(c)
|
|
88
|
+
self.cyls = sorted(cyls)
|
|
89
|
+
elif k == 'h':
|
|
90
|
+
heads = [False]*2
|
|
91
|
+
for hrange in v.split(','):
|
|
92
|
+
m = re.match(r'([01])(-([01]))?$', hrange)
|
|
93
|
+
if m is None: raise ValueError()
|
|
94
|
+
if m.group(3) is None:
|
|
95
|
+
s,e = int(m.group(1)), int(m.group(1))
|
|
96
|
+
else:
|
|
97
|
+
s,e = int(m.group(1)), int(m.group(3))
|
|
98
|
+
for h in range(s, e+1):
|
|
99
|
+
heads[h] = True
|
|
100
|
+
self.heads = []
|
|
101
|
+
for h in range(len(heads)):
|
|
102
|
+
if heads[h]: self.heads.append(h)
|
|
103
|
+
elif re.match(r'h[01].off$', k):
|
|
104
|
+
h = int(re.match(r'h([01]).off$', k).group(1))
|
|
105
|
+
m = re.match(r'([+-]\d+)$', v)
|
|
106
|
+
if m is None: raise ValueError()
|
|
107
|
+
self.h_off[h] = int(m.group(1))
|
|
108
|
+
elif k == 'step':
|
|
109
|
+
m = re.match(r'1/(\d+)$', v)
|
|
110
|
+
self.step = -int(m.group(1)) if m is not None else int(v)
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError()
|
|
113
|
+
|
|
114
|
+
def __str__(self):
|
|
115
|
+
s = 'c=%s' % range_str(self.cyls)
|
|
116
|
+
s += ':h=%s' % range_str(self.heads)
|
|
117
|
+
for i in range(len(self.h_off)):
|
|
118
|
+
x = self.h_off[i]
|
|
119
|
+
if x != 0:
|
|
120
|
+
s += ':h%d.off=%s%d' % (i, '+' if x >= 0 else '', x)
|
|
121
|
+
if self.step != 1:
|
|
122
|
+
s += ':step=' + (('1/%d' % -self.step) if self.step < 0
|
|
123
|
+
else ('%d' % self.step))
|
|
124
|
+
if self.hswap: s += ':hswap'
|
|
125
|
+
return s
|
|
126
|
+
|
|
127
|
+
def __iter__(self):
|
|
128
|
+
return self.TrackIter(self)
|
|
129
|
+
|
|
130
|
+
def __contains__(self, key):
|
|
131
|
+
c, h = key
|
|
132
|
+
return c in self.cyls and h in self.heads
|
|
133
|
+
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
# greaseweazle/track.py
|
|
2
|
+
#
|
|
3
|
+
# Written & released by Keir Fraser <keir.xen@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This is free and unencumbered software released into the public domain.
|
|
6
|
+
# See the file COPYING for more details, or visit <http://unlicense.org>.
|
|
7
|
+
|
|
8
|
+
from typing import Any, List, Optional, Tuple, Union, Protocol
|
|
9
|
+
import binascii
|
|
10
|
+
import itertools as it
|
|
11
|
+
from bitarray import bitarray
|
|
12
|
+
from diskstack._vendor.greaseweazle.flux import Flux, WriteoutFlux
|
|
13
|
+
from diskstack._vendor.greaseweazle import optimised
|
|
14
|
+
|
|
15
|
+
class PLL:
|
|
16
|
+
def __init__(self, pllspec: str):
|
|
17
|
+
self.period_adj_pct = 5
|
|
18
|
+
self.phase_adj_pct = 60
|
|
19
|
+
self.lowpass_thresh = None
|
|
20
|
+
for x in pllspec.split(':'):
|
|
21
|
+
k,v = x.split('=')
|
|
22
|
+
if k == 'period':
|
|
23
|
+
self.period_adj_pct = int(v)
|
|
24
|
+
elif k == 'phase':
|
|
25
|
+
self.phase_adj_pct = int(v)
|
|
26
|
+
elif k == 'lowpass':
|
|
27
|
+
self.lowpass_thresh = float(v)/1e6
|
|
28
|
+
else:
|
|
29
|
+
raise ValueError()
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
s = ("PLL: period_adj=%d%% phase_adj=%d%%"
|
|
32
|
+
% (self.period_adj_pct, self.phase_adj_pct))
|
|
33
|
+
if self.lowpass_thresh is not None:
|
|
34
|
+
s += (" lowpass_thresh=%.2f" % (self.lowpass_thresh*1e6))
|
|
35
|
+
return s
|
|
36
|
+
|
|
37
|
+
plls = [
|
|
38
|
+
# Default: An aggressive PLL which will quickly sync to extreme
|
|
39
|
+
# bit timings. For example: long tracks and variable-rate tracks.
|
|
40
|
+
PLL('period=5:phase=60'),
|
|
41
|
+
# Fallback: A conservative PLL which is good at ignoring noise in
|
|
42
|
+
# otherwise fairly well-behaved tracks. For example: high-frequency
|
|
43
|
+
# noise caused by dirt and mould on an old disk.
|
|
44
|
+
PLL('period=1:phase=10')
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Precompensation to apply to a MasterTrack for writeout.
|
|
48
|
+
class Precomp:
|
|
49
|
+
MFM = 0
|
|
50
|
+
FM = 1
|
|
51
|
+
GCR = 2
|
|
52
|
+
TYPESTRING = [ 'MFM', 'FM', 'GCR' ]
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
return "Precomp: %s, %dns" % (Precomp.TYPESTRING[self.type], self.ns)
|
|
55
|
+
def __init__(self, type: int, ns: float):
|
|
56
|
+
self.type = type
|
|
57
|
+
self.ns = ns
|
|
58
|
+
def apply(self, bits: bitarray, bit_ticks: List[float],
|
|
59
|
+
scale: float) -> None:
|
|
60
|
+
t = self.ns * scale
|
|
61
|
+
if self.type == Precomp.MFM:
|
|
62
|
+
for i in bits.search(bitarray('10100', endian='big')):
|
|
63
|
+
bit_ticks[i+2] -= t
|
|
64
|
+
bit_ticks[i+3] += t
|
|
65
|
+
for i in bits.search(bitarray('00101', endian='big')):
|
|
66
|
+
bit_ticks[i+2] += t
|
|
67
|
+
bit_ticks[i+3] -= t
|
|
68
|
+
# This is primarily for GCR and FM which permit adjacent 1s (and
|
|
69
|
+
# have correspondingly slower bit times). However it may be useful
|
|
70
|
+
# for illegal MFM sequences too, especially on Amiga (custom syncwords,
|
|
71
|
+
# 4us-bitcell tracks). Normal MFM should not trigger these patterns.
|
|
72
|
+
for i in bits.search(bitarray('110', endian='big')):
|
|
73
|
+
bit_ticks[i+1] -= t
|
|
74
|
+
bit_ticks[i+2] += t
|
|
75
|
+
for i in bits.search(bitarray('011', endian='big')):
|
|
76
|
+
bit_ticks[i+1] += t
|
|
77
|
+
bit_ticks[i+2] -= t
|
|
78
|
+
|
|
79
|
+
class HasVerify(Protocol):
|
|
80
|
+
verify_revs: float
|
|
81
|
+
def verify_track(self, flux: Flux) -> bool:
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
# A pristine representation of a track, from a codec and/or a perfect image.
|
|
85
|
+
class MasterTrack:
|
|
86
|
+
|
|
87
|
+
# Verify capability may be added "ad hoc" to a MasterTrack object
|
|
88
|
+
verify: Optional[HasVerify] = None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def bitrate(self) -> float:
|
|
92
|
+
return len(self.bits) / self.time_per_rev
|
|
93
|
+
|
|
94
|
+
def scale(self, factor: float):
|
|
95
|
+
"""Scale up index timing by specified factor."""
|
|
96
|
+
self.time_per_rev *= factor
|
|
97
|
+
|
|
98
|
+
# bits: Track bitcell data, aligned to the write splice (bitarray or bytes)
|
|
99
|
+
# time_per_rev: Time per revolution, in seconds (float)
|
|
100
|
+
# bit_ticks: Per-bitcell time values, in unitless 'ticks'
|
|
101
|
+
# splice: Location of the track splice, in bitcells, after the index
|
|
102
|
+
# weak: List of (start, length) weak ranges
|
|
103
|
+
# hardsector_bits: Optional list of hard-sector lengths, in bitcells
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
bits: Union[bitarray, bytes],
|
|
107
|
+
time_per_rev: float,
|
|
108
|
+
bit_ticks: Optional[List[float]] = None,
|
|
109
|
+
splice: int = 0,
|
|
110
|
+
weak: List[Tuple[int,int]] = [],
|
|
111
|
+
hardsector_bits: Optional[List[int]] = None
|
|
112
|
+
) -> None:
|
|
113
|
+
self.bits: bitarray
|
|
114
|
+
if isinstance(bits, bytes):
|
|
115
|
+
self.bits = bitarray(endian='big')
|
|
116
|
+
self.bits.frombytes(bits)
|
|
117
|
+
else:
|
|
118
|
+
self.bits = bits
|
|
119
|
+
self.time_per_rev = time_per_rev
|
|
120
|
+
self.bit_ticks = bit_ticks
|
|
121
|
+
self.splice = splice
|
|
122
|
+
self.weak = weak
|
|
123
|
+
self.precomp: Optional[Precomp] = None
|
|
124
|
+
self.force_random_weak = True
|
|
125
|
+
self.hardsector_bits = hardsector_bits
|
|
126
|
+
|
|
127
|
+
def __str__(self) -> str:
|
|
128
|
+
s = "\nMaster Track: splice @ %d\n" % self.splice
|
|
129
|
+
s += (" %d bits, %.1f kbit/s"
|
|
130
|
+
% (len(self.bits), self.bitrate/1000))
|
|
131
|
+
if self.bit_ticks:
|
|
132
|
+
s += " (variable)"
|
|
133
|
+
s += ("\n %.1f ms / rev (%.1f rpm)"
|
|
134
|
+
% (self.time_per_rev * 1000, 60 / self.time_per_rev))
|
|
135
|
+
if len(self.weak) > 0:
|
|
136
|
+
s += "\n %d weak range" % len(self.weak)
|
|
137
|
+
if len(self.weak) > 1: s += "s"
|
|
138
|
+
s += ": " + ", ".join(str(n) for _,n in self.weak) + " bits"
|
|
139
|
+
#s += str(binascii.hexlify(self.bits.tobytes()))
|
|
140
|
+
return s
|
|
141
|
+
|
|
142
|
+
def summary_string(self) -> str:
|
|
143
|
+
s = ('Bitcells (%d bits, %.1f kbit/s, %.1f rpm'
|
|
144
|
+
% (len(self.bits), self.bitrate/1000, 60 / self.time_per_rev))
|
|
145
|
+
if self.bit_ticks:
|
|
146
|
+
s += ', variable'
|
|
147
|
+
if len(self.weak) > 0:
|
|
148
|
+
s += ', weak'
|
|
149
|
+
s += ')'
|
|
150
|
+
return s
|
|
151
|
+
|
|
152
|
+
def reverse(self) -> None:
|
|
153
|
+
bitlen = len(self.bits)
|
|
154
|
+
if bitlen == 0: return
|
|
155
|
+
self.bits.reverse()
|
|
156
|
+
if self.bit_ticks is not None:
|
|
157
|
+
self.bit_ticks.reverse()
|
|
158
|
+
self.splice = -self.splice % bitlen
|
|
159
|
+
self.weak = list(map(lambda x: (-x[0] % bitlen, x[1]), self.weak))
|
|
160
|
+
if self.hardsector_bits is not None:
|
|
161
|
+
self.hardsector_bits.reverse()
|
|
162
|
+
|
|
163
|
+
def flux(self, revs: Optional[int] = None) -> Flux:
|
|
164
|
+
flux = self._flux(for_writeout=False, cue_at_index=True, revs=revs)
|
|
165
|
+
assert isinstance(flux, Flux)
|
|
166
|
+
return flux
|
|
167
|
+
|
|
168
|
+
def flux_for_writeout(self, cue_at_index) -> WriteoutFlux:
|
|
169
|
+
wflux = self._flux(for_writeout=True, cue_at_index=cue_at_index)
|
|
170
|
+
assert isinstance(wflux, WriteoutFlux)
|
|
171
|
+
return wflux
|
|
172
|
+
|
|
173
|
+
def _flux(self,
|
|
174
|
+
for_writeout: bool,
|
|
175
|
+
cue_at_index: bool,
|
|
176
|
+
revs: Optional[int] = None
|
|
177
|
+
) -> Union[Flux, WriteoutFlux]:
|
|
178
|
+
|
|
179
|
+
# We're going to mess with the track data, so take a copy.
|
|
180
|
+
bits = self.bits.copy()
|
|
181
|
+
bitlen = len(bits)
|
|
182
|
+
|
|
183
|
+
# Also copy the bit_ticks array (or create a dummy one), and remember
|
|
184
|
+
# the total ticks that it contains.
|
|
185
|
+
bit_ticks = self.bit_ticks.copy() if self.bit_ticks else [1] * bitlen
|
|
186
|
+
ticks_to_index = sum(bit_ticks)
|
|
187
|
+
|
|
188
|
+
# Weak regions need special processing for correct flux representation.
|
|
189
|
+
for s, n in self.weak:
|
|
190
|
+
if n < 2: continue # Too short to reliably weaken
|
|
191
|
+
e = s + n
|
|
192
|
+
assert 0 <= s < e <= bitlen
|
|
193
|
+
pattern = bitarray(endian="big")
|
|
194
|
+
if n < 400 or self.force_random_weak:
|
|
195
|
+
# Short weak regions are written with no flux transitions.
|
|
196
|
+
# Actually we insert a flux transition every 32 bitcells, else
|
|
197
|
+
# we risk triggering Greaseweazle's No Flux Area generator.
|
|
198
|
+
pattern.frombytes(b"\x80\x00\x00\x00")
|
|
199
|
+
bits[s:e] = (pattern * (n//32+1))[:n]
|
|
200
|
+
else:
|
|
201
|
+
# Long weak regions we present a fuzzy clock bit in an
|
|
202
|
+
# otherwise normal byte (16 bits MFM). The byte may be
|
|
203
|
+
# interpreted as
|
|
204
|
+
# MFM 0001001010100101 = 12A5 = byte 0x43, or
|
|
205
|
+
# MFM 0001001010010101 = 1295 = byte 0x47
|
|
206
|
+
pattern.frombytes(b"\x12\xA5")
|
|
207
|
+
bits[s:e] = (pattern * (n//16+1))[:n]
|
|
208
|
+
for i in range(0, n-10, 16):
|
|
209
|
+
x, y = bit_ticks[s+i+10], bit_ticks[s+i+11]
|
|
210
|
+
bit_ticks[s+i+10], bit_ticks[s+i+11] = x+y*0.5, y*0.5
|
|
211
|
+
# To prevent corrupting a preceding sync word by effectively
|
|
212
|
+
# starting the weak region early, we start with a 1 if we just
|
|
213
|
+
# clocked out a 0.
|
|
214
|
+
bits[s] = not bits[s-1]
|
|
215
|
+
# Similarly modify the last bit of the weak region.
|
|
216
|
+
bits[e-1] = not(bits[e-2] or bits[e % bitlen])
|
|
217
|
+
|
|
218
|
+
if cue_at_index:
|
|
219
|
+
# Rotate data to start at the index.
|
|
220
|
+
index = -self.splice % bitlen
|
|
221
|
+
if index != 0:
|
|
222
|
+
bits = bits[index:] + bits[:index]
|
|
223
|
+
bit_ticks = bit_ticks[index:] + bit_ticks[:index]
|
|
224
|
+
splice_at_index = index < 4 or bitlen - index < 4
|
|
225
|
+
else:
|
|
226
|
+
assert for_writeout
|
|
227
|
+
splice_at_index = False
|
|
228
|
+
|
|
229
|
+
if not for_writeout:
|
|
230
|
+
# Do not extend the track for reliable writeout to disk.
|
|
231
|
+
pass
|
|
232
|
+
elif not cue_at_index:
|
|
233
|
+
# We write the track wherever it may fall (uncued).
|
|
234
|
+
# We stretch the track with extra header gap bytes, in case the
|
|
235
|
+
# drive spins slow and we need more length to create an overlap.
|
|
236
|
+
# Thus if the drive spins slow, the track gets a longer header.
|
|
237
|
+
pos = 4
|
|
238
|
+
# We stretch by 10 percent, which is way more than enough.
|
|
239
|
+
rep = bitlen // (10 * 32)
|
|
240
|
+
bit_ticks = bit_ticks[pos:pos+32] * rep + bit_ticks[pos:]
|
|
241
|
+
bits = bits[pos:pos+32] * rep + bits[pos:]
|
|
242
|
+
elif splice_at_index:
|
|
243
|
+
# Splice is at the index (or within a few bitcells of it).
|
|
244
|
+
# We stretch the track with extra footer gap bytes, in case the
|
|
245
|
+
# drive motor spins slower than expected and we need more filler
|
|
246
|
+
# to get us to the index pulse (where the write will terminate).
|
|
247
|
+
# Thus if the drive spins slow, the track gets a longer footer.
|
|
248
|
+
pos = (self.splice - 4) % bitlen
|
|
249
|
+
# We stretch by 10 percent, which is way more than enough.
|
|
250
|
+
rep = bitlen // (10 * 32)
|
|
251
|
+
bit_ticks = bit_ticks[:pos] + bit_ticks[pos-32:pos] * rep
|
|
252
|
+
bits = bits[:pos] + bits[pos-32:pos] * rep
|
|
253
|
+
else:
|
|
254
|
+
# Splice is not at the index. We will write more than one
|
|
255
|
+
# revolution, and terminate the second revolution at the splice.
|
|
256
|
+
# For the first revolution we repeat the track header *backwards*
|
|
257
|
+
# to the very start of the write. This is in case the drive motor
|
|
258
|
+
# spins slower than expected and the write ends before the original
|
|
259
|
+
# splice position.
|
|
260
|
+
# Thus if the drive spins slow, the track gets a longer header.
|
|
261
|
+
bit_ticks += bit_ticks[:self.splice-4]
|
|
262
|
+
bits += bits[:self.splice-4]
|
|
263
|
+
pos = self.splice+4
|
|
264
|
+
fill_pattern = bits[pos:pos+32]
|
|
265
|
+
while pos >= 32:
|
|
266
|
+
pos -= 32
|
|
267
|
+
bits[pos:pos+32] = fill_pattern
|
|
268
|
+
|
|
269
|
+
if for_writeout and self.precomp is not None:
|
|
270
|
+
self.precomp.apply(bits, bit_ticks,
|
|
271
|
+
ticks_to_index / (self.time_per_rev*1e9))
|
|
272
|
+
|
|
273
|
+
# Convert the stretched track data into flux.
|
|
274
|
+
bit_ticks_i = iter(bit_ticks)
|
|
275
|
+
flux_list = []
|
|
276
|
+
flux_ticks: float = 0
|
|
277
|
+
for bit in bits:
|
|
278
|
+
flux_ticks += next(bit_ticks_i)
|
|
279
|
+
if bit:
|
|
280
|
+
flux_list.append(flux_ticks)
|
|
281
|
+
flux_ticks = 0
|
|
282
|
+
|
|
283
|
+
# Package up WriteoutFlux.
|
|
284
|
+
if for_writeout:
|
|
285
|
+
if flux_ticks:
|
|
286
|
+
flux_list.append(flux_ticks)
|
|
287
|
+
return WriteoutFlux(
|
|
288
|
+
ticks_to_index, flux_list,
|
|
289
|
+
sample_freq = ticks_to_index / self.time_per_rev,
|
|
290
|
+
index_cued = cue_at_index,
|
|
291
|
+
terminate_at_index = splice_at_index)
|
|
292
|
+
|
|
293
|
+
# Package up Flux.
|
|
294
|
+
index_list = [ticks_to_index]
|
|
295
|
+
if revs is None:
|
|
296
|
+
# Emit two revolutions if track data crosses the index.
|
|
297
|
+
revs = 1 if splice_at_index else 2
|
|
298
|
+
assert revs is not None and revs > 0
|
|
299
|
+
if revs > 1:
|
|
300
|
+
l = flux_list
|
|
301
|
+
for i in range(revs-1):
|
|
302
|
+
flux_list = l + [flux_ticks+flux_list[0]] + flux_list[1:]
|
|
303
|
+
index_list *= revs
|
|
304
|
+
flux = Flux(index_list, flux_list,
|
|
305
|
+
sample_freq = ticks_to_index / self.time_per_rev,
|
|
306
|
+
index_cued = True)
|
|
307
|
+
flux.splice = sum(bit_ticks[:self.splice])
|
|
308
|
+
return flux
|
|
309
|
+
|
|
310
|
+
class PLLRevolution:
|
|
311
|
+
def __init__(self, nr_bits: int,
|
|
312
|
+
hardsector_bits: Optional[List[int]] = None) -> None:
|
|
313
|
+
self.nr_bits = nr_bits
|
|
314
|
+
self.hardsector_bits = hardsector_bits
|
|
315
|
+
|
|
316
|
+
# Track data generated from flux.
|
|
317
|
+
class PLLTrack:
|
|
318
|
+
|
|
319
|
+
# clock: Expected time per raw bitcell, in seconds (float)
|
|
320
|
+
# data: Flux object, or a form convertible to a Flux object
|
|
321
|
+
# time_per_rev: Expected time per revolution, in seconds (optional, float)
|
|
322
|
+
# lowpass_thresh: Merge short fluxes with adjacent fluxes (optional, float)
|
|
323
|
+
def __init__(self, clock: float, data, time_per_rev=None, pll=None,
|
|
324
|
+
lowpass_thresh=None):
|
|
325
|
+
self.clock = clock
|
|
326
|
+
self.time_per_rev = time_per_rev
|
|
327
|
+
self.clock_max_adj = 0.10
|
|
328
|
+
if pll is None: pll = plls[0]
|
|
329
|
+
self.pll_period_adj = pll.period_adj_pct / 100
|
|
330
|
+
self.pll_phase_adj = pll.phase_adj_pct / 100
|
|
331
|
+
self.lowpass_thresh = (lowpass_thresh if pll.lowpass_thresh is None
|
|
332
|
+
else pll.lowpass_thresh)
|
|
333
|
+
self.bitarray = bitarray(endian='big')
|
|
334
|
+
self.timearray: List[float] = []
|
|
335
|
+
self.revolutions: List[PLLRevolution] = []
|
|
336
|
+
self.import_flux_data(data)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def __str__(self) -> str:
|
|
340
|
+
s = "\nRaw Track: %d revolutions\n" % len(self.revolutions)
|
|
341
|
+
for rev in range(len(self.revolutions)):
|
|
342
|
+
b, _ = self.get_revolution(rev)
|
|
343
|
+
s += "Revolution %u (%u bits): " % (rev, len(b))
|
|
344
|
+
s += str(binascii.hexlify(b.tobytes())) + "\n"
|
|
345
|
+
b = self.bitarray[sum([x.nr_bits for x in self.revolutions]):]
|
|
346
|
+
s += "Tail (%u bits): " % (len(b))
|
|
347
|
+
s += str(binascii.hexlify(b.tobytes())) + "\n"
|
|
348
|
+
return s[:-1]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def get_revolution(self, nr) -> Tuple[bitarray, List[float]]:
|
|
352
|
+
start = sum([x.nr_bits for x in self.revolutions[:nr]])
|
|
353
|
+
end = start + self.revolutions[nr].nr_bits
|
|
354
|
+
return self.bitarray[start:end], self.timearray[start:end]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def get_all_data(self) -> Tuple[bitarray, List[float]]:
|
|
358
|
+
return self.bitarray, self.timearray
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def import_flux_data(self, data) -> None:
|
|
362
|
+
|
|
363
|
+
flux = data.flux()
|
|
364
|
+
freq = flux.sample_freq
|
|
365
|
+
if self.time_per_rev is not None:
|
|
366
|
+
# Adjust the raw flux to have the expected time per revolution.
|
|
367
|
+
freq *= flux.time_per_rev / self.time_per_rev
|
|
368
|
+
|
|
369
|
+
clock = self.clock
|
|
370
|
+
clock_min = self.clock * (1 - self.clock_max_adj)
|
|
371
|
+
clock_max = self.clock * (1 + self.clock_max_adj)
|
|
372
|
+
|
|
373
|
+
index_iter = it.chain(iter(map(lambda x: x/freq, flux.index_list)),
|
|
374
|
+
[float('inf')])
|
|
375
|
+
|
|
376
|
+
if self.lowpass_thresh is not None:
|
|
377
|
+
# Short fluxes below the threshold are merged together, and with
|
|
378
|
+
# adjacent fluxes. The scenario discussed in issue #325 is that
|
|
379
|
+
# a long flux (>=10us) gets a single short flux-reversal pair
|
|
380
|
+
# inserted in the middle by drive electronics. So all we really
|
|
381
|
+
# need to filter in practice is:
|
|
382
|
+
# (Long),(Short),(Long) -> (Long+Short+Long)
|
|
383
|
+
# Sequences of short pulses have not been seen in real disk dumps.
|
|
384
|
+
flux_list, thresh = [0.0], self.lowpass_thresh
|
|
385
|
+
flux_iter = iter(flux.list)
|
|
386
|
+
while (x := next(flux_iter, None)) is not None:
|
|
387
|
+
if x/freq <= thresh:
|
|
388
|
+
y = next(flux_iter, 0.0)
|
|
389
|
+
# Merge a group-of-three centred on shortest of {x, y}.
|
|
390
|
+
if y <= x:
|
|
391
|
+
flux_list.append(x + y + next(flux_iter, 0.0))
|
|
392
|
+
else:
|
|
393
|
+
flux_list[-1] += x + y
|
|
394
|
+
else:
|
|
395
|
+
flux_list.append(x)
|
|
396
|
+
else:
|
|
397
|
+
flux_list = flux.list
|
|
398
|
+
|
|
399
|
+
# Make sure there's enough time in the flux list to cover all
|
|
400
|
+
# revolutions by appending a "large enough" final flux value.
|
|
401
|
+
tail = max(0, sum(flux.index_list) - sum(flux_list) + clock*freq*2)
|
|
402
|
+
flux_iter = it.chain(flux_list, [tail])
|
|
403
|
+
|
|
404
|
+
revolutions: List[int] = []
|
|
405
|
+
try:
|
|
406
|
+
optimised.flux_to_bitcells(
|
|
407
|
+
self.bitarray, self.timearray, revolutions,
|
|
408
|
+
index_iter, flux_iter,
|
|
409
|
+
freq, clock, clock_min, clock_max,
|
|
410
|
+
self.pll_period_adj, self.pll_phase_adj)
|
|
411
|
+
except AttributeError:
|
|
412
|
+
flux_to_bitcells(
|
|
413
|
+
self.bitarray, self.timearray, revolutions,
|
|
414
|
+
index_iter, flux_iter,
|
|
415
|
+
freq, clock, clock_min, clock_max,
|
|
416
|
+
self.pll_period_adj, self.pll_phase_adj)
|
|
417
|
+
|
|
418
|
+
hardsector_bits = None
|
|
419
|
+
for i, nr_bits in enumerate(revolutions):
|
|
420
|
+
if flux.sector_list is not None:
|
|
421
|
+
start = sum(revolutions[:i])
|
|
422
|
+
cell_sum = it.accumulate(self.timearray[start:start+nr_bits])
|
|
423
|
+
hardsector_bits = []
|
|
424
|
+
for sector_end in it.accumulate(map(lambda x: x/freq,
|
|
425
|
+
flux.sector_list[i])):
|
|
426
|
+
nbits = 0
|
|
427
|
+
try:
|
|
428
|
+
while next(cell_sum) < sector_end:
|
|
429
|
+
nbits += 1
|
|
430
|
+
nbits += 1
|
|
431
|
+
except StopIteration:
|
|
432
|
+
pass
|
|
433
|
+
hardsector_bits.append(nbits)
|
|
434
|
+
self.revolutions.append(PLLRevolution(nr_bits, hardsector_bits))
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def flux_to_bitcells(bit_array, time_array, revolutions,
|
|
438
|
+
index_iter, flux_iter,
|
|
439
|
+
freq, clock_centre, clock_min, clock_max,
|
|
440
|
+
pll_period_adj, pll_phase_adj) -> None:
|
|
441
|
+
|
|
442
|
+
nbits = 0
|
|
443
|
+
ticks = 0.0
|
|
444
|
+
clock = clock_centre
|
|
445
|
+
to_index = next(index_iter)
|
|
446
|
+
|
|
447
|
+
for x in flux_iter:
|
|
448
|
+
|
|
449
|
+
# Gather enough ticks to generate at least one bitcell.
|
|
450
|
+
ticks += x / freq
|
|
451
|
+
if ticks < clock/2:
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
# Clock out zero or more 0s, followed by a 1.
|
|
455
|
+
zeros = 0
|
|
456
|
+
while True:
|
|
457
|
+
ticks -= clock
|
|
458
|
+
if ticks < clock/2:
|
|
459
|
+
break
|
|
460
|
+
zeros += 1
|
|
461
|
+
bit_array.append(False)
|
|
462
|
+
bit_array.append(True)
|
|
463
|
+
|
|
464
|
+
# PLL: Adjust clock window position according to phase mismatch.
|
|
465
|
+
new_ticks = ticks * (1 - pll_phase_adj)
|
|
466
|
+
|
|
467
|
+
# Distribute the clock adjustment across all bits we just emitted.
|
|
468
|
+
_clock = clock + (ticks - new_ticks) / (zeros + 1)
|
|
469
|
+
for i in range(zeros + 1):
|
|
470
|
+
# Check if we cross the index mark.
|
|
471
|
+
to_index -= _clock
|
|
472
|
+
if to_index < 0:
|
|
473
|
+
revolutions.append(nbits)
|
|
474
|
+
nbits = 0
|
|
475
|
+
to_index += next(index_iter)
|
|
476
|
+
# Emit bit time.
|
|
477
|
+
nbits += 1
|
|
478
|
+
time_array.append(_clock)
|
|
479
|
+
|
|
480
|
+
# PLL: Adjust clock frequency according to phase mismatch.
|
|
481
|
+
if zeros <= 3:
|
|
482
|
+
# In sync: adjust clock by a fraction of the phase mismatch.
|
|
483
|
+
clock += ticks * pll_period_adj
|
|
484
|
+
else:
|
|
485
|
+
# Out of sync: adjust clock towards centre.
|
|
486
|
+
clock += (clock_centre - clock) * pll_period_adj
|
|
487
|
+
# Clamp the clock's adjustment range.
|
|
488
|
+
clock = min(max(clock, clock_min), clock_max)
|
|
489
|
+
|
|
490
|
+
ticks = new_ticks
|
|
491
|
+
|
|
492
|
+
# Local variables:
|
|
493
|
+
# python-indent: 4
|
|
494
|
+
# End:
|