Skip to content

Feature/#99 - #100

Merged
p14c31355 merged 103 commits into
mainfrom
feature/#99
Aug 28, 2025
Merged

Feature/#99#100
p14c31355 merged 103 commits into
mainfrom
feature/#99

Conversation

@p14c31355

Copy link
Copy Markdown
Owner

🚀 Pull Request

Overview


Change details

  • New feature
  • Refactoring
  • Bug fix
  • CI / Build settings correction
  • Documentation update

Build / Test Results

$ cargo check     #
$ cargo test      #

@p14c31355 p14c31355 self-assigned this Aug 26, 2025
@p14c31355 p14c31355 added the enhancement New feature or request label Aug 26, 2025

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @p14c31355, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a substantial refactoring of the I2C command sequence explorer module. The primary goal is to improve the architecture by separating the core permutation algorithm from the I2C execution logic, making the code more modular and testable. Key changes include the adoption of an iterator-based API for permutations, the use of const generics for flexible capacity management, and the abstraction of logging through a new Logger trait. Additionally, error handling has been made more robust, and the command dependency mechanism has been refined for clarity and reliability. The PrefixExecutor in the scanner module has also been optimized for better performance.

Highlights

  • Architectural Refactoring: The explorer module has undergone a significant architectural refactoring, separating the core permutation logic into a new PermutationIter struct, which now implements the Iterator trait for a more idiomatic and composable API.
  • Generic Capacity: The Explorer and related data structures now leverage Rust's const generics by using a generic constant N for capacity. This allows for compile-time fixed-size allocations, which is crucial for memory-constrained embedded environments.
  • Abstracted Logging: A new Logger trait has been introduced, providing a flexible and abstract way to handle logging. This decouples the logging mechanism from specific I/O implementations, making the library more adaptable to various embedded platforms without direct core::fmt::Write dependencies.
  • Enhanced Error Handling: The ExplorerError enum has been expanded to include more granular error types such as DependencyCycle, NoValidAddressesFound, ExecutionFailed, and BufferOverflow, providing more robust and informative error handling.
  • Updated Dependency Mechanism: The CmdNode dependencies have been updated to refer to command indices (usize) instead of the first byte of the dependent command (u8). This change provides a more explicit and robust way to define the dependency graph, preventing potential ambiguities.
  • Optimized PrefixExecutor: The PrefixExecutor in scanner.rs has been optimized to apply the initial sequence only once per I2C address when it's first encountered, rather than for every command. This significantly improves performance by reducing redundant I2C bus operations.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@p14c31355 p14c31355 linked an issue Aug 26, 2025 that may be closed by this pull request

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant and well-executed refactoring of the I2C command explorer. The separation of the permutation logic into a dedicated PermutationIter and the introduction of Logger and CmdExecutor traits greatly improve modularity, testability, and separation of concerns. The change from byte-based to index-based dependencies is also a major improvement in robustness.

My review focuses on a few areas for further improvement:

  • A potential correctness issue in how the initialization sequence is sent in the new PrefixExecutor.
  • Several opportunities to enhance performance by replacing linear searches over Vec with constant-time lookups in boolean arrays, which is more suitable for the embedded context.

Overall, this is a high-quality refactoring that makes the code more robust and maintainable. Addressing the feedback will further enhance its correctness and performance.

Comment thread src/scanner.rs Outdated
Comment on lines +421 to +424
let mut init_buf: Vec<u8, 64> = Vec::new();
init_buf.push(self.prefix).map_err(|_| ())?;
init_buf.extend_from_slice(&self.init_sequence).map_err(|_| ())?;
i2c.write(addr, &init_buf).map_err(|_| ())?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The new PrefixExecutor sends the entire initialization sequence as a single I2C write transaction. The previous implementation sent each command from the sequence in a separate transaction.

Many I2C devices require each command to be in its own transaction. Sending the whole sequence at once might cause the device initialization to fail. It's safer to revert to the previous behavior of iterating through the init_sequence and sending each command individually.

Suggested change
let mut init_buf: Vec<u8, 64> = Vec::new();
init_buf.push(self.prefix).map_err(|_| ())?;
init_buf.extend_from_slice(&self.init_sequence).map_err(|_| ())?;
i2c.write(addr, &init_buf).map_err(|_| ())?;
// First, send the init_sequence with the prefix
for &c in self.init_sequence.iter() {
let command = [self.prefix, c];
i2c.write(addr, &command).map_err(|_| ())?;
}

Comment thread src/explorer.rs Outdated
Comment on lines +342 to +344
let deps_satisfied = node.deps.iter().all(|&d| {
self.staged_and_current_indices.contains(&d)
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation checks for dependency satisfaction by calling .contains() on staged_and_current_indices, which is an O(N) operation. This check is in a hot loop within the permutation generation, and a similar check exists in the stage function. This can lead to significant performance degradation as the number of commands increases.

For better performance, consider replacing the staged_and_current_indices: Vec<usize, N> with a boolean array, like is_processed: [bool; N]. This would allow for O(1) dependency checks.

Here's how you could adapt the logic:

  1. In Explorer::permutations, initialize a [bool; N] array, setting true for all indices that are part of the initial staged_indices.
  2. Pass this boolean array to PermutationIter.
  3. In try_extend, check dependencies with self.is_processed[d].
  4. When a command is added, set self.is_processed[idx] = true.
  5. In backtrack, reset it with self.is_processed[node_idx] = false.

This change would also apply to the stage function and would remove the need for the potentially panicking .unwrap() in backtrack since you would no longer need to find and remove elements from the Vec.

Comment thread src/scanner.rs Outdated
use heapless::Vec;

// Check if the address has already been initialized
if self.initialized_addrs.iter().find(|&&a| a == addr).is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check to see if an address has been initialized is done by a linear search (.iter().find()) on initialized_addrs. Since this check is performed for every command executed by the explorer, it can become a performance bottleneck.

A more performant approach would be to use a boolean array for tracking initialized addresses, which provides O(1) lookup time.

You would need to change the PrefixExecutor struct definition at line 397 from initialized_addrs: heapless::Vec<u8, 128> to initialized_addrs: [bool; 128] and initialize it accordingly in new().

Suggested change
if self.initialized_addrs.iter().find(|&&a| a == addr).is_none() {
if !self.initialized_addrs[addr as usize] {

@p14c31355

Copy link
Copy Markdown
Owner Author

/gemini summary

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

This pull request introduces a significant refactoring of the I2C command sequence explorer, enhancing its modularity, flexibility, and error handling, particularly for embedded systems. The core algorithm is now separated from I2C execution logic, and a new iterator-based API improves testability and composability.

Highlights

  • Core Explorer Refactoring: The I2C command sequence explorer has been extensively refactored, separating the core permutation algorithm from I2C execution.
  • New Permutation Iterator: A new PermutationIter provides a stack-safe, non-recursive way to generate command permutations, improving robustness for memory-constrained environments.
  • Generic Capacity Support: The Explorer now supports generic capacity (N), allowing for flexible memory allocation based on device constraints.
  • Abstracted Logging and Execution: Logging and command execution are now abstracted through new Logger and CmdExecutor traits, enabling custom implementations.
  • Enhanced Error Handling: Error handling has been made more robust with an expanded ExplorerError enum, including specific errors for dependency cycles and buffer overflows.
  • Updated CmdNode Dependencies: The CmdNode dependency mechanism was updated to use indices instead of command bytes, improving clarity and consistency.
  • Scanner Module Updates: The run_explorer function in scanner.rs was updated to integrate with the new Logger trait and a refactored PrefixExecutor.
Changelog
  • src/explorer.rs
    • Refactored Explorer to manage dependency graphs and generate permutations.
    • Introduced PermutationIter for iterative, stack-safe permutation generation.
    • Defined CmdExecutor and Logger traits for abstracting I2C operations and logging.
    • Expanded ExplorerError enum to include DependencyCycle, NoValidAddressesFound, ExecutionFailed, and BufferOverflow.
    • Updated CmdNode to use usize for dependencies instead of u8.
    • Removed CMD_CAPACITY constant and PermutationState struct.
    • Removed internal hex_byte, write_sequence, and write_unresolved_sequence helper functions.
    • Updated the explore method to utilize the new PermutationIter and Logger traits.
  • src/prelude.rs
    • Removed PrefixExecutor from public exports.
  • src/scanner.rs
    • Added a file header comment.
    • Modified run_explorer to accept a const generic N for capacity.
    • Integrated a new SerialLogger wrapper to bridge core::fmt::Write with the new Logger trait.
    • Refactored PrefixExecutor to ensure the initial sequence is sent only once per I2C address.
Activity
  • The author, p14c31355, requested a summary of the pull request.
  • gemini-code-assist[bot] provided a high-priority review comment regarding the PrefixExecutor in scanner.rs, suggesting to revert a change that sends the entire initialization sequence as a single I2C write transaction, as many I2C devices require individual transactions.
  • gemini-code-assist[bot] provided a medium-priority review comment on PermutationIter in explorer.rs, recommending replacing the O(N) .contains() check for dependency satisfaction with an O(1) boolean array lookup to improve performance.
  • gemini-code-assist[bot] provided a medium-priority review comment on PrefixExecutor in scanner.rs, suggesting to replace the O(N) linear search for initialized_addrs with an O(1) boolean array lookup for better performance.

@p14c31355

Copy link
Copy Markdown
Owner Author

@gemini-code-assist

Refactoring Candidates

1. Separation of Responsibilities

  • Currently, Explorer has all of the following:

  • Toposort

  • Permutation Generation

  • Execution & Log Output

  • → Separating the "search algorithm" from the "I²C execution & log" makes it easier to reuse, even on small microcontrollers.

Example:

struct Explorer<'a> { sequence: &'a [CmdNode<'a>] }

impl<'a> Explorer<'a> {
fn stage(&self) -> (Vec<&[u8], CMD_CAPACITY>, Vec<usize, CMD_CAPACITY>);
fn permutations(&self, staged: &[&[u8]], unresolved: &[usize]) -> PermutationIter<'a>;
}

explore() combines these functions to create a wrapper that performs "I²C execution."


2. Hiding PermutationState

  • The PermutationState fields are directly accessed in the algorithm, but their responsibilities are broad.
  • → Only provide three methods: "push," "pop," and "can extend?", and do not directly manipulate the array from outside.

3. Abstraction of Log Processing

  • writeln!(serial, …) is scattered throughout each step of the algorithm.
  • → By providing a Logger trait and making it interchangeable with "minimal logging" for embedded applications and "verbose logging" for PCs, RAM/ROM can be reduced.

4. Change dependency resolution keys from u8 to enums

  • deps is easily misidentified because it is "identified only by the first byte."
  • → To ensure type safety, use enum CmdId { DisplayOn, DisplayOff, … } and make CmdNode have id: CmdId, bytes: &'a [u8], deps: &'a [CmdId].

5. Adjusting Constants and Genericization

  • CMD_CAPACITY and LOG_BUFFER_CAPACITY are fixed as global constants.
  • → By making the capacity selectable using generics like Explorer<'a, const N: usize>, the same code can be used on Uno and Cortex-M.

--

6. Revising the Error Design

  • Currently, only TooManyCommands is supported.

  • In reality:

  • Dependency cycle

  • I²C write failure

  • Insufficient stack/capacity in permutation search

It would be more user-friendly if it could return the following:

--

7. Iteratorization

  • permute executes directly in a loop, but it would be easier to test if it "returns all sequences as an iterator."
    (Changed PermutationIter to Iterator<Item = Vec<&[u8], N>>)

Summary

  • More loose coupling between algorithms (Explorer) and execution (Executor)
  • Safely encapsulate PermutationState
  • Log and capacity can be controlled using traits and generics
  • Improved dependency ID representation

Improved topological sorting efficiency: The Explorer::stage function in explorer.rs has been replaced with an algorithm based on in-degree. This improves dependency checking from a linear search to constant time (O(1)), significantly reducing computational cost.

Removed linear search: The linear search using heapless::Vec::contains has been replaced with a constant time (O(1)) check using a bitmask (a [bool] array). In explorer.rs, initialized_addrs has been replaced with a [bool; 128] array, and staged_and_current_indices has been replaced with a [bool; 128] array, eliminating the search overhead.

Reduced memory copies: The staged.clone() call in PermutationIter's next method has been removed. Instead, the logic builds a permutation on current and then combines staged at the end. This eliminates unnecessary memory copies.

Explore Loop Optimization: Introduced logic to filter I2C addresses in the explore function of explorer.rs, so that when testing each permutation, it is only attempted against addresses that have already been confirmed as valid, dramatically reducing the number of I2C transactions.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thank you for this very detailed and insightful breakdown of the refactoring candidates and the improvements implemented! It's clear a lot of thought has gone into enhancing the design and robustness of the explorer module.

I particularly appreciate the emphasis on:

  • Separation of Responsibilities: Decoupling the core algorithm from I2C execution and logging significantly improves modularity and reusability, especially for embedded environments.
  • Iterator-based API (PermutationIter): This is an excellent change that enhances testability and composability, making the permutation generation more flexible.
  • Generic Capacity (const N: usize): This is a powerful feature in Rust that allows for highly adaptable code across different hardware constraints without sacrificing type safety or performance.
  • Abstraction of Logging (Logger trait): Providing a flexible logging mechanism is crucial for embedded development, allowing for tailored verbosity based on the target platform.
  • Robust Error Handling: Expanding the ExplorerError enum to cover more specific failure modes is a great step towards more reliable and debuggable code.

Regarding point 4, the change from u8 (first byte) to usize indices for CmdNode dependencies is indeed a significant improvement for clarity and type safety, as it directly references the command's position in the sequence rather than relying on potentially ambiguous byte values.

Your summary also highlights the performance optimizations, such as the improved topological sorting efficiency and the removal of linear searches, which are critical for resource-constrained systems. The reduction in memory copies is also a welcome improvement.

Overall, this refactoring demonstrates a strong commitment to clean architecture, maintainability, and performance, which are all highly valued in embedded systems development. Excellent work!

@p14c31355

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant and well-designed refactoring of the explorer module, greatly improving its structure by separating concerns, introducing an iterator-based API, and enhancing error handling. The addition of the compat/ascii.rs module for hex formatting is also a good improvement. My review focuses on a critical bug in the new permutation algorithm, a design inconsistency regarding generic capacities that undermines one of the refactoring's goals, and a few smaller opportunities for improving code style and API consistency. Despite these issues, the overall direction of the changes is excellent.

Comment thread src/explorer.rs
Comment on lines +373 to 388
fn backtrack(&mut self) -> bool {
if let Some(last_added_pos) = self.path_stack.pop() {
let node_idx = self.unresolved_indices[last_added_pos];
self.used_indices[node_idx] = false;

self.used[last_added_pos] = false;
self.current_permutation.pop();

state.loop_start_indices.pop();
self.path_stack.pop();
self.path_stack.push(last_added_pos).unwrap();

if let Some(last_loop_idx) = state.loop_start_indices.last_mut() {
*last_loop_idx += 1;
} else {
return false;
}
true
} else {
false
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There appears to be a critical logic error in the backtrack function. The path_stack is popped twice: once in the if let Some(...) binding, and then again on line 381. This will corrupt the state of the permutation iterator, likely causing it to terminate prematurely or produce incorrect results.

The logic to advance to the next sibling in the search is correctly handled by the try_extend function, which uses the new top of the path_stack after the initial pop. The second pop and subsequent push are incorrect and should be removed.

    fn backtrack(&mut self) -> bool {
        if let Some(last_added_pos) = self.path_stack.pop() {
            let node_idx = self.unresolved_indices[last_added_pos];
            self.used_indices[node_idx] = false;
            
            self.used[last_added_pos] = false;
            self.current_permutation.pop();

            true
        } else {
            false
        }
    }

Comment thread src/explorer.rs Outdated
Comment on lines 137 to 139
pub struct Explorer<'a> {
/// The input sequence of command nodes (with dependencies).
pub sequence: &'a [CmdNode<'a>],
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The documentation and overall design goal of this refactoring is to make the command capacity generic (e.g., Explorer<CAPACITY>). However, the Explorer struct itself is not generic, and its related components like PermutationIter and the stage function use hardcoded heapless::Vec capacities of 128. This creates a significant inconsistency between the documented design and the implementation, preventing the capacity from being truly configurable by the user.

To fully realize the generic capacity goal, Explorer and PermutationIter should be made generic over a const N: usize capacity parameter, which would then be used for the internal heapless collections.

Comment thread src/compat/ascii.rs Outdated
Comment thread src/scanner.rs Outdated
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@p14c31355

Copy link
Copy Markdown
Owner Author

@gemini-code-assist
Controllers like the SH1107/SH1106/SSD130x

  • Multiple register combinations "may work"
  • However, the screen layout and memory mapping may not be correct (bands, snow, up/down/left/right inversion, etc.)
  • The datasheet only lists "recommended values" and "example init sequences," but ACKs are returned for other combinations.

This results in "multiple initialization sequences that appear correct."

--

Reasons for this phenomenon:

  1. Register redundancy
  • For example, all four combinations of COM scan direction (C0/C8) and SEG remap (A0/A1) "work properly," but the display changes depending on the direction.
  1. Offset/Start Line Differences
  • 0xD3 (Display Offset) and 0x40+ (Start Line) determine the physical line at which the display begins. Even if you adjust the value, the display will still display, but bands may appear or lines may be hidden.
  1. Panel Implementation Differences
  • Even with the same SH1107G chip, module manufacturers use SEG0 on the left or right edge depending on the wiring.
  • Therefore, the "recommended sequence" varies slightly from manufacturer to manufacturer.

Therefore, during bring-up, it is necessary to first resolve dependencies using an algorithm (Kahn algorithm or DFS search),
and then select the sequence based on how the display looks.

For example:

  • A band appears in the middle → D3 (offset) is suspicious
  • Snowflakes appear → Incorrect column/page address initialization
  • Upside down → C0/C8
  • Reverse left and right → A0/A1

In other words, the "search + log output framework" you're currently building is a tool for narrowing down the correct answer from multiple seemingly correct sequences.


Directions:

  • The current Kahn algorithm has confirmed that the "order of dependencies is correct."
  • Now it's time to systematically switch and test candidate sequences (especially offset, remap, and scan direction)

If you want to automate bring-up, you'll need an observable and quantifiable "snowstorm" definition.


Possible definitions

1. I²C-level errors

  • If an invalid command causes no ACK to be returned, the sequence is clearly invalid.
  • However, the SH1107G continues to return ACKs, making it vulnerable to "snowstorm" detection.

2. Frame buffer echo check

  • Write a known pattern (stripes or checkerboard).
  • Compare the behavior returned on the bus by reading (the SH1107 has a read-modify-write mode) or using an SPI/I²C mirror.
  • If what is written and what is read do not match, it's "snowstorm."

3. Observation using an external sensor

  • Take a picture of the screen with a photodiode or a simple camera.
  • Identify the pattern by the contrast ratio of the on/off state.
  • "I put out a checkerboard, but it's flashing randomly" = snow.

4. Internal Logical Inference

  • Back-calculate from the specifications, such as "This command order prevents page/column addresses from being initialized."
  • In other words, turn off "candidates with a high probability of snow" before checking the display.
  • Example: Forgetting to include 0xAE (Display OFF) and only executing 0xAF → screen crashes.

Practical Step-by-Step Approach

  1. Prune any sequences that are clearly logically inconsistent (Explorer handles this)
  2. Write a test pattern → Check the read (if possible)
  3. Final confirmation by human eye (final check for upside-down or misaligned bands is done manually)

In other words, if we "define snow algorithmically,"
the easiest way to understand it is to consider a state in which the expected pattern is written but not reproduced as snow.


Key points:

  1. Recursive backtracking based on dependencies
  2. Check VRAM after each sequence transmission
  3. If a sequence fails, immediately reset and search for the next sequence

This image is for Arduino Uno + SH1107G 128x128.

#![no_std]
#![no_main]

use core::fmt::Write;
use panic_abort as _;
use dvcdbg::prelude::*;
use dvcdbg::explorer::{CmdNode, ExplorerError};

adapt_serial!(UnoWrapper);

const BUF_CAP: usize = 4;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);

    let mut serial = UnoWrapper(arduino_hal::default_serial!(dp, pins, 57600));
    arduino_hal::delay_ms(1000);
    writeln!(serial, "[SH1107G Auto Backtrack Test]").ok();

    let mut i2c = arduino_hal::I2c::new(
        dp.TWI,
        pins.a4.into_pull_up_input(),
        pins.a5.into_pull_up_input(),
        100_000,
    );
    writeln!(serial, "[Info] I2C initialized").ok();

    static EXPLORER_CMDS: [CmdNode; 17] = [
        CmdNode { bytes: &[0xAE], deps: &[] },
        CmdNode { bytes: &[0xD5, 0x51], deps: &[0] },
        CmdNode { bytes: &[0xA8, 0x3F], deps: &[1] },
        CmdNode { bytes: &[0xD3, 0x60], deps: &[2] },
        CmdNode { bytes: &[0x40, 0x00], deps: &[3] },
        CmdNode { bytes: &[0xA1, 0x00], deps: &[4] },
        CmdNode { bytes: &[0xA0], deps: &[5] },
        CmdNode { bytes: &[0xC8], deps: &[6] },
        CmdNode { bytes: &[0xAD, 0x8A], deps: &[7] },
        CmdNode { bytes: &[0xD9, 0x22], deps: &[8] },
        CmdNode { bytes: &[0xDB, 0x35], deps: &[9] },
        CmdNode { bytes: &[0x8D, 0x14], deps: &[10] },
        CmdNode { bytes: &[0xB0], deps: &[11] },
        CmdNode { bytes: &[0x00], deps: &[11] },
        CmdNode { bytes: &[0x10], deps: &[11] },
        CmdNode { bytes: &[0xA6], deps: &[12, 13, 14] },
        CmdNode { bytes: &[0xAF], deps: &[15] },
    ];

    writeln!(serial, "[Info] Starting backtrack exploration...").ok();

    let mut sequence = heapless::Vec::<usize, 17>::new();
    let mut visited = [false; 17];

    if backtrack(
        &EXPLORER_CMDS,
        &mut i2c,
        &mut serial,
        0x3C,
        0x00,
        &mut visited,
        &mut sequence,
    ) {
        writeln!(serial, "[OK] Found working sequence: {:?}", sequence).ok();
    } else {
        writeln!(serial, "[Fail] No working sequence found").ok();
    }

    loop { arduino_hal::delay_ms(1000); }
}

fn backtrack<I2C, S>(
    cmds: &[CmdNode],
    i2c: &mut I2C,
    serial: &mut S,
    addr: u8,
    prefix: u8,
    visited: &mut [bool; 17],
    sequence: &mut heapless::Vec<usize, 17>,
) -> bool
where
    I2C: embedded_hal::blocking::i2c::Write + embedded_hal::blocking::i2c::WriteRead,
    <I2C as embedded_hal::blocking::i2c::Write>::Error: core::fmt::Debug,
    S: core::fmt::Write,
{
    if sequence.len() == cmds.len() {
        if verify_vram(i2c, addr) {
            return true;
        } else {
            writeln!(serial, "[Info] Sequence invalid, backtracking...").ok();
            return false;
        }
    }

    for i in 0..cmds.len() {
        if visited[i] { continue; }

        if cmds[i].deps.iter().all(|&d| visited[d]) {
            writeln!(serial, "[Try] Node {} bytes={:02X?}", i, cmds[i].bytes).ok();

            let buf_len = 1 + cmds[i].bytes.len();
            if buf_len > BUF_CAP { continue; }
            let mut buf = [0u8; BUF_CAP];
            buf[0] = prefix;
            buf[1..buf_len].copy_from_slice(cmds[i].bytes);

            if i2c.write(addr, &buf[..buf_len]).is_err() {
                writeln!(serial, "[Fail] Node {} write failed", i).ok();
                continue;
            }

            visited[i] = true;
            sequence.push(i).ok();

            if backtrack(cmds, i2c, serial, addr, prefix, visited, sequence) {
                return true;
            }

            visited[i] = false;
            sequence.pop();
        }
    }

    false
}

fn verify_vram<I2C>(i2c: &mut I2C, addr: u8) -> bool
where
    I2C: embedded_hal::blocking::i2c::WriteRead,
{
    let mut buf = [0u8; 16];
    let cmd = [0xB0, 0x00, 0x10];
    if i2c.write_read(addr, &cmd, &mut [0u8]).is_err() { return false; }

    true
}

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thank you for this incredibly detailed and insightful explanation of the challenges involved in bringing up I2C display controllers like the SH1107/SH1106/SSD130x! Your breakdown of why multiple sequences might return ACKs but still result in a "snowstorm" is very clear and highlights the complexities beyond simple I2C communication.

Your analysis perfectly aligns with the broader design philosophy behind the explorer module. You've articulated precisely why a tool capable of systematically exploring permutations, even those that are logically valid but visually incorrect, is essential for this kind of embedded development. The explorer's role in narrowing down the possibilities by respecting dependencies and then allowing for external validation (like your verify_vram concept or human observation) is exactly what it's intended for.

I particularly appreciate your practical step-by-step approach to defining and detecting "snowstorm" conditions. This kind of domain-specific knowledge is invaluable for turning a generic permutation explorer into a truly effective bring-up tool. It's a testament to the iterative nature of embedded development, where initial algorithmic pruning is followed by increasingly sophisticated validation methods.

It's great to see the explorer module being leveraged for such a complex and real-world problem. Excellent work on this deep dive!

@p14c31355

Copy link
Copy Markdown
Owner Author

/gemini summary

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

This pull request introduces a comprehensive refactoring of the I2C command sequence explorer, specifically tailored for embedded systems. The changes enhance modularity by separating the core permutation algorithm from I2C execution, improve flexibility through generic capacity and abstracted interfaces for logging and command execution, and bolster robustness with refined error handling and optimized performance. The update also provides new utilities for hex formatting and addresses critical issues related to I2C communication reliability and serial output clarity.

Highlights

  • Core Explorer Refactoring: The I2C command sequence explorer has undergone a significant architectural overhaul, separating the core permutation algorithm from the I2C execution logic for enhanced modularity and testability.
  • New Permutation Iterator: A new PermutationIter has been introduced, providing a stack-safe, non-recursive approach to generating command permutations, which is crucial for memory-constrained embedded environments.
  • Generic Capacity Support: The Explorer now supports generic capacity (const N: usize), allowing developers to configure memory allocation at compile time based on specific device constraints, making the library highly adaptable.
  • Abstracted Logging and Execution: Logging and command execution are now abstracted through new Logger and CmdExecutor traits, enabling custom implementations and flexible control over verbosity and I2C operations.
  • Enhanced Error Handling: Error handling has been made more robust with an expanded ExplorerError enum, including specific errors for dependency cycles, buffer overflows, and invalid dependency indices, providing clearer diagnostics.
  • Updated CmdNode Dependencies: The CmdNode dependency mechanism has been updated to use usize indices instead of raw command bytes, improving clarity, consistency, and type safety in defining command relationships.
  • Performance Optimizations: Significant performance optimizations have been implemented, including an in-degree based topological sort, O(1) dependency checks using boolean arrays, and reduced memory copies, all critical for resource-constrained systems.
  • New ASCII Formatting Utilities: A new ascii module has been added, providing efficient utilities for hex formatting, which is particularly useful for debugging and logging in embedded contexts.
  • Improved I2C Error Handling: The explore method's I2C error handling has been refined to gracefully handle execution failures by logging errors and continuing to test other addresses, ensuring comprehensive device discovery.
  • Refined Permutation Logic: The PermutationIter's backtrack logic has been refined to correctly manage search state and loop_start_indices, preventing infinite loops and ensuring all valid permutations are explored.
  • Single Sequence Execution: A new get_one_topological_sort function and run_single_sequence_explorer have been introduced, offering a more efficient way to execute a single valid command sequence, ideal for device initialization where exhaustive permutation search is not required.
  • Robust I2C Execution: The PrefixExecutor::exec method now includes delays and retry logic for I2C writes, significantly improving robustness against timing and transient bus issues common in embedded hardware.
  • Refined Error Logging: Error logging has been refined to explicitly format ExecutorError variants, preventing garbled output on serial consoles that could arise from problematic Debug implementations of underlying hardware errors.
Changelog
  • src/compat/ascii.rs
    • New file added, providing write_byte_hex, write_bytes_hex, write_bytes_hex_prefixed, write_bytes_hex_line, and write_bytes_hex_fmt functions for efficient ASCII hex formatting, compatible with embedded_io::Write and core::fmt::Write.
  • src/compat/err_compat.rs
    • Simplified error compatibility layer by removing heapless::String and core::fmt::Write imports.
    • Removed string parsing for NACK detection in the ehal_0_2 implementation of HalErrorExt, now attempting direct matching of Nack variant.
    • Updated documentation for HalErrorExt trait.
  • src/compat/mod.rs
    • Updated to publicly expose the new ascii module.
  • src/explorer.rs
    • Major refactoring of the Explorer struct to support const N: usize generic capacity.
    • Introduced PermutationIter as a new iterator for generating command permutations, replacing the previous iterative permutation logic.
    • Defined CmdExecutor and Logger traits to abstract I2C operations and logging.
    • Enhanced ExplorerError enum with new error types: DependencyCycle, NoValidAddressesFound, ExecutionFailed, BufferOverflow, and InvalidDependencyIndex.
    • Modified CmdNode to use usize for command dependencies, improving type safety.
    • Removed the stage function; its logic for initial in-degree calculation and cycle detection is now integrated into Explorer::permutations.
    • Refined the explore method to integrate with the new PermutationIter and Logger traits, and to optimize address filtering.
    • Added get_one_topological_sort function to generate a single valid topological sort, useful for device initialization where only one sequence is needed.
    • Refined PermutationIter's try_extend and backtrack logic for correct iterative backtracking and loop_start_indices management.
  • src/lib.rs
    • Added pub mod logger; to expose the new logging module.
  • src/logger.rs
    • New file added, defining LogLevel enum, Logger trait, SerialLogger (for serial console output), and NullLogger (for platforms without console output).
    • Implemented core::fmt::Write for SerialLogger.
  • src/macros.rs
    • Updated quick_diag! macro to accept ctrl_byte and use crate::logger::LogLevel for consistency with new logging infrastructure.
  • src/prelude.rs
    • Exported newly added ascii functions (write_byte_hex, write_bytes_hex, write_bytes_hex_prefixed).
    • Exported ExecutorError from explorer module.
    • Exported run_explorer and run_single_sequence_explorer from scanner module.
    • Exported LogLevel, Logger, and SerialLogger from logger module.
    • Removed PrefixExecutor from public exports.
  • src/scanner.rs
    • Added use crate::compat::ascii; and use crate::logger::Logger; imports.
    • Refactored PrefixExecutor to use a [bool; 128] array for initialized_addrs for O(1) lookup, and incorporated retry logic and short_delay in its exec method for robustness.
    • Updated scan_i2c and scan_init_sequence functions within the define_scanner! macro to accept a ctrl_byte parameter and return Result<heapless::Vec<u8, 128>, crate::error::ErrorKind> for better error propagation and programmatic use.
    • Modified define_scanner! macro to utilize ascii::write_bytes_hex_fmt for consistent hex output formatting.
    • Updated run_explorer function to accept const N: usize and const BUF_CAP: usize generic parameters, and integrated with the new Logger trait and refactored PrefixExecutor.
    • Added run_single_sequence_explorer function to execute a single topological sort, providing a more efficient option for device initialization.
    • Refined error logging in run_single_sequence_explorer to explicitly format ExecutorError variants, preventing garbled output.
  • tests/integration.rs
    • Updated the call to scan_i2c to match its new signature, including the ctrl_byte parameter and checking the Result.
Activity
  • The author, p14c31355, requested an initial summary of the pull request.
  • gemini-code-assist[bot] provided an initial summary and review comments, highlighting potential issues with PrefixExecutor's single-transaction init sequence, PermutationIter's dependency check performance, and PrefixExecutor's address lookup performance.
  • p14c31355 provided a detailed breakdown of refactoring candidates and implemented improvements, leading to significant architectural changes.
  • gemini-code-assist[bot] praised the refactoring efforts for improving modularity, testability, and performance.
  • Subsequent critical and high-priority issues were identified by gemini-code-assist[bot], including a logic error in backtrack, the Explorer not being fully generic, write_bytes_hex_prefixed's trailing space, the stage function's change in behavior (from permutation to single sequence), the PermutationIter's missing loop_start_indices, and a flaw in the explore function's address filtering.
  • Discussions ensued regarding the intended behavior of the Explorer (single sequence vs. all permutations), with p14c31355 clarifying the focus on single device initialization sequences.
  • gemini-code-assist[bot] provided corrected in_degree logic for topological sorting.
  • A discussion on the 'never finishing' calculation for large command sets led to the introduction of get_one_topological_sort and run_single_sequence_explorer for single sequence execution.
  • The issue of garbled serial output was discussed, tracing it to Debug formatting of embedded-hal 0.2 errors, which led to changes in err_compat.rs and refined error logging to prevent non-ASCII characters.
  • Further discussions refined PermutationIter's loop_start_indices and is_done timing, and improved PrefixExecutor::exec with delays and retries to address I2C timing/protocol issues.
  • The complexities of display controller initialization, where multiple register combinations may appear to work but lead to 'snowstorm' effects, were discussed, emphasizing the explorer's role in narrowing down correct sequences.

@p14c31355
p14c31355 merged commit aabd269 into main Aug 28, 2025
1 check failed
This was referenced Aug 28, 2025
p14c31355 added a commit that referenced this pull request Aug 29, 2025
@p14c31355
p14c31355 deleted the feature/#99 branch August 29, 2025 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactoring explorer.rs

1 participant