unit Dashboard;
  (* home of the character eater, negotiator with the base station
   *)


  interface
    uses
      Classes           (**),
      ComCtrls          (* tDateTimePicker, tPageControl, tProgressBar,
                             tTabSheet *),
      Controls          (**),
      ExtCtrls          (* tLabeledEdit, tShape, tTimer *),
      Forms             (* Application, tForm *),
      HC3DM             (* Global, tGlobalStatus, gsConnected, gsReceivingDump,
                             gsUnconnected, msOneMinute *),
      MyConsts          (* CR, CRLF, EXCLAIM, LF, nullstring, PERIOD, TAB,
                             UsualFormColor *),
      ObservedAndWM     (* tBaseToWindows, btwAlreadyConnected, btwConnected,
                             btwDebugging, btwDisconnectedBS, btwDisconnectedRQ,
                             btwDumpEnded, btwDumping, btwFailedTransmission,
                             btwFileSystemTrouble, btwInterrupted,
                             btwJustConnected, btwNotDeadYet,
                             btwNotDuringDumping, btwPassThrough,
                             btwSetupComplete, btwStartingFileSystem,
                             btwStartingXBee, btwTimedOut, btwUnknown,
                             btwXBeeDifficultCommandMode, btwXBeeInCommandMode,
                             wm2Debugging, wtbReadTime,
                             wtbSendToXBee, wtbSetTime, wtbStartDump *),
      SerialManager     (* tSerialManager *),
      StdCtrls          (* tButton, tCheckBox, tLabel, tListBox, TMemo *),
      Windows           (**);

    type
      tLastListboxLine = (llblBlank, llblCreated, llblData, llblMessage, llblParenData);

    type
      tfmDashboard = class(tForm)
        btnSaveChat:  tButton;
        lbDumpingBlock: tLabel;
        lbLinesFlunked: tLabel;
        lbxDashboard: tListBox;
        pgctlDasahboard: tPageControl;
          tabConnect: tTabSheet;
            btnConnect:    tButton;
            ckbxAutoscroll:                  tCheckBox;
            ckbxDataInListbox:               tCheckBox;
            ckbxShowAnnouncement:            tCheckBox;
            ckbxShowGetRecentSQL:            tCheckBox;
            ckbxShowMessagesFromBaseStation: tCheckBox;
            ckbxShowMessagesToBaseStation:   tCheckBox;
            ckbxVC0706:                      tCheckBox;
            lbHowConnected:    tLabel;
            lbInputBufferSize: tLabel;
            lbProcessingTime: tLabel;
            lbxPorts: tListBox;
            timerTickleBaseStation: tTimer;
          tabBaseStationClock: tTabSheet;
            btnNow:                 tButton;
            btnReadRTC:             tButton;
            btnSetPeripheralClocks: tButton;
            btnSetRTC:              tButton;
            dtpSetDate:  tDateTimePicker;
            dtpSetTime:  tDateTimePicker;
            lbLocalDateTime:    tLabel;
            lbLocalRTCDateTime: tLabel;
            lbPCLocal:          tLabel;
            lbRTCBehind:        tLabel;
            lbUTCRTCDateTime:   tLabel;
            timerClockValid: tTimer;
          tabMiscDebugging: tTabSheet;
            btnSaveTranscript: tButton;
            ckbxDebugBoard: tCheckBox;
            lbedTagOfInterest: tLabeledEdit;
            lbedThisTag:       tLabeledEdit;
            memoTranscribeBoard: tMemo;
            shapeClearTranscript: tShape;
              btnClearTranscript: tButton;
          tabINI: tTabSheet;
            btnINILoad: tButton;
            btnINISave: tButton;
            memoINI: tMemo;
        progressDump: tProgressBar;
        shapeClearAlert: tShape;
          btnClearChat: tButton;

        procedure btnClearChatClick                   (Sender: tObject);
        procedure btnClearTranscriptClick             (Sender: tObject);
        procedure btnConnectClick                     (Sender: tObject);
        procedure btnINILoadClick                     (Sender: tObject);
        procedure btnINISaveClick                     (Sender: tObject);
        procedure btnNowClick                         (Sender: tObject);
        procedure btnReadRTCClick                     (Sender: tObject);
        procedure btnSaveChatClick                    (Sender: tObject);
        procedure btnSaveTranscriptClick              (Sender: tObject);
        procedure btnSetPeripheralClocksClick         (Sender: tObject);
        procedure btnSetRTCClick                      (Sender: tObject);
        procedure ckbxDebugBoardClick                 (Sender: tObject);
        procedure ckbxShowAnnouncementClick           (Sender: tObject);
        procedure ckbxShowMessagesFromBaseStationClick(Sender: tObject);
        procedure ckbxShowMessagesToBaseStationClick  (Sender: tObject);
        procedure FormDblClick                        (Sender: tObject);
        procedure lbxPortsClick                       (Sender: tObject);
        procedure timerClockValidTimer                (Sender: tObject);
        procedure timerTickleBaseStationTimer         (Sender: tObject);
      private
        TranscribeAll: boolean;
        LastChatIndex: integer;
        LinesFlunked: integer;
        procedure ClockReadingValid(const IsValid: boolean);
      public
        ShowMessagesFromBaseStation: boolean;
        ShowMessagesToBaseStation: boolean;
        SerialManager: tSerialManager;

        procedure BlankLine;
        procedure CharEater(const C: char);
        procedure ExtendChat(const Msg: string;
                             LastLine: tLastListboxLine = llblMessage);
        procedure ForceDisconnection;
        procedure GlobalStatusSet(const Connected: boolean);
        procedure Initialize;
        procedure LineFlunked;
        procedure LogBoring(const BoringChar: char);
        procedure LostConnection;
        procedure VC0706News(const S: string);
        procedure SaveLog;
      end (* tfmDashboard *);

    var
      fmDashboard: tfmDashboard;

  implementation
    uses
      BoardManager           (* fmBoardManager *),
      DateUtils              (* DayOf, HourOf, MinuteOf, MonthOf, Now, SecondOf,
                                 SecondsBetween, Today, YearOf *),
      Dialogs                (* ShowMessage *),
      Dispatcher             (* InitializeDispatcher, ProcessDataLine *),
      GraphManager2          (* fmGraphManager *),
      HC4Main                (* fmHC4Main *),
      HC3Utilities           (* ExpandTabsOfLine, INIChanged, INIFileName,
                                  INILoadBoolean, INISaveBoolean, INISaveString,
                                  LogDirectory, SaveListBox, SaveMemo *),
      LogManager             (* fmLogManager *),
      RRF_StringAndTimeUtils (* LocalToUTC, Plural, UTCToLocal *),
      SysUtils               (* Format, FormatDateTime, IntToStr, ReplaceTime,
                                  StrToIntDef *);
      {$R *.dfm}

    const
      ChatLineMax    = 84;

    type
      tbtwNames = array[tBaseToWindows] of string;

    var
      BTWName: tbtwNames;
      LastListboxLine: tLastListboxLine;

