BUILD LOG

Inside the 6 GHz power mode toolkit: decide, configure, verify, monitor

A public repo that walks the whole 6 GHz Low Power Indoor versus Standard Power decision on a Cisco Catalyst 9800: a questionnaire that refuses to recommend Standard Power over a missing prerequisite, NETCONF scripts for the one configuration bit, and a pyATS precheck that reports controller-side readiness as pass or fail. Lab-verified on a live controller, then audited three times before publishing.

Tested with: Catalyst 9800-CL (IOS-XE 17.17.1), ncclient, pyATS Blitz, IOS-XE YANG models

What is the 6 GHz power mode toolkit?

6ghz-power-mode-toolkit is a public, MIT-licensed repo for one decision and its aftermath: whether a 6 GHz Wi-Fi deployment should run Low Power Indoor (LPI) or Standard Power, and how to configure, verify, and monitor that choice programmatically on a Cisco Catalyst 9800.

That is a narrow scope on purpose. Every enterprise 6 GHz AP runs in one of those two modes. LPI needs no coordination and gets the full band, at low power ceilings, with client ceilings 6 dB lower still. Standard Power raises the ceilings on both sides of the link, and in exchange the AP has to register its 3D location with an Automated Frequency Coordination (AFC) service, re-authorize every 24 hours, and give up whatever channels the AFC masks off at that location. LPI buys spectrum at the cost of power. Standard Power buys power at the cost of spectrum and a standing operational dependency.

EIRP ceilings, both modes
243036Low Power IndoorStandard Powerauthorized by the AFC grant30 dBmAP24 dBmClientup to 36 dBmAPup to 30 dBmClient6 dB6 dBEIRP ceilings, US rules (47 CFR 15.407). PSD ceilings differ by more.

Standard Power raises the ceilings on both sides of the link, but the client stays capped at least 6 dB below the AP's authorized power. The asymmetry never goes away; the whole ladder moves up.

EIRP ceilings, both modes

The repo holds four things: a decision questionnaire, the comparison as data, reference docs (a decision guide, an AFC primer, and a C9800 programmability map), and runnable code for the NETCONF surface plus a pyATS precheck.

Why does the decision script refuse to answer sometimes?

decision/decide.py asks eight yes/no questions. The first three are prerequisites, not preferences: hardware and clients that support Standard Power in a regulatory domain that authorizes it, reliable AP geolocation, and a team willing to own AFC operations. If any one of those is missing, the script recommends LPI and lists what is missing, no matter how strongly the other five answers lean toward Standard Power.

That behavior came out of an audit, and it is the most important design choice in the repo. An earlier version scored every question the same way, which meant a site with no geolocation and no AFC owner could still be told to deploy Standard Power. Points cannot buy back a missing requirement, so the requirements now come first. The script also states its scope out loud: it assumes an indoor site where LPI is available as the fallback, because LPI is an indoor-only mode and cannot be the answer for outdoor or Standard-Power-only designs.

How decide.py reaches a recommendation
Rendering diagram…
View diagram source - it's just text (Mermaid). Diagrams-as-code is how modern network docs work; the flagship course has a free module on it.
flowchart TD
  A["eight yes/no questions"] --> B{"Standard Power capable,<br/>authorized domain?"}
  B -- no --> L["LPI, with the missing<br/>requirements listed"]
  B -- yes --> C{"reliable AP geolocation?"}
  C -- no --> L
  C -- yes --> D{"someone owns<br/>AFC operations?"}
  D -- no --> L
  D -- yes --> E["score the five<br/>preference questions"]
  E --> F{"which way does it lean?"}
  F -- "Standard Power" --> G["Standard Power candidate:<br/>validate with a survey<br/>and a real AFC grant"]
  F -- LPI --> H["LPI"]
  F -- tie --> I["default to LPI"]
How decide.py reaches a recommendation

What is the one configuration bit?

On the Catalyst 9800, the entire writable control surface for 6 GHz Standard Power is a single boolean on the 6 GHz RF profile. It has a different name on each management surface, and none of the three documentation sets mentions the other two names:

Surface Name
C9800 CLI tx-power standard under ap dot11 6ghz rf-profile
IOS-XE YANG std-pwr-mode-allowed in Cisco-IOS-XE-wireless-rf-cfg
Catalyst Center Intent API enableStandardPowerService

If you have ever searched one surface with another surface’s name, found nothing, and concluded the feature was missing, that naming split is why. Everything else is read-only: the AFC request and response per AP, per-channel granted power, grant expiry, and the cloud health check all live in operational YANG models you can poll or stream.

One bit, three names
C9800 CLItx-power standardIOS-XE YANGstd-pwr-mode-allowedCatalyst Center APIenableStandardPowerServiceone boolean on the 6 GHz RF profiledefault: falseEverything else on the controller's Standard Power surface is read-only.

