TMM API Integration Guide

Using the TMM API in your application.

Summary

The TMM API allows your application to configure Trimble and Spectra Geospatial GNSS receivers, and get precise GNSS positions in your application.

The TMM API consists of:

  • The Trimble Mobile Manager application
  • Platform specific methods for showing TMM user interface
    • Android: Intents
    • iOS: Custom URL Schemes
    • Windows: Custom URI Scheme
  • A REST API on localhost for TMM operations that can be performed without showing TMM user interface
  • A WebSocket on localhost for streaming precise GNSS positions from a connected Trimble or Spectra Geospatial GNSS receiver.

To use the TMM API, you need to:

  1. Contact Trimble and get a unique Application ID for your application.
  2. Register your Application ID with Trimble Mobile Manager (TMM) using the platform specific method described below. This will do two things:
    • Add your application to TMM’s client application registry.
    • Return the localhost port of the REST API server.
  3. Use the positionStream REST API endpoint to start streaming positions, and get the WebSocket Position port.
  4. Open the WebSocket Position port and start receiving precise GNSS positions in JSON format.

Client Application Registration

Before you can use TMM’s REST API, you must register your application with TMM. On Android this is done by sending the REGISTER intent. On iOS this is done using the tmmregister URL scheme. On Windows this is done by sending the tmmRegister URI Request. Registration accomplishes three things:

  • It launches TMM if TMM is not already running.
  • It registers your Application ID with TMM so you can use the REST API.
  • It returns a list of ports for interacting with TMM.

Android: REGISTER Intent

On Android, TMM uses the REGISTER Intent mechanism to register client applications.

Setup (Java)

Intent intent = new Intent("com.trimble.tmm.REGISTER")
intent.putExtra("applicationID", applicationID);
startActivityForResult(intent, registerRequestCode);

Response (Java)

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == registerRequestCode) {
if (resultCode == RESULT_OK) {
// registrationResult:
// OK: Your app is registered
// Unauthorized: TMM was not able to verify the Application ID
// NoNetwork: There is not internet connection
String result = data.getStringExtra("registrationResult");
// localhost port for WebSocket Locations, Version 1
int PositionsPort = data.getIntExtra("locationPort", -1);
// localhost port for WebSocket Locations, Version 2
int PositionsV2Port = data.getIntExtra("locationV2Port", -1);
// The REST API is at $"WS://localhost:{apiPort}"
int apiPort = data.getIntExtra("apiPort", -1);
}
}
}

iOS: tmmregister URL Scheme

Use the tmmregister URL scheme to register your client app with TMM.

Setup (Swift)

First you will open the tmmregister URL scheme, supplying:

  • application_id: String, your assigned application ID from Trimble
  • returnurl: the callback URL for your own app that TMM will open when the registration operation is complete.
var params: [String: String] =
[
"application_id": myApplicationID,
"returnurl": "ThisAppCallback://com.mycompany.ThisApp"
]
let jsonData = try JSONSerialization.data(withJSONObject: params, options: [])
let jsonString = String(data: jsonData, encoding: .utf8)
let base64Encoded = jsonString?.data(using: .utf8)?.base64EncodedString()
let customUrl = URL(string: "tmmregister://?" + base64Encoded!)!
UIApplication.shared.open(customUrl) { (success) in
if success{
// The URL was delivered successfully
}
}

Response (Swift)

When TMM has finished it will return control back to your application by opening the previously provided returnurl. Add code to your app delegate to handle the incoming callback url.

