﻿unit MagGpsLoc;

{$IFNDEF VER140}
  {$WARN UNSAFE_TYPE off}
  {$WARN UNSAFE_CAST off}
  {$WARN UNSAFE_CODE off}
{$ENDIF}

{ Magenta GPS and Location Component
Updated by Angus Robertson, Magenta Systems Ltd, England, 2nd February 2022
delphi@magsys.co.uk, https://www.magsys.co.uk/delphi/
Copyright Magenta Systems Ltd

The TMagGpsLoc component is designed to process GPS location data from
various sources with an event triggered when movement is detected.

Sources supported are:
1 - Windows Location API, available on most Windows tablets and higher end
laptops with a GPS sensor.
2 - NMEA 0183 sentences commonly generated by GPS receivers, these are
text lines starting with $. NMEA 0183 sentences processed are: GGA, GSA,
GSV, RMC, GLL and VTG, others are ignored.
3 - GT02 GPS Tracker Protocol, used by Concox TR02 vehicle trackers that
combine GPS, GPRS and GSM is a small 12V driven package designed for mounting
in vehicles.
4 - TK102/103 Tracker Protocol, essentially the NMEA RMC sentence, preceded by
date/time and mobile number, followed by useful stuff from other NMEA sentences
like satellite count, mobile IMEI and cell station stuff.
5 - WondeX/TK5000 Tracker Protocol used by VT-10, VT300 and other devices is a
simple format with IMEI, time, co-ordinates, speed and direction.

Further description and samples of these protocols will be found in the
functions that decode them.

Some of these GPS protocols are also generated by Android and iOS mobile
apps for location tracking.

There is a sample application Sensortest that provides serial, UDP and TCP Server
support for NMEA 0183 sentences, and TCP Server for all the other protocols,
showing position and movement on a Google map.

Most testing was with a GlobalSat BU-353-S4 USB GPS Receiver a two inch diameter
device with a roof magnet that presents as a Prolific serial port (a version with
a real serial connector is also available), a DIYmalls VK-162 USB GPS/Glonass Dongle
using an eighth generation U-Blox module, and the Concox TR02 vehicle tracker.
Also tested were a battery operated GlobalSat BT-359 Bluetooth CoPilot GPS device
(but Bluetooth serial ports are not always very reliable) and NMEA 0183 streaming
from Android tablets and phones.  GlobalSat, DIYmalls and TK102/103 devices are
available from Amazon.  Android GPSd Forwarder is available in the Play store and
sends raw NMEA packets to a UDP server (no IMEI).

}

{
24 March 2013 - Angus - baseline
17 June 2014 - Messing
24 Apr 2015  - Added GT02 GPS Tracker Commmunications Protocol for Concox TR02
11 May 2015  - TR02 event for handshakes
24 July 2017 - Added Xenun TK102/TLK103 and WondeX/TK5000 protocols, both generated
                 by Android app MyLiveTracker by Michael Skerwiderski
               RMC packet using date from sentence
10 Oct 2017  - TK102 mobile stuff may be missing from packet
23 Jan 2021  - Added documentation
               Show satellites in view from all constallations, probably only works
                  on recent GPS devices (NMEA updated spec in 2018).  


}

interface

uses
  Windows, SysUtils, Classes, ActiveX, OleServer, ExtCtrls, Registry, Math,
  DateUtils,
  magsubs1, magsubs4,
  MagGpsConv, LocationApiLib_TLB ;

const
    MaxSatellites = 16 ;
    MaxSystems = 6;    // Different constallations
    SystemIdGP = 1;    // GPS USA
    SystemIdGL = 2;    // Glonass Russia   (GN is mixed GPS/Glonass) 
    SystemIdGA = 3;    // Galileo Europe
    SystemIdBD = 4;    // BeiDou System (GB/BD) China
    SystemIdGQ = 5;    // QZSS Japan
    SystemIdGI = 6;    // NavIC India

    SystemNames: array[1..MaxSystems] of String =
        ('GPS', 'Glonass', 'Galileo', 'BeiDou', 'QZSS', 'NavIC');

  // Concox GT02 protocol, mostly terminal client makes requests and server responds
    Gt02ProtoIP = $0A;         // IP request and response
    Gt02ProtoData = $10;       // GPS data
    Gt02ProtoHeartbeat = $1A;  // Heartbeat request and response
    Gt02ProtoAddrResp = $97;   // Address response, English
    Gt02ProtoAddrReq = $1B;    // Address request
    Gt02ProtoInst = $1C;       // Issued instruction request and response

    Gt02StartIP: word = $7878;            // IP request and response
    Gt02StartData: word = $6868;          // GPS data
    Gt02StartHeartbeatReq: word = $6868;  // Heartbeat request
    Gt02StartHeartbeatResp: word = Swap ($5468); // Heartbeat response
    Gt02StartAddrResp: word = $7878;      // Address response, English
    Gt02StartAddrReq: word = $6868;       // Address request
    Gt02StartInstReq: word = $8888;       // Issued instruction request, from server
    Gt02StartInstResp: word = $6868;      // Issued instruction response, from terminal

    Gt02StatGpsFix = $01 ;   // Status masks
    Gt02StatLatDir = $02 ;
    Gt02StatLongDir = $04 ;


type
    TGpsType = (GpsTypeNone, GpsTypeLocApi, GpsTypeNMEA, GpsTypeGT02, GpsTypeTK10X, GpsTypeWondeX) ;

    TGpsSatInfo = record
        Prn: integer ;
        Elevation: integer ;
        Azimuth: integer ;
        SigNoise: integer ;
    end ;

  // Heartbeat request packet from terminal
    TGpsGt02HBPacket = packed record
        Start: Word ;
        PackLen: Byte ;
        Volts: Byte ;
        GsmSig: Byte ;
        TermId: array [0..7] of AnsiChar;
        Serial: Word ;
        Protocol: Byte ;
        State: Byte ;
        SatTot: Byte ;
        SatSNR: array [1..MaxSatellites] of Byte ;
    end ;
    PGpsGt02HBPacket = ^TGpsGt02HBPacket ;

  // Heartbeat response packet to terminal
    TGpsGt02HBResp = packed record
        Start: Word ;
        Protocol: Byte ;
        Stop: array [0..1] of AnsiChar;
    end;

  // GPS Data packet from terminal
    TGpsGt02DataPacket = packed record
        Start: Word ;
        PackLen: Byte ;
        LAC: Word ;
        TermId: array [0..7] of AnsiChar ;
        Serial: Word ;
        Protocol: Byte ;
        DateTime: array [1..6] of Byte ;
        Lat: LongWord ;
        Long: LongWord ;
        Speed: Byte ;
        Course: Word ;
        MNC: Byte ;
        CellId: Word ;
        Status: LongWord ;
     end ;
     PGpsGt02DataPacket = ^TGpsGt02DataPacket;

  // issue command instruction to terminal - ie STATUS,666666
    TGpsGt02CmdSendPacket = packed record
        Start: Word ;
        PackLen: Byte ;
        Protocol: Byte ;
        CmdLen: Byte ;
        ServerFlags: Longword;
        // variable length command
        // Stop CRLF
    end ;
    PGpsGt02CmdSendPacket = ^TGpsGt02CmdSendPacket ;

  // command instruction response from terminal
    TGpsGt02CmdRespPacket = packed record
        Start: Word ;
        PackLen: Byte ;
        Resv: Word ;
        TermId: array [0..7] of AnsiChar ;
        Serial: Word ;
        Protocol: Byte ;
        CmdLen: Byte ;
        ServerFlags: Longword;
        CmdData: array [0..99] of AnsiChar ;
     end ;
     PGpsGt02CmdRespPacket = ^TGpsGt02CmdRespPacket;

    TGpsSendDataEvent = procedure (Sender: TObject; const Data: string) of object;
    TGpsLogEvent = procedure (Sender: TObject; const Data: string) of object;

    TMagGpsLoc = class(TComponent)
    private
    { Private declarations }
        FActive: boolean;
        FGpsType: TGpsType;
        FLocApiOK: boolean;
        FLocation: TLocation;
        FLocationEvents: ILocationEvents;
        FLatitude: double;
        FLastLatitude: double;
        FLongitude: double;
        FLastLongitude: double;
        FAltitude: double;
        FErrorRadius: double;
        FErrorAltitude: double;
        FGpsTalker: string ;
        FGpsQuality: integer ;
