Agent Skill · NVIDIA NIM

jetson-customize-fan

Use when you need to add, remove, edit, list, or change the boot default of an nvfancontrol fan profile on a Jetson/Tegra (Orin, Thor) target. Triggers: edit fan profile, tune fan curve.

Provider: NVIDIA NIM Path in repo: skills/jetson-customize-fan/SKILL.md

Skill body

Modify nvfancontrol Fan Profile (BSP-side)

Purpose

Edit the per-board nvfancontrol configuration so the device boots with the desired fan curve / control mode / governor / default profile. BSP-side only — all writes land in the overlay tracker, the upstream bsp_image copy is read-only.

This skill handles BSP-side edits to the per-board nvfancontrol configuration file: adding profiles, removing profiles, editing the temp → PWM / RPM curves, changing the boot default profile / control mode / governor, and listing defined profiles. Applies on Jetson / Tegra platforms (T234 Orin, T264 Thor).

File format (canonical, per the BSP file header)

POLLING_INTERVAL <seconds>

<FAN <index>>
    TMARGIN <ENABLED|DISABLED>
    FAN_GOVERNOR <type> {
        STEP_SIZE <int>
    }
    FAN_CONTROL <close_loop|open_loop> {
        RPM_TOLERANCE <rpm>
    }
    FAN_PROFILE <name> {
        # TEMP HYST PWM RPM
        <T0> <H0> <P0> <R0>
        ...
    }
    FAN_PROFILE <name> { ... }       # one or more profiles
    THERMAL_GROUP <id> {
        GROUP_MAX_TEMP <C>
        # zone-name <coeffs csv> <max-temp>
        <zone> <coeffs> <max-temp>
        ...
    }
    FAN_DEFAULT_CONTROL  <close_loop|open_loop>
    FAN_DEFAULT_PROFILE  <name>
    FAN_DEFAULT_GOVERNOR <type>
    KICKSTART_PWM <0..255>

Rules:

Prerequisites

Resolve the active profile per ../../context/target-platform-contract.md. Refuse and route in these cases:

Condition Refuse with
No active profile, or active: NA Route to /jetson-set-target or /jetson-init-target.
Profile lacks bsp_image: block Route to /jetson-init-image.
<bsp_image.root_path>/Linux_for_Tegra/ missing Route to /jetson-init-image.
<source.root_path>/Linux_for_Tegra/ missing or not a git repo Route to /jetson-init-source.

Resolve paths:

<bsp_image.root_path> is read-only for this skill; every write lands under <source.root_path> (the overlay tracker). This is the workflow invariant in ../../context/bsp-customization-workflow.md#workflow-invariants — hand-editing upstream silently destroys the diff trail and makes /jetson-promote-image a noop.

The per-board file

The conf this skill edits has the relative path:

Linux_for_Tegra/rootfs/etc/nvpower/nvfancontrol/nvfancontrol_<active-sku>.conf

It lives in two roots; the skill walks both:

Role Location Skill writes?
Detection + pristine source <bsp_image.root_path>/Linux_for_Tegra/rootfs/etc/nvpower/nvfancontrol/ no — read-only
Overlay edit target + git commit <source.root_path>/Linux_for_Tegra/rootfs/etc/nvpower/nvfancontrol/ yes

Subsequent sections refer to the per-board file to mean the overlay copy under <source.root_path>. Operations 1–4 all read, edit, and save against that overlay copy. The <bsp_image.root_path> copy is read once during the “Resolving <active-sku>” detection step and once during the Overlay edit recipe’s pristine-import step, then never touched again.

Resolving <active-sku> — which file to edit

Filename conventions vary per product family:

