Skip to content

feat: add Jetson Thor fan discovery for split PWM/RPM hwmon pairing#837

Merged
johnnynunez merged 3 commits intorbonghi:masterfrom
whitesscott:thor-fan
Apr 2, 2026
Merged

feat: add Jetson Thor fan discovery for split PWM/RPM hwmon pairing#837
johnnynunez merged 3 commits intorbonghi:masterfrom
whitesscott:thor-fan

Conversation

@whitesscott
Copy link
Copy Markdown
Contributor

@whitesscott whitesscott commented Mar 27, 2026

Enable jtop fan discovery for Jetson Thor, where fan control and tachometer data are exposed in separate hwmon nodes. This change merges split PWM/RPM hwmon entries into one logical fan, allowing jtop to show Thor fan speed control and RPM correctly.

Summary by Sourcery

Improve fan discovery and control metadata handling, including support for split PWM/RPM hwmon layouts such as Jetson Thor.

New Features:

  • Support discovering and merging fans whose PWM control and RPM tachometer are exposed in separate hwmon nodes, including Jetson Thor-specific topology and fallback matching.
  • Expose additional fan metadata such as pwm_enable state and kickstart PWM from hwmon and nvfancontrol configuration.

Enhancements:

  • Refine hwmon fan scanning to capture multiple pwm, rpm, and enable files per device and handle legacy rpm naming variants.
  • Improve robustness of nvfancontrol parsing and status handling, including safer line parsing, optional config path resolution, and richer metadata merging with discovered fans.
  • Simplify and harden Fan and FanService logic by removing inline documentation, standardizing logging, and avoiding brittle zip-based pairing of fans and nvfancontrol entries.

Enable jtop fan discovery for Jetson Thor, where fan control and tachometer
data are exposed in separate hwmon nodes. This change merges split PWM/RPM
hwmon entries into one logical fan, allowing jtop to show Thor fan speed
control and RPM correctly.
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Mar 27, 2026

Reviewer's Guide

Refactors fan discovery and nvfancontrol integration to support Jetson Thor’s split PWM/RPM hwmon layout, adds richer metadata (pwm_enable, kickstart, topology-based matching), and simplifies the Fan/FanService APIs and logging while improving robustness of parsing and matching.

Sequence diagram for hwmon fan discovery and Thor split PWM/RPM merge

sequenceDiagram
    participant FanService
    participant get_all_cooling_system
    participant _collect_hwmon_fans
    participant _merge_split_fans
    participant is_thor
    participant sys_class_hwmon as sys_class_hwmon

    FanService->>get_all_cooling_system: root_dir
    alt root_dir_not_directory
        get_all_cooling_system-->>FanService: {}
    else root_dir_ok
        get_all_cooling_system->>sys_class_hwmon: listdir(root_dir)
        get_all_cooling_system->>_collect_hwmon_fans: root_dir
        _collect_hwmon_fans->>sys_class_hwmon: listdir(each_hwmon_dir)
        _collect_hwmon_fans-->>get_all_cooling_system: pwm_files,rpm_only
        get_all_cooling_system->>_merge_split_fans: pwm_files,rpm_only
        alt no_pwm_or_no_rpm_only
            _merge_split_fans-->>get_all_cooling_system: pwm_files
        else have_pwm_and_rpm_only
            loop match_by_topology
                _merge_split_fans->>_merge_split_fans: compare of_node/device
                alt match_found
                    _merge_split_fans->>_merge_split_fans: attach rpm_to_pwm_entry
                end
            end
            _merge_split_fans->>is_thor: ()
            alt is_thor_true_and_single_pwm_and_rpm
                _merge_split_fans->>_merge_split_fans: attach_rpm_to_single_pwm
            else generic_single_device_fallback
                _merge_split_fans->>_merge_split_fans: attach_rpm_to_single_pwm
            end
            _merge_split_fans-->>get_all_cooling_system: merged_pwm_files
        end
        get_all_cooling_system-->>FanService: merged_pwm_files
    end

    FanService->>FanService: _fan_list = merged_pwm_files + get_all_legacy_fan()
Loading

Sequence diagram for nvfancontrol metadata merging into fan status

sequenceDiagram
    participant FanService
    participant get_status
    participant nvfancontrol_is_active
    participant nvfancontrol_query

    FanService->>get_status:()
    get_status->>get_status: build fan_status from _fan_list
    alt _nvfancontrol_false
        get_status-->>FanService: fan_status_with_manual_or_temp_control
    else _nvfancontrol_true
        get_status->>nvfancontrol_is_active:()
        alt service_active
            get_status->>nvfancontrol_query:()
            nvfancontrol_query-->>get_status: nvfan_query
        else service_inactive
            get_status-->>get_status: nvfan_query = {}
        end

        alt nvfan_query_not_empty
            alt single_fan_single_nv_entry
                get_status->>get_status: merge_single_nv_entry_into_single_fan
            else multiple_fans_or_entries
                loop for_each_fan_in_fan_status
                    alt fan_name_in_nvfan_query
                        get_status->>get_status: update_fan_status_with_nv_metadata
                    else no_matching_nv_entry
                        get_status->>get_status: set_profile_to_manual
                    end
                end
            end
        else nvfan_query_empty
            loop for_each_fan
                get_status->>get_status: set_profile_to_manual
            end
        end
        get_status-->>FanService: fan_status_with_nvfan_metadata
    end
Loading

Updated class diagram for Fan and FanService

classDiagram
    class GenericInterface {
    }

    class Fan {
        +Fan()
        +all_profiles(name)
        +set_profile(name, profile)
        +get_profile(name)
        +get_profile_default(name)
        +get_speed(name, idx)
        +set_speed(name, speed, idx)
        +get_rpm(name, idx)
        +profile
        +speed
        +rpm
        -_data
        -_init
        -_controller
    }

    class FanService {
        +FanService(config)
        +initialization()
        +get_configs()
        +get_profile(name)
        +set_profile(name, profile)
        +set_speed(name, speed, index)
        +get_status()
        -_config
        -_fan_list
        -_nvfancontrol
    }

    class Config {
        +get(key, default)
        +set(key, value)
    }

    Fan --|> GenericInterface
    FanService --> Config
Loading

File-Level Changes

Change Details Files
Refactor hwmon fan discovery to support split PWM/RPM nodes and Thor-specific layouts.
  • Replace get_all_rpm_system with _collect_hwmon_fans that discovers PWM, PWM-enable, and RPM files and records of_node/device symlinks for topology matching.
  • Introduce _merge_split_fans to combine standalone RPM-only hwmon entries with PWM-capable ones using of_node/device paths and Thor/single-device fallbacks.
  • Update get_all_cooling_system to use the new collection/merge helpers and handle missing root directories robustly.
  • Extend fan RPM discovery to accept rpm, rpm_measured, and fan1_input across both standard and legacy fan discovery.
jtop/core/fan.py
Improve nvfancontrol configuration and status integration, including safer parsing and better fan-to-config/status pairing.
  • Harden nvfancontrol_query parsing by trimming fields, skipping malformed lines, and normalizing keys.
  • Add read_nvfancontrol_status (currently unused) for reading /var/lib/nvfancontrol/status into a structured dict.
  • Introduce _nvfancontrol_paths to centralize /etc/nvfancontrol.conf existence checks and reuse in decode_nvfancontrol and change_nvfancontrol_default.
  • Extend decode_nvfancontrol to trim lines, reuse compiled regexes, and parse KICKSTART_PWM into kickstart_pwm per fan.
  • Add _merge_nvfan_metadata to pair discovered fans with nvfancontrol profiles by exact name, single-fan shortcut, or positional fallback and always append the manual profile where applicable.
  • Update FanService.init, get_profile, set_profile, and get_status to use the merged metadata and more robust fan/status pairing instead of zip-based index matching.
jtop/core/fan.py
Expose additional fan status metadata and simplify Fan/FanService APIs and logging.
  • Include pwm_enable and kickstart_pwm (when present) in FanService.get_status and normalize profile to manual when nvfancontrol has no matching entry.
  • Simplify and de-duplicate logging, converting old format strings to logger-style placeholders and adding clearer messages for profile/speed operations and errors.
  • Remove extensive docstrings and inline comments from Fan methods (all_profiles, set_profile, get_profile, get_profile_default, profile, speed, get_speed, get_rpm, rpm) without changing their external behavior.
  • Handle JTOP_TESTING root paths and error conditions (missing directories, OSErrors on sysfs access) more gracefully with guarded operations and logging.
jtop/core/fan.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • Consider either using or removing the new read_nvfancontrol_status() helper to avoid keeping unused parsing logic that can drift from nvfancontrol_query() over time.
  • In _merge_split_fans, any remaining unmatched_rpm entries are silently dropped; adding a warning log for those cases would make diagnosing mis-paired or unpaired hwmon RPM nodes much easier.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider either using or removing the new `read_nvfancontrol_status()` helper to avoid keeping unused parsing logic that can drift from `nvfancontrol_query()` over time.
- In `_merge_split_fans`, any remaining `unmatched_rpm` entries are silently dropped; adding a warning log for those cases would make diagnosing mis-paired or unpaired hwmon RPM nodes much easier.

## Individual Comments

### Comment 1
<location path="jtop/core/fan.py" line_range="76-85" />
<code_context>
+    for entry in sorted(os.listdir(root_dir)):
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Guard against `OSError` when listing `root_dir` itself to make `_collect_hwmon_fans` more robust.

In `_collect_hwmon_fans`, `os.listdir(full_path)` is wrapped in `try/except OSError`, but the initial `sorted(os.listdir(root_dir))` is not. If `root_dir` is temporarily unreadable, this will raise and skip the error handling in `get_all_cooling_system`. Consider wrapping the outer `os.listdir(root_dir)` in the same `try/except OSError` and returning empty dicts on failure to keep fan discovery robust.

Suggested implementation:

```python
    rpm_only = {}

    try:
        entries = sorted(os.listdir(root_dir))
    except OSError:
        return {}, {}

    for entry in entries:

```

1. Ensure `_collect_hwmon_fans`'s return statement matches the `return {}, {}` shape used in the new `except OSError` block. If `_collect_hwmon_fans` currently returns a different number or type of values, adjust the `return` in the `except` to match (e.g., `return {}` or `return {}, {}, {}`).
2. Confirm all call sites of `_collect_hwmon_fans` correctly handle the empty return structure so that downstream code treats this as "no fans discovered" rather than an error.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread jtop/core/fan.py Outdated
@johnnynunez johnnynunez merged commit ab58fd6 into rbonghi:master Apr 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants