Skip to content
singular0Public

About

MeshCore companion daemon for uConsole with HackerGadgets AIO V2

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

62 Commits

Folders and files

Repository files navigation

coreletd

A MeshCore node for the ClockworkPi uConsole with the HackerGadgets / OpenSourceSDRLab AIO v2 expansion board — the firmware part, running as a Linux daemon.

It drives the board's SX1262 LoRa transceiver directly, speaks the MeshCore mesh protocol, and offers the companion protocol over a local Unix socket (or optional TCP), so the uConsole behaves like any other MeshCore radio: point a compatible app at the socket, or enable loopback TCP for Corelet, meshcore-cli or meshtui.

One binary, one systemd unit, no Arduino layer underneath.

Please read this before you put it on the air

coreletd is an independent, clean-room implementation of the MeshCore protocol, written from the public documentation. It is not affiliated with, endorsed by, certified by or reviewed by the MeshCore project, and compatibility with real MeshCore firmware is an intention backed by testing, not a guarantee anybody has signed off.

A mesh is shared infrastructure. A bug here does not stay local: badly formed packets, traffic that should never have been repeated, or a node that ignores the airtime budget affect everyone in radio range, including people relying on that network. Try it on a mesh you own before joining one you don't, and don't make it the node anything important depends on.

The usual radio rules apply too, and the daemon cannot check them for you: transmit only on a band and at a power your local regulations allow, keep within the duty cycle for that band, and fit an antenna before starting it — a 22 dBm PA into an open connector will damage the radio.

What it does

Unchecked boxes are not implemented — use different firmware for those.

Mesh

  • Adverts, sent and received, with signatures verified before anything is trusted.
  • A contact store that survives restarts, with routes learned from the traffic it sees.
  • Encrypted direct messages, with acknowledgements, retries and automatic fallback to flood when a known route stops working.
  • Channel (group) messages — the public channel, hashtag channels and private ones.
  • Relaying for other nodes, off by default: flood traffic repeated and direct traffic routed through this node forwarded, as a MeshCore companion with repeat enabled does. Relaying on a companion radio spends your duty-cycle budget and your battery on other people's packets.
  • Duty-cycle pacing, and a priority transmit queue so an ack is never stuck behind an advert.
  • Repeater and room-server roles. It is a companion node today; whether it grows into those is undecided.
  • Telemetry, path tracing, and the room-server login commands.

Clients

  • The companion protocol over a group-restricted Unix domain socket by default.
  • Optional TCP, bound to loopback by default.
  • One app at a time, as the protocol assumes. A new connection takes over from the old one, so a client that died without closing its socket cannot lock you out.
  • Settings from the app, as on a MeshCore radio: the node's name and position, the radio preset, TX power, the airtime budget and repeat, applied at once and kept with the node's state.
  • Bluetooth LE and USB-serial are not planned. They are how you reach a microcontroller; this runs on a Linux box that already has a network stack.

Operationally

  • Survives the radio disappearing: if the AIO v2's LoRa power rail is off, the daemon starts anyway, waits for the chip, and picks it up when it appears. The same check reconnects if the rail is cut mid-session.
  • Crash-durable, private state — contacts, channels and your identity are written atomically, and only the daemon's own user can read them.
  • A Debian package with a systemd unit and udev rules.

Tested against real hardware

Validated on a uConsole (CM4, Debian trixie) with an AIO v2, against a LilyGo T-Echo running stock MeshCore firmware on 869.618 MHz: adverts received and signature-verified, public-channel messages exchanged, encrypted direct messages decrypted correctly at both ends, and acks matched in both directions — which is the part that shows the two implementations really agree, since an ack is only sent by a node that decrypted the message.

What you need

  • A ClockworkPi uConsole with the AIO v2 expansion board, running Raspberry Pi OS or Debian trixie or newer (older images ship libgpiod 1.6, and the build stops with an explanation).
  • An antenna for the band you intend to use.
  • A MeshCore client to talk to it, on the uConsole or on another machine.

Install

Download the arm64 .deb from Releases onto the uConsole and install it:

sudo apt install ./coreletd_0.1.0_arm64.deb

Install the file path, with the leading ./, so apt resolves the runtime dependencies. Nothing is signed, so the SHA256SUMS published beside the package is the only way to check what you got.

Or build the package yourself, on the uConsole:

tools/build-deb.sh deps                             # build dependencies, once
tools/build-deb.sh                                  # -> dist/coreletd_<version>_arm64.deb
sudo apt install ./dist/coreletd_*_arm64.deb

A package built from a checkout is named after the commit it came from, so it is a release version only when you are on a release tag; anything else builds as 0.0.0+g<hash>, which coreletd --version then reports too. That is deliberate — a build should not claim to be a release nobody made.

The package installs the daemon, its systemd unit and the udev rules, and creates a coreletd system user and group. It deliberately does not start the service — the frequency is a legal question and an unfitted antenna is a hardware one, so it waits for you. /usr/share/doc/coreletd/ has the packaging notes, including why purging leaves your node identity alone.