(********** infrastructure ****************************************************)
    procedure tfmDashboard.BlankLine;
      (* prettyprinting *)
    
      begin (* tfmDashboard.BlankLine *)
        with lbxDashboard do
          ItemIndex := Items.Add(nullstring)
      end   (* tfmDashboard.BlankLine *);

    procedure tfmDashboard.btnClearChatClick(Sender: tObject);
      (* convenience *)

      begin (* tfmDashboard.btnClearChatClick *)
        lbxDashboard.Clear
      end   (* tfmDashboard.btnClearChatClick *);

    procedure tfmDashboard.btnClearTranscriptClick(Sender: tObject);
      (* convenience *)
    
      begin (* tfmDashboard.btnClearTranscriptClick *)
        memoTranscribeBoard.Clear
      end   (* tfmDashboard.btnClearTranscriptClick *);

    procedure tfmDashboard.btnSaveChatClick(Sender: tObject);
      (* save to a text file *)

      begin (* tfmDashboard.btnSaveChatClick *)
        SaveListBox(lbxDashboard, 'HC3 dashboard.txt', 'save chat to file')
      end   (* tfmDashboard.btnSaveChatClick *);

    procedure tfmDashboard.btnSaveTranscriptClick(Sender: tObject);
      begin (* tfmDashboard.btnSaveTranscriptClick *)
        SaveMemo(memoTranscribeBoard, 'HC3 Transcript.txt', 'save debugging memo')
      end   (* tfmDashboard.btnSaveTranscriptClick *);

    procedure tfmDashboard.ckbxDebugBoardClick(Sender: tObject);
      (* keep INI file updated *)
    
      begin (* tfmDashboard.ckbxDebugBoardClick *)
        INISaveBoolean('DashBoard', 'DebugSelectedBoard', ckbxDebugBoard.Checked)
      end   (* tfmDashboard.ckbxDebugBoardClick *);

    procedure tfmDashboard.ckbxShowMessagesFromBaseStationClick(Sender: tObject);
      (* make state public *)

      begin (* tfmDashboard.ckbxShowMessagesFromBaseStationClick *)
        ShowMessagesFromBaseStation := ckbxShowMessagesFromBaseStation.Checked
      end   (* tfmDashboard.ckbxShowMessagesFromBaseStationClick *);

    procedure tfmDashboard.ckbxShowMessagesToBaseStationClick(Sender: tObject);
      (* make state public *)

      begin (* tfmDashboard.ckbxShowMessagesToBaseStationClick *)
        ShowMessagesToBaseStation := ckbxShowMessagesToBaseStation.Checked
      end   (* tfmDashboard.ckbxShowMessagesToBaseStationClick *);

    procedure tfmDashboard.ExtendChat(const Msg: string;
                                      LastLine: tLastListboxLine = llblMessage);
      (* add to listbox.  Unit tags are
           db_ Dashboard
           di_ Dispatcher
           dm_ HC3DM
           li_ table_Light
           on_ table_OnOff
           pd_ table_Pond
           rn_ table_Rain
           sm_ SerialManager
           tg_ table_generic
           th_ table_Thermostats
           tm_ TableManager
           vc_ VC0706
       *)

      procedure TimestampIndex;
        (* recurs *)

        begin (* TimestampIndex *)
          LastChatIndex := lbxDashboard.Items.Add(FormatDateTime('hh:nn:ss" "', Now));
        end   (* TimestampIndex *);

      begin (* tfmDashboard.ExtendChat *)
        TimeStampIndex;
        with lbxDashboard do
          Items[LastChatIndex] := Items[LastChatIndex] + Msg;
        if LastLine = llblCreated then
          begin (* leave a space *)
            TimeStampIndex;
            LastListBoxLine := llblBlank
          end   (* leave a space *)
        else
          LastListboxLine := LastLine ;

        if ckbxAutoscroll.Checked then
          lbxDashboard.ItemIndex := LastChatIndex
      end   (* tfmDashboard.ExtendChat *);

    procedure tfmDashboard.FormDblClick(Sender: tObject);
      (* show master form *)
    
      begin (* tfmDashboard.FormDblClick *)
        fmHC4Main.Show
      end   (* tfmDashboard.FormDblClick *);

    procedure tfmDashboard.Initialize;
      (* initialize *)

      const
        TickleBaseStationMinutes    =  3;  // base station gives up after 5 minutes
        msTickleBaseStationInterval = TickleBaseStationMinutes * msOneMinute;

      procedure SetBTWNames;
        (* strings used in chat *)

        var
          Runner: tBaseToWindows;

        procedure SetBTWName(const BTW: tBaseToWindows; const S: string);
          (* one setting *)

          begin (* SetBTWName *)
            BTWName[BTW] := S
          end   (* SetBTWName *);

        begin (* SetBTWNames *)
          for Runner := low(tBaseToWindows) to high(tBaseToWindows) do
            SetBTWName(Runner, Format('(btw #%d not named)', [ord(Runner)]));

          SetBTWName(btwAlreadyConnected,         'already connected');
          SetBTWName(btwConnected,                'connected');
          SetBTWName(btwDebugging,                'debugging');
          SetBTWName(btwDisconnectedBS,           'disconnected by base station');
          SetBTWName(btwDisconnectedRQ,           'disconnected per request');
          SetBTWName(btwDumpEnded,                'dump ended');
          SetBTWName(btwDumping,                  'dumping');
          SetBTWName(btwFailedTransmission,       'transmission failed.');
          SetBTWName(btwFileSystemTrouble,        'trouble in file system');
          SetBTWName(btwInterrupted,              'interrupted');
          SetBTWName(btwJustConnected,            'just connected');
          SetBTWName(btwNotDeadYet,               '(base responded)');
          SetBTWName(btwNotDuringDumping,         'this request not accepted during dumping.');
          SetBTWName(btwPassThrough,              'pass-through');
          SetBTWName(btwSetupComplete,            'finished setup');
          SetBTWName(btwStartingFileSystem,       'starting file system');
          SetBTWName(btwStartingXBee,             'starting XBee');
          SetBTWName(btwTimedOut,                 'timed out');
          SetBTWName(btwUnknown,                  'mystery message');
          SetBTWName(btwXBeeDifficultCommandMode, 'difficulty entering XBee command mode');
          SetBTWName(btwXBeeInCommandMode,        'in XBee command mode');
        end   (* SetBTWNames *);

      begin (* tfmDashboard.Initialize *)
        { global }
            lbDumpingBlock.Visible      := false;
            lbLinesFlunked.Visible      := false;
            lbProcessingTime.Visible    := false;
            LinesFlunked                := 0;
            progressDump.Visible        := false;
            with Global.LatestData do
              ReceivedLine              := nullstring;
            self.Color                  := UsualFormColor;
            ShowMessagesFromBaseStation := true;
            ShowMessagesToBaseStation   := false;
            memoTranscribeBoard.Clear;

        { auxiliaries }
            InitializeDispatcher;

        { prepare to make connection }
            ckbxShowAnnouncement.Checked := INILoadBoolean('DashBoard', 'ShowAnnouncements' );
            ckbxDebugBoard.Checked       := INILoadBoolean(             'DebugSelectedBoard');
            TranscribeAll                := INILoadBoolean(             'TranscribeAll'     );

            SerialManager :=
              tSerialManager.Create(CharEater, lbHowConnected, lbInputBufferSize, lbProcessingTime);
            SerialManager.ListPorts(lbxPorts);
            with lbxPorts do
              if Items.Count = 1 then
                ItemIndex := 0;
            SerialManager.SetGlobalStatus(gsUnconnected, 'db_330 app never connected');
            SetBTWNames;
            timerTickleBaseStation.Interval := msTickleBaseStationInterval;
