التواصل برمجيا مع خدمة Protrack لتتبع السيارات

نظرة عامة

خدمة Protrack هي أحد خدمات تتابع السيارات GPS المعروفة، وفي هذا الدرس نتعلم كيفية التواصل برمجيا مع الخدمة وتتبع السيارات الموجودة في الحساب. سنقوم ببرمجة إطار للتواصل مع الخدمة باستخدام لغة سي شارب وبيئة عمل الدوت نت. يظهر الدرس العديد من الأفكار الخاصة بالبرمجة كائنية التوجه.

المتطلبات

يتطلب الدرس أن يكون لديك حساب على موقع Protrack. إذا واجهت أخطاء خاصة بالصلاحيات يمكنك التواصل مع البائع أو الدعم الفني للخدمة.

مقدمة

الواجهة البرمجية لخدمة Protrack (https://www.protrack365.com/api.html) هي واجهة بسيطة جدا تعتمد على إرسال نداءات HTTP وقرءاة النتائج والتي تكون بصيغ JSON. الجديد هنا هو استخدام البرمجة الموجهة للكائنات في برمجة إطار للخدمة يمكن إعادة استخدامه في أي تطبيق.

برامج مساعدة

سنحتاج في هذا الدرس إلى مكونات Newtonsoft JSON.NET لقراءة صيغة JSON والتي يمكن تحميلها من الموقع https://www.newtonsoft.com/json أو عن طريق Nuget Manager في فيجوال ستوديو.

التجريد Abstraction

الأوامر Requests

بمراجعة واجهة برمجة Protrack نجد أن كل الأوامر (ما عدا أمر التوثيق) تتطلب إدخال رمز وصول Access Token. وهذا الرمز (والصالح لمدة ساعتين فقط) يمكن الحصول عليه عن طريق أمر التوثيق Authorizatoion فقط.

 مع وضع هذه المعلومة في الرأس يمكننا كتابة نموذج أولي للأمر الذي سيتم إرساله إلى Protrack:

  internal abstract class ProtrackRequest {
    /// <summary>
    /// اسم الأمر
    /// </summary>
    public abstract string BaseUri { get; }
    /// <summary>
    /// رمز الوصول والذي سيستخدم في كل الأوامر عدا التوثيق 
    /// authorization
    /// </summary>
    public string AccessToken { get; set; }

    /// <summary>
    /// نقوم بإرجاع جميع المدخلات المطلوبة للأمر في صيغة قاموس (اسم وقيمة)
    /// </summary>
    public virtual IDictionary<string, object> GetParams() {
      var list = new Dictionary<string, object>();
      if (AccessToken != null) // adding access token only if necessary
        list.Add("access_token", AccessToken);
      return list;
    }

    /// <summary>
    /// تقوم بإرجاع المدخلات المطلوبة للأمر في صيغة سلسلة استعلام
    /// query string
    /// </summary>
    public virtual string GetParamsQueryString() {
      string queryString = string.Empty;

      foreach (var itm in GetParams()) {
        string valueStr = string.Empty;

        if (itm.Value != null)
          valueStr = System.Uri.EscapeDataString(itm.Value.ToString()); 

        queryString += string.Format("{0}={1}&", itm.Key, valueStr);
      }

      return queryString;
    }

    /// <summary>
    /// تقوم بإرجاع اسم الدالة والأوامر الخاصة بها
    /// </summary>
    public virtual string GetRequestUri() {
      return BaseUri + "?" + GetParamsQueryString();
    }

    public override string ToString() {
      return GetRequestUri();
    }
  }

في الكود السابق نلاحظ أن كل أمر سنقوم بإنشائه يجب أن يرث من الأب وهو ProtrackRequest ويجب أن يقوم بملء اسم الأمر في خاصية BaseUri والمعطيات الخاصة به في الدالة GetParams().

الإجابات Responses

عندما تقوم بإرسال أي أمر إلى السيرفر تحص على إجابة Response بصيغة JSON. وبمراجعة واجهة برمجة Protrack نجد أن هناك خاصيتان مشتركتان في كل الإجابات وهم code ويمثل كود الخطأ (إن وُجِد)، و message وهي نص الخطا (إن وُجِد أيضا). يمكنك قراءة أكواد الأخطاء كاملة في شرح الواجهة البرمجية على موقعهم. بمعرفة ذلك يمكننا ببساطة كتابة تصنيف يعمل كأب لجميع الإجابات:

  internal class ProtrackResponse {
    [JsonProperty("code")]
    public int Code { get; set; }

    [JsonIgnore]
    public ProtrackResponseCode ResponseCode { get { return (ProtrackResponseCode)Code; } }
    [JsonProperty("message")]
    public string Message { get; set; }
  }


  internal enum ProtrackResponseCode {
    Success = 0,
    SystemError = 10000,
    UnknownRequest = 10001,
    LoginTimeout = 10002,
    Unauthorized = 10003,
    ParameterError = 10004,
    MissingParameter = 10005,
    ParamOutOfRange = 10006,
    PermissionDenied = 10007,
    RequestLimit = 10009,
    AccessTokenNotExist = 10010,
    AccessTokenInvalid = 10011,
    AccessTokenExpired = 10012,
    ImeiUnauthorized = 10013,
    RequestTimeError = 10014,
    LoginFailed = 20001,
    TargetNotExist = 20005,
    DeviceOffline = 20017,
    SendCommandFailed = 20018,
    NoData = 20023,
    TargetExpired = 20046,
    Unsupported = 20048
  }

لم نقم بتحديد التصنيف كأب abstract وذلك لأنه إن قمنا بذلك يمكننا إنشاء نسخة منه أثناء تحويل الرد من JSON إلى عنصر.

إطار العمل

الآن نقوم بكتابة الكود الذي يقوم بربط الأوامر بالردود الخاصة بها ويقوم بالتواصل مع السيرفر.

  class ProtrackWrapper {
    protected string Account { get; set; }
    protected string Password { get; set; }
    /// <summary>
    /// عنوان واجهة البرمجة
    /// </summary>
    protected string BaseUri { get { return "http://api.protrack365.com"; } }
    /// <summary>
    /// رمز الوصول والذي سنقوم باستخدامه مع أغلب الأوامر
    /// </summary>
    public string AccessToken { get; protected set; }
    /// <summary>
    /// توقيت انتهاء رمز الوصول بتوقيت جرينيتش
    /// </summary>
    public DateTime? AccessTokenExpiresOnUtc { get; protected set; }


    public ProtrackWrapper(string account, string password) {
      this.Account = account;
      this.Password = password;
    }

    /// <summary>
    /// تقوم بإرسال الأمر إلى السيرفر واستلام الإجابة كنص
    /// </summary>
    protected static string GetResponse(string requestUri) {
      HttpWebRequest req = WebRequest.CreateHttp(requestUri);
      using (var rsp = req.GetResponse())
      using (var stm = rsp.GetResponseStream())
      using (var rdr = new StreamReader(stm)) {
        return rdr.ReadToEnd();
      }
    }

    /// <summary>
    /// تقوم بإرسال الأمر إلى السيرفر واستلام الإجابة وتحويلها إى عنصر
    /// </summary>
    public virtual T GetResponse<T>(ProtrackRequest req) where T : ProtrackResponse {
      var requestUrl = new Uri(new Uri(BaseUri), req.GetRequestUri()).ToString();

      var rspStr = GetResponse(requestUrl);

      T rsp = JsonConvert.DeserializeObject<T>(rspStr);
      // نقوم بإرسال خطأ إذا لم يكن الكود يساوي صفر
      // هذا ليس  إجراءا صحيحا في كل الأحوال لكن يمكننا تجاوزه الآن
      if (rsp.Code != 0)
        throw new Exception(rsp.Message);

      return rsp;
    }
  }

كما يمكنك المشاهدة في الكود السابق، قمنا باستخدام التعدد Polymorphism لإنشاء صيغتين من الدالة GetResponse()، الصيغة الأولى تقوم باستلام نص الأمر وإرساله وإرجاع نص الإجابة، الثانية تقوم باستلام الأمر بصيغة عنصر وإرساله إلى الدالة الأولى وإرجاع الرد بصيغة عنصر آخر. وهذا يمنح تعددية مع عدم تكرار أي أكواد. نلاحظ أيضا أن الصيغة الثانية هي صيغة عامة Generic يمكنها إرجاع أي نوع من العناصر بشرط أن تكون العناصر من نوع ProtrackResponse أو أي ابن له.

التوثيق Authorization

لاستخدام واجهة برمجة Protrack أول أمر يجب استخدامه وهو أمر التوثيق، وهو للحصول على رمز الوصول Access Token. هذا الأمر يقبل 3 مدخلات: توقيت الدخول (بصيغة يونكس Unix)، اسم المستخدم، وتوقيعه.

توقيت يونكس Unix

التوقيت بصيغة يونكس Unix هو عدد الثواني التي مرت منذ بداية يوم 1 يناير 1970 بتوقيت جرينيتش. وهذه هي الصيغة التي يتم تمثيل التوقيت فيها في أنظمة Unix وهي أيضا الصيغة التي يعتمد عليها Protrack..

بما أن توقيت يونكس سيتم استخدامه أكثر من مرة قمنا بإنشاء تصنيف يقوم بالتحويل بينه وبين DateTime

  static class UnixTimeHelper {
    /// <summary>
    /// Converts DateTime to Unix time.
    /// </summary>
    public static long ToUnixTime(this DateTime time) {
      var totalSeconds = (long)(time.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;

      if (totalSeconds < 0)
        throw new ArgumentOutOfRangeException("Unix time starts Jan 1 1970 00:00:00");

      return totalSeconds;
    }
    /// <summary>
    /// Converts Unix time to DateTime.
    /// </summary>
    public static DateTime ToDateTime(long unixTime) {
      return new DateTime(1970, 1, 1).Add(TimeSpan.FromSeconds(unixTime));
    }
  }

التوقيع

لن يمكن إرسال كلمة المرور بشكل مباشر إلى السيرفر، بدلا من ذلك يمكنك إرسال “توقيع” وهو عبارة عن الهاش Hash الخاص بالهاش الخاص بكلمة المرور والتوقيت. يتم الحصول على الهاش بتقنية MD5. تتطلب الخدمة تحويل التوقيع إلى 32 حرف.

signature = MD5 ( MD5(password) + unix_time ) 

أمر التوثيق Authorization

يمكننا كتابة كلاس أمر التوثيق كما يلي:

  internal class ProtrackAuthorizationRequest : ProtrackRequest {
    /// <summary>
    /// اسم الأمر
    /// </summary>
    public override string BaseUri { get { return "api/authorization"; } }
    public string Account { get; protected set; }
    protected string Password { get; set; }
    public DateTime RequestTimeUtc { get; private set; }

    public ProtrackAuthorizationRequest() { }
    public ProtrackAuthorizationRequest(string account, string password) {
      this.Account = account;
      this.Password = password;
    }

    public override IDictionary<string, object> GetParams() {
      RequestTimeUtc = DateTime.UtcNow;
      var unixTime = UnixTimeHelper.ToUnixTime(RequestTimeUtc);

      string signature = GetSignature(unixTime);

      var list = base.GetParams(); // الحصول على المدخلات من الأب
      list.Add("time", unixTime);
      list.Add("account", this.Account);
      list.Add("signature", signature);

      return list;
    }

    private string GetSignature(long unixTime) {
      // signature is md5(md5(password) + time) encoded as a 32 bytes lower-case characters.
      var signature = ProtrackHelper.HashMD5(this.Password);
      signature = ProtrackHelper.HashMD5(signature + unixTime.ToString());
      return signature;
    }
  }

كما يمكنك المشاهدة، قمنا بالوراثة من الأب ProtrackRequest وقمنا بتزويد اسم الدالة والمدخلات الخاصة بها.

وهذا هو الكود الخاص بالهاش، سوف نحتاجه أيضا:

  static class ProtrackHelper {
    public static string HashMD5(string input) {
      byte[] data = System.Text.Encoding.UTF8.GetBytes(input);
      data = System.Security.Cryptography.MD5.Create().ComputeHash(data);
      return BitConverter.ToString(data).Replace("-", "").ToLower();
    }
  }

نتيجة التوثيق Authorization

لكتابة الكلاس الخاصة بالنتيجة الخاصة بالتوثيق، نقوم بالوراثة من الأب ProtrackResponse ونقوم بتزويد الخصائص الخاصة بالتوثيق فقط.

  internal class ProtrackAuthorizationResponse : ProtrackResponse {
    [JsonProperty("record")]
    public ProtrackAuthorizationRecord Record { get; set; }
  }

  internal class ProtrackAuthorizationRecord {
    [JsonProperty("access_token")]
    public string AccessToken { get; set; }
    [JsonProperty("expires_in")]
    public int ExpiresInSeconds { get; set; }
  }

قمنا بإضافة الإشارة JsonPropertyAttribute إلى خصائص الكلاس وذلك لكتابة الاسم الأصلي المذكور في صيغة JSON.

ربط الأمر بالنتيجة والتجربة

الآن يمكننا إضافة الكود التالي إلى الإطار الخاص بنا ProtrackWrapper ويقوم بإرسال أمر التوثيق واستلام نتيجته وتسجيل رمز الوصول Access Token لاستعماله في الأوامر التالية:

    public void Authorize() {
      this.AccessToken = null;
      this.AccessTokenExpiresOnUtc = null;

      var req = new ProtrackAuthorizationRequest(this.Account, this.Password);
      var rsp = GetResponse<ProtrackAuthorizationResponse>(req);
      
      // تسجيل رمز الوصول لاستعماله مباشرة في الأوامر التالية
      this.AccessToken = rsp.Record.AccessToken;
      this.AccessTokenExpiresOnUtc = req.RequestTimeUtc.AddSeconds(rsp.Record.ExpiresInSeconds);
    }

الآن كود التجربة:

      var wrapper = new ProtrackWrapper("test", "123456");
      wrapper.Authorize( );
      Console.WriteLine("Authorization code is: {0}", wrapper.AccessToken);.
      // Prints:
      // Authorization code is: A156321......69a0ef614ef3f582

التتبع Tracking

أول أمر معنا بعد التوثيق هو التتبع Tracking. يعمل التتبع عن طريق إرسال كود Server IMEI الخاص بجهاز GPS أو أكثر إلى السيرفر ومعه رمز الوصول Access Token ويقوم السيرفر بالرد بآخر بإحداثيات آخر مكان لأجهزة الـ GPS المرسلة.

يمكنك الحصول على كود Server IMEI من خلال موقع Protrack نفسه أو من خلال الأمر Param# (ربما لا يعمل في جميع الموديلات). هذا الكود مختلف عن الكود المكتوب على الجهاز نفسه.

أمر التتبع Track Request

بعد علمنا بالمتطلبات يمكننا كتابة الكلاس:

  internal class ProtrackTrackRequest : ProtrackRequest {
    public override string BaseUri { get { return "api/track"; } }
    public string[] ImeiList { get; set; }

    public ProtrackTrackRequest() {

    }

    public ProtrackTrackRequest(string accessToken, string[] imeiList) {
      this.AccessToken = accessToken;
      this.ImeiList = imeiList;
    }

    public override IDictionary<string, object> GetParams() {
      var list = base.GetParams();
      list.Add("imeis", string.Join(",", ImeiList));

      return list;
    }
  }

رد التتبع Track Response

وهذا هو كود الرد ويظهر فيه استخدامنا لخصائص تمثل القيم التي ترجع إلينا من السيرفر.

  internal class ProtrackTrackResponse : ProtrackResponse {
    [JsonProperty("record")]
    public ProtrackTrackRecord[] Records { get; set; }
  }

  internal class ProtrackTrackRecord {
    [JsonProperty("imei")]
    public string IMEI { get; set; }
    [JsonProperty("longitude")]
    public decimal Longitude { get; set; }
    [JsonProperty("latitude")]
    public decimal Latitude { get; set; }
    [JsonProperty("systemtime")]
    public long SystemUnixTime { get; set; }
    [JsonProperty("gpstime")]
    public long GpsUnixTime { get; set; }

    // لتسهيل الأمور، قمنا بعمل خاصيتين لتحويل التوقيت من يونكس إلى دوت نت
    public DateTime SystemTimeUtc { get { return UnixTimeHelper.ToDateTime(SystemUnixTime); } }
    public DateTime GpsTimeUtc { get { return UnixTimeHelper.ToDateTime(GpsUnixTime); } }

    // قم بإضافة أي خصاص أخرى
  } 

ربط الأمر بالنتيجة والتجربة

الآن يمكنك إضافة الكود التالي إلى كلاس الإطار ProtrackWrapper:

    public ProtrackTrackRecord Track(string imei) {
      return Track(new string[] { imei })[0];
    }
    public ProtrackTrackRecord[] Track(string[] imeiList) {
      if (this.AccessToken == null || DateTime.UtcNow >= this.AccessTokenExpiresOnUtc) {
        Authorize();
      }

      var req = new ProtrackTrackRequest(this.AccessToken, imeiList);
      var rsp = GetResponse<ProtrackTrackResponse>(req);

      return rsp.Records;
    }

والتجربة:

      var track = wrapper.Track("123456789012345");
      Console.WriteLine("{0},{1},{2}", track.Latitude, track.Longitude, track.GpsTimeUtc);
      // Prints
      // 30.193456, 31.463092, 15 / 07 / 2019 19:41:38

البحث Playback

يمكنك استخدام أمر Playback للحصول على خطوط السير. يستقبل هذا الأمر كود Server IMEI لجهاز الـ GPS ورمز الوصول Access Token وتوقيت البداية والنهاية بصيغة يونكس.

أمر البحث Playback Request

من السهل توقع الكود:

  internal class ProtrackPlaybackRequest : ProtrackRequest {
    public override string BaseUri { get { return "api/playback"; } }
    public string Imei { get; set; }
    public DateTime BeginTimeUtc{ get; set; }
    public DateTime EndTimeUtc { get; set; }

    public ProtrackPlaybackRequest() {

    }

    public ProtrackPlaybackRequest(string accessToken, string imei, DateTime beginTimeUtc, DateTime endTimeUtc) {
      this.AccessToken = accessToken;
      this.Imei = imei;
      this.BeginTimeUtc = beginTimeUtc;
      this.EndTimeUtc = endTimeUtc;
    }

    public override IDictionary<string, object> GetParams() {
      var list = base.GetParams();
      list.Add("imei", this.Imei);
      list.Add("begintime", UnixTimeHelper.ToUnixTime(BeginTimeUtc));
      list.Add("endtime", UnixTimeHelper.ToUnixTime(EndTimeUtc));

      return list;
    }
  }

رد أمر البحث Playback Response

  internal class ProtrackPlaybackResponse : ProtrackResponse {
    [JsonProperty("record")]
    public string RecordString { get; set; }

    public ProtrackPlaybackRecord[] GetRecords() {
      var recordsStrList = RecordString.Split(';');
      List<ProtrackPlaybackRecord> records = new List<ConsoleApp.ProtrackPlaybackRecord>(recordsStrList.Length);

      foreach (var recordStr in recordsStrList) {
        if (recordStr.Length == 0)
          continue;

        var record = new ProtrackPlaybackRecord(recordStr);
        records.Add(record);
      }

      return records.ToArray();
    }
  }

  internal class ProtrackPlaybackRecord {
    public decimal Longitude { get; set; }
    public decimal Latitude { get; set; }
    public DateTime GpsTimeUtc { get; set; }
    public int Speed { get; set; }
    public int Course { get; set; }

    public ProtrackPlaybackRecord() {

    }
    public ProtrackPlaybackRecord(string str) {
      string[] args = str.Split(',');

      Longitude = decimal.Parse(args[0]);
      Latitude = decimal.Parse(args[1]);
      GpsTimeUtc = UnixTimeHelper.ToDateTime(int.Parse(args[2]));
      Speed = int.Parse(args[3]);
      Course = int.Parse(args[4]);
    }
  }

ربط الأمر بالنتيجة والتجربة

كود ProtrackWrapper:

    public ProtrackPlaybackRecord[] Playback(string imei, DateTime beginTimeUtc, DateTime endTimeUtc) {
      if (this.AccessToken == null || DateTime.UtcNow >= this.AccessTokenExpiresOnUtc) {
        Authorize();
      }

      var req = new ProtrackPlaybackRequest(this.AccessToken, imei, beginTimeUtc, endTimeUtc);
      var rsp = GetResponse<ProtrackPlaybackResponse>(req);

      return rsp.GetRecords();
    }

والتجربة:

      var records = wrapper.Playback("123456789012345", DateTime.UtcNow, DateTime.Today);
      foreach (var rec in records)
        Console.WriteLine("{0},{1},{2}", rec.GpsTimeUtc, rec.Latitude, rec.Longitude);

ما التالي؟

باستخدام القوالب الموجودة بالأعلى يمكنكم برمجة باقي الأوامر إذا كنتم تريدون أو الاكتفاء بالأوامر الحالية.

الكود كاملا

يمكنكم تحميل الكود كاملا من هنا:
https://app.box.com/s/0nqvt7atf6hjueixk18qt23a9b3b5g7u

رأي واحد حول “التواصل برمجيا مع خدمة Protrack لتتبع السيارات

اترك تعليقا