From a machine that isn't the uConsole, tools/build-deb.sh --docker builds the same arm64 package in a container. It runs emulated, so it is slow, but it works from anywhere with Docker.

Or build and install by hand
sudo apt install build-essential cmake pkg-config libsodium-dev libssl-dev libgpiod-dev
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build -j4
ctest --test-dir build --output-on-failure

sudo install -m 755 build/coreletd /usr/sbin/coreletd
sudo install -D -m 644 etc/coreletd.ini /etc/coreletd/coreletd.ini
sudo install -m 644 etc/coreletd.service /etc/systemd/system/
sudo groupadd -f -r coreletd && sudo useradd -r -g coreletd -s /usr/sbin/nologin coreletd
sudo install -m 644 etc/99-coreletd.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
sudo systemctl daemon-reload

You get no man page and no upgrade path this way, and removing it is your problem.

Building without a uConsole

The test suite supplies its own test radios, so building and testing do not require radio hardware. The SX1262 backend is Linux-only and is skipped automatically on macOS; a daemon built there has no radio backend and will not start, but all hardware-independent development and tests remain available.

brew install cmake libsodium openssl@3 pkgconf     # macOS
cmake -S . -B build && cmake --build build -j
ctest --test-dir build --output-on-failure

OpenSSL is keg-only on Homebrew, so if CMake can't find libcrypto, point it at the right place: export PKG_CONFIG_PATH="$(brew --prefix openssl@3)/lib/pkgconfig".

Set up the board

1. Enable SPI. The AIO v2 puts the SX1262 on SPI1. Add to /boot/firmware/config.txt and reboot:

dtparam=spi=on
dtoverlay=spi1-1cs

Confirm /dev/spidev1.0 exists afterwards — the service refuses to start without it.

2. Turn on the LoRa power rail. The AIO v2 gates power to each subsystem behind a GPIO, and GPIO16 must be high or the SX1262 is unpowered. This is far and away the most common cause of "the radio isn't responding". The daemon does not touch that switch: it is board-level state, owned by whatever manages the uConsole's power.

aiov2_ctl lora on                     # the AIO v2 board utility
gpioset -c gpiochip0 16=1             # fallback; keep it running, the kernel reverts on exit

For reference, the other rails are GPS=27, SDR=7, internal USB=23.

You can do this before or after starting the daemon. A radio that isn't answering yet is a normal state, not a failure.

3. Fit an antenna. Before anything transmits.

4. If meshtasticd is installed, stop and disable it. It holds the same GPIO lines, and coreletd will fail with EBUSY.

Configure

sudoedit /etc/coreletd/coreletd.ini

The file is divided into [hardware], [node], [companion], [storage], and [logging] sections, and says how the daemon runs on this machine: the board's wiring, where apps connect, where the node's state lives, how much it logs. The shipped values fit a uConsole with an AIO v2, and the file documents every option.

state_dir in [storage] is the one setting with no default, but the shipped file already names /var/lib/coreletd, the directory systemd creates for the service. Leave it unless you run the daemon some other way; it must be an absolute path to a directory that exists.

Radio and node settings

What a MeshCore radio lets its app change is not in that file: the radio preset, the TX power, the airtime budget, the node's name and position, and repeat. As on a MeshCore radio, your companion app sets them — meshcore-cli's set radio 869.618,62.5,8,5, for one — and the daemon applies each change at once and keeps it in /var/lib/coreletd/settings.

There is no default frequency. Which band you may transmit on is a legal question, not a technical one, so nothing picks it for you. Until your app chooses a preset, the daemon runs with the radio silent, the way a new MeshCore radio waits for its app. Every node on a mesh must agree on the frequency, bandwidth and spreading factor; the coding rate is carried in each packet's header, so a different one only costs airtime.

Key Default Notes
[radio] lora_freq none MHz, 150 to 960: 869.618 for EU868, 910.525 for US915
[radio] lora_bw 62.5 kHz, one the SX1262 has, 7.81 to 500
[radio] lora_sf 8 5 to 12
[radio] lora_cr 5 5 to 8, for 4/5 to 4/8
[radio] lora_tx_power 22 dBm, −9 to 22
[radio] duty_cycle 10 Percent of each rolling hour, up to 100 for no limit. EU868 allows 1% in most sub-bands and 10% at 869.4–869.65 MHz. An app sets it as MeshCore's airtime factor, as 100 / (1 + factor).
[node] advert_name from the key What other nodes see: at most 31 bytes, or 23 with a position. Unset, it is the first 4 bytes of the node's public key in hex, such as 4FD099CC, as MeshCore names a new node.
[node] lat, lon 0 Decimal degrees; both 0 publishes no position
[node] repeat 0 Relay other nodes' traffic, as a MeshCore companion with repeat enabled does. It spends your airtime budget and battery on theirs. An app may turn it on only at 433.000, 869.495 or 918.000 MHz, as MeshCore allows.

Without an app, write the file yourself while the daemon is stopped, in the same syntax as the configuration file. It holds only what has been set, and the rest stay at their defaults:

sudo systemctl stop coreletd
sudoedit -u coreletd /var/lib/coreletd/settings
sudo systemctl start coreletd
[radio]
lora_freq = 869.618

