Difference between revisions of "Databar decoding"

From YobiWiki
Jump to navigation Jump to search
Line 29: Line 29:
 
==Python decoder==
 
==Python decoder==
 
With the patent in hands, it's doable to write a Python script to decode the symbols and do some consistency checks and interpretations.
 
With the patent in hands, it's doable to write a Python script to decode the symbols and do some consistency checks and interpretations.
  +
Note that the databars we got are 2 bytes longer than the ones described in the patent.
 
  +
===Principles==
  +
The high level steps of the Python script are:
  +
* Convert the image to 1-bit graylevels (not needed here but more future-proofs for other scan sources)
  +
* Scan horizontal lines, look for at least 30 consecutive lines with at least 20% black pixels, there is likely one databar there. Remember its middle line and its width.
  +
* For each databar:
  +
** Average the core 2/3 of the horizontal lines into into single line
  +
** Isolate pulses/stripes
  +
*** Get a list of pulses with their value (0=white, 1=black) and duration (in pixels)
  +
*** If first pulse is larger than last one, revert the list, it's probably starting with an EOL.
  +
** Check that the last stripe is at least 5x larger than the first one, that's our EOL symbol which is 7 or 8-unit large, depending on the previous symbol.
  +
** Convert pulses/stripes into nibbles
  +
** Parse nibbles
 
===Code===
 
===Code===
 
