lif80utils 0.0.1__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.
- lif80utils/amigo.py +271 -0
- lif80utils/amigofile.py +160 -0
- lif80utils/decode_0001.py +144 -0
- lif80utils/decode_E010.py +127 -0
- lif80utils/encfocal.py +41 -0
- lif80utils/encode_0001.py +105 -0
- lif80utils/focal.py +311 -0
- lif80utils/lif.py +294 -0
- lif80utils/lif80cat.py +71 -0
- lif80utils/lif80copy.py +98 -0
- lif80utils/lif80decode.py +61 -0
- lif80utils/lif80dump.py +158 -0
- lif80utils/lif80encode.py +49 -0
- lif80utils/lif80info.py +69 -0
- lif80utils/lif80init.py +164 -0
- lif80utils/lif80purge.py +50 -0
- lif80utils/lif80rename.py +51 -0
- lif80utils/lifpath.py +41 -0
- lif80utils/msus.py +111 -0
- lif80utils/source.py +247 -0
- lif80utils/ss80.py +191 -0
- lif80utils/ss80file.py +123 -0
- lif80utils/target.py +333 -0
- lif80utils-0.0.1.dist-info/METADATA +7 -0
- lif80utils-0.0.1.dist-info/RECORD +29 -0
- lif80utils-0.0.1.dist-info/WHEEL +5 -0
- lif80utils-0.0.1.dist-info/entry_points.txt +10 -0
- lif80utils-0.0.1.dist-info/licenses/LICENSE.md +651 -0
- lif80utils-0.0.1.dist-info/top_level.txt +1 -0
lif80utils/amigo.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the lif80utils distribution (https://codeberg.org/Boeingflieger/lif80utils).
|
|
3
|
+
# Copyright (c) 2026 Christian Grosz.
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
# the Free Software Foundation, version 3.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful, but
|
|
10
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12
|
+
# General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
import gpib
|
|
19
|
+
import time # sleep
|
|
20
|
+
|
|
21
|
+
class Amigo:
|
|
22
|
+
"""This class implements the amigo command set."""
|
|
23
|
+
|
|
24
|
+
# https://linux-gpib.sourceforge.io/doc_html/reference-function-ibeot.html
|
|
25
|
+
# If send_eoi is non-zero, then the EOI line will be asserted with
|
|
26
|
+
# the last byte sent by calls to ibwrt() and related functions.
|
|
27
|
+
EOT = 1
|
|
28
|
+
|
|
29
|
+
# https://linux-gpib.sourceforge.io/doc_html/reference-function-ibeos.html
|
|
30
|
+
# The least significant 8 bits of eosmode specify the eos character. You may
|
|
31
|
+
# also bitwise-or one or more of the following bits to set the eos mode:
|
|
32
|
+
REOS = 0x400 # Enable termination of reads when eos character is received.
|
|
33
|
+
XEOS = 0x800 # Assert the EOI line whenever the eos character is sent during writes.
|
|
34
|
+
BIN = 0x1000 # Match eos character using all 8 bits (instead of only looking at the 7 least significant bits).
|
|
35
|
+
|
|
36
|
+
def __init__ ( self, board, pad):
|
|
37
|
+
self.ppmask = 0x80 >> pad
|
|
38
|
+
|
|
39
|
+
self.con_00 = gpib.dev( board, pad, 0x60 + 0x00, gpib.T1s, 0) # Receive Data
|
|
40
|
+
self.con_08 = gpib.dev( board, pad, 0x60 + 0x08, gpib.T10s, self.EOT) # Unbuffered
|
|
41
|
+
self.con_09 = gpib.dev( board, pad, 0x60 + 0x09, gpib.T10s, self.EOT) # Buffered Write
|
|
42
|
+
self.con_0A = gpib.dev( board, pad, 0x60 + 0x0A, gpib.T1s, self.EOT) # Buffered Read
|
|
43
|
+
self.con_0C = gpib.dev( board, pad, 0x60 + 0x0C, gpib.T10s, self.EOT) # REQU PHY, FORMAT
|
|
44
|
+
self.con_10 = gpib.dev( board, pad, 0x60 + 0x10, gpib.T1s, self.EOT) # DSJ (Device Specified Jump)
|
|
45
|
+
self.con_1E = gpib.dev( board, pad, 0x60 + 0x1E, gpib.T1s, self.EOT) # Write/Read Loopback
|
|
46
|
+
|
|
47
|
+
self.con_1F = gpib.dev( board, pad, 0x60 + 0x1F, gpib.T1s, self.EOT, 0) # Write/Read Loopback # 10011
|
|
48
|
+
|
|
49
|
+
def __del__(self):
|
|
50
|
+
self.end( )
|
|
51
|
+
|
|
52
|
+
gpib.close( self.con_00)
|
|
53
|
+
gpib.close( self.con_08)
|
|
54
|
+
gpib.close( self.con_09)
|
|
55
|
+
gpib.close( self.con_0A)
|
|
56
|
+
gpib.close( self.con_0C)
|
|
57
|
+
gpib.close( self.con_10)
|
|
58
|
+
gpib.close( self.con_1E)
|
|
59
|
+
gpib.close( self.con_1F)
|
|
60
|
+
|
|
61
|
+
def dsj ( self):
|
|
62
|
+
return gpib.read( self.con_10, 1)
|
|
63
|
+
|
|
64
|
+
# Request Status
|
|
65
|
+
def request_status ( self, unit = 0):
|
|
66
|
+
gpib.write \
|
|
67
|
+
( self.con_08
|
|
68
|
+
, b'\x03'
|
|
69
|
+
+ unit .to_bytes( 1, 'big')
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
dsj = self.dsj( )
|
|
73
|
+
|
|
74
|
+
if b'\x00' != dsj:
|
|
75
|
+
print( f"{dsj} REQUEST STATUS")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
return gpib.read( self.con_08, 4)
|
|
79
|
+
|
|
80
|
+
# Read Self-Test
|
|
81
|
+
def read_self_test ( self):
|
|
82
|
+
return gpib.read( self.con_1F, 2)
|
|
83
|
+
|
|
84
|
+
# Write Loopback
|
|
85
|
+
def write_loopback ( self, data):
|
|
86
|
+
gpib.write( self.con_1E, data)
|
|
87
|
+
|
|
88
|
+
# Read Loopback
|
|
89
|
+
def read_loopback ( self):
|
|
90
|
+
return gpib.read( self.con_1E, 255)
|
|
91
|
+
|
|
92
|
+
# Seek
|
|
93
|
+
def seek ( self, unit = 0, cylinder = 0, head = 0, sector = 0):
|
|
94
|
+
# Seek can need more then 1s timeout.
|
|
95
|
+
gpib.write \
|
|
96
|
+
( self.con_08
|
|
97
|
+
, b'\x02'
|
|
98
|
+
+ unit .to_bytes( 1, 'big')
|
|
99
|
+
+ cylinder.to_bytes( 2, 'big')
|
|
100
|
+
+ head .to_bytes( 1, 'big')
|
|
101
|
+
+ sector .to_bytes( 1, 'big')
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
dsj = self.dsj( )
|
|
105
|
+
|
|
106
|
+
if b'\x00' != dsj:
|
|
107
|
+
print( f"{dsj} SEEK")
|
|
108
|
+
|
|
109
|
+
return dsj
|
|
110
|
+
|
|
111
|
+
# Buffered Read
|
|
112
|
+
def buffered_read ( self, unit = 0):
|
|
113
|
+
gpib.write \
|
|
114
|
+
( self.con_0A
|
|
115
|
+
, b'\x05'
|
|
116
|
+
+ unit .to_bytes( 1, 'big')
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
dsj = self.dsj( )
|
|
120
|
+
|
|
121
|
+
if b'\x00' != dsj:
|
|
122
|
+
print( f"{dsj} READ")
|
|
123
|
+
|
|
124
|
+
return dsj
|
|
125
|
+
|
|
126
|
+
#
|
|
127
|
+
def receive ( self, size = 256):
|
|
128
|
+
res = gpib.read( self.con_00, size)
|
|
129
|
+
return res
|
|
130
|
+
|
|
131
|
+
# Request (Logical) Disc Address
|
|
132
|
+
def request_logical ( self):
|
|
133
|
+
gpib.write \
|
|
134
|
+
( self.con_08
|
|
135
|
+
, b'\x14\x00'
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
dsj = self.dsj( )
|
|
139
|
+
|
|
140
|
+
if b'\x00' != dsj:
|
|
141
|
+
print( f"{dsj} REQU LOG")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
return gpib.read( self.con_08, 4)
|
|
145
|
+
|
|
146
|
+
# Request (Physical) Disc Address
|
|
147
|
+
def request_physical ( self):
|
|
148
|
+
gpib.write \
|
|
149
|
+
( self.con_0C
|
|
150
|
+
, b'\x14\x00'
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
dsj = self.dsj( )
|
|
154
|
+
|
|
155
|
+
if b'\x00' != dsj:
|
|
156
|
+
print( f"{dsj} REQU PHY")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
return gpib.read( self.con_08, 4)
|
|
160
|
+
|
|
161
|
+
# End
|
|
162
|
+
def end ( self):
|
|
163
|
+
gpib.write \
|
|
164
|
+
( self.con_08
|
|
165
|
+
, b'\x15\x00'
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
dsj = self.dsj( )
|
|
169
|
+
|
|
170
|
+
if b'\x00' != dsj:
|
|
171
|
+
print( f"{dsj} END")
|
|
172
|
+
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Buffered Write
|
|
176
|
+
def buffered_write ( self, unit = 0):
|
|
177
|
+
gpib.write \
|
|
178
|
+
( self.con_09
|
|
179
|
+
, b'\x08'
|
|
180
|
+
+ unit .to_bytes( 1, 'big')
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
dsj = self.dsj( )
|
|
184
|
+
|
|
185
|
+
if b'\x00' != dsj:
|
|
186
|
+
print( f"{dsj} WRITE")
|
|
187
|
+
|
|
188
|
+
return dsj
|
|
189
|
+
|
|
190
|
+
#
|
|
191
|
+
def send ( self, buf):
|
|
192
|
+
gpib.write \
|
|
193
|
+
( self.con_00
|
|
194
|
+
, buf
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
dsj = self.dsj( )
|
|
198
|
+
|
|
199
|
+
if b'\x00' != dsj:
|
|
200
|
+
print( f"{dsj} send")
|
|
201
|
+
|
|
202
|
+
return dsj
|
|
203
|
+
|
|
204
|
+
# Format
|
|
205
|
+
def request_format ( self, unit, option, interleave, databyte):
|
|
206
|
+
gpib.write \
|
|
207
|
+
( self.con_0C
|
|
208
|
+
, b'\x18'
|
|
209
|
+
+ unit .to_bytes( 1, 'big')
|
|
210
|
+
+ option .to_bytes( 1, 'big')
|
|
211
|
+
+ interleave.to_bytes( 1, 'big')
|
|
212
|
+
+ databyte.to_bytes( 1, 'big')
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Formatting needs around 18 sec on my 9121D and around 90 sec on my 9123D.
|
|
216
|
+
# (The manual states that the 9123D is doing verification as well)
|
|
217
|
+
time.sleep( 18)
|
|
218
|
+
|
|
219
|
+
# DSJ (QSTAT) will timeout regardless of the timeout settings.
|
|
220
|
+
# Added my own ibrpp call to python.
|
|
221
|
+
# TODO: Check if I made a mistake with timout or wait.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Execution
|
|
225
|
+
# Reporting
|
|
226
|
+
while True:
|
|
227
|
+
# ppres = gpib.parallel_poll( 0) # board
|
|
228
|
+
ppres = gpib.parallel_poll( self.con_0C) # device
|
|
229
|
+
print( f"{ppres:08b} {ppres:02x} {self.ppmask:02x}")
|
|
230
|
+
if self.ppmask & ppres:
|
|
231
|
+
break
|
|
232
|
+
time.sleep( 1)
|
|
233
|
+
|
|
234
|
+
dsj = self.dsj( )
|
|
235
|
+
|
|
236
|
+
if b'\x00' != dsj:
|
|
237
|
+
print( f"{dsj} REQUEST FORMAT")
|
|
238
|
+
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Initiate Selftest
|
|
243
|
+
def initiate_selftest ( self):
|
|
244
|
+
gpib.write \
|
|
245
|
+
( self.con_1F
|
|
246
|
+
, b'\x00' # Ignored by the 9121D/S
|
|
247
|
+
b'\x00' # 0000W000, 0 = no format test, 1 = format test
|
|
248
|
+
# b'\x08' # 0000W000, 0 = no format test, 1 = format test
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
time.sleep( 8)
|
|
252
|
+
|
|
253
|
+
# Execution
|
|
254
|
+
# Reporting
|
|
255
|
+
while True:
|
|
256
|
+
# ppres = gpib.parallel_poll( 0) # board
|
|
257
|
+
ppres = gpib.parallel_poll( self.con_1F) # device
|
|
258
|
+
print( f"{ppres:08b} {ppres:02x} {self.ppmask:02x}")
|
|
259
|
+
if self.ppmask & ppres:
|
|
260
|
+
break
|
|
261
|
+
time.sleep( 1)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
dsj = self.dsj( )
|
|
267
|
+
|
|
268
|
+
if b'\x00' != dsj:
|
|
269
|
+
print( f"{dsj} INITIATE SELFTEST")
|
|
270
|
+
|
|
271
|
+
return
|
lif80utils/amigofile.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the lif80utils distribution (https://codeberg.org/Boeingflieger/lif80utils).
|
|
3
|
+
# Copyright (c) 2026 Christian Grosz.
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
# the Free Software Foundation, version 3.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful, but
|
|
10
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12
|
+
# General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
from .amigo import Amigo
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
class AmigoFile:
|
|
22
|
+
"""This class is an [informal] interface providing needed "file" functions."""
|
|
23
|
+
|
|
24
|
+
# Actually this information should come from the volume header.
|
|
25
|
+
# But all is set to 0 there.
|
|
26
|
+
# tracks_per_surface
|
|
27
|
+
# surfaces
|
|
28
|
+
# sectors_per_track
|
|
29
|
+
SECTOR_SIZE = 256
|
|
30
|
+
|
|
31
|
+
def __init__ ( self, board, pad, unit, verbose = False):
|
|
32
|
+
self.verbose = verbose
|
|
33
|
+
|
|
34
|
+
self.amigo = Amigo( board, pad)
|
|
35
|
+
self.unit = unit
|
|
36
|
+
|
|
37
|
+
self.protocol = self.amigo
|
|
38
|
+
|
|
39
|
+
dsj = self.amigo.dsj( )
|
|
40
|
+
|
|
41
|
+
# if b'\x00' != dsj:
|
|
42
|
+
if self.verbose:
|
|
43
|
+
print( f"{dsj} HOLDOFF STATE")
|
|
44
|
+
|
|
45
|
+
# Request status
|
|
46
|
+
res = self.amigo.request_status( unit)
|
|
47
|
+
|
|
48
|
+
if self.verbose:
|
|
49
|
+
print( f"{res[0]:08b} {res[1]:08b} {res[2]:08b} {res[3]:08b}")
|
|
50
|
+
|
|
51
|
+
tttt = (res[2] & 0x1E) >> 1
|
|
52
|
+
r = (res[2] & 0x01) >> 0
|
|
53
|
+
w = (res[3] & 0x40) >> 6
|
|
54
|
+
ss = (res[3] & 0x03) >> 0
|
|
55
|
+
|
|
56
|
+
if self.verbose:
|
|
57
|
+
print( f" TTTT {tttt:04b}")
|
|
58
|
+
print( f" R {r}")
|
|
59
|
+
print( f" W {w}")
|
|
60
|
+
print( f" SS {ss:02b}")
|
|
61
|
+
|
|
62
|
+
if 0 != ss:
|
|
63
|
+
print( "Error 130 : DISC")
|
|
64
|
+
exit( -1)
|
|
65
|
+
|
|
66
|
+
# TTTT 0110
|
|
67
|
+
self.cylinders = 33
|
|
68
|
+
self.heads = 2
|
|
69
|
+
self.sectors = 16
|
|
70
|
+
|
|
71
|
+
# self.offset = self.SECTOR_SIZE
|
|
72
|
+
# self.buf = None
|
|
73
|
+
self.seek( 0)
|
|
74
|
+
|
|
75
|
+
def seek ( self, offset, whence = os.SEEK_SET):
|
|
76
|
+
# We only support SEEK_SET for now.
|
|
77
|
+
|
|
78
|
+
# Only allow sector aligned seek.
|
|
79
|
+
if 0 != offset % self.SECTOR_SIZE:
|
|
80
|
+
exit( -1)
|
|
81
|
+
|
|
82
|
+
# Invalidate buffer.
|
|
83
|
+
self.offset = self.SECTOR_SIZE
|
|
84
|
+
self.buf = None
|
|
85
|
+
|
|
86
|
+
# capacity = cylinders * heads * sectors * SECTOR_SIZE
|
|
87
|
+
# 270336 = 33 * 2 * 16 * 256
|
|
88
|
+
|
|
89
|
+
sector = offset // self.SECTOR_SIZE
|
|
90
|
+
|
|
91
|
+
cylinder = sector // self.sectors
|
|
92
|
+
cylinder = cylinder // self.heads
|
|
93
|
+
|
|
94
|
+
head = (sector % (self.sectors * self.heads)) // self.sectors
|
|
95
|
+
sector = sector % self.sectors
|
|
96
|
+
|
|
97
|
+
self.amigo.seek( self.unit, cylinder, head, sector)
|
|
98
|
+
|
|
99
|
+
return offset
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def tell ( self):
|
|
103
|
+
offset = 0
|
|
104
|
+
|
|
105
|
+
log = self.amigo.request_logical( )
|
|
106
|
+
|
|
107
|
+
cylinder = int.from_bytes( log[0:2], byteorder='big')
|
|
108
|
+
head = log[2]
|
|
109
|
+
sector = log[3]
|
|
110
|
+
|
|
111
|
+
offset += cylinder * self.sectors * self.heads
|
|
112
|
+
offset += self.sectors * head
|
|
113
|
+
offset += sector
|
|
114
|
+
|
|
115
|
+
offset *= self.SECTOR_SIZE
|
|
116
|
+
|
|
117
|
+
if self.verbose:
|
|
118
|
+
print( f"LOG C {cylinder:02} H {head} S {sector:02} {offset:06x}")
|
|
119
|
+
|
|
120
|
+
return offset
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def read ( self, size = 256):
|
|
124
|
+
res = b''
|
|
125
|
+
|
|
126
|
+
while size:
|
|
127
|
+
# Remaining bytes in buffer.
|
|
128
|
+
avail = self.SECTOR_SIZE - self.offset
|
|
129
|
+
|
|
130
|
+
# If there are none, read the next sector.
|
|
131
|
+
if 0 == avail:
|
|
132
|
+
dsj = self.amigo.buffered_read( self.unit)
|
|
133
|
+
|
|
134
|
+
if b'\x00' == dsj:
|
|
135
|
+
self.buf = self.amigo.receive( self.SECTOR_SIZE)
|
|
136
|
+
self.offset = 0
|
|
137
|
+
avail = self.SECTOR_SIZE # - self.offset
|
|
138
|
+
else:
|
|
139
|
+
return res
|
|
140
|
+
|
|
141
|
+
n = size if avail > size else avail
|
|
142
|
+
|
|
143
|
+
res = res + self.buf[self.offset:self.offset+n]
|
|
144
|
+
size -= n
|
|
145
|
+
self.offset += n
|
|
146
|
+
|
|
147
|
+
return res
|
|
148
|
+
|
|
149
|
+
def write ( self, buf):
|
|
150
|
+
size = len( buf)
|
|
151
|
+
|
|
152
|
+
for n in range( 0, size, self.SECTOR_SIZE):
|
|
153
|
+
dsj = self.amigo.buffered_write( self.unit)
|
|
154
|
+
|
|
155
|
+
if b'\x00' == dsj:
|
|
156
|
+
dsj = self.amigo.send( buf[n:n + self.SECTOR_SIZE])
|
|
157
|
+
else:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
return size
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the lif80utils distribution (https://codeberg.org/Boeingflieger/lif80utils).
|
|
3
|
+
# Copyright (c) 2026 Christian Grosz.
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
# the Free Software Foundation, version 3.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful, but
|
|
10
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12
|
+
# General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
def decode_0001 ( infile, codepage = 'focal', linenumber = True):
|
|
21
|
+
# Always ascii. Overwrite for now.
|
|
22
|
+
# codepage = 'ascii'
|
|
23
|
+
# codepage = 'cp1252'
|
|
24
|
+
# codepage = 'utf-8'
|
|
25
|
+
# codepage = 'focal'
|
|
26
|
+
|
|
27
|
+
# MAIN.. .................
|
|
28
|
+
# 4D 41 49 4E 00 00 20 1B 06 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00
|
|
29
|
+
|
|
30
|
+
# HP 85 Assembler ROM source code internal format
|
|
31
|
+
# https://groups.io/g/hpseries80/wiki/20863
|
|
32
|
+
# MAIN NUL NUL Program name
|
|
33
|
+
program_name = infile.read( 6)
|
|
34
|
+
# 20 Program type (MAIN, BASIC, ALLOCATED)
|
|
35
|
+
program_type = int.from_bytes( infile.read( 1), byteorder='little')
|
|
36
|
+
# 1B 06 Program length (061B = 1563 bytes total)
|
|
37
|
+
program_length = int.from_bytes( infile.read( 2), byteorder='little')
|
|
38
|
+
# 00... 12 decimal bytes of NUL (unused)
|
|
39
|
+
infile.read( 12)
|
|
40
|
+
# 80 Security bits
|
|
41
|
+
security_bits = int.from_bytes( infile.read( 1), byteorder='little')
|
|
42
|
+
# 00 00 No security code
|
|
43
|
+
security_code = infile.read( 2)
|
|
44
|
+
|
|
45
|
+
# print( f"{program_name} {program_type:x} {program_length} {security_bits:x} {security_code}")
|
|
46
|
+
|
|
47
|
+
program_length = program_length - 24
|
|
48
|
+
length = 24
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
# 01 00 | 1E | FE | 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A | 0E
|
|
52
|
+
# 01 00 BCD line# = 0001 = 1
|
|
53
|
+
line_number = int( infile.read( 2)[::-1].hex( ))
|
|
54
|
+
# 1E len of this line of 'code'
|
|
55
|
+
line_length = int.from_bytes( infile.read( 1), byteorder='little')
|
|
56
|
+
|
|
57
|
+
line = infile.read( line_length)
|
|
58
|
+
# print( f"{line_number} {line_length:x} {line}")
|
|
59
|
+
|
|
60
|
+
# program_length = program_length - line_length - 3
|
|
61
|
+
program_length -= line_length + 3
|
|
62
|
+
length += line_length + 3
|
|
63
|
+
|
|
64
|
+
# FE Assembler ROM's COMMENT 'token'
|
|
65
|
+
# 2A 2A... ASCII comment (line of *'s) term'd by 0E
|
|
66
|
+
# 0E octal 16, END-OF-LINE token
|
|
67
|
+
|
|
68
|
+
# 20 00 | 0C | 47 4C 4F | FF | 47 4C 4F 42 41 4C 20 | 0E
|
|
69
|
+
# FF Assembler ROM's LABEL 'token'
|
|
70
|
+
# ..GLOÿGLOBAL.
|
|
71
|
+
|
|
72
|
+
label = ""
|
|
73
|
+
comment = ""
|
|
74
|
+
opcode = ""
|
|
75
|
+
arguments = ""
|
|
76
|
+
|
|
77
|
+
if 0xFF == line[0]:
|
|
78
|
+
label = line[1:7].decode( codepage)
|
|
79
|
+
line = line[7:]
|
|
80
|
+
|
|
81
|
+
comment_pos = line.find( b'\xFE')
|
|
82
|
+
|
|
83
|
+
if 0 <= comment_pos:
|
|
84
|
+
comment = f"!{line[comment_pos+1:-1].decode( codepage)}"
|
|
85
|
+
line = line[:comment_pos] + line[-1:]
|
|
86
|
+
|
|
87
|
+
if 0 == comment_pos:
|
|
88
|
+
if linenumber:
|
|
89
|
+
yield f"{line_number} {comment}\n"
|
|
90
|
+
else:
|
|
91
|
+
yield f"{comment}\n"
|
|
92
|
+
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# print( f" {label} {line} {comment}")
|
|
96
|
+
|
|
97
|
+
if 0x0E != line[0]:
|
|
98
|
+
opcode = line[0:3].decode( codepage)
|
|
99
|
+
line = line[3:]
|
|
100
|
+
|
|
101
|
+
if 0xFF == line[0]:
|
|
102
|
+
arguments = line[1:7].decode( codepage)
|
|
103
|
+
line = line[7:]
|
|
104
|
+
elif b'=\xFF' == line[0:2]:
|
|
105
|
+
arguments = (line[0:1] + line[2:-1]).decode( codepage)
|
|
106
|
+
line = line[-1:]
|
|
107
|
+
else:
|
|
108
|
+
arguments = line[:-1].decode( codepage)
|
|
109
|
+
line = line[-1:]
|
|
110
|
+
|
|
111
|
+
if linenumber:
|
|
112
|
+
yield f"{line_number} {label:6s} {opcode:3s} {arguments:15s} {comment}\n"
|
|
113
|
+
else:
|
|
114
|
+
yield f"{label:6s} {opcode:3s} {arguments:15s} {comment}\n"
|
|
115
|
+
|
|
116
|
+
# Another 5 bytes in the file.
|
|
117
|
+
# 99 A9 02 19 0E
|
|
118
|
+
# Constant, same also for all PGM files.
|
|
119
|
+
# Rest of block filled with garbage.
|
|
120
|
+
|
|
121
|
+
# https://groups.io/g/hpseries80/wiki/1893
|
|
122
|
+
# HP 85 System ROMs [Memory Layout (and Tokens and Allocation and...)]
|
|
123
|
+
# 231 251 002 031 016 (A999 len=2 STOP EOL)
|
|
124
|
+
|
|
125
|
+
# HP LIF/assets/attachments/parse_hp85.c.txt
|
|
126
|
+
# case 0031:
|
|
127
|
+
# printf("<\\031 END>\n");
|
|
128
|
+
# break;
|
|
129
|
+
|
|
130
|
+
# This is simple the tokenized END statement from Basic.
|
|
131
|
+
# Probably it is also used by the editor to detect the end of the program.
|
|
132
|
+
|
|
133
|
+
# https://archived.hpcalc.org/greendyk/hp85/83.html
|
|
134
|
+
# The END statement should be the highest numbered statement in a program. It
|
|
135
|
+
# not only tells the computer where a program ends, but also terminates program
|
|
136
|
+
# execution. (You may also use the STOP statement; on the HP-85 both END and
|
|
137
|
+
# STOP perform exactly the same function. END is provided on the HP-85 for
|
|
138
|
+
# compatibility with other BASIC systems.)
|
|
139
|
+
|
|
140
|
+
# print( f"{program_length} {length}")
|
|
141
|
+
|
|
142
|
+
if 5 == program_length:
|
|
143
|
+
break
|
|
144
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the lif80utils distribution (https://codeberg.org/Boeingflieger/lif80utils).
|
|
3
|
+
# Copyright (c) 2026 Christian Grosz.
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
# the Free Software Foundation, version 3.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful, but
|
|
10
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12
|
+
# General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
# My notes on decoding E010 format LIF images for HP-85
|
|
19
|
+
# https://github.com/magore/hp85disk/blob/master/lif/lif-notes.txt
|
|
20
|
+
#
|
|
21
|
+
# ef [ff]* = end of data in this sector (no size) , pad with ff's optionally if there is room
|
|
22
|
+
# df size = string
|
|
23
|
+
# cf size = split accross sector end "6f size" at start of next sector
|
|
24
|
+
# but the 6f size bytes are not included in cf size! (yuck!)
|
|
25
|
+
# 6f size = split continue (always starts at sector boundry)
|
|
26
|
+
# df 00 00 ef [ff]* = EOF (df size = 0) ef send of sector and optional padding
|
|
27
|
+
#
|
|
28
|
+
# cf 29 00 (19 is to sector end) (new sector start with 6F 10 00 (10 is remainder)
|
|
29
|
+
# So 29 = 19 and 10 (yuck!)
|
|
30
|
+
|
|
31
|
+
import sys
|
|
32
|
+
import re
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
#
|
|
36
|
+
#
|
|
37
|
+
|
|
38
|
+
SECTOR_SIZE = 256
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def decode_E010 ( infile, codepage = 'focal', linenumber = True):
|
|
42
|
+
|
|
43
|
+
n = SECTOR_SIZE
|
|
44
|
+
|
|
45
|
+
while True:
|
|
46
|
+
b = infile.read( 1)
|
|
47
|
+
n -= 1
|
|
48
|
+
|
|
49
|
+
# if not b:
|
|
50
|
+
# print( "End of file")
|
|
51
|
+
# break
|
|
52
|
+
|
|
53
|
+
if b == b'\xdf': # Start of line.
|
|
54
|
+
l = infile.read( 2)
|
|
55
|
+
n -= 2
|
|
56
|
+
|
|
57
|
+
# Length of line including CR.
|
|
58
|
+
len = int.from_bytes( l, "little")
|
|
59
|
+
# print( f'DF {l} {len}')
|
|
60
|
+
|
|
61
|
+
if 0 == len:
|
|
62
|
+
# print( "End of program")
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
s = infile.read( len)
|
|
66
|
+
n -= len
|
|
67
|
+
# print( s.translate(trantab).decode( codepage), end = '')
|
|
68
|
+
if linenumber:
|
|
69
|
+
yield s.decode( codepage, 'combined')
|
|
70
|
+
else:
|
|
71
|
+
yield re.sub( r'^\d+ ', '', s.decode( codepage, 'combined'))
|
|
72
|
+
|
|
73
|
+
# yield s[:-1].decode( codepage, 'combined')
|
|
74
|
+
|
|
75
|
+
if 0 == n:
|
|
76
|
+
n = SECTOR_SIZE
|
|
77
|
+
|
|
78
|
+
elif b == b'\xcf': # Split accross sector end.
|
|
79
|
+
l = infile.read( 2)
|
|
80
|
+
n -= 2
|
|
81
|
+
|
|
82
|
+
# Length of line including CR.
|
|
83
|
+
len = int.from_bytes( l, "little")
|
|
84
|
+
# print( f'CF {l} {len}')
|
|
85
|
+
|
|
86
|
+
if len > n:
|
|
87
|
+
len = n
|
|
88
|
+
|
|
89
|
+
s = infile.read( len)
|
|
90
|
+
n -= len
|
|
91
|
+
# print( s.translate(trantab).decode( codepage), end = '')
|
|
92
|
+
# yield s.decode( codepage, 'combined')
|
|
93
|
+
n = SECTOR_SIZE
|
|
94
|
+
|
|
95
|
+
elif b == b'\x6f': # Split continue.
|
|
96
|
+
l = infile.read( 2)
|
|
97
|
+
n -= 2
|
|
98
|
+
|
|
99
|
+
# Length of line including CR.
|
|
100
|
+
len = int.from_bytes( l, "little")
|
|
101
|
+
# print( f'6F {l} {len}')
|
|
102
|
+
|
|
103
|
+
# s = infile.read( len)
|
|
104
|
+
s += infile.read( len)
|
|
105
|
+
n -= len
|
|
106
|
+
|
|
107
|
+
if linenumber:
|
|
108
|
+
yield s.decode( codepage, 'combined')
|
|
109
|
+
else:
|
|
110
|
+
yield re.sub( r'^\d+ ', '', s.decode( codepage, 'combined'))
|
|
111
|
+
|
|
112
|
+
elif b == b'\xef': # Pad sector.
|
|
113
|
+
while True:
|
|
114
|
+
b = infile.read( 1)
|
|
115
|
+
|
|
116
|
+
# TODO: Is that smart? Actually I know the size of the sector.
|
|
117
|
+
if b != b'\xff':
|
|
118
|
+
# print( f'EF {n}')
|
|
119
|
+
infile.seek(-1, 1) # goes back by one relative to current position
|
|
120
|
+
n = SECTOR_SIZE
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
n -= 1
|
|
124
|
+
else:
|
|
125
|
+
print( "Unexpected")
|
|
126
|
+
break
|
|
127
|
+
|