Skip to content

Extending the code

Three common modification recipes. Each is small enough that the right way is documented; deviating is fine when there's a real reason.

Add a new tab to ADCL WinSoft

code/adcl_winsoft/src/adcl_winsoft/ui/tabs/ is where tabs live. The pattern is:

  1. Create ui/tabs/<your_tab>.py with a QWidget subclass. Inject AppState in the constructor.
  2. In __init__, build the layout, connect to whichever AppState signals are relevant, and start a QTimer if you need a refresh loop.
  3. In app.py's MainWindow.__init__, instantiate the tab and add it to the QTabWidget.
  4. Add the tab name to the sidebar list in ui/widgets/sidebar.py.
  5. If the tab needs admin gating, wrap the add step behind state.is_admin and re-add on state.admin_changed.

Use ui/tabs/placeholder.py as the smallest possible template.

Add a new Modbus register to read

If the new register is a quick-access (CW, REF1, SW, ACTn) register:

  1. Add a field to VfdSnapshot (vfd/__init__.py or wherever the dataclass lives).
  2. In VfdPoller.run() (vfd/worker.py), read the new address each tick and populate the field.
  3. Update any UI tabs that should display it.

If the new register is a parameter (group/index):

  1. Use VfdClient.read_param(group, index) which already does the group × 100 + index − 1 calculation.
  2. If you are reading the parameter on every poll, fold it into VfdPoller so you do not multiply the Modbus traffic.
  3. If you are reading the parameter once at startup (e.g. nameplate values), do it on the main thread before the workers spin up, or inside AppState.initialize().

Update Reference → Parameter index and Reference → Register map when adding either kind.

Refresh the REF1 ↔ RPM calibration

When the calibration drifts (after a hardware change, a long idle period, or a motor swap), re-run the sweep scripts and update the constants.

  1. Pre-run checklist complete; test section empty.
  2. Launch code/wind_tunnel_control/ref1_sweep.ps1:
    powershell.exe -ExecutionPolicy RemoteSigned -File ref1_sweep.ps1 `
        -OutputCsv "C:\Users\kshit\Desktop\ref1_sweep_$(Get-Date -Format yyyyMMdd_HHmmss).csv"
    
    The script walks REF1 from 0 to a maximum, polling the drive's frequency feedback, writing a CSV row per setpoint.
  3. Plot the CSV: REF1 on the x-axis, drive frequency on the y-axis. A linear fit gives the new constant: REF1 ≈ frequency × (REF1_max / nominal_freq).
  4. Convert the linear fit into a REF1 = drive_RPM × C constant. The relationship between frequency and RPM is the motor's pole-pair count and slip; for our motor 60 Hz = 880 RPM, so the constant comes out near 22.42 when the drive is in spec.
  5. Run rpm_velocity_sweep.ps1 next to capture the RPM → wind speed mapping under the new drive calibration:
    powershell.exe -ExecutionPolicy RemoteSigned -File rpm_velocity_sweep.ps1
    
  6. Update the two source-of-truth constants:
    • code/adcl_winsoft/src/adcl_winsoft/vfd/controller.py: REF1_PER_RPM = <new value>
    • code/wind_tunnel_control/WindTunnelControl.ps1: the equivalent constant (search for 22.42)
    • docs/manual/content/reference/calibration_constants.md: update both numbers and the date.
  7. Commit the change with a message like vfd: recalibrate REF1 = RPM × 22.51 (2026-05-20 sweep). Reference the sweep CSV in the commit body.
  8. File the sweep CSVs in a new WT_MS_<n>/captures/ folder with a one-line debug_session.md summarizing the date, the operator, and the resulting constants.

A note on changing the threading model

Don't, unless there is a concrete reason. The two-worker + command-worker arrangement in app.py survived F.1–F.6 and the explicit "belt and suspenders" pattern (signals and shared state) makes the UI robust to a class of bugs that the simpler single-path designs would expose. If you are tempted to consolidate into one worker or to remove the direct shared-state path, run a long-soak test first.