unit SerialManager;

  interface
    uses
      Classes       (* tStringlist *),
      HC3DM         (* tGlobalStatus, gsConnected, gsReceivingDump,
                         gsUnconnected, msOneSecond *),
      MyConsts      (* EXCLAIM, nullstring, TAB *),
      StdCtrls      (* tLabel, tListBox *),
      ObservedAndWM (* tWindowsToBase, wtbConnect, wtbTickleBaseStation *) ,
      ZylSerialPort (* br038400, tCommPort, spCustom, dw8Bits, hfNONE,
                         sb1Bit, sfNone, StringToCommPort, tZylSerialPort *);
    type
      tCharEater = procedure(const C: char) of object;

    type
      tSerialManager = class(tObject)
          procedure ZylSerialPortDisconnect(Sender: tObject; Port: tCommPort);
          procedure ZylSerialPortFault     (Sender: tObject; Port: tCommPort);
          procedure ZylSerialPortReceive   (Sender: tObject; Buffer: string);
          procedure ZylSerialPortReconnect (Sender: tObject;
                                              const Port: tCommPort;
                                              const BaudRate: tBaudRate;
                                              var Cancel: Boolean);
        private
          TheShowAnnouncements: boolean;
          Verbose:              boolean;
          TheCharEater: tCharEater;
          LastBufferReceived: tDateTime;
          LastMessageSent:    tDateTime;
          msMinInterval: integer;
          msMaxInterval: integer;
          msMinTXGap:    integer;
          lbTheHowConnected:    tLabel;
          lbTheInputBufferSize: tLabel;
          lbTheProcessingTime:  tLabel;
          ZylUnlockKey: string;

          procedure ApplySettings;
        public
          PortConnected: boolean;

          procedure   Announce(const MYID: integer; const S: string);
          constructor Create(CharEater: tCharEater;
                             lbHowConnected, lbInputBufferSize,
                             lbProcessingTime: tLabel);
          destructor  Destroy; override;
          procedure   Disconnect(Reason: string = nullstring);
          procedure   IntMessageToBaseStation(const WTB: tWindowsToBase;
                                              const Arg: array of integer;
                                              MessageForChat: string = nullstring);
          procedure   ListPorts(ListBox: tListBox);
          function    MadeConnection(const AnnotatedPortName: string): boolean;
          procedure   MessageToBaseStation(const WTB: tWindowsToBase;
                                           Payload: string = nullstring;
                                           MessageForChat: string = nullstring);
          procedure   SetGlobalStatus(const NewStatus: tGlobalStatus;
                                      const Reason: string);
          procedure   ShowAnnouncements(const ShowThem: boolean);
          procedure   WTBToBaseStation(const WTB: tWindowsToBase;
                                       MessageForChat: string = nullstring);
        end (* tSerialManager *);

  implementation
    uses
      Dashboard    (* fmDashboard *),
      DateUtils    (* MilliSecondsBetween *),
      Graphics     (* clRed, clWindowText *),
      HC3Utilities (* ExpandTabsOfLine, INILoadInteger, INILoadString,
                        INISaveString, LogDirectory, SaveListBox *),
      HC4Main      (* fmHC4Main *),
      LogManager   (* fmLogManager *),
      MAHUSBUtils  (* TeensySerialNumber *),
      RRF_StringAndTimeUtils  (* Plural *),
      SysUtils     (* Format, IntToStr, Now, Sleep *);

    var
      ZylSerialPort: tZylSerialPort;

