Unify device controls and restore the WiFi console
This commit is contained in:
1 parent
3746f9d5f4
commit
4c8aad5b16
44 files changed
+3573
-2948
No files matched your search
@@ -114,6 +114,38 @@ TEST(CLICommandUtils, NormalizesCapitalizedSetVerb) {
|
||||
EXPECT_STREQ("set altpath 600000,0d2784,F8DADA", command);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, LoggingToggleHasOneGrammarForEveryRole) {
|
||||
bool enabled = false, reboot = false;
|
||||
ASSERT_TRUE(mesh::cli::parseLoggingToggle("on", enabled, reboot));
|
||||
EXPECT_TRUE(enabled);
|
||||
EXPECT_FALSE(reboot);
|
||||
ASSERT_TRUE(mesh::cli::parseLoggingToggle("off reboot", enabled, reboot));
|
||||
EXPECT_FALSE(enabled);
|
||||
EXPECT_TRUE(reboot);
|
||||
ASSERT_TRUE(mesh::cli::parseLoggingToggle("on reboot", enabled, reboot));
|
||||
EXPECT_TRUE(enabled);
|
||||
EXPECT_TRUE(reboot);
|
||||
ASSERT_TRUE(mesh::cli::parseLoggingToggle("off", enabled, reboot));
|
||||
EXPECT_FALSE(enabled);
|
||||
EXPECT_FALSE(reboot);
|
||||
for (const char* value : {"", "onward", "1", "off reboot now", "on\nreboot"}) {
|
||||
EXPECT_FALSE(mesh::cli::parseLoggingToggle(value, enabled, reboot)) << value;
|
||||
}
|
||||
EXPECT_FALSE(mesh::cli::parseLoggingToggle(nullptr, enabled, reboot));
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, LoggingOutputsControlUsbAndWifiIndependently) {
|
||||
bool usb = true, wifi = true;
|
||||
for (const char* mode : {"off", "usb", "wifi", "both"}) {
|
||||
ASSERT_TRUE(mesh::cli::parseLoggingOutput(mode, usb, wifi));
|
||||
EXPECT_STREQ(mode, mesh::cli::loggingOutputName(usb, wifi));
|
||||
EXPECT_EQ(usb, strcmp(mode, "usb") == 0 || strcmp(mode, "both") == 0);
|
||||
EXPECT_EQ(wifi, strcmp(mode, "wifi") == 0 || strcmp(mode, "both") == 0);
|
||||
}
|
||||
EXPECT_FALSE(mesh::cli::parseLoggingOutput("bluetooth", usb, wifi));
|
||||
EXPECT_FALSE(mesh::cli::parseLoggingOutput(nullptr, usb, wifi));
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, NormalizesAnyCommandVerbCase) {
|
||||
char get_command[] = "GET outpath";
|
||||
char clear_command[] = "cLeAr recent.repeater";
|
||||
|
||||
@@ -19,7 +19,7 @@ struct CompanionMqttSetupPortal {
|
||||
struct MyMesh {
|
||||
void onConfigBatchEnd();
|
||||
void syncWiFiPowerSaving();
|
||||
bool _wc_mqtt_dirty, _mqtt_started, _mqtt_configured;
|
||||
bool _wc_mqtt_dirty, _wc_batch_active, _mqtt_started, _mqtt_configured;
|
||||
MQTTBridge* _mqtt_bridge;
|
||||
MQTTPrefs _mqtt_prefs;
|
||||
};
|
||||
@@ -29,7 +29,7 @@ struct MyMesh {
|
||||
def callback_source():
|
||||
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
start = source.index(CALLBACK)
|
||||
return source[start:source.index("void MyMesh::execCommand", start)]
|
||||
return source[start:source.index("void MyMesh::execAdminCommand", start)]
|
||||
|
||||
|
||||
class CompanionMqttStackTests(unittest.TestCase):
|
||||
|
||||
@@ -865,8 +865,7 @@ assert.strictEqual(
|
||||
|
||||
console.log("generalized firmware picker tests passed");
|
||||
|
||||
// User-selected output paths must produce role-correct commands. In
|
||||
// particular, a Full Companion with MQTT must never get CommonCLI setters.
|
||||
// Shared features must produce the same commands for every role.
|
||||
const controls = require('../docs/_data/firmware_controls.json');
|
||||
const liveFamily = controls.familyTag;
|
||||
const controlledReleases = [release(liveFamily, '2026-09-01T00:00:00Z', [
|
||||
@@ -887,44 +886,50 @@ const observer = findControlled('heltec_v4_repeater_observer_mqtt-full-usb-wifi'
|
||||
for (const mode of ['none', 'usb', 'wifi', 'both']) {
|
||||
const directions = picker.runtimeDirections(observer, {logging: mode});
|
||||
assert.deepStrictEqual(directions[0].actions[0].commands,
|
||||
(mode === 'usb' || mode === 'both' ? ['powersaving off'] : []).concat(
|
||||
(mode === 'usb' || mode === 'both' ? ['set powersaving off'] : []).concat(
|
||||
['set logging.output ' + (mode === 'none' ? 'off' : mode), 'get logging.output']));
|
||||
}
|
||||
const mqttCompanion = findControlled('heltec_v4_2_v4_3_companion_radio_full_femon');
|
||||
assert.deepStrictEqual(mqttCompanion.loggingModes, ['none', 'usb', 'wifi', 'both']);
|
||||
for (const mode of ['usb', 'both']) {
|
||||
assert.deepStrictEqual(picker.runtimeDirections(mqttCompanion, {logging: mode})[0].actions[0].commands,
|
||||
['powersaving off', 'set usb.logging on']);
|
||||
['set powersaving off', 'set logging.output ' + mode, 'get logging.output']);
|
||||
}
|
||||
assert(picker.installSteps(mqttCompanion, 'bin').some(step => step.includes('powersaving off')));
|
||||
assert(picker.installSteps(mqttCompanion, 'bin').some(step => step.includes('set powersaving off')));
|
||||
// The workaround belongs to the affected release, not every future ESP32 build.
|
||||
for (const releaseFamily of ['v1.17.1.4-old', 'v1.17.1.5-halo-keymind-cascade-dev-newfix',
|
||||
'v1.17.1.6-next', 'v1.17.1.50-next', '']) {
|
||||
const otherRelease = {...mqttCompanion, releaseFamily};
|
||||
assert.deepStrictEqual(picker.runtimeDirections(otherRelease, {logging: 'usb'})[0].actions[0].commands,
|
||||
['set usb.logging on']);
|
||||
assert(!picker.installSteps(otherRelease, 'bin').some(step => step.includes('powersaving off')));
|
||||
['set logging.output usb', 'get logging.output']);
|
||||
assert(!picker.installSteps(otherRelease, 'bin').some(step => step.includes('set powersaving off')));
|
||||
}
|
||||
const companionWifi = picker.runtimeDirections(mqttCompanion, {logging: 'wifi'});
|
||||
assert.deepStrictEqual(companionWifi[0].actions[0].commands, ['set usb.logging off']);
|
||||
assert.deepStrictEqual(companionWifi[0].actions[0].commands, ['set logging.output wifi', 'get logging.output']);
|
||||
assert(companionWifi[0].actions[0].text.includes('enable the desired MQTT'));
|
||||
assert(commands(companionWifi).every(c => !/logging.output|bridge.enabled|set mqtt\./.test(c)));
|
||||
assert(companionWifi.some(s => s.title === 'GPS' && s.actions[0].text.includes('gps=1')));
|
||||
assert(commands(companionWifi).includes('set mqtt.enabled on'));
|
||||
assert(commands(companionWifi).includes('get wifi.cli'));
|
||||
assert(commands(companionWifi).includes('set usb.logging on') === false);
|
||||
assert(commands(companionWifi).includes('set gps on'));
|
||||
const companionOff = picker.runtimeDirections(mqttCompanion, {logging: 'none'});
|
||||
assert(companionOff[0].actions[0].text.includes('none for every MQTT'));
|
||||
assert(companionOff[0].actions[0].text.includes('saved broker settings'));
|
||||
for (const title of ['Device power saving', 'GPS', 'MQTT broker connections', 'WebConfig command terminal']) {
|
||||
assert.deepStrictEqual(companionWifi.find(s => s.title === title).actions,
|
||||
picker.runtimeDirections(observer, {}).find(s => s.title === title).actions);
|
||||
}
|
||||
const nrf = findControlled('RAK_4631_companion_radio_full');
|
||||
assert.deepStrictEqual(picker.runtimeDirections(nrf, {logging: 'usb'})[0].actions[0].commands,
|
||||
['set usb.logging on reboot']);
|
||||
const rakRepeater = findControlled('RAK_4631_repeater');
|
||||
const g3Repeater = findControlled('Station_G3_ESP32_repeater');
|
||||
assert.deepStrictEqual(picker.runtimeDirections(g3Repeater, {logging: 'usb'})[0].actions[0].commands,
|
||||
['powersaving off', 'set usb.logging on']);
|
||||
['set powersaving off', 'set usb.logging on']);
|
||||
assert.deepStrictEqual(picker.runtimeDirections(g3Repeater, {logging: 'none'})[0].actions[0].commands,
|
||||
['set usb.logging off']);
|
||||
const rakCommands = picker.runtimeDirections(rakRepeater, {logging: 'usb', mode: 'standard'});
|
||||
assert.deepStrictEqual(rakCommands[0].actions[0].commands, ['set usb.logging on']);
|
||||
assert.strictEqual(rakCommands.find(s => s.title.startsWith('RS232')).actions[0].label, 'Off');
|
||||
assert(commands(rakCommands).includes('gps on'));
|
||||
assert(commands(rakCommands).includes('set gps on'));
|
||||
assert(!rakCommands.some(s => s.title === 'MQTT broker connections'));
|
||||
const indicator = picker.runtimeDirections(findControlled('SenseCapIndicator-LoRa_companion_radio_full'), {});
|
||||
assert(commands(indicator).includes('set companion.transport ble'));
|
||||
|
||||
@@ -347,6 +347,16 @@ TEST(WebConfigBatch, OnlyWritesGateTheDeferredReboot) {
|
||||
EXPECT_FALSE(Batch::cliWriteSucceeded(""));
|
||||
}
|
||||
|
||||
TEST(WebConfigBatch, PasswordEchoIsMaskedOnlyAfterARealChange) {
|
||||
EXPECT_TRUE(Batch::cliPasswordChanged("password now: new-password"));
|
||||
EXPECT_TRUE(Batch::cliPasswordChanged("OK"));
|
||||
EXPECT_FALSE(Batch::cliPasswordChanged("Error: unsupported command"));
|
||||
EXPECT_FALSE(Batch::cliPasswordChanged("Error: failed to save"));
|
||||
EXPECT_FALSE(Batch::cliPasswordChanged("(no reply)"));
|
||||
EXPECT_FALSE(Batch::cliPasswordChanged(""));
|
||||
EXPECT_FALSE(Batch::cliPasswordChanged(NULL));
|
||||
}
|
||||
|
||||
TEST(WebConfigBatch, CliRebootIsWithheldWhenAnyCommandInTheSequenceFailed) {
|
||||
EXPECT_TRUE(Batch::cliRebootAllowed(/*has_reboot=*/true, /*all_ok=*/true));
|
||||
// Same rule a config save follows: do not reboot into a half-applied config
|
||||
|
||||
@@ -114,6 +114,105 @@ class WebConfigUiRuntimeTest(unittest.TestCase):
|
||||
self.assertNotIn("data-test-error=", dom)
|
||||
return dom
|
||||
|
||||
def test_companion_console_runs_commands_and_uses_role_suggestions(self):
|
||||
status, config = self.setup_values()
|
||||
status.update(mode="lan", cli=True, mqtt=True, active_slots=1,
|
||||
password_supported=False)
|
||||
prelude = """
|
||||
<script>
|
||||
(function(){
|
||||
var status=%s,config=%s,submitted=null;
|
||||
function response(value){
|
||||
return Promise.resolve({ok:true,status:200,json:function(){return Promise.resolve(value)}});
|
||||
}
|
||||
window.fetch=function(path,options){
|
||||
if(path==="/api/status")return response(status);
|
||||
if(path==="/api/config")return response(config);
|
||||
if(path==="/api/presets")return response({presets:[]});
|
||||
if(path==="/api/cli"){
|
||||
submitted=JSON.parse(options.body);
|
||||
return response({state:"running",reqid:submitted.reqid,total:1});
|
||||
}
|
||||
if(path.indexOf("/api/cli/result?")===0)return response({
|
||||
state:"done",reqid:submitted.reqid,results:[{ok:true,reply:"companion-console-test-version"}]
|
||||
});
|
||||
return response({});
|
||||
};
|
||||
window.addEventListener("load",function(){
|
||||
setTimeout(function(){
|
||||
var button=document.querySelector('#tabs button[data-t="cli"]');
|
||||
document.body.setAttribute("data-test-cli-tab",button?"yes":"no");
|
||||
if(!button)return;
|
||||
button.click();
|
||||
document.getElementById("term-in").value="ver";
|
||||
cliSubmit();
|
||||
},75);
|
||||
setTimeout(function(){
|
||||
var table=cliTable().map(function(row){return row[0]});
|
||||
document.body.setAttribute("data-test-suggestions",JSON.stringify(table));
|
||||
document.body.setAttribute("data-test-submitted",submitted?submitted.cmds.join(","):"none");
|
||||
document.body.setAttribute("data-test-reply",document.getElementById("term-out").textContent);
|
||||
document.body.setAttribute("data-test-enum",cliEnum("wifi.cli").join(","));
|
||||
document.body.setAttribute("data-test-busy",String(cli.busy));
|
||||
var master=document.querySelector('[data-k="mqtt.enabled"]');
|
||||
document.body.setAttribute("data-test-mqtt-enabled",master?String(master.checked):"missing");
|
||||
document.body.setAttribute("data-test-mqtt-enum",cliEnum("mqtt.enabled").join(","));
|
||||
cliHelp();
|
||||
document.body.setAttribute("data-test-companion-help",document.getElementById("term-out").textContent);
|
||||
// Switching role must invalidate the cached table even with one slot.
|
||||
st.role="repeater";
|
||||
document.body.setAttribute("data-test-repeater-erase",String(cliTable().some(function(row){return row[0]==="erase"})));
|
||||
},500);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
""" % (json.dumps(status), json.dumps(config))
|
||||
dom = self.run_page(prelude)
|
||||
self.assertIn('data-test-cli-tab="yes"', dom)
|
||||
self.assertIn('data-test-submitted="ver"', dom)
|
||||
self.assertIn("companion-console-test-version", dom)
|
||||
self.assertIn('data-test-busy="false"', dom)
|
||||
self.assertIn('data-test-enum="on,off"', dom)
|
||||
self.assertIn('data-test-repeater-erase="true"', dom)
|
||||
self.assertIn('data-test-mqtt-enabled="false"', dom)
|
||||
self.assertIn('data-test-mqtt-enum="on,off"', dom)
|
||||
import html
|
||||
help_text = html.unescape(re.search(
|
||||
r'data-test-companion-help="([^"]*)"', dom).group(1))
|
||||
self.assertIn("set mqtt.enabled on|off", help_text)
|
||||
self.assertNotIn("neighbors", help_text)
|
||||
self.assertNotIn("stats-core", help_text)
|
||||
suggestions = json.loads(html.unescape(re.search(
|
||||
r'data-test-suggestions="([^"]*)"', dom).group(1)))
|
||||
for command in ("set powersaving off", "get usb.logging", "set wifi.cli ",
|
||||
"set mqtt1.preset ", "get name", "get radio"):
|
||||
self.assertIn(command, suggestions)
|
||||
for command in ("erase", "password ", "setperm ", "get prv.key",
|
||||
"set bridge.enabled ", "get mqtt1.password"):
|
||||
self.assertNotIn(command, suggestions)
|
||||
|
||||
def test_console_stays_hidden_when_disabled_or_on_setup_ap(self):
|
||||
for mode in ("lan", "setup"):
|
||||
with self.subTest(mode=mode):
|
||||
status, config = self.setup_values()
|
||||
status.update(mode=mode, cli=False)
|
||||
prelude = """
|
||||
<script>
|
||||
(function(){
|
||||
var status=%s,config=%s;
|
||||
window.fetch=function(path){
|
||||
var value=path==="/api/status"?status:path==="/api/config"?config:{state:"done",networks:[]};
|
||||
return Promise.resolve({ok:true,status:200,json:function(){return Promise.resolve(value)}});
|
||||
};
|
||||
window.addEventListener("load",function(){setTimeout(function(){
|
||||
document.body.setAttribute("data-test-cli-hidden",String(!document.querySelector('#tabs button[data-t="cli"]')));
|
||||
},500)});
|
||||
})();
|
||||
</script>
|
||||
""" % (json.dumps(status), json.dumps(config))
|
||||
dom = self.run_page(prelude)
|
||||
self.assertIn('data-test-cli-hidden="true"', dom)
|
||||
|
||||
def run_early_selection_case(self, explicit_password=None):
|
||||
status, config = self.setup_values()
|
||||
|
||||
|
||||
Reference in new issue
Block a user