unit HC3DM;

  interface
    uses
      AdsCnnct      (* tAdsConnection *),
      AdsData       (* tAdsStringField *),
      AdsFunc       (* used by AdsTable *),
      AdsTable      (* tAdsQuery *),
      Classes       (* tDataModule *),
      Contnrs       (* tObjectList *),
      Controls      (* tDate *),
      DB            (* tIntegerField; also used by AdsTable *),
      LineParser    (* tLineParser, sepTab *),
      ObservedAndWM (* tBoardSlot, tMeasurements2, tSensorType, tTableUsed,
                         tWhatMeasured, tWindowsToBase *),
      TableManager  (* TableInfo *);

    const
      MarlettCheckMark  = 'a';
      MaxArguments      = 20;
      msOneSecond       = 1000;
      SillyIntegerValue = -99999;
      SillyRealValue    = -999.99;
      TinyRealValue     = 0.0001;
      
      msOneMinute       = 60 * msOneSecond;

    type
      tAxisType       = (atStandard, atAdaptive, atZeroAdaptive);
      tGlobalStatus   = (gsConnected, gsReceivingDump, gsUnconnected);
      tUIDForSet      = 0 .. 255;
      tUIDSet         = set of tUIDForSet;
      tAnnunciatedVar = (avGCFridge, avHeader, avMMRainToday, avOutsideHumidexF,
                           avOutsideRH, avOutsideTempF, avPM25, avPondTempF,
                           avRainMMPerHour, avShopTemp, avSpacer, avTimeStamp,
                           avTrailer);

      tArgument = record
        TheTable: tTableUsed;
        TheFieldName: string;
        end (* tArgument *);

    type
      tAvSet = set of tAnnunciatedVar;

    type
      tSensorData = class(tLineParser)
        private
          Argument: array[1 .. MaxArguments] of tArgument;
          function GetBoardName: string;
          function GetExpanded: string;
          function GetBoolArg  (I: integer): boolean;
          function GetFieldName(I: integer): string;
          function GetIntArg   (I: integer): integer;
          function GetRealArg  (I: integer): real;
          function GetTable    (I: integer): tTableUsed;
          function GetWMArg    (I: integer): tWhatMeasured2;
          function GetWTBArg   (I: integer): tWindowsToBase;
        public
          BoardTag: char;  // set in Dispatcher
          UTCStamp, LocalStamp: tDateTime;
          BoardSlot: tBoardSlot;
          NArgs: integer;
          RecentUID: integer; // from RunStoreDataQuery
          TheSensorUID: integer;
          Measurements: tMeasurements2;
          TheObservedUID: tObserved;
          SensorType: tSensorType;
          BoardSlotName: string;
          ReceivedLine: string;
          SensorTypeName: string;

          constructor Create;
          function    IntValueOf(const WM: tWhatMeasured2): integer;
          function    ObjectsOrganized: boolean;
          function    RealValueOf(const WM: tWhatMeasured2): real;
          procedure   SetValueOf(const WM: tWhatMeasured2; const Replacement: real);
          procedure   StoreMystery(const Sensor_xxx: string; const I: integer);

          property BoardName:             string         read GetBoardName;
          property BoolArg  [I: integer]: boolean        read GetBoolArg;
          property ExpandedLine:          string         read GetExpanded;
          property FieldName[I: integer]: string         read GetFieldName;
          property IntArg   [I: integer]: integer        read GetIntArg;
          property ObservedUID:           tObserved      read TheObservedUID;
          property RealArg  [I: integer]: real           read GetRealArg;
          property Table    [I: integer]: tTableUsed     read GetTable;
          property WMArg    [I: integer]: tWhatMeasured2 read GetWMArg;
          property WTBArg   [I: integer]: tWindowsToBase read GetWTBArg;
        end (* tSensorData *);

    type
      tGlobal = class(tObject)
           EarliestPossibleDate: tDate;
           DistantFuture:             tDateTime;
           ImpossibleDateTime:        tDateTime;
           LocalTimeLastCharReceived: tDateTime;
           DecentGraphInterval: integer;
           LineVoltage:         integer;
           OldestThrough:       integer (* oldest tables have data through this year *);
           RTCSecondsBehind: Int64;
           LatestData: tSensorData;
           GraphPrinterName: string;
           Status: tGlobalStatus; // set only in Dashboard/SerialManager
           constructor Create;
           destructor Destroy; override;
           procedure SaveOldestThrough;
         end (* tGlobal *);

      tdmHC3 = class(tDataModule)
        cnHomeControl3: tAdsConnection;
          qBoards:                   tAdsQuery;
          qFieldWidth:               tAdsQuery;
          qLastUIDOld:               tAdsQuery;
          qObserved:                 tAdsQuery;
          qSaveBadData:              tAdsQuery;
          qSensorMeasurementsWanted: tAdsQuery;
          qSensorTableField:         tAdsQuery;
            qSensorTableFieldFieldName: tAdsStringField;
            qSensorTableFieldTableUID: tIntegerField;
        cnHC3Data: tAdsConnection;
          qAdvanceOnOffEdge:        tAdsQuery;
          qClearWhatMeasured:       tAdsQuery;
          qGCFridgeSettingx10:      tAdsQuery;
          qGetRecentPondDepth:      tAdsQuery;
          qGetRecentPondTempF:      tAdsQuery;
          qLastUIDHC3Data:          tAdsQuery;
          qLastUIDHomeControl:      tAdsQuery;
          qMoveLightEdge:           tAdsQuery;
          qMoveThermostatEdge:      tAdsQuery;
          qNewCO2ppm:               tAdsQuery;
          qNewLightPoint:           tAdsQuery;
          qNewOnOffPair:            tAdsQuery;
          qNewPondData:             tAdsQuery;
          qNewRainEntry:            tAdsQuery;
          qNewThermostatEntry:      tAdsQuery;
          qNewWhatMeasured:         tAdsQuery;
          qRainYesterday:           tAdsQuery;
          qSaveMystery:             tAdsQuery;
          qSensorAndObserved:       tAdsQuery;
          qUpdateRainFence:         tAdsQuery;
          qUpdateTRHPWithDewpoint:  tAdsQuery;
          qUpdateWindWithDirection: tAdsQuery;
        procedure DataModuleCreate (Sender: tObject);
        procedure DataModuleDestroy(Sender: tObject);
      end (* tdmHC3 *);

    var
      dmHC3: tdmHC3;
      Global: tGlobal;

  implementation
    uses
      DashBoard    (* fmDashboard *),
      DateUtils    (* EncodeDateTime, Now *),
      HC3Utilities (* ExpandTabsOfLine, INILoadInteger, INISaveInteger,
                        TrimInternalWhiteSpace *),
      LogManager   (* fmLogManager *),
      Math         (* IntPower *),
      MyConsts     (* BLANK, nullstring *),
      RRF_StringAndTimeUtils (* MyStrToDateTime, UTCToLocal *),
      SysUtils     (* EncodeDate, Format, IntToStr, StrToInt *);
      {$R *.dfm}

    procedure tdmHC3.DataModuleCreate(Sender: tObject);
      (* initialize *)
    
      const
        DefaultGraphInterval = 120;

      begin (* tdmHC3.DataModuleCreate *)
        cnHomeControl3.IsConnected := true;
        cnHC3Data.IsConnected := true;
        Global := tGlobal.Create;
        with Global do
          begin (* with Global *)
            DistantFuture             := EncodeDate(2050, 12, 31);
            EarliestPossibleDate      := EncodeDate(2009, 9, 1);
            ImpossibleDateTime        := EncodeDateTime(1, 1, 1, 0, 0, 0, 0);
            LocalTimeLastCharReceived := Now;
            DecentGraphInterval       := INILoadInteger('Data Module', 'DecentGraphInterval', DefaultGraphInterval);
            LineVoltage               := INILoadInteger(               'LineVoltage',         118);
            OldestThrough             := INILoadInteger(               'OldestThrough',         0)
          end   (* with Global *)
      end   (* tdmHC3.DataModuleCreate *);

    procedure tdmHC3.DataModuleDestroy(Sender: tObject);
      (* clean up *)

      begin (* tdmHC3.DataModuleDestroy *)
        Global.Free
      end   (* tdmHC3.DataModuleDestroy *);