func application(_ application: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool {
do {
let query = url.query()!
let data = Data(base64Encoded: query)!
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
let results = (jsonObject as? [String: String])!
// registrationResult:
// OK: Your app is registered
// Unauthorized: TMM was not able to verify the Application ID
// NoNetwork: There is not internet connection
let registrationResult = results["registrationResult"]!
// localhost port for WebSocket Locations, Version 1
let locationPort = Int(results["locationPort"]!)
// localhost port for WebSocket Locations, Version 2
let locationV2Port = Int(results["locationV2Port"]!)
// The REST API is at $"WS://localhost:{apiPort}"
let apiPort = Int(results["apiPort"]!)
return true;
} catch {
return false;
}
}

Windows: tmmRegister URI Request

TMM registers a custom URI scheme with Windows for handling requests from client applications. The calling application supplies a callback URI, which TMM will launch when it has finished processing the request.

Use the tmmRegister request to register your client application with TMM to use TMM API.

Setup (C#)

Package.appxmanifest

First, register your own custom URI scheme with Windows so that you can receive the response from TMM. Do this by adding a windows.protocol extension to your Application in Package.appxmanifest file.

<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<Extensions>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="myapp">
<uap:DisplayName>My TMM Client App</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>
Launching the URI

Use the Windows URI launcher to send the tmmRegister request. Windows will route the URI to an instance of TMM.

public async Task RegisterWithTMMAsync()
{
// Your Application ID GUID, assigned by Trimble
string applicationId = "your_app_id_guid_here";
// A callback URI that TMM will invoke to send the response
// back to your application.
string callbackUri = Uri.EscapeDataString("myapp://response/tmmRegister");
// Format the URI request
string requestString = $"trimbleMobileManager://request/tmmRegister?applicationId={applicationId}&callback={callbackUri}";
Uri requestUri = new Uri(requestString);
// Launch the URI
if (await Launcher.Default.CanOpenAsync(requestUri))
{
bool result = await Launcher.Default.OpenAsync(requestUri);
}
}

Response (C#, MAUI WinUI)

To get a response back from the TMM request URI scheme, you must register your own app’s URI scheme, and then handle the incoming URI from your app. Below we show how to do this in a .NET MAUI Windows app, but the procedure is similar for any Windows app.

App.xaml.windows.cs
public partial class App : MauiWinUIApplication
{
public App()
{
InitializeComponent();
// Windows will launch a new instance of TMM with every URI activation. We only want the
// 'main' instance to handle the URI activation.
var mainInstance = AppInstance.FindOrRegisterForKey("a unique identifier for my app");
if (mainInstance.IsCurrent)
{
// This is the 'main' instance handle the URI
AppInstance.GetCurrent().Activated += OnActivated;
var args = AppInstance.GetCurrent().GetActivatedEventArgs();
HandleProtocolActivation(args);
}
else
{
// This is not the 'main' instance. Redirect the URI to the 'main'
// instance, end kill this instance.
await mainInstance.RedirectActivationToAsync(AppInstance.GetCurrent().GetActivatedEventArgs());
Process.GetCurrentProcess().Kill();
}
}
private void OnActivated(object sender, AppActivationArguments args)
{
HandleProtocolActivation(args);
}
private void HandleProtocolActivation(AppActivationArguments args)
{
if (args.Kind == ExtendedActivationKind.Protocol && args.Data is ProtocolActivatedEventArgs protocolArgs)
{
Uri uri = protocolArgs.Uri;
if (uri.AbsolutePath.StartsWith("myapp://response/tmmRegister"))
{
// this is the callbackUri you sent to TMM earlier
NameValueCollection queryDictionary = HttpUtility.ParseQueryString(uri.Query);
string id = queryDictionary["id"]; // "tmmRegister"
string status = queryDictionary["status"]; // “success” or “error”
string message = queryDictionary["message "]; // Additional information
string registrationResult = queryDictionary["registrationResult"]; // “OK”, “NoNetwork”, or “Unauthorized”
int.TryParse(queryDictionary["apiPort"], out var apiPort); // The REST API is at $"WS://localhost:{apiPort}"
}
}
}
}

REST API

Now that you’ve registered your application with TMM, you can start sending requests to the REST API.

Access Codes

A valid access code is required to grant access to TMM’s REST API.

  • Access codes are generated using a combination of your Application ID, and the current time.
  • The access code is added to the HTTP request in the Authorization header.
  • An access code is valid for 1 second after generation.
  • A new access code should be generated for every REST API call.

Access Code Generation (C#):

public static string GenerateAccessCode(string appID, DateTime utcTime)
{
string lowercaseID = appID.ToLowerInvariant();
// Format utcTime as an ISO8601 compliant string, like this:
// 2024-03-15T18:42:31Z
string iso8601Time = utcTime.ToString("yyyy-MM-dd'T'HH:mm:ssK", CultureInfo.InvariantCulture);
string plaintextAccessCode = lowercaseID + iso8601Time;
byte[] utf8Bytes = Encoding.UTF8.GetBytes(plaintextAccessCode);
byte[] hashedBytes = SHA256.HashData(utf8Bytes);
string base64String = Convert.ToBase64String(hashedBytes);
return base64String;
}

To test and verify your access code generation code, we have provided you with access-code-gold-file.json, which contains a set of random ApplicationID + UTC Time and the access code they should generate. The data looks like this:

{
"ApplicationID": "ce1e7a1e-3128-4f63-b829-223c7cb7ca1d",
"UtcTime": "2074-03-16T08:46:27Z",
"AccessCode": "BF4hkCZWjxyw2WN9xVPHj7gpyxrS3CRUX0BaXutzw14="
}

HTTP Authorization Header

When you have generated an access code, insert it into the HTTP Authorization header, using the Basic scheme. Upon receiving the request, TMM will validate the Access Code against all registered client applications. If the Code checks out, then TMM will process the request. If the code does not check out, then TMM will respond with 401 Unauthorized.

Example Request (http://localhost:9637/api/v1/tmmInfo):

GET api/v1/tmmInfo HTTP/1.1
Authorization: Basic BF4hkCZWjxyw2WN9xVPHj7gpyxrS3CRUX0BaXutzw14=

WebSocket Positions

Start the Position Stream (C#)

Now that your client app is registered, you can use the positionStream REST API endpoint to start the WebSocket position stream.

private async Task<int> GetPositionStreamPortAsync()
{
// prerequisites
string appID = ""; // your app's client app ID
int apiPort = 0; // The API Port as received from client app registration earlier
// set up the HTTP client
HttpClient client = new HttpClient
{
BaseAddress = new Uri($"http://localhost:{apiPort}/"),
Timeout = TimeSpan.FromSeconds(30);
};
// generate the access code and put it in the authorization header
string accessCode = GenerateAccessCode(appID, DateTime.UtcNow);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", accessCode);
// send the request
string url = $"api/v1/positionStream?format=locationV2";
HttpResponseMessage response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode)
throw new Exception("Failed to get position stream port");
// parse the response
string jsonString = await response.Content.ReadAsStringAsync();
JsonNode? jnode = JsonNode.Parse(jsonString);
if (jnode is null)
throw new Exception("Failed to parse position stream port");
int port = jnode["port"]?.GetValue<int>() ?? 0;
return port;
}

Reading WebSocket Positions (C#)

Now that you have the WebSocket port for the position stream, you can open the WebSocket and start reading positions. See v2SocketPort for more details on the JSON format.

private async Task ReadPositionsAsync(CancellationToken cancel)
{
// query the position port
int port = await GetPositionStreamPortAsync();
// connect to the WebSocket
using ClientWebSocket client = new ClientWebSocket();
await client.ConnectAsync(new Uri($"ws://localhost:{port}"), cancel);
while (!cancel.IsCancellationRequested)
{
// read the next position
var data = new ArraySegment<byte>(new byte[10240]);
WebSocketReceiveResult result = await client.ReceiveAsync(data, cancel);
if (result.MessageType == WebSocketMessageType.Close)
{
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
break;
}
if (result.Count > 0 && result.MessageType == WebSocketMessageType.Text)
{
// parse the position data
string jsonString = Encoding.UTF8.GetString(data.ToArray(), 0, result.Count);
JsonNode? jnode = JsonNode.Parse(jsonString);
if (jnode is not null)
{
double? latitude = jnode["latitude"]?.GetValue<double>();
double? longitude = jnode["longitude"]?.GetValue<double>();
double? altitude = jnode["altitude"]?.GetValue<double>();
System.Diagnostics.Debug.WriteLine($"Position: {latitude}, {longitude}, {altitude}");
}
}
}
}