unit OutdoorInstruments;

  (* This code does not contemplate outdoor use of DHT20s, since they would
  need to be too far away from their I²C masters *)

  interface
    uses
      AdsData       (* used by AdsTable *),
      AdsFunc       (* used by AdsTable *),
      AdsTable      (* tAdsQuery *),
      Classes       (**),
      Controls      (**),
      DB            (* used by AdsTable *),
      Forms         (* tForm *),
      HC3DM         (* avOutsideHumidexF, avOutsideTempF, avOutsideRH,
                         avPondTempF, avRainMMPerHourGlobal, SillyRealValue,
                         SPS30FailureReasons *),
      ObservedAndWM (* tFailureReason, frDepthImplausible, frDHT22ReadFailure,
                         frImplausibleFrequency, frImplausiblePressure,
                         frImplausibleRH, frImplausibleTemp, frResistanceOpen,
                         frResistanceShorted, frSCD30DataNotReady,
                         frSCD30SetAmbient, GetObservedName, obCarport, obDeck,
                         obPond, tTableUsed, tuLight, tuPond, tuSPS30, tuTRHP,
                         tuWind, tWhatMeasured2, wm2DepthInches, wm2FrequencyMax,
                         wm2FrequencyRaw, wm2MassPM25, wm2Resistance, wm2TempF,
                         wm2VC0706Motion, wm2VC0706Snap *),
      StdCtrls      (* tLabel *),
      Windows       (**);

    type
      tfmOutdoorInstruments = class(tForm)
        qPressureRecent6: tAdsQuery;
          qRecentRain: tAdsQuery;
          lbAirTempValue:    tLabel;
          lbDepth:           tLabel;
          lbDepthValue:      tLabel;
          lbDP:              tLabel;
          lbDPValue:         tLabel;
          lbHeaderAir:       tLabel;
          lbHeaderMisc:      tLabel;
          lbHeaderPond:      tLabel;
          lbHeaderVC0706:    tLabel;
          lbHumidex:         tLabel;
          lbHumidexValue:    tLabel;
          lbLight:           tLabel;
          lbLightValue:      tLabel;
          lbPM25:            tLabel;
          lbPM25Value:       tLabel;
          lbPondTemperature: tLabel;
          lbPondTempValue:   tLabel;
          lbPressure:        tLabel;
          lbPressureValue:   tLabel;
          lbRain:            tLabel;
          lbRainGaugeJammed: tLabel;
          lbRH:              tLabel;
          lbRHValue:         tLabel;
          lbTempF:           tLabel;
          lbVC0706CarPort:   tLabel;
          lbVC0706Deck:      tLabel;
          lbWaterfall:       tLabel;
          lbWaterfallValue:  tLabel;
          lbWind:            tLabel;
          procedure FormDblClick(Sender: tObject);
          procedure FormShow    (Sender: tObject);
        private
          VC0706Used: boolean;
          LastRainTime: tDateTime;
          mmRainTrace: real;
          procedure FormatRain(const When: tDateTime; const Rainmm, MMPerHour: real);
        public
          procedure BadData(const Reason: tFailureReason);
          procedure Initialize;
          procedure NewDataDewpointF(const DPF, HumidexF: real);
          procedure NewDataGeneric(const TU: tTableUsed);
          procedure NewDataRain(const mmToday: real;
                                Jammed: boolean = false;
                                JammedSince: tDateTime = 0;
                                MMPerHour: real = 0);
          procedure NewDataVC0706(const WM: tWhatMeasured2;
                                  const ObservedUID: tObserved;
                                  const FileNumber: integer);
          procedure NewDataWaterfall(const AnchorLocal: tDateTime;
                                     const IsNowOn: boolean;
                                     const HoursPassed, HoursOn: real);
      end (* tfmOutdoorInstruments *);

    var
      fmOutdoorInstruments: tfmOutdoorInstruments;

  implementation
    uses
      BoardManager  (* fmBoardManager *),
      DateUtils     (* DateOf *),
      Dialogs       (* ShowMessage *),
      eFrames       (* eFrameReal *),
      Graphics      (* clPurple, clRed, clWindowText *),
      HC4Main       (* fmHC4Main *),
      HC3Utilities  (* FormatFCTemp, FtoC, INILoadFloat, OnOffDisplay,
                         SPS30FailureString *),
      LabelAdjuster (* LeftMonitorScale, ms175, TheLabelAdjuster *),
      MyConsts      (* nullstring, UsualFormColor *),
      MyUtils       (* Plural *),
      SysUtils      (* Format, FormatDateTime *);
      {$R *.dfm}

    var
      OldPressure, PressureDamping: real;
      WindCache: record
        MPH: real;
        strDirection: string;
        end (* WindCache *);

    procedure tfmOutdoorInstruments.BadData(const Reason: tFailureReason);
      (* note in UI *)

      var
        I: integer;
        FailureList: string;

      procedure BadLabel(TheLabel: tLabel; const NewText: string);
        (* cliche *)

        begin (* BadLabel *)
          with TheLabel do
            begin (* TheLabel *)
              if FailureList = nullstring then
                Caption := NewText
              else
                Caption := Format('%s (%s)', [NewText, FailureList]);
              Caption := Format('%s as of %s',
                                [Caption,
                                 FormatDateTime('hhnn',
                                                Global.LatestData.LocalStamp)]);
              Font.Color := clPurple;
              Enabled := true
            end   (* TheLabel *)
        end   (* BadLabel *);

      begin (* tfmOutdoorInstruments.BadData *)
        with Global.LatestData do
          begin (* Global.LatestData *)
            FailureList := nullstring;
            for I := 2 to NArgs do
              if FailureList = nullstring then
                FailureList := Format('%.3f', [RealArg[I]])
              else
                FailureList := Format('%s, %.3f', [FailureList, RealArg[I]]);

            if Reason in SPS30FailureReasons then
              BadLabel(lbPM25Value, SPS30FailureString(Reason))

            else
              case Reason of
                    frDHT22ReadFailure: if ObservedUID = obPond then
                                          BadLabel(lbPondTempValue, 'DHT22 read failure')
                                        else
                                          begin (* all measurements bad *)
                                            BadLabel(lbAirTempValue, 'DHT22 read failure');
                                            BadLabel(lbRHValue,      'DHT22 read failure');
                                            BadLabel(lbDPValue,      'DHT22 read failure')
                                          end   (* all measurements bad *);

                frImplausibleFrequency: BadLabel(lbWind, 'implausible wind speed');
                 frImplausiblePressure: BadLabel(lbPressureValue, 'implausible');
                       frImplausibleRH: BadLabel(lbRHValue, 'implausible');
                     frImplausibleTemp: if ObservedUID = obPond then
                                          BadLabel(lbPondTempValue, 'implausible')
                                        else
                                          BadLabel(lbAirTempValue, 'implausible');
                      frResistanceOpen: if Table[1] = tuLight then
                                          BadLabel(lbLight, 'photocell open circuit')
                                        else
                                          BadLabel(lbWind, 'weathervane open circuit');
                   frResistanceShorted: if Table[1] = tuLight then
                                          BadLabel(lbLight, 'photocell short circuit')
                                        else
                                          BadLabel(lbWind, 'weathervane short circuit')
              end (* case *);
          end   (* Global.LatestData *);

        if not self.Visible then
          Show;
        self.SetFocus
      end   (* tfmOutdoorInstruments.BadData *);

    procedure tfmOutdoorInstruments.FormatRain(const When: tDateTime;
                                               const Rainmm, MMPerHour: real);
      (* utility *)
    
      const
        RainDescription = 'rain today: %5.1f mm through %s';

      begin (* tfmOutdoorInstruments.FormatRain *)
        with lbRain do
          begin (* lbRain *)
            if Rainmm = 0 then
              Caption := 'no rain today'
            else if Rainmm < mmRainTrace then
              Caption := 'rain today: trace'
            else if MMPerHour <= 0.01 then   // mmPerHour format %.2f
              Caption := Format(RainDescription,
                                [Rainmm, FormatDateTime('hhnn', When)])
            else
              Caption := Format(RainDescription + ' (%.2f mm/hr)',
                                [Rainmm, FormatDateTime('hhnn', When),
                                 MMPerHour]);
            Enabled := true
          end   (* lbRain *);
        LastRainTime := When
      end   (* tfmOutdoorInstruments.FormatRain *);

    procedure tfmOutdoorInstruments.FormDblClick(Sender: tObject);
      (* show master form *)

      begin (* tfmOutdoorInstruments.FormDblClick *)
        fmHC4Main.Show
      end   (* tfmOutdoorInstruments.FormDblClick *);

    procedure tfmOutdoorInstruments.FormShow(Sender: tObject);
      (* initialize.  beyond font sizes, need linespacing fix and
      panel resizing *)

      const
        BottomMargin = 35;

      var
        JammedSimple, LastBottom, RainSimple, WindSimple: integer;

      begin (* tfmOutdoorInstruments.FormShow *)
        self.Color := UsualFormColor;
        JammedSimple :=  65;
        RainSimple   := 104;
        WindSimple   := 139;

        with TheLabelAdjuster do
          begin (* TheLabelAdjuster *)
            NextTop := 4;
            case LeftMonitorScale of
              ms150,
              ms175,
              ms200: begin (* scale 200% *)
                       JammedSimple :=  65;
                       RainSimple   := 104;
                       WindSimple   := 139
                     end   (* scale 200% *)
              end (* case *);

            PlaceHeader(lbHeaderAir);
              PlaceDetailPair(lbTempF,    lbAirTempValue);
              PlaceDetailPair(lbRH,       lbRHValue);
              PlaceDetailPair(lbDP,       lbDPValue);
              PlaceDetailPair(lbHumidex,  lbHumidexValue);
              PlaceDetailPair(lbPressure, lbPressureValue);
              PlaceDetailPair(lbPM25,     lbPM25Value);

            PlaceHeader(lbHeaderPond);
              PlaceDetailPair(lbPondTemperature, lbPondTempValue);
              PlaceDetailPair(lbDepth,           lbDepthValue);
              PlaceDetailPair(lbWaterfall,       lbWaterfallValue);

            if VC0706Used then
              begin (* VC0706 used *)
                PlaceHeader(lbHeaderVC0706);
                PlaceSimple(lbVC0706CarPort);
                PlaceSimple(lbVC0706Deck)
              end   (* VC0706 used *)
            else
              begin (* not used *)
                lbHeaderVC0706.Visible  := false;
                lbVC0706CarPort.Visible := false;
                lbVC0706Deck.Visible    := false
              end   (* not used *);

            PlaceHeader(lbHeaderMisc);
              PlaceSimple    (lbWind,            WindSimple);
              PlaceDetailPair(lbLight,           lbLightValue);
              PlaceSimple    (lbRain,            RainSimple);
              PlaceSimple    (lbRainGaugeJammed, JammedSimple);
            LastBottom := NextTop
          end   (* TheLabelAdjuster *);

         self.Height := LastBottom + BottomMargin
       end   (* tfmOutdoorInstruments.FormShow *);

    procedure tfmOutdoorInstruments.Initialize;
      (* no data yet *)

      var
        I: integer;

      procedure InitPressure;
        (* debulk *)

        const
          PD_Default = 0.9;

        var
          I: integer;
          OlderPressure: real;

        begin (* InitPressure *)
          PressureDamping := INILoadFloat('Outdoor', 'PressureDamping', PD_Default);
          OldPressure := 0;
          with qPressureRecent6 do
            begin (* qPressureRecent6 *)
              Close;
              Open;
              Last;
              for I := 1 to 6 do
                begin (* one record *)
                  OlderPressure := FieldByName('PressureInches').AsFloat;
                  if OldPressure = 0 then
                    OldPressure := OlderPressure
                  else
                    OldPressure := PressureDamping * OldPressure +
                                 (1 - PressureDamping) * OlderPressure;
                  Prior
                end   (* one record *);

              Close
            end   (* qPressureRecent6 *)
        end   (* InitPressure *);

      procedure InitRain;
        (* debulk *)

        var
          LastRainmm: real;

        begin (* InitRain *)
          mmRainTrace               := INILoadFloat('Rain', 'mmTrace');
          with qRecentRain do
            begin (* qRecentRain *)
              Close;
              Open;
              LastRainTime := FieldByName('WhenRecorded').AsDateTime;
              Next;
              LastRainmm   := FieldByName('Rainmm'      ).AsFloat;
              Close
            end   (* qRecentRain *);

          // if DateOf(LastRainTime) < DateOf(Today) then
          //   LastRainmm := 0;
          FormatRain(LastRainTime, LastRainmm, 0);
          fmeFrames.eFrameReal(avMMRainToday, LastRainmm);
          lbRainGaugeJammed.Visible := false;
        end   (* InitRain *);

      begin (* tfmOutdoorInstruments.Initialize *)
        self.Color := UsualFormColor;
        for I := 1 to ComponentCount do
          if Components[I-1] is tControl then
            with tControl(Components[I-1]) do
              if Tag = 0 then
                Enabled := false;
        InitPressure;
        InitRain;
        VC0706Used := INILoadBoolean('Outdoor', 'VC0706');
        WindCache.MPH := SillyRealValue;
        WindCache.strDirection := nullstring;
      end   (* tfmOutdoorInstruments.Initialize *);

    procedure InstallAndEnableAsOf(const FormattedValue: string;
                                   ReportLabel: tLabel;
                                   Herald: tLabel = nil);
      (* put the Report in the right place *)

      begin (* InstallAndEnableAsOf *)
        with Global.LatestData do
          ReportLabel.Caption := Format('%s as of %s',
                                        [FormattedValue,
                                         FormatDateTime('hhnn', LocalStamp)]);
        ReportLabel.Font.Color := clWindowText;
        ReportLabel.Enabled := true;
        if assigned(Herald) then
          Herald.Enabled := true;
        fmOutdoorInstruments.Repaint
      end   (* InstallAndEnableAsOf *);

    procedure tfmOutdoorInstruments.NewDataDewpointF(const DPF, HumidexF: real);
      (* special-purpose, since Dewpoint not part of LatestData *)

      begin (* tfmOutdoorInstruments.NewDataDewpointF *)
        InstallAndEnableAsOf(FormatFCTemp(DPF),      lbDPValue,      lbDP);
        InstallAndEnableAsOf(FormatFCTemp(HumidexF), lbHumidexValue, lbHumidex);
        fmeFrames.eFrameReal(avOutsideHumidexF, HumidexF)
      end   (* tfmOutdoorInstruments.NewDataDewpointF *);

    procedure tfmOutdoorInstruments.NewDataGeneric(const TU: tTableUsed);
      (* dispatcher *)

      procedure NewPond;
        (* debulk *)

        var
          NewDepth, NewTempF: real;

        begin (* NewPond *)
          with Global.LatestData do
            case WMArg[1] of
              wm2DepthInches: begin (* depth *)
                                NewDepth := RealArg[1];
                                if NewDepth > 0 then
                                  InstallAndEnableAsOf(Format('%.1f"', [NewDepth]),
                                                              lbDepthValue,
                                                              lbDepth)
                                else
                                 begin (* bad data *)
                                   InstallAndEnableAsOf('sensor flooded',
                                                        lbDepthValue,
                                                        lbDepth);
                                   lbDepthValue.Font.Color := clPurple
                                 end   (* bad data *)
                              end (* depth *);

                    wm2TempF: begin (* pond temp *)
                                NewTempF := RealArg[1];
                                InstallAndEnableAsOf(FormatFCTemp(NewTempF),
                                                     lbPondTempValue,
                                                     lbPondTemperature);
                                fmeFrames.eFrameReal(avPondTempF, NewTempF)
                              end   (* pond temp *)
              end (* case *)
        end   (* NewPond *);

      procedure NewSPS30;
        (* select from too much data *)

        var
          PM25: real;

        begin (* NewSPS30 *)
          lbPM25.Enabled := true;
          PM25 := Global.LatestData.RealValueOf(wm2MassPM25);
          with lbPM25Value do
            begin (* lbPM25Value *)
              Caption := Format('%.2f µg/m^3 as of %s',
                                [PM25,
                                 FormatDateTime('hhnn',
                                                Global.LatestData.LocalStamp)]);
              Font.Color := clWindowText;
              Enabled := true
            end   (* lbPM25Value *);

          fmeFrames.eFrameReal(avPM25, PM25)
        end   (* NewSPS30 *);

      procedure NewTRHP;
        (* debulk *)

        const
          kPaPerInchHg = 3.3864;

        var
          I: integer;
          NewPressure, RH, TempF: real;

        begin (* NewTRHP *)
          with Global.LatestData do
            begin (* Global.LatestData *)
              for I := 1 to NArgs do
                case WMArg[I] of
                  wm2PressureInches: begin (* pressure *)
                                       NewPressure := RealArg[I];

                                       InstallAndEnableAsOf(Format('%.2f" Hg (%.1f kPa)',
                                                                   [NewPressure, kPaPerInchHg * NewPressure]),
                                                            lbPressureValue, lbPressure);
                                       if OldPressure = 0 then
                                         OldPressure := NewPressure
                                       else
                                         OldPressure := PressureDamping     * OldPressure +
                                                      (1 - PressureDamping) * NewPressure;
                                       fmBoardManager.BroadcastPressureInches(NewPressure)
                                     end   (* pressure *);

                              wm2RH: begin (* might be announcing *)
                                       RH := RealArg[I];
                                       InstallAndEnableAsOf(Format('%.0f%%', [RH]),
                                                          lbRHValue, lbRH);
                                       fmeFrames.eFrameReal(avOutsideRH, RH)
                                     end  (* might be announcing *);

                           wm2TempF: begin (* might be announcing *)
                                       TempF := RealArg[I];
                                       InstallAndEnableAsOf(FormatFCTemp(TempF),
                                                            lbAirTempValue, lbTempF);
                                       fmeFrames.eFrameReal    (avOutsideTempF, TempF);
                                       fmBoardManager.BroadcastOutsideTempF(TempF)
                                     end  (* might be announcing *)
                  end (* case *)
            end   (* Global.LatestData *)
        end   (* NewTRHP *);

      procedure NewWind;
        (* debulk *)
      
        const
          GustIncrement = 5;
          DirArray: array[0 .. 15] of string = ('N', 'NNE', 'NE', 'ENE',
                                                'E', 'ESE', 'SE', 'SSE',
                                                'S', 'SSW', 'SW', 'WSW',
                                                'W', 'WNW', 'NW', 'NNW');

        var
          I, intDirection: integer;
          WindMax: real;

        begin (* NewWind *)
          WindMax := -abs(SillyRealValue);
          with Global.LatestData, WindCache do
            begin (* Global.LatestData *)
              for I := 1 to NArgs do
                case WMArg[I] of
                  wm2FrequencyMax: WindMax := RealArg[I];
                  wm2FrequencyRaw: MPH := RealArg[I];
                    wm2Resistance: begin (* direction *)
                                     intDirection := trunc((RealArg[I] + 11.25) / 22.5) mod 16;
                                     strDirection := DirArray[intDirection]
                                   end   (* direction *);
                  end (* case *);

              if (MPH >= 0) and (strDirection <> nullstring) then
                with lbWind do
                  begin (* reportable *)
                    if MPH = 0 then
                      Caption := 'calm'
                    else if MPH <= 0.5 then
                      Caption := Format('wind from the %s at < 1 MPH', [strDirection])
                    else
                      Caption := Format('wind from the %s at %.0f MPH',
                                        [strDirection, MPH]);

                    if WindMax >= MPH + GustIncrement then
                      Caption := Format('%s with gusts to %.0f', [Caption, WindMax]);
                    Font.Color := clWindowText;
                    Enabled := true
                  end   (* reportable *)
            end   (* Global.LatestData *)
        end   (* NewWind *);

      begin (* tfmOutdoorInstruments.NewDataGeneric *)
        if Today > DateOf(LastRainTime) then
          NewDataRain(0);

        case TU of
          tuLight: InstallAndEnableAsOf(Format('%.0f',
                                               [Global.LatestData.RealArg[1]]),
                                                lbLightValue, lbLight);
           tuPond: NewPond;
          tuSPS30: NewSPS30;
           tuTRHP: NewTRHP;
           tuWind: NewWind
          end (* case *)
      end   (* tfmOutdoorInstruments.NewDataGeneric *);

    procedure tfmOutdoorInstruments.NewDataRain(const mmToday: real;
                                                Jammed: boolean;
                                                JammedSince: tDateTime;
                                                MMPerHour: real);
      (* data are not global *)
    
      var
        SinceFormat: string;

      begin (* tfmOutdoorInstruments.NewDataRain *)
        FormatRain(Global.LatestData.LocalStamp, mmToday, MMPerHour);
        fmeFrames.eFrameReal(avMMRainToday,   mmToday);

        with lbRainGaugeJammed do
          begin (* lbRainGaugeJammed *)
            if Jammed then
              begin (* need caption *)
                if DateOf(JammedSince) = Date then
                  SinceFormat := 'hhnn'
                else
                  SinceFormat := 'd mmm @ hhnn';
                Caption := Format('(gauge apparently jammed since %s)',
                                 [FormatDateTime(SinceFormat, JammedSince)]);
                Show
              end   (* need caption *);
            Visible := Jammed
          end   (* lbRainGaugeJammed *)
      end   (* tfmOutdoorInstruments.NewDataRain *);

    procedure tfmOutdoorInstruments.NewDataVC0706(const WM: tWhatMeasured2;
                                                  const ObservedUID: tObserved;
                                                  const FileNumber: integer);
      (* picture taken *)

      procedure FillLabel(TheLabel: tLabel);
        (* common code *)
      
        begin (* FillLabel *)
          with TheLabel do
            begin (* TheLabel *)
              Enabled := true;
              Font.Color := clWindowText;
              Caption := Format('%s image #%d taken at %s',
                                [GetObservedName(ObservedUID),
                                 FileNumber,
                                 FormatDateTime('hhnn', Global.LatestData.LocalStamp)]);
              if WM = wm2VC0706Motion then
                begin (* motion detected *)
                  Font.Color := clPurple;
                  Caption := 'motion-triggered ' + Caption
                end   (* motion detected *)
            end   (* TheLabel *)
        end   (* FillLabel *);

      begin (* tfmOutdoorInstruments.NewDataVC0706 *)
        case ObservedUID of
          obCarport: FillLabel(lbVC0706CarPort);
             obDeck: FillLabel(lbVC0706Deck)
          end (* case *)
      end   (* tfmOutdoorInstruments.NewDataVC0706 *);

    procedure tfmOutdoorInstruments.NewDataWaterfall(const AnchorLocal: tDateTime;
                                                     const IsNowOn: boolean;
                                                     const HoursPassed, HoursOn: real);
      (* analogous to furnace, A/C, shop heater *)
    
      begin (* tfmOutdoorInstruments.NewDataWaterfall *)
        if INILoadBoolean('Pond', 'Deicer') then
          begin (* deicer *)
            lbWaterfall.Caption := 'deicer';
            lbWaterfallValue.Enabled := true;
            lbWaterfall.Caption := 'deicer';
            lbWaterfallValue.Enabled := true
          end   (* deicer *)
        else
          begin (* waterfall *)
            lbWaterfall.Caption := 'waterfall';
            OnOffDisplay(lbWaterfall, lbWaterfallValue,
                           AnchorLocal,
                           IsNowOn,
                           HoursPassed, HoursOn)
          end   (* waterfall *)
      end   (* tfmOutdoorInstruments.NewDataWaterfall *);

end.
