-- HalfDuplexImpl.mesa (last edited by: Garlick on: February 27, 1981  5:22 PM) --

DIRECTORY
  BufferDefs USING [Buffer],
  CommFlags USING [doStats],
  DriverTypes USING [
    Encapsulation, PhonePacketType, phoneEncapsulationOffset,
    phoneEncapsulationBytes],
  HalfDuplex USING [],
  Process USING [SetTimeout, MsecToTicks],
  ProcessorFace USING [microsecondsPerHundredPulses, GetClockPulses],
  RS232C USING [
    ChannelHandle, LineSpeed, DeviceStatus, CompletionHandle, PhysicalRecord,
    ChannelSuspended, Put, TransmitNow, SetParameter, GetStatus, StatusWait],
  NSAddress USING [ProcessorID];

HalfDuplexImpl: MONITOR
  IMPORTS RS232C, Process, ProcessorFace EXPORTS HalfDuplex SHARES BufferDefs =
  BEGIN
  -- types
  ControlMode: TYPE = {unknown, master, slave};
  TurnAroundState: TYPE = {sending, receiving};
  Timer: TYPE = {
    turnAround,  -- max time sending in one direction
    noMoreToSend,  -- idle sender becomes receiver after this time
    masterResponse};
  -- master sends turn-around if it receives nothing in this time
  StatsRecord: TYPE = RECORD [
    turnArndSent, turnArndRcvd, masterTOs, noSendTOs, turnTOs: CARDINAL];
  -- writeable data
  timerProcess: PROCESS;
  turnAroundState: TurnAroundState;
  controlMode: ControlMode;
  timer: ARRAY Timer OF LONG CARDINAL;  -- in pulses
  timeout: ARRAY Timer OF LONG CARDINAL ← [
    turnAround:, noMoreToSend: nothingSentNMTSPulses,
    masterResponse: initialMasterResponsePulses];
  stopTimer: BOOLEAN;
  timeoutTimer: CONDITION;
  turnAroundArrived: CONDITION;
  ourTurnToSend: BOOLEAN;
  driverSending: BOOLEAN;
  senderHasMore: BOOLEAN;
  sendFinished: CONDITION;
  channel: RS232C.ChannelHandle;
  clearToSendUp: BOOLEAN;
  ourProcessorID: NSAddress.ProcessorID;
  statsRec: StatsRecord;
  -- constants
  timerWakeup: CARDINAL = 250;
  -- the following are optimized for the current packet stream allocation window size (statically set to 3, currently).  Our algorithm won't give great performance if window size gets bigger.
  bps1200TurnAroundTimeout: CARDINAL = 13000;  -- msecs
  bps2400TurnAroundTimeout: CARDINAL = bps1200TurnAroundTimeout/2;  -- msecs
  bps4800TurnAroundTimeout: CARDINAL = bps1200TurnAroundTimeout/4;  -- msecs
  bps9600TurnAroundTimeout: CARDINAL = bps1200TurnAroundTimeout/8;  -- msecs
  -- noMoreToSend (NMTS) Timeouts
  nothingSentNMTSPulses: LONG CARDINAL ← MilliSecondsToPulses[
    nothingSentNMTSTimeout];
  sentSomethingNMTSPulses: LONG CARDINAL ← MilliSecondsToPulses[
    sentSomethingNMTSTimeout];
  remoteAnxiousNMTSPulses: LONG CARDINAL ← MilliSecondsToPulses[
    remoteAnxiousNMTSTimeout];
  initialMasterResponsePulses: LONG CARDINAL ← MilliSecondsToPulses[
    initialMasterResponseTimeout];
  nothingSentNMTSTimeout: CARDINAL = 500;  -- msecs
  sentSomethingNMTSTimeout: CARDINAL = 250;  -- msecs
  remoteAnxiousNMTSTimeout: CARDINAL = 250;  -- msecs
  initialMasterResponseTimeout: CARDINAL = 5000;  -- msecs
  -- errors and signals
  -- ********Init/termination********
  Initialize: PUBLIC PROCEDURE [
    chHandle: RS232C.ChannelHandle, lineSpeed: RS232C.LineSpeed,
    ourHostNumber: NSAddress.ProcessorID] =
    -- initialize timer BOOLEANs and start timer process
    BEGIN
    -- local
    timeInMilliSecs: CARDINAL;
    channel ← chHandle;  -- save channel handle
    -- the following is to optimize for OISCP windowing, which currently batches 3 packets before requesting an ACK
    timeInMilliSecs ←
      SELECT lineSpeed FROM
        bps2400 => bps2400TurnAroundTimeout,
        bps4800 => bps4800TurnAroundTimeout,
        bps9600 => bps9600TurnAroundTimeout,
        ENDCASE => bps1200TurnAroundTimeout;
    timeout[turnAround] ← MilliSecondsToPulses[timeInMilliSecs];
    stopTimer ← FALSE;
    clearToSendUp ← FALSE;
    controlMode ← unknown;
    turnAroundState ← receiving;
    ourProcessorID ← ourHostNumber;
    ourTurnToSend ← FALSE;
    driverSending ← FALSE;
    senderHasMore ← FALSE;
    StartTimer[masterResponse];
    Process.SetTimeout[@timeoutTimer, Process.MsecToTicks[timerWakeup]];
    timerProcess ← FORK TimerProcess[];
    IF CommFlags.doStats THEN statsRec ← [0, 0, 0, 0, 0];
    END;

  Destroy: PUBLIC PROCEDURE =
    -- terminate timer process
    BEGIN
    -- local
    NotifyConditions: ENTRY PROCEDURE =
      BEGIN
      stopTimer ← TRUE;
      ourTurnToSend ← TRUE;
      driverSending ← FALSE;
      NOTIFY timeoutTimer;
      NOTIFY turnAroundArrived;
      NOTIFY sendFinished;
      END;
    NotifyConditions[];
    JOIN timerProcess;
    END;
  -- ********timers********

  TimerProcess: PROCEDURE =
    -- loop checking times, depending on the turnAroundState
    BEGIN
    CheckIfTimedOut: PROCEDURE [timerSelect: Timer] RETURNS [timedOut: BOOLEAN] =
      BEGIN
      timedOut ← (GetClock[] - timer[timerSelect] > timeout[timerSelect]);
      IF CommFlags.doStats THEN
        IF timedOut THEN
          SELECT timerSelect FROM
            masterResponse => StatIncr[@statsRec.masterTOs];
            turnAround => StatIncr[@statsRec.turnTOs];
            noMoreToSend => StatIncr[@statsRec.noSendTOs];
            ENDCASE => ERROR;
      END;
    RandomWaitTime: PROCEDURE RETURNS [CARDINAL] = INLINE
      -- gives random number in range [1000..3048)
      BEGIN
      LowExtractor: TYPE = MACHINE DEPENDENT RECORD [
        highBitsLowWord: [0..32), lowTenBits: [0..2048), secondWord: CARDINAL];
      RETURN[(LOOPHOLE[GetClock[], LowExtractor]).lowTenBits + 1000];
      END;
    UNTIL stopTimer DO
      SELECT turnAroundState FROM
        sending =>
          IF CheckIfTimedOut[noMoreToSend] OR CheckIfTimedOut[turnAround] THEN
            BEGIN
            SendLTA[];  -- will happen after any SendFrame in progress
            IF controlMode = unknown THEN
              SetTimer[masterResponse, RandomWaitTime[]];
            IF controlMode # slave THEN StartTimer[masterResponse];
            turnAroundState ← receiving;
            END;
        receiving =>  -- slave side has no timeout, it just awaits turnaround
          IF controlMode # slave THEN
            BEGIN
            IF CheckIfTimedOut[masterResponse] THEN
              BEGIN  -- assume some packet lost
              StartTimer[turnAround];
              SetTimer[noMoreToSend, nothingSentNMTSPulses];
              -- longer if waiting to send first packet than if we sent one already
              StartTimer[noMoreToSend];
              turnAroundState ← sending;
              senderHasMore ← FALSE;
              IF controlMode # unknown THEN NotifyOurTurnToSend[];
              END;
            END;
        ENDCASE => ERROR;
      WaitTimer[];
      ENDLOOP;
    END;

  WaitTimer: ENTRY PROCEDURE = BEGIN WAIT timeoutTimer; END;

  StartTimer: PROCEDURE [timerSelect: Timer] = INLINE
    -- put current time in the timer
    BEGIN timer[timerSelect] ← ProcessorFace.GetClockPulses[]; END;

  SetTimer: PROCEDURE [timer: Timer, pulses: LONG CARDINAL] = INLINE
    BEGIN timeout[timer] ← pulses; END;

  GetClock: PROCEDURE RETURNS [LONG CARDINAL] = INLINE
    -- put current pulses in the timer; pulse arithmetic works in wraparound case
    BEGIN RETURN[ProcessorFace.GetClockPulses[]]; END;
  -- ********Turn-around********

  WaitToSend: PUBLIC PROCEDURE =
    -- wait to be in send mode and for CTS
    BEGIN
    -- locals
    AwaitLineTurnAround: ENTRY PROCEDURE =
      BEGIN
      UNTIL ourTurnToSend DO WAIT turnAroundArrived; ENDLOOP;
      driverSending ← TRUE;
      END;
    AwaitLineTurnAround[];
    AwaitCTS[];  -- set RTS if necessary, wait for CTS

    END;

  SendCompleted: PUBLIC ENTRY PROCEDURE [moreToSend: BOOLEAN] =
    -- tells us we can send LTA now
    BEGIN
    ENABLE UNWIND => NULL;
    driverSending ← FALSE;
    NOTIFY sendFinished;
    SetTimer[noMoreToSend, sentSomethingNMTSPulses];  -- reduce timeout
    StartTimer[noMoreToSend];
    senderHasMore ← moreToSend;
    END;

  CheckForTurnAround: PUBLIC PROCEDURE [buffer: BufferDefs.Buffer]
    RETURNS [throwAway: BOOLEAN] =
    -- check packet for line turn-around; determine mode if we need to
    BEGIN OPEN phoneEncap: buffer.encapsulation;
    -- locals
    remoteProcessorID: NSAddress.ProcessorID;
    IF controlMode = unknown THEN
      BEGIN
      remoteProcessorID ← LOOPHOLE[phoneEncap.pnSrcID];
      SELECT ourProcessorID.a FROM  -- determine mode by comparing processorIDs

        = remoteProcessorID.a =>
          SELECT ourProcessorID.b FROM
            = remoteProcessorID.b =>
              controlMode ←
                (IF ourProcessorID.c > remoteProcessorID.c THEN master
                 ELSE slave);
            > remoteProcessorID.b => controlMode ← master;
            ENDCASE => controlMode ← slave;
        > remoteProcessorID.a => controlMode ← master;
        ENDCASE => controlMode ← slave;
      IF controlMode = master THEN
        SetTimer[masterResponse, initialMasterResponsePulses];
      END;
    IF phoneEncap.pnType IN [turnAroundPhonePacket..turnAroundMTSPhonePacket] THEN
      BEGIN  -- a driver turn-around packet (no piggybacking yet)
      IF phoneEncap.pnType = turnAroundMTSPhonePacket THEN
        SetTimer[noMoreToSend, remoteAnxiousNMTSTimeout];
      IF CommFlags.doStats THEN StatIncr[@statsRec.turnArndRcvd];
      StartTimer[turnAround];
      StartTimer[noMoreToSend];
      turnAroundState ← sending;
      NotifyOurTurnToSend[];  -- tell anyone waiting to send real packets
      senderHasMore ← FALSE;
      throwAway ← TRUE;
      END
    ELSE
      BEGIN
      throwAway ← FALSE;
      StartTimer[masterResponse];  -- time only from last thing heard

      END;
    END;

  NotifyOurTurnToSend: ENTRY PROCEDURE =
    -- tell those waiting to send real packets
    BEGIN ourTurnToSend ← TRUE; NOTIFY turnAroundArrived; END;

  AwaitCTS: PROCEDURE =
    -- if modem not ready, make it so (set RTS and await CTS)
    BEGIN
    -- locals
    status: RS232C.DeviceStatus;
    IF ~clearToSendUp THEN
      BEGIN
      ENABLE RS232C.ChannelSuspended => GOTO fatalPlace;
      RS232C.SetParameter[channel, [requestToSend[TRUE]]];
      status ← RS232C.GetStatus[channel];
      UNTIL status.clearToSend DO
        status ← RS232C.StatusWait[channel, status]; ENDLOOP;
      clearToSendUp ← TRUE;
      END;
    EXITS fatalPlace => NULL;
    END;

  SendLTA: PROCEDURE =
    -- set flag under monitor and so SendFrame won't send (and not piggyback the turnaround on another packet)
    BEGIN
    -- locals
    complHandle: RS232C.CompletionHandle;
    rec: RS232C.PhysicalRecord ← [
      header: [NIL, 0, 0], body:, trailer: [NIL, 0, 0]];
    buffer: DriverTypes.Encapsulation ← [
      phonenet[
      framing0:, framing1:, framing2:, framing3:, framing4:, framing5:,
      recognition: 0,
      pnType:
      (IF senderHasMore THEN turnAroundMTSPhonePacket ELSE turnAroundPhonePacket),
      pnSrcID: LOOPHOLE[ourProcessorID]]];
    ClearOurTurn[];
    AwaitNoSending[];
    AwaitCTS[];
    -- send it, wait for completion, and lower RTS
    rec.body.blockPointer ← @buffer + DriverTypes.phoneEncapsulationOffset;
    -- word boundary
    rec.body.startIndex ← 0;
    rec.body.stopIndexPlusOne ← DriverTypes.phoneEncapsulationBytes;
    BEGIN
    ENABLE RS232C.ChannelSuspended => GOTO fatalPlace;
    complHandle ← RS232C.Put[channel, @rec];
    [, ] ← RS232C.TransmitNow[channel, complHandle];
    RS232C.SetParameter[channel, [requestToSend[FALSE]]];
    IF CommFlags.doStats THEN StatIncr[@statsRec.turnArndSent];
    END;  -- ENABLE
    clearToSendUp ← FALSE;
    EXITS fatalPlace => NULL;
    END;

  ClearOurTurn: ENTRY PROCEDURE = BEGIN ourTurnToSend ← FALSE; END;

  AwaitNoSending: ENTRY PROCEDURE =
    BEGIN UNTIL ~driverSending DO WAIT sendFinished; ENDLOOP; END;
  -- **************** Pulse conversion ****************

  Pulses: TYPE = RECORD[LONG CARDINAL];
  
  MilliSecondsToPulses: PROCEDURE [ms: LONG CARDINAL]
    RETURNS [pulses: Pulses] =
    BEGIN
    -- we must be carefull about multiplication overflow since milliSeconds must be
    -- converted to microSeconds
    micro: LONG CARDINAL = MIN[LAST[LONG CARDINAL]/1000,ms] * 1000;
    hundreds: LONG CARDINAL = micro / ProcessorFace.microsecondsPerHundredPulses; 
    pulses ← [MIN[LAST[LONG CARDINAL]/100,hundreds]*100];
    END; -- end MilliSecondsToPulses

  -- **************** Statistics ****************

  StatIncr: PROCEDURE [counter: POINTER TO CARDINAL] =
    -- add one to counter
    BEGIN
    -- locals
    counter↑ ← (counter↑ + 1) MOD (LAST[CARDINAL] - 1);
    END;

  StatBump: PROCEDURE [counter: POINTER TO CARDINAL, bumpAmount: CARDINAL] =
    -- add bumpAmount to counter; if bumpAmount < 10000, there will never be overflow
    BEGIN
    -- locals
    counter↑ ← (counter↑ + bumpAmount) MOD (LAST[CARDINAL] - 10000);
    END;
  -- MAIN PROGRAM --

  END.
LOG
Time: July 11, 1980  3:46 PM	By: Garlick	Action: Created.
Time: August 11, 1980  3:12 PM	By: Garlick	Action: Added notification of the turnAroundArrived and sendFinished conditions in Destroy.
Time: January 23, 1981  3:06 PM	By: Garlick	Action: Converted all timers to pulses.  Pulse arithmetic works correctly with overflow and clock wraparound.
Time: February 27, 1981  5:22 PM	By: Garlick	Action: Fixed a couple of places that didn't get all timers converted to pulses.