Refresh 1.17.1.6 picker metadata and preserve capacity directions

This commit is contained in:
mikecarper committed 2026-09-13 12:51:12 -07:00
1 parent fff92f0588
commit a7f58dd437
7 files changed
+803 -1003

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
{
"familyTag": "v1.17.1.5-halo-keymind-cascade-dev-26303793",
"memoryFollowupSources": {
"Heltec_Wireless_Paper_companion_radio_full": "1e4d1e167a984abee34e92288b44d55f3d0e15a6"
},
"memoryReplacementSource": "aa20e9278ebdd574c4769fdb58d46fd9d4face68",
"profiles": {
"Heltec_Wireless_Paper_companion_radio_full": {
"display": true,
"espnowBridge": false,
"femRx": false,
"femTx": false,
"gps": false,
"memoryNote": "Wireless Paper Full: 350 contacts and 40 channels; 256 offline message slots normally, 128 while mOTA borrows shared storage. Idle WiFi mOTA listening keeps all 256 slots. Sync unread messages with a Companion app before starting mOTA if more than 128 are pending. Stopping or disconnecting the source restores all 256 slots and frees scratch buffers. USB, Bluetooth, WiFi and the display are retained.",
"memorySource": "1e4d1e167a984abee34e92288b44d55f3d0e15a6",
"mqtt": false,
"platform": "ESP32_PLATFORM",
"primaryEspnow": false,
"rs232": false,
"rxgain": true,
"rxps": true,
"snmp": false,
"updateMethods": [
"wifi"
],
"webconfig": true
},
"Heltec_v3_companion_radio_full": {
"display": true,
"espnowBridge": false,
"femRx": false,
"femTx": false,
"gps": true,
"memoryNote": "Memory-corrected Full Companion: 150 contacts; the 256-message queue and all Full transports are retained. Export contacts before updating if you have more than 150; entries beyond this limit may be unavailable and a later save may omit them.",
"memorySource": "aa20e9278ebdd574c4769fdb58d46fd9d4face68",
"mqtt": true,
"platform": "ESP32_PLATFORM",
"primaryEspnow": false,
"rs232": false,
"rxgain": true,
"rxps": true,
"snmp": false,
"updateMethods": [
"wifi"
],
"webconfig": true
},
"RAK_4631_companion_radio_full": {
"display": true,
"espnowBridge": false,
"femRx": false,
"femTx": false,
"gps": true,
"memoryNote": "Corrected nRF52 Full: 256 offline message slots normally; 128 while mOTA borrows shared storage. Sync unread messages with a Companion app before starting mOTA if more than 128 are pending. Stopping or disconnecting the source returns all 256 slots. Contacts, channels and Full transports are retained.",
"memorySource": "aa20e9278ebdd574c4769fdb58d46fd9d4face68",
"mqtt": false,
"platform": "NRF52_PLATFORM",
"primaryEspnow": false,
"rs232": false,
"rxgain": true,
"rxps": true,
"snmp": false,
"updateMethods": [
"bluetooth"
],
"webconfig": false
},
"RAK_4631_repeater": {
"display": true,
"espnowBridge": false,
"femRx": false,
"femTx": false,
"gps": true,
"mqtt": false,
"platform": "NRF52_PLATFORM",
"primaryEspnow": false,
"rs232": true,
"rxgain": true,
"rxps": true,
"snmp": false,
"updateMethods": [
"bluetooth"
],
"webconfig": false
},
"SenseCapIndicator-LoRa_companion_radio_full": {
"display": true,
"espnowBridge": false,
"femRx": false,
"femTx": false,
"gps": false,
"mqtt": false,
"platform": "ESP32_PLATFORM",
"primaryEspnow": false,
"rs232": false,
"rxgain": true,
"rxps": true,
"snmp": false,
"updateMethods": [
"wifi"
],
"webconfig": true
},
"Station_G3_ESP32_repeater": {
"display": true,
"espnowBridge": false,
"femRx": true,
"femTx": true,
"gps": true,
"mqtt": false,
"platform": "ESP32_PLATFORM",
"primaryEspnow": false,
"rs232": false,
"rxgain": false,
"rxps": true,
"snmp": false,
"updateMethods": [
"wifi"
],
"webconfig": false
},
"heltec_v4_2_v4_3_companion_radio_full_femon": {
"display": true,
"espnowBridge": false,
"femRx": true,
"femTx": false,
"gps": true,
"mqtt": true,
"platform": "ESP32_PLATFORM",
"primaryEspnow": false,
"rs232": false,
"rxgain": true,
"rxps": true,
"snmp": false,
"updateMethods": [
"wifi"
],
"webconfig": true
},
"heltec_v4_repeater_observer_mqtt-full-usb-wifi": {
"display": true,
"espnowBridge": false,
"femRx": true,
"femTx": false,
"gps": true,
"mqtt": true,
"platform": "ESP32_PLATFORM",
"primaryEspnow": false,
"rs232": false,
"rxgain": true,
"rxps": true,
"snmp": true,
"updateMethods": [
"wifi",
"lora"
],
"webconfig": true
}
},
"source": "2630379365cf47cb7eec146b7d1b57b354efe251"
}
+26 -1
View File
@@ -866,7 +866,9 @@ assert.strictEqual(
console.log("generalized firmware picker tests passed");
// Shared features must produce the same commands for every role.
const controls = require('../docs/_data/firmware_controls.json');
// Keep the old release's workarounds and replacement-image checks independent
// of whichever release the live site currently serves.
const controls = require('./fixtures/firmware_picker_1_17_1_5_controls.json');
const liveFamily = controls.familyTag;
const controlledReleases = [release(liveFamily, '2026-09-01T00:00:00Z', [
asset('heltec_v4_2_v4_3_companion_radio_full_femon-' + liveFamily + '.bin'),
@@ -980,6 +982,29 @@ assert(stale.profiles.every(p => !p.controls));
assert(!picker.runtimeDirections({...mqttCompanion, controls: undefined}, {}).some(s => s.title === 'GPS'));
console.log('role-specific runtime directions tests passed');
// Exercise every profile from the current generated catalog while the tests
// above keep the older release's USB and capacity workarounds intact.
const currentControls = require('../docs/_data/firmware_controls.json');
const currentAssets = Object.entries(currentControls.profiles).map(([target, info]) => {
const source = info.memorySource || currentControls.source;
const tag = currentControls.familyTag.replace(/-[0-9a-f]{8}$/, '-' + source.slice(0, 8));
return asset(target + '-' + tag + (info.platform === 'NRF52_PLATFORM' ? '.uf2' : '.bin'));
});
const currentCatalog = picker.buildCatalog([
release(currentControls.familyTag, '2026-09-13T00:00:00Z', currentAssets),
], currentControls);
assert.strictEqual(currentCatalog.rows.length, currentAssets.length);
assert(currentCatalog.profiles.every(profile => profile.controls && profile.chipFamily !== 'unknown'));
const capacityProfiles = currentCatalog.profiles.filter(profile => profile.controls.memoryNote);
assert(capacityProfiles.length > 0, 'Regeneration must preserve capacity directions');
for (const profile of capacityProfiles) {
assert(picker.installSteps(profile, profile.installKinds[0]).includes(profile.controls.memoryNote), profile.target);
}
const currentObserver = currentCatalog.profiles.find(profile => profile.target === observer.target);
assert.deepStrictEqual(picker.runtimeDirections(currentObserver, {logging: 'usb'})[0].actions[0].commands,
['set logging.output usb', 'get logging.output']);
console.log('current release metadata and capacity directions tests passed');
assert.strictEqual(nrf.chipFamily, 'nrf52');
assert.strictEqual(mqttCompanion.chipFamily, 'esp32');
assert.deepStrictEqual(picker.facetValues(controlled.profiles, {chipFamily: 'nrf52'}, 'hardwareFamily'), ['RAK_4631']);
+88
View File
@@ -0,0 +1,88 @@
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location('picker_controls', ROOT / 'scripts/generate_picker_controls.py')
GENERATOR = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(GENERATOR)
class PickerControlsTests(unittest.TestCase):
def generate(self, reductions, *, source=None, flags=(), verified=True):
initial = 'a' * 40
manifest = dict(target='sample_companion_radio_full', platformio_env='sample',
platform='ESP32_PLATFORM', verified=verified, capabilities=['profile.full'],
reductions=reductions, ota_update_methods=['wifi'],
files=['sample_companion_radio_full-v1.17.1.6-dev-' + (source or initial)[:8] + '.bin'])
if source is not None:
manifest['source_commit'] = source
with tempfile.TemporaryDirectory(prefix='meshcore-picker-controls-') as temporary:
stage = Path(temporary)
(stage / 'companion').mkdir()
(stage / 'release-plan.json').write_text(json.dumps(dict(source=initial, groups=[
dict(key='companion', tag='v1.17.1.6-dev-aaaaaaaa')
])))
(stage / 'companion/TARGET-MANIFEST.json').write_text(json.dumps([manifest]))
config = [('env:sample', [('build_flags', list(flags))])]
result = GENERATOR.generate(stage, config)
return result['profiles']['sample_companion_radio_full']
def test_compiled_capacity_overrides_unreduced_config_and_binds_repair_source(self):
control = self.generate([
'companion.capacity limited to 150 contacts for runtime RAM; 256 queued frames and all Full transports retained'
], source='b' * 40, flags=['-DMAX_CONTACTS=350'])
self.assertIn('150 contacts and 256 queued messages', control['memoryNote'])
self.assertNotIn('350 contacts', control['memoryNote'])
self.assertEqual(control['memorySource'], 'b' * 40)
def test_compact_contacts_channels_and_queue_limits(self):
for channels in (8, 30):
with self.subTest(channels=channels):
control = self.generate([
f'companion.capacity limited to 100 contacts, {channels} channels, and 16 queued frames by measured internal DRAM'
])
self.assertIn(f'100 contacts, {channels} channels and 16 queued messages', control['memoryNote'])
def test_borrowed_queue_notes_retain_normal_and_active_capacities(self):
control = self.generate(['nRF52 Full: 256 offline frames normally; 128 while mOTA borrows queue storage'])
self.assertIn('256 offline messages normally; 128 while mOTA', control['memoryNote'])
self.assertIn('restores all 256 slots', control['memoryNote'])
self.assertEqual(control['memorySource'], 'a' * 40)
def test_paper_includes_declared_channels_after_flag_overrides(self):
control = self.generate([
'Wireless Paper Full: 350 contacts; 256 offline frames normally, 128 while mOTA borrows queue storage'
], flags=['-DMAX_GROUP_CHANNELS=80', '-UMAX_GROUP_CHANNELS', '-D MAX_GROUP_CHANNELS=40'])
self.assertIn('350 contacts and 40 channels', control['memoryNote'])
self.assertIn('256 offline messages normally; 128 while mOTA', control['memoryNote'])
def test_repeater_table_limits_are_kept_together(self):
control = self.generate([
'mesh.neighbors limited to 50 by measured RAM/flash capacity',
'mesh.flood_rules limited to 16 by measured internal RAM; complete rule engine, color display, GPS, and OTA retained',
])
self.assertIn('Neighbor table: 50 entries', control['memoryNote'])
self.assertIn('Flood rules: 16 entries', control['memoryNote'])
def test_other_reductions_do_not_invent_capacity_notes(self):
control = self.generate(['web.webconfig omitted to preserve the legacy portable ESP32 app slot'])
self.assertNotIn('memoryNote', control)
self.assertNotIn('memorySource', control)
def test_unknown_capacity_formats_are_not_silently_discarded(self):
with self.assertRaisesRegex(ValueError, 'Unrecognized capacity reduction'):
self.generate(['companion.capacity changed without numeric limits'])
def test_unqualified_or_unbound_notes_are_rejected(self):
reduction = ['nRF52 Full: 256 offline frames normally; 128 while mOTA borrows queue storage']
with self.assertRaisesRegex(ValueError, 'Unqualified target'):
self.generate(reduction, verified=False)
with self.assertRaisesRegex(ValueError, 'exact source commit'):
self.generate(reduction, source='abcdef12')
if __name__ == '__main__':
unittest.main()