/*  altairz80_sio.c: MITS Altair serial I/O card

    Copyright (c) 2002-2023, Peter Schorn

    Permission is hereby granted, free of charge, to any person obtaining a
    copy of this software and associated documentation files (the "Software"),
    to deal in the Software without restriction, including without limitation
    the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
    PETER SCHORN BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
    IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    Except as contained in this notice, the name of Peter Schorn shall not
    be used in advertising or otherwise to promote the sale, use or other dealings
    in this Software without prior written authorization from Peter Schorn.

    Based on work by Charles E Owen (c) 1997

    These functions support a simulated MITS 2SIO interface card.
    The card had two physical I/O ports which could be connected
    to any serial I/O device that would connect to a current loop,
    RS232, or TTY interface. Available baud rates were jumper
    selectable for each port from 110 to 9600.

    All I/O is via programmed I/O. Each device has a status port
    and a data port. A write to the status port can select
    some options for the device (0x03 will reset the port).
    A read of the status port gets the port status:

    +---+---+---+---+---+---+---+---+
    | X | X | X | X | X | X | O | I |
    +---+---+---+---+---+---+---+---+

    I - A 1 in this bit position means a character has been received
        on the data port and is ready to be read.
    O - A 1 in this bit means the port is ready to receive a character
        on the data port and transmit it out over the serial line.

    A read to the data port gets the buffered character, a write
    to the data port writes the character to the device.
*/

#include "altairz80_defs.h"
#include "sim_tmxr.h"

uint8 *URLContents(const char *URL, uint32 *length);
#ifndef URL_READER_SUPPORT
uint8 *URLContents(const char *URL, uint32 *length) {
    *length = 0;
    return (uint8*)NULL;
}
#endif

/* Debug flags */
#define IN_MSG              (1 << 0)
#define OUT_MSG             (1 << 1)
#define CMD_MSG             (1 << 2)
#define VERBOSE_MSG         (1 << 3)
#define BUFFER_EMPTY_MSG    (1 << 4)

#define UNIT_V_SIO_ANSI     (UNIT_V_UF + 0)     /* ANSI mode, strip bit 8 on output             */
#define UNIT_SIO_ANSI       (1 << UNIT_V_SIO_ANSI)
#define UNIT_V_SIO_UPPER    (UNIT_V_UF + 1)     /* upper case mode                              */
#define UNIT_SIO_UPPER      (1 << UNIT_V_SIO_UPPER)
#define UNIT_V_SIO_BS       (UNIT_V_UF + 2)     /* map delete to backspace                      */
#define UNIT_SIO_BS         (1 << UNIT_V_SIO_BS)
#define UNIT_V_SIO_VERBOSE  (UNIT_V_UF + 3)     /* verbose mode, i.e. show error messages       */
#define UNIT_SIO_VERBOSE    (1 << UNIT_V_SIO_VERBOSE)
#define UNIT_V_SIO_MAP      (UNIT_V_UF + 4)     /* mapping mode on                              */
#define UNIT_SIO_MAP        (1 << UNIT_V_SIO_MAP)
#define UNIT_V_SIO_BELL     (UNIT_V_UF + 5)     /* ^G (bell character) rings bell               */
#define UNIT_SIO_BELL       (1 << UNIT_V_SIO_BELL)
#define UNIT_V_SIO_INTERRUPT (UNIT_V_UF + 6)     /* create keyboard interrupts                  */
#define UNIT_SIO_INTERRUPT  (1 << UNIT_V_SIO_INTERRUPT)
#define UNIT_V_SIO_SLEEP    (UNIT_V_UF + 7)     /* sleep after keyboard status check            */
#define UNIT_SIO_SLEEP      (1 << UNIT_V_SIO_SLEEP)

#define UNIT_V_SIMH_TIMERON (UNIT_V_UF + 1)     /* SIMH pseudo device timer generate interrupts */
#define UNIT_SIMH_TIMERON   (1 << UNIT_V_SIMH_TIMERON)

#define TERMINALS           33                  /* lines per mux (increased to 33 for IF3 board)*/
#define SIO_CAN_READ        0x01                /* bit 0 is set iff character available         */
#define SIO_CAN_WRITE       0x02                /* bit 1 is set iff character can be sent       */
#define SIO_RESET           0x03                /* Command to reset SIO                         */
#define VGSIO_CAN_READ      0x02                /* bit 1 is set iff character available         */
#define VGSIO_CAN_WRITE     0x01                /* bit 0 is set iff character can be sent       */
#define KBD_HAS_CHAR        0x40                /* bit 6 is set iff character available         */
#define KBD_HAS_NO_CHAR     0x01                /* bit 0 is set iff no character is available   */

#define BACKSPACE_CHAR      0x08                /* backspace character                          */
#define DELETE_CHAR         0x7f                /* delete character                             */
#define CONTROLC_CHAR       0x03                /* control C character                          */
#define CONTROLG_CHAR       0x07                /* control G char., rings bell when displayed   */
#define CONTROLZ_CHAR       0x1a                /* control Z character                          */

#define PORT_TABLE_SIZE     256                 /* size of port mapping table                   */
#define SLEEP_ALLOWED_START_DEFAULT 100         /* default initial value for sleepAllowedCounter*/
#define DEFAULT_TIMER_DELTA         100         /* default value for timer delta in ms          */
#define CPM_COMMAND_LINE_LENGTH     128
#define CPM_FCB_ADDRESS             0x0080      /* Default FCB address for CP/M.                */

static t_stat simh_dev_set_timeron  (UNIT *uptr, int32 value, CONST char *cptr, void *desc);
static t_stat simh_dev_set_timeroff (UNIT *uptr, int32 value, CONST char *cptr, void *desc);
static t_stat sio_reset(DEVICE *dptr);
static t_stat sio_attach(UNIT *uptr, CONST char *cptr);
static t_stat sio_detach(UNIT *uptr);
static t_stat ptr_reset(DEVICE *dptr);
static t_stat ptp_reset(DEVICE *dptr);
static t_stat toBool(char tf, int32 *result);
static t_stat sio_dev_set_port(UNIT *uptr, int32 value, CONST char *cptr, void *desc);
static t_stat sio_dev_show_port(FILE *st, UNIT *uptr, int32 val, CONST void *desc);
static t_stat sio_dev_set_interrupton(UNIT *uptr, int32 value, CONST char *cptr, void *desc);
static t_stat sio_dev_set_interruptoff(UNIT *uptr, int32 value, CONST char *cptr, void *desc);
static t_stat sio_svc(UNIT *uptr);
static t_stat simh_dev_reset(DEVICE *dptr);
static t_stat simh_svc(UNIT *uptr);
static const char* sio_description(DEVICE *dptr);
static const char* simh_description(DEVICE *dptr);
static const char* ptr_description(DEVICE *dptr);
static const char* ptp_description(DEVICE *dptr);
static t_stat ptpptr_dev_set_port(UNIT *uptr, int32 value, CONST char *cptr, void *desc);
static t_stat ptpptr_dev_show_port(FILE *st, UNIT *uptr, int32 val, CONST void *desc);
static void mapAltairPorts(void);
int32 nulldev   (const int32 port, const int32 io, const int32 data);
int32 sr_dev    (const int32 port, const int32 io, const int32 data);
int32 simh_dev  (const int32 port, const int32 io, const int32 data);
int32 sio0d     (const int32 port, const int32 io, const int32 data);
int32 sio0s     (const int32 port, const int32 io, const int32 data);
int32 sio1d     (const int32 port, const int32 io, const int32 data);
int32 sio1s     (const int32 port, const int32 io, const int32 data);
void do_SIMH_sleep(void);
static void pollConnection(void);
static int32 mapCharacter(int32 ch);
static void checkSleep(void);
static void voidSleep(void);

extern int32 getBankSelect(void);
extern void setBankSelect(const int32 b);
extern uint32 getCommon(void);
extern uint8 GetBYTEWrapper(const uint32 Addr);
extern uint32 sim_map_resource(uint32 baseaddr, uint32 size, uint32 resource_type,
                               int32 (*routine)(const int32, const int32, const int32), const char* name, uint8 unmap);
extern uint32 getClockFrequency(void);
extern void setClockFrequency(const uint32 Value);

extern uint32 PCX;
extern int32 SR;
extern int32 DS_S;
extern UNIT cpu_unit;
extern const char* handlerNameForPort(const int32 port);
extern uint32 vectorInterrupt;            /* Interrupt Request */
extern uint8 dataBus[MAX_INT_VECTORS];    /* Data Bus Value    */

/* Debug Flags */
static DEBTAB generic_dt[] = {
    { "IN",             IN_MSG,             "IN messages"           },
    { "OUT",            OUT_MSG,            "OUT messages"          },
    { "CMD",            CMD_MSG,            "Commands"              },
    { "VERBOSE",        VERBOSE_MSG,        "Verbose messages"      },
    { "BUFFEREMPTY",    BUFFER_EMPTY_MSG,   "IN for empty buffer"   },
    { NULL,             0                                           }
};

/* SIMH pseudo device status registers                                                                          */
/* ZSDOS clock definitions                                                                                      */
static int32 ClockZSDOSDelta        = 0;        /* delta between real clock and Altair clock                    */
static int32 setClockZSDOSPos       = 0;        /* determines state for receiving address of parameter block    */
static int32 setClockZSDOSAdr       = 0;        /* address in M of 6 byte parameter block for setting time      */
static int32 getClockZSDOSPos       = 0;        /* determines state for sending clock information               */

/* CPM3 clock definitions                                                                                       */
static int32 ClockCPM3Delta         = 0;        /* delta between real clock and Altair clock                    */
static int32 setClockCPM3Pos        = 0;        /* determines state for receiving address of parameter block    */
static int32 setClockCPM3Adr        = 0;        /* address in M of 5 byte parameter block for setting time      */
static int32 getClockCPM3Pos        = 0;        /* determines state for sending clock information               */
static int32 daysCPM3SinceOrg       = 0;        /* days since 1 Jan 1978                                        */

/* timer interrupt related                                                                                      */
static uint32 timeOfNextInterrupt;              /* time when next interrupt is scheduled                        */
       int32 timerInterrupt         = FALSE;    /* timer interrupt pending                                      */
       int32 timerInterruptHandler  = 0x0fc00;  /* default address of interrupt handling routine                */
static int32 setTimerInterruptAdrPos= 0;        /* determines state for receiving timerInterruptHandler         */
static int32 timerDelta             = DEFAULT_TIMER_DELTA;  /* interrupt every 100 ms                           */
static int32 setTimerDeltaPos       = 0;        /* determines state for receiving timerDelta                    */

/* stop watch and timer related                                                                                 */
static uint32 stopWatchDelta        = 0;        /* stores elapsed time of stop watch                            */
static int32 getStopWatchDeltaPos   = 0;        /* determines the state for receiving stopWatchDelta            */
static uint32 stopWatchNow          = 0;        /* stores starting time of stop watch                           */
static int32 markTimeSP             = 0;        /* stack pointer for timer stack                                */
       uint32 SIMHSleep             = 1;        /* default time in milliseconds to sleep for SIMHSleepCmd is 1  */
static uint32 sleepAllowedCounter   = 0;        /* only sleep on no character available when == 0               */
static uint32 sleepAllowedStart     = SLEEP_ALLOWED_START_DEFAULT;  /* default start for above counter          */

/* miscellaneous                                                                                                */
static int32 versionPos             = 0;        /* determines state for sending device identifier               */
static int32 lastCPMStatus          = 0;        /* result of last attachCPM command                             */
static int32 lastCommand            = 0;        /* most recent command processed on port 0xfeh                  */
static int32 getCommonPos           = 0;        /* determines state for sending the 'common' register           */
static int32 genInterruptPos        = 0;        /* determines state for receiving interrupt vector and data     */
static int32 genInterruptVec        = 0;        /* stores interrupt vector                                      */

/* CPU Clock Frequency related                                                                                  */
static uint32 newClockFrequency;
static int32 setClockFrequencyPos   = 0;        /* determines state for sending the clock frequency             */
static int32 getClockFrequencyPos   = 0;        /* determines state for receiving the clock frequency           */

/* Set FCB Address (needed for MS-DOS READ and WRITE commands. */
static int32 setFCBAddressPos       = 0;        /* determines state for setting the FCB address                 */
static int32 FCBAddress = CPM_FCB_ADDRESS;      /* FCB Address                                                  */

/* support for wild card file expansion */

#if defined (_WIN32)
const static char hostPathSeparator     = '\\'; /* back slash in Windows    */
const static char hostPathSeparatorAlt  = '/';  /* '/' is an alternative    */
#else
const static char hostPathSeparator     = '/';  /* slash in UNIX            */
const static char hostPathSeparatorAlt  = '/';  /* no alternative           */
#endif

typedef struct NameNode {
    char *name;
    struct NameNode *next;
} NameNode_t;

static char cpmCommandLine[CPM_COMMAND_LINE_LENGTH];
static NameNode_t *nameListHead         = NULL;
static NameNode_t *currentName          = NULL;
static int32 currentNameIndex           = 0;
static int32 lastPathSeparatorIndex     = 0;
static int32 firstPathCharacterIndex    = 0;

static void deleteNameList(void) {
    while (nameListHead != NULL) {
        NameNode_t *next = nameListHead -> next;
        free(nameListHead -> name);
        free(nameListHead);
        nameListHead = next;
    }
    currentName = NULL;
    currentNameIndex = 0;
}

static void processDirEntry (const char *directory,
                             const char *filename,
                             t_offset FileSize,
                             const struct stat *filestat,
                             void *context) {
    if (filename != NULL) {
        NameNode_t *top = (NameNode_t *)malloc(sizeof(NameNode_t));
        if (top) {
            top->name = strdup(filename);
            top->next = nameListHead;
            nameListHead = top;
        }
    }
}


/* SIO status registers                                                                                         */
static int32 warnLevelSIO           = 3;        /* display at most 'warnLevelSIO' times the same warning        */
static int32 warnUnattachedPTP      = 0;        /* display a warning message if < warnLevel and SIO set to
                                                VERBOSE and output to PTP without an attached file              */
static int32 warnUnattachedPTR      = 0;        /* display a warning message if < warnLevel and SIO set to
                                                VERBOSE and attempt to read from PTR without an attached file   */
static int32 warnPTREOF             = 0;        /* display a warning message if < warnLevel and SIO set to
                                                VERBOSE and attempt to read from PTR past EOF                   */
static int32 warnUnassignedPort     = 0;        /* display a warning message if < warnLevel and SIO set to
                                                VERBOSE and attempt to perform IN or OUT on an unassigned PORT  */

       int32 keyboardInterrupt = FALSE;         /* keyboard interrupt pending                                   */
       uint32 keyboardInterruptHandler = 0x0038;/* address of keyboard interrupt handler                        */

/* PTR/PTP port assignments (read only)                                                                         */
static int32 ptpptrStatusPort       = 0x12;     /* default status port for PTP/PTR device                       */
static int32 ptpptrDataPort         = 0x13;     /* default data port for PTP/PTR device                         */
       int32 kbdIrqPort             = 0;        /* Keyboard Interrupt port number.                              */

static TMLN TerminalLines[TERMINALS] = {        /* four terminals   */
    { 0 }
};

static TMXR altairTMXR = {                      /* mux descriptor   */
    TERMINALS, 0, 0, TerminalLines
};

static UNIT sio_unit = {
    UDATA (&sio_svc, UNIT_ATTABLE | UNIT_SIO_MAP | UNIT_SIO_SLEEP, 0),
    100000, /* wait                                                 */
    FALSE,  /* u3 = FALSE, no character available in buffer         */
    FALSE,  /* u4 = FALSE, terminal input is not attached to a file */
    0,      /* u5 = 0, not used                                     */
    0       /* u6 = 0, not used                                     */
};

static REG sio_reg[] = {
    { DRDATAD (SIOWLEV,  warnLevelSIO,       32,
               "Warn level SIO register")                                                   },
    { DRDATAD (WRNUPTP,  warnUnattachedPTP,  32,
               "Counter for unattached PTP access")                                         },
    { DRDATAD (WRNUPTR,  warnUnattachedPTR,  32,
               "Counter for unattached PTR access")                                         },
    { DRDATAD (WRNPTRE,  warnPTREOF,         32,
               "Counter for EOF reached for PTR")                                           },
    { DRDATAD (WRUPORT,  warnUnassignedPort, 32,
               "Counter for unassigned port")                                               },
    { HRDATAD (FILEATT,  sio_unit.u4,        8,
               "BOOL to determine whether terminal input is attached to a file"), REG_RO    },
    /* TRUE iff terminal input is attached to a file    */
    { HRDATAD (TSTATUS,  sio_unit.u3,        8,
               "BOOL to determine whether a character is available")                       },
    /* TRUE iff a character available in sio_unit.buf   */
    { DRDATAD (TBUFFER,  sio_unit.buf,       8,
               "Input buffer register")                                                     },
    /* input buffer for one character                   */
    { DRDATAD (KEYBDI,   keyboardInterrupt, 3,
               "BOOL to determine whether a keyboard interrupt is pending"), REG_RO         },
    { HRDATAD (KEYBDH,   keyboardInterruptHandler,   16,
               "Address of keyboard interrupt handler")                                     },
    { HRDATAD(KBDIRQPORT,   kbdIrqPort, 8,
               "Port number of keyboardInterrupt SIO status register."),                    },
    { NULL }
};

static MTAB sio_mod[] = {
    { UNIT_SIO_ANSI,        0,                  "TTY",      "TTY",      NULL, NULL, NULL,
        "Do not touch bit 8 of console output"}, /* keep bit 8 as is for output             */
    { UNIT_SIO_ANSI,        UNIT_SIO_ANSI,      "ANSI",     "ANSI",     NULL, NULL, NULL,
        "Set bit 8 of console output to 0"}, /* set bit 8 to 0 before output                */
    { UNIT_SIO_UPPER,       0,                  "ALL",      "ALL",      NULL, NULL, NULL,
        "Console input remains unchanged" }, /* do not change case of input characters      */
    { UNIT_SIO_UPPER,       UNIT_SIO_UPPER,     "UPPER",    "UPPER",    NULL, NULL, NULL,
        "Convert console input to upper case" }, /* change input characters to upper case   */
    { UNIT_SIO_BS,          0,                  "BS",       "BS",       NULL, NULL, NULL,
        "Map delete to backspace" }, /* map delete to backspace                             */
    { UNIT_SIO_BS,          UNIT_SIO_BS,        "DEL",      "DEL",      NULL, NULL, NULL,
        "Map backspace to delete" }, /* map backspace to delete                             */
    { UNIT_SIO_VERBOSE,     0,                  "QUIET",    "QUIET",    NULL, NULL, NULL,
        "Do not display SIO error messages" }, /* quiet, no error messages                  */
    { UNIT_SIO_VERBOSE,     UNIT_SIO_VERBOSE,   "VERBOSE",  "VERBOSE",  NULL, NULL, NULL,
        "Display verbose messages" }, /* verbose, display warning messages                  */
    { UNIT_SIO_MAP,         0,                  "NOMAP",    "NOMAP",    NULL, NULL, NULL,
        "Do not map any character" }, /*  disable character mapping                         */
    { UNIT_SIO_MAP,         UNIT_SIO_MAP,       "MAP",      "MAP",      NULL, NULL, NULL,
        "Enable mapping of characters" }, /*  enable all character mapping                  */
    { UNIT_SIO_BELL,        0,                  "BELL",     "BELL",     NULL, NULL, NULL,
        "Control-G sounds the bell" }, /*  enable bell character                            */
    { UNIT_SIO_BELL,        UNIT_SIO_BELL,      "NOBELL",   "NOBELL",   NULL, NULL, NULL,
        "The bell sound is suppressed" }, /*  suppress ringing the bell                     */
    { UNIT_SIO_SLEEP,       0,                  "NOSLEEP",  "NOSLEEP",  NULL, NULL, NULL,
        "Do not sleep after SIO status checks" }, /*  no sleep after keyboard status check  */
    { UNIT_SIO_SLEEP,       UNIT_SIO_SLEEP,     "SLEEP",    "SLEEP",    NULL, NULL, NULL,
        "Sleep after SIO status checks" }, /*  sleep after keyboard status check            */
    /*  no keyboard interrupts                  */
    { UNIT_SIO_INTERRUPT,   0,                  "NOINTERRUPT",  "NOINTERRUPT",
        &sio_dev_set_interruptoff, NULL, NULL, "Status port 0 does not create interrupts" },
    /*  create keyboard interrupts              */
    { UNIT_SIO_INTERRUPT,   UNIT_SIO_INTERRUPT, "INTERRUPT",    "INTERRUPT",
        &sio_dev_set_interrupton, NULL, NULL,
        "Status port 0 creates an interrupt when a character becomes available" },
    { MTAB_XTD|MTAB_VDV,    0,                  "PORT",     "PORT",
        &sio_dev_set_port, &sio_dev_show_port, NULL,
        "Set port to Port/Terminal/Read/NotRead/Write/Reset/Reset/Data" },
    { 0 }
};

static const char* sio_description(DEVICE *dptr) {
    return "Serial Input Output";
}

DEVICE sio_dev = {
    "SIO", &sio_unit, sio_reg, sio_mod,
    1, 10, 31, 1, 8, 8,
    NULL, NULL, &sio_reset,
    NULL, &sio_attach, &sio_detach,
    NULL, DEV_DEBUG | DEV_MUX, 0,
    generic_dt, NULL, NULL, NULL, NULL, NULL, &sio_description
};

static MTAB ptpptr_mod[] = {
    { MTAB_XTD|MTAB_VDV, 0, "PORT", "PORT",
        &ptpptr_dev_set_port, &ptpptr_dev_show_port, NULL,
        "Set status and data port for PTP/PTR device" },
    { 0 }
};

static UNIT ptr_unit = {
    UDATA (NULL, UNIT_ATTABLE | UNIT_ROABLE, 0)
};

static REG ptr_reg[] = {
    { HRDATAD (PTRSTATUSPORT, ptpptrStatusPort, 8, "PTR status port (shared with PTP)"), REG_RO  },
    { HRDATAD (PTRDATAPORT, ptpptrDataPort, 8, "PTR data port (shared with PTP)"), REG_RO  },
    { HRDATAD (PTRSTATUS, ptr_unit.u3, 8, "PTR Status register")  },
    { NULL }
};

static const char* ptr_description(DEVICE *dptr) {
    return "Paper Tape Reader";
}

DEVICE ptr_dev = {
    "PTR", &ptr_unit, ptr_reg, ptpptr_mod,
    1, 10, 31, 1, 8, 8,
    NULL, NULL, &ptr_reset,
    NULL, NULL, NULL,
    NULL, (DEV_DISABLE | DEV_DEBUG), 0,
    generic_dt, NULL, NULL, NULL, NULL, NULL, &ptr_description
};

static UNIT ptp_unit = {
    UDATA (NULL, UNIT_ATTABLE, 0)
};

static REG ptp_reg[] = {
    { HRDATAD (PTPSTATUSPORT, ptpptrStatusPort, 8, "PTP status port (shared with PTR)"), REG_RO  },
    { HRDATAD (PTPDATAPORT, ptpptrDataPort, 8, "PTP data port (shared with PTR)"), REG_RO  },
    { NULL }
};

static const char* ptp_description(DEVICE *dptr) {
    return "Paper Tape Puncher";
}

DEVICE ptp_dev = {
    "PTP", &ptp_unit, ptp_reg, ptpptr_mod,
    1, 10, 31, 1, 8, 8,
    NULL, NULL, &ptp_reset,
    NULL, NULL, NULL,
    NULL, (DEV_DISABLE | DEV_DEBUG), 0,
    generic_dt, NULL, NULL, NULL, NULL, NULL, &ptp_description
};

/*  Synthetic device SIMH for communication
    between Altair and SIMH environment using port 0xfe */
static UNIT simh_unit = {
    UDATA (&simh_svc, 0, 0), KBD_POLL_WAIT
};

static REG simh_reg[] = {
    { DRDATAD (CZD,      ClockZSDOSDelta,        32,
               "ZSDOS Clock - Delta between real clock and AltairZ80 clock")                        },
    { DRDATAD (SCZP,     setClockZSDOSPos,       8,
               "ZSDOS Clock - Status register for receiving address of parameter block"), REG_RO    },
    { HRDATAD (SCZA,     setClockZSDOSAdr,       16,
               "ZSDOS Clock - Address of 6 byte parameter block for setting time"), REG_RO          },
    { DRDATAD (GCZP,     getClockZSDOSPos,       8,
               "ZSDOS Clock - Status register for sending clock information"), REG_RO               },

    { DRDATAD (CC3D,     ClockCPM3Delta,         32,
               "CP/M 3 Clock - Delta between real clock and AltairZ80 clock")                       },
    { DRDATAD (SC3DP,    setClockCPM3Pos,        8,
               "CP/M 3 Clock - Status register for receiving address of parameter block"), REG_RO   },
    { HRDATAD (SC3DA,    setClockCPM3Adr,        16,
               "CP/M 3 Clock - Address of 5 byte parameter block for setting time"), REG_RO         },
    { DRDATAD (GC3DP,    getClockCPM3Pos,        8,
               "CP/M 3 Clock - Status register for sending clock information"), REG_RO              },
    { DRDATAD (D3DO,     daysCPM3SinceOrg,       32,
               "CP/M 3 Clock - Days since 1-Jan-1978"), REG_RO                                      },

    { DRDATAD (TOFNI,    timeOfNextInterrupt,    32,
               "Time when next interrupt is scheduled"), REG_RO                                     },
    { DRDATAD (TIMI,     timerInterrupt,         3,
               "BOOL - determines whether a timer interrupt is pending")                            },
    { HRDATAD (TIMH,     timerInterruptHandler,  16,
               "Address of timer interrupt handling routine")                                       },
    { DRDATAD (STIAP,    setTimerInterruptAdrPos,8,
               "Status register for receiving address of timer interrupt handler"), REG_RO          },
    { DRDATAD (TIMD,     timerDelta,             32,
               "Time in milliseconds between timer interrupts")                                     },
    { DRDATAD (STDP,     setTimerDeltaPos,       8,
               "Status register for receiving the timer delta"), REG_RO                             },
    { DRDATAD (SLEEP,    SIMHSleep,              32,
               "Sleep time in milliseconds after SIO status check (when enabled)")                  },
    { DRDATAD (VOSLP,    sleepAllowedStart,      32,
               "Only sleep when this many unsuccessful SIO status checks have been made")           },

    { DRDATAD (STPDT,    stopWatchDelta,         32,
               "Elapsed time of stop watch"), REG_RO                                                },
    { DRDATAD (STPOS,    getStopWatchDeltaPos,   8,
               "Status register for receiving stop watch delta"), REG_RO                            },
    { DRDATAD (STPNW,    stopWatchNow,           32,
               "Starting time of stop watch"), REG_RO                                               },
    { DRDATAD (MTSP,     markTimeSP,             8,
               "Stack pointer of timer stack"), REG_RO                                              },

    { DRDATAD (VPOS,     versionPos,             8,
               "Status register for sending version information"), REG_RO                           },
    { DRDATAD (LCPMS,    lastCPMStatus,          8,
               "Result of last attachCPM command"), REG_RO                                          },
    { DRDATAD (LCMD,     lastCommand,            8,
               "Last command processed on SIMH port"), REG_RO                                       },
    { DRDATAD (CPOS,     getCommonPos,           8,
               "Status register for sending the COMMON register"), REG_RO                           },
    { HRDATAD (FCBA,     FCBAddress,  16,
               "Address of the FCB for file operations")                                            },
    { DRDATAD (FCBAP,    setFCBAddressPos,8,
               "Status register for receiving address of the FCB"), REG_RO                          },
    { NULL }
};

static MTAB simh_mod[] = {
    /* timer generated interrupts are off   */
    { UNIT_SIMH_TIMERON,    0,                  "TIMEROFF", "TIMEROFF", &simh_dev_set_timeroff,
        NULL, NULL, "Stop periodic timer interrupts"    },
    /* timer generated interrupts are on    */
    { UNIT_SIMH_TIMERON,    UNIT_SIMH_TIMERON,  "TIMERON",  "TIMERON",  &simh_dev_set_timeron,
        NULL, NULL, "Start periodic timer interrupts"   },
    { 0 }
};

const char* simh_description(DEVICE *dptr) {
    return "Pseudo Device";
}

DEVICE simh_device = {
    "SIMH", &simh_unit, simh_reg, simh_mod,
    1, 10, 31, 1, 16, 4,
    NULL, NULL, &simh_dev_reset,
    NULL, NULL, NULL,
    NULL, (DEV_DISABLE | DEV_DEBUG), 0,
    generic_dt, NULL, NULL, NULL, NULL, NULL, &simh_description
};

static void resetSIOWarningFlags(void) {
    warnUnattachedPTP = warnUnattachedPTR = warnPTREOF = warnUnassignedPort = 0;
}

static t_stat sio_attach(UNIT *uptr, CONST char *cptr) {
    t_stat r = SCPE_IERR;
    sio_unit.u3 = FALSE;                                    /* no character in terminal input buffer    */
    get_uint(cptr, 10, 65535, &r);                          /* attempt to get port, discard result      */
    if (r == SCPE_OK) {                                     /* string can be interpreted as port number */
        sio_unit.u4 = FALSE;                                /* terminal input is not attached to a file */
        return tmxr_attach(&altairTMXR, uptr, cptr);        /* attach mux                               */
    }
    sio_unit.u4 = TRUE;                                     /* terminal input is attached to a file     */
    return attach_unit(uptr, cptr);
}

static t_stat sio_detach(UNIT *uptr) {
    sio_unit.u3 = FALSE;                                    /* no character in terminal input buffer    */
    if (sio_unit.u4) {                                      /* is terminal input attached to a file?    */
        sio_unit.u4 = FALSE;                                /* not anymore, detach                      */
        return detach_unit(uptr);
    }
    return tmxr_detach(&altairTMXR, uptr);
}

static void pollConnection(void) {
    if (sio_unit.flags & UNIT_ATT) {
        int32 temp = tmxr_poll_conn(&altairTMXR);           /* poll connection                          */
        if (temp >= 0)
            TerminalLines[temp].rcve = 1;                   /* enable receive                           */
        tmxr_poll_rx(&altairTMXR);                          /* poll input                               */
        tmxr_poll_tx(&altairTMXR);                          /* poll output                              */
    }
}

/* reset routines */
static t_stat sio_reset(DEVICE *dptr) {
    int32 i;
    sim_debug(VERBOSE_MSG, &sio_dev, "SIO: " ADDRESS_FORMAT " Reset\n", PCX);
    sio_unit.u3 = FALSE;                                    /* no character in terminal input buffer    */
    sio_unit.buf = 0;
    resetSIOWarningFlags();
    if (sio_unit.u4)                                        /* is terminal input attached to a file?    */
        rewind(sio_unit.fileref);                           /* yes, rewind input                        */
    else if (sio_unit.flags & UNIT_ATT)
        for (i = 0; i < TERMINALS; i++)
            if (TerminalLines[i].conn)
                tmxr_reset_ln(&TerminalLines[i]);
    mapAltairPorts();
    return SCPE_OK;
}

static t_stat ptr_reset(DEVICE *dptr) {
    sim_debug(VERBOSE_MSG, &ptr_dev, "PTR: " ADDRESS_FORMAT " Reset\n", PCX);
    resetSIOWarningFlags();
    ptr_unit.u3 = FALSE;                                    /* End Of File not yet reached              */
    ptr_unit.buf = 0;
    if (ptr_unit.flags & UNIT_ATT)                          /* attached?                                */
        rewind(ptr_unit.fileref);
    sim_map_resource(0x12, 1, RESOURCE_TYPE_IO, &sio1s, "sio1s", dptr->flags & DEV_DIS);
    sim_map_resource(0x13, 1, RESOURCE_TYPE_IO, &sio1d, "sio1d", dptr->flags & DEV_DIS);
    return SCPE_OK;
}

static t_stat ptp_reset(DEVICE *dptr) {
    sim_debug(VERBOSE_MSG, &ptp_dev, "PTP: " ADDRESS_FORMAT " Reset\n", PCX);
    resetSIOWarningFlags();
    sim_map_resource(0x12, 1, RESOURCE_TYPE_IO, &sio1s, "sio1s", dptr->flags & DEV_DIS);
    sim_map_resource(0x13, 1, RESOURCE_TYPE_IO, &sio1d, "sio1d", dptr->flags & DEV_DIS);
    return SCPE_OK;
}

static int32 mapCharacter(int32 ch) {
    ch &= 0xff;
    if (sio_unit.flags & UNIT_SIO_MAP) {
        if (sio_unit.flags & UNIT_SIO_BS) {
            if (ch == BACKSPACE_CHAR)
                return DELETE_CHAR;
        } else if (ch == DELETE_CHAR)
            return BACKSPACE_CHAR;
        if (sio_unit.flags & UNIT_SIO_UPPER)
            return toupper(ch);
    }
    return ch;
}

/*  I/O instruction handlers, called from the CPU module when an
    IN or OUT instruction is issued.

    Each function is passed an 'io' flag, where 0 means a read from
    the port, and 1 means a write to the port. On input, the actual
    input is passed as the return value, on output, 'data' is written
    to the device.

    Port 1 controls console I/O. We distinguish three cases:
    1) SIO attached to a file       (i.e. input taken from a file   )
    2) SIO attached to a port       (i.e. Telnet console I/O        )
    3) SIO not attached to a port   (i.e. "regular" console I/O     )
*/

typedef struct {
    int32 port;             /* this information belongs to port number 'port'           */
    int32 terminalLine;     /* map to this 'terminalLine'                               */
    int32 sio_can_read;     /* bit mask to indicate that one can read from this port    */
    int32 sio_cannot_read;  /* bit mask to indicate that one cannot read from this port */
    int32 sio_can_write;    /* bit mask to indicate that one can write to this port     */
    int32 hasReset;         /* TRUE iff SIO has reset command                           */
    int32 sio_reset;        /* reset command                                            */
    int32 hasOUT;           /* TRUE iff port supports OUT command                       */
    int32 isBuiltin;        /* TRUE iff mapping is built in                             */
} SIO_PORT_INFO;

static SIO_PORT_INFO port_table[PORT_TABLE_SIZE] = {
    {0x00, 0, KBD_HAS_CHAR,     KBD_HAS_NO_CHAR, SIO_CAN_WRITE, FALSE, 0, FALSE, TRUE   },
    {0x01, 0, 0,                0,      0, FALSE, 0, FALSE, TRUE                        },
    {0x02, 0, VGSIO_CAN_READ,   0,      VGSIO_CAN_WRITE, FALSE, 0, TRUE, TRUE           },
    {0x03, 0, VGSIO_CAN_READ,   0,      VGSIO_CAN_WRITE, FALSE, 0, FALSE, TRUE          },
    {0x04, 0, VGSIO_CAN_READ,   0,      VGSIO_CAN_WRITE, FALSE, 0, TRUE, TRUE           },
    {0x05, 0, VGSIO_CAN_READ,   0,      VGSIO_CAN_WRITE, FALSE, 0, FALSE, TRUE          },
    {0x10, 0, SIO_CAN_READ,     0,      SIO_CAN_WRITE, TRUE, SIO_RESET, FALSE, TRUE     },
    {0x11, 0, SIO_CAN_READ,     0,      SIO_CAN_WRITE, TRUE, SIO_RESET, TRUE, TRUE      },
    {0x14, 1, SIO_CAN_READ,     0,      SIO_CAN_WRITE, TRUE, SIO_RESET, FALSE, TRUE     },
    {0x15, 1, SIO_CAN_READ,     0,      SIO_CAN_WRITE, TRUE, SIO_RESET, TRUE, TRUE      },
    {0x16, 2, SIO_CAN_READ,     0,      SIO_CAN_WRITE, TRUE, SIO_RESET, FALSE, TRUE     },
    {0x17, 2, SIO_CAN_READ,     0,      SIO_CAN_WRITE, TRUE, SIO_RESET, TRUE, TRUE      },
    {0x18, 3, SIO_CAN_READ,     0,      SIO_CAN_WRITE, TRUE, SIO_RESET, FALSE, TRUE     },
    {0x19, 3, SIO_CAN_READ,     0,      SIO_CAN_WRITE, TRUE, SIO_RESET, TRUE, TRUE      },

    /* CompuPro System Support 1 Board */
    {0x5c, 0, 0x0,              0,      0,             FALSE,0,         TRUE, TRUE      },
    {0x5d, 0, 0xC2,             0,      0xC5,          FALSE,0,         FALSE, TRUE     },

    /* CompuPro Interfacer 3 (IF3) Board 0 */
    {0x300, 1, 0x0,             0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x301, 1, 0xC2,            0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x302, 2, 0x0,             0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x303, 2, 0xC2,            0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x304, 3, 0x0,             0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x305, 3, 0xC2,            0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x306, 4, 0x0,             0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x307, 4, 0xC2,            0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x308, 5, 0x0,             0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x309, 5, 0xC2,            0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x30a, 6, 0x0,             0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x30b, 6, 0xC2,            0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x30c, 7, 0x0,             0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x30d, 7, 0xC2,            0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x30e, 8, 0x0,             0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x30f, 8, 0xC2,            0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    /* CompuPro Interfacer 3 (IF3) Board 1 */
    {0x310, 9, 0x0,             0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x311, 9, 0xC2,            0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x312, 10, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x313, 10, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x314, 11, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x315, 11, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x316, 12, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x317, 12, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x318, 13, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x319, 13, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x31a, 14, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x31b, 14, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x31c, 15, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x31d, 15, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x31e, 16, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x31f, 16, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    /* CompuPro Interfacer 3 (IF3) Board 2 */
    {0x320, 17, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x321, 17, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x322, 18, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x323, 18, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x324, 19, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x325, 19, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x326, 20, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x327, 20, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x328, 21, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x329, 21, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x32a, 22, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x32b, 22, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x32c, 23, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x32d, 23, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x32e, 24, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x32f, 24, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    /* CompuPro Interfacer 3 (IF3) Board 3 */
    {0x330, 25, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x331, 25, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x332, 26, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x333, 26, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x334, 27, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x335, 27, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x336, 28, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x337, 28, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x338, 29, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x339, 29, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x33a, 30, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x33b, 30, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x33c, 31, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x33d, 31, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {0x33e, 32, 0x0,            0,      0,             TRUE, SIO_RESET, TRUE, TRUE      },
    {0x33f, 32, 0xC2,           0,      0xC5,          TRUE, SIO_RESET, FALSE, TRUE     },
    {-1, 0, 0, 0, 0, 0, 0, 0, 0}   /* must be last */
};

static SIO_PORT_INFO lookupPortInfo(const int32 port, int32 *position) {
    int32 i = 0;
    while ((port_table[i].port != -1) && (port_table[i].port != port))
        i++;
    *position = i;
    return port_table[i];
}

/* keyboard idle detection: sleep when feature enabled, no character available
    (duty of caller) and operation not voided (e.g. when output is available) */
static void checkSleep(void) {
    if (sio_unit.flags & UNIT_SIO_SLEEP) {
        if (sleepAllowedCounter)
            sleepAllowedCounter--;
        else
            do_SIMH_sleep();
    }
}

/* void sleep for next 'sleepAllowedStart' tests */
static void voidSleep(void) {
    sleepAllowedCounter = sleepAllowedStart;
}

/* generic status port for keyboard input / terminal output */
static int32 sio0sCore(const int32 port, const int32 io, const int32 data) {
    int32 ch, result;
    const SIO_PORT_INFO spi = lookupPortInfo(port, &ch);
    ASSURE(spi.port == port);
    pollConnection();
    if (io == 0) { /* IN */
        if (sio_unit.u4) {                                  /* attached to a file?                      */
            if (sio_unit.u3)                                /* character available?                     */
                return spi.sio_can_read | spi.sio_can_write;
            ch = getc(sio_unit.fileref);
            if (ch == EOF) {
                sio_detach(&sio_unit);                      /* detach file and switch to keyboard input */
                return spi.sio_cannot_read | spi.sio_can_write;
            } else {
                sio_unit.u3 = TRUE;                         /* indicate character available             */
                sio_unit.buf = ch;                          /* store character in buffer                */
                return spi.sio_can_read | spi.sio_can_write;
            }
        }
        if (sio_unit.flags & UNIT_ATT) {                    /* attached to a port?                      */
            if (tmxr_rqln(&TerminalLines[spi.terminalLine]))
                result = spi.sio_can_read;
            else {
                result = spi.sio_cannot_read;
                if (!sim_signaled_int_char)
                    sim_poll_kbd();                         /* check for WRU when signaling is not available */
                checkSleep();
            }
            return result |                                 /* read possible if character available     */
                (TerminalLines[spi.terminalLine].conn && TerminalLines[spi.terminalLine].xmte ? spi.sio_can_write : 0x00);
                                                            /* write possible if connected and transmit
                                                                enabled                                 */
        }
        if (sio_unit.u3)                                    /* character available?                     */
            return spi.sio_can_read | spi.sio_can_write;
        ch = sim_poll_kbd();                                /* no, try to get a character               */
        if ((ch == SCPE_OK) && stop_cpu) {
            sim_interval = 0;                               /* detect stop condition as soon as possible*/
            return spi.sio_cannot_read | spi.sio_can_write; /* do not consume stop character        */
        }
        if (ch) {                                           /* character available?                     */
            sio_unit.u3 = TRUE;                             /* indicate character available             */
            sio_unit.buf = ch;                              /* store character in buffer                */
            return spi.sio_can_read | spi.sio_can_write;
        }
        checkSleep();
        return spi.sio_cannot_read | spi.sio_can_write;
    }                                                       /* OUT follows, no fall-through from IN     */
    if (spi.hasReset && (data == spi.sio_reset)) {          /* reset command                            */
        if (!sio_unit.u4)                                   /* only reset for regular console I/O       */
            sio_unit.u3 = FALSE;                            /* indicate that no character is available  */
        sim_debug(CMD_MSG, &sio_dev, "\tSIO_S: " ADDRESS_FORMAT
                  " Command OUT(0x%03x) = 0x%02x\n", PCX, port, data);
    }
    return 0x00;                                            /* ignored since OUT                        */
}

int32 sio0s(const int32 port, const int32 io, const int32 data) {
    const int32 result = sio0sCore(port, io, data);
    if (io == 0) {
        sim_debug(IN_MSG, &sio_dev, "\tSIO_S: " ADDRESS_FORMAT
                  " IN(0x%03x) = 0x%02x\n", PCX, port, result);
    } else if (io) {
        sim_debug(OUT_MSG, &sio_dev, "\tSIO_S: " ADDRESS_FORMAT
                  " OUT(0x%03x) = 0x%02x\n", PCX, port, data);
        }
    return result;
}

/* generic data port for keyboard input / terminal output */
static int32 sio0dCore(const int32 port, const int32 io, const int32 data) {
    int32 ch;
    const SIO_PORT_INFO spi = lookupPortInfo(port, &ch);
    ASSURE(spi.port == port);
    pollConnection();
    if (io == 0) { /* IN */
        if ((sio_unit.flags & UNIT_ATT) && (!sio_unit.u4))
            return mapCharacter(tmxr_getc_ln(&TerminalLines[spi.terminalLine]));
        if (!sio_unit.u3) {
            sim_debug(BUFFER_EMPTY_MSG, &sio_dev, "\tSIO_D: " ADDRESS_FORMAT
                      " IN(0x%03x) for empty character buffer\n", PCX, port);
        }
        sio_unit.u3 = FALSE;                                /* no character is available any more       */
        return mapCharacter(sio_unit.buf);                  /* return previous character                */
    }                                                       /* OUT follows, no fall-through from IN     */
    if (spi.hasOUT) {
        ch = sio_unit.flags & UNIT_SIO_ANSI ? data & 0x7f : data;   /* clear highest bit in ANSI mode   */
        if ((ch != CONTROLG_CHAR) || !(sio_unit.flags & UNIT_SIO_BELL)) {
            voidSleep();
            if ((sio_unit.flags & UNIT_ATT) && (!sio_unit.u4)) {    /* attached to a port and not to a file */
                tmxr_putc_ln(&TerminalLines[spi.terminalLine], ch); /* status ignored                   */
                tmxr_poll_tx(&altairTMXR);                          /* poll xmt                         */
            } else
                sim_putchar(ch);
        }
    }
    return 0x00;                                            /* ignored since OUT                        */
}

static char* printable(char* result, int32 data, const int32 isIn) {
    result[0] = 0;
    data &= 0x7f;
    if ((0x20 <= data) && (data < 0x7f))
        sprintf(result, isIn ? " <-\"%c\"" : " ->\"%c\"", data);
    return result;
}

int32 sio0d(const int32 port, const int32 io, const int32 data) {
    char buffer[8];
    const int32 result = sio0dCore(port, io, data);
    if (io == 0) {
        sim_debug(IN_MSG, &sio_dev, "\tSIO_D: " ADDRESS_FORMAT
                  " IN(0x%03x) = 0x%02x%s\n", PCX, port, result, printable(buffer, result, TRUE));
    } else if (io) {
        sim_debug(OUT_MSG, &sio_dev, "\tSIO_D: " ADDRESS_FORMAT
                  " OUT(0x%03x) = 0x%02x%s\n", PCX, port, data, printable(buffer, data, FALSE));
        }
    return result;
}

/* PTR/PTP status port */
static int32 sio1sCore(const int32 port, const int32 io, const int32 data) {
    if (io == 0) {                                          /* IN                                       */
                                                            /* reset I bit iff PTR unit not attached or
                                                                no more data available. O bit is always
                                                                set since write always possible.        */
        if ((ptr_unit.flags & UNIT_ATT) == 0) {             /* PTR is not attached                      */
            if ((ptr_dev.dctrl & VERBOSE_MSG) && (warnUnattachedPTR < warnLevelSIO)) {
                warnUnattachedPTR++;
/*06*/          sim_debug(VERBOSE_MSG, &ptr_dev, "PTR: " ADDRESS_FORMAT
                          " Attempt to test status of unattached PTR[0x%02x]. 0x02 returned.\n", PCX, port);
            }
            return SIO_CAN_WRITE;
        }
                                                            /* if EOF then SIO_CAN_WRITE else
                                                                (SIO_CAN_WRITE and SIO_CAN_READ)        */
        return ptr_unit.u3 ? SIO_CAN_WRITE : (SIO_CAN_READ | SIO_CAN_WRITE);
    }                                                       /* OUT follows                              */
    if (data == SIO_RESET) {
        ptr_unit.u3 = FALSE;                                /* reset EOF indicator                      */
        sim_debug(CMD_MSG, &ptr_dev, "PTR: " ADDRESS_FORMAT
                  " Command OUT(0x%03x) = 0x%02x\n", PCX, port, data);
    }
    return 0x00;                                            /* ignored since OUT                        */
}

int32 sio1s(const int32 port, const int32 io, const int32 data) {
    const int32 result = sio1sCore(port, io, data);
    if (io == 0) {
        sim_debug(IN_MSG, &ptr_dev, "PTR_S: " ADDRESS_FORMAT
                  " IN(0x%02x) = 0x%02x\n", PCX, port, result);
        sim_debug(IN_MSG, &ptp_dev, "PTP_S: " ADDRESS_FORMAT
                  " IN(0x%02x) = 0x%02x\n", PCX, port, result);
    } else if (io) {
        sim_debug(OUT_MSG, &ptr_dev, "PTR_S: " ADDRESS_FORMAT
                  " OUT(0x%02x) = 0x%02x\n", PCX, port, data);
        sim_debug(OUT_MSG, &ptp_dev, "PTP_S: " ADDRESS_FORMAT
                  " OUT(0x%02x) = 0x%02x\n", PCX, port, data);
        }
    return result;
}

/* PTR/PTP data port */
static int32 sio1dCore(const int32 port, const int32 io, const int32 data) {
    int32 ch;
    if (io == 0) {                                          /* IN                                       */
        if (ptr_unit.u3) {                                  /* EOF reached, no more data available      */
            if ((ptr_dev.dctrl & VERBOSE_MSG) && (warnPTREOF < warnLevelSIO)) {
                warnPTREOF++;
/*07*/          sim_debug(VERBOSE_MSG, &ptr_dev, "PTR: " ADDRESS_FORMAT
                          " PTR[0x%02x] attempted to read past EOF. 0x00 returned.\n", PCX, port);
            }
            return 0x00;
        }
        if ((ptr_unit.flags & UNIT_ATT) == 0) {             /* not attached                             */
            if ((ptr_dev.dctrl & VERBOSE_MSG) && (warnUnattachedPTR < warnLevelSIO)) {
                warnUnattachedPTR++;
/*08*/          sim_debug(VERBOSE_MSG, &ptr_dev, "PTR: " ADDRESS_FORMAT
                          " Attempt to read from unattached PTR[0x%02x]. 0x00 returned.\n", PCX, port);
            }
            return 0x00;
        }
        if ((ch = getc(ptr_unit.fileref)) == EOF) {         /* end of file?                             */
            ptr_unit.u3 = TRUE;                             /* remember EOF reached                     */
            return CONTROLZ_CHAR;                           /* ^Z denotes end of text file in CP/M      */
        }
        return ch & 0xff;
    }                                                       /* OUT follows                              */
    if (ptp_unit.flags & UNIT_ATT)                          /* unit must be attached                    */
        putc(data, ptp_unit.fileref);
                                                            /* else ignore data                         */
    else if ((ptp_dev.dctrl & VERBOSE_MSG) && (warnUnattachedPTP < warnLevelSIO)) {
        warnUnattachedPTP++;
/*09*/  sim_debug(VERBOSE_MSG, &ptp_dev, "PTP: " ADDRESS_FORMAT
                  " Attempt to output '0x%02x' to unattached PTP[0x%02x] - ignored.\n", PCX, data, port);
    }
    return 0x00;                                            /* ignored since OUT                        */
}

int32 sio1d(const int32 port, const int32 io, const int32 data) {
    const int32 result = sio1dCore(port, io, data);
    if (io == 0) {
        sim_debug(IN_MSG, &ptr_dev, "PTR_D: " ADDRESS_FORMAT
                  " IN(0x%02x) = 0x%02x\n", PCX, port, result);
        sim_debug(IN_MSG, &ptp_dev, "PTP_D: " ADDRESS_FORMAT
                  " IN(0x%02x) = 0x%02x\n", PCX, port, result);
    } else if (io) {
        sim_debug(OUT_MSG, &ptr_dev, "PTR_D: " ADDRESS_FORMAT
                  " OUT(0x%02x) = 0x%02x\n", PCX, port, data);
        sim_debug(OUT_MSG, &ptp_dev, "PTP_D: " ADDRESS_FORMAT
                  " OUT(0x%02x) = 0x%02x\n", PCX, port, data);
    }
    return result;
}

static t_stat toBool(char tf, int32 *result) {
    if (tf == 'T') {
        *result = TRUE;
        return SCPE_OK;
    }
    if (tf == 'F') {
        *result = FALSE;
        return SCPE_OK;
    }
    return SCPE_ARG;
}

static void show_sio_port_info(FILE *st, SIO_PORT_INFO sip) {
    if (sio_unit.flags & UNIT_SIO_VERBOSE)
        fprintf(st, "(Port=%02x/Terminal=%1i/Read=0x%02x/NotRead=0x%02x/"
            "Write=0x%02x/Reset?=%s/Reset=0x%02x/Data?=%s), %s",
            sip.port, sip.terminalLine, sip.sio_can_read, sip.sio_cannot_read,
            sip.sio_can_write, sip.hasReset ? "True" : "False", sip.sio_reset,
            sip.hasOUT ? "True" : "False",
                handlerNameForPort(sip.port));
    else
        fprintf(st, "(%02x/%1i/%02x/%02x/%02x/%s/%02x/%s), %s",
            sip.port, sip.terminalLine, sip.sio_can_read, sip.sio_cannot_read,
            sip.sio_can_write, sip.hasReset ? "T" : "F", sip.sio_reset,
                sip.hasOUT ? "T" : "F",
                handlerNameForPort(sip.port));
}

static uint32 equalSIP(SIO_PORT_INFO x, SIO_PORT_INFO y) {
    /* isBuiltin is not relevant for equality, only for display */
    return (x.port == y.port) && (x.terminalLine == y.terminalLine) &&
    (x.sio_can_read == y.sio_can_read) && (x.sio_cannot_read == y.sio_cannot_read) &&
    (x.sio_can_write == y.sio_can_write) && (x.hasReset == y.hasReset) &&
    (x.sio_reset == y.sio_reset) && (x.hasOUT == y.hasOUT);
}

static t_stat ptpptr_dev_set_port(UNIT *uptr, int32 value, CONST char *cptr, void *desc) {
    int32 result, n, statusPort, dataPort;
    if (cptr == NULL)
        return SCPE_ARG;
    result = sscanf(cptr, "%x/%x%n", &statusPort, &dataPort, &n);
    if ((result != 2) || (result == EOF) || (cptr[n] != 0))
        return SCPE_ARG;
    if (statusPort != (statusPort & 0xff)) {
        sim_printf("Truncating status port 0x%x to 0x%02x.\n", statusPort, statusPort & 0xff);
        statusPort &= 0xff;
    }
    if (dataPort != (dataPort & 0xff)) {
        sim_printf("Truncating data port 0x%x to 0x%02x.\n", dataPort, dataPort & 0xff);
        dataPort &= 0xff;
    }
    sim_map_resource(statusPort, 1, RESOURCE_TYPE_IO, &sio1s, "sio1s", ptp_dev.flags & ptr_dev.flags & DEV_DIS);
    ptpptrStatusPort = statusPort;
    sim_map_resource(dataPort, 1, RESOURCE_TYPE_IO, &sio1d, "sio1d", ptp_dev.flags & ptr_dev.flags & DEV_DIS);
    ptpptrDataPort = dataPort;
    return SCPE_OK;
}

static t_stat ptpptr_dev_show_port(FILE *st, UNIT *uptr, int32 val, CONST void *desc) {
    fprintf(st, "\n\tStatus port = 0x%02x\n\t  Data port = 0x%02x\n", ptpptrStatusPort, ptpptrDataPort);
    return SCPE_OK;
}

static t_stat sio_dev_set_port(UNIT *uptr, int32 value, CONST char *cptr, void *desc) {
    int32 result, n, position, isDataPort;
    SIO_PORT_INFO sip = { 0 }, old;
    char hasReset, hasOUT;
    if (cptr == NULL)
        return SCPE_ARG;
    result = sscanf(cptr, "%x%n", &sip.port, &n);
    if ((result == 1) && (cptr[n] == 0)) {
        old = lookupPortInfo(sip.port, &position);
        if (old.port == -1) {
            sim_printf("No mapping for port 0x%02x exists - cannot remove.\n", sip.port);
            return SCPE_ARG;
        }
        do {
            port_table[position] = port_table[position + 1];
            position++;
        } while (port_table[position].port != -1);
        sim_map_resource(sip.port, 1, RESOURCE_TYPE_IO, &nulldev, "nulldev", FALSE);
        if (sio_unit.flags & UNIT_SIO_VERBOSE) {
            sim_printf("Removing mapping for port 0x%02x.\n\t", sip.port);
            show_sio_port_info(stdout, old);
        }
        return SCPE_OK;
    }
    result = sscanf(cptr, "%x/%d/%x/%x/%x/%1c/%x/%1c%n", &sip.port,
        &sip.terminalLine, &sip.sio_can_read, &sip.sio_cannot_read,
        &sip.sio_can_write, &hasReset, &sip.sio_reset, &hasOUT, &n);
    if ((result != 8) || (result == EOF) || (cptr[n] != 0))
        return SCPE_ARG;
    result = toBool(hasReset, &sip.hasReset);
    if (result != SCPE_OK)
        return result;
    result = toBool(hasOUT, &sip.hasOUT);
    if (result != SCPE_OK)
        return result;
    if (sip.port != (sip.port & 0xff)) {
        sim_printf("Truncating port 0x%x to 0x%02x.\n", sip.port, sip.port & 0xff);
        sip.port &= 0xff;
    }
    isDataPort = (sip.hasOUT || ((sip.sio_can_read == 0) && (sip.sio_cannot_read == 0) &&
                                 (sip.sio_can_write == 0)));
    old = lookupPortInfo(sip.port, &position);
    if (old.port == sip.port) {
        if (sio_unit.flags & UNIT_SIO_VERBOSE) {
            sim_printf("Replacing mapping for port 0x%02x.\n\t", sip.port);
            show_sio_port_info(stdout, old);
            sim_printf("-> ");
            show_sio_port_info(stdout, sip);
            if (equalSIP(sip, old))
                sim_printf("[same definition, %s]", (isDataPort && (strcmp(handlerNameForPort(old.port), "sio0d") == 0)) || (!isDataPort && (strcmp(handlerNameForPort(old.port), "sio0s") == 0)) ? "same handler" : "different handler");
        }
    } else {
        port_table[position + 1] = old;
        if (sio_unit.flags & UNIT_SIO_VERBOSE) {
            sim_printf("Adding mapping for port 0x%02x.\n\t", sip.port);
            show_sio_port_info(stdout, sip);
        }
    }
    if (sio_unit.flags & UNIT_SIO_VERBOSE)
        sim_printf("\n");
    port_table[position] = sip;
    sim_map_resource(sip.port, 1, RESOURCE_TYPE_IO,
                     isDataPort ? &sio0d : &sio0s, isDataPort ? "sio0d" : "sio0s", FALSE);
    return SCPE_OK;
}

static t_stat sio_dev_show_port(FILE *st, UNIT *uptr, int32 val, CONST void *desc) {
    int32 i, first = TRUE;
    for (i = 0; port_table[i].port != -1; i++)
        if (!port_table[i].isBuiltin) {
            if (first)
                first = FALSE;
            else
                fprintf(st, " ");
            show_sio_port_info(st, port_table[i]);
        }
    if (first)
        fprintf(st, "no extra port");
    return SCPE_OK;
}

static t_stat sio_dev_set_interrupton(UNIT *uptr, int32 value, CONST char *cptr, void *desc) {
    keyboardInterrupt = FALSE;
    return sim_activate(&sio_unit, sio_unit.wait);          /* activate unit */
}

static t_stat sio_dev_set_interruptoff(UNIT *uptr, int32 value, CONST char *cptr, void *desc) {
    keyboardInterrupt = FALSE;
    sim_cancel(&sio_unit);
    return SCPE_OK;
}

static t_stat sio_svc(UNIT *uptr) {
    int32 ch;
    const SIO_PORT_INFO spi = lookupPortInfo(kbdIrqPort, &ch);
    ASSURE(spi.port == kbdIrqPort);
    if (sio0s(kbdIrqPort, 0, 0) & spi.sio_can_read)
        keyboardInterrupt = TRUE;
    if (sio_unit.flags & UNIT_SIO_INTERRUPT)
        sim_activate(&sio_unit, sio_unit.wait);             /* activate unit    */
    return SCPE_OK;
}

static void mapAltairPorts(void) {
    int32 i = 0;
    SIO_PORT_INFO spi;
    do {
        spi = port_table[i++];
        if ((0x02 <= spi.port) && (spi.port <= 0x19))
            sim_map_resource(spi.port, 1, RESOURCE_TYPE_IO,
                             spi.hasOUT ? &sio0d : &sio0s, spi.hasOUT ? "sio0d" : "sio0s", FALSE);
    } while (spi.port >= 0);
}

int32 nulldev(const int32 port, const int32 io, const int32 data) {
    if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnUnassignedPort < warnLevelSIO)) {
        warnUnassignedPort++;
        if (io == 0)
            sim_debug(VERBOSE_MSG, &sio_dev, "SIO: " ADDRESS_FORMAT
                      " Attempt to input from unassigned port 0x%04x - ignored.\n",
                      PCX, port);
        else
            sim_debug(VERBOSE_MSG, &sio_dev, "SIO: " ADDRESS_FORMAT
                      " Attempt to output 0x%02x to unassigned port 0x%04x - ignored.\n",
                      PCX, data, port);
    }
    return io == 0 ? 0xff : 0;
}

int32 sr_dev(const int32 port, const int32 io, const int32 data) {
    if (io == 0) {
        return SR;
    }

    /* Simulate IMSAI functionality of displaying the A */
    /* register on the Programmed Output front panel LEDs */
    if (cpu_unit.flags & UNIT_CPU_PO) {
        sim_printf("PO: %02X\n", data & 0xff);
    }

    return 0;
}

static int32 toBCD(const int32 x) {
    return (x / 10) * 16 + (x % 10);
}

static int32 fromBCD(const int32 x) {
    return 10 * ((0xf0 & x) >> 4) + (0x0f & x);
}

/*  Z80 or 8080 programs communicate with the SIMH pseudo device via port 0xfe.
        The following principles apply:

    1)  For commands that do not require parameters and do not return results
        ld  a,<cmd>
        out (0feh),a
        Special case is the reset command which needs to be send 128 times to make
        sure that the internal state is properly reset.

    2)  For commands that require parameters and do not return results
        ld  a,<cmd>
        out (0feh),a
        ld  a,<p1>
        out (0feh),a
        ld  a,<p2>
        out (0feh),a
        ...
        Note: The calling program must send all parameter bytes. Otherwise
        the pseudo device is left in an undefined state.

    3)  For commands that do not require parameters and return results
        ld  a,<cmd>
        out (0feh),a
        in  a,(0feh)    ; <A> contains first byte of result
        in  a,(0feh)    ; <A> contains second byte of result
        ...
        Note: The calling program must request all bytes of the result. Otherwise
        the pseudo device is left in an undefined state.

    4)  For commands that do require parameters and return results
        ld  a,<cmd>
        out (0feh),a
        ld  a,<p1>
        out (0feh),a
        ld  a,<p2>
        out (0feh),a
        ...             ; send all parameters
        in  a,(0feh)    ; <A> contains first byte of result
        in  a,(0feh)    ; <A> contains second byte of result
        ...

*/

enum simhPseudoDeviceCommands { /* do not change order or remove commands, add only at the end          */
    printTimeCmd,               /*  0 print the current time in milliseconds                            */
    startTimerCmd,              /*  1 start a new timer on the top of the timer stack                   */
    stopTimerCmd,               /*  2 stop timer on top of timer stack and show time difference         */
    resetPTRCmd,                /*  3 reset the PTR device                                              */
    attachPTRCmd,               /*  4 attach the PTR device                                             */
    detachPTRCmd,               /*  5 detach the PTR device                                             */
    getSIMHVersionCmd,          /*  6 get the current version of the SIMH pseudo device                 */
    getClockZSDOSCmd,           /*  7 get the current time in ZSDOS format                              */
    setClockZSDOSCmd,           /*  8 set the current time in ZSDOS format                              */
    getClockCPM3Cmd,            /*  9 get the current time in CP/M 3 format                             */
    setClockCPM3Cmd,            /* 10 set the current time in CP/M 3 format                             */
    getBankSelectCmd,           /* 11 get the selected bank                                             */
    setBankSelectCmd,           /* 12 set the selected bank                                             */
    getCommonCmd,               /* 13 get the base address of the common memory segment                 */
    resetSIMHInterfaceCmd,      /* 14 reset the SIMH pseudo device                                      */
    showTimerCmd,               /* 15 show time difference to timer on top of stack                     */
    attachPTPCmd,               /* 16 attach PTP to the file with name at beginning of CP/M command line*/
    detachPTPCmd,               /* 17 detach PTP                                                        */
    hasBankedMemoryCmd,         /* 18 determines whether machine has banked memory                      */
    setZ80CPUCmd,               /* 19 set the CPU to a Z80                                              */
    set8080CPUCmd,              /* 20 set the CPU to an 8080                                            */
    startTimerInterruptsCmd,    /* 21 start timer interrupts                                            */
    stopTimerInterruptsCmd,     /* 22 stop timer interrupts                                             */
    setTimerDeltaCmd,           /* 23 set the timer interval in which interrupts occur                  */
    setTimerInterruptAdrCmd,    /* 24 set the address to call by timer interrupts                       */
    resetStopWatchCmd,          /* 25 reset the millisecond stop watch                                  */
    readStopWatchCmd,           /* 26 read the millisecond stop watch                                   */
    SIMHSleepCmd,               /* 27 let SIMH sleep for SIMHSleep milliseconds                         */
    getHostOSPathSeparatorCmd,  /* 28 obtain the file path separator of the OS under which SIMH runs    */
    getHostFilenamesCmd,        /* 29 perform wildcard expansion and obtain list of file names          */
    readURLCmd,                 /* 30 read the contents of an URL                                       */
    getCPUClockFrequency,       /* 31 get the clock frequency of the CPU                                */
    setCPUClockFrequency,       /* 32 set the clock frequency of the CPU                                */
    genInterruptCmd,            /* 33 generate interrupt                                                */
    setFCBAddressCmd,           /* 34 set the FCB address for file operations                           */
    kSimhPseudoDeviceCommands
};

static const char *cmdNames[kSimhPseudoDeviceCommands] = {
    "printTime",
    "startTimer",
    "stopTimer",
    "resetPTR",
    "attachPTR",
    "detachPTR",
    "getSIMHVersion",
    "getClockZSDOS",
    "setClockZSDOS",
    "getClockCPM3",
    "setClockCPM3",
    "getBankSelect",
    "setBankSelect",
    "getCommon",
    "resetSIMHInterface",
    "showTimer",
    "attachPTP",
    "detachPTP",
    "hasBankedMemory",
    "setZ80CPU",
    "set8080CPU",
    "startTimerInterrupts",
    "stopTimerInterrupts",
    "setTimerDelta",
    "setTimerInterruptAdr",
    "resetStopWatch",
    "readStopWatch",
    "SIMHSleep",
    "getHostOSPathSeparator",
    "getHostFilenames",
    "readURL",
    "getCPUClockFrequency",
    "setCPUClockFrequency",
    "genInterrupt",
    "setFCBAddressCmd",
};

#define TIMER_STACK_LIMIT          10       /* stack depth of timer stack   */
static uint32 markTime[TIMER_STACK_LIMIT];  /* timer stack                  */
static struct tm currentTime;
static int32 currentTimeValid = FALSE;
static char version[] = "SIMH005";

#define URL_MAX_LENGTH              1024
static uint32 urlPointer;
static char urlStore[URL_MAX_LENGTH];
static uint8 *urlResult = NULL;
static uint32 resultLength;
static uint32 resultPointer;
static int32 showAvailability;
static int32 isInReadPhase;

static t_stat simh_dev_reset(DEVICE *dptr) {
    sim_map_resource(0xfe, 1, RESOURCE_TYPE_IO, &simh_dev, "simh_dev", dptr->flags & DEV_DIS);
    currentTimeValid        = FALSE;
    ClockZSDOSDelta         = 0;
    setClockZSDOSPos        = 0;
    getClockZSDOSPos        = 0;
    ClockCPM3Delta          = 0;
    setClockCPM3Pos         = 0;
    getClockCPM3Pos         = 0;
    getStopWatchDeltaPos    = 0;
    getCommonPos            = 0;
    setTimerDeltaPos        = 0;
    setTimerInterruptAdrPos = 0;
    markTimeSP              = 0;
    versionPos              = 0;
    lastCommand             = 0;
    lastCPMStatus           = SCPE_OK;
    timerInterrupt          = FALSE;
    urlPointer              = 0;
    getClockFrequencyPos    = 0;
    setClockFrequencyPos    = 0;
    setFCBAddressPos        = 0;
    FCBAddress              = CPM_FCB_ADDRESS;
    if (urlResult != NULL) {
        free(urlResult);
        urlResult = NULL;
    }
    simh_dev_set_timeron(NULL, 0, NULL, NULL);
    return SCPE_OK;
}

static void warnNoRealTimeClock(void) {
    sim_debug(VERBOSE_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
              " Sorry - no real time clock available.\n", PCX);
}

static t_stat simh_dev_set_timeron(UNIT *uptr, int32 value, CONST char *cptr, void *desc) {
    timeOfNextInterrupt = sim_os_msec() + timerDelta;
    return sim_activate(&simh_unit, simh_unit.wait);    /* activate unit */
}

static t_stat simh_dev_set_timeroff(UNIT *uptr, int32 value, CONST char *cptr, void *desc) {
    timerInterrupt = FALSE;
    return SCPE_OK;
}

static t_stat simh_svc(UNIT *uptr) {
    if (simh_unit.flags & UNIT_SIMH_TIMERON) {
        uint32 now = sim_os_msec();
        if (now >= timeOfNextInterrupt) {
            timerInterrupt = TRUE;
            if (timerDelta == 0)
                timeOfNextInterrupt = now + DEFAULT_TIMER_DELTA;
            else {
                uint32 newTimeOfNextInterrupt = now + timerDelta - (now - timeOfNextInterrupt) % timerDelta;
                if (newTimeOfNextInterrupt != timeOfNextInterrupt + timerDelta) {
                    sim_debug(VERBOSE_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                              " Timer interrupts skipped %i. Delta %i. Expect %i. Got %i.\n",
                              PCX, (newTimeOfNextInterrupt - timeOfNextInterrupt) / timerDelta - 1,
                              timerDelta, timeOfNextInterrupt + timerDelta - now,
                              newTimeOfNextInterrupt - now);
                }
                timeOfNextInterrupt = newTimeOfNextInterrupt;
            }
        }
        /* post condition: now < timeOfNextInterrupt */
    }
    sim_activate(&simh_unit, simh_unit.wait);   /* activate unit */
    return SCPE_OK;
}

extern void setViewRegisters(void);

static void createCPMCommandLine(void) {
    int32 i, len = (GetBYTEWrapper(FCBAddress) & 0x7f); /* 0x80 contains length of command line, discard first char   */
    for (i = 0; i < len - 1; i++) {
        cpmCommandLine[i] = (char)GetBYTEWrapper(FCBAddress + 0x02 + i); /* the first char, typically ' ', is discarded      */
    }
    cpmCommandLine[i] = 0; /* make C string */
}

/* The CP/M command line is used as the name of a file and UNIT* uptr is attached to it. */
static void attachCPM(UNIT *uptr) {
    createCPMCommandLine();
    if (uptr == &ptr_unit)
        sim_switches = SWMASK('R') | SWMASK('Q');
    else if (uptr == &ptp_unit)
        sim_switches = SWMASK('W') | SWMASK('N') | SWMASK('Q');
    /* 'N' option makes sure that file is properly truncated if it had existed before   */
    sim_quiet = sim_switches & SWMASK ('Q');    /* -q means quiet                       */
    lastCPMStatus = attach_unit(uptr, cpmCommandLine);
    if (lastCPMStatus != SCPE_OK) {
        sim_debug(VERBOSE_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                  " Cannot open '%s' (%s).\n", PCX, cpmCommandLine,
                  sim_error_text(lastCPMStatus));
    }
}

/* setClockZSDOSAdr points to 6 byte block in M: YY MM DD HH MM SS in BCD notation */
static void setClockZSDOS(void) {
    struct tm newTime;
    int32 year = fromBCD(GetBYTEWrapper(setClockZSDOSAdr));
    newTime.tm_year = year < 50 ? year + 100 : year;
    newTime.tm_mon  = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 1)) - 1;
    newTime.tm_mday = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 2));
    newTime.tm_hour = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 3));
    newTime.tm_min  = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 4));
    newTime.tm_sec  = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 5));
    newTime.tm_isdst = 0;
    ClockZSDOSDelta = (int32)(mktime(&newTime) - sim_get_time(NULL));
}

#define SECONDS_PER_MINUTE  60
#define SECONDS_PER_HOUR    (60 * SECONDS_PER_MINUTE)
#define SECONDS_PER_DAY     (24 * SECONDS_PER_HOUR)
static time_t mkCPM3Origin(void) {
    struct tm date;
    date.tm_year    = 77;
    date.tm_mon     = 11;
    date.tm_mday    = 31;
    date.tm_hour    = 0;
    date.tm_min     = 0;
    date.tm_sec     = 0;
    date.tm_isdst   = 0;
    return mktime(&date);
}

/* setClockCPM3Adr points to 5 byte block in M:
    0 - 1 int16:    days since 31 Dec 77
        2 BCD byte: HH
        3 BCD byte: MM
        4 BCD byte: SS                              */
static void setClockCPM3(void) {
    struct tm targetDate;
    const time_t targetSeconds = mkCPM3Origin()                                                                 +
    (GetBYTEWrapper(setClockCPM3Adr) + GetBYTEWrapper(setClockCPM3Adr + 1) * 256) * SECONDS_PER_DAY +
    fromBCD(GetBYTEWrapper(setClockCPM3Adr + 2)) * SECONDS_PER_HOUR                                 +
    fromBCD(GetBYTEWrapper(setClockCPM3Adr + 3)) * SECONDS_PER_MINUTE                               +
    fromBCD(GetBYTEWrapper(setClockCPM3Adr + 4));
    // compute target year, month and day and replace hour, minute and second fields
    targetDate = *localtime(&targetSeconds);
    targetDate.tm_hour = fromBCD(GetBYTEWrapper(setClockCPM3Adr + 2));
    targetDate.tm_min = fromBCD(GetBYTEWrapper(setClockCPM3Adr + 3));
    targetDate.tm_sec = fromBCD(GetBYTEWrapper(setClockCPM3Adr + 4));
    ClockCPM3Delta = (int32)(mktime(&targetDate) - sim_get_time(NULL));
}

static int32 simh_in(const int32 port) {
    int32 result = 0;
    switch(lastCommand) {
        case readURLCmd:
            if (isInReadPhase) {
                if (showAvailability) {
                    if (resultPointer < resultLength)
                        result = 1;
                    else {
                        if (urlResult != NULL)
                            free(urlResult);
                        urlResult = NULL;
                        lastCommand = 0;
                    }
                } else if (resultPointer < resultLength)
                    result = urlResult[resultPointer++];
                showAvailability = 1 - showAvailability;
            } else
                lastCommand = 0;
            break;

        case getHostFilenamesCmd:
            if (nameListHead != NULL) {
                if (currentName == NULL) {
                    deleteNameList();
                    lastCommand = 0;
                } else if (firstPathCharacterIndex <= lastPathSeparatorIndex)
                    result = cpmCommandLine[firstPathCharacterIndex++];
                else {
                    result = currentName -> name[currentNameIndex];
                    if (result == 0) {
                        currentName = currentName -> next;
                        firstPathCharacterIndex = currentNameIndex = 0;
                    } else
                        currentNameIndex++;
                }
            }
            break;

        case attachPTRCmd:

        case attachPTPCmd:
            result = lastCPMStatus;
            lastCommand = 0;
            break;

        case getClockZSDOSCmd:
            if (currentTimeValid)
                switch(getClockZSDOSPos) {

                    case 0:
                        result = toBCD(currentTime.tm_year > 99 ?
                            currentTime.tm_year - 100 : currentTime.tm_year);
                        getClockZSDOSPos = 1;
                        break;

                    case 1:
                        result = toBCD(currentTime.tm_mon + 1);
                        getClockZSDOSPos = 2;
                        break;

                    case 2:
                        result = toBCD(currentTime.tm_mday);
                        getClockZSDOSPos = 3;
                        break;

                    case 3:
                        result = toBCD(currentTime.tm_hour);
                        getClockZSDOSPos = 4;
                        break;

                    case 4:
                        result = toBCD(currentTime.tm_min);
                        getClockZSDOSPos = 5;
                        break;

                    case 5:
                        result = toBCD(currentTime.tm_sec);
                        getClockZSDOSPos = lastCommand = 0;
                        break;
                }
            else
                result = getClockZSDOSPos = lastCommand = 0;
            break;

        case getClockCPM3Cmd:
            if (currentTimeValid)
                switch(getClockCPM3Pos) {
                    case 0:
                        result = daysCPM3SinceOrg & 0xff;
                        getClockCPM3Pos = 1;
                        break;

                    case 1:
                        result = (daysCPM3SinceOrg >> 8) & 0xff;
                        getClockCPM3Pos = 2;
                        break;

                    case 2:
                        result = toBCD(currentTime.tm_hour);
                        getClockCPM3Pos = 3;
                        break;

                    case 3:
                        result = toBCD(currentTime.tm_min);
                        getClockCPM3Pos = 4;
                        break;

                    case 4:
                        result = toBCD(currentTime.tm_sec);
                        getClockCPM3Pos = lastCommand = 0;
                        break;
                }
            else
                result = getClockCPM3Pos = lastCommand = 0;
            break;

        case getSIMHVersionCmd:
            result = version[versionPos++];
            if (result == 0)
                versionPos = lastCommand = 0;
            break;

        case getBankSelectCmd:
            if (cpu_unit.flags & UNIT_CPU_BANKED)
                result = getBankSelect();
            else {
                result = 0;
                sim_debug(VERBOSE_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                          " Get selected bank ignored for non-banked memory.\n", PCX);
            }
            lastCommand = 0;
            break;

        case getCommonCmd:
            if (getCommonPos == 0) {
                result = getCommon() & 0xff;
                getCommonPos = 1;
            } else {
                result = (getCommon() >> 8) & 0xff;
                getCommonPos = lastCommand = 0;
            }
            break;

        case getCPUClockFrequency:
            if (getClockFrequencyPos == 0) {
                result = getClockFrequency() & 0xff;
                getClockFrequencyPos = 1;
            } else {
                result = (getClockFrequency() >> 8) & 0xff;
                getClockFrequencyPos = lastCommand = 0;
            }
            break;

        case hasBankedMemoryCmd:
            result = cpu_unit.flags & UNIT_CPU_BANKED ? MAXBANKS : 0;
            lastCommand = 0;
            break;

        case readStopWatchCmd:
            if (getStopWatchDeltaPos == 0) {
                result = stopWatchDelta & 0xff;
                getStopWatchDeltaPos = 1;
            } else {
                result = (stopWatchDelta >> 8) & 0xff;
                getStopWatchDeltaPos = lastCommand = 0;
            }
            break;

        case getHostOSPathSeparatorCmd:
            result = hostPathSeparator;
            break;

        default:
            sim_debug(VERBOSE_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                      " Undefined IN from SIMH pseudo device on port %03xh ignored.\n",
                      PCX, port);
            result = lastCommand = 0;
    }
    return result;
}

void do_SIMH_sleep(void) {
    /* Do not sleep when timer interrupts are pending or are about to be created.
     Otherwise there is the possibility that such interrupts are skipped. */
    if ((simh_unit.flags & UNIT_SIMH_TIMERON) && rtc_avail && (sim_os_msec() + 1 >= timeOfNextInterrupt))
        return;
    if (SIMHSleep && !sio_unit.u4)
        /* time to sleep and SIO not attached to a file.
         Use 'D SLEEP 0' to disable this functionality when not needed. */
        sim_os_ms_sleep(SIMHSleep);
}

static int32 simh_out(const int32 port, const int32 data) {
    time_t now;
    switch(lastCommand) {
        case readURLCmd:
            if (isInReadPhase)
                lastCommand = 0;
            else {
                if (data) {
                    if (urlPointer < URL_MAX_LENGTH - 1)
                        urlStore[urlPointer++] = data & 0xff;
                } else {
                    if (urlResult != NULL)
                        free(urlResult);
                    urlStore[urlPointer] = 0;
                    urlResult = URLContents(urlStore, &resultLength);
                    urlPointer = resultPointer = 0;
                    showAvailability = 1;
                    isInReadPhase = TRUE;
                }
            }
            break;

        case setClockZSDOSCmd:
            if (setClockZSDOSPos == 0) {
                setClockZSDOSAdr = data;
                setClockZSDOSPos = 1;
            } else {
                setClockZSDOSAdr |= (data << 8);
                setClockZSDOS();
                setClockZSDOSPos = lastCommand = 0;
            }
            break;

        case setClockCPM3Cmd:
            if (setClockCPM3Pos == 0) {
                setClockCPM3Adr = data;
                setClockCPM3Pos = 1;
            } else {
                setClockCPM3Adr |= (data << 8);
                setClockCPM3();
                setClockCPM3Pos = lastCommand = 0;
            }
            break;

        case setCPUClockFrequency:
            if (setClockFrequencyPos == 0) {
                newClockFrequency = data;
                setClockFrequencyPos = 1;
            } else {
                setClockFrequency((data << 8) | newClockFrequency);
                setClockFrequencyPos = lastCommand = 0;
            }
            break;

        case setBankSelectCmd:
            if (cpu_unit.flags & UNIT_CPU_BANKED)
                setBankSelect(data & BANKMASK);
            else
                sim_debug(VERBOSE_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                          " Set selected bank to %i ignored for non-banked memory.\n",
                          PCX, data & 3);
            lastCommand = 0;
            break;

        case setTimerDeltaCmd:
            if (setTimerDeltaPos == 0) {
                timerDelta          = data;
                setTimerDeltaPos    = 1;
            } else {
                timerDelta |= (data << 8);
                setTimerDeltaPos = lastCommand = 0;
                if (timerDelta == 0) {
                    timerDelta = DEFAULT_TIMER_DELTA;
                    sim_debug(VERBOSE_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                              " Timer delta set to 0 ms ignored. Using %i ms instead.\n",
                              PCX, DEFAULT_TIMER_DELTA);
                }
            }
            break;

        case setTimerInterruptAdrCmd:
            if (setTimerInterruptAdrPos == 0) {
                timerInterruptHandler       = data;
                setTimerInterruptAdrPos     = 1;
            } else {
                timerInterruptHandler |= (data << 8);
                setTimerInterruptAdrPos = lastCommand = 0;
            }
            break;

        case genInterruptCmd:
            if (genInterruptPos == 0) {
                genInterruptVec = data;
                genInterruptPos = 1;
                sim_printf("genInterruptVec=%d genInterruptPos=%d\n", genInterruptVec, genInterruptPos);
            } else {
                vectorInterrupt |= (1 << genInterruptVec);
                dataBus[genInterruptVec] = data;
                genInterruptPos = lastCommand = 0;
                sim_printf("genInterruptVec=%d vectorInterrupt=%X dataBus=%02X genInterruptPos=%d\n", genInterruptVec, vectorInterrupt, data, genInterruptPos);
            }
            break;
        case setFCBAddressCmd:
            /* SETFCBADDR command always takes four bytes:
             * Byte Z80     8086    68000   32-Bit Address Space (future)
             * ---- -----   -----   -----   -----------------------------
             * 1     A7:0    A7:0    A7:0    A7:0
             * 2    A15:8   A15:8   A15:8   A15:8
             * 3        0      DS   A23:16  A23:16
             * 4        0       0       0   A31:24
             * For 8086, the FCB address is updated by writing the fourth
             * byte, using the offset A15:0 from the first two bytes and
             * taking the segment from the CPU's DS register.
             */
            switch (setFCBAddressPos) {
            case 0: /* Address 7:0 */
                FCBAddress = data;
                setFCBAddressPos++;
                break;
            case 1: /* Address 15:8 */
                FCBAddress |= (data << 8);
                setFCBAddressPos++;
                break;
            case 2: /* Address 23:16 */
                FCBAddress |= (data << 16);
                setFCBAddressPos++;
                break;
            default: /* Address 31:24 */
                if (chiptype == CHIP_TYPE_8086) {
                    /* Mask the offset to 16-bits and add in the segment from DS register. */
                    setViewRegisters();
                    FCBAddress &= 0xFFFF;
                    FCBAddress += (DS_S << 4);
                    sim_debug(CMD_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                        " FCBAddress=0x%05x, DS=0x%04x\n", PCX, FCBAddress, DS_S);
                } else {
                    FCBAddress |= (data << 24);
                    sim_debug(CMD_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                        " FCBAddress=0x%08x\n", PCX, FCBAddress);
                }
                setFCBAddressPos = lastCommand = 0;
                break;
            }
            break;

        default: /* lastCommand not yet set */
            sim_debug(CMD_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                      " CMD(0x%02x) <- %i (0x%02x, '%s')\n",
                      PCX, port, data, data,
                      (0 <= data) && (data < kSimhPseudoDeviceCommands) ?
                      cmdNames[data] : "Unknown command");

            lastCommand = data;
            switch(data) {
                case readURLCmd:
                    urlPointer = 0;
                    isInReadPhase = FALSE;
                    break;

                case getHostFilenamesCmd:   /* list files of host file directory */
                    if (nameListHead == NULL) {
                        t_stat result;

                        createCPMCommandLine();
                        lastPathSeparatorIndex = 0;
                        while (cpmCommandLine[lastPathSeparatorIndex])
                            lastPathSeparatorIndex++;
                        while ((lastPathSeparatorIndex >= 0) && (cpmCommandLine[lastPathSeparatorIndex] != hostPathSeparator) && (cpmCommandLine[lastPathSeparatorIndex] != hostPathSeparatorAlt))
                            lastPathSeparatorIndex--;
                        firstPathCharacterIndex = 0;
                        deleteNameList();
                        result = sim_dir_scan(cpmCommandLine, processDirEntry, NULL);
                        if (result == SCPE_OK) {
                            currentName = nameListHead;
                            currentNameIndex = 0;
                        } else {
                            deleteNameList();
                            sim_debug(VERBOSE_MSG, &simh_device,
                                      "SIMH: " ADDRESS_FORMAT
                                      " Cannot expand '%s'. Error is %s.\n",
                                      PCX, cpmCommandLine, sim_error_text(result));
                        }
                    }
                    break;

                case SIMHSleepCmd:
                    do_SIMH_sleep();
                    break;

                case printTimeCmd:  /* print time */
                    if (rtc_avail)
                        sim_printf("SIMH: " ADDRESS_FORMAT " Current time in milliseconds = %d.\n", PCX, sim_os_msec());
                    else
                        warnNoRealTimeClock();
                    break;

                case startTimerCmd: /* create a new timer on top of stack */
                    if (rtc_avail)
                        if (markTimeSP < TIMER_STACK_LIMIT)
                            markTime[markTimeSP++] = sim_os_msec();
                        else
                            sim_printf("SIMH: " ADDRESS_FORMAT " Timer stack overflow.\n", PCX);
                    else
                        warnNoRealTimeClock();
                    break;

                case stopTimerCmd:  /* stop timer on top of stack and show time difference */
                    if (rtc_avail)
                        if (markTimeSP > 0) {
                            uint32 delta = sim_os_msec() - markTime[--markTimeSP];
                            sim_printf("SIMH: " ADDRESS_FORMAT " Timer stopped. Elapsed time in milliseconds = %d.\n", PCX, delta);
                        } else
                            sim_printf("SIMH: " ADDRESS_FORMAT " No timer active.\n", PCX);
                    else
                        warnNoRealTimeClock();
                    break;

                case resetPTRCmd:   /* reset ptr device */
                    ptr_reset(&ptr_dev);
                    break;

                case attachPTRCmd:  /* attach ptr to the file with name at beginning of CP/M command line */
                    attachCPM(&ptr_unit);
                    break;

                case detachPTRCmd:  /* detach ptr */
                    detach_unit(&ptr_unit);
                    break;

                case getSIMHVersionCmd:
                    versionPos = 0;
                    break;

                case getClockZSDOSCmd:
                    sim_get_time(&now);
                    now += ClockZSDOSDelta;
                    currentTime = *localtime(&now);
                    currentTimeValid = TRUE;
                    getClockZSDOSPos = 0;
                    break;

                case setClockZSDOSCmd:
                    setClockZSDOSPos = 0;
                    break;

                case getClockCPM3Cmd:
                    sim_get_time(&now);
                    now += ClockCPM3Delta;
                    currentTime = *localtime(&now);
                    currentTimeValid = TRUE;
                    daysCPM3SinceOrg = (int32) ((now - mkCPM3Origin()) / SECONDS_PER_DAY);
                    getClockCPM3Pos = 0;
                    break;

                case setClockCPM3Cmd:
                    setClockCPM3Pos = 0;
                    break;

                case getCommonCmd:
                    getCommonPos = 0;
                    break;

                case getCPUClockFrequency:
                    getClockFrequencyPos = 0;
                    break;

                case setCPUClockFrequency:
                    setClockFrequencyPos = 0;
                    break;

                case getBankSelectCmd:
                case setBankSelectCmd:
                case hasBankedMemoryCmd:
                case getHostOSPathSeparatorCmd:
                    break;

                case resetSIMHInterfaceCmd:
                    markTimeSP  = 0;
                    lastCommand = 0;
                    deleteNameList();
                    setFCBAddressPos = 0;
                    FCBAddress = CPM_FCB_ADDRESS;
                    break;

                case showTimerCmd:  /* show time difference to timer on top of stack */
                    if (rtc_avail)
                        if (markTimeSP > 0) {
                            uint32 delta = sim_os_msec() - markTime[markTimeSP - 1];
                            sim_printf("SIMH: " ADDRESS_FORMAT " Timer running. Elapsed in milliseconds = %d.\n", PCX, delta);
                        } else
                            sim_printf("SIMH: " ADDRESS_FORMAT " No timer active.\n", PCX);
                    else
                        warnNoRealTimeClock();
                    break;

                case attachPTPCmd:  /* attach ptp to the file with name at beginning of CP/M command line */
                    attachCPM(&ptp_unit);
                    break;

                case detachPTPCmd:  /* detach ptp */
                    detach_unit(&ptp_unit);
                    break;

                case setZ80CPUCmd:
                    chiptype = CHIP_TYPE_Z80;
                    break;

                case set8080CPUCmd:
                    chiptype = CHIP_TYPE_8080;
                    break;

                case startTimerInterruptsCmd:
                    if (simh_dev_set_timeron(NULL, 0, NULL, NULL) == SCPE_OK) {
                        timerInterrupt = FALSE;
                        simh_unit.flags |= UNIT_SIMH_TIMERON;
                    }
                    break;

                case stopTimerInterruptsCmd:
                    simh_unit.flags &= ~UNIT_SIMH_TIMERON;
                    simh_dev_set_timeroff(NULL, 0, NULL, NULL);
                    break;

                case setTimerDeltaCmd:
                    setTimerDeltaPos = 0;
                    break;

                case setTimerInterruptAdrCmd:
                    setTimerInterruptAdrPos = 0;
                    break;

                case resetStopWatchCmd:
                    stopWatchNow = rtc_avail ? sim_os_msec() : 0;
                    break;

                case readStopWatchCmd:
                    getStopWatchDeltaPos = 0;
                    stopWatchDelta = rtc_avail ? sim_os_msec() - stopWatchNow : 0;
                    break;

                case setFCBAddressCmd:
                    setFCBAddressPos = 0;
                    break;

                default:
                    sim_debug(CMD_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                              " Unknown command (%i) to SIMH pseudo device on port %03xh ignored.\n",
                              PCX, data, port);
            }
    }
    return 0x00; /* ignored, since OUT */
}

/* port 0xfe is a device for communication SIMH <--> Altair machine */
int32 simh_dev(const int32 port, const int32 io, const int32 data) {
    int32 result = 0;
    if (io == 0) {
        result = simh_in(port);
        sim_debug(IN_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                  " IN(0x%02x) -> %i (0x%02x, '%c')\n", PCX,
                  port, result, result,
                  (32 <= (result & 0xff)) && ((result & 0xff) <= 127) ? (result & 0xff) : '?');

    } else {
        sim_debug(OUT_MSG, &simh_device, "SIMH: " ADDRESS_FORMAT
                  " OUT(0x%02x) <- %i (0x%02x, '%c')\n", PCX,
                  port, data, data,
                  (32 <= (data & 0xff)) && ((data & 0xff) <= 127) ? (data & 0xff) : '?');
        simh_out(port, data);
    }
    return result;
}
