From dfbf01fce0b9bbb4a3f94e44b28fc36a4f5a47d2 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:23:04 +0000 Subject: [PATCH 01/10] Route board detection through a common session Introduce detection request and outcome types and run every chip's embedded detection through one session (attempt count, reset escalation, hint, failure reason). Keep confirmed identification separate from successful display adoption. Behavior is unchanged; the legacy C5/C61 autodetect output pointers are preserved and generated detector lists are checked against the session family limit. --- .../lib/emit/m5gfx_detector_order.js | 4 +- extras/board_spec/test/board_spec.test.js | 34 +- extras/board_spec/test/detect_session.test.js | 56 ++ .../board_spec/test/detect_session_host.cpp | 153 ++++++ extras/board_spec/test/detector_order.test.js | 23 + src/M5GFX.cpp | 519 ++++++++---------- src/board_detect/board_detect.hpp | 77 +-- src/board_detect/detect_session.hpp | 71 +++ src/board_detect/detect_types.hpp | 115 ++++ .../generated/esp32_d0wdq6_detector_order.hpp | 2 + .../generated/esp32_pico_detector_order.hpp | 4 + .../m5/generated/esp32c3_detector_order.hpp | 2 + .../m5/generated/esp32c5_detector_order.hpp | 2 + .../m5/generated/esp32c61_detector_order.hpp | 2 + .../m5/generated/esp32c6_detector_order.hpp | 4 + .../m5/generated/esp32h2_detector_order.hpp | 2 + .../m5/generated/esp32p4_detector_order.hpp | 2 + .../m5/generated/esp32s3_detector_order.hpp | 4 + 18 files changed, 694 insertions(+), 382 deletions(-) create mode 100644 extras/board_spec/test/detect_session.test.js create mode 100644 extras/board_spec/test/detect_session_host.cpp create mode 100644 src/board_detect/detect_session.hpp create mode 100644 src/board_detect/detect_types.hpp diff --git a/extras/board_spec/lib/emit/m5gfx_detector_order.js b/extras/board_spec/lib/emit/m5gfx_detector_order.js index cc718cf7..d6a229f3 100644 --- a/extras/board_spec/lib/emit/m5gfx_detector_order.js +++ b/extras/board_spec/lib/emit/m5gfx_detector_order.js @@ -68,7 +68,9 @@ export function renderDetectorOrderHeaders(boards, groups) { maxFamilies = Math.max(maxFamilies, group.ordered.length); const array = `static const board_detector_t* const ${name}[] = {\n` + group.ordered.map((detector) => ` &${detector},`).join("\n") - + "\n nullptr,\n};\n"; + + "\n nullptr,\n};\n" + + `static_assert(sizeof(${name}) / sizeof(${name}[0]) - 1 <= max_detector_families,\n` + + ` "${name} exceeds the detection session family limit");\n`; outputs.set(group.output, (outputs.get(group.output) ?? "// Generated from detector_order.json and board catalog.\n#pragma once\n\n") + array + "\n"); } const ids = boards.filter(hasGpioPowerHold).map((board) => board.legacy_board_id).sort((a, b) => a - b); diff --git a/extras/board_spec/test/board_spec.test.js b/extras/board_spec/test/board_spec.test.js index cb98caec..2196824b 100644 --- a/extras/board_spec/test/board_spec.test.js +++ b/extras/board_spec/test/board_spec.test.js @@ -596,15 +596,11 @@ test("generated ESP32 wiring preserves the replaced board values", async () => { assert.deepEqual(parseGeneratedWiring(source), legacyEsp32Wiring); }); -test("ESP32 detector reset permission is promoted before the final retry", async () => { +test("all chip entries pass reset and hint policy through the common session", async () => { const main = await fs.readFile(path.join(root, "../../src/M5GFX.cpp"), "utf8"); - const loop = /int retry = 4;([\s\S]*?)board = autodetect\(use_reset, board, retry == 0, &transient_fallback,\s*&no_signature, &_board_candidate\);/.exec(main)?.[1]; - assert.ok(loop); - const promotion = loop.indexOf("if (retry == 1) { use_reset = true; }"); - const detected = loop.indexOf("try_setup_detected(esp32_detectors"); - assert.ok(promotion >= 0 && promotion < detected); - assert.match(loop, /static_cast\(nvs_board\), use_reset,/); - assert.doesNotMatch(loop, /detector_allow_reset/); + assert.match(main, /request.allow_reset = use_reset;[\s\S]*?request.max_attempts = 5;[\s\S]*?run_detection_session/); + assert.match(main, /package.policy.keep_initial_hint = true;/); + assert.match(main, /probe.allow_reset = request.allow_reset;/); }); test("displayless candidates do not retry and confirmed boards clear candidates", async () => { @@ -612,7 +608,8 @@ test("displayless candidates do not retry and confirmed boards clear candidates" const detector = await fs.readFile(path.join(root, "../../src/board_detect/board_detect.inl"), "utf8"); const c3 = await fs.readFile(path.join(root, "../../src/board_detect/m5/esp32c3.inl"), "utf8"); assert.match(detector, /if \(!signature\)[\s\S]*?return false;[\s\S]*?ctx\.confirm_attempted = true;/); - assert.match(main, /if \(board == board_t::board_unknown && no_signature\)[\s\S]*?break;/); + const session = await fs.readFile(path.join(root, "../../src/board_detect/detect_session.hpp"), "utf8"); + assert.match(session, /outcome.reason == fail_reason_t::no_signature[\s\S]*?return outcome;/); assert.match(main, /if \(board != board_t::board_unknown\) \{ _board_candidate = board_t::board_unknown; \}/); assert.match(c3, /if \(c3_detail::sof_advances\(\)\) \{ return true; \}/); assert.match(c3, /ctx\.candidate = &desc_stampc3\.def;[\s\S]*?return false;/); @@ -1301,7 +1298,7 @@ test("ESP32-C6 catalogs and detector preserve both display boards", async () => /specs::unitc6l::i2c_ioe::id_reg[\s\S]*?!is_pi4io\(value\)[\s\S]*?ctx\.final_attempt[\s\S]*?transient_fallback = true/); const c6Main = await fs.readFile(path.join(root, "../../src/M5GFX.cpp"), "utf8"); assert.match(c6Main, - /try_setup_detected\(board_detect::m5::esp32c6_detectors_qfn40,[\s\S]*?board, use_reset, final_attempt, &board,[\s\S]*?transient_fallback,\s*no_signature\)/); + /package.detectors = board_detect::m5::esp32c6_detectors_qfn40;/); assert.match(esp32c6Source, /i2c_pi4io2::id_reg[\s\S]*?is_pi4io\(value\)[\s\S]*?i2c_pi4io1::id_reg[\s\S]*?is_pi4io\(value\)/); assert.match(boardRegistrySource, /is_pi4io\(std::uint8_t value\).*?\(value & 0xE0\) == 0xA0/); @@ -1328,7 +1325,7 @@ test("ESP32-C6 catalogs and detector preserve both display boards", async () => assert.equal(classify({ pulldownHigh: 0n, pullupHigh: 0n }), "none"); const main = await fs.readFile(path.join(root, "../../src/M5GFX.cpp"), "utf8"); - assert.match(main, /pkg_ver == 0[\s\S]*?try_setup_detected\(board_detect::m5::esp32c6_detectors_qfn40/); + assert.match(main, /pkg == 0[\s\S]*?package.detectors = board_detect::m5::esp32c6_detectors_qfn40/); }); test("M5GFX probe dummy bits reject invalid catalog values", () => { @@ -1617,7 +1614,7 @@ test("confirmed boards survive post-detection power setup failures", async () => assert.match(implementation, /prepare_power\(desc, result, i2c\.port, true\)/); assert.match(implementation, /if \(!retain_confirmed_board\) \{ return false; \}[\s\S]*?power_on stopped after board confirmation:[\s\S]*?failed_index[\s\S]*?status[\s\S]*?0/); assert.match(main, /construct_result == board_detect::m5::construct_status_t::no_display[\s\S]*?transaction\.restore_start\(result\.desc->hold_high_pins\.data,[\s\S]*?result\.desc->hold_high_pins\.size\)/); - assert.match(main, /if \(!setup\(parts\)\)[\s\S]*?destroy_display_parts\(&parts\)[\s\S]*?transaction\.rollback\(\)/); + assert.match(main, /if \(!setup\(parts, setup_board\)\)[\s\S]*?destroy_display_parts\(&parts\)[\s\S]*?transaction\.rollback\(\)/); assert.match(setup, /destroy_display_parts\(display_parts_t\* parts\)[\s\S]*?parts->bus->release\(\)[\s\S]*?delete parts->touch;[\s\S]*?delete parts->light;[\s\S]*?delete parts->panel;[\s\S]*?delete parts->bus;/); }); @@ -1641,7 +1638,8 @@ test("confirmed boards tolerate reset-list faults while confirm remains strict", }); test("post-power member refinement and confirmed option variants are opt-in", async () => { - const header = await fs.readFile(path.join(root, "../../src/board_detect/board_detect.hpp"), "utf8"); + const header = await fs.readFile(path.join(root, "../../src/board_detect/board_detect.hpp"), "utf8") + + await fs.readFile(path.join(root, "../../src/board_detect/detect_types.hpp"), "utf8"); const implementation = await fs.readFile(path.join(root, "../../src/board_detect/board_detect.inl"), "utf8"); assert.match(header, /refine_fn_t refine = nullptr/); assert.match(header, /option_select_mask;[\s\S]*?option_select_value;/); @@ -1653,10 +1651,10 @@ test("post-power member refinement and confirmed option variants are opt-in", as test("PM1 extension failure injection rejects only its selected board", async () => { const main = await fs.readFile(path.join(root, "../../src/M5GFX.cpp"), "utf8"); - assert.match(main, /board_t setup_board = board_t::board_unknown/); + assert.match(main, /const auto setup_board = static_cast\(result.def->id\)/); assert.match(main, /FAIL_CHAINCAPTAIN_SETUP[\s\S]*?setup_board == board_t::board_M5ChainCaptain/); assert.match(main, /FAIL_PAPERCOLOR_SETUP[\s\S]*?setup_board == board_t::board_M5PaperColor/); - assert.doesNotMatch(main, /FAIL_PAPERCOLOR_SETUP\)[\s\S]{0,100}\(void\)parts;\s*return false;\s*#endif\s*return _adopt/); + assert.doesNotMatch(main, /FAIL_PAPERCOLOR_SETUP\)[\s\S]{0,100}\(void\)parts;\s*return true;\s*#endif\s*return _adopt/); }); test("generated wiring header links from two translation units", async (t) => { @@ -2414,7 +2412,7 @@ test("CoreP4X generated DSI setup preserves the legacy fields", async () => { assert.deepEqual(detectionPinsForEntries([{ board: source, chip: chipP4, emitted: wiring }]), [9, 11]); assert.match(esp32p4Source, /probe_i2c_read[\s\S]*?pm1_i2c_addr[\s\S]*?200[\s\S]*?probe_i2c_read[\s\S]*?ioe1_i2c_addr[\s\S]*?200/); const main = await fs.readFile(path.join(root, "../../src/M5GFX.cpp"), "utf8"); - assert.match(main, /pkg_ver == 0[\s\S]*?try_setup_detected\(board_detect::m5::esp32p4_detectors/); + assert.match(main, /pkg == 0[\s\S]*?package.detectors = board_detect::m5::esp32p4_detectors/); }); test("Tab5 touch identities select the three DSI panels", () => { @@ -2628,8 +2626,8 @@ test("CoreS3 family catalog keeps shared wiring and option power variants", asyn assert.match(coreSource, /refine_panel[\s\S]*?soft_spi_read32\(\s*probe, display\.sclk, display\.mosi, display\.mosi, display\.dc/); assert.doesNotMatch(detectorSource, /pin_miso_ == pin_dc_/); const main = await fs.readFile(path.join(root, "../../src/M5GFX.cpp"), "utf8"); - assert.match(main, /assigned before prepare\/refine[\s\S]*?representative family ID[\s\S]*?setup_board = static_cast\(result\.def->id\)/); - assert.match(main, /case 0: detectors = board_detect::m5::esp32s3_detectors_qfn56;[\s\S]*?case 1: detectors = board_detect::m5::esp32s3_detectors_lga56;[\s\S]*?try_setup_detected\(detectors, board/); + assert.match(main, /representative before refine[\s\S]*?setup_board = static_cast\(result\.def->id\)/); + assert.match(main, /case 0: package.detectors = board_detect::m5::esp32s3_detectors_qfn56;[\s\S]*?case 1: package.detectors = board_detect::m5::esp32s3_detectors_lga56;[\s\S]*?run_detection_attempt\(package.detectors/); }); test("detect classes include only explicit pins and retain 64-bit GPIO masks", () => { diff --git a/extras/board_spec/test/detect_session.test.js b/extras/board_spec/test/detect_session.test.js new file mode 100644 index 00000000..a2fa9ab0 --- /dev/null +++ b/extras/board_spec/test/detect_session.test.js @@ -0,0 +1,56 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +test("detection session preserves legacy hint, reset, retries, candidates and NVS decisions", async (t) => { + const compiler = process.env.CXX || "c++"; + if (spawnSync(compiler, ["--version"]).error?.code === "ENOENT") return t.skip("C++ compiler unavailable"); + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m5gfx-session-")); + try { + const binary = path.join(directory, "detect_session_host"); + const compiled = spawnSync(compiler, ["-std=c++11", "-O2", `-I${path.resolve(here, "../../../src")}`, + path.join(here, "detect_session_host.cpp"), "-o", binary], { encoding: "utf8" }); + assert.equal(compiled.status, 0, compiled.stderr || compiled.stdout); + const ran = spawnSync(binary, [], { encoding: "utf8" }); + assert.equal(ran.status, 0, ran.stderr || ran.stdout); + assert.match(ran.stdout, /equivalence cases=4800000/); + } finally { + await fs.rm(directory, { recursive: true, force: true }); + } +}); + +test("compatibility autodetect preserves the C5/C61 transient output sentinel", async (t) => { + const compiler = process.env.CXX || "c++"; + if (spawnSync(compiler, ["--version"]).error?.code === "ENOENT") return t.skip("C++ compiler unavailable"); + const sourceRoot = path.resolve(here, "../../../src"); + const main = await fs.readFile(path.join(sourceRoot, "M5GFX.cpp"), "utf8"); + const assignment = /#if !defined\(CONFIG_IDF_TARGET_ESP32C5\) && !defined\(CONFIG_IDF_TARGET_ESP32C61\)([\s\S]*?)#endif/.exec(main)?.[0]; + assert.ok(assignment, "compile the actual compatibility output block"); + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m5gfx-transient-")); + try { + const source = path.join(directory, "transient.cpp"); + await fs.writeFile(source, `#include "board_detect/detect_types.hpp"\n#include \n` + + `void apply(const m5gfx::board_detect::detect_outcome_t& outcome, bool* transient_fallback) {\n${assignment}\n}\n` + + `int main() { for (int successful = 0; successful < 2; ++successful) {\n` + + `for (int result = 0; result < 2; ++result) { for (int initial = 0; initial < 2; ++initial) {\n` + + `m5gfx::board_detect::detect_outcome_t out; out.setup_succeeded = successful; out.result.transient_fallback = result;\n` + + `bool caller = initial; apply(out, &caller); apply(out, nullptr);\n` + + `#if defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C61)\nassert(caller == bool(initial));\n` + + `#else\nassert(caller == bool(successful ? result : initial));\n#endif\n} } } }\n`); + for (const chip of ["ESP32C5", "ESP32C61", "ESP32S3"]) { + const binary = path.join(directory, chip); + const compiled = spawnSync(compiler, ["-std=c++11", `-I${sourceRoot}`, `-DCONFIG_IDF_TARGET_${chip}`, + source, "-o", binary], { encoding: "utf8" }); + assert.equal(compiled.status, 0, compiled.stderr || compiled.stdout); + const ran = spawnSync(binary, [], { encoding: "utf8" }); + assert.equal(ran.status, 0, ran.stderr || ran.stdout); + } + } finally { + await fs.rm(directory, { recursive: true, force: true }); + } +}); diff --git a/extras/board_spec/test/detect_session_host.cpp b/extras/board_spec/test/detect_session_host.cpp new file mode 100644 index 00000000..60d12c73 --- /dev/null +++ b/extras/board_spec/test/detect_session_host.cpp @@ -0,0 +1,153 @@ +#include "board_detect/detect_session.hpp" +#include +#include +#include +using namespace m5gfx::board_detect; + +static const board_def_t proven = { 11, "proven", 0 }; +static const board_def_t suggested = { 12, "suggested", 0 }; +static const board_def_t alternate = { 13, "alternate", 0 }; +struct trace_t { + unsigned attempt; + board_id_t hint; + bool reset, final; + bool operator==(const trace_t& rhs) const { + return attempt == rhs.attempt && hint == rhs.hint && reset == rhs.reset && final == rhs.final; + } +}; +struct observed_t { + board_id_t board = 0, candidate = 0; + bool transient = false; + unsigned attempts = 0, calls = 0; + std::array trace; +}; +static detect_outcome_t injected(unsigned kind) { + detect_outcome_t out; + if (kind == 0 || kind == 1) { out.reason = fail_reason_t::no_signature; } + if (kind == 1 || kind == 7) { + out.result.candidate = kind == 1 ? &suggested : &alternate; + out.verdict = verdict_t::candidate; + } + if (kind == 3 || kind == 5 || kind == 6 || kind == 8 || kind == 9) { + out.verdict = verdict_t::confirmed; + out.result.status = detect_status_t::matched; + out.result.def = &proven; + out.result.transient_fallback = kind == 3 || kind == 6; + out.setup_succeeded = kind == 5 || kind == 6; + if (kind == 3) { out.reason = fail_reason_t::prepare_failed; } + if (kind == 8) { out.reason = fail_reason_t::construct_failed; } + if (kind == 9) { out.reason = fail_reason_t::adopt_failed; } + } + if (kind == 4) { out.reason = fail_reason_t::excluded_by_set; } + return out; +} +// Independent transcription of the pre-session loop: retry=4..0, ESP32's +// original NVS hint versus the other chips' previous board, and hint gates. +static observed_t legacy(unsigned sequence, const detect_session_policy_t& policy, + board_id_t initial_hint, bool reset, bool has_list) { + observed_t obs; + board_id_t board = initial_hint; + int retry = 4; + do { + ++obs.attempts; + if (retry == 1) { reset = true; } + const auto hint = policy.keep_initial_hint ? initial_hint : board; + bool allowed = hint == 0 || policy.hint_gate == nullptr; + if (!allowed) { + for (auto p = policy.hint_gate; *p; ++p) { if (*p == hint) { allowed = true; } } + } + detect_outcome_t out; + if (has_list && allowed) { + obs.trace[obs.calls++] = { unsigned(4 - retry), hint, reset, + retry == 0 && !policy.suppress_final_attempt }; + unsigned digits = sequence; + for (int i = 0; i < 4 - retry; ++i) { digits /= 10; } + out = injected(digits % 10); + } + if (!obs.candidate && out.result.candidate) { obs.candidate = out.result.candidate->id; } + if (out.setup_succeeded) { + board = out.result.def->id; + obs.transient = out.result.transient_fallback; + break; + } + board = 0; + if (out.reason == fail_reason_t::no_signature) { obs.transient = true; break; } + } while (board == 0 && --retry >= 0); + obs.board = board; + if (board) { obs.candidate = 0; } + return obs; +} +static observed_t session(unsigned sequence, const detect_session_policy_t& policy, + board_id_t initial_hint, bool reset, bool has_list) { + observed_t obs; + detect_request_t request; + request.hint = initial_hint; request.allow_reset = reset; request.max_attempts = 5; + const auto outcome = run_detection_session(request, policy, + [&](const detect_request_t& attempt, bool final) { + assert(attempt.target_set == nullptr); + if (!has_list) { return detect_outcome_t{}; } + obs.trace[obs.calls++] = { attempt.attempt, attempt.hint, attempt.allow_reset, final }; + unsigned digits = sequence; + for (unsigned i = 0; i < attempt.attempt; ++i) { digits /= 10; } + return injected(digits % 10); + }); + obs.attempts = outcome.attempts; + obs.board = outcome.setup_succeeded ? outcome.result.def->id : 0; + obs.candidate = !obs.board && outcome.result.candidate ? outcome.result.candidate->id : 0; + obs.transient = outcome.reason == fail_reason_t::no_signature + || (outcome.setup_succeeded && outcome.result.transient_fallback); + return obs; +} +int main() { + static const board_id_t gate[] = { 11, 0 }; + unsigned cases = 0; + for (unsigned flags = 0; flags < 8; ++flags) { + detect_session_policy_t policy; + policy.hint_gate = flags & 1 ? gate : nullptr; + policy.keep_initial_hint = flags & 2; + policy.suppress_final_attempt = flags & 4; + for (board_id_t hint : { board_id_t(0), board_id_t(11), board_id_t(99) }) { + for (bool reset : { false, true }) { + for (unsigned sequence = 0; sequence < 100000; ++sequence) { + const auto before = legacy(sequence, policy, hint, reset, true); + const auto after = session(sequence, policy, hint, reset, true); + assert(before.board == after.board && before.candidate == after.candidate); + assert(before.transient == after.transient && before.attempts == after.attempts); + assert(before.calls == after.calls); + for (unsigned i = 0; i < before.calls; ++i) { assert(before.trace[i] == after.trace[i]); } + // The unchanged NVS expression also retains unknown=0 writes. + assert((!before.transient && hint != before.board) == (!after.transient && hint != after.board)); + ++cases; + } + const auto before = legacy(0, policy, hint, reset, false); + const auto after = session(0, policy, hint, reset, false); + assert(before.board == after.board && before.transient == after.transient); + assert(before.attempts == after.attempts && after.calls == 0); + } + } + } + // A wide NVS value ending in an allowed board ID still fails the old gate + // before narrowing. Only the following unknown-hint attempt may probe. + detect_session_policy_t narrowed; + narrowed.hint_gate = gate; + narrowed.reject_initial_hint = true; + detect_request_t wide; + wide.hint = 11; wide.max_attempts = 5; + unsigned calls = 0; + const auto after_wide = run_detection_session(wide, narrowed, + [&](const detect_request_t& request, bool) { + ++calls; assert(request.attempt == 1 && request.hint == 0); return injected(5); + }); + assert(calls == 1 && after_wide.attempts == 2 && after_wide.setup_succeeded); + // The protected compatibility entry is a single attempt and must not promote + // reset simply because max_attempts is one. + detect_request_t single; + detect_session_policy_t policy; + policy.suppress_final_attempt = true; + const auto one = run_detection_session(single, policy, + [](const detect_request_t& request, bool final) { + assert(!request.allow_reset && !final); return injected(3); + }); + assert(one.attempts == 1 && one.verdict == verdict_t::confirmed && !one.setup_succeeded); + std::cout << "legacy/session equivalence cases=" << cases << '\n'; +} diff --git a/extras/board_spec/test/detector_order.test.js b/extras/board_spec/test/detector_order.test.js index bb738eb1..b7bb1f71 100644 --- a/extras/board_spec/test/detector_order.test.js +++ b/extras/board_spec/test/detector_order.test.js @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; import { promises as fs } from "node:fs"; import path from "node:path"; import test from "node:test"; @@ -96,3 +97,25 @@ test("GPIO power-hold catalog and M5GFX descriptions agree", async () => { assert.equal(/gpio_power(?:_low)?\(/.test(power), expected, id); } }); + +test("generated detector count accepts the limit and rejects an overflowing list", (t) => { + const compiler = process.env.CXX || "c++"; + if (spawnSync(compiler, ["--version"]).error?.code === "ENOENT") return t.skip("C++ compiler unavailable"); + const board = { id: "board", chip: "test", legacy_board_id: 1 }; + const groups = { test_detectors: { chip: "test", output: "test_order.hpp", families: [ + { detector: "first", boards: ["board"] }, { detector: "second", boards: ["board"] }, + ] } }; + const array = renderDetectorOrderHeaders([board], groups).get("test_order.hpp"); + const compile = (limit) => spawnSync(compiler, ["-std=c++11", "-x", "c++", "-fsyntax-only", "-"], { + encoding: "utf8", input: `struct board_detector_t {}; +static const board_detector_t first{}, second{}; +` + + `static constexpr unsigned max_detector_families = ${limit}; +` + array, + }); + const atLimit = compile(2); + assert.equal(atLimit.status, 0, atLimit.stderr); + const overflow = compile(1); + assert.notEqual(overflow.status, 0); + assert.match(overflow.stderr, /test_detectors exceeds the detection session family limit/); +}); diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index d3a1db03..5211776f 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -39,6 +39,7 @@ #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) || defined (CONFIG_IDF_TARGET_ESP32S3) || defined (CONFIG_IDF_TARGET_ESP32C3) || defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C6) || defined (CONFIG_IDF_TARGET_ESP32C61) || defined (CONFIG_IDF_TARGET_ESP32P4) || defined (CONFIG_IDF_TARGET_ESP32H2) #include "board_detect/board_detect.inl" +#include "board_detect/detect_session.hpp" #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) #include "board_detect/m5/esp32_d0wdq6.inl" #elif defined (CONFIG_IDF_TARGET_ESP32S3) @@ -789,24 +790,154 @@ namespace m5gfx } #endif + struct detection_package_t + { + const board_detect::board_detector_t* const* detectors = nullptr; + board_detect::detect_session_policy_t policy; + bool conditional_pins_unavailable = false; + bool direct_setup = false; + }; + + __attribute__((always_inline)) static inline detection_package_t select_detection_package(bool legacy_autodetect = false) + { + detection_package_t package; + (void)legacy_autodetect; + // Retain these legacy differences until S1 aligns hint and final-attempt + // semantics. Selection itself does not probe or configure any GPIO. +#if !defined(CONFIG_IDF_TARGET) || defined(CONFIG_IDF_TARGET_ESP32) + package.policy.keep_initial_hint = true; + package.direct_setup = true; + // The protected legacy autodetect method never probed ESP32 directly. + if (legacy_autodetect) { return package; } + const auto pkg = m5gfx::get_pkg_ver(); + if (pkg == EFUSE_RD_CHIP_VER_PKG_ESP32D0WDQ6) + { package.detectors = board_detect::m5::esp32_d0wdq6_detectors; } + else if (pkg == EFUSE_RD_CHIP_VER_PKG_ESP32PICOD4) + { package.detectors = board_detect::m5::esp32_pico_d4_detectors; } + else if (pkg == 6) + { package.detectors = board_detect::m5::esp32_picov3_detectors; } +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + package.conditional_pins_unavailable = conditional_detection_pins_unavailable(); + switch (m5gfx::get_pkg_ver()) + { + case 0: package.detectors = board_detect::m5::esp32s3_detectors_qfn56; break; + case 1: package.detectors = board_detect::m5::esp32s3_detectors_lga56; break; + default: break; + } +#elif defined(CONFIG_IDF_TARGET_ESP32C3) + static const board_detect::board_id_t gate[] = { + board_t::board_M5StampC3, board_t::board_M5StampC3U, board_detect::board_id_unknown, + }; + package.policy.hint_gate = gate; + package.detectors = board_detect::m5::esp32c3_detectors; +#elif defined(CONFIG_IDF_TARGET_ESP32C5) + static const board_detect::board_id_t gate[] = { + board_t::board_M5ToughC5, board_t::board_M5StampC5, board_detect::board_id_unknown, + }; + package.policy.hint_gate = gate; + package.policy.suppress_final_attempt = true; + package.detectors = board_detect::m5::esp32c5_detectors; +#elif defined(CONFIG_IDF_TARGET_ESP32C6) + const auto pkg = m5gfx::get_pkg_ver(); + ESP_LOGD(LIBRARY_NAME, "pkg_ver : %02x", (int)pkg); + if (pkg == 1) { package.detectors = board_detect::m5::esp32c6_detectors_qfn32; } + else if (pkg == 0) + { + static const board_detect::board_id_t gate[] = { + board_t::board_ArduinoNessoN1, board_t::board_M5UnitC6L, board_detect::board_id_unknown, + }; + package.policy.hint_gate = gate; + package.detectors = board_detect::m5::esp32c6_detectors_qfn40; + } +#elif defined(CONFIG_IDF_TARGET_ESP32C61) + static const board_detect::board_id_t gate[] = { + board_t::board_M5CoreMatrix, board_detect::board_id_unknown, + }; + package.policy.hint_gate = gate; + package.policy.suppress_final_attempt = true; + package.detectors = board_detect::m5::esp32c61_detectors; +#elif defined(CONFIG_IDF_TARGET_ESP32H2) + package.detectors = board_detect::m5::esp32h2_detectors; +#elif defined(CONFIG_IDF_TARGET_ESP32P4) + const auto pkg = m5gfx::get_pkg_ver(); + ESP_LOGD(LIBRARY_NAME, "pkg_ver : %02x", (int)pkg); + if (pkg == 0) { package.detectors = board_detect::m5::esp32p4_detectors; } +#endif + return package; + } + + static bool reject_detected_setup(board_t setup_board) + { + (void)setup_board; +#if (!defined(CONFIG_IDF_TARGET) || defined(CONFIG_IDF_TARGET_ESP32)) && defined (M5GFX_AUTODETECT_TEST_FAIL_STICKCPLUS_SETUP) + if (setup_board == board_t::board_M5StickCPlus) { return true; } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined (M5GFX_AUTODETECT_TEST_FAIL_STOPWATCH_SETUP) + if (setup_board == board_t::board_M5StopWatch + || setup_board == board_t::board_M5PaperMono) { return true; } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined (M5GFX_AUTODETECT_TEST_FAIL_CHAINCAPTAIN_SETUP) + if (setup_board == board_t::board_M5ChainCaptain) { return true; } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined (M5GFX_AUTODETECT_TEST_FAIL_PAPERCOLOR_SETUP) + if (setup_board == board_t::board_M5PaperColor) { return true; } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined (M5GFX_AUTODETECT_TEST_FAIL_PAPERS3_SETUP) + if (setup_board == board_t::board_M5PaperS3) { return true; } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined (M5GFX_AUTODETECT_TEST_FAIL_PAPERDIY_SETUP) + if (setup_board == board_t::board_M5PaperDIY) { return true; } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined (M5GFX_AUTODETECT_TEST_FAIL_CARDPUTER_SETUP) + if (setup_board == board_t::board_M5Cardputer + || setup_board == board_t::board_M5CardputerADV + || setup_board == board_t::board_M5VAMeter) { return true; } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined (M5GFX_AUTODETECT_TEST_FAIL_AIRQ_SETUP) + if (setup_board == board_t::board_M5AirQ) { return true; } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined (M5GFX_AUTODETECT_TEST_FAIL_STAMPLC_SETUP) + if (setup_board == board_t::board_M5StamPLC) { return true; } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32P4) && defined (M5GFX_AUTODETECT_TEST_FAIL_COREP4X_SETUP) + if (setup_board == board_t::board_M5CoreP4X) + { + return true; + } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32P4) && defined (M5GFX_AUTODETECT_TEST_FAIL_TAB5_SETUP) + if (setup_board == board_t::board_M5Tab5 + || setup_board == board_t::board_M5Tab5X) + { + return true; + } +#endif +#if defined(CONFIG_IDF_TARGET_ESP32C5) && defined (M5GFX_AUTODETECT_TEST_FAIL_TOUGHC5_SETUP) + + return true; +#endif +#if defined(CONFIG_IDF_TARGET_ESP32C61) && defined (M5GFX_AUTODETECT_TEST_FAIL_COREMATRIX_SETUP) + + return true; +#endif + return false; + } + + template - static bool try_setup_detected(const board_detect::board_detector_t* const* detectors, - board_t hint, bool allow_reset, bool final_attempt, - board_t* detected_board, SetupDetected setup, - bool* detector_matched = nullptr, - board_t* setup_board = nullptr, - bool* transient_fallback = nullptr, - bool* no_signature = nullptr, - board_t* candidate_board = nullptr) + static board_detect::detect_outcome_t run_detection_attempt( + const board_detect::board_detector_t* const* detectors, + const board_detect::detect_request_t& request, bool final_attempt, + SetupDetected setup) { - if (detector_matched != nullptr) { *detector_matched = false; } - if (no_signature != nullptr) { *no_signature = false; } + board_detect::detect_outcome_t outcome; + if (detectors == nullptr) { return outcome; } #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) || defined (CONFIG_IDF_TARGET_ESP32C3) || defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C6) || defined (CONFIG_IDF_TARGET_ESP32C61) || defined (CONFIG_IDF_TARGET_ESP32P4) || defined (CONFIG_IDF_TARGET_ESP32H2) board_detect::detection_transaction_t transaction( board_detect::pins(board_detect::m5::wiring::detection::unconditional_pins), board_detect::no_pins(), false); #else - const bool conditional_pins_unavailable = conditional_detection_pins_unavailable(); + const bool conditional_pins_unavailable = request.conditional_pins_unavailable; board_detect::detection_transaction_t transaction( board_detect::pins(board_detect::m5::wiring::detection::unconditional_pins), board_detect::pins(board_detect::m5::wiring::detection::opi_pins), @@ -815,13 +946,14 @@ namespace m5gfx if (!transaction.valid()) { ESP_LOGW(LIBRARY_NAME, "[Autodetect] detection transaction could not capture GPIO state"); - return false; + return outcome; } board_detect::probe_ctx_t probe; - probe.allow_reset = allow_reset; + probe.allow_reset = request.allow_reset; probe.final_attempt = final_attempt; probe.i2c_port_probe = probe_i2c_port; probe.transaction = &transaction; + probe.enabled_ids = request.target_set; #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) #if defined (M5GFX_AUTODETECT_TEST_STATION_TO_CORE2) // Test-only: after the forced Station miss and Core2 match, convert the @@ -846,27 +978,26 @@ namespace m5gfx #endif #endif auto result = board_detect::detect_board( - detectors, static_cast(hint), probe); - if (no_signature != nullptr) { *no_signature = !probe.confirm_attempted; } - if (candidate_board != nullptr && *candidate_board == board_t::board_unknown - && result.candidate != nullptr) - { *candidate_board = static_cast(result.candidate->id); } + detectors, request.hint, probe); + outcome.result = result; + if (!probe.confirm_attempted) { outcome.reason = board_detect::fail_reason_t::no_signature; } + if (result.candidate != nullptr) { outcome.verdict = board_detect::verdict_t::candidate; } if (result.status == board_detect::detect_status_t::excluded) { ESP_LOGW(LIBRARY_NAME, "[Autodetect] detected board:%u is excluded", static_cast(result.def->id)); + outcome.reason = board_detect::fail_reason_t::excluded_by_set; transaction.rollback(); - return false; + return outcome; } if (result.status != board_detect::detect_status_t::matched) { transaction.rollback(); - return false; + return outcome; } - if (detector_matched != nullptr) { *detector_matched = true; } - // This is assigned before prepare/refine; for a family with a refine hook it - // is the representative family ID, not necessarily the final member ID. - if (setup_board != nullptr) { *setup_board = static_cast(result.def->id); } + outcome.verdict = board_detect::verdict_t::confirmed; + // Failure injection historically sees the representative before refine. + const auto setup_board = static_cast(result.def->id); const board_detect::prepare_ctx_t& prepare_ctx = probe; const int adopted_i2c_port = result.desc->internal_i2c.hw_port; @@ -876,8 +1007,10 @@ namespace m5gfx { ESP_LOGW(LIBRARY_NAME, "[Autodetect] prepare failed for detected board:%u", static_cast(result.def->id)); + outcome.result = result; + outcome.reason = board_detect::fail_reason_t::prepare_failed; transaction.rollback(); - return false; + return outcome; } if (adopted_i2c_port >= 0 && !adopted_i2c_was_open && lgfx::i2c::isInitialized(adopted_i2c_port)) @@ -890,8 +1023,10 @@ namespace m5gfx { ESP_LOGW(LIBRARY_NAME, "[Autodetect] setup failed for detected board:%u", static_cast(result.def->id)); + outcome.result = result; + outcome.reason = board_detect::fail_reason_t::construct_failed; transaction.rollback(); - return false; + return outcome; } if (construct_result == board_detect::m5::construct_status_t::no_display) { @@ -900,7 +1035,7 @@ namespace m5gfx transaction.restore_start(result.desc->hold_high_pins.data, result.desc->hold_high_pins.size); } - if (!setup(parts)) + if (!setup(parts, setup_board)) { // A rejecting adopter must not retain any of these raw pointers. Release // the bus before rollback restores its GPIO routing, then destroy every @@ -908,17 +1043,18 @@ namespace m5gfx board_detect::m5::destroy_display_parts(&parts); ESP_LOGW(LIBRARY_NAME, "[Autodetect] setup failed for detected board:%u", static_cast(result.def->id)); + outcome.result = result; + outcome.reason = board_detect::fail_reason_t::adopt_failed; transaction.rollback(); - return false; + return outcome; } transaction.commit(); - *detected_board = static_cast(result.def->id); - if (transient_fallback != nullptr) - { *transient_fallback = result.transient_fallback; } + outcome.result = result; + outcome.setup_succeeded = true; const auto log = board_detect::m5::success_log(result); if (log.name != nullptr && log.name[0] != '\0') { ESP_LOGI(LIBRARY_NAME, "[Autodetect] %s%s", log.name, log.annotation); } - return true; + return outcome; } bool M5GFX::_adopt_detected_parts(lgfx::IBus* bus, lgfx::Panel_Device* panel_part, @@ -1014,68 +1150,40 @@ namespace m5gfx #endif } - auto board = (board_t)nvs_board; - -#if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) - const auto esp32_pkg_ver = m5gfx::get_pkg_ver(); - const board_detect::board_detector_t* const* esp32_detectors = nullptr; - if (esp32_pkg_ver == EFUSE_RD_CHIP_VER_PKG_ESP32D0WDQ6) - { esp32_detectors = board_detect::m5::esp32_d0wdq6_detectors; } - else if (esp32_pkg_ver == EFUSE_RD_CHIP_VER_PKG_ESP32PICOD4) - { esp32_detectors = board_detect::m5::esp32_pico_d4_detectors; } - else if (esp32_pkg_ver == 6) // EFUSE_RD_CHIP_VER_PKG_ESP32PICOV3_02 - { esp32_detectors = board_detect::m5::esp32_picov3_detectors; } -#endif - - bool transient_fallback = false; - int retry = 4; - do - { - bool no_signature = false; -#if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) - // Match the other chip paths: after repeated no-reset attempts, the - // ESP32 detector must see the promoted reset permission too. - if (retry == 1) { use_reset = true; } - if (esp32_detectors != nullptr) + board_detect::detect_request_t request; + request.hint = static_cast(nvs_board); + request.allow_reset = use_reset; + request.max_attempts = 5; + auto package = select_detection_package(); + package.policy.reject_initial_hint = package.policy.hint_gate != nullptr + && nvs_board != request.hint; + request.conditional_pins_unavailable = package.conditional_pins_unavailable; + const auto outcome = board_detect::run_detection_session( + request, package.policy, + [this, &package](const board_detect::detect_request_t& attempt, bool final_attempt) { - bool detector_matched; - board_t setup_board = board_t::board_unknown; - if (try_setup_detected(esp32_detectors, - static_cast(nvs_board), use_reset, - retry == 0, &board, - [this, &setup_board](board_detect::m5::display_parts_t& parts) - { -#if defined (M5GFX_AUTODETECT_TEST_FAIL_STICKCPLUS_SETUP) - if (setup_board == board_t::board_M5StickCPlus) { return false; } -#endif - return _adopt_detected_parts(parts.bus, parts.panel, - parts.light, parts.touch); - }, &detector_matched, &setup_board, &transient_fallback, - &no_signature, &_board_candidate)) - { - break; - } - if (detector_matched) + if (!package.direct_setup) { panel(nullptr); } + auto result = run_detection_attempt(package.detectors, attempt, final_attempt, + [this](board_detect::m5::display_parts_t& parts, board_t setup_board) + { + return !reject_detected_setup(setup_board) + && _adopt_detected_parts(parts.bus, parts.panel, parts.light, parts.touch); + }); + if (!package.direct_setup || result.verdict != board_detect::verdict_t::confirmed) { - board = board_t::board_unknown; - continue; + if (package.direct_setup) { panel(nullptr); } + panel(_panel_last.get()); } - } -#endif -#if defined (CONFIG_IDF_TARGET) && !defined (CONFIG_IDF_TARGET_ESP32) - if (retry == 1) use_reset = true; -#endif - board = autodetect(use_reset, board, retry == 0, &transient_fallback, - &no_signature, &_board_candidate); - // Only a positive signature that entered confirm and failed can justify - // another attempt. A candidate alone does not retry or become NVS state. - if (board == board_t::board_unknown && no_signature) - { - transient_fallback = true; - break; - } - //ESP_LOGD(LIBRARY_NAME,"autodetect board:%d", (int)board); - } while (board_t::board_unknown == board && --retry >= 0); + return result; + }); + const auto board = outcome.setup_succeeded + ? static_cast(outcome.result.def->id) : board_t::board_unknown; + // Only a successful adopter exported transient_fallback in the old path. + // A partial failed result must not change the NVS write decision in S0. + const bool transient_fallback = outcome.reason == board_detect::fail_reason_t::no_signature + || (outcome.setup_succeeded && outcome.result.transient_fallback); + if (outcome.result.candidate != nullptr) + { _board_candidate = static_cast(outcome.result.candidate->id); } _board = board; if (board != board_t::board_unknown) { _board_candidate = board_t::board_unknown; } @@ -1129,207 +1237,44 @@ namespace m5gfx bool* no_signature, board_t* candidate_board) { panel(nullptr); - -#if defined (CONFIG_IDF_TARGET_ESP32S3) - - const board_detect::board_detector_t* const* detectors = nullptr; - switch (m5gfx::get_pkg_ver()) - { - case 0: detectors = board_detect::m5::esp32s3_detectors_qfn56; break; - case 1: detectors = board_detect::m5::esp32s3_detectors_lga56; break; - default: break; - } - if (detectors != nullptr) - { - board_t setup_board = board_t::board_unknown; - // A hint tries its family first, then the remaining families in package - // order, within one GPIO transaction. - if (try_setup_detected(detectors, board, use_reset, final_attempt, &board, - [this, &setup_board](board_detect::m5::display_parts_t& parts) - { -#if defined (M5GFX_AUTODETECT_TEST_FAIL_STOPWATCH_SETUP) - if (setup_board == board_t::board_M5StopWatch - || setup_board == board_t::board_M5PaperMono) { return false; } -#endif -#if defined (M5GFX_AUTODETECT_TEST_FAIL_CHAINCAPTAIN_SETUP) - if (setup_board == board_t::board_M5ChainCaptain) { return false; } -#endif -#if defined (M5GFX_AUTODETECT_TEST_FAIL_PAPERCOLOR_SETUP) - if (setup_board == board_t::board_M5PaperColor) { return false; } -#endif -#if defined (M5GFX_AUTODETECT_TEST_FAIL_PAPERS3_SETUP) - if (setup_board == board_t::board_M5PaperS3) { return false; } -#endif -#if defined (M5GFX_AUTODETECT_TEST_FAIL_PAPERDIY_SETUP) - if (setup_board == board_t::board_M5PaperDIY) { return false; } -#endif -#if defined (M5GFX_AUTODETECT_TEST_FAIL_CARDPUTER_SETUP) - if (setup_board == board_t::board_M5Cardputer - || setup_board == board_t::board_M5CardputerADV - || setup_board == board_t::board_M5VAMeter) { return false; } -#endif -#if defined (M5GFX_AUTODETECT_TEST_FAIL_AIRQ_SETUP) - if (setup_board == board_t::board_M5AirQ) { return false; } -#endif -#if defined (M5GFX_AUTODETECT_TEST_FAIL_STAMPLC_SETUP) - if (setup_board == board_t::board_M5StamPLC) { return false; } -#endif - return _adopt_detected_parts(parts.bus, parts.panel, - parts.light, parts.touch); - }, nullptr, &setup_board, transient_fallback, - no_signature, candidate_board)) + const auto package = select_detection_package(true); + board_detect::detect_request_t request; + request.hint = static_cast(board); + request.allow_reset = use_reset; + request.conditional_pins_unavailable = package.conditional_pins_unavailable; + auto policy = package.policy; + policy.reject_initial_hint = policy.hint_gate != nullptr + && static_cast(board) != request.hint; + policy.suppress_final_attempt = policy.suppress_final_attempt || !final_attempt; + const auto outcome = board_detect::run_detection_session(request, policy, + [this, &package](const board_detect::detect_request_t& attempt, bool last) { - goto init_clear; - } - } - -#elif defined (CONFIG_IDF_TARGET_ESP32P4) - - std::uint32_t pkg_ver = m5gfx::get_pkg_ver(); - ESP_LOGD(LIBRARY_NAME, "pkg_ver : %02x", (int)pkg_ver); - - if (pkg_ver == 0) // pkg_ver == EFUSE_RD_CHIP_VER_PKG_ + return run_detection_attempt(package.detectors, attempt, last, + [this](board_detect::m5::display_parts_t& parts, board_t setup_board) + { + return !reject_detected_setup(setup_board) + && _adopt_detected_parts(parts.bus, parts.panel, parts.light, parts.touch); + }); + }); + if (package.detectors != nullptr && !policy.reject_initial_hint + && board_detect::session_hint_allowed(policy, request.hint)) { - board_t setup_board = board_t::board_unknown; - if (try_setup_detected(board_detect::m5::esp32p4_detectors, - board, use_reset, final_attempt, &board, - [this, &setup_board](board_detect::m5::display_parts_t& parts) - { -#if defined (M5GFX_AUTODETECT_TEST_FAIL_COREP4X_SETUP) - if (setup_board == board_t::board_M5CoreP4X) - { - (void)parts; - return false; - } -#endif -#if defined (M5GFX_AUTODETECT_TEST_FAIL_TAB5_SETUP) - if (setup_board == board_t::board_M5Tab5 - || setup_board == board_t::board_M5Tab5X) - { - (void)parts; - return false; - } -#endif - return _adopt_detected_parts(parts.bus, parts.panel, - parts.light, parts.touch); - }, nullptr, &setup_board, transient_fallback, no_signature, - candidate_board)) - { - goto init_clear; - } - - } - -#elif defined (CONFIG_IDF_TARGET_ESP32C6) - - std::uint32_t pkg_ver = m5gfx::get_pkg_ver(); - ESP_LOGD(LIBRARY_NAME, "pkg_ver : %02x", (int)pkg_ver); - - if (pkg_ver == 1) - { // QFN32: eFuse first (StampC6), then the NanoC6 internal IR LED. - if (try_setup_detected(board_detect::m5::esp32c6_detectors_qfn32, - board, use_reset, final_attempt, &board, - [this](board_detect::m5::display_parts_t& parts) - { return _adopt_detected_parts(parts.bus, parts.panel, - parts.light, parts.touch); }, - nullptr, nullptr, transient_fallback, no_signature, - candidate_board)) - { goto init_clear; } - } else - if (pkg_ver == 0) - { // ESP32C6(QFN40) : NessoN1, UnitC6L - if (board == 0 || board == board_t::board_ArduinoNessoN1 || board == board_t::board_M5UnitC6L) - { - if (try_setup_detected(board_detect::m5::esp32c6_detectors_qfn40, - board, use_reset, final_attempt, &board, - [this](board_detect::m5::display_parts_t& parts) - { - return _adopt_detected_parts(parts.bus, parts.panel, - parts.light, parts.touch); - }, nullptr, nullptr, transient_fallback, - no_signature)) - { - goto init_clear; - } - } - } - -#elif defined (CONFIG_IDF_TARGET_ESP32C3) - - if (board == 0 || board == board_t::board_M5StampC3 - || board == board_t::board_M5StampC3U) - { - if (try_setup_detected(board_detect::m5::esp32c3_detectors, - board, use_reset, final_attempt, &board, - [this](board_detect::m5::display_parts_t& parts) - { return _adopt_detected_parts(parts.bus, parts.panel, - parts.light, parts.touch); }, - nullptr, nullptr, transient_fallback, no_signature, - candidate_board)) - { goto init_clear; } - } - -#elif defined (CONFIG_IDF_TARGET_ESP32C5) - - if (board == 0 || board == board_t::board_M5ToughC5 - || board == board_t::board_M5StampC5) - { - if (try_setup_detected(board_detect::m5::esp32c5_detectors, - board, use_reset, false, &board, - [this](board_detect::m5::display_parts_t& parts) - { -#if defined (M5GFX_AUTODETECT_TEST_FAIL_TOUGHC5_SETUP) - (void)parts; - return false; + if (no_signature != nullptr) + { *no_signature = outcome.reason == board_detect::fail_reason_t::no_signature; } + if (candidate_board != nullptr && *candidate_board == board_t::board_unknown + && outcome.result.candidate != nullptr) + { *candidate_board = static_cast(outcome.result.candidate->id); } + } + // These legacy chip entries did not export a transient output, even on success. +#if !defined(CONFIG_IDF_TARGET_ESP32C5) && !defined(CONFIG_IDF_TARGET_ESP32C61) + if (outcome.setup_succeeded && transient_fallback != nullptr) + { *transient_fallback = outcome.result.transient_fallback; } #else - return _adopt_detected_parts(parts.bus, parts.panel, - parts.light, parts.touch); -#endif - }, nullptr, nullptr, nullptr, no_signature)) - { - goto init_clear; - } - - } - -#elif defined (CONFIG_IDF_TARGET_ESP32C61) - - if (board == 0 || board == board_t::board_M5CoreMatrix) - { - if (try_setup_detected(board_detect::m5::esp32c61_detectors, - board, use_reset, false, &board, - [this](board_detect::m5::display_parts_t& parts) - { -#if defined (M5GFX_AUTODETECT_TEST_FAIL_COREMATRIX_SETUP) - (void)parts; - return false; -#else - return _adopt_detected_parts(parts.bus, parts.panel, - parts.light, parts.touch); -#endif - }, nullptr, nullptr, nullptr, no_signature)) - { - goto init_clear; - } - } - -#elif defined (CONFIG_IDF_TARGET_ESP32H2) - if (try_setup_detected(board_detect::m5::esp32h2_detectors, - board, use_reset, final_attempt, &board, - [this](board_detect::m5::display_parts_t& parts) - { return _adopt_detected_parts(parts.bus, parts.panel, - parts.light, parts.touch); }, - nullptr, nullptr, transient_fallback, no_signature)) - { goto init_clear; } + (void)transient_fallback; #endif - - board = board_t::board_unknown; - goto init_clear; -init_clear: - panel(_panel_last.get()); - - return board; + return outcome.setup_succeeded ? static_cast(outcome.result.def->id) + : board_t::board_unknown; } #else diff --git a/src/board_detect/board_detect.hpp b/src/board_detect/board_detect.hpp index 4f46c8bb..93326c8b 100644 --- a/src/board_detect/board_detect.hpp +++ b/src/board_detect/board_detect.hpp @@ -9,88 +9,13 @@ #include "../lgfx/v1/platforms/esp32/common.hpp" #include "dedicated_release_probe.hpp" #include "ops.hpp" +#include "detect_types.hpp" #include "detect_class.hpp" namespace m5gfx { namespace board_detect { - using board_id_t = std::uint16_t; - static constexpr board_id_t board_id_unknown = 0; - - enum board_def_flag_t : std::uint8_t - { - // A board with no positive signature. Such members cannot promote their - // detector through an NVS hint; a detector may expose them as candidates. - def_flag_fallback = 1 << 0, - }; - - struct board_def_t - { - board_id_t id; - const char* name; - std::uint8_t flags; - }; - -#if __cplusplus >= 201703L - inline const board_def_t board_def_unknown = { board_id_unknown, "unknown", 0 }; -#else - // Arduino-ESP32 2.x still compiles as C++11. Internal linkage provides the - // same ODR safety there; C++17 and newer use the single inline definition. - static const board_def_t board_def_unknown = { board_id_unknown, "unknown", 0 }; -#endif - - enum prepared_state_t : std::uint32_t - { - // Bits 0..15 are manufacturer-independent completed operations or probe - // side effects. Bits 16..31 are reserved for board-specific state. - prepared_power = 1u << 0, - prepared_reset = 1u << 1, - prepared_sd_spi = 1u << 2, - // The GPIO reset line has been driven inactive, but a reset pulse may not - // have been allowed. Keep this distinct from prepared_reset so a later - // reset-enabled prepare can still pulse the panel reset. - prepared_reset_line = 1u << 4, - // A confirmed family's post-power refinement has been resolved (run, or - // deliberately skipped after a failed retained power sequence). - prepared_refine = 1u << 5, - // The confirmed board was retained after its power operation list stopped. - prepared_power_failed = 1u << 6, - }; - - enum class detect_status_t : std::uint8_t - { - no_match, - matched, - excluded, - }; - - struct board_desc_t; - struct prepare_ctx_t; - class detection_transaction_t; - struct board_result_t; - using refine_fn_t = bool (*)(board_result_t&, const prepare_ctx_t&); - - struct board_result_t - { - // A matched/direct result owns no description; this points at the static - // canonical description and def aliases &desc->def. Unknown results have - // desc == nullptr and retain board_def_unknown for compatibility. - const board_desc_t* desc = nullptr; - const board_def_t* def = &board_def_unknown; - std::uint32_t option = 0; - std::uint32_t prepared = 0; - // A final-attempt family default can show the display, but is not an NVS hint. - bool transient_fallback = false; - // A weak, displayless suggestion returned only when no board is confirmed. - const board_def_t* candidate = nullptr; - detect_status_t status = detect_status_t::no_match; - // Optional read-only member refinement after power preparation. - refine_fn_t refine = nullptr; - - void assign(const board_desc_t* value); - }; - struct prepare_ctx_t { // A retry must not override the caller's reset policy. diff --git a/src/board_detect/detect_session.hpp b/src/board_detect/detect_session.hpp new file mode 100644 index 00000000..896d443c --- /dev/null +++ b/src/board_detect/detect_session.hpp @@ -0,0 +1,71 @@ +// Copyright (c) M5Stack. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +#pragma once + +#include "detect_types.hpp" + +namespace m5gfx +{ +namespace board_detect +{ + // Preserve the chip-specific hint and last-attempt policies until S1 aligns + // them. The gate is a zero-terminated allow-list; an unknown hint is allowed. + struct detect_session_policy_t + { + const board_id_t* hint_gate = nullptr; + bool keep_initial_hint = false; + bool suppress_final_attempt = false; + // Keep a legacy gate rejection before a wider NVS hint is narrowed. + bool reject_initial_hint = false; + }; + + __attribute__((always_inline)) inline bool session_hint_allowed(const detect_session_policy_t& policy, board_id_t hint) + { + if (hint == board_id_unknown || policy.hint_gate == nullptr) { return true; } + for (auto p = policy.hint_gate; *p != board_id_unknown; ++p) + { + if (*p == hint) { return true; } + } + return false; + } + + // RunAttempt owns each GPIO/bus transaction and the prepare/construct/adopt + // steps. Session state keeps the first weak candidate across failed attempts. + // Keep constant chip policies visible to the caller under size optimization. + template + __attribute__((always_inline)) inline detect_outcome_t run_detection_session(detect_request_t request, + const detect_session_policy_t& policy, + RunAttempt run_attempt) + { + detect_outcome_t outcome; + const auto initial_hint = request.hint; + const board_def_t* candidate = nullptr; + std::uint8_t attempts = 0; + for (unsigned attempt = request.attempt; attempt < request.max_attempts; ++attempt) + { + request.attempt = static_cast(attempt); + // The legacy loop promotes reset permission on its penultimate attempt. + if (attempt + 2 == request.max_attempts) { request.allow_reset = true; } + request.hint = policy.keep_initial_hint ? initial_hint : request.hint; + const bool final_attempt = !policy.suppress_final_attempt + && attempt + 1 == request.max_attempts; + outcome = {}; + if ((!policy.reject_initial_hint || attempts != 0) + && session_hint_allowed(policy, request.hint)) + { + outcome = run_attempt(request, final_attempt); + } + outcome.attempts = ++attempts; + if (candidate == nullptr) { candidate = outcome.result.candidate; } + if (outcome.setup_succeeded) { return outcome; } + outcome.result.candidate = candidate; + if (outcome.verdict == verdict_t::unknown && candidate != nullptr) + { outcome.verdict = verdict_t::candidate; } + if (outcome.reason == fail_reason_t::no_signature) { return outcome; } + // Non-ESP32 legacy callers feed the previous unknown board into retries. + request.hint = board_id_unknown; + } + return outcome; + } +} +} diff --git a/src/board_detect/detect_types.hpp b/src/board_detect/detect_types.hpp new file mode 100644 index 00000000..debf20d7 --- /dev/null +++ b/src/board_detect/detect_types.hpp @@ -0,0 +1,115 @@ +// Copyright (c) M5Stack. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +#pragma once + +#include + +namespace m5gfx +{ +namespace board_detect +{ + using board_id_t = std::uint16_t; + static constexpr board_id_t board_id_unknown = 0; + + enum board_def_flag_t : std::uint8_t + { + // A board with no positive signature. Such members cannot promote their + // detector through an NVS hint; a detector may expose them as candidates. + def_flag_fallback = 1 << 0, + }; + + struct board_def_t + { + board_id_t id; + const char* name; + std::uint8_t flags; + }; + +#if __cplusplus >= 201703L + inline const board_def_t board_def_unknown = { board_id_unknown, "unknown", 0 }; +#else + // Arduino-ESP32 2.x still compiles as C++11. Internal linkage provides the + // same ODR safety there; C++17 and newer use the single inline definition. + static const board_def_t board_def_unknown = { board_id_unknown, "unknown", 0 }; +#endif + + enum prepared_state_t : std::uint32_t + { + // Bits 0..15 are manufacturer-independent completed operations or probe + // side effects. Bits 16..31 are reserved for board-specific state. + prepared_power = 1u << 0, + prepared_reset = 1u << 1, + prepared_sd_spi = 1u << 2, + // The GPIO reset line has been driven inactive, but a reset pulse may not + // have been allowed. Keep this distinct from prepared_reset so a later + // reset-enabled prepare can still pulse the panel reset. + prepared_reset_line = 1u << 4, + // A confirmed family's post-power refinement has been resolved (run, or + // deliberately skipped after a failed retained power sequence). + prepared_refine = 1u << 5, + // The confirmed board was retained after its power operation list stopped. + prepared_power_failed = 1u << 6, + }; + + enum class detect_status_t : std::uint8_t + { + no_match, + matched, + excluded, + }; + + struct board_desc_t; + struct prepare_ctx_t; + class detection_transaction_t; + struct board_result_t; + using refine_fn_t = bool (*)(board_result_t&, const prepare_ctx_t&); + + struct board_result_t + { + // A matched/direct result owns no description; this points at the static + // canonical description and def aliases &desc->def. Unknown results have + // desc == nullptr and retain board_def_unknown for compatibility. + const board_desc_t* desc = nullptr; + const board_def_t* def = &board_def_unknown; + std::uint32_t option = 0; + std::uint32_t prepared = 0; + // A final-attempt family default can show the display, but is not an NVS hint. + bool transient_fallback = false; + // A weak, displayless suggestion returned only when no board is confirmed. + const board_def_t* candidate = nullptr; + detect_status_t status = detect_status_t::no_match; + // Optional read-only member refinement after power preparation. + refine_fn_t refine = nullptr; + + void assign(const board_desc_t* value); + }; + + enum class verdict_t : std::uint8_t { unknown, candidate, confirmed }; + enum class fail_reason_t : std::uint8_t + { + none, no_signature, confirm_transient, confirm_definitive, excluded_by_set, + family_unresolved, prepare_failed, construct_failed, adopt_failed, + }; + + struct detect_request_t + { + const board_id_t* target_set = nullptr; + board_id_t hint = board_id_unknown; + bool allow_reset = false; + std::uint8_t attempt = 0; + std::uint8_t max_attempts = 1; + bool conditional_pins_unavailable = false; + }; + + struct detect_outcome_t + { + verdict_t verdict = verdict_t::unknown; + board_result_t result; + fail_reason_t reason = fail_reason_t::none; + std::uint8_t attempts = 0; + // Identification and successful adoption remain separate: legacy init + // retries a confirmed family when preparation or display adoption fails. + bool setup_succeeded = false; + }; +} +} diff --git a/src/board_detect/m5/generated/esp32_d0wdq6_detector_order.hpp b/src/board_detect/m5/generated/esp32_d0wdq6_detector_order.hpp index 1b29c740..6ed277d4 100644 --- a/src/board_detect/m5/generated/esp32_d0wdq6_detector_order.hpp +++ b/src/board_detect/m5/generated/esp32_d0wdq6_detector_order.hpp @@ -8,4 +8,6 @@ static const board_detector_t* const esp32_d0wdq6_detectors[] = { &timercam_detector, nullptr, }; +static_assert(sizeof(esp32_d0wdq6_detectors) / sizeof(esp32_d0wdq6_detectors[0]) - 1 <= max_detector_families, + "esp32_d0wdq6_detectors exceeds the detection session family limit"); diff --git a/src/board_detect/m5/generated/esp32_pico_detector_order.hpp b/src/board_detect/m5/generated/esp32_pico_detector_order.hpp index 870e02d6..4900c905 100644 --- a/src/board_detect/m5/generated/esp32_pico_detector_order.hpp +++ b/src/board_detect/m5/generated/esp32_pico_detector_order.hpp @@ -7,9 +7,13 @@ static const board_detector_t* const esp32_pico_d4_detectors[] = { &atom_family_detector, nullptr, }; +static_assert(sizeof(esp32_pico_d4_detectors) / sizeof(esp32_pico_d4_detectors[0]) - 1 <= max_detector_families, + "esp32_pico_d4_detectors exceeds the detection session family limit"); static const board_detector_t* const esp32_picov3_detectors[] = { &stickcplus2_detector, nullptr, }; +static_assert(sizeof(esp32_picov3_detectors) / sizeof(esp32_picov3_detectors[0]) - 1 <= max_detector_families, + "esp32_picov3_detectors exceeds the detection session family limit"); diff --git a/src/board_detect/m5/generated/esp32c3_detector_order.hpp b/src/board_detect/m5/generated/esp32c3_detector_order.hpp index 957d7484..8f80d7b4 100644 --- a/src/board_detect/m5/generated/esp32c3_detector_order.hpp +++ b/src/board_detect/m5/generated/esp32c3_detector_order.hpp @@ -5,4 +5,6 @@ static const board_detector_t* const esp32c3_detectors[] = { &c3_family_detector, nullptr, }; +static_assert(sizeof(esp32c3_detectors) / sizeof(esp32c3_detectors[0]) - 1 <= max_detector_families, + "esp32c3_detectors exceeds the detection session family limit"); diff --git a/src/board_detect/m5/generated/esp32c5_detector_order.hpp b/src/board_detect/m5/generated/esp32c5_detector_order.hpp index 198c1668..880334fd 100644 --- a/src/board_detect/m5/generated/esp32c5_detector_order.hpp +++ b/src/board_detect/m5/generated/esp32c5_detector_order.hpp @@ -6,4 +6,6 @@ static const board_detector_t* const esp32c5_detectors[] = { &toughc5_detector, nullptr, }; +static_assert(sizeof(esp32c5_detectors) / sizeof(esp32c5_detectors[0]) - 1 <= max_detector_families, + "esp32c5_detectors exceeds the detection session family limit"); diff --git a/src/board_detect/m5/generated/esp32c61_detector_order.hpp b/src/board_detect/m5/generated/esp32c61_detector_order.hpp index 2b4d8929..2adcf0c8 100644 --- a/src/board_detect/m5/generated/esp32c61_detector_order.hpp +++ b/src/board_detect/m5/generated/esp32c61_detector_order.hpp @@ -5,4 +5,6 @@ static const board_detector_t* const esp32c61_detectors[] = { &corematrix_detector, nullptr, }; +static_assert(sizeof(esp32c61_detectors) / sizeof(esp32c61_detectors[0]) - 1 <= max_detector_families, + "esp32c61_detectors exceeds the detection session family limit"); diff --git a/src/board_detect/m5/generated/esp32c6_detector_order.hpp b/src/board_detect/m5/generated/esp32c6_detector_order.hpp index d0257a79..c4715660 100644 --- a/src/board_detect/m5/generated/esp32c6_detector_order.hpp +++ b/src/board_detect/m5/generated/esp32c6_detector_order.hpp @@ -5,10 +5,14 @@ static const board_detector_t* const esp32c6_detectors_qfn40[] = { &c6_display_family_detector, nullptr, }; +static_assert(sizeof(esp32c6_detectors_qfn40) / sizeof(esp32c6_detectors_qfn40[0]) - 1 <= max_detector_families, + "esp32c6_detectors_qfn40 exceeds the detection session family limit"); static const board_detector_t* const esp32c6_detectors_qfn32[] = { &stampc6_detector, &nanoc6_detector, nullptr, }; +static_assert(sizeof(esp32c6_detectors_qfn32) / sizeof(esp32c6_detectors_qfn32[0]) - 1 <= max_detector_families, + "esp32c6_detectors_qfn32 exceeds the detection session family limit"); diff --git a/src/board_detect/m5/generated/esp32h2_detector_order.hpp b/src/board_detect/m5/generated/esp32h2_detector_order.hpp index 4899e030..c70395fd 100644 --- a/src/board_detect/m5/generated/esp32h2_detector_order.hpp +++ b/src/board_detect/m5/generated/esp32h2_detector_order.hpp @@ -5,4 +5,6 @@ static const board_detector_t* const esp32h2_detectors[] = { &nanoh2_detector, nullptr, }; +static_assert(sizeof(esp32h2_detectors) / sizeof(esp32h2_detectors[0]) - 1 <= max_detector_families, + "esp32h2_detectors exceeds the detection session family limit"); diff --git a/src/board_detect/m5/generated/esp32p4_detector_order.hpp b/src/board_detect/m5/generated/esp32p4_detector_order.hpp index c3dc18f2..cfa0e846 100644 --- a/src/board_detect/m5/generated/esp32p4_detector_order.hpp +++ b/src/board_detect/m5/generated/esp32p4_detector_order.hpp @@ -8,4 +8,6 @@ static const board_detector_t* const esp32p4_detectors[] = { &stampp4_detector, nullptr, }; +static_assert(sizeof(esp32p4_detectors) / sizeof(esp32p4_detectors[0]) - 1 <= max_detector_families, + "esp32p4_detectors exceeds the detection session family limit"); diff --git a/src/board_detect/m5/generated/esp32s3_detector_order.hpp b/src/board_detect/m5/generated/esp32s3_detector_order.hpp index d9586525..7e3fd569 100644 --- a/src/board_detect/m5/generated/esp32s3_detector_order.hpp +++ b/src/board_detect/m5/generated/esp32s3_detector_order.hpp @@ -16,6 +16,8 @@ static const board_detector_t* const esp32s3_detectors_qfn56[] = { &dualkey_detector, nullptr, }; +static_assert(sizeof(esp32s3_detectors_qfn56) / sizeof(esp32s3_detectors_qfn56[0]) - 1 <= max_detector_families, + "esp32s3_detectors_qfn56 exceeds the detection session family limit"); static const board_detector_t* const esp32s3_detectors_lga56[] = { &atomvoices3r_detector, @@ -23,4 +25,6 @@ static const board_detector_t* const esp32s3_detectors_lga56[] = { &pmic_id_detector, nullptr, }; +static_assert(sizeof(esp32s3_detectors_lga56) / sizeof(esp32s3_detectors_lga56[0]) - 1 <= max_detector_families, + "esp32s3_detectors_lga56 exceeds the detection session family limit"); From a39343c6b9f2d60aecc4082f78f993f6b2fc6008 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:23:04 +0000 Subject: [PATCH 02/10] Report provisional and weak candidates and keep detection hints Classify results after preparation as confirmed, provisional (family identified, member unresolved; display built) or weak (suggestion only; no display). Only confirmed adoption may write NVS, and only when the value differs; unknown and candidate results leave the stored hint intact. Keep the hint for every attempt and use it only for ordering and provisional member selection. Restrict hinted-member probing to the first non-final attempt so retries recover other members. Remove chip-specific hint gates and final-attempt policies; the final attempt re-measures the same gates. Add setDetectConfig()/detect_config_t to receive the application fallback board before init. A provisional member that differs from it is not constructed; the candidate is retained and the fallback wins on the final attempt. Select unresolved Core2/Tough and Cardputer members by fallback, then hint, then representative, excluding Tough when AXP2101 is observed. Keep PaperMono when PM1 member evidence is unreadable. Mark the Stack signature bypass provisional and report unreadable UnitC6L identification as a weak candidate without replacing an earlier one. Retain directly adopted panels by setup success rather than confidence, keep candidate metadata aligned after setup failures, and warn about configuration changes after init only when the value changes. Add member-suppression injection and host tests for the result projection. --- extras/board_spec/test/board_spec.test.js | 4 +- extras/board_spec/test/detect_attempt.test.js | 197 +++++++++++++ .../board_spec/test/detect_families.test.js | 255 +++++++++++++++++ extras/board_spec/test/detect_host_helpers.js | 26 ++ extras/board_spec/test/detect_session.test.js | 35 +-- .../board_spec/test/detect_session_host.cpp | 269 ++++++++---------- src/M5GFX.cpp | 114 ++++---- src/M5GFX.h | 9 + src/board_detect/board_detect.hpp | 2 + src/board_detect/board_detect.inl | 18 +- src/board_detect/detect_session.hpp | 79 ++--- src/board_detect/detect_types.hpp | 7 +- src/board_detect/m5/esp32_d0wdq6.inl | 19 +- src/board_detect/m5/esp32_pico.inl | 8 +- src/board_detect/m5/esp32c6/c6_display.inl | 10 +- src/board_detect/m5/esp32p4/tab5.inl | 3 +- src/board_detect/m5/esp32s3/families.inl | 39 ++- 17 files changed, 776 insertions(+), 318 deletions(-) create mode 100644 extras/board_spec/test/detect_attempt.test.js create mode 100644 extras/board_spec/test/detect_families.test.js create mode 100644 extras/board_spec/test/detect_host_helpers.js diff --git a/extras/board_spec/test/board_spec.test.js b/extras/board_spec/test/board_spec.test.js index 2196824b..0e0df1c9 100644 --- a/extras/board_spec/test/board_spec.test.js +++ b/extras/board_spec/test/board_spec.test.js @@ -599,7 +599,7 @@ test("generated ESP32 wiring preserves the replaced board values", async () => { test("all chip entries pass reset and hint policy through the common session", async () => { const main = await fs.readFile(path.join(root, "../../src/M5GFX.cpp"), "utf8"); assert.match(main, /request.allow_reset = use_reset;[\s\S]*?request.max_attempts = 5;[\s\S]*?run_detection_session/); - assert.match(main, /package.policy.keep_initial_hint = true;/); + assert.doesNotMatch(main, /package\.policy/); assert.match(main, /probe.allow_reset = request.allow_reset;/); }); @@ -1295,7 +1295,7 @@ test("ESP32-C6 catalogs and detector preserve both display boards", async () => assert.match(esp32c6Source, /probe_i2c_bus_present\(ctx, c6_display_detail::sda,\s*c6_display_detail::scl\)[\s\S]*?probe_pin_pulls\(ctx, c6_display_detail::signature_bit\)/); assert.match(esp32c6Source, - /specs::unitc6l::i2c_ioe::id_reg[\s\S]*?!is_pi4io\(value\)[\s\S]*?ctx\.final_attempt[\s\S]*?transient_fallback = true/); + /specs::unitc6l::i2c_ioe::id_reg[\s\S]*?!is_pi4io\(value\)[\s\S]*?ctx\.candidate == nullptr[\s\S]*?ctx\.candidate = &desc_unitc6l.def;[\s\S]*?return false/); const c6Main = await fs.readFile(path.join(root, "../../src/M5GFX.cpp"), "utf8"); assert.match(c6Main, /package.detectors = board_detect::m5::esp32c6_detectors_qfn40;/); diff --git a/extras/board_spec/test/detect_attempt.test.js b/extras/board_spec/test/detect_attempt.test.js new file mode 100644 index 00000000..604dff97 --- /dev/null +++ b/extras/board_spec/test/detect_attempt.test.js @@ -0,0 +1,197 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +const here = path.dirname(fileURLToPath(import.meta.url)); +const src = path.resolve(here, "../../../src"); +import { body, compileRun } from "./detect_host_helpers.js"; + +test("production attempt yields before construction and preserves adoption and rollback semantics", async () => { + const main = await fs.readFile(path.join(src, "M5GFX.cpp"), "utf8"); + const attempt = body(main, "static board_detect::detect_outcome_t run_detection_attempt("); + const compat = body(main, "board_t M5GFX::autodetect("); + const projection = main.slice(main.indexOf(" const auto board = outcome.setup_succeeded"), main.indexOf("#if defined ( ARDUINO_M5STACK_ATOM )", main.indexOf(" const auto board = outcome.setup_succeeded"))); + const header = await fs.readFile(path.join(src, "M5GFX.h"), "utf8"); + const candidateGetter = body(header, "board_t getBoardCandidate("); + await compileRun(` +#include "board_detect/detect_session.hpp" +#include +#include +#define CONFIG_IDF_TARGET_ESP32C6 1 +#define ESP_LOGW(...) ((void)0) +#define ESP_LOGI(...) ((void)0) +using namespace m5gfx; +enum class board_t : std::uint32_t {board_unknown=0,member=1}; +int rollback_count, commit_count, construct_count, adopt_count, reset_count; +bool power_failed, prepare_ok, adopt_ok, construct_ok, no_display; +namespace lgfx { namespace i2c { bool isInitialized(int) { return false; } } } +namespace m5gfx { namespace board_detect { +struct board_desc_t { board_def_t def; struct { int hw_port; } internal_i2c; struct { const int* data; int size; } hold_high_pins; }; +void board_result_t::assign(const board_desc_t* d) { desc=d; def=&d->def; } +const board_desc_t* refined_member=nullptr; +struct board_detector_t {}; +struct prepare_ctx_t { bool allow_reset; board_id_t preferred; unsigned attempt; bool final_attempt; int i2c_port_probe; detection_transaction_t* transaction; const board_id_t* enabled_ids; }; +struct probe_ctx_t : prepare_ctx_t { bool confirm_attempted; }; +inline int pins(int) {return 0;} inline int no_pins() {return 0;} +struct detection_transaction_t { + detection_transaction_t(int,int,bool) {} bool valid() {return true;} + void rollback() {++rollback_count;} void commit() {++commit_count;} + void restore_start(const int*, int) {++reset_count;} + struct buses_t {void opened_i2c(int) {}} buses_; + buses_t& buses() {return buses_;} +}; +board_result_t detected; +board_result_t detect_board(const board_detector_t* const*, board_id_t, probe_ctx_t& p) {p.confirm_attempted=true; return detected;} +bool prepare(const board_desc_t&, board_result_t& r, const prepare_ctx_t&) {if(power_failed) {r.provisional=true;} else if(refined_member) {r.assign(refined_member);} + return prepare_ok;} +namespace m5 { +namespace wiring { namespace detection { const int unconditional_pins=0; } } +struct display_parts_t {int* bus=nullptr;int* panel=nullptr;int* light=nullptr;int* touch=nullptr;}; +enum class construct_status_t {failed, no_display, success}; +construct_status_t setup_detected_board(const board_result_t&, display_parts_t*) {++construct_count; return !construct_ok ? construct_status_t::failed : no_display ? construct_status_t::no_display : construct_status_t::success;} +void destroy_display_parts(display_parts_t*) {} +struct log_t {const char* name; const char* annotation;}; +log_t success_log(const board_result_t&) {return {nullptr,nullptr};} +} +} } +const int probe_i2c_port=0; +template board_detect::detect_outcome_t run_detection_attempt(const board_detect::board_detector_t* const* detectors, const board_detect::detect_request_t& request, bool final_attempt, SetupDetected setup) ${attempt} +struct package_t {const board_detect::board_detector_t* const* detectors;bool conditional_pins_unavailable=false;}; +package_t active_package; +package_t select_detection_package(bool) {return active_package;} +bool reject_detected_setup(board_t) {return false;} +struct holder_t {int* get() {return nullptr;}}; +struct M5GFX { + board_t _board=board_t::board_unknown,_board_candidate=board_t::board_unknown; + board_t getBoard() const {return _board;} + board_t getBoardCandidate() const ${candidateGetter} + void project(const board_detect::detect_outcome_t& outcome) { ${projection} } + struct {board_t fallback_board=board_t::board_unknown;} _detect_config; + holder_t _panel_last; + void panel(int*) {} + bool _adopt_detected_parts(int*,int*,int*,int*) {return true;} + board_t autodetect(bool use_reset,board_t board,bool final_attempt,bool* transient_fallback,bool* no_signature,board_t* candidate_board) ${compat} +}; +int main() { + using namespace board_detect; + const board_desc_t desc={{1,"member",0},{-1},{nullptr,0}}; + const board_detector_t d; const board_detector_t* list[]={&d,nullptr}; + for(int provisional=0; provisional<2; ++provisional) + for(int preferred=0; preferred<3; ++preferred) + for(int failure=0; failure<4; ++failure) + for(int displayless=0; displayless<2; ++displayless) { + rollback_count=commit_count=construct_count=adopt_count=reset_count=0; + detected={}; detected.assign(&desc); detected.status=detect_status_t::matched; + power_failed=provisional; prepare_ok=failure!=1; construct_ok=failure!=2; adopt_ok=failure!=3; no_display=displayless; + detect_request_t req; req.preferred=preferred; req.max_attempts=5; + auto out=run_detection_session(req,[&](const detect_request_t& r,bool final) { + return run_detection_attempt(list,r,final,[](m5::display_parts_t&,board_t) {++adopt_count; return adopt_ok;}); + }); + const bool yields=provisional && preferred==2 && prepare_ok; + const bool success=failure==0 && !yields; + M5GFX standalone;standalone.project(out); + assert(standalone.getBoard()==(success?board_t::member:board_t::board_unknown)); + assert(standalone.getBoardCandidate()==(provisional&&!success?board_t::member:board_t::board_unknown)); + assert(out.setup_succeeded==success); + assert(out.verdict==(provisional ? verdict_t::candidate : verdict_t::confirmed)); + assert(out.candidate_kind==(provisional ? candidate_kind_t::provisional : candidate_kind_t::none)); + assert(out.attempts==(success ? 1:5)); + assert(construct_count==(failure==1||yields ? 0:out.attempts)); + assert(adopt_count==(failure==1||failure==2||yields ? 0:out.attempts)); + assert(commit_count==int(success)); assert(rollback_count==(success ? 0:out.attempts)); + assert(!yields || out.result.candidate==&desc.def); + assert(should_persist_detection(out,0)==(success&&!provisional)); + } + // A retained power failure retries without construction, then refinement can select the preference. + const board_desc_t preferred_desc={{2,"preferred",0},{-1},{nullptr,0}}; + rollback_count=commit_count=construct_count=adopt_count=reset_count=0; + detected={};detected.assign(&desc);detected.status=detect_status_t::matched; + prepare_ok=construct_ok=adopt_ok=true;no_display=false;refined_member=&preferred_desc; + detect_request_t recovery;recovery.preferred=2;recovery.max_attempts=5; + auto recovered=run_detection_session(recovery,[&](const detect_request_t& r,bool final) { + power_failed=r.attempt==0; + return run_detection_attempt(list,r,final,[](m5::display_parts_t&,board_t){++adopt_count;return true;}); + }); + assert(recovered.attempts==2&&recovered.setup_succeeded&&recovered.result.def==&preferred_desc.def); + assert(recovered.verdict==verdict_t::confirmed&&recovered.candidate_kind==candidate_kind_t::none); + assert(construct_count==1&&adopt_count==1&&rollback_count==1&&commit_count==1); + // The final refined provisional member replaces the initial retained representative. + rollback_count=commit_count=construct_count=adopt_count=reset_count=0; + recovery.preferred=3;recovery.hint=2; + auto yielded=run_detection_session(recovery,[&](const detect_request_t& r,bool final) { + detected={};detected.assign(&desc);detected.status=detect_status_t::matched; + power_failed=r.attempt==0;prepare_ok=r.attempt==0||final; + detected.provisional=final;refined_member=final?&preferred_desc:nullptr; + return run_detection_attempt(list,r,final,[](m5::display_parts_t&,board_t){++adopt_count;return true;}); + }); + assert(yielded.attempts==5&&!yielded.setup_succeeded&&yielded.result.candidate==&preferred_desc.def); + assert(yielded.candidate_kind==candidate_kind_t::provisional&&yielded.verdict==verdict_t::candidate); + M5GFX projected;projected.project(yielded); + assert(projected.getBoard()==board_t::board_unknown&&projected.getBoardCandidate()==static_cast(2)); + assert(construct_count==0&&adopt_count==0&&rollback_count==5&&commit_count==0); + assert(!should_persist_detection(yielded,0)); + refined_member=nullptr; + // The protected compatibility wrapper must deliver QFN40 weak candidates. + detected={};detected.candidate=&desc.def;detected.status=detect_status_t::no_match; + active_package.detectors=list;M5GFX gfx;board_t candidate=board_t::board_unknown; + bool transient=false,no_signature=false; + assert(gfx.autodetect(false,board_t::board_unknown,true,&transient,&no_signature,&candidate)==board_t::board_unknown); + assert(candidate==board_t::member);assert(!transient); +} +`, "attempt"); +}); + +test("public config stores the next preference and warns after either successful or failed init", async () => { + const main=await fs.readFile(path.join(src,"M5GFX.cpp"),"utf8"); + const header=await fs.readFile(path.join(src,"M5GFX.h"),"utf8"); + const setter=body(main,"void M5GFX::setDetectConfig("); + const config=body(header,"struct detect_config_t"); + const getter=body(header,"const detect_config_t& getDetectConfig("); + await compileRun(` +#include +#define ESP_PLATFORM 1 +int warnings=0; +#define ESP_LOGW(...) (++warnings) +enum class board_t {board_unknown,member,other}; +struct detect_config_t ${config}; +struct M5GFX {detect_config_t _detect_config;bool _detect_started=false;board_t adopted=board_t::board_unknown; + void setDetectConfig(const detect_config_t& config) ${setter} + const detect_config_t& getDetectConfig() const ${getter} +}; +int main(){M5GFX gfx;detect_config_t cfg;assert(gfx.getDetectConfig().fallback_board==board_t::board_unknown); + cfg.fallback_board=board_t::member;gfx.setDetectConfig(cfg);assert(warnings==0); + gfx._detect_started=true;gfx.setDetectConfig(cfg);assert(warnings==0);assert(gfx.adopted==board_t::board_unknown); + gfx.adopted=board_t::member;cfg.fallback_board=board_t::other;gfx.setDetectConfig(cfg); + assert(warnings==1);assert(gfx.adopted==board_t::member);assert(gfx.getDetectConfig().fallback_board==board_t::other); + cfg.fallback_board=board_t::board_unknown;gfx.setDetectConfig(cfg);assert(gfx.getDetectConfig().fallback_board==board_t::board_unknown);assert(warnings==2); +} +`,"public config"); +}); + +test("production init restores panel according to adoption independently of verdict", async () => { + const main = await fs.readFile(path.join(src, "M5GFX.cpp"), "utf8"); + const start = main.indexOf(" if (!package.direct_setup ||"); + assert.ok(start >= 0); + const restore = main.slice(start, main.indexOf(" return result;", start)); + await compileRun([ + '#include "board_detect/detect_types.hpp"', + '#include ', + 'using namespace m5gfx;', + 'int calls, clears;', + 'void panel(int* p) {++calls; if (!p) ++clears;}', + 'struct holder_t {int value; int* get() {return &value;}} _panel_last;', + 'void restore(bool direct, const board_detect::detect_outcome_t& result) {', + 'struct {bool direct_setup;} package={direct};', + restore, + '}', + 'int main() {for(int verdict=0;verdict<3;++verdict) for(int adopted=0;adopted<2;++adopted) for(int direct=0;direct<2;++direct) {', + 'calls=clears=0;board_detect::detect_outcome_t result;', + 'result.verdict=static_cast(verdict);result.setup_succeeded=adopted;', + 'restore(direct,result);', + 'assert(calls==(!direct?1:adopted?0:2));assert(clears==(direct&&!adopted));', + '}}', + ].join("\n"), "panel restoration"); +}); diff --git a/extras/board_spec/test/detect_families.test.js b/extras/board_spec/test/detect_families.test.js new file mode 100644 index 00000000..e7fbe055 --- /dev/null +++ b/extras/board_spec/test/detect_families.test.js @@ -0,0 +1,255 @@ +import { promises as fs } from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { body, compileRun } from "./detect_host_helpers.js"; +const here = path.dirname(fileURLToPath(import.meta.url)); +const src = path.resolve(here, "../../../src/board_detect"); +const common = ` +#include "board_detect/detect_session.hpp" +#include +#include +#define ESP_LOGW(...) ((void)0) +#define ESP_LOGD(...) ((void)0) +#define ESP_LOGI(...) ((void)0) +using namespace m5gfx::board_detect; +namespace m5gfx { namespace board_detect { +struct board_desc_t {board_def_t def;}; +void board_result_t::assign(const board_desc_t* d) {desc=d; def=&d->def;} +struct prepare_ctx_t {board_id_t preferred=0,hint=0; unsigned attempt=0; bool final_attempt=false;}; +struct probe_ctx_t : prepare_ctx_t {const board_def_t* candidate=nullptr; bool conditional_pins_unavailable=false;}; +} } +const board_desc_t desc_core2={{1,"Core2",0}},desc_tough={{2,"Tough",0}},desc_cardputer={{3,"Cardputer",0}},desc_cardputer_adv={{4,"ADV",0}},desc_vameter={{5,"VAMeter",0}},desc_unitc6l={{6,"UnitC6L",0}},desc_nesson1={{7,"NessoN1",0}},desc_papers3={{8,"PaperS3",0}},desc_paperdiy={{9,"PaperDIY",0}}; +`; + +test("production Core2 and Cardputer selectors cover preference, hint and observed compatibility", async () => { + const detector = await fs.readFile(path.join(src, "board_detect.inl"),"utf8"); + const core = await fs.readFile(path.join(src,"m5/esp32_d0wdq6.inl"),"utf8"); + const families = await fs.readFile(path.join(src,"m5/esp32s3/families.inl"),"utf8"); + const helper = body(detector,"static bool select_provisional_member"); + const begin=core.indexOf('const auto possible = [&](board_id_t id)'), end=core.indexOf('\n log_panel_variant',begin); + const selectCore=core.slice(begin, end).replace(/\n }\s*$/, ""); + const card=body(families.slice(families.indexOf('class cardputer_family_detector_t')),"bool confirm("); + await compileRun(common+` +namespace wiring { +namespace cardputer { namespace cardputer_subdivision {constexpr int sense_pins[]={0,1,2,3}; constexpr int vameter_i2c_sda=0,vameter_i2c_scl=1; constexpr int vameter_i2c_addrs[]={0x40,0x41};} } +namespace cardputer_adv {constexpr int internal_i2c_sda=2,internal_i2c_scl=3;} +} +namespace specs {namespace cardputer {constexpr bool bus_three_wire=true;}} +const int cardputer_probes[]={1}; +struct pulls_t {std::uint64_t pulldown_high;}; +std::uint64_t observed; +bool probe_spi_id(probe_ctx_t&,const board_desc_t& d,const int*,int,board_result_t* r,bool) {r->assign(&d); return true;} +pulls_t probe_pin_pulls(probe_ctx_t&,std::uint64_t) {return {observed};} +pulls_t recover_held_sda_and_resample(probe_ctx_t&,pulls_t p,std::uint64_t,int,int) {return p;} +bool probe_i2c_read(probe_ctx_t&,int,int,int,int,std::uint8_t*,int,int,int,bool) {return false;} +bool probe_i2c_ack(probe_ctx_t&,int,int,int) {return false;} +bool select_provisional_member(const prepare_ctx_t& ctx,board_result_t* result,const board_desc_t* preferred_if_possible,const board_desc_t* hinted_if_possible,const board_desc_t* family_default,const char* why) ${helper} +namespace generated_options {namespace core2 {constexpr unsigned new_pmic=1;}} +struct tx_t {void restore_start(int) {}}; +bool core_select(prepare_ctx_t ctx,board_result_t& result) {tx_t tx; struct wrapper: prepare_ctx_t {tx_t* transaction;}; wrapper wrapped; static_cast(wrapped)=ctx; wrapped.transaction=&tx; const auto signals=0; + ${selectCore.replaceAll('ctx.transaction','wrapped.transaction')} + return true; +} +bool card_select(probe_ctx_t& ctx,board_result_t* result) ${card} +int main() { + for(bool new_pmic: {false,true}) for(bool hint_present: {false,true}) for(board_id_t preferred: {board_id_t(0),board_id_t(1),board_id_t(2),board_id_t(42)}) { + prepare_ctx_t ctx;ctx.preferred=preferred;ctx.hint=hint_present ? 2:0;board_result_t r;r.option=new_pmic?generated_options::core2::new_pmic:0; + ctx.final_attempt=false;assert(!core_select(ctx,r));ctx.final_attempt=true;assert(core_select(ctx,r)); + assert(r.provisional);assert(r.def->id==(preferred==1 ? 1:preferred==2&&!new_pmic ? 2:hint_present&&!new_pmic ? 2:1)); + detect_outcome_t out;out.result=r;const bool setup=finalize_prepared_result(out,preferred); + assert(setup==(preferred==0||preferred==r.def->id));assert(!should_persist_detection(out,0)); + } + for(std::uint64_t mask: {std::uint64_t(3),std::uint64_t(12)}) + for(board_id_t preferred: {board_id_t(0),board_id_t(3),board_id_t(4),board_id_t(5),board_id_t(42)}) + for(board_id_t hint: {board_id_t(0),board_id_t(3),board_id_t(4),board_id_t(5)}) { + observed=mask;probe_ctx_t ctx;ctx.hint=hint;ctx.preferred=preferred;board_result_t r; + assert(!card_select(ctx,&r));ctx.final_attempt=true;assert(card_select(ctx,&r));assert(r.provisional); + const auto compatible=[&](board_id_t id){return id==3||(id==4&&mask==12)||(id==5&&mask==3);}; + const auto expected=compatible(preferred)?preferred:compatible(hint)?hint:3; + assert(r.def->id==expected);detect_outcome_t out;out.result=r; + assert(finalize_prepared_result(out,preferred)==(preferred==0||preferred==expected)); + } +} +`,"provisional selectors"); +}); + +test("production SPI and Paper hint restrictions recover a different member on attempt one", async () => { + const detector=await fs.readFile(path.join(src,"board_detect.inl"),"utf8"); + const families=await fs.readFile(path.join(src,"m5/esp32s3/families.inl"),"utf8"); + const spi=body(detector.slice(detector.indexOf('class spi_id_detector_t')),'bool confirm('); + const paper=body(families.slice(families.indexOf('class paper_family_detector_t')),'bool confirm('); + const main=await fs.readFile(path.join(src,"../M5GFX.cpp"),"utf8"); + const compat=body(main,"board_t M5GFX::autodetect(").replaceAll("board_t::board_unknown","board_id_unknown"); + await compileRun(common+` +struct member_t {const board_desc_t* desc;}; +namespace wiring {namespace papers3 {constexpr int internal_i2c_sda=0,internal_i2c_scl=1;}} +namespace pmic_ops {constexpr int pm1_i2c_addr=0x6e,pm1_i2c_freq=100000,pm1_device_id=0x2050;} +namespace specs {namespace papers3 {namespace touch {constexpr int i2c_freq=400000;}}} +board_id_t actual; +bool probe_i2c_read(probe_ctx_t&,int,int,int addr,int,std::uint8_t* p,int,int,int,bool=false) { + if(actual==9&&addr==0x6e) {p[0]=0x50;p[1]=0x20;return true;} + if(actual==8&&addr==0x14) {p[0]='9';p[1]='1';p[2]='1';p[3]=0;return true;}return false; +} +struct spi_t { + bool shared_id_read_=false;std::uint8_t member_count_=2; + member_t members_desc_[2]={{&desc_core2},{&desc_tough}}; + bool probe_family(probe_ctx_t&,board_result_t*) const {return false;} + bool probe_member(probe_ctx_t&,const member_t& m,board_result_t* r) const {if(m.desc->def.id!=actual) {return false;}r->assign(m.desc);return true;} + bool confirm(probe_ctx_t& ctx,board_result_t* result) const ${spi} +}; +bool paper_confirm(probe_ctx_t& ctx,board_result_t* result) ${paper} +using board_t=board_id_t; +namespace board_detect=m5gfx::board_detect; +namespace m5gfx {namespace board_detect {namespace m5 { + struct display_parts_t {int* bus=nullptr;int* panel=nullptr;int* light=nullptr;int* touch=nullptr;}; +}}} +struct package_t {const int* detectors=nullptr;bool conditional_pins_unavailable=false;}; +package_t select_detection_package(bool) {static const int family=1;package_t p;p.detectors=&family;return p;} +bool reject_detected_setup(board_t) {return false;} +template detect_outcome_t run_detection_attempt(const int*,const detect_request_t& request,bool final,Setup) { + probe_ctx_t ctx;ctx.hint=request.hint;ctx.attempt=request.attempt;ctx.final_attempt=final;detect_outcome_t out; + if(actual<3 ? spi_t{}.confirm(ctx,&out.result):paper_confirm(ctx,&out.result)) { + finalize_prepared_result(out,request.preferred);out.setup_succeeded=true; + } + return out; +} +struct M5GFX { + struct {board_t fallback_board=0;} _detect_config; + struct {int* get() {return nullptr;}} _panel_last; + void panel(int*) {} + bool _adopt_detected_parts(int*,int*,int*,int*) {return true;} + board_t autodetect(bool use_reset,board_t board,bool final_attempt,bool* transient_fallback,bool* no_signature,board_t* candidate_board) ${compat} +}; +int main() { + for(board_id_t target: {board_id_t(1),board_id_t(2),board_id_t(8),board_id_t(9)}) { + actual=target;probe_ctx_t ctx;ctx.hint=target<3 ? 3-target:17-target;board_result_t r; + const auto confirm=[&](const detect_request_t& req,bool) {ctx.attempt=req.attempt;ctx.hint=req.hint;ctx.final_attempt=req.attempt+1==req.max_attempts;detect_outcome_t out; + bool match=target<3 ? spi_t{}.confirm(ctx,&out.result):paper_confirm(ctx,&out.result); + if(match) {finalize_prepared_result(out,0);out.setup_succeeded=true;}return out;}; + detect_request_t req;req.max_attempts=5;req.hint=ctx.hint;auto out=run_detection_session(req,confirm); + assert(out.setup_succeeded&&out.result.def->id==target&&out.attempts==2);assert(out.verdict==verdict_t::confirmed); + ctx.attempt=0;ctx.final_attempt=true;board_result_t final_result; + assert(target<3 ? spi_t{}.confirm(ctx,&final_result):paper_confirm(ctx,&final_result)); + assert(final_result.def->id==target); + M5GFX gfx;board_t candidate=0;bool transient=false,no_signature=false; + assert(gfx.autodetect(false,ctx.hint,false,&transient,&no_signature,&candidate)==0); + assert(gfx.autodetect(false,ctx.hint,true,&transient,&no_signature,&candidate)==target); + } +} +`,"retry hint restrictions"); +}); + +test("production C6 display signature returns an unsaved weak UnitC6L independently of hint", async () => { + const source=await fs.readFile(path.join(src,"m5/esp32c6/c6_display.inl"),"utf8"); + const family=source.slice(source.indexOf('class c6_display_family_detector_t')); + const signature=body(family,'bool signature('),confirm=body(family,'bool confirm('); + await compileRun(common+` +namespace c6_display_detail {constexpr int sda=0,scl=1,pi4io_freq=100000;constexpr std::uint64_t signature_bit=1ull<<18;enum class candidate_t {none,unitc6l,nesson1};} +namespace wiring {namespace unitc6l {constexpr int internal_i2c_sda=0,internal_i2c_scl=1;}} +namespace specs {namespace unitc6l {namespace i2c_ioe {constexpr int i2c_addr=0x43,id_reg=0,i2c_freq=100000;}} +namespace nesson1 {namespace i2c_pi4io1 {constexpr int i2c_addr=0x43,id_reg=0;}namespace i2c_pi4io2 {constexpr int i2c_addr=0x44,id_reg=0;}}} +bool present, responds, high; +bool probe_i2c_bus_present(probe_ctx_t&,int,int) {return present;} +struct pulls_t {std::uint64_t pullup_high;}; +pulls_t probe_pin_pulls(probe_ctx_t&,std::uint64_t) {return {high?c6_display_detail::signature_bit:0};} +bool probe_i2c_read(probe_ctx_t&,int,int,int,int,std::uint8_t* p,int,int,int) {*p=0xa0;return responds;} +bool is_pi4io(std::uint8_t p) {return (p&0xe0)==0xa0;} +struct c6_t {mutable c6_display_detail::candidate_t candidate_; + bool signature(probe_ctx_t& ctx) const ${signature} + bool confirm(probe_ctx_t& ctx,board_result_t* result) const ${confirm} +}; +int main() { + for(bool bus:{false,true}) for(bool response:{false,true}) for(board_id_t hint:{board_id_t(0),board_id_t(6),board_id_t(42)}) { + present=bus;responds=response;high=true;c6_t detector;detect_request_t req;req.hint=hint;req.max_attempts=5; + auto out=run_detection_session(req,[&](const detect_request_t& r,bool last){probe_ctx_t ctx;ctx.hint=r.hint;ctx.final_attempt=last;detect_outcome_t out; + if(!detector.signature(ctx)) {out.reason=fail_reason_t::no_signature;return out;} + if(detector.confirm(ctx,&out.result)) {finalize_prepared_result(out,0);out.setup_succeeded=true;} + else if(ctx.candidate) {out.verdict=verdict_t::candidate;out.candidate_kind=candidate_kind_t::weak;out.result.candidate=ctx.candidate;} + return out; + }); + assert(out.verdict==(!bus?verdict_t::unknown:response?verdict_t::confirmed:verdict_t::candidate)); + assert(out.setup_succeeded==(bus&&response));assert(!bus||response||out.result.candidate==&desc_unitc6l.def); + assert(should_persist_detection(out,0)==(bus&&response));assert(out.attempts==(!bus||response?1:5)); + } + present=true;responds=false;high=true;c6_t detector;probe_ctx_t ctx;board_result_t result; + ctx.candidate=&desc_core2.def; + assert(detector.signature(ctx));assert(!detector.confirm(ctx,&result)); + assert(ctx.candidate==&desc_core2.def); + ctx.candidate=nullptr;assert(!detector.confirm(ctx,&result)); + assert(ctx.candidate==&desc_unitc6l.def); +} +`,"C6 weak candidate"); +}); + +test("production Atom pull mismatch stays provisional on attempt zero and Stack bypass keeps its retry timing", async () => { + const atomSource=await fs.readFile(path.join(src,"m5/esp32_pico.inl"),"utf8"); + const stackSource=await fs.readFile(path.join(src,"m5/esp32_d0wdq6.inl"),"utf8"); + const atom=body(atomSource.slice(atomSource.indexOf('class atom_family_detector_t')),'bool probe('); + const stack=body(stackSource.slice(stackSource.indexOf('class stack_family_detector_t')),'bool signature('); + const bypass=body(stackSource.slice(stackSource.indexOf('class stack_family_detector_t')),'if (values[2] & 2u)'); + await compileRun(common+` +struct tx_t {void restore_start(std::initializer_list){} void restore_start(int){} } tx; +struct hw_ctx_t:probe_ctx_t {tx_t* transaction=&tx;struct {std::uint64_t values[3];} detector_workspace;}; +const board_desc_t desc_atomvoice={{10,"Voice",0}},desc_atommatrix={{11,"Matrix",0}},desc_atomlite={{12,"Lite",0}},desc_atomu={{13,"U",0}}; +namespace lgfx {enum class pin_mode_t {input,input_pullup,input_pulldown};pin_mode_t mode;void pinMode(int pin,pin_mode_t m){if(pin==23)mode=m;}bool gpio_in(int pin) {if(pin==2)return true;if(pin==34)return mode==pin_mode_t::input_pullup;return false;}} +void esp_rom_delay_us(int){} +namespace atom_touch {bool measure(std::uint32_t* a,std::uint32_t* b){*a=100;*b=25;return true;}void clear_led(){}} +struct atom_t {mutable bool measured_=false,touch_ok_=false;mutable std::uint32_t t4_=0,t7_=0; + bool probe(hw_ctx_t& ctx,board_result_t* result) const ${atom} +}; +struct stack_desc_t {board_def_t def;struct {int dc,cs;} display;}; +const stack_desc_t desc_stack={{14,"Stack",0},{0,1}}; +namespace startup_detail {bool description_valid(const stack_desc_t&){return true;}bool gpio_valid(int){return true;}void pin_level(int,bool){}} +namespace detail {bool sd_pull_mask(const stack_desc_t&,std::uint64_t* mask){*mask=3;return true;}} +struct stack_pulls_t {std::uint64_t pulldown_high,pullup_high;}; +stack_pulls_t probe_pin_pulls(hw_ctx_t&,std::uint64_t){return {0,3};} +bool stack_signature(hw_ctx_t& ctx) ${stack} +int main() { + atom_t detector;detect_request_t req;req.max_attempts=5; + auto out=run_detection_session(req,[&](const detect_request_t& r,bool){hw_ctx_t ctx;ctx.attempt=r.attempt;detect_outcome_t out; + assert(detector.probe(ctx,&out.result));assert(out.result.def==&desc_atommatrix.def);assert(out.result.provisional); + finalize_prepared_result(out,0);out.setup_succeeded=true;return out;}); + assert(out.attempts==1&&out.candidate_kind==candidate_kind_t::provisional);assert(!should_persist_detection(out,0)); + for(bool hinted:{false,true}) { + req.hint=hinted?14:0; + out=run_detection_session(req,[&](const detect_request_t& r,bool last){hw_ctx_t ctx;ctx.hint=r.hint;ctx.final_attempt=last;detect_outcome_t out; + if(!stack_signature(ctx)){return out;} + const auto& values=ctx.detector_workspace.values;auto* result=&out.result;result->def=&desc_stack.def; + if(values[2]&2u) ${bypass} + finalize_prepared_result(out,0);out.setup_succeeded=true;return out;}); + assert(out.attempts==(hinted?1:5));assert(out.result.provisional);assert(!should_persist_detection(out,0)); + req.preferred=desc_core2.def.id; + out=run_detection_session(req,[&](const detect_request_t& r,bool last){hw_ctx_t ctx;ctx.hint=r.hint;ctx.final_attempt=last;detect_outcome_t out; + if(!stack_signature(ctx)){return out;} + const auto& values=ctx.detector_workspace.values;auto* result=&out.result;result->def=&desc_stack.def; + if(values[2]&2u) ${bypass} + if(finalize_prepared_result(out,r.preferred)) {out.setup_succeeded=true;}return out;}); + assert(out.attempts==5&&!out.setup_succeeded&&out.result.candidate==&desc_stack.def); + req.preferred=0; + } +} +`,"Atom and Stack timing"); +}); + +test("production PM1 unresolved tail keeps PaperMono without running hinted StopWatch power", async () => { + const source=await fs.readFile(path.join(src,"m5/esp32s3/families.inl"),"utf8"); + const begin=source.indexOf(' if (!stopwatch_touch && !ctx.final_attempt)'); + const end=source.indexOf('\n }',begin); + const tail=source.slice(begin,end); + await compileRun(common+` +const board_desc_t desc_stopwatch={{20,"StopWatch",0}},desc_papermono={{21,"PaperMono",0}}; +int power_calls=0; +namespace startup_detail {bool prepare_power(const board_desc_t&,board_result_t&,int,bool) {++power_calls;return true;}} +bool refine_tail(board_result_t& result,const prepare_ctx_t& ctx) { + bool stopwatch_touch=false;struct {bool opened;int port;} i2c={true,0}; + ${tail} +} +int main(){for(board_id_t hint:{board_id_t(0),board_id_t(20),board_id_t(21)}) for(board_id_t preferred:{board_id_t(0),board_id_t(20),board_id_t(21)}) { + prepare_ctx_t ctx;ctx.hint=hint;ctx.preferred=preferred;board_result_t r;r.assign(&desc_papermono); + assert(!refine_tail(r,ctx));ctx.final_attempt=true;assert(refine_tail(r,ctx)); + assert(r.def==&desc_papermono.def&&r.provisional);assert(power_calls==0); + detect_outcome_t out;out.result=r;assert(finalize_prepared_result(out,preferred)==(preferred!=20)); +}} +`,"PM1 fixed representative"); +}); diff --git a/extras/board_spec/test/detect_host_helpers.js b/extras/board_spec/test/detect_host_helpers.js new file mode 100644 index 00000000..053cab5a --- /dev/null +++ b/extras/board_spec/test/detect_host_helpers.js @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +const here = path.dirname(fileURLToPath(import.meta.url)); +const src = path.resolve(here, "../../../src"); +export function body(source, marker) { + const begin = source.indexOf(marker); assert.ok(begin >= 0, marker); + const brace = source.indexOf("{", begin); let depth = 1, end = brace + 1; + while (depth && end < source.length) { if (source[end] === "{") ++depth; if (source[end] === "}") --depth; ++end; } + assert.equal(depth, 0); return source.slice(brace, end); +} +export async function compileRun(source, label) { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m5gfx-s1-")); + try { + const file = path.join(directory, "host.cpp"), binary = path.join(directory, "host"); + await fs.writeFile(file, source); + const result = spawnSync(process.env.CXX || "c++", ["-std=c++11", "-O2", `-I${src}`, file, "-o", binary], {encoding: "utf8"}); + assert.equal(result.status, 0, `${label}: ${result.stderr}`); + const ran = spawnSync(binary, [], {encoding: "utf8"}); + assert.equal(ran.status, 0, `${label}: ${ran.stderr}`); + } finally { await fs.rm(directory, {recursive:true, force:true}); } +} diff --git a/extras/board_spec/test/detect_session.test.js b/extras/board_spec/test/detect_session.test.js index a2fa9ab0..bea5db3c 100644 --- a/extras/board_spec/test/detect_session.test.js +++ b/extras/board_spec/test/detect_session.test.js @@ -7,7 +7,7 @@ import test from "node:test"; import { fileURLToPath } from "node:url"; const here = path.dirname(fileURLToPath(import.meta.url)); -test("detection session preserves legacy hint, reset, retries, candidates and NVS decisions", async (t) => { +test("detection session enforces provisional preference, stable hints, candidates and confirmed-only persistence", async (t) => { const compiler = process.env.CXX || "c++"; if (spawnSync(compiler, ["--version"]).error?.code === "ENOENT") return t.skip("C++ compiler unavailable"); const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m5gfx-session-")); @@ -18,38 +18,7 @@ test("detection session preserves legacy hint, reset, retries, candidates and NV assert.equal(compiled.status, 0, compiled.stderr || compiled.stdout); const ran = spawnSync(binary, [], { encoding: "utf8" }); assert.equal(ran.status, 0, ran.stderr || ran.stdout); - assert.match(ran.stdout, /equivalence cases=4800000/); - } finally { - await fs.rm(directory, { recursive: true, force: true }); - } -}); - -test("compatibility autodetect preserves the C5/C61 transient output sentinel", async (t) => { - const compiler = process.env.CXX || "c++"; - if (spawnSync(compiler, ["--version"]).error?.code === "ENOENT") return t.skip("C++ compiler unavailable"); - const sourceRoot = path.resolve(here, "../../../src"); - const main = await fs.readFile(path.join(sourceRoot, "M5GFX.cpp"), "utf8"); - const assignment = /#if !defined\(CONFIG_IDF_TARGET_ESP32C5\) && !defined\(CONFIG_IDF_TARGET_ESP32C61\)([\s\S]*?)#endif/.exec(main)?.[0]; - assert.ok(assignment, "compile the actual compatibility output block"); - const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m5gfx-transient-")); - try { - const source = path.join(directory, "transient.cpp"); - await fs.writeFile(source, `#include "board_detect/detect_types.hpp"\n#include \n` - + `void apply(const m5gfx::board_detect::detect_outcome_t& outcome, bool* transient_fallback) {\n${assignment}\n}\n` - + `int main() { for (int successful = 0; successful < 2; ++successful) {\n` - + `for (int result = 0; result < 2; ++result) { for (int initial = 0; initial < 2; ++initial) {\n` - + `m5gfx::board_detect::detect_outcome_t out; out.setup_succeeded = successful; out.result.transient_fallback = result;\n` - + `bool caller = initial; apply(out, &caller); apply(out, nullptr);\n` - + `#if defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C61)\nassert(caller == bool(initial));\n` - + `#else\nassert(caller == bool(successful ? result : initial));\n#endif\n} } } }\n`); - for (const chip of ["ESP32C5", "ESP32C61", "ESP32S3"]) { - const binary = path.join(directory, chip); - const compiled = spawnSync(compiler, ["-std=c++11", `-I${sourceRoot}`, `-DCONFIG_IDF_TARGET_${chip}`, - source, "-o", binary], { encoding: "utf8" }); - assert.equal(compiled.status, 0, compiled.stderr || compiled.stdout); - const ran = spawnSync(binary, [], { encoding: "utf8" }); - assert.equal(ran.status, 0, ran.stderr || ran.stdout); - } + assert.match(ran.stdout, /selection cases=32/); } finally { await fs.rm(directory, { recursive: true, force: true }); } diff --git a/extras/board_spec/test/detect_session_host.cpp b/extras/board_spec/test/detect_session_host.cpp index 60d12c73..4caa433c 100644 --- a/extras/board_spec/test/detect_session_host.cpp +++ b/extras/board_spec/test/detect_session_host.cpp @@ -1,153 +1,136 @@ #include "board_detect/detect_session.hpp" -#include #include -#include +#include +#include using namespace m5gfx::board_detect; +static const board_def_t member = { 1, "member", 0 }, other = { 2, "other", 0 }, weak = { 3, "weak", 0 }; -static const board_def_t proven = { 11, "proven", 0 }; -static const board_def_t suggested = { 12, "suggested", 0 }; -static const board_def_t alternate = { 13, "alternate", 0 }; -struct trace_t { - unsigned attempt; - board_id_t hint; - bool reset, final; - bool operator==(const trace_t& rhs) const { - return attempt == rhs.attempt && hint == rhs.hint && reset == rhs.reset && final == rhs.final; - } -}; -struct observed_t { - board_id_t board = 0, candidate = 0; - bool transient = false; - unsigned attempts = 0, calls = 0; - std::array trace; -}; -static detect_outcome_t injected(unsigned kind) { - detect_outcome_t out; - if (kind == 0 || kind == 1) { out.reason = fail_reason_t::no_signature; } - if (kind == 1 || kind == 7) { - out.result.candidate = kind == 1 ? &suggested : &alternate; - out.verdict = verdict_t::candidate; - } - if (kind == 3 || kind == 5 || kind == 6 || kind == 8 || kind == 9) { - out.verdict = verdict_t::confirmed; - out.result.status = detect_status_t::matched; - out.result.def = &proven; - out.result.transient_fallback = kind == 3 || kind == 6; - out.setup_succeeded = kind == 5 || kind == 6; - if (kind == 3) { out.reason = fail_reason_t::prepare_failed; } - if (kind == 8) { out.reason = fail_reason_t::construct_failed; } - if (kind == 9) { out.reason = fail_reason_t::adopt_failed; } - } - if (kind == 4) { out.reason = fail_reason_t::excluded_by_set; } - return out; -} -// Independent transcription of the pre-session loop: retry=4..0, ESP32's -// original NVS hint versus the other chips' previous board, and hint gates. -static observed_t legacy(unsigned sequence, const detect_session_policy_t& policy, - board_id_t initial_hint, bool reset, bool has_list) { - observed_t obs; - board_id_t board = initial_hint; - int retry = 4; - do { - ++obs.attempts; - if (retry == 1) { reset = true; } - const auto hint = policy.keep_initial_hint ? initial_hint : board; - bool allowed = hint == 0 || policy.hint_gate == nullptr; - if (!allowed) { - for (auto p = policy.hint_gate; *p; ++p) { if (*p == hint) { allowed = true; } } - } - detect_outcome_t out; - if (has_list && allowed) { - obs.trace[obs.calls++] = { unsigned(4 - retry), hint, reset, - retry == 0 && !policy.suppress_final_attempt }; - unsigned digits = sequence; - for (int i = 0; i < 4 - retry; ++i) { digits /= 10; } - out = injected(digits % 10); - } - if (!obs.candidate && out.result.candidate) { obs.candidate = out.result.candidate->id; } - if (out.setup_succeeded) { - board = out.result.def->id; - obs.transient = out.result.transient_fallback; - break; - } - board = 0; - if (out.reason == fail_reason_t::no_signature) { obs.transient = true; break; } - } while (board == 0 && --retry >= 0); - obs.board = board; - if (board) { obs.candidate = 0; } - return obs; -} -static observed_t session(unsigned sequence, const detect_session_policy_t& policy, - board_id_t initial_hint, bool reset, bool has_list) { - observed_t obs; - detect_request_t request; - request.hint = initial_hint; request.allow_reset = reset; request.max_attempts = 5; - const auto outcome = run_detection_session(request, policy, - [&](const detect_request_t& attempt, bool final) { - assert(attempt.target_set == nullptr); - if (!has_list) { return detect_outcome_t{}; } - obs.trace[obs.calls++] = { attempt.attempt, attempt.hint, attempt.allow_reset, final }; - unsigned digits = sequence; - for (unsigned i = 0; i < attempt.attempt; ++i) { digits /= 10; } - return injected(digits % 10); - }); - obs.attempts = outcome.attempts; - obs.board = outcome.setup_succeeded ? outcome.result.def->id : 0; - obs.candidate = !obs.board && outcome.result.candidate ? outcome.result.candidate->id : 0; - obs.transient = outcome.reason == fail_reason_t::no_signature - || (outcome.setup_succeeded && outcome.result.transient_fallback); - return obs; -} -int main() { - static const board_id_t gate[] = { 11, 0 }; +int main() +{ unsigned cases = 0; - for (unsigned flags = 0; flags < 8; ++flags) { - detect_session_policy_t policy; - policy.hint_gate = flags & 1 ? gate : nullptr; - policy.keep_initial_hint = flags & 2; - policy.suppress_final_attempt = flags & 4; - for (board_id_t hint : { board_id_t(0), board_id_t(11), board_id_t(99) }) { - for (bool reset : { false, true }) { - for (unsigned sequence = 0; sequence < 100000; ++sequence) { - const auto before = legacy(sequence, policy, hint, reset, true); - const auto after = session(sequence, policy, hint, reset, true); - assert(before.board == after.board && before.candidate == after.candidate); - assert(before.transient == after.transient && before.attempts == after.attempts); - assert(before.calls == after.calls); - for (unsigned i = 0; i < before.calls; ++i) { assert(before.trace[i] == after.trace[i]); } - // The unchanged NVS expression also retains unknown=0 writes. - assert((!before.transient && hint != before.board) == (!after.transient && hint != after.board)); - ++cases; - } - const auto before = legacy(0, policy, hint, reset, false); - const auto after = session(0, policy, hint, reset, false); - assert(before.board == after.board && before.transient == after.transient); - assert(before.attempts == after.attempts && after.calls == 0); + // All selection-table rows, including displayless adoption and setup failure. + for (int verdict = 0; verdict != 4; ++verdict) + for (board_id_t preferred = 0; preferred != 4; ++preferred) + for (int adopted = 0; adopted != 2; ++adopted) + { + detect_request_t request; request.preferred = preferred; request.max_attempts = 5; + unsigned calls = 0, construct = 0, adopt = 0, saves = 0; + auto outcome = run_detection_session(request, [&](const detect_request_t& r, bool) { + ++calls; detect_outcome_t out; + if (verdict < 2) { + out.reason = fail_reason_t::no_signature; + if (verdict == 1) { out.verdict = verdict_t::candidate; out.candidate_kind = candidate_kind_t::weak; out.result.candidate = &weak; } + return out; } - } + out.result.def = &member; out.result.provisional = verdict == 2; + if (!finalize_prepared_result(out, r.preferred)) { return out; } + ++construct; ++adopt; + out.setup_succeeded = adopted; + out.reason = adopted ? fail_reason_t::none : fail_reason_t::adopt_failed; + return out; + }); + const bool yields = verdict == 2 && preferred != 0 && preferred != member.id; + assert(calls == unsigned(verdict < 2 || (adopted && !yields) ? 1 : 5)); + assert(construct == (verdict < 2 || yields ? 0 : calls)); assert(adopt == construct); + const auto board = outcome.setup_succeeded ? outcome.result.def->id : board_id_unknown; + const auto candidate = board ? board_id_unknown : outcome.result.candidate ? outcome.result.candidate->id : board_id_unknown; + assert(board == (verdict >= 2 && !yields && adopted ? member.id : board_id_unknown)); + assert(candidate == (yields ? member.id : verdict == 1 ? weak.id : verdict == 2 && !adopted ? member.id : board_id_unknown)); + assert(outcome.verdict == (verdict == 3 ? verdict_t::confirmed : verdict ? verdict_t::candidate : verdict_t::unknown)); + assert(outcome.candidate_kind == (verdict == 2 ? candidate_kind_t::provisional : verdict == 1 ? candidate_kind_t::weak : candidate_kind_t::none)); + if (should_persist_detection(outcome, 0)) { ++saves; } + assert(saves == unsigned(verdict == 3 && adopted)); + assert(!should_persist_detection(outcome, member.id)); + ++cases; } - // A wide NVS value ending in an allowed board ID still fails the old gate - // before narrowing. Only the following unknown-hint attempt may probe. - detect_session_policy_t narrowed; - narrowed.hint_gate = gate; - narrowed.reject_initial_hint = true; - detect_request_t wide; - wide.hint = 11; wide.max_attempts = 5; - unsigned calls = 0; - const auto after_wide = run_detection_session(wide, narrowed, - [&](const detect_request_t& request, bool) { - ++calls; assert(request.attempt == 1 && request.hint == 0); return injected(5); + // A weak candidate survives unknown attempts; provisional replaces it through the final retry. + detect_request_t request; request.max_attempts = 5; request.hint = other.id; request.preferred = other.id; + auto out = run_detection_session(request, [&](const detect_request_t& r, bool final) { + assert(r.hint == other.id); assert(r.preferred == other.id); assert(final == (r.attempt == 4)); + assert(r.allow_reset == (r.attempt >= 3)); + detect_outcome_t result; + if (r.attempt == 0) { result.verdict = verdict_t::candidate; result.candidate_kind = candidate_kind_t::weak; result.result.candidate = &weak; } + if (r.attempt == 3) { result.result.def = &member; result.result.provisional = true; assert(!finalize_prepared_result(result, r.preferred)); } + return result; + }); + assert(out.attempts == 5 && out.result.candidate == &member && out.candidate_kind == candidate_kind_t::provisional); + assert(!should_persist_detection(out, 0)); + // A provisional setup failure cannot be downgraded by weak/unknown retries. + request.preferred = 0; + out = run_detection_session(request, [&](const detect_request_t& r, bool) { + detect_outcome_t result; + if (r.attempt == 0) { result.result.def = &member; result.result.provisional = true; finalize_prepared_result(result, 0); result.reason = fail_reason_t::construct_failed; } + else if (r.attempt == 1) { result.verdict = verdict_t::candidate; result.candidate_kind = candidate_kind_t::weak; result.result.candidate = &weak; } + return result; + }); + assert(out.attempts == 5 && out.result.candidate == &member && out.candidate_kind == candidate_kind_t::provisional); + assert(out.verdict == verdict_t::candidate); + // Retained candidate kind survives every confirmed-but-failed setup reason. + for (auto kind : {candidate_kind_t::weak, candidate_kind_t::provisional}) + for (auto failure : {fail_reason_t::prepare_failed, fail_reason_t::construct_failed, fail_reason_t::adopt_failed}) { + out = run_detection_session(request, [&](const detect_request_t& r, bool) { + detect_outcome_t result; + if (r.attempt == 0) { + result.verdict = verdict_t::candidate; result.candidate_kind = kind; + result.result.candidate = &weak; + } else { + result.result.def = &member; finalize_prepared_result(result, 0); + result.reason = failure; + } + return result; }); - assert(calls == 1 && after_wide.attempts == 2 && after_wide.setup_succeeded); - // The protected compatibility entry is a single attempt and must not promote - // reset simply because max_attempts is one. - detect_request_t single; - detect_session_policy_t policy; - policy.suppress_final_attempt = true; - const auto one = run_detection_session(single, policy, - [](const detect_request_t& request, bool final) { - assert(!request.allow_reset && !final); return injected(3); + assert(out.attempts == 5 && out.verdict == verdict_t::confirmed && !out.setup_succeeded); + assert(out.result.candidate == &weak && out.candidate_kind == kind && !should_persist_detection(out, 0)); + } + // An early mismatch may refine successfully on the next attempt. + request.preferred = other.id; + out = run_detection_session(request, [&](const detect_request_t& r, bool) { + detect_outcome_t result; result.result.def = r.attempt == 0 ? &member : &other; + result.result.provisional = r.attempt == 0; + if (finalize_prepared_result(result, r.preferred)) { result.setup_succeeded = true; } + return result; + }); + assert(out.attempts == 2 && out.setup_succeeded && out.result.def == &other); + request.preferred = 0; + // Retained candidates follow confidence first, and provisional recency second. + for (auto first : {candidate_kind_t::weak, candidate_kind_t::provisional}) + for (auto next : {candidate_kind_t::weak, candidate_kind_t::provisional}) { + out = run_detection_session(request, [&](const detect_request_t& r, bool) { + detect_outcome_t result; + if (r.attempt < 2) { + result.verdict = verdict_t::candidate; + result.result.candidate = r.attempt == 0 ? &member : &other; + result.candidate_kind = r.attempt == 0 ? first : next; + } + return result; }); - assert(one.attempts == 1 && one.verdict == verdict_t::confirmed && !one.setup_succeeded); - std::cout << "legacy/session equivalence cases=" << cases << '\n'; + assert(out.attempts == 5 && out.verdict == verdict_t::candidate); + assert(out.result.candidate == (next == candidate_kind_t::provisional ? &other : &member)); + assert(out.candidate_kind == (next == candidate_kind_t::provisional ? next : first)); + } + // Initial power failure retains Core2; final refinement selects Tough while yielding to Paper. + request.hint = other.id; request.preferred = weak.id; + out = run_detection_session(request, [&](const detect_request_t& r, bool final) { + detect_outcome_t result; + if (r.attempt == 0 || final) { + result.result.def = final ? &other : &member; + result.result.provisional = true; + assert(!finalize_prepared_result(result, r.preferred)); + } + return result; + }); + assert(out.attempts == 5 && !out.setup_succeeded && out.result.candidate == &other); + assert(out.verdict == verdict_t::candidate && out.candidate_kind == candidate_kind_t::provisional); + assert(!should_persist_detection(out, 0)); + request.preferred = 0; + // Reset escalation and immutable hint remain true through the final retry. + out = run_detection_session(request, [&](const detect_request_t& r, bool final) { + assert(r.hint == other.id); assert(final == (r.attempt == 4)); assert(r.allow_reset == (r.attempt >= 3)); + return detect_outcome_t{}; + }); assert(out.attempts == 5); + for (const auto reason : {fail_reason_t::prepare_failed, fail_reason_t::construct_failed, fail_reason_t::adopt_failed}) { + out = {}; out.result.def = &member; finalize_prepared_result(out, 0); out.reason = reason; + assert(!should_persist_detection(out, 0)); + } + std::printf("selection cases=%u\n", cases); } diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 5211776f..4275a32f 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -793,7 +793,6 @@ namespace m5gfx struct detection_package_t { const board_detect::board_detector_t* const* detectors = nullptr; - board_detect::detect_session_policy_t policy; bool conditional_pins_unavailable = false; bool direct_setup = false; }; @@ -802,10 +801,8 @@ namespace m5gfx { detection_package_t package; (void)legacy_autodetect; - // Retain these legacy differences until S1 aligns hint and final-attempt - // semantics. Selection itself does not probe or configure any GPIO. + // Selection does not probe or configure any GPIO. #if !defined(CONFIG_IDF_TARGET) || defined(CONFIG_IDF_TARGET_ESP32) - package.policy.keep_initial_hint = true; package.direct_setup = true; // The protected legacy autodetect method never probed ESP32 directly. if (legacy_autodetect) { return package; } @@ -825,17 +822,8 @@ namespace m5gfx default: break; } #elif defined(CONFIG_IDF_TARGET_ESP32C3) - static const board_detect::board_id_t gate[] = { - board_t::board_M5StampC3, board_t::board_M5StampC3U, board_detect::board_id_unknown, - }; - package.policy.hint_gate = gate; package.detectors = board_detect::m5::esp32c3_detectors; #elif defined(CONFIG_IDF_TARGET_ESP32C5) - static const board_detect::board_id_t gate[] = { - board_t::board_M5ToughC5, board_t::board_M5StampC5, board_detect::board_id_unknown, - }; - package.policy.hint_gate = gate; - package.policy.suppress_final_attempt = true; package.detectors = board_detect::m5::esp32c5_detectors; #elif defined(CONFIG_IDF_TARGET_ESP32C6) const auto pkg = m5gfx::get_pkg_ver(); @@ -843,18 +831,9 @@ namespace m5gfx if (pkg == 1) { package.detectors = board_detect::m5::esp32c6_detectors_qfn32; } else if (pkg == 0) { - static const board_detect::board_id_t gate[] = { - board_t::board_ArduinoNessoN1, board_t::board_M5UnitC6L, board_detect::board_id_unknown, - }; - package.policy.hint_gate = gate; package.detectors = board_detect::m5::esp32c6_detectors_qfn40; } #elif defined(CONFIG_IDF_TARGET_ESP32C61) - static const board_detect::board_id_t gate[] = { - board_t::board_M5CoreMatrix, board_detect::board_id_unknown, - }; - package.policy.hint_gate = gate; - package.policy.suppress_final_attempt = true; package.detectors = board_detect::m5::esp32c61_detectors; #elif defined(CONFIG_IDF_TARGET_ESP32H2) package.detectors = board_detect::m5::esp32h2_detectors; @@ -950,6 +929,8 @@ namespace m5gfx } board_detect::probe_ctx_t probe; probe.allow_reset = request.allow_reset; + probe.preferred = request.preferred; + probe.attempt = request.attempt; probe.final_attempt = final_attempt; probe.i2c_port_probe = probe_i2c_port; probe.transaction = &transaction; @@ -981,7 +962,8 @@ namespace m5gfx detectors, request.hint, probe); outcome.result = result; if (!probe.confirm_attempted) { outcome.reason = board_detect::fail_reason_t::no_signature; } - if (result.candidate != nullptr) { outcome.verdict = board_detect::verdict_t::candidate; } + if (result.candidate != nullptr) + { outcome.verdict = board_detect::verdict_t::candidate; outcome.candidate_kind = board_detect::candidate_kind_t::weak; } if (result.status == board_detect::detect_status_t::excluded) { ESP_LOGW(LIBRARY_NAME, "[Autodetect] detected board:%u is excluded", @@ -995,7 +977,6 @@ namespace m5gfx transaction.rollback(); return outcome; } - outcome.verdict = board_detect::verdict_t::confirmed; // Failure injection historically sees the representative before refine. const auto setup_board = static_cast(result.def->id); @@ -1003,19 +984,28 @@ namespace m5gfx const int adopted_i2c_port = result.desc->internal_i2c.hw_port; const bool adopted_i2c_was_open = adopted_i2c_port >= 0 && lgfx::i2c::isInitialized(adopted_i2c_port); - if (result.desc == nullptr || !board_detect::prepare(*result.desc, result, prepare_ctx)) + const bool prepared = result.desc != nullptr + && board_detect::prepare(*result.desc, result, prepare_ctx); + if (adopted_i2c_port >= 0 && !adopted_i2c_was_open + && lgfx::i2c::isInitialized(adopted_i2c_port)) { + transaction.buses().opened_i2c(adopted_i2c_port); + } + outcome.result = result; + const bool accepted = board_detect::finalize_prepared_result(outcome, request.preferred); + if (!prepared) + { + // Preserve the matched family's identification; adoption still failed. ESP_LOGW(LIBRARY_NAME, "[Autodetect] prepare failed for detected board:%u", static_cast(result.def->id)); - outcome.result = result; outcome.reason = board_detect::fail_reason_t::prepare_failed; transaction.rollback(); return outcome; } - if (adopted_i2c_port >= 0 && !adopted_i2c_was_open - && lgfx::i2c::isInitialized(adopted_i2c_port)) + if (!accepted) { - transaction.buses().opened_i2c(adopted_i2c_port); + transaction.rollback(); + return outcome; } board_detect::m5::display_parts_t parts; const auto construct_result = board_detect::m5::setup_detected_board(result, &parts); @@ -1070,6 +1060,18 @@ namespace m5gfx } #endif + void M5GFX::setDetectConfig(const detect_config_t& config) + { + const bool changed = _detect_config.fallback_board != config.fallback_board; + _detect_config = config; +#if defined (ESP_PLATFORM) + if (_detect_started && changed) + { ESP_LOGW(LIBRARY_NAME, "[Autodetect] configuration stored after init; applies to the next detection"); } +#else + (void)changed; +#endif + } + void M5GFX::_set_backlight(lgfx::ILight* bl) { // if (_light_last) { delete _light_last; } @@ -1092,6 +1094,7 @@ namespace m5gfx bool M5GFX::init_impl(bool use_reset, bool use_clear) { + _detect_started = true; _board_candidate = board_t::board_unknown; if (getBoard() != board_t::board_unknown) { @@ -1151,15 +1154,15 @@ namespace m5gfx } board_detect::detect_request_t request; - request.hint = static_cast(nvs_board); + request.hint = nvs_board <= UINT16_MAX + ? static_cast(nvs_board) : board_detect::board_id_unknown; + request.preferred = static_cast(_detect_config.fallback_board); request.allow_reset = use_reset; request.max_attempts = 5; auto package = select_detection_package(); - package.policy.reject_initial_hint = package.policy.hint_gate != nullptr - && nvs_board != request.hint; request.conditional_pins_unavailable = package.conditional_pins_unavailable; const auto outcome = board_detect::run_detection_session( - request, package.policy, + request, [this, &package](const board_detect::detect_request_t& attempt, bool final_attempt) { if (!package.direct_setup) { panel(nullptr); } @@ -1169,7 +1172,7 @@ namespace m5gfx return !reject_detected_setup(setup_board) && _adopt_detected_parts(parts.bus, parts.panel, parts.light, parts.touch); }); - if (!package.direct_setup || result.verdict != board_detect::verdict_t::confirmed) + if (!package.direct_setup || !result.setup_succeeded) { if (package.direct_setup) { panel(nullptr); } panel(_panel_last.get()); @@ -1178,10 +1181,6 @@ namespace m5gfx }); const auto board = outcome.setup_succeeded ? static_cast(outcome.result.def->id) : board_t::board_unknown; - // Only a successful adopter exported transient_fallback in the old path. - // A partial failed result must not change the NVS write decision in S0. - const bool transient_fallback = outcome.reason == board_detect::fail_reason_t::no_signature - || (outcome.setup_succeeded && outcome.result.transient_fallback); if (outcome.result.candidate != nullptr) { _board_candidate = static_cast(outcome.result.candidate->id); } _board = board; @@ -1196,7 +1195,8 @@ namespace m5gfx #endif - if (!transient_fallback && nvs_board != board) { + // Unresolved members and failed adoption must never replace a saved hint. + if (board_detect::should_persist_detection(outcome, nvs_board)) { if (0 == nvs_open(LIBRARY_NAME, NVS_READWRITE, &nvs_handle)) { ESP_LOGI(LIBRARY_NAME, "[Autodetect] save to NVS : board:%d", (int)board); nvs_set_u32(nvs_handle, NVS_KEY, board); @@ -1239,39 +1239,28 @@ namespace m5gfx panel(nullptr); const auto package = select_detection_package(true); board_detect::detect_request_t request; - request.hint = static_cast(board); + request.hint = static_cast(board) <= UINT16_MAX + ? static_cast(board) : board_detect::board_id_unknown; + request.preferred = static_cast(_detect_config.fallback_board); request.allow_reset = use_reset; request.conditional_pins_unavailable = package.conditional_pins_unavailable; - auto policy = package.policy; - policy.reject_initial_hint = policy.hint_gate != nullptr - && static_cast(board) != request.hint; - policy.suppress_final_attempt = policy.suppress_final_attempt || !final_attempt; - const auto outcome = board_detect::run_detection_session(request, policy, - [this, &package](const board_detect::detect_request_t& attempt, bool last) + const auto outcome = board_detect::run_detection_session(request, + [this, &package, final_attempt](const board_detect::detect_request_t& attempt, bool) { - return run_detection_attempt(package.detectors, attempt, last, + return run_detection_attempt(package.detectors, attempt, final_attempt, [this](board_detect::m5::display_parts_t& parts, board_t setup_board) { return !reject_detected_setup(setup_board) && _adopt_detected_parts(parts.bus, parts.panel, parts.light, parts.touch); }); }); - if (package.detectors != nullptr && !policy.reject_initial_hint - && board_detect::session_hint_allowed(policy, request.hint)) - { - if (no_signature != nullptr) - { *no_signature = outcome.reason == board_detect::fail_reason_t::no_signature; } - if (candidate_board != nullptr && *candidate_board == board_t::board_unknown - && outcome.result.candidate != nullptr) - { *candidate_board = static_cast(outcome.result.candidate->id); } - } - // These legacy chip entries did not export a transient output, even on success. -#if !defined(CONFIG_IDF_TARGET_ESP32C5) && !defined(CONFIG_IDF_TARGET_ESP32C61) + if (no_signature != nullptr) + { *no_signature = outcome.reason == board_detect::fail_reason_t::no_signature; } + if (candidate_board != nullptr && *candidate_board == board_t::board_unknown + && outcome.result.candidate != nullptr) + { *candidate_board = static_cast(outcome.result.candidate->id); } if (outcome.setup_succeeded && transient_fallback != nullptr) - { *transient_fallback = outcome.result.transient_fallback; } -#else - (void)transient_fallback; -#endif + { *transient_fallback = outcome.result.provisional; } panel(_panel_last.get()); return outcome.setup_succeeded ? static_cast(outcome.result.def->id) : board_t::board_unknown; @@ -1281,6 +1270,7 @@ namespace m5gfx bool M5GFX::init_impl(bool use_reset, bool use_clear) { + _detect_started = true; board_t b = board_t::board_unknown; #if defined (M5GFX_BOARD) b = M5GFX_BOARD; diff --git a/src/M5GFX.h b/src/M5GFX.h index 447a2077..0798963d 100644 --- a/src/M5GFX.h +++ b/src/M5GFX.h @@ -58,6 +58,11 @@ namespace m5gfx { using namespace lgfx; + + struct detect_config_t + { + board_t fallback_board = board_t::board_unknown; + }; //---------------------------------------------------------------------------- namespace ili9341_colors // Color definitions for backwards compatibility with old sketches @@ -193,6 +198,8 @@ namespace m5gfx #endif std::vector _displayStateStack; board_t _board_candidate = board_t::board_unknown; + detect_config_t _detect_config; + bool _detect_started = false; bool init_impl(bool use_reset, bool use_clear) override; board_t autodetect(bool use_reset = false, board_t board = board_t::board_unknown, @@ -211,6 +218,8 @@ namespace m5gfx static M5GFX* getInstance(void) { return _instance; } board_t getBoardCandidate(void) const { return _board_candidate; } + void setDetectConfig(const detect_config_t& config); + const detect_config_t& getDetectConfig(void) const { return _detect_config; } void progressBar(int x, int y, int w, int h, uint8_t val); void pushState(void); diff --git a/src/board_detect/board_detect.hpp b/src/board_detect/board_detect.hpp index 93326c8b..a36aa561 100644 --- a/src/board_detect/board_detect.hpp +++ b/src/board_detect/board_detect.hpp @@ -21,6 +21,8 @@ namespace board_detect // A retry must not override the caller's reset policy. bool allow_reset = true; board_id_t hint = board_id_unknown; + board_id_t preferred = board_id_unknown; + std::uint8_t attempt = 0; bool final_attempt = false; int i2c_port_probe = -1; // Non-null while board-detection components are running. diff --git a/src/board_detect/board_detect.inl b/src/board_detect/board_detect.inl index 52b47f9d..76a0e516 100644 --- a/src/board_detect/board_detect.inl +++ b/src/board_detect/board_detect.inl @@ -127,18 +127,18 @@ namespace board_detect // that member remains possible. Defaults may be absent for open families. // Only some chips' detectors use it. __attribute__((unused)) - static bool finish_unresolved(const prepare_ctx_t& ctx, board_result_t* result, + static bool select_provisional_member(const prepare_ctx_t& ctx, board_result_t* result, + const board_desc_t* preferred_if_possible, const board_desc_t* hinted_if_possible, - const board_desc_t* family_default, - bool persist, const char* why) + const board_desc_t* family_default, const char* why) { if (!ctx.final_attempt || result == nullptr) { return false; } - const auto* chosen = hinted_if_possible ? hinted_if_possible : family_default; + const auto* chosen = preferred_if_possible ? preferred_if_possible + : hinted_if_possible ? hinted_if_possible : family_default; if (chosen == nullptr) { return false; } result->assign(chosen); - if (!persist) { result->transient_fallback = true; } - ESP_LOGW("board_detect_m5", "%s; using %s%s", why, chosen->def.name, - persist ? "" : " for this boot"); + result->provisional = true; + ESP_LOGW("board_detect_m5", "%s; using %s for this boot", why, chosen->def.name); return true; } @@ -1704,7 +1704,7 @@ namespace board_detect bool confirm(probe_ctx_t& ctx, board_result_t* result) const override { if (shared_id_read_) { return probe_family(ctx, result); } - if (ctx.hint != board_id_unknown) + if (ctx.attempt == 0 && !ctx.final_attempt && ctx.hint != board_id_unknown) { for (std::uint8_t index = 0; index < member_count_; ++index) { @@ -1829,7 +1829,7 @@ namespace board_detect if (result.prepared & prepared_power_failed) { // The family member is still provisional: show it, but do not cache it. - result.transient_fallback = true; + result.provisional = true; ESP_LOGW("board_detect", "member refinement skipped after retained power_on failure; board=%u", static_cast(result.def->id)); diff --git a/src/board_detect/detect_session.hpp b/src/board_detect/detect_session.hpp index 896d443c..8fc8730e 100644 --- a/src/board_detect/detect_session.hpp +++ b/src/board_detect/detect_session.hpp @@ -1,69 +1,72 @@ // Copyright (c) M5Stack. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #pragma once - #include "detect_types.hpp" namespace m5gfx { namespace board_detect { - // Preserve the chip-specific hint and last-attempt policies until S1 aligns - // them. The gate is a zero-terminated allow-list; an unknown hint is allowed. - struct detect_session_policy_t - { - const board_id_t* hint_gate = nullptr; - bool keep_initial_hint = false; - bool suppress_final_attempt = false; - // Keep a legacy gate rejection before a wider NVS hint is narrowed. - bool reject_initial_hint = false; - }; - - __attribute__((always_inline)) inline bool session_hint_allowed(const detect_session_policy_t& policy, board_id_t hint) + // Classify after preparation: refinement may retain an unresolved member. + inline bool finalize_prepared_result(detect_outcome_t& outcome, board_id_t preferred) { - if (hint == board_id_unknown || policy.hint_gate == nullptr) { return true; } - for (auto p = policy.hint_gate; *p != board_id_unknown; ++p) + outcome.verdict = outcome.result.provisional ? verdict_t::candidate : verdict_t::confirmed; + outcome.candidate_kind = outcome.result.provisional ? candidate_kind_t::provisional : candidate_kind_t::none; + if (outcome.result.provisional && preferred != board_id_unknown + && outcome.result.def->id != preferred) { - if (*p == hint) { return true; } + outcome.result.candidate = outcome.result.def; + outcome.reason = fail_reason_t::family_unresolved; + return false; } - return false; + return true; + } + + inline bool should_persist_detection(const detect_outcome_t& outcome, std::uint32_t previous) + { + return outcome.verdict == verdict_t::confirmed && outcome.setup_succeeded + && previous != outcome.result.def->id; } - // RunAttempt owns each GPIO/bus transaction and the prepare/construct/adopt - // steps. Session state keeps the first weak candidate across failed attempts. - // Keep constant chip policies visible to the caller under size optimization. template - __attribute__((always_inline)) inline detect_outcome_t run_detection_session(detect_request_t request, - const detect_session_policy_t& policy, - RunAttempt run_attempt) + __attribute__((always_inline)) inline detect_outcome_t run_detection_session( + detect_request_t request, RunAttempt run_attempt) { detect_outcome_t outcome; - const auto initial_hint = request.hint; const board_def_t* candidate = nullptr; + candidate_kind_t kind = candidate_kind_t::none; std::uint8_t attempts = 0; for (unsigned attempt = request.attempt; attempt < request.max_attempts; ++attempt) { request.attempt = static_cast(attempt); - // The legacy loop promotes reset permission on its penultimate attempt. if (attempt + 2 == request.max_attempts) { request.allow_reset = true; } - request.hint = policy.keep_initial_hint ? initial_hint : request.hint; - const bool final_attempt = !policy.suppress_final_attempt - && attempt + 1 == request.max_attempts; - outcome = {}; - if ((!policy.reject_initial_hint || attempts != 0) - && session_hint_allowed(policy, request.hint)) - { - outcome = run_attempt(request, final_attempt); - } + const bool final_attempt = attempt + 1 == request.max_attempts; + outcome = run_attempt(request, final_attempt); outcome.attempts = ++attempts; - if (candidate == nullptr) { candidate = outcome.result.candidate; } if (outcome.setup_succeeded) { return outcome; } + // Failed setup still leaves stronger family evidence than a weak hint. + if (outcome.candidate_kind == candidate_kind_t::provisional + && outcome.result.candidate == nullptr) + { outcome.result.candidate = outcome.result.def; } + // Later provisional evidence can refine an earlier family representative. + // Weak suggestions cannot replace it; weak-only candidates keep the first. + if (outcome.result.candidate != nullptr + && (candidate == nullptr || outcome.candidate_kind == candidate_kind_t::provisional)) + { + candidate = outcome.result.candidate; + kind = outcome.candidate_kind; + } outcome.result.candidate = candidate; - if (outcome.verdict == verdict_t::unknown && candidate != nullptr) + // Candidate metadata follows its pointer even after confirmed setup fails. + outcome.candidate_kind = kind; + if (outcome.verdict != verdict_t::confirmed && candidate != nullptr) { outcome.verdict = verdict_t::candidate; } + // A mismatched preference retries until the final attempt. A retained + // power failure skips member selection; a retry can run refinement once + // power preparation succeeds and apply the observed member preference. + if (final_attempt && outcome.reason == fail_reason_t::family_unresolved + && outcome.candidate_kind == candidate_kind_t::provisional) { return outcome; } if (outcome.reason == fail_reason_t::no_signature) { return outcome; } - // Non-ESP32 legacy callers feed the previous unknown board into retries. - request.hint = board_id_unknown; } return outcome; } diff --git a/src/board_detect/detect_types.hpp b/src/board_detect/detect_types.hpp index debf20d7..7768b926 100644 --- a/src/board_detect/detect_types.hpp +++ b/src/board_detect/detect_types.hpp @@ -73,8 +73,8 @@ namespace board_detect const board_def_t* def = &board_def_unknown; std::uint32_t option = 0; std::uint32_t prepared = 0; - // A final-attempt family default can show the display, but is not an NVS hint. - bool transient_fallback = false; + // Family evidence permits setup, but its member is not confirmed. + bool provisional = false; // A weak, displayless suggestion returned only when no board is confirmed. const board_def_t* candidate = nullptr; detect_status_t status = detect_status_t::no_match; @@ -85,6 +85,7 @@ namespace board_detect }; enum class verdict_t : std::uint8_t { unknown, candidate, confirmed }; + enum class candidate_kind_t : std::uint8_t { none, weak, provisional }; enum class fail_reason_t : std::uint8_t { none, no_signature, confirm_transient, confirm_definitive, excluded_by_set, @@ -95,6 +96,7 @@ namespace board_detect { const board_id_t* target_set = nullptr; board_id_t hint = board_id_unknown; + board_id_t preferred = board_id_unknown; bool allow_reset = false; std::uint8_t attempt = 0; std::uint8_t max_attempts = 1; @@ -104,6 +106,7 @@ namespace board_detect struct detect_outcome_t { verdict_t verdict = verdict_t::unknown; + candidate_kind_t candidate_kind = candidate_kind_t::none; board_result_t result; fail_reason_t reason = fail_reason_t::none; std::uint8_t attempts = 0; diff --git a/src/board_detect/m5/esp32_d0wdq6.inl b/src/board_detect/m5/esp32_d0wdq6.inl index d57e5c78..603ba8b7 100644 --- a/src/board_detect/m5/esp32_d0wdq6.inl +++ b/src/board_detect/m5/esp32_d0wdq6.inl @@ -267,6 +267,9 @@ namespace m5 if (core2) { break; } lgfx::delay(1); } while (lgfx::millis() - touch_start < touch_startup_poll_ms); +#if defined (M5GFX_AUTODETECT_TEST_FAIL_CORE2_TOUCH) + tough = core2 = false; +#endif // Core2 requires its own touch response; a Station LCD miss on shared // AXP192 must not be saved as Core2 when neither touch answers. if (!tough && !core2) @@ -278,9 +281,17 @@ namespace m5 ctx.transaction->restore_start(signals); return false; } - if (!finish_unresolved(ctx, &result, - ctx.hint == desc_tough.def.id ? &desc_tough : nullptr, - &desc_core2, false, "Core2/Tough touch unidentified")) + const auto possible = [&](board_id_t id) -> const board_desc_t* + { + if (id == desc_core2.def.id) { return &desc_core2; } + // Tough has no AXP2101 variant; an explicit preference cannot negate it. + if (id == desc_tough.def.id + && !(result.option & generated_options::core2::new_pmic)) { return &desc_tough; } + return nullptr; + }; + if (!select_provisional_member(ctx, &result, + possible(ctx.preferred), possible(ctx.hint), + &desc_core2, "Core2/Tough touch unidentified")) { ctx.transaction->restore_start(signals); return false; @@ -462,6 +473,8 @@ namespace m5 const auto& values = ctx.detector_workspace.values; if (values[2] & 2u) { + // A bypassed signature can display, but cannot establish a saved identity. + result->provisional = true; ESP_LOGI("board_detect_m5", "M5Stack detected after bypassing pull signature pd=%08x%08x pu=%08x%08x", static_cast(values[0] >> 32), diff --git a/src/board_detect/m5/esp32_pico.inl b/src/board_detect/m5/esp32_pico.inl index 2dd7618a..5fc1e38b 100644 --- a/src/board_detect/m5/esp32_pico.inl +++ b/src/board_detect/m5/esp32_pico.inl @@ -182,7 +182,7 @@ private: if (down) { result->assign(&desc_atomvoice); // Microphone DATA remains high with G23 low. - if (!(up1 && down2 && up2)) { result->transient_fallback = true; } + if (!(up1 && down2 && up2)) { result->provisional = true; } return true; } if (!measured_) @@ -211,17 +211,17 @@ private: esp_rom_delay_us(10); const bool matrix_pulls = lgfx::gpio_in(21) && lgfx::gpio_in(25); ctx.transaction->restore_start({ 21, 25 }); - if (!linked || !matrix_pulls) { result->transient_fallback = true; } + if (!linked || !matrix_pulls) { result->provisional = true; } } else if (ratio100 <= lite_limit * reference) { result->assign(&desc_atomlite); - if (!linked) { result->transient_fallback = true; } + if (!linked) { result->provisional = true; } } else { result->assign(&desc_atomu); - if (linked || up1 || down2 || up2) { result->transient_fallback = true; } + if (linked || up1 || down2 || up2) { result->provisional = true; } } return true; } diff --git a/src/board_detect/m5/esp32c6/c6_display.inl b/src/board_detect/m5/esp32c6/c6_display.inl index e939ac9d..8a211cf6 100644 --- a/src/board_detect/m5/esp32c6/c6_display.inl +++ b/src/board_detect/m5/esp32c6/c6_display.inl @@ -79,13 +79,9 @@ public: &value, 1, specs::unitc6l::i2c_ioe::i2c_freq, 0) || !is_pi4io(value)) { - // GPIO18 alone does not identify UnitC6L. Preserve a known UnitC6L - // only on the last retry, without caching the unverified fallback. - if (!ctx.final_attempt || ctx.hint != desc_unitc6l.def.id) { return false; } - result->assign(&desc_unitc6l); - result->transient_fallback = true; - ESP_LOGW("board_detect_m5", "UnitC6L PI4IO unanswered; using hinted board for this boot"); - return true; + // Presence and GPIO18 only suggest UnitC6L; never construct on a hint. + if (ctx.candidate == nullptr) { ctx.candidate = &desc_unitc6l.def; } + return false; } result->assign(&desc_unitc6l); return true; diff --git a/src/board_detect/m5/esp32p4/tab5.inl b/src/board_detect/m5/esp32p4/tab5.inl index d8e7ab26..2d8c36c4 100644 --- a/src/board_detect/m5/esp32p4/tab5.inl +++ b/src/board_detect/m5/esp32p4/tab5.inl @@ -89,7 +89,8 @@ private: } static void warn_hint_mismatch(const probe_ctx_t& ctx, const board_def_t* def) { - if ((ctx.hint == desc_tab5.def.id || ctx.hint == desc_tab5x.def.id) + if (ctx.attempt == 0 + && (ctx.hint == desc_tab5.def.id || ctx.hint == desc_tab5x.def.id) && ctx.hint != def->id) { esp_chip_info_t info; diff --git a/src/board_detect/m5/esp32s3/families.inl b/src/board_detect/m5/esp32s3/families.inl index de6e799c..0ee165e5 100644 --- a/src/board_detect/m5/esp32s3/families.inl +++ b/src/board_detect/m5/esp32s3/families.inl @@ -496,10 +496,10 @@ namespace m5 } } if (!stopwatch_touch && !ctx.final_attempt) { return false; } - if (stopwatch_touch || ctx.hint == desc_stopwatch.def.id) + if (stopwatch_touch) { // The provisional PaperMono sequence has run. Apply StopWatch power - // before selecting it, whether touch replied or the hint is used. + // before selecting the member confirmed by its touch response. board_result_t stopwatch; stopwatch.assign(&desc_stopwatch); if (!i2c.opened @@ -508,7 +508,7 @@ namespace m5 result.assign(&desc_stopwatch); } if (stopwatch_touch) { return true; } - result.transient_fallback = true; + result.provisional = true; ESP_LOGW("board_detect_m5", "StopWatch/PaperMono touch unanswered; using %s for this boot", result.def->name); return true; @@ -658,8 +658,8 @@ namespace m5 if (result == nullptr) { return false; } // The legacy block skipped the PM1 read when hinted PaperS3 and the // GT911 read when hinted PaperDIY; each skip saves up to 200 ms or two - // touch-controller transactions, so the hint keeps that meaning here. - if (ctx.hint != desc_papers3.def.id) + // touch-controller transactions. Restrict these skips to the first attempt. + if (ctx.attempt != 0 || ctx.final_attempt || ctx.hint != desc_papers3.def.id) { std::uint8_t pm1_id[2] = {}; if (probe_i2c_read(ctx, wiring::papers3::internal_i2c_sda, @@ -672,7 +672,7 @@ namespace m5 return true; } } - if (ctx.hint != desc_paperdiy.def.id) + if (ctx.attempt != 0 || ctx.final_attempt || ctx.hint != desc_paperdiy.def.id) { // An ACK alone is not enough to identify the PaperS3 touch controller // (DinMeter shares these pins): require the GT911 product ID string. @@ -765,17 +765,28 @@ namespace m5 wiring::cardputer_adv::internal_i2c_scl, 0x34); if (!variant_unanswered) { chosen = &desc_cardputer_adv; } } +#if defined (M5GFX_AUTODETECT_TEST_FAIL_CARDPUTER_VARIANT) + if ((pulls.pulldown_high & vameter_mask) == vameter_mask + || (pulls.pulldown_high & adv_mask) == adv_mask) + { variant_unanswered = true; } +#endif if (variant_unanswered) { if (!ctx.final_attempt) { return false; } - // A same-family NVS hint is a better last resort than the base model. - if (ctx.hint == desc_vameter.def.id - && (pulls.pulldown_high & vameter_mask) == vameter_mask) - { chosen = &desc_vameter; } - else if (ctx.hint == desc_cardputer_adv.def.id - && (pulls.pulldown_high & adv_mask) == adv_mask) - { chosen = &desc_cardputer_adv; } - result->transient_fallback = true; + // Only select members compatible with the observed subdivision pulls. + const auto possible = [&](board_id_t id) -> const board_desc_t* + { + if (id == desc_cardputer.def.id) { return &desc_cardputer; } + if (id == desc_vameter.def.id + && (pulls.pulldown_high & vameter_mask) == vameter_mask) { return &desc_vameter; } + if (id == desc_cardputer_adv.def.id + && (pulls.pulldown_high & adv_mask) == adv_mask) { return &desc_cardputer_adv; } + return nullptr; + }; + const auto* preferred = possible(ctx.preferred); + const auto* hinted = possible(ctx.hint); + chosen = preferred ? preferred : hinted ? hinted : &desc_cardputer; + result->provisional = true; ESP_LOGW("board_detect_m5", "Cardputer variant unanswered; using %s for this boot", chosen->def.name); } From 9d0b0d627420292a1d07c4ee3c94e7bbe71d4fec Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:23:04 +0000 Subject: [PATCH 03/10] Start a fixed BOARD_ID board without detection Add detect_config_t::fixed_board and getFixedBoard(). A fixed identity is resolved only against the chip construction table: no detector probing, no package filtering and no NVS access. Startup uses a clean result and a transaction over the board pins, keeps the accepted identity across display startup failures and retries with reset escalation on later init calls. An identity for another chip is rejected with a warning. Share read-only panel, PMIC and VBUS variant observations with family detection, and add fixed startup callbacks for Core2, Tough, CoreInk, CoreS3, AirQ and Atom displays in their required power order. Member selection by touch, camera, capacitance or firmware is omitted; failed variant reads keep the default panel with a warning. Core2-family startup stops when the PMIC identity is unreadable, and fixed Tough uses the observed AXP2101 backlight instead of writing AXP192 registers. Protect pins owned by active OPI memory, capture the StopWatch TE GPIO before construction, and warn when GPIO ownership cannot be acquired. Add host tests for pin guards, retry policy and the BOARD_ID selection guard. --- extras/board_spec/cli/spec.js | 1 + extras/board_spec/test/board_spec.test.js | 6 +- extras/board_spec/test/detect_attempt.test.js | 5 + extras/board_spec/test/fixed_board.test.js | 401 ++++++++++++++++++ src/M5GFX.cpp | 224 ++++++++-- src/M5GFX.h | 4 + src/board_detect/board_detect.hpp | 1 + src/board_detect/board_detect.inl | 123 ++++-- src/board_detect/m5/board_registry.inl | 17 + src/board_detect/m5/esp32_d0wdq6.inl | 57 ++- src/board_detect/m5/esp32_d0wdq6_setup.inl | 5 +- src/board_detect/m5/esp32_pico.inl | 2 + src/board_detect/m5/esp32s3/cores3.inl | 51 ++- src/board_detect/m5/esp32s3/families.inl | 26 +- .../m5/esp32s3/families_setup.inl | 2 +- 15 files changed, 816 insertions(+), 109 deletions(-) create mode 100644 extras/board_spec/test/fixed_board.test.js diff --git a/extras/board_spec/cli/spec.js b/extras/board_spec/cli/spec.js index 2c49332e..b0702bcf 100644 --- a/extras/board_spec/cli/spec.js +++ b/extras/board_spec/cli/spec.js @@ -437,6 +437,7 @@ class M5Unified { public: static int8_t _get_pin_table[${PIN_NAMES.length}]; static void _setup_pinmap(board_t); + static int8_t _get_power_hold_pin(board_t); }; int8_t M5Unified::_get_pin_table[${PIN_NAMES.length}]; ${tableSource} diff --git a/extras/board_spec/test/board_spec.test.js b/extras/board_spec/test/board_spec.test.js index 0e0df1c9..de19cba9 100644 --- a/extras/board_spec/test/board_spec.test.js +++ b/extras/board_spec/test/board_spec.test.js @@ -640,7 +640,7 @@ test("explicit fallback takes precedence over detector candidates", async (t) => assert.ok(explicit >= 0 && explicit < candidate && candidate < build); assert.match(selection, /if \(!board_detected\)/); const defaults = await fs.readFile(path.join(m5unified, "src/M5Unified.inl"), "utf8"); - assert.match(defaults, /BOARD_ID == 147[\s\S]*?return board_t::board_M5DualKey;/); + assert.doesNotMatch(defaults, /BOARD_ID/); assert.match(defaults, /get_pkg_ver\(\) == 1\) \{ return board_t::board_M5StampS3Mini; \}/); }); @@ -1628,7 +1628,7 @@ test("embedded autodetect routes detected boards through descriptor setup", asyn test("confirmed boards tolerate reset-list faults while confirm remains strict", async () => { const implementation = await fs.readFile(path.join(root, "../../src/board_detect/board_detect.inl"), "utf8"); const esp32 = await fs.readFile(path.join(root, "../../src/board_detect/m5/esp32_d0wdq6.inl"), "utf8"); - assert.match(implementation, /prepare_reset\(\*current, result, ctx, i2c\.port, nullptr, true\)/); + assert.match(implementation, /prepare_reset\(\*current, result, ctx, i2c\.port,\s*nullptr, true\)/); assert.match(implementation, /variant_confirmed && desc\.power\.variant_count == 1[\s\S]*?&desc\.power\.variants\[0\]/); assert.match(implementation, /if \(!retain_confirmed_board\) \{ return false; \}[\s\S]*?reset_release stopped after board confirmation: op=%u status=%u native=%d/); assert.match(implementation, /reset_assert stopped after board confirmation: op=%u status=%u native=%d/); @@ -2466,7 +2466,7 @@ test("every descriptor operation GPIO is inside its execution and rollback scope ["papercolor", "papercolor_power_on"], ]) { assert.match(esp32s3Source, new RegExp( - `ops::list\\(pmic_ops::${sequence}\\)[\\s\\S]*?desc_${boardName} = \\{[\\s\\S]*?pins\\(wiring::${boardName}::hold\\)[\\s\\S]*?pins\\(wiring::${boardName}::hold\\)`, + `ops::list\\(pmic_ops::${sequence}\\)[\\s\\S]*?desc_${boardName} = \\{[\\s\\S]*?pins\\(wiring::${boardName}::hold\\)[\\s\\S]*?pins\\(${boardName === "stopwatch" ? "stopwatch_startup_pins" : `wiring::${boardName}::hold`}\\)`, )); for (const pin of gpioPins[sequence]) assert.ok(s3Wiring[boardName].hold.includes(pin), `${boardName} GPIO${pin}`); } diff --git a/extras/board_spec/test/detect_attempt.test.js b/extras/board_spec/test/detect_attempt.test.js index 604dff97..fb8c723d 100644 --- a/extras/board_spec/test/detect_attempt.test.js +++ b/extras/board_spec/test/detect_attempt.test.js @@ -12,6 +12,7 @@ import { body, compileRun } from "./detect_host_helpers.js"; test("production attempt yields before construction and preserves adoption and rollback semantics", async () => { const main = await fs.readFile(path.join(src, "M5GFX.cpp"), "utf8"); const attempt = body(main, "static board_detect::detect_outcome_t run_detection_attempt("); + const finish = body(main, "static board_detect::detect_outcome_t finish_detection_setup("); const compat = body(main, "board_t M5GFX::autodetect("); const projection = main.slice(main.indexOf(" const auto board = outcome.setup_succeeded"), main.indexOf("#if defined ( ARDUINO_M5STACK_ATOM )", main.indexOf(" const auto board = outcome.setup_succeeded"))); const header = await fs.readFile(path.join(src, "M5GFX.h"), "utf8"); @@ -58,6 +59,7 @@ log_t success_log(const board_result_t&) {return {nullptr,nullptr};} } } } const int probe_i2c_port=0; +template board_detect::detect_outcome_t finish_detection_setup(board_detect::board_result_t& result, board_detect::detect_outcome_t outcome, board_detect::detection_transaction_t& transaction, board_t setup_board, SetupDetected setup) ${finish} template board_detect::detect_outcome_t run_detection_attempt(const board_detect::board_detector_t* const* detectors, const board_detect::detect_request_t& request, bool final_attempt, SetupDetected setup) ${attempt} struct package_t {const board_detect::board_detector_t* const* detectors;bool conditional_pins_unavailable=false;}; package_t active_package; @@ -167,6 +169,9 @@ int main(){M5GFX gfx;detect_config_t cfg;assert(gfx.getDetectConfig().fallback_b gfx.adopted=board_t::member;cfg.fallback_board=board_t::other;gfx.setDetectConfig(cfg); assert(warnings==1);assert(gfx.adopted==board_t::member);assert(gfx.getDetectConfig().fallback_board==board_t::other); cfg.fallback_board=board_t::board_unknown;gfx.setDetectConfig(cfg);assert(gfx.getDetectConfig().fallback_board==board_t::board_unknown);assert(warnings==2); + cfg.fixed_board=board_t::member;gfx.setDetectConfig(cfg);assert(warnings==3); + gfx.setDetectConfig(cfg);assert(warnings==3);assert(gfx.adopted==board_t::member); + cfg.fixed_board=board_t::other;gfx.setDetectConfig(cfg);assert(warnings==4); } `,"public config"); }); diff --git a/extras/board_spec/test/fixed_board.test.js b/extras/board_spec/test/fixed_board.test.js new file mode 100644 index 00000000..f6e457c3 --- /dev/null +++ b/extras/board_spec/test/fixed_board.test.js @@ -0,0 +1,401 @@ +import assert from "node:assert/strict"; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { body, compileRun } from "./detect_host_helpers.js"; +const src = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../src"); + +test("fixed acceptance bypasses NVS and detection and retries clean startup after failure", async () => { + const main = await fs.readFile(path.join(src, "M5GFX.cpp"), "utf8"); + const registry = await fs.readFile(path.join(src, "board_detect/m5/board_registry.inl"), "utf8"); + const fixed = body(main, "static board_detect::detect_outcome_t run_fixed_detection("); + const session = body(main, "static board_detect::detect_outcome_t run_fixed_detection_session("); + const select = body(main, "const board_detect::m5::board_entry_t* select_fixed_board("); + const finish = body(main, "static board_detect::detect_outcome_t finish_detection_setup("); + const find = body(registry, "const board_entry_t* find_board("); + const start = main.indexOf(" _detect_started = true;", main.indexOf("bool M5GFX::init_impl(")); + const prefix = main.slice(start, main.indexOf(" static constexpr char NVS_KEY", start)); + const getterSource = await fs.readFile(path.join(src, "M5GFX.h"), "utf8"); + const fixedGetter = body(getterSource, "board_t getFixedBoard("); + await compileRun(` +#include "board_detect/detect_session.hpp" +#include +#include +#include +#include +#include +#define CONFIG_IDF_TARGET 1 +#define CONFIG_IDF_TARGET_ESP32S3 1 +#define ESP_LOGW(...) (++warnings) +#define ESP_LOGI(...) ((void)0) +using namespace m5gfx; +enum class board_t:std::uint32_t {board_unknown=0,member=1,unsupported=2}; +int warnings,nvs_calls,detect_calls,construct_calls,adopt_calls,finish_calls,rollback_calls,commit_calls,start_calls; +bool prepare_ok,construct_ok,adopt_ok,displayless; +bool opi_active=false,description_ok=true,capture_ok=true;int captures; +bool conditional_detection_pins_unavailable(){return opi_active;} +std::vector reset_flags;int startup_countdown=0; +namespace lgfx {namespace i2c {bool isInitialized(int) {return false;}}} +namespace m5gfx {namespace board_detect { +constexpr int max_detection_pins=64; +struct list_t {const std::int8_t* data;std::size_t size;}; +using pin_list_t=list_t; +struct board_desc_t { + board_def_t def; + struct {int hold_pin;} power; + struct {int pin;} reset; + struct {int sclk,mosi,miso,dc,cs,rst,busy;} display; + struct {int sclk,mosi,miso,sd_cs,other_cs;} sd; + struct {int sda,scl,hw_port;} internal_i2c; + list_t hold_high_pins,op_gpio_pins; +}; +void board_result_t::assign(const board_desc_t* value) {desc=value;def=value?&value->def:&board_def_unknown;} +struct prepare_ctx_t {bool allow_reset=true;int i2c_port_probe=-1;detection_transaction_t* transaction=nullptr;}; +inline pin_list_t no_pins() {return {nullptr,0};} +struct detection_transaction_t { + detection_transaction_t(pin_list_t pins,pin_list_t,bool) { + ++captures;for(std::size_t i=0;i=0);} + } + bool valid() {return capture_ok;} + void rollback() {++rollback_calls;}void commit(){++commit_calls;} + void restore_start(const std::int8_t*,std::size_t) {} + struct buses_t {void opened_i2c(int){}} buses_; + buses_t& buses(){return buses_;} +}; +namespace startup_detail {bool description_valid(const board_desc_t&) {return description_ok;}} +bool prepare(const board_desc_t&,board_result_t& result,const prepare_ctx_t& ctx) { + reset_flags.push_back(ctx.allow_reset); + assert(result.option==0&&result.prepared==0&&!result.provisional&&result.refine==nullptr&&result.candidate==nullptr); + ++start_calls;result.option=4;result.prepared=prepared_power;return prepare_ok||(startup_countdown>0&&--startup_countdown==0); +} +namespace m5 { +namespace wiring {namespace detection {const std::int8_t opi_pins[]={33,34,35,36,37};}} +struct display_parts_t {int* bus=nullptr;int* panel=nullptr;int* light=nullptr;int* touch=nullptr;}; +enum class construct_status_t {ok,no_display,failed}; +using start_t=bool(*)(board_result_t&,const prepare_ctx_t&); +struct board_entry_t {const board_desc_t* desc;start_t fixed_start;}; +template const board_entry_t* find_board(const board_entry_t (&boards)[BoardCount],board_id_t id) ${find} +board_desc_t desc={{1,"member",0},{0},{1},{2,3,4,5,6,1,7},{-1,-1,-1,-1,-1},{-1,-1,-1},{nullptr,0},{nullptr,0}}; +const board_entry_t entries[]={{&desc,nullptr}}; +const board_entry_t (&esp32s3_boards)[1]=entries; +construct_status_t setup_detected_board(const board_result_t& result,display_parts_t*) { + assert(result.desc==&desc&&result.def==&desc.def&&result.option==4&&result.prepared==prepared_power&&!result.provisional&&result.refine==nullptr); + ++construct_calls;return !construct_ok?construct_status_t::failed:displayless?construct_status_t::no_display:construct_status_t::ok; +} +void destroy_display_parts(display_parts_t*) {} +struct log_t {const char* name;const char* annotation;}; +log_t success_log(const board_result_t&){return {nullptr,nullptr};} +} +}} +const int probe_i2c_port=-1; +template board_detect::detect_outcome_t finish_detection_setup(board_detect::board_result_t& result,board_detect::detect_outcome_t outcome,board_detect::detection_transaction_t& transaction,board_t setup_board,SetupDetected setup) ${finish} +template board_detect::detect_outcome_t run_fixed_detection(const board_detect::m5::board_entry_t& entry,bool allow_reset,SetupDetected setup) ${fixed} +template board_detect::detect_outcome_t run_fixed_detection_session(const board_detect::m5::board_entry_t& entry,bool allow_reset,SetupDetected setup) ${session} +const board_detect::m5::board_entry_t* select_fixed_board(board_t board) ${select} +bool reject_detected_setup(board_t){return false;} +struct M5GFX { + bool _detect_started=false; + board_t _board=board_t::board_unknown,_board_candidate=board_t::member,_fixed_board=board_t::board_unknown; + struct {board_t fixed_board=board_t::board_unknown,fallback_board=board_t::board_unknown;} _detect_config; + board_t getBoard() const {return _board;} + board_t getFixedBoard() const ${fixedGetter} + bool _adopt_detected_parts(int*,int*,int*,int*){++adopt_calls;return adopt_ok;} + bool _finish_detected_init(bool){++finish_calls;return true;} + bool init(bool use_reset=true,bool use_clear=true) { + ${prefix} + ++nvs_calls;++detect_calls;return false; + } +}; +int main() { + using namespace board_detect; + using board_detect::m5::desc; + prepare_ok=construct_ok=adopt_ok=true; + M5GFX unsupported;unsupported._detect_config.fixed_board=board_t::unsupported; + assert(!unsupported.init()&&unsupported.getFixedBoard()==board_t::board_unknown); + assert(nvs_calls==0&&detect_calls==0&&start_calls==0); + for(int failure=0;failure<3;++failure) { + M5GFX gfx;gfx._detect_config.fixed_board=board_t::member; + prepare_ok=failure!=0;construct_ok=failure!=1;adopt_ok=failure!=2; + assert(!gfx.init());assert(gfx.getBoard()==board_t::board_unknown&&gfx.getFixedBoard()==board_t::member); + prepare_ok=construct_ok=adopt_ok=true; + assert(gfx.init());assert(gfx.getBoard()==board_t::member&&gfx.getFixedBoard()==board_t::member); + assert(gfx._board_candidate==board_t::board_unknown); + assert(nvs_calls==0&&detect_calls==0); + } + assert(commit_calls==3&&rollback_calls==15); + prepare_ok=false;reset_flags.clear();M5GFX retry;retry._detect_config.fixed_board=board_t::member; + assert(!retry.init(false));assert(reset_flags==std::vector({false,false,false,true,true}));prepare_ok=true; + prepare_ok=false;startup_countdown=4;reset_flags.clear();M5GFX transient;transient._detect_config.fixed_board=board_t::member; + assert(transient.init(false));assert(reset_flags==std::vector({false,false,false,true})); + assert(transient.getBoard()==board_t::member);prepare_ok=true; + const int old_captures=captures,old_starts=start_calls; + opi_active=true;desc.display.cs=33;M5GFX opi;opi._detect_config.fixed_board=board_t::member; + assert(!opi.init()&&opi.getFixedBoard()==board_t::member&&opi.getBoard()==board_t::board_unknown); + assert(captures==old_captures&&start_calls==old_starts);desc.display.cs=6; + assert(opi.init());opi_active=false; + description_ok=false;M5GFX invalid;invalid._detect_config.fixed_board=board_t::member; + int before_warnings=warnings;assert(!invalid.init());assert(warnings==before_warnings+5);description_ok=true; + capture_ok=false;M5GFX snapshot;snapshot._detect_config.fixed_board=board_t::member; + before_warnings=warnings;assert(!snapshot.init());assert(warnings==before_warnings+5);capture_ok=true; + std::int8_t excess[65];for(int i=0;i<65;++i)excess[i]=i;desc.op_gpio_pins={excess,65}; + M5GFX overflow;overflow._detect_config.fixed_board=board_t::member;before_warnings=warnings; + assert(!overflow.init());assert(warnings==before_warnings+5);desc.op_gpio_pins={nullptr,0}; + M5GFX wide;wide._detect_config.fixed_board=static_cast(0x10001); + assert(!wide.init()&&wide.getFixedBoard()==board_t::board_unknown); + displayless=true;M5GFX no_display;no_display._detect_config.fixed_board=board_t::member; + assert(no_display.init()&&no_display.getBoard()==board_t::member&&no_display.getFixedBoard()==board_t::member); + M5GFX initialized;initialized._board=board_t::member;initialized._detect_config.fixed_board=board_t::unsupported; + const int before=start_calls;assert(initialized.init()&&initialized.getFixedBoard()==board_t::board_unknown&&start_calls==before); + assert(nvs_calls==0&&detect_calls==0); + M5GFX auto_gfx;assert(!auto_gfx.init());assert(nvs_calls==1&&detect_calls==1); +} +`, "fixed entry and startup"); +}); + +test("fixed variant callbacks never invoke family identification", async () => { + const detector = await fs.readFile(path.join(src, "board_detect/board_detect.inl"), "utf8"); + const core = await fs.readFile(path.join(src, "board_detect/m5/esp32_d0wdq6.inl"), "utf8"); + const s3 = await fs.readFile(path.join(src, "board_detect/m5/esp32s3/cores3.inl"), "utf8"); + const families = await fs.readFile(path.join(src, "board_detect/m5/esp32s3/families.inl"), "utf8"); + const coreStart = body(core, "bool fixed_start_core(board_result_t& result, const prepare_ctx_t& ctx)\n {"); + assert.doesNotMatch(coreStart, /touch_|try_station|probe_pin_pulls|assign\(/); + const s3Start = body(s3, "bool fixed_start(board_result_t& result"); + assert.doesNotMatch(s3Start, /camera_id|probe_dedicated|firmware_reg|assign\(/); + for (const name of ["atoms3", "atoms3r", "airq"]) { + const start = body(families, `bool fixed_start_${name}(`); + assert.match(start, /fixed_start_spi_variant/); + assert.doesNotMatch(start, /camera|signature|detector|refine/); + } + const spiStart = body(detector, "bool fixed_start_spi_variant("); + assert.doesNotMatch(spiStart, /detector|candidate|assign\(/); +}); + +test("fixed Core2 and CoreS3 observe only construction variants in startup order", async () => { + const core = await fs.readFile(path.join(src,"board_detect/m5/esp32_d0wdq6.inl"),"utf8"); + const s3 = await fs.readFile(path.join(src,"board_detect/m5/esp32s3/cores3.inl"),"utf8"); + const coreStart=body(core,"bool fixed_start_core(board_result_t& result, const prepare_ctx_t& ctx)\n {"); + const s3Start=body(s3,"bool fixed_start(board_result_t& result"); + await compileRun(` +#include "board_detect/detect_types.hpp" +#include +#include +#include +#define ESP_LOGW(...) (++warnings) +using namespace m5gfx::board_detect; +std::vector sequence;bool pmic_known, new_pmic, lcd_e, powered;int warnings; +namespace m5gfx {namespace board_detect { +struct board_desc_t {board_def_t def;struct {int sda,scl;} internal_i2c;struct {std::int8_t dc,sclk,mosi,miso;} display;}; +void board_result_t::assign(const board_desc_t* d){desc=d;def=&d->def;} +struct detection_transaction_t {void restore_start(const std::int8_t(&)[4]){}}; +struct prepare_ctx_t {detection_transaction_t* transaction;int i2c_port_probe;}; +struct probe_ctx_t:prepare_ctx_t {}; +struct pmic_variant_t {std::uint32_t detected_option;}; +}} +namespace generated_options {namespace core2 {constexpr unsigned new_pmic=1,lcd_e=2;}} +constexpr unsigned vbus_5v=4; +enum class panel_variant_t {unknown,c,e}; +namespace startup_detail { +struct i2c_scope_t {bool opened=true;int port=0;i2c_scope_t(detection_transaction_t&,int,const decltype(board_desc_t::internal_i2c)&){}}; +bool prepare_power(const board_desc_t&,board_result_t& r,int,bool) {sequence.push_back(2);r.prepared|=prepared_power;return powered;} +} +const pmic_variant_t* observe_core_pmic(int){sequence.push_back(1);static pmic_variant_t p;p.detected_option=new_pmic?1:0;return pmic_known?&p:nullptr;} +bool observe_core_panel(board_result_t&,const prepare_ctx_t&,panel_variant_t& v,std::uint32_t[4]){sequence.push_back(3);v=lcd_e?panel_variant_t::e:panel_variant_t::unknown;return true;} +void log_panel_variant(panel_variant_t,const std::uint32_t[4]){} +bool prepare(const board_desc_t&,board_result_t&,const prepare_ctx_t&){sequence.push_back(4);return true;} +std::uint32_t observe_vbus(probe_ctx_t&){sequence.push_back(1);return vbus_5v;} +void enable_bus_out(const prepare_ctx_t&){sequence.push_back(3);} +bool refine_panel(board_result_t& r,const prepare_ctx_t&){sequence.push_back(5);if(lcd_e)r.option|=2;return true;} +bool fixed_core(board_result_t& result,const prepare_ctx_t& ctx) ${coreStart} +bool fixed_s3(board_result_t& result,const prepare_ctx_t& ctx) ${s3Start} +int main(){ + const board_desc_t desc={{1,"fixed",0},{0,1},{2,3,4,5}};detection_transaction_t tx;prepare_ctx_t ctx={&tx,-1}; + board_result_t r;r.assign(&desc);pmic_known=false;powered=true; + assert(!fixed_core(r,ctx));assert(sequence==std::vector({1}));assert(warnings==1); + for(bool pmic:{false,true})for(bool panel:{false,true}){ + sequence.clear();r={};r.assign(&desc);pmic_known=true;new_pmic=pmic;lcd_e=panel; + assert(fixed_core(r,ctx));assert(sequence==std::vector({1,2,3,4})); + assert(r.option==unsigned((pmic?1:0)|(panel?2:0)));assert(r.def==&desc.def&&!r.provisional&&r.refine==nullptr); + } + sequence.clear();r={};r.assign(&desc);lcd_e=true; + assert(fixed_s3(r,ctx));assert(sequence==std::vector({1,2,3,5,4})); + assert(r.option==(vbus_5v|2));assert(r.def==&desc.def&&r.refine==nullptr); +} +`,"fixed construction variants"); +}); + +test("M5Unified BOARD_ID guard accepts positive values without M5GFX_BOARD and fixed selection wins", async (t) => { + const unified=process.env.M5UNIFIED_PATH||path.resolve(src,"../../M5Unified"); + let header;try{header=await fs.readFile(path.join(unified,"src/M5Unified.hpp"),"utf8");}catch{return t.skip("M5Unified checkout absent");} + const guard=[...header.matchAll(/#if defined \(BOARD_ID\) && \(\(BOARD_ID \+ 0\) > 0\)[\s\S]*?#endif/g)].map(m=>m[0]).find(s=>s.includes("detect_config.fixed_board")); + assert.ok(guard); + const start=header.indexOf(" const auto fixed_board = Display.getFixedBoard();"); + const end=header.indexOf(" _board = board;",start)+" _board = board;".length; + const selection=header.slice(start,end); + const definitions=["","#define BOARD_ID","#define BOARD_ID 0","#define BOARD_ID 10"]; + for(const define of definitions){ + await compileRun(` +#include +#include +${define} +enum class board_t{board_unknown=0,fixed=1,fallback=2,candidate=3,default_board=4}; +struct config_t {board_t fixed_board=board_t::board_unknown;}; +int main(){config_t detect_config; +${guard} +assert(static_cast(detect_config.fixed_board)==${define.includes("10")?10:0});} +`,"BOARD_ID guard"); + } + await compileRun(` +#include +#include +#define ESP_LOG_WARN 1 +enum class board_t{board_unknown=0,fixed=1,fallback=2,candidate=3,default_board=4}; +int warnings;void Log(int,const char*,unsigned,unsigned){++warnings;} +struct display_t { + board_t fixed,adopted,candidate; + board_t getFixedBoard(){return fixed;}board_t getBoard(){return adopted;}board_t getBoardCandidate(){return candidate;} +}; +struct unified_t { + display_t Display;struct {board_t fallback_board;} cfg; + board_t _board; + board_t _default_fallback_board(){return board_t::default_board;} + void select(){ + ${selection} + } +}; +int main(){for(int fixed=0;fixed<2;++fixed)for(int adopted=0;adopted<2;++adopted)for(int fallback=0;fallback<2;++fallback)for(int candidate=0;candidate<2;++candidate){ + warnings=0;unified_t u;u.Display={fixed?board_t::fixed:board_t::board_unknown,adopted?board_t::fallback:board_t::board_unknown,candidate?board_t::candidate:board_t::board_unknown};u.cfg.fallback_board=fallback?board_t::fallback:board_t::board_unknown; + u.select();assert(u._board==(fixed?board_t::fixed:adopted||fallback?board_t::fallback:candidate?board_t::candidate:board_t::default_board)); + assert(warnings==(fixed&&fallback)); +}} +`,"fixed and fallback selection"); +}); + +test("fixed Tough backlight follows observed PMIC while its panel and touch remain fixed", async () => { + const setup=await fs.readFile(path.join(src,"board_detect/m5/esp32_d0wdq6_setup.inl"),"utf8"); + const construct=body(setup,"construct_status_t construct_tough("); + await compileRun(` +#include "board_detect/detect_types.hpp" +#include +#include +#include +using namespace m5gfx::board_detect; +int selected; +namespace lgfx {struct ILight {virtual ~ILight()=default;};struct Touch_CHSC6540 {}; } +struct Light_M5StackCore2_AXP2101:lgfx::ILight {Light_M5StackCore2_AXP2101(){selected=1;}}; +struct Light_M5Tough:lgfx::ILight {Light_M5Tough(){selected=2;}}; +struct panel_t {void touch(lgfx::Touch_CHSC6540*){}}; +struct display_parts_t{}; +struct display_parts_owner_t { + std::unique_ptr light; + std::unique_ptr touch; + std::unique_ptr panel; + bool release_to(display_parts_t*){return true;} +}; +namespace generated_options {namespace core2 {constexpr unsigned new_pmic=1;}namespace tough {constexpr unsigned lcd_e=2;}} +constexpr int bus_tough=0,touch_tough=0; +templateT* make_default_part(){return new T();} +templateT* make_i2c_touch(int){return new T();} +void construct_core_panel(const board_result_t&,unsigned,int,display_parts_owner_t* out){out->panel.reset(new panel_t);} +using construct_status_t=bool;bool construct_status(bool v){return v;} +construct_status_t construct_tough(const board_result_t& result,display_parts_t* parts) ${construct} +int main(){for(unsigned option=0;option<2;++option){board_result_t r;r.option=option;display_parts_t p;assert(construct_tough(r,&p));assert(selected==(option?1:2));}} +`,"Tough PMIC backlight"); +}); + +test("Paper IT8951 initialization waits for BUSY before sending panel commands", async () => { + const panel=await fs.readFile(path.join(src,"lgfx/v1/panel/Panel_IT8951.inl"),"utf8"); + const init=body(panel,"bool Panel_IT8951::init("); + assert.ok(init.indexOf("_wait_busy();")>=0&&init.indexOf("_wait_busy();") { + const source=await fs.readFile(path.join(src,"board_detect/board_detect.inl"),"utf8"); + const observe=body(source,"bool observe_spi_variant("); + const start=body(source,"bool fixed_start_spi_variant("); + await compileRun(` +#include "board_detect/detect_types.hpp" +#include +#include +#define ESP_LOGW(...) (++warnings) +using namespace m5gfx::board_detect; +int mode,reads,warnings,prepares; +namespace m5gfx {namespace board_detect { +struct board_desc_t {board_def_t def;struct {std::int8_t sclk,mosi,miso,dc,cs;} display;}; +void board_result_t::assign(const board_desc_t* d){desc=d;def=&d->def;} +struct detection_transaction_t {void restore_start(const std::int8_t(&pins)[4]){for(int i=0;i<4;++i)assert(pins[i]>=2&&pins[i]<=5);}}; +struct prepare_ctx_t {detection_transaction_t* transaction;}; +struct probe_ctx_t:prepare_ctx_t {}; +struct spi_id_probe_t {unsigned cmd,dummy_bits,mask;const std::uint32_t* values;unsigned value_count,option_bit;}; +struct spi_id_member_t {const board_desc_t* desc;const spi_id_probe_t* probes;unsigned probe_count;bool three_wire;std::uint8_t slow_retry_half_us;bool legacy_zero_preamble;}; +}} +bool prepare(const board_desc_t&,board_result_t& r,const prepare_ctx_t&){++prepares;r.prepared=prepared_power|prepared_reset;return true;} +std::uint32_t soft_spi_read32(probe_ctx_t&,int sclk,int mosi,int miso,int dc,int cs,unsigned,unsigned,unsigned half,bool){ + assert(sclk==2&&mosi==3&&miso==3&&dc==5&&cs==6);assert(half==1||half==5);++reads; + return mode==0?0xffffffff:mode==1?0x11:mode==3&&reads==1?0xffffffff:0x22; +} +bool observe_spi_variant(probe_ctx_t& ctx,const board_desc_t& desc,const spi_id_probe_t* probes,std::size_t probe_count,std::uint32_t* option,bool three_wire,std::uint8_t slow_retry_half_us,bool legacy_zero_preamble) ${observe} +bool fixed_start_spi_variant(board_result_t& result,const prepare_ctx_t& ctx,const spi_id_member_t& member) ${start} +int main(){ + const board_desc_t desc={{1,"fixed display",0},{2,3,4,5,6}}; + const std::uint32_t standard[]={0x11},alternate[]={0x22}; + const spi_id_probe_t probes[]={{4,1,0xff,standard,1,0},{4,1,0xff,alternate,1,1}}; + const spi_id_member_t member={&desc,probes,2,true,5,false};detection_transaction_t tx;prepare_ctx_t ctx={&tx}; + for(mode=0;mode<4;++mode){board_result_t r;r.assign(&desc);reads=warnings=prepares=0; + assert(fixed_start_spi_variant(r,ctx,member));assert(r.def==&desc.def&&r.option==unsigned(mode>=2)); + assert(prepares==1&&reads==(mode==0||mode==3?2:1)&&warnings==(mode==0)); + assert(r.prepared==(prepared_power|prepared_reset)&&!r.provisional&&r.refine==nullptr); + } +} +`,"fixed SPI-only observation"); +}); + +test("fixed GPIO46 pre-hold follows the existing power pin table", async () => { + const unified=process.env.M5UNIFIED_PATH||path.resolve(src,"../../M5Unified"); + const header=await fs.readFile(path.join(unified,"src/M5Unified.hpp"),"utf8"); + const impl=await fs.readFile(path.join(unified,"src/M5Unified.inl"),"utf8"); + const table=/static constexpr const uint8_t _pin_table_other1\[\]\[2\] = \{[\s\S]*?\n\};/.exec(impl)[0]; + const getter=body(impl,"int8_t M5Unified::_get_power_hold_pin(board_t id)"); + const guard=[...header.matchAll(/#if defined \(BOARD_ID\) && \(\(BOARD_ID \+ 0\) > 0\)[\s\S]*?#endif/g)].map(m=>m[0]).find(s=>s.includes("gpio46_hold")); + for(const fixed of [undefined,0,1,2,3,4,5,6]) { + await compileRun(` +#include +#include +#define CONFIG_IDF_TARGET_ESP32S3 1 +#define GPIO_NUM_46 46 +#define GPIO_NUM_44 44 +${fixed===undefined?"":"#define BOARD_ID "+fixed} +struct board_t {enum value {board_unknown=0,board_M5Dial=1,board_M5Capsule=2,board_M5AirQ=3,board_M5DinMeter=4,board_M5PaperS3=5,board_M5AtomS3=6};uint8_t id;board_t(int v):id(v){} operator uint8_t() const{return id;}}; +namespace m5gfx {using ::board_t;} +${table} +struct M5Unified { + static int8_t _get_power_hold_pin(board_t id) ${getter} + struct {int board=0;int getBoard(){return board;}} Display; + bool hold(){ + ${guard} + return gpio46_hold; + } +}; +int main(){M5Unified u;assert(u.hold()==${fixed===undefined||fixed===0||(fixed>=1&&fixed<=4)?"true":"false"});u.Display.board=6;assert(!u.hold());assert(M5Unified::_get_power_hold_pin(board_t(99))==-1);} +`,"fixed power hold"); + } +}); + +test("fixed Stack startup no longer collects unused IPS options", async () => { + const declaration=await fs.readFile(path.join(src,"board_detect/board_detect.hpp"),"utf8"); + const implementation=await fs.readFile(path.join(src,"board_detect/board_detect.inl"),"utf8"); + const main=await fs.readFile(path.join(src,"M5GFX.cpp"),"utf8"); + const normal=await fs.readFile(path.join(src,"board_detect/m5/esp32_d0wdq6.inl"),"utf8"); + assert.doesNotMatch(declaration+implementation+main,/collect_reset_option/); + assert.match(normal,/stack_reset_and_sample_ips/); + assert.doesNotMatch(body(main,"static board_detect::detect_outcome_t run_fixed_detection("),/stack::ips|stack_reset_and_sample_ips/); +}); + + test("StopWatch constructor GPIO is included in its rollback pins",async()=>{ + const desc=await fs.readFile(path.join(src,"board_detect/m5/esp32s3/families.inl"),"utf8"); + const setup=await fs.readFile(path.join(src,"board_detect/m5/esp32s3/families_setup.inl"),"utf8"); + assert.match(desc,/stopwatch_te_pin = GPIO_NUM_38/); + assert.match(desc,/stopwatch_startup_pins\[\] = \{[\s\S]*?stopwatch_te_pin/); + assert.match(body(setup,"construct_status_t construct_stopwatch("),/pinMode\(stopwatch_te_pin/); + assert.match(desc,/desc_stopwatch = \{[\s\S]*?pins\(stopwatch_startup_pins\)/); + }); diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 4275a32f..e4c96a78 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -845,6 +845,30 @@ namespace m5gfx return package; } + static const board_detect::m5::board_entry_t* select_fixed_board(board_t board) + { + using namespace board_detect::m5; +#if !defined(CONFIG_IDF_TARGET) || defined(CONFIG_IDF_TARGET_ESP32) + const auto& boards = esp32_d0wdq6_boards; +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + const auto& boards = esp32s3_boards; +#elif defined(CONFIG_IDF_TARGET_ESP32C3) + const auto& boards = esp32c3_boards; +#elif defined(CONFIG_IDF_TARGET_ESP32C5) + const auto& boards = esp32c5_boards; +#elif defined(CONFIG_IDF_TARGET_ESP32C6) + const auto& boards = esp32c6_boards; +#elif defined(CONFIG_IDF_TARGET_ESP32C61) + const auto& boards = esp32c61_boards; +#elif defined(CONFIG_IDF_TARGET_ESP32H2) + const auto& boards = esp32h2_boards; +#elif defined(CONFIG_IDF_TARGET_ESP32P4) + const auto& boards = esp32p4_boards; +#endif + const auto id = static_cast(board); + return static_cast(board) == id ? find_board(boards, id) : nullptr; + } + static bool reject_detected_setup(board_t setup_board) { (void)setup_board; @@ -903,6 +927,52 @@ namespace m5gfx } + template + static board_detect::detect_outcome_t finish_detection_setup( + board_detect::board_result_t& result, board_detect::detect_outcome_t outcome, + board_detect::detection_transaction_t& transaction, + board_t setup_board, SetupDetected setup) + { + board_detect::m5::display_parts_t parts; + const auto construct_result = board_detect::m5::setup_detected_board(result, &parts); + if (construct_result == board_detect::m5::construct_status_t::failed) + { + ESP_LOGW(LIBRARY_NAME, "[Autodetect] setup failed for detected board:%u", + static_cast(result.def->id)); + outcome.result = result; + outcome.reason = board_detect::fail_reason_t::construct_failed; + transaction.rollback(); + return outcome; + } + if (construct_result == board_detect::m5::construct_status_t::no_display) + { + ESP_LOGI(LIBRARY_NAME, "[Autodetect] display is unavailable for detected board:%u", + static_cast(result.def->id)); + transaction.restore_start(result.desc->hold_high_pins.data, + result.desc->hold_high_pins.size); + } + if (!setup(parts, setup_board)) + { + // A rejecting adopter must not retain any of these raw pointers. Release + // the bus before rollback restores its GPIO routing, then destroy every + // constructed part so repeated failure injection cannot leak them. + board_detect::m5::destroy_display_parts(&parts); + ESP_LOGW(LIBRARY_NAME, "[Autodetect] setup failed for detected board:%u", + static_cast(result.def->id)); + outcome.result = result; + outcome.reason = board_detect::fail_reason_t::adopt_failed; + transaction.rollback(); + return outcome; + } + transaction.commit(); + outcome.result = result; + outcome.setup_succeeded = true; + const auto log = board_detect::m5::success_log(result); + if (log.name != nullptr && log.name[0] != '\0') + { ESP_LOGI(LIBRARY_NAME, "[Autodetect] %s%s", log.name, log.annotation); } + return outcome; + } + template static board_detect::detect_outcome_t run_detection_attempt( const board_detect::board_detector_t* const* detectors, @@ -1007,43 +1077,108 @@ namespace m5gfx transaction.rollback(); return outcome; } - board_detect::m5::display_parts_t parts; - const auto construct_result = board_detect::m5::setup_detected_board(result, &parts); - if (construct_result == board_detect::m5::construct_status_t::failed) + return finish_detection_setup(result, outcome, transaction, setup_board, setup); + } + + // Fixed startup honors constructor/adopter hooks and observe_* injections (CORES3_FORCE_VBUS), but bypasses detector-only hooks. + template + static board_detect::detect_outcome_t run_fixed_detection( + const board_detect::m5::board_entry_t& entry, bool allow_reset, SetupDetected setup) + { + using namespace board_detect; + detect_outcome_t outcome; + const auto& desc = *entry.desc; + if (!startup_detail::description_valid(desc)) { - ESP_LOGW(LIBRARY_NAME, "[Autodetect] setup failed for detected board:%u", - static_cast(result.def->id)); - outcome.result = result; - outcome.reason = board_detect::fail_reason_t::construct_failed; - transaction.rollback(); + ESP_LOGW(LIBRARY_NAME, "Fixed board:%u has an invalid startup description", unsigned(desc.def.id)); return outcome; } - if (construct_result == board_detect::m5::construct_status_t::no_display) + std::int8_t owned[max_detection_pins]; + std::size_t count = 0; + bool complete = true; + const auto add_pin = [&](int pin) { - ESP_LOGI(LIBRARY_NAME, "[Autodetect] display is unavailable for detected board:%u", - static_cast(result.def->id)); - transaction.restore_start(result.desc->hold_high_pins.data, - result.desc->hold_high_pins.size); + if (pin < 0) { return; } + for (std::size_t i = 0; i < count; ++i) { if (owned[i] == pin) { return; } } + if (count == max_detection_pins) { complete = false; return; } + owned[count++] = static_cast(pin); + }; + add_pin(desc.power.hold_pin); add_pin(desc.reset.pin); + add_pin(desc.display.sclk); add_pin(desc.display.mosi); add_pin(desc.display.miso); + add_pin(desc.display.dc); add_pin(desc.display.cs); add_pin(desc.display.rst); + add_pin(desc.display.busy); + add_pin(desc.sd.sclk); add_pin(desc.sd.mosi); add_pin(desc.sd.miso); + add_pin(desc.sd.sd_cs); add_pin(desc.sd.other_cs); + add_pin(desc.internal_i2c.sda); add_pin(desc.internal_i2c.scl); + for (std::size_t i = 0; i < desc.hold_high_pins.size; ++i) { add_pin(desc.hold_high_pins.data[i]); } + for (std::size_t i = 0; i < desc.op_gpio_pins.size; ++i) { add_pin(desc.op_gpio_pins.data[i]); } + if (!complete) + { + ESP_LOGW(LIBRARY_NAME, "Fixed board:%u exceeds the startup pin limit", unsigned(desc.def.id)); + return outcome; } - if (!setup(parts, setup_board)) +#if defined(CONFIG_IDF_TARGET_ESP32S3) + if (conditional_detection_pins_unavailable()) { - // A rejecting adopter must not retain any of these raw pointers. Release - // the bus before rollback restores its GPIO routing, then destroy every - // constructed part so repeated failure injection cannot leak them. - board_detect::m5::destroy_display_parts(&parts); - ESP_LOGW(LIBRARY_NAME, "[Autodetect] setup failed for detected board:%u", - static_cast(result.def->id)); - outcome.result = result; - outcome.reason = board_detect::fail_reason_t::adopt_failed; - transaction.rollback(); + for (std::size_t i = 0; i < count; ++i) + { + for (const auto reserved : m5::wiring::detection::opi_pins) + { + if (owned[i] == reserved) + { + // Acceptance is chip-based; active memory ownership can prevent startup. + ESP_LOGW(LIBRARY_NAME, "Fixed board:%u needs active OPI PSRAM pin:%d", + unsigned(desc.def.id), int(reserved)); + return outcome; + } + } + } + } +#endif + detection_transaction_t transaction({ owned, static_cast(count) }, no_pins(), false); + if (!transaction.valid()) + { + ESP_LOGW(LIBRARY_NAME, "Fixed board:%u could not capture startup GPIO state", unsigned(desc.def.id)); return outcome; } - transaction.commit(); + // Fixed startup must not inherit a detected member's state or refinement. + board_result_t result{}; + result.assign(&desc); + prepare_ctx_t ctx; + ctx.allow_reset = allow_reset; + ctx.i2c_port_probe = probe_i2c_port; + ctx.transaction = &transaction; + const int port = desc.internal_i2c.hw_port; + const bool was_open = port >= 0 && lgfx::i2c::isInitialized(port); + const bool started = entry.fixed_start ? entry.fixed_start(result, ctx) + : prepare(desc, result, ctx); + if (port >= 0 && !was_open && lgfx::i2c::isInitialized(port)) + { transaction.buses().opened_i2c(port); } outcome.result = result; - outcome.setup_succeeded = true; - const auto log = board_detect::m5::success_log(result); - if (log.name != nullptr && log.name[0] != '\0') - { ESP_LOGI(LIBRARY_NAME, "[Autodetect] %s%s", log.name, log.annotation); } + outcome.verdict = verdict_t::confirmed; + outcome.attempts = 1; + if (!started) + { + outcome.reason = fail_reason_t::prepare_failed; + transaction.rollback(); + return outcome; + } + return finish_detection_setup(result, outcome, transaction, + static_cast(desc.def.id), setup); + } + + template + static board_detect::detect_outcome_t run_fixed_detection_session( + const board_detect::m5::board_entry_t& entry, bool allow_reset, SetupDetected setup) + { + board_detect::detect_outcome_t outcome; + for (std::uint8_t attempt = 0; attempt < 5; ++attempt) + { + if (attempt == 3) { allow_reset = true; } + outcome = run_fixed_detection(entry, allow_reset, setup); + outcome.attempts = attempt + 1; + if (outcome.setup_succeeded) { return outcome; } + } return outcome; } @@ -1062,7 +1197,8 @@ namespace m5gfx void M5GFX::setDetectConfig(const detect_config_t& config) { - const bool changed = _detect_config.fallback_board != config.fallback_board; + const bool changed = _detect_config.fallback_board != config.fallback_board + || _detect_config.fixed_board != config.fixed_board; _detect_config = config; #if defined (ESP_PLATFORM) if (_detect_started && changed) @@ -1101,6 +1237,29 @@ namespace m5gfx return true; } + _fixed_board = board_t::board_unknown; + if (_detect_config.fixed_board != board_t::board_unknown) + { + const auto* entry = select_fixed_board(_detect_config.fixed_board); + if (entry == nullptr) + { + ESP_LOGW(LIBRARY_NAME, "Fixed board:%u is not supported by this chip", + static_cast(_detect_config.fixed_board)); + return false; + } + // Acceptance is independent of display construction; a failed init may retry. + _fixed_board = _detect_config.fixed_board; + const auto outcome = run_fixed_detection_session(*entry, use_reset, + [this](board_detect::m5::display_parts_t& parts, board_t setup_board) + { + return !reject_detected_setup(setup_board) + && _adopt_detected_parts(parts.bus, parts.panel, parts.light, parts.touch); + }); + if (!outcome.setup_succeeded) { return false; } + _board = _fixed_board; + return _finish_detected_init(use_clear); + } + static constexpr char NVS_KEY[] = "AUTODETECT"; std::uint32_t nvs_board = 0; std::uint32_t nvs_handle = 0; @@ -1204,6 +1363,11 @@ namespace m5gfx } } + return _finish_detected_init(use_clear); + } + + bool M5GFX::_finish_detected_init(bool use_clear) + { // The new path performs every permitted reset in prepare(). Construction // and panel initialisation never pulse reset a second time. if (false == LGFX_Device::init_impl(false, use_clear)) { @@ -1211,7 +1375,7 @@ namespace m5gfx } #if defined (CONFIG_IDF_TARGET_ESP32S3) - switch (board) { + switch (_board) { default: break; diff --git a/src/M5GFX.h b/src/M5GFX.h index 0798963d..0b2b6b7f 100644 --- a/src/M5GFX.h +++ b/src/M5GFX.h @@ -62,6 +62,7 @@ namespace m5gfx struct detect_config_t { board_t fallback_board = board_t::board_unknown; + board_t fixed_board = board_t::board_unknown; }; //---------------------------------------------------------------------------- @@ -200,8 +201,10 @@ namespace m5gfx board_t _board_candidate = board_t::board_unknown; detect_config_t _detect_config; bool _detect_started = false; + board_t _fixed_board = board_t::board_unknown; bool init_impl(bool use_reset, bool use_clear) override; + bool _finish_detected_init(bool use_clear); board_t autodetect(bool use_reset = false, board_t board = board_t::board_unknown, bool final_attempt = false, bool* transient_fallback = nullptr, bool* no_signature = nullptr, @@ -218,6 +221,7 @@ namespace m5gfx static M5GFX* getInstance(void) { return _instance; } board_t getBoardCandidate(void) const { return _board_candidate; } + board_t getFixedBoard(void) const { return _fixed_board; } void setDetectConfig(const detect_config_t& config); const detect_config_t& getDetectConfig(void) const { return _detect_config; } diff --git a/src/board_detect/board_detect.hpp b/src/board_detect/board_detect.hpp index a36aa561..df3667b2 100644 --- a/src/board_detect/board_detect.hpp +++ b/src/board_detect/board_detect.hpp @@ -239,6 +239,7 @@ namespace board_detect option_list_t option_names; // GPIOs that typed operation lists may access. This is intentionally // separate from hold_high_pins: an operation may restore a pin to input. + // Also includes construction pins captured during fixed startup, beyond power-operation GPIO permissions. pin_list_t op_gpio_pins; }; diff --git a/src/board_detect/board_detect.inl b/src/board_detect/board_detect.inl index 76a0e516..fd789076 100644 --- a/src/board_detect/board_detect.inl +++ b/src/board_detect/board_detect.inl @@ -1583,49 +1583,11 @@ namespace board_detect return ok; } - bool probe_spi_id(probe_ctx_t& ctx, const board_desc_t& desc, - const spi_id_probe_t* probes, std::size_t probe_count, - board_result_t* result, bool three_wire, - std::uint8_t slow_retry_half_us, - bool legacy_zero_preamble) + bool observe_spi_variant(probe_ctx_t& ctx, const board_desc_t& desc, + const spi_id_probe_t* probes, std::size_t probe_count, + std::uint32_t* option, bool three_wire, + std::uint8_t slow_retry_half_us, bool legacy_zero_preamble) { - if (probes == nullptr || probe_count == 0 || result == nullptr - || !startup_detail::description_valid(desc) - || desc.display.cs < 0) { return false; } // no_display_pins() has no SPI ID - const std::int8_t pins[] = { - desc.display.cs, desc.display.sclk, desc.display.mosi, - desc.display.dc, desc.display.rst, - }; - const std::int8_t shared_sd_pins[] = { - desc.display.cs, desc.display.sclk, desc.display.mosi, - desc.display.dc, desc.display.rst, - desc.sd.sclk, desc.sd.mosi, desc.sd.miso, desc.sd.sd_cs, - }; - const auto restore_probe_pins = [&]() - { - if (desc.sd.sd_cs >= 0) { ctx.transaction->restore_start(shared_sd_pins); } - else { ctx.transaction->restore_start(pins); } - }; - board_result_t candidate; - candidate.assign(&desc); - const prepare_ctx_t& prepare_ctx = ctx; - if (desc.sd.sd_cs >= 0) - { - // Keep both devices deselected while the shared SD bus is switched to - // SPI mode. Descriptions without shared SD take the original path. - startup_detail::pin_level(desc.sd.sd_cs, true); - startup_detail::pin_level(desc.display.cs, true); - if (!startup_detail::prepare_sd_spi(desc, candidate, prepare_ctx)) - { - restore_probe_pins(); - return false; - } - } - if (!prepare_reset(desc, candidate, prepare_ctx, ctx.i2c_port_probe)) - { - restore_probe_pins(); - return false; - } const int read_pin = three_wire || desc.display.miso < 0 ? desc.display.mosi : desc.display.miso; std::uint8_t last_cmd = 0; @@ -1647,8 +1609,7 @@ namespace board_detect for (std::size_t value = 0; value < probe.value_count; ++value) { if ((id & probe.mask) != probe.values[value]) { continue; } - candidate.option = probe.option_bit; - *result = candidate; + *option = probe.option_bit; return true; } } @@ -1669,12 +1630,63 @@ namespace board_detect for (std::size_t value = 0; value < probe.value_count; ++value) { if ((id & probe.mask) != probe.values[value]) { continue; } - candidate.option = probe.option_bit; - *result = candidate; + *option = probe.option_bit; return true; } } } + return false; + } + + bool probe_spi_id(probe_ctx_t& ctx, const board_desc_t& desc, + const spi_id_probe_t* probes, std::size_t probe_count, + board_result_t* result, bool three_wire, + std::uint8_t slow_retry_half_us, + bool legacy_zero_preamble) + { + if (probes == nullptr || probe_count == 0 || result == nullptr + || !startup_detail::description_valid(desc) + || desc.display.cs < 0) { return false; } // no_display_pins() has no SPI ID + const std::int8_t pins[] = { + desc.display.cs, desc.display.sclk, desc.display.mosi, + desc.display.dc, desc.display.rst, + }; + const std::int8_t shared_sd_pins[] = { + desc.display.cs, desc.display.sclk, desc.display.mosi, + desc.display.dc, desc.display.rst, + desc.sd.sclk, desc.sd.mosi, desc.sd.miso, desc.sd.sd_cs, + }; + const auto restore_probe_pins = [&]() + { + if (desc.sd.sd_cs >= 0) { ctx.transaction->restore_start(shared_sd_pins); } + else { ctx.transaction->restore_start(pins); } + }; + board_result_t candidate; + candidate.assign(&desc); + const prepare_ctx_t& prepare_ctx = ctx; + if (desc.sd.sd_cs >= 0) + { + // Keep both devices deselected while the shared SD bus is switched to + // SPI mode. Descriptions without shared SD take the original path. + startup_detail::pin_level(desc.sd.sd_cs, true); + startup_detail::pin_level(desc.display.cs, true); + if (!startup_detail::prepare_sd_spi(desc, candidate, prepare_ctx)) + { + restore_probe_pins(); + return false; + } + } + if (!prepare_reset(desc, candidate, prepare_ctx, ctx.i2c_port_probe)) + { + restore_probe_pins(); + return false; + } + if (observe_spi_variant(ctx, desc, probes, probe_count, &candidate.option, + three_wire, slow_retry_half_us, legacy_zero_preamble)) + { + *result = candidate; + return true; + } restore_probe_pins(); return false; } @@ -1690,6 +1702,25 @@ namespace board_detect bool legacy_zero_preamble; }; + bool fixed_start_spi_variant(board_result_t& result, const prepare_ctx_t& ctx, + const spi_id_member_t& member) + { + if (!prepare(*result.desc, result, ctx)) { return false; } + probe_ctx_t probe; + static_cast(probe) = ctx; + if (!observe_spi_variant(probe, *result.desc, member.probes, member.probe_count, + &result.option, member.three_wire, + member.slow_retry_half_us, member.legacy_zero_preamble)) + { + ESP_LOGW("M5GFX", "Fixed board:%u panel variant unreadable; using default", + static_cast(result.def->id)); + } + const auto& display = result.desc->display; + const std::int8_t signals[] = { display.dc, display.sclk, display.mosi, display.miso }; + ctx.transaction->restore_start(signals); + return true; + } + class spi_id_detector_t final : public board_detector_t { public: diff --git a/src/board_detect/m5/board_registry.inl b/src/board_detect/m5/board_registry.inl index aaacbcfe..8b289f0c 100644 --- a/src/board_detect/m5/board_registry.inl +++ b/src/board_detect/m5/board_registry.inl @@ -21,14 +21,31 @@ namespace m5 using construct_fn_t = construct_status_t (*)(const board_result_t&, display_parts_t*); using success_annotation_fn_t = const char* (*)(const board_result_t&); + using fixed_start_fn_t = bool (*)(board_result_t&, const prepare_ctx_t&); + struct board_entry_t { const board_desc_t* desc; construct_fn_t construct; const char* success_log_name; success_annotation_fn_t success_annotation; + fixed_start_fn_t fixed_start; + + constexpr board_entry_t(const board_desc_t* desc_, construct_fn_t construct_, + const char* name_, success_annotation_fn_t annotation_, + fixed_start_fn_t fixed_start_ = nullptr) + : desc(desc_), construct(construct_), success_log_name(name_), + success_annotation(annotation_), fixed_start(fixed_start_) {} }; + template + const board_entry_t* find_board(const board_entry_t (&boards)[BoardCount], board_id_t id) + { + for (const auto& entry : boards) + { if (entry.desc->def.id == id) { return &entry; } } + return nullptr; + } + struct success_log_t { const char* name; diff --git a/src/board_detect/m5/esp32_d0wdq6.inl b/src/board_detect/m5/esp32_d0wdq6.inl index 603ba8b7..b0f4f07b 100644 --- a/src/board_detect/m5/esp32_d0wdq6.inl +++ b/src/board_detect/m5/esp32_d0wdq6.inl @@ -36,6 +36,7 @@ namespace m5 namespace detail { + bool fixed_start_core(board_result_t& result, const prepare_ctx_t& ctx); bool stack_reset_and_sample_ips(const board_desc_t& desc, const prepare_ctx_t& ctx, std::uint32_t* detected_option); } @@ -195,13 +196,13 @@ namespace m5 static const board_entry_t esp32_d0wdq6_boards[] = { { &desc_timercam, construct_displayless, "board_M5TimerCam", nullptr }, { &desc_station, construct_station, nullptr, nullptr }, - { &desc_core2, construct_core2, nullptr, nullptr }, - { &desc_tough, construct_tough, nullptr, nullptr }, + { &desc_core2, construct_core2, nullptr, nullptr, detail::fixed_start_core }, + { &desc_tough, construct_tough, nullptr, nullptr, detail::fixed_start_core }, { &desc_stack, construct_stack, nullptr, nullptr }, { &desc_paper, construct_paper, nullptr, nullptr }, { &desc_stickcplus, construct_stickcplus, "M5StickCPlus", nullptr }, { &desc_stickc, construct_stickc, "M5StickC", nullptr }, - { &desc_coreink, construct_coreink, "M5StackCoreInk", nullptr }, + { &desc_coreink, construct_coreink, "M5StackCoreInk", nullptr, fixed_start_coreink }, { &desc_stickcplus2, construct_stickcplus2, "M5StickCPlus2", nullptr }, { &desc_atompsram, construct_atompsram, "", nullptr }, { &desc_atomvoice, construct_displayless, "board_M5AtomVoice", nullptr }, @@ -213,7 +214,11 @@ namespace m5 namespace detail { - bool refine_core_family(board_result_t& result, const prepare_ctx_t& ctx) + const pmic_variant_t* observe_core_pmic(int port) + { return startup_detail::read_variant(desc_core2.power, port); } + + bool observe_core_panel(board_result_t& result, const prepare_ctx_t& ctx, + panel_variant_t& variant, std::uint32_t keys[4]) { const auto& display = desc_core2.display; const std::int8_t signals[] = { @@ -223,9 +228,8 @@ namespace m5 startup_detail::pin_level(desc_core2.sd.sd_cs, true); soft_spi_t bus(display.sclk, display.mosi, display.mosi, display.dc); bus.init(); - std::uint32_t keys[4] = {}; // Without reset, leave the same 120 ms window for a waking panel. - auto variant = identify_panel_variant(bus, display.cs, keys, + variant = identify_panel_variant(bus, display.cs, keys, ctx.allow_reset ? 1 : 120); if (ctx.allow_reset) { @@ -245,6 +249,45 @@ namespace m5 for (int i = 0; i < 4; ++i) { keys[i] = after_keys[i]; } } } + return true; + } + + bool fixed_start_core(board_result_t& result, const prepare_ctx_t& ctx) + { + const auto& desc = *result.desc; + { + startup_detail::i2c_scope_t i2c(*ctx.transaction, ctx.i2c_port_probe, desc.internal_i2c); + const auto* pmic = i2c.opened ? observe_core_pmic(i2c.port) : nullptr; + // Never send a default controller's write list when its identity is unknown. + if (pmic == nullptr) + { + ESP_LOGW("M5GFX", "Fixed board:%u PMIC unreadable; startup stopped", + static_cast(desc.def.id)); + return false; + } + result.option |= pmic->detected_option; + if (!startup_detail::prepare_power(desc, result, i2c.port, true)) { return false; } + } + panel_variant_t variant; + std::uint32_t keys[4] = {}; + if (!observe_core_panel(result, ctx, variant, keys)) { return false; } + log_panel_variant(variant, keys); + if (variant == panel_variant_t::e) { result.option |= generated_options::core2::lcd_e; } + const auto& display = desc.display; + const std::int8_t signals[] = { display.dc, display.sclk, display.mosi, display.miso }; + ctx.transaction->restore_start(signals); + return prepare(desc, result, ctx); + } + + bool refine_core_family(board_result_t& result, const prepare_ctx_t& ctx) + { + const auto& display = desc_core2.display; + const std::int8_t signals[] = { + display.dc, display.sclk, display.mosi, display.miso + }; + std::uint32_t keys[4] = {}; + panel_variant_t variant; + if (!observe_core_panel(result, ctx, variant, keys)) { return false; } startup_detail::i2c_scope_t i2c(*ctx.transaction, ctx.i2c_port_probe, desc_core2.internal_i2c); if (!i2c.opened) @@ -339,7 +382,7 @@ namespace m5 if (!i2c.opened) { return false; } prepare_ctx_t prepare_ctx = ctx; prepare_ctx.i2c_port_probe = i2c.port; - const auto* pmic = startup_detail::read_variant(desc_core2.power, i2c.port); + const auto* pmic = detail::observe_core_pmic(i2c.port); if (pmic == nullptr) { return false; } ESP_LOGD("board_detect_m5", "power controller id=%02x", pmic->id_value); diff --git a/src/board_detect/m5/esp32_d0wdq6_setup.inl b/src/board_detect/m5/esp32_d0wdq6_setup.inl index be42f1d8..a5a15d2d 100644 --- a/src/board_detect/m5/esp32_d0wdq6_setup.inl +++ b/src/board_detect/m5/esp32_d0wdq6_setup.inl @@ -71,7 +71,10 @@ namespace m5 { display_parts_owner_t out; construct_core_panel(result, generated_options::tough::lcd_e, bus_tough, &out); - out.light.reset(make_default_part()); + // Only fixed Tough can carry AXP2101; avoid sending it AXP192 backlight writes. + out.light.reset((result.option & generated_options::core2::new_pmic) + ? static_cast(make_default_part()) + : static_cast(make_default_part())); out.touch.reset(make_i2c_touch(touch_tough)); out.panel->touch(out.touch.get()); return construct_status(out.release_to(parts)); diff --git a/src/board_detect/m5/esp32_pico.inl b/src/board_detect/m5/esp32_pico.inl index 5fc1e38b..14dc9629 100644 --- a/src/board_detect/m5/esp32_pico.inl +++ b/src/board_detect/m5/esp32_pico.inl @@ -131,6 +131,8 @@ static const board_def_t* const coreink_members[] = { &desc_coreink.def, nullptr static const spi_id_member_t coreink_member_descs[] = { { &desc_coreink, coreink_probes, 2, true, false, 0, true }, }; +static bool fixed_start_coreink(board_result_t& result, const prepare_ctx_t& ctx) +{ return fixed_start_spi_variant(result, ctx, coreink_member_descs[0]); } static const spi_id_detector_t coreink_detector(coreink_members, coreink_member_descs, 1); static const board_def_t* const stickcplus2_members[] = { &desc_stickcplus2.def, nullptr }; diff --git a/src/board_detect/m5/esp32s3/cores3.inl b/src/board_detect/m5/esp32s3/cores3.inl index 81cd3136..69123a01 100644 --- a/src/board_detect/m5/esp32s3/cores3.inl +++ b/src/board_detect/m5/esp32s3/cores3.inl @@ -81,6 +81,24 @@ && camera == specs::cores3::i2c_camera::id_value; } + std::uint32_t observe_vbus(probe_ctx_t& ctx) + { + constexpr std::uint64_t spi_mask = + (std::uint64_t(1) << wiring::cores3::display_miso) + | (std::uint64_t(1) << wiring::cores3::display_sclk) + | (std::uint64_t(1) << wiring::cores3::display_mosi); + const auto pull = probe_pin_pulls(ctx, spi_mask); + (void)pull; // FORCE_VBUS still performs/restores the legacy-equivalent measurement. + // Unlike the legacy input_pullup sequence, probe_pin_pulls restores the + // captured pin modes immediately. Construction's SPI init overwrites + // these pins next, so retaining the temporary pull-ups had no effect. +#if defined(M5GFX_AUTODETECT_TEST_CORES3_FORCE_VBUS) + return vbus_5v; +#else + return (pull.pullup_high & spi_mask) == 0 ? vbus_5v : 0; +#endif + } + bool refine_panel(board_result_t& result, const prepare_ctx_t& ctx) { const auto& display = desc_cores3.display; @@ -188,6 +206,23 @@ } } + bool fixed_start(board_result_t& result, const prepare_ctx_t& ctx) + { + probe_ctx_t probe; + static_cast(probe) = ctx; + result.option |= observe_vbus(probe); + const auto& desc = *result.desc; + { + startup_detail::i2c_scope_t i2c(*ctx.transaction, ctx.i2c_port_probe, desc.internal_i2c); + if (!i2c.opened || !startup_detail::prepare_power(desc, result, i2c.port, true)) + { return false; } + } + if (result.option & vbus_5v) { enable_bus_out(ctx); } + // Identity is fixed: no capacitance, camera or IOE firmware observation. + if (!refine_panel(result, ctx)) { return false; } + return prepare(desc, result, ctx); + } + bool refine(board_result_t& result, const prepare_ctx_t& ctx) { if (result.option & vbus_5v) { enable_bus_out(ctx); } @@ -310,23 +345,11 @@ ESP_LOGW("M5GFX", "[Autodetect] CoreS3 release result was ambiguous; biasing toward camera family"); } } - constexpr std::uint64_t spi_mask = - (std::uint64_t(1) << wiring::cores3::display_miso) - | (std::uint64_t(1) << wiring::cores3::display_sclk) - | (std::uint64_t(1) << wiring::cores3::display_mosi); - const auto pull = probe_pin_pulls(ctx, spi_mask); - (void)pull; // FORCE_VBUS still performs/restores the legacy-equivalent measurement. - // Unlike the legacy input_pullup sequence, probe_pin_pulls restores the - // captured pin modes immediately. Construction's SPI init overwrites - // these pins next, so retaining the temporary pull-ups had no effect. + const auto vbus_option = cores3_detail::observe_vbus(ctx); result->assign(family_band == pin_release_band_t::short_release ? &desc_cores3se : &desc_cores3); if (has_camera) { result->option |= cores3_detail::internal_camera_confirmed; } -#if defined(M5GFX_AUTODETECT_TEST_CORES3_FORCE_VBUS) - result->option |= cores3_detail::vbus_5v; -#else - if ((pull.pullup_high & spi_mask) == 0) { result->option |= cores3_detail::vbus_5v; } -#endif + result->option |= vbus_option; result->refine = cores3_detail::refine; return true; } diff --git a/src/board_detect/m5/esp32s3/families.inl b/src/board_detect/m5/esp32s3/families.inl index 0ee165e5..5d796550 100644 --- a/src/board_detect/m5/esp32s3/families.inl +++ b/src/board_detect/m5/esp32s3/families.inl @@ -240,6 +240,11 @@ namespace m5 reg_bit(0, 0), reg_bit(0, 0), ops::no_ops(), ops::no_ops(), { nullptr, 0 }, 0), }; + // Construction configures the panel TE input before display adoption. + static constexpr std::int8_t stopwatch_te_pin = GPIO_NUM_38; + static const std::int8_t stopwatch_startup_pins[] = { + wiring::stopwatch::display_cs, stopwatch_te_pin, + }; static constexpr board_desc_t desc_stopwatch = { { id(lgfx::board_M5StopWatch), "M5StopWatch", 0 }, i2c_power_confirmed(specs::stopwatch::pmic::i2c_freq, stopwatch_pmic_variants, @@ -253,7 +258,8 @@ namespace m5 pins(wiring::stopwatch::hold), internal_i2c(wiring::stopwatch::internal_i2c_sda, wiring::stopwatch::internal_i2c_scl, wiring::stopwatch::internal_i2c_port), - no_options(), pins(wiring::stopwatch::hold), + // op_gpio_pins also captures the construction-time TE input (GPIO38) for fixed startup rollback. + no_options(), pins(stopwatch_startup_pins), }; static constexpr board_desc_t desc_papermono = { { id(lgfx::board_M5PaperMono), "M5PaperMono", 0 }, @@ -807,6 +813,8 @@ namespace m5 { &desc_dinmeter, dinmeter_probes, sizeof(dinmeter_probes) / sizeof(dinmeter_probes[0]), specs::dinmeter::bus_three_wire, wiring::dinmeter::touches_opi_pins, 0, false }, }; + static bool fixed_start_atoms3(board_result_t& result, const prepare_ctx_t& ctx) + { return fixed_start_spi_variant(result, ctx, spi_id_member_descs[0]); } static const spi_id_detector_t spi_display_detector( spi_id_members, spi_id_member_descs, sizeof(spi_id_member_descs) / sizeof(spi_id_member_descs[0])); @@ -949,6 +957,8 @@ namespace m5 { &desc_atoms3r, atoms3r_probes, sizeof(atoms3r_probes) / sizeof(atoms3r_probes[0]), specs::atoms3r::bus_three_wire, wiring::atoms3r::touches_opi_pins, 5, false }, }; + static bool fixed_start_atoms3r(board_result_t& result, const prepare_ctx_t& ctx) + { return fixed_start_spi_variant(result, ctx, atoms3r_member_descs[0]); } static const spi_id_detector_t atoms3r_display_detector( atoms3r_members, atoms3r_member_descs, sizeof(atoms3r_member_descs) / sizeof(atoms3r_member_descs[0])); @@ -1106,6 +1116,8 @@ namespace m5 { &desc_airq, airq_probes, sizeof(airq_probes) / sizeof(airq_probes[0]), specs::airq::bus_three_wire, wiring::airq::touches_opi_pins, 0, false }, }; + static bool fixed_start_airq(board_result_t& result, const prepare_ctx_t& ctx) + { return fixed_start_spi_variant(result, ctx, airq_member_descs[0]); } static const spi_id_detector_t airq_detector( airq_members, airq_member_descs, sizeof(airq_member_descs) / sizeof(airq_member_descs[0]), false, airq_signature); @@ -1179,23 +1191,23 @@ namespace m5 static const board_entry_t esp32s3_boards[] = { { &desc_dualkey, construct_displayless, "board_M5DualKey", nullptr }, - { &desc_cores3, construct_cores3, "board_M5StackCoreS3", nullptr }, - { &desc_cores3se, construct_cores3, "board_M5StackCoreS3SE", nullptr }, - { &desc_stackchan, construct_cores3, "board_M5StackChan", nullptr }, - { &desc_atoms3, construct_atoms3, "board_M5AtomS3", atoms3_success_annotation }, + { &desc_cores3, construct_cores3, "board_M5StackCoreS3", nullptr, cores3_detail::fixed_start }, + { &desc_cores3se, construct_cores3, "board_M5StackCoreS3SE", nullptr, cores3_detail::fixed_start }, + { &desc_stackchan, construct_cores3, "board_M5StackChan", nullptr, cores3_detail::fixed_start }, + { &desc_atoms3, construct_atoms3, "board_M5AtomS3", atoms3_success_annotation, fixed_start_atoms3 }, { &desc_atoms3lite, construct_displayless, "board_M5AtomS3Lite", nullptr }, { &desc_atoms3u, construct_displayless, "board_M5AtomS3U", nullptr }, { &desc_stamps3, construct_displayless, "board_M5StampS3", nullptr }, { &desc_capsule, construct_displayless, "board_M5Capsule", nullptr }, { &desc_powerhub, construct_displayless, "board_M5PowerHub", nullptr }, - { &desc_atoms3r, construct_atoms3r, "board_M5AtomS3R", atoms3r_success_annotation }, + { &desc_atoms3r, construct_atoms3r, "board_M5AtomS3R", atoms3r_success_annotation, fixed_start_atoms3r }, { &desc_atoms3rcam, construct_displayless, "board_M5AtomS3RCam", nullptr }, { &desc_atoms3rext, construct_displayless, "board_M5AtomS3RExt", nullptr }, { &desc_atomvoices3r, construct_displayless, "board_M5AtomVoiceS3R", nullptr }, { &desc_stamps3bat, construct_displayless, "board_M5StampS3Bat", nullptr }, { &desc_stamps3mini, construct_displayless, "board_M5StampS3Mini", nullptr }, { &desc_dinmeter, construct_dinmeter, "board_M5DinMeter", nullptr }, - { &desc_airq, construct_airq, "M5AirQ", nullptr }, + { &desc_airq, construct_airq, "M5AirQ", nullptr, fixed_start_airq }, { &desc_stamplc, construct_stamplc, "board_M5StamPLC", nullptr }, { &desc_dial, construct_dial, "board_M5Dial", nullptr }, { &desc_cardputer, construct_cardputer, "board_M5Cardputer", nullptr }, diff --git a/src/board_detect/m5/esp32s3/families_setup.inl b/src/board_detect/m5/esp32s3/families_setup.inl index eeddaad9..be3877f0 100644 --- a/src/board_detect/m5/esp32s3/families_setup.inl +++ b/src/board_detect/m5/esp32s3/families_setup.inl @@ -424,7 +424,7 @@ namespace m5 display_parts_owner_t out; out.bus.reset(make_spi_bus(bus_stopwatch)); out.panel.reset(make_panel(panel_stopwatch, out.bus.get())); - lgfx::pinMode(GPIO_NUM_38, lgfx::pin_mode_t::input_pullup); + lgfx::pinMode(stopwatch_te_pin, lgfx::pin_mode_t::input_pullup); out.touch.reset(make_i2c_touch(touch_stopwatch)); out.panel->touch(out.touch.get()); return construct_status(out.release_to(parts)); From 397046b9ecb39c11567ac79f6256d6366083ed4e Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:23:04 +0000 Subject: [PATCH 04/10] Identify PM1 family members before power Probe both StopWatch and PaperMono touch controllers and G12/G13 pulls before selecting a member. Pull-based abnormal rows select a provisional member only on the final attempt. Remove the StopWatch rescue power sequence and the NFC 0x50 exclusion (both members can carry that device). Cold PaperMono keeps one pre-confirmation power sequence for D/D and U/U pulls, followed only by its own touch check. This path can also reach StopWatch when touch is unanswered and both IRQ lines read low or are driven high; it is kept as a documented cold-start exception and never switches members during refinement. IOE1 boots hundreds of milliseconds later than PM1, so its response no longer gates identification; the power lists wait for it once before the first IOE1 access (500 ms deadline from the wait). Pending refinement after a power failure retries on non-final attempts instead of adopting an unresolved member; this also applies to Core2/Tough and CoreS3/CoreS3 SE refinement when their power sequence fails. Add touch-address injection and host tests. --- .../board_spec/test/detect_families.test.js | 116 ++++++++++++++++- extras/board_spec/test/ops_host.cpp | 68 +++++++++- src/board_detect/board_detect.inl | 7 +- src/board_detect/m5/esp32s3/families.inl | 118 ++++++++++-------- src/board_detect/m5/pmic_ops.hpp | 6 + src/board_detect/ops.hpp | 13 +- src/board_detect/ops.inl | 7 ++ 7 files changed, 280 insertions(+), 55 deletions(-) diff --git a/extras/board_spec/test/detect_families.test.js b/extras/board_spec/test/detect_families.test.js index e7fbe055..555d374b 100644 --- a/extras/board_spec/test/detect_families.test.js +++ b/extras/board_spec/test/detect_families.test.js @@ -234,7 +234,7 @@ int main() { test("production PM1 unresolved tail keeps PaperMono without running hinted StopWatch power", async () => { const source=await fs.readFile(path.join(src,"m5/esp32s3/families.inl"),"utf8"); - const begin=source.indexOf(' if (!stopwatch_touch && !ctx.final_attempt)'); + const begin=source.indexOf(' if (!ctx.final_attempt)'); const end=source.indexOf('\n }',begin); const tail=source.slice(begin,end); await compileRun(common+` @@ -253,3 +253,117 @@ int main(){for(board_id_t hint:{board_id_t(0),board_id_t(20),board_id_t(21)}) fo }} `,"PM1 fixed representative"); }); + +test("production PM1 pre-power table gates writes and fixes observed members", async () => { + const source=await fs.readFile(path.join(src,"m5/esp32s3/families.inl"),"utf8"); + const detector=await fs.readFile(path.join(src,"board_detect.inl"),"utf8"); + const confirm=body(source.slice(source.indexOf('class pm1_family_detector_t')),'bool confirm('); + const helper=body(detector,'static bool select_provisional_member'); + await compileRun(common+` +const board_desc_t desc_stopwatch={{20,"StopWatch",0}},desc_papermono={{21,"PaperMono",0}}; +namespace wiring {namespace stopwatch {constexpr int internal_i2c_sda=1,internal_i2c_scl=2;} +namespace papermono {constexpr int internal_i2c_sda=1,internal_i2c_scl=2;}} +namespace detail {constexpr int stopwatch_probe_addr=0x15,papermono_probe_addr=0x38; +bool refine_papermono_touch(board_result_t&,const prepare_ctx_t&){return true;}} +struct pulls_t {std::uint64_t pulldown_high,pullup_high;}; +int sw,pm,g12,g13,reads; +pulls_t probe_pin_pulls(probe_ctx_t&,std::uint64_t){return {std::uint64_t(g12&1)<<12|std::uint64_t(g13&1)<<13,std::uint64_t((g12>>1)&1)<<12|std::uint64_t((g13>>1)&1)<<13};} +bool probe_i2c_ack(probe_ctx_t&,int,int,int addr){++reads;assert(addr==0x15||addr==0x38);return addr==0x15?sw:pm;} +bool probe_i2c_read(probe_ctx_t&,int,int,int addr,int,std::uint8_t* data,int,int,int){data[0]=0x50;data[1]=0x20;assert(addr==0x6e);return true;} +bool select_provisional_member(const prepare_ctx_t& ctx,board_result_t* result,const board_desc_t* preferred_if_possible,const board_desc_t* hinted_if_possible,const board_desc_t* family_default,const char* why) ${helper} +bool confirm_member(probe_ctx_t& ctx,board_result_t* result) ${confirm} +int main(){ + for(sw=0;sw<=1;++sw)for(pm=0;pm<=1;++pm)for(g12=0;g12<4;++g12)for(g13=0;g13<4;++g13) + for(bool final:{false,true})for(board_id_t preferred:{board_id_t(0),board_id_t(20),board_id_t(21),board_id_t(42)}) + for(board_id_t hint:{board_id_t(0),board_id_t(20),board_id_t(21)}){ + probe_ctx_t ctx;ctx.final_attempt=final;ctx.attempt=final?4:0;ctx.preferred=preferred;ctx.hint=hint; + board_result_t r;reads=0;bool ok=confirm_member(ctx,&r);assert(reads==2); + int expected=0;bool provisional=false,refine=false; + if(sw!=pm)expected=sw?20:21; + else if(!sw&&((g12==0&&g13==0)||(g12==3&&g13==3))){expected=21;refine=true;} + else if(final){provisional=true; + if(!sw&&(g12==2||(g12==0&&g13==3)))expected=20; + else expected=(preferred==20||preferred==21)?preferred:(hint==20||hint==21)?hint:0;} + assert(ok==(expected!=0));int stopwatch_power=0,papermono_power=0; + if(ok){assert(r.def->id==expected&&r.provisional==provisional); + assert(bool(r.refine)==refine);if(expected==20)++stopwatch_power;else ++papermono_power; + detect_outcome_t out;out.result=r;if(provisional)assert(!should_persist_detection(out,0));} + assert(stopwatch_power==(ok&&expected==20));assert(papermono_power==(ok&&expected==21)); + } +} +`,"PM1 pre-power table"); +}); + +test("production PM1 D/D refinement polls only PaperMono and never switches power", async () => { + const source=await fs.readFile(path.join(src,"m5/esp32s3/families.inl"),"utf8"); + const refine=body(source,'bool refine_papermono_touch'); + await compileRun(common.replace('bool final_attempt=false;','bool final_attempt=false; int* transaction=nullptr; int i2c_port_probe=0;') + .replace('#define ESP_LOGW(...) ((void)0)', '#include \nconst char* last_reason;\n#define ESP_LOGW(tag, format, reason, ...) (last_reason=reason)')+` +const board_desc_t desc_papermono={{21,"PaperMono",0}}; +namespace specs {namespace papermono {namespace touch {constexpr int i2c_freq=100000;}}} +constexpr int papermono_probe_addr=0x38; +bool ack,opened;unsigned clock_ms,probes; +namespace lgfx {unsigned millis(){return clock_ms;}void delay(unsigned ms){clock_ms+=ms;} +namespace i2c {struct response {bool ok;bool has_value(){return ok;}}; +response beginTransaction(int,int addr,int,bool){assert(addr==0x38);++probes;return {ack};} +response endTransaction(int){return {true};}}} +namespace startup_detail {struct i2c_scope_t {bool opened;int port=0; +i2c_scope_t(int&,int,const board_desc_t&):opened(::opened){}};} +bool refine_member(board_result_t& result,const prepare_ctx_t& ctx) ${refine.replace('desc_papermono.internal_i2c','desc_papermono')} +int main(){int tx=0;for(bool final:{false,true})for(bool response:{false,true})for(bool bus:{false,true}){ + ack=response;opened=bus;clock_ms=probes=0;prepare_ctx_t ctx;ctx.final_attempt=final;ctx.transaction=&tx; + board_result_t r;r.assign(&desc_papermono);int papermono_power=1,stopwatch_power=0; + bool ok=refine_member(r,ctx);assert(ok==((bus&&response)||final)); + assert(r.def==&desc_papermono.def&&r.provisional==(final&&!(bus&&response))); + assert(probes==(bus?(response?1:200):0));assert(papermono_power==1&&stopwatch_power==0); + if(final&&!(bus&&response))assert(std::strcmp(last_reason,bus?"PaperMono touch unanswered":"PaperMono internal I2C unavailable")==0); +}} +`,"PM1 post-power refine"); +}); + +test("production prepare retries failed power before pending member refinement", async () => { + const source=await fs.readFile(path.join(src,"board_detect.inl"),"utf8"); + const prepare=body(source,'bool prepare(const board_desc_t& desc,'); + const extended=common.replace('#define ESP_LOGW', '#define ESP_LOGE(...) ((void)0)\n#define ESP_LOGW') + .replace('struct board_desc_t {board_def_t def;};',`enum class reset_kind_t {none,i2c_regs}; + struct board_desc_t {board_def_t def;struct {const int* variants;} power; + struct {int hw_port,sda,scl;} internal_i2c;struct {reset_kind_t kind;} reset;};`) + .replace('bool final_attempt=false;','bool final_attempt=false; int* transaction=nullptr; int i2c_port_probe=0;'); + await compileRun(extended+` +int power_calls,refine_calls,sd_calls,construct_calls;bool fail_power; +namespace lgfx {namespace i2c {struct value_t {bool has_value() const{return true;}int value() const{return 0;}}; + bool isInitialized(int){return false;}value_t getPinSDA(int){return {};} + value_t getPinSCL(int){return {};}value_t init(int,int,int){return {};}}} +namespace startup_detail { + bool description_valid(const board_desc_t&){return true;} + struct i2c_scope_t {bool opened=true;int port=0;i2c_scope_t(int&,int,decltype(board_desc_t::internal_i2c)) {}}; + bool prepare_power(const board_desc_t&,board_result_t& r,int,bool){++power_calls;r.prepared|=prepared_power; + if(fail_power)r.prepared|=prepared_power_failed;return true;} + bool prepare_sd_spi(const board_desc_t&,board_result_t&,const prepare_ctx_t&){++sd_calls;return true;} + void hold_chip_selects(const board_desc_t&){} +} +bool prepare_reset(const board_desc_t&,board_result_t&,const prepare_ctx_t&,int,void*,bool){return true;} +bool refine_member(board_result_t&,const prepare_ctx_t&){++refine_calls;return true;} +bool prepare_member(const board_desc_t& desc,board_result_t& result,const prepare_ctx_t& ctx) ${prepare} +int main(){int tx=0,variant=0; + // Pending refine represents both D/D and U/U; no refine represents touch-confirmed members. + for(int row:{0,1,2})for(bool final:{false,true})for(bool failed:{false,true})for(bool scoped:{false,true}){ + board_desc_t desc={{21,"PaperMono",0},{scoped?&variant:nullptr},{-1,47,48},{reset_kind_t::none}}; + board_result_t r;r.assign(&desc);r.refine=row<2?refine_member:nullptr; + prepare_ctx_t ctx;ctx.transaction=&tx;ctx.final_attempt=final; + power_calls=refine_calls=sd_calls=construct_calls=0;fail_power=failed; + const bool ok=prepare_member(desc,r,ctx);if(ok)++construct_calls; + const bool retry=failed&&row<2&&!final; + assert(ok==!retry&&construct_calls==int(!retry));assert(power_calls==1); + assert(refine_calls==int(row<2&&!failed));assert(sd_calls==int(!retry)); + assert(r.provisional==(failed&&row<2&&final)); + assert(bool(r.prepared&prepared_refine)==(row<2&&!retry)); + if(failed)assert(r.prepared&prepared_power_failed); + } + // Completed refinement does not turn a confirmed member's power failure into a retry. + board_desc_t desc={{21,"PaperMono",0},{nullptr},{-1,47,48},{reset_kind_t::none}}; + board_result_t r;r.assign(&desc);r.refine=refine_member;r.prepared=prepared_power|prepared_power_failed|prepared_refine; + prepare_ctx_t ctx;ctx.transaction=&tx;assert(prepare_member(desc,r,ctx));assert(!r.provisional); +} +`,"pending-refinement power failure"); +}); diff --git a/extras/board_spec/test/ops_host.cpp b/extras/board_spec/test/ops_host.cpp index f009a4c6..9dd9626c 100644 --- a/extras/board_spec/test/ops_host.cpp +++ b/extras/board_spec/test/ops_host.cpp @@ -50,12 +50,18 @@ struct fake_t int ready_failures = 0; int ready_attempts = 0; std::uint32_t ready_attempt_ms = 0; + bool check_ioe_wait = false; + std::uint32_t ioe_ready_at = 0; + int wait_notifications = 0; + std::uint32_t wait_elapsed = 0; + bool wait_succeeded = false; }; static bool read8(void* context, const bdops::i2c_device_t& device, std::uint16_t reg, std::uint8_t* value) { auto& fake = *static_cast(context); + if (fake.check_ioe_wait && device.addr == 0x4F) { assert(fake.now >= fake.ioe_ready_at); } ++fake.reads; if (fake.read_failures-- > 0) { return false; } for (auto attempt : fake.read_nacks) @@ -70,6 +76,7 @@ static bool write8(void* context, const bdops::i2c_device_t& device, std::uint16_t reg, std::uint8_t value) { auto& fake = *static_cast(context); + if (fake.check_ioe_wait && device.addr == 0x4F) { assert(fake.now >= fake.ioe_ready_at); } ++fake.writes_attempted; if (fake.write_failures-- > 0) { return false; } const auto key = std::make_pair(device.addr, reg); @@ -89,12 +96,17 @@ static bool write16le(void* context, const bdops::i2c_device_t& device, return true; } -static bool ready(void* context, const bdops::i2c_device_t&, std::uint32_t remaining_ms) +static bool ready(void* context, const bdops::i2c_device_t& device, std::uint32_t remaining_ms) { auto& fake = *static_cast(context); if (remaining_ms == 0) { return false; } ++fake.ready_attempts; fake.now += fake.ready_attempt_ms; + if (fake.check_ioe_wait) + { + assert(device.addr == 0x4F); + return fake.now >= fake.ioe_ready_at; + } return fake.ready_failures-- <= 0; } @@ -494,6 +506,59 @@ static void test_pm1_family_sequences() } } +static void wait_finished(void* context, const bdops::i2c_device_t& device, + std::uint32_t elapsed, bool succeeded) +{ + auto& fake = *static_cast(context); + assert(device.addr == 0x4F); + ++fake.wait_notifications; + fake.wait_elapsed = elapsed; + fake.wait_succeeded = succeeded; +} + +static void test_pm1_member_boot_wait() +{ + const std::int8_t pins[] = { 39, 16 }; + const bdops::gpio_scope_t scope = { 49, pins, 2 }; + const bdops::op_list_t sequences[] = { + bdops::list(pmicops::stopwatch_power_on), bdops::list(pmicops::papermono_power_on) + }; + for (const auto& sequence : sequences) + { + for (unsigned ready_at : { 0u, 230u, 499u, 500u, 501u }) + { + fake_t fake; + fake.check_ioe_wait = true; + fake.ioe_ready_at = ready_at; + const auto result = bdops::run_ops(backend(fake), pmicops::pm1_family_devices, 2, + sequence.data, sequence.size, scope, nullptr, + wait_finished); + const bool success = ready_at < 500; + assert((result.status == bdops::op_status_t::ok) == success); + assert(fake.wait_notifications == 1 && fake.wait_succeeded == success); + assert(fake.wait_elapsed == (success ? ready_at : 500)); + // PM1 writes precede the wait; no IOE1 access precedes readiness. + assert(fake.writes.size() == (success ? (sequence.data == pmicops::stopwatch_power_on ? 12u : 11u) : 3u)); + if (!success) { assert(result.status == bdops::op_status_t::timeout && result.failed_index == 5); } + } + } + // Shared devices do not add a boot wait to other PM1/IOE1 users. + fake_t chain; + chain.ready_failures = 1000; + auto result = bdops::run_ops(backend(chain), pmicops::pm1_family_devices, 2, + pmicops::chaincaptain_power_on, + sizeof(pmicops::chaincaptain_power_on) / sizeof(pmicops::chaincaptain_power_on[0]), + scope, nullptr, wait_finished); + assert(result.status == bdops::op_status_t::ok && chain.ready_attempts == 0 && chain.wait_notifications == 0); + fake_t stick; + stick.ready_failures = 1000; + result = bdops::run_ops(backend(stick), pmicops::pm1_devices, 1, + pmicops::sticks3_power_on, + sizeof(pmicops::sticks3_power_on) / sizeof(pmicops::sticks3_power_on[0]), + scope, nullptr, wait_finished); + assert(result.status == bdops::op_status_t::ok && stick.ready_attempts == 0 && stick.wait_notifications == 0); +} + static void test_pm1_ext_family_sequences() { { @@ -837,6 +902,7 @@ int main() test_core2_restore_coverage(); test_sticks3_retry_scopes(); test_pm1_family_sequences(); + test_pm1_member_boot_wait(); test_pm1_ext_family_sequences(); test_paper_family_sequences(); test_toughc5_sequences(); diff --git a/src/board_detect/board_detect.inl b/src/board_detect/board_detect.inl index fd789076..3ef5c91f 100644 --- a/src/board_detect/board_detect.inl +++ b/src/board_detect/board_detect.inl @@ -1132,7 +1132,8 @@ namespace board_detect const ops::gpio_scope_t gpio_scope { GPIO_NUM_MAX, gpio_pins.data, gpio_pins.size }; return ops::run_ops(ops::lgfx_backend(&backend_context), power.devices, power.device_count, - sequence.data, sequence.size, gpio_scope, policy); + sequence.data, sequence.size, gpio_scope, policy, + ops::lgfx_wait_ready_finished); } const pmic_variant_t* read_variant(const power_desc_t& power, int port, @@ -1859,7 +1860,9 @@ namespace board_detect { if (result.prepared & prepared_power_failed) { - // The family member is still provisional: show it, but do not cache it. + // Pending refinement means the member is not yet confirmed. Give a + // failed power sequence another attempt before retaining it provisionally. + if (!ctx.final_attempt) { return false; } result.provisional = true; ESP_LOGW("board_detect", "member refinement skipped after retained power_on failure; board=%u", diff --git a/src/board_detect/m5/esp32s3/families.inl b/src/board_detect/m5/esp32s3/families.inl index 5d796550..e33f7cc1 100644 --- a/src/board_detect/m5/esp32s3/families.inl +++ b/src/board_detect/m5/esp32s3/families.inl @@ -471,51 +471,40 @@ namespace m5 namespace detail { + // Share the injected address between pre-power confirmation and refine. +#if defined (M5GFX_AUTODETECT_TEST_FAIL_STOPWATCH_TOUCH) + static constexpr std::uint8_t stopwatch_probe_addr = 0x16; +#else + static constexpr std::uint8_t stopwatch_probe_addr = specs::stopwatch::touch::i2c_addr; +#endif +#if defined (M5GFX_AUTODETECT_TEST_FAIL_PAPERMONO_TOUCH) + static constexpr std::uint8_t papermono_probe_addr = 0x39; +#else + static constexpr std::uint8_t papermono_probe_addr = specs::papermono::touch::i2c_addr; +#endif + bool refine_papermono_touch(board_result_t& result, const prepare_ctx_t& ctx) { startup_detail::i2c_scope_t i2c(*ctx.transaction, ctx.i2c_port_probe, - desc_papermono.internal_i2c); - const auto touch_answers = [&](std::uint8_t addr, std::uint32_t freq) + desc_papermono.internal_i2c); + if (i2c.opened) { const auto started = lgfx::millis(); do { - if (lgfx::i2c::beginTransaction(i2c.port, addr, freq, false).has_value() + if (lgfx::i2c::beginTransaction(i2c.port, papermono_probe_addr, + specs::papermono::touch::i2c_freq, false).has_value() && lgfx::i2c::endTransaction(i2c.port).has_value()) { return true; } lgfx::delay(1); } while (lgfx::millis() - started < 200); - return false; - }; - bool stopwatch_touch = false; - if (i2c.opened) - { - if (touch_answers(specs::papermono::touch::i2c_addr, - specs::papermono::touch::i2c_freq)) { return true; } - stopwatch_touch = touch_answers(specs::stopwatch::touch::i2c_addr, - specs::stopwatch::touch::i2c_freq); - if (stopwatch_touch - && lgfx::i2c::beginTransaction(i2c.port, 0x50, 100000, false).has_value()) - { - lgfx::i2c::endTransaction(i2c.port); - return false; - } - } - if (!stopwatch_touch && !ctx.final_attempt) { return false; } - if (stopwatch_touch) - { - // The provisional PaperMono sequence has run. Apply StopWatch power - // before selecting the member confirmed by its touch response. - board_result_t stopwatch; - stopwatch.assign(&desc_stopwatch); - if (!i2c.opened - || !startup_detail::prepare_power(desc_stopwatch, stopwatch, i2c.port, true)) - { return false; } - result.assign(&desc_stopwatch); } - if (stopwatch_touch) { return true; } + if (!ctx.final_attempt) { return false; } + // Keep the powered member; no second member's power sequence in this attempt. + // Later attempts can switch only after unanswered touch with D/D or U/U. result.provisional = true; - ESP_LOGW("board_detect_m5", "StopWatch/PaperMono touch unanswered; using %s for this boot", + ESP_LOGW("board_detect_m5", "%s; using %s for this boot", + i2c.opened ? "PaperMono touch unanswered" : "PaperMono internal I2C unavailable", result.def->name); return true; } @@ -543,28 +532,59 @@ namespace m5 { return false; } - std::uint8_t ioe_id[2] = {}; - if (!probe_i2c_read(ctx, wiring::stopwatch::internal_i2c_sda, - wiring::stopwatch::internal_i2c_scl, 0x4F, 0, - ioe_id, sizeof(ioe_id), 100000, 200)) - { - return false; - } - // StopWatch can answer before power preparation. PaperMono's touch is - // held in reset until its PM1 power sequence, so check it in refine. + // IOE1 takes hundreds of milliseconds to boot; its presence is not + // member evidence. Wait only when its selected power sequence needs it. const bool stopwatch_touch = probe_i2c_ack( ctx, wiring::stopwatch::internal_i2c_sda, - wiring::stopwatch::internal_i2c_scl, specs::stopwatch::touch::i2c_addr); - if (stopwatch_touch) + wiring::stopwatch::internal_i2c_scl, detail::stopwatch_probe_addr); + const bool papermono_touch = probe_i2c_ack( + ctx, wiring::papermono::internal_i2c_sda, + wiring::papermono::internal_i2c_scl, detail::papermono_probe_addr); + const auto pulls = probe_pin_pulls(ctx, (1ULL << 12) | (1ULL << 13)); + const auto classify = [&](unsigned pin) -> char + { + const unsigned sample = ((pulls.pulldown_high >> pin) & 1) + | (((pulls.pullup_high >> pin) & 1) << 1); + return "DXFU"[sample]; + }; + const char g12 = classify(12), g13 = classify(13); + ESP_LOGD("board_detect_m5", "PM1 attempt=%u touch15=%u touch38=%u G12=%c G13=%c", + ctx.attempt, stopwatch_touch, papermono_touch, g12, g13); + // Both members carry NFC at 0x50; its ACK cannot exclude StopWatch. + if (stopwatch_touch != papermono_touch) { - if (probe_i2c_ack(ctx, wiring::stopwatch::internal_i2c_sda, - wiring::stopwatch::internal_i2c_scl, 0x50)) { return false; } - result->assign(&desc_stopwatch); + result->assign(stopwatch_touch ? &desc_stopwatch : &desc_papermono); return true; } - result->assign(&desc_papermono); - result->refine = detail::refine_papermono_touch; - return true; + if (!stopwatch_touch + && ((g12 == 'D' && g13 == 'D') || (g12 == 'U' && g13 == 'U'))) + { + // Cold PaperMono needs power before touch confirmation, even after its + // SD pull-ups rise. This pre-confirmation write also admits StopWatch + // with unanswered touch and both IRQ lines low or both driven high. + result->assign(&desc_papermono); + result->refine = detail::refine_papermono_touch; + return true; + } + const board_desc_t* chosen = nullptr; + if (!stopwatch_touch) + { + if (g12 == 'F' || (g12 == 'D' && g13 == 'U')) { chosen = &desc_stopwatch; } + } + if (chosen) + { + return select_provisional_member(ctx, result, chosen, nullptr, nullptr, + "PM1 member touch unanswered"); + } + const auto possible = [](board_id_t id) -> const board_desc_t* + { + if (id == desc_stopwatch.def.id) { return &desc_stopwatch; } + if (id == desc_papermono.def.id) { return &desc_papermono; } + return nullptr; + }; + return select_provisional_member(ctx, result, possible(ctx.preferred), + possible(ctx.hint), nullptr, + "PM1 member evidence ambiguous"); } private: diff --git a/src/board_detect/m5/pmic_ops.hpp b/src/board_detect/m5/pmic_ops.hpp index bc3d5574..377b89e5 100644 --- a/src/board_detect/m5/pmic_ops.hpp +++ b/src/board_detect/m5/pmic_ops.hpp @@ -94,6 +94,9 @@ namespace pmic_ops ops::i2c_write8(0, 0x09, 0x00), ops::i2c_write8(0, 0x0A, 0x00), ops::i2c_bit_on(0, 0x06, 0x17), ops::gpio_set_mode(39, ops::gpio_mode_t::output), ops::gpio_write_high(39), + // IOE1 boots later than PM1. This power-stage wait starts a relative + // 500 ms deadline for new probes; an in-flight I2C call may overrun it. + ops::i2c_wait_ready(1, 500, 1), ops::i2c_write8(1, 0x23, 0x00), ops::i2c_bit_off(1, 0x13, 0x9D), ops::i2c_bit_on(1, 0x03, 0x9D), ops::i2c_bit_on(1, 0x05, 0x99), ops::delay_ms(10), ops::i2c_bit_off(1, 0x05, 0x18), ops::delay_ms(8), @@ -161,6 +164,9 @@ namespace pmic_ops ops::i2c_write8(0, 0x09, 0x00), ops::i2c_write8(0, 0x0A, 0x00), ops::i2c_bit_on(0, 0x06, 0x17), ops::gpio_set_mode(16, ops::gpio_mode_t::output), ops::gpio_write_high(16), + // IOE1 boots later than PM1. This power-stage wait starts a relative + // 500 ms deadline for new probes; an in-flight I2C call may overrun it. + ops::i2c_wait_ready(1, 500, 1), ops::i2c_bit_on(1, 0x03, 0x34), ops::i2c_bit_on(1, 0x04, 0x30), ops::i2c_bit_on(1, 0x05, 0x04), ops::i2c_bit_on(1, 0x06, 0x30), ops::i2c_bit_off(1, 0x13, 0x34), ops::i2c_bit_off(1, 0x14, 0x34), diff --git a/src/board_detect/ops.hpp b/src/board_detect/ops.hpp index 96d46a17..f757feec 100644 --- a/src/board_detect/ops.hpp +++ b/src/board_detect/ops.hpp @@ -427,7 +427,9 @@ namespace ops const i2c_device_t* devices, std::size_t device_count, const op_t* operations, std::size_t count, const gpio_scope_t& gpio_scope, - const retry_policy_t* policy = nullptr) + const retry_policy_t* policy = nullptr, + void (*wait_ready_finished)(void*, const i2c_device_t&, + std::uint32_t, bool) = nullptr) { auto checked = validate_ops(devices, device_count, operations, count, gpio_scope); if (checked.status != op_status_t::ok) { return checked; } @@ -511,9 +513,16 @@ namespace ops ? remaining : (device.wire_timeout_ms < remaining ? device.wire_timeout_ms : remaining); - if (backend.i2c_ready(backend.ctx, device, wire_timeout)) { break; } + if (backend.i2c_ready(backend.ctx, device, wire_timeout)) + { + if (wait_ready_finished) + { wait_ready_finished(backend.ctx, device, backend.millis(backend.ctx) - started, true); } + break; + } if (static_cast(backend.millis(backend.ctx) - deadline) >= 0) { + if (wait_ready_finished) + { wait_ready_finished(backend.ctx, device, backend.millis(backend.ctx) - started, false); } return result(op_status_t::timeout, i); } const auto left = deadline - backend.millis(backend.ctx); diff --git a/src/board_detect/ops.inl b/src/board_detect/ops.inl index 39b81888..c6787f8a 100644 --- a/src/board_detect/ops.inl +++ b/src/board_detect/ops.inl @@ -67,6 +67,13 @@ namespace ops return began && ended; } + inline void lgfx_wait_ready_finished(void*, const i2c_device_t& device, + std::uint32_t elapsed_ms, bool ready) + { + ESP_LOGD("board_detect", "power wait_ready addr=0x%02x elapsed=%ums ready=%u", + device.addr, static_cast(elapsed_ms), ready); + } + inline bool lgfx_gpio_set_mode(void*, std::uint16_t pin, gpio_mode_t mode) { lgfx::pin_mode_t native = lgfx::pin_mode_t::output; From 4935a67a5255b8e6275924a4c4a93463dd993c5d Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:23:04 +0000 Subject: [PATCH 05/10] Identify AXP family members before power Confirm Core2 and Tough from exclusive touch responses before power. For silent AXP192 members, use G12 with G32/G33 to choose the shared Core2/Tough power sequence or the Station LCD probe. Station has only a weak G12 pull-down, so both Port A lines must read up before Core2 power is chosen; a Station with external pull-ups on both Port A lines can still take that path. Station LCD misses and ambiguous subdivisions never select Core2 power from a hint or preference. Contradictory touch answers (M-Bus devices at 0x2E/0x38) never confirm a member. AXP2101 with unanswered touch takes all five attempts before a provisional Core2. A cold AXP192 Core2/Tough with a Port A line held low follows the Station branch and stays unidentified after a Station LCD miss. Treat post-power panel observation failure as optional and keep the touch-confirmed board. After a valid AXP or PM1 ID, later families are not probed in the same attempt even when member confirmation fails. Prepare SD SPI mode before panel observation, including fixed startup. Declare G12/G32/G33 as probe pins so the transaction restores them. Add pre/post-power injection and host tests. --- .../board_spec/test/detect_families.test.js | 201 +++++++++++++++++- src/M5GFX.cpp | 10 - src/board_detect/board_detect.hpp | 3 + src/board_detect/board_detect.inl | 15 ++ src/board_detect/detect_types.hpp | 4 + src/board_detect/m5/esp32_d0wdq6.inl | 173 ++++++++++----- src/board_detect/m5/esp32s3/families.inl | 1 + 7 files changed, 333 insertions(+), 74 deletions(-) diff --git a/extras/board_spec/test/detect_families.test.js b/extras/board_spec/test/detect_families.test.js index 555d374b..ceafb0b0 100644 --- a/extras/board_spec/test/detect_families.test.js +++ b/extras/board_spec/test/detect_families.test.js @@ -17,7 +17,7 @@ namespace m5gfx { namespace board_detect { struct board_desc_t {board_def_t def;}; void board_result_t::assign(const board_desc_t* d) {desc=d; def=&d->def;} struct prepare_ctx_t {board_id_t preferred=0,hint=0; unsigned attempt=0; bool final_attempt=false;}; -struct probe_ctx_t : prepare_ctx_t {const board_def_t* candidate=nullptr; bool conditional_pins_unavailable=false;}; +struct probe_ctx_t : prepare_ctx_t {bool family_identified=false;const board_def_t* candidate=nullptr; bool conditional_pins_unavailable=false;}; } } const board_desc_t desc_core2={{1,"Core2",0}},desc_tough={{2,"Tough",0}},desc_cardputer={{3,"Cardputer",0}},desc_cardputer_adv={{4,"ADV",0}},desc_vameter={{5,"VAMeter",0}},desc_unitc6l={{6,"UnitC6L",0}},desc_nesson1={{7,"NessoN1",0}},desc_papers3={{8,"PaperS3",0}},desc_paperdiy={{9,"PaperDIY",0}}; `; @@ -266,10 +266,10 @@ namespace papermono {constexpr int internal_i2c_sda=1,internal_i2c_scl=2;}} namespace detail {constexpr int stopwatch_probe_addr=0x15,papermono_probe_addr=0x38; bool refine_papermono_touch(board_result_t&,const prepare_ctx_t&){return true;}} struct pulls_t {std::uint64_t pulldown_high,pullup_high;}; -int sw,pm,g12,g13,reads; +int sw,pm,g12,g13,reads;bool pm1_identity=true; pulls_t probe_pin_pulls(probe_ctx_t&,std::uint64_t){return {std::uint64_t(g12&1)<<12|std::uint64_t(g13&1)<<13,std::uint64_t((g12>>1)&1)<<12|std::uint64_t((g13>>1)&1)<<13};} bool probe_i2c_ack(probe_ctx_t&,int,int,int addr){++reads;assert(addr==0x15||addr==0x38);return addr==0x15?sw:pm;} -bool probe_i2c_read(probe_ctx_t&,int,int,int addr,int,std::uint8_t* data,int,int,int){data[0]=0x50;data[1]=0x20;assert(addr==0x6e);return true;} +bool probe_i2c_read(probe_ctx_t&,int,int,int addr,int,std::uint8_t* data,int,int,int){data[0]=0x50;data[1]=0x20;assert(addr==0x6e);return pm1_identity;} bool select_provisional_member(const prepare_ctx_t& ctx,board_result_t* result,const board_desc_t* preferred_if_possible,const board_desc_t* hinted_if_possible,const board_desc_t* family_default,const char* why) ${helper} bool confirm_member(probe_ctx_t& ctx,board_result_t* result) ${confirm} int main(){ @@ -277,7 +277,7 @@ int main(){ for(bool final:{false,true})for(board_id_t preferred:{board_id_t(0),board_id_t(20),board_id_t(21),board_id_t(42)}) for(board_id_t hint:{board_id_t(0),board_id_t(20),board_id_t(21)}){ probe_ctx_t ctx;ctx.final_attempt=final;ctx.attempt=final?4:0;ctx.preferred=preferred;ctx.hint=hint; - board_result_t r;reads=0;bool ok=confirm_member(ctx,&r);assert(reads==2); + board_result_t r;reads=0;bool ok=confirm_member(ctx,&r);assert(reads==2&&ctx.family_identified); int expected=0;bool provisional=false,refine=false; if(sw!=pm)expected=sw?20:21; else if(!sw&&((g12==0&&g13==0)||(g12==3&&g13==3))){expected=21;refine=true;} @@ -290,6 +290,7 @@ int main(){ detect_outcome_t out;out.result=r;if(provisional)assert(!should_persist_detection(out,0));} assert(stopwatch_power==(ok&&expected==20));assert(papermono_power==(ok&&expected==21)); } + pm1_identity=false;probe_ctx_t ctx;board_result_t r;assert(!confirm_member(ctx,&r)&&!ctx.family_identified); } `,"PM1 pre-power table"); }); @@ -330,7 +331,7 @@ test("production prepare retries failed power before pending member refinement", struct {int hw_port,sda,scl;} internal_i2c;struct {reset_kind_t kind;} reset;};`) .replace('bool final_attempt=false;','bool final_attempt=false; int* transaction=nullptr; int i2c_port_probe=0;'); await compileRun(extended+` -int power_calls,refine_calls,sd_calls,construct_calls;bool fail_power; +int power_calls,refine_calls,sd_calls,construct_calls,observe_calls;bool fail_power,fail_observe; namespace lgfx {namespace i2c {struct value_t {bool has_value() const{return true;}int value() const{return 0;}}; bool isInitialized(int){return false;}value_t getPinSDA(int){return {};} value_t getPinSCL(int){return {};}value_t init(int,int,int){return {};}}} @@ -344,17 +345,19 @@ namespace startup_detail { } bool prepare_reset(const board_desc_t&,board_result_t&,const prepare_ctx_t&,int,void*,bool){return true;} bool refine_member(board_result_t&,const prepare_ctx_t&){++refine_calls;return true;} +bool observe_member(board_result_t&,const prepare_ctx_t&){++observe_calls;return !fail_observe;} bool prepare_member(const board_desc_t& desc,board_result_t& result,const prepare_ctx_t& ctx) ${prepare} int main(){int tx=0,variant=0; // Pending refine represents both D/D and U/U; no refine represents touch-confirmed members. - for(int row:{0,1,2})for(bool final:{false,true})for(bool failed:{false,true})for(bool scoped:{false,true}){ + for(int row:{0,1,2})for(bool final:{false,true})for(bool failed:{false,true})for(bool scoped:{false,true})for(bool observation:{false,true})for(bool observation_failed:{false,true}){ board_desc_t desc={{21,"PaperMono",0},{scoped?&variant:nullptr},{-1,47,48},{reset_kind_t::none}}; - board_result_t r;r.assign(&desc);r.refine=row<2?refine_member:nullptr; + board_result_t r;r.assign(&desc);r.refine=row<2?refine_member:nullptr;r.observe_after_power=(row==2&&observation)?observe_member:nullptr; prepare_ctx_t ctx;ctx.transaction=&tx;ctx.final_attempt=final; - power_calls=refine_calls=sd_calls=construct_calls=0;fail_power=failed; + power_calls=refine_calls=sd_calls=construct_calls=observe_calls=0;fail_power=failed;fail_observe=observation_failed; const bool ok=prepare_member(desc,r,ctx);if(ok)++construct_calls; const bool retry=failed&&row<2&&!final; - assert(ok==!retry&&construct_calls==int(!retry));assert(power_calls==1); + assert(ok==!retry&&construct_calls==int(!retry));assert(power_calls==1);assert(observe_calls==int(row==2&&observation&&!failed)); + assert(bool(r.prepared&prepared_observation)==(row==2&&observation)); assert(refine_calls==int(row<2&&!failed));assert(sd_calls==int(!retry)); assert(r.provisional==(failed&&row<2&&final)); assert(bool(r.prepared&prepared_refine)==(row<2&&!retry)); @@ -367,3 +370,183 @@ int main(){int tx=0,variant=0; } `,"pending-refinement power failure"); }); + +test("production AXP subdivision excludes unsafe Station fallbacks before power", async () => { + const source=await fs.readFile(path.join(src,"m5/esp32_d0wdq6.inl"),"utf8"); + const confirm=body(source.slice(source.indexOf('class axp_family_detector_t')),'bool confirm('); + const detector=await fs.readFile(path.join(src,"board_detect.inl"),"utf8"); + const helper=body(detector,'static bool select_provisional_member'); + const prepare=body(detector,'bool prepare(const board_desc_t& desc,'); + const extended=common.replace('#define ESP_LOGW', '#define ESP_LOGE(...) ((void)0)\n#define ESP_LOGW').replace('struct board_desc_t {board_def_t def;};',`enum class reset_kind_t {none,i2c_regs}; struct board_desc_t {board_def_t def;struct {const int* variants;} power; + struct {int hw_port,sda,scl;} internal_i2c;struct {std::int8_t dc,cs,sclk,mosi,miso;} display; + struct {int sd_cs;} sd;struct {int pin;reset_kind_t kind;} reset;};`) + .replace('struct prepare_ctx_t {', 'struct tx_t {templatevoid restore_start(T){}};\nstruct prepare_ctx_t {tx_t* transaction=nullptr; int i2c_port_probe=0; '); + await compileRun(extended+` +const board_desc_t desc_station={{22,"Station",0}}; +namespace generated_options {namespace core2 {constexpr unsigned new_pmic=1;}} +struct pmic_variant_t {unsigned id_value,detected_option;}; +namespace lgfx {namespace i2c {struct value_t {bool has_value() const{return true;}int value() const{return 0;}}; + bool isInitialized(int){return false;}value_t getPinSDA(int){return {};} + value_t getPinSCL(int){return {};}value_t init(int,int,int){return {};} + struct response_t {bool ok;bool has_value() const{return ok;}}; + response_t beginTransaction(int,int,int,bool);response_t endTransaction(int);}} +int pmic_id,core_ack,tough_ack,g12,g32,g33,ack_reads,station_probes,sd_calls,core_power,sense_calls; +bool panel_ok,pmic_ok=true,reset_ok=true; +namespace lgfx {namespace i2c { + response_t beginTransaction(int,int addr,int freq,bool){++ack_reads;assert(freq==100000);assert(addr==0x38||addr==0x2e);return {addr==0x38?bool(core_ack):bool(tough_ack)};} + response_t endTransaction(int){return {true};}}} +namespace startup_detail { + bool description_valid(const board_desc_t&){return true;}bool gpio_valid(int){return true;} + struct i2c_scope_t {bool opened=true;int port=0;i2c_scope_t(tx_t&,int,decltype(board_desc_t::internal_i2c)) {}}; + void pin_level(int,bool){} + void hold_chip_selects(const board_desc_t&){} + bool prepare_power(const board_desc_t& d,board_result_t& r,int,bool){if(d.def.id==1||d.def.id==2)++core_power;r.prepared|=prepared_power;return true;} + bool prepare_sd_spi(const board_desc_t&,board_result_t& r,const prepare_ctx_t&){++sd_calls;r.prepared|=prepared_sd_spi;return true;} +} +namespace detail {constexpr int station_pmic_id=3,core2_touch_address=0x38,tough_touch_address=0x2e; + constexpr unsigned station_id_mask=0xfb,station_id=0x81,panel_id_command=4; + pmic_variant_t pmic; + const pmic_variant_t* observe_core_pmic(int){pmic={unsigned(pmic_id),pmic_id==0x4a?1u:0u};return pmic_ok?&pmic:nullptr;} + bool sd_pull_mask(const board_desc_t&,std::uint64_t* mask){*mask=7;return true;} + bool refine_core_family(board_result_t&,const prepare_ctx_t&){return true;} + bool observe_confirmed_core_panel(board_result_t&,const prepare_ctx_t&){return true;}} +struct pulls_t {std::uint64_t pulldown_high,pullup_high;}; +pulls_t probe_pin_pulls(probe_ctx_t&,std::uint64_t mask){if(mask==7)return {7,7};++sense_calls; + return {std::uint64_t(g12&1)<<12|std::uint64_t(g32&1)<<32|std::uint64_t(g33&1)<<33, + std::uint64_t((g12>>1)&1)<<12|std::uint64_t((g32>>1)&1)<<32|std::uint64_t((g33>>1)&1)<<33};} +bool probe_i2c_ack(probe_ctx_t&,int,int,int addr){++ack_reads;assert(addr==0x38||addr==0x2e);return addr==0x38?core_ack:tough_ack;} +bool prepare_reset(const board_desc_t&,board_result_t&,const prepare_ctx_t&,int){return reset_ok;} +bool prepare_reset(const board_desc_t&,board_result_t&,const prepare_ctx_t&,int,void*,bool){return reset_ok;} +unsigned soft_spi_read32(probe_ctx_t&,int,int,int,int,int,int,int){++station_probes;return panel_ok?0x81:0;} +bool select_provisional_member(const prepare_ctx_t& ctx,board_result_t* result,const board_desc_t* preferred_if_possible,const board_desc_t* hinted_if_possible,const board_desc_t* family_default,const char* why) ${helper} +bool confirm_member(probe_ctx_t& ctx,board_result_t* result) ${confirm} +bool prepare_member(const board_desc_t& desc,board_result_t& result,const prepare_ctx_t& ctx) ${prepare} +int main(){tx_t tx; + for(int pmic_value:{3,0x4a})for(core_ack=0;core_ack<=1;++core_ack)for(tough_ack=0;tough_ack<=1;++tough_ack) + for(g12=0;g12<4;++g12)for(g32=0;g32<4;++g32)for(g33=0;g33<4;++g33) + for(bool final:{false,true})for(bool panel:{false,true}) + for(board_id_t preference:{board_id_t(0),board_id_t(1),board_id_t(2),board_id_t(22),board_id_t(42)}) + for(board_id_t hint:{board_id_t(0),board_id_t(1),board_id_t(2),board_id_t(22),board_id_t(42)}){ + probe_ctx_t ctx;ctx.transaction=&tx;ctx.final_attempt=final;ctx.attempt=final?4:0;ctx.preferred=preference;ctx.hint=hint; + pmic_id=pmic_value;panel_ok=panel;ack_reads=station_probes=sd_calls=core_power=sense_calls=0;board_result_t r; + bool ok=confirm_member(ctx,&r);int expected=0;bool provisional=false,refine=false,observe=false,station=false; + if(!(core_ack&&tough_ack)&&!(pmic_id==0x4a&&tough_ack)){ + if(core_ack||tough_ack){expected=core_ack?1:2;observe=true;} + else if(pmic_id==0x4a){if(final){expected=1;provisional=true;observe=true;}} + else if(g12==0&&g32==3&&g33==3){expected=1;refine=true;} + else if(g12==2||g12==0){station=true;if(panel)expected=22;}} + assert(ack_reads==2&&ok==(expected!=0));assert(ctx.family_identified); + assert(sense_calls==int(!core_ack&&!tough_ack&&pmic_id==3));assert(station_probes==int(station)&&sd_calls==int(station)); + if(ok){assert(r.def->id==expected&&r.provisional==provisional); + assert(bool(r.refine)==refine&&bool(r.observe_after_power)==observe); + assert(r.option==(pmic_id==0x4a?1u:0u));assert(prepare_member(*r.desc,r,ctx));} + assert(core_power==int(ok&&(expected==1||expected==2))); + if(station||(!core_ack&&!tough_ack&&pmic_id==3&&g12!=0&&g12!=2))assert(core_power==0); + } + probe_ctx_t ctx;ctx.transaction=&tx;board_result_t r;pmic_ok=false;assert(!confirm_member(ctx,&r));assert(!ctx.family_identified); +} +`,"AXP pre-power table"); +}); + +test("production AXP refinement reads both touches after SD preparation and panel-only observation preserves identity", async () => { + const source=await fs.readFile(path.join(src,"m5/esp32_d0wdq6.inl"),"utf8"); + const detector=await fs.readFile(path.join(src,"board_detect.inl"),"utf8"); + const panel=body(source,'bool observe_core_panel('),observer=body(source,'bool observe_confirmed_core_panel('),refine=body(source,'bool refine_core_family('); + const helper=body(detector,'static bool select_provisional_member'); + const extended=common.replace('struct board_desc_t {board_def_t def;};',`struct board_desc_t {board_def_t def; + struct {int sda,scl;} internal_i2c;struct {std::int8_t dc,cs,sclk,mosi,miso;} display;struct {int sd_cs;} sd;};`) + .replace('struct prepare_ctx_t {','struct tx_t {templatevoid restore_start(T){}};\nstruct prepare_ctx_t {tx_t* transaction=nullptr; int i2c_port_probe=0; bool allow_reset=false; '); + await compileRun(extended+` +namespace generated_options {namespace core2 {constexpr unsigned new_pmic=1,lcd_e=2;}} +enum class panel_variant_t {unknown,e}; +unsigned clock_ms;int sd_calls,panel_calls,tough_reads,core_reads;bool tough_ack,core_ack,panel_i2c_ok=true,panel_reset_ok=true; +namespace lgfx {unsigned millis(){return clock_ms;}void delay(unsigned ms){clock_ms+=ms;} +namespace i2c {struct response {bool ok;bool has_value(){return ok;}}; + response readRegister8(int,int addr,int,int){if(addr==0x2e){++tough_reads;return {tough_ack};}assert(addr==0x38);++core_reads;return {core_ack};}}} +namespace startup_detail {void pin_level(int,bool){} + struct i2c_scope_t {bool opened;int port=0;i2c_scope_t(tx_t&,int,decltype(board_desc_t::internal_i2c)):opened(panel_i2c_ok) {}}; + bool prepare_sd_spi(const board_desc_t&,board_result_t& r,const prepare_ctx_t&){if(!(r.prepared&prepared_sd_spi)){++sd_calls;r.prepared|=prepared_sd_spi;}return true;}} +struct soft_spi_t {soft_spi_t(int,int,int,int){}void init(){assert(sd_calls==1);}}; +panel_variant_t identify_panel_variant(soft_spi_t&,int,std::uint32_t*,int,bool=true){assert(sd_calls==1);++panel_calls;return panel_variant_t::e;} +bool prepare_reset(const board_desc_t&,board_result_t&,const prepare_ctx_t&,int){return panel_reset_ok;} +void log_panel_variant(panel_variant_t,const std::uint32_t*){} +constexpr int tough_touch_address=0x2e,core2_touch_address=0x38,touch_probe_register=0,tough_touch_i2c_frequency=400000,touch_startup_poll_ms=300; +bool select_provisional_member(const prepare_ctx_t& ctx,board_result_t* result,const board_desc_t* preferred_if_possible,const board_desc_t* hinted_if_possible,const board_desc_t* family_default,const char* why) ${helper} +bool observe_core_panel(board_result_t& result,const prepare_ctx_t& ctx,panel_variant_t& variant,std::uint32_t keys[4]) ${panel} +bool observe_panel(board_result_t& result,const prepare_ctx_t& ctx) ${observer} +bool refine_member(board_result_t& result,const prepare_ctx_t& ctx) ${refine} +int main(){tx_t tx; + for(bool new_pmic:{false,true})for(bool core:{false,true})for(bool tough:{false,true})for(bool final:{false,true}) + for(board_id_t preference:{board_id_t(0),board_id_t(1),board_id_t(2),board_id_t(22),board_id_t(42)}) + for(board_id_t hint:{board_id_t(0),board_id_t(1),board_id_t(2),board_id_t(22),board_id_t(42)}){ + prepare_ctx_t ctx;ctx.transaction=&tx;ctx.final_attempt=final;ctx.preferred=preference;ctx.hint=hint; + board_result_t r;r.assign(&desc_core2);r.option=new_pmic?1:0;sd_calls=panel_calls=tough_reads=core_reads=0;clock_ms=0;tough_ack=tough;core_ack=core; + bool ok=refine_member(r,ctx);assert(tough_reads==core_reads&&tough_reads>0);assert(sd_calls==1&&panel_calls==1); + const bool contradiction=(core&&tough)||(new_pmic&&tough); + assert(ok==(!contradiction&&(core||tough||final))); + if(ok){const auto possible=[&](board_id_t id){return id==1||(id==2&&!new_pmic);}; + const auto expected=core?1:tough?2:possible(preference)?preference:possible(hint)?hint:1; + assert(r.def->id==expected&&r.provisional==(!core&&!tough));assert(r.option==(unsigned(expected==2?0:new_pmic)|2));} + } + for(const auto* confirmed:{&desc_core2,&desc_tough}){ + board_result_t r;r.assign(confirmed);prepare_ctx_t ctx;ctx.transaction=&tx; + sd_calls=panel_calls=tough_reads=core_reads=0;tough_ack=core_ack=true; + assert(observe_panel(r,ctx));assert(r.def==&confirmed->def&&r.option==2&&!r.provisional); + assert(sd_calls==1&&panel_calls==1&&tough_reads==0&&core_reads==0); + assert(observe_panel(r,ctx));assert(sd_calls==1); + } + for(bool opened:{false,true})for(bool reset:{false,true}){ + board_result_t r;r.assign(&desc_core2);prepare_ctx_t ctx;ctx.transaction=&tx;ctx.allow_reset=true; + sd_calls=panel_calls=0;panel_i2c_ok=opened;panel_reset_ok=reset; + assert(observe_panel(r,ctx)==(opened&&reset));assert(r.def==&desc_core2.def&&!r.provisional); + if(!opened||!reset)assert(!(r.option&generated_options::core2::lcd_e)); + } +} +`,"AXP post-power refinement and observation"); +}); + +test("production detector runner stops later families only after identified AXP or PM1", async () => { + const source=await fs.readFile(path.join(src,"board_detect.inl"),"utf8"); + const run=body(source,'bool run_detector('),detect=body(source,'board_result_t detect_board('); + const extended=common.replace('#define ESP_LOGW', '#define ESP_LOGE(...) ((void)0)\n#define ESP_LOGW') + .replace('struct board_desc_t {board_def_t def;};','struct board_desc_t {board_def_t def;struct {const char* const* data;unsigned size;} option_names;};') + .replace('struct prepare_ctx_t {', 'struct tx_t {int rollbacks=0;void rollback(){++rollbacks;}};\nstruct prepare_ctx_t {tx_t* transaction=nullptr; ') + .replace('bool family_identified=false;', 'bool confirm_attempted=false;bool family_identified=false;'); + await compileRun(extended+` +#include +#include +#include +const char* tag="host";constexpr unsigned max_detector_families=4; +struct edge_t {board_id_t before,after;};const std::array detector_order_edges={}; +struct board_detector_t { + const board_def_t* const* members;board_detector_t(const board_def_t* const* m):members(m){} + virtual bool signature(probe_ctx_t&) const=0;virtual bool confirm(probe_ctx_t&,board_result_t*) const=0; + bool has_member(board_id_t id) const {for(auto p=members;*p;++p)if((*p)->id==id)return true;return false;} +}; +bool enabled(const probe_ctx_t&,board_id_t){return true;} +bool fallback_only(const board_detector_t*){return false;} +bool gpio_power_hold_family(const board_detector_t*){return false;} +bool run_detector(const board_detector_t* detector,probe_ctx_t& ctx,board_result_t* result) ${run} +board_result_t detect_board(const board_detector_t* const* list,board_id_t hint,probe_ctx_t& ctx) ${detect} +const board_desc_t desc_pm1={{20,"StopWatch",0}}; +struct fake_detector_t:board_detector_t { + mutable int signatures=0,confirms=0;bool signature_ok=true,id_ok=false,member_ok=false;const board_desc_t* desc; + fake_detector_t(const board_def_t* const* members,const board_desc_t* d):board_detector_t(members),desc(d){} + bool signature(probe_ctx_t&) const override {++signatures;return signature_ok;} + bool confirm(probe_ctx_t& ctx,board_result_t* r) const override {++confirms;if(!id_ok)return false;ctx.family_identified=true;if(!member_ok)return false;r->assign(desc);return true;} +}; +int main(){for(const auto* desc:{&desc_core2,&desc_pm1})for(bool signature:{false,true})for(bool identified:{false,true})for(bool member:{false,true}){ + const board_def_t* first_members[]={&desc->def,nullptr};const board_def_t* later_members[]={&desc_tough.def,nullptr}; + fake_detector_t first(first_members,desc),later(later_members,&desc_tough);first.signature_ok=signature;first.id_ok=identified;first.member_ok=member;later.id_ok=later.member_ok=true; + const board_detector_t* list[]={&first,&later,nullptr};tx_t tx;probe_ctx_t ctx;ctx.transaction=&tx;ctx.family_identified=true; + const auto result=detect_board(list,0,ctx);const bool stops=signature&&identified; + assert(first.signatures==1&&later.signatures==int(!stops));assert(first.confirms==int(signature)); + if(stops&&member)assert(result.def==&desc->def); + else if(stops)assert(result.status==detect_status_t::no_match&&result.desc==nullptr&&result.def->id==board_id_unknown); + else assert(result.def==&desc_tough.def); + // A known-family miss still rolls back this probe before the session retries. + if(signature&&!member)assert(tx.rollbacks==1); + } +} +`,"identified family stops subsequent probes"); +}); diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index e4c96a78..20e34097 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -1005,16 +1005,6 @@ namespace m5gfx probe.i2c_port_probe = probe_i2c_port; probe.transaction = &transaction; probe.enabled_ids = request.target_set; -#if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) -#if defined (M5GFX_AUTODETECT_TEST_STATION_TO_CORE2) - // Test-only: after the forced Station miss and Core2 match, convert the - // result to excluded so the retained success-state GPIOs are rolled back. - static const board_detect::board_id_t enabled_ids[] = { - board_detect::board_id_unknown, - }; - probe.enabled_ids = enabled_ids; -#endif -#endif #if defined (CONFIG_IDF_TARGET_ESP32S3) probe.conditional_pins_unavailable = conditional_pins_unavailable; #if defined (M5GFX_AUTODETECT_TEST_EXCLUDE_ATOMS3) diff --git a/src/board_detect/board_detect.hpp b/src/board_detect/board_detect.hpp index df3667b2..027e367b 100644 --- a/src/board_detect/board_detect.hpp +++ b/src/board_detect/board_detect.hpp @@ -435,6 +435,9 @@ namespace board_detect detector_workspace_t detector_workspace; // True once any family entered confirm() in this detection pass. bool confirm_attempted = false; + // A chip ID has established this family. Stop later family probes in this + // attempt even if its member is unresolved; retry/session verdicts stay unchanged. + bool family_identified = false; // A signature may suggest one board without entering confirm(). The first // detector in probe order owns the candidate for this attempt. const board_def_t* candidate = nullptr; diff --git a/src/board_detect/board_detect.inl b/src/board_detect/board_detect.inl index 3ef5c91f..94847eb4 100644 --- a/src/board_detect/board_detect.inl +++ b/src/board_detect/board_detect.inl @@ -423,6 +423,7 @@ namespace board_detect { board_result_t result; ctx.confirm_attempted = false; + ctx.family_identified = false; if (list == nullptr) { return result; } ctx.hint = hint; @@ -481,6 +482,7 @@ namespace board_detect if (chosen == count) { break; } // Generated constraints must be acyclic. tried[chosen] = true; if (run_detector(list[chosen], ctx, &result)) { return result; } + if (ctx.family_identified) { break; } if (list[chosen] == hinted) { ESP_LOGD(tag, "hint=%u did not match its detector family", static_cast(hint)); @@ -1882,6 +1884,19 @@ namespace board_detect { current = result.desc; } + if (!(result.prepared & prepared_observation) && result.observe_after_power != nullptr) + { + // Panel metadata is optional after retained power failure; the member + // was already confirmed before this observer was attached. + if (!(result.prepared & prepared_power_failed) + && !result.observe_after_power(result, ctx)) + { + // Optional panel metadata must not discard a member confirmed by touch. + ESP_LOGW("board_detect", "post-power observation failed; retaining board=%u", + static_cast(result.def->id)); + } + result.prepared |= prepared_observation; + } if (!startup_detail::prepare_sd_spi(*current, result, ctx)) { return false; } const bool reset_was_prepared = result.prepared & prepared_reset; if (!reset_was_prepared) diff --git a/src/board_detect/detect_types.hpp b/src/board_detect/detect_types.hpp index 7768b926..92997aa1 100644 --- a/src/board_detect/detect_types.hpp +++ b/src/board_detect/detect_types.hpp @@ -49,6 +49,7 @@ namespace board_detect prepared_refine = 1u << 5, // The confirmed board was retained after its power operation list stopped. prepared_power_failed = 1u << 6, + prepared_observation = 1u << 7, }; enum class detect_status_t : std::uint8_t @@ -80,6 +81,9 @@ namespace board_detect detect_status_t status = detect_status_t::no_match; // Optional read-only member refinement after power preparation. refine_fn_t refine = nullptr; + // Optional post-power observation for an already confirmed member. Unlike + // refinement, failure of retained power may skip it without losing identity. + refine_fn_t observe_after_power = nullptr; void assign(const board_desc_t* value); }; diff --git a/src/board_detect/m5/esp32_d0wdq6.inl b/src/board_detect/m5/esp32_d0wdq6.inl index b0f4f07b..bf78a45c 100644 --- a/src/board_detect/m5/esp32_d0wdq6.inl +++ b/src/board_detect/m5/esp32_d0wdq6.inl @@ -220,6 +220,10 @@ namespace m5 bool observe_core_panel(board_result_t& result, const prepare_ctx_t& ctx, panel_variant_t& variant, std::uint32_t keys[4]) { + // Power may have just enabled a cold SD card. Put it in SPI mode before + // sharing the panel clocks, including the fixed-board startup path. + if (!startup_detail::prepare_sd_spi(*result.desc, result, ctx)) { return false; } + ESP_LOGD("board_detect_m5", "Core panel observation after SD SPI preparation"); const auto& display = desc_core2.display; const std::int8_t signals[] = { display.dc, display.sclk, display.mosi, display.miso @@ -279,6 +283,20 @@ namespace m5 return prepare(desc, result, ctx); } + bool observe_confirmed_core_panel(board_result_t& result, const prepare_ctx_t& ctx) + { + panel_variant_t variant; + std::uint32_t keys[4] = {}; + const bool observed = observe_core_panel(result, ctx, variant, keys); + const auto& display = desc_core2.display; + const std::int8_t signals[] = { display.dc, display.sclk, display.mosi, display.miso }; + ctx.transaction->restore_start(signals); + if (!observed) { return false; } + log_panel_variant(variant, keys); + if (variant == panel_variant_t::e) { result.option |= generated_options::core2::lcd_e; } + return true; + } + bool refine_core_family(board_result_t& result, const prepare_ctx_t& ctx) { const auto& display = desc_core2.display; @@ -303,27 +321,25 @@ namespace m5 tough = lgfx::i2c::readRegister8( i2c.port, tough_touch_address, touch_probe_register, tough_touch_i2c_frequency).has_value(); - if (tough) { break; } core2 = lgfx::i2c::readRegister8( i2c.port, core2_touch_address, touch_probe_register, tough_touch_i2c_frequency).has_value(); - if (core2) { break; } +#if defined (M5GFX_AUTODETECT_TEST_FAIL_CORE2_TOUCH) || defined (M5GFX_AUTODETECT_TEST_FAIL_CORE2_TOUCH_POST) + tough = core2 = false; +#endif + // M-Bus devices can answer at either address. Read both even if Tough + // answers first; simultaneous responses cannot confirm a member. + if (tough || core2) { break; } lgfx::delay(1); } while (lgfx::millis() - touch_start < touch_startup_poll_ms); -#if defined (M5GFX_AUTODETECT_TEST_FAIL_CORE2_TOUCH) - tough = core2 = false; -#endif - // Core2 requires its own touch response; a Station LCD miss on shared - // AXP192 must not be saved as Core2 when neither touch answers. + if ((tough && core2) + || (tough && (result.option & generated_options::core2::new_pmic))) + { + ctx.transaction->restore_start(signals); + return false; + } if (!tough && !core2) { - // An AXP192 with no LCD or touch response may still be Station. - if (variant == panel_variant_t::unknown - && !(result.option & generated_options::core2::new_pmic)) - { - ctx.transaction->restore_start(signals); - return false; - } const auto possible = [&](board_id_t id) -> const board_desc_t* { if (id == desc_core2.def.id) { return &desc_core2; } @@ -384,11 +400,80 @@ namespace m5 prepare_ctx.i2c_port_probe = i2c.port; const auto* pmic = detail::observe_core_pmic(i2c.port); if (pmic == nullptr) { return false; } + ctx.family_identified = true; ESP_LOGD("board_detect_m5", "power controller id=%02x", pmic->id_value); + const bool new_pmic = pmic->id_value != detail::station_pmic_id; + // Keep the scoped port alive; probe_i2c_ack would release it between reads. + const auto touch_answers = [&](std::uint8_t addr) -> bool + { + const bool began = lgfx::i2c::beginTransaction(i2c.port, addr, 100000, false).has_value(); + const bool ended = lgfx::i2c::endTransaction(i2c.port).has_value(); + return began && ended; + }; + bool core2_touch = touch_answers(detail::core2_touch_address); + bool tough_touch = touch_answers(detail::tough_touch_address); +#if defined (M5GFX_AUTODETECT_TEST_FAIL_CORE2_TOUCH) || defined (M5GFX_AUTODETECT_TEST_FAIL_CORE2_TOUCH_PRE) || defined (M5GFX_AUTODETECT_TEST_CORE_FAMILY_FORCE_STATION) + core2_touch = tough_touch = false; +#endif + char g12 = '-', g32 = '-', g33 = '-'; + // Port A is external. Sample its pulls only for silent AXP192 members; + // positive touch evidence and AXP2101 do not need this subdivision. + if (!core2_touch && !tough_touch && !new_pmic) + { + const auto pulls = probe_pin_pulls(ctx, (1ULL << 12) | (1ULL << 32) | (1ULL << 33)); + const auto classify = [&](unsigned pin) -> char + { + const unsigned sample = ((pulls.pulldown_high >> pin) & 1) + | (((pulls.pullup_high >> pin) & 1) << 1); + return "DXFU"[sample]; + }; + g12 = classify(12); g32 = classify(32); g33 = classify(33); + } +#if defined (M5GFX_AUTODETECT_TEST_CORE_FAMILY_FORCE_STATION) + g12 = 'F'; +#endif + ESP_LOGD("board_detect_m5", "AXP attempt=%u pmic=%02x touch38=%u touch2e=%u G12=%c G32=%c G33=%c", + ctx.attempt, pmic->id_value, core2_touch, tough_touch, g12, g32, g33); + *result = {}; + // External M-Bus ICs at these addresses can make the evidence contradictory. + if ((core2_touch && tough_touch) || (new_pmic && tough_touch)) { return false; } + if (core2_touch || tough_touch) + { + result->assign(core2_touch ? &desc_core2 : &desc_tough); + result->option = pmic->detected_option; + // Observe panel metadata without selecting the confirmed member again. + result->observe_after_power = detail::observe_confirmed_core_panel; + return true; + } + if (new_pmic) + { + // AXP2101 belongs only to Core2, but absent touch is abnormal. A failed + // touch controller deliberately consumes all attempts before fallback. + if (!select_provisional_member(ctx, result, &desc_core2, nullptr, nullptr, + "AXP2101 Core2 touch unanswered")) { return false; } + result->option = pmic->detected_option; + result->observe_after_power = detail::observe_confirmed_core_panel; + return true; + } + if (g12 == 'D' && g32 == 'U' && g33 == 'U') + { + // Cold AXP192 Core2 needs the shared Core2/Tough power list before touch + // confirmation. Station's weak G12 pull-down may also read D, so both + // Port A lines must read U. External pull-ups on both lines still admit + // such a Station; no second member power list runs in this attempt. + result->assign(&desc_core2); + result->option = pmic->detected_option; + result->refine = detail::refine_core_family; + return true; + } + if (g12 != 'F' && g12 != 'D') { return false; } + std::uint64_t sd_mask; if (!detail::sd_pull_mask(desc_core2, &sd_mask)) { return false; } - // Even pull probing toggles shared clocks, so deselect the LCD first. + // A cold AXP192 Core2/Tough with either Port A line externally held low + // also enters this Station path and stays unidentified after an LCD miss. + // Only the Station probe needs the exceptional pre-power SD transition. startup_detail::pin_level(desc_core2.display.cs, true); const auto sd_pulls = probe_pin_pulls(ctx, sd_mask); const bool sd_present = sd_pulls.pulldown_high == sd_mask @@ -396,53 +481,31 @@ namespace m5 std::uint32_t preprepared = 0; if (sd_present) { - // This exceptional pre-power transition protects the Station probe on - // powered Core2 revisions. Unpowered cards transition after PMIC power. startup_detail::pin_level(desc_core2.sd.sd_cs, true); board_result_t sd_result; sd_result.assign(&desc_core2); - if (!startup_detail::prepare_sd_spi(desc_core2, sd_result, prepare_ctx)) - { - return false; - } + if (!startup_detail::prepare_sd_spi(desc_core2, sd_result, prepare_ctx)) { return false; } preprepared |= sd_result.prepared & prepared_sd_spi; } - - auto try_station = [&]() -> bool + result->assign(&desc_station); + result->prepared = preprepared; + if (!prepare_reset(desc_station, *result, prepare_ctx, i2c.port)) { -#if !defined (M5GFX_AUTODETECT_TEST_STATION_TO_CORE2) - if (pmic->id_value != detail::station_pmic_id) { return false; } -#endif + ctx.transaction->restore_start(desc_station.reset.pin); + return false; + } + const auto& display = desc_station.display; + const auto detected_id = soft_spi_read32( + ctx, display.sclk, display.mosi, display.mosi, display.dc, display.cs, + detail::panel_id_command, 1); + if ((detected_id & detail::station_id_mask) != detail::station_id) + { + ctx.transaction->restore_start(desc_station.reset.pin); + // Neither hint nor preference may send Core2 power after a Station miss. *result = {}; - result->assign(&desc_station); - result->prepared = preprepared; - if (!prepare_reset(desc_station, *result, prepare_ctx, i2c.port)) - { - ctx.transaction->restore_start(desc_station.reset.pin); - return false; - } - const auto& display = desc_station.display; - const auto detected_id = soft_spi_read32( - ctx, display.sclk, display.mosi, display.mosi, display.dc, display.cs, - detail::panel_id_command, 1); - if ((detected_id & detail::station_id_mask) != detail::station_id) - { - ctx.transaction->restore_start(desc_station.reset.pin); - return false; - } - // A matching Station keeps reset and LCD CS inactive through prepare. - // Core2's SD CS may have been raised on the way here; Station does not own it. - ctx.transaction->restore_start(desc_core2.sd.sd_cs); - return true; - }; - - // Station must be excluded before accepting the shared AXP signature. - if (try_station()) { return true; } - *result = {}; - result->assign(&desc_core2); - result->option = pmic->detected_option; - result->prepared = preprepared; - result->refine = detail::refine_core_family; + return false; + } + ctx.transaction->restore_start(desc_core2.sd.sd_cs); return true; } diff --git a/src/board_detect/m5/esp32s3/families.inl b/src/board_detect/m5/esp32s3/families.inl index e33f7cc1..c13d1380 100644 --- a/src/board_detect/m5/esp32s3/families.inl +++ b/src/board_detect/m5/esp32s3/families.inl @@ -532,6 +532,7 @@ namespace m5 { return false; } + ctx.family_identified = true; // IOE1 takes hundreds of milliseconds to boot; its presence is not // member evidence. Wait only when its selected power sequence needs it. const bool stopwatch_touch = probe_i2c_ack( From 696843e8619aad79b2a817996e26c00549cde897 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:23:04 +0000 Subject: [PATCH 06/10] Classify pin pulls with a released sample Extend probe_pin_pulls() with optional released samples after each bias and add classify_pin_pull(): strong up/down, weak up/down, floating, pull dependent (no released sample) and conflicting. A zero release delay keeps the previous probe sequence. Replace the DualKey-only floating probe with the common probe and classifier. Its release wait grows from 32 to 128 microseconds so slower weak pulls are not taken as floating. Add C++11 host tests for all sample combinations. --- extras/board_spec/test/board_spec.test.js | 4 +- extras/board_spec/test/pull_release.test.js | 120 ++++++++++++++++++++ src/board_detect/board_detect.hpp | 10 +- src/board_detect/board_detect.inl | 35 +++--- src/board_detect/detect_class.hpp | 29 +++++ src/board_detect/m5/esp32s3/families.inl | 5 +- 6 files changed, 176 insertions(+), 27 deletions(-) create mode 100644 extras/board_spec/test/pull_release.test.js diff --git a/extras/board_spec/test/board_spec.test.js b/extras/board_spec/test/board_spec.test.js index de19cba9..a61e5167 100644 --- a/extras/board_spec/test/board_spec.test.js +++ b/extras/board_spec/test/board_spec.test.js @@ -622,8 +622,8 @@ test("DualKey requires NC pins to float and treats a pressed second key as a can assert.match(order, /esp32s3_detectors_qfn56\[] = \{[\s\S]*?&dualkey_detector,[\s\S]*?nullptr/); assert.doesNotMatch(order, /&stamps3_detector/); assert.doesNotMatch(families, /ctx\.candidate = &desc_stamps3\.def/); - assert.match(families, /probe_pin_floating\(ctx, 38, 32\) \|\| !probe_pin_floating\(ctx, 39, 32\)/); - assert.match(detector, /return charged_high && held_high && charged_low && held_low;/); + assert.match(families, /probe_pin_pulls\(ctx, \(std::uint64_t\(1\) << 38\) \| \(std::uint64_t\(1\) << 39\), pull_release_us\)[\s\S]*?classify_pin_pull\(socket_pulls, 38\) != pull_class_t::floating[\s\S]*?classify_pin_pull\(socket_pulls, 39\) != pull_class_t::floating/); + assert.doesNotMatch(detector, /bool probe_pin_floating\(/); assert.match(families, /ctx\.candidate = &desc_dualkey\.def;[\s\S]*?return false;/); }); diff --git a/extras/board_spec/test/pull_release.test.js b/extras/board_spec/test/pull_release.test.js new file mode 100644 index 00000000..647c68b9 --- /dev/null +++ b/extras/board_spec/test/pull_release.test.js @@ -0,0 +1,120 @@ +import { promises as fs } from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { body, compileRun } from "./detect_host_helpers.js"; +const here = path.dirname(fileURLToPath(import.meta.url)); +const src = path.resolve(here, "../../../src/board_detect"); + +test("release pull classifier covers every four-sample combination", async () => { + await compileRun(` +#include "board_detect/detect_class.hpp" +#include +#include +using namespace m5gfx::board_detect; +int main(){ + // Index bits are PD, PU, released PD, released PU. Values follow measured U/D/u/d/F/X semantics. + const pull_class_t expected[]={ + pull_class_t::down,pull_class_t::conflict,pull_class_t::weak_down,pull_class_t::up, + pull_class_t::down,pull_class_t::conflict,pull_class_t::conflict,pull_class_t::up, + pull_class_t::down,pull_class_t::conflict,pull_class_t::floating,pull_class_t::up, + pull_class_t::down,pull_class_t::conflict,pull_class_t::weak_up,pull_class_t::up}; + for(int pin:{0,17,63})for(unsigned sample=0;sample<16;++sample)for(bool released:{false,true}){ + const auto bit=std::uint64_t(1)< { + const source=await fs.readFile(path.join(src,"board_detect.inl"),"utf8"); + const probe=body(source,'pin_pull_result_t probe_pin_pulls('); + const header=await fs.readFile(path.join(src,"board_detect.hpp"),"utf8"); + const declaration=header.match(/pin_pull_result_t probe_pin_pulls\([^;]+;/)[0]; + await compileRun(` +#include "board_detect/detect_class.hpp" +#include +#include +#include +using namespace m5gfx::board_detect; +struct event_t {int pin,kind;unsigned value;bool operator==(const event_t& b)const{return pin==b.pin&&kind==b.kind&&value==b.value;}}; +std::vector events;std::vector samples;unsigned cursor;int current_pin; +struct tx_t {void restore_start(std::int8_t pin){events.push_back({pin,3,0});}}; +struct probe_ctx_t {tx_t* transaction;}; +${declaration} +namespace lgfx { +enum class pin_mode_t {input_pulldown,input_pullup,input}; +void pinMode(int pin,pin_mode_t mode){current_pin=pin;events.push_back({pin,0,unsigned(mode)});} +void delayMicroseconds(unsigned us){events.push_back({current_pin,1,us});} +bool gpio_in(int pin){const bool value=samples.at(cursor++);events.push_back({pin,2,unsigned(value)});return value;}} +pin_pull_result_t probe_pin_pulls(probe_ctx_t& ctx,std::uint64_t pin_mask,std::uint32_t release_us) ${probe} +int main(){tx_t tx;probe_ctx_t ctx={&tx};const std::uint64_t mask=(1ull<<3)|(1ull<<9); + samples={false,true,true,false};cursor=0; + const auto legacy=probe_pin_pulls(ctx,mask); + const std::vector expected_legacy={ + {3,0,0},{3,1,10},{3,2,0},{3,0,1},{3,1,10},{3,2,1},{3,3,0}, + {9,0,0},{9,1,10},{9,2,1},{9,0,1},{9,1,10},{9,2,0},{9,3,0}}; + assert(events==expected_legacy&&cursor==4); + assert(legacy.pulldown_high==(1ull<<9)&&legacy.pullup_high==(1ull<<3)); + assert(legacy.pulldown_release_high==0&&legacy.pullup_release_high==0&&!legacy.release_sampled); + events.clear();cursor=0;probe_pin_pulls(ctx,mask,0);assert(events==expected_legacy&&cursor==4); + events.clear();samples={false,false,true,true,true,false,false,true};cursor=0; + const auto released=probe_pin_pulls(ctx,mask,128); + const std::vector expected_release={ + {3,0,0},{3,1,10},{3,2,0},{3,0,2},{3,1,128},{3,2,0},{3,0,1},{3,1,10},{3,2,1},{3,0,2},{3,1,128},{3,2,1},{3,3,0}, + {9,0,0},{9,1,10},{9,2,1},{9,0,2},{9,1,128},{9,2,0},{9,0,1},{9,1,10},{9,2,0},{9,0,2},{9,1,128},{9,2,1},{9,3,0}}; + assert(events==expected_release&&cursor==8); + assert(released.pulldown_high==(1ull<<9)&&released.pullup_high==(1ull<<3)); + assert(released.pulldown_release_high==0&&released.pullup_release_high==mask&&released.release_sampled); + assert(classify_pin_pull(released,3)==pull_class_t::floating); + assert(classify_pin_pull(released,9)==pull_class_t::conflict); + events.clear();cursor=0;probe_pin_pulls(ctx,0,128);assert(events.empty()&&cursor==0); +} +`,"per-pin release probe sequence"); +}); + +test("production DualKey rejects weak pulls with the shared release delay and preserves OPI exclusion", async () => { + const source=await fs.readFile(path.join(src,"m5/esp32s3/families.inl"),"utf8"); + const header=await fs.readFile(path.join(src,"board_detect.hpp"),"utf8"); + const constant=header.match(/static constexpr std::uint32_t pull_release_us = \d+;/)[0]; + const signature=body(source.slice(source.indexOf('class dualkey_detector_t')),'bool signature('); + const fixture=` +#include "board_detect/detect_class.hpp" +#include +#include +using namespace m5gfx::board_detect; +${constant} +struct {int def=1;} desc_dualkey; +struct probe_ctx_t {const int* candidate=nullptr;}; +pin_pull_result_t socket;int probes;bool pressed; +pin_pull_result_t probe_pin_pulls(probe_ctx_t&,std::uint64_t mask,std::uint32_t release=0){++probes; + if(mask==((1ull<<38)|(1ull<<39))){assert(release==128);return socket;} + assert(release==0&&(mask==(1ull<<21)||mask==(1ull<<17))); + pin_pull_result_t r;r.pulldown_high=(mask==(1ull<<17)&&pressed)?0:mask;return r;} +bool signature_member(probe_ctx_t& ctx) ${signature} +int main(){ +#ifdef CONFIG_SPIRAM_MODE_OCT + probe_ctx_t ctx;assert(!signature_member(ctx)&&probes==0&&ctx.candidate==nullptr); +#else + for(unsigned a=0;a<16;++a)for(unsigned b=0;b<16;++b)for(bool key:{false,true}){ + socket={};socket.release_sampled=true;for(int pin:{38,39}){const unsigned sample=pin==38?a:b;const auto bit=1ull<restore_start(static_cast(pin)); } return result; } - bool probe_pin_floating(probe_ctx_t& ctx, std::int8_t pin, std::uint32_t release_us) - { - lgfx::pinMode(pin, lgfx::pin_mode_t::input_pullup); - lgfx::delayMicroseconds(10); - const bool charged_high = lgfx::gpio_in(pin); - lgfx::pinMode(pin, lgfx::pin_mode_t::input); - lgfx::delayMicroseconds(release_us); - const bool held_high = lgfx::gpio_in(pin); - - lgfx::pinMode(pin, lgfx::pin_mode_t::input_pulldown); - lgfx::delayMicroseconds(10); - const bool charged_low = !lgfx::gpio_in(pin); - lgfx::pinMode(pin, lgfx::pin_mode_t::input); - lgfx::delayMicroseconds(release_us); - const bool held_low = !lgfx::gpio_in(pin); - ctx.transaction->restore_start(pin); - return charged_high && held_high && charged_low && held_low; - } - dedicated_release_result_t probe_dedicated_pin_release( probe_ctx_t& ctx, const std::int8_t* pins, std::uint8_t pin_count, std::uint16_t reads, std::uint8_t samples, std::uint32_t settle_us) diff --git a/src/board_detect/detect_class.hpp b/src/board_detect/detect_class.hpp index fbcd7f0c..bb81399a 100644 --- a/src/board_detect/detect_class.hpp +++ b/src/board_detect/detect_class.hpp @@ -8,8 +8,37 @@ namespace m5gfx { namespace board_detect { { std::uint64_t pulldown_high = 0; std::uint64_t pullup_high = 0; + std::uint64_t pulldown_release_high = 0; + std::uint64_t pullup_release_high = 0; + bool release_sampled = false; }; + // Catalog detect_class floating follows internal pulls after 10 us only. + // This floating class additionally requires both biased levels to survive release. + enum class pull_class_t : std::uint8_t + { + up, down, weak_up, weak_down, floating, conflict, pull_dependent, + }; + + // Requires a measured pin in 0..63. Pull-dependent pins without release + // samples cannot be distinguished as floating or weakly biased. + inline pull_class_t classify_pin_pull(const pin_pull_result_t& measured, int pin) + { + const auto bit = std::uint64_t(1) << pin; + const bool pd = measured.pulldown_high & bit; + const bool pu = measured.pullup_high & bit; + if (pd && pu) { return pull_class_t::up; } + if (!pd && !pu) { return pull_class_t::down; } + if (pd && !pu) { return pull_class_t::conflict; } + if (!measured.release_sampled) { return pull_class_t::pull_dependent; } + const bool pd_release = measured.pulldown_release_high & bit; + const bool pu_release = measured.pullup_release_high & bit; + if (!pd_release && pu_release) { return pull_class_t::floating; } + if (!pd_release && !pu_release) { return pull_class_t::weak_down; } + if (pd_release && pu_release) { return pull_class_t::weak_up; } + return pull_class_t::conflict; + } + struct detect_class_expected_t { std::uint64_t mask; diff --git a/src/board_detect/m5/esp32s3/families.inl b/src/board_detect/m5/esp32s3/families.inl index c13d1380..6ada5d02 100644 --- a/src/board_detect/m5/esp32s3/families.inl +++ b/src/board_detect/m5/esp32s3/families.inl @@ -950,7 +950,10 @@ namespace m5 // that populate either NC net. constexpr std::uint64_t g21 = std::uint64_t(1) << 21; if (!(probe_pin_pulls(ctx, g21).pulldown_high & g21)) { return false; } - if (!probe_pin_floating(ctx, 38, 32) || !probe_pin_floating(ctx, 39, 32)) + // Release for 128 us instead of 32 us so a slow weak pull is no longer mistaken for NC. + const auto socket_pulls = probe_pin_pulls(ctx, (std::uint64_t(1) << 38) | (std::uint64_t(1) << 39), pull_release_us); + if (classify_pin_pull(socket_pulls, 38) != pull_class_t::floating + || classify_pin_pull(socket_pulls, 39) != pull_class_t::floating) { return false; } constexpr std::uint64_t g17 = std::uint64_t(1) << 17; if (probe_pin_pulls(ctx, g17).pulldown_high & g17) { return true; } From 63e56de622ade9b3a0f0ca40d71050b8a2784ded Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:23:04 +0000 Subject: [PATCH 07/10] Select CoreS3 and Cardputer members from positive evidence CoreS3 family: the camera or a long release band confirms the camera family; ambiguous or unavailable release without a camera retries and selects fallback, hint or representative only on the final attempt (unavailable deliberately uses all five attempts). StackChan evidence (G5/G6/G7 base pulls, then IOE ACK and firmware in a gated 50 ms window) is observed before member selection. An ACK with unreadable firmware retries before a provisional member; base firmware below 0x04 remains CoreS3. Confirmed SE is kept when camera observation fails and dedicated GPIO timing is unavailable. Cardputer family: read both variant ICs when both pin pairs are high; conflicting answers fail, and the plain Cardputer requires all four lines to float after release. Booting with a key pressed reaches a displayable provisional member after all attempts. Carry the release band in detector-private refine state. Add catalog notes, injection controls and host outcome tables. --- .../board_spec/boards/m5stack_stackchan.json | 9 +- extras/board_spec/dist/board_spec_editor.html | 2 +- .../generated/resolved/m5stack_stackchan.json | 9 +- extras/board_spec/test/board_spec.test.js | 6 +- .../board_spec/test/detect_families.test.js | 10 +- extras/board_spec/test/s4c.test.js | 157 ++++++++++++++++++ src/board_detect/detect_types.hpp | 2 + src/board_detect/m5/esp32s3/cores3.inl | 147 ++++++++++++---- src/board_detect/m5/esp32s3/families.inl | 50 +++--- 9 files changed, 329 insertions(+), 63 deletions(-) create mode 100644 extras/board_spec/test/s4c.test.js diff --git a/extras/board_spec/boards/m5stack_stackchan.json b/extras/board_spec/boards/m5stack_stackchan.json index 74fe5b63..0791e777 100644 --- a/extras/board_spec/boards/m5stack_stackchan.json +++ b/extras/board_spec/boards/m5stack_stackchan.json @@ -75,7 +75,8 @@ ], "verified": { "roles": "generated" - } + }, + "note": "Base pulls GPIO5 D (measured)." }, "6": { "roles": [ @@ -83,7 +84,8 @@ ], "verified": { "roles": "generated" - } + }, + "note": "Base pulls GPIO6 U (measured)." }, "7": { "roles": [ @@ -91,7 +93,8 @@ ], "verified": { "roles": "generated" - } + }, + "note": "Base pulls GPIO7 D (measured)." }, "8": { "roles": [ diff --git a/extras/board_spec/dist/board_spec_editor.html b/extras/board_spec/dist/board_spec_editor.html index 7c1145e8..93df81c2 100644 --- a/extras/board_spec/dist/board_spec_editor.html +++ b/extras/board_spec/dist/board_spec_editor.html @@ -964,7 +964,7 @@

- + - - + +