Each management surface has its own name for the same boolean, and none of the three documentation sets mentions the other two. Searching one surface with another surface's name finds nothing.

One bit, three names

What do the NETCONF scripts do?

The c9800/netconf/ folder holds three runnable ncclient scripts. Two are readers: one reports std-pwr-mode-allowed for every 6 GHz RF profile, and one summarizes the AFC operational state, including per-AP grants with their expiry and the cloud health check with the exact blocker when the controller is not healthy. The third is the one write path: it sets the bit on a named profile, and it refuses to write if that profile does not exist or is not a 6 GHz profile, because a NETCONF merge to a misspelled name would otherwise create a new profile without a word of warning.

The connection defaults are the safe ones. Host keys are verified against your known_hosts unless you pass --insecure for a lab box, and the password comes from an environment variable or a prompt, never from the command line where other users could read it in the process list.

What does the pyATS precheck prove, and what does it not?

The pyats/ folder is a Blitz trigger, plain YAML with no custom Python, that runs seven pass/fail checks against the controller: six over NETCONF and one CLI cross-check kept on purpose, because genieparser ships no parser for any show wireless afc command as of version 26.8. Easypy gives you HTML and JSON reports without extra work.

The docs are specific about scope. A green run means exactly those seven checks passed. It does not check per-AP location, capability, radio state, or grants, and it does not rule out every possible controller-side blocker. Against the lab controller it reports five checks passing and two failing, and both failures are true statements about that box: the controller is not onboarded to Cisco’s AFC cloud, and the Standard Power bit is still at its default of false. A readiness check that passed everything on that controller would be reporting a false result.

The precheck against the bench controller
AFC oper model get acceptedPASSafc-msg-err equals 0PASShealthcheck num-hc-down equals 0PASScountry is supportedPASScloud health reports healthyFAILtrue: not onboarded to the AFC cloudprofile allows Standard PowerFAILtrue: the bit is still default falseCLI cross-check fields presentPASSCatalyst 9800-CL, IOS-XE 17.17.1, over NETCONF. Six YANG checks plus one CLI cross-check.

Five checks passing and two failing, and both failures are true statements about that controller. A readiness check that passed everything on this box would be reporting a false result.

The precheck against the bench controller

What was verified on a live controller?

The C9800 findings were measured on a live Catalyst 9800-CL running IOS-XE 17.17.1, with the on-box YANG pulled over NETCONF get-schema. The precheck ran against that controller twice, before and after its health assertion was recut, and reported the same correct results both times.

The repo also says plainly what is not verified: whether the AFC operational models support on-change telemetry, the behavior of an undocumented per-radio RPC input (rlp-sp-pwrmode-switch, present in the models since 17.11.1), and the write script itself, whose structure comes from the schema but which you should test in a lab before production use. Claims a stranger cannot check are labeled instead of asserted.

How was the repo checked before it went public?

Three separate context-free audit sessions reviewed the repo against primary sources before it published, and each round found something real. The first audit caught the biggest one: the docs claimed Standard Power removes the 6 GHz client power penalty, and the FCC rules say otherwise. Clients stay capped at least 6 dB below the AP’s authorized power in Standard Power. Both ceilings move up, so clients can run louder than LPI allows, but the asymmetry never goes away. The same round caught XML parsing that broke on namespace prefixes and a health check that would have failed a healthy controller.

The second audit checked the fixes and found two bugs the fixes had introduced. The third confirmed the code and cleaned up the remaining wording. Every claim in the repo now traces to 47 CFR 15.407, FCC 20-51, Cisco’s configuration guides, or the published IOS-XE YANG models, and the regulatory statements carry a checked-as-of date because rules change.

That process is the same standard Field Notes holds itself to: run the thing, keep the transcript, and let someone with no stake in the outcome try to break the claims before readers see them.

How do you use it?

Clone the repo and start with the decision:

git clone https://github.com/bleekley/6ghz-power-mode-toolkit
cd 6ghz-power-mode-toolkit
python3 decision/decide.py

Read the Standard Power readiness state off a live controller:

python3 -m venv venv && . venv/bin/activate
pip install ncclient
python3 c9800/netconf/get_afc_oper.py --host <wlc> --user <user>

Run the controller precheck (the pyATS README covers setup, including a macOS note about a Tcl crash and why the virtualenv must be activated rather than called by path):

pip install "pyats[full]"
cd pyats
cp testbed.example.yaml testbed.yaml   # then edit host and credentials
pyats run job afc_job.py --testbed-file testbed.yaml

The controller needs netconf-yang enabled. If you have a 9136 or 9166-class AP joined to a lab controller, the undocumented RPC in the programmability doc is the most interesting open question in the repo, and the README asks you to open an issue with what you find.