//            ConnectionMgr                   := tConnectionMgr.Create;
//            ConnectionMgr.Initialize(lbHowConnected, lbDisconnectedBecause,
//                                     nil,
//                                     ckbxShowAnnouncement.Checked);
            timerTickleBaseStation.Enabled := true;
            fmLogManager.Initialize;

        { base station clock }
            ClockReadingValid(false);

        pgctlDasahboard.ActivePage := tabConnect
      end   (* tfmDashboard.Initialize *);

    procedure tfmDashboard.LineFlunked;
      (* unparseable line *)
    
      begin (* tfmDashboard.LineFlunked *)
        ExtendChat(Format('db_350 line "%s" flunked by HC3DM.ObjectsOrganized',
                                          [Global.LatestData.ExpandedLine]));
        inc(LinesFlunked);
        lbLinesFlunked.Caption := Plural(LinesFlunked, 'line') + ' flunked';
        lbLinesFlunked.Visible := true
      end   (* tfmDashboard.LineFlunked *);

    procedure tfmDashboard.SaveLog;
      (* service for shutdown *)
    
      begin (* tfmDashboard.SaveLog *)
        btnSaveChatClick(nil)
      end   (* tfmDashboard.SaveLog *);

(********** end infrastructure ************************************************)

(********** base-station clock ************************************************)
    procedure tfmDashboard.btnNowClick(Sender: tObject);
      (* set the datetime pickers *)

      begin (* tfmDashboard.btnNowClick *)
        dtpSetDate.Date := Today;
        dtpSetTime.Time := Now
      end   (* tfmDashboard.btnNowClick *);

    procedure tfmDashboard.btnReadRTCClick(Sender: tObject);
      (* read the base-station clock *)
    
      begin (* tfmDashboard.btnReadRTCClick *)
        SerialManager.WTBToBaseStation(wtbReadTime, 'reading base-station clock')
      end   (* tfmDashboard.btnReadRTCClick *);

    procedure tfmDashboard.btnSetPeripheralClocksClick(Sender: tObject);
      (* board manager reads local clock *)
    
      begin (* tfmDashboard.btnSetPeripheralClocksClick *)
        fmBoardManager.BroadcastLocalTime
      end   (* tfmDashboard.btnSetPeripheralClocksClick *);

    procedure tfmDashboard.btnSetRTCClick(Sender: tObject);
      (* set the RTC *)

      var
        LocalDateTime, UTCDateTime: tDateTime;

      begin (* tfmDashboard.btnSetRTCClick *)
        LocalDateTime := dtpSetDate.Date;
        ReplaceTime(LocalDateTime, dtpSetTime.Time);
        UTCDateTime := LocalToUTC(LocalDateTime);
        with SerialManager do
          IntMessageToBaseStation(wtbSetTime,
                                  [YearOf  (UTCDateTime), MonthOf  (UTCDateTime),
                                   DayOf   (UTCDateTime), HourOf   (UTCDateTime),
                                   MinuteOf(UTCDateTime), SecondOf (UTCDateTime)],
                                  FormatDateTime('"setting base-station clock to "yyyy-mm-dd hh:nn:ss',
                                                 UTCDateTime));
        Global.RTCSecondsBehind := 0
      end   (* tfmDashboard.btnSetRTCClick *);

    procedure tfmDashboard.ClockReadingValid(const IsValid: boolean);
      (* don't mislead user *)

      begin (* tfmDashboard.ClockReadingValid *)
        lbUTCRTCDateTime.Enabled   := IsValid;
        lbLocalRTCDateTime.Enabled := IsValid;
        lbRTCBehind.Enabled        := IsValid;
        lbPCLocal.Enabled          := IsValid;
        timerClockValid.Enabled    := IsValid
      end   (* tfmDashboard.ClockReadingValid *);

    procedure tfmDashboard.timerClockValidTimer(Sender: tObject);
      (* clock reading is obsolete *)

      const
        msOneMinute = 60 * 1000;

      begin (* tfmDashboard.timerClockValidTimer *)
        timerClockValid.Interval := msOneMinute;
        ClockReadingValid(false)
      end   (* tfmDashboard.timerClockValidTimer *);

(********** end base-station clock ********************************************)

(********** connection-related ************************************************)
    procedure tfmDashboard.btnConnectClick(Sender: TObject);
      (* try to connect *)

      const
        ConnectMargin     =   12;
        ConnectDashHeight =  549;
        ConnectLeft       =  160;
        ConnectWidth      = 1100;
        ConnectLogHeight  =  468;

      begin (* tfmDashboard.btnConnectClick *)
        with lbxPorts do
          if SerialManager.MadeConnection(Items[ItemIndex]) then
            begin (* success *)
              ExtendChat('db_448 connected');
              btnConnect.Enabled := false
            end   (* success *)
          else
            ExtendChat('db_452 failed to connect');

        self.Left           := ConnectLeft;
        self.Top            := ConnectMargin;
        self.Height         := ConnectDashHeight;
        self.Width          := ConnectWidth;

        fmLogManager.Left   := ConnectLeft;
        fmLogManager.Top    := self.Top + self.Height + ConnectMargin;
        fmLogManager.Height := ConnectLogHeight;
        fmLogManager.Width  := ConnectWidth
      end   (* tfmDashboard.btnConnectClick *);

    procedure tfmDashboard.lbxPortsClick(Sender: tObject);
      (* once selection made, connect is possible *)

      begin (* tfmDashboard.lbxPortsClick *)
        btnConnect.Enabled := not SerialManager.PortConnected
      end   (* tfmDashboard.lbxPortsClick *);

    procedure tfmDashboard.GlobalStatusSet(const Connected: boolean);
      (* callback from SerialManager *)

      begin (* tfmDashboard.GlobalStatusSet *)
        btnConnect.Enabled             := (not Connected) and (lbxPorts.ItemIndex >= 0);
        tabBaseStationClock.TabVisible := Connected;
        tabMiscDebugging.TabVisible    := Connected
      end   (* tfmDashboard.GlobalStatusSet *);

    procedure tfmDashboard.ckbxShowAnnouncementClick(Sender: tObject);
      (* control verbosity *)
    
      begin (* tfmDashboard.ckbxShowAnnouncementClick *)
        SerialManager.ShowAnnouncements(ckbxShowAnnouncement.Checked)
      end   (* tfmDashboard.ckbxShowAnnouncementClick *);

    procedure tfmDashboard.ForceDisconnection;
      (* called from HC4Main when app going down *)
    
      begin (* tfmDashboard.ForceDisconnection *)
        SerialManager.Disconnect
      end   (* tfmDashboard.ForceDisconnection *);

    procedure tfmDashboard.timerTickleBaseStationTimer(Sender: tObject);
      (* remind base station that we are here *)

      begin (* tfmDashboard.timerTickleBaseStationTimer *)
        with SerialManager do
          case Global.Status of
                gsConnected,
            gsReceivingDump: WTBToBaseStation(wtbTickleBaseStation, 'tickled base station');
              gsUnconnected: ExtendChat('db_503 tickle time but not connected')
            end (* case *)
      end   (* tfmDashboard.timerTickleBaseStationTimer *);

(********** end onnection-related *********************************************)

(********** input processing **************************************************)
    procedure tfmDashboard.CharEater(const C: char);
      (* First stop for all characters from base station.  Here we parse them
      by tabs and pass the data to the dispatcher *)

      procedure ProcessLine;
        (* parse the line and dispatch it *)

        const
          BTWData          = [btwDumpData, btwLiveData];
          BTWDumpRelated   = [btwDumpEnded, btwDumping, btwDumpingBlock];
          BTWGlobal        = [btwAlreadyConnected, btwConnected,
                                btwDisconnectedBS, btwDisconnectedRQ, btwDumping,
                                btwFileSystemTrouble, btwInterrupted,
                                btwJustConnected, btwLoopTiming, btwPassThrough,
                                btwSetupComplete, btwStartingFileSystem,
                                btwTimedOut];
          BTWResponses     = [btwFailedTransmission, btwNotDeadYet,
                                btwNotDuringDumping, btwReadTime];
          BTWXBeeRelated   = [btwStartingXBee, btwXBeeBufferBusy,
                                btwXBeeCommandIssued,
                                btwXBeeDifficultCommandMode,
                                btwXBeeInCommandMode,
                                btwXBeeNoCommandMode,
                                btwXBeeNoSetDestination,
                                btwXBeeSettingDestination,
                                btwXBeeStuckInCommandMode];

        var
          MessageID: tBaseToWindows;
          RawMessageID: integer;

        function BTWCodeOK: boolean;
          (* shouldn't fail *)

          begin (* BTWCodeOK *)
            Result := true;
            with Global.LatestData do
              begin (* Parser *)
                RawMessageID := StrToIntDef(Strings[0], -99);
                if (RawMessageID < ord(Low(tBaseToWindows))) or
                   (RawMessageID > ord(High(tBaseToWindows))) then
                  begin (* out of range *)
                    ExtendChat(Format('db_555 ReceivedLine: "%s"' , [ExpandedLine]));
                    ExtendChat(Format('db_556   Strings[0]: "%s"' , [Strings[0]]));
                    ExtendChat(Format('db_557   RawMessageID = %d', [RawMessageID]));
                    Result := false
                  end   (* out of range *)
              end   (* Parser *)
          end   (* BTWCodeOK *);

        procedure DataAction;
          (*   ReceivedLine is live data or dumped data, so the work is done
            by the Dispatcher, but here we need to decide how or if to display
            the line *)
          // or   0         1             2         3       4,7,10     5,8,11
          //    <BTW> <UTC timestamp> <BoardTag> <SlotUID> <triple>* <checksum>

          var
            OldItemIndex: integer;

          begin (* DataAction *)
            with lbxDashboard, Global.LatestData do
              begin (* lbxDashboard *)
                OldItemIndex := ItemIndex;
                try     (* must re-find place *)
                  ItemIndex := LastChatIndex;
                  if ckbxDataInListbox.Checked then
                    begin (* set line off *)
                      ExtendChat('db_581', llblData);
                      ExtendChat('db_582   ' + ExpandedLine, llblData)
                    end   (* set line off *)
                  else
                    case LastListboxLine of
                          llblBlank: ExtendChat('db_586 .', llblParenData);
                           llblData,
                        llblMessage: ExtendChat('db_588 (data) .', llblParenData);
                      llblParenData: if Items.Count = 0 then
                                       ExtendChat('db_590 .', llblParenData)
                                     else if length(Items[ItemIndex]) < ChatLineMax then
                                       Items[ItemIndex] := Items[ItemIndex] + PERIOD
                                     else
                                       ExtendChat('db_594 .', llblParenData)
                      end (* case *);

                  ProcessDataLine
                finally (* must re-find place *)
                  if not ckbxAutoscroll.Checked then
                    ItemIndex := OldItemIndex
                end     (* must re-find place *)
              end   (* lbxDashboard *)
          end   (* DataAction *);

        procedure DumpRelatedAction;
          (* debulk ProcessLine *)

          var
            BlockNumber, NBlocks: integer;

          begin (* DumpRelatedAction *)
            with Global.LatestData, SerialManager do
              case MessageID of
                     btwDumping: SetGlobalStatus(gsReceivingDump, 'db_614 starting dump');

                btwDumpingBlock: begin (* dumping block *)
                                   // line is
                                   //    0       1      2     3       4      5       6
                                   //  (BTW)  FS:FDB  <block> of <blocks>  from  <filename>
                                   BlockNumber := StrToIntDef(Strings[2], 0);
                                   NBlocks := StrToIntDef(Strings[4], 0);
                                   lbDumpingBlock.Caption :=
                                     Format('dumping block %d/%d from %s',
                                            [BlockNumber, NBlocks, Strings[6]]);
                                   lbDumpingBlock.Visible := true;
                                   progressDump.Max := NBlocks;
                                   progressDump.Position := BlockNumber
                                 end   (* dumping block *);

                   btwDumpEnded: begin (* dump over *)
                                   btnNow.Click;
                                   btnSetRTC.Click;
                                   SetGlobalStatus(gsConnected, 'db_634 base station says dump is over');
                                   fmLogManager.EndDump;
                                   btnSetRTC.Enabled := true;
                                   fmGraphManager.ReplotOpenGraphs;
                                   lbDumpingBlock.Visible := false
                                 end   (* dump over *)
                end (* case *);
            progressDump.Visible := lbDumpingBlock.Visible
          end   (* DumpRelatedAction *);

        procedure GlobalAction;
          (* debulking *)

          begin (* GlobalAction *)
            with SerialManager do
              case MessageID of
                      btwConnected: begin (* connected *)
                                      case Global.Status of
                                            gsConnected: ExtendChat('db_651 connected (base station restates so)'               );
                                        gsReceivingDump: ExtendChat('db_652 dumping (but base station now says it''s connected)');
                                          gsUnconnected: SetGlobalStatus(gsConnected, 'db_592 connected (base station says so)' )
                                        end (* case on Status *)
                                    end   (* connected *);

                 btwDisconnectedBS: SetGlobalStatus(gsUnconnected, 'db_657 disconnected (by base station)');
                 btwDisconnectedRQ: SetGlobalStatus(gsUnconnected, 'db_658 disconnected (as requested)');
                  btwJustConnected: SetGlobalStatus(gsConnected,   'db_659 just connected')
                end (* case *)
          end   (* GlobalAction *);

        procedure ResponseAction;
          (* messages that arrive after being requested *)

          const
                   //            1111111111
                   //   1234567890123456789
            DTFormat = 'yyyy-mm-dd hh:nn:ss';

          var
            strReportedTime: string;

          procedure ReceiveClockReading;
            (*   Note extent to which base-station's RTC is behind
              (see Global.RTCSecondsBehind), but don't correct it.  It will be
              corrected automatically when a dump is over.  Alternatively, it
              can be manually corrected using the button.
             *)

            type
              tPerRTC = record
                LocalDateTime, UTCDateTime: tDateTime;
                strLocalDT, strUTCDT: string;
                end (* tPerRTC *);

            var
              PerRTC: tPerRTC;

            procedure CheckDiscrepancy;
              (* debulk ReceiveClockReading *)

              var
                AbsBehind: Int64;
                AheadBehind: string;

              begin (* CheckDiscrepancy *)
                AheadBehind := 'behind';
                if Global.RTCSecondsBehind < 0 then
                  AheadBehind := 'ahead of';
                AbsBehind := abs(Global.RTCSecondsBehind);

                lbPCLocal.Caption := Format('PC clock: %s',
                                            [FormatDateTime(DTFormat, Now)]);
                with lbRTCBehind do
                  if AbsBehind = 0 then
                    Caption := 'RTC is synchronized with PC clock'
                  else if AbsBehind < 60 then
                    Caption := Format('RTC is %s %s PC clock',
                                      [Plural(AbsBehind, 'second'),
                                       AheadBehind])
                  else if (AbsBehind div 60) < 60 then
                    Caption := Format('RTC is about %s %s PC clock',
                                      [Plural(AbsBehind div 60, 'minute'),
                                       AheadBehind])
                  else if (AbsBehind div 3600) < 24 then
                    Caption := Format('RTC is about %s %s PC clock',
                                      [Plural(AbsBehind div 3600, 'hour'),
                                       AheadBehind])
                  else
                    Caption := Format('RTC is about %s %s PC clock',
                                      [Plural(AbsBehind div (24 * 3600), 'day'),
                                       AheadBehind])
              end   (* CheckDiscrepancy *);

            begin (* ReceiveClockReading *)
              with PerRTC do
                begin (* PerRTC *)
                  UTCDateTime := MyStrToDateTime(strReportedTime);
                  LocalDateTime := UTCToLocal(UTCDateTime);
                  strUTCDT := FormatDateTime(DTFormat, UTCDateTime);
                  lbUTCRTCDateTime.Caption := Format('UTC per RTC: %s', [strUTCDT]);
                  strLocalDT := FormatDateTime(DTFormat, LocalDateTime);
                  lbLocalRTCDateTime.Caption := Format('(local per RTC: %s)',
                                                       [strLocalDT]);

                  Global.RTCSecondsBehind := SecondsBetween(Now, LocalDateTime);
                end   (* PerRTC *);
              CheckDiscrepancy;
              ClockReadingValid(true)
           end   (* ReceiveClockReading *);

          begin (* ResponseAction *)
            with Global.LatestData do
              case MessageID of
                      btwNotDeadYet: Global.LocalTimeLastCharReceived := Now;

                        btwReadTime: begin (* read BS time *)
                                       // line is
                                       //     0        1        2
                                       // btwReadTime RTC <reported time>
                                       strReportedTime := Strings[2];
                                       if length(strReportedTime) = length(DTFormat) then
                                         ReceiveClockReading
                                       else
                                         ExtendChat('db_756 *** bad format')
                                     end   (* read BS time *)
               end (* case *)
          end   (* ResponseAction *);

        procedure XBeeAction;
          (* none of the XBee-related codes requires any action *)

          begin (* XBeeAction *)
          end   (* XBeeAction *);

        begin (* ProcessLine *)
          // line should be
          //    <BTW> message
          // or   0         1             2         3       4,7,10     5,8,11
          //    <BTW> <UTC timestamp> <BoardTag> <SlotUID> <triple>* <checksum>

          with lbxDashboard, Global.LatestData do
            begin (* lbxDashboard, Global.LatestData *)
              Parse(ReceivedLine);
              if Count > 0 then
                begin (* nondegenerate *)
                  RawMessageID := StrToIntDef(Strings[0], -99);
                  if BTWCodeOK then
                    begin (* worth processing *)
                      MessageID := tBaseToWindows(RawMessageID);
                      if MessageID in BTWData then
                        DataAction
                      else if MessageID in BTWDumpRelated then
                        DumpRelatedAction
                      else
                        begin (* some sort of message *)
                          if ShowMessagesFromBaseStation then
                            if Count > 1 then
                              ExtendChat('db_790 ' + ExpandedLine)
                            else if MessageID = btwDebugging then
                              ExtendChat('db_792 debug: ' +
                                       ExpandTabsOfLine(ReceivedLine))
                            else
                              ExtendChat(Format('db_795 %d (%s)',
                                         [ord(MessageID), BTWName[MessageID]]));

                          if      MessageID in BTWGlobal      then GlobalAction
                          else if MessageID in BTWResponses   then ResponseAction
                          else if MessageID in BTWXBeeRelated then XBeeAction
                        end   (* some sort of message *)
                    end   (* worth processing *)
                end   (* nondegenerate *);
              ReceivedLine := nullstring
            end   (* lbxDashboard, Global.LatestData *)
        end   (* ProcessLine *);

      begin (* tfmDashboard.CharEater *)
        with Global, LatestData do
          begin (* Global, LatestData *)
            LocalTimeLastCharReceived := Now;

            if C in [CR, EXCLAIM, LF] then
              begin (* process break char *)
                if ckbxDebugBoard.Checked then
                  if lbedThisTag.Text = lbedTagOfInterest.Text then
                    memoTranscribeBoard.Lines.Add(ExpandTabsOfLine(ReceivedLine));
                if ReceivedLine <> nullstring then
                  ProcessLine
              end   (* process break char *)

            else
              begin (* process non-break *)
                ReceivedLine := ReceivedLine + C;
                // line should be
                //    <BTW> message
                //                  111111111122 2 2
                // or   1  2 3456789012345678901 2 3
                //    <BTW>  2024-12-04 18:37:35   T <-- tag
                if length(ReceivedLine) = 23 then
                  lbedThisTag.Text := C
              end   (* process non-break *)
          end   (* Global, LatestData *)
      end   (* tfmDashboard.CharEater *);

    procedure tfmDashboard.LogBoring(const BoringChar: char);
      (* note that uninteresting input is being ignored *)

      var
        OldItemIndex: integer;

      begin (* tfmDashboard.LogBoring *)
        with lbxDashboard do
          begin (* lbxDashboard *)
            OldItemIndex := ItemIndex;
            try     (* must re-find place *)
              ItemIndex := LastChatIndex;
              case LastListboxLine of
                    llblBlank: ExtendChat('db_849 ' + BoringChar, llblParenData);
                     llblData,
                  llblMessage: ExtendChat('db_851 (data) ' + BoringChar, llblParenData);
                llblParenData: if length(Items[ItemIndex]) < ChatLineMax then
                                 Items[ItemIndex] := Items[ItemIndex] + BoringChar
                               else
                                 ExtendChat('db_855 ' + BoringChar, llblParenData)
                end (* case *)
            finally (* must re-find place *)
              ItemIndex := OldItemIndex
            end     (* must re-find place *)
          end   (* lbxDashboard *)
      end   (* tfmDashboard.LogBoring *);

    procedure tfmDashboard.LostConnection;
      (* called from HC4Main on timeout of input *)

      begin (* tfmDashboard.LostConnection *)
        SerialManager.SetGlobalStatus(gsUnconnected, 'disconnected after timeout');
        Show
      end   (* tfmDashboard.LostConnection *);

    procedure tfmDashboard.VC0706News(const S: string);
      (* conditional display *)
    
      begin (* tfmDashboard.VC0706News *)
        if ckbxVC0706.Checked then
          ExtendChat('db_876 ' + S)
      end   (* tfmDashboard.VC0706News *);

(********** end input processing **********************************************)

(********** INI maintenance ***************************************************)
    procedure tfmDashboard.btnINILoadClick(Sender: tObject);
      (* get fresh copy.  Unfortunately, can't edit listbox. *)

      begin (* tfmDashboard.btnINILoadClick *)
        with memoINI.Lines do
          LoadFromFile(INIFileName);
        INIChanged := false;
        btnINISave.Enabled := true
      end   (* tfmDashboard.btnINILoadClick *);

    procedure tfmDashboard.btnINISaveClick(Sender: tObject);
      (* memo seems to work *)
    
      begin (* tfmDashboard.btnINISaveClick *)
        if INIChanged then
          begin (* don't overwrite *)
            ShowMessage('INI file changed: Copy your changes, reload, and paste');
            btnINISave.Enabled := false
          end   (* don't overwrite *)
        else
          with memoINI.Lines do
            SaveToFile(INIFileName)
      end   (* tfmDashboard.btnINISaveClick *);

(********** end INI maintenance ***********************************************)
(********** unallocated *******************************************************)
end.