{ tGlobal }

    constructor tGlobal.Create;
      (* register and create slots *)
    
      begin (* tGlobal.Create *)
        inherited Create;
        LatestData := tSensorData.Create;
        RTCSecondsBehind := 0
      end   (* tGlobal.Create *);

    destructor tGlobal.Destroy;
      (* clean up *)

      begin (* tGlobal.Destroy *)
        LatestData.Free;
        inherited Destroy
      end   (* tGlobal.Destroy *);

    procedure tGlobal.SaveOldestThrough;
      (* service for TableManager *)
    
      begin (* tGlobal.SaveOldestThrough *)
        INISaveInteger('Data Module', 'OldestThrough', OldestThrough)
      end   (* tGlobal.SaveOldestThrough *);

{ tSensorData }

    constructor tSensorData.Create;
      (* constructor *)
    
      const
        RespectQuotes = true;

      begin (* tSensorData.Create *)
        inherited Create(sepTab, not RespectQuotes);
      end   (* tSensorData.Create *);

    function tSensorData.GetBoardName: string;
      (* service for VC0706, if no one else *)

      begin (* tSensorData.GetBoardName *)
        with dmHC3.qBoards do
          begin (* qBoards *)
            Close;
            ParamByName('BoardTag').AsString := BoardTag;
            Open;
            Result := FieldByName('BoardName').AsString;
            Close
          end   (* qBoards *)
      end   (* tSensorData.GetBoardName *);
                                                                 {$WARNINGS OFF}

    function tSensorData.GetBoolArg(I: integer): boolean;
      (* objects to boolean *)
    
      begin (* tSensorData.GetBoolArg *)
        Result := (integer(Objects[3 * I + 2]) > 0)
      end   (* tSensorData.GetBoolArg *);
                                                                 {$WARNINGS ON}

    function tSensorData.GetExpanded: string;
      (* expand tabs *)
    
      begin (* tSensorData.GetExpanded *)
        Result := ExpandTabsOfLine(LineFromPiece1)
      end   (* tSensorData.GetExpanded *);

    function tSensorData.GetFieldName(I: integer): string;
      (* try to avoid unnecessary queries *)

      begin (* tSensorData.GetFieldName *)
        Result := Argument[I].TheFieldName
      end   (* tSensorData.GetFieldName *);

    function tSensorData.GetIntArg(I: integer): integer;
      (* objects to integers *)

      begin (* tSensorData.GetIntArg *)
        Result := trunc(GetRealArg(I))
      end   (* tSensorData.GetIntArg *);

    function tSensorData.GetRealArg(I: integer): real;
      (* can't use Objects, because signs have been lost *)

      var
        Mantissa: real;

      begin (* tSensorData.GetRealArg *)
        Mantissa := StrToFloatDef(Strings[3*I+2], 0);
        if Strings[3*I+3] = '0' then
          Result := Mantissa
        else
          Result := Mantissa / IntPower(10.0, StrToIntDef(Strings[3*I+3], 0))
      end   (* tSensorData.GetRealArg *);

    function tSensorData.GetTable(I: integer): tTableUsed;
      (* try to avoid unnecessary queries *)

      begin (* tSensorData.GetTableName *)
        Result := Argument[I].TheTable
      end   (* tSensorData.GetTableName *);
                                                                 {$WARNINGS OFF}
    function tSensorData.GetWMArg(I: integer): tWhatMeasured2;
      (* objects to WhatMeasured *)

      begin (* tSensorData.GetWMArg *)
        Result := tWhatMeasured2(integer(Objects[3*I+1]))
      end   (* tSensorData.GetWMArg *);

    function tSensorData.GetWTBArg(I: integer): tWindowsToBase;
      (* object to tWindowsToBase (for acks from relays) *)

      begin (* tSensorData.GetWTBArg *)
        Result := tWindowsToBase(integer(Objects[3*I+2]))
      end   (* tSensorData.GetWTBArg *);

                                                                 {$WARNINGS ON}
    function tSensorData.IntValueOf(const WM: tWhatMeasured2): integer;
      (* simple lookup *)

      var
        I: integer;

      begin (* tSensorData.IntValueOf *)
        Result := SillyIntegerValue;
        for I := 1 to NArgs do
          if WMArg[I] = WM then
            begin (* found it *)
              Result := IntArg[I];
              break
            end   (* found it *)
      end   (* tSensorData.IntValueOf *);

   function tSensorData.ObjectsOrganized: boolean;
      (* here iff tag is good
           Objects[3 .. end] = StrToInt(Strings[3 .. end]
           TheSensorType defined
      *)

      var
        FoundLink: boolean;
        I: integer;
        WM: tWhatMeasured2;
                                                                 {$WARNINGS OFF}

      function ValidLine: boolean;
        (* validate checksum and line length *)

        var
          ComputedChecksum, Converted, I: integer;

        begin (* ValidLine *)
          ComputedChecksum := 0;
          Result := false;

          for I := 3 to Count - 1 do
            begin (* convert and (mostly) add to ComputedChecksum *)
              // Because of 'abs' here, RealArg and IntArg must re-convert
              Converted := abs(StrToIntDef(Strings[I], 0));
              Objects[I] := tObject(Converted);
              if I < Count - 1 then
                inc(ComputedChecksum, Converted);
              // fmDashboard.ExtendChat(Format('dm_364    Strings[%d] = "%s" (%d) --> %d',
              //                               [I, Strings[I], Converted, ComputedChecksum]))
            end   (* convert and (mostly) add to ComputedChecksum *);
          if ComputedChecksum <> integer(Objects[Count-1]) then
            fmDashboard.ExtendChat(Format('HC3DM 361: checksum mismatch; Count %d, sum should be %d',
                                             [Count, ComputedChecksum]))
          else if (Count < 5) or ((Count mod 3) <> 2) then
            fmDashboard.ExtendChat('HC3DM 364: line ill-formed')
          else
            Result := true
        end   (* ValidLine *);

      begin (* tSensorData.ObjectsOrganized *)
        // fmDashboard.ExtendChat('dm_370 ObjectsOrganized');
        Result := false;

        if ValidLine then
          begin (* valid line *)
            UTCStamp   := MyStrToDateTime(Strings[1]);
            LocalStamp := UTCToLocal(UTCStamp);
            BoardSlot  := tBoardSlot(integer(Objects[3]));

            with dmHC3.qSensorAndObserved do
              begin (* qSensorAndObserved *)
                Close;
                ParamByName('BoardTag').AsString := BoardTag;
                ParamByName('BoardSlot').AsInteger := ord(BoardSlot);
                Open;
                BoardSlotName  :=             FieldByName('BoardSlotName' ).AsString;
                SensorTypeName :=             FieldByName('SensorTypeName').AsString;
                SensorType     := tSensorType(FieldByName('SensorTypeUID' ).AsInteger);
                TheObservedUID :=   tObserved(FieldByName('ObservedUID'   ).AsInteger);
                TheSensorUID   :=             FieldByName('SensorUID'     ).AsInteger;
                FoundLink      := (TheSensorUID <> 0);
                Close
              end   (* qSensorAndObserved *);

            // case BoardTag of
              // 'F': fmDashboard.ExtendChat(Format('DM_394: Slot %s (#%d); %s sensor (#%d, type #%d); observed %s (#%d)',
              //                                    [BoardSlotName, ord(BoardSlot),
              //                                     SensorTypeName, TheSensorUID, ord(SensorType),
              //                                     GetObservedName(TheObservedUID), ord(TheObservedUID)]));
              // 'G': fmDashboard.ExtendChat(Format('DM_399: Slot %s (#%d); %s sensor (#%d, type #%d); observed %s (#%d)',
              //                                    [BoardSlotName, ord(BoardSlot),
              //                                     SensorTypeName, TheSensorUID, ord(SensorType),
              //                                     GetObservedName(TheObservedUID), ord(TheObservedUID)]));
              // 'O': fmDashboard.ExtendChat(Format('HC3DM 399: line from %s (#%d) at office %s (#%d)',
              //                                    [SensorTypeName, TheSensorUID, BoardSlotName, ord(BoardSlot)]))
              // end (* case *);

            if FoundLink then
              begin (* found link *)
                NArgs := (Count - 5) div 3;
                Measurements := [];
                for I := 1 to NArgs do
                  begin (* collect arg-specific data *)
                    WM := WMArg[I];
                    if not (WM in [wm2Bogus, wm2Calibrate, wm2SpeedupChanged, wm2Undefined]) then
                      begin (* real measurement *)
                        Measurements := Measurements + [WM];
                        with dmHC3.qSensorTableField, Argument[I] do
                          begin (* qSensorTableField *)
                            Close;
                            ParamByName('SensorUID').AsInteger := TheSensorUID;
                            ParamByName('WM'       ).AsInteger := ord(GetWMArg(I));
                            Open;
                            TheTable     := tTableUsed(FieldByName('TableUID').AsInteger);
                            TheFieldName :=            FieldByName('FieldName').AsString;
                            Close
                          end   (* qSensorTableField *)
                      end   (* real measurement *)
                  end   (* collect arg-specific data *);

                if UTCStamp = 0 then
                  fmDashboard.ExtendChat('HC3DM 428: UTCStamp zero')
                else
                  Result := true
              end   (* found link *)
          end   (* valid line *)
      end   (* tSensorData.ObjectsOrganized *);
                                                                 {$WARNINGS ON}

    function tSensorData.RealValueOf(const WM: tWhatMeasured2): real;
      (* just a lookup *)

      var
        I: integer;

      begin (* tSensorData.RealValueOf *)
        Result := SillyRealValue;
        for I := 1 to NArgs do
          if WMArg[I] = WM then
            begin (* found it *)
              Result := RealArg[I];
              break
            end   (* found it *)
      end   (* tSensorData.RealValueOf *);

    procedure tSensorData.SetValueOf(const WM: tWhatMeasured2;
                                     const Replacement: real);
      (* kludge to gloss over bad value amid good ones *)
    
      var
        I, Mantissa, NegExp: integer;
        RealMantissa: real;

      begin (* tSensorData.SetValueOf *)
        RealMantissa := abs(Replacement);
        NegExp := 0;

        if RealMantissa <> 0 then
          begin (* adjust *)
            while RealMantissa > 99999.9 do
              begin (* reduce it *)
                RealMantissa := 0.1 * RealMantissa;
                dec(NegExp)
              end   (* reduce it *);
            while RealMantissa < 10000.0 do
              begin (* increase it *)
                RealMantissa := 10.0 * RealMantissa;
                inc(NegExp)
              end   (* increase it *);

          end   (* adjust *);
        Mantissa := Sign(Replacement) * round(RealMantissa);

        for I := 1 to NArgs do
          if WMArg[I] = WM then
            begin (* found it *)
              Strings[3*I+2] := IntToStr(Mantissa);
              Strings[3*I+3] := IntToStr(NegExp);
              break
            end   (* found it *)
      end   (* tSensorData.SetValueOf *);
                                                                 {$WARNINGS OFF}
    procedure tSensorData.StoreMystery(const Sensor_xxx: string; const I: integer);
      (* triple not recognized by the Sensor_xxx routine to which it was sent *)

      var
        NewUID: integer;

      begin (* tSensorData.StoreMysteryy *)
        NewUID := LastUIDOf('MysteryTriples') + 1;

        with dmHC3.qSaveMystery do
          begin (* dmHC3.qSaveMystery *)
            ParamByName('BoardSlot'   ).AsInteger  := integer(Objects[3]);
            ParamByName('Sensor_xxx'  ).AsString   := Sensor_xxx;
            ParamByName('Tag'         ).AsString   := BoardTag;
            ParamByName('UID'         ).AsInteger  := NewUID;
            ParamByName('UTCTimestamp').AsDateTime := UTCStamp;
            ParamByName('Value'       ).AsFloat    := RealArg[I];
            ParamByName('WM'          ).AsInteger  := ord(WMArg[I]);
            ExecSQL
          end   (* dmHC3.qSaveMystery *)
      end   (* tSensorData.StoreMystery *);
                                                                  {$WARNINGS ON}
end.
