Add INW Mesh preset and enhance credential handling in MQTTBridge

- Introduced a new MQTT preset for INW Mesh with username/password authentication.
- Updated documentation to include the new preset and its configuration requirements.
- Enhanced credential handling in MQTTBridge to support dynamic username/password assignment for presets requiring per-slot credentials.
- Improved feedback in CLI for users to set credentials when needed.
This commit is contained in:
Adam Gessaman
2026-05-25 12:04:23 -07:00
parent 39eea82b3b
commit a1d47277b4
4 changed files with 51 additions and 9 deletions
+5 -3
View File
@@ -111,6 +111,7 @@ The MQTT bridge uses a slot-based architecture with up to 6 concurrent connectio
| `coloradomesh` | wss://mqtt.meshcore.coloradomesh.org:1883 | JWT (Ed25519) | WSS |
| `meshcore-ca-1` | mqtt1.meshcore.ca:443 | JWT (Ed25519) | WSS |
| `meshcore-ca-2` | mqtt2.meshcore.ca:443 | JWT (Ed25519) | WSS |
| `inwmesh` | scope.inwmesh.org:8883 | Username/password (per slot via `mqttN.username` / `mqttN.password`) | MQTT over TLS |
| `custom` | User-configured | Username/Password | MQTT or WSS |
| `none` | (disabled) | — | — |
@@ -288,18 +289,19 @@ Each slot (1-6) supports the following commands:
- `set mqttN.preset meshat.se` - Set slot N to Meshat.se
- `set mqttN.preset eastidahomesh` - Set slot N to EastIdahoMesh (WSS/TLS, no auth; packets on `meshcore/{IATA}/{PUBLIC_KEY}/packets`)
- `set mqttN.preset coloradomesh` - Set slot N to ColoradoMesh
- `set mqttN.preset inwmesh` - Set slot N to INW Mesh Scope (`mqtts://scope.inwmesh.org:8883`; set `mqttN.username` and `mqttN.password`)
- `set mqttN.preset custom` - Set slot N to custom broker (configure server/port/username/password)
- `set mqttN.preset none` - Disable slot N
- `set mqttN.server <hostname>` - Set custom server hostname for slot N
- `set mqttN.port <port>` - Set custom server port for slot N (1-65535)
- `set mqttN.username <username>` - Set custom username for slot N
- `set mqttN.password <password>` - Set custom password for slot N
- `set mqttN.username <username>` - Set username for slot N (`custom` preset, or presets like `inwmesh` that require per-device credentials)
- `set mqttN.password <password>` - Set password for slot N (`custom` preset, or presets like `inwmesh` that require per-device credentials)
- `set mqttN.token <token>` - Set per-slot token (required for MeshRank preset)
- `set mqttN.topic <template>` - Set custom topic template (custom preset only, see below)
- `set mqttN.audience <audience>` - Set JWT audience for custom slot (enables Ed25519 JWT auth)
- `set mqttN.audience` - Clear JWT audience (reverts to username/password auth)
**Note:** Custom server/port/username/password settings only apply when the slot's preset is `custom`.
**Note:** Custom server/port settings only apply when the slot's preset is `custom`. Username/password also apply to built-in presets that use per-slot credentials (e.g. `inwmesh`); other userpass presets (`tennmesh`, `nashmesh`, `meshat.se`) ship fixed credentials in firmware.
#### Example: Configure MeshRank on Slot 3
```bash
+8
View File
@@ -1452,6 +1452,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (p && p->topic_style == MQTT_TOPIC_MESHCORE &&
(strlen(_prefs->mqtt_iata) == 0 || strcmp(_prefs->mqtt_iata, "XXX") == 0)) {
sprintf(reply, "OK - slot %d preset: %s (run 'set mqtt.iata <airport_code>' to publish)", slot + 1, preset_name);
} else if (p && mqttPresetNeedsSlotCredentials(p) &&
(_prefs->mqtt_slot_username[slot][0] == '\0' ||
_prefs->mqtt_slot_password[slot][0] == '\0')) {
sprintf(reply,
"OK - slot %d preset: %s (run 'set mqtt%d.username <user>' and 'set mqtt%d.password <pass>' to connect)",
slot + 1, preset_name, slot + 1, slot + 1);
} else {
sprintf(reply, "OK - slot %d preset: %s", slot + 1, preset_name);
}
@@ -1475,10 +1481,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (memcmp(subcmd, "username ", 9) == 0) {
StrHelper::strncpy(_prefs->mqtt_slot_username[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_username[slot]));
savePrefs();
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (memcmp(subcmd, "password ", 9) == 0) {
StrHelper::strncpy(_prefs->mqtt_slot_password[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_password[slot]));
savePrefs();
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (memcmp(subcmd, "token ", 6) == 0) {
StrHelper::strncpy(_prefs->mqtt_slot_token[slot], &subcmd[6], sizeof(_prefs->mqtt_slot_token[slot]));
+10 -3
View File
@@ -36,10 +36,16 @@ struct MQTTPresetDef {
unsigned long token_lifetime; // JWT token lifetime in seconds (0 = use default 86400)
bool allow_retain; // Whether the broker allows the MQTT retain flag
uint16_t keepalive; // MQTT keepalive in seconds (0 = library default 120s)
const char* userpass_username; // MQTT_AUTH_USERPASS: broker username; else nullptr
const char* userpass_password; // MQTT_AUTH_USERPASS: broker password; else nullptr
const char* userpass_username; // MQTT_AUTH_USERPASS: embedded username, or nullptr to use mqttN.username
const char* userpass_password; // MQTT_AUTH_USERPASS: embedded password, or nullptr to use mqttN.password
};
// True when preset uses MQTT_AUTH_USERPASS but credentials come from slot prefs (mqttN.username/password).
static inline bool mqttPresetNeedsSlotCredentials(const MQTTPresetDef* preset) {
return preset && preset->auth_type == MQTT_AUTH_USERPASS &&
(!preset->userpass_username || !preset->userpass_password);
}
// Google Trust Services - GTS Root R4 (used by LetsMesh Analyzer)
static const char GTS_ROOT_R4[] PROGMEM =
"-----BEGIN CERTIFICATE-----\n"
@@ -99,7 +105,7 @@ static const char ISRG_ROOT_X1[] PROGMEM =
"-----END CERTIFICATE-----\n";
// Number of built-in presets
static const int MQTT_PRESET_COUNT = 17;
static const int MQTT_PRESET_COUNT = 18;
// Built-in preset definitions (stored in flash)
static const MQTTPresetDef MQTT_PRESETS[MQTT_PRESET_COUNT] = {
@@ -121,6 +127,7 @@ static const MQTTPresetDef MQTT_PRESETS[MQTT_PRESET_COUNT] = {
{ "dutchmeshcore-2", "wss://collector2.dutchmeshcore.nl:443/mqtt", "collector2.dutchmeshcore.nl", GTS_ROOT_R4, MQTT_AUTH_JWT, MQTT_TOPIC_MESHCORE, 0, true, 55, nullptr, nullptr },
{ "meshcore-ca-1", "wss://mqtt1.meshcore.ca:443/mqtt", "mqtt1.meshcore.ca", ISRG_ROOT_X1, MQTT_AUTH_JWT, MQTT_TOPIC_MESHCORE, 0, true, 55, nullptr, nullptr },
{ "meshcore-ca-2", "wss://mqtt2.meshcore.ca:443/mqtt", "mqtt2.meshcore.ca", ISRG_ROOT_X1, MQTT_AUTH_JWT, MQTT_TOPIC_MESHCORE, 0, true, 55, nullptr, nullptr },
{ "inwmesh", "mqtts://scope.inwmesh.org:8883", nullptr, ISRG_ROOT_X1, MQTT_AUTH_USERPASS, MQTT_TOPIC_MESHCORE, 0, true, 55, nullptr, nullptr },
};
// Find a preset by name, returns nullptr if not found
+28 -3
View File
@@ -544,6 +544,12 @@ void MQTTBridge::begin() {
if (preset) {
_slots[i].enabled = true;
_slots[i].preset = preset;
if (mqttPresetNeedsSlotCredentials(preset)) {
strncpy(_slots[i].username, _prefs->mqtt_slot_username[i], sizeof(_slots[i].username) - 1);
_slots[i].username[sizeof(_slots[i].username) - 1] = '\0';
strncpy(_slots[i].password, _prefs->mqtt_slot_password[i], sizeof(_slots[i].password) - 1);
_slots[i].password[sizeof(_slots[i].password) - 1] = '\0';
}
} else {
MQTT_DEBUG_PRINTLN("MQTT%d: unknown preset '%s', disabling", i + 1, preset_name);
_slots[i].enabled = false;
@@ -1143,9 +1149,12 @@ void MQTTBridge::setupSlot(int index) {
if (slot.auth_token[0] != '\0') {
slot.client->setCredentials(_jwt_username, slot.auth_token);
}
} else if (slot.preset->auth_type == MQTT_AUTH_USERPASS &&
slot.preset->userpass_username && slot.preset->userpass_password) {
slot.client->setCredentials(slot.preset->userpass_username, slot.preset->userpass_password);
} else if (slot.preset->auth_type == MQTT_AUTH_USERPASS) {
if (slot.preset->userpass_username && slot.preset->userpass_password) {
slot.client->setCredentials(slot.preset->userpass_username, slot.preset->userpass_password);
} else if (strlen(slot.username) > 0) {
slot.client->setCredentials(slot.username, slot.password);
}
}
} else {
// Custom broker slot — build persistent URI
@@ -1817,6 +1826,12 @@ void MQTTBridge::applySlotPreset(int slot_index, const char* preset_name) {
if (preset) {
slot.enabled = true;
slot.preset = preset;
if (mqttPresetNeedsSlotCredentials(preset)) {
strncpy(slot.username, _prefs->mqtt_slot_username[slot_index], sizeof(slot.username) - 1);
slot.username[sizeof(slot.username) - 1] = '\0';
strncpy(slot.password, _prefs->mqtt_slot_password[slot_index], sizeof(slot.password) - 1);
slot.password[sizeof(slot.password) - 1] = '\0';
}
if (_initialized) {
char reason[80];
if (!isSlotReady(slot_index, reason, sizeof(reason))) {
@@ -1967,6 +1982,16 @@ bool MQTTBridge::isSlotReady(int index, char* reason_buf, size_t reason_size) co
return false;
}
}
if (mqttPresetNeedsSlotCredentials(slot.preset)) {
if (_prefs->mqtt_slot_username[index][0] == '\0') {
if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.username <user>", index + 1);
return false;
}
if (_prefs->mqtt_slot_password[index][0] == '\0') {
if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.password <pass>", index + 1);
return false;
}
}
} else {
// Custom slot without a topic template uses meshcore format, needs IATA
if (_prefs->mqtt_slot_topic[index][0] == '\0' && !isIATAValid()) {