Tools for working with Cidco Mailstations
at master 257 lines 5.6 kB view raw
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}