Skip to main content

Python client for Scentience olfaction instruments over BLE, with a hosted OVL inference client

Project description

scentience

Python client for Scentience olfaction instruments over BLE Bluetooth.

Installation

BLE only:

pip install scentience

BLE + COLIP embedding models:

pip install "scentience[models]"

Requirements

  • Python 3.8+
  • Bluetooth 4.0+ adapter
  • A Scentience developer API key (obtain from the Scentience portal)
  • For embedding models: torch, torchvision, transformers, huggingface-hub, Pillow (installed via [models] extra)

Connecting a single device

Auto-discover

Scans for the first Scentience device in range and connects to it.

import scentience as scn

device = scn.ScentienceDevice(api_key="YOUR_API_KEY")
device.connect_ble(char_uuid="YOUR_CHAR_UUID")

Target a specific device

Pass the device UID (serial number or BLE name) to connect to a particular instrument.

device.connect_ble(char_uuid="YOUR_CHAR_UUID", device_uid="A00022")

Take a single reading

data = device.sample_ble()   # returns dict
print(data)

Stream continuously

def on_sample(data: dict) -> None:
    print(data["UID"], data.get("CO2"), data.get("ENV_temperatureC"))

device.stream_ble(callback=on_sample)

# ... your application runs here ...

device.stop_stream()
device.disconnect()

Context manager

disconnect() is called automatically on exit.

with scn.ScentienceDevice(api_key="YOUR_API_KEY") as device:
    device.connect_ble(char_uuid="YOUR_CHAR_UUID", device_uid="A00022")
    data = device.sample_ble()
    print(data)

Connecting multiple devices

Pass a list of device UIDs to connect_ble. All devices are scanned for in a single BLE scan and connected concurrently. The API key and characteristic UUID are the same for every device.

Snapshot from all devices

import scentience as scn

device = scn.ScentienceDevice(api_key="YOUR_API_KEY")
device.connect_ble(
    char_uuid="YOUR_CHAR_UUID",
    device_uids=["A00022", "A00010"],   # as many UIDs as needed
)

readings = device.sample_ble()   # returns List[dict], one entry per device
for r in readings:
    print(r)

Stream from all devices simultaneously

The same callback receives packets from every device. Use the UID field to identify the source.

def on_sample(data: dict) -> None:
    print(f"[{data['UID']}]  CO2={data.get('CO2')}  temp={data.get('ENV_temperatureC')}°C")

device.stream_ble(callback=on_sample)

# ... your application runs here ...

device.stop_stream()
device.disconnect()

Context manager (multiple devices)

with scn.ScentienceDevice(api_key="YOUR_API_KEY") as device:
    device.connect_ble(
        char_uuid="YOUR_CHAR_UUID",
        device_uids=["A00022", "A00010"],
    )
    readings = device.sample_ble()

Discovering devices

Before connecting, use scan_devices() to see exactly what BLE devices are visible and confirm the names your instruments advertise.

nearby = scn.ScentienceDevice.scan_devices(timeout=10.0)
for d in nearby:
    print(d["name"], d["address"], d["rssi"])

Pass any value from the name column as device_uid / device_uids.


Logging and export

All readings from sample_ble() and stream_ble() are automatically buffered in memory across all connected devices.

print(device.log)             # list of dicts

device.export_json("readings.json")
device.export_csv("readings.csv")

device.clear_log()

CSV column order: UID and TIMESTAMP first, then all remaining fields alphabetically. Missing fields are written as empty values.


Response payload

Each reading dict may contain:

Key Description
UID Device serial number
TIMESTAMP Reading timestamp
ENV_temperatureC Ambient temperature (°C)
ENV_humidity Relative humidity (%)
ENV_pressureHpa Barometric pressure (hPa)
BATT_health Battery health
BATT_v Battery voltage
BATT_charge Battery charge (%)
BATT_time Estimated battery time remaining
STATUS_opuA Operational status
CO2, NH3, NO, NO2, CO, C2H5OH Chemical compounds (non-zero only)
H2, CH4, C3H8, C4H10, H2S, HCHO, SO2, VOC Chemical compounds (non-zero only)

COLIP embedding models

ColipModel integrates the Olfaction-Vision-Language Embeddings to produce joint embeddings across olfaction, vision, and language modalities.

Four variants are available:

Variant Embed dim Architecture Best for
colip-small-base 512 base GNN Fast inference / edge devices
colip-small-gat 512 GAT Higher accuracy on edge devices
colip-large-base 2048 base GNN Accuracy-critical tasks
colip-large-gat 2048 GAT Highest accuracy, slower inference

Weights are downloaded from HuggingFace Hub and cached locally on first use.

Example — small base model

import scentience as scn
from PIL import Image

model = scn.ColipModel.from_pretrained("colip-small-base")
# ColipModel(variant='colip-small-base', embed_dim=512, device='cpu')

image   = Image.open("scene.jpg")
olf_vec = [0.0] * 138          # 138-dimensional olfactory descriptor

embedding = model.embed(image, olf_vec)       # torch.Tensor, shape (512,)
arr       = model.embed_numpy(image, olf_vec) # numpy array

Example — large GAT model

model = scn.ColipModel.from_pretrained("colip-large-gat")
embedding = model.embed(image, olf_vec)       # torch.Tensor, shape (2048,)

