badline 0.11.1 → 0.12.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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +8 -7
  4. data/CONTRIBUTING.md +7 -6
  5. data/README.md +12 -5
  6. data/doc/media.md +28 -1
  7. data/doc/pinned-behaviour.md +31 -1
  8. data/exe/badline-ruby +1 -1
  9. data/lib/badline/c128/bus.rb +4 -17
  10. data/lib/badline/c128/drives.rb +35 -22
  11. data/lib/badline/c128/fast_serial.rb +1 -0
  12. data/lib/badline/c128/io_pages.rb +35 -0
  13. data/lib/badline/c128/mmu.rb +9 -7
  14. data/lib/badline/c128/modes.rb +17 -4
  15. data/lib/badline/c128/saved_state.rb +22 -0
  16. data/lib/badline/c128/z80.rb +49 -0
  17. data/lib/badline/c128/z80_pages.rb +118 -0
  18. data/lib/badline/c128/z80_turns.rb +106 -0
  19. data/lib/badline/c128.rb +16 -19
  20. data/lib/badline/cartridge/action_replay.rb +2 -2
  21. data/lib/badline/cartridge/comal80.rb +1 -7
  22. data/lib/badline/cartridge/dinamic.rb +0 -8
  23. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  24. data/lib/badline/cartridge/fun_play.rb +0 -8
  25. data/lib/badline/cartridge/game_system.rb +0 -8
  26. data/lib/badline/cartridge/kcs_power.rb +10 -7
  27. data/lib/badline/cartridge/mach5.rb +2 -4
  28. data/lib/badline/cartridge/magic_desk.rb +0 -8
  29. data/lib/badline/cartridge/ocean.rb +0 -8
  30. data/lib/badline/cartridge/pagefox.rb +2 -2
  31. data/lib/badline/cartridge/retro_replay.rb +7 -6
  32. data/lib/badline/cartridge/rex_utility.rb +2 -6
  33. data/lib/badline/cartridge/rgcd.rb +2 -2
  34. data/lib/badline/cartridge/simons_basic.rb +0 -5
  35. data/lib/badline/cartridge/super_games.rb +4 -2
  36. data/lib/badline/cartridge/westermann.rb +2 -6
  37. data/lib/badline/cartridge.rb +37 -9
  38. data/lib/badline/cia/fast_forward.rb +21 -3
  39. data/lib/badline/cia.rb +8 -5
  40. data/lib/badline/computer/attachments.rb +4 -56
  41. data/lib/badline/computer/true_drives.rb +80 -0
  42. data/lib/badline/computer.rb +4 -0
  43. data/lib/badline/core.rb +2 -0
  44. data/lib/badline/drive/core.rb +8 -33
  45. data/lib/badline/drive/fast_serial.rb +73 -0
  46. data/lib/badline/drive/idle.rb +1 -11
  47. data/lib/badline/drive/orbit.rb +2 -5
  48. data/lib/badline/drive/sleep.rb +0 -2
  49. data/lib/badline/drive/vias.rb +84 -0
  50. data/lib/badline/drive.rb +2 -0
  51. data/lib/badline/drive1541.rb +1 -0
  52. data/lib/badline/drive1571/wd1770.rb +94 -8
  53. data/lib/badline/drive1571.rb +8 -58
  54. data/lib/badline/drive1581/bus.rb +137 -0
  55. data/lib/badline/drive1581/cpu.rb +11 -0
  56. data/lib/badline/drive1581/disk.rb +135 -0
  57. data/lib/badline/drive1581/disk_state.rb +87 -0
  58. data/lib/badline/drive1581/mechanism.rb +117 -0
  59. data/lib/badline/drive1581/serial_port.rb +52 -0
  60. data/lib/badline/drive1581/slot.rb +78 -0
  61. data/lib/badline/drive1581/track.rb +100 -0
  62. data/lib/badline/drive1581/track_bytes.rb +75 -0
  63. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  64. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  65. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  66. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  67. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  68. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  69. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  70. data/lib/badline/drive1581/wd1772.rb +216 -0
  71. data/lib/badline/drive1581.rb +233 -0
  72. data/lib/badline/frontend/app.rb +1 -3
  73. data/lib/badline/frontend/audio_device.rb +67 -0
  74. data/lib/badline/frontend/audio_sink.rb +16 -50
  75. data/lib/badline/frontend/boot.rb +18 -10
  76. data/lib/badline/frontend/buttons.rb +7 -7
  77. data/lib/badline/frontend/keys.rb +33 -8
  78. data/lib/badline/frontend/media_slots.rb +42 -0
  79. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  80. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  81. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  82. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  83. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  84. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  85. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  86. data/lib/badline/frontend/menu/theme.rb +18 -0
  87. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  88. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  89. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  90. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  91. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  92. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  93. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  94. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  95. data/lib/badline/frontend/player/theme.rb +18 -0
  96. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  97. data/lib/badline/frontend/sound.rb +19 -38
  98. data/lib/badline/frontend/timeline.rb +3 -14
  99. data/lib/badline/frontend.rb +24 -20
  100. data/lib/badline/iec_bus.rb +14 -2
  101. data/lib/badline/machine.rb +5 -4
  102. data/lib/badline/media/boot_disk.rb +50 -0
  103. data/lib/badline/media/not_disk.rb +8 -0
  104. data/lib/badline/media/true_drive.rb +35 -15
  105. data/lib/badline/media.rb +9 -3
  106. data/lib/badline/options/event.rb +2 -2
  107. data/lib/badline/options/family.rb +3 -9
  108. data/lib/badline/options/help.rb +2 -1
  109. data/lib/badline/roms/README +9 -0
  110. data/lib/badline/roms/dos1581.rom +0 -0
  111. data/lib/badline/sdl.rb +37 -20
  112. data/lib/badline/snapshot/state.rb +5 -4
  113. data/lib/badline/storage/p00.rb +8 -0
  114. data/lib/badline/time_of_day.rb +3 -3
  115. data/lib/badline/version.rb +1 -1
  116. data/lib/badline/vic20/attachments.rb +21 -7
  117. data/lib/badline/vic20/saved_state.rb +2 -0
  118. data/lib/badline/vic20.rb +3 -0
  119. data/lib/badline/z80/core.rb +2 -0
  120. data/lib/badline/z80/saved_state.rb +69 -0
  121. metadata +54 -21
  122. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  123. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  124. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  125. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1581
5
+ class WD1772
6
+ # The type II commands and read address, which find an ID field on
7
+ # the track under the head:
8
+ #
9
+ # 100mhE00 read sector the data field after the ID that carries
10
+ # the track and sector registers
11
+ # 101mhEPa write sector a data field after that ID, deleted with a
12
+ # 11000hE00 read address the next ID field's six bytes, the track
13
+ # going into the sector register
14
+ #
15
+ # The search reads each ID field as it passes. One that matches but
16
+ # whose CRC doesn't read back sets CRC_ERROR and the search goes on;
17
+ # after five index pulses it gives up with NOT_FOUND. Each byte of a
18
+ # field passes in BYTE cycles and sets DRQ as it lands in the data
19
+ # register, and LOST_DATA where the last one was still unread. With m
20
+ # the command goes on to the next sector until one isn't found. E
21
+ # lets the head settle first. SectorWrites writes the data field.
22
+ module Sectors
23
+ private
24
+
25
+ def start_type2
26
+ start_search(@command < 0xa0 ? READ : WRITE)
27
+ end
28
+
29
+ def start_read_address
30
+ start_search(ADDRESS)
31
+ end
32
+
33
+ def start_search(mode)
34
+ @type1 = false
35
+ @status = 0
36
+ @drq = false
37
+ @mode = mode
38
+ resume_type2 unless spinning_up?(SEARCH)
39
+ end
40
+
41
+ def resume_type2
42
+ if @mode == WRITE && @mechanism.write_protected?
43
+ @status |= PROTECTED
44
+ return finish
45
+ end
46
+ return begin_search if @command.nobits?(SETTLE_DELAY)
47
+
48
+ @phase = SETTLE_SEARCH
49
+ @due = @now + SETTLE
50
+ end
51
+
52
+ # Searches for five index pulses from now.
53
+ def begin_search
54
+ index = next_index
55
+ @deadline = index == NEVER ? NEVER : index + (4 * Mechanism::REVOLUTION)
56
+ search
57
+ end
58
+
59
+ # Waits for the next ID field to pass under the head.
60
+ def search
61
+ @phase = SEARCH
62
+ track = @mechanism.track
63
+ sector = track&.next_id((@mechanism.angle(@now) / BYTE) + 1)
64
+ @found = sector ? track.sectors.index(sector) : -1
65
+ @id_at = sector ? byte_end(sector.id.mark) : NEVER
66
+ @due = [@id_at, @deadline].min
67
+ end
68
+
69
+ def found = @mechanism.track&.sectors&.[](@found)
70
+
71
+ def id_passed
72
+ return not_found unless @now == @id_at
73
+
74
+ sector = found
75
+ return search if sector.nil?
76
+
77
+ id = sector.id
78
+ return id_read(id) if @mode == ADDRESS
79
+ return search unless matches?(id.bytes)
80
+ return crc_mismatch unless id.good
81
+
82
+ @status &= ~CRC_ERROR
83
+ @mode == VERIFY_TRACK ? finish : sector_found(sector)
84
+ end
85
+
86
+ def matches?(id)
87
+ id[0] == @track && (@mode == VERIFY_TRACK || id[2] == @sector)
88
+ end
89
+
90
+ def crc_mismatch
91
+ @status |= CRC_ERROR
92
+ search
93
+ end
94
+
95
+ def not_found
96
+ @status |= NOT_FOUND
97
+ finish
98
+ end
99
+
100
+ def sector_found(sector)
101
+ return write_sector(sector) if @mode == WRITE
102
+
103
+ data = sector.data
104
+ return search if data.nil? || data.mark > sector.id.mark + 7 + Track::DATA_MARK_WINDOW
105
+
106
+ @status |= DELETED if data.deleted
107
+ transfer(data.bytes, data.mark, READ_DATA)
108
+ @crc_good = data.good
109
+ end
110
+
111
+ # Read address: the ID's bytes and its CRC.
112
+ def id_read(id)
113
+ @status |= CRC_ERROR unless id.good
114
+ transfer(id.bytes + Track.field_crc(Track::ID_MARK, id.bytes), id.mark, READ_ADDRESS)
115
+ end
116
+
117
+ def transfer(bytes, mark, phase)
118
+ @bytes = bytes
119
+ @index = 0
120
+ @phase = phase
121
+ @due = byte_end(mark + 1)
122
+ end
123
+
124
+ # A byte of the field lands in the data register. After the last,
125
+ # the CRC passes.
126
+ def byte_read
127
+ return field_read if @index == @bytes.length
128
+
129
+ @status |= LOST_DATA if @drq
130
+ @data = @bytes[@index]
131
+ @drq = true
132
+ @index += 1
133
+ @due += @index == @bytes.length && @phase == READ_DATA ? 2 * BYTE : BYTE
134
+ end
135
+
136
+ def field_read
137
+ if @phase == READ_ADDRESS
138
+ @sector = @bytes[0]
139
+ elsif !@crc_good
140
+ @status |= CRC_ERROR
141
+ elsif @command.anybits?(MULTIPLE)
142
+ @sector = (@sector + 1) & 0xff
143
+ return begin_search
144
+ end
145
+ finish
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1581
5
+ class WD1772
6
+ # Saving and restoring the WD1772 for a snapshot: its registers and
7
+ # lines, and the command running, down to the byte it is at.
8
+ module State
9
+ def save_state(out)
10
+ [@now, @due, @phase, @command, @track, @sector, @data, @status, @steps, @count, @after_spin_up, @mode,
11
+ @deadline, @id_at, @found, @index, @crc, @write_id, @size].each { |value| out.int(value) }
12
+ [@type1, @drq, @intrq, @motor_on, @inward, @crc_good, @gate_open, @late,
13
+ @settling].each { |flag| out.boolean(flag) }
14
+ out.ints(@bytes).ints(@syncs.map { |sync| sync ? 1 : 0 })
15
+ end
16
+
17
+ def load_state(input)
18
+ load_registers(input)
19
+ load_command(input)
20
+ load_flags(input)
21
+ @bytes = input.ints
22
+ @syncs = input.ints.map { |sync| sync == 1 }
23
+ end
24
+
25
+ private
26
+
27
+ def load_registers(input)
28
+ @now = input.int
29
+ @due = input.int
30
+ @phase = input.int
31
+ @command = input.int
32
+ @track = input.int
33
+ @sector = input.int
34
+ @data = input.int
35
+ @status = input.int
36
+ end
37
+
38
+ def load_command(input)
39
+ @steps = input.int
40
+ @count = input.int
41
+ @after_spin_up = input.int
42
+ @mode = input.int
43
+ @deadline = input.int
44
+ @id_at = input.int
45
+ @found = input.int
46
+ @index = input.int
47
+ @crc = input.int
48
+ @write_id = input.int
49
+ @size = input.int
50
+ end
51
+
52
+ def load_flags(input)
53
+ @type1 = input.boolean?
54
+ @drq = input.boolean?
55
+ @intrq = input.boolean?
56
+ @motor_on = input.boolean?
57
+ @inward = input.boolean?
58
+ @crc_good = input.boolean?
59
+ @gate_open = input.boolean?
60
+ @late = input.boolean?
61
+ @settling = input.boolean?
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1581
5
+ class WD1772
6
+ # The type I commands, which move the head:
7
+ #
8
+ # 0000hVrr restore step out to track 0, the track register to 0
9
+ # 0001hVrr seek step until the track register holds the data
10
+ # register's track
11
+ # 001uhVrr step one step the way the last one went
12
+ # 010uhVrr step in one step towards the hub
13
+ # 011uhVrr step out one step towards the rim
14
+ #
15
+ # Each step waits out the step rate rr picks (STEP_RATES), and u has
16
+ # a step update the track register, as restore and seek always do.
17
+ # With V the head settles and the command reads IDs until one carries
18
+ # the track register's track, or sets NOT_FOUND after five turns.
19
+ module Stepping
20
+ private
21
+
22
+ def start_type1
23
+ @type1 = true
24
+ @status = 0
25
+ @drq = false
26
+ @phase = STEPPED
27
+ @steps = 0
28
+ if @command < 0x10
29
+ @track = 0xff
30
+ @data = 0
31
+ elsif @command >= 0x40
32
+ @inward = @command < 0x60
33
+ end
34
+ step_done unless spinning_up?(STEPPED)
35
+ end
36
+
37
+ def spun_up
38
+ @status |= SPUN_UP
39
+ @phase = @after_spin_up
40
+ case @after_spin_up
41
+ when STEPPED then step_done
42
+ when SEARCH then resume_type2
43
+ else resume_track
44
+ end
45
+ end
46
+
47
+ # The step rate has passed: the next step, or the end of the moves.
48
+ def step_done
49
+ return next_step if @steps.zero? || @command < 0x20
50
+
51
+ moved
52
+ end
53
+
54
+ # Restore and seek step until the track register matches, restore
55
+ # also stopping at track 0. The others step once.
56
+ def next_step
57
+ return single_step if @command >= 0x20
58
+ return seek_restore if @command < 0x10 && restored?
59
+
60
+ if @track == @data
61
+ moved
62
+ else
63
+ @inward = @data > @track
64
+ @track = (@track + (@inward ? 1 : -1)) & 0xff
65
+ pulse
66
+ end
67
+ end
68
+
69
+ def restored?
70
+ return false unless @mechanism.track0? || @steps >= 255
71
+
72
+ @status |= NOT_FOUND if !@mechanism.track0? && @command.anybits?(VERIFY)
73
+ true
74
+ end
75
+
76
+ def seek_restore
77
+ @track = 0 if @mechanism.track0?
78
+ moved
79
+ end
80
+
81
+ def single_step
82
+ @track = (@track + (@inward ? 1 : -1)) & 0xff if @command.anybits?(UPDATE)
83
+ pulse
84
+ end
85
+
86
+ def pulse
87
+ @steps += 1
88
+ @mechanism.step(@inward)
89
+ @phase = STEPPED
90
+ @due = @now + STEP_RATES[@command & 0x03]
91
+ end
92
+
93
+ # The head is there: with V it settles, and the command ends
94
+ # otherwise.
95
+ def moved
96
+ return finish unless @command.anybits?(VERIFY) && @status.nobits?(NOT_FOUND)
97
+
98
+ @phase = SETTLED
99
+ @due = @now + SETTLE
100
+ end
101
+
102
+ def verify_track
103
+ @mode = VERIFY_TRACK
104
+ begin_search
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1581
5
+ class WD1772
6
+ # The type III commands that take a whole turn, from one index pulse
7
+ # to the next:
8
+ #
9
+ # 11100hE00 read track every byte under the head (Track#raw)
10
+ # 11110hEP0 write track every byte the CPU sends, for a format
11
+ #
12
+ # Write track asks for its first byte at once and starts at the index
13
+ # pulse, giving up with LOST_DATA if the byte hasn't come three bytes
14
+ # later. $F5 writes a sync mark, an $A1, and the first of a run starts
15
+ # the CRC over; $F6 writes $C2; $F7 writes the CRC's two bytes. At
16
+ # the next index pulse the track under the head becomes the fields
17
+ # found in what went down (Track.parse).
18
+ module Tracks
19
+ LATE = 3
20
+
21
+ private
22
+
23
+ def start_type3
24
+ @type1 = false
25
+ @status = 0
26
+ @drq = false
27
+ @mode = @command.anybits?(0x10) ? WRITE : READ
28
+ resume_track unless spinning_up?(TRACK_INDEX)
29
+ end
30
+
31
+ def resume_track
32
+ if @mode == WRITE
33
+ if @mechanism.write_protected?
34
+ @status |= PROTECTED
35
+ return finish
36
+ end
37
+ @drq = true
38
+ end
39
+ @phase = @mode == WRITE ? WRITE_TRACK_START : TRACK_INDEX
40
+ return wait_index(@phase) if @command.nobits?(SETTLE_DELAY)
41
+
42
+ @due = @now + SETTLE
43
+ @settling = true
44
+ end
45
+
46
+ def wait_index(phase)
47
+ @settling = false
48
+ @phase = phase
49
+ @due = next_index
50
+ end
51
+
52
+ def track_index
53
+ return wait_index(TRACK_INDEX) if @settling
54
+
55
+ @bytes = (@mechanism.track || Track.new([])).raw
56
+ @index = 0
57
+ @phase = READ_TRACK
58
+ @due = @now + BYTE
59
+ end
60
+
61
+ def track_byte_read
62
+ @status |= LOST_DATA if @drq
63
+ @data = @bytes[@index]
64
+ @drq = true
65
+ @index += 1
66
+ @index == Track::LENGTH ? finish : @due += BYTE
67
+ end
68
+
69
+ def write_track_start
70
+ return wait_index(WRITE_TRACK_START) if @settling
71
+
72
+ @bytes = []
73
+ @syncs = []
74
+ @crc = 0xffff
75
+ @phase = WRITE_TRACK
76
+ return track_byte_written unless @drq
77
+
78
+ @late = true
79
+ @due = @now + (LATE * BYTE)
80
+ end
81
+
82
+ # The next byte from the data register goes under the head, a 0
83
+ # where DRQ went unanswered.
84
+ def track_byte_written
85
+ return lost if @late && @drq
86
+ return track_written if @bytes.length >= Track::LENGTH
87
+
88
+ @late = false
89
+ value = @drq ? 0 : @data
90
+ @status |= LOST_DATA if @drq
91
+ @drq = true
92
+ before = @bytes.length
93
+ lay_byte(value)
94
+ @due = @now + ((@bytes.length - before) * BYTE)
95
+ end
96
+
97
+ def lay_byte(value)
98
+ case value
99
+ when 0xf5
100
+ @crc = 0xffff unless @syncs.last
101
+ push_byte(Track::SYNC, true)
102
+ when 0xf6 then push_byte(0xc2, false)
103
+ when 0xf7
104
+ crc = @crc
105
+ push_byte(crc >> 8, false)
106
+ push_byte(crc & 0xff, false)
107
+ else push_byte(value, false)
108
+ end
109
+ end
110
+
111
+ def push_byte(value, sync)
112
+ @bytes << value
113
+ @syncs << sync
114
+ @crc = Track.crc([value], @crc)
115
+ end
116
+
117
+ def track_written
118
+ @drq = false
119
+ @mechanism.write(Track.parse(@bytes.first(Track::LENGTH), @syncs))
120
+ finish
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1581
5
+ class WD1772
6
+ # The disk turning under the head: the index pulses a phase waits
7
+ # for or counts, and the cycle the head comes to a place round the
8
+ # track.
9
+ module Turn
10
+ # The disk began or stopped turning: a phase waiting on the turn
11
+ # starts its wait again.
12
+ def spin_changed
13
+ case @phase
14
+ when SPIN_UP, MOTOR_IDLE, INDEX_INTERRUPT then count_index
15
+ when SEARCH then begin_search
16
+ when TRACK_INDEX, WRITE_TRACK_START then wait_index(@phase)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ # The motor starts: with h clear and the motor off, the command waits
23
+ # out six index pulses first, and the phase comes after them.
24
+ def spinning_up?(phase)
25
+ spin_up = !@motor_on && @command.nobits?(NO_SPIN_UP)
26
+ @motor_on = true
27
+ return false unless spin_up
28
+
29
+ @after_spin_up = phase
30
+ @phase = SPIN_UP
31
+ @count = 6
32
+ count_index
33
+ true
34
+ end
35
+
36
+ # Waits for @count index pulses, or for the disk to turn.
37
+ def count_index
38
+ @due = next_index
39
+ end
40
+
41
+ def index_counted
42
+ @count -= 1
43
+ return @due += Mechanism::REVOLUTION if @count.positive?
44
+
45
+ case @phase
46
+ when SPIN_UP then spun_up
47
+ when MOTOR_IDLE
48
+ @motor_on = false
49
+ idle
50
+ else
51
+ @intrq = true
52
+ idle
53
+ end
54
+ end
55
+
56
+ # The cycle the next index pulse starts, or NEVER while no disk turns.
57
+ def next_index = at_angle(0)
58
+
59
+ # The cycle the head next comes to +angle+, from the index.
60
+ def at_angle(angle)
61
+ return NEVER unless @mechanism.spinning?
62
+
63
+ delta = (angle - @mechanism.angle(@now)) % Mechanism::REVOLUTION
64
+ @now + (delta.zero? ? Mechanism::REVOLUTION : delta)
65
+ end
66
+
67
+ # The cycle the byte at +position+ round the track has passed under
68
+ # the head.
69
+ def byte_end(position) = at_angle(((position + 1) % Track::LENGTH) * BYTE)
70
+ end
71
+ end
72
+ end
73
+ end