*/ 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.

Overview

                            Aeros Unified API                      Your Script
                      ┌────────────────────────────┐    ┌─INCOMING ───────────────────────────┐
  Legacy device ────► │                            │───►│-78001  (notifications, !key=val)    │ 
  Obsidian device ──► │    auto-detect & route     │───►│-78002  (same-owner command echoes)  │
         ▲            │                            │───►│-727001 (raw legacy, obeys setFilter)│
         │            │                            │    │                                     │
         │            │                            │    │-OUTGOING ───────────────────────────│
 (auto-translated) ◄──│         (translate)        │◄───│ via -78000 in Obsidian API syntax   │ 
                      │                            │    └─────────────────────────────────────┘
                      │                            │
                      └────────────────────────────┘

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't
match `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);
}
 

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.

NumberDirectionPurpose
-78000Script → APISend a command in Obsidian format (key=val;...). Translated to Legacy automatically when needed.
-78001API → ScriptDevice notifications in Obsidian format (!key=val;...).
-78002API → ScriptRaw commands echoed from objects owned by the same avatar (key=val;...).
-727001API → ScriptRaw 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:

NumberDirectionPurpose
-7279990Script → APIDisable Legacy API processing
-7279991Script → APIEnable Legacy API processing
-7279992Script → APIToggle Legacy API processing

Obsidian API Parameters

See the full and comprehensive list of the parameters in the API References.

ParamMeaningRangeNotes
visVisibility0 off, 0.5 alpha, 1 on
sFirmness / stage0 flaccid, 0.333, 0.667, 1 hardLegacy has no s=0.333 stage (3 vs 4 stages), so it will round to 0.667
vShaft vertical angle0.25 down, 0.5 level, 0.75 up
hShaft horizontal angle0.25 right, 0.5 center, 0.75 leftLegacy devices are limited to the range 0.442-0.558
f_modeForeskin position0 open, 0.5 half, 1 closed
fpFluid particle choice0 stop, 0.25 pee, 0.5 splash, 0.75 pre, 1 cum
fmMesh fluids0 stop, 0.75 pre, 1 cumNo legacy equivalent
v_bBall vertical angle0.3 back, 0.5 center, 1 forwardNo legacy equivalent
z_bBall vertical offset0 low, 0.5 mid, 1 highNo legacy equivalent
thGirth0 thin, 1 thickNo legacy equivalent
aoAnimation overrider0 off, 1 onNo legacy equivalent
cCurve toggle0 straight, 1 curvedNo legacy equivalent
fp_intFluid intensity0-1
fp_dirFluid direction0 forward, 1 downObsidian lacks ‘balls’ visual source
fp_thrThrobbing0 stop, 1 max
fxEffects layer0 off, 1 onNo 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 sendsTranslated -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 -78000Sent to legacy device
vis=0off
vis=1;v=0.5;s=0.667;h=0.5;f_mode=0on0|1|0.0|0.0|
v=0.6;h=0.48;s=1set|2|0.628|-0.131|
fp=0.75flowStart|knob|down|pre|0.5
fp=0flowStop

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:

ParamWhy
v_b, z_bLegacy devices have no ball control
th, th_eq, le_eqGirth/length equations not in Legacy API
m_kin, m_int, m_vibeMotion parameters not in Legacy API
f_autoAuto foreskin not in Legacy API
aoAnimation overrider not in Legacy API
cCurve toggle not in Legacy API
fxEffects layer not in Legacy API
fmMesh fluids (legacy devices use particles only)
subSubscription 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't
match `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); 
    }
  }
}