You can download the following Python script [{{#file: oscar.py}} as oscar.py]:
 
You can download the following Python script [{{#file: oscar.py}} as oscar.py]:
Line 69: Line 81:
 
return line
 
return line
   
def line2nibbles(v):
+
def findlines(pix):
  +
# find middle of horizontal lines
  +
# candlines = lines with at least 20% black
  +
candlines=[sum(pix.getline(i))<(255*pix.width)*0.80
  +
for i in xrange(pix.height)]
  +
goodlines=[]
  +
offset=0
  +
while True in candlines:
  +
startline=candlines.index(True)
  +
stopline=candlines.index(False,startline)
  +
candlines=candlines[stopline:]
  +
oldoffset, offset=offset, offset+stopline
  +
bandwidth=stopline - startline
  +
if bandwidth < 30:
  +
continue
  +
midline=startline+(bandwidth/2)+oldoffset
  +
if debuglevel > 0:
  +
print "Found %i-px wide line at y=%i" % (bandwidth, midline)
  +
goodlines.append((bandwidth, midline))
  +
return goodlines
  +
  +
def linepx2pulses(linepx):
  +
pulses=[]
  +
while linepx:
  +
if linepx[0]==0:
  +
if 1 in linepx:
  +
stop=linepx.index(1)
  +
linepx=linepx[stop:]
  +
else:
  +
stop=len(linepx)
  +
linepx=[]
  +
if len(pulses)>0 and 1 in linepx:
  +
pulses.append((0,stop))
  +
else:
  +
if 0 in linepx:
  +
stop=linepx.index(0)
  +
linepx=linepx[stop:]
  +
else:
  +
stop=len(linepx)
  +
linepx=[]
  +
pulses.append((1,stop))
  +
# where is the END marker?
  +
# revert line if needed
  +
if pulses[0][1] > pulses[-1][1]:
  +
pulses=pulses[::-1]
  +
return pulses
  +
  +
def pulses2nibbles(v):
 
factor_learn=0.5
 
factor_learn=0.5
 
factor_squeeze=0.95
 
factor_squeeze=0.95
Line 127: Line 186:
 
break
 
break
 
return nibbles
 
return nibbles
 
def findlines(pix):
 
# find middle of horizontal lines
 
# candlines = lines with at least 20% black
 
candlines=[sum(pix.getline(i))<(255*pix.width)*0.80
 
for i in xrange(pix.height)]
 
goodlines=[]
 
offset=0
 
while True in candlines:
 
startline=candlines.index(True)
 
stopline=candlines.index(False,startline)
 
candlines=candlines[stopline:]
 
oldoffset, offset=offset, offset+stopline
 
bandwidth=stopline - startline
 
if bandwidth < 30:
 
continue
 
midline=startline+(bandwidth/2)+oldoffset
 
if debuglevel > 0:
 
print "Found %i-px wide line at y=%i" % (bandwidth, midline)
 
goodlines.append((bandwidth, midline))
 
return goodlines
 
 
def linepx2pulses(linepx):
 
pulses=[]
 
while linepx:
 
if linepx[0]==0:
 
if 1 in linepx:
 
stop=linepx.index(1)
 
linepx=linepx[stop:]
 
else:
 
stop=len(linepx)
 
linepx=[]
 
if len(pulses)>0 and 1 in linepx:
 
pulses.append((0,stop))
 
else:
 
if 0 in linepx:
 
stop=linepx.index(0)
 
linepx=linepx[stop:]
 
else:
 
stop=len(linepx)
 
linepx=[]
 
pulses.append((1,stop))
 
# where is the END marker?
 
# revert line if needed
 
if pulses[0][1] > pulses[-1][1]:
 
pulses=pulses[::-1]
 
return pulses
 
   
 
def parsenibbles(nibbles):
 
def parsenibbles(nibbles):
Line 228: Line 240:
 
# do we have a END marker?
 
# do we have a END marker?
 
assert pulses[-1][1] > (pulses[0][1]*5)
 
assert pulses[-1][1] > (pulses[0][1]*5)
nibbles=line2nibbles(pulses)
+
nibbles=pulses2nibbles(pulses)
 
parsenibbles(nibbles)
 
parsenibbles(nibbles)
 
</source>
 
</source>

Revision as of 14:11, 22 June 2016

Intro

PoC||GTFO 12 contains two pages (pp53-54) of giant barcodes as a puzzle:

Pocorgtfo12-53.pngPocorgtfo12-54.png

The pages are available in high resolution here and here.

Identification

The hardest part was to find which obscure format was used.
Initially I tried to parse manually the barcode: we easily see start and stop sequences, pulse widths being unit or double, and and some repetitions 7-unit large. But I couldn't go further.

The revelation came when I stumbled upon this example:

Pocorgtfo12-sample.jpg

It's taken from http://www.mainbyte.com/ti99/hardware/oscar/oscar.html where we learn about Oscar, a "databar" optical reader meant to input programs into computers:

Pocorgtfo12-databar.jpg

The manual (pdf) refers to several supported computers of the 80' : Atari 1200XL/1400XL, Atari 400/600/800, Commodore Pet, Commodore VIC 20/64, TI99/4A, TRS 80. Regarding the computer it acts as an ordinary cassette reader.

Patent

Oscar was produced by Databar Corporation.
Looking a patents from Databar Corporation, we can find two describing the Oscar reader, perfect!
The most relevant one for us is US4550247A which is easier to read as pdf. Note that both patents have expired.

Pocorgtfo12-US4550247-1.pngPocorgtfo12-US4550247-5.png

Python decoder

With the patent in hands, it's doable to write a Python script to decode the symbols and do some consistency checks and interpretations. Note that the databars we got are 2 bytes longer than the ones described in the patent.

=Principles

The high level steps of the Python script are:

  • Convert the image to 1-bit graylevels (not needed here but more future-proofs for other scan sources)
  • Scan horizontal lines, look for at least 30 consecutive lines with at least 20% black pixels, there is likely one databar there. Remember its middle line and its width.
  • For each databar:
    • Average the core 2/3 of the horizontal lines into into single line
    • Isolate pulses/stripes
      • Get a list of pulses with their value (0=white, 1=black) and duration (in pixels)
      • If first pulse is larger than last one, revert the list, it's probably starting with an EOL.
    • Check that the last stripe is at least 5x larger than the first one, that's our EOL symbol which is 7 or 8-unit large, depending on the previous symbol.
    • Convert pulses/stripes into nibbles
    • Parse nibbles

Code

You can download the following Python script [{{#file: oscar.py}} as oscar.py]:

#!/usr/bin/env python

import sys
from PIL import Image

debuglevel=0

# Reference: patent US4550247
conv={
0b0100110:0x0, 0b0101010:0x1, 0b0110010:0x2, 0b0110110:0x3,
0b1011010:0x4, 0b1010010:0x5, 0b1010110:0x6, 0b1001010:0x7,
0b0100101:0x8, 0b0101101:0x9, 0b0101001:0xa, 0b0110101:0xb,
0b1011001:0xc, 0b1010101:0xd, 0b1001101:0xe, 0b1001001:0xf,
0b1111111:0x10}

class Pix:
    def __init__(self, filename):
        self.im = Image.open(filename).convert('1')
        self.pixels = list(self.im.getdata())
        self.width, self.height = self.im.size
    def getpix(self, x, y):
        return self.pixels[x+y*self.width]
    def getline(self, y):
        return self.pixels[y*self.width:(y+1)*self.width]
    def getavgline(self, y, radius):
        lines=[]
        lines.append(self.getline(lineindex))
        for i in range(radius):
            lines.append(self.getline(lineindex-i))
            lines.append(self.getline(lineindex+i))
        line=[]
        for i in range(self.width):
            line.append(sum([lines[x][i] 
                for x in range(radius*2+1)]) < 255*radius)
        return line

def findlines(pix):
    # find middle of horizontal lines
    # candlines = lines with at least 20% black
    candlines=[sum(pix.getline(i))<(255*pix.width)*0.80
        for i in xrange(pix.height)]
    goodlines=[]
    offset=0
    while True in candlines:
        startline=candlines.index(True)
        stopline=candlines.index(False,startline)
        candlines=candlines[stopline:]
        oldoffset, offset=offset, offset+stopline
        bandwidth=stopline - startline
        if bandwidth < 30:
            continue
        midline=startline+(bandwidth/2)+oldoffset
        if debuglevel > 0:
            print "Found %i-px wide line at y=%i" % (bandwidth, midline)
        goodlines.append((bandwidth, midline))
    return goodlines

def linepx2pulses(linepx):
    pulses=[]
    while linepx:
        if linepx[0]==0:
            if 1 in linepx:
                stop=linepx.index(1)
                linepx=linepx[stop:]
            else:
                stop=len(linepx)
                linepx=[]
            if len(pulses)>0 and 1 in linepx:
                pulses.append((0,stop))
        else:
            if 0 in linepx:
                stop=linepx.index(0)
                linepx=linepx[stop:]
            else:
                stop=len(linepx)
                linepx=[]
            pulses.append((1,stop))
    # where is the END marker?
    # revert line if needed
    if pulses[0][1] > pulses[-1][1]:
        pulses=pulses[::-1]
    return pulses

def pulses2nibbles(v):
    factor_learn=0.5
    factor_squeeze=0.95
    # train width on START seq
    # train ones and zeroes separately due to image contrast
    smallw=[(v[1][1]+v[3][1]+v[5][1])/3.0*factor_squeeze, 
            (v[0][1]+v[2][1]+v[4][1])/3.0*factor_squeeze]
    if debuglevel > 1:
        print "bitwidths:", smallw
    symbols=[]
    currentsymbol=1
    for i in range(len(v)):
        b,w = v[i]
        if debuglevel > 2:
            print w, smallw[currentsymbol], w/smallw[currentsymbol],
            print round(w/smallw[currentsymbol],0)
        if w < smallw[currentsymbol] * 1.5:
            guess=1
        elif w > smallw[currentsymbol] * 5:
            guess=8 # max is EOL + last bit==1
        else:
            guess=2
        if guess <=2:
            sw=float(w)/guess
        smallw[currentsymbol]=factor_squeeze*\
            (smallw[currentsymbol]/factor_squeeze+sw*factor_learn)/\
            (1+factor_learn)
        if guess <=2:
            for k in range(guess):
                symbols.append(b)
        else: #EOL
            for k in range(guess):
                symbols.append(1)
        currentsymbol^=1
    if debuglevel > 1:
        print "symbols:", len(symbols)
    if debuglevel > 2:
        print symbols
    nibbles=[]
    nn=0
    while symbols:
        s, symbols=symbols[:7],symbols[7:]
        bit7=0
        for k in s:
            bit7<<=1
            bit7+=k
        if debuglevel > 2:
            print "symbols #%02i:" % nn, s, ("7-bit:%x" % bit7),
        nn+=1
        assert bit7 in conv
        if debuglevel > 2:
            print "conv: %x" % conv[bit7]
        if conv[bit7]<0x10:
            nibbles.append(conv[bit7])
        else:
            assert len(symbols)<=1
            break
    return nibbles

def parsenibbles(nibbles):
    if debuglevel > 1:
        print "raw: ", ''.join(["%x" % x for x in nibbles])
    rawlen=len(nibbles)
    assert nibbles[0]==0xd
    nibbles.pop(0)
    assert len(nibbles)%2==0
    bytes=[]
    for i in range(len(nibbles)/2):
        bytes.append((nibbles[i*2+1]<<4) + nibbles[i*2])
    linenumber=bytes[0]
    if debuglevel > 0:
        print "linenumber: %3i" % linenumber
    functionbyte=bytes[1]
    assert functionbyte < 9
    function=['GENERATE TONES', 'CONTROL ARRAY', 'RAM FIRMWARE', 
              'DATA LINE3', 'DATA LINE4', 'DATA LINE5', 'DATA LINE6',
              'DATA LINE7', 'ENDFW'][functionbyte]
    if debuglevel > 0:
        print "function:   %3i = %s" % (functionbyte, function)
    lastnibblenumber=bytes[2]
    assert lastnibblenumber==rawlen
    if function == 'CONTROL ARRAY':
        assert linenumber==0
        mtbyte=bytes[3]
        assert 0 < mtbyte < 10
        machinetype=['Atari400/800/1200', 'Commodore', 'TI99/4',
                     'Timex 1000', 'Timex 2000', 'RADIO SHACK',
                     'RS-232', 'Commodore no loader',
                     'Commodore no loader/no header'][mtbyte-1]
        if debuglevel > 0:
            print "machinetype:%3i = %s" % (mtbyte, machinetype)
    check=bytes[-1]
    if debuglevel > 0:
        print "checksum:  0x%02X = %s" % (bytes[-1],
            ['incorrect!', 'correct'][(sum(bytes) & 0xff)==0])
    if 'DATA LINE' in function:
        data=bytes[3:-1]
        print(''.join(["%02x" % x for x in data]))

for f in sys.argv[1:]:
    pix=Pix(f)
    goodlines=findlines(pix)
    for n in range(len(goodlines)):
        bandwidth, lineindex = goodlines[n]
        if debuglevel > 1:
            print "line #%i: y=%i" % (n, lineindex)
        # sample average of the middle 2/3 of the databar
        radius=int(bandwidth/3)
        linepx=pix.getavgline(lineindex, radius)
        pulses=linepx2pulses(linepx)
        # do we have a END marker?
        assert pulses[-1][1] > (pulses[0][1]*5)
        nibbles=pulses2nibbles(pulses)
        parsenibbles(nibbles)

Usage

First we've to extract the images from the Poc||GTFO 12 pdf, then use our script:

pdftk pocorgtfo12.pdf cat 53-54 output databar.pdf
pdfimages -all databar.pdf databar
./oscar.py databar-000.png databar-001.png |tee oscar_output.hex
ff0edb392537fe3fff0302392702f8394202ee395f02e4397d02da399e02d039bc02c639e002bc3a0402b23a28
02a83a60029e3a7e02943a9f028a3aa202803aa6fa0000000000000000ff02763a46026c3aae02623ac002583a
c4024e3acf02443aed023a3b0f02303b2f02263b50021c3b7302123b7702083b7e01fe3b8a01f43b8e01ea3ba5
01e03bc3200000000000000000ff01d63be301cc3be701c23c0201b83c1601ae3c1901a43c30019a3c3f01903c
4301863c5c017c3c7f01723c8301683ca1015e3cc401543ce4014a3d0601403d25ff0000000000000000ff0136
3d47012c3d6801223d8901183dad010e3dcd01043df000fa3e1200f03e3500e63e5500dc3e7600d23e7a00c83e
9e00be3eb600b43eba00aa3ed800a03ef99d0000000000000000ff00963f17008c3f3500823f5300783f71006e
...

This is only the data part being extracted from the databar encapsulation.
With debuglevel=1 we get some more details:

linenumber:   0
function:     1 = CONTROL ARRAY
machinetype:  3 = TI99/4
checksum:  0x4E = correct
linenumber:   1
function:     3 = DATA LINE3
checksum:  0x75 = correct
ff0edb392537fe3fff0302392702f8394202ee395f02e4397d02da399e02d039bc02c639e002bc3a0402b23a28
linenumber:   2
function:     3 = DATA LINE3
checksum:  0x8A = correct
02a83a60029e3a7e02943a9f028a3aa202803aa6fa0000000000000000ff02763a46026c3aae02623ac002583a
...

The control array contains far more info, ainly to tell to Oscar how to emulate a cassette reader for the specific machine.
According to the patent description, the control array contains a byte that tells us the databar is foreseen for a TI99/4.
But we've seen in the User Manual that it's more likely to be for its successor TI99/4A.

TI99/4A payload

So the databar payload is likely to be interpretable by a TI99/4A.
We can observe regular repetitions of "0000000000000000", let's realign the data:

cat oscar_output.hex|tr -d '\n'|sed 's/00000000ff/00000000\nff/g'|tee oscar_output2.hex
ff0edb392537fe3fff0302392702f8394202ee395f02e4397d02da399e02d039bc02c639e002bc3a0402b23a2802a83a60029e3a7e02943a9f028a3aa202803aa6fa0000000000000000
ff02763a46026c3aae02623ac002583ac4024e3acf02443aed023a3b0f02303b2f02263b50021c3b7302123b7702083b7e01fe3b8a01f43b8e01ea3ba501e03bc3200000000000000000
...
ff4e30393937303030370080808080b0b2a5b3b380a580b4af80a5b8a9b480808080808080808080808080808080808080808080808080808080808080808080801d0000000000000000
000000000000000000000000000000000000000000000000000000000000000000

So the structure seems to be something like

ff................................................................................................................................cc0000000000000000

with cc being a kind of checksum byte (see especially the last line and this will be confirmed when extracting strings later.
Let's isolate what's supposed to be the payload and let's see if we can observe strings:

sed 's/^ff//;s/..0000000000000000$//' oscar_output2.hex > oscar_output3.hex
xxd -r -p oscar_output3.hex > oscar_output3.hex.bin
hexdump -C -v oscar_output3.hex.bin
00000000  0e db 39 25 37 fe 3f ff  03 02 39 27 02 f8 39 42  |..9%7.?...9'..9B|
00000010  02 ee 39 5f 02 e4 39 7d  02 da 39 9e 02 d0 39 bc  |..9_..9}..9...9.|
00000020  02 c6 39 e0 02 bc 3a 04  02 b2 3a 28 02 a8 3a 60  |..9...:...:(..:`|
00000030  02 9e 3a 7e 02 94 3a 9f  02 8a 3a a2 02 80 3a a6  |..:~..:...:...:.|
00000040  02 76 3a 46 02 6c 3a ae  02 62 3a c0 02 58 3a c4  |.v:F.l:..b:..X:.|
00000050  02 4e 3a cf 02 44 3a ed  02 3a 3b 0f 02 30 3b 2f  |.N:..D:..:;..0;/|
00000060  02 26 3b 50 02 1c 3b 73  02 12 3b 77 02 08 3b 7e  |.&;P..;s..;w..;~|
00000070  01 fe 3b 8a 01 f4 3b 8e  01 ea 3b a5 01 e0 3b c3  |..;...;...;...;.|
00000080  01 d6 3b e3 01 cc 3b e7  01 c2 3c 02 01 b8 3c 16  |..;...;...<...<.|
00000090  01 ae 3c 19 01 a4 3c 30  01 9a 3c 3f 01 90 3c 43  |..<...<0..<?..<C|
000000a0  01 86 3c 5c 01 7c 3c 7f  01 72 3c 83 01 68 3c a1  |..<\.|<..r<..h<.|
000000b0  01 5e 3c c4 01 54 3c e4  01 4a 3d 06 01 40 3d 25  |.^<..T<..J=..@=%|
000000c0  01 36 3d 47 01 2c 3d 68  01 22 3d 89 01 18 3d ad  |.6=G.,=h."=...=.|
000000d0  01 0e 3d cd 01 04 3d f0  00 fa 3e 12 00 f0 3e 35  |..=...=...>...>5|
000000e0  00 e6 3e 55 00 dc 3e 76  00 d2 3e 7a 00 c8 3e 9e  |..>U..>v..>z..>.|
000000f0  00 be 3e b6 00 b4 3e ba  00 aa 3e d8 00 a0 3e f9  |..>...>...>...>.|
00000100  00 96 3f 17 00 8c 3f 35  00 82 3f 53 00 78 3f 71  |..?...?5..?S.x?q|
00000110  00 6e 3f 75 00 64 3f 79  00 5a 3f 97 00 50 3f a9  |.n?u.d?y.Z?..P?.|
00000120  00 46 3f bc 00 3c 3f c0  00 32 3f c4 00 28 3f d9  |.F?..<?..2?..(?.|
00000130  1a 9a 20 49 4e 20 54 48  45 20 4e 45 58 54 20 46  |.. IN THE NEXT F|
00000140  45 57 20 49 53 53 55 45  53 2e 00 1c 9a 20 50 52  |EW ISSUES.... PR|
00000150  4f 47 52 41 4d 53 20 44  4f 20 57 48 41 54 20 59  |OGRAMS DO WHAT Y|
00000160  4f 55 20 57 41 4e 54 00  1d 9a 20 57 45 27 4c 4c  |OU WANT... WE'LL|
00000170  20 54 45 4c 4c 20 59 4f  55 20 48 4f 57 20 54 4f  | TELL YOU HOW TO|
00000180  20 4d 41 4b 45 00 20 9a  20 4d 41 4b 45 20 54 48  | MAKE. . MAKE TH|
00000190  49 53 20 50 52 4f 47 52  41 4d 20 53 54 41 52 54  |IS PROGRAM START|
000001a0  20 4f 56 45 52 3f 00 1d  9a 20 57 48 41 54 20 4c  | OVER?... WHAT L|
000001b0  49 4e 45 20 57 4f 55 4c  44 20 59 4f 55 20 41 44  |INE WOULD YOU AD|
000001c0  44 20 54 4f 00 23 93 c8  07 31 32 33 39 34 2e 30  |D TO.#...12394.0|
000001d0  b3 c8 04 31 38 2e 34 b3  c8 07 31 32 36 35 37 2e  |...18.4...12657.|
000001e0  37 b3 c8 04 31 36 2e 32  00 23 93 c8 07 31 31 38  |7...16.2.#...118|
000001f0  32 33 2e 32 b3 c8 04 38  2e 36 35 b3 c8 07 31 32  |23.2...8.65...12|
00000200  31 31 38 2e 34 b3 c8 04  32 30 2e 37 00 23 93 c8  |118.4...20.7.#..|
00000210  07 31 31 35 32 30 2e 37  b3 c8 04 37 2e 33 30 b3  |.11520.7...7.30.|
00000220  c8 07 31 31 36 37 38 2e  36 b3 c8 04 39 2e 38 30  |..11678.6...9.80|
00000230  00 1d 9a 20 20 4d 49 4c  45 53 2d 2d 20 47 41 4c  |...  MILES-- GAL|
00000240  53 2d 2d 4d 49 4c 45 53  2d 2d 47 41 4c 53 00 19  |S--MILES--GALS..|
00000250  9c 4d 32 b4 fc b7 c8 02  31 32 b6 b4 47 b4 fc b7  |.M2.....12..G...|
00000260  c8 02 32 30 b6 b4 4d 47  00 1d 9a 20 47 41 4c 4c  |..20..MG... GALL|
00000270  4f 4e 53 20 55 53 45 44  20 49 53 20 4e 45 58 54  |ONS USED IS NEXT|
00000280  2e 20 20 20 20 20 00 20  9a 20 54 48 45 20 44 41  |.     . . THE DA|
00000290  54 41 20 46 4f 52 20 54  48 45 20 4d 49 4c 45 41  |TA FOR THE MILEA|
000002a0  47 45 20 41 4e 44 20 00  02 8b 00 03 96 58 00 07  |GE AND ......X..|
000002b0  8d 4d 31 be 4d 32 00 11  8d 4d 47 be cf b7 b7 4d  |.M1.M2...MG....M|
000002c0  32 c2 4d 31 b6 c4 47 b6  00 03 9a 20 00 0a 9a 20  |2.M1..G.... ... |
000002d0  41 47 41 49 4e 2e 20 00  1d 9a 20 43 41 4c 43 55  |AGAIN. ... CALCU|
000002e0  4c 41 54 49 4f 4e 2e 20  20 44 4f 20 54 48 45 20  |LATION.  DO THE |
000002f0  4c 4f 4f 50 20 00 21 9a  20 53 54 41 52 54 49 4e  |LOOP .!. STARTIN|
00000300  47 20 4d 49 4c 45 41 47  45 20 46 4f 52 20 54 48  |G MILEAGE FOR TH|
00000310  45 20 4e 45 58 54 20 00  1f 9a 20 54 48 45 20 4e  |E NEXT ... THE N|
00000320  45 57 20 4d 49 4c 45 41  47 45 20 52 45 41 44 49  |EW MILEAGE READI|
00000330  4e 47 20 54 48 45 20 00  20 9a 20 50 52 49 4e 54  |NG THE . . PRINT|
00000340  20 54 48 45 20 41 4e 53  57 45 52 2e 20 20 54 48  | THE ANSWER.  TH|
00000350  45 4e 20 4d 41 4b 45 20  00 22 9a 20 43 41 4c 43  |EN MAKE .". CALC|
00000360  55 4c 41 54 45 20 4d 49  4c 45 53 20 50 45 52 20  |ULATE MILES PER |
00000370  47 41 4c 4c 4f 4e 20 41  4e 44 20 00 03 9a 20 00  |GALLON AND ... .|
00000380  06 97 4d 32 b3 47 00 0b  8c 58 be c8 01 31 b1 c8  |..M2.G...X...1..|
00000390  01 36 00 03 9a 20 00 16  9a 20 47 41 4c 4c 4f 4e  |.6... ... GALLON|
000003a0  53 20 55 53 45 44 20 44  41 54 41 2e 20 00 1d 9a  |S USED DATA. ...|
000003b0  20 52 45 41 44 20 54 48  45 20 4e 45 58 54 20 4d  | READ THE NEXT M|
000003c0  49 4c 45 41 47 45 20 41  4e 44 20 00 1f 9a 20 54  |ILEAGE AND ... T|
000003d0  48 45 52 45 20 41 52 45  20 36 20 52 45 41 44 49  |HERE ARE 6 READI|
000003e0  4e 47 53 20 54 4f 20 44  4f 2e 20 00 03 9a 20 00  |NGS TO DO. ... .|
000003f0  1a 9c c7 16 20 4d 49 4c  45 41 47 45 20 20 47 41  |.... MILEAGE  GA|
00000400  4c 4c 4f 4e 53 20 20 4d  50 47 00 13 9c c7 0f 20  |LLONS  MPG..... |
00000410  20 20 20 20 20 20 20 20  20 20 23 20 4f 46 00 02  |          # OF..|
00000420  9c 00 16 9c c7 12 20 20  20 54 52 49 50 20 54 4f  |......   TRIP TO|
00000430  20 50 48 4f 45 4e 49 58  00 0e 8d 4d 31 be c8 07  | PHOENIX...M1...|
00000440  31 31 34 30 36 2e 34 00  03 9a 20 00 18 9a 20 54  |11406.4... ... T|
00000450  48 45 20 43 4f 4c 55 4d  4e 20 48 45 41 44 49 4e  |HE COLUMN HEADIN|
00000460  47 53 2e 20 00 22 9a 20  53 45 54 20 53 54 41 52  |GS. .". SET STAR|
00000470  54 49 4e 47 20 4d 49 4c  45 41 47 45 20 41 4e 44  |TING MILEAGE AND|
00000480  20 50 52 49 4e 54 20 00  03 9a 20 00 1d 9a 20 54  | PRINT ... ... T|
00000490  48 45 20 4d 49 4c 45 41  47 45 20 52 45 41 44 49  |HE MILEAGE READI|
000004a0  4e 47 53 2e 20 20 20 20  20 00 22 9a 20 50 52 4f  |NGS.     .". PRO|
000004b0  43 45 53 53 20 49 53 20  52 45 50 45 41 54 45 44  |CESS IS REPEATED|
000004c0  20 46 4f 52 20 41 4c 4c  20 4f 46 20 00 1f 9a 20  | FOR ALL OF ... |
000004d0  54 48 45 20 4d 49 4c 45  53 20 50 45 52 20 47 41  |THE MILES PER GA|
000004e0  4c 4c 4f 4e 2e 20 20 54  48 49 53 20 00 21 9a 20  |LLON.  THIS .!. |
000004f0  4e 55 4d 42 45 52 20 4f  46 20 47 41 4c 4c 4f 4e  |NUMBER OF GALLON|
00000500  53 20 55 53 45 44 20 54  4f 20 47 45 54 20 00 1e  |S USED TO GET ..|
00000510  9a 20 4f 46 20 4d 49 4c  45 53 20 49 53 20 44 49  |. OF MILES IS DI|
00000520  56 49 44 45 44 20 42 59  20 54 48 45 20 00 21 9a  |VIDED BY THE .!.|
00000530  20 42 45 54 57 45 45 4e  20 46 49 4c 4c 2d 55 50  | BETWEEN FILL-UP|
00000540  53 2e 20 20 54 48 45 20  4e 55 4d 42 45 52 20 00  |S.  THE NUMBER .|
00000550  20 9a 20 54 48 45 20 4e  55 4d 42 45 52 20 4f 46  | . THE NUMBER OF|
00000560  20 4d 49 4c 45 53 20 54  52 41 56 45 4c 45 44 20  | MILES TRAVELED |
00000570  00 20 9a 20 4d 49 4c 45  41 47 45 20 52 45 41 44  |. . MILEAGE READ|
00000580  49 4e 47 2e 20 20 54 48  49 53 20 47 49 56 45 53  |ING.  THIS GIVES|
00000590  20 00 23 9a 20 53 55 42  54 52 41 43 54 49 4e 47  | .#. SUBTRACTING|
000005a0  20 49 54 20 46 52 4f 4d  20 54 48 45 20 43 55 52  | IT FROM THE CUR|
000005b0  52 45 4e 54 20 00 1f 9a  20 52 45 41 44 49 4e 47  |RENT ... READING|
000005c0  20 46 52 4f 4d 20 54 48  45 20 2d 44 41 54 41 2d  | FROM THE -DATA-|
000005d0  20 41 4e 44 20 00 22 9a  20 57 4f 52 4b 53 20 42  | AND .". WORKS B|
000005e0  59 20 47 45 54 54 49 4e  47 20 41 20 4e 45 57 20  |Y GETTING A NEW |
000005f0  4d 49 4c 45 41 47 45 20  00 21 9a 20 44 55 52 49  |MILEAGE .!. DURI|
00000600  4e 47 20 54 48 45 20 54  52 49 50 2e 20 20 54 48  |NG THE TRIP.  TH|
00000610  45 20 50 52 4f 47 52 41  4d 20 00 22 9a 20 4d 49  |E PROGRAM .". MI|
00000620  4c 45 41 47 45 20 41 4e  44 20 47 41 4c 4c 4f 4e  |LEAGE AND GALLON|
00000630  53 20 44 41 54 41 20 54  41 4b 45 4e 20 00 1f 9a  |S DATA TAKEN ...|
00000640  20 45 4e 44 20 4f 46 20  54 48 45 20 50 52 4f 47  | END OF THE PROG|
00000650  52 41 4d 20 48 41 56 45  20 54 48 45 20 00 20 9a  |RAM HAVE THE . .|
00000660  20 54 48 45 20 2d 44 41  54 41 2d 20 53 54 41 54  | THE -DATA- STAT|
00000670  45 4d 45 4e 54 53 20 41  54 20 54 48 45 20 00 03  |EMENTS AT THE ..|
00000680  9a 20 00 23 9a 20 4d 47  3d 49 4e 54 45 47 45 52  |. .#. MG=INTEGER|
00000690  20 56 41 4c 55 45 20 4f  46 20 28 28 4d 31 2d 4d  | VALUE OF ((M1-M|
000006a0  32 29 2f 47 29 20 00 17  9a 20 2a 2a 2a 2a 2a 20  |2)/G) ... ***** |
000006b0  46 4f 52 4d 55 4c 41 20  2a 2a 2a 2a 2a 20 00 03  |FORMULA ***** ..|
000006c0  9a 20 00 1d 9a 20 2a 2a  2a 20 4d 47 20 3d 20 4d  |. ... *** MG = M|
000006d0  49 4c 45 53 20 50 45 52  20 47 41 4c 4c 4f 4e 20  |ILES PER GALLON |
000006e0  00 20 9a 20 2a 2a 2a 20  47 20 20 3d 20 47 41 4c  |. . *** G  = GAL|
000006f0  4c 4f 4e 53 20 4f 46 20  47 41 53 20 55 53 45 44  |LONS OF GAS USED|
00000700  20 00 1d 9a 20 2a 2a 2a  20 4d 32 20 3d 20 45 4e  | ... *** M2 = EN|
00000710  44 49 4e 47 20 4d 49 4c  45 41 47 45 20 20 20 00  |DING MILEAGE   .|
00000720  1d 9a 20 2a 2a 2a 20 4d  31 20 3d 20 53 54 41 52  |.. *** M1 = STAR|
00000730  54 49 4e 47 20 4d 49 4c  45 41 47 45 20 00 1d 9a  |TING MILEAGE ...|
00000740  20 2a 2a 2a 20 58 20 20  3d 20 4c 4f 4f 50 20 43  | *** X  = LOOP C|
00000750  4f 55 4e 54 45 52 20 20  20 20 20 00 1d 9a 20 2a  |OUNTER     ... *|
00000760  2a 2a 2a 2a 20 20 44 49  43 54 49 4f 4e 41 52 59  |****  DICTIONARY|
00000770  20 20 2a 2a 2a 2a 2a 20  20 00 03 9a 20 00 03 9a  |  *****  ... ...|
00000780  20 00 1d 9a 20 45 44 45  4e 20 50 52 41 49 52 49  | ... EDEN PRAIRI|
00000790  45 2c 20 4d 4e 20 20 35  35 33 34 34 20 20 20 00  |E, MN  55344   .|
000007a0  11 9a 20 44 41 54 41 42  41 52 20 43 4f 52 50 2e  |.. DATABAR CORP.|
000007b0  20 00 12 9a 20 43 4f 50  59 52 49 47 48 54 20 31  | ... COPYRIGHT 1|
000007c0  39 38 33 20 00 03 9a 20  00 03 9a 20 00 14 9a 20  |983 ... ... ... |
000007d0  4d 49 4c 45 53 20 50 45  52 20 47 41 4c 4c 4f 4e  |MILES PER GALLON|
000007e0  20 00 27 58 31 24 be c7  20 28 43 29 20 31 39 38  | .'X1$.. (C) 198|
000007f0  33 20 44 41 54 41 42 41  52 20 43 4f 52 50 2e 53  |3 DATABAR CORP.S|
00000800  4e 30 39 39 37 30 30 30  37 00 80 80 80 80 b0 b2  |N09970007.......|
00000810  a5 b3 b3 80 a5 80 b4 af  80 a5 b8 a9 b4 80 80 80  |................|
00000820  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  |................|
00000830  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  |................|
00000840  1d 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000850  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000860  00                                                |.|

Strangely the lines seem to be in reverse order, but at other boundaries than those observed in oscar_output2.hex.

strings oscar_output3.hex.bin |tac |tee oscar_output3.txt
 (C) 1983 DATABAR CORP.SN09970007
'X1$
 MILES PER GALLON 
 COPYRIGHT 1983 
 DATABAR CORP. 
 EDEN PRAIRIE, MN  55344   
 *****  DICTIONARY  *****  
 *** X  = LOOP COUNTER     
 *** M1 = STARTING MILEAGE 
 *** M2 = ENDING MILEAGE   
 *** G  = GALLONS OF GAS USED 
 *** MG = MILES PER GALLON 
 ***** FORMULA ***** 
 MG=INTEGER VALUE OF ((M1-M2)/G) 
 THE -DATA- STATEMENTS AT THE 
 END OF THE PROGRAM HAVE THE 
 MILEAGE AND GALLONS DATA TAKEN 
 DURING THE TRIP.  THE PROGRAM 
 WORKS BY GETTING A NEW MILEAGE 
 READING FROM THE -DATA- AND 
 SUBTRACTING IT FROM THE CURRENT 
 MILEAGE READING.  THIS GIVES 
 THE NUMBER OF MILES TRAVELED 
 BETWEEN FILL-UPS.  THE NUMBER 
 OF MILES IS DIVIDED BY THE 
 NUMBER OF GALLONS USED TO GET 
 THE MILES PER GALLON.  THIS 
 PROCESS IS REPEATED FOR ALL OF 
 THE MILEAGE READINGS.     
 SET STARTING MILEAGE AND PRINT 
 THE COLUMN HEADINGS. 
11406.4
   TRIP TO PHOENIX
           # OF
 MILEAGE  GALLONS  MPG
 THERE ARE 6 READINGS TO DO. 
 READ THE NEXT MILEAGE AND 
 GALLONS USED DATA. 
 CALCULATE MILES PER GALLON AND 
 PRINT THE ANSWER.  THEN MAKE 
 THE NEW MILEAGE READING THE 
 STARTING MILEAGE FOR THE NEXT 
 CALCULATION.  DO THE LOOP 
 AGAIN. 
 THE DATA FOR THE MILEAGE AND 
 GALLONS USED IS NEXT.     
  MILES-- GALS--MILES--GALS
9.80
11678.6
7.30
11520.7
20.7
12118.4
8.65
11823.2
16.2
12657.7
18.4
12394.0
 WHAT LINE WOULD YOU ADD TO
 MAKE THIS PROGRAM START OVER?
 WE'LL TELL YOU HOW TO MAKE
 PROGRAMS DO WHAT YOU WANT
 IN THE NEXT FEW ISSUES.

TI99/4A emulation

There are apparently a number of emulators around, but I didn't see how to match the payload we have with the various expected data formats.

The Oscar manual says in the section specific to TI99/4A that it's to be used as a cassette reader "TI Basic" "OLD CS1".
So, theoretically, one approach is that one could interpret the Control Array with both patents and produce a wav file to feed e.g. in the MESS emulator.
Any volunteer?

An other approach is to take the payload we got and understand how to convert it into a "disk" format.

One could try to disassemble the payload, but it's Basic so full of refs to routines in TI99/4A ROM.

If anyone could go further than I, contact me on twitter (@doegox).