{ *************** infrastructure **************************** }
    procedure tSerialManager.ApplySettings;
      (* port parameters *)

      const
        HC4BaudRate        = br038400;
        HC4DataBits        = dw8Bits;
        HC4DTR             = false; // tentative
        HC4RTS             = false;
        HC4HWFlowControl   = hfNone;
        HC4Parity          = pbNone;
        HC4StopBits        = sb1Bit;
        HC4SWFlowControl   = sfNone;

      begin (* tSerialManager.ApplySettings *)
        with ZylSerialPort do
          begin (* ZylSerialPort *)
            UnlockKey           := ZylUnlockKey;
            AutoReceive         := true; // tentative
            BaudRate            := HC4BaudRate;
            DataWidth           := HC4DataBits;
            EnableDTROnOpen     := HC4DTR;
            EnableRTSOnOpen     := HC4RTS;
            HwFlowControl       := HC4HWFlowControl;
            IdleInterval        := 0;
            InputBuffer         := 4096;                // Zyl default
            Interval            := msMinInterval;
            NeedSynchronization := true;
            OutputBuffer        := 4096;                // Zyl default
            Parity              := HC4Parity;
            StopBits            := HC4StopBits;
            SwFlowControl       := HC4SWFlowControl;

            with lbTheInputBufferSize do
              Caption := Format('checking %d-byte buffer every %d ms',
                                [InputBuffer, Interval])
          end   (* ZylSerialPort *)
      end   (* tSerialManager.ApplySettings *);

    constructor tSerialManager.Create(CharEater: tCharEater;
                                      lbHowConnected,
                                      lbInputBufferSize,
                                      lbProcessingTime: tLabel);
      (* initialize *)

      begin (* tSerialManager.Create *)
        LastMessageSent      := Now;
        lbTheHowConnected    := lbHowConnected;
        lbTheInputBufferSize := lbInputBufferSize;
        lbTheProcessingTime  := lbProcessingTime;
        PortConnected        := false;
        TheCharEater         := CharEater;
        ZylUnlockKey         := INILoadString('SerialManager', 'UnlockKey', nullstring);
        msMaxInterval        := INILoadInteger(                'msMaxInterval');
        msMinInterval        := INILoadInteger(                'msMinInterval');
        msMinTXGap           := INILoadInteger(                'msMinTXGap'   );
        Verbose              := INILoadBoolean(                'Verbose'      );

        ZylSerialPort        := tZylSerialPort.Create(nil);
        with ZylSerialPort do
          begin (* ZylSerialPort *)
            UnlockKey    := ZylUnlockKey;
            OnDisconnect := ZylSerialPortDisconnect;
            OnFault      := ZylSerialPortFault;
            OnReceive    := ZylSerialPortReceive;
            OnReconnect  := ZylSerialPortReconnect
          end   (* ZylSerialPort *)
      end   (* tSerialManager.Create *);

    destructor tSerialManager.Destroy;
      (* clean up *)

      begin (* tSerialManager.Destroy *)
        ZylSerialPort.Free;
        inherited Destroy
      end   (* tSerialManager.Destroy *);


