Monitor of a PellX wooden pellets burner.
Intended to be run on a Raspberry Pi-equivalent device connected via GPIO to terminals on the controller board of a PellX burner. Terminals 1 and 2 are electrically connected when the burner is operating normally, and the circuit is broken when it is in an error state (including on power failures). See page 11 of the manual.
A notification is sent when this is detected. The program supports sending such through four different backends; as messages sent to Slack channels, as short emails sent via the free Batsign service, and by invocations of external commands.
Usage: pellxd [OPTIONS]
Options:
--source <option> Input source to poll [default: gpio] [possible values: gpio, dummy]
-c, --config <file> Specify an alternative configuration file
--save Write configuration to disk
--disable-timestamps Disable timestamps in terminal output
-v, --verbose Print some additional information
-d, --debug Print much more additional information
--dry-run Perform a dry run, echoing what would be done
Create a configuration file by passing --save.
cargo run -- --save[monitor]
source = "gpio"
[slack]
enabled = true
urls = ["https://hooks.slack.com/services/..."]cargo run -- --verboseThis project uses Cargo for compilation and dependency management. Grab it from your repositories, install it via Homebrew, or download it with the official rustup installation script.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shYou may have to add $HOME/.cargo/bin to your $PATH.
Use cargo build to build the project. This stores the resulting binary as target/<profile>/pellxd, where <profile> is one of debug or release, depending on what profile is being built. debug is the default; you can make it build in release mode with --release.
cargo build
cargo build --releaseTo compile the program and run it immediately, use cargo run. If you also want to pass command-line flags to the program, separate them from cargo run with double dashes --.
cargo run -- --help
cargo run -- --saveYou can find the binaries you compile with Cargo in the target/<profile>/ subdirectory of the project, where <profile> is either debug or release, depending on what profile you built with.
See the systemd section for instructions on how to set it up as a system daemon that is automatically started on boot.
Weaker Raspberry Pi models, such as the Pi Zero 2W, can run the program but may not have enough memory to compile it with default flags. Your alternatives there are to either build it in serial mode (with -j1) on the device itself, or to cross-compile it on a more powerful machine.
Regrettably, manually setting up cross-compilation can be non-trivial. As such, use of one of cargo-cross or cargo-zigbuild is recommended (but not required). For the latter you need to install a Zig compiler. Refer to your repositories, alternatively install it via Homebrew (brew install zig).
Note that your $CFLAGS environment variable must not contain -march=native for all dependencies to successfully build.
cargo install cargo-cross
CFLAGS="-O2 -pipe" cargo cross build --target=aarch64-unknown-linux-gnucargo install cargo-zigbuild
CFLAGS="-O2 -pipe" cargo zigbuild --target=aarch64-unknown-linux-gnuThis should require upwards of 500 Mb of free system memory, effectively exceeding the total RAM of a Pi Zero 2W.
Both cargo cross build and cargo zigbuild default to compiling with the --profile=release flag, applying some optimizations and considerably lowering the resulting binary file size as compared to when building with --profile=dev.
rsync -avz --progress target/aarch64-unknown-linux-gnu/release/pellxd user@pi:~/Replace release with debug to transfer the binary of a --profile=dev build.
This will build it in a serial mode (--jobs=1), compiling one dependency at a time. Swap is probably still required.
cargo build -j1Mind that build times will be very long. Remember to use a heatsink. (Cross-compilation remains recommended.)
Run the program with --save to generate a configuration file. By default, this will be stored as ~/.config/pellxd.toml. You can specify an alternative path with --config <file>.
cargo run -- --config ~/pellxd.toml --saveIf a filename is not specified, the program will infer a configuration directory in which to place one, based on your user and some environment variables.
$PELLXD_CONFIG_ROOTif set/etcif run as the root user$XDG_CONFIG_HOMEif set$HOME/.configif$HOMEis set- ...or fail to start if none of the above apply.
The default filename for the configuration file is pellxd.toml. If the file does not exist, the program will create it with default values.
[monitor]
source = "gpio"
loop_interval = "1s"
startup_window = "8m"
[gpio]
pin = 24
[dummy_input]
modulus = 30
threshold = 15Subsequent calls to --save will not overwrite existing configuration, but mind that comments will be removed.
Each notification backend can be configured with its own set of customizable strings, defined in the configuration file to tailor notification messages to your liking.
[backend.strings]
alert_header = 'PellX burner failure\n'
alert_body = "It went into an error state at {fuzzy_high}."
reminder_header = 'PellX burner still in failure\n'
reminder_body = "It has been in an error state since {fuzzy_high}."
startup_failed_header = "PellX burner startup failed"
startup_failed_body = "It tried to start up but failed at {fuzzy_high}."
startup_success_header = "PellX burner startup succeeded"
startup_success_body = "It successfully started up at {fuzzy_low}."
footer = ""alert_headerandalert_bodyare used in the message sent when a failure is first detected.reminder_headerandreminder_bodyare used in messages sent on subsequent iterations of the main loop while the burner is still in an error state.startup_failed_headerandstartup_failed_bodyare used in the message sent when a startup attempt is made but fails.startup_success_headerandstartup_success_bodyare used in the message sent when a startup attempt is made and succeeds.footeris appended to the end of all messages.
All of these strings are optional and can be left as an empty string "" to disable. A message whose header is empty will not be sent, neither will a message whose body ended up empty after composing.
Messages can container certain placeholders that will be replaced with dynamic content when composing the message, such as {fuzzy_high} and {fuzzy_low} in the examples above.
| Placeholder | Description |
|---|---|
{fuzzy_now} |
The current time in a human-friendly format that may be a mixture of date and time, depending how long ago the time was. In the case of the current time, this will always be a timestamp without date. |
{time_now} |
The current time in HH:MM format. |
{date_now} |
The current date in YYYY-MM-DD format. |
{fuzzy_then} |
The time of the context's now field. This is generally the same as the current time, but may be different if the context is from a retry of a previously failed send. |
{time_then} |
The time of the context's now field, in HH:MM format. |
{date_then} |
The date of the context's now field, in YYYY-MM-DD format. |
{name} |
The name of the program. |
{version} |
The version of the program. |
{fuzzy_low} |
The time of the most recent transition to a LOW reading, in a human-friendly format. |
{fuzzy_high} |
The time of the most recent transition to a HIGH reading, in a human-friendly format. |
{fuzzy_state_change} |
The time of the most recent transition to either a LOW or HIGH reading, in a human-friendly format. |
Notifications can be sent as messages to Slack channels, as short emails sent via the free Batsign service, and by invocations of external commands. Each of these backends can be enabled or disabled independently of the others, and each has its own set of customizable strings.
Messages to Slack channels can trivially be pushed through use of webhook URLs. HTTP requests made to these will end up as messages in the channels they refer to. See this guide in the Slack documentation for developers on how to get started.
It is recommended that you make an entry in /etc/hosts to manually resolve hooks.slack.com to an IP of the underlying Slack server, to avoid potential DNS lookup failures.
URLs must be quoted. You may enter any number of URLs as long as you separate the individual strings with a comma.
[slack]
enabled = true
urls = ["https://hooks.slack.com/services/REDACTED/SECRET/KEY", "https://hooks.slack.com/services/SUPPORTS/MORE/THANONE"]
show_response = falseshow_response will make the response body of the HTTP request be printed to the terminal.
Slack supports some formatting. Text between asterisks * will be in *bold*, text between underscores _ will be in _italics_, text between tildes ~ will be in ~strikethrough~, etc.
Strings defined in the configuration file can make use of this.
[slack.strings]
alert_header = ":x: *PellX burner failure*"
reminder_header = ":alarm-clock: *PellX burner still in failure*"
startup_failed_header = ":x: *PellX burner startup _failed_*"
startup_success_header = ":fire: *PellX burner startup _succeeded_*"See this help article for the full listing.
Batsign is a free (gratis) service with which you can send brief emails. Requires registration, after which you will receive a unique URL that should be kept secret. HTTP requests made to this URL will send an email to the address you specified when registering.
It is recommended that you make an entry in /etc/hosts to manually resolve batsign.me to the IP of the underlying Batsign server, to avoid potential DNS lookup failures.
URLs must be quoted. You may enter any number of URLs as long as you separate the individual strings with a comma.
[batsign]
enabled = true
urls = ["https://batsign.me/at/name@host.tld/secretkey", "https://batsign.me/at/other@host.ld/supportsmultiple"]
show_response = falseshow_response will make the response body of the HTTP request be printed to the terminal.
It is not possible to format text in Batsign emails with HTML markup. The best you can do is to get creative with Unicode characters, like emoji.
[batsign.strings]
alert_header = "β PellX burner failure"
reminder_header = "β° PellX burner still in failure"
startup_failed_header = "β PellX burner startup failed"
startup_success_header = "π₯ PellX burner startup succeeded"You can have the program execute external commands as a way to push notifications, although there are several caveats.
-
The commands run will be passed several arguments in a specific hardcoded order, and it is unlikely that this will immediately suit whatever notification program you want to use. Realistically what you will end up doing is writing some glue-layer scripts that map the arguments to something the notifying programs can use. (Remember to
chmodthe scripts executable+x.) -
If you run the project binary as root, the external commands executed will in turn also be run as root. If you need them to be run as a different user, you will have to wrap them or have them recurse into themselves with something like
systemd-runorsu.
Command paths must be quoted. You may enter any number of commands as long as you separate the individual strings with a comma.
[command]
enabled = true
commands = ["/absolute/path/to/script.sh", "/absolute/path/to/other/script.sh"]
show_response = falseshow_response will print the standard output and standard error of the command to the terminal.
The command-line arguments passed are as follows:
$1: The composed message body, formatted with strings as defined in the configuration file$2: A string of the type of message, which is one ofalert,reminder,startup_failedorstartup_success$3: The number of times the main loop has run, starting at 0$4: The UNIX timestamp of whenLOWwas last read from the pellets burner, which qualifies as a desired state$5: The UNIX timestamp of whenHIGHwas last read from the pellets burner, which qualifies as an error state$6: The UNIX timestamp of when the reading from the pellets burner last changed (regardless of the values it went from or to)$7: The UNIX timestamp of when the pellets burner last tried to start up, which is the firstLOWafter aHIGH
In cases where there is no UNIX timestamp to provide, the value passed will instead be 0.
The additional println backend is intended for logging and debugging purposes. It will print the composed message body to the terminal.
[println]
enabled = trueThe program lends itself to being run as a systemd service. This allows it to be automatically started on boot, and be restarted in the case of a crash.
To facilitate this, a basic service unit file is included in the repository. Copy it into /etc/systemd/system/, then use systemctl edit to modify it to point the ExecStart directive to the actual location of your compiled binary.
If yours is in the default path of
/usr/local/bin/pellxd, you can skip ahead to enable now.
sudo cp pellxd.service /etc/systemd/system/
sudo systemctl edit pellxd.service### Editing /etc/systemd/system/pellxd.service.d/override.conf
### Anything between here and the comment below will become the contents of the drop-in file
[Service]
ExecStart=
ExecStart=/home/user/src/pellxd/target/release/pellxd --verbose --config=/home/user/.config/pellxd.toml
### Edits below this comment will be discarded
### [...]Be sure to include the empty ExecStart= line to clear the default value, as Exec directives are additive.
Do not include the --debug flag in the systemd service ExecStart, as it will make the program produce a very large amount of output.
sudo systemctl enable --now pellxd.serviceThe enable command will make the service automatically start on boot, and the --now option will make it start immediately.
If the program is run via systemd, the output of the program can be found in the journal.
journalctl -u pellxd.serviceLogs will probably not persist across reboots. This is a common setting for Raspberry Pi OS to spare your SD card the extra writes. If logs spanning multiple boots are desirable, you will have to enable persistent logging.
GitHub Copilot AI was used (in Visual Studio Code) for inline suggestions and to tab-complete some code and documentation. Claude was used to answer questions and teach Rust. No code from "write me a function doing xyz" prompts is included in this project.
- flesh out documentation (correct documentation)
This project is licensed under the terms of the GNU General Public License version 2.0 or later (GPL-2.0-or-later). See the LICENSE file for details.