cpu/
io.rs

1//! This module simulates the CPU behaviours which relate to I/O
2//! devices (connect/disconnect, the ways the IOS and TSD instructions
3//! with devices).
4//!
5//! ## Report Word
6//!
7//! The Report word of I/O units looks like this:
8//!
9//! | Special - Mag tape | Sequence Number | Flag  | Avail | Maint  | Connected |  EIA  | MISIND |    Mode |
10//! | ------------------ | --------------- | ----- | ----- | -----  | --------- | ----- | ------ | ------- |
11//! | 3.7-4.9            | 3.1-3.6         | 2.9   | 2.8   | 2.7    | 2.6       | 2.5   | 2.4    | 1.1-2.3 |
12//! | (12 bits)          | (6 bits)        |(1 bit)|(1 bit)|(1 bit )| (1 bit)   |(1 bit)|(1 bit) |(12 bits)|
13//!
14//!
15//! ## Sequence Number Assignments
16//! 0: Sequence which is run to start the computer (e.g. when "CODABO"
17//! or "START OVER" is pressed).
18//!
19//! 41: Handles various I/O alarm conditions.
20//! 42: Handles various trap conditions (see Users Handbook page 42).
21//! 47: Handles miscellaneous inputs
22//! 50: DATRAC (A/D converter)
23//! 51: Xerox printer
24//! 52: PETR (paper tape reader)
25//! 54: Interval timer
26//! 55: Light pen
27//! 60: Oscilloscope display
28//! 61: RNG
29//! 63: Punch
30//! 65: Lincoln Writer input
31//! 66: Lincoln Writer output
32//! 71: Lincoln Writer input
33//! 72: Lincoln Writer output
34//! 75: Misc output
35//! 76: Not for physical devices.
36//! 77: Not for physical devices.
37//!
38//! # Attached Units
39//!
40//! An "attached" unit is a unit which is actually present in the
41//! emulation (i.e. implemented in the code and the emulator has set
42//! it up).
43//!
44//! # Connected Units
45//!
46//! A "connected" unit is a unit which is attached and which the
47//! program has performed an action which puts it in the "connected"
48//! state as understood byt he TX-2.
49//!
50//! On the "real" TX-2, each unit had a `C` flip-flop (Users Handbook
51//! section 4-2.3) to indicate whether it was connected.  To connect a
52//! unit on the TX-2, it is necessary to execute an `IOSâ‚– 3XXXX`
53//! instruction (which connects unit _k_ and sets its mode to _XXXX_).
54//!
55//! See section 4-3.5 (THE IOS OPERATION - "INOUT SELECT") of the
56//! Users Handbook.
57//!
58use std::cell::RefCell;
59use std::collections::BTreeMap;
60use std::fmt::{self, Debug, Display, Formatter};
61use std::ops::Shl;
62use std::rc::Rc;
63use std::time::Duration;
64
65use serde::Serialize;
66use tracing::{Level, event, span};
67
68use crate::diagnostics::CurrentInstructionDiagnostics;
69
70use super::PETR;
71use super::alarm::{Alarm, AlarmDetails, Alarmer};
72use super::alarmunit::AlarmUnit;
73use super::changelog::ChangeIndex;
74use super::context::Context;
75use super::control::ControlUnit;
76use super::event::*;
77use super::types::*;
78use base::charset::LincolnState;
79use base::prelude::*;
80
81mod dev_lincoln_writer;
82mod dev_petr;
83mod pollq;
84
85use dev_lincoln_writer::{LincolnWriterInput, LincolnWriterOutput};
86pub(crate) use dev_petr::Petr;
87use pollq::PollQueue;
88
89/// When set, indicates that the controlling sequence has missed a data item.
90const IO_MASK_MISIND: Unsigned36Bit = Unsigned36Bit::MAX.and(0o_000_000_010_000);
91
92/// When set, indicates the unit is (already) connected.
93const IO_MASK_CONNECTED: Unsigned36Bit = Unsigned36Bit::MAX.and(0o_000_000_040_000);
94
95/// When set, indicates that the unit is in maintenance mode (i.e. is not available)
96const IO_MASK_MAINT: Unsigned36Bit = Unsigned36Bit::MAX.and(0o_000_000_100_000);
97
98/// When set, indicates that the unit's buffer is available for use (read or write)
99/// by the CPU.
100const IO_MASK_AVAIL: Unsigned36Bit = Unsigned36Bit::MAX.and(0o_000_000_200_000);
101
102/// When set, indicates that the unit wants attention.  That is, is ready for a
103/// TSD instruction or has just been connected.
104const IO_MASK_FLAG: Unsigned36Bit = Unsigned36Bit::MAX.and(0o_000_000_400_000);
105
106/// Indicates an unsuccessful I/O.
107#[derive(Debug)]
108pub enum TransferFailed {
109    /// The unit is connected but the buffer is still busy.
110    BufferNotFree,
111    /// An access to the unit raised an alarm (typically BUGAL).
112    Alarm(Alarm), // TODO: narrow this to only BUGAL to enforce that?
113}
114
115impl Display for TransferFailed {
116    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
117        match self {
118            TransferFailed::BufferNotFree => {
119                f.write_str("Unit buffer not available for use by the CPU")
120            }
121            TransferFailed::Alarm(alarm) => write!(f, "{alarm}"),
122        }
123    }
124}
125
126impl std::error::Error for TransferFailed {}
127
128pub enum UnitType {
129    /// Sequence 0
130    StartOver,
131    /// No associated unit; values 0o01..=0o37.
132    IndexRegister(SequenceNumber),
133    /// Hardware exists, or might in theory; values 0o40..
134    Hardware(SequenceNumber),
135
136    /// A software-only sequence.  Sequences 0o76 and 0o77 are
137    /// reserved for this purpose (Users Handbook, page 4-2).  TSD for
138    /// these sequences should perform the memory-cycle operation only
139    /// (Users Handbook, page 4-3).
140    SoftwareOnly(SequenceNumber),
141}
142
143impl UnitType {
144    pub fn sequence(&self) -> SequenceNumber {
145        match self {
146            UnitType::StartOver => SequenceNumber::ZERO,
147            UnitType::IndexRegister(s) | UnitType::SoftwareOnly(s) | UnitType::Hardware(s) => *s,
148        }
149    }
150}
151
152impl From<Unsigned6Bit> for UnitType {
153    fn from(n: Unsigned6Bit) -> UnitType {
154        match u8::from(n) {
155            0 => UnitType::StartOver,
156            1..=0o40 => UnitType::IndexRegister(n),
157            0o41..=0o75 => UnitType::Hardware(n),
158            0o76..=0o77 => UnitType::SoftwareOnly(n),
159            _ => unreachable!("unit number outside the range of an unsigned 6-bit quantity"),
160        }
161    }
162}
163
164/// Units report their status (which is used to construct their report
165/// word) with this struct.
166#[derive(Debug, Serialize)]
167pub struct UnitStatus {
168    pub special: Unsigned12Bit,
169    pub change_flag: Option<FlagChange>,
170    pub buffer_available_to_cpu: bool,
171    pub inability: bool,
172    pub missed_data: bool,
173    pub mode: Unsigned12Bit,
174
175    /// Indicates that the unit wishes to be polled for its status
176    /// before the indicated (simulated) duration has elapsed.
177    pub poll_after: Duration,
178
179    /// True for units which are input units.  Some devices (Lincoln
180    /// Writers for example) occupy two units, one for read (input)o
181    /// and the other for write (output).
182    pub is_input_unit: bool,
183}
184
185/// Status information for a unit which is actually connected.
186#[derive(Debug, Serialize)]
187pub struct ExtendedConnectedUnitStatus {
188    pub buffer_available_to_cpu: bool,
189    pub inability: bool,
190    pub missed_data: bool,
191    pub special: u16,
192    pub mode: u16,
193}
194
195/// The state of a unit (hardware belonging to a sequence).
196#[derive(Debug)]
197pub struct ExtendedUnitState {
198    /// Indicates that the unit's flag is raised.
199    pub flag: bool,
200
201    /// Sequence number value for this unit.
202    pub index_value: Signed18Bit,
203
204    /// Is the unit connected (in the `IOS` sense)?
205    pub connected: bool, // perhaps redundant with respect to `status.is_none()`.
206
207    /// Is the unit in maintenance?
208    pub in_maintenance: bool,
209
210    /// Name of the unit (not a feature of the real TX-2)
211    pub name: String,
212
213    /// A human-readable description of the state of the unit (not a feature of the real TX-2).
214    pub text_info: String,
215
216    /// `status` is None for units which are attached but not
217    /// currently connected.
218    pub status: Option<ExtendedConnectedUnitStatus>,
219}
220
221fn make_unit_report_word(
222    unit: Unsigned6Bit,
223    is_connected: bool,
224    is_maint: bool,
225    current_flag: bool,
226    status: &UnitStatus,
227) -> Unsigned36Bit {
228    let mut report: Unsigned36Bit = Unsigned36Bit::from(status.mode);
229    if status.missed_data {
230        report = report | IO_MASK_MISIND;
231    }
232    if is_connected {
233        report = report | IO_MASK_CONNECTED;
234    }
235    if is_maint {
236        report = report | IO_MASK_MAINT;
237    }
238    if status.buffer_available_to_cpu {
239        report = report | IO_MASK_AVAIL;
240    }
241    // A unit can raise but not lower its flag.
242    if current_flag || matches!(&status.change_flag, Some(FlagChange::Raise(_))) {
243        report = report | IO_MASK_FLAG;
244    }
245    report | Unsigned36Bit::from(unit).shl(18) | Unsigned36Bit::from(status.special).shl(24)
246}
247
248fn make_report_word_for_invalid_unit(unit: Unsigned6Bit, current_flag: bool) -> Unsigned36Bit {
249    make_unit_report_word(
250        unit,
251        false, // not connected
252        true,  // in maintenance
253        current_flag,
254        &UnitStatus {
255            special: Unsigned12Bit::ZERO,
256            change_flag: None,
257            buffer_available_to_cpu: false,
258            inability: true,
259            missed_data: false,
260            mode: Unsigned12Bit::ZERO,
261            poll_after: Duration::from_secs(10),
262            is_input_unit: false,
263        },
264    )
265}
266
267pub trait Unit {
268    /// Query the status of the unit.
269    fn poll(&mut self, ctx: &Context) -> UnitStatus;
270
271    /// Provide a text summary of the state of the device.
272    fn text_info(&self, ctx: &Context) -> String;
273
274    /// Connect the unit.
275    fn connect(&mut self, ctx: &Context, mode: Unsigned12Bit);
276
277    /// Disconnect the unit.
278    fn disconnect(&mut self, ctx: &Context);
279
280    /// Query the `TransferMode` of the unit.
281    fn transfer_mode(&self) -> TransferMode;
282
283    /// Handle a TSD on an input channel.
284    fn read(
285        &mut self,
286        ctx: &Context,
287        diagnostics: &CurrentInstructionDiagnostics,
288    ) -> Result<MaskedWord, TransferFailed>;
289
290    /// Handle a TSD on an output channel.
291    fn write(
292        &mut self,
293        ctx: &Context,
294        source: Unsigned36Bit,
295        diagnostics: &CurrentInstructionDiagnostics,
296    ) -> Result<Option<OutputEvent>, TransferFailed>;
297
298    /// Query the name of the unit.
299    fn name(&self) -> String;
300
301    /// Announce an input event for a unit.
302    fn on_input_event(
303        &mut self,
304        ctx: &Context,
305        event: InputEvent,
306    ) -> Result<InputFlagRaised, InputEventError>;
307}
308
309/// A unit which is known to the emulator (whether or not it is in
310/// "connected" mode).
311pub struct AttachedUnit {
312    inner: RefCell<Box<dyn Unit>>,
313
314    unit: Unsigned6Bit,
315
316    /// True for units which are input units.  Some devices (Lincoln
317    /// Writers for example) occupy two units, one for read (input)
318    /// and the other for write (output).
319    pub is_input_unit: bool,
320
321    pub connected: bool,
322    pub in_maintenance: bool,
323}
324
325impl AttachedUnit {
326    fn is_disconnected_output_unit(&self) -> bool {
327        (!self.is_input_unit) && (!self.connected)
328    }
329
330    /// Call function `f` on a unit, which must already be connected.
331    ///
332    /// If the unit is not connected, calling this method is a bug.
333    fn call_inner<A, F, T>(
334        &self,
335        what: &str,
336        alarmer: &mut A,
337        diagnostics: &CurrentInstructionDiagnostics,
338        f: F,
339    ) -> Result<T, Alarm>
340    where
341        A: Alarmer,
342        F: Fn(&dyn Unit) -> T,
343    {
344        if self.connected {
345            let output = f(self.inner.borrow().as_ref());
346            Ok(output)
347        } else {
348            Err(alarmer.always_fire(
349                Alarm {
350                    sequence: Some(self.unit),
351                    details: AlarmDetails::BUGAL {
352                        activity: crate::alarm::BugActivity::Io,
353                        diagnostics: diagnostics.clone(),
354                        message: format!(
355                            "attempt read-only use (for {}) of disconnected unit {:o}",
356                            what, self.unit
357                        ),
358                    },
359                },
360                diagnostics,
361            ))
362        }
363    }
364
365    /// Call function `f` on a unit, which must already be connected.
366    ///
367    /// If the unit is not connected, calling this method is a bug.
368    fn call_mut_inner<A, F, T>(
369        &self,
370        what: &str,
371        alarmer: &mut A,
372        diagnostics: &CurrentInstructionDiagnostics,
373        mut f: F,
374    ) -> Result<T, Alarm>
375    where
376        A: Alarmer,
377        F: FnMut(&mut dyn Unit) -> T,
378    {
379        if self.connected {
380            let output = f(self.inner.borrow_mut().as_mut());
381            Ok(output)
382        } else {
383            Err(alarmer.always_fire(
384                Alarm {
385                    sequence: Some(self.unit),
386                    details: AlarmDetails::BUGAL {
387                        activity: crate::alarm::BugActivity::Io,
388                        diagnostics: diagnostics.clone(),
389                        message: format!(
390                            "attempt read-write use (for {}) of disconnected unit {:o}",
391                            what, self.unit
392                        ),
393                    },
394                },
395                diagnostics,
396            ))
397        }
398    }
399
400    /// Query the status of an attached (but perhaps not connected) unit.
401    pub fn poll<A: Alarmer>(&self, ctx: &Context, _alarmer: &mut A) -> Result<UnitStatus, Alarm> {
402        Ok(self.inner.borrow_mut().poll(ctx))
403    }
404
405    /// Fetch a human-readable description of a unit (which may not be
406    /// connected).
407    fn text_info(&self, ctx: &Context) -> String {
408        self.inner.borrow().text_info(ctx)
409    }
410
411    /// Connect an attached (and perhaps already connected) unit.
412    pub fn connect(&self, ctx: &Context, mode: Unsigned12Bit) {
413        self.inner.borrow_mut().connect(ctx, mode);
414    }
415
416    /// Disconnect an attached unit, leaving it attached but not connected.
417    pub fn disconnect<A: Alarmer>(&self, ctx: &Context, _alarmer: &mut A) -> Result<(), Alarm> {
418        if !self.connected {
419            // It's permissible to disconnect an attached but not
420            // connected unit.  But we generate a warning, since the
421            // code shouldn't do it.
422            //
423            // TODO: eliminate this special case (i.e. don't
424            // disconnect a disconnected unit).
425            event!(
426                Level::WARN,
427                "disconnecting the not-connected unit {:o}",
428                self.unit
429            );
430        }
431        self.inner.borrow_mut().disconnect(ctx);
432        Ok(())
433    }
434
435    /// Query the `TransferMode` of a connected unit.
436    pub fn transfer_mode<A: Alarmer>(
437        &self,
438        alarmer: &mut A,
439        diagnostics: &CurrentInstructionDiagnostics,
440    ) -> Result<TransferMode, Alarm> {
441        self.call_inner("transfer_mode", alarmer, diagnostics, |unit: &dyn Unit| {
442            unit.transfer_mode()
443        })
444    }
445
446    /// Perform a TSD for a connected input unit.
447    pub fn read<A: Alarmer>(
448        &self,
449        ctx: &Context,
450        alarmer: &mut A,
451        diagnostics: &CurrentInstructionDiagnostics,
452    ) -> Result<MaskedWord, TransferFailed> {
453        match self.call_mut_inner("read", alarmer, diagnostics, |unit: &mut dyn Unit| {
454            unit.read(ctx, diagnostics)
455        }) {
456            Ok(Ok(mw)) => Ok(mw),
457            Ok(Err(e)) => Err(e),
458            Err(alarm) => Err(TransferFailed::Alarm(alarm)),
459        }
460    }
461
462    /// Perform a TSD for a connected output unit.
463    pub fn write(
464        &mut self,
465        ctx: &Context,
466        source: Unsigned36Bit,
467        diagnostics: &CurrentInstructionDiagnostics,
468    ) -> Result<Option<OutputEvent>, TransferFailed> {
469        self.inner.borrow_mut().write(ctx, source, diagnostics)
470    }
471
472    /// Announce an input event for a connected unit.
473    pub fn on_input_event(
474        &self,
475        ctx: &Context,
476        event: InputEvent,
477    ) -> Result<InputFlagRaised, InputEventError> {
478        self.inner.borrow_mut().on_input_event(ctx, event)
479    }
480
481    /// Query the name of an attached (but possibly not connected) unit.
482    pub fn name(&self) -> String {
483        self.inner.borrow().name()
484    }
485}
486
487impl Debug for AttachedUnit {
488    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
489        f.debug_struct("AttachedUnit")
490            .field("inner", &format_args!("<unit: {}>", self.name()))
491            .field("is_input_unit", &self.is_input_unit)
492            .field("connected", &self.connected)
493            .field("in_maintenance", &self.in_maintenance)
494            .finish()
495    }
496}
497
498/// Manages a collection of devices.  Does not actually correspond to
499/// a tangible physical component of the TX-2 system.
500#[derive(Debug)]
501pub struct DeviceManager {
502    devices: BTreeMap<Unsigned6Bit, AttachedUnit>,
503    poll_queue: PollQueue,
504    changes: ChangeIndex<Unsigned6Bit>,
505}
506
507/// Indicates whether or not a unit's input flag is raised.
508#[derive(Debug, Clone, Copy, PartialEq, Eq)]
509pub enum InputFlagRaised {
510    No,
511    Yes,
512}
513
514impl From<InputFlagRaised> for bool {
515    fn from(f: InputFlagRaised) -> bool {
516        match f {
517            InputFlagRaised::Yes => true,
518            InputFlagRaised::No => false,
519        }
520    }
521}
522
523impl DeviceManager {
524    #[must_use]
525    pub fn new() -> DeviceManager {
526        DeviceManager {
527            devices: BTreeMap::new(),
528            poll_queue: PollQueue::new(),
529            changes: ChangeIndex::default(),
530        }
531    }
532
533    #[must_use]
534    pub fn get(&self, unit_number: &Unsigned6Bit) -> Option<&AttachedUnit> {
535        self.devices.get(unit_number)
536    }
537
538    pub fn get_mut(&mut self, unit_number: &Unsigned6Bit) -> Option<&mut AttachedUnit> {
539        self.devices.get_mut(unit_number)
540    }
541
542    pub fn update_poll_time(&mut self, ctx: &Context, seq: SequenceNumber) {
543        if let Err(pollq::PollQueueUpdateFailure::UnknownSequence(seq)) =
544            self.poll_queue.update(seq, ctx.simulated_time)
545        {
546            // This happens when we complete an IOS or TSD
547            // instruction from a sequence that has no attached
548            // hardware.  For example software-only sequences such
549            // as 0o0, 0o76 or 0o77.  It's harmless.
550            match UnitType::from(seq) {
551                UnitType::SoftwareOnly(_) => (), // this is OK.
552                UnitType::Hardware(_) => (),     // No attached hardware, this is also OK.
553                UnitType::StartOver => {
554                    unreachable!("attempted to update poll time for STARTOVER unit");
555                }
556                UnitType::IndexRegister(n) => {
557                    unreachable!("attempted to update poll time for index register unit {n:o}");
558                }
559            }
560        }
561    }
562
563    fn get_extended_status<A: Alarmer>(
564        &self,
565        ctx: &Context,
566        unit: &AttachedUnit,
567        alarmer: &mut A,
568        index_value: Signed18Bit,
569    ) -> Result<ExtendedUnitState, Alarm> {
570        let (extended_unit_status, flag): (Option<ExtendedConnectedUnitStatus>, bool) =
571            if unit.connected {
572                let unit_status = unit.poll(ctx, alarmer)?;
573                let flag: bool = matches!(unit_status.change_flag, Some(FlagChange::Raise(_)));
574                let ext_status = ExtendedConnectedUnitStatus {
575                    buffer_available_to_cpu: unit_status.buffer_available_to_cpu,
576                    inability: unit_status.inability,
577                    missed_data: unit_status.missed_data,
578                    special: u16::from(unit_status.special),
579                    mode: u16::from(unit_status.mode),
580                };
581                (Some(ext_status), flag)
582            } else {
583                (None, false)
584            };
585        Ok(ExtendedUnitState {
586            flag,
587            connected: unit.connected,
588            in_maintenance: unit.in_maintenance,
589            name: unit.name(),
590            text_info: unit.text_info(ctx),
591            status: extended_unit_status,
592            index_value,
593        })
594    }
595
596    pub fn device_statuses(
597        &self,
598        ctx: &Context,
599        control: &mut ControlUnit,
600    ) -> Result<BTreeMap<Unsigned6Bit, ExtendedUnitState>, Alarm> {
601        let mut result: BTreeMap<Unsigned6Bit, ExtendedUnitState> = BTreeMap::new();
602        for (unit, attached) in &self.devices {
603            let xreg_value = match control
604                .inspect_registers()
605                .index_regs
606                .get(usize::from(*unit))
607            {
608                Some(xreg_value) => xreg_value,
609                None => {
610                    // This line should be unreachable as an
611                    // Unsigned6Bit value should never be out-of-range
612                    // as a lookup value into control.regs.index_regs.
613                    unreachable!("There is no index register for unit {unit:o}");
614                }
615            };
616            let ext_status = self.get_extended_status(ctx, attached, control, *xreg_value)?;
617            result.insert(*unit, ext_status);
618        }
619        Ok(result)
620    }
621
622    pub fn attach(
623        &mut self,
624        ctx: &Context,
625        unit_type: UnitType,
626        in_maintenance: bool,
627        mut unit: Box<dyn Unit>,
628    ) {
629        let unit_number = match unit_type {
630            UnitType::StartOver => {
631                panic!("Cannot attach hardware to unit 0");
632            }
633            UnitType::IndexRegister(reg) => {
634                panic!(
635                    "Cannot attach hardware to unit {reg:o}, it's reserved for use as an index register"
636                );
637            }
638            UnitType::SoftwareOnly(seq) => {
639                panic!("Cannot attach hardware to software-only unit {seq:o}");
640            }
641            UnitType::Hardware(seq) => seq,
642        };
643        self.mark_device_changed(unit_number);
644        let status: UnitStatus = unit.poll(ctx);
645        self.devices.insert(
646            unit_number,
647            AttachedUnit {
648                inner: RefCell::new(unit),
649                unit: unit_number,
650                is_input_unit: status.is_input_unit,
651                connected: false,
652                in_maintenance,
653            },
654        );
655        self.poll_queue.push(unit_number, status.poll_after);
656    }
657
658    pub fn on_input_event(
659        &mut self,
660        ctx: &Context,
661        unit_number: Unsigned6Bit,
662        input_event: InputEvent,
663    ) -> Result<InputFlagRaised, InputEventError> {
664        self.mark_device_changed(unit_number);
665        if let Some(attached) = self.devices.get_mut(&unit_number) {
666            attached.on_input_event(ctx, input_event)
667        } else {
668            // The simulator doesn't believe this unit exists in the
669            // system at all.
670            Err(InputEventError::InputOnUnattachedUnit)
671        }
672    }
673
674    /// Generate a report word for a unit.
675    pub fn report(
676        &mut self,
677        ctx: &Context,
678        // The sequence which is currently executing.
679        current_sequence: Option<SequenceNumber>,
680        // The sequence for which we are generating a report word.
681        unit: Unsigned6Bit,
682        current_flag: bool,
683        alarm_unit: &mut AlarmUnit,
684        diagnostics: &CurrentInstructionDiagnostics,
685    ) -> Result<Unsigned36Bit, Alarm> {
686        match self.devices.get_mut(&unit) {
687            Some(attached) => {
688                // Because the unit report word contains a `Connect`
689                // (2.6) and `Maintenance` bit (2.7) we need to be
690                // able to collect status from a unit which is
691                // attached but not otherwise usable.
692                let unit_status = attached.poll(ctx, alarm_unit)?;
693                self.poll_queue.push(unit, unit_status.poll_after);
694                Ok(make_unit_report_word(
695                    unit,
696                    attached.connected,
697                    attached.in_maintenance,
698                    current_flag,
699                    &unit_status,
700                ))
701            }
702            None => {
703                alarm_unit.fire_if_not_masked(
704                    Alarm {
705                        sequence: current_sequence,
706                        details: AlarmDetails::IOSAL {
707                            unit,
708                            operand: None,
709                            message: format!("unit {unit:o} is not known"),
710                        },
711                    },
712                    diagnostics,
713                )?;
714                // IOSAL is masked.
715                Ok(make_report_word_for_invalid_unit(unit, current_flag))
716            }
717        }
718    }
719
720    pub(super) fn poll(
721        &mut self,
722        ctx: &Context,
723        alarm_unit: &mut AlarmUnit,
724    ) -> Result<(u64, Option<Alarm>, Option<Duration>), Alarm> {
725        let system_time = &ctx.simulated_time;
726        let mut raised_flags: u64 = 0;
727        let mut alarm: Option<Alarm> = None;
728        let mut next_poll: Option<Duration> = None;
729
730        event!(Level::DEBUG, "poll_queue is: {:?}", &self.poll_queue);
731        loop {
732            match self.poll_queue.peek() {
733                None => {
734                    break;
735                }
736                Some((_, poll_time)) => {
737                    if poll_time > system_time {
738                        // Next poll action is not due yet.
739                        event!(
740                            Level::TRACE,
741                            "poll: next poll is not due yet; due={:?}, now={:?}",
742                            poll_time,
743                            system_time
744                        );
745                        next_poll = Some(*poll_time);
746                        break;
747                    }
748                }
749            }
750            match self.poll_queue.pop() {
751                None => unreachable!(),
752                Some((devno, poll_time)) => {
753                    let span = span!(Level::ERROR, "poll", unit=%devno);
754                    let _enter = span.enter();
755
756                    // We don't know for sure that there will be an
757                    // update to the (console-visible) state of the
758                    // device, but it might be the case.  So we mark
759                    // the device has changed so that the UI collects
760                    // the possibly-updated state.
761                    self.mark_device_changed(devno);
762
763                    event!(
764                        Level::DEBUG,
765                        "poll: next poll is now due  for unit {devno:o}; due={:?}, now={:?}",
766                        poll_time,
767                        system_time
768                    );
769                    assert!(poll_time <= *system_time);
770
771                    let attached = match self.devices.get_mut(&devno) {
772                        Some(attached) => attached,
773                        None => {
774                            event!(
775                                Level::ERROR,
776                                "Device {:?} is present in the polling queue but not in the device map; ignoring it",
777                                devno
778                            );
779                            continue;
780                        }
781                    };
782                    if !attached.connected {
783                        event!(Level::TRACE, "not polling unit. it's not connected");
784                        continue;
785                    }
786                    assert!(!attached.in_maintenance); // cannot connect in-maint devices.
787                    event!(
788                        Level::TRACE,
789                        "polling unit at system time {:?}",
790                        system_time
791                    );
792                    let unit_status = attached.poll(ctx, alarm_unit)?;
793                    event!(Level::TRACE, "unit {devno:02o} status is {unit_status:?}");
794                    self.poll_queue.push(devno, unit_status.poll_after);
795                    if let Some(FlagChange::Raise(reason)) = unit_status.change_flag {
796                        event!(
797                            Level::DEBUG,
798                            "unit {devno:02o} has raised its flag: {reason}"
799                        );
800                        raised_flags |= 1 << u8::from(devno);
801                    }
802                    if alarm.is_none() {
803                        // TODO: support masking for alarms (hardware and
804                        // software masking are both available; either should
805                        // be able to mask it).
806                        if unit_status.inability {
807                            alarm = Some(Alarm {
808                                sequence: Some(devno),
809                                details: AlarmDetails::IOSAL {
810                                    unit: devno,
811                                    operand: None,
812                                    message: format!("unit {devno:o} reports inability (EIA)"),
813                                },
814                            });
815                        } else if unit_status.missed_data {
816                            alarm = Some(Alarm {
817                                sequence: None,
818                                details: AlarmDetails::MISAL {
819                                    affected_unit: devno,
820                                },
821                            });
822                        }
823                    }
824                }
825            }
826        }
827        Ok((raised_flags, alarm, next_poll))
828    }
829
830    pub fn disconnect_all<A: Alarmer>(
831        &mut self,
832        ctx: &Context,
833        alarmer: &mut A,
834    ) -> Result<(), Alarm> {
835        let mut changes: Vec<Unsigned6Bit> = Vec::with_capacity(self.devices.len());
836        for attached in self.devices.values_mut() {
837            if attached.connected {
838                changes.push(attached.unit);
839                attached.disconnect(ctx, alarmer)?;
840                attached.connected = false;
841            }
842        }
843        for unit in changes.into_iter() {
844            self.mark_device_changed(unit);
845        }
846        Ok(())
847    }
848
849    pub fn disconnect(
850        &mut self,
851        ctx: &Context,
852        device: &Unsigned6Bit,
853        alarm_unit: &mut AlarmUnit,
854        diagnostics: &CurrentInstructionDiagnostics,
855    ) -> Result<(), Alarm> {
856        let mut changed = false;
857        if *device == u6!(0o42) {
858            return Ok(());
859        }
860        let result = match self.devices.get_mut(device) {
861            Some(attached) => {
862                if attached.connected {
863                    attached.connected = false;
864                } else {
865                    event!(
866                        Level::WARN,
867                        "disconnecting unit {device:o} but it is not connected"
868                    );
869                }
870                changed = true;
871                attached.disconnect(ctx, alarm_unit)
872            }
873            None => {
874                alarm_unit.fire_if_not_masked(
875                    Alarm {
876                        sequence: Some(*device),
877                        details: AlarmDetails::IOSAL {
878                            unit: *device,
879                            operand: None,
880                            message: format!("Attempt to disconnect missing unit {device:o}"),
881                        },
882                    },
883                    diagnostics,
884                )?;
885                Ok(()) // IOSAL is masked, carry on.
886            }
887        };
888        if changed {
889            self.mark_device_changed(*device);
890        }
891        result
892    }
893
894    pub fn connect(
895        &mut self,
896        ctx: &Context,
897        calling_sequence: Option<SequenceNumber>,
898        device: &Unsigned6Bit,
899        mode: Unsigned12Bit,
900        alarm_unit: &mut AlarmUnit,
901        diagnostics: &CurrentInstructionDiagnostics,
902    ) -> Result<Option<FlagChange>, Alarm> {
903        self.mark_device_changed(*device);
904        match self.devices.get_mut(device) {
905            Some(attached) => {
906                if attached.in_maintenance {
907                    event!(
908                        Level::INFO,
909                        "attempt to connect in-maintenance unit {device:o}, raising IOSAL"
910                    );
911                    Err(Alarm {
912                        sequence: Some(*device),
913                        details: AlarmDetails::IOSAL {
914                            unit: *device,
915                            operand: None,
916                            message: format!("Attempt to connect in-maint unit {device:o}"),
917                        },
918                    })
919                } else {
920                    event!(Level::DEBUG, "connecting unit {device:o}");
921                    // If the unit being connected is an OUTPUT unit,
922                    // and the unit was not already connected, then
923                    // its flag is raised (Users Handbook page 4-7).
924                    let flag_change = if attached.is_disconnected_output_unit() {
925                        event!(
926                            Level::DEBUG,
927                            "Connecting previously-unconnected output unit {device:o}, so raising its flag"
928                        );
929                        Some(FlagChange::Raise(
930                            "attaching a previously-disconnected output unit",
931                        ))
932                    } else {
933                        None
934                    };
935                    attached.connect(ctx, mode);
936                    attached.connected = true;
937                    Ok(flag_change)
938                }
939            }
940            None => {
941                event!(
942                    Level::WARN,
943                    "attempt to connect nonexistent unit {device:o}"
944                );
945                alarm_unit.fire_if_not_masked(
946                    Alarm {
947                        sequence: calling_sequence, // NOTE: not the same as *device.
948                        details: AlarmDetails::IOSAL {
949                            unit: *device,
950                            operand: None,
951                            message: format!("Attempt to connect missing unit {device:o}"),
952                        },
953                    },
954                    diagnostics,
955                )?;
956                Ok(None) // IOSAL is masked, carry on
957            }
958        }
959    }
960
961    pub fn mark_device_changed(&mut self, unit: Unsigned6Bit) {
962        self.changes.add(unit);
963    }
964
965    pub fn drain_changes(
966        &mut self,
967        ctx: &Context,
968        control: &mut ControlUnit,
969    ) -> Result<BTreeMap<Unsigned6Bit, ExtendedUnitState>, Alarm> {
970        let mut result: BTreeMap<Unsigned6Bit, ExtendedUnitState> = BTreeMap::new();
971        for unit_with_change in self.changes.drain().into_iter() {
972            if let Some(attached_unit) = self.get(&unit_with_change) {
973                let xreg_value = match control
974                    .inspect_registers()
975                    .index_regs
976                    .get(usize::from(unit_with_change))
977                {
978                    Some(xreg_value) => xreg_value,
979                    None => {
980                        // This line should be unreachable as an
981                        // Unsigned6Bit value should never be out-of-range
982                        // as a lookup value into control.regs.index_regs.
983                        unreachable!("There is no index register for unit {unit_with_change:o}");
984                    }
985                };
986                match self.get_extended_status(ctx, attached_unit, control, *xreg_value) {
987                    Ok(state) => {
988                        result.insert(unit_with_change, state);
989                    }
990                    Err(alarm) => {
991                        return Err(alarm);
992                    }
993                }
994            }
995        }
996        Ok(result)
997    }
998}
999
1000impl Default for DeviceManager {
1001    /// We're implementing this mainly to keep clippy happy.
1002    fn default() -> DeviceManager {
1003        Self::new()
1004    }
1005}
1006
1007pub fn set_up_peripherals(ctx: &Context, devices: &mut DeviceManager) {
1008    const NOT_IN_MAINTENANCE: bool = false;
1009    fn attach_lw(
1010        ctx: &Context,
1011        input_unit: UnitType,
1012        output_unit: UnitType,
1013        devices: &mut DeviceManager,
1014    ) {
1015        let state = Rc::new(RefCell::new(LincolnState::default()));
1016        let output = Box::new(LincolnWriterOutput::new(
1017            output_unit.sequence(),
1018            state.clone(),
1019        ));
1020        let input = Box::new(LincolnWriterInput::new(input_unit.sequence(), state));
1021        devices.attach(ctx, output_unit, NOT_IN_MAINTENANCE, output);
1022        devices.attach(ctx, input_unit, NOT_IN_MAINTENANCE, input);
1023    }
1024
1025    devices.attach(
1026        ctx,
1027        UnitType::from(PETR),
1028        NOT_IN_MAINTENANCE,
1029        Box::new(Petr::new()),
1030    );
1031    attach_lw(
1032        ctx,
1033        UnitType::from(u6!(0o65)),
1034        UnitType::from(u6!(0o66)),
1035        devices,
1036    );
1037}