Skip to main content

cpu/io/
dev_petr.rs

1//! Photoelectric Paper tape reader, unit 52
2//!
3//! The paper tape reader has 7 channels.
4//!
5//! Tape movement can occur in either of two directions, in the bin
6//! direction and in the reel direction.  Tape reads occur in the
7//! "reel" direction.  The modes of operation of the motor in the
8//! "bin" direction both stop and reverse direction when the END MARK
9//! is detected.  In other words, this is analogous to rewinding to the
10//! beginning-of-file in preparation for reading the file.
11//!
12//! The reader can read both strip tape and reel tape, and does so at
13//! different speeds (because of the different levels of rubustness of
14//! the two types of tape), but we operate as if all tapes are read
15//! at the same speed.
16//!
17//! We do not currently simulate acceleration/deceleration of the
18//! tape.  On start, reading begins immediately at full speed, and on
19//! stop, the tape movement stops immediately.  This will likely
20//! change in the future.
21//!
22//! Lincoln Lab Group Report 51-8 "The Lincoln Writer" states (page
23//! 13) that the "END MARK" punched by the LW is the 6-bit code octal
24//! 72 without a 7th hole punched on the tape.
25use std::fmt::Write;
26use std::fmt::{self, Debug, Display, Formatter};
27use std::time::Duration;
28
29use std::cmp;
30
31use conv::*;
32use tracing::{Level, event};
33
34use crate::diagnostics::CurrentInstructionDiagnostics;
35
36use super::*;
37use super::{TransferFailed, Unit, UnitStatus};
38
39/// Is the tape motor running?
40#[derive(Debug, Clone, Copy, Eq, PartialEq)]
41enum Activity {
42    Stopped,
43    Started,
44}
45
46impl Display for Activity {
47    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
48        f.write_str(match self {
49            Activity::Stopped => "stopped",
50            Activity::Started => "running",
51        })
52    }
53}
54
55#[derive(Debug, Clone, Copy, Eq, PartialEq)]
56enum Direction {
57    // Capstan drive is running in the bin direction (as in
58    // immediately after IOS 30104, before END MARK has been read).
59    Bin,
60
61    // Capstan drive is running in the reel direction (as in after IOS
62    // 30104, after END MARK has been read).
63    Reel,
64}
65
66impl Display for Direction {
67    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
68        f.write_str(match self {
69            Direction::Bin => "bin",
70            Direction::Reel => "reel",
71        })
72    }
73}
74
75fn next_line_time(direction: Direction, system_time: &Duration) -> Duration {
76    // The reader reads at between 400 and 2500 lines per second.
77    //
78    // We don't want to avoidably generate missing data alarms, so
79    // for now, we simulate data reading at the slowest speed and
80    // non-reading tape movements at the highest speed.
81    *system_time
82        + match direction {
83            Direction::Bin => {
84                // At 2500 lines per second, the interval between
85                // lines is 1s / 2500 = 400 microseconds.
86                Duration::from_micros(400)
87            }
88            Direction::Reel => {
89                // At 400 lines per second, the interval between lines
90                // is 1s / 400 = 2500 microseconds.
91                Duration::from_micros(2500)
92            }
93        }
94}
95
96pub(crate) struct Petr {
97    // Activity and direction cannot just be left encoded in mode,
98    // because we need to be able to start/stop the motor and change
99    // direction without changing the mode value (since the programmer
100    // controls that).
101    activity: Activity,
102    direction: Direction,
103    tape_data: Vec<u8>,
104    tape_pos: usize,
105    data: Option<u8>,
106    already_warned_eof: bool,
107    read_failed: bool,
108    overrun: bool,
109    time_of_next_read: Option<Duration>,
110    connected_at_system_time: Option<Duration>,
111    connected_at_elapsed_time: Option<Duration>,
112    rewind_line_counter: usize,
113    mode: Unsigned12Bit,
114}
115
116impl Debug for Petr {
117    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
118        f.debug_struct("Petr")
119            .field("activity", &self.activity)
120            .field("direction", &self.direction)
121            .field("tape_data", &self.tape_data)
122            .field("tape_pos", &self.tape_pos)
123            .field("data", &self.data)
124            .field("already_warned_eof", &self.already_warned_eof)
125            .field("read_failed", &self.read_failed)
126            .field("overrun", &self.overrun)
127            .field("time_of_next_read", &self.time_of_next_read)
128            .field("connected_at_system_time", &self.connected_at_system_time)
129            .field("connected_at_elapsed_time", &self.connected_at_elapsed_time)
130            .field("rewind_line_counter", &self.rewind_line_counter)
131            .field("mode", &self.mode)
132            .finish_non_exhaustive()
133    }
134}
135
136impl Petr {
137    pub(crate) fn new() -> Petr {
138        Petr {
139            activity: Activity::Stopped,
140            direction: Direction::Bin,
141            tape_data: Vec::new(),
142            tape_pos: 0,
143            data: None,
144            already_warned_eof: false,
145            read_failed: false,
146            overrun: false,
147            time_of_next_read: None,
148            connected_at_system_time: None,
149            connected_at_elapsed_time: None,
150            rewind_line_counter: 0,
151            mode: Unsigned12Bit::ZERO,
152        }
153    }
154
155    fn next_poll_time(&mut self, system_time: &Duration) -> Duration {
156        match self.time_of_next_read {
157            Some(t) => {
158                let result = cmp::max(t, *system_time);
159                if let Some(interval) = result.checked_sub(*system_time) {
160                    event!(
161                        Level::DEBUG,
162                        "next_poll_time: please poll after {interval:?} at system time {t:?}"
163                    );
164                }
165                result
166            }
167            None => {
168                let interval = Duration::from_secs(1);
169                let next = *system_time + interval;
170                event!(
171                    Level::DEBUG,
172                    "next_poll_time: time_of_next_read is None, using default {system_time:?}+{interval:?}={:?}; self={:?}",
173                    next,
174                    &self
175                );
176                next
177            }
178        }
179    }
180
181    fn do_rewind(&mut self) {
182        match self.rewind_line_counter.checked_sub(1) {
183            None | Some(0) => {
184                // We reached - or were already at - the END MARK,
185                // reverse direction.
186                event!(Level::INFO, "reached the end mark, reversing direction");
187                assert!(self.direction == Direction::Bin);
188                self.direction = Direction::Reel;
189            }
190            Some(reduced_by_1) => {
191                self.rewind_line_counter = reduced_by_1;
192                event!(
193                    Level::DEBUG,
194                    "rewound over a line: {} more to go",
195                    self.rewind_line_counter
196                );
197            }
198        }
199    }
200
201    fn do_read(&mut self) {
202        // A line of the simulated tape should have appeared under the
203        // read head.
204        match self.tape_data.get(self.tape_pos) {
205            None => {
206                // At EOF.  We don't stop the motor, but if the
207                // real PETR device can detect when the whole tape
208                // has already passed through, perhaps we should.
209                if self.already_warned_eof {
210                    event!(
211                        Level::DEBUG,
212                        "reading again at end-of-file at position {}",
213                        self.tape_pos
214                    );
215                } else {
216                    self.already_warned_eof = true;
217                    event!(
218                        Level::WARN,
219                        "end-of-file on tape input file at position {}",
220                        self.tape_pos
221                    );
222                }
223                self.time_of_next_read = None;
224            }
225            Some(byte) => {
226                event!(
227                    Level::DEBUG,
228                    "read a byte at file position {}: {:?}",
229                    self.tape_pos,
230                    byte
231                );
232                self.already_warned_eof = false;
233                if !self.overrun {
234                    self.overrun = self.data.is_some();
235                    if self.overrun {
236                        event!(Level::WARN, "PETR input overrun");
237                    }
238                }
239                self.data = Some(*byte);
240                self.tape_pos += 1;
241            }
242        }
243    }
244
245    fn maybe_simulate_event(&mut self, system_time: &Duration) {
246        event!(
247            Level::TRACE,
248            "maybe_simulate_event: activity={:?}",
249            &self.activity
250        );
251        match self.activity {
252            Activity::Started => {
253                if let Some(t) = self.time_of_next_read
254                    && t > *system_time
255                {
256                    // The next line has not yet appeared under the read
257                    // head.
258                    let to_wait = || t - *system_time;
259                    event!(
260                        Level::TRACE,
261                        "motor running ({}) but next line will not be read for {:?} yet",
262                        self.direction,
263                        to_wait()
264                    );
265                    return;
266                }
267                match self.direction {
268                    Direction::Bin => {
269                        event!(Level::TRACE, "motor running, doing rewind action");
270                        self.do_rewind();
271                    }
272                    Direction::Reel => {
273                        event!(Level::TRACE, "motor running, doing read action");
274                        self.do_read();
275                    }
276                }
277                // do_read may have stopped the motor, so take account
278                // of that.
279                self.time_of_next_read = match self.activity {
280                    Activity::Started => Some(next_line_time(self.direction, system_time)),
281                    Activity::Stopped => None,
282                }
283            }
284            Activity::Stopped => {
285                // The motor is not running.  So no events (of lines
286                // passing under the photodetector) will happen.
287                event!(Level::TRACE, "motor stopped, nothing more to simulate");
288                self.time_of_next_read = None;
289            }
290        }
291    }
292
293    fn transfer_mode(&self) -> TransferMode {
294        if self.mode & 0o02 != 0 {
295            TransferMode::Assembly
296        } else {
297            TransferMode::Exchange
298        }
299    }
300}
301
302struct Throughput {
303    lines_per_second: f64,
304    total_seconds: f64,
305}
306
307fn compute_throughput(
308    pos: usize,
309    connect_time: Option<Duration>,
310    now: Duration,
311) -> Option<Throughput> {
312    if let Some(connected_at) = connect_time
313        && let Some(elapsed) = now.checked_sub(connected_at)
314    {
315        let elapsed = elapsed.as_secs_f64();
316
317        // It seems that in a WASM build, f64::value_from(usize)
318        // is irrefutable.  But clippy doesn't warn about it in
319        // non-WASM builds because it's not always irrefutable.
320        #[allow(irrefutable_let_patterns)]
321        if let Ok(n) = f64::value_from(pos) {
322            let throughput = n / elapsed;
323            return Some(Throughput {
324                lines_per_second: throughput,
325                total_seconds: elapsed,
326            });
327        }
328    }
329    None
330}
331
332impl Unit for Petr {
333    fn poll(&mut self, ctx: &Context) -> UnitStatus {
334        let system_time = &ctx.simulated_time;
335        event!(Level::TRACE, "poll called at system time  {system_time:?}");
336
337        self.maybe_simulate_event(system_time);
338        let data_ready: bool = self.data.is_some();
339        let poll_after = self.next_poll_time(system_time);
340        event!(Level::TRACE, "PETR poll: poll_after={poll_after:?}");
341        UnitStatus {
342            special: Unsigned12Bit::ZERO,
343            change_flag: if data_ready {
344                Some(FlagChange::Raise("data is ready"))
345            } else {
346                None
347            },
348            buffer_available_to_cpu: data_ready,
349            inability: self.read_failed,
350            missed_data: self.overrun,
351            mode: self.mode,
352            poll_after,
353            is_input_unit: true,
354        }
355    }
356
357    fn connect(&mut self, ctx: &Context, mode: Unsigned12Bit) {
358        let system_time = &ctx.simulated_time;
359        self.connected_at_elapsed_time = Some(ctx.real_elapsed_time);
360        self.connected_at_system_time = Some(ctx.simulated_time);
361        self.direction = if mode & 0o04 != 0 {
362            Direction::Bin
363        } else {
364            Direction::Reel
365        };
366        self.activity = if mode & 0o100 != 0 {
367            self.time_of_next_read = Some(next_line_time(self.direction, system_time));
368            Activity::Started
369        } else {
370            // While the motor is not running, no data will arrive.
371            self.time_of_next_read = None;
372            Activity::Stopped
373        };
374        self.tape_pos = 0;
375        self.mode = mode;
376        let transfer_mode_name = match self.transfer_mode() {
377            TransferMode::Assembly => "assembly",
378            TransferMode::Exchange => "exchange",
379        };
380        event!(
381            Level::INFO,
382            "PETR connected; motor {}, direction {}; {} mode {:o} (time_of_next_read={:?})",
383            self.activity,
384            self.direction,
385            transfer_mode_name,
386            self.mode,
387            self.time_of_next_read,
388        );
389    }
390
391    fn transfer_mode(&self) -> TransferMode {
392        self.transfer_mode()
393    }
394
395    fn read(
396        &mut self,
397        ctx: &Context,
398        _diagnostics: &CurrentInstructionDiagnostics,
399    ) -> Result<MaskedWord, TransferFailed> {
400        let system_time = &ctx.simulated_time;
401        match self.data.take() {
402            None => {
403                event!(Level::DEBUG, "no data is ready yet");
404                Err(TransferFailed::BufferNotFree)
405            }
406            Some(byte) => {
407                // This calculation of time_of_next_read is not right,
408                // as the interval is between physical paper-tape
409                // lines, not between I/O instrucitons.
410                self.time_of_next_read = Some(next_line_time(self.direction, system_time));
411                event!(Level::DEBUG, "read value {:03o}", byte & 0o77);
412                Ok(MaskedWord {
413                    bits: Unsigned36Bit::from(byte & 0o77),
414                    mask: u36!(0o77),
415                })
416            }
417        }
418    }
419
420    fn write(
421        &mut self,
422        _ctx: &Context,
423        _source: Unsigned36Bit,
424        diagnostics: &CurrentInstructionDiagnostics,
425    ) -> Result<Option<OutputEvent>, TransferFailed> {
426        unreachable!("attempted to write to the paper tape reader ({diagnostics})")
427    }
428
429    fn name(&self) -> String {
430        "PETR photoelectric paper tape reader".to_string()
431    }
432
433    fn disconnect(&mut self, _ctx: &Context) {
434        event!(Level::INFO, "PETR disconnecting");
435        self.activity = Activity::Stopped;
436    }
437
438    fn on_input_event(
439        &mut self,
440        _ctx: &Context,
441        event: InputEvent,
442    ) -> Result<InputFlagRaised, InputEventError> {
443        if let InputEvent::PetrMountPaperTape { data } = event {
444            event!(Level::DEBUG, "Mounting a tape ({} bytes)", data.len());
445            self.tape_data = data;
446            // The input flag is raised only when input is actually
447            // available.  That is, when a line passes under the
448            // detector.
449            Ok(InputFlagRaised::No)
450        } else {
451            Err(InputEventError::InputEventNotValidForDevice)
452        }
453    }
454
455    fn text_info(&self, ctx: &Context) -> String {
456        let build = || -> Result<String, std::fmt::Error> {
457            let mut result = String::new();
458            write!(result, "Motor {}", self.activity)?;
459            match self.activity {
460                Activity::Started => {
461                    write!(result, " (direction {})", self.direction)?;
462                }
463                Activity::Stopped => (),
464            }
465            write!(result, ". ")?;
466            if self.tape_data.is_empty() {
467                write!(result, "No tape (or blank tape) loaded. ")?;
468            } else {
469                write!(
470                    result,
471                    "Loaded tape has {} lines, we have read {}. ",
472                    self.tape_data.len(),
473                    self.tape_pos
474                )?;
475                if let Some(throughput) = compute_throughput(
476                    self.tape_pos,
477                    self.connected_at_system_time,
478                    ctx.simulated_time,
479                ) {
480                    write!(
481                        result,
482                        "Emulated duration {:.1} seconds, so emulated throughput is {:.1} lines/sec. ",
483                        throughput.total_seconds, throughput.lines_per_second,
484                    )?;
485                }
486                if let Some(throughput) = compute_throughput(
487                    self.tape_pos,
488                    self.connected_at_elapsed_time,
489                    ctx.real_elapsed_time,
490                ) {
491                    write!(
492                        result,
493                        "Real duration {:.1} seconds, so real throughput is {:.1} lines/sec.",
494                        throughput.total_seconds, throughput.lines_per_second,
495                    )?;
496                }
497            }
498            Ok(result)
499        };
500        build().expect("write! calls on a String should always succeed")
501    }
502}