Get Started with Photon Bridge
Communicate with scripted objects in Second Life from your Photon apps using Photon Bridge
Photon OS enables bidirectional communication between your apps and scripted objects in Second Life. Objects can register themselves with your Photon account and exchange messages in real-time.
Prerequisites
Before using device communication, you must:
- Link your Second Life account to your Photon account. (Settings > Account)
- Add the PhotonDevice.lsl script to an object you own in Second Life.
SDK API Reference
The device API is available via os.devices in the SDK.
Getting Registered Devices
Fetch all devices registered to the current user:
import { OS, type SLDevice } from "@photon-os/sdk";
const os = new OS();
const devices: SLDevice[] = await os.devices.getRegisteredDevices();
devices.forEach((device) => {
console.log(
`${device.objectName} - ${device.isOnline ? "Online" : "Offline"}`
);
});Sending Messages to Devices
Send a typed message with a JSON payload to a specific device:
const result = await os.devices.sendMessage(
deviceId, // The device ID from getRegisteredDevices()
"chat", // Message type - your scripts can filter on this
{ text: "Hello from Photon!" } // JSON payload
);
if (result.success) {
console.log("Message sent!");
} else {
console.error(result.error);
if (result.deviceOffline) {
console.log("Device appears to be offline");
}
}Subscribing to Device Messages
Listen for messages sent from a device back to your app:
const unsubscribe = os.devices.subscribeToMessages(deviceId, (message) => {
console.log(`Received ${message.type} from ${message.objectName}`);
console.log("Payload:", message.payload);
});
// Later, to stop listening:
unsubscribe();Unregistering Devices
Remove a device from your account:
await os.devices.unregisterDevice(deviceId);Type Reference
SLDevice
interface SLDevice {
id: string; // Unique device ID
objectKey: string; // SL object UUID
objectName: string; // Name of the object in SL
regionName?: string; // SL region name (if available)
isOnline: boolean; // Whether device is currently reachable
lastHeartbeatAt: Date; // Last heartbeat timestamp
registeredAt: Date; // When device was registered
metadata?: Record<string, unknown>; // Optional metadata
}DeviceMessage
interface DeviceMessage {
deviceId: string; // ID of the device that sent the message
objectKey: string; // SL object UUID
objectName: string; // Name of the object in SL
type: string; // Message type defined by your script
payload: Record<string, unknown>; // JSON payload
timestamp: Date; // When the message was received
}SendMessageResult
interface SendMessageResult {
success: boolean; // Whether message was delivered
error?: string; // Error message if failed
deviceOffline?: boolean; // True if device couldn't be reached
}LSL Scripts
PhotonDevice.lsl
This is the core library script. Drop it into any object to enable Photon connectivity. The script auto-registers on startup and handles heartbeats automatically.
Requirements:
- Object owner must have their SL account linked to their Photon OS account
// ============================================
// PhotonDevice.lsl - Photon OS Device Library
// ============================================
// Drop this script into any object to enable
// Photon connectivity. The script auto-registers
// on startup. Other scripts in the object
// communicate via link messages.
//
// REQUIREMENTS:
// - Object owner must have their SL account linked
// to their Photon OS account first
//
// USAGE:
// The script registers automatically on startup.
// Send link messages to this script:
// llMessageLinked(LINK_SET, PHOTON_SEND, "message_type", "json_payload");
// llMessageLinked(LINK_SET, PHOTON_STATUS, "", ""); // Request current status
//
// Receive link messages from this script:
// PHOTON_REGISTERED - Registration confirmed (key = device_id)
// PHOTON_OFFLINE - Connection lost
// PHOTON_RECEIVE - Message received (str = type, key = payload JSON)
// PHOTON_ERROR - Error occurred (key = error message)
// ===================
// Link Message Channels
// ===================
// Inbound (to PhotonDevice)
integer PHOTON_REGISTER = 90001; // Request registration
integer PHOTON_SEND = 90002; // Send message to Photon
integer PHOTON_STATUS = 90003; // Request current status
// Outbound (from PhotonDevice)
integer PHOTON_REGISTERED = 90010; // Registration confirmed (key = device_id)
integer PHOTON_OFFLINE = 90011; // Connection lost
integer PHOTON_RECEIVE = 90012; // Message received (str = type, key = payload JSON)
integer PHOTON_ERROR = 90019; // Error occurred (key = error message)
// ===================
// Configuration
// ===================
string SUPABASE_URL = "https://rujeueevbvywlfrcjeya.supabase.co/functions/v1";
integer HEARTBEAT_INTERVAL = 300; // 5 minutes
// ===================
// State Variables
// ===================
string gCallbackUrl = "";
string gDeviceId = "";
integer gRegistered = FALSE;
// Request tracking
key gRegisterRequest = NULL_KEY;
key gHeartbeatRequest = NULL_KEY;
key gMessageRequest = NULL_KEY;
// ===================
// Helper Functions
// ===================
broadcast(integer channel, string str, key id)
{
llMessageLinked(LINK_SET, channel, str, id);
}
requestRegistration()
{
if (gCallbackUrl == "")
{
// Request URL first - registration will happen when we get it
llRequestURL();
return;
}
string body = llList2Json(JSON_OBJECT, [
"callback_url", (string)gCallbackUrl
]);
gRegisterRequest = llHTTPRequest(
SUPABASE_URL + "/register-sl-device",
[HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json"],
body
);
}
sendHeartbeat()
{
if (gCallbackUrl == "") return;
string body = llList2Json(JSON_OBJECT, [
"callback_url", (string)gCallbackUrl
]);
gHeartbeatRequest = llHTTPRequest(
SUPABASE_URL + "/sl-device-heartbeat",
[HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json"],
body
);
}
sendMessageToPhoton(string msgType, string payload)
{
if (!gRegistered)
{
broadcast(PHOTON_ERROR, "error", "Not registered");
return;
}
string body = llList2Json(JSON_OBJECT, [
"type", msgType,
"payload", payload
]);
gMessageRequest = llHTTPRequest(
SUPABASE_URL + "/sl-device-message",
[HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json"],
body
);
}
markOffline()
{
gRegistered = FALSE;
gDeviceId = "";
llSetTimerEvent(0.0);
broadcast(PHOTON_OFFLINE, "offline", "");
}
// ===================
// Main State
// ===================
default
{
state_entry()
{
// Request a URL for incoming HTTP requests
llRequestURL();
}
on_rez(integer param)
{
// Reset state when rezzed - will auto-register when URL is granted
gCallbackUrl = "";
gRegistered = FALSE;
gDeviceId = "";
llSetTimerEvent(0.0);
broadcast(PHOTON_OFFLINE, "offline", "");
llRequestURL();
}
changed(integer change)
{
if (change & CHANGED_REGION)
{
// URL is invalid after region change
gCallbackUrl = "";
markOffline();
llRequestURL();
}
if (change & CHANGED_OWNER)
{
// New owner needs to re-register
llResetScript();
}
}
// ===================
// URL Events
// ===================
http_request(key id, string method, string body)
{
if (method == URL_REQUEST_GRANTED)
{
// The actual URL is in the body parameter, id is just a handle
gCallbackUrl = body;
// Auto-register when we get a URL
requestRegistration();
return;
}
if (method == URL_REQUEST_DENIED)
{
llOwnerSay("[Photon] ERROR: URL request denied - no available URLs");
broadcast(PHOTON_ERROR, "error", "URL request denied - no available URLs");
return;
}
// Incoming HTTP request from Photon
if (method == "POST")
{
// Parse the message
string msgType = llJsonGetValue(body, ["type"]);
string payload = llJsonGetValue(body, ["payload"]);
if (msgType == JSON_INVALID)
{
llHTTPResponse(id, 400, "Invalid request");
return;
}
// Broadcast to other scripts
broadcast(PHOTON_RECEIVE, msgType, payload);
llHTTPResponse(id, 200, "OK");
return;
}
// Unknown request
llHTTPResponse(id, 405, "Method not allowed");
}
// ===================
// HTTP Responses
// ===================
http_response(key id, integer status, list metadata, string body)
{
// Registration response
if (id == gRegisterRequest)
{
gRegisterRequest = NULL_KEY;
if (status == 200)
{
string success = llJsonGetValue(body, ["success"]);
if (success == JSON_TRUE)
{
gDeviceId = llJsonGetValue(body, ["device_id"]);
gRegistered = TRUE;
llSetTimerEvent(HEARTBEAT_INTERVAL);
broadcast(PHOTON_REGISTERED, "registered", gDeviceId);
return;
}
}
// Registration failed
string error = llJsonGetValue(body, ["error"]);
if (error == JSON_INVALID) error = "Unknown error";
llOwnerSay("[Photon] ERROR: Registration failed (HTTP " + (string)status + "): " + error);
broadcast(PHOTON_ERROR, "register_failed", error);
return;
}
// Heartbeat response
if (id == gHeartbeatRequest)
{
gHeartbeatRequest = NULL_KEY;
if (status != 200)
{
// Heartbeat failed - might need to re-register
string error = llJsonGetValue(body, ["error"]);
if (error == JSON_INVALID) error = "Unknown error";
llOwnerSay("[Photon] WARNING: Heartbeat failed (HTTP " + (string)status + "): " + error);
broadcast(PHOTON_ERROR, "heartbeat_failed", error);
// Don't mark offline yet - could be temporary
}
return;
}
// Message send response
if (id == gMessageRequest)
{
gMessageRequest = NULL_KEY;
if (status != 200)
{
string error = llJsonGetValue(body, ["error"]);
if (error == JSON_INVALID) error = "Unknown error";
llOwnerSay("[Photon] ERROR: Send message failed (HTTP " + (string)status + "): " + error);
broadcast(PHOTON_ERROR, "send_failed", error);
}
return;
}
}
// ===================
// Link Messages (from other scripts)
// ===================
link_message(integer sender, integer num, string str, key id)
{
if (num == PHOTON_REGISTER)
{
requestRegistration();
return;
}
if (num == PHOTON_SEND)
{
sendMessageToPhoton(str, (string)id);
return;
}
if (num == PHOTON_STATUS)
{
// Respond with current status
if (gRegistered)
{
broadcast(PHOTON_REGISTERED, "registered", gDeviceId);
}
else
{
broadcast(PHOTON_OFFLINE, "offline", "");
}
return;
}
}
// ===================
// Heartbeat Timer
// ===================
timer()
{
sendHeartbeat();
}
}PhotonChat.lsl
This is an example companion script that receives "chat" messages and echoes them back. Add this alongside PhotonDevice.lsl to test messaging.
// ============================================
// PhotonChat.lsl - Chat Output for Photon
// ============================================
// Add this script alongside PhotonDevice.lsl
// to output received "chat" messages to local chat
// and echo them back to the sender.
// PhotonDevice link message channels
integer PHOTON_SEND = 90002;
integer PHOTON_RECEIVE = 90012;
default
{
link_message(integer sender, integer num, string str, key id)
{
if (num == PHOTON_RECEIVE && str == "chat")
{
// Parse the text from the payload
string text = llJsonGetValue((string)id, ["text"]);
if (text != JSON_INVALID && text != "")
{
// Say in local chat
llSay(0, text);
// Echo back to the app
string echoPayload = llList2Json(JSON_OBJECT, [
"original", text,
"echo", "Echo: " + text
]);
llMessageLinked(LINK_SET, PHOTON_SEND, "echo", echoPayload);
}
}
}
}Link Message Protocol
PhotonDevice.lsl communicates with other scripts in the same object using link messages. Use these channel numbers:
Inbound Channels (to PhotonDevice)
| Channel | Constant | Purpose |
|---|---|---|
| 90001 | PHOTON_REGISTER | Request re-registration |
| 90002 | PHOTON_SEND | Send message to Photon app |
| 90003 | PHOTON_STATUS | Request current status |
Outbound Channels (from PhotonDevice)
| Channel | Constant | Purpose |
|---|---|---|
| 90010 | PHOTON_REGISTERED | Registration confirmed |
| 90011 | PHOTON_OFFLINE | Connection lost |
| 90012 | PHOTON_RECEIVE | Message received from app |
| 90019 | PHOTON_ERROR | Error occurred |
Message Formats
Sending a message to your app:
string payload = llList2Json(JSON_OBJECT, [
"key1", "value1",
"key2", "value2"
]);
llMessageLinked(LINK_SET, PHOTON_SEND, "my_message_type", payload);Receiving a message from your app:
link_message(integer sender, integer num, string str, key id)
{
if (num == PHOTON_RECEIVE)
{
// str = message type (e.g., "chat")
// id = JSON payload (cast to string to parse)
string payload = (string)id;
string value = llJsonGetValue(payload, ["key1"]);
}
}Building Custom Scripts
To create your own companion script:
- Define the link message channels at the top of your script:
integer PHOTON_SEND = 90002;
integer PHOTON_RECEIVE = 90012;- Listen for
PHOTON_RECEIVEto handle incoming messages:
link_message(integer sender, integer num, string str, key id)
{
if (num == PHOTON_RECEIVE && str == "your_type")
{
// Handle the message
}
}- Use
PHOTON_SENDto send messages back to your app:
string payload = llList2Json(JSON_OBJECT, ["status", "ok"]);
llMessageLinked(LINK_SET, PHOTON_SEND, "response", payload);Example: LED Controller
// Control an object's color from your Photon app
integer PHOTON_SEND = 90002;
integer PHOTON_RECEIVE = 90012;
default
{
link_message(integer sender, integer num, string str, key id)
{
if (num == PHOTON_RECEIVE && str == "led")
{
string color = llJsonGetValue((string)id, ["color"]);
if (color == "red")
llSetColor(<1,0,0>, ALL_SIDES);
else if (color == "green")
llSetColor(<0,1,0>, ALL_SIDES);
else if (color == "blue")
llSetColor(<0,0,1>, ALL_SIDES);
// Confirm the change
string response = llList2Json(JSON_OBJECT, [
"color", color,
"status", "applied"
]);
llMessageLinked(LINK_SET, PHOTON_SEND, "led_status", response);
}
}
}Then in your app:
// Send color command
await os.devices.sendMessage(deviceId, "led", { color: "red" });
// Listen for confirmation
os.devices.subscribeToMessages((msg) => {
if (msg.type === "led_status") {
console.log(`LED set to ${msg.payload.color}`);
}
});System Messages
Photon OS reserves message types prefixed with photon: for system-level functionality. These messages are handled directly by the OS rather than being forwarded to apps.
Suggesting App Installation
Devices can prompt users to install an app on their Photon Tool by sending a photon:suggest_app_install message. This displays an install confirmation dialog showing which device is making the suggestion.
Message Format:
| Field | Type | Description |
|---|---|---|
bundleId | string | Unique app identifier |
name | string | Display name for the app |
author | string | App author/developer name |
url | string | URL where the app is hosted |
SuggestAppInstall.lsl
This example script demonstrates how to suggest an app installation. When the owner touches the object, it sends the install suggestion to their Photon Tool.
// ============================================
// SuggestAppInstall.lsl - Example Script
// ============================================
// This script demonstrates how to prompt the
// device owner to install an app on their
// Photon Tool.
//
// REQUIREMENTS:
// - PhotonDevice.lsl must be in the same object
// - Object owner must have their SL account linked
// to their Photon OS account
//
// USAGE:
// Touch the object to suggest installing the app.
// Link message channels (must match PhotonDevice.lsl)
integer PHOTON_SEND = 90002;
integer PHOTON_STATUS = 90003;
integer PHOTON_REGISTERED = 90010;
integer PHOTON_OFFLINE = 90011;
integer PHOTON_ERROR = 90019;
// Track registration state
integer gRegistered = FALSE;
// App details - customize these for your app
string APP_BUNDLE_ID = "com.example.myapp";
string APP_NAME = "My Cool App";
string APP_AUTHOR = "Your Name";
string APP_URL = "https://example.com/myapp";
suggestAppInstall()
{
if (!gRegistered)
{
llOwnerSay("Not connected to Photon yet. Please wait...");
return;
}
string payload = llList2Json(JSON_OBJECT, [
"bundleId", APP_BUNDLE_ID,
"name", APP_NAME,
"author", APP_AUTHOR,
"url", APP_URL
]);
llMessageLinked(LINK_SET, PHOTON_SEND, "photon:suggest_app_install", payload);
llOwnerSay("Sent app install suggestion to your Photon Tool!");
}
default
{
state_entry()
{
llOwnerSay("Touch me to suggest installing " + APP_NAME);
// Request current status in case PhotonDevice already registered
llMessageLinked(LINK_SET, PHOTON_STATUS, "", "");
}
touch_start(integer num)
{
if (llDetectedKey(0) == llGetOwner())
{
suggestAppInstall();
}
}
link_message(integer sender, integer num, string str, key id)
{
if (num == PHOTON_REGISTERED)
{
gRegistered = TRUE;
llOwnerSay("Connected to Photon! Touch me to suggest installing " + APP_NAME);
}
else if (num == PHOTON_OFFLINE)
{
gRegistered = FALSE;
}
else if (num == PHOTON_ERROR)
{
llOwnerSay("Photon error: " + (string)id);
}
}
}Key points:
- Uses
PHOTON_STATUSon startup to check if already registered (handles race condition with PhotonDevice.lsl) - Sends the
photon:suggest_app_installmessage type with app details as JSON payload - The user sees a dialog showing the device name and app info before confirming installation