Example — BLE streaming + real-time embedding

import scentience as scn
from PIL import Image

model  = scn.ColipModel.from_pretrained("colip-small-base")
device = scn.ScentienceDevice(api_key="YOUR_API_KEY")
device.connect_ble(char_uuid="YOUR_CHAR_UUID", device_uid="A00022")

scene = Image.open("scene.jpg")

def on_sample(data: dict) -> None:
    olf_vec   = [data.get(k, 0.0) for k in sorted(data) if k not in ("UID", "TIMESTAMP")]
    embedding = model.embed(scene, olf_vec)
    print(f"embedding shape: {embedding.shape}, norm: {embedding.norm():.4f}")

device.stream_ble(callback=on_sample)

Targeting a specific compute backend

model = scn.ColipModel.from_pretrained("colip-large-base", device="cuda")  # GPU
model = scn.ColipModel(variant="colip-small-gat", device="mps")             # Apple Silicon

Cloud inference — OVL API

OVLClient calls the hosted Olfaction-Vision-Language model with an API key, so you don't download weights or install torch/transformers — it uses only the Python standard library. It maps a molecule, a gas-sensor reading, or text/image into one shared embedding space, and can localize which object in an image is emitting a detected aroma (with an optional heatmap overlay).

import scentience as scn

ovl = scn.OVLClient(api_key="YOUR_API_KEY")   # get a key from the Scentience portal

# 1) molecule -> embedding + predicted odor descriptors
r = ovl.embed_molecule("CC(=C)C1CCC(C)=CC1")  # limonene
print([d["descriptor"] for d in r["descriptors"]])
# -> ['citrus', 'terpenic', 'herbal', 'woody']

# 2) "which object in this image emits this smell?" + save the heatmap overlay
with open("scene.jpg", "rb") as f:
    image = f.read()
g = ovl.ground(image, smiles="CC(=C)C1CCC(C)=CC1", return_heatmap=True)
print(g["regions"][0])                         # highest-scoring image region
scn.OVLClient.save_heatmap(g, "aroma_heatmap.png")

The first vision call (ground / embed_image / embed_text) cold-loads SigLIP on the scale-to-zero server and can take ~1–2 minutes; warm calls are much faster. Vision methods use a longer timeout (vision_timeout=300s) by default — tune with OVLClient(api_key=…, vision_timeout=…), or pass ground(…, grid=2) for faster, coarser heatmaps.

method what it does
embed_molecule(smiles, top_k=8) molecule → 1152-d embedding + odor descriptors
embed_sensor(readings, top_k=8) e-nose window [T, 6] → embedding + substance + descriptors
descriptors(smiles=… | readings=…) just the top odor descriptors
embed_text(text) / embed_image(bytes) text / image → shared-space embedding
ground(image, smiles=… | readings=…, return_heatmap=False) aroma-source regions (+ optional heatmap PNG)
info() / health() endpoint metadata / liveness

Example — live device reading → cloud inference

import scentience as scn

ovl    = scn.OVLClient(api_key="YOUR_API_KEY")
device = scn.ScentienceDevice(api_key="YOUR_API_KEY")
device.connect_ble(char_uuid="YOUR_CHAR_UUID", device_uid="A00022")

# collect a short window of sensor samples, then embed it
window = [scn.OVLClient.device_reading_to_ovl(device.sample_ble()) for _ in range(64)]
result = ovl.embed_sensor(window)
print(result["substance"], [d["descriptor"] for d in result["descriptors"]])

Note on the sensor path. The OVL sensor encoder was trained on the SmellNet MOX array, whose channels don't line up 1:1 with the Scentience device. device_reading_to_ovl provides a best-effort mapping, so device-sourced sensor inference is experimental until the encoder is recalibrated to your hardware. The molecule and vision/grounding endpoints are unaffected and production-ready.

A private deployment can be targeted with OVLClient(api_key=..., base_url="https://your-host"). A runnable end-to-end script is in example_ovl_cloud.py.


API reference

See the full BLE API documentation.

License

Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

scentience-2.2.1.tar.gz (29.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

scentience-2.2.1-py3-none-any.whl (24.3 kB view details)

Uploaded Python 3

File details

Details for the file scentience-2.2.1.tar.gz.

File metadata

  • Download URL: scentience-2.2.1.tar.gz
  • Upload date:
  • Size: 29.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.0

File hashes

Hashes for scentience-2.2.1.tar.gz
Algorithm Hash digest
SHA256 95c21a21dea03df380867341c7759dfa40670f51b8335d80a0920f80d622b983
MD5 a08cc576b692701fba66b2bbdd88f56a
BLAKE2b-256 278d4630932dd33ec776c0ca7e7346c9d157cc378bf478fa8358a95b9ac804a6

See more details on using hashes here.

File details

Details for the file scentience-2.2.1-py3-none-any.whl.

File metadata

  • Download URL: scentience-2.2.1-py3-none-any.whl
  • Upload date:
  • Size: 24.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.0

File hashes

Hashes for scentience-2.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9264fa9f0d26c391c4971bbc19ec56afa45a2b502cc3a238349977ab7e4e52fc
MD5 cfaeb7c9994e2482263de97af48e6086
BLAKE2b-256 0529ab56c5d6e5a0bec6ea2da5e4bd8d0145fe280afa55fe8aa0a30532539a96

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page