From 5398d5c97d3839fcec56aafcef1041207ddfeaf2 Mon Sep 17 00:00:00 2001 From: Keir Fraser Date: Thu, 23 May 2024 14:12:07 +0100 Subject: [PATCH] apple2: first cut --- setup.py | 1 + src/greaseweazle/codec/apple2/__init__.py | 0 src/greaseweazle/codec/apple2/apple2_gcr.py | 231 ++++++++++++++++++++ src/greaseweazle/codec/codec.py | 3 + src/greaseweazle/data/diskdefs.cfg | 18 ++ src/greaseweazle/image/apple2.py | 18 ++ src/greaseweazle/image/hfe.py | 11 +- src/greaseweazle/optimised/apple2.c | 95 ++++++++ src/greaseweazle/optimised/apple2.h | 17 ++ src/greaseweazle/optimised/apple2_gcr.h | 64 ++++++ src/greaseweazle/optimised/optimised.c | 54 +++++ src/greaseweazle/optimised/optimised.pyi | 6 + src/greaseweazle/tools/util.py | 2 + 13 files changed, 516 insertions(+), 4 deletions(-) create mode 100644 src/greaseweazle/codec/apple2/__init__.py create mode 100644 src/greaseweazle/codec/apple2/apple2_gcr.py create mode 100644 src/greaseweazle/image/apple2.py create mode 100644 src/greaseweazle/optimised/apple2.c create mode 100644 src/greaseweazle/optimised/apple2.h create mode 100644 src/greaseweazle/optimised/apple2_gcr.h diff --git a/setup.py b/setup.py index 2e368bef..3395fd67 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ def version(): ext_modules = [ Extension('greaseweazle.optimised.optimised', sources = ['src/greaseweazle/optimised/optimised.c', + 'src/greaseweazle/optimised/apple2.c', 'src/greaseweazle/optimised/c64.c', 'src/greaseweazle/optimised/mac.c', 'src/greaseweazle/optimised/td0_lzss.c'], diff --git a/src/greaseweazle/codec/apple2/__init__.py b/src/greaseweazle/codec/apple2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/greaseweazle/codec/apple2/apple2_gcr.py b/src/greaseweazle/codec/apple2/apple2_gcr.py new file mode 100644 index 00000000..3103a0c4 --- /dev/null +++ b/src/greaseweazle/codec/apple2/apple2_gcr.py @@ -0,0 +1,231 @@ +# greaseweazle/codec/apple2/apple2_gcr.py +# +# Written & released by Keir Fraser +# +# This is free and unencumbered software released into the public domain. +# See the file COPYING for more details, or visit . + +from typing import List, Optional, Tuple + +import struct +from bitarray import bitarray + +from greaseweazle import error +from greaseweazle import optimised +from greaseweazle.codec import codec +from greaseweazle.track import MasterTrack, PLL, PLLTrack +from greaseweazle.flux import Flux, HasFlux + +default_revs = 1.1 + +ff40_presync_bytes = b'\xff\x3f\xcf\xf3\xfc\xff\x3f\xcf\xf3\xfc' +trailer_bytes = b'\xde\xaa\xeb' +sector_sync_bytes = b'\xd5\xaa\x96' +data_sync_bytes = b'\xd5\xaa\xad' + +sector_sync = bitarray(endian='big') +sector_sync.frombytes(sector_sync_bytes) + +data_sync = bitarray(endian='big') +data_sync.frombytes(data_sync_bytes) + +bad_sector = b'-=[BAD SECTOR]=-' * 16 + +class Apple2GCR(codec.Codec): + + time_per_rev = 0.2 + + verify_revs = default_revs + + def __init__(self, cyl: int, head: int, config): + self.cyl, self.head = cyl, head + self.config = config + self.clock = config.clock + self.sector: List[Optional[bytes]] + self.sector = [None] * self.nsec + self.vol_id: Optional[int] = None + error.check(optimised.enabled, + 'Apple2 GCR requires optimised C extension') + + @property + def nsec(self) -> int: + return len(self.config.secs) + + def summary_string(self) -> str: + nsec, nbad = self.nsec, self.nr_missing() + s = "Apple2 GCR (%d/%d sectors)" % (nsec - nbad, nsec) + return s + + def set_vol_id(self, vol_id: int): + assert self.vol_id is None + self.vol_id = vol_id + + # private + def add(self, sec_id, data) -> None: + assert not self.has_sec(sec_id) + self.sector[sec_id] = data + + # private + def tracknr(self) -> int: + return self.cyl + + def has_sec(self, sec_id: int) -> bool: + return self.sector[sec_id] is not None + + def nr_missing(self) -> int: + return len([sec for sec in self.sector if sec is None]) + + def get_img_track(self) -> bytearray: + tdat = bytearray() + for i in self.config.secs: + sec = self.sector[i] + tdat += sec if sec is not None else bad_sector + return tdat + + def set_img_track(self, tdat: bytes) -> int: + totsize = self.nsec * 256 + if len(tdat) < totsize: + tdat += bytes(totsize - len(tdat)) + for i,sec in enumerate(self.config.secs): + self.sector[sec] = tdat[i*256:(i+1)*256] + return totsize + + def decode_flux(self, track: HasFlux, pll: Optional[PLL]=None) -> None: + raw = PLLTrack(time_per_rev = self.time_per_rev, + clock = self.clock, data = track, pll = pll, + lowpass_thresh = 2.5e-6) + bits, _ = raw.get_all_data() + + for offs in bits.itersearch(sector_sync): + + if self.nr_missing() == 0: + break + + # Decode header + offs += 3*8 + sec = bits[offs:offs+8*8].tobytes() + if len(sec) != 8: + continue + hdr = map(lambda x: (x & x>>7) & 255, + list(struct.unpack('>4H', sec))) + vol_id, trk_id, sec_id, csum = tuple(hdr) + + # Validate header + if csum != vol_id ^ trk_id ^ sec_id: + continue + if (trk_id != self.tracknr() or sec_id >= self.nsec): + print('T%d.%d: Ignoring unexpected sector C:%d S:%d ID:%04x' + % (self.cyl, self.head, trk_id, sec_id, vol_id)) + continue + if self.vol_id is None: + self.vol_id = vol_id + elif self.vol_id != vol_id: + print('T%d.%d: Expected ID %04x in sector C:%d S:%d ID:%04x' + % (self.cyl, self.head, self.vol_id, + trk_id, sec_id, vol_id)) + continue + if self.has_sec(sec_id): + continue + + # Find data + offs += 8*8 + dat_offs = bits[offs:offs+100*8].search(data_sync) + if len(dat_offs) != 1: + continue + offs += dat_offs[0] + + # Decode data + offs += 3*8 + sec = bits[offs:offs+344*8].tobytes() + if len(sec) != 344: + continue + sec, csum = optimised.decode_apple2_sector(sec) + if csum != 0: + continue + + self.add(sec_id, sec) + + + def master_track(self) -> MasterTrack: + + def gcr44(x): + x = x | x << 7 | 0xaaaa + return bytes([x>>8, x&255]) + + vol_id = self.vol_id if self.vol_id is not None else 254 + trk_id = self.tracknr() + + # Post-index track gap. + t = ff40_presync_bytes * 3 + + for sec_id in range(self.nsec): + sector = self.sector[sec_id] + data = bad_sector if sector is None else sector + # Header + t += ff40_presync_bytes*2 + b'\xff' + sector_sync_bytes + t += gcr44(vol_id) + t += gcr44(trk_id) + t += gcr44(sec_id) + t += gcr44(vol_id ^ trk_id ^ sec_id) + t += trailer_bytes + t += ff40_presync_bytes + data_sync_bytes + t += optimised.encode_apple2_sector(data) + t += trailer_bytes + + # Add the pre-index gap. + tlen = int((self.time_per_rev / self.clock)) & ~31 + t += bytes([0x55] * (tlen//8-len(t))) + + track = MasterTrack(bits = t, time_per_rev = 0.2) + track.verify = self + return track + + + def verify_track(self, flux): + readback_track = self.__class__(self.cyl, self.head, self.config) + readback_track.decode_flux(flux) + return (readback_track.nr_missing() == 0 + and self.sector == readback_track.sector) + + +class Apple2GCRDef(codec.TrackDef): + + default_revs = default_revs + + def __init__(self, format_name: str): + self.secs: List[int] = [] + self.clock: Optional[float] = None + self.finalised = False + + def add_param(self, key: str, val) -> None: + if key == 'secs': + self.secs = [] + for x in val.split(','): + self.secs.append(int(x)) + rsecs = [-1] * len(self.secs) + for i,x in enumerate(self.secs): + error.check(x < len(rsecs), f'sector {x} is out of range') + error.check(rsecs[x] == -1, f'sector {x} is repeated') + rsecs[x] = i + elif key == 'clock': + val = float(val) + self.clock = val * 1e-6 + else: + raise error.Fatal('unrecognised track option %s' % key) + + def finalise(self) -> None: + if self.finalised: + return + error.check(self.secs, + 'sector list not specified') + error.check(self.clock is not None, + 'clock period not specified') + self.finalised = True + + def mk_track(self, cyl: int, head: int) -> Apple2GCR: + return Apple2GCR(cyl, head, self) + + +# Local variables: +# python-indent: 4 +# End: diff --git a/src/greaseweazle/codec/codec.py b/src/greaseweazle/codec/codec.py index d8356046..517f327f 100644 --- a/src/greaseweazle/codec/codec.py +++ b/src/greaseweazle/codec/codec.py @@ -153,6 +153,7 @@ def read_diskdef_file_lines(filename: Optional[str]) -> Tuple[List[str], str]: from greaseweazle.codec.amiga import amigados from greaseweazle.codec.macintosh import mac_gcr from greaseweazle.codec.commodore import c64_gcr +from greaseweazle.codec.apple2 import apple2_gcr def mk_trackdef(format_name: str) -> TrackDef: if format_name in ['amiga.amigados']: @@ -165,6 +166,8 @@ def mk_trackdef(format_name: str) -> TrackDef: return mac_gcr.MacGCRDef(format_name) if format_name in ['c64.gcr']: return c64_gcr.C64GCRDef(format_name) + if format_name in ['apple2.gcr']: + return apple2_gcr.Apple2GCRDef(format_name) if format_name in ['bitcell']: return bitcell.BitcellTrackDef(format_name) raise error.Fatal('unrecognised format name: %s' % format_name) diff --git a/src/greaseweazle/data/diskdefs.cfg b/src/greaseweazle/data/diskdefs.cfg index c5a4c570..ded78240 100644 --- a/src/greaseweazle/data/diskdefs.cfg +++ b/src/greaseweazle/data/diskdefs.cfg @@ -171,6 +171,24 @@ disk akai.1600 end end +disk apple2.appledos.140 + cyls = 35 + heads = 1 + tracks * apple2.gcr + clock = 3.92 + secs = 0,13,11,9,7,5,3,1,14,12,10,8,6,4,2,15 + end +end + +disk apple2.prodos.140 + cyls = 35 + heads = 1 + tracks * apple2.gcr + clock = 3.92 + secs = 0,2,4,6,8,10,12,14,1,3,5,7,9,11,13,15 + end +end + disk atari.90 cyls = 40 heads = 1 diff --git a/src/greaseweazle/image/apple2.py b/src/greaseweazle/image/apple2.py new file mode 100644 index 00000000..fbc97554 --- /dev/null +++ b/src/greaseweazle/image/apple2.py @@ -0,0 +1,18 @@ +# greaseweazle/image/apple2.py +# +# Written & released by Keir Fraser +# +# This is free and unencumbered software released into the public domain. +# See the file COPYING for more details, or visit . + +from greaseweazle.image.img import IMG + +class DO(IMG): + default_format = 'apple2.appledos.140' + +class PO(IMG): + default_format = 'apple2.prodos.140' + +# Local variables: +# python-indent: 4 +# End: diff --git a/src/greaseweazle/image/hfe.py b/src/greaseweazle/image/hfe.py index 32a2e302..ab06e447 100644 --- a/src/greaseweazle/image/hfe.py +++ b/src/greaseweazle/image/hfe.py @@ -15,6 +15,7 @@ from greaseweazle.tools import util from greaseweazle.codec import codec from greaseweazle.codec.ibm import ibm +from greaseweazle.codec.apple2 import apple2_gcr from greaseweazle.track import MasterTrack, PLLTrack from bitarray import bitarray from .image import Image, ImageOpts @@ -212,15 +213,17 @@ def get_track(self, cyl: int, side: int) -> Optional[MasterTrack]: def emit_track(self, cyl: int, side: int, track) -> None: - # HFE convention is that FM is recorded at double density - is_fm = isinstance(track, ibm.IBMTrack) and track.mode is ibm.Mode.FM + # HFE convention is that FM and GCR are recorded at double rate + double_rate = ( + (isinstance(track, ibm.IBMTrack) and track.mode is ibm.Mode.FM) + or isinstance(track, apple2_gcr.Apple2GCR)) t = track.master_track() if isinstance(track, codec.Codec) else track if self.opts.bitrate is None: error.check(hasattr(t, 'bitrate'), 'HFE: Requires bitrate to be specified' ' (eg. filename.hfe::bitrate=500)') self.opts.bitrate = round(t.bitrate / 2e3) - if is_fm: + if double_rate: self.opts.bitrate *= 2 print('HFE: Data bitrate detected: %d kbit/s' % self.opts.bitrate) if isinstance(t, MasterTrack): @@ -241,7 +244,7 @@ def emit_track(self, cyl: int, side: int, track) -> None: weak.append((s % len(t.bits), n)) else: weak.append((s, n)) - if is_fm: # FM data is recorded to HFE at double rate + if double_rate: double_bytes = ibm.doubler(bits.tobytes()) double_bits = bitarray(endian='big') double_bits.frombytes(double_bytes) diff --git a/src/greaseweazle/optimised/apple2.c b/src/greaseweazle/optimised/apple2.c new file mode 100644 index 00000000..8b0c00ac --- /dev/null +++ b/src/greaseweazle/optimised/apple2.c @@ -0,0 +1,95 @@ +/* + * Incorporates code from FluxEngine by David Given. + * + * In turn this is extremely inspired by the MESS implementation, written by + * Nathan Woods and R. Belmont: + * https://github.com/mamedev/mame/blob/7914a6083a3b3a8c243ae6c3b8cb50b023f21e0e/src/lib/formats/ap2_dsk.cpp + */ + +#include "apple2.h" + +static int decode_data_gcr(uint8_t x) +{ + switch (x) + { +#define GCR_ENTRY(gcr, data) case gcr: return data; +#include "apple2_gcr.h" +#undef GCR_ENTRY + } + return -1; +} + +static int encode_data_gcr(uint8_t x) +{ + switch (x) { +#define GCR_ENTRY(gcr, data) case data: return gcr; +#include "apple2_gcr.h" +#undef GCR_ENTRY + } + return -1; +} + +int decode_apple2_sector(const uint8_t *input, uint8_t *output) +{ + unsigned int i; + uint8_t checksum = 0; + + for (i = 0; i < APPLE2_ENCODED_SECTOR_LENGTH; i++) { + checksum ^= decode_data_gcr(*input++); + + if (i >= 86) { + /* 6 bit */ + output[i - 86] |= (checksum << 2); + } else { + /* 3 * 2 bit */ + output[i + 0] = ((checksum >> 1) & 0x01) | ((checksum << 1) & 0x02); + output[i + 86] = + ((checksum >> 3) & 0x01) | ((checksum >> 1) & 0x02); + if ((i + 172) < APPLE2_SECTOR_LENGTH) + output[i + 172] = + ((checksum >> 5) & 0x01) | ((checksum >> 3) & 0x02); + } + } + + checksum &= 0x3f; + return (checksum != decode_data_gcr(*input)); +} + +void encode_apple2_sector(const uint8_t *input, uint8_t *output) +{ +#define TWOBIT_COUNT 0x56 /* 'twobit' area at the start of the GCR data */ + uint8_t tmp, checksum = 0; + int i, value; + + for (i = 0; i < APPLE2_ENCODED_SECTOR_LENGTH; i++) { + if (i >= TWOBIT_COUNT) { + value = input[i - TWOBIT_COUNT] >> 2; + } else { + tmp = input[i]; + value = ((tmp & 1) << 1) | ((tmp & 2) >> 1); + + tmp = input[i + TWOBIT_COUNT]; + value |= ((tmp & 1) << 3) | ((tmp & 2) << 1); + + if (i + 2 * TWOBIT_COUNT < APPLE2_SECTOR_LENGTH) { + tmp = input[i + 2 * TWOBIT_COUNT]; + value |= ((tmp & 1) << 5) | ((tmp & 2) << 3); + } + } + checksum ^= value; + *output++ = encode_data_gcr(checksum); + checksum = value; + } + *output++ = encode_data_gcr(checksum); +#undef TWOBIT_COUNT +} + +/* + * Local variables: + * mode: C + * c-file-style: "Linux" + * c-basic-offset: 4 + * tab-width: 4 + * indent-tabs-mode: nil + * End: + */ diff --git a/src/greaseweazle/optimised/apple2.h b/src/greaseweazle/optimised/apple2.h new file mode 100644 index 00000000..2cc39038 --- /dev/null +++ b/src/greaseweazle/optimised/apple2.h @@ -0,0 +1,17 @@ +#include + +#define APPLE2_SECTOR_LENGTH 256 +#define APPLE2_ENCODED_SECTOR_LENGTH 342 + +int decode_apple2_sector(const uint8_t *input, uint8_t *output); +void encode_apple2_sector(const uint8_t *input, uint8_t *output); + +/* + * Local variables: + * mode: C + * c-file-style: "Linux" + * c-basic-offset: 4 + * tab-width: 4 + * indent-tabs-mode: nil + * End: + */ diff --git a/src/greaseweazle/optimised/apple2_gcr.h b/src/greaseweazle/optimised/apple2_gcr.h new file mode 100644 index 00000000..b3093eb2 --- /dev/null +++ b/src/greaseweazle/optimised/apple2_gcr.h @@ -0,0 +1,64 @@ +GCR_ENTRY(0x96, 0x00) +GCR_ENTRY(0x97, 0x01) +GCR_ENTRY(0x9a, 0x02) +GCR_ENTRY(0x9b, 0x03) +GCR_ENTRY(0x9d, 0x04) +GCR_ENTRY(0x9e, 0x05) +GCR_ENTRY(0x9f, 0x06) +GCR_ENTRY(0xa6, 0x07) +GCR_ENTRY(0xa7, 0x08) +GCR_ENTRY(0xab, 0x09) +GCR_ENTRY(0xac, 0x0a) +GCR_ENTRY(0xad, 0x0b) +GCR_ENTRY(0xae, 0x0c) +GCR_ENTRY(0xaf, 0x0d) +GCR_ENTRY(0xb2, 0x0e) +GCR_ENTRY(0xb3, 0x0f) +GCR_ENTRY(0xb4, 0x10) +GCR_ENTRY(0xb5, 0x11) +GCR_ENTRY(0xb6, 0x12) +GCR_ENTRY(0xb7, 0x13) +GCR_ENTRY(0xb9, 0x14) +GCR_ENTRY(0xba, 0x15) +GCR_ENTRY(0xbb, 0x16) +GCR_ENTRY(0xbc, 0x17) +GCR_ENTRY(0xbd, 0x18) +GCR_ENTRY(0xbe, 0x19) +GCR_ENTRY(0xbf, 0x1a) +GCR_ENTRY(0xcb, 0x1b) +GCR_ENTRY(0xcd, 0x1c) +GCR_ENTRY(0xce, 0x1d) +GCR_ENTRY(0xcf, 0x1e) +GCR_ENTRY(0xd3, 0x1f) +GCR_ENTRY(0xd6, 0x20) +GCR_ENTRY(0xd7, 0x21) +GCR_ENTRY(0xd9, 0x22) +GCR_ENTRY(0xda, 0x23) +GCR_ENTRY(0xdb, 0x24) +GCR_ENTRY(0xdc, 0x25) +GCR_ENTRY(0xdd, 0x26) +GCR_ENTRY(0xde, 0x27) +GCR_ENTRY(0xdf, 0x28) +GCR_ENTRY(0xe5, 0x29) +GCR_ENTRY(0xe6, 0x2a) +GCR_ENTRY(0xe7, 0x2b) +GCR_ENTRY(0xe9, 0x2c) +GCR_ENTRY(0xea, 0x2d) +GCR_ENTRY(0xeb, 0x2e) +GCR_ENTRY(0xec, 0x2f) +GCR_ENTRY(0xed, 0x30) +GCR_ENTRY(0xee, 0x31) +GCR_ENTRY(0xef, 0x32) +GCR_ENTRY(0xf2, 0x33) +GCR_ENTRY(0xf3, 0x34) +GCR_ENTRY(0xf4, 0x35) +GCR_ENTRY(0xf5, 0x36) +GCR_ENTRY(0xf6, 0x37) +GCR_ENTRY(0xf7, 0x38) +GCR_ENTRY(0xf9, 0x39) +GCR_ENTRY(0xfa, 0x3a) +GCR_ENTRY(0xfb, 0x3b) +GCR_ENTRY(0xfc, 0x3c) +GCR_ENTRY(0xfd, 0x3d) +GCR_ENTRY(0xfe, 0x3e) +GCR_ENTRY(0xff, 0x3f) \ No newline at end of file diff --git a/src/greaseweazle/optimised/optimised.c b/src/greaseweazle/optimised/optimised.c index f0255220..31de0312 100644 --- a/src/greaseweazle/optimised/optimised.c +++ b/src/greaseweazle/optimised/optimised.c @@ -5,6 +5,7 @@ #include #include "mac.h" #include "c64.h" +#include "apple2.h" #define FLUXOP_INDEX 1 #define FLUXOP_SPACE 2 @@ -396,6 +397,57 @@ py_encode_c64_gcr(PyObject *self, PyObject *args) return out; } +static PyObject * +py_decode_apple2_sector(PyObject *self, PyObject *args) +{ + Py_buffer in; + PyObject *out; + int status; + PyObject *res = NULL; + + if (!PyArg_ParseTuple(args, "y*", &in)) + return NULL; + if (in.len < APPLE2_ENCODED_SECTOR_LENGTH+2) + goto fail; + + out = PyBytes_FromStringAndSize(NULL, APPLE2_SECTOR_LENGTH); + if (out == NULL) + goto fail; + + status = decode_apple2_sector((const uint8_t *)in.buf, + (uint8_t *)PyBytes_AsString(out)); + + res = Py_BuildValue("Oi", out, status); + + Py_DECREF(out); +fail: + PyBuffer_Release(&in); + return res; +} + +static PyObject * +py_encode_apple2_sector(PyObject *self, PyObject *args) +{ + Py_buffer in; + PyObject *out = NULL; + + if (!PyArg_ParseTuple(args, "y*", &in)) + return NULL; + if (in.len < APPLE2_SECTOR_LENGTH) + goto fail; + + out = PyBytes_FromStringAndSize(NULL, APPLE2_ENCODED_SECTOR_LENGTH+1); + if (out == NULL) + goto fail; + + encode_apple2_sector((const uint8_t *)in.buf, + (uint8_t *)PyBytes_AsString(out)); + +fail: + PyBuffer_Release(&in); + return out; +} + uint8_t *td0_unpack(uint8_t *packeddata, unsigned int size, unsigned int *unpacked_size); @@ -431,6 +483,8 @@ static PyMethodDef modulefuncs[] = { { "encode_mac_sector", py_encode_mac_sector, METH_VARARGS, NULL }, { "decode_c64_gcr", py_decode_c64_gcr, METH_VARARGS, NULL }, { "encode_c64_gcr", py_encode_c64_gcr, METH_VARARGS, NULL }, + { "decode_apple2_sector", py_decode_apple2_sector, METH_VARARGS, NULL }, + { "encode_apple2_sector", py_encode_apple2_sector, METH_VARARGS, NULL }, { "td0_unpack", py_td0_unpack, METH_VARARGS, NULL }, { NULL } }; diff --git a/src/greaseweazle/optimised/optimised.pyi b/src/greaseweazle/optimised/optimised.pyi index 60931b39..4a714a82 100644 --- a/src/greaseweazle/optimised/optimised.pyi +++ b/src/greaseweazle/optimised/optimised.pyi @@ -32,6 +32,12 @@ def decode_c64_gcr(dat: bytes) -> bytes: def encode_c64_gcr(dat: bytes) -> bytes: ... +def decode_apple2_sector(dat: bytes) -> Tuple[bytes, int]: + ... + +def encode_apple2_sector(dat: bytes) -> bytes: + ... + def td0_unpack(dat: bytes) -> bytes: ... diff --git a/src/greaseweazle/tools/util.py b/src/greaseweazle/tools/util.py index 8ab2780d..b5b9ef8f 100644 --- a/src/greaseweazle/tools/util.py +++ b/src/greaseweazle/tools/util.py @@ -270,6 +270,7 @@ def split_opts(seq): '.d88': 'D88', '.dcp': 'DCP', '.dim': 'DIM', + '.do' : ('DO','apple2'), '.dsd': ('DSD','acorn'), '.dsk': 'DSK', '.edsk': 'EDSK', @@ -282,6 +283,7 @@ def split_opts(seq): '.ipf': ('IPF','caps'), '.mgt': 'MGT', '.msa': 'MSA', + '.po' : ('PO','apple2'), '.raw': 'KryoFlux', '.sf7': 'SF7', '.scp': 'SCP',