unit eFrames;

  interface
    uses
      AdsData   (* used by AdsFunc *),
      AdsFunc   (* used by AdsTable *),
      AdsTable  (* tAdsQuery *),
      AdvGrid   (* glCustom *),
      AdvObj    (* TMS *),
      AdvUtil   (* TMS *),
      BaseGrid  (* TMS *),
      Classes   (**),
      Contnrs   (* tQueue *),
      Controls  (* tDate *),
      DB        (* tDataSet, tDataSource; also used by AdsTable *),
      DBAdvGrid (* tDBAdvGrid *),
      ExtCtrls  (* tLabeledEdit, tTimer *),
      Forms     (* tForm *),
      Graphics  (* TMS *),
      Grids     (* TMS *),
      HC3DM     (* tAnnunciatedVar, avGCFridge, avHeader, avMMRainToday,
                     avOutsideHumidexF, avOutsideRH, avOutsideTempF, avPM25,
                     avPondTempF, avRainMMPerHour, avShopTemp, avSpacer,
                     avTimeStamp, Global *),
      StdCtrls  (* tButton, tGroupBox, tLabel, tListBox *),
      Windows   (**);

    type
      tfmeFrames = class(tForm)
          qeFrames: tAdsQuery;
            dataeFrames: tDataSource;
              dbadvgrideFrames: tDBAdvGrid;
          btnClear:       tButton;
          btnSaveListBox: tButton;
          btnSendReport:  tButton;
          lbTimerRunning: tLabel;
          grpSpecialFile: tGroupBox;
            btnSetFileName: tButton;
            lbedSpecialFileName: tLabeledEdit;
          lbxLinesSent: tListBox;
          timerTransmission: tTimer;

          procedure qeFramesAfterScroll(DataSet: tDataSet);

          procedure btnClearClick         (Sender: tObject);
          procedure btnSaveListBoxClick   (Sender: tObject);
          procedure btnSendReportClick    (Sender: tObject);
          procedure btnSetFileNameClick   (Sender: tObject);
          procedure FormDblClick          (Sender: tObject);
          procedure timerTransmissionTimer(Sender: tObject);
        private
          TransmissionInProgress: boolean;
          minReportInterval: integer;
          msPerCharacter: integer;
          QueuedData: tQueue;
          procedure SetFlagDay;
          procedure StartTransmission(const eFrameIndex: integer);
          procedure TransmitOneLine;
          procedure TransmitThis(const MyID: integer; const S: string);
        public
          procedure DisconnecteFrames;
          procedure eFrameReal(const Which: tAnnunciatedVar; const NewValue: real);
          procedure InitializeeFrames;
        end (* tfmeFrames *);

    var
      fmeFrames: tfmeFrames;

  implementation
    uses
      BoardManager   (* fmBoardManager *),
      Dashboard      (* fmDashboard *),
      DateUtils      (* DayOf, DayOfTheWeek, MinutesBetween, MonthOf, Today *),
      DBAdvGridUtils (* SetUpDBAdvGridReadOnly *),
      Dialogs        (* tSaveDialog, ShowMessage, ShowMessageFmt *),
      HC3Utilities   (* INILoadInteger *),
      HC4Main        (* fmHC4Main *),
      MyConsts       (* nullstring, PERIOD, UsualFormColor *),
      SysUtils       (* Format, FormatDateTime, IntToStr, Now *);

      {$R *.dfm}

    const
      MaxeFrames = 3;
      MaxWeatherUnits = 15;

    type        //    0         1        2          3         4        5
      tTFTColor = (tftBlack, tftRed, tftOrange, tftYellow, tftGreen, tftCyan,
                //      6         7          8           9
                     tftBlue, tftMagenta, tftPurple, tftWhite);

// possible fonts are Arial_8, Arial_9, Arial_10, Arial_11, Arial_12, Arial_13,
//    Arial_14, Arial_16, Arial_18, Arial_20, Arial_24, Arial_28, Arial_32,
//    Arial_40, Arial_48, Arial_60, Arial_72, Arial_96
//                   0          1         2         3        4         5
      tTFTFont = (Arial_10, Arial_14, Arial_18, Arial_24, Arial_32, Arial_20);

    var
      FlagDay: boolean;
      FlagDayDate: tDate;

    procedure ListTheLine(const S: string);
      (* utility *)

      begin (* ListTheLine *)
        with fmeFrames.lbxLinesSent do
          ItemIndex := Items.Add(S)
      end   (* ListTheLine *);

    procedure tfmeFrames.FormDblClick(Sender: tObject);
      (* go home *)
    
      begin (* tfmeFrames.FormDblClick *)
        fmHC4Main.Show
      end   (* tfmeFrames.FormDblClick *);

    procedure tfmeFrames.SetFlagDay;
      (* when should flag be shown? *)
    
      var
        ThisDay, WeekdayM1: integer;

      begin (* tfmeFrames.SetFlagDay *)
        ThisDay   := DayOf(Today);
        WeekdayM1 := DayOfTheWeek(Today);

        case MonthOf(Today) of
            5: FlagDay := ((WeekdayM1 = 1) and   // Victoria Day
                           (ThisDay >= 18) and   // ..
                           (ThisDay <= 24));     // ..
            7: FlagDay :=  (ThisDay = 1);        // Canada Day
            8: FlagDay := ((WeekdayM1 = 1) and   // BC Day
                           (ThisDay <= 7));      // ..
           11: FlagDay :=  (ThisDay = 11);       // Remembrance Day
          else FlagDay := false                  
          end (* case *)
      end   (* tfmeFrames.SetFlagDay *);
 
{ tWeatherUnit *************************************************************** }
    type
      tWeatherUnit = class(tObject)
        TheFormat: string;
        TheColor: tTFTColor;
        TheFont: tTFTFont;
        constructor Create(const AFormat: string;
                           Font: tTFTFont = Arial_24;
                           Color: tTFTColor = tftBlack);
        end (* tWeatherUnit *);


    constructor tWeatherUnit.Create(const AFormat: string;
                                    Font: tTFTFont = Arial_24;
                                    Color: tTFTColor = tftBlack);
      (* constructor *)

      begin (* tWeatherUnit.Create *)
        inherited Create;
        TheFormat := AFormat;
        TheFont   := Font;
        TheColor  := Color
      end   (* tWeatherUnit.Create *);

    var
      WeatherUnit: array[tAnnunciatedVar] of tWeatherUnit;

{ tWeatherDatum ************************************************************** }
    type
      tWeatherDatum = class(tObject)
      TheWhich: tAnnunciatedVar;
      TheMyID: integer;
      TheValue: real;
      constructor Create(const MyID: integer; const Which: tAnnunciatedVar);
      end (* tWeatherDatum *);

    constructor tWeatherDatum.Create(const MyID: integer; const Which: tAnnunciatedVar);
      (* constructor *)

      begin (* tWeatherDatum.Create *)
        inherited Create;
        TheWhich  := Which;
        TheMyID   := MyID
      end   (* tWeatherDatum.Create *);

{ eFrame ********************************************************************* }
    type
      teFrame = class(tObject)
        private
          HaveWeatherValues: tAvSet;
          WeatherValuesToReport: tAvSet;
          FlagDayStatusKnown, Full, ReportWaiting: boolean;
          KnownDate, LastQueued: tDateTime;
          MyIndex, NWeatherUnits: integer;
          TheMyID: integer;
          TheName: string;

          WeatherData: array[1 .. MaxWeatherUnits] of tWeatherDatum;
          procedure QueueReport;
        public
          procedure AcceptWeatherValue(const Which: tAnnunciatedVar;
                                       const NewValue: real);
          procedure AddWeatherUnit(const Which: tAnnunciatedVar);
          constructor Create(const Index: integer;
                             const BoardTag: char;
                             const BoardName: string);
        end { teFrame };

    procedure teFrame.AcceptWeatherValue(const Which: tAnnunciatedVar;
                                         const NewValue: real);

      (* store it, report if set complete *)

      const
        InterReportMinutes = 10;

      var
        I, MinutesSinceQ: integer;

      begin (* teFrame.AcceptWeatherValue *)
        if Which in WeatherValuesToReport then
          begin (* save it *)

            for I := 1 to NWeatherUnits do
              with WeatherData[I] do
                if TheWhich = Which then
                  begin (* save it here *)
                    if not (Which in HaveWeatherValues) then
                      ListTheLine(Format('AWD_213: Board at MyID = %d accepting datum %d/%d',
                                         [TheMyID, I, NWeatherUnits]));
                    TheValue := NewValue;
                    break
                  end   (* save it here *);

            HaveWeatherValues := HaveWeatherValues + [Which];
            if Global.Status = gsConnected then
              if HaveWeatherValues = WeatherValuesToReport then
                begin (* this frame has all needed data *)
                  if not Full then
                    begin (* newly full *)
                      ListTheLine(Format('AWD_235: Board at MyID = %d is full',
                                         [TheMyID]));
                      Full := true;
                      ReportWaiting := true
                    end   (* newly full *);
                  MinutesSinceQ := MinutesBetween(Now, LastQueued);
                  // ListTheLine(Format('AWD_240: Board(%d) MSQ = %d', [TheMyID, MinutesSinceQ]));
                  with fmeFrames do
                    if MinutesSinceQ > minReportInterval then
                        begin (* time to queue another report *)
                          // ListTheLine(Format('AWD 247: about to QueueReport(%d)', [TheMyID]));
                          QueueReport;
                          LastQueued := Now;
                          if not TransmissionInProgress then
                            begin (* debugging *)
                              // ListTheLine(Format('AWD 253: about to call StartTransmission(%d)', [TheMyID]));
                              StartTransmission(MyIndex)
                            end   (* debugging *)
                        end   (* time to queue another report *)

                    else
                      with lbxLinesSent do
                        Items[ItemIndex] := Items[ItemIndex] + PERIOD
                end   (* this frame has all needed data *)
          end   (* save it *)
      end   (* teFrame.AcceptWeatherDatum *);

    var
      eFrame: array[1 .. MaxeFrames] of teFrame;
      CurrentMyID: integer;
      NeFrames: integer;
      CurrentLabel: string;

    procedure teFrame.AddWeatherUnit(const Which: tAnnunciatedVar);
      (* define report lines in order *)

      begin (* teFrame.AddWeatherUnit *)
        inc(NWeatherUnits);
        WeatherData[NWeatherUnits] := tWeatherDatum.Create(TheMyID, Which);
        WeatherValuesToReport := WeatherValuesToReport + [Which]
      end   (* teFrame.AddWeatherUnit *);

    constructor teFrame.Create(const Index: integer;
                               const BoardTag: char;
                               const BoardName: string);
      (* constructor *)

      begin (* teFrame.Create *)
        inherited Create;
        FlagDayStatusKnown    := false;
        Full                  := false;
        HaveWeatherValues     := [avHeader, avSpacer, avTimeStamp, avTrailer];
        KnownDate             := 0;
        LastQueued            := 0;
        MyIndex               := Index;
        NWeatherUnits         := 0;
        ReportWaiting         := false;
        TheMYID               := fmBoardManager.GetMyID(BoardTag);
        TheName               := BoardName;
        WeatherValuesToReport := []
      end   (* teFrame.Create *);

    procedure teFrame.QueueReport;
      (* add to list *)

      var
        I: integer;

      begin (* teFrame.QueueReport *)
        with fmeFrames do
          begin (* fmeFrames *)
            qeFrames.Locate('MyID', TheMyID, []);
            CurrentMYID := TheMyID;
            ListTheLine(nullstring);
            ListTheLine(Format(' QR_293: report being sent to %s (MyID %d (0x%x))',
                               [CurrentLabel, TheMyID, TheMyID]));
            for I := 1 to NWeatherUnits do
              QueuedData.Push(WeatherData[I]);

            if not TransmissionInProgress then
              begin (* debugging *)
                // ListTheLine(Format(' QR_300: about to call StartTransmission(%d)', [MyIndex])
                StartTransmission(MyIndex)
              end   (* debugging *);

          end   (* fmeFrames *)
      end   (* teFrame.QueueReport *);

{ global services************************************************************* }
    procedure tfmeFrames.btnClearClick(Sender: tObject);
      (* cosmesis *)

      begin (* tfmeFrames.btnClearClick *)
        lbxLinesSent.Clear
      end   (* tfmeFrames.btnClearClick *);

    procedure tfmeFrames.btnSaveListBoxClick(Sender: tObject);
      (* mainly for debugging *)
    
      begin (* tfmeFrames.btnSaveListBoxClick *)
        with tSaveDialog.Create(nil) do
          try     (* must free dialog *)
            DefaultExt := 'txt';
            FileName := 'C:\Users\rrf\Desktop\eFrames.txt'; // full path, to set initial directory
            Title := 'save listbox to file';
            if Execute then
              lbxLinesSent.Items.SaveToFile(FileName)
          finally (* must free dialog *)
            Free
          end     (* must free dialog *);
      end   (* tfmeFrames.btnSaveListBoxClick *);

    procedure tfmeFrames.btnSendReportClick(Sender: tObject);
      (* don't wait for any interval *)

      var
        I: integer;

      begin (* tfmeFrames.btnSendReportClick *)
        for I := 1 to NeFrames do
          with eFrame[I] do
            if TheMyID = qeFrames.FieldByName('MyID').AsInteger then
              QueueReport
      end   (* tfmeFrames.btnSendReportClick *);

    procedure tfmeFrames.btnSetFileNameClick(Sender: tObject);
      (* one-shot message *)   // disable if transmission in progress

      begin (* tfmeFrames.btnSetFileNameClick *)
        with fmDashboard.SerialManager do
          Announce(qeFrames.FieldByName('MyID').AsInteger,
                   'F' + lbedSpecialFileName.Text)
      end   (* tfmeFrames.btnSetFileNameClick *);

    procedure tfmeFrames.DisconnecteFrames;
      (* avert timer interrupts *)
    
      begin (* tfmeFrames.DisconnecteFrames *)
        timerTransmission.Enabled := false
      end   (* tfmeFrames.DisconnecteFrames *);

    procedure tfmeFrames.eFrameReal(const Which: tAnnunciatedVar;
                                    const NewValue: real);
      (* advise all eFrames *)

      var
        NewDay: boolean;
        I: integer;

      begin (* tfmeFrames.eFrameReal *)
        NewDay := (FlagDayDate <> Today);
        if NewDay then
          begin (* new day *)
            SetFlagDay;
            ListTheLine('eFR_366: new day noted');
            FlagDayDate := Today
          end   (* new day *);

        with qeFrames do
          begin (* qeFrames *)
            First;
            while not EOF do
              begin (* one record *)
                for I := 1 to NeFrames do
                  with eFrame[I] do
                    begin (* this eFrame *)
                      if TheMyID = FieldByName('MyID').AsInteger then
                        AcceptWeatherValue(Which, NewValue)
                    end   (* this eFrame *);

                Next
              end   (* one record *)
          end   (* qeFrames *);
      end   (* tfmeFrames.eFrameReal *);

    procedure tfmeFrames.InitializeeFrames;
      (* routine initialization *)

      procedure DefineUnits;
        (* debulk InitializeFrames *)
      
        begin (* DefineUnits *)
          WeatherUnit[avGCFridge]        := tWeatherUnit.Create('       gc fridge  %.1f F (%.1f C)'                   );
          WeatherUnit[avHeader]          := tWeatherUnit.Create('A');
          WeatherUnit[avMMRainToday]     := tWeatherUnit.Create('               rain  %.1f mm today'                  );
          WeatherUnit[avOutsideHumidexF] := tWeatherUnit.Create('      Humidex   %.0f F (%.0f C)', Arial_24, tftPurple);
          WeatherUnit[avOutsideRH]       := tWeatherUnit.Create('               RH   %.0f%%'                          );
          WeatherUnit[avOutsideTempF]    := tWeatherUnit.Create('     outside   %.0f F (%.0f C)'                      );
          WeatherUnit[avPondTempF]       := tWeatherUnit.Create('           pond   %.0f F (%.0f C)'                   );
          WeatherUnit[avPM25]            := tWeatherUnit.Create('         PM2.5   %.2f ug/m^3'                        );
          WeatherUnit[avRainMMPerHour]   := tWeatherUnit.Create('          rain   %.1f mm/hour'                       );
          WeatherUnit[avShopTemp]        := tWeatherUnit.Create('     shop temp  %.0f F (%.0f C)'                     );
          WeatherUnit[avSpacer]          := tWeatherUnit.Create(                nullstring       , Arial_10           );
          WeatherUnit[avTimeStamp]       := tWeatherUnit.Create(                nullstring       , Arial_18, tftBlue  );
          WeatherUnit[avTrailer]         := tWeatherUnit.Create('Z');
        end   (* DefineUnits *);

      begin (* tfmeFrames.InitializeeFrames *)
        SetFlagDay;
        lbTimerRunning.Visible    := false;
        minReportInterval         := INILoadInteger('eFrames', 'minReportInterval');
        msPerCharacter            := INILoadInteger('eFrames', 'msPerCharacter');
        QueuedData                := tQueue.Create;
        self.Color                := UsualFormColor;
        timerTransmission.Enabled := false;
        TransmissionInProgress    := false;

        qeFrames.Open;
        SetUpDBAdvGridReadOnly(dbadvgrideFrames);
        DefineUnits;

        eFrame[1] := teFrame.Create(1, 'E', 'office annunciator');
        with eFrame[1] do
          begin (* eFrame[1] *)
            AddWeatherUnit(avHeader);
            AddWeatherUnit(avTimeStamp);
            AddWeatherUnit(avSpacer);
            AddWeatherUnit(avOutsideTempF);
            AddWeatherUnit(avOutsideRH);
            AddWeatherUnit(avOutsideHumidexF);
            AddWeatherUnit(avPM25);
            AddWeatherUnit(avShopTemp);
            AddWeatherUnit(avGCFridge);
            AddWeatherUnit(avMMRainToday);
            AddWeatherUnit(avTrailer)
          end   (* eFrame[1] *);

        eFrame[2] := teFrame.Create(2, 'J', 'kitchen annunciator');
        with eFrame[2] do
          begin (* eFrame[2] *)
            AddWeatherUnit(avHeader);
            AddWeatherUnit(avTimeStamp);
            AddWeatherUnit(avSpacer);
            AddWeatherUnit(avOutsideTempF);
            AddWeatherUnit(avOutsideRH);
            AddWeatherUnit(avOutsideHumidexF);
            AddWeatherUnit(avPM25);
            AddWeatherUnit(avMMRainToday);
            AddWeatherUnit(avPondTempF);
            AddWeatherUnit(avTrailer)
          end   (* eFrame[2] *);

        NeFrames := 2;
      end   (* tfmeFrames.InitializeeFrames *);

    procedure tfmeFrames.qeFramesAfterScroll(DataSet: tDataSet);
      (* update group caption *)

      begin (* tfmeFrames.qeFramesAfterScroll *)
        with DataSet do
          CurrentLabel := FieldByName('Label').AsString;

        grpSpecialFile.Caption := 'special file in ' + CurrentLabel;
        btnSendReport.Caption  := 'send report to ' + CurrentLabel
      end   (* tfmeFrames.qeFramesAfterScroll *);

    procedure tfmeFrames.StartTransmission(const eFrameIndex: integer);
      (* timer does the hard part *)

      begin (* tfmeFrames.StartTransmission *)
        TransmissionInProgress := true;
        // ListTheLine('ST_480: transmitting');
        btnSendReport.Enabled := false;
        with eFrame[eFrameIndex] do
          begin (* eFrame[eFrameIndex] *)
            CurrentMYID := TheMyID;
            if not FlagDayStatusKnown then
              begin (* send new status *)
                if FlagDay then
                   TransmitThis(TheMyID, 'Fflag.bmp')
                else
                   TransmitThis(TheMyID, 'F');
                FlagDayStatusKnown := true
              end   (* send new status *)
              
            else if QueuedData.Count > 0 then
              TransmitOneLine

            else
              begin (* stopped *)
                TransmissionInProgress := false;
                // ListTheLine('ST_498: not transmitting')
              end   (* stopped *)
          end   (* eFrame[eFrameIndex] *)
      end   (* tfmeFrames.StartTransmission *);

    procedure tfmeFrames.TransmitOneLine;
      (* just pop the queue.  Pointers cause warnings
         At entry, TransmissionInProgress is true *)

      var
        JAB_birthday: boolean;
        JAB_age: integer;
        EncodedLine, FormattedLine, TheFormat: string;

                                                                 {$WARNINGS OFF}
      begin (* tfmeFrames.TransmitOneLine *)
        JAB_birthday := ((MonthOf(Now) = 8) and (DayOf(Now) = 10));
        JAB_age      := YearOf(Now) - 1941;  // good only after August 9

        with tWeatherDatum(QueuedData.Pop) do
          begin (* TheDatum *)
            TheFormat := WeatherUnit[TheWhich].TheFormat;
            case TheWhich of
                       avHeader,
                       avSpacer,
                      avTrailer: FormattedLine := TheFormat;

                     avGCFridge,
              avOutsideHumidexF,
                 avOutsideTempF,
                    avPondTempF,
                     avShopTemp: FormattedLine := Format(TheFormat, [TheValue, 5 * (TheValue - 32) / 9]);

                  avMMRainToday: if TheValue = 0 then
                                   FormattedLine := '        no rain today'
                                 else if TheValue < 0.05 then
                                   FormattedLine := '      rain today: trace'
                                 else FormattedLine := Format(TheFormat, [TheValue]);

                    avOutsideRH,
                         avPM25,
                avRainMMPerHour: FormattedLine := Format(TheFormat, [TheValue]);

                    avTimeStamp: if JAB_birthday then
                                   FormattedLine := Format('Judy''s %dth birthday @ %s',
                                                           [JAB_age, FormatDateTime('hhnn', Now)])
                                 else
                                   FormattedLine := FormatDateTime('      yyyy-mm-dd @ hhnn', Now)
              end (* case *);

            if TheWhich in [avHeader, avTrailer] then
              EncodedLine := FormattedLine
            else if JAB_birthday then
              EncodedLine := Format('%d%d%s',
                                    [ord(WeatherUnit[TheWhich].TheFont),
                                     ord(tftRed),
                                     FormattedLine])
            else
              EncodedLine := Format('%d%d%s',
                                    [ord(WeatherUnit[TheWhich].TheFont),
                                     ord(WeatherUnit[TheWhich].TheColor),
                                     FormattedLine]);

            TransmitThis(TheMyID, EncodedLine);
            if TheWhich = avTrailer then
              fmDashboard.BlankLine
          end   (* TheDatum *)
      end   (* tfmeFrames.TransmitOneLine *);
                                                                 {$WARNINGS ON}
    procedure tfmeFrames.timerTransmissionTimer(Sender: tObject);
      (* try to send another line *)

      var
        I: integer;

      begin (* tfmeFrames.timerTransmissionTimer *)
        timerTransmission.Enabled := false;
        lbTimerRunning.Visible := false;
        if QueuedData.Count > 0 then
          TransmitOneLine
        else
          begin (* done for now *)
            TransmissionInProgress := false;
            // ListTheLine('TTT_578: not transmitting');

            btnSendReport.Enabled := true;
            for I := 1 to NeFrames do
              with eFrame[I] do
                if ReportWaiting then
                  begin (* start the next report *)
                    fmDashboard.ExtendChat('ef_608 sending data to ' + TheName);
                    QueueReport;
                    ReportWaiting := false;
                    // ListTheLine(Format('TTT_569: about to call StartTransmission(%d)', [MyIndex])
                    StartTransmission(MyIndex);
                    break
                  end   (* start the next report *)
          end   (* done for now *)
      end   (* tfmeFrames.timerTransmissionTimer *);

    procedure tfmeFrames.TransmitThis(const MyID: integer; const S: string);
      (* final common path *)

      begin (* tfmeFrames.TransmitThis *)
        with fmDashboard.SerialManager do
          Announce(MyID, S);
        ListTheLine(Format('%d: %s', [MyID, S]));
        timerTransmission.Interval := msPerCharacter * length(S);
        timerTransmission.Enabled := true;
        lbTimerRunning.Visible := true
      end   (* tfmeFrames.TransmitThis *);

end.

