Node.js, Web tech, and Web Bluetooth

I’m wondering about connecting Litho to a Node.js app.

I wanted to do something similar that the Leap Motion controllers do, like having a Node.js server connect to the Litho directly, then serving a websocket server that can be connected to from a library in a website.

Another alternative would be using Web Bluetooth (although a websocket server means multiple sites/apps could connect and receive controller input) to directly talk to the Litho and building a library from there.

It’s mostly for serving personal and creative creative development projects, art installations, experimenting with new free form interaction models, UX/UI research. There’s also cool local arcade mutliplayer game concepts that could be done with this.

It’d be really great to open it to experimentation and access to web tech.

I just ordered two Litho devices and looking to try mess with the Node side of things, probably going to look into using:
node-hid or noble
Web Bluetooth

This is a question to Litho devs:
Is there documentation on the communication with Litho to build and maintain a library for connecting to it?
Is it okay to connect and use it from Web Bluetooth? See: web-bluetooth/#security-and-privacy and old article on security model if this would be ok; E.g. specifically any OTA firmware update mechanism.
However you could allow your users to update firmware OTA from your website directly which would be pretty epic. But if you just want to block any OTA update GATT attribute entirely though from the web, you could add a pull request to the blocklist: https://github.com/WebBluetoothCG/registries

The Web Bluetooth specification is pretty comprehensive and easy to read.

1 Like

Glad to hear you’re so excited to use Litho. Would be really interested to see what you manage to create, but a few quick caveats:

  1. We likely won’t officially support any libraries like this.
  2. Whilst we don’t plan on it, and if we do it’s likely to be in a backwards-compatible manner, it’s possible that we may change the communication format in future firmware releases.
  3. You won’t have access to the hand position tracking - that is part of the Unity SDK and plugin, it’s not calculated on-device.

You can definitely connect to Litho using Web Bluetooth - in fact, this is something we did in testing. You can probably modify this example to get something working with Litho pretty easily: https://github.com/mrdoob/daydream-controller.js/. The Node and socket-based approach sounds like it should work fine too.

Firmware updates could be done with something like https://github.com/thegecko/web-bluetooth-dfu but I’d recommend using the Litho Companion App unless there’s a strong case not to. The Companion App also handles download of the latest firmware, and ensures that the firmware version matches the device. It also handles recovery in the case of a failed update. I can go into more detail about these if you have a specific use case in mind though. Our updates are signed and verified on-device, so no need for us to be on the blocklist.

Here’s the information you would need to make the library. Might be useful to use something like nRF Connect to explore the services on the device first.

Hope this is useful - looking forward to see what you create!

Standard Services

The Battery Level and Device Information are both standard characteristics, so other libraries should be able to describe this. In the DIS, Hardware versions are “1.5” - Silver and “1.6” - Slate Grey.

Controller Updates

State updates are sent through the Notify characteristic of the Litho service at ~60 Hz.

typedef struct  {
    int16_t ori_w;
    int16_t ori_x;
    int16_t ori_y;
    int16_t ori_z;
    int16_t accel_x;
    int16_t accel_y;
    int16_t accel_z;
    uint8_t touch_x;
    uint8_t touch_y;
    uint8_t touch_flags;
    int16_t unused;
    uint8_t misc_flags;
} __attribute__((packed))
litho_state_t;

Quaternion components are scaled from range (-1, 1) to range (-32767, 32767). I believe this is in an X-forward, Y-left, Z-up coordinate space but don’t hold me to it!
Accelerations are scaled from (-8g, 8g) to range (-32767, 32767).
Misc flags are not implemented, but may be used for things like charge status or estimated accuracy in future.
Touch flags are as follows:

#define TOUCHING 0x40
#define SWIPE_Y_NEG 0x20 
#define SWIPE_Y_POS 0x10
#define SWIPE_X_POS 0x08
#define SWIPE_X_NEG 0x04
#define SWIPE_ANY_X (SWIPE_X_POS | SWIPE_X_NEG)
#define SWIPE_ANY_Y (SWIPE_Y_POS | SWIPE_Y_NEG)
#define SWIPE_ANY (SWIPE_ANY_X | SWIPE_ANY_Y)
#define TAP_AND_HOLD 0x02
#define SINGLE_TAP 0x01

Haptics

Haptic events are handled by the Write characteristic of the Litho service:

  • 0x01 - send the default effect
  • 0x01 0x02 - play effect 2
  • 0x01 0x01 0x02 - play effect 1 then play effect 2. You can continue the list up to 8 events, which will be played in sequence.
    At the moment you can’t override the default haptic event, which is played on every touch of the touch surface.

Effects are as follows:

// Litho uses a licensed effects library from Immersion Corporation, as described below
typedef enum {
  Effect_StrongClick_100 = 1,             //  1 Strong Click - 100%
  Effect_StrongClick_60,                //  2 Strong Click - 60%
  Effect_StrongClick_30,                //  3 Strong Click - 30%
  Effect_SharpClick_100,                //  4 Sharp Click - 100%
  Effect_SharpClick_60,                 //  5 Sharp Click - 60%
  Effect_SharpClick_30,               //  6 Sharp Click - 30%
  Effect_SoftBump_100,                //  7 Soft Bump - 100%
  Effect_SoftBump_60,                 //  8 Soft Bump - 60%
  Effect_SoftBump_30,                 //  9 Soft Bump - 30%
  Effect_DoubleClick_100,               //  10 Double Click - 100%
  Effect_DoubleClick_60,                //  11 Double Click - 60%
  Effect_TripleClick_100,               //  12 Triple Click - 100%
  Effect_SoftFuzz_60,                 //  13 Soft Fuzz - 60%
  Effect_StrongBuzz_100,                //  14 Strong Buzz - 100%
  Effect_Alert_750ms_100,               //  15 750 ms Alert 100%
  Effect_Alert_1000ms_100,              //  16 1000 ms Alert 100%
  Effect_StrongClick_1_100,             //  17 Strong Click 1 - 100%
  Effect_StrongClick_2_80,              //  18 Strong Click 2 - 80%
  Effect_StrongClick_3_60,              //  19 Strong Click 3 - 60%
  Effect_StrongClick_4_30,              //  20 Strong Click 4 - 30%
  Effect_MediumClick_1_100,             //  21 Medium Click 1 - 100%
  Effect_MediumClick_2_80,              //  22 Medium Click 2 - 80%
  Effect_MediumClick_3_60,              //  23 Medium Click 3 - 60%
  Effect_SharpTick_1_100,               //  24 Sharp Tick 1 - 100%
  Effect_SharpTick_2_80,                //  25 Sharp Tick 2 - 80%
  Effect_SharpTick_3_60,                //  26 Sharp Tick 3 – 60%
  Effect_ShortDoubleClickStrong_1_100,        //  27 Short Double Click Strong 1 – 100%
  Effect_ShortDoubleClickStrong_2_80,         //  28 Short Double Click Strong 2 – 80%
  Effect_ShortDoubleClickStrong_3_60,         //  29 Short Double Click Strong 3 – 60%
  Effect_ShortDoubleClickStrong_4_30,         //  30 Short Double Click Strong 4 – 30%
  Effect_ShortDoubleClickMedium_1_100,        //  31 Short Double Click Medium 1 – 100%
  Effect_ShortDoubleClickMedium_2_80,         //  32 Short Double Click Medium 2 – 80%
  Effect_ShortDoubleClickMedium_3_60,         //  33 Short Double Click Medium 3 – 60%
  Effect_ShortDoubleSharpTick_1_100,          //  34 Short Double Sharp Tick 1 – 100%
  Effect_ShortDoubleSharpTick_2_80,         //  35 Short Double Sharp Tick 2 – 80%
  Effect_ShortDoubleSharpTick_3_60,         //  36 Short Double Sharp Tick 3 – 60%
  Effect_LongDoubleSharpClickStrong_1_100,      //  37 Long Double Sharp Click Strong 1 – 100%
  Effect_LongDoubleSharpClickStrong_2_80,       //  38 Long Double Sharp Click Strong 2 – 80%
  Effect_LongDoubleSharpClickStrong_3_60,       //  39 Long Double Sharp Click Strong 3 – 60%
  Effect_LongDoubleSharpClickStrong_4_30,       //  40 Long Double Sharp Click Strong 4 – 30%
  Effect_LongDoubleSharpClickMedium_1_100,      //  41 Long Double Sharp Click Medium 1 – 100%
  Effect_LongDoubleSharpClickMedium_2_80,       //  42 Long Double Sharp Click Medium 2 – 80%
  Effect_LongDoubleSharpClickMedium_3_60,       //  43 Long Double Sharp Click Medium 3 – 60%
  Effect_LongDoubleSharpTick_1_100,         //  44 Long Double Sharp Tick 1 – 100%
  Effect_LongDoubleSharpTick_2_80,          //  45 Long Double Sharp Tick 2 – 80%
  Effect_LongDoubleSharpTick_3_60,          //  46 Long Double Sharp Tick 3 – 60%
  Effect_Buzz_1_100,                  //  47 Buzz 1 – 100%
  Effect_Buzz_2_80,                 //  48 Buzz 2 – 80%
  Effect_Buzz_3_60,                 //  49 Buzz 3 – 60%
  Effect_Buzz_4_40,                 //  50 Buzz 4 – 40%
  Effect_Buzz_5_20,                 //  51 Buzz 5 – 20%
  Effect_PulsingStrong_1_100,             //  52 Pulsing Strong 1 – 100%
  Effect_PulsingStrong_2_60,              //  53 Pulsing Strong 2 – 60%
  Effect_PulsingMedium_1_100,             //  54 Pulsing Medium 1 – 100%
  Effect_PulsingMedium_2_60,              //  55 Pulsing Medium 2 – 60%
  Effect_PulsingSharp_1_100,              //  56 Pulsing Sharp 1 – 100%
  Effect_PulsingSharp_2_60,             //  57 Pulsing Sharp 2 – 60%
  Effect_TransitionClick_1_100,           //  58 Transition Click 1 – 100%
  Effect_TransitionClick_2_80,            //  59 Transition Click 2 – 80%
  Effect_TransitionClick_3_60,            //  60 Transition Click 3 – 60%
  Effect_TransitionClick_4_40,            //  61 Transition Click 4 – 40%
  Effect_TransitionClick_5_20,            //  62 Transition Click 5 – 20%
  Effect_TransitionClick_6_10,            //  63 Transition Click 6 – 10%
  Effect_TransitionHum_1_100,             //  64 Transition Hum 1 – 100%
  Effect_TransitionHum_2_80,              //  65 Transition Hum 2 – 80%
  Effect_TransitionHum_3_60,              //  66 Transition Hum 3 – 60%
  Effect_TransitionHum_4_40,              //  67 Transition Hum 4 – 40%
  Effect_TransitionHum_5_20,              //  68 Transition Hum 5 – 20%
  Effect_TransitionHum_6_10,              //  69 Transition Hum 6 – 10%
  Effect_TransitionRampDownLongSmooth_1_100_to_0,   //  70 Transition Ramp Down Long Smooth 1 – 100 to 0%
  Effect_TransitionRampDownLongSmooth_2_100_to_0,   //  71 Transition Ramp Down Long Smooth 2 – 100 to 0%
  Effect_TransitionRampDownMediumSmooth_1_100_to_0, //  72 Transition Ramp Down Medium Smooth 1 – 100 to 0%
  Effect_TransitionRampDownMediumSmooth_2_100_to_0, //  73 Transition Ramp Down Medium Smooth 2 – 100 to 0%
  Effect_TransitionRampDownShortSmooth_1_100_to_0,  //  74 Transition Ramp Down Short Smooth 1 – 100 to 0%
  Effect_TransitionRampDownShortSmooth_2_100_to_0,  //  75 Transition Ramp Down Short Smooth 2 – 100 to 0%
  Effect_TransitionRampDownLongSharp_1_100_to_0,    //  76 Transition Ramp Down Long Sharp 1 – 100 to 0%
  Effect_TransitionRampDownLongSharp_2_100_to_0,    //  77 Transition Ramp Down Long Sharp 2 – 100 to 0%
  Effect_TransitionRampDownMediumSharp_1_100_to_0,  //  78 Transition Ramp Down Medium Sharp 1 – 100 to 0%
  Effect_TransitionRampDownMediumSharp_2_100_to_0,  //  79 Transition Ramp Down Medium Sharp 2 – 100 to 0%
  Effect_TransitionRampDownShortSharp_1_100_to_0,   //  80 Transition Ramp Down Short Sharp 1 – 100 to 0%
  Effect_TransitionRampDownShortSharp_2_100_to_0,   //  81 Transition Ramp Down Short Sharp 2 – 100 to 0%
  Effect_TransitionRampUpLongSmooth_1_0_to_100,   //  82 Transition Ramp Up Long Smooth 1 – 0 to 100%
  Effect_TransitionRampUpLongSmooth_2_0_to_100,   //  83 Transition Ramp Up Long Smooth 2 – 0 to 100%
  Effect_TransitionRampUpMediumSmooth_1_0_to_100,   //  84 Transition Ramp Up Medium Smooth 1 – 0 to 100%
  Effect_TransitionRampUpMediumSmooth_2_0_to_100,   //  85 Transition Ramp Up Medium Smooth 2 – 0 to 100%
  Effect_TransitionRampUpShortSmooth_1_0_to_100,    //  86 Transition Ramp Up Short Smooth 1 – 0 to 100%
  Effect_TransitionRampUpShortSmooth_2_0_to_100,    //  87 Transition Ramp Up Short Smooth 2 – 0 to 100%
  Effect_TransitionRampUpLongSharp_1_0_to_100,    //  88 Transition Ramp Up Long Sharp 1 – 0 to 100%
  Effect_TransitionRampUpLongSharp_2_0_to_100,    //  89 Transition Ramp Up Long Sharp 2 – 0 to 100%
  Effect_TransitionRampUpMediumSharp_1_0_to_100,    //  90 Transition Ramp Up Medium Sharp 1 – 0 to 100%
  Effect_TransitionRampUpMediumSharp_2_0_to_100,    //  91 Transition Ramp Up Medium Sharp 2 – 0 to 100%
  Effect_TransitionRampUpShortSharp_1_0_to_100,   //  92 Transition Ramp Up Short Sharp 1 – 0 to 100%
  Effect_TransitionRampUpShortSharp_2_0_to_100,   //  93 Transition Ramp Up Short Sharp 2 – 0 to 100%
  Effect_TransitionRampDownLongSmooth_1_50_to_0,    //  94 Transition Ramp Down Long Smooth 1 – 50 to 0%
  Effect_TransitionRampDownLongSmooth_2_50_to_0,    //  95 Transition Ramp Down Long Smooth 2 – 50 to 0%
  Effect_TransitionRampDownMediumSmooth_1_50_to_0,  //  96 Transition Ramp Down Medium Smooth 1 – 50 to 0%
  Effect_TransitionRampDownMediumSmooth_2_50_to_0,  //  97 Transition Ramp Down Medium Smooth 2 – 50 to 0%
  Effect_TransitionRampDownShortSmooth_1_50_to_0,   //  98 Transition Ramp Down Short Smooth 1 – 50 to 0%
  Effect_TransitionRampDownShortSmooth_2_50_to_0,   //  99 Transition Ramp Down Short Smooth 2 – 50 to 0%
  Effect_TransitionRampDownLongSharp_1_50_to_0,   //  100 Transition Ramp Down Long Sharp 1 – 50 to 0%
  Effect_TransitionRampDownLongSharp_2_50_to_0,   //  101 Transition Ramp Down Long Sharp 2 – 50 to 0%
  Effect_TransitionRampDownMediumSharp_1_50_to_0,   //  102 Transition Ramp Down Medium Sharp 1 – 50 to 0%
  Effect_TransitionRampDownMediumSharp_2_50_to_0,   //  103 Transition Ramp Down Medium Sharp 2 – 50 to 0%
  Effect_TransitionRampDownShortSharp_1_50_to_0,    //  104 Transition Ramp Down Short Sharp 1 – 50 to 0%
  Effect_TransitionRampDownShortSharp_2_50_to_0,    //  105 Transition Ramp Down Short Sharp 2 – 50 to 0%
  Effect_TransitionRampUpLongSmooth_1_0_to_50,    //  106 Transition Ramp Up Long Smooth 1 – 0 to 50%
  Effect_TransitionRampUpLongSmooth_2_0_to_50,    //  107 Transition Ramp Up Long Smooth 2 – 0 to 50%
  Effect_TransitionRampUpMediumSmooth_1_0_to_50,    //  108 Transition Ramp Up Medium Smooth 1 – 0 to 50%
  Effect_TransitionRampUpMediumSmooth_2_0_to_50,    //  109 Transition Ramp Up Medium Smooth 2 – 0 to 50%
  Effect_TransitionRampUpShortSmooth_1_0_to_50,   //  110 Transition Ramp Up Short Smooth 1 – 0 to 50%
  Effect_TransitionRampUpShortSmooth_2_0_to_50,   //  111 Transition Ramp Up Short Smooth 2 – 0 to 50%
  Effect_TransitionRampUpLongSharp_1_0_to_50,     //  112 Transition Ramp Up Long Sharp 1 – 0 to 50%
  Effect_TransitionRampUpLongSharp_2_0_to_50,     //  113 Transition Ramp Up Long Sharp 2 – 0 to 50%
  Effect_TransitionRampUpMediumSharp_1_0_to_50,   //  114 Transition Ramp Up Medium Sharp 1 – 0 to 50%
  Effect_TransitionRampUpMediumSharp_2_0_to_50,   //  115 Transition Ramp Up Medium Sharp 2 – 0 to 50%
  Effect_TransitionRampUpShortSharp_1_0_to_50,    //  116 Transition Ramp Up Short Sharp 1 – 0 to 50%
  Effect_TransitionRampUpShortSharp_2_0_to_50,    //  117 Transition Ramp Up Short Sharp 2 – 0 to 50%
  Effect_LongBuzz_100,                //  118 Long buzz for programmatic stopping – 100%
  Effect_SmoothHum_1_50,                //  119 Smooth Hum 1 (No kick or brake pulse) – 50%
  Effect_SmoothHum_2_40,                //  120 Smooth Hum 2 (No kick or brake pulse) – 40%
  Effect_SmoothHum_3_30,                //  121 Smooth Hum 3 (No kick or brake pulse) – 30%
  Effect_SmoothHum_4_20,                //  122 Smooth Hum 4 (No kick or brake pulse) – 20%
  Effect_SmoothHum_5_10,                //  123 Smooth Hum 5 (No kick or brake pulse) – 10%
  Effect_Delay = 0x80,                // When the high bit is set, the low 7 bits are a delay, in 10ms units. Use like "Effect_Delay | 100" for a 1000ms delay
} HapticEffect;
2 Likes

Wow. Thank you so much, that gives a ton to play with!

Stuck trying to make sense of the quaternion somehow now :grin:, have got everything coming through now apart from acceleration which is zero (I assume is disabled atm)

If your quaternion doesn’t make sense, it might be an endianness issue?

1 Like

Ah yep, switching endianness helped there

1 Like