Draft page
Access: Prod

Simulate a Device Moving Along a GPS Route

This tutorial walks through building an automation that moves a webmate-deployed device along a real-world GPS route at a configurable speed, with smooth interpolation between waypoints so the device never appears to teleport.

Goal

By the end of this article you will have a self-contained TypeScript script that:

  • requests an Android device through the webmate SDK,
  • attaches to it with an Appium driver,
  • and feeds it a continuous stream of GPS fixes that trace a list of waypoints at a target speed (km/h).

This is the foundation for testing navigation apps, fitness and sports trackers, ride-share and delivery flows, and any feature that depends on geofences or trip detection. Naive setLocation(start) → setLocation(end) calls produce visible teleports that break step-counters, route-matching, and trip-recording logic; this suite shows how to avoid that.

Before You Start

You need:

  • A webmate account with an API key — see Generate an API Key .
  • A project ID in which to deploy the device.
  • Node.js 18 or newer and a working TypeScript toolchain.
  • The webmate-sdk-js package. The fastest starting point is the TypeScript SDK samples repository (opens in a new tab) — clone it and add this suite as a new test file.
  • An Appium-aware WebDriver client. The samples repo already pulls in selenium-webdriver, which is W3C compatible and can talk to the Appium server that backs webmate mobile sessions — that is what the code below assumes.

Design the Route

A route is just an ordered list of waypoints plus a target ground speed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type LatLon = { lat: number; lon: number };

// A short loop around Alexanderplatz in Berlin.
const ROUTE: LatLon[] = [
  { lat: 52.5219, lon: 13.4132 },
  { lat: 52.5225, lon: 13.4145 },
  { lat: 52.5231, lon: 13.4158 },
  { lat: 52.5237, lon: 13.4171 },
];

const SPEED_KMH = 30;

Make It Smooth

The interesting part of this suite is not pushing a coordinate to the device — that is one Appium call. The interesting part is producing a believable sequence of coordinates.

Three rules cover most cases:

  1. Fix a tick interval. Real GPS chipsets emit fixes at roughly 1 Hz. Match that. A 1-second tick keeps map UIs smooth and avoids hammering the API; a 200 ms tick looks identical to the user but quadruples request volume.
  2. Measure each leg in meters. Use the haversine formula to get the great-circle distance between two waypoints. This is what lets you translate “30 km/h” into “how far should I advance this tick”.
  3. Interpolate along the leg. At each tick, advance a progress fraction t ∈ [0, 1] along the current leg by (speed_mps × dt) / leg_length_m, then linearly interpolate latitude and longitude between the leg’s endpoints. When t overflows past 1, carry the remainder into the next leg so the cadence stays smooth across waypoint boundaries.

Linear interpolation of lat/lon is fine for legs up to a few kilometers at human or vehicle speeds. For continent-scale legs you would want proper great-circle interpolation, but that is overkill here.

If you want to polish the result further, apply an ease-in/ease-out curve to the first and last few seconds of the full route, so the device does not jump from stationary straight to cruising speed. This matters mostly for fitness apps that record acceleration.

Push Updates to the Device

webmate exposes GPS simulation as a device capability (sensorsimulation.gps), but the JavaScript SDK does not currently wrap it with a convenience method. The simplest cross-platform path is to send the Appium mobile: setLocation extension command through any W3C WebDriver client — Appium’s bridge accepts it on both UIA2 (Android) and XCUITest (iOS):

1
await driver.executeScript("mobile: setLocation", [{ latitude, longitude }]);

Use the SDK to deploy the device and pull the Selenium / Appium endpoint, then hand control to the driver for the location stream. This mirrors the pattern used in the existing sample at webmate-sdk-js-samples/src/device-interaction.test.ts, which deploys a device via Webmate.startSession(...) and DeviceRequest before driving it.

Complete Sample

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
import * as Webmate from "webmate-sdk-js";
import { DeviceRequest, Platform, PlatformType } from "webmate-sdk-js";
import { Builder, ThenableWebDriver } from "selenium-webdriver";
import {
  MY_WEBMATE_APIKEY,
  MY_WEBMATE_PROJECTID,
  WEBMATE_API_URL,
  WEBMATE_SELENIUM_ADDRESS,
} from "./credentials";

type LatLon = { lat: number; lon: number };

