Named, ready-to-run device profiles from plain data or a JSON manifest, run as a node in every language. One capability of pamoja, one memory-safe Rust core with bindings for TypeScript, Python, and C#.
npm install @pamoja/profile
This pulls in @pamoja/native, the compiled engine. npm install pamoja is the whole framework in one package.
The test that runs in CI, spliced here as it ran.
From bindings/node/guides/profile.ts:
import { readFileSync } from 'node:fs'
import { LoopbackBroker } from '@pamoja/loopback'
import { Node, Profile } from '@pamoja/profile'
// A profile is a file. This one ships in the catalog under profiles/: it holds a brooder at
// 32 C by switching a heat lamp, says what it reads, and says how a dashboard draws it.
const text = readFileSync('profiles/brooder-heater.json', 'utf8')
const profile = Profile.fromJson(text)
async function example(): Promise<boolean> {
const reads = profile.reads!
console.log(`profile ${profile.name} reads ${reads.quantity} in ${reads.unit} and reports on ${profile.topic}`)
// A node is the profile and the parts that make it run: a sensor, an output, and a link.
// A morning of readings stands in for the probe, and a dashboard listens on the same
// broker.
const broker = new LoopbackBroker()
const link = broker.link()
const dashboard = broker.link()
await link.connect()
await dashboard.connect()
await dashboard.subscribe(profile.topic)
const morning = [27.5, 31.8, 32.6, 32.1, 31.4]
let lamp = false
const node = new Node({
profile,
read: () => morning.shift()!,
drive: (on) => {
lamp = on
},
link,
})
// Each tick reads, decides, switches the lamp, and publishes the reading. The lamp comes on
// at 31.5 C or below and goes off at 32.5 C or above, and in between it stays as it was; a
// reading more than 4 C from 32 raises an alert as well.
let was = false
for (let at = 0; at < 5; at += 1) {
const { reading, reaction } = await node.tick()
const on = reaction.actuator === true
const change = on ? (was ? 'lamp stays on' : 'lamp on') : was ? 'lamp off' : 'lamp stays off'
const alert = reaction.alert ? `, alert ${reaction.alert.kind}` : ''
console.log(`${`${reading} C`.padEnd(10)}${change}${alert}`)
was = on
}
// The dashboard heard every reading the node published.
const heard: number[] = []
for (let at = 0; at < 5; at += 1) {
heard.push((await dashboard.recv())!.number!)
}
console.log(`heard ${heard.join(', ')} on ${profile.topic}`)
// Between ticks the node waits as long as its battery allows: often on a healthy charge,
// sparingly on a low one. run() does this until stopped, waiting each interval.
for (const charge of [0.8, 0.3, 0.1]) {
const { mode, waitMs } = node.schedule(charge)
console.log(`battery at ${(charge * 100).toFixed(0)}% it runs ${mode} and waits ${waitMs / 1000} s`)
}
// The same file says how a dashboard draws the node.
const element = profile.presentation!.elements[0]
const [low, high] = element.band!
console.log(`draws ${element.key} in ${element.unit} on a ${element.viz}, safe from ${low} to ${high}`)
return lamp
}
| Language | Package | Reference |
|---|---|---|
| Rust | pamoja-profile |
reference, docs.rs, install |
| TypeScript | @pamoja/profile |
reference, install |
| Python | pamoja-profile |
reference, install |
| C# | Pamoja.Profile |
reference, install |
@pamoja/profile reference, every class, function, and type this package exports.MIT
Ergonomic facade over the generated device-profile binding.
A profile is a named, pre-wired bundle: a control policy, a publish topic, and a power schedule. Instantiate one rather than choosing algorithms and tuning constants by hand.
A profile is the manifest, which loads from and saves to JSON so it ships as a file. A controller is the decision logic that manifest describes: hand it a reading and it says what the output should do and whether the reading crossed a threshold worth raising. The presentation a dashboard reads travels inside the manifest JSON and is read and built here as a typed Presentation.
A Node runs a profile: each tick reads, lets the profile's policy decide, switches the output, and publishes the reading over a link, and
runrepeats that at the cadence the battery allows. A PolicyRegistry resolves a control kind the library never shipped to the program's own code.A rule file decides between nodes: a RuleEngine runs it off a link, switching outputs by name and publishing as rules set and clear, and RuleEvaluator is its deciding half for a program that moves its own messages.
The alert, control, and rule action kinds are re-exported as runtime objects, because the generated enums are types-only.