The nvfancontrol daemon resolves the right file at startup based on the booted hardware’s DT compatible plus board IDs (no helper script — it’s done inside the binary). To map BSP-side without a running target, against <bsp_image.root_path>:

  1. List <bsp_image.root_path>/Linux_for_Tegra/rootfs/etc/nvpower/nvfancontrol/.
  2. Filter to filenames that contain <module.id>_<module.sku> from the active profile.
  3. If multiple candidates remain, refine with <carrier.id>_<carrier.sku> and (when relevant) the carrier revision tag.
  4. If still ambiguous, run the per-board flash conf dispatch chain to read the kernel DTB’s compatible and pick the file whose name aligns with the resolved board / carrier.
  5. Verify the chosen file actually exists under <bsp_image.root_path>/Linux_for_Tegra/rootfs/etc/nvpower/nvfancontrol/.

Don’t blindly compose a filename — the naming convention varies by product family.

Propagation set — confs to keep in sync

The active-SKU file is rarely the only conf that should carry a customization. After editing it, apply the same edit to every sibling in the propagation set so the change survives regardless of which module / baseboard SKU is booted:

“Apply the same edit” ≠ blanket file copy. Port the changed FAN_PROFILE / control lines into each sibling; preserve every other line. Sibling confs often hold carrier-specific deltas (different THERMAL_GROUP coefficients for a different thermal solution, different tach RPM ceilings) that must stay intact — a blanket overwrite would mis-tune the fan on those carriers. Blanket-copying is safe only when both confs were byte-identical before the edit.

Overlay edit recipe (apply before any Operation)

Follow the canonical Off-skill edits recipe in the workflow doc — pristine import + customization commit pair, both gated by the preview gate. Apply once per run, covering every per-board file the run touches (the active conf plus every sibling in the Propagation set).

Concrete substitutions for this skill:

Instructions

Pick the operation that matches the user’s intent and follow the matching subsection. All write-side operations (1–4) must first apply the Overlay edit recipe.

After any write-side operation, run the Deploy chain (## Deploy) to land the change on the device.

Examples

Add an aggressive profile to a P3767-0000 (Orin Nano dev kit) target and pin it as the boot default:

/jetson-customize-fan
> add a profile called "aggressive" with the curve {0:255, 40:150, 80:0}
> set FAN_DEFAULT_PROFILE to aggressive

Soften the fan ramp on the existing quiet profile (raise HYST, lower mid-PWM):

/jetson-customize-fan
> edit FAN_PROFILE quiet — raise HYST to 5 from 30 °C up, drop PWM at 60 °C to 80

List which profiles the active SKU currently defines and which is the boot default:

/jetson-customize-fan
> list defined fan profiles

Operation 1 — Add a new fan profile

Apply the Overlay edit recipe first.

  1. In the per-board file, inside the <FAN N> block, add a new FAN_PROFILE <name> { ... } between existing profiles. Daemon parse order doesn’t matter, but grouping with siblings keeps the file scannable.
  2. Pick <name> lowercase, no whitespace (e.g. quiet, cool, aggressive).
  3. Fill the curve table — 4-column TEMP HYST PWM RPM tuples, ascending TEMP. End with at least one row above GROUP_MAX_TEMP so behavior at over-temp is defined.

Example skeleton:

FAN_PROFILE aggressive {
    #TEMP HYST PWM RPM
    0    0    255 6000
    20   2    255 6000
    40   2    150 3500
    60   2    50  1500
    80   0    0   0
    105  0    0   0
}

Validate (see Rules):

If you intend the new profile to be the boot default, follow Operation 4.

Operation 2 — Remove a fan profile

Apply the Overlay edit recipe first.

  1. In the per-board file, delete the entire FAN_PROFILE <name> { ... } block including all curve rows and the closing }.
  2. If the deleted <name> matches the trailing FAN_DEFAULT_PROFILE, point FAN_DEFAULT_PROFILE at a remaining profile — otherwise the daemon fails to start.
  3. Search for hard-coded references in the rootfs before declaring the change safe:
    grep -rn "nvfancontrol.*profile\|FAN_PROFILE" \
      Linux_for_Tegra/rootfs/etc 2>/dev/null
    

The remaining profiles do not need renaming.

Operation 3 — Edit an existing fan profile

Apply the Overlay edit recipe first.

  1. In the per-board file, modify the curve rows of the target FAN_PROFILE <name>. Keep the 4-column TEMP HYST PWM RPM shape.
  2. Maintain ascending TEMP ordering; insert or remove rows as needed.
  3. Adjust HYST to tune anti-oscillation: too low → fan thrashes near a curve point; too high → fan lags reality.
  4. Validate per Operation 1’s rules.

Edits to KICKSTART_PWM, RPM_TOLERANCE (inside FAN_CONTROL), or STEP_SIZE (inside FAN_GOVERNOR) sit outside the profiles but tune the same fan; same per-board file.

Operation 4 — Change the boot default

Apply the Overlay edit recipe first.

Edit the trailing default lines inside the <FAN N> block:

FAN_DEFAULT_CONTROL  <close_loop|open_loop>
FAN_DEFAULT_PROFILE  <name>
FAN_DEFAULT_GOVERNOR <type>

FAN_DEFAULT_PROFILE must reference an existing FAN_PROFILE in the same <FAN N> block. FAN_DEFAULT_CONTROL and FAN_DEFAULT_GOVERNOR must reference values the binary supports — copy from existing per-board files when changing.

Operation 5 — List defined fan profiles

This is a read-only operation; no overlay-tracker setup is needed. Run against whichever copy you want to inspect (<bsp_image.root_path>/... for the pristine state, <source.root_path>/... for the post-edit state):

grep -E '^[[:space:]]*FAN_PROFILE ' <per-board file>
grep -E '^[[:space:]]*FAN_DEFAULT_'  <per-board file>

The first prints every profile name in the file; the second prints the boot defaults (control / profile / governor).

Limitations

Troubleshooting

Error Cause Solution
nvfancontrol.service fails to start after edit FAN_DEFAULT_PROFILE references a profile that was removed or renamed Set FAN_DEFAULT_PROFILE to an existing FAN_PROFILE <name> in the same <FAN N>.
Fan thrashes near a curve point HYST too low Raise HYST on the affected row (typical 2–5 °C).
Fan lags reality HYST too high Lower HYST on the affected row.
Fan never spins up at high temp Curve missing a row above GROUP_MAX_TEMP, or trailing 0 0 truncates above ambient Add a high-temp tuple with non-zero PWM/RPM; pin the over-temp row only above GROUP_MAX_TEMP.
Daemon parse error referencing column count Curve row not 4-column TEMP HYST PWM RPM Restore 4-column shape; remove trailing whitespace and stray columns.
RPM target never reached in close-loop Target above tach-reported max, or FAN_CONTROL set to open_loop Lower RPM to within the fan’s mechanical max, or switch FAN_DEFAULT_CONTROL to close_loop.
Change vanished after /jetson-init-image re-extract Edit landed in <bsp_image.root_path> instead of <source.root_path> overlay Re-apply via the Overlay edit recipe so the change is committed in the overlay tracker.
Sibling carrier boots with wrong tach ceiling after propagation Blanket file copy clobbered carrier-specific THERMAL_GROUP / RPM rows Port only the changed lines per Propagation set; restore the carrier’s original THERMAL_GROUP.

Deploy

The customization commit in the overlay tracker does not reach the device on its own. The Deploy chain:

  1. /jetson-promote-image — copies every tracked file in the overlay into <bsp_image.root_path>/Linux_for_Tegra/. Diff-aware (skip byte-identical); uses sudo cp -p for rootfs/* destinations.
  2. /jetson-flash-image — flashes the updated bsp_image to the device.
  3. (Alternate, no flash) Copy <source.root_path>/Linux_for_Tegra/rootfs/etc/nvpower/nvfancontrol/<conf> directly to the running target’s /etc/nvpower/nvfancontrol/<conf>, then sudo systemctl restart nvfancontrol.service (or reboot).

Editing <source.root_path>/... without committing — or editing <bsp_image.root_path>/... directly — does nothing for /jetson-promote-image and is silently lost on the next /jetson-init-image re-extract.

Skill frontmatter

version: 0.0.1 license: Apache-2.0 metadata: {"data-classification" => "public", "author" => "Jetson Team", "tags" => ["thermal", "fan", "nvfancontrol", "profile"], "domain" => "thermal"}