badline 0.17.0 → 0.18.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +153 -0
- data/CLAUDE.md +11 -7
- data/CONTRIBUTING.md +7 -1
- data/README.md +169 -20
- data/doc/pinned-behaviour.md +128 -0
- data/doc/snapshot-format.md +253 -18
- data/doc/snapshots.md +57 -23
- data/exe/badline-ruby +5 -0
- data/lib/badline/audio/terminal.rb +34 -2
- data/lib/badline/c128/keyed_state.rb +2 -0
- data/lib/badline/c128.rb +1 -0
- data/lib/badline/c64/keyed_state.rb +2 -0
- data/lib/badline/c64.rb +1 -0
- data/lib/badline/cbm2/keyed_state.rb +4 -0
- data/lib/badline/cbm2/p500.rb +5 -3
- data/lib/badline/cbm2.rb +6 -3
- data/lib/badline/chips/cia/timer.rb +2 -2
- data/lib/badline/chips/cpu/instruction_set.rb +1 -1
- data/lib/badline/chips/m6809/arithmetic.rb +140 -0
- data/lib/badline/chips/m6809/bus_steps.rb +223 -0
- data/lib/badline/chips/m6809/core.rb +149 -0
- data/lib/badline/chips/m6809/indexed.rb +82 -0
- data/lib/badline/chips/m6809/interrupts.rb +159 -0
- data/lib/badline/chips/m6809/keyed_state.rb +147 -0
- data/lib/badline/chips/m6809/opcodes.rb +159 -0
- data/lib/badline/chips/m6809/operations.rb +258 -0
- data/lib/badline/chips/m6809/plans.rb +143 -0
- data/lib/badline/chips/m6809/registers.rb +82 -0
- data/lib/badline/chips/m6809/stack.rb +140 -0
- data/lib/badline/chips/m6809.rb +13 -0
- data/lib/badline/chips/riot/timer.rb +14 -3
- data/lib/badline/chips/rriot.rb +4 -2
- data/lib/badline/chips/sid/filter.rb +4 -4
- data/lib/badline/chips/sid/waveform.rb +6 -6
- data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
- data/lib/badline/chips/vic_i.rb +84 -21
- data/lib/badline/data_path.rb +3 -3
- data/lib/badline/drives/ieee_models.rb +54 -0
- data/lib/badline/drives/model1001.rb +37 -0
- data/lib/badline/drives/model2031.rb +6 -0
- data/lib/badline/drives/model4040/disk.rb +62 -0
- data/lib/badline/drives/model4040/ip_bus.rb +46 -0
- data/lib/badline/drives/model4040/keyed_state.rb +57 -0
- data/lib/badline/drives/model4040/mechanism.rb +192 -0
- data/lib/badline/drives/model4040/parts.rb +20 -0
- data/lib/badline/drives/model4040.rb +179 -0
- data/lib/badline/drives/model8050/board.rb +77 -0
- data/lib/badline/drives/model8050/dos_board.rb +91 -0
- data/lib/badline/drives/model8050/idle.rb +11 -7
- data/lib/badline/drives/model8050/keyed_state.rb +5 -2
- data/lib/badline/drives/model8050.rb +18 -130
- data/lib/badline/drives/model9060/idle.rb +149 -0
- data/lib/badline/drives/model9060/keyed_state.rb +123 -0
- data/lib/badline/drives/model9060/parts.rb +98 -0
- data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
- data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
- data/lib/badline/drives/model9060/sasi_target.rb +161 -0
- data/lib/badline/drives/model9060.rb +195 -0
- data/lib/badline/frontend/app.rb +8 -0
- data/lib/badline/frontend/boot.rb +3 -2
- data/lib/badline/frontend/controls.rb +62 -6
- data/lib/badline/frontend/keys.rb +36 -0
- data/lib/badline/frontend/kim1_panel.rb +208 -0
- data/lib/badline/frontend/media_slots.rb +8 -1
- data/lib/badline/frontend/menu/drive_types.rb +56 -0
- data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
- data/lib/badline/frontend/menu/menu_media.rb +42 -12
- data/lib/badline/frontend/menu/menu_pages.rb +97 -2
- data/lib/badline/frontend/menu/menu_ports.rb +38 -1
- data/lib/badline/frontend/menu/pause_menu.rb +5 -1
- data/lib/badline/frontend/pet_keyboard.rb +23 -1
- data/lib/badline/frontend/screen.rb +1 -1
- data/lib/badline/frontend/screens.rb +40 -2
- data/lib/badline/frontend/teletype_pane.rb +152 -0
- data/lib/badline/frontend/timeline.rb +31 -2
- data/lib/badline/frontend.rb +3 -0
- data/lib/badline/kim1/bus.rb +117 -0
- data/lib/badline/kim1/cpu.rb +25 -0
- data/lib/badline/kim1/host_terminal.rb +74 -0
- data/lib/badline/kim1/key_entry.rb +74 -0
- data/lib/badline/kim1/keyed_modules.rb +53 -0
- data/lib/badline/kim1/keyed_state.rb +133 -0
- data/lib/badline/kim1/panel.rb +176 -0
- data/lib/badline/kim1/rriots.rb +28 -0
- data/lib/badline/kim1/sound.rb +29 -0
- data/lib/badline/kim1/tape.rb +116 -0
- data/lib/badline/kim1/teletype.rb +193 -0
- data/lib/badline/kim1.rb +190 -0
- data/lib/badline/machine/ieee_drives.rb +94 -117
- data/lib/badline/machine/keyboard_buffer.rb +20 -3
- data/lib/badline/machine/line_sound.rb +122 -0
- data/lib/badline/machine.rb +46 -15
- data/lib/badline/media/extensions.rb +8 -4
- data/lib/badline/media/kim1_media.rb +150 -0
- data/lib/badline/media/pet_media.rb +3 -1
- data/lib/badline/media/true_drive.rb +42 -45
- data/lib/badline/media.rb +12 -6
- data/lib/badline/options/family.rb +98 -8
- data/lib/badline/options/help.rb +44 -12
- data/lib/badline/options/table.rb +43 -11
- data/lib/badline/options/validation.rb +44 -8
- data/lib/badline/options.rb +27 -8
- data/lib/badline/pet/bus.rb +16 -2
- data/lib/badline/pet/keyboard_ports.rb +9 -2
- data/lib/badline/pet/keyed_state.rb +4 -0
- data/lib/badline/pet/model.rb +11 -2
- data/lib/badline/pet/rom_sets.rb +1 -0
- data/lib/badline/pet/sound.rb +5 -105
- data/lib/badline/pet/superpet/acia.rb +12 -0
- data/lib/badline/pet/superpet/board.rb +221 -0
- data/lib/badline/pet/superpet/dongle.rb +81 -0
- data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
- data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
- data/lib/badline/pet/superpet/m6809.rb +32 -0
- data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
- data/lib/badline/pet/superpet.rb +216 -0
- data/lib/badline/pet.rb +19 -3
- data/lib/badline/plus4/keyed_state.rb +2 -0
- data/lib/badline/plus4.rb +1 -0
- data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
- data/lib/badline/roms/4040/dos-1.2.rom +0 -0
- data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
- data/lib/badline/roms/4040/dos-2.1.rom +0 -0
- data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
- data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
- data/lib/badline/roms/9060/dos-3.0.rom +0 -0
- data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
- data/lib/badline/roms/README +166 -0
- data/lib/badline/roms/kim1/6530-002.rom +0 -0
- data/lib/badline/roms/kim1/6530-003.rom +0 -0
- data/lib/badline/roms/pet/characters-superpet.rom +0 -0
- data/lib/badline/roms/pet/superpet-a000.rom +0 -0
- data/lib/badline/roms/pet/superpet-c000.rom +0 -0
- data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
- data/lib/badline/roms/pet/superpet-e000.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +27 -6
- data/lib/badline/snapshot/image.rb +4 -1
- data/lib/badline/snapshot/kim1_setup.rb +28 -0
- data/lib/badline/snapshot/pet_setup.rb +27 -11
- data/lib/badline/snapshot/storage_setup.rb +4 -1
- data/lib/badline/snapshot/vic20_setup.rb +11 -7
- data/lib/badline/snapshot/vice/pet.rb +13 -7
- data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
- data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
- data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
- data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
- data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
- data/lib/badline/snapshot.rb +4 -0
- data/lib/badline/status.rb +30 -30
- data/lib/badline/storage/d67_image.rb +55 -0
- data/lib/badline/storage/d90_image.rb +180 -0
- data/lib/badline/storage/disk_image/writing.rb +2 -2
- data/lib/badline/storage/kim_tape.rb +254 -0
- data/lib/badline/storage/papertape.rb +90 -0
- data/lib/badline/storage.rb +4 -0
- data/lib/badline/support/kim1_checkpoint.rb +41 -0
- data/lib/badline/support/superpet_checkpoint.rb +35 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/keyed_modules.rb +6 -3
- data/lib/badline/vic20/keyed_state.rb +6 -4
- data/lib/badline/vic20.rb +26 -12
- data/lib/badline.rb +1 -0
- metadata +77 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model9060
|
|
6
|
+
# The commands the DOS board's SASI ROM (300515) sends, against the
|
|
7
|
+
# disk's blocks. A command's second to fourth bytes hold the drive
|
|
8
|
+
# (bits 5-7) and the block's LBA, its fifth the blocks to move.
|
|
9
|
+
#
|
|
10
|
+
# | Op | Command | Data |
|
|
11
|
+
# | --- | --- | --- |
|
|
12
|
+
# | $00 | Test Unit Ready | |
|
|
13
|
+
# | $01 | Recalibrate: seek to cylinder 0 | |
|
|
14
|
+
# | $03 | Request Sense: the last command's error and its LBA | 4 in |
|
|
15
|
+
# | $04 | Format Unit | |
|
|
16
|
+
# | $08 | Read | 256 a block in |
|
|
17
|
+
# | $0A | Write | 256 a block out |
|
|
18
|
+
# | $0B | Seek | |
|
|
19
|
+
# | $0C | Vendor: read the controller's block buffer (unverified) | 256 in |
|
|
20
|
+
# | $0E | Vendor: write the controller's block buffer (unverified) | 256 out |
|
|
21
|
+
# | $E3 | Vendor: four zero bytes from a drive that is there (unverified) | 4 in |
|
|
22
|
+
# | $C0 | Vendor: set the geometry (unverified) | |
|
|
23
|
+
#
|
|
24
|
+
# DOS 3.0 sends $E3 before it first reads a drive, and leaves a drive
|
|
25
|
+
# alone that fails it as DRIVE NOT READY, as drive 1 does here.
|
|
26
|
+
#
|
|
27
|
+
# $C0's fifth and sixth bytes are taken as the last head and the last
|
|
28
|
+
# sector of a head, as the ROM sends them (5 and 31 on a D9090 at
|
|
29
|
+
# boot). Its other bytes aren't used.
|
|
30
|
+
#
|
|
31
|
+
# An error's sense byte has its class in bits 4-5 and its code in bits
|
|
32
|
+
# 0-3, with bit 7 set when the LBA after it names the block. The ROM
|
|
33
|
+
# reads that byte from the message phase after a check condition,
|
|
34
|
+
# and the controller sends it there.
|
|
35
|
+
#
|
|
36
|
+
# Format Unit erases the disk at once and puts the controller in a
|
|
37
|
+
# fast mode, where Read, Write and Seek take no seek or rotation time,
|
|
38
|
+
# for the DOS's passes over the whole disk. Any other command ends
|
|
39
|
+
# it. Outside it a seek takes 3 ms a cylinder and 15 ms to settle, and
|
|
40
|
+
# the disk turns 32 blocks in 16.7 ms.
|
|
41
|
+
module SASICommands
|
|
42
|
+
DRIVE_NOT_READY = 0x04
|
|
43
|
+
WRITE_FAULT = 0x03
|
|
44
|
+
INVALID_COMMAND = 0x20
|
|
45
|
+
ILLEGAL_ADDRESS = 0xa1
|
|
46
|
+
|
|
47
|
+
BLOCK = 256
|
|
48
|
+
SECTOR_CYCLES = 521
|
|
49
|
+
STEP_CYCLES = 3_000
|
|
50
|
+
SETTLE_CYCLES = 15_000
|
|
51
|
+
|
|
52
|
+
DISK_COMMANDS = [0x00, 0x01, 0x04, 0x08, 0x0a, 0x0b, 0xe3].freeze
|
|
53
|
+
FAST_COMMANDS = [0x08, 0x0a, 0x0b].freeze
|
|
54
|
+
|
|
55
|
+
attr_reader :heads, :sectors, :cylinder
|
|
56
|
+
|
|
57
|
+
def fast? = @fast
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def reset_geometry
|
|
62
|
+
@heads = 4
|
|
63
|
+
@sectors = 32
|
|
64
|
+
@block_buffer = Array.new(BLOCK, 0)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def execute
|
|
68
|
+
@fast = false unless FAST_COMMANDS.include?(@command[0])
|
|
69
|
+
case @command[0]
|
|
70
|
+
when 0x03 then enter(SASITarget::DATA_IN, @last_sense.dup, 1)
|
|
71
|
+
when 0x0c then enter(SASITarget::DATA_IN, @block_buffer.dup, 1)
|
|
72
|
+
when 0x0e then enter(SASITarget::DATA_OUT, Array.new(BLOCK, 0), 1)
|
|
73
|
+
when 0xc0 then set_geometry
|
|
74
|
+
else disk_command
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def disk_command
|
|
79
|
+
return refuse(INVALID_COMMAND) unless DISK_COMMANDS.include?(@command[0])
|
|
80
|
+
return refuse(DRIVE_NOT_READY) unless @disk && (@command[1] >> 5).zero?
|
|
81
|
+
|
|
82
|
+
case @command[0]
|
|
83
|
+
when 0x00 then report
|
|
84
|
+
when 0x01 then report(travel(0))
|
|
85
|
+
when 0x04 then format_unit
|
|
86
|
+
when 0xe3 then enter(SASITarget::DATA_IN, [0, 0, 0, 0], 1)
|
|
87
|
+
else block_command
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def block_command
|
|
92
|
+
return refuse(ILLEGAL_ADDRESS) unless @disk.blocks?(lba, @command[0] == 0x0b ? 1 : count)
|
|
93
|
+
|
|
94
|
+
case @command[0]
|
|
95
|
+
when 0x08 then read
|
|
96
|
+
when 0x0a then enter(SASITarget::DATA_OUT, Array.new(count * BLOCK, 0), 1)
|
|
97
|
+
else report(travel(cylinder_of(lba)))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def lba = ((@command[1] & 0x1f) << 16) | (@command[2] << 8) | @command[3]
|
|
102
|
+
|
|
103
|
+
def count = @command[4].zero? ? 256 : @command[4]
|
|
104
|
+
|
|
105
|
+
def cylinder_of(block) = block / (@heads * @sectors)
|
|
106
|
+
|
|
107
|
+
def set_geometry
|
|
108
|
+
@heads = @command[4] + 1
|
|
109
|
+
@sectors = @command[5] + 1
|
|
110
|
+
report
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def read
|
|
114
|
+
bytes = @disk.read_blocks(lba, count)
|
|
115
|
+
@block_buffer = bytes.last(BLOCK)
|
|
116
|
+
enter(SASITarget::DATA_IN, bytes, access_cycles)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def data_received
|
|
120
|
+
if @command[0] == 0x0e
|
|
121
|
+
@block_buffer = @buffer.dup
|
|
122
|
+
report
|
|
123
|
+
else
|
|
124
|
+
@writing = true
|
|
125
|
+
@delay = access_cycles
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def store_buffer
|
|
130
|
+
@disk.write_blocks(lba, @buffer)
|
|
131
|
+
@block_buffer = @buffer.last(BLOCK)
|
|
132
|
+
report
|
|
133
|
+
rescue Storage::WriteError
|
|
134
|
+
refuse(WRITE_FAULT)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def format_unit
|
|
138
|
+
blocks = @heads * @sectors
|
|
139
|
+
(0...@disk.block_count).step(blocks) do |first|
|
|
140
|
+
@disk.write_blocks(first, Array.new([blocks, @disk.block_count - first].min * BLOCK, 0))
|
|
141
|
+
end
|
|
142
|
+
@fast = true
|
|
143
|
+
report
|
|
144
|
+
rescue Storage::WriteError
|
|
145
|
+
refuse(WRITE_FAULT)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The seek to the block's cylinder, the turn until the block comes
|
|
149
|
+
# under the head, and the blocks passing under it.
|
|
150
|
+
def access_cycles
|
|
151
|
+
return 1 if @fast
|
|
152
|
+
|
|
153
|
+
seek = travel(cylinder_of(lba))
|
|
154
|
+
slot = ((@cycle + seek) / SECTOR_CYCLES) % @sectors
|
|
155
|
+
seek + ((((lba % @sectors) - slot) % @sectors) * SECTOR_CYCLES) + (count * SECTOR_CYCLES)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def travel(target)
|
|
159
|
+
steps = (target - @cylinder).abs
|
|
160
|
+
@cylinder = target
|
|
161
|
+
return 1 if @fast || steps.zero?
|
|
162
|
+
|
|
163
|
+
(steps * STEP_CYCLES) + SETTLE_CYCLES
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drives/model9060/sasi_commands"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
module Drives
|
|
7
|
+
class Model9060
|
|
8
|
+
# The D9060's and D9090's hard disk controller (Tandon 188008) as the
|
|
9
|
+
# DOS board's SASI host sees it, at the level of the bus's lines.
|
|
10
|
+
#
|
|
11
|
+
# The host selects it by raising SEL with bit 0 on the data bus, and
|
|
12
|
+
# it answers with BSY. It then runs one command through the bus's
|
|
13
|
+
# phases, which C/D, I/O and MSG name: six command bytes from the
|
|
14
|
+
# host, the data in or out, a status byte and a message byte, after
|
|
15
|
+
# which it lets go of BSY. Each byte waits on REQ, which the host
|
|
16
|
+
# answers with ACK, and REQ rises again a cycle later for the next.
|
|
17
|
+
#
|
|
18
|
+
# The controller's work between the phases (a seek, the turn to the
|
|
19
|
+
# block, the block under the head) delays the next REQ. `clock`
|
|
20
|
+
# counts that time out, one cycle of the DOS board's 1 MHz at a time,
|
|
21
|
+
# and `quiet?` says when there is none left to count. SASICommands
|
|
22
|
+
# runs the commands against the disk, a Storage::D90Image, or nil
|
|
23
|
+
# with none inserted.
|
|
24
|
+
class SASITarget
|
|
25
|
+
include SASICommands
|
|
26
|
+
|
|
27
|
+
BUS_FREE = 0
|
|
28
|
+
COMMAND = 1
|
|
29
|
+
DATA_IN = 2
|
|
30
|
+
DATA_OUT = 3
|
|
31
|
+
STATUS = 4
|
|
32
|
+
MESSAGE = 5
|
|
33
|
+
|
|
34
|
+
COMMAND_LENGTH = 6
|
|
35
|
+
|
|
36
|
+
attr_accessor :disk
|
|
37
|
+
attr_reader :phase
|
|
38
|
+
|
|
39
|
+
def initialize(disk = nil)
|
|
40
|
+
@disk = disk
|
|
41
|
+
@cycle = 0
|
|
42
|
+
@cylinder = 0
|
|
43
|
+
reset_geometry
|
|
44
|
+
reset
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# RST: the controller drops whatever it was doing and frees the bus.
|
|
48
|
+
def reset
|
|
49
|
+
@phase = BUS_FREE
|
|
50
|
+
@request = false
|
|
51
|
+
@delay = 0
|
|
52
|
+
@command = []
|
|
53
|
+
@buffer = []
|
|
54
|
+
@index = 0
|
|
55
|
+
@writing = false
|
|
56
|
+
@sense = [0, 0, 0, 0]
|
|
57
|
+
@last_sense = @sense
|
|
58
|
+
@fast = false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def busy? = @phase != BUS_FREE
|
|
62
|
+
|
|
63
|
+
def request? = @request
|
|
64
|
+
|
|
65
|
+
# C/D: a command, status or message byte rather than data.
|
|
66
|
+
def control? = [COMMAND, STATUS, MESSAGE].include?(@phase)
|
|
67
|
+
|
|
68
|
+
# I/O: a byte from the controller to the host.
|
|
69
|
+
def input? = [DATA_IN, STATUS, MESSAGE].include?(@phase)
|
|
70
|
+
|
|
71
|
+
def message? = @phase == MESSAGE
|
|
72
|
+
|
|
73
|
+
def quiet? = @delay.zero?
|
|
74
|
+
|
|
75
|
+
# The byte the controller puts on the data bus, while I/O is set.
|
|
76
|
+
def data = input? ? @buffer[@index] || 0 : 0
|
|
77
|
+
|
|
78
|
+
# SEL with the host's data bus. The controller answers to bit 0
|
|
79
|
+
# while the bus is free.
|
|
80
|
+
def select(bus)
|
|
81
|
+
return if busy? || bus.nobits?(0x01)
|
|
82
|
+
|
|
83
|
+
enter(COMMAND, Array.new(COMMAND_LENGTH, 0), 1)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ACK for the byte REQ asked for. The host's data bus carries a
|
|
87
|
+
# byte to the controller in the command and data out phases, and
|
|
88
|
+
# is ignored in the others.
|
|
89
|
+
def acknowledge(bus)
|
|
90
|
+
return unless @request
|
|
91
|
+
|
|
92
|
+
@request = false
|
|
93
|
+
@buffer[@index] = bus if @phase == COMMAND || @phase == DATA_OUT
|
|
94
|
+
@index += 1
|
|
95
|
+
@index < @buffer.length ? @delay = 1 : finish_phase
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Runs the clock on through cycles the controller has nothing to
|
|
99
|
+
# count down in.
|
|
100
|
+
def fast_forward(cycles)
|
|
101
|
+
@cycle += cycles
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def clock
|
|
105
|
+
@cycle += 1
|
|
106
|
+
return if @delay.zero?
|
|
107
|
+
|
|
108
|
+
@delay -= 1
|
|
109
|
+
arrive if @delay.zero?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def enter(phase, bytes, delay)
|
|
115
|
+
@phase = phase
|
|
116
|
+
@buffer = bytes
|
|
117
|
+
@index = 0
|
|
118
|
+
@request = false
|
|
119
|
+
@delay = [delay, 1].max
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def arrive
|
|
123
|
+
if @writing
|
|
124
|
+
@writing = false
|
|
125
|
+
store_buffer
|
|
126
|
+
else
|
|
127
|
+
@request = true
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def finish_phase
|
|
132
|
+
case @phase
|
|
133
|
+
when COMMAND then start(@buffer.dup)
|
|
134
|
+
when DATA_IN then report
|
|
135
|
+
when DATA_OUT then data_received
|
|
136
|
+
when STATUS then enter(MESSAGE, [@sense[0]], 1)
|
|
137
|
+
else @phase = BUS_FREE
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def start(command)
|
|
142
|
+
@command = command
|
|
143
|
+
@last_sense = @sense
|
|
144
|
+
@sense = [0, 0, 0, 0]
|
|
145
|
+
execute
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The status byte: 0 for a command that went well, $02 (check
|
|
149
|
+
# condition) for one that failed, whose error Request Sense reads.
|
|
150
|
+
def report(delay = 1)
|
|
151
|
+
enter(STATUS, [@sense[0].positive? ? 0x02 : 0x00], delay)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def refuse(error)
|
|
155
|
+
@sense = [error, (@command[1] & 0xe0) | ((lba >> 16) & 0x1f), (lba >> 8) & 0xff, lba & 0xff]
|
|
156
|
+
report
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drives/model8050"
|
|
4
|
+
require "badline/drives/model9060/sasi_target"
|
|
5
|
+
require "badline/drives/model9060/sasi_bus"
|
|
6
|
+
require "badline/drives/model9060/parts"
|
|
7
|
+
require "badline/drives/model9060/idle"
|
|
8
|
+
require "badline/drives/model9060/keyed_state"
|
|
9
|
+
|
|
10
|
+
module Badline
|
|
11
|
+
module Drives
|
|
12
|
+
# The D9060 hard disk of the PET and CBM-II, one 5 MB drive on an
|
|
13
|
+
# IEEE-488 unit (technical manual PN 990441; DOS board 300010).
|
|
14
|
+
#
|
|
15
|
+
# The DOS board is the 8050's with the floppy half given over to a SASI
|
|
16
|
+
# host. The interface processor (IP) runs DOS 3.0 from 16 KB of ROM at
|
|
17
|
+
# $C000, with the 8050's two 6532s on the IEEE-488 bus (Model8050's
|
|
18
|
+
# IPBus and IEEEPort). The SASI host, a second 6502 in the floppy
|
|
19
|
+
# controller's place, runs 300515 from 2 KB at $F800, with a 6810 for
|
|
20
|
+
# its zero page and stack and a VIA facing the hard disk's controller
|
|
21
|
+
# (SASIBus, SASILines, SASITarget). The two meet in the 8050's 4 KB of
|
|
22
|
+
# common RAM, where the IP leaves jobs and the host turns each into a
|
|
23
|
+
# SASI command.
|
|
24
|
+
#
|
|
25
|
+
# Reset starts both. The host waits on the common RAM for the IP, which
|
|
26
|
+
# tests it and answers, and the DOS settles into its idle loop with the
|
|
27
|
+
# host polling the job queue.
|
|
28
|
+
#
|
|
29
|
+
# The interface processor's side of the board and the clocking against
|
|
30
|
+
# the host are the 8050's (Model8050::DOSBoard), and the unit sleeps
|
|
31
|
+
# through both processors' idle loops (Idle).
|
|
32
|
+
#
|
|
33
|
+
# N runs as the DOS runs it, but for the six passes that write and
|
|
34
|
+
# read back test patterns over every block, an hour of the hardware's
|
|
35
|
+
# time: once Format Unit has erased the disk, the IP goes from the first
|
|
36
|
+
# pass straight on to where they end, as if every block had passed.
|
|
37
|
+
# The DOS then writes the bad-block list, the BAM, the header and the
|
|
38
|
+
# directory as c1541 formats a .d90, byte for byte.
|
|
39
|
+
class Model9060
|
|
40
|
+
include Model8050::DOSBoard
|
|
41
|
+
include Idle
|
|
42
|
+
|
|
43
|
+
CLOCK_HZ = 1_000_000
|
|
44
|
+
|
|
45
|
+
# The host cycles by which a unit switched on with the machine has
|
|
46
|
+
# run its RAM tests and the host's start-up, and reads its jumpers.
|
|
47
|
+
BOOT_CYCLES = 1_500_000
|
|
48
|
+
|
|
49
|
+
# DOS 3.0's format: the first of its pattern passes (FORMAT_PASSES),
|
|
50
|
+
# where the pass number ($2D) is 0, and the code after the last
|
|
51
|
+
# (FORMAT_DONE), with the pass number at 6.
|
|
52
|
+
FORMAT_PASSES = 0xcb02
|
|
53
|
+
FORMAT_DONE = 0xcc15
|
|
54
|
+
PASS = 0x2d
|
|
55
|
+
|
|
56
|
+
def sasi_cpu = settled(@sasi_cpu)
|
|
57
|
+
|
|
58
|
+
def sasi_bus = settled(@sasi_bus)
|
|
59
|
+
|
|
60
|
+
def target = settled(@target)
|
|
61
|
+
|
|
62
|
+
def model_name = "9060"
|
|
63
|
+
|
|
64
|
+
# The heads the drive has, which J14 tells the DOS.
|
|
65
|
+
def heads = 4
|
|
66
|
+
|
|
67
|
+
def boot_cycles = BOOT_CYCLES
|
|
68
|
+
|
|
69
|
+
# Whether the unit is where VICE saves it: both processors between
|
|
70
|
+
# instructions, the host at the start of its job loop.
|
|
71
|
+
def settled_for_vice?
|
|
72
|
+
host = sasi_cpu
|
|
73
|
+
ip_cpu.boundary? && host.boundary? && host.program_counter == Idle::SASI_LOOP
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# +dos_rom+ covers $C000-$FFFF and +sasi_rom+ $F800-$FFFF of the
|
|
77
|
+
# host's, each defaulting to the image in the ROM path. +device+ is
|
|
78
|
+
# the number the jumpers set, 8 to 15. Only the default DOS has its
|
|
79
|
+
# pattern passes skipped.
|
|
80
|
+
def initialize(dos_rom: nil, sasi_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
|
|
81
|
+
@device = device
|
|
82
|
+
@host_clock_hz = host_clock_hz
|
|
83
|
+
@phase = 0
|
|
84
|
+
@cycles = 0
|
|
85
|
+
@format_trap = dos_rom.nil?
|
|
86
|
+
build_ip(dos_rom || ROM.load("9060/dos-3.0.rom", 0xc000))
|
|
87
|
+
build_sasi(sasi_rom || ROM.read("9060/sasi-300515-revb.rom"))
|
|
88
|
+
@ip_cpu = Model8050::IPCPU.new(@ip_bus)
|
|
89
|
+
@sasi_cpu = SASICPU.new(@sasi_bus)
|
|
90
|
+
init_idle
|
|
91
|
+
@ieee_bus.attach(self)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Whether the activity LED is lit: of the LEDs, bit 1 is the
|
|
95
|
+
# activity LED and bit 2 the status LED.
|
|
96
|
+
def led_on? = leds.anybits?(0x02)
|
|
97
|
+
|
|
98
|
+
# Puts the Storage::D90Image in the drive. Nil takes it out.
|
|
99
|
+
def insert(image)
|
|
100
|
+
settle!
|
|
101
|
+
@target.disk = image
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Puts in the .d90 image at +path+, write-protected with `read_only`.
|
|
105
|
+
# An image of neither drive's size takes this drive's heads.
|
|
106
|
+
def insert_image(path, read_only: false)
|
|
107
|
+
image_heads = Storage::D90Image.heads_for(File.size(path)) || heads
|
|
108
|
+
insert(Storage::D90Image.new(path, read_only:, heads: image_heads))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The image in the drive, or nil.
|
|
112
|
+
def disk = @target.disk
|
|
113
|
+
|
|
114
|
+
# Writes reach the image as each Write ends, so there is nothing to
|
|
115
|
+
# store.
|
|
116
|
+
def flush = nil
|
|
117
|
+
|
|
118
|
+
# The bus's IFC line resets both processors and every chip. RAM keeps
|
|
119
|
+
# its contents, and the hard disk's controller runs on.
|
|
120
|
+
def reset!
|
|
121
|
+
settle!
|
|
122
|
+
@data_riot.reset!
|
|
123
|
+
@control_riot.reset!
|
|
124
|
+
@via.reset!
|
|
125
|
+
@ip_cpu.reset!
|
|
126
|
+
@sasi_cpu.reset!
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def inspect
|
|
130
|
+
"#<#{self.class.name} cycles=#{@cycles} ip=(#{@ip_cpu.inspect}) sasi=(#{@sasi_cpu.inspect})>"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def run_cycle
|
|
136
|
+
step
|
|
137
|
+
skip_format_passes if @format_trap && @target.fast?
|
|
138
|
+
watch_loops if @idle_skip
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def build_ip(rom)
|
|
142
|
+
@common = Memory.new([], length: 0x1000)
|
|
143
|
+
@ieee_bus = IEEEBus.new
|
|
144
|
+
@ieee_port = Model8050::IEEEPort.new(device: @device, bus: @ieee_bus)
|
|
145
|
+
@data_riot = Model8050::DataRIOT.new(peripheral: Model8050::DataLines.new(@ieee_port))
|
|
146
|
+
@control_riot = Model8050::ControlRIOT.new(peripheral: Model8050::ControlLines.new(@ieee_port))
|
|
147
|
+
@ieee_port.data_riot = @data_riot
|
|
148
|
+
@ieee_port.control_riot = @control_riot
|
|
149
|
+
@ip_bus = Model8050::IPBus.new(rom:, common: @common, data_riot: @data_riot, control_riot: @control_riot)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def build_sasi(rom)
|
|
153
|
+
@target = SASITarget.new
|
|
154
|
+
@lines = SASILines.new(@target, four_heads: heads == 4)
|
|
155
|
+
@via = SASIVIA.new(start: 0x0080, peripheral: @lines)
|
|
156
|
+
@lines.via = @via
|
|
157
|
+
@sasi_bus = SASIBus.new(rom: ROM.new(rom, length: rom.length), common: @common, via: @via)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# One cycle of the board: the controller and the chips, then the IP
|
|
161
|
+
# and the host, each in its own half of the cycle. ATN reaches UE1's
|
|
162
|
+
# PA7 as the port latched it. UE1 pulls the IP's IRQ and the VIA the
|
|
163
|
+
# host's.
|
|
164
|
+
def step
|
|
165
|
+
@control_riot.pa7 = @ieee_port.atn_low?
|
|
166
|
+
@data_riot.cycle!
|
|
167
|
+
@control_riot.cycle!
|
|
168
|
+
@via.cycle!
|
|
169
|
+
@lines.cycle!
|
|
170
|
+
@ip_cpu.irq = @control_riot.irq?
|
|
171
|
+
@ip_cpu.cycle!
|
|
172
|
+
@sasi_cpu.irq = @via.irq?
|
|
173
|
+
@sasi_cpu.cycle!
|
|
174
|
+
@cycles += 1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Sends the IP from the first pattern pass on to the end of the last.
|
|
178
|
+
def skip_format_passes
|
|
179
|
+
ip = @ip_cpu
|
|
180
|
+
return unless ip.program_counter == FORMAT_PASSES && ip.boundary? && @ip_bus.peek(PASS).zero?
|
|
181
|
+
|
|
182
|
+
@ip_bus.poke(PASS, 6)
|
|
183
|
+
ip.program_counter = FORMAT_DONE
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The D9090: the D9060's board on a six-head drive, 7.5 MB, with J14
|
|
188
|
+
# telling the DOS so.
|
|
189
|
+
class Model9090 < Model9060
|
|
190
|
+
def model_name = "9090"
|
|
191
|
+
|
|
192
|
+
def heads = 6
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
data/lib/badline/frontend/app.rb
CHANGED
|
@@ -105,6 +105,7 @@ module Badline
|
|
|
105
105
|
@controls.mouse_motion(SDL.event_xrel(SDL.event), SDL.event_yrel(SDL.event))
|
|
106
106
|
when SDL::MOUSEBUTTONDOWN, SDL::MOUSEBUTTONUP
|
|
107
107
|
@controls.mouse_button(SDL.event_button(SDL.event), type == SDL::MOUSEBUTTONDOWN)
|
|
108
|
+
click_panel(type == SDL::MOUSEBUTTONDOWN) if @computer.family == :kim1 && !@computer.tty?
|
|
108
109
|
when SDL::CONTROLLERDEVICEADDED, SDL::CONTROLLERDEVICEREMOVED
|
|
109
110
|
@gamepads.rescan
|
|
110
111
|
when SDL::DROPFILE then resume(false) unless @menu.drop(@computer, @controls, @sound)
|
|
@@ -124,6 +125,13 @@ module Badline
|
|
|
124
125
|
end
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
# A click on the KIM-1's panel presses the key under the pointer, and
|
|
129
|
+
# its release lets go.
|
|
130
|
+
def click_panel(down)
|
|
131
|
+
event = SDL.event
|
|
132
|
+
@controls.panel_click(@screens.kim1_panel.key_at(SDL.event_x(event), SDL.event_y(event)), down)
|
|
133
|
+
end
|
|
134
|
+
|
|
127
135
|
def toggle?(scancode)
|
|
128
136
|
[Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
|
|
129
137
|
end
|
|
@@ -39,8 +39,9 @@ module Badline
|
|
|
39
39
|
""
|
|
40
40
|
rescue Machine::Unsupported, Media::TrueDrive::Error, Media::DiskList::Error, Media::NotDisk, Drives::Datasette::Missing,
|
|
41
41
|
SystemCallError, SIDTune::File::FormatError, Storage::T64::FormatError, Storage::TAP::FormatError,
|
|
42
|
+
Storage::KIMTape::FormatError,
|
|
42
43
|
Storage::CRTFile::FormatError, Storage::G64Image::FormatError, C64::Cartridge::UnsupportedTypeError,
|
|
43
|
-
Snapshot::FormatError => e
|
|
44
|
+
Storage::Papertape::FormatError, Snapshot::FormatError => e
|
|
44
45
|
e.message
|
|
45
46
|
end
|
|
46
47
|
|
|
@@ -83,7 +84,7 @@ module Badline
|
|
|
83
84
|
options.cartridges.each { |slot, path| puts Media.attach(computer, path, cartridge: { slot: }) }
|
|
84
85
|
unless media.nil?
|
|
85
86
|
puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
|
|
86
|
-
disk: { read_only: !writable })
|
|
87
|
+
disk: { read_only: !writable }, kim1: Media::KIM1Media.loading(options))
|
|
87
88
|
end
|
|
88
89
|
computer
|
|
89
90
|
end
|
|
@@ -10,7 +10,8 @@ module Badline
|
|
|
10
10
|
# #swap_ports swaps them. The VIC-20's one joystick takes both. The
|
|
11
11
|
# PET's and the CBM-II's keys go by the character they type
|
|
12
12
|
# (PETKeyboard), and the PET and the CBM-II's B series have no
|
|
13
|
-
# joystick.
|
|
13
|
+
# joystick. The KIM-1's host keys press its panel's (Keys::KIM1_KEYS),
|
|
14
|
+
# and so does a click on one the window draws.
|
|
14
15
|
# - A pot device the pause menu plugs into a control port of a C64, a
|
|
15
16
|
# C128 or a P500: a 1351 mouse or a pair of paddles, which the host
|
|
16
17
|
# mouse drives. Motion
|
|
@@ -30,7 +31,7 @@ module Badline
|
|
|
30
31
|
# B series.
|
|
31
32
|
def self.joysticks?(computer)
|
|
32
33
|
family = computer.family
|
|
33
|
-
family != :pet && (family != :cbm2 || p500?(computer))
|
|
34
|
+
family != :pet && family != :kim1 && (family != :cbm2 || p500?(computer))
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# Whether the machine is the CBM-II's P500, which has the B series'
|
|
@@ -49,6 +50,8 @@ module Badline
|
|
|
49
50
|
@mouse = Input::Mouse1351.new
|
|
50
51
|
@paddles = Input::Paddles.new
|
|
51
52
|
@held = {}
|
|
53
|
+
@clicked = ""
|
|
54
|
+
@shifts = 0
|
|
52
55
|
@pet_keyboard = pet_keyboard(computer)
|
|
53
56
|
end
|
|
54
57
|
|
|
@@ -127,6 +130,18 @@ module Badline
|
|
|
127
130
|
paddles? ? @paddles.move(delta_x, delta_y) : @mouse.move(delta_x, delta_y)
|
|
128
131
|
end
|
|
129
132
|
|
|
133
|
+
# Presses the KIM-1 key KIM1Panel names `name` for a click on it, or
|
|
134
|
+
# lets go of the key the last click pressed.
|
|
135
|
+
def panel_click(name, down)
|
|
136
|
+
if down
|
|
137
|
+
@clicked = name
|
|
138
|
+
kim1_key(name, true) unless name.empty?
|
|
139
|
+
else
|
|
140
|
+
kim1_key(@clicked, false) unless @clicked.empty?
|
|
141
|
+
@clicked = ""
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
130
145
|
def mouse_button(button, down)
|
|
131
146
|
name = MOUSE_BUTTONS[button]
|
|
132
147
|
return unless name && pot_device?
|
|
@@ -143,7 +158,11 @@ module Badline
|
|
|
143
158
|
# A host key lets go of the key it pressed, though the C128 has
|
|
144
159
|
# switched between C64 and C128 mode since and maps it elsewhere.
|
|
145
160
|
def machine_key(scancode, down)
|
|
146
|
-
|
|
161
|
+
if Controls.symbolic?(@computer)
|
|
162
|
+
@pet_keyboard.positional = @computer.family == :pet && @computer.apl_characters? if down
|
|
163
|
+
return @pet_keyboard.key(scancode, down)
|
|
164
|
+
end
|
|
165
|
+
return kim1_input(scancode, down) if @computer.family == :kim1
|
|
147
166
|
|
|
148
167
|
@held[scancode] = host_key(scancode) if down || !@held.key?(scancode)
|
|
149
168
|
key = @held[scancode]
|
|
@@ -181,12 +200,45 @@ module Badline
|
|
|
181
200
|
end
|
|
182
201
|
end
|
|
183
202
|
|
|
203
|
+
# The keys type on the teletype with the KIM-1's jumper fitted, and
|
|
204
|
+
# press the panel's keys without it.
|
|
205
|
+
def kim1_input(scancode, down)
|
|
206
|
+
return kim1_key(Keys.kim1_key(scancode), down) unless @computer.tty?
|
|
207
|
+
return shift(down) if [225, 229].include?(scancode)
|
|
208
|
+
|
|
209
|
+
text = Keys.kim1_teletype(scancode, @shifts.positive?)
|
|
210
|
+
@computer.teletype.type(text) if down && !text.empty?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def shift(down)
|
|
214
|
+
@shifts += down ? 1 : -1
|
|
215
|
+
@shifts = 0 if @shifts.negative?
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# ST pulls NMI and RS resets as they go down, and T flips the SST
|
|
219
|
+
# switch. The other keys stay down until let go.
|
|
220
|
+
def kim1_key(name, down)
|
|
221
|
+
case name
|
|
222
|
+
when "" then nil
|
|
223
|
+
when "ST" then @computer.press_stop if down
|
|
224
|
+
when "RS" then @computer.press_reset if down
|
|
225
|
+
when "SST" then @computer.sst = !@computer.sst if down
|
|
226
|
+
else
|
|
227
|
+
if down
|
|
228
|
+
@computer.press_key(name)
|
|
229
|
+
elsif @computer.panel.held == name
|
|
230
|
+
@computer.release_key
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
184
235
|
def paddles? = POTS.index(@pot) >= 3
|
|
185
236
|
|
|
186
237
|
# A fresh pot device on its port, and none on the other. A machine
|
|
187
238
|
# without the ports takes none.
|
|
188
239
|
def attach_pot_device
|
|
189
|
-
|
|
240
|
+
portless = Controls.symbolic?(@computer) || @computer.family == :kim1
|
|
241
|
+
ports = Controls.p500?(@computer) || !portless ? @computer.control_ports : nil
|
|
190
242
|
if ports.nil?
|
|
191
243
|
@pot = :none
|
|
192
244
|
return
|
|
@@ -239,17 +291,21 @@ module Badline
|
|
|
239
291
|
end
|
|
240
292
|
|
|
241
293
|
# The PET's keyboard for a PET or a CBM-II, and for any other machine
|
|
242
|
-
# one it never uses
|
|
294
|
+
# one it never uses, on a keyboard of its own for the KIM-1, which has
|
|
295
|
+
# none.
|
|
243
296
|
def pet_keyboard(computer)
|
|
244
297
|
layout = case computer.family
|
|
245
298
|
when :pet then computer.model.keyboard
|
|
246
299
|
when :cbm2 then :cbm2
|
|
247
300
|
else :graphics
|
|
248
301
|
end
|
|
249
|
-
|
|
302
|
+
keyboard = computer.family == :kim1 ? Input::Keyboard.new : computer.keyboard
|
|
303
|
+
PETKeyboard.new(keyboard, layout)
|
|
250
304
|
end
|
|
251
305
|
|
|
252
306
|
def release_all
|
|
307
|
+
return @computer.release_key if @computer.family == :kim1
|
|
308
|
+
|
|
253
309
|
if Controls.symbolic?(@computer)
|
|
254
310
|
@pet_keyboard.release_all
|
|
255
311
|
else
|