npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

scentience

v2.1.1

Published

Connect to Scentience olfaction instruments via BLE Bluetooth

Readme

scentience

npm-installable package for interacting with Scentience olfaction instruments via BLE Bluetooth.

Browser support

Web Bluetooth is required for browser use. It is supported in Chrome, Edge, and other Chromium-based browsers on desktop and Android. It is not supported in Firefox, Safari, or any browser on iOS.

Your page must also be served over HTTPS (or localhost) — Web Bluetooth is blocked on plain HTTP.

| Environment | Status | Notes | |--------------------|---------|--------------------------------------------| | Chrome / Edge | ✅ | Desktop and Android | | Other Chromium | ✅ | Opera, Brave, Samsung Internet, etc. | | Firefox | ❌ | No Web Bluetooth support | | Safari / iOS | ❌ | No Web Bluetooth support on any iOS browser| | Node.js | ✅ | Via webbluetooth peer dependency |

Installation

npm install scentience

For Node.js environments, also install the optional BLE polyfill:

npm install webbluetooth

Browser and React environments use the native Web Bluetooth API — no extra dependencies needed.

Quick Start

The Scentience service and characteristic UUIDs are set as defaults — for standard Scentience devices you can call connectBLE() with no UUID arguments.

import { ScentienceDevice } from 'scentience';

const device = new ScentienceDevice('<YOUR_API_KEY>');

// Defaults work for standard Scentience devices.
// Pass serviceUuid / charUuid explicitly if your firmware uses different values.
await device.connectBLE();

// Single reading
const sample = await device.sampleBLE({ async: true });
console.log(sample);

// Continuous stream
device.on('data', (data) => console.log(data));
device.on('error', (err) => console.error(err));
await device.streamBLE({ async: true });

React Example

import { useEffect, useState } from 'react';
import { ScentienceDevice } from 'scentience';

export default function App() {
  const [reading, setReading] = useState(null);

  useEffect(() => {
    const device = new ScentienceDevice('<YOUR_API_KEY>');

    device
      .connectBLE({ serviceUuid: '<YOUR_SERVICE_UUID>', charUuid: '<YOUR_CHAR_UUID>' })
      .then(() => {
        device.on('data', setReading);
        return device.streamBLE({ async: true });
      })
      .catch(console.error);

    return () => device.disconnect();
  }, []);

  return <pre>{JSON.stringify(reading, null, 2)}</pre>;
}

API

new ScentienceDevice(apiKey)

| Parameter | Type | Description | |-----------|----------|-------------------------| | apiKey | string | Your Scentience API key |


device.connectBLE(options)

Scan for and connect to one or more Scentience peripherals.

Both serviceUuid and charUuid must be valid 128-bit Bluetooth UUIDs in 8-4-4-4-12 format using only hex characters (0–9, a–f), e.g. 00001234-0000-1000-8000-00805f9b34fb. Obtain these values from your device's firmware or BLE GATT table — they are distinct identifiers and cannot be the same string.

| Option | Type | Default | Description | |---------------|------------|--------------------------------------------|---------------------------------------------------------------| | serviceUuid | string | '569a1101-b87f-490c-92cb-11ba5ea5167c' | GATT service UUID (declared in optionalServices) | | charUuid | string | '569a2000-b87f-490c-92cb-11ba5ea5167c' | GATT characteristic UUID within that service | | deviceUid | string | null | Single-device mode: filter scan to a specific device name/UID | | deviceUids | string[] | null | Multi-device mode: connect to multiple devices by UID |

When deviceUids is provided the browser picker is shown once per requested UID. Each selected device is matched against the UID using this priority: (1) exact device.id, (2) exact device name, (3) case-insensitive substring of device name (e.g. "A00022" matches "Scentience A00022"). All matched devices are then connected concurrently and an error is thrown listing any UIDs that were not matched.

Web Bluetooth constraint: The browser's requestDevice() API requires an explicit user gesture and returns exactly one device per call. This is a hard security requirement of the Web Bluetooth spec — no library can bypass it. Consequently, deviceUids with N entries will open the browser device picker N times, once per UID. After all selections are confirmed the connections are established in parallel. In Node.js environments using the webbluetooth polyfill the picker interaction is replaced by a programmatic scan, so multiple devices can be selected in a single pass.

Returns Promise<ScentienceDevice>.


device.sampleBLE(options)

Request a single sensor reading from the connected device(s).

| Option | Type | Default | Description | |---------|-----------|---------|--------------------------------------------------| | async | boolean | false | Return a Promise resolving with the data payload |

Returns Promise<ScienceData> when one device is connected, or Promise<ScienceData[]> when multiple devices are connected (one entry per device, in the same order as deviceUids).


device.streamBLE(options)

Begin continuous streaming from all connected devices. Use .on('data', cb) to receive readings. The UID field inside each packet identifies which device sent it.

| Option | Type | Default | Description | |---------|-----------|---------|----------------------------------------------------------| | async | boolean | false | Return a Promise that stays pending until stopStream() |

Returns Promise<void>.


device.on(event, callback) / device.off(event, callback)

Subscribe or unsubscribe from stream events.

| Event | Callback signature | |---------|-------------------------| | data | (data: ScienceData) => void | | error | (err: Error) => void |


device.stopStream()

Stop an active stream and release BLE notification subscriptions on all connected devices.


device.disconnect()

Stop the stream and close all active GATT connections.


device.isConnected

booleantrue when at least one GATT connection is active.


ScentienceDevice.scanDevices(timeoutMs?)

Static method. Scan for visible BLE devices without connecting and return their info.

| Parameter | Type | Default | Description | |--------------|----------|---------|------------------------------------| | timeoutMs | number | 10000 | Scan duration in milliseconds |

Returns Promise<DeviceInfo[]> where each entry is { name: string | null, address: string, rssi: number | null }.

Requires the experimental Web Bluetooth Scanning API (requestLEScan). In Chrome, enable it via chrome://flags/#enable-web-bluetooth-new-permissions-backend.

const devices = await ScentienceDevice.scanDevices(5000);
console.log(devices);
// [{ name: 'Scentience A00022', address: '...', rssi: -62 }, ...]

Data Payload

Sensor readings are delivered as a JSON object. Chemical compound fields are only present when their detected magnitude is greater than zero.

{
  "UID": "device_identifier",
  "TIMESTAMP": "2026-03-23T12:00:00.000Z",
  "ENV_temperatureC": 22.4,
  "ENV_humidity": 45.1,
  "ENV_pressureHpa": 1013.2,
  "STATUS_opuA": 120,
  "BATT_health": 98,
  "BATT_v": 3.85,
  "BATT_charge": 87,
  "BATT_time": 14.2,
  "CO2": 412,
  "VOC": 0.03
}

| Prefix | Description | |----------|---------------------------| | ENV_ | Environmental parameters | | BATT_ | Battery metrics | | (none) | Chemical compound readings |

Connect to a Specific Device

await device.connectBLE({
  serviceUuid: '<YOUR_SERVICE_UUID>',
  charUuid: '<YOUR_CHAR_UUID>',
  deviceUid: '<DEVICE_UID>',
});

Connect to Multiple Devices Simultaneously

Pass deviceUids to connect to several instruments at once. All matched devices are connected concurrently.

Browser limitation: The Web Bluetooth API requires one user gesture per device. Passing deviceUids: ['A', 'B'] will open the browser device picker twice — once for each UID. This is a hard constraint of the Web Bluetooth security model; it cannot be changed by any library. After both devices are confirmed they are connected in parallel. Node.js environments (with the webbluetooth polyfill) do not have this restriction.

import { ScentienceDevice } from 'scentience';

const device = new ScentienceDevice('<YOUR_API_KEY>');

// Browser: picker opens twice (once per UID).
// UIDs are matched by exact device name or case-insensitive substring
// (e.g. "A00022" matches "Scentience A00022").
await device.connectBLE({ deviceUids: ['Scentience A00022', 'Scentience B00031'] });

// sampleBLE returns an array when multiple devices are connected.
const [sampleA, sampleB] = await device.sampleBLE({ async: true });
console.log(sampleA.UID, sampleB.UID);

// streamBLE receives packets from all devices.
// The UID field in each packet identifies the source device.
device.on('data', (data) => {
  console.log(`[${data.UID}]`, data);
});
await device.streamBLE({ async: true });

Scan for Devices

Use ScentienceDevice.scanDevices() to list visible BLE devices before connecting. Requires the experimental Web Bluetooth Scanning API (Chrome flag: enable-web-bluetooth-new-permissions-backend).

import { ScentienceDevice } from 'scentience';

const devices = await ScentienceDevice.scanDevices(5000);
console.log(devices);
// [{ name: 'Scentience A00022', address: '...', rssi: -62 }, ...]

Error Handling

All async methods throw on failure. Common failure points and what to expect:

| Situation | Error message | |-----------|---------------| | No API key passed to constructor | A Scentience API key is required. | | Malformed UUID passed to connectBLE | Invalid Bluetooth UUID: "..." | | Empty deviceUids array | deviceUids must be a non-empty array... | | UID not matched by any selected device | The following device UIDs were not matched: ... | | sampleBLE / streamBLE before connectBLE | Not connected. Call connectBLE() first. | | Web Bluetooth unavailable (wrong browser / HTTP) | Web Bluetooth API is unavailable. | | scanDevices without the experimental flag | scanDevices() requires the experimental Web Bluetooth Scanning API... |

Wrap connection and streaming logic in try/catch:

try {
  await device.connectBLE();
  device.on('data', (data) => console.log(data));
  device.on('error', (err) => console.error('Stream error:', err));
  await device.streamBLE({ async: true });
} catch (err) {
  console.error('BLE error:', err.message);
  device.disconnect();
}

The error event on the device fires for individual packet decode failures during a stream without stopping the stream. The outer try/catch covers connection and setup failures.


Olfaction-Vision-Language Embeddings

ScentienceEmbedder provides access to the four COLIP OVL models — a joint multimodal embedding space for olfaction, vision, and language data.

Install the required peer dependency:

npm install @huggingface/inference

| Variant | Description | |-------------------|--------------------------------------------------------| | colip-small-base| Fast inference; edge robotics, mobile | | colip-small-gat | Graph-Attention variant; edge with higher accuracy | | colip-large-base| Best accuracy for online/server tasks | | colip-large-gat | Graph-Attention variant; highest accuracy |

Example 1 — Embed a live sensor reading and match against text labels

import { ScentienceDevice, ScentienceEmbedder } from 'scentience';

const device = new ScentienceDevice('<SCENTIENCE_API_KEY>');
const embedder = new ScentienceEmbedder({
  model: 'colip-large-base',
  apiToken: '<HF_TOKEN>',
});

await device.connectBLE({ serviceUuid: '<SERVICE_UUID>', charUuid: '<CHAR_UUID>' });
const reading = await device.sampleBLE({ async: true });

// Embed the sensor reading and a set of candidate text labels
const [scentVec, ...labelVecs] = await Promise.all([
  embedder.embedOlfaction(reading),
  embedder.embedText('fresh coffee'),
  embedder.embedText('pine forest'),
  embedder.embedText('ocean breeze'),
]);

const ranked = ScentienceEmbedder.rankBySimilarity(scentVec, labelVecs);
const labels = ['fresh coffee', 'pine forest', 'ocean breeze'];
console.log('Best match:', labels[ranked[0].index], `(score: ${ranked[0].score.toFixed(3)})`);

Example 2 — Load the small-GAT model and embed text + image

import { ScentienceEmbedder } from 'scentience';

const embedder = new ScentienceEmbedder({
  model: 'colip-small-gat',
  apiToken: '<HF_TOKEN>',
});

const textVec  = await embedder.embedText('smoky oak barrel');
const imageVec = await embedder.embedImage('https://example.com/barrel.jpg');

const similarity = ScentienceEmbedder.cosineSimilarity(textVec, imageVec);
console.log('Text–image similarity:', similarity.toFixed(4));

ScentienceEmbedder API

| Method / Property | Description | |-------------------------------------------------|--------------------------------------------------------------------| | new ScentienceEmbedder({ model, apiToken, modelId? }) | Create a client for the given model variant. | | embedder.embedText(text) | Embed a text string. Returns Promise<number[]>. | | embedder.embedImage(blobOrUrl) | Embed an image (Blob, ArrayBuffer, or URL). Returns Promise<number[]>. | | embedder.embedOlfaction(sensorData) | Embed a ScienceData sensor reading. Returns Promise<number[]>. | | embedder.modelId | The resolved HF repo ID being used. | | ScentienceEmbedder.cosineSimilarity(a, b) | Cosine similarity between two vectors. Returns number in [-1, 1].| | ScentienceEmbedder.rankBySimilarity(q, vecs) | Rank candidates by similarity to query. Returns sorted array. |

Note on model IDs: The COLIP_MODELS map assumes repos at kordelfrance/colip-{size}-{variant}. If the models are published under different IDs, pass modelId directly:

new ScentienceEmbedder({ apiToken: '<HF_TOKEN>', modelId: 'kordelfrance/my-custom-id' })

Logging & Export

ScentienceLogger collects readings and exports them to JSON or CSV.

Stream to CSV (Node.js)

import { ScentienceDevice, ScentienceLogger } from 'scentience';

const device = new ScentienceDevice('<YOUR_API_KEY>');
const logger = new ScentienceLogger();

await device.connectBLE({ serviceUuid: '<YOUR_SERVICE_UUID>', charUuid: '<YOUR_CHAR_UUID>' });

device.on('data', (d) => logger.log(d));
await device.streamBLE({ async: true });

// Later — write readings.csv to disk
await logger.exportCSV('readings.csv');

Stream to CSV (React / browser download)

import { useEffect, useRef } from 'react';
import { ScentienceDevice, ScentienceLogger } from 'scentience';

export default function App() {
  const loggerRef = useRef(new ScentienceLogger());
  const deviceRef = useRef(null);

  useEffect(() => {
    const device = new ScentienceDevice('<YOUR_API_KEY>');
    deviceRef.current = device;

    device
      .connectBLE({ serviceUuid: '<YOUR_SERVICE_UUID>', charUuid: '<YOUR_CHAR_UUID>' })
      .then(() => {
        device.on('data', (d) => loggerRef.current.log(d));
        return device.streamBLE({ async: true });
      })
      .catch(console.error);

    return () => device.disconnect();
  }, []);

  const downloadCSV = () => loggerRef.current.exportCSV('readings.csv');
  const downloadJSON = () => loggerRef.current.exportJSON('readings.json');

  return (
    <>
      <button onClick={downloadCSV}>Download CSV</button>
      <button onClick={downloadJSON}>Download JSON</button>
    </>
  );
}

ScentienceLogger API

| Method / Property | Description | |---------------------------------|------------------------------------------------------| | logger.log(data) | Add a reading to the log. Returns this. | | logger.data | Array of all logged records. | | logger.size | Number of records in the log. | | logger.clear() | Remove all records. Returns this. | | logger.toJSON(indent?) | Serialize records to a JSON string. | | logger.toCSV() | Serialize records to a CSV string. | | logger.exportJSON(filename?) | Write/download JSON file. Default: scentience-data.json | | logger.exportCSV(filename?) | Write/download CSV file. Default: scentience-data.csv |

CSV column handling: the header row is the union of all keys across every record. Chemical compound columns are included whenever at least one reading detected them — missing values in other rows appear as empty cells.

License

Apache-2.0