const ROUTE: LatLon[] = [
  { lat: 52.5219, lon: 13.4132 },
  { lat: 52.5225, lon: 13.4145 },
  { lat: 52.5231, lon: 13.4158 },
  { lat: 52.5237, lon: 13.4171 },
];

const SPEED_KMH = 30;
const TICK_MS = 1000;

export function haversineMeters(a: LatLon, b: LatLon): number {
  const R = 6_371_000;
  const toRad = (d: number) => (d * Math.PI) / 180;
  const dLat = toRad(b.lat - a.lat);
  const dLon = toRad(b.lon - a.lon);
  const lat1 = toRad(a.lat);
  const lat2 = toRad(b.lat);
  const h =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(h));
}

export function interpolate(a: LatLon, b: LatLon, t: number): LatLon {
  return {
    lat: a.lat + (b.lat - a.lat) * t,
    lon: a.lon + (b.lon - a.lon) * t,
  };
}

async function setLocation(driver: ThenableWebDriver, p: LatLon): Promise<void> {
  await driver.executeScript("mobile: setLocation", [{ latitude: p.lat, longitude: p.lon }]);
}

export async function driveRoute(
  driver: ThenableWebDriver,
  waypoints: LatLon[],
  speedKmh: number,
  tickMs: number = TICK_MS,
): Promise<void> {
  const speedMps = (speedKmh * 1000) / 3600;
  const stepMeters = speedMps * (tickMs / 1000);

  for (let i = 0; i < waypoints.length - 1; i++) {
    const from = waypoints[i];
    const to = waypoints[i + 1];
    const legMeters = haversineMeters(from, to);
    let traveled = 0;

    while (traveled < legMeters) {
      const t = Math.min(traveled / legMeters, 1);
      await setLocation(driver, interpolate(from, to, t));
      await new Promise((r) => setTimeout(r, tickMs));
      traveled += stepMeters;
    }
  }

  // Snap to the final waypoint so the device ends exactly on the route.
  await setLocation(driver, waypoints[waypoints.length - 1]);
}

async function main(): Promise<void> {
  const session = Webmate.startSession(MY_WEBMATE_APIKEY, WEBMATE_API_URL, MY_WEBMATE_PROJECTID);

  const platform = new Platform(PlatformType.ANDROID, "13");
  const request = new DeviceRequest(
    "GPS Route Suite",
    new Map([["machine.platform", platform.toString()]]),
  );
  const device = await session.device
    .requestDeviceByRequirements(MY_WEBMATE_PROJECTID, request)
    .toPromise();

  const driver = new Builder()
    .usingServer(WEBMATE_SELENIUM_ADDRESS)
    .withCapabilities({
      platformName: "Android",
      "appium:automationName": "UiAutomator2",
      "webmate:apikey": MY_WEBMATE_APIKEY,
      "webmate:projectId": MY_WEBMATE_PROJECTID,
      "webmate:deviceId": device!.id,
    })
    .build();

  try {
    await driveRoute(driver, ROUTE, SPEED_KMH);
  } finally {
    await driver.quit();
    await session.device.releaseDevice(device!.id).toPromise();
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Verify the Result

Open the running device in the webmate Live View (or launch a map app on it before starting the script) and watch the blue dot trace the route. The motion should look continuous — no teleports between waypoints, and no stalls at corners.

Two tuning levers to keep in mind:

  • A tick interval larger than ~2 seconds will look choppy to the user and may cause map-matching libraries to drop fixes.
  • A tick interval below ~250 ms quadruples API volume for no perceivable gain, and can rate-limit your session.

If the route looks fine but your application under test still sees teleports, check that the app actually listens to the OS-level fused location provider and is not caching its own last-known position.

Next Steps

Two extensions are planned for this suite:

  • OSM-routed paths. Replace the hand-supplied waypoint list with a call to a routing service — OSRM, GraphHopper, or Valhalla — that takes a start and end point and returns a dense polyline. Feed that polyline into driveRoute unchanged.
  • Mixing GPS with other sensor simulation. Combine the GPS stream with camera simulation so a “scan a delivery QR code while standing at the destination” flow can be exercised end-to-end. The existing camera-simulation sample at webmate-sdk-js-samples/src/device-interaction.test.ts shows the API shape to extend.