scentience
v2.1.1
Published
Connect to Scentience olfaction instruments via BLE Bluetooth
Maintainers
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 scentienceFor Node.js environments, also install the optional BLE polyfill:
npm install webbluetoothBrowser 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,deviceUidswith 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 thewebbluetoothpolyfill 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
boolean — true 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 thewebbluetoothpolyfill) 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
