Next
Create a mobile app
You can use Viam’s TypeScript SDK to create a custom web application to interact with your devices. The TypeScript SDK includes:
Run the following command in your terminal to install the Viam TypeScript SDK:
npm install @viamrobotics/sdk
You can find sample connection code on each machine’s CONNECT tab in the Viam app. Select TypeScript to display a code snippet, with connection code as well as some calls to the APIs of the resources you’ve configured on your machine.
You can use the toggle to include the machine API key and API key ID, though we strongly recommend storing your API keys in environment variables to reduce the risk of accidentally sharing your API key and granting access to your machines.
If your code will connect to multiple machines or use Platform APIs you can create an API key with broader access.
Refer to the Viam TypeScript SDK documentation for available methods.
The following files create an example TypeScript web app that connects to a machine and displays the latest image from the machine’s camera, and the latest sensor readings.
// This code must be run in a browser environment.
import * as VIAM from "@viamrobotics/sdk";
import { CameraClient, SensorClient } from "@viamrobotics/sdk";
let isStreaming = false;
let camera: CameraClient;
let stream: StreamClient;
let animationFrameId: number;
let machine: VIAM.RobotClient;
const main = async () => {
const host = "demo-main.abcdefg1234.viam.cloud";
const machine = await VIAM.createRobotClient({
host,
credentials: {
type: "api-key",
/* Replace "<API-KEY>" (including brackets) with your machine's API key */
payload: "<API-KEY>",
/* Replace "<API-KEY-ID>" (including brackets) with your machine's API key ID */
authEntity: "<API-KEY-ID>",
},
signalingAddress: "https://app.viam.com:443",
});
try {
// Get the camera and stream clients
camera = new CameraClient(machine, "camera-1");
// Start the camera stream
startStream();
// Get readings from sensor
const sensor = new SensorClient(machine, "sensor-1");
const readings = await sensor.getReadings();
// Create HTML to display sensor readings
const readingsHtml = document.createElement("div");
for (const [key, value] of Object.entries(readings)) {
const reading = document.createElement("p");
reading.textContent = `${key}: ${value}`;
readingsHtml.appendChild(reading);
}
// Display the readings
const readingsContainer = document.getElementById("insert-readings");
if (readingsContainer) {
readingsContainer.innerHTML = "";
readingsContainer.appendChild(readingsHtml);
}
// Add refresh button functionality
const refreshButton = document.getElementById("refresh-button");
if (refreshButton) {
refreshButton.onclick = async () => {
try {
const newReadings = await sensor.getReadings();
const readingsHtml = document.createElement("div");
for (const [key, value] of Object.entries(newReadings)) {
const reading = document.createElement("p");
reading.textContent = `${key}: ${value}`;
readingsHtml.appendChild(reading);
}
if (readingsContainer) {
readingsContainer.innerHTML = "";
readingsContainer.appendChild(readingsHtml);
}
} catch (error) {
console.error("Error refreshing sensor data:", error);
}
};
}
} catch (error) {
console.error("Error:", error);
const errorMessage = `<p>Error: ${error instanceof Error ? error.message : "Failed to get data"}</p>`;
const imageContainer = document.getElementById("insert-stream");
if (imageContainer) {
imageContainer.innerHTML = errorMessage;
}
const readingsContainer = document.getElementById("insert-readings");
if (readingsContainer) {
readingsContainer.innerHTML = errorMessage;
}
}
// Add cleanup on page unload
window.addEventListener("beforeunload", () => {
stopStream();
machine.disconnect();
});
};
const updateCameraStream = async () => {
if (!isStreaming) return;
try {
const imageContainer = document.getElementById("insert-stream");
if (imageContainer) {
// Create or update video element
let videoElement = imageContainer.querySelector("video");
if (!videoElement) {
videoElement = document.createElement("video");
videoElement.autoplay = true;
videoElement.muted = true;
imageContainer.innerHTML = "";
imageContainer.appendChild(videoElement);
}
// Get and set the stream every frame
const mediaStream = await stream.getStream("camera-1");
videoElement.srcObject = mediaStream;
// Ensure video plays
try {
await videoElement.play();
} catch (playError) {
console.error("Error playing video:", playError);
}
}
// Request next frame
animationFrameId = requestAnimationFrame(() => updateCameraStream());
} catch (error) {
console.error("Stream error:", error);
stopStream();
}
};
const startStream = () => {
// Initialize stream client
stream = new StreamClient(machine);
isStreaming = true;
updateCameraStream();
};
const stopStream = () => {
isStreaming = false;
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
main().catch((error: unknown) => {
console.error("encountered an error:", error);
});
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="main">
<div>
<h1>My Dashboard</h1>
</div>
<script type="module" src="main.js"></script>
<div>
<h2>Camera Feed</h2>
<p>Live view from the machine's camera:</p>
</div>
<div id="insert-stream">
<p>
<i
>Loading stream... It may take a few moments for the stream to load.
Do not refresh page.</i
>
</p>
</div>
<div>
<h2>Sensor Data</h2>
<p>Recent readings from the machine's sensor:</p>
</div>
<div id="insert-readings">
<p>
<i
>Loading data... It may take a few moments for the data to load. Do
not refresh page.</i
>
</p>
</div>
<br />
<button id="refresh-button">Refresh Data</button>
</div>
</body>
</html>
body {
margin: 0;
padding: 0;
background-color: #f0f2f5;
font-family: "Segoe UI", Arial, sans-serif;
color: #1a1a1a;
}
#main {
max-width: 1200px;
margin: 20px 20px;
padding: 30px;
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #2c3e50;
margin-bottom: 30px;
font-size: 2.2em;
}
h2 {
color: #34495e;
margin-top: 30px;
font-size: 1.5em;
}
div {
background-color: transparent;
}
video {
background: black;
border-radius: 8px;
}
button#refresh-button {
padding: 12px 24px;
font-size: 16px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
button#refresh-button:hover {
background-color: #45a049;
}
p {
line-height: 1.6;
color: #4a4a4a;
}
{
"name": "my-ts-dashboard",
"description": "A dashboard for getting an image from a machine.",
"scripts": {
"start": "esbuild ./main.ts --bundle --outfile=static/main.js --servedir=static --format=esm",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "<YOUR NAME>",
"license": "ISC",
"devDependencies": {
"esbuild": "*"
},
"dependencies": {
"@viamrobotics/sdk": "^0.38.0",
"bson": "^6.10.0"
}
}
For an example using Vite to connect to a machine, see Viam’s vanilla TypeScript quickstart example on GitHub.
The following tutorial uses the Viam TypeScript SDK to query data that has been uploaded to the Viam cloud from a sensor, and display it in a web dashboard.
You can run your app directly on the machine’s single-board computer (SBC) if applicable, or you can run it from a separate computer connected to the internet or to the same local network as your machine’s SBC or microcontroller. The connection code will establish communication with your machine over LAN or WAN.
You can also host your app on a server or hosting service of your choice.
Viam uses FusionAuth for authentication and authorization.
You can use Viam to authenticate end users while using a branded login screen.
Was this page helpful?
Glad to hear it! If you have any other feedback please let us know:
We're sorry about that. To help us improve, please tell us what we can do better:
Thank you!