#!/usr/bin/env python3 # Simple tool to process LINCtape images found on Bitsavers # # Paul Koning, March 2021 # # The images in question are decoded at the bit level but no further; # they contain one LINCtape frame per byte. The mark track is bit 0, # the data tracks are bits 3, 2 and 1 (corresponding to data bits 0, # 1, 2). There is no record indicating how these were captured or why # it was done in this particular manner. # # It appears the capture was done on a DECtape drive; the capture # files look like a reverse direction record of the LINCtape data. # # There is no SIMH format for LINCtape, but the output format used # here is analogous to that used for 12-bit DECtape. A LINCtape has # 512 blocks with 256 12-bit words per block; the output file has that # data, with each word written in two bytes, little endian. # # According to the PDP-12 training notes, there are a number of blocks # with negative block numbers at the start of the tape; these are # omitted for now. import sys import os import collections import gzip FILL12 = [ 0o7777 ] * 256 # Build the mapping table from the data track bits (1-3 in the dump # file) to the actual data. bmap = list () for i in range (8): r = 0 if i & 1: r |= 0x001 if i & 2: r |= 0x010 if i & 4: r |= 0x100 bmap.append (~r & 0x111) bmap = tuple (bmap) def rdata (fn): # Read the capture file, and return its contents in reversed order bfn, x = os.path.splitext (fn) if x == ".gz": f = gzip.GzipFile (fn) fn = bfn else: f = open (fn, "rb") ret = f.read () f.close () return reversed (ret.rstrip (b"\000")), fn def pstats (d): stats = collections.Counter () dlen = len (d) for b in d: for i in range (8): bit = 1 << i if b & bit: stats[i] += 1 for i in range (8): print ("{}: {:>7d} {:>7.3f}%".format (i, stats[i], stats[i] * 100 / dlen)) def writevcd (fn, d): import vcd ofn, x = os.path.splitext (fn) ofn += ".vcd" with open (ofn, "wt") as v: with vcd.writer.VCDWriter (v, timescale = "1 us") as vf: vl = vf.register_var ("module", "track", vcd.writer.VarType.integer, 8) for x, b in enumerate (d): vf.change (vl, x, b) mark = 0 data = 0 def frame (di): # Read one frame, accumulate current mark and data words global mark, data b = next (di) mark = ((mark << 1) | (b & 1)) & 0o17 data = (((data << 1) & 0xeee) | bmap [(b >> 1) & 7]) & 0o7777 def word (di): # Read a 12 bit word. Note that we need to have block sync at # this point, so this is only done after the start of block (block # number field) has been recognized. for i in range (4): frame (di) def ocframe (di): # Read one frame obverse complement, accumulate current mark and # data words global mark, data b = next (di) mark = ((mark << 1) | (b & 1)) & 0o17 data = (((data >> 1) & 0x777) | (bmap [(~b >> 1) & 7] << 3)) & 0o7777 class MTE (Exception): def __init__ (self, got, exp): self.got = got self.exp = exp def __str__ (self): exp = self.exp if isinstance (exp, set): exp = " or ".join ("{:0>2o}".format (i) for i in exp) else: exp = "{:0>2o}".format (exp) return "Mark track error, got {:0>2o}, expected {}" \ .format (self.got, exp) def rmark (exp): if mark != exp: raise MTE (mark, exp) def block (di, eblk): while True: if mark == 0o17: # Interblock mark break frame (di) while True: if mark == 0o16: # Block mark break frame (di) bnum = data if bnum != eblk: print ("Unexpected block, got {:0>4o}, expected {:0>4o}".format (bnum, eblk)) # Skip a frame to restart the block number search #frame (di) #return bnum, None # Guard word word (di) rmark (0o02) bd = list () while mark != 0o13: word (di) exp = { 0o11, 0o13 } if mark not in exp: raise MTE (mark, exp) bd.append (data) # TODO: check words (3 of them) word (di) rmark (0o01) word (di) #rmark (0o01) word (di) #rmark (0o01) # Guard word (not in the student manual, but described # in the system reference manual) word (di) #rmark (0o02) # Reverse block number, assemble obverse complement frames for i in range (4): ocframe (di) rmark (0o07) brev = data if bnum != brev: print ("block {:0>4o} rev mismatch {:0>4o}".format (bnum, brev)) else: print ("good block {:0>4o}".format (bnum)) return bnum, bd def process (d): blks = [ None ] * 2000 di = iter (d) bs = None curblk = blkcnt = 0 while True: try: ret = block (di, curblk) except MTE as e: print ("block {:0>4o} {!s}".format (curblk, e)) continue #ret = curblk, FILL12 except StopIteration: print ("Unexpected EOF, expected block", curblk) break if ret: bnum, bd = ret if bd is None: continue #bd = FILL12 curblk = bnum + 1 if bs: if len (bd) != bs: print ("Block length mismatch, got {}, expected {}, block {}".format (len (bd), bs, bnum)) bs = len (bd) else: bs = len (bd) if bs == 256: # LINC blkcnt = 512 # actually, usually more else: print ("Strange block length", bs) if bnum == 0o0346: # LAP6-DIAL directory block ? print ("block 0346") for i in range (0, 256, 8): s = [ "{:0>4o}: ".format (i) ] for j in range (8): s.append ("{:0>4o} ".format (bd[i +j])) print ("".join (s)) if bnum < len (blks): blks[bnum] = bd else: pass return blks[:curblk] def write12 (fn, blks): # Write the data as 12 bits per 2 bytes bfn, x = os.path.splitext (fn) with open (bfn + ".t12", "wb") as f: for blk in blks: if blk is None: blk = FILL12 for w in blk: f.write ((w).to_bytes (2, "little")) def main (args): vcdsw = False for fn in args: if fn == "-v": vcdsw = True continue print (fn) d, fn = rdata (fn) if vcdsw: writevcd (fn, d) blks = process (d) if not blks: print ("No readable blocks found in", fn) continue if len (blks) != 512: print ("block count is off, expecting 512, got", len (blks),"in", fn) write12 (fn, blks) if __name__ == "__main__": main (sys.argv[1:])