Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ jobs:
-I firmware/lib/odh-radio \
-I firmware/lib/odh-telemetry \
-I firmware/lib/odh-web \
-I firmware/lib/odh-shell \
-I firmware/lib/odh-channel \
firmware/src \
firmware/lib

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@
*.bak
*.000
fp-info-cache

# Python bytecode
__pycache__
6 changes: 0 additions & 6 deletions firmware/data/transmitter/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,6 @@ <h2>Device</h2>
<label>Device Name</label>
<input type="text" id="c-devname" maxlength="28">

<h2>Radio</h2>
<label>WiFi Channel (1–13)</label>
<input type="number" id="c-radioch" min="1" max="13">

<h2>Battery</h2>
<label>TX Cells</label>
<select id="c-txcells">
Expand Down Expand Up @@ -266,7 +262,6 @@ <h2>Modules</h2>
configData = await fetchJson('/api/config');
const c = configData;
document.getElementById('c-devname').value = c.device_name || '';
document.getElementById('c-radioch').value = c.radio_channel || 1;
document.getElementById('c-txcells').value = c.tx_cells || 0;
document.getElementById('c-rxcells').value = c.rx_cells || 0;

Expand Down Expand Up @@ -345,7 +340,6 @@ <h2>Modules</h2>

const body = {
device_name: document.getElementById('c-devname').value,
radio_channel: parseInt(document.getElementById('c-radioch').value),
model_type: parseInt(document.getElementById('c-model').value),
tx_cells: parseInt(document.getElementById('c-txcells').value),
rx_cells: parseInt(document.getElementById('c-rxcells').value),
Expand Down
83 changes: 83 additions & 0 deletions firmware/lib/odh-channel/Channel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2026 Peter Buchegger
*
* This file is part of OpenDriveHub.
*
* OpenDriveHub is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenDriveHub is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenDriveHub. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/

#pragma once

#include <cstdint>

namespace odh {
namespace channel {

/// The three non-overlapping 2.4 GHz WiFi channels used for discovery.
inline constexpr uint8_t kCandidateChannels[] = {1, 6, 11};
inline constexpr uint8_t kCandidateChannelCount = 3;

/// Returns true only for channels 1, 6, or 11.
inline constexpr bool isValidChannel(uint8_t ch) {
return ch == 1 || ch == 6 || ch == 11;
}

/// Fallback channel when NVS is empty or contains an invalid value.
inline constexpr uint8_t kDefaultChannel = 1;

// -- Discovery timing constants --

/// Wait after switching WiFi channel before sending (ms).
inline constexpr uint32_t kChannelSettleMs = 10;

/// Wait for a discovery response after sending a request (ms).
inline constexpr uint32_t kDiscoveryWaitMs = 25;

/// Number of discovery requests per channel scan attempt.
inline constexpr uint8_t kDiscoveryRetries = 2;

/// Approximate wall-clock time for one full scan of all 3 channels (ms).
inline constexpr uint32_t kScanRoundMs = 180;

/// Minimum random backoff before a receiver claims a channel (ms).
inline constexpr uint32_t kFoundingBackoffMinMs = 200;

/// Maximum random backoff before a receiver claims a channel (ms).
inline constexpr uint32_t kFoundingBackoffMaxMs = 700;

/// Interval at which a receiver broadcasts its presence (ms).
inline constexpr uint32_t kPresenceIntervalMs = 500;

/// Receiver re-enters discovery if it hears no transmitter for this long (ms).
inline constexpr uint32_t kTransmitterLossTimeoutMs = 3000;

/// Transmitter prunes receivers not seen for this long (ms).
inline constexpr uint32_t kPresenceStaleTimeoutMs = 5000;

/// Maps a WiFi channel number to a UDP simulation port.
/// ch 1 → 7001, ch 6 → 7006, ch 11 → 7011, anything else → 7001.
inline constexpr uint16_t channelToSimPort(uint8_t ch) {
if (ch == 1)
return 7001;
if (ch == 6)
return 7006;
if (ch == 11)
return 7011;
return 7001;
}

} // namespace channel
} // namespace odh
105 changes: 105 additions & 0 deletions firmware/lib/odh-channel/ChannelScanner.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2026 Peter Buchegger
*
* This file is part of OpenDriveHub.
*
* OpenDriveHub is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenDriveHub is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenDriveHub. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/

#include "ChannelScanner.h"

#include <cstdlib>

namespace odh {

ChannelScanner::ChannelScanner(SetChannelFn setChannel, SendDiscoveryFn sendDiscovery, DelayFn delayMs)
: _setChannel(std::move(setChannel)),
_sendDiscovery(std::move(sendDiscovery)),
_delay(std::move(delayMs)) {}

ScanResult ChannelScanner::scanChannel(uint8_t channel) {
ScanResult result;
result.channel = channel;

// Switch to channel and wait for it to settle
_setChannel(channel);
_delay(channel::kChannelSettleMs);

// Reset response flag
_responseReceived = false;
_responseRssi = -127;
_responseDeviceCount = 0;

// Send discovery requests with retries
for (uint8_t attempt = 0; attempt < channel::kDiscoveryRetries; ++attempt) {
_sendDiscovery(channel);
_delay(channel::kDiscoveryWaitMs);

if (_responseReceived) {
result.foundTransmitter = true;
result.rssi = _responseRssi;
result.deviceCount = _responseDeviceCount;
return result;
}
}

return result;
}

void ChannelScanner::scanAllChannels(ScanResult results[channel::kCandidateChannelCount]) {
for (uint8_t i = 0; i < channel::kCandidateChannelCount; ++i) {
results[i] = scanChannel(channel::kCandidateChannels[i]);
}
}

uint8_t ChannelScanner::bestChannel(const ScanResult results[channel::kCandidateChannelCount]) {
// First: prefer channels with an active transmitter (best RSSI)
int8_t bestRssi = -128;
uint8_t bestCh = channel::kCandidateChannels[0];
bool foundAny = false;

for (uint8_t i = 0; i < channel::kCandidateChannelCount; ++i) {
if (results[i].foundTransmitter && results[i].rssi > bestRssi) {
bestRssi = results[i].rssi;
bestCh = results[i].channel;
foundAny = true;
}
}

if (foundAny) {
return bestCh;
}

// No transmitter found: return first candidate (channel selection for
// founding a new cluster – caller may do channel quality evaluation)
return channel::kCandidateChannels[0];
}

bool ChannelScanner::anyTransmitterFound(const ScanResult results[channel::kCandidateChannelCount]) {
for (uint8_t i = 0; i < channel::kCandidateChannelCount; ++i) {
if (results[i].foundTransmitter)
return true;
}
return false;
}

void ChannelScanner::onDiscoveryResponse(uint8_t /*channel*/, int8_t rssi, uint8_t deviceCount) {
_responseRssi = rssi;
_responseDeviceCount = deviceCount;
_responseReceived = true;
}

} // namespace odh
84 changes: 84 additions & 0 deletions firmware/lib/odh-channel/ChannelScanner.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2026 Peter Buchegger
*
* This file is part of OpenDriveHub.
*
* OpenDriveHub is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenDriveHub is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenDriveHub. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/

#pragma once

#include "Channel.h"

#include <Protocol.h>
#include <cstdint>
#include <functional>

namespace odh {

/// Result of scanning a single WiFi channel for transmitter activity.
struct ScanResult {
uint8_t channel = 0;
bool foundTransmitter = false;
int8_t rssi = -127;
uint8_t deviceCount = 0; ///< From DiscoveryResponse
};

/// Callback types for platform-abstracted channel operations.
using SetChannelFn = std::function<bool(uint8_t channel)>;
using SendDiscoveryFn = std::function<bool(uint8_t channel)>;
using DelayFn = std::function<void(uint32_t ms)>;

/**
* Shared channel scanner used by both transmitter and receiver.
*
* Platform-agnostic: uses callbacks for channel switching, packet
* sending, and delays so it works on both ESP32 and the simulator.
*/
class ChannelScanner {
public:
ChannelScanner(SetChannelFn setChannel, SendDiscoveryFn sendDiscovery, DelayFn delayMs);

/// Scan a single channel: switch, send discovery request(s), wait for response.
ScanResult scanChannel(uint8_t channel);

/// Scan all candidate channels (1, 6, 11). Returns results for each.
void scanAllChannels(ScanResult results[channel::kCandidateChannelCount]);

/// Pick the best channel from scan results.
/// Prefers channels with an active transmitter. Among those, picks best RSSI.
/// If none have a transmitter, returns the channel that appears quietest.
static uint8_t bestChannel(const ScanResult results[channel::kCandidateChannelCount]);

/// Returns true if any candidate channel has an active transmitter.
static bool anyTransmitterFound(const ScanResult results[channel::kCandidateChannelCount]);

/// Called by the radio layer when a DiscoveryResponse is received.
/// Thread-safe: sets internal flag for the current scan wait.
void onDiscoveryResponse(uint8_t channel, int8_t rssi, uint8_t deviceCount);

private:
SetChannelFn _setChannel;
SendDiscoveryFn _sendDiscovery;
DelayFn _delay;

// Volatile scan state (set by onDiscoveryResponse, read by scanChannel)
volatile bool _responseReceived = false;
volatile int8_t _responseRssi = -127;
volatile uint8_t _responseDeviceCount = 0;
};

} // namespace odh
8 changes: 8 additions & 0 deletions firmware/lib/odh-channel/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "odh-channel",
"version": "1.0.0",
"description": "OpenDriveHub shared channel model and discovery scanner",
"dependencies": [
{"name": "odh-protocol"}
]
}
6 changes: 0 additions & 6 deletions firmware/lib/odh-config/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ namespace odh::config {

// ── Shared settings ─────────────────────────────────────────────────────

/// ESP-NOW WiFi channel (1-13). Must match on TX and RX.
inline constexpr uint8_t kRadioWifiChannel = 1;

/// Failsafe timeout (ms) – if no control packet arrives within this window,
/// the receiver applies failsafe values.
inline constexpr uint32_t kRadioFailsafeTimeoutMs = 500;
Expand Down Expand Up @@ -85,9 +82,6 @@ inline constexpr uint8_t kChannelCount = 8;
/// Default vehicle name for broadcast announcements.
inline constexpr const char *kVehicleName = "Vehicle";

/// Announce broadcast interval (ms).
inline constexpr uint32_t kAnnounceIntervalMs = 500;

/// Telemetry send interval (ms).
inline constexpr uint32_t kTelemetrySendIntervalMs = 100;

Expand Down
10 changes: 5 additions & 5 deletions firmware/lib/odh-protocol/FunctionMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,13 @@ inline const char *odh_model_name(uint8_t model) {
#endif
}

inline uint8_t odh_default_function_map(uint8_t model, OdhFunctionMapEntry_t *entries) {
inline uint8_t odh_default_function_map(uint8_t model, FunctionMapEntry *entries) {
auto map = odh::defaultFunctionMap(model);
std::memcpy(entries, map.entries.data(), sizeof(OdhFunctionMapEntry_t) * map.count);
std::memcpy(entries, map.entries.data(), sizeof(FunctionMapEntry) * map.count);
return map.count;
}

inline uint8_t odh_function_to_channel(const OdhFunctionMapEntry_t *entries, uint8_t count, uint8_t function) {
inline uint8_t odh_function_to_channel(const FunctionMapEntry *entries, uint8_t count, uint8_t function) {
for (uint8_t i = 0; i < count; i++) {
if (entries[i].function == function) {
return entries[i].channel;
Expand All @@ -274,13 +274,13 @@ inline uint8_t odh_function_to_channel(const OdhFunctionMapEntry_t *entries, uin
return 0xFF;
}

inline uint8_t odh_channel_to_function(const OdhFunctionMapEntry_t *entries, uint8_t count, uint8_t channel) {
inline uint8_t odh_channel_to_function(const FunctionMapEntry *entries, uint8_t count, uint8_t channel) {
for (uint8_t i = 0; i < count; i++) {
if (entries[i].channel == channel) {
return entries[i].function;
}
}
return ODH_FUNC_NONE;
return static_cast<uint8_t>(Function::None);
}

} // namespace odh
Expand Down
Loading
Loading