//        FGpsSatellView: integer ;
//        FGpsSatellFix: integer ;
        FGpsAltUnits: string ;
        FGpsGeoidalSep: double ;
        FGpsGeoUnits: string ;
        FGpsSpeedKnots: double ;
        FGpsSpeedKm: double ;
        FGpsDegreeTrue: double ;
        FGpsDegreeMag: double ;
        FGpsMagDir: string ;
        FTimeStamp: TDateTime;
        FDistance: integer ;
        FSensorID: string;
        FLocAreaCode: string ;
        FMobNetCode: string ;
        FMobCellId: string ;
        FGsmSigLev: string ;
        FDevVolt: String ;
        FStatus: integer;
        FPacketSerial: Integer;
        FStatusStr: string;
        FRequestPermission: boolean;
        FMobileNum: string;  // July 2017
        FMinInterval: integer;
        FMaxInterval: integer;
        FMinEventTrg: longword;
        FMaxEventTrg: longword;
        FMonitorTimer: TTimer;
        FLastErrorStr: string;
        FLastCmdResponse: string ;
        FExtendedRep: boolean;
        FOnLocChange: TNotifyEvent;
        FOnStatusChange: TNotifyEvent;
        FOnGpsSendData: TGpsSendDataEvent;
        FOnGpsLog: TGpsLogEvent;
        procedure LocationChangedEvent(Sender: TObject;
                        const LocationReport: ILocationReport; const ReportType: TGUID);
        procedure LocationStatusEvent(Sender: TObject;
                        const newStatus: LOCATION_REPORT_STATUS; const ReportType: TGUID);
        procedure KeepLocation(const LocationReport: ILocationReport);
        procedure MonitorTimerTimer(Sender: TObject);

    protected
    { Protected declarations }
        procedure SetActive (Active: Boolean);
        procedure ClearSatInfo (SysId: Integer) ;
        function FindSysId (const Talker: String): Integer ;
        function GetGpsSatellView: integer ;
        function GetGpsSatellFix: integer ;
    public
    { Public declarations }
        GpsSatInfo: array [0..MaxSatellites, 1..MaxSystems] of TGpsSatInfo ;  // 0=totals
        GpsSatFix: array [0..12, 1..MaxSystems] of integer ;                  // 0=totals
        GpsPDOP: double ;
        GpsHDOP: double ;
        GpsVDOP: double ;
        constructor Create(AOwner: TComponent); override;
        destructor Destroy; override;
        procedure StopLocApi ;
        function StartLocApi: boolean ;
        function GetStatus (IgnoreActive: boolean = false): boolean ;
        function GetLocation: boolean ;
        function GetSensorInfo: string ;
        function GetLocComma: string;
        function GetLocText: string;
        function GetDistance (Lat1, Long1, Lat2, Long2: double): integer;
        function SetNMEASentence (const Sentence: string): boolean;
        function SetGT02Packet (const Packet: string): boolean;
        function SetRecvData (const Info: string): boolean;
        function GetNmeaInfo: string ;
        function SendGT02Cmd (const Cmd: string): boolean;
        function SetTK10XPacket (const Packet: string): boolean;    // July 2017
        function SetWondeXPacket (const Packet: string): boolean;    // July 2017
    published
    { Published declarations }
        property GpsType: TGpsType        read FGpsType write FGpsType;
        property LocApiOK: boolean        read FLocApiOK;
        property Active: boolean          read FActive  write SetActive;
        property ExtendedRep: boolean     read FExtendedRep write FExtendedRep;
        property Latitude: double         read FLatitude;
        property Longitude: double        read FLongitude;
        property Altitude: double         read FAltitude;
        property GpsQuality: integer      read FGpsQuality ;
        property GpsSatellView: integer   read GetGpsSatellView ;
        property GpsSatellFix: integer    read GetGpsSatellFix ;
        property GpsAltUnits: string      read FGpsAltUnits ;
        property GpsGeoidalSep: double    read FGpsGeoidalSep ;
        property GpsGeoUnits: string      read FGpsGeoUnits ;
        property GpsSpeedKnots: double    read FGpsSpeedKnots ;
        property GpsSpeedKm: double       read FGpsSpeedKm ;
        property GpsDegreeTrue: double    read FGpsDegreeTrue ;
        property GpsDegreeMag: double     read FGpsDegreeMag;
        property GpsMagDir: string        read FGpsMagDir;
        property ErrorRadius: double      read FErrorRadius;
        property ErrorAltitude: double    read FErrorAltitude;
        property TimeStamp: TDateTime     read FTimeStamp;
        property Distance: integer        read FDistance;
        property SensorID: string         read FSensorID;
        property Status: integer          read FStatus;
        property PacketSerial: Integer    read FPacketSerial;
        property LocAreaCode: string      read FLocAreaCode;
        property MobNetCode: string       read FMobNetCode;
        property MobCellId: string        read FMobCellId;
        property GsmSigLev: string        read FGsmSigLev;
        property DevVolt: String          read FDevVolt;
        property StatusStr: string        read FStatusStr;
        property LastErrorStr: string     read FLastErrorStr;
        property MobileNum: string        read FMobileNum;  // July 2017
        property RequestPermission: boolean read FRequestPermission write FRequestPermission;
        property MinInterval: integer     read FMinInterval write FMinInterval;
        property MaxInterval: integer     read FMaxInterval write FMaxInterval;
        property OnLocationChange: TNotifyEvent read FOnLocChange write FOnLocChange;
        property OnStatusChange: TNotifyEvent read FOnStatusChange write FOnStatusChange;
        property OnGpsSendData: TGpsSendDataEvent read FOnGpsSendData write FOnGpsSendData ;
        property OnGpsLog: TGpsLogEvent   read FOnGpsLog write FOnGpsLog;
     end;

    TLocationChangedEvent = procedure (Sender: TObject; const LocationReport: ILocationReport;
                                                            const ReportType: TGUID) of object;
    TLocationStatusEvent = procedure (Sender: TObject; const newStatus: LOCATION_REPORT_STATUS;
                                                            const ReportType: TGUID) of object;

    TLocationSinkEvent = class(TInterfacedObject, ILocationEvents)
    private
        FLocationChanged: TLocationChangedEvent;
        FLocationStatusEvent: TLocationStatusEvent;
    protected
        function OnLocationChanged(var reportType: TGUID; const pLocationReport: ILocationReport): HResult; stdcall;
        function OnStatusChanged(var reportType: TGUID; newStatus: LOCATION_REPORT_STATUS): HResult; stdcall;
    public
        constructor Create(LocationChanged: TLocationChangedEvent; LocationStatusEvent: TLocationStatusEvent = Nil);
    end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Magenta Systems', [TMagGpsLoc]);
end;

{ TLocationSinkEvent }

constructor TLocationSinkEvent.Create(LocationChanged: TLocationChangedEvent;
                                                        LocationStatusEvent: TLocationStatusEvent);
begin
    inherited Create;
    FLocationChanged := LocationChanged;
    FLocationStatusEvent := LocationStatusEvent;
end;

function TLocationSinkEvent.OnLocationChanged(var reportType: TGUID;
                                    const pLocationReport: ILocationReport): HResult;
begin
    try
        if Assigned(FLocationChanged) then
            FLocationChanged(Self, pLocationReport, reportType);
        Result := S_OK;
    except
        Result := E_UNEXPECTED;
    end;
end;

function TLocationSinkEvent.OnStatusChanged(var reportType: TGUID;
                                        newStatus: LOCATION_REPORT_STATUS): HResult;
begin
    try
        if Assigned(FLocationStatusEvent) then
            FLocationStatusEvent(Self, newStatus, reportType);
        Result := S_OK;
    except
        Result := E_UNEXPECTED;
    end;
end;

function ReportStatus (const Status: TOleEnum): string ;
begin
    case status of
        REPORT_NOT_SUPPORTED: Result := 'Report Not Supported' ;
        REPORT_ERROR: Result := 'Report Error' ;
        REPORT_ACCESS_DENIED: Result := 'Report Access Denied' ;
        REPORT_INITIALIZING: Result := 'Report Initialising' ;
        REPORT_RUNNING: Result := 'Report Running' ;
    else
        Result := 'Unknown Result' ;
    end;
end;

function ReportOleResult (const myresult: hresult): string ;
var
    win32err: integer ;
begin
    case myresult of
        S_OK: Result := 'No Error' ;
        E_ACCESSDENIED: Result := 'No Permission for Location Provider' ;
        E_FAIL: Result := 'Sensor Disconnected or Failed' ;
        E_INVALIDARG: Result := 'Report Values Out of Bounds' ;
    else
        begin
            win32err := HResultCode (myresult) ;
            case win32err of
                ERROR_NOT_SUPPORTED: Result := 'Invalid Report Request' ;
                ERROR_NO_DATA: Result := 'No Data Available' ;
                ERROR_CANCELLED: Result := 'Permission Refused for Location Provider' ;
                ERROR_ALREADY_REGISTERED: Result := 'Event Already Registered' ;
            else
                Result := 'Unknown Result (' + IntToHex (myresult, 8) + ')' ;
            end;
        end;
    end;
end;

function GetSensorType (SensType: TGUID): string ;
begin
 if IsEqualGUID (SensType, SENSOR_TYPE_LOCATION_GPS) then
    Result := 'GPS'
  else if IsEqualGUID (SensType, SENSOR_TYPE_LOCATION_STATIC) then
    Result := 'Static'
  else if IsEqualGUID (SensType, SENSOR_TYPE_LOCATION_LOOKUP) then
    Result := 'Lookup'
  else if IsEqualGUID (SensType, SENSOR_TYPE_LOCATION_TRIANGULATION) then
    Result := 'Triangulation'
  else if IsEqualGUID (SensType, SENSOR_TYPE_LOCATION_BROADCAST) then
    Result := 'Broadcast'
  else if IsEqualGUID (SensType, SENSOR_TYPE_LOCATION_DEAD_RECKONING) then
    Result := 'DeadReckoning'
  else if IsEqualGUID (SensType, SENSOR_TYPE_LOCATION_OTHER) then
    Result := 'Other'
  else
    Result := GUIDToString(SensType) ;
end;

{ TMagGpsLoc }

constructor TMagGpsLoc.Create(AOwner: TComponent);
begin
    FGpsType := GpsTypeNMEA ;
    FLocApiOK := false ;
    FMinInterval := 2;   // minimum interval between reports, may be ignored so sent more often
    FMaxInterval := 30;  // maximum interval, repeats last report if nothing new
    FRequestPermission := false;
    FMonitorTimer := TTimer.Create(Self);
    FMonitorTimer.OnTimer := MonitorTimerTimer;
    FMonitorTimer.Interval := 1 * TicksPerSecond ;
    FMonitorTimer.Enabled := false ;

  // test if location classes registered, does not mean it has any sensors
    with TRegistry.Create do
    try
        RootKey := HKEY_CLASSES_ROOT ;
        Access := KEY_READ ;
        FLocApiOK := OpenKey ('CLSID\{E5B8E079-EE6D-4E33-A438-C87F2E959254}\InprocServer32', false) ;  // CLASS_Location
        CloseKey ;
        if FLocApiOK then
        begin
            FLocApiOK := OpenKey ('Interface\{7FED806D-0EF8-4F07-80AC-36A0BEAE3134}', false) ; // IID_ILatLongReport
            CloseKey ;
        end;
    finally
        Free ;
    end ;
    inherited Create(AOwner);
end;

destructor TMagGpsLoc.Destroy;
begin
    SetActive (false);
    inherited;
end;

procedure TMagGpsLoc.ClearSatInfo(SysId: Integer) ;
var
    I, J: integer ;
begin
    for J := 1 to MaxSystems do
    begin
        if (J <> SysId) and (SysId > 0) then continue;
        for I := 0 to MaxSatellites do  // 0 is total
        begin
            GpsSatInfo [I,J].Prn := 0 ;
            GpsSatInfo [I,J].Elevation := 0 ;
            GpsSatInfo [I,J].Azimuth := 0 ;
            GpsSatInfo [I,J].SigNoise := 0 ;
       end;
    end;
end ;

function TMagGpsLoc.GetGpsSatellView: integer ;
var
    J: Integer;
begin
    Result := 0;
    for J := 1 to MaxSystems do
        Result := Result + GpsSatInfo [0,J].Prn;
end;

function TMagGpsLoc.GetGpsSatellFix: integer ;
var
    J: Integer;
begin
    Result := 0;
    for J := 1 to MaxSystems do
        Result := Result + GpsSatFix [0,J];
end;

function TMagGpsLoc.FindSysId (const Talker: String): Integer ;
begin
    Result := 0;
    if Talker = 'GP' then result := SystemIdGP
    else if Talker = 'GL' then result := SystemIdGL
    else if Talker = 'GA' then result := SystemIdGA
    else if Talker = 'QZ' then result := SystemIdGQ
    else if Talker = 'BD' then result := SystemIdBD
    else if Talker = 'GB' then result := SystemIdBD
    else if Talker = 'GI' then result := SystemIdGI
    else if Talker = 'GQ' then result := SystemIdGQ;
  // ignore GN means mixed GPS/Glonass
end;

// start Windows Location API COM object

function TMagGpsLoc.StartLocApi: boolean ;
var
    myresult: hresult;
    hParent: _RemotableHandle ;
    reportGuid: TGUID ;
    interval: DWORD;
    flag: boolean ;
    loop: integer ;
begin
    result := false ;
    if NOT FLocApiOK then
    begin
        FLastErrorStr := 'Location Error - Not Supported' ;
        exit ;
    end ;
    try
        FLocation := TLocation.Create (Nil);
        FLocation.Connect ;

     // request permissions - optional!!!!
        if FRequestPermission then
        begin
            reportGuid := IID_ILatLongReport ;
            myresult := FLocation.RequestPermissions (hParent, reportGuid, 1, 0) ;
            if myresult <> S_OK then
            begin
                FLastErrorStr := 'Request Permission Error - ' + ReportOleResult (myresult) ;
                exit ;
            end ;
        end;

    // see if main report is available, wait 500ms since may not initialise immediately
        for loop := 1 to 5 do
        begin
            flag := GetStatus (true) ;
            if NOT flag then exit;
            if (FStatus <> REPORT_NOT_SUPPORTED) then break;
            sleep (100) ;
        end;
        if (FStatus = REPORT_NOT_SUPPORTED) or (FStatus = REPORT_ERROR) or
                                               (FStatus = REPORT_ACCESS_DENIED) then
        begin
           FLastErrorStr := 'LatLongReport Error - ' + ReportStatus (FStatus) ;
           exit ;
        end;

    // warning - report might not be ready for a few moments

     // set events
        if Assigned (FOnLocChange) then
        begin
            FLocationEvents := TLocationSinkEvent.Create(LocationChangedEvent, LocationStatusEvent);
         // warning, report interval may be ingored
            myresult := FLocation.RegisterForReport (FLocationEvents, IID_ILatLongReport,
                                                            LongWord (FMinInterval) * TicksPerSecond) ;
            if myresult <> S_OK then
            begin
                FLastErrorStr := 'Failed to Start Events - ' + ReportOleResult (myresult) ;
                exit ;
            end ;
            myresult := FLocation.GetReportInterval (IID_ILatLongReport, interval) ;
            if myresult <> S_OK then
            begin
                FLastErrorStr := 'Failed to Get Interval - ' + ReportOleResult (myresult) ;
                exit ;
            end ;
            myresult := FLocation.SetReportInterval (IID_ILatLongReport,
                                                        LongWord (FMinInterval) * TicksPerSecond) ;
            if myresult <> S_OK then
            begin
                FLastErrorStr := 'Failed to Set Interval - ' + ReportOleResult (myresult) ;
                exit ;
            end ;
        end;
        FGpsAltUnits := 'm' ;
        FActive := true;
        result := true ;
     except
        FLastErrorStr := 'Location Error - ' + GetExceptMess (ExceptObject) ;
        if Pos ('Class not', FLastErrorStr) > 1 then
            FLastErrorStr := 'Location Error - Not Supported' ;
        FActive := false;
        FMonitorTimer.Enabled := false ;
     end;
end;

procedure TMagGpsLoc.StopLocApi ;
begin
    if NOT Assigned (FLocation) then exit ;
    try
        if Assigned (FLocationEvents) then
        begin
            FLocation.UnregisterForReport (IID_ILatLongReport);
        end;
        FLocation.Disconnect;
    except
    end;
    FLocationEvents := Nil ;
    FLocation.Destroy;
    FLocation := Nil ;
end;

procedure TMagGpsLoc.SetActive (Active: Boolean);
var
    J: Integer;
begin
    if Active = FActive then exit;
    FLastErrorStr := '';
    FActive := false;
    if Active then
    begin
        FLastErrorStr := 'Unknown SetActiveError';
        FLatitude := 0;
        FLongitude := 0;
        FLastLatitude := 0;
        FLastLongitude := 0;
        FAltitude := 0;
        FErrorRadius := 0;
        FErrorAltitude := 0;
        FTimeStamp := 0;
        FSensorID := '';
        FStatus := 0;
        FStatusStr := '';
        FGpsQuality := 0 ;
        FGpsSpeedKnots := 0 ;
        FGpsSpeedKm := 0 ;
        FGpsDegreeTrue := 0 ;
        FGpsDegreeMag := 0 ;
        FGpsMagDir := '' ;
        FGsmSigLev := '' ;
        FDevVolt := '' ;
        FLocAreaCode := '' ;
        FMobNetCode := '' ;
        FMobCellId := '' ;
        FMobileNum := '';
        ClearSatInfo (0) ;
        for J := 1 to MaxSystems do GpsSatFix [0,J] := 0;
        FMinEventTrg := TriggerImmediate ;
        if FGpsType = GpsTypeLocApi then
        begin
            StartLocApi ;
        end
        else if FGpsType = GpsTypeNMEA then
            FActive := true
        else if FGpsType = GpsTypeGT02 then
            FActive := true
        else if GpsType = GpsTypeTK10X then
            FActive := true
        else if GpsType = GpsTypeWondeX then
            FActive := true
        else
            FLastErrorStr := 'Unsupported GPS Type';
    end;
    if FActive then
    begin
        if FMaxInterval > 0 then
        begin
            FMaxEventTrg := TriggerImmediate ;
            FMonitorTimer.Enabled := true ;
        end;
        exit;
    end;

    // close down
    FMonitorTimer.Enabled := false ;
    if FGpsType = GpsTypeLocApi then StopLocApi ;
end;

function TMagGpsLoc.GetStatus (IgnoreActive: boolean = false): boolean ;
var
    mystatus: TOleEnum ;
    myresult: hresult;
begin
    result := false ;
    if (NOT FActive) and (NOT IgnoreActive) then
    begin
        FStatus := REPORT_NOT_SUPPORTED ;
        FStatusStr := 'Location Inactive' ;
        FLastErrorStr := FStatusStr ;
        exit ;
    end;
    if NOT Assigned (FLocation) then exit ;
    myresult := FLocation.GetReportStatus (IID_ILatLongReport, mystatus) ;
    if myresult <> S_OK then
    begin
        FLastErrorStr := 'Failed to Get LatLong Report Status - ' + ReportOleResult (myresult) ;
        exit ;
    end
    else
    begin
        result := true ;
        FStatus := mystatus ;
        FStatusStr := ReportStatus (mystatus) ;
    end;
end;

function TMagGpsLoc.GetLocation: boolean ;
var
    myresult: hresult;
    LocationReport: ILocationReport ;
begin
    result := false ;
    if NOT FActive then exit ;
    if NOT GetStatus then exit ;
    if (FStatus = REPORT_NOT_SUPPORTED) or (FStatus = REPORT_ERROR) or
                                           (FStatus = REPORT_ACCESS_DENIED) then
    begin
       FLastErrorStr := 'LatLongReport Error - ' + ReportStatus (FStatus) ;
       exit ;
    end;

// get location report
    myresult := FLocation.GetReport (IID_ILatLongReport, LocationReport) ;
    if myresult <> S_OK then
    begin
        FLastErrorStr := 'Failed to Get LatLong Report - ' + ReportOleResult (myresult) ;
    end
    else if Assigned (LocationReport) then
    begin
        KeepLocation (LocationReport);
        result := true ;
    end ;
end;

function TMagGpsLoc.GetSensorInfo: string ;
var
    myresult: hresult;
    LocationReport: ILocationReport ;
    reportGuid: TGUID ;
    Value: TPropVariant;
begin
    result := '' ;
    if NOT FActive then exit ;

// get location report
    myresult := FLocation.GetReport (IID_ILatLongReport, LocationReport) ;
    if myresult <> S_OK then
    begin
        result := 'Failed to Get LatLong Report - ' + ReportOleResult (myresult) ;
    end
    else if Assigned (LocationReport) then
    begin
        KeepLocation (LocationReport);
        LocationReport.GetSensorID (reportGuid) ;
     // GPS sensors vary on exactly what values they support
        result := result + 'SensorId = ' +  GUIDToString (reportGuid) + CRLF_ ;
     {   if Succeeded (LocationReport.GetValue (SENSOR_DATA_TYPE_LATITUDE_DEGREES, Value)) then
            result := result + 'Latitude = ' + FloatToStr (Value.dblVal) + CRLF_ ;
        if Succeeded (LocationReport.GetValue (SENSOR_DATA_TYPE_LONGITUDE_DEGREES, Value)) then
            result := result + 'Longitude = ' + FloatToStr (Value.dblVal) + CRLF_ ;    }
        if Succeeded (LocationReport.GetValue (SENSOR_DATA_TYPE_SATELLITES_IN_VIEW, Value)) then
            result := result + 'Satellites in View = ' +  IntToStr (Value.lVal) + CRLF_ ;
        if Succeeded (LocationReport.GetValue (SENSOR_DATA_TYPE_SATELLITES_USED_COUNT, Value)) then
            result := result + 'Satellites Used = ' +  IntToStr (Value.lVal) + CRLF_ ;
        if FErrorRadius <> 0 then
            result := result + 'Error Radius = ' +  FloatToStr (FErrorRadius) + CRLF_ ;
        if Succeeded (LocationReport.GetValue (SENSOR_DATA_TYPE_SPEED_KNOTS, Value)) then
            result := result + 'Speed (Knots) = ' +  FloatToStr (Value.dblVal) + CRLF_ ;
        if Succeeded (LocationReport.GetValue (SENSOR_DATA_TYPE_TRUE_HEADING_DEGREES, Value)) then
            result := result + 'True Heading = ' +  FloatToStr (Value.dblVal) + CRLF_ ;
        if Succeeded (LocationReport.GetValue (SENSOR_DATA_TYPE_CITY, Value)) then
            result := result + 'City = ' +  Value.bstrVal + CRLF_ ;
        if Succeeded (LocationReport.GetValue (SENSOR_DATA_TYPE_NMEA_SENTENCE, Value)) then
            result := result + 'NMEA Sentence = ' +  Value.bstrVal + CRLF_ ;
        if Succeeded (LocationReport.GetValue (SENSOR_PROPERTY_TYPE, Value)) then
            result := result + 'Sensor Type = ' +  GetSensorType (Value.puuid^) + CRLF_ ;
        if Succeeded (LocationReport.GetValue (SENSOR_PROPERTY_MANUFACTURER, Value)) then
            result := result + 'Manufacturer = ' +  Value.bstrVal + CRLF_ ;
        if Succeeded (LocationReport.GetValue (SENSOR_PROPERTY_FRIENDLY_NAME, Value)) then
            result := result + 'Sensor Name = ' +  Value.bstrVal + CRLF_ ;
    end
    else
        result := 'Failed to get Location Report';
end;

procedure TMagGpsLoc.KeepLocation(const LocationReport: ILocationReport);
var
    MyReport: ILatLongReport ;
    myresult: hresult;
    Timestamp: TSystemTime ;
    reportGuid: TGUID ;
begin
    if NOT Assigned (LocationReport) then exit ;
    myresult := LocationReport.QueryInterface (ILatLongReport, MyReport) ;
    if (myresult = S_OK) and Assigned (MyReport) then
    begin
        if NOT Succeeded (MyReport.GetLatitude (FLatitude)) then exit ;
        MyReport.GetLongitude (FLongitude) ;
        MyReport.GetErrorRadius (FErrorRadius) ;
        if Succeeded (MyReport.GetAltitude (FAltitude)) then
        begin
            MyReport.GetAltitudeError (FErrorAltitude) ;
        end;
        MyReport.GetTimestamp (Timestamp) ;
        FTimeStamp := SystemTimeToDateTime (Windows._SYSTEMTIME (timestamp)) ;
        if FSensorID = '' then
        begin
             LocationReport.GetSensorID (reportGuid) ;
             FSensorId := GUIDToString (reportGuid) ;
        end;
    end ;
end;

function TMagGpsLoc.GetLocComma: string;
begin
    result := '' ;
//    if (NOT FActive) then exit ;
    if (FLatitude = 0) and (FLongitude = 0) then exit;  // Oct 2017 return old info
    result := Double2EStr (FLatitude, 5) +
                ',' + Double2EStr (FLongitude, 5) +
                   ',' + IntToStr (Trunc (FAltitude)) + FGpsAltUnits;
    if FExtendedRep then result := result + ',' + IntToStr (Trunc (FGpsSpeedKnots)) +
                             ',' + IntToStr (Trunc (FGpsDegreeTrue)) + ',' + FSensorID;
    result := result + ',' + DT2ISODT (FTimeStamp) ;
end;

function TMagGpsLoc.GetLocText: string;
begin
    result := '' ;
//    if (NOT FActive) then exit ;
    if (FLatitude = 0) and (FLongitude = 0) then exit;  // Oct 2017 return old info
    result :=  'Latitude ' + Double2EStr (FLatitude, 5) +
                ', Longitude ' + Double2EStr (FLongitude, 5) +
                   ', Altitude ' + IntToStr (Trunc (FAltitude)) + FGpsAltUnits;
    if FExtendedRep then result := result + ', Speed ' + IntToStr (Trunc (FGpsSpeedKnots)) +
                             ', Course ' + IntToStr (Trunc (FGpsDegreeTrue)) +
                                ', ID ' + FSensorID + ', PackSer ' + IntToStr(FPacketSerial) ;
    result := result + ', Time ' + DateTimetoStr (FTimeStamp) ;
end;

// timer is used to get location periodically if no events are being triggered

procedure TMagGpsLoc.MonitorTimerTimer(Sender: TObject);
begin
    if (NOT FActive) then exit ;
    if NOT TestTrgTick (FMaxEventTrg) then exit ;
    FMaxEventTrg := AddTrgSecs (GetTickCountX, FMaxInterval) ;
    if FTimeStamp = 0 then exit ;
    if GpsType = GpsTypeLocApi then
    begin
        if NOT GetLocation then exit ;
    end
    else if GpsType >= GpsTypeGT02 then
        exit ;  // event every 3 mins due to handshake
    if Assigned (FOnLocChange) then FOnLocChange (Self) ;
end;

// event called rapidly, perhaps for each new NMEA sentence, even if not changed

procedure TMagGpsLoc.LocationChangedEvent(Sender: TObject;
                        const LocationReport: ILocationReport; const ReportType: TGUID);
begin
    if NOT Assigned (LocationReport) then exit ;
    if NOT IsEqualGUID (ReportType, IID_ILatLongReport) then exit ;
    if FMinInterval > 0 then
    begin
        if NOT TestTrgTick (FMinEventTrg) then exit ;  // don't want too many events
        FMinEventTrg := AddTrgSecs (GetTickCountX, FMinInterval) ;  // set next triggger
        FMaxEventTrg := AddTrgSecs (GetTickCountX, FMaxInterval) ;
    end;
    KeepLocation (LocationReport) ;
    if FLastLatitude <> 0 then
        FDistance := GetDistance (FLatitude, FLongitude, FLastLatitude, FLastLongitude)
    else
        FDistance := 0 ;
    FLastLatitude := FLatitude ;
    FLastLongitude := FLongitude ;
    if Assigned (FOnLocChange) then FOnLocChange (Self) ;
end;

procedure TMagGpsLoc.LocationStatusEvent(Sender: TObject;
                        const newStatus: LOCATION_REPORT_STATUS; const ReportType: TGUID);
begin
    if NOT IsEqualGUID (ReportType, IID_ILatLongReport) then exit ;
    FStatus := newStatus ;
    FStatusStr := ReportStatus (newStatus) ;
    if Assigned (FOnStatusChange) then FOnStatusChange (Self) ;
end;

// http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates
// http://www.movable-type.co.uk/scripts/latlong.html
// dist = arccos(sin(lat1) * sin(lat2) + cos(lat1) *· cos(lat2) *· cos(lon1 - lon2)) *· 6371009

function TMagGpsLoc.GetDistance (Lat1, Long1, Lat2, Long2: double): integer;
begin
    result := 0 ;
    if (Lat1 = Lat2) and (Long1 = Long2) then Exit ;
    try
        result := Trunc (Arccos (Sin (DegToRad (Lat1)) * Sin (DegToRad (Lat2)) +
                      Cos (DegToRad (Lat1)) * Cos (DegToRad (Lat2)) *
                              Cos (DegToRad (Long1) - DegToRad (Long2))) * 6371009) ;
 // magic number is average earth diameter ellipsoid in metres
    except
        result := 0 ;
    end;
end;

// converts Degree Minutes to Decimal Degrees, making negative for East or South
function DM2DD (const DM, Sign: string; var DD: double): boolean ;
var
    sep: integer ;
begin
    result := false ;
    try
        if (DM = '') or (Sign = '') then exit ;
        if NOT (IsDigitsDec (DM, true)) then exit ;
        sep := Pos ('.', DM) ;
        if (sep = 5) then   // 5122.987473 - latitude
        begin
            DD := Str2Ext (Copy (DM, 1, 2)) + (Str2Ext (Copy (DM, 3, 9)) / 60) ;
            if Sign = 'S' then DD := 0 - DD ;
            result := true ;
        end
        else if (sep = 6) then // 00005.157330 longitude
        begin
            DD := Str2Ext (Copy (DM, 1, 3)) + (Str2Ext (Copy (DM, 4, 9)) / 60) ;
            if Sign = 'W' then DD := 0 - DD ;
            result := true ;
        end;
    except
    end;
end;

// convert ASCII time (HHMMSS-NN) and date (DDMMYY) with optional millisecs to TDateTime as today if no date
function Time2DT (STime, SDate: string; var DT: TDateTime): boolean ;
var
    sep: integer ;
    yy, mm, dd: Word;
begin
    result := false ;
    if (Length (STime) < 6) or (NOT (IsDigitsDec (STime, true))) then exit ;
    if (Length (SDate) <> 6) or (NOT (IsDigitsDec (SDate, false))) then
        DT := Date
    else
    begin
        dd := Str2Word (copy (SDate, 1, 2)) ;
        mm := Str2Word (copy (SDate, 3, 2)) ;
        yy := Str2Word (copy (SDate, 5, 2)) + 2000;
        if NOT TryEncodeDate (yy, mm, dd, DT) then DT := Date;
    end;
    sep := Pos ('.', STime) ;  // 154734.00
    if sep = 7 then STime [7] := '-' ;
    DT := DT + TimeOf(Packed2Time (STime));
end ;

// convert ASCII floating number to double
function Num2Double (Num: string; var Float: Double): boolean ;
begin
    result := false ;
    if num = '' then exit ;
    if NOT (IsDigitsDec (num, true)) then exit ;
    Float := Str2Ext (Num) ;
    result := true ;
end;

function TMagGpsLoc.SetNMEASentence (const Sentence: string): boolean;
var
    Fields: TStringList;
    TypeMsg: string;
    I, J, fnr, satnr, systemid: integer ;
    locflag: boolean ;

  // locate checksum at end of NMEA-0183 sentence and check line not corrupted
    function NMEACheckSumOK (const S1: string): boolean;
    var
        I, J: integer;
        Checksum: integer;
        ChecksumVal: string;
    begin
        Checksum := 0;
        result := false ;
        J := Pos ('*', S1) ;
        if J < 5 then exit ;
        for I := 2 to (J - 1) do   //Checksum done on everthing between $ and * .
            Checksum := Checksum XOR Ord (S1 [I]) ;
        ChecksumVal := IntToHex (Checksum, 2) ;
        result := (ChecksumVal = Copy (S1, J + 1, 2)) ;
    end;

begin
    result := false ;
    locflag := false ;
    FLastErrorStr := '' ;
    if NOT FActive then
    begin
        FLastErrorStr := 'Can not save data, inactive' ;
        exit ;
    end;
    if Length (Sentence) < 10 then exit ;
    if Sentence [1] <> '$' then exit ;  // all sentences start with $
    if Sentence [2] = 'P' then exit ;   // ignore proprietary messages
    TypeMsg := Copy (Sentence, 4, 3);   // Get the message type
    if NOT NMEACheckSumOK (Sentence) then
    begin
        FLastErrorStr := 'NMEA-0183 sentence check sum failure - ' + Sentence;
        exit ;
    end;
    Fields := TStringList.Create;
    Fields.Delimiter := ',' ;
    try

// sentence descriptions from: https://gpsd.gitlab.io/gpsd/NMEA.html

// system Id, constellations or talker: GP=GPS, GL=Glonass, GN=Mixed GPS/Glonass, QZ=QZSS, BD=BeiDou, GA=Galileo, GQ=QZSS, GI=IRNSS
// note lots of other listeners for compasses, transducers, gyros, etc.

// NMEA GGA Sentence format - Global Positioning System Fix Data
//                                                      11
//        1         2       3 4        5 6 7  8   9  10 |  12 13  14   15
//        |         |       | |        | | |  |   |   | |   | |   |    |
// $--GGA,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh<CR><LF>
// $GPGGA,154734.00,5122.987473,N,00005.157330,W,1,08,1.2,68.2,M,47.1,M,,*4F
// $GPGGA,154738.00,5122.989193,N,00005.156894,W,1,08,0.8,67.6,M,47.1,M,,*42
// $GPGGA,184459.00,,,,,0,00,300.0,,M,,M,,*60
// $GNGGA,182237.00,5122.988318,N,00005.156333,W,1,12,0.6,56.4,M,47.0,M,,*5F
// 0) message type tag, $, constellation XX, packet type XXX
// 1) Universal Time Coordinated (UTC)  - time only hhmmss-zz
// 2) Latitude  - 5122.987473 = 51 degrees, 22.9874 minutes - not decimal degrees
// 3) N or S (North or South)
// 4) Longitude - 00005.157330 = 00 degrees, 5.157330 minutes
// 5) E or W (East or West)
// 6) GPS Quality Indicator,
//    0 - fix not available,
//    1 - GPS fix,
//    2 - Differential GPS fix
// 7) Number of satellites in view, 00 - 12
// 8) Horizontal Dilution of precision (metres)
// 9) Antenna Altitude above/below mean-sea-level (geoid)
//10) Units of antenna altitude, meters
//11) Geoidal separation, the difference between the WGS-84 earth ellipsoid and mean-sea-level (geoid),
//    "-" means mean-sea-level below ellipsoid
//12) Units of geoidal separation, meters
//13) Age of differential GPS data, time in seconds since last SC104
//    type 1 or 9 update, null field when DGPS is not used
//14) Differential reference station ID, 0000-1023
//15) Checksum

        if TypeMsg = 'GGA' then   // Global Positioning System Fix Data
        begin
            Fields.CommaText := Sentence ;  // Split the message into different parts.
            if Fields.Count < 14 then exit ;
            FGpsTalker := Copy (Sentence, 2, 2) ;  // keep fix talker constellation, GP=GPS, GL=Glonass, GN=Mixed GPS/Glonass, QZ=Japanese
            FGpsQuality := AscToInt (Fields [6]) ;
        //    FGpsSatellFix := AscToInt (Fields [7]) ;
            systemid := AscToInt (FGpsTalker);
            if systemid = 0 then systemid := 1;
            GpsSatFix [0, systemid] := AscToInt (Fields [7]) ;  // totals
            if (FGpsQuality = 1) or (FGpsQuality = 2) then  // don't change last location unless got a new one
            begin
                if DM2DD (Fields [2], Fields [3], FLatitude) and
                    DM2DD (Fields [4], Fields [5], FLongitude) then
                begin
                    FAltitude := AscToInt (Fields [9]) ;
                    Time2DT (Fields [1], '', FTimeStamp) ;
                    Num2Double (Fields [8], FErrorRadius) ;
                    FGpsAltUnits := Lowercase (Fields [10]) ;
                    Num2Double (Fields [11], FGpsGeoidalSep) ;
                    FGpsGeoUnits := Lowercase (Fields [12]) ;
                    locflag := true ;
                end ;
            end ;
            result := true ;
        end

// NMEA GSA Sentence format - GPS DOP and active satellites
// Implementation note: Number satalites might variate in nature so structure may need to be adjusted.
//	      1 2 3                        14 15  16  17  18
//      	| | |                         |  |   |   |   |
// $--GSA,a,a,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x.x,x.x,x.x*hh<CR><LF>
// $GNGSA,A,3,08,15,17,18,22,24,26,28,,,,,2.5,1.5,2.1*27
// $QZGSA,A,3,,,,,,,,,,,,,2.5,1.5,2.1*2E
// $GNGSA,A,3,04,05,09,18,25,26,29,31,,,,,0.9,0.6,0.6,1*33  // NMEA 4.11, id=1=GPS
// $GNGSA,A,3,71,78,79,82,88,,,,,,,,0.9,0.6,0.6,2*36        // NMEA 4.11, id=2=Glonass
// $GNGSA,A,3,01,05,24,26,31,,,,,,,,0.9,0.6,0.6,3*3E        // NMEA 4.11, id=3=Galileo
// 0) message type
// 1) Selection mode A=auto fix, M=manual fix
// 2) Mode 1=no fix, 2=2d fix, 3=3d fix
// 3) to 14) IDs of 1st to 12th satellite used for fix
//15) PDOP in meters, dilution of precision
//16) HDOP in meters, horiz dilution of precision
//17) VDOP in meters, vert dilution of precision
//18) System ID (NMEA 4.11 Sept 2018) optional, which constallation
//19) checksum
        else if TypeMsg = 'GSA' then  // GPS DOP and active satellites
        begin
            Fields.CommaText := Sentence ;  // Split the message into different parts.
            if Fields.Count < 19 then exit ;
            systemid := AscToInt (Fields [18]);  // only available in NMEA 4.11 Sept 2018
            if systemid = 0 then systemid := 1;
            if (Fields [1] = 'A') and ((Fields [2] = '2') or (Fields [2] = '3')) then
            begin
                Num2Double (Fields [15], GpsPDOP) ;
                Num2Double (Fields [16], GpsHDOP) ;
                Num2Double (Fields [17], GpsVDOP) ;
                fnr := 3 ;
//                FGpsSatellFix := 0 ;
                GpsSatFix [0, systemid] := 0; 
                for I := 1 to 12 do
                begin
                    GpsSatFix [I, systemid] := AscToInt (Fields [fnr]) ;
                    if GpsSatFix [I, systemid] > 0 then inc (GpsSatFix [0, systemid]);  // totals
//                    if GpsSatFix [I] > 0 then inc (FGpsSatellFix) ;
                    inc (fnr) ;
                end ;
            end
            else
            begin
                for I := 0 to 12 do GpsSatFix [I, systemid] := 0 ;
            end;
            result := true ;
        end

// NMEA GSV Sentence format - Satellites in view, may be multiple constallations
//	1 2 3 4 5 6 7     n
//	| | | | | | |     |
// $--GSV,x,x,x,x,x,x,x,...*hh<CR><LF>
// $GPGSV,3,1,12,04,13,309,21,05,14,070,16,09,04,341,18,16,21,291,21,1*65    GPS constallation 1 of 3
// $GPGSV,3,2,12,18,44,156,24,20,13,042,16,25,25,113,16,26,59,289,35,1*63    GPS constallation 2 of 3
// $GPGSV,3,3,12,29,53,061,25,31,50,210,34,02,04,033,,27,00,000,,1*6A        GPS constallation 3 of 3
// $GLGSV,2,1,07,78,49,142,22,82,11,212,19,79,72,310,28,88,58,045,15,1*71    Glonass constallation 1 of 2
// $GLGSV,2,2,07,81,61,205,24,71,11,357,13,87,15,036,,1*4B                   Glonass constallation 2 of 2
// $GAGSV,2,1,07,01,29,122,27,05,08,286,20,24,40,298,19,26,39,053,22,7*7B    Galileo constallation 1 of 2
// $GAGSV,2,2,07,31,85,165,23,03,07,336,,12,23,192,,7*42                     Galileo constallation 2 of 2
// 0) message type, sysuid/talker/constallation $xx
// 1) total number of messages for this constallation, usually 4 satellites per message
// 2) message number
// 3) satellites in view
// 4) satx - satellite number
// 5) satx - elevation in degrees
// 6) satx - azimuth in degrees to true
// 7) satx - SNR in dB
// 8) optional, repeat 4) to 7) four times
// last) checksum
// note no systemid so need to convert talker letters

        else if TypeMsg = 'GSV' then  // GPS Satellites in view
        begin
            Fields.CommaText := Sentence ;  // Split the message into different parts.
            if Fields.Count < 8 then exit ;
            systemid := FindSysId (Copy (Fields[0], 2, 2));  // Jan 2022
            if systemid = 0 then systemid := 1;   // not available, set GPS
            J := AscToInt (Fields [2]) ;  // message number, 1 to 4 (max 16 satellites}
            if J = 1 then ClearSatInfo(systemid) ;  // first clear everything
