Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Expand All @@ -20,33 +23,17 @@ jobs:
matrix:
include:
# Ubuntu – GCC
- os: ubuntu-24.04
compiler: gcc-13
cc: gcc-13
cxx: g++-13
std: 17
- os: ubuntu-24.04
compiler: gcc-14
cc: gcc-14
cxx: g++-14
std: 17
# Ubuntu – Clang
- os: ubuntu-24.04
compiler: clang-17
cc: clang-17
cxx: clang++-17
std: 17
- os: ubuntu-24.04
compiler: clang-18
cc: clang-18
cxx: clang++-18
std: 17
# macOS – Apple Clang
- os: macos-14
compiler: apple-clang
cc: clang
cxx: clang++
std: 17

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -74,3 +61,20 @@ jobs:
- name: Test
working-directory: build
run: ctest --output-on-failure --parallel $(nproc 2>/dev/null || sysctl -n hw.logicalcpu)

build-native-module:
name: Build native module
runs-on: ubuntu-latest
env:
WORKING_DIRECTORY: 'node-module'


steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
working-directory: ${{ env.WORKING_DIRECTORY }}
run: bun install
- name: Build
working-directory: ${{ env.WORKING_DIRECTORY }}
run: bun run build
Comment thread Fixed
40 changes: 40 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,43 @@ jobs:
sero-${{ steps.version.outputs.VERSION }}.zip
draft: false
prerelease: ${{ contains(steps.version.outputs.VERSION, '-') }}

build-and-publish-native-module:
name: Build and Publish Native Module
runs-on: ubuntu-latest
needs:
- build-and-release
env:
REGISTRY_URL: ${{ secrets.REGISTRY_URL }}
WORKING_DIRECTORY: 'node-module'
APP_NAME: 'sero-node'
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
registries: |
https://registry.npmjs.org/
@asl-gokart:${{ env.REGISTRY_URL }}
- name: Install dependencies
working-directory: ${{ env.WORKING_DIRECTORY }}
run: bun install
- name: Build
working-directory: ${{ env.WORKING_DIRECTORY }}
run: bun run build
- name: Rename artifact
working-directory: ${{ env.WORKING_DIRECTORY }}
run: |
mv ./build/Release/${{ env.APP_NAME }}.node ./${{ env.APP_NAME }}-linux-x64.node
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.APP_NAME }}-linux-x64
path: |
${{ env.WORKING_DIRECTORY }}/${{ env.APP_NAME }}-linux-x64.node
if-no-files-found: error
- name: Publish to registry
working-directory: ${{ env.WORKING_DIRECTORY }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_REGISTRY_AUTH_TOKEN }}
run: bun publish --registry ${{ env.REGISTRY_URL }} --access public
36 changes: 36 additions & 0 deletions node-module/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# dependencies (bun install)
node_modules

# output
out
build
dist
*.tgz
index.js

# code coverage
coverage
*.lcov

# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# caches
.eslintcache
.cache
*.tsbuildinfo

# IntelliJ based IDEs
.idea

# Finder (MacOS) folder config
.DS_Store
140 changes: 140 additions & 0 deletions node-module/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# sero-node

Node.js native bindings for the [sero](../sero) C++17 header-only protocol library. Exposes the full `sero::Runtime` API — service discovery, request/response, event pub/sub, HMAC authentication, and diagnostics — over an embedded POSIX UDP transport (unicast + multicast).

## Prerequisites

- Node.js ≥ 18 (or Bun)
- `node-gyp` build toolchain (Python 3, C++17 compiler)
- The `sero` library checked out at `../sero` (sibling directory)

## Install & build

```bash
bun install # or npm install
bun run build # or npm run build
```

For Electron:

```bash
bun run electron-rebuild
```

## Quick start

```ts
import { SeroRuntime, ReturnCode, DtcSeverity } from "./index";

// ── Create a runtime (binds UDP on port 30491) ──────────────
const rt = new SeroRuntime({ port: 30491, clientId: 0x7701 });

// ── Provider: register & offer a service ────────────────────
rt.registerService(0x1000, {
onRequest(methodId, payload) {
if (methodId === 0x0001) {
return { returnCode: ReturnCode.E_OK, response: Buffer.from("pong") };
}
return { returnCode: ReturnCode.E_UNKNOWN_METHOD };
},
});
rt.offerService(0x1000, 30);

// ── Consumer: discover & call a remote service ──────────────
rt.onServiceFound((serviceId, address) => {
console.log(`Found 0x${serviceId.toString(16)} at ${address.ip}:${address.port}`);
});
rt.findService(0x0001); // e.g. ZC_LIGHTS_ID from ESP32

// Send a request (returns a Promise resolved during a future process() call)
const result = await rt.request(0x0001, 0x0002, Buffer.from([0x01]), 2000);
console.log("Response:", result.returnCode, result.payload);

// ── Event subscription (consumer) ───────────────────────────
rt.subscribeEvent(0x1000, 0x8001, (sid, eid, payload) => {
console.log("Event:", sid, eid, payload);
});

// ── Event notification (provider) ───────────────────────────
rt.registerEvent(0x1000, 0x8001);
rt.notifyEvent(0x1000, 0x8001, Buffer.from([0x42]));

// ── Drive the protocol at ~100 Hz ───────────────────────────
setInterval(() => rt.process(), 10);

// ── Cleanup ─────────────────────────────────────────────────
// rt.destroy();
```

## API overview

### `new SeroRuntime(options)`

| Option | Type | Default | Description |
| ---------- | -------- | ----------- | ------------------------------------ |
| `bindIp` | `string` | `"0.0.0.0"` | IPv4 address to bind |
| `port` | `number` | — | UDP port for unicast traffic |
| `clientId` | `number` | — | Unique client identifier (uint16) |

### Lifecycle

| Method | Description |
| --------------- | ---------------------------------------------- |
| `process()` | Drive one protocol cycle (poll, dispatch, SD) |
| `destroy()` | Release all resources |

### Provider

| Method | Description |
| --------------------------------------------------- | -------------------------------------------- |
| `registerService(id, handler, opts?)` | Register a service implementation |
| `unregisterService(id)` | Unregister a service |
| `offerService(id, ttl?)` | Announce via Service Discovery |
| `stopOffer(id)` | Stop announcing |
| `registerEvent(serviceId, eventId)` | Register a producible event |
| `notifyEvent(serviceId, eventId, payload)` | Push notification to subscribers |

### Consumer

| Method | Description |
| ---------------------------------------------------------------------- | ------------------------------------- |
| `findService(id, major?)` | Search via SD |
| `request(serviceId, methodId, payload?, timeout?) → Promise` | Send request, await response |
| `requestTo(addr, serviceId, methodId, payload?, timeout?) → Promise` | Request to explicit address |
| `fireAndForget(serviceId, methodId, payload?)` | One-way request |
| `subscribeEvent(serviceId, eventId, handler, ttl?)` | Subscribe to remote event |
| `unsubscribeEvent(serviceId, eventId)` | Unsubscribe |

### SD callbacks

| Method | Callback signature |
| ----------------------- | --------------------------------------------------------------- |
| `onServiceFound(cb)` | `(serviceId: number, address: Address) => void` |
| `onServiceLost(cb)` | `(serviceId: number) => void` |
| `onSubscriptionAck(cb)` | `(serviceId, eventId, returnCode, grantedTtl) => void` |

### Security

| Method | Description |
| ----------------------------- | ------------------------------------ |
| `setHmacKey(peer, key)` | Set 32-byte HMAC key for a peer |

### Diagnostics & DTCs

| Method | Description |
| ------------------------------ | ------------------------------------------ |
| `onDiagnostic(cb)` | Callback on counter increment |
| `getDiagnostics()` | Snapshot of all counters |
| `enableDiagnostics(auth?)` | Enable built-in diagnostics service |
| `reportDtc(code, severity)` | Report a trouble code |
| `clearDtc(code)` | Clear one DTC |
| `clearAllDtcs()` | Clear all DTCs |
| `getDtcs()` | Read all active DTCs |

### Constants

Exported as frozen objects: `ReturnCode`, `DtcSeverity`, `DiagnosticCounter`, `MessageType`.

## Compatibility with ESP32 devices

This module uses the same wire format and multicast group (`239.0.0.1:30490`) as the ESP32 PlatformIO projects (`zc_buttons`, `zc_lights`). A desktop Node.js app using sero-node can discover, call methods on, and receive events from ESP32 devices on the same network — no configuration needed.
29 changes: 29 additions & 0 deletions node-module/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"targets": [
{
"target_name": "sero-node",
"sources": [ "src/sero-node.cpp" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"../include"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"cflags_cc": [ "-std=c++17" ],
"xcode_settings": {
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
"CLANG_CXX_LANGUAGE_STANDARD": "c++17",
"MACOSX_DEPLOYMENT_TARGET": "10.15"
},
"msvs_settings": {
"VCCLCompilerTool": {
"ExceptionHandling": 1,
"AdditionalOptions": ["/std:c++17"]
}
}
}
]
}
Loading