pamoja

An SDK for IoT, robotics, and drones

pamojaRev 0.2.0MIT licensepamoja.molex.cloud

Running a profile#

A profile says what a node does: what it reads, when it switches its output, where it reports, and how often it samples as its battery drains. A wiring file says where that happens: the part on this site's bus, the line its relay is on, and the broker down the hall. pamoja-node reads the two and runs the node, with no program to write. Every coop runs the same profile, and each coop has a wiring file of its own.

Install#

Shell
cargo install pamoja-profile --features runner

From a checkout of the repository, cargo install --path crates/pamoja-profile --features runner builds the same binary. The runner reaches real parts through the Linux kernel's I2C, GPIO, and 1-Wire files, so it runs them on a Raspberry Pi or any Linux board. A replayed reading, a simulated part, and a printed output and link work on every system, which is how the next section tries a profile on a laptop.

Try it with nothing wired#

Fetch a profile from the catalog, and write a wiring file beside it that plays back three readings and prints what the node does:

Shell
curl -O https://raw.githubusercontent.com/molexxxx/pamoja/main/profiles/brooder-heater.json
JSON
{
  "$schema": "https://pamoja.molex.cloud/schema/wiring-1.json",
  "site": "coop-2",
  "profile": "brooder-heater.json",
  "sensor": { "part": "replay", "readings": [27.5, 31.8, 32.6] },
  "output": { "print": "heat lamp" },
  "link": { "print": true }
}
Shell
pamoja-node try.json --fast
Text
coop-2 runs brooder-heater: temperature in celsius from 3 replayed readings, the heat lamp printed, reporting on poultry/brooder/temperature over print
-> poultry/brooder/temperature 27.5
27.5 celsius, heat lamp on, alert OutOfRange
-> poultry/brooder/temperature 31.8
31.8 celsius, heat lamp on
-> poultry/brooder/temperature 32.6
32.6 celsius, heat lamp off
the sensor has no more readings

The profile holds the brooder at 32 C with the lamp on below 31.5 and off above 32.5, so 31.8 leaves the lamp on and 32.6 turns it off. --fast ticks again at once rather than waiting the two minutes the profile asks for. A "bus": "sim" in place of the replay reads a part's simulated twin through its real driver, one fixed measurement at a time.

Wire a site#

The same profile on a real coop: a BME280 at address 0x77 on the header's I2C bus, a relay board on GPIO 17 that switches on when the line is driven low, and a broker on the local network. The Raspberry Pi page shows the wires.

JSON
{
  "$schema": "https://pamoja.molex.cloud/schema/wiring-1.json",
  "site": "coop-2",
  "profile": "brooder-heater.json",
  "sensor": { "part": "bme280", "bus": "/dev/i2c-1", "address": "0x77" },
  "output": { "gpio": "/dev/gpiochip0", "line": 17, "active_low": true },
  "link": { "mqtt": "192.168.1.10" }
}

--check reads both files, says what would run, and opens nothing:

Shell
pamoja-node coop-2.json --check
Text
coop-2 runs brooder-heater: temperature in celsius from a bme280 at 0x77 on /dev/i2c-1, the output on /dev/gpiochip0 line 17, reporting on poultry/brooder/temperature over mqtt 192.168.1.10:1883

Without it, the node runs until it is stopped. A tick that fails, a loose wire or a broker that went away, is reported and tried again at the next interval rather than ending the node.

A part that measures in another unit is converted to the profile's: a BME280 reads celsius, and a profile that reads fahrenheit gets fahrenheit. The runner converts temperature between celsius, fahrenheit, and kelvin, pressure between pascal, hectopascal, millibar, kilopascal, and bar, and voltage between volt and millivolt. A probe that reads high or low is corrected with the sensor's scale and offset, which apply once the reading is in the profile's unit.

Run it at boot#

A node has to come back after a power cut without anyone logging in, which on Linux means a systemd unit. Copy the binary somewhere every account can run it, put the two files under /etc/pamoja, and write the unit to /etc/systemd/system/pamoja-node.service:

Shell
sudo install ~/.cargo/bin/pamoja-node /usr/local/bin/
ini
[Unit]
Description=pamoja node
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/pamoja-node /etc/pamoja/coop-2.json
Restart=on-failure
RestartSec=10
DynamicUser=yes
SupplementaryGroups=i2c gpio

[Install]
WantedBy=multi-user.target

sudo systemctl enable --now pamoja-node starts it now and at every boot, and journalctl -u pamoja-node -f follows the lines it prints. The node runs as an account of its own in the i2c and gpio groups, so it never needs root.

On a battery#

A node on a panel and a battery reads the battery's voltage through a power monitor, and the profile's power schedule sets how long it waits between readings as the charge drops. Name the monitor and the battery's empty and full voltages:

JSON
"battery": { "part": "ina219", "bus": "/dev/i2c-1", "empty_volts": 11.8, "full_volts": 12.7 }

The charge is read as a straight line between the two voltages. A monitor that does not answer is taken as a critical battery, so a fault makes the node sample less rather than more. Without a battery, the node is on mains and samples at the profile's active interval.

Parts#

The parts the runner reads, the quantities each measures as a profile's reads names them, and where each is found. The sensor drivers guide has the datasheet behind each one.

partMeasures, as reads names itOnUsual address
bme280temperature in celsius, relative_humidity in percent, pressure in hectopascalan I2C bus0x76
bmp280temperature in celsius, pressure in hectopascalan I2C bus0x76
sht3xtemperature in celsius, relative_humidity in percentan I2C bus0x44
hdc1080temperature in celsius, relative_humidity in percentan I2C bus0x40
tmp117temperature in celsiusan I2C bus0x48
scd4xco2 in ppm, temperature in celsius, relative_humidity in percentan I2C bus0x62
opt3001illuminance in luxan I2C bus0x44
ina219voltage in voltan I2C bus0x40
ina226voltage in voltan I2C bus0x40
ds18b20temperature in celsiusthe kernel's 1-Wire files, by serial-
replaywhatever the profile reads, from its readingsnothing-

What it refuses#

Both files are checked before any part is opened, and each refusal says what to change:

  • A profile that reads what the part does not measure: the profile soil-moisture-valve reads soil_moisture, and a bme280 measures temperature, relative_humidity, or pressure.
  • A misspelled field, with the nearest one it knows: unknown field outptu, did you mean output?
  • A profile that switches an output with no output wired, or an output for a profile that switches nothing.
  • A unit the runner does not convert, an address past 0x7f, a password without a username, a client certificate without its key, and a battery whose full voltage is not above its empty one.
  • A profile whose control kind is not built in. A kind of your own is decided by code, so it runs in a program of its own that registers that code, as the profile guide shows.

Every field#

The wiring file has a published JSON Schema, named by its $schema field, so an editor completes the fields and marks a wrong one as it is typed. The runner's own check is the last word, since it also knows which parts measure what.

The wiring file#

One site's wiring: the part that reads what a profile reads, the line its output drives, the link its readings go over, and the battery it runs from. pamoja-node reads it with the profile it names and runs the node.

FieldValueRequiredWhat it does
$schematextnoThe format the file is written in: this schema's address, or a copy of it by the same file name. An editor reads it to check the file as it is typed.
sitetextyesThe site's name, such as coop-2, which the node's logs and its MQTT client id carry.
profiletextyesThe profile to run, as a path relative to this file, such as brooder-heater.json.
sensorobject, see sensoryesThe part that takes the readings. It must measure the quantity the profile reads.
outputobject, gpio or printnoThe output a setpoint profile switches: a GPIO line, or a printed line for trying a profile with nothing wired. Leave it out for a profile that switches nothing.
linkobject, mqtt or printyesThe link each reading is published over.
batteryobject, see batterynoThe battery the node runs from, read as a voltage through a power monitor to set how often the node samples. Leave it out on mains power, and the node samples at the profile's active cadence.

sensor#

The part that takes the readings. It must measure the quantity the profile reads.

FieldValueRequiredWhat it does
partone of bme280, bmp280, sht3x, hdc1080, tmp117, scd4x, opt3001, ina219, ina226, ds18b20, replayyesThe part, as the table of parts names it.
bustextnoThe I2C bus the part is on, such as /dev/i2c-1, or sim for the part's simulated twin, which answers with one fixed measurement. Every part but a ds18b20 and a replay needs one.
addresswhole number, at least 0, at most 127, or textnoThe part's I2C address, as a number or as hexadecimal text such as "0x77", when it is not the part's usual one.
serialtextnoA ds18b20's 1-Wire serial, such as 28-0316a2795cff, as the kernel names its folder under /sys/bus/w1/devices.
readingslist of numbernoThe readings a replay plays back in turn, in the profile's unit, for trying a profile with nothing wired.
offsetnumberno, 0Added to each reading once it is in the profile's unit, to correct a probe that reads high or low.
scalenumberno, 1Multiplied into each reading before the offset is added.

output, gpio#

A GPIO line, such as the input of a relay board.

FieldValueRequiredWhat it does
gpiotextyesThe GPIO chip, such as /dev/gpiochip0.
linewhole number, at least 0yesThe line on the chip, which on a Raspberry Pi is the GPIO number, such as 17.
active_lowtrue or falseno, falsetrue when the line is driven low to switch the output on, as most relay boards want.

output, print#

Names the output in each tick's line instead of driving a line.

FieldValueRequiredWhat it does
printtextyesWhat the output is called in the text, such as heat lamp.

link, mqtt#

An MQTT broker.

FieldValueRequiredWhat it does
mqtttextyesThe broker's host name or address.
portwhole number, at least 1, at most 65535noThe broker's port: 1883 unless given, or 8883 with tls.
client_idtextnoThe client id; the site's name unless given.
usernametextnoThe username to sign in with.
passwordtextnoThe password to sign in with, which goes with a username.
tlsobject, see link, tlsnoTLS to the broker. An empty object trusts the system's certificate authorities.

Prints each reading with its topic instead of publishing it.

FieldValueRequiredWhat it does
printtrueyestrue.

link, tls#

TLS to the broker. An empty object trusts the system's certificate authorities.

FieldValueRequiredWhat it does
catextnoThe certificate authority to trust, as a PEM file; the system's own unless given.
certificatetextnoThe client certificate to present, as a PEM file, for a broker that asks for one.
keytextnoThe client certificate's private key, as a PEM file, which goes with a certificate.

battery#

The battery the node runs from, read as a voltage through a power monitor to set how often the node samples. Leave it out on mains power, and the node samples at the profile's active cadence.

FieldValueRequiredWhat it does
partone of ina219, ina226yesThe power monitor across the battery.
bustextyesThe I2C bus the monitor is on, or sim.
addresswhole number, at least 0, at most 127, or textnoThe monitor's I2C address, when it is not 0x40.
empty_voltsnumberyesThe voltage the battery reads when it is empty.
full_voltsnumberyesThe voltage the battery reads when it is full, above empty_volts.

Where next#

Edit this page on GitHub