Fixing "dual" modes for Raspberry Pi build, adding logging script

This commit is contained in:
John Poole 2026-05-22 15:55:37 -07:00
commit 49c2ccd49a
9 changed files with 314 additions and 20 deletions

View file

@ -309,6 +309,19 @@ Run one on each Pi. There should be no required start order for the dual-role te
./microreticulum_306_rpi_arm64_dual_children
```
Dual builds default to `--ble-dual-policy=first-path-wins`. Both BlueZ roles start, but once either central or peripheral establishes the first peer path, the opposite role is stopped. This avoids exposing two simultaneous Reticulum interfaces to the same peer.
Temporary policy overrides are available for debugging:
```bash
./microreticulum_306_rpi_arm64_dual_little_boy_blue --ble-dual-policy=both
./microreticulum_306_rpi_arm64_dual_little_boy_blue --ble-dual-policy=first-path-wins
./microreticulum_306_rpi_arm64_dual_little_boy_blue --ble-dual-policy=central-only
./microreticulum_306_rpi_arm64_dual_little_boy_blue --ble-dual-policy=peripheral-only
```
`both` preserves the earlier Linux dual behavior and may produce Reticulum `Link-associated packet received on unexpected interface` errors if two BLE paths form at the same time. The `central-only` and `peripheral-only` policies are useful when isolating BlueZ adapter behavior without rebuilding split-role binaries.
You can copy those binaries to another Pi Zero 2W and rename them for clarity if both Pis use the same CPU architecture, OS bitness, and compatible runtime libraries. Check with:
```bash

View file

@ -60,6 +60,8 @@
<p class="note">
Use two different payload binaries for a simple exchange test. Start one on
each Pi; no fixed client/server start order is intended for the dual builds.
The default BLE policy is first-path-wins, which stops the opposite BlueZ
role after the first peer path is established.
</p>
<h2>Downloads</h2>

View file

@ -106,6 +106,8 @@ microreticulum_306_rpi_arm64_dual_little_boy_blue
microreticulum_306_rpi_arm64_dual_children
```
Dual binaries default to `--ble-dual-policy=first-path-wins`: both BlueZ roles start, but the first working peer path stops the opposite role. To reproduce the older experimental behavior, run with `--ble-dual-policy=both`.
Example scp command to a new server named "trixie1":
```bash
scp microreticulum_306_rpi_arm64_dual_children trixie1:~
@ -162,6 +164,7 @@ Expected healthy startup:
jlpoole@trixie1:~ $ ./microreticulum_306_rpi_arm64_dual_children
Exercise 306 native BLE file transfer console
Node=Node-PIZERO2-DUAL
BLE dual policy=first-path-wins
Selected file=children.txt bytes=1422 chunk=32 interval_ms=500 repeat_rest_ms=10000
[ustore] Initializing PosixFileSystem
[ustore] WARNING: FlashFSFileSystem check failed, reformatting!
@ -175,6 +178,20 @@ BLE linux-dual waiting for peer
BLE linux-dual waiting for peer
```
When one role wins, expect a line like:
```text
BLE dual policy first-path-wins: peripheral path won peer=:1.42; stopping central role
```
or:
```text
BLE dual policy first-path-wins: central path won peer=/org/bluez/hci0/dev_D8_3A_DD_1D_CF_B5; stopping peripheral role
```
The native BLE interfaces also emit temporary `BLE-FRAME` lines for Reticulum frame tracing. Those lines include timestamp, node label, BLE role, peer path/address, BlueZ object path, interface pointer, packet length, and the first packet byte.
When you have a second unit running, your console will show something like:
```bash
[Startup stuff]

View file

@ -48,6 +48,34 @@ static void log_error(const char* what, GError* error) {
}
}
static std::string peer_address_from_path(const std::string& path) {
size_t pos = path.find("/dev_");
if (pos == std::string::npos) {
return path.empty() ? std::string("unknown") : path;
}
std::string addr = path.substr(pos + 5);
std::replace(addr.begin(), addr.end(), '_', ':');
return addr;
}
static void log_reticulum_frame(const char* direction,
const std::string& node_label,
const std::string& peer_path,
const std::string& object_path,
const void* iface,
const RNS::Bytes& packet) {
unsigned first = packet.size() > 0 ? packet[0] : 0;
std::printf("BLE-FRAME t=%llu node=%s role=central dir=%s peer=%s object=%s if=%p len=%zu packet0=0x%02X link_hint=unknown\n",
(unsigned long long)now_ms(),
node_label.c_str(),
direction,
peer_address_from_path(peer_path).c_str(),
object_path.empty() ? "unknown" : object_path.c_str(),
iface,
packet.size(),
first);
}
HostBluezBleInterface::HostBluezBleInterface(const std::string& node_label, const char* name)
: InterfaceImpl(name), node_label_(node_label) {
_IN = true;
@ -281,7 +309,6 @@ bool HostBluezBleInterface::connect_device(const std::string& device_path) {
if (!result) {
log_error("BLE linux-central: Device1.Connect failed", error);
next_scan_ms_ = now_ms() + SCAN_RETRY_MS;
start_discovery();
return false;
}
g_variant_unref(result);
@ -457,6 +484,8 @@ void HostBluezBleInterface::send_outgoing(const RNS::Bytes& data) {
return;
}
log_reticulum_frame("tx", node_label_, device_path_, rx_char_path_, this, data);
size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
if (total == 0 || total > 65535) {
std::fprintf(stderr, "BLE linux-central: cannot fragment packet len=%zu fragments=%zu\n", data.size(), total);
@ -617,6 +646,7 @@ void HostBluezBleInterface::handle_fragment(const uint8_t* data, size_t len) {
}
void HostBluezBleInterface::enqueue_packet(const RNS::Bytes& packet) {
log_reticulum_frame("rx", node_label_, device_path_, tx_char_path_, this, packet);
incoming_packets_.push_back(packet);
}

View file

@ -21,6 +21,7 @@ public:
bool connected() const { return connected_; }
const char* role_name() const { return "linux-central"; }
const std::string& peer_path() const { return device_path_; }
private:
void send_outgoing(const RNS::Bytes& data) override;

View file

@ -62,6 +62,34 @@ static GVariant* flags_variant(std::initializer_list<const char*> flags) {
return g_variant_builder_end(&builder);
}
static std::string peer_address_from_path(const std::string& path) {
size_t pos = path.find("/dev_");
if (pos == std::string::npos) {
return path.empty() ? std::string("unknown") : path;
}
std::string addr = path.substr(pos + 5);
std::replace(addr.begin(), addr.end(), '_', ':');
return addr;
}
static void log_reticulum_frame(const char* direction,
const std::string& node_label,
const std::string& peer_path,
const char* object_path,
const void* iface,
const RNS::Bytes& packet) {
unsigned first = packet.size() > 0 ? packet[0] : 0;
std::printf("BLE-FRAME t=%llu node=%s role=peripheral dir=%s peer=%s object=%s if=%p len=%zu packet0=0x%02X link_hint=unknown\n",
(unsigned long long)now_ms(),
node_label.c_str(),
direction,
peer_address_from_path(peer_path).c_str(),
object_path ? object_path : "unknown",
iface,
packet.size(),
first);
}
static const char* OBJECT_MANAGER_XML =
"<node>"
" <interface name='org.freedesktop.DBus.ObjectManager'>"
@ -167,6 +195,7 @@ void HostBluezPeripheralInterface::stop() {
online_ = false;
connected_ = false;
notifying_ = false;
peer_path_.clear();
_online = false;
}
@ -381,6 +410,8 @@ void HostBluezPeripheralInterface::send_outgoing(const RNS::Bytes& data) {
return;
}
log_reticulum_frame("tx", node_label_, peer_path_, TX_PATH, this, data);
size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE;
if (total == 0 || total > 65535) {
std::fprintf(stderr, "BLE linux-peripheral: cannot fragment packet len=%zu fragments=%zu\n", data.size(), total);
@ -496,6 +527,7 @@ void HostBluezPeripheralInterface::handle_fragment(const uint8_t* data, size_t l
}
void HostBluezPeripheralInterface::enqueue_packet(const RNS::Bytes& packet) {
log_reticulum_frame("rx", node_label_, peer_path_, RX_PATH, this, packet);
incoming_packets_.push_back(packet);
}
@ -615,6 +647,7 @@ GVariant* HostBluezPeripheralInterface::get_property(const char* object_path,
void HostBluezPeripheralInterface::handle_method_call(const char* object_path,
const char* interface_name,
const char* method_name,
const char* sender,
GVariant* parameters,
GDBusMethodInvocation* invocation) {
if (std::strcmp(interface_name, "org.freedesktop.DBus.ObjectManager") == 0 &&
@ -661,6 +694,12 @@ void HostBluezPeripheralInterface::handle_method_call(const char* object_path,
GVariant* value = nullptr;
GVariant* options = nullptr;
g_variant_get(parameters, "(@ay@a{sv})", &value, &options);
const gchar* device = nullptr;
if (options && g_variant_lookup(options, "device", "&o", &device) && device) {
peer_path_ = device;
} else if (sender && peer_path_.empty()) {
peer_path_ = sender;
}
gsize len = 0;
const uint8_t* data = static_cast<const uint8_t*>(g_variant_get_fixed_array(value, &len, sizeof(uint8_t)));
if (data && len > 0) {
@ -683,6 +722,9 @@ void HostBluezPeripheralInterface::handle_method_call(const char* object_path,
}
notifying_ = true;
connected_ = true;
if (sender && peer_path_.empty()) {
peer_path_ = sender;
}
std::printf("BLE linux-peripheral: central subscribed to TX notifications\n");
g_dbus_method_invocation_return_value(invocation, nullptr);
return;
@ -691,6 +733,7 @@ void HostBluezPeripheralInterface::handle_method_call(const char* object_path,
if (std::strcmp(method_name, "StopNotify") == 0) {
notifying_ = false;
connected_ = false;
peer_path_.clear();
reset_reassembly();
std::printf("BLE linux-peripheral: central unsubscribed from TX notifications\n");
g_dbus_method_invocation_return_value(invocation, nullptr);
@ -715,7 +758,7 @@ void HostBluezPeripheralInterface::method_call(GDBusConnection* connection,
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "No peripheral instance");
return;
}
self->handle_method_call(object_path, interface_name, method_name, parameters, invocation);
self->handle_method_call(object_path, interface_name, method_name, sender, parameters, invocation);
}
GVariant* HostBluezPeripheralInterface::property_get(GDBusConnection* connection,

View file

@ -21,6 +21,7 @@ public:
bool connected() const { return connected_; }
const char* role_name() const { return "linux-peripheral"; }
const std::string& peer_path() const { return peer_path_; }
private:
void send_outgoing(const RNS::Bytes& data) override;
@ -44,6 +45,7 @@ private:
void handle_method_call(const char* object_path,
const char* interface_name,
const char* method_name,
const char* sender,
GVariant* parameters,
GDBusMethodInvocation* invocation);
@ -102,6 +104,7 @@ private:
bool advertisement_registered_ = false;
bool has_gatt_manager_ = false;
bool has_advertising_manager_ = false;
std::string peer_path_;
uint32_t tx_message_id_ = 0;
RNS::Bytes tx_value_;
RNS::Bytes identity_value_;

View file

@ -59,9 +59,18 @@ static constexpr uint32_t FNV1A_PRIME = 16777619UL;
static RNS::Reticulum reticulum({RNS::Type::NONE});
#if defined(HOST_BLE_DUAL)
enum class BleDualPolicy {
Both,
FirstPathWins,
CentralOnly,
PeripheralOnly,
};
static std::vector<RNS::Interface> ble_interfaces;
static HostBluezBleInterface* ble_central_impl = nullptr;
static HostBluezPeripheralInterface* ble_peripheral_impl = nullptr;
static BleDualPolicy ble_dual_policy = BleDualPolicy::FirstPathWins;
static const char* ble_dual_winner = nullptr;
#else
static RNS::Interface ble_interface({RNS::Type::NONE});
static HostBleInterface* ble_impl = nullptr;
@ -79,6 +88,42 @@ static bool link_attempted = false;
static std::string node_label = HOST_NODE_LABEL;
static bool running = true;
#if defined(HOST_BLE_DUAL)
static const char* ble_dual_policy_name(BleDualPolicy policy) {
switch (policy) {
case BleDualPolicy::Both:
return "both";
case BleDualPolicy::FirstPathWins:
return "first-path-wins";
case BleDualPolicy::CentralOnly:
return "central-only";
case BleDualPolicy::PeripheralOnly:
return "peripheral-only";
}
return "unknown";
}
static bool set_ble_dual_policy(const char* value) {
if (std::strcmp(value, "both") == 0) {
ble_dual_policy = BleDualPolicy::Both;
return true;
}
if (std::strcmp(value, "first-path-wins") == 0) {
ble_dual_policy = BleDualPolicy::FirstPathWins;
return true;
}
if (std::strcmp(value, "central-only") == 0) {
ble_dual_policy = BleDualPolicy::CentralOnly;
return true;
}
if (std::strcmp(value, "peripheral-only") == 0) {
ble_dual_policy = BleDualPolicy::PeripheralOnly;
return true;
}
return false;
}
#endif
struct RxTransferState {
bool active = false;
std::string sender;
@ -433,21 +478,25 @@ static void setup_reticulum() {
RNS::Utilities::OS::register_filesystem(filesystem);
#if defined(HOST_BLE_DUAL)
auto central = std::shared_ptr<RNS::InterfaceImpl>(new HostBluezBleInterface(node_label));
ble_central_impl = static_cast<HostBluezBleInterface*>(central.get());
RNS::Interface central_interface(central);
central_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
RNS::Transport::register_interface(central_interface);
ble_interfaces.push_back(central_interface);
central_interface.start();
if (ble_dual_policy != BleDualPolicy::PeripheralOnly) {
auto central = std::shared_ptr<RNS::InterfaceImpl>(new HostBluezBleInterface(node_label));
ble_central_impl = static_cast<HostBluezBleInterface*>(central.get());
RNS::Interface central_interface(central);
central_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
RNS::Transport::register_interface(central_interface);
ble_interfaces.push_back(central_interface);
central_interface.start();
}
auto peripheral = std::shared_ptr<RNS::InterfaceImpl>(new HostBluezPeripheralInterface(node_label));
ble_peripheral_impl = static_cast<HostBluezPeripheralInterface*>(peripheral.get());
RNS::Interface peripheral_interface(peripheral);
peripheral_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
RNS::Transport::register_interface(peripheral_interface);
ble_interfaces.push_back(peripheral_interface);
peripheral_interface.start();
if (ble_dual_policy != BleDualPolicy::CentralOnly) {
auto peripheral = std::shared_ptr<RNS::InterfaceImpl>(new HostBluezPeripheralInterface(node_label));
ble_peripheral_impl = static_cast<HostBluezPeripheralInterface*>(peripheral.get());
RNS::Interface peripheral_interface(peripheral);
peripheral_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
RNS::Transport::register_interface(peripheral_interface);
ble_interfaces.push_back(peripheral_interface);
peripheral_interface.start();
}
#else
auto impl = std::shared_ptr<RNS::InterfaceImpl>(new HostBleInterface(node_label));
ble_impl = static_cast<HostBleInterface*>(impl.get());
@ -478,12 +527,12 @@ static void setup_reticulum() {
static void loop_ble_interfaces() {
#if defined(HOST_BLE_DUAL)
if (ble_central_impl) {
ble_central_impl->loop();
}
if (ble_peripheral_impl) {
ble_peripheral_impl->loop();
}
if (ble_central_impl) {
ble_central_impl->loop();
}
#else
if (ble_impl) {
ble_impl->loop();
@ -491,6 +540,33 @@ static void loop_ble_interfaces() {
#endif
}
#if defined(HOST_BLE_DUAL)
static void enforce_ble_dual_policy() {
if (ble_dual_policy != BleDualPolicy::FirstPathWins || ble_dual_winner) {
return;
}
if (ble_peripheral_impl && ble_peripheral_impl->connected()) {
ble_dual_winner = "peripheral";
std::printf("BLE dual policy first-path-wins: peripheral path won peer=%s; stopping central role\n",
ble_peripheral_impl->peer_path().empty() ? "unknown" : ble_peripheral_impl->peer_path().c_str());
if (ble_central_impl) {
ble_central_impl->stop();
}
return;
}
if (ble_central_impl && ble_central_impl->connected()) {
ble_dual_winner = "central";
std::printf("BLE dual policy first-path-wins: central path won peer=%s; stopping peripheral role\n",
ble_central_impl->peer_path().empty() ? "unknown" : ble_central_impl->peer_path().c_str());
if (ble_peripheral_impl) {
ble_peripheral_impl->stop();
}
}
}
#endif
static bool ble_connected() {
#if defined(HOST_BLE_DUAL)
return (ble_central_impl && ble_central_impl->connected()) ||
@ -502,6 +578,9 @@ static bool ble_connected() {
static const char* ble_role_status() {
#if defined(HOST_BLE_DUAL)
if (ble_dual_winner) {
return ble_dual_winner;
}
if (ble_central_impl && ble_central_impl->connected()) {
return ble_central_impl->role_name();
}
@ -534,13 +613,37 @@ static void handle_signal(int signal) {
running = false;
}
int main() {
int main(int argc, char** argv) {
std::signal(SIGINT, handle_signal);
std::signal(SIGTERM, handle_signal);
#if defined(HOST_BLE_DUAL)
static constexpr const char* policy_prefix = "--ble-dual-policy=";
for (int i = 1; i < argc; ++i) {
if (std::strncmp(argv[i], policy_prefix, std::strlen(policy_prefix)) == 0) {
const char* value = argv[i] + std::strlen(policy_prefix);
if (!set_ble_dual_policy(value)) {
std::fprintf(stderr,
"Unknown --ble-dual-policy=%s; valid values: both, first-path-wins, central-only, peripheral-only\n",
value);
return 2;
}
} else {
std::fprintf(stderr, "Unknown option: %s\n", argv[i]);
return 2;
}
}
#else
(void)argc;
(void)argv;
#endif
RNS::loglevel(RNS::LOG_NOTICE);
std::printf("Exercise 306 native BLE file transfer console\n");
std::printf("Node=%s\n", node_label.c_str());
#if defined(HOST_BLE_DUAL)
std::printf("BLE dual policy=%s\n", ble_dual_policy_name(ble_dual_policy));
#endif
std::printf("Selected file=%s bytes=%u chunk=%u interval_ms=%lu repeat_rest_ms=%lu\n",
SELECTED_TEXT_NAME, (unsigned)SELECTED_TEXT_SIZE,
(unsigned)TRANSFER_CHUNK_SIZE,
@ -557,6 +660,9 @@ int main() {
while (running) {
reticulum.loop();
loop_ble_interfaces();
#if defined(HOST_BLE_DUAL)
enforce_ble_dual_policy();
#endif
uint64_t now = millis64();
if (!ble_connected()) {

79
tools/run_sync.sh Executable file
View file

@ -0,0 +1,79 @@
#!/bin/bash
# 20260522 ChatGPT
# $Header$
#
# Example:
# ./run_sync.sh little_boy_blue 14:40
# ./run_sync.sh childrens_hour 14:40
# ./run_sync.sh ./little_boy_blue 14:40
#
# Optional with timeout:
# ./run_sync.sh little_boy_blue 14:40 300
#
set -euo pipefail
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
echo "Usage: $0 binary_name HH:MM [run_seconds]"
echo "Example: $0 little_boy_blue 14:40"
echo "Example: $0 little_boy_blue 14:40 300"
exit 1
fi
BINARY="$1"
START_TIME="$2"
RUN_SECONDS="${3:-}"
# If the binary name does not contain a slash, assume it is in the current directory.
case "$BINARY" in
*/*) CMD="$BINARY" ;;
*) CMD="./$BINARY" ;;
esac
if [ ! -x "$CMD" ]; then
echo "ERROR: command is not executable: $CMD"
echo
echo "Check with:"
echo " ls -l $CMD"
echo " chmod 755 $CMD"
exit 1
fi
TARGET_EPOCH=$(date -d "today ${START_TIME}" +%s)
NOW_EPOCH=$(date +%s)
# If requested time already passed today, assume tomorrow.
if [ "$TARGET_EPOCH" -le "$NOW_EPOCH" ]; then
TARGET_EPOCH=$(date -d "tomorrow ${START_TIME}" +%s)
fi
SLEEP_SECONDS=$(( TARGET_EPOCH - NOW_EPOCH ))
mkdir -p "$HOME/logs"
echo "Host: $(hostname)"
echo "Binary: $CMD"
echo "Now: $(date '+%Y%m%d_%H%M%S %Z')"
echo "Target time: $(date -d "@${TARGET_EPOCH}" '+%Y%m%d_%H%M%S %Z')"
echo "Sleeping for: ${SLEEP_SECONDS} seconds"
sleep "$SLEEP_SECONDS"
STAMP=$(date +%Y%m%d_%H%M%S)
SAFE_BINARY=$(basename "$BINARY")
LOG="$HOME/logs/$(hostname)_${SAFE_BINARY}_${STAMP}.log"
{
echo "SYNC_START_HOST=$(hostname)"
echo "SYNC_START_BINARY=$CMD"
echo "SYNC_START_EPOCH=$(date +%s.%N)"
echo "SYNC_START_LOCAL=$(date '+%Y%m%d_%H%M%S.%N %Z')"
echo "SYNC_LOG_FILE=$LOG"
echo "========================================"
} | tee "$LOG"
if [ -n "$RUN_SECONDS" ]; then
exec timeout "$RUN_SECONDS"s "$CMD" 2>&1 | tee -a "$LOG"
else
exec "$CMD" 2>&1 | tee -a "$LOG"
fi