Browse Source

IPF: Initial support for writing IPF images

Keir Fraser 5 years ago
parent
commit
8e28f04d14

+ 6 - 3
scripts/greaseweazle/flux.py

@@ -24,16 +24,19 @@ class Flux:
 
 
 
 
     @classmethod
     @classmethod
-    def from_bitarray(cls, bitarray, bitrate):
+    def from_bitarray(cls, bitarray, bitrate, timing=None):
+        if not timing:
+            timing = [1000] * len(bitarray)
+        timing_i = iter(timing)
         flux_list = []
         flux_list = []
         count = 0
         count = 0
         for bit in bitarray:
         for bit in bitarray:
-            count += 1
+            count += next(timing_i)
             if bit:
             if bit:
                 flux_list.append(count)
                 flux_list.append(count)
                 count = 0
                 count = 0
         flux_list[0] += count
         flux_list[0] += count
-        return Flux([sum(flux_list)], flux_list, bitrate)
+        return Flux([sum(flux_list)], flux_list, bitrate * 1000)
 
 
  
  
 # Local variables:
 # Local variables:

+ 6 - 0
scripts/greaseweazle/image/hfe.py

@@ -22,6 +22,12 @@ class HFE:
         self.track_list = []
         self.track_list = []
 
 
 
 
+    @classmethod
+    def to_file(cls, start_cyl, nr_sides):
+        hfe = cls(start_cyl, nr_sides)
+        return hfe
+
+
     @classmethod
     @classmethod
     def from_file(cls, dat):
     def from_file(cls, dat):
 
 

+ 275 - 0
scripts/greaseweazle/image/ipf.py

@@ -0,0 +1,275 @@
+# 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
+import platform
+import ctypes as ct
+from bitarray import bitarray
+from greaseweazle.flux import Flux
+
+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 IPF:
+
+    def __init__(self, start_cyl, nr_sides):
+        self.lib = get_libcaps()
+        self.start_cyl = start_cyl
+        self.nr_sides = nr_sides
+
+    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_filename(cls, name):
+
+        ipf = cls(0, 0)
+
+        ipf.iid = ipf.lib.CAPSAddImage()
+        assert ipf.iid >= 0, "Could not create IPF image container"
+        cname = ct.c_char_p(name.encode())
+        res = ipf.lib.CAPSLockImage(ipf.iid, cname)
+        assert res == 0, "Could not open IPF image '%s'" % name
+        res = ipf.lib.CAPSLoadImage(ipf.iid, DI_LOCK.def_flags)
+        assert res == 0, "Could not load IPF image '%s'" % name
+        ipf.pi = CapsImageInfo()
+        res = ipf.lib.CAPSGetImageInfo(ct.byref(ipf.pi), ipf.iid)
+        assert res == 0
+        print(ipf)
+
+        return ipf
+
+
+    def get_track(self, cyl, head, writeout=False):
+        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)
+        assert res == 0
+
+        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]
+
+        # Write splice is at trackbuf[ti.overlap]. Index is at trackbuf[0].
+        #for i in range(ti.sectorcnt):
+        #    si = CapsSectorInfo()
+        #    res = self.lib.CAPSGetInfo(ct.byref(si), self.iid,
+        #                               cyl, head, 1, i)
+        #    assert res == 0
+        #    # Data is at trackbuf[si.datastart:si.datastart + si.datasize]
+        #for i in range(ti.weakcnt):
+        #    wi = CapsDataInfo()
+        #    res = self.lib.CAPSGetInfo(ct.byref(wi), self.iid,
+        #                               cyl, head, 2, i)
+        #    assert res == 0
+        #    # Weak data at trackbuf[wi.start:wi.start + wi.size]
+
+        assert ti.weakcnt == 0, "Can't yet handle weak data"
+
+        # 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).
+        bitrate = ti.tracklen * 5
+
+        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]
+
+        # TODO: Place overlap (write splice) at the correct position.
+        if ti.overlap != 0:
+            trackbuf = trackbuf[ti.overlap:] + trackbuf[:ti.overlap]
+            if timebuf:
+                timebuf = timebuf[ti.overlap:] + timebuf[:ti.overlap]
+
+        return Flux.from_bitarray(trackbuf, bitrate, timebuf)
+
+
+# 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
+    assert "lib" in locals(), "Could not find SPS/CAPS IPF decode library"
+    print(name)
+    
+    # We have opened the library. Now initialise it.
+    res = lib.CAPSInit()
+    assert res == 0, "Failure initialising %s" % libname
+
+    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:

+ 6 - 0
scripts/greaseweazle/image/scp.py

@@ -21,6 +21,12 @@ class SCP:
         self.track_list = []
         self.track_list = []
 
 
 
 
+    @classmethod
+    def to_file(cls, start_cyl, nr_sides):
+        hfe = cls(start_cyl, nr_sides)
+        return hfe
+
+
     @classmethod
     @classmethod
     def from_file(cls, dat):
     def from_file(cls, dat):
 
 

+ 5 - 1
scripts/greaseweazle/tools/read.py

@@ -19,7 +19,11 @@ def read_to_image(usb, args):
     image_class = util.get_image_class(args.file)
     image_class = util.get_image_class(args.file)
     if not image_class:
     if not image_class:
         return
         return
-    image = image_class(args.scyl, args.nr_sides)
+    if not hasattr(image_class, 'to_file'):
+        print("%s: Cannot create %s image files"
+              % (args.file, image_class.__name__))
+        return
+    image = image_class.to_file(args.scyl, args.nr_sides)
 
 
     for cyl in range(args.scyl, args.ecyl+1):
     for cyl in range(args.scyl, args.ecyl+1):
         for side in range(0, args.nr_sides):
         for side in range(0, args.nr_sides):

+ 3 - 2
scripts/greaseweazle/tools/util.py

@@ -14,6 +14,7 @@ from greaseweazle import version
 from greaseweazle import usb as USB
 from greaseweazle import usb as USB
 from greaseweazle.image.scp import SCP
 from greaseweazle.image.scp import SCP
 from greaseweazle.image.hfe import HFE
 from greaseweazle.image.hfe import HFE
+from greaseweazle.image.ipf import IPF
 
 
 
 
 def drive_letter(letter):
 def drive_letter(letter):
@@ -30,10 +31,10 @@ def drive_letter(letter):
 
 
 
 
 def get_image_class(name):
 def get_image_class(name):
-    image_types = { '.scp': SCP, '.hfe': HFE }
+    image_types = { '.scp': SCP, '.hfe': HFE, '.ipf': IPF }
     _, ext = os.path.splitext(name)
     _, ext = os.path.splitext(name)
     if not ext.lower() in image_types:
     if not ext.lower() in image_types:
-        print("**Error: Unrecognised file suffix '%s'" % ext)
+        print("%s: Unrecognised file suffix '%s'" % (name, ext))
         return None
         return None
     return image_types[ext.lower()]
     return image_types[ext.lower()]
 
 

+ 5 - 2
scripts/greaseweazle/tools/write.py

@@ -26,8 +26,11 @@ def write_from_image(usb, args):
     image_class = util.get_image_class(args.file)
     image_class = util.get_image_class(args.file)
     if not image_class:
     if not image_class:
         return
         return
-    with open(args.file, "rb") as f:
-        image = image_class.from_file(f.read())
+    if hasattr(image_class, 'from_filename'):
+        image = image_class.from_filename(args.file)
+    else:
+        with open(args.file, "rb") as f:
+            image = image_class.from_file(f.read())
 
 
     for cyl in range(args.scyl, args.ecyl+1):
     for cyl in range(args.scyl, args.ecyl+1):
         for side in range(0, args.nr_sides):
         for side in range(0, args.nr_sides):