# JavaScript Quickstart (Linux)

Use the SmartSpectra C++ SDK from Node.js to measure pulse and breathing rate from a webcam.

**Platform:** Ubuntu 22.04 / Linux Mint 21 (x86_64) only

**Requirements:** Node.js 18+, webcam, API key from [physiology.presagetech.com](https://physiology.presagetech.com)

> **Note:** There is no `npm install` package for SmartSpectra. This guide builds a Node.js native addon around the C++ SDK.

---

## 1. Install the C++ SDK and Node.js

```bash
sudo apt update
sudo apt install -y build-essential libcurl4-openssl-dev libssl-dev \
  pkg-config libv4l-dev libgles2-mesa-dev libunwind-dev gpg curl \
  libopencv-dev

# CMake 3.27+
curl -L -o cmake-3.27.0-linux-x86_64.sh \
  https://github.com/Kitware/CMake/releases/download/v3.27.0/cmake-3.27.0-linux-x86_64.sh
chmod +x cmake-3.27.0-linux-x86_64.sh
sudo ./cmake-3.27.0-linux-x86_64.sh --skip-license --prefix=/usr/local

# Node.js 18+ (via NodeSource)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs

# Presage PPA
curl -s "https://presage-security.github.io/PPA/KEY.gpg" | gpg --dearmor | \
  sudo tee /etc/apt/trusted.gpg.d/presage-technologies.gpg >/dev/null
sudo curl -s --compressed -o /etc/apt/sources.list.d/presage-technologies.list \
  "https://presage-security.github.io/PPA/presage-technologies.list"
sudo apt update
sudo apt install libsmartspectra-dev
```

## 2. Create the Project

```bash
mkdir smartspectra-node && cd smartspectra-node
npm init -y
npm install node-addon-api
```

### Native Addon (C++)

**smartspectra_addon.cpp:**

```cpp
#include <napi.h>
#include <smartspectra/container/foreground_container.hpp>
#include <smartspectra/container/settings.hpp>
#include <physiology/modules/messages/metrics.h>
#include <physiology/modules/messages/status.h>
#include <glog/logging.h>
#include <opencv2/opencv.hpp>
#include <memory>
#include <mutex>
#include <atomic>
#include <thread>

using namespace presage::smartspectra;

struct SmartSpectraHandle {
    std::unique_ptr<container::CpuContinuousRestForegroundContainer> container;
    std::mutex mu;
    float pulse_rate = 0;
    float breathing_rate = 0;
    bool has_data = false;
    std::atomic<bool> running{false};
    std::thread processing_thread;
};

static SmartSpectraHandle* g_handle = nullptr;

Napi::Value Create(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();

    if (info.Length() < 1 || !info[0].IsString()) {
        Napi::TypeError::New(env, "API key string required")
            .ThrowAsJavaScriptException();
        return env.Null();
    }

    static bool glog_init = false;
    if (!glog_init) {
        google::InitGoogleLogging("smartspectra_node");
        FLAGS_alsologtostderr = false;
        glog_init = true;
    }

    std::string api_key = info[0].As<Napi::String>().Utf8Value();
    int camera_index = info.Length() > 1 ? info[1].As<Napi::Number>().Int32Value() : 0;

    g_handle = new SmartSpectraHandle();

    container::settings::Settings<
        container::settings::OperationMode::Continuous,
        container::settings::IntegrationMode::Rest
    > settings;

    settings.video_source.device_index = camera_index;
    settings.video_source.capture_width_px = 1280;
    settings.video_source.capture_height_px = 720;
    settings.video_source.codec = presage::camera::CaptureCodec::MJPG;
    settings.video_source.auto_lock = true;
    settings.headless = true;
    settings.enable_edge_metrics = true;
    settings.verbosity_level = 0;
    settings.continuous.preprocessed_data_buffer_duration_s = 0.5;
    settings.integration.api_key = api_key;

    g_handle->container =
        std::make_unique<container::CpuContinuousRestForegroundContainer>(
            settings);

    g_handle->container->SetOnCoreMetricsOutput(
        [](const presage::physiology::MetricsBuffer& metrics,
           int64_t timestamp) {
            std::lock_guard<std::mutex> lock(g_handle->mu);
            if (!metrics.pulse().rate().empty())
                g_handle->pulse_rate =
                    metrics.pulse().rate().rbegin()->value();
            if (!metrics.breathing().rate().empty())
                g_handle->breathing_rate =
                    metrics.breathing().rate().rbegin()->value();
            g_handle->has_data = true;
            return absl::OkStatus();
        }
    );

    g_handle->container->SetOnVideoOutput(
        [](cv::Mat& frame, int64_t timestamp) {
            if (!g_handle->running.load())
                return absl::CancelledError("Stopped");
            return absl::OkStatus();
        }
    );

    g_handle->container->SetOnStatusChange(
        [](presage::physiology::StatusValue status) {
            fprintf(stderr, "[SmartSpectra] Status: %s\n",
                presage::physiology::GetStatusDescription(
                    status.value()).c_str());
            return absl::OkStatus();
        }
    );

    return Napi::Boolean::New(env, true);
}

Napi::Value Initialize(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (!g_handle) {
        Napi::Error::New(env, "Call create() first").ThrowAsJavaScriptException();
        return env.Null();
    }
    bool ok = g_handle->container->Initialize().ok();
    return Napi::Boolean::New(env, ok);
}

Napi::Value Start(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (!g_handle) {
        Napi::Error::New(env, "Call create() first").ThrowAsJavaScriptException();
        return env.Null();
    }
    g_handle->running.store(true);

    // Run processing in a separate thread (it blocks)
    g_handle->processing_thread = std::thread([] {
        g_handle->container->Run();
    });

    return Napi::Boolean::New(env, true);
}

Napi::Value GetMetrics(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (!g_handle) return env.Null();

    std::lock_guard<std::mutex> lock(g_handle->mu);
    if (!g_handle->has_data) return env.Null();

    Napi::Object result = Napi::Object::New(env);
    result.Set("pulseRate", Napi::Number::New(env, g_handle->pulse_rate));
    result.Set("breathingRate", Napi::Number::New(env, g_handle->breathing_rate));
    return result;
}

Napi::Value Stop(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (g_handle) {
        g_handle->running.store(false);
        if (g_handle->processing_thread.joinable()) {
            g_handle->processing_thread.join();
        }
    }
    return Napi::Boolean::New(env, true);
}

Napi::Value Destroy(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (g_handle) {
        delete g_handle;
        g_handle = nullptr;
    }
    return Napi::Boolean::New(env, true);
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
    exports.Set("create", Napi::Function::New(env, Create));
    exports.Set("initialize", Napi::Function::New(env, Initialize));
    exports.Set("start", Napi::Function::New(env, Start));
    exports.Set("getMetrics", Napi::Function::New(env, GetMetrics));
    exports.Set("stop", Napi::Function::New(env, Stop));
    exports.Set("destroy", Napi::Function::New(env, Destroy));
    return exports;
}

NODE_API_MODULE(smartspectra_addon, Init)
```

### Build Configuration

**binding.gyp:**

```json
{
  "targets": [
    {
      "target_name": "smartspectra_addon",
      "sources": ["smartspectra_addon.cpp"],
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      "libraries": [
        "<!@(pkg-config --libs smartspectra opencv4 2>/dev/null || echo '')",
        "-lsmartspectra_container",
        "-lglog"
      ],
      "cflags!": ["-fno-exceptions"],
      "cflags_cc!": ["-fno-exceptions"],
      "cflags_cc": [
        "-std=c++17",
        "<!@(pkg-config --cflags smartspectra opencv4 2>/dev/null || echo '')"
      ],
      "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"]
    }
  ]
}
```

> **Note:** The `libraries` and `cflags_cc` lines use `pkg-config` to find SmartSpectra and OpenCV. If `pkg-config` doesn't find SmartSpectra, you may need to adjust the paths to match your installation (typically `/usr/local/lib` and `/usr/local/include`).

Alternatively, use **CMake** to build the addon with `cmake-js` for more reliable dependency resolution:

```bash
npm install cmake-js
```

**CMakeLists.txt:**

```cmake
cmake_minimum_required(VERSION 3.27.0)
project(SmartSpectraAddon CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(SmartSpectra REQUIRED)
find_package(OpenCV REQUIRED)

# cmake-js setup
include_directories(${CMAKE_JS_INC})
file(GLOB SOURCE_FILES "smartspectra_addon.cpp")

add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")

target_include_directories(${PROJECT_NAME} PRIVATE
  ${CMAKE_JS_INC}
  "${CMAKE_SOURCE_DIR}/node_modules/node-addon-api"
)

target_link_libraries(${PROJECT_NAME}
  SmartSpectra::Container
  ${OpenCV_LIBS}
  ${CMAKE_JS_LIB}
)
```

## 3. Build

Using node-gyp (simpler):

```bash
npx node-gyp configure build
```

Or using cmake-js (more reliable with CMake-based deps):

```bash
npx cmake-js build
```

The native addon is compiled to `build/Release/smartspectra_addon.node`.

## 4. Write the App

**hello_vitals.js:**

```javascript
const smartspectra = require('./build/Release/smartspectra_addon');

const apiKey = process.argv[2] || process.env.SMARTSPECTRA_API_KEY;

if (!apiKey) {
  console.log('Usage: node hello_vitals.js YOUR_API_KEY');
  console.log('Or:    export SMARTSPECTRA_API_KEY=YOUR_KEY');
  console.log('Get a key at: https://physiology.presagetech.com');
  process.exit(1);
}

console.log('Creating SmartSpectra instance...');
smartspectra.create(apiKey);

console.log('Initializing camera...');
if (!smartspectra.initialize()) {
  console.error('Failed to initialize');
  process.exit(1);
}

console.log('Starting processing...');
smartspectra.start();

console.log('Running! Reading metrics every 2 seconds.');
console.log('Press Ctrl+C to stop.\n');

// Poll for metrics
const interval = setInterval(() => {
  const metrics = smartspectra.getMetrics();
  if (metrics) {
    console.log(
      `Pulse: ${metrics.pulseRate.toFixed(0)} BPM | ` +
      `Breathing: ${metrics.breathingRate.toFixed(1)} BPM`
    );
  } else {
    console.log('Waiting for metrics...');
  }
}, 2000);

// Graceful shutdown
process.on('SIGINT', () => {
  console.log('\nStopping...');
  clearInterval(interval);
  smartspectra.stop();
  smartspectra.destroy();
  console.log('Done.');
  process.exit(0);
});
```

## 5. Run

```bash
node hello_vitals.js YOUR_API_KEY

# Or with environment variable
export SMARTSPECTRA_API_KEY=YOUR_API_KEY
node hello_vitals.js
```

Output:

```
Creating SmartSpectra instance...
Initializing camera...
Starting processing...
Running! Reading metrics every 2 seconds.
Press Ctrl+C to stop.

Waiting for metrics...
Waiting for metrics...
Pulse: 72 BPM | Breathing: 16.2 BPM
Pulse: 73 BPM | Breathing: 15.8 BPM
^C
Stopping...
Done.
```

## How It Works

```
Node.js → Native addon (N-API) → SmartSpectra C++ SDK → Camera + REST API
```

1. `smartspectra_addon.cpp` wraps the C++ SDK as a Node.js native addon using N-API
2. `hello_vitals.js` calls the addon to create, initialize, and start processing
3. The SDK runs in a background thread, capturing frames and calling the Presage REST API
4. JavaScript polls for the latest metrics using `setInterval`

## Platform Support

This approach only works on **Linux x86_64** (Ubuntu 22.04 / Mint 21). There is no browser-based JavaScript SDK at this time.

## Troubleshooting

**`Error: Cannot find module './build/Release/smartspectra_addon'`**
- Run `npx node-gyp configure build` (or `npx cmake-js build`) first
- Check that `build/Release/smartspectra_addon.node` exists

**node-gyp build errors**
- Ensure `node-addon-api` is installed: `npm install node-addon-api`
- Verify SmartSpectra is installed: `dpkg -l | grep smartspectra`
- Try the CMake approach with `cmake-js` instead

**Camera not found**
- Check: `ls /dev/video*`
- Change `cameraIndex` in the `create()` call

For other issues, see the [C++ troubleshooting](/docs/quickstart-cpp#troubleshooting) section.

## Next Steps

- See the [C++ quickstart](/docs/quickstart-cpp) for the full C++ API
- Browse the [SDK source](https://github.com/Presage-Security/SmartSpectra/tree/main/cpp)
