Tools for working with Cidco Mailstations
1#!/usr/bin/env ruby
2#
3# Copyright (c) 2019 joshua stein <jcs@jcs.org>
4#
5# Permission to use, copy, modify, and distribute this software for any
6# purpose with or without fee is hereby granted, provided that the above
7# copyright notice and this permission notice appear in all copies.
8#
9# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16#
17
18#
19# Extract an application from a Mailstation dataflash dump including its
20# icons/screens and program code.
21#
22# Usage: app_extractor.rb -d <path to dataflash> -a <app 0-4>
23#
24# Assemble output with:
25# ruby app_extractor.rb -f dataflash.bin -a 0 > app.asm
26# sdasz80 -l app.lst app.asm
27#
28# Then compare the byte listing in app.lst with the hexdump output of
29# dataflash.bin and they should be identical.
30#
31
32require "getoptlong"
33
34ORG = 0x4000
35
36opts = GetoptLong.new(
37 [ "--file", "-f", GetoptLong::REQUIRED_ARGUMENT ],
38 [ "--app", "-a", GetoptLong::REQUIRED_ARGUMENT ],
39)
40
41dataflash = nil
42app = -1
43
44opts.each do |opt,arg|
45 case opt
46 when "--file"
47 dataflash = arg
48
49 when "--app"
50 app = arg.to_i
51 end
52end
53
54if !dataflash || app < 0 || app > 4
55 usage
56end
57
58@file = File.open(dataflash, "rb")
59@file.seek(ORG * app)
60
61if (b = read(1)) != 0xc3
62 raise "expected jp, got #{b.inspect}"
63end
64
65puts "\t.module\tapp#{app}"
66puts ""
67puts "\t.area\t_DATA"
68puts "\t.area\t_HEADER (ABS)"
69puts "\t.org\t#{sprintf("0x%04x", ORG)}"
70puts ""
71
72code_base = read(2)
73puts "\tjp\t#{x code_base, 4}"
74
75icon_base = read(2)
76puts "\t.dw\t(icons)\t\t; #{x icon_base, 4}"
77puts "\t.dw\t(caption)\t; #{x read(2), 4}"
78puts "\t.dw\t(dunno)\t\t; #{x read(2), 4}"
79
80puts "dunno:"
81puts "\t.db\t##{x read(1), 2}"
82
83puts "xpos:"
84puts "\t.dw\t##{x read(2), 4}"
85puts "ypos:"
86puts "\t.dw\t##{x read(2), 4}"
87
88puts "caption:"
89puts "\t.dw\t##{x read(2), 4}"
90puts "\t.dw\t(endcap - caption - 6) ; calc caption len (##{x read(2), 4})"
91puts "\t.dw\t##{x read(2), 4}\t\t; offset to first char"
92
93print "\t.ascii\t\""
94while true do
95 z = read(1)
96 if z == 0
97 break
98 end
99
100 print z.chr
101end
102
103puts "\""
104puts "endcap:"
105
106puts ""
107puts "\t.org\t#{x icon_base, 4}"
108puts ""
109
110@file.seek((ORG * app) - ORG + icon_base)
111
112puts "icons:"
113
114icons = [ {}, {} ]
115
1162.times do |x|
117 icons[x][:size] = read(2)
118 puts "\t.dw\t##{x icons[x][:size], 4}\t\t; size icon#{x}"
119 icons[x][:pos] = read(2)
120 puts "\t.dw\t(icon#{x} - icons)\t; offset to icon#{x} (#{x icons[x][:pos], 4})"
121end
122
1232.times do |i|
124 puts "icon#{i}:"
125
126 icons[i][:width] = read(2)
127 puts "\t.dw\t##{x icons[i][:width], 4}\t\t; icon width (#{icons[i][:width]})"
128 icons[i][:height] = read(1)
129 puts "\t.db\t##{x icons[i][:height], 2}\t\t; icon height " <<
130 "(#{icons[i][:height]})"
131
132 puts ""
133
134 row = []
135 icons[i][:cols] = (icons[i][:width] / 8.0).ceil
136
137 (icons[i][:size] - 3).times do |j|
138 row.push read(1)
139
140 if row.count == icons[i][:cols]
141 # each byte is stored in memory in order, but each byte's bits are drawn
142 # on the screen right-to-left
143 puts "\t.db\t" + row.map{|z| "##{x(z, 2)}" }.join(", ") +
144 "\t; " + row.map{|z| sprintf("%08b", z).reverse }.join.
145 gsub("0", ".").gsub("1", "#")
146 row = []
147 end
148 end
149
150 puts ""
151end
152
153@file.seek((ORG * app) - ORG + code_base)
154
155while !@file.eof?
156 o = [ read(1) ]
157
158 if o[0] == 0
159 fp = @file.pos
160 if @file.read(10) == ("\0" * 10)
161 # assume a long string of nops is the end of the line
162 break
163 else
164 # put those nops back
165 @file.seek(fp)
166 end
167 end
168
169 l = oclen(o[0])
170 if l > 1
171 o += (l - 1).times.map{ read(1) }
172 end
173
174 puts oc(o)
175end
176
177BEGIN {
178 def read(l)
179 d = @file.read(l)
180 if l == 1
181 d.unpack("C*")[0]
182 elsif l == 2
183 d.unpack("v*")[0]
184 end
185 end
186
187 def x(v, l = 1)
188 sprintf("0x%0#{l}x", v)
189 end
190
191 def usage
192 puts "usage: #{$0} -f <path to dataflash.bin> -a <app number 0-4>"
193 exit 1
194 end
195
196 @opcodes = {}
197 File.open("z80_opcodes.txt") do |f|
198 while f && !f.eof?
199 oc, desc = f.gets.strip.split(/ +/, 2)
200
201 t = oc.split(" ")
202 @opcodes[t[0].to_i(16)] = {
203 :desc => desc,
204 :length => t.count,
205 :format => t,
206 }
207 end
208 end
209
210 def oclen(c)
211 return @opcodes[c][:length]
212 end
213
214 def oc(c_a)
215 # ld de, nn
216 # ld (nn), hl
217 # call nn
218 # jr z, e
219 # ld b, n
220 # rlc b
221 # ret
222
223 op = @opcodes[c_a[0]]
224
225 desc = op[:desc].split(" ").map.with_index{|c,x|
226 if c == "nn" || c == "(nn)"
227 # use two bytes of c_a
228 i = (c_a[1].chr + c_a[2].chr).unpack("v*")[0]
229
230 if op[:desc][0, 2] == "jp"
231 # jp takes an address, not a number
232 c = sprintf("0x%04x", i)
233 elsif c == "(nn)"
234 c = sprintf("(#0x%04x)", i)
235 else
236 c = sprintf("#0x%04x", i)
237 end
238
239 elsif c == "n"
240 c = sprintf("#0x%02x", c_a[1])
241
242 elsif c == "e" && op[:format][1] == "xx" # e when xx not in opcode is register e
243 if op[:desc][0, 2] == "jr"
244 c = c_a[1]
245 else
246 # calculate relative offset
247 c = sprintf("0x%04x", (@file.pos + c_a[1]))
248 end
249 end
250
251 c
252 }.join(" ")
253
254 "\t#{desc}#{desc.length < 8 ? "\t" : ""}#{desc.length < 16 ? "\t" : ""}\t;" +
255 c_a.map{|b| sprintf(" %02x", b) }.join
256 end
257}