[node]
advert_name = Field unit

A value out of range, or a name too long for the advert, stops the daemon at startup and the log says which, rather than have it guess at a frequency.

Run

sudo systemctl enable --now coreletd
journalctl -u coreletd -f

Or run it in the foreground while you are still setting things up — one at a time, since two daemons cannot share the radio:

sudo systemctl stop coreletd
sudo install -d -o coreletd -g coreletd -m 0700 /var/lib/coreletd
sudo install -d -o coreletd -g coreletd -m 0750 /run/coreletd
sudo -u coreletd /usr/sbin/coreletd --config /etc/coreletd/coreletd.ini --verbose

The default companion endpoint is /run/coreletd/companion.sock. It is owned by coreletd:coreletd with mode 0660; add the local user running the client to that group, then log out and back in:

sudo adduser "$USER" coreletd

For a client that only supports TCP, select the TCP interface in [companion] and restart the daemon:

[companion]
companion_interface = tcp
companion_bind = 127.0.0.1
companion_port = 5000

Then connect as before:

meshcore-cli -t 127.0.0.1:5000
corelet --host 127.0.0.1 --port 5000

Options

Option Meaning
-c, --config <path> Configuration file. Default /etc/coreletd/coreletd.ini.
-v, --verbose Log at debug level, overriding the config. Repeat for trace, which reports every SPI command and interrupt.
-s, --syslog Drop timestamps from log lines, for running under systemd.
-V, --version Print the version.
-h, --help Usage summary.

man coreletd has the same reference when installed from the package.

Your node identity

On first start the daemon generates an Ed25519 key and writes it to /var/lib/coreletd/identity.

That file is your node. Back it up. Delete it and you get a new public key, which means every contact you have sees you as a stranger, and your old identity lingers in their contact list as a node that never speaks again. Purging the package leaves the file in place for exactly this reason.

A key that goes missing is not replaced: if the node's other files are still there, the daemon refuses to start rather than generate a new identity on top of them. Restore the key, or move the rest of the directory aside if a new node is what you want.

Back up and restore

Everything the node is lives in /var/lib/coreletd: identity, contacts, channels, the messages waiting for an app, and its settings. Each file is replaced atomically, so a copy taken while the daemon runs is never half-written — though it can trail by a second for anything you just changed, and by a minute for when contacts were last heard:

sudo tar -C /var/lib -czf coreletd-backup.tar.gz coreletd

The archive holds your private key and channel secrets, so keep it where only you can read it.

To restore, stop the daemon first — a running one writes what it holds in memory over the restored files at its next save — and make sure the files end up owned by coreletd. Files left owned by root are unreadable to it, and it refuses to start rather than replace a key it cannot read:

sudo systemctl stop coreletd
sudo tar -C /var/lib -xzf coreletd-backup.tar.gz
sudo chown -R coreletd:coreletd /var/lib/coreletd
sudo systemctl start coreletd

Security

The companion protocol has no authentication of any kind. The default Unix socket limits access to the coreletd user and group. If TCP is selected, anyone who can reach the port can read your messages, send messages as you, and retune the radio — its frequency and power included; companion_bind therefore defaults to 127.0.0.1.

Selecting the TCP interface is the explicit opt-in to network service. Prefer keeping it on loopback and reaching it over an SSH tunnel or WireGuard instead of widening companion_bind:

ssh -L 5000:127.0.0.1:5000 uconsole.local

The node's state is private to the coreletd user: /var/lib/coreletd is 0700 and every file in it 0600 — the identity, the channel secrets, your contacts, and messages waiting for an app. Joining the coreletd group grants the socket and the radio, not these; read them with sudo.

Troubleshooting

Every SPI read is zeroes / the radio never appears. The LoRa rail is off — see step 2 above.

XOSC_START_ERR, device error 0x0020, at startup. Expected and harmless. The SX1262 latches it at power-up because it tries to start its oscillator before DIO3 is configured to power the TCXO; it is cleared after calibration. If it is still reported after that, lora_tcxo in [hardware] does not match your board.

The daemon starts but hears nothing. If the log says no frequency set, the radio is idle until your app sets the radio parameters. Otherwise, air settings must match the rest of the mesh exactly — frequency, bandwidth and spreading factor. A mismatch is silence, not an error. Coding rate is the exception: it rides in the packet header, so a node using a different one is still heard.

EBUSY opening the GPIO chip. Something else holds the lines; usually meshtasticd, sometimes a gpioset you left running, sometimes an earlier coreletd that did not exit.

Permission denied on /dev/spidev1.0. The udev rules aren't applied, or your user isn't in the coreletd group: sudo adduser "$USER" coreletd, then log out and back in.

--verbose --verbose traces every SPI command and interrupt, which is where to start if you are adapting this to a different board.

License

GPL-3.0-or-later. There is no warranty; see LICENSE for the exact terms.

Credits

Protocol documentation and constants come from the MeshCore project. coreletd is an independent implementation and is not affiliated with it.

About

MeshCore companion daemon for uConsole with HackerGadgets AIO V2

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages