Introduction
Accessing Protrack API (https://www.protrack365.com/api.html) is fairly simple, it’s all about creating simple HTTP requests and handling the results which are represented in JSON. The trick here is using OOP concepts to make our code more reusable and more maintainable.
Full Code Listing
This package has been posted to,
Requirements
A Protrack account with one or more GPS devices available for testing. If you are facing permission problems contact your vendor or Protrack technical support.
Dependencies
Start by adding a reference to Newtonsoft JSON.NET (https://www.newtonsoft.com/json) library to your project. This can be directly downloaded from the vendor’s website or installed through NuGet Manager.
Abstraction
Base Request
Referring to Protrack API we can see that all functions (except the authorization function) accept an access token as a parameter. This token is valid for a certain time (2 hours) and it can be received through authorization only.
We will start by creating a base class that will represent the request data. The source code is fairly self-explanatory:
- internal abstract class ProtrackRequest {
-
-
-
- public abstract string BaseUri { get; }
-
-
-
- public string AccessToken { get; set; }
-
-
-
-
- public virtual IDictionary<string, object> GetParams() {
- var list = new Dictionary<string, object>();
- if (AccessToken != null)
- list.Add("access_token", AccessToken);
- return list;
- }
-
-
-
-
- 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;
- }
-
-
-
-
- public virtual string GetRequestUri() {
- return BaseUri + "?" + GetParamsQueryString();
- }
-
- public override string ToString() {
- return GetRequestUri();
- }
- }
From the above code we can see that every request class derived from the base will have to fill its path (BaseUri property; mandatory) and parameter list (GetParams() method; optional).
Base Response
Referring to the Protrack API again we can see that every call response returned from the server, besides being in JSON format, has two common attributes: code and message. The code may refer to one of the error codes available as a list in the API reference, while the message is the description. Keeping those two attributes in mind, we can create our response class,
- 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
- }
As response class needs to be instantiated, we cannot just mark it as abstract. Abstract classes cannot be instantiated.
API Wrapper
Now for the actual code that connects things together. This code represents the wrapper itself. The code is very generic. We will add function wrappers later.
- class ProtrackWrapper {
- protected string Account { get; set; }
- protected string Password { get; set; }
-
-
-
- protected string BaseUri { get { return "http://api.protrack365.com"; } }
-
-
-
- public string AccessToken { get; protected set; }
-
-
-
- public DateTime? AccessTokenExpiresOnUtc { get; protected set; }
-
-
- public ProtrackWrapper(string account, string password) {
- this.Account = account;
- this.Password = password;
- }
-
-
-
-
-
- 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();
- }
- }
-
- 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;
- }
- }
As you can see in the above code, we used static polymorphism to create two versions of GetResponse(), one that returns a bare response for the bare request URI, and another one that accepts a typed request object and returns a typed response object. In fact, the other version is a generic one that returns only objects that derive from ProtrackResponse.
Authorization
To call any API function you need an access token, and this can be retrieved through the authorization function. The authorization function accepts three arguments: request time (in Unix format), the username (i.e. account), and signature.
Unix Time
As described by
Wikipedia, Unix time (also known as POSIX time or UNIX Epoch time) is a system for describing a point in time. It is the number of seconds that have elapsed since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds.
Unix time format will be used throughout the API, so we have created a helper class for it,
- static class UnixTimeHelper {
-
-
-
- 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;
- }
-
-
-
- public static DateTime ToDateTime(long unixTime) {
- return new DateTime(1970, 1, 1).Add(TimeSpan.FromSeconds(unixTime));
- }
- }
You do not have to worry about leap seconds as System.DateTime does not take leap seconds into account.
Signature
Signature is an MD5 hash of a combination of MD5 password hash and request time (in Unix format). In other words:
signature = MD5 ( MD5(password) + unix_time )
Signature is represented as 32 bytes lower-case string.
Authorization Request
The authorization request class is as follows,
- internal class ProtrackAuthorizationRequest : ProtrackRequest {
-
-
-
- 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) {
-
- var signature = ProtrackHelper.HashMD5(this.Password);
- signature = ProtrackHelper.HashMD5(signature + unixTime.ToString());
- return signature;
- }
- }
As you can see, you need to provide the function path through BaseUri. And by overriding GetParams() you can provide your parameter list.
To make things work, here’s the declaration of the MD5 hashing function:
- 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 Response
The authorization response class is fairly simple. It reflects the JSON response data returned from the server. While ProtrackAuthorizationResponse focuses on authorization attributes, the base ProtrackResponse has the two common attributes, code and message.
- 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; }
- }
We tagged properties with JsonPropertyAttribute
attribute to allow our code to use different names for properties.
Connecting Things Together
Now we can add the following authorization code to the wrapper class,
- 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);
- }
Now test your code and check if everything is going well,
- var wrapper = new ProtrackWrapper("test", "123456");
- wrapper.Authorize( );
- Console.WriteLine("Authorization code is: {0}", wrapper.AccessToken);.
-
-
Tracking
Now that everything is going well, we can move next to the tracking function. The tracking function accepts one or more GPS device server IMEI codes and returns the latest coordinates for each code. Device server IMEI can be found through the Protrack web/mobile interface or through Param# command (specific GPS device models only.)
Track Request
Now that we have our requirement list, create the request class:
- 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
The response class lists the attributes that are returned from the server. I did not list all attributes, just for clarity.
- 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); } }
-
-
- }
Connecting Things Together
Now add the following code to the wrapper class. Notice how we test access token expiration before making our request:
- 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;
- }
And test:
- var track = wrapper.Track("123456789012345");
- Console.WriteLine("{0},{1},{2}", track.Latitude, track.Longitude, track.GpsTimeUtc);
-
-
One Step Further
In the previous response code as you can notice in those lines we have added two extra properties to convert Unix time to DateTime,
- public DateTime SystemTimeUtc { get { return UnixTimeHelper.ToDateTime(SystemUnixTime); } }
- public DateTime GpsTimeUtc { get { return UnixTimeHelper.ToDateTime(GpsUnixTime); } }
An alternative is to use a JSON converter,
- internal class JsonUnixTimeConverter : Newtonsoft.Json.Converters.DateTimeConverterBase {
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
- if (reader.TokenType != JsonToken.Integer)
- throw new Exception("Unexpected token type.");
-
- var unixTime = (long)reader.Value;
-
- return UnixTimeHelper.ToDateTime(unixTime);
- }
-
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
- if (false == value is DateTime)
- throw new Exception("Unexpected object type.");
-
- var dateTime = (DateTime)value;
-
- var unixTime = UnixTimeHelper.ToUnixTime(dateTime);
-
- writer.WriteValue(unixTime);
- }
- }
You will need to modify the ProtrackTrackRecord class,
- [JsonProperty("systemtime")]
- public DateTime SystemTimeUtc { get; set; }
- [JsonProperty("gpstime")]
- public DateTime GpsTimeUtc { get; set; }
And ProtrackWrapper.GetResponse<T> method,
- T rsp = JsonConvert.DeserializeObject<T>(rspStr, new JsonUnixTimeConverter());
Playback
Besides the access token, the playback method accepts single IMEI code, range start time and end time (both in Unix format.)
Playback Request
You can easily guess the request class code,
- 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
The response class is fairly simple too,
- 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]);
- }
- }
Connecting Things Together
ProtrackWrapper code,
- 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();
- }
And test,
- 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);
What’s Next
Using the above-mentioned mechanism, you can easily create wrappers for the rest of the API functions. I will be happy to receive your feedback and comments over this code.