While we were building the
mkos8 tool (predecessor to
os8-run), we built a set of facilities for driving SIMH and
OS/8 running under SIMH from the outside using Python, a very
powerful programming language well suited to scripting tasks. It
certainly beats writing PDP-8 code to achieve the same ends!
When someone on the mailing list asked for a way to automatically
drive a demo script he'd found online, it was natural to generalize
the core functionality of
mkos8 as a reusable Python class, then
write a script to make use of it. The result is
class simh, currently
used by six different scripts in the PiDP-8/I software distribution
os8-run and the
teco-pi-demo demo script.
The basis for this work is
pexpect the Python Expect library.
This document describes how
teco-pi-demo works, and through it, how
class simh works, with an eye toward teaching you how to reuse this
functionality for your own ends.
Because we do not install these components in the system's Python library path, you must modify that path to allow your script to find these components. Simply copy this invocation block into the top of your script:
import os import sys sys.path.insert (0, os.path.dirname (__file__) + '/../lib') sys.path.insert (0, os.getcwd () + '/lib') from pidp8i import * from simh import *
That adjusts the path, then imports all of the generic functionality
from the PiDP-8/I
lib directory into the current namespace.
sys.path.insert business assumes that your script is installed
into the PiDP-8/I's
bin directory alongside
teco-pi-demo. If you've
installed it somewhere else, you'll need to adjust these paths.
The first thing we'll do is start SIMH as a child process of our
Python script under control of an instance of
s = simh (dirs.build)
We call that instance
s for short, because we will be calling its
methods a lot in this script.
dirs.build as the first parameter to the constructor, which
tells it how to find the PDP-8 simulator program, derived from the
code shipped on GitHub by the SIMH project, configured and modified
for the needs of the PiDP-8/I project. We call this the child program,
as it is what
class simh controls from the outside.
There is an optional second parameter to the constructor, a Boolean
flag that controls whether
class simh starts the fully-featured
PiDP-8/I simulator or falls back to something closer to the pristine
upstream SIMH PDP-8 simulator. By default, we do the former, so
that the simulator updates front panel LEDs with internal simulator
state, and toggling front panel switches affect the internal state
of the simulator.
If you don't want the PiDP-8/I GPIO thread to run while your script
runs, pass True here instead, since this is the "skip GPIO" flag,
and its default is therefore False. We do that from programs like
os8-cp because we want them to run everywhere, even on
an RPi while another simulator is running; we also don't want the front
panel switches to affect these programs' operations. If your program
never runs on an RPi, passing True here will usuall make it run faster,
since the GPIO thread saps computer resources and so shouldn’t be
started if it isn’t needed.
The next step is to tell the
s object where to send its logging
s.set_logfile (os.fdopen (sys.stdout.fileno (), 'wb', 0))
Contrast the corresponding line in
os8-run which chooses whether to send
logging output to the console or to a log file:
s.set_logfile (open (dirs.log + 'os8-run' + '.log', 'ab') \ if not VERY_VERBOSE else os.fdopen (sys.stdout.fileno (), 'wb', 0))
Note that this more complicated scheme appends to the log file instead
of overwriting it because there are cases where
os8-run gets run
more than once with different script inputs, so we want to preserve
the prior script outputs, not keep only the latest.
Driving SIMH and OS/8
The basic control flow is:
- Send to SIMH text to act upon.
- Harvest results.
- Check results.
- Goto 1 or quit.
There are a number of helper methods and data structures to help in checking results.
pexpect can search replies for a regular expression string,
or a list of such strings, the helper methods use an array of compiled
The simh class contains two arrays,
with corresponding arrays of compiled regular expressions,
You can use the simh Class replies or define some for yourself. Example:
my_replies = [ ["Sample Reply", "Sample Reply\s+.*\n$", "False"], ["Fatal Error", "Fatal error was\s+.*\n$", "True"] ] my_replies_rex =  for item in my_replies: my_replies_rex.append(re.compile(item.encode()))
Often you want your replies in addition to the errors you might want from OS/8. In that case you'd do something like:
Of course the extend would appear before the computation of
Running SIMH or OS/8 commands
High level calls to run commands in SIMH can be made from
simh_cmd for SIMH commands and
os8_cmd for OS/8 commands.
These two methods default to searching results for replies in the relevant arrays. They return an index into the array that says which reply was received.
test_result method takes a reply number, an expected reply name, an array
replies and a string for helping identify the caller of the test. (There's also an
optional debug flag. These scripts can be difficult to debug.)
To see if the reply from running a command that would reply with
my_replies the code would be:
s.test_result(reply, "Fatal Error", my_replies, "myfunc")
The reply item at the index given by
reply is examined. And the desired reply
is matched against the first element of that item. If it matches,
True is returned,
False is returned. If the caller string is present, (in this case,
"myfunc", a message is printed if the reply doesn't match the expected reply.
If the caller string is the empty string, no message is printed. This makes it
easy to add error diagnostics without a lot of extra work.
Sometimes you want to try a couple different expected values, and don't want to print anything if there isn't a match. That's why we special case an empty caller string.
For SIMH and OS/8 command testing, there are convenience wrappers,
os8_test_result that use the relevant array so you don't have to keep typing it.
So the following two are equivalent:
s.test_result(reply, "Prompt", s._simh_replies, "myfunc", debug=True) s.simh_test_result(reply, "Prompt", "myfunc", debug=True)
Armed with an understanding of how we make calls into SIMH and OS/8, and how we test results, we're ready to continue our exploration.
Finding and Booting the OS/8 Media
If your program will use our OS/8 boot disk, you can find it
programmatically by using the
dirs.os8mo constant, which means "OS/8
media output directory", where "output" refers to the worldview of
dirs.os8mi, which points to the directory holding
the input media for
This snippet shows how to use it:
rk = os.path.join (dirs.os8mo, 'v3d.rk05') if not os.path.isfile (rk): print "Could not find " + rk + "; OS/8 media not yet built?" exit (1)
Now we attach the RK05 disk image to the PiDP-8/I simulator found by the
simh object and boot from it:
print "Booting " + rk + "..." s.simh_cmd ("att rk0 " + rk) s.simh_test_result (reply, "Prompt", "main 1") reply = s.simh_cmd ("boot rk0", s._os8_replies_rex) s.os8_test_result (reply, "Monitor Prompt", "main 2")
A couple subtle points: We issued a command to SIMH to attach the rk0
device. If we didn't get the SIMH prompt back,
main 1: Expecting Prompt. Instead got: Fatal Error
Then we issued the SIMH command to boot that device. We used the
method instead of the
simh_test_result method because we expected
the panoply of replies would more likely be from the OS/8 list.
After the simulator starts up, and we've confirmed we've got our OS/8 monitor prompt as a result, we send the first OS/8 command to start our demo.
s.os8_cmd ("R TECO") s.os8_test_result (reply, "Command Decoder Prompt", "main 2")
The bulk of
teco-pi-demo consists of more calls to
simh.cmd. Read the script if you want more examples.
IMPORTANT: When you specify the regular expression strings
for result matching, and want literal matches for characters that
are special to regular expressions such as dot
etc., you need to be preface the characterpair of backslashes.
Example: To match a literal dollar sign you would say
The operation of OS/8 under SIMH requires awareness of who is getting the commands: SIMH, the OS/8 Keyboard Monitor, the OS/8 Command Decoder, or some read/eval/print loop in a program being run.
Your use of the simh class needs to be mindful of this. Throughout this document every attempt has been made to be clear on which methods keep track of context switches for you and which do not.
Context Within a program under OS/8
If you've forgotten to exit a sub-program, that program will still be getting your subsequent commands instead of OS/8.
You may have a program that keeps running and asking for more input,
for example OS/8
PIP returns to the command decoder after each
There is a subtle issue with program interrupts: You need to check for the string that gets echoed when you do an interrupt. Otherwise pexpect can get confused.
Two methods that abstract this for you are provided:
os8_escape which send those interrupt characters, ask
pexpect to listen for their echo back (
$ comes back from
escape), and confirms a return to the OS/8 monitor.
Here is the implementation of
os8_ctrl_c as an example if you
need to run a sub-program with a different interrupt character:
#### os8_ctrl_c ################################################## # Return to OS/8 monitor using the ^C given escape character. # We need to listen for the ^C echo or else cfm_monitor gets confused. # Confirm we got our monitor prompt. # Optional caller argument enables a message if escape failed. # Note: OS/8 will respond to this escape IMMEDIATELY, # even if it has pending output. # You will need to make sure all pending output is in # a known state and the running program is quiescent # before calling this method. Otherwise pexpect may get lost. def os8_ctrl_c (self, caller = "", debug=False): self.os8_send_ctrl ("c") self._child.expect("\\^C") return self.os8_cfm_monitor (caller)
Sending Control Characters
Several OS/8 programs expect an Escape (a.k.a.
keystroke to do things. Examples are
(Yes, Escape is Ctrl-[. Now you can be the life of the party with that bit of trivia up your sleeve. Or maybe you go to better parties than I do.)
os8_send_ctrl method enables you to send arbitrary control
characters but it does not keep track of whether you're in the OS/8 or
SIMH context. Note also that the
e control character escapes to SIMH.
So avoid writing programs that need that control character as input.
Context Between SIMH and OS/8
It is important to make sure that commands intended for SIMH
go there, and not to OS/8 or any programs running under SIMH. The
simh_cmd methods keep track of context. If
you call the
simh_cmd method but aren't actually escaped out to
SIMH, an escape will be made for you, and the context change will be
If you issue
os8_cmd when OS/8 is not running, it will complain
and refuse to send the command.
The cleanest way to explicitly escape from OS/8 to SIMH
is to call
esc_to_simh. It manages the context switch, and tests
to see that you got the SIMH prompt. Example:
Subtle points: Calling
simh_cmd will leave you in SIMH. You will
need to resume OS/8 explicitly. There are a variety of ways
to do this.
Getting Back to OS/8 from SIMH
There are several ways to get back to the simulated OS/8 environment from SIMH context, each with different tradeoffs.
FIXME We used to have a lot of trouble with continue commands. We think they're all fixed now, so we can fully flesh out this section.
You saw the first one above: send a
boot rk0 command to SIMH. This
restarts OS/8 entirely. This is good if you need a clean environment.
If you need to save state between one run of OS/8 and the next, save it
to the RK05 disk pack or other SIMH media, then re-load it when OS/8
It's important to check that you got your OS/8 prompt so the recommended code looks like this:
reply = s.simh_cmd ("boot rk0", my_replies_rex) s.os8_test_result (reply, "Monitor Prompt", "myprog")
teco-pi-demo does it is to send a
cont command to SIMH:
A previous version of the simh class would sometime hang the
simulator unless a small delay were inserted before escaping to
the SIMH context. We believe this is no longer necessary.
However the problems with
cont made implementors gun shy
using it. Most code you will see does a restart with an explicit
confirmation we are at the OS/8 command level.
If your use of OS/8 is such that all required state is saved to disk
before re-entering OS/8, you can call the
simh_restart_os8 method to
avoid the need for a delay or a reboot.
It sends the simh command
go 7600 which is the traditional "restart
at the OS/8 entrypoint" commonly used from the PDP-8 front panel.
It then uses
os8_test_result to confirm that it got a monitor prompt.
simh_restart_os8 has an optional
caller argument to make it
quick and easy to print an error if returning to the monitor failed.
s.simh_restart_os8 (caller = "myprog")
os8-run uses this option extensively.
Sending without testing results.
At some point you always need to test your results and make sure you are where you think you are. Otherwise some corner case will trip up your use of the simh class, and the error message you will get is a 60 second pause, and a big backtrace.
But often you need to send and receive data in a much less structured
way than that used by
simh_cmd. Here is what you need:
||Send the given line blind without before or after checks.|
||Wait an amount of time proportional to what OS/8 should be able to handle on the hosting platform without overflowing the input buffer and dying.|
||Send a control character to OS/8. Use
||Send a string of characters to OS/8, and wait for os8_kbd_delay afterwards.|
||Add a carriage return to the given string and calk os8_send_str
correctly from the PiDP-8/I software's build directory, which has a somewhat different directory structure from the installation tree.
Written by and copyright © 2017-2020 by Warren Young and William Cattey. Licensed under the terms of the SIMH license.