*/ aeros Unified API lets you send commands in the Obsidian API syntax and
have them reach both Obsidian and Legacy (pre-2025) Aeros devices. Incoming
notifications from either device type come back to you in a unified
Obsidian-style format.
The original API Script may eventually become replaced with the */ aeros Unified API.
What it does:
Outgoing Obsidian commands are automatically translated to the Legacy protocol
when the target is a legacy device.
Incoming legacy notifications are translated into Obsidian !key=val format for
unified consumption.
What it does not do:
It does not let you aim Obsidian devices using Legacy API syntax.
This is a WIP and may contain bugs. Please report issues to Pi Rain.
You always send in Obsidian format. The API detects whether each target is an
Obsidian or Legacy device and routes/translates accordingly.
Outgoing
To send commands use the secret, located in the notecard sent to you with the Unified API Script.
string creator_SHARED_SECRET = "<provided by Aeros team>";
Use this function to send commands to both Legacy and Obsidian devices using Obsidian API syntax:
// aeros_obsidian_send is used for Obsidian API syntax. It adds verification of the message source// with a signature from `creator_SHARED_SECRET` global constant known by both the Unified API Script// and store creator.//// * `target` - Legacy Device or Obsidian Device Key (or the owner key to broadcast)// * `payload` - Obsidian API syntax payload. Example: `"v=0.5;h=0"`aeros_obsidian_send(key target, string payload) { string nonce = (string)llFrand(1.0); llMessageLinked(LINK_THIS, -78000, llSHA1String(payload + ":" + nonce + ":" + creator_SHARED_SECRET) + "|" + nonce + "|" + payload, target);}
Example:
aeros_obsidian_send(llGetOwner(), "v=0.5");
Example:
(Ping the owner - you can provide any key - for their Aeros Obsidian or legacy devices):
aeros_obsidian_send(llGetOwner(), "ack=test");
This should return a message containing "!ack=test" among the key-value ;-separated values if either Obsidian or legacy device worn by the
target owner.
Incoming
To consume notifications “listen” to them as linked messages:
link_message(integer sender_num, integer num, string str, key id) { if (num == -78001) { // Parse notifications and read the values your script is interested in // API Reference: // https://docs.meshtoolbox.com/aeros/obsidian/api/reference#notifications } }
Example:
link_message(integer sender_num, integer num, string str, key id) { if (num == -78001) { // Notifications arrive with ! prefixes for each value string v = cached_str_kv_value(str, "!v"); } }
The above example uses this helper function to quickly retrieve a value from a ;-separated key=value list:
// ["key1=", v, "key2=", v, ...]list cached_kv_pairs;string cached_kv_str;/*Extract a value from a `key1=val1;key2=val2` string by exact key with a cache for `str`.Uses boundary matching to prevent partial hits (e.g. `v` won'tmatch `vis` or `v_b`). Returns `""` if the key is not found.*/string cached_str_kv_value(string str, string k) { if (str != cached_kv_str) { list kvPairs; cached_kv_pairs = llParseString2List(str, [";"], []); integer i; list parsed; for (i = 0; i < llGetListLength(cached_kv_pairs); i++) { parsed = llParseString2List(llList2String(cached_kv_pairs, i), ["="], []); if (llList2String(parsed, 0) != "") { kvPairs += [llList2String(parsed, 0) + "=", llList2String(parsed, 1)]; } } cached_kv_str = str; cached_kv_pairs = kvPairs; } integer f = llListFindList(cached_kv_pairs, [k + "="]); if (f < 0) return ""; return llList2String(cached_kv_pairs, f + 1);}
Link Message Interface
Your script talks to the API through link messages. Pass the target device key (or the
owner key to broadcast) as the link message id.
Number
Direction
Purpose
-78000
Script → API
Send a command in Obsidian format (key=val;...). Translated to Legacy automatically when needed.
-78001
API → Script
Device notifications in Obsidian format (!key=val;...).
-78002
API → Script
Raw commands echoed from objects owned by the same avatar (key=val;...).
-727001
API → Script
Raw commands from Legacy devices (Legacy format).
setFilter only affects what comes through -727001.
AVSitter toggles (Legacy)
Parameter-less quick toggles, in AVSitter custom-command style:
Number
Direction
Purpose
-7279990
Script → API
Disable Legacy API processing
-7279991
Script → API
Enable Legacy API processing
-7279992
Script → API
Toggle Legacy API processing
Obsidian API Parameters
See the full and comprehensive list of the parameters in the API References.
Param
Meaning
Range
Notes
vis
Visibility
0 off, 0.5 alpha, 1 on
s
Firmness / stage
0 flaccid, 0.333, 0.667, 1 hard
Legacy has no s=0.333 stage (3 vs 4 stages), so it will round to 0.667
v
Shaft vertical angle
0.25 down, 0.5 level, 0.75 up
h
Shaft horizontal angle
0.25 right, 0.5 center, 0.75 left
Legacy devices are limited to the range 0.442-0.558
f_mode
Foreskin position
0 open, 0.5 half, 1 closed
fp
Fluid particle choice
0 stop, 0.25 pee, 0.5 splash, 0.75 pre, 1 cum
fm
Mesh fluids
0 stop, 0.75 pre, 1 cum
No legacy equivalent
v_b
Ball vertical angle
0.3 back, 0.5 center, 1 forward
No legacy equivalent
z_b
Ball vertical offset
0 low, 0.5 mid, 1 high
No legacy equivalent
th
Girth
0 thin, 1 thick
No legacy equivalent
ao
Animation overrider
0 off, 1 on
No legacy equivalent
c
Curve toggle
0 straight, 1 curved
No legacy equivalent
fp_int
Fluid intensity
0-1
fp_dir
Fluid direction
0 forward, 1 down
Obsidian lacks ‘balls’ visual source
fp_thr
Throbbing
0 stop, 1 max
fx
Effects layer
0 off, 1 on
No legacy equivalent
Notifications use the ! prefix (!vis, !v, !h, …). Commands you send use no prefix.
Translation Examples
Incoming — legacy device → your script on -78001
Legacy device sends
Translated -78001 notification
on0|1|1.36|0.0|
!vis=1;!s=0.667;!v=0.637;!h=0.5
on1|2|0.5|-0.2|
!vis=1;!f_mode=1;!s=1;!v=0.55;!h=0.519
off
!vis=0
flowStart|knob|side|cum|0.5
!fp=1;!fp_vis=1;!fp_int=0.5;!fp_dir=0
flowStop
!fp=0;!fp_vis=0;!fp_int=0;!fp_dir=0
twitchStart|0.5|3
!fp_thr=0.5 (approximation)
twitchStop
!fp_thr=0
info|<typeOfDevice>
!info=<typeOfDevice>
Outgoing — your -78000 command → legacy device
Obsidian command via -78000
Sent to legacy device
vis=0
off
vis=1;v=0.5;s=0.667;h=0.5;f_mode=0
on0|1|0.0|0.0|
v=0.6;h=0.48;s=1
set|2|0.628|-0.131|
fp=0.75
flowStart|knob|down|pre|0.5
fp=0
flowStop
Presence check (info / ack)
Legacy devices have no native acknowledgement. Sending info=<x> or ack=<x> asks a
legacy device to confirm it is present; if it responds you receive !info=<x> /
!ack=<x> back on -78001. A legacy device may also report its type on its own, which
arrives as !info=<typeOfDevice>.
What won’t reach Legacy devices
These Obsidian parameters have no Legacy equivalent and are skipped when the target is a
legacy device:
Param
Why
v_b, z_b
Legacy devices have no ball control
th, th_eq, le_eq
Girth/length equations not in Legacy API
m_kin, m_int, m_vibe
Motion parameters not in Legacy API
f_auto
Auto foreskin not in Legacy API
ao
Animation overrider not in Legacy API
c
Curve toggle not in Legacy API
fx
Effects layer not in Legacy API
fm
Mesh fluids (legacy devices use particles only)
sub
Subscription control; not yet supported (TODO)
Legacy permissions, permFlags, and touch notifications have no Obsidian
equivalent and are dropped on the way back.
Other limitations
vis=0.5 (alpha) does not translate to alpha-cutting on Legacy devices, and the
API cannot tell whether a Legacy device changed its alpha-cut.
twitchStart/twitchStop translation is approximate — Legacy interval/count
params collapse to a single fp_thr intensity (twitchStart→!fp_thr=0.5,
twitchStop→!fp_thr=0).
Stage is lossy toward Legacy: Obsidian has 4 stops, Legacy only 3.
Known Issues
Obsidian: On/Off button and Alpha don’t react to changes coming from the API.
Outgoing vis=0.5 does not translate to alpha-cutting on Legacy devices, nor do we
know whether the Legacy device changed the alpha-cut.
cum sent to a Legacy device comes through even though it might be hidden.
When a condom is worn on Legacy devices, pre and splash particles emit but the
notification doesn’t come through the API.
Script Example
// Requires the following script to be present in the same prim (cross-linkset// communication is not supported):// // */ aeros / Unified API//string creator_SHARED_SECRET = "<insert your secret from the notecard>";// aeros_obsidian_send is used for Obsidian API syntax. It adds verification of the message source// with a signature from `creator_SHARED_SECRET` global constant known by both the Unified API Script// and store creator.//// * `target` - Legacy Device or Obsidian Device Key (or the owner key to broadcast)// * `payload` - Obsidian API syntax payload. Example: `"v=0.5;h=0"`aeros_obsidian_send(key target, string payload) { string nonce = (string)llFrand(1.0); llMessageLinked(LINK_THIS, -78000, llSHA1String(payload + ":" + nonce + ":" + creator_SHARED_SECRET) + "|" + nonce + "|" + payload, target);}aeros_obsidian_subscribe() { if (!llGetAttached()) { // If not attached, subscribe to notifications from the same-owner Obsidian aeros_obsidian_send(llGetOwner(), "sub=1"); }}// ["key1=", v, "key2=", v, ...]list cached_kv_pairs;string cached_kv_str;/*Extract a value from a `key1=val1;key2=val2` string by exact key with a cache for `str`.Uses boundary matching to prevent partial hits (e.g. `v` won'tmatch `vis` or `v_b`). Returns `""` if the key is not found.*/string cached_str_kv_value(string str, string k) { if (str != cached_kv_str) { list kvPairs; cached_kv_pairs = llParseString2List(str, [";"], []); integer i; list parsed; for (i = 0; i < llGetListLength(cached_kv_pairs); i++) { parsed = llParseString2List(llList2String(cached_kv_pairs, i), ["="], []); if (llList2String(parsed, 0) != "") { kvPairs += [llList2String(parsed, 0) + "=", llList2String(parsed, 1)]; } } cached_kv_str = str; cached_kv_pairs = kvPairs; } integer f = llListFindList(cached_kv_pairs, [k + "="]); if (f < 0) return ""; return llList2String(cached_kv_pairs, f + 1);}default { state_entry() { aeros_obsidian_subscribe(); llSetTimerEvent(15); } timer() { aeros_obsidian_subscribe(); } touch_start(integer i) { // On touch, send ";"-separated command var=val pairs // In this example: random stage and vertical angle float s = llFrand(1); float v = 0.25 + llFrand(0.5); float h = 0.25 + llFrand(0.5); aeros_obsidian_send(llGetOwner(), "s=" + (string)s + ";v=" + (string)v + ";h=" + (string)h); aeros_obsidian_send(llGetOwner(), "ack=test"); } link_message(integer sender_num, integer num, string str, key id) { if (num >= -78002 && num <= -78001) { // Obsidian Incoming Messaging Channels if (llGetOwnerKey(id) != llGetOwner()) return; if (num == -78001) { // Notifications llOwnerSay("\n[!]\t" + llKey2Name(id) + "+\n\t"+str); string ack = cached_str_kv_value(str, "!ack"); if (ack != "") { llOwnerSay("!ack="+ack); } } else if (num == -78002) { // Intents and constraints sent by the HUD or other attached add-ons - SAME OWNER ONLY // Use it to learn HUD's commands. // llOwnerSay("\t"+str); } } else if (num == -727001) { // Legacy Incoming API Messages (ONLY from Legacy Devices) // llOwnerSay("[L]\t"+str); } }}