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,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class KIM1
|
|
5
|
+
# The CPU's bus. The board decodes A10-A12 into eight 1K blocks and
|
|
6
|
+
# leaves A13-A15 alone, so the 8K repeats through all 64K, and the
|
|
7
|
+
# vectors at $FFFA-$FFFF are the monitor's at $1FFA-$1FFF (User Manual,
|
|
8
|
+
# figure 3.12 and section 6.2):
|
|
9
|
+
#
|
|
10
|
+
# $0000-$03FF - the 1K of RAM
|
|
11
|
+
# $0400-$16FF - nothing
|
|
12
|
+
# $1700-$173F - U3's ports and timer, its 16 registers repeated
|
|
13
|
+
# $1740-$177F - U2's
|
|
14
|
+
# $1780-$17BF - U3's 64 bytes of RAM
|
|
15
|
+
# $17C0-$17FF - U2's
|
|
16
|
+
# $1800-$1BFF - U3's ROM, the tape routines
|
|
17
|
+
# $1C00-$1FFF - U2's ROM, the monitor
|
|
18
|
+
#
|
|
19
|
+
# A read from nothing finds the last byte on the bus. Writes to ROM
|
|
20
|
+
# and to nothing go nowhere.
|
|
21
|
+
#
|
|
22
|
+
# The board takes more RAM, as RAM_CONFIGURATIONS names it: the stock
|
|
23
|
+
# 1K, 5K with a board in K1-K4 filling $0400-$13FF, or those 5K and
|
|
24
|
+
# RAM at $2000-$DFFF, with an expansion that decodes A13-A15 and
|
|
25
|
+
# repeats the first 8K only at $E000-$FFFF, so the vectors are still
|
|
26
|
+
# the monitor's (User Manual, sections 6.1 and 6.2).
|
|
27
|
+
class Bus
|
|
28
|
+
include Addressable
|
|
29
|
+
|
|
30
|
+
RAM_CONFIGURATIONS = %i[1k 5k expanded].freeze
|
|
31
|
+
|
|
32
|
+
# The RAM, and the one of RAM_CONFIGURATIONS it comes as.
|
|
33
|
+
attr_reader :ram, :ram_configuration
|
|
34
|
+
|
|
35
|
+
# The last byte the CPU read or wrote.
|
|
36
|
+
attr_reader :data
|
|
37
|
+
|
|
38
|
+
# +monitor+ is U2, the 6530-002, and +user+ U3, the 6530-003, with
|
|
39
|
+
# the RAM `ram` names.
|
|
40
|
+
def initialize(monitor, user, ram: :"1k")
|
|
41
|
+
addressable_at(0, length: 2**16)
|
|
42
|
+
raise ArgumentError, "no KIM-1 RAM configuration named #{ram}" unless RAM_CONFIGURATIONS.include?(ram)
|
|
43
|
+
|
|
44
|
+
@u2 = monitor
|
|
45
|
+
@u3 = user
|
|
46
|
+
@ram_configuration = ram
|
|
47
|
+
# The end of RAM in the first 8K, and where the first 8K stops
|
|
48
|
+
# repeating, past the RAM above it.
|
|
49
|
+
@low_end = ram == :"1k" ? 0x0400 : 0x1400
|
|
50
|
+
@mirror_end = ram == :expanded ? 0xe000 : 0x2000
|
|
51
|
+
@ram = Memory.new([], length: @mirror_end == 0x2000 ? @low_end : @mirror_end, start: 0)
|
|
52
|
+
@data = 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The RAM in K, without the 6530s' 64 bytes each.
|
|
56
|
+
def ram_kb = (@low_end + @mirror_end - 0x2000) >> 10
|
|
57
|
+
|
|
58
|
+
# Whether RAM answers at `addr`, the board's or a 6530's.
|
|
59
|
+
def ram?(addr)
|
|
60
|
+
offset = addr < 0x2000 || addr >= @mirror_end ? addr & 0x1fff : addr
|
|
61
|
+
offset < @low_end || offset >= 0x2000 || offset.between?(0x1780, 0x17ff)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def peek(addr)
|
|
65
|
+
offset = addr < 0x2000 || addr >= @mirror_end ? addr & 0x1fff : addr
|
|
66
|
+
@data = if offset < @low_end || offset >= 0x2000
|
|
67
|
+
@ram.peek(offset)
|
|
68
|
+
elsif offset >= 0x1c00
|
|
69
|
+
@u2.read_rom(offset)
|
|
70
|
+
elsif offset >= 0x1800
|
|
71
|
+
@u3.read_rom(offset)
|
|
72
|
+
elsif (offset >> 8) == 0x17
|
|
73
|
+
read_chips(offset)
|
|
74
|
+
else
|
|
75
|
+
@data
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def poke(addr, value)
|
|
80
|
+
@data = value
|
|
81
|
+
offset = addr < 0x2000 || addr >= @mirror_end ? addr & 0x1fff : addr
|
|
82
|
+
if offset < @low_end || offset >= 0x2000
|
|
83
|
+
@ram.poke(offset, value)
|
|
84
|
+
elsif (offset >> 8) == 0x17
|
|
85
|
+
write_chips(offset, value)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def power_on!
|
|
90
|
+
@ram.clear!
|
|
91
|
+
@u2.ram.map! { 0 }
|
|
92
|
+
@u3.ram.map! { 0 }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# A6 picks U2 over U3, and A7 the RAM over the registers.
|
|
98
|
+
def read_chips(offset)
|
|
99
|
+
ram = offset.anybits?(0x80)
|
|
100
|
+
if offset.anybits?(0x40)
|
|
101
|
+
ram ? @u2.read_ram(offset) : @u2.peek(offset & 0x0f)
|
|
102
|
+
else
|
|
103
|
+
ram ? @u3.read_ram(offset) : @u3.peek(offset & 0x0f)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def write_chips(offset, value)
|
|
108
|
+
ram = offset.anybits?(0x80)
|
|
109
|
+
if offset.anybits?(0x40)
|
|
110
|
+
ram ? @u2.write_ram(offset, value) : @u2.poke(offset & 0x0f, value)
|
|
111
|
+
else
|
|
112
|
+
ram ? @u3.write_ram(offset, value) : @u3.poke(offset & 0x0f, value)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class KIM1
|
|
5
|
+
# The KIM-1's 6502, U1, on the KIM1::Bus.
|
|
6
|
+
#
|
|
7
|
+
# With the SST switch on, the board's U26 gate pulls NMI low through
|
|
8
|
+
# each opcode fetch outside K7, $1C00-$1FFF and its mirrors, where the
|
|
9
|
+
# monitor runs. The CPU takes the NMI at the end of the instruction
|
|
10
|
+
# that fetch starts, a 2-cycle one too, so the monitor steps a program
|
|
11
|
+
# one instruction at a time (User Manual, figure 5.6).
|
|
12
|
+
class CPU
|
|
13
|
+
include Badline::Chips::CPU::Core
|
|
14
|
+
|
|
15
|
+
# The K7 block's address lines, A10-A12, which the board decodes.
|
|
16
|
+
K7 = 0x1c00
|
|
17
|
+
|
|
18
|
+
# Pulls NMI if the next cycle fetches an opcode outside K7. The
|
|
19
|
+
# board calls it ahead of each cycle while SST is on.
|
|
20
|
+
def single_step
|
|
21
|
+
@nmi = true if boundary? && (@program_counter & K7) != K7
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class KIM1
|
|
5
|
+
# The KIM-1's teletype on the host's terminal, for `badline kim1
|
|
6
|
+
# --headless`: what the terminal types goes down the line, and what
|
|
7
|
+
# the teletype prints comes out on it, without the NULs and RUBOUTs a
|
|
8
|
+
# terminal doesn't show. The machine runs at its own 1 MHz against the
|
|
9
|
+
# host's clock, catching up at most CATCH_UP seconds at a time, until
|
|
10
|
+
# Ctrl-C.
|
|
11
|
+
#
|
|
12
|
+
# `console` answers #typing, #read_text and #print_text, as
|
|
13
|
+
# Audio::Terminal does.
|
|
14
|
+
class HostTerminal
|
|
15
|
+
SLICE = 0.01
|
|
16
|
+
CATCH_UP = 0.1
|
|
17
|
+
|
|
18
|
+
def self.run(machine, console) = new(machine, console).run
|
|
19
|
+
|
|
20
|
+
# Builds the KIM-1 the command line's `options` name, loads the
|
|
21
|
+
# program they name (Media::KIM1Media), and runs it on `console`
|
|
22
|
+
# until Ctrl-C. Returns the exit status, 1 when the program won't
|
|
23
|
+
# load, which it warns about.
|
|
24
|
+
def self.start(options, console)
|
|
25
|
+
path = options.media_path
|
|
26
|
+
machine = Machine.for_options(options, path)
|
|
27
|
+
unless path.nil?
|
|
28
|
+
loaded = Media.attach(machine, path, autostart: true, kim1: Media::KIM1Media.loading(options))
|
|
29
|
+
console.print_text("#{loaded}\r\n")
|
|
30
|
+
end
|
|
31
|
+
run(machine, console)
|
|
32
|
+
rescue Machine::Unsupported, Storage::Papertape::FormatError, SystemCallError => e
|
|
33
|
+
warn "#{options.program}: #{path}: #{e.message}"
|
|
34
|
+
1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(machine, console)
|
|
38
|
+
@machine = machine
|
|
39
|
+
@console = console
|
|
40
|
+
@printed = 0
|
|
41
|
+
@started = 0.0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Runs until Ctrl-C, and returns the exit status.
|
|
45
|
+
def run
|
|
46
|
+
@started = now
|
|
47
|
+
@console.print_text("KIM-1 teletype at #{@machine.teletype.baud} baud. Ctrl-C: Quit\r\n")
|
|
48
|
+
@console.typing { loop { step } }
|
|
49
|
+
rescue Interrupt
|
|
50
|
+
0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def step
|
|
56
|
+
text = @console.read_text(SLICE)
|
|
57
|
+
@machine.teletype.type(text) unless text.empty?
|
|
58
|
+
behind = ((now - @started) * 1_000_000).to_i - @machine.cycles
|
|
59
|
+
@machine.run_cycles([behind, (CATCH_UP * 1_000_000).to_i].min) if behind.positive?
|
|
60
|
+
print_output
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def print_output
|
|
64
|
+
output = @machine.teletype.output
|
|
65
|
+
return if output.size == @printed
|
|
66
|
+
|
|
67
|
+
@console.print_text(output[@printed, output.size - @printed].gsub("\x00", "").gsub("\x7f", ""))
|
|
68
|
+
@printed = output.size
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class KIM1
|
|
5
|
+
# Pressing the panel's keys in turn, each long enough for the monitor
|
|
6
|
+
# to take it. The monitor takes a key it reads down on two scans in a
|
|
7
|
+
# row, after a scan with no key down, and a scan takes some 4,300
|
|
8
|
+
# cycles, so each key is held for HOLD_CYCLES and let go for
|
|
9
|
+
# RELEASE_CYCLES.
|
|
10
|
+
module KeyEntry
|
|
11
|
+
HOLD_CYCLES = 12_000
|
|
12
|
+
RELEASE_CYCLES = 8_000
|
|
13
|
+
|
|
14
|
+
# The panel's keys that type_keys takes by name. Any other word is a
|
|
15
|
+
# run of hex keys.
|
|
16
|
+
NAMED_KEYS = %w[AD DA GO PC + ST RS].freeze
|
|
17
|
+
|
|
18
|
+
# Presses the keys `text` names once the monitor is running: AD, DA,
|
|
19
|
+
# GO, PC, + and the hex digits, ST, which pulls NMI, and RS, which
|
|
20
|
+
# resets the machine. Words go by whitespace, and a word that isn't a
|
|
21
|
+
# key's name is a hex key for each of its characters, so
|
|
22
|
+
# "AD 0200 DA A9 +" presses nine keys, and the byte $AD goes in as
|
|
23
|
+
# "A D".
|
|
24
|
+
def type_keys(text)
|
|
25
|
+
keys = text.upcase.split.flat_map { |word| NAMED_KEYS.include?(word) ? [word] : word.chars }
|
|
26
|
+
unknown = keys.find { |key| !NAMED_KEYS.include?(key) && !Panel::KEYS.include?(key) }
|
|
27
|
+
raise ArgumentError, "no KIM-1 key named #{unknown}" unless unknown.nil?
|
|
28
|
+
|
|
29
|
+
on_init { queue_keys(keys) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Whether keys type_keys was given are still to be pressed.
|
|
33
|
+
def typing? = !@pending_keys.nil?
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def queue_keys(keys)
|
|
38
|
+
@pending_keys = keys
|
|
39
|
+
@key_down = false
|
|
40
|
+
@next_key_at = @cycles
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def feed_keys
|
|
44
|
+
return if @cycles < @next_key_at
|
|
45
|
+
|
|
46
|
+
if @key_down
|
|
47
|
+
release_key
|
|
48
|
+
@key_down = false
|
|
49
|
+
@next_key_at = @cycles + RELEASE_CYCLES
|
|
50
|
+
else
|
|
51
|
+
press_next_key
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def press_next_key
|
|
56
|
+
keys = @pending_keys
|
|
57
|
+
name = keys.shift
|
|
58
|
+
if name.nil?
|
|
59
|
+
@pending_keys = nil
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
if name == "ST"
|
|
63
|
+
press_stop
|
|
64
|
+
elsif name == "RS"
|
|
65
|
+
press_reset
|
|
66
|
+
else
|
|
67
|
+
press_key(name)
|
|
68
|
+
end
|
|
69
|
+
@key_down = true
|
|
70
|
+
@next_key_at = @cycles + HOLD_CYCLES
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class KIM1
|
|
5
|
+
# The whole machine as keyed snapshot modules (Snapshot::Keyed), in the
|
|
6
|
+
# order a load applies them: BL-MACHINE with the setup, the RAM, U2 and
|
|
7
|
+
# U3, each with its 64 bytes, ports and timer, the panel, the cassette
|
|
8
|
+
# deck, the teletype, the sound's wiring and the CPU.
|
|
9
|
+
module KeyedModules
|
|
10
|
+
def save_modules
|
|
11
|
+
sound = @sound
|
|
12
|
+
[Snapshot::Keyed.section("BL-MACHINE") { |out| save_machine_fields(out) },
|
|
13
|
+
Snapshot::Keyed.section("BL-CPU") { |out| @cpu.save_fields(out) },
|
|
14
|
+
Snapshot::Keyed.section("BL-RAM") { |out| @bus.save_fields(out) },
|
|
15
|
+
Snapshot::Keyed.section("BL-U2") { |out| @u2.save_fields(out) },
|
|
16
|
+
Snapshot::Keyed.section("BL-U3") { |out| @u3.save_fields(out) },
|
|
17
|
+
Snapshot::Keyed.section("BL-PANEL") { |out| @panel.save_fields(out) },
|
|
18
|
+
Snapshot::Keyed.section("BL-DECK") { |out| @panel.tape.save_fields(out) },
|
|
19
|
+
Snapshot::Keyed.section("BL-TTY") { |out| @teletype.save_fields(out) },
|
|
20
|
+
Snapshot::Keyed.section("BL-SOUND") { |out| out.bool("speaker", sound.speaker) }]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Raises Snapshot::FormatError for the modules of a machine built
|
|
24
|
+
# another way.
|
|
25
|
+
def load_modules(modules)
|
|
26
|
+
machine = modules.machine
|
|
27
|
+
check_snapshot_setup(Snapshot::KIM1Setup.read_fields(machine))
|
|
28
|
+
load_machine_fields(machine)
|
|
29
|
+
@bus.load_fields(modules.required("BL-RAM"))
|
|
30
|
+
@u2.load_fields(modules.required("BL-U2"))
|
|
31
|
+
@u3.load_fields(modules.required("BL-U3"))
|
|
32
|
+
@panel.load_fields(modules.required("BL-PANEL"))
|
|
33
|
+
@panel.tape.load_fields(modules.required("BL-DECK"))
|
|
34
|
+
@teletype.load_fields(modules.required("BL-TTY"))
|
|
35
|
+
@sound.speaker = modules.required("BL-SOUND").bool("speaker", false)
|
|
36
|
+
@cpu.load_fields(modules.required("BL-CPU"))
|
|
37
|
+
@cpu.restored!
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def check_snapshot_setup(setup)
|
|
44
|
+
ours = Snapshot::KIM1Setup.of(self)
|
|
45
|
+
return if setup == ours
|
|
46
|
+
|
|
47
|
+
raise Snapshot::FormatError, "the snapshot is of a #{setup}, not a #{ours}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
include KeyedModules
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class KIM1
|
|
5
|
+
class Bus
|
|
6
|
+
# The RAM, the expansion's too, and the last byte on the bus, in a
|
|
7
|
+
# keyed snapshot module (BL-RAM). The 6530s keep their own 64 bytes.
|
|
8
|
+
module KeyedState
|
|
9
|
+
def save_fields(out)
|
|
10
|
+
@ram.save_fields(out, "ram")
|
|
11
|
+
out.int("data", @data)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def load_fields(input)
|
|
15
|
+
@ram.load_fields(input, "ram")
|
|
16
|
+
@data = input.byte("data", @data)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
include KeyedState
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Panel
|
|
24
|
+
# The panel's fields in a keyed snapshot module (BL-PANEL): the
|
|
25
|
+
# TTY/KB jumper, the key held down, the digit lit and its segments
|
|
26
|
+
# since the cycle they lit, the cycles each segment has been lit,
|
|
27
|
+
# and what each digit last showed and when. The teletype restores
|
|
28
|
+
# the line in.
|
|
29
|
+
module KeyedState
|
|
30
|
+
def save_fields(out)
|
|
31
|
+
out.bool("tty", @tty).int("held", @held).int("digit", @digit).int("segments", @segments)
|
|
32
|
+
out.int("since", @since).ints("lit", @lit).ints("shown", @shown).ints("shown_at", @shown_at)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def load_fields(input)
|
|
36
|
+
@tty = input.bool("tty", false)
|
|
37
|
+
held = input.int("held", -1)
|
|
38
|
+
@held = held.between?(0, KEYS.length - 1) ? held : -1
|
|
39
|
+
digit = input.int("digit", -1)
|
|
40
|
+
@digit = digit.between?(0, DIGITS - 1) ? digit : -1
|
|
41
|
+
@segments = input.masked("segments", 0, 0x7f)
|
|
42
|
+
@since = input.int("since", 0)
|
|
43
|
+
input.ints_into("lit", @lit)
|
|
44
|
+
input.ints_into("shown", @shown)
|
|
45
|
+
input.ints_into("shown_at", @shown_at)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
include KeyedState
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Tape
|
|
53
|
+
# The deck's fields in a keyed snapshot module (BL-DECK): whether
|
|
54
|
+
# PLAY or RECORD is down, how far into the tape the deck is, the next
|
|
55
|
+
# of the tape's edges to play and PB7's level from it, the level U2
|
|
56
|
+
# last drove PB7 to, and the tape itself under tape.*: its file's
|
|
57
|
+
# path, and the time from each crossing to the next, from the start.
|
|
58
|
+
# The tape goes back in from the module, and STOP after a recording
|
|
59
|
+
# writes it to that path.
|
|
60
|
+
module KeyedState
|
|
61
|
+
def save_fields(out)
|
|
62
|
+
out.bool("playing", @playing).bool("recording", @recording).int("position", position)
|
|
63
|
+
out.int("next_edge", @next_edge).bool("high", @high).bool("output_high", @output_high)
|
|
64
|
+
crossings = @tape.crossings
|
|
65
|
+
gaps = crossings.each_with_index.map { |time, index| index.zero? ? time : time - crossings[index - 1] }
|
|
66
|
+
out.text("tape.path", @tape.path).ints("tape.gaps", gaps)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def load_fields(input)
|
|
70
|
+
time = 0
|
|
71
|
+
crossings = input.ints("tape.gaps", []).map { |gap| time += gap }
|
|
72
|
+
@tape = Storage::KIMTape.new(input.text("tape.path", ""), crossings:)
|
|
73
|
+
@playing = input.bool("playing", false)
|
|
74
|
+
@recording = input.bool("recording", false) && !@playing
|
|
75
|
+
@stopped_at = input.int("position", 0)
|
|
76
|
+
@started = @clock.cycles
|
|
77
|
+
@edges = @playing ? @tape.edges : []
|
|
78
|
+
@next_edge = input.int("next_edge", 0).clamp(0, @edges.length)
|
|
79
|
+
@high = input.bool("high", true)
|
|
80
|
+
@output_high = input.bool("output_high", true)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
include KeyedState
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The machine's own fields in a keyed snapshot module (BL-MACHINE):
|
|
88
|
+
# the family, its setup (Snapshot::KIM1Setup), the cycle count, the
|
|
89
|
+
# SST switch and U3's PB7 wiring, and under host.* whether type_keys
|
|
90
|
+
# is typing, the keys it has still to press, by their place in KEY_NAMES, the one held, when
|
|
91
|
+
# the next goes, and the on_init handlers still to run. The badline
|
|
92
|
+
# version that wrote it is written for reports and never read.
|
|
93
|
+
module KeyedState
|
|
94
|
+
# The keys type_keys presses, as host.pending_keys numbers them.
|
|
95
|
+
KEY_NAMES = (Panel::KEYS + %w[ST RS]).freeze
|
|
96
|
+
|
|
97
|
+
# How many more on_init handlers the machine a snapshot was taken of
|
|
98
|
+
# had yet to run than this one has. Nil until the machine is restored.
|
|
99
|
+
attr_reader :init_handlers_lost
|
|
100
|
+
|
|
101
|
+
# Whether a snapshot saves now (Machine::Settling): the CPU between
|
|
102
|
+
# instructions or jammed.
|
|
103
|
+
def settled_for_snapshot? = @cpu.boundary? || @cpu.jammed?
|
|
104
|
+
|
|
105
|
+
# Nothing on the board defers its work.
|
|
106
|
+
def settle_chips_for_snapshot = nil
|
|
107
|
+
|
|
108
|
+
def save_machine_fields(out)
|
|
109
|
+
out.text("family", "kim1").text("written_by", VERSION)
|
|
110
|
+
Snapshot::KIM1Setup.of(self).write_fields(out)
|
|
111
|
+
out.int("cycles", @cycles).bool("sst", @sst).bool("irq_wired", @irq_wired)
|
|
112
|
+
pending = @pending_keys
|
|
113
|
+
out.bool("host.typing", !pending.nil?)
|
|
114
|
+
out.ints("host.pending_keys", pending.map { |name| KEY_NAMES.index(name) }) if pending
|
|
115
|
+
out.bool("host.key_down", @key_down).int("host.next_key_at", @next_key_at)
|
|
116
|
+
out.int("host.init_pending", @cycles <= @init_threshold ? @init_handlers.length : 0)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def load_machine_fields(input)
|
|
120
|
+
@cycles = input.int("cycles", @cycles)
|
|
121
|
+
@sst = input.bool("sst", false)
|
|
122
|
+
@irq_wired = input.bool("irq_wired", false)
|
|
123
|
+
pending = input.ints("host.pending_keys", []).filter_map { |index| KEY_NAMES[index] if index >= 0 }
|
|
124
|
+
@pending_keys = input.bool("host.typing", false) ? pending : nil
|
|
125
|
+
@key_down = input.bool("host.key_down", false)
|
|
126
|
+
@next_key_at = input.int("host.next_key_at", 0)
|
|
127
|
+
@init_handlers_lost = [input.int("host.init_pending", 0) - @init_handlers.length, 0].max
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
include KeyedState
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class KIM1
|
|
5
|
+
# The keypad and display on U2's ports (User Manual, figures 3.5 and
|
|
6
|
+
# 3.6), and the TTY/KB jumper.
|
|
7
|
+
#
|
|
8
|
+
# PB1-PB4 feed a 74145 decoder, U24. Its outputs 0-2 select the
|
|
9
|
+
# keypad's three rows, 3 the jumper, and 4-9 the six LED digits, left
|
|
10
|
+
# to right. PA0-PA6 drive the selected digit's segments a-g, a high
|
|
11
|
+
# line lighting one, and read the selected row's keys, a pressed key
|
|
12
|
+
# pulling its line low: PA6 is the row's first key, PA0 its seventh.
|
|
13
|
+
# Fitted, the jumper pulls PA0 low while output 3 is selected, which
|
|
14
|
+
# the monitor reads as teletype mode, and PA7 takes the teletype's
|
|
15
|
+
# line in (Teletype). PB7 is the cassette's line (Tape).
|
|
16
|
+
#
|
|
17
|
+
# Nothing holds the display lit. A program lights each digit in turn
|
|
18
|
+
# (the monitor's SCANDS, $1F1F), so the panel times how long each
|
|
19
|
+
# segment is lit from the writes to U2's ports: lit_cycles hands the
|
|
20
|
+
# totals to a front end, and display reads back what the digits show.
|
|
21
|
+
class Panel
|
|
22
|
+
# The keys of the matrix, by the code the monitor's GETKEY ($1F6A)
|
|
23
|
+
# gives them: the row times 7 plus the column.
|
|
24
|
+
KEYS = %w[0 1 2 3 4 5 6 7 8 9 A B C D E F AD DA + GO PC].freeze
|
|
25
|
+
|
|
26
|
+
# The monitor's segment patterns for 0-F, its table at $1FE7 without
|
|
27
|
+
# bit 7, which PA7 doesn't drive.
|
|
28
|
+
HEX_SEGMENTS = [0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07,
|
|
29
|
+
0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71].freeze
|
|
30
|
+
|
|
31
|
+
DIGITS = 6
|
|
32
|
+
|
|
33
|
+
# The decoder output of the leftmost digit.
|
|
34
|
+
FIRST_DIGIT = 4
|
|
35
|
+
|
|
36
|
+
# A digit lit for less than this many cycles at a time doesn't count
|
|
37
|
+
# as showing anything: the monitor passes its digits through that
|
|
38
|
+
# quickly between patterns.
|
|
39
|
+
SHOWN_CYCLES = 100
|
|
40
|
+
|
|
41
|
+
# A digit not lit for this many cycles, some five of the monitor's
|
|
42
|
+
# scans, reads as dark.
|
|
43
|
+
DARK_CYCLES = 20_000
|
|
44
|
+
|
|
45
|
+
# Whether the TTY/KB jumper is fitted.
|
|
46
|
+
attr_accessor :tty
|
|
47
|
+
|
|
48
|
+
# Whether the teletype's line in is high, its idle level. Low, it
|
|
49
|
+
# pulls PA7 low while PB5 is low, which gates it through.
|
|
50
|
+
attr_accessor :tty_in_high
|
|
51
|
+
|
|
52
|
+
# The cassette deck on PB7.
|
|
53
|
+
attr_reader :tape
|
|
54
|
+
|
|
55
|
+
# +clock+ answers +cycles+, the cycle each write to the ports lands
|
|
56
|
+
# on.
|
|
57
|
+
def initialize(clock)
|
|
58
|
+
@clock = clock
|
|
59
|
+
@rriot = nil
|
|
60
|
+
@tty = false
|
|
61
|
+
@tty_in_high = true
|
|
62
|
+
@tape = Tape.new(clock)
|
|
63
|
+
@held = -1
|
|
64
|
+
@digit = -1
|
|
65
|
+
@segments = 0
|
|
66
|
+
@since = 0
|
|
67
|
+
@lit = Array.new(DIGITS * 7, 0)
|
|
68
|
+
@shown = Array.new(DIGITS, 0)
|
|
69
|
+
@shown_at = Array.new(DIGITS, -DARK_CYCLES - 1)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# U2, whose port B selects the row the keys are read from.
|
|
73
|
+
def connect(rriot)
|
|
74
|
+
@rriot = rriot
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Holds down the key KEYS names `name`, and lets go of any other.
|
|
78
|
+
def press(name)
|
|
79
|
+
code = KEYS.index(name)
|
|
80
|
+
raise ArgumentError, "no KIM-1 key named #{name}" if code.nil?
|
|
81
|
+
|
|
82
|
+
@held = code
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def release
|
|
86
|
+
@held = -1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The name of the key held down, or nil.
|
|
90
|
+
def held = @held.negative? ? nil : KEYS[@held]
|
|
91
|
+
|
|
92
|
+
def read_a(_lines)
|
|
93
|
+
port_b = @rriot.port_b_output
|
|
94
|
+
row = decoded(port_b)
|
|
95
|
+
mask = @tty_in_high || port_b.anybits?(0x20) ? 0xff : 0x7f
|
|
96
|
+
mask &= 0xfe if row == 3 && @tty
|
|
97
|
+
held = @held
|
|
98
|
+
mask &= ~(0x40 >> (held % 7)) & 0xff if held >= 0 && held / 7 == row
|
|
99
|
+
mask
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def read_b(_lines) = @tape.input
|
|
103
|
+
|
|
104
|
+
# The lines U2's ports drive after a write to them.
|
|
105
|
+
def lines_written(port_a, port_b)
|
|
106
|
+
@tape.output(port_b)
|
|
107
|
+
digit = decoded(port_b) - FIRST_DIGIT
|
|
108
|
+
digit = -1 unless digit >= 0 && digit < DIGITS
|
|
109
|
+
segments = digit.negative? ? 0 : port_a & 0x7f
|
|
110
|
+
return if digit == @digit && segments == @segments
|
|
111
|
+
|
|
112
|
+
now = @clock.cycles
|
|
113
|
+
end_lit(now)
|
|
114
|
+
@digit = digit
|
|
115
|
+
@segments = segments
|
|
116
|
+
@since = now
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The cycles each segment has been lit since the last call, seven to
|
|
120
|
+
# a digit from the left, a to g, and starts counting again.
|
|
121
|
+
def lit_cycles
|
|
122
|
+
now = @clock.cycles
|
|
123
|
+
end_lit(now)
|
|
124
|
+
@since = now
|
|
125
|
+
totals = @lit.dup
|
|
126
|
+
@lit.map! { 0 }
|
|
127
|
+
totals
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The segments, a in bit 0 to g in bit 6, each digit shows: its
|
|
131
|
+
# pattern the last time it was lit for SHOWN_CYCLES or more, or 0
|
|
132
|
+
# once it has been dark for DARK_CYCLES.
|
|
133
|
+
def display
|
|
134
|
+
now = @clock.cycles
|
|
135
|
+
Array.new(DIGITS) do |digit|
|
|
136
|
+
if digit == @digit && @segments != 0 && now - @since >= SHOWN_CYCLES
|
|
137
|
+
@segments
|
|
138
|
+
elsif now - @shown_at[digit] <= DARK_CYCLES
|
|
139
|
+
@shown[digit]
|
|
140
|
+
else
|
|
141
|
+
0
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The display as text, such as "0200 A9": a space for a dark digit
|
|
147
|
+
# and ? for a pattern that isn't a hex digit, with a space between
|
|
148
|
+
# the address and the data.
|
|
149
|
+
def display_text
|
|
150
|
+
chars = display.map do |segments|
|
|
151
|
+
next " " if segments.zero?
|
|
152
|
+
|
|
153
|
+
index = HEX_SEGMENTS.index(segments)
|
|
154
|
+
index.nil? ? "?" : index.to_s(16).upcase
|
|
155
|
+
end
|
|
156
|
+
"#{chars[0, 4].join} #{chars[4, 2].join}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def decoded(port_b) = (port_b >> 1) & 0x0f
|
|
162
|
+
|
|
163
|
+
def end_lit(now)
|
|
164
|
+
digit = @digit
|
|
165
|
+
return if digit.negative? || @segments.zero?
|
|
166
|
+
|
|
167
|
+
lit = now - @since
|
|
168
|
+
7.times { |segment| @lit[(digit * 7) + segment] += lit if @segments.anybits?(1 << segment) }
|
|
169
|
+
return if lit < SHOWN_CYCLES
|
|
170
|
+
|
|
171
|
+
@shown[digit] = @segments
|
|
172
|
+
@shown_at[digit] = now
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class KIM1
|
|
5
|
+
# Each 6530 a class of its own, so each keeps one type of peripheral.
|
|
6
|
+
|
|
7
|
+
# U2, the 6530-002: the monitor's ROM, and the ports that drive the
|
|
8
|
+
# keypad, the display, the teletype and the tape. Its peripheral is
|
|
9
|
+
# the Panel, which hears every write to the port and direction
|
|
10
|
+
# registers, so it can time how long each LED segment is lit.
|
|
11
|
+
class MonitorRRIOT < Chips::RRIOT
|
|
12
|
+
def reset!
|
|
13
|
+
super
|
|
14
|
+
@peripheral&.lines_written(port_a_output, port_b_output)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def poke(addr, value)
|
|
18
|
+
super
|
|
19
|
+
@peripheral.lines_written(port_a_output, port_b_output) if addr.nobits?(0x04)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# U3, the 6530-003: the tape routines' ROM, and the ports the
|
|
24
|
+
# application connector carries for the user, with nothing on them.
|
|
25
|
+
class UserRRIOT < Chips::RRIOT
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|