//            FGpsSatellView :=  AscToInt (Fields [3]) ;
//            if FGpsSatellView = 0 then exit ;
            GpsSatInfo [0, systemid].Prn :=  AscToInt (Fields [3]) ;   // total satellites in view
            if GpsSatInfo [0, systemid].Prn = 0 then exit;  // should not happen
            fnr := 4 ;
            satnr := ((J - 1) * 4) + 1 ;
            for I := 0 to 3 do
            begin
                if ((fnr + 4) >= Fields.Count) then break ;
                if satnr >= MaxSatellites then break ;
                with GpsSatInfo [satnr, systemid] do
                begin
                    if Fields [fnr] <> '' then
                    begin
                        Prn := AscToInt (Fields [fnr]) ;
                        Elevation := AscToInt (Fields [fnr + 1]) ;
                        Azimuth := AscToInt (Fields [fnr + 2]) ;
                        SigNoise := AscToInt (Fields [fnr + 3]) ;
                    end;
                end;
                inc (satnr) ;
                fnr := fnr + 4 ;
            end;
            result := true ;
        end

// NMEA RMC Sentence format - Recommended Minimum Navigation Information
//                                                            12
//        1         2 3       4 5        6 7   8   9    10  11|
//        |         | |       | |        | |   |   |    |   | |
// $--RMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,xxxx,x.x,a*hh<CR><LF>
// $GPRMC,135929.00,A,5122.981183,N,00005.154369,W,000.0,323.0,120314,,,A*49
// $GPRMC,135948.00,A,5122.981297,N,00005.154325,W,000.0,323.0,120314,,,A*40
// $GNRMC,182240.00,A,5122.988312,N,00005.156334,W,0.0,,190122,3.1,W,A,V*77
// 0) message type
// 1) UTC Time
// 2) Status, A = Active, V = Navigation receiver warning
// 3) Latitude
// 4) N or S
// 5) Longitude
// 6) E or W
// 7) Speed over ground, knots
// 8) Track made good, degrees true
// 9) Date, ddmmyy
//10) Magnetic Variation, degrees
//11) E or W
//12) FAA mode indicator (NMEA 2.3 and later)
//13) Nav Status (NMEA 4.1 and later) A=autonomous, D=differential, E=Estimated, M=Manual input mode N=not valid, S=Simulator, V = Valid
//14) Checksum

        else if TypeMsg = 'RMC' then  // Recommended Minimum Navigation Information
        begin
            FGpsTalker := Copy (Sentence, 2, 2) ;  // keep fix talker constellation, GP=GPS, GL=Glonass, GN=Mixed GPS/Glonass, QZ=Japanese
            Fields.CommaText := Sentence ;  // Split the message into different parts.
            if Fields.Count < 12 then exit ;
            FGpsQuality := 0 ;
            if (Fields [2] = 'A') then FGpsQuality := 1 ;
            if (FGpsQuality = 1) then // don't change last location unless got a new one
            begin
                if DM2DD (Fields [3], Fields [4], FLatitude) and
                    DM2DD (Fields [5], Fields [6], FLongitude) then
                begin
                    Time2DT (Fields [1], Fields [9], FTimeStamp) ;
                    Num2Double (Fields [7], FGpsSpeedKnots) ;
                    Num2Double (Fields [8], FGpsDegreeTrue) ;
                    Num2Double (Fields [10], FGpsDegreeMag) ;
                    FGpsMagDir := Fields [11] ;
                    locflag := true ;
                end ;
            end;
            result := true ;
        end

// NMEA GLL Sentence format - Geographic Position - Latitude/Longitude
// (not seen with any tested devices)
//       1       2 3        4 5         6 7
//       |       | |        | |         | |
// $--GLL,llll.ll,a,yyyyy.yy,a,hhmmss.ss,A*hh<CR><LF>
// 0) message type
// 1) Latitude
// 2) N or S (North or South)
// 3) Longitude
// 4) E or W (East or West)
// 5) Universal Time Coordinated (UTC)
// 6) Status A - Data Valid, V - Data Invalid
// 7) Checksum

        else if TypeMsg = 'GLL' then  // Geographic Position - Latitude/Longitude
        begin
            Fields.CommaText := Sentence ;  // Split the message into different parts.
            if Fields.Count < 7 then exit ;
            FGpsQuality := 0 ;
            if (Fields [6] = 'A') then FGpsQuality := 1 ;
            if (FGpsQuality = 1) then // don't change last location unless got a new one
            begin
                if DM2DD (Fields [1], Fields [2], FLatitude) and
                    DM2DD (Fields [3], Fields [4], FLongitude) then
                begin
                    Time2DT (Fields [5], '', FTimeStamp) ;
                    locflag := true ;
                end ;
            end;
            result := true ;
        end

// NMEA VTG Sentence format - Track made good and Ground speed
//        1   2 3   4 5	 6 7   8 9
//        |   | |   | |	 | |   | |
// $--VTG,x.x,T,x.x,M,x.x,N,x.x,K*hh<CR><LF>
// $GNVTG,,T,,M,0.0,N,0.0,K,A*3D
// 0) message type
// 1) Track Degrees
// 2) T = True
// 3) Track Degrees
// 4) M = Magnetic
// 5) Speed Knots
// 6) N = Knots
// 7) Speed Kilometers Per Hour
// 8) K = Kilometers Per Hour
// 9. FAA mode indicator (NMEA 2.3 and later)
// 10) Checksum
        else if TypeMsg = 'VTG' then  // Track made good and Ground speed
        begin
            Fields.CommaText := Sentence ;  // Split the message into different parts.
            if Fields.Count < 9 then exit ;
            if Fields [2] <> 'T' then exit ;  // might be older version
            Num2Double (Fields [1], FGpsDegreeTrue) ;
            Num2Double (Fields [3], FGpsDegreeMag) ;
            Num2Double (Fields [5], FGpsSpeedKnots) ;
            Num2Double (Fields [7], FGpsSpeedKm) ;
            result := true ;
        end ;
    finally
        Fields.Free ;
    end;

 // event
   if NOT result then exit ;
 // should be earlier...
   if locflag then
   begin
       if FMinInterval > 0 then
            begin
            if NOT TestTrgTick (FMinEventTrg) then exit ;  // don't want too many events
            FMinEventTrg := AddTrgSecs (GetTickCountX, FMinInterval) ;  // set next triggger
            FMaxEventTrg := AddTrgSecs (GetTickCountX, FMaxInterval) ;
        end;
        if FLastLatitude <> 0 then
            FDistance := GetDistance (FLatitude, FLongitude, FLastLatitude, FLastLongitude)
        else
            FDistance := 0 ;
        FLastLatitude := FLatitude ;
        FLastLongitude := FLongitude ;
        if Assigned (FOnLocChange) then FOnLocChange (Self) ;
   end;
end;

function TMagGpsLoc.GetNmeaInfo: string ;
var
    I, J, K: integer ;
begin
    result := '' ;
    if (FLatitude = 0) and (FLongitude = 0) then
        result := result + 'No position information available' + CRLF_ ;
    if FSensorID <> '' then result := result + 'Sensor ID: ' + FSensorID + CRLF_ ;
    if FMobileNum <> '' then result := result + 'Mobile Number: ' + FMobileNum + CRLF_ ;
    if FGsmSigLev <> '' then result := result + 'GSM Signal Level: ' + FGsmSigLev + CRLF_ ;
    if FDevVolt <> '' then result := result + 'Voltage Level: ' + FDevVolt + CRLF_ ;
    if FLocAreaCode <> '' then result := result + 'Location Area Code: ' + FLocAreaCode + CRLF_ ;
    if FMobNetCode <> '' then result := result + 'Mobile Network Code: ' + FMobNetCode + CRLF_ ;
    if FMobCellId <> '' then result := result + 'Mobile Cell Id: ' + FMobCellId + CRLF_ ;
    result := result + 'Satellites in View = ' +  IntToStr (GetGpsSatellView) + CRLF_ ;
    result := result + 'Satellites Used = ' +  IntToStr (GetGpsSatellFix) + CRLF_ ;
    if FGpsSpeedKnots <> 0 then
        result := result + 'Speed (Knots) = ' +  FloatToStr (FGpsSpeedKnots) + CRLF_ ;
    if FGpsSpeedKm <> 0 then
        result := result + 'Speed (Km/Hr) = ' +  FloatToStr (FGpsSpeedKm) + CRLF_ ;
    if FGpsDegreeTrue <> 0 then
        result := result + 'True Heading = ' +  FloatToStr (FGpsDegreeTrue) + CRLF_ ;
    if FGpsDegreeMag <> 0 then
        result := result + 'Magnetic Heading = ' +  FloatToStr (FGpsDegreeMag) + CRLF_ ;
    if FErrorRadius <> 0 then
        result := result + 'Error Radius = ' +  FloatToStr (FErrorRadius) + CRLF_ ;
    if FGpsGeoidalSep <> 0 then
        result := result + 'Geoidal Separation = ' +  FloatToStr (FGpsGeoidalSep) + FGpsGeoUnits + CRLF_ ;
    if GpsPDOP <> 0 then
        result := result + 'PDOP = ' +  FloatToStr (GpsPDOP) + CRLF_ ;
    if GpsHDOP <> 0 then
        result := result + 'HDOP = ' +  FloatToStr (GpsHDOP) + CRLF_ ;
    if GpsVDOP <> 0 then
        result := result + 'VDOP = ' +  FloatToStr (GpsVDOP) + CRLF_ ;
    for J := 1 to MaxSystems do
    begin
        if GpsSatInfo [0, J].Prn = 0 then continue;
        result := result + 'Constellation: ' + SystemNames[J] + CRLF_ ;
        for I := 1 to MaxSatellites do
        begin
            with GpsSatInfo [I, J] do
            begin
                if Prn > 0 then
                begin
                    result := result + 'Satellite ' + IntToStr (Prn) +
                                     ', Azimuth ' + IntToStr (Azimuth) +
                                     ', Elevation ' + IntToStr (Elevation) +
                                     ', S/N ' + IntToStr (SigNoise) ;
                    for K := 1 to 12 do
                    begin
                        if GpsSatFix [K, J] = Prn then result := result + ', Fix' ;
                    end;
                    result := result + CRLF_ ;
                end;
            end;
        end;
    end ;
end;


function TMagGpsLoc.SetGT02Packet (const Packet: string): boolean;
var
    GpsGt02HBPacket: PGpsGt02HBPacket ;
    GpsGt02DataPacket: TGpsGt02DataPacket ;
    GpsGt02HBResp: TGpsGt02HBResp ;
    GpsGt02CmdRespPacket: TGpsGt02CmdRespPacket ;
    Len, I, DataStatus: Integer ;
    Sendpacket: string ;
    DateDT, TimeDT: TDateTime ;
begin
    result := false ;
    FLastErrorStr := '' ;
    if NOT FActive then
    begin
        FLastErrorStr := 'Can not save data, inactive' ;
        exit ;
    end;
     Len := Length (Packet) ;
    if Len < 15 then exit ;
    GpsGt02HBPacket := @Packet [1] ;

  // need to respond to Heartbeat packet within 10 secs, rapidly on connect, then every three minutes
    if (GpsGt02HBPacket.Protocol = Gt02ProtoHeartbeat) then
    begin
        if (GpsGt02HBPacket.Start = Gt02StartHeartbeatReq) then
        begin
          // keep satellite info and stuff
            ConvHexStr (String(GpsGt02HBPacket.TermId), FSensorID) ;
            if Pos ('0', FSensorID) = 1 then FSensorID := Copy (FSensorID, 2, 99) ;  // strip leading zero
            FGsmSigLev := IntToStr (GpsGt02HBPacket.GsmSig) ;
            FDevVolt := IntToStr (GpsGt02HBPacket.Volts) ;
            FPacketSerial := Swap(GpsGt02HBPacket.Serial);
//            FGpsSatellFix := 0 ;
//            FGpsSatellView :=  GpsGt02HBPacket.SatTot ;
//            if FGpsSatellView > MaxSatellites then FGpsSatellView := MaxSatellites ;
//            if FGpsSatellView > 0 then
            GpsSatInfo [0, 1].Prn :=  GpsGt02HBPacket.SatTot ;
            if GpsSatInfo [0, 1].Prn > MaxSatellites then GpsSatInfo [0, 1].Prn := MaxSatellites ;
            if GpsSatInfo [0, 1].Prn > 0 then
            begin
                if GpsGt02HBPacket.State =  1 then GpsSatFix [0, 1] := GpsSatInfo [0, 1].Prn ;
                for I := 1 to GpsSatInfo [0, 1].Prn do
                begin
                    with GpsSatInfo [I, 1] do
                   begin
                        Prn := I ;
                        SigNoise := GpsGt02HBPacket.SatSNR [I] ;
                    end;
                end;
            end;

         // now respond
            GpsGt02HBResp.Start := Gt02StartHeartbeatResp ;
            GpsGt02HBResp.Protocol := Gt02ProtoHeartbeat ;
            GpsGt02HBResp.Stop := CRLF_ ;
            SetLength (Sendpacket, SizeOf (TGpsGt02HBResp));
            Move (GpsGt02HBResp.Start, SendPacket [1], Length (SendPacket));
            if Assigned (FOnGpsSendData) then
            begin
                FOnGpsSendData (Self, SendPacket) ;
                result := True ;
            end
            else
                FLastErrorStr := 'Can not respond, no SendData event' ;

         // see if triggering location event with old data
            if FMinInterval > 0 then
            begin
                if NOT TestTrgTick (FMinEventTrg) then exit ;  // don't want too many events
                FMinEventTrg := AddTrgSecs (GetTickCountX, FMinInterval) ;  // set next triggger
                FMaxEventTrg := AddTrgSecs (GetTickCountX, FMaxInterval) ;
            end;
            if (FLastLatitude = 0) and (FLastLongitude = 0) then exit ;  // only if got something
            if Assigned (FOnLocChange) then FOnLocChange (Self) ;
             Exit;
        end;
        FLastErrorStr := 'Unknown Heathrbeat Packet: ' + ConvHexQuads (Packet) ;
    end

  // read location packet with lat and long, only sent when location changed
    else if (GpsGt02HBPacket.Protocol = Gt02ProtoData) then
    begin
        if (GpsGt02HBPacket.Start = Gt02StartData) then
         begin
            result := True ;
            Move (Packet [1], GpsGt02DataPacket, SizeOf (TGpsGt02DataPacket));
            FPacketSerial := Swap(GpsGt02DataPacket.Serial);
            ConvHexStr (String(GpsGt02HBPacket.TermId), FSensorID) ;
            if Pos ('0', FSensorID) = 1 then FSensorID := Copy (FSensorID, 2, 99) ;  // strip leading zero
            FLocAreaCode := IntToHex (Swap (GpsGt02DataPacket.LAC), 4) ;
            FMobNetCode := IntToStr (GpsGt02DataPacket.MNC) ;
            FMobCellId := IntToStr (Swap (GpsGt02DataPacket.CellId)) ;
            DataStatus := EndianLong (GpsGt02DataPacket.Status) ;
            if DataStatus AND Gt02StatGpsFix = Gt02StatGpsFix then
            begin
                with GpsGt02DataPacket do
                begin
                    if TryEncodeDate (DateTime [1] + 2000, DateTime [2], DateTime [3], DateDT) and
                            TryEncodeTime (DateTime [4], DateTime [5], DateTime [6], 0, TimeDT) then
                             FTimeStamp := DateDT + TimeDT ;
                end;
                FLatitude := EndianLong (GpsGt02DataPacket.Lat) / 1800000 ;
                FLongitude := EndianLong (GpsGt02DataPacket.Long) / 1800000 ;
                FGpsSpeedKnots := GpsGt02DataPacket.Speed ;
                FGpsDegreeTrue := Swap(GpsGt02DataPacket.Course) ;
                FGpsDegreeMag := FGpsDegreeTrue ;
                if DataStatus AND Gt02StatLatDir <> Gt02StatLatDir then FLatitude := -FLatitude ;
                if DataStatus AND Gt02StatLongDir <> Gt02StatLongDir then FLongitude := -FLongitude ;
                if FLastLatitude <> 0 then
                    FDistance := GetDistance (FLatitude, FLongitude, FLastLatitude, FLastLongitude)
                else
                    FDistance := 0 ;
                FLastLatitude := FLatitude ;
                FLastLongitude := FLongitude ;
                if Assigned (FOnLocChange) then FOnLocChange (Self) ;
             end;
        end;
    end

  // address request packet - seems designed to request a postal street address for the lat/long
    else if (GpsGt02HBPacket.Protocol = Gt02ProtoAddrReq) then
    begin
        if (GpsGt02HBPacket.Start = Gt02StartAddrReq) then
         begin
            FPacketSerial := Swap(GpsGt02HBPacket.Serial);
            FLastErrorStr := 'Address Packet Ignored' ;
         end;
    end
    else if (GpsGt02HBPacket.Protocol = Gt02ProtoInst) then
    begin
        if (GpsGt02HBPacket.Start = Gt02StartAddrReq) then
         begin
            result := True ;
            Move (Packet [1], GpsGt02CmdRespPacket, SizeOf (TGpsGt02CmdRespPacket));
            FPacketSerial := Swap(GpsGt02CmdRespPacket.Serial);
            ConvHexStr (String(GpsGt02CmdRespPacket.TermId), FSensorID) ;
            FLastCmdResponse := String(GpsGt02CmdRespPacket.CmdData) ;
            SetLength (FLastCmdResponse, GpsGt02CmdRespPacket.CmdLen) ;
            if Assigned (FOnGpsLog) then FOnGpsLog (Self, FLastCmdResponse);
         end;
    end
    else
    begin
        FLastErrorStr := 'Unknown Packet Type: ' + ConvHexQuads (Packet) ;

    end;
end;

// protocol for Xenun ARM CPU products(tk102-2,tk103-2, tk202，tk201-2,xt007,xt008) etc)

function TMagGpsLoc.SetTK10XPacket (const Packet: string): boolean;    // July 2017
var
    Fields: TStringList;
    S: string;
    I: Integer;
{
The following is the gprs data for ARM CPU products(tk102-2,tk103-2, tk202，tk201-2,xt007,xt008) etc)
Essentially the NMEA RMC sentence, preceded by date/time and mobile number, followed by useful
stuff from other NMEA sentences like satellite count, mobile IMEI and cell station stuff.

090805215127,+22663173122,GPRMC,215127.083,A,4717.3044,N,01135.0005,E,0.39,217.95,050809,,,A*6D,F,, imei:354776030393299,05,552.4,F:4.06V,0,141,54982,232,01,1A30,0949
170720093445,,GPRMC,093445.7,A,5122.9769,N,00005.1644,W,3.6,184.20,200717,0.0,E,A*21,F,imei:351865085761832,07,132.0,F:4.37V,0,127,,234,10,188ED6E,3890
170720093510,,GPRMC,093510.8,A,5122.9902,N,00005.1538,W,0.0,0.00,200717,0.0,E,A*2E,F,imei:351865085761832,12,96.0,F:4.37V,0,124,,234,10,188ED6E,3890

0) 090805215127    = 2009, 5th,Aug. 21:51:27
1) +22663173122    = admin number, it is the mobile number which you use to set up apn,ip,port
2) GPRMC           = Message ID - start of NMEA RMC sentence (add $ to front)
3) 215127.083      = UTC time( 21:51:27:083) the time in your place
4) A               = Status, always A never change
5) 4717.3044       = Latitude, from GMS module directly - Degree Minutes
6) N               = N or S
7) 01135.0005      = Longitude - Degree Minutes
8) E               = E or W
9) 0.39            = speed knots
10) 217.95         = angle it is direction of travel, but not accurate, so please ignore this part
11) 050809         = date it is from GSM module directly, we can not change it DDMMYY
12) 0.0            = Magnetic Variation, degrees
13) E              = E or W
14) A*6D           = CRC16 correction for GPRMC (end of NMEA RMC sentence)
15) F              = valid GPS signal
16) imei:354776030393299  = IMEI
17) 05             = can get signal from 5 satellites the tracker can get satellites for this data
18) 552.4          = the height it is horizon level, not accurate
19) F:4.06V        = power left in the battery , it is 4.2V-3.7V
20) 0              = no charging state if the usb connector on the tracker is connected with power , it is 1, otherwise 0
21) 141            = the byte in this data it is the total bytes before 141, count them and will be 141
22) 54982          = CRC16 correction for the whote gprs data, decimal system
23) 232            = MCC Mobile Country Code
24) 01             = MNC Mobile Network Code
25) 1A30           = LAC Location area code
26) 0949           = cell ID
}

begin
    result := false ;
    FLastErrorStr := '' ;
    if NOT FActive then
    begin
        FLastErrorStr := 'Can not save data, inactive' ;
        exit ;
    end;
    if Length (Packet) < 20 then exit ;
    Fields := TStringList.Create;
    Fields.Delimiter := ',' ;
    try
        Fields.CommaText := Packet ;  // Split the message into different parts.
        if Fields.Count < 23 then   // Oct 2017  mobile stuff may be missing
        begin
            FLastErrorStr := 'Only found ' + IntToStr(Fields.Count) + ' fields: ' + Packet ;
            exit ;
        end;
        if (Fields [2]  = 'GPRMC') then  // GPS Recommended Minimum Navigation Information
        begin
            FMobileNum := Fields [1];
            FSensorID := Fields [16];
            if Pos ('imei:', FSensorID) = 1 then FSensorID := Copy (FSensorID, 6, 99) ;  // strip imei:
            S := Fields [19];
            if Pos ('F:', S) = 1 then FDevVolt := Copy (S, 3, 9) ;
//            FGpsSatellFix := AscToInt(Fields [17]);
//            FGpsSatellView := FGpsSatellFix;
//            if FGpsSatellView > MaxSatellites then FGpsSatellView := MaxSatellites ;
            GpsSatFix [0, 1] := AscToInt(Fields [17]);
            if GpsSatFix [0, 1] > MaxSatellites then GpsSatFix [0, 1] := MaxSatellites ;
            GpsSatInfo [0, 1].Prn := GpsSatFix [0, 1];
            if Fields.Count >= 24 then      // Oct 2017 mobile stuff may be missing
            begin
                FLocAreaCode := Fields [25];
                FMobNetCode := Fields [24];
                FMobCellId := Fields [26];
            end;

       // extract and process $GPRMC sentence
            I := Pos (',GPRMC,', Packet);
            result := SetNMEASentence ('$' + Copy (Packet, I + 1, 999)) ;
            if NOT result then exit;
        end
    finally
        Fields.Free ;
    end;
end;


function TMagGpsLoc.SetWondeXPacket (const Packet: string): boolean;    // July 2017
var
    Fields: TStringList;
    S: string;
    I: integer;
    Lat, Long: Double;

{ TK5000 uses WondeX VTxx protocol

2000000001,20080330074922,121.648699,25.060560,0,159,0,5,1,0.0,0,,,0
2000000001,20080330074923,121.648699,25.060560,0,159,0,6,1,0.0,0,,,0
351865085761832,20170720110703,-0.08594539,51.38311989,0.0,0.0,102.0,15,2
351865085761832,20170720110714,-0.0858705,51.383071,0.0,0.0,0.0,7,2

Parameter format for returning string:
(RP Header): Header for returning message

0) Device ID: The ID of the device. (Maximum length is 10 digits)
1) DateTime: YYYYMMDDhhmmss (GMT)
2) Longitude: WGS-84 coordinate system
3) Latitude: WGS-84 coordinate system
4) Speed: 0~65535 km/h
5) Heading: 0~360 degrees
6) Altitude: Parameter column Reserved (currently showing ‘0’)
7) Satellite: 0~12
8) Event ID: xxx. Different event ID indicates different meaning of each returning message, Please refer to appendix 8.1 for detailed description.
(following missing from some implementations)
9) Mileage: the mileage value in kilometer
10) Input status: Input status indication (bitwise), the returning value is in “decimal” format. Please convert it to “binary” mode to read the input status:
11) Voltage level of Analog 1 : 0.00~30.00 V
12) Voltage level of Analog 2: 0.00~30.00 V
13) Output Status: Output status indication (bitwise),
}
begin
    result := false ;
    FLastErrorStr := '' ;
    if NOT FActive then
    begin
        FLastErrorStr := 'Can not save data, inactive' ;
        exit ;
    end;
    if Length (Packet) < 20 then exit ;
    Fields := TStringList.Create;
    Fields.Delimiter := ',' ;
    try
        Fields.CommaText := Packet ;  // Split the message into different parts.
        if Fields.Count < 9 then
        begin
            FLastErrorStr := 'Only found ' + IntToStr(Fields.Count) + ' fields: ' + Packet ;
            exit ;
        end;
        I := AscToInt (Fields [8]);
        if (I <= 2) and (Length (Fields [2]) >= 3) then  // position data
        begin
            Num2Double (Fields [2], long) ;
            Num2Double (Fields [3], lat) ;
            if (Abs (long) <= 180) and (Abs (lat) <= 180) then
            begin
                FLatitude := lat;
                FLongitude := long;
                FSensorID := Fields [0];
                S := Fields [1];
                if Length (S) >= 12 then
                    FTimeStamp := Packed2Date(Copy (S, 1, 8) + '-' + Copy (S, 7, 6));
                Num2Double (Fields [4], FGpsSpeedKnots) ;
                Num2Double (Fields [5], FGpsDegreeTrue) ;
//                FGpsSatellFix := AscToInt(Fields [7]);
//                FGpsSatellView := FGpsSatellFix;
//                if FGpsSatellView > MaxSatellites then FGpsSatellView := MaxSatellites ;
                GpsSatFix [0, 1]  := AscToInt(Fields [7]);
                if GpsSatFix [0, 1] > MaxSatellites then GpsSatFix [0, 1] := MaxSatellites ;
                GpsSatInfo [0, 1].Prn := GpsSatFix [0, 1];
                if FLastLatitude <> 0 then
                    FDistance := GetDistance (FLatitude, FLongitude, FLastLatitude, FLastLongitude)
                else
                    FDistance := 0 ;
                FLastLatitude := FLatitude ;
                FLastLongitude := FLongitude ;
                if Assigned (FOnLocChange) then FOnLocChange (Self) ;
            end;
            result := true ;
        end
    finally
        Fields.Free ;
    end;

end;


function TMagGpsLoc.SetRecvData (const Info: string): boolean;
begin
    try
        if GpsType = GpsTypeGT02 then
            Result := SetGT02Packet (Info)
        else if GpsType = GpsTypeNMEA then
            Result := SetNMEASentence (Info)
        else if GpsType = GpsTypeTK10X then
            Result := SetTK10XPacket (Info)
        else if GpsType = GpsTypeWondeX then
            Result := SetWondeXPacket (Info)
        else
            Result := false ;
    except
        FLastErrorStr := 'Data Error - ' + GetExceptMess (ExceptObject) ;
        Result := false ;
    end;
end;

// send command instructiob to sensor - not got a response yet!!!

function TMagGpsLoc.SendGT02Cmd (const Cmd: string): boolean;
var
    GpsGt02CmdSendPacket: TGpsGt02CmdSendPacket ;
    Sendpacket: string ;
begin
    result := false ;
    FLastErrorStr := '' ;
    if NOT FActive then
    begin
        FLastErrorStr := 'Can not send command, inactive' ;
        exit ;
    end;
    if NOT Assigned (FOnGpsSendData) then
    begin
        FLastErrorStr := 'Can not send command, no SendData event' ;
        exit ;
    end;
    GpsGt02CmdSendPacket.Start := Gt02StartInstReq ;
    GpsGt02CmdSendPacket.Protocol := Gt02ProtoInst ;
    GpsGt02CmdSendPacket.ServerFlags := EndianLong ($12345678) ;
    GpsGt02CmdSendPacket.CmdLen := Length (Cmd) + 4 ;
    GpsGt02CmdSendPacket.PackLen := Length (Cmd) + 6 ;
    SetLength (Sendpacket, 9);
    Move (GpsGt02CmdSendPacket.Start, SendPacket [1], Length (SendPacket));
    SendPacket := SendPacket + Cmd + CRLF_ ;
    FOnGpsSendData (Self, SendPacket) ;
    result := True ;
end;

end.