{ *************** services ********************************** }
    procedure tSerialManager.Announce(const MYID: integer; const S: string);
      (* send S to annunciator with this MYID *)

      var
        TheMessage: string;

      begin (* tSerialManager.Announce *)
        if Global.Status <> gsReceivingDump then
          begin (* sending OK *)
            if TheShowAnnouncements then
              TheMessage := Format('"%s" to annunciator/eFrame at %d',
                                [ExpandTabsOfLine(S), MyID])
            else
              TheMessage := nullstring;

            MessageToBaseStation(wtbAnnunciator,
                                 IntToStr(MYID) + TAB + S,
                                 TheMessage)
          end   (* sending OK *)
      end   (* tSerialManager.Announce *);

    procedure tSerialManager.Disconnect(Reason: string);
      (* close the port *)

      begin (* tSerialManager.Disconnect *)
        MessageToBaseStation(wtbDisconnect, nullstring, 'requested disconnection');
        ZylSerialPort.Close;
        SetGlobalStatus(gsUnconnected, Reason)
      end   (* tSerialManager.Disconnect *);

    procedure tSerialManager.IntMessageToBaseStation(const WTB: tWindowsToBase;
                                                     const Arg: array of integer;
                                                     MessageForChat: string);
      (* send a tab-delimited line of the WTB and the args *)

      var
        I: integer;
        Payload: string;

      begin (* tSerialManager.IntMessageToBaseStation *)
        Payload := nullstring;
        if length(Arg) > 0 then
          for I := 0 to high(Arg) do
            if I = 0 then
              Payload := IntToStr(Arg[0])
            else
              Payload := Payload + TAB + IntToStr(Arg[I]);
        // ShowMessageFmt('ConnectionMgr.IntMTBS(%d, [%s], "%s")',
        //                              [ord(WTB), Payload, MessageForChat]);
        MessageToBaseStation(WTB, Payload, MessageForChat)
      end   (* tSerialManager.IntMessageToBaseStation *);

    procedure tSerialManager.ListPorts(ListBox: tListBox);
      (* Zyl does all the work *)

      var
        I: integer;
        AnnotatedPortItem, RawPortName: string;
        RawList: tStringlist;

      begin (* tSerialManager.ListPorts *)
        RawList := tStringlist.Create;
        try     (* must free RawList *)
          ZylSerialPort.GetExistingCommPortNames(RawList);
          ListBox.Clear;
          for I := 1 to RawList.Count do
            begin (* annotate, if possible *)
              RawPortName := RawList[I-1];
              with TeensySerialNumber(RawPortName) do
                if not AssociatedTeensy then
                  AnnotatedPortItem := RawPortName
                else if RRFCPU = 0 then
                  AnnotatedPortItem := Format('%s (Teensy #%s)', [RawPortName, SN])
                else
                  AnnotatedPortItem := Format('%s (Teensy #%s (RRF #%d))',
                                              [RawPortName, SN, RRFCPU]);
              ListBox.Items.Add(AnnotatedPortItem);
            end   (* annotate, if possible *)
        finally (* must free RawList *)
          RawList.Free
        end     (* must free RawList *);

        with ListBox do
          ItemIndex :=
            Items.IndexOf(INILoadString('SerialManager', 'LastPortUsed', nullstring));
      end   (* tSerialManager.ListPorts *);

    function tSerialManager.MadeConnection(const AnnotatedPortName: string): boolean;
      (* really do it *)

      var
        posLPAR: integer;
        RawPortName: string;

      begin (* tSerialManager.MadeConnection *)
        with ZylSerialPort do
          begin (* ZylSerialPort *)
            AutoReconnect := true;
            posLPAR := pos(LPAR, AnnotatedPortName);
            if posLPAR = 0 then
              RawPortName := AnnotatedPortName
            else
              RawPortName := copy(AnnotatedPortName, 1, posLPAR-2);
            Port := StringToCommPort(RawPortName);
            if Port = spCustom then
              CustomPortName := RawPortName;
            ApplySettings;
            if Open() then
              begin (* success *)
                PortConnected := true;
                MessageToBaseStation(wtbConnect, nullstring,
                                     'requested connection via ' + AnnotatedPortName);
                SetGlobalStatus(gsConnected, 'sm_261 connected to ' + AnnotatedPortName);
                INISaveString('SerialManager', 'LastPortUsed', AnnotatedPortName);
                // MessageToBaseStation(wtbConnect, nullstring,
                //                  'requested connection via ' + AnnotatedPortName);
                fmLogManager.Show;
                LastBufferReceived := Now
              end   (* success *)
          end   (* ZylSerialPort *);
        Result := PortConnected
      end   (* tSerialManager.MadeConnection *);

    procedure tSerialManager.MessageToBaseStation(const WTB: tWindowsToBase;
                                                  Payload, MessageForChat: string);
      (* send a tab-delimited line of the WTB and the args *)

      var
        UnusedDelay: integer;
        TheLine: string;

      begin (* tSerialManager.MessageToBaseStation *)
        if Global.Status <> gsReceivingDump then
          begin (* worth trying *)
            TheLine := IntToStr(ord(WTB));
            if Payload <> nullstring then
              TheLine := TheLine + TAB + Payload;
            with fmDashboard do
              begin (* fmDashboard *)
                if WTB <> wtbTickleBaseStation then
                  if ShowMessagesToBaseStation then
                    ExtendChat(Format('sm_295 about to send "%s"',
                                      [ExpandTabsOfLine(TheLine)+EXCLAIM]));
                UnusedDelay := msMinTXGap - MillisecondsBetween(Now, LastMessageSent);
                if UnusedDelay > 0 then
                  begin (* add delay *)
                    if Verbose then
                      ExtendChat(Format('sm_300 waited %d ms before TX',
                                        [UnusedDelay]));
                    Sleep(UnusedDelay)
                  end   (* add delay *);
                ZylSerialPort.SendString(TheLine + EXCLAIM);
                if MessageForChat <> nullstring then
                  if ShowMessagesToBaseStation then
                    ExtendChat('sm_308 ' + MessageForChat)
              end   (* fmDashboard *);

            LastMessageSent := Now
          end   (* worth trying *)
      end   (* tSerialManager.MessageToBaseStation *);

    procedure tSerialManager.SetGlobalStatus(const NewStatus: tGlobalStatus;
                                             const Reason: string);
      (* all setting comes through here *)

      const
        ConnectedNow = true;

      procedure UseStatus(const NewCaption: string;
                          NewConnected: boolean = true;
                          NewColor: tColor = clWindowText);
        (* effects of NewStatus *)

        begin (* UseStatus *)
          lbTheHowConnected.Caption    := NewCaption;
          lbTheHowConnected.Font.Color := NewColor;
          fmDashboard.GlobalStatusSet(NewConnected);
          fmHC4Main.MenuEnabled(NewStatus <> gsReceivingDump)
        end   (* UseStatus *);

      begin (* tSerialManager.SetGlobalStatus *)
        fmDashBoard.ExtendChat(Reason);
        case NewStatus of
              gsConnected: UseStatus('connected');
          gsReceivingDump: UseStatus('receiving dump');
            gsUnconnected: UseStatus('not connected',
                                     not ConnectedNow,
                                     clRed)
          end (* case *);
        Global.Status := NewStatus;
      end   (* tSerialManager.SetGlobalStatus *);

    procedure tSerialManager.ShowAnnouncements(const ShowThem: boolean);
      (* service for dashboard *)

      begin (* tSerialManager.ShowAnnouncements *)
        TheShowAnnouncements := ShowThem
      end   (* tSerialManager.ShowAnnouncements *);

    procedure tSerialManager.WTBToBaseStation(const WTB: tWindowsToBase;
                                              MessageForChat: string);
      (* no args *)

      begin (* tSerialManager.WTBToBaseStation *)
        MessageToBaseStation(WTB, nullstring, MessageForChat)
      end   (* tSerialManager.WTBToBaseStation *);

{ *************** event catchers **************************** }

    procedure tSerialManager.ZylSerialPortDisconnect(Sender: tObject;  Port: tCommPort);
      (* xxx *)

      begin (* tSerialManager.ZylSerialPortDisconnect *)
        fmDashboard.ExtendChat('sm_351 port disconnected')
      end   (* tSerialManager.ZylSerialPortDisconnect *);

    procedure tSerialManager.ZylSerialPortFault(Sender: tObject; Port: tCommPort);
      (* xxx *)

      begin (* tSerialManager.ZylSerialPortFault *)
        fmDashboard.ExtendChat('sm_358 port fault')
      end   (* tSerialManager.ZylSerialPortFault *);

    procedure tSerialManager.ZylSerialPortReceive(Sender: tObject; Buffer: string);
      (* process the buffer *)

      var
        ThisBufferReceived: tDateTime;
        I: integer;

      begin (* tSerialManager.ZylSerialPortReceive *)
        ThisBufferReceived := Now;
        with lbTheProcessingTime do
          begin (* processing took xx ms *)
            Visible := false;
            for I := 1 to length(Buffer) do
              TheCharEater(Buffer[I]);

            with ZylSerialPort, lbTheInputBufferSize do
              begin (* received xx byte(s), checking every xx ms *)
                Caption := Format('received %s, checking every %d ms',
                                  [Plural(length(Buffer), 'byte'),
                                   Interval]);
                if 2 * cardinal(length(Buffer)) > InputBuffer then
                  begin (* buffer filling *)
                    InputBuffer := 5 * InputBuffer div 4;
                    if Interval > cardinal(2 * msMinInterval) then
                      Interval := Interval div 2
                  end   (* buffer filling *)

                else if MilliSecondsBetween(ThisBufferReceived,
                                            LastBufferReceived) > 10 * msMaxInterval then
                  if Interval < cardinal(msMaxInterval div 2) then
                    Interval := 5 * Interval div 4
              end   (* received xx byte(s), checking every xx ms *);
            Caption := Format('processing took %d ms',
                              [MilliSecondsBetween(Now, ThisBufferReceived)]);
            Visible := true
          end   (* processing took xx ms *);

        LastBufferReceived := ThisBufferReceived
      end   (* tSerialManager.ZylSerialPortReceive *);

    procedure tSerialManager.ZylSerialPortReconnect(Sender: tObject;
                                                      const Port: tCommPort;
                                                      const BaudRate: tBaudRate;
                                                      var Cancel: Boolean);
      (* not clear when this occurs *)
    
      begin (* tSerialManager.ZylSerialPortReconnect *)
        fmDashboard.ExtendChat('sm_412 port reconnected')
      end   (* tSerialManager.ZylSerialPortReconnect *);

end.
