On-demand thermal focus compensator for the ZWO EAF focuser via ASCOM. Reads the regression model and last autofocus reference produced by sharpcap-focus-temperature and moves the focuser to the thermally corrected position.
Designed to be called from a nightly sequencer (SharpCap Advanced Sequencer, NINA, SGP'Pro, etc.) on a periodic schedule.
focus_target = focus_ref + TCF × (T_current − T_ref)
| Variable | Description |
|---|---|
focus_ref |
Focuser position at the reference autofocus point |
T_ref |
Temperature at the reference autofocus point |
T_current |
Current temperature read from the EAF external sensor |
TCF |
Temperature compensation factor (steps/°C) = 1/k from the regression |
- Refreshes the state JSON automatically by calling
sharpcap_focuser.py(sibling repository) — so the latest SharpCap autofocus result is always reflected before each correction. - Connects to the ZWO EAF via ASCOM and reads the real focuser position and temperature.
- Aborts immediately if
IsMoving = True(e.g. SharpCap autofocus is running) — the next scheduled cycle will retry. - Calculates the thermally compensated target position.
- Skips the move if the correction is below
--min-correctionand a backlash overshoot would be needed; always moves if the target is in the favourable direction (no overshoot required). - Moves the focuser to that position using backlash-compensated movement (always arrives from below).
- Updates
last_temp_appliedandlast_focus_appliedin the JSON state file.
This project is designed to work alongside sharpcap-focus-temperature as two sibling repositories cloned under the same parent folder. The exact parent path does not matter; only the sibling relationship is required:
<any-parent>\
├── sharpcap-focus-temperature\ ← produces both state JSON files
│ ├── sharpcap_focuser.py
│ ├── sharpcap_focus_state.json ← main tube (single source of truth)
│ └── sharpcap_focus_state_guide.json ← guide tube (single source of truth)
└── sharpcap-focus-sequencer\ ← consumes both state JSON files
├── focus_sequencer.py
├── run_focus.bat ← main tube (C8, via Device Hub)
└── run_focus_guide.bat ← guide tube (50ED, direct ASCOM)
Do not copy the state JSON files into this repository.
They are generated by sharpcap_focuser.py and must always be read
from their original location. Copying them would create stale duplicates
that silently drift from the real models.
[New night]
│
├─ Focus guide tube once at session start:
│ run_focus_guide.bat (direct ASCOM.EAF_2.Focuser)
│
└─ Main tube imaging sequence loop:
├─ Capture subframes
├─ Dither
└─ Run run_focus.bat (calls focus_sequencer.py)
│
├─ busy check (abort if SharpCap autofocus is running)
├─ call sharpcap_focuser.py ← refreshes main tube state JSON
└─ apply thermal correction to main tube
│
(repeat every 7 min)
│
└─ SharpCap PERIODIC Refocus at ΔT = 1 °C
└─ next cycle picks up the new reference automatically
Clone both repositories as sibling directories under the same parent folder:
cd <any-parent>
git clone https://github.com/davidglt/sharpcap-focus-sequencer.git
git clone https://github.com/davidglt/sharpcap-focus-temperature.gitCreate and populate each virtual environment:
cd sharpcap-focus-sequencer
python -m venv .venv
.venv\Scripts\pip install -r requirements\requirements.txt
cd ..
cd sharpcap-focus-temperature
python -m venv .venv
.venv\Scripts\pip install -r requirements\requirements.txtThe provided wrapper scripts use %~dp0 to locate themselves, so they work
from any parent directory without any path configuration.
By default, Windows may block scripts downloaded from the internet.
To allow the virtual environment activation scripts and .bat wrappers to run,
set the execution policy for the current user once from an elevated
PowerShell prompt:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedWhat this does: allows locally created scripts to run, and allows downloaded scripts that are signed by a trusted publisher. It does not disable Windows Defender or any other security mechanism.
If you prefer a narrower change, you can unblock only the specific files instead:
Unblock-File -Path C:\astro\sharpcap-focus-sequencer\run_focus.bat Unblock-File -Path C:\astro\sharpcap-focus-sequencer\run_focus_guide.bat Unblock-File -Path C:\astro\sharpcap-focus-temperature\sharpcap_focuser.py
Both state JSON files live in the sibling repository sharpcap-focus-temperature.
The sequencer always reads from and writes to:
| Tube | State JSON | Entry point |
|---|---|---|
| Main (C8 + ASI2600MC Pro) | ..\sharpcap-focus-temperature\sharpcap_focus_state.json |
run_focus.bat |
| Guide (50ED + ASI224MC) | ..\sharpcap-focus-temperature\sharpcap_focus_state_guide.json |
run_focus_guide.bat |
Use --state-json <path> only in exceptional cases (e.g. a non-standard
clone layout). Do not copy the files into this repository.
- Python 3.10 or newer.
- Windows (ASCOM platform required).
- ASCOM Platform installed.
- ZWO EAF ASCOM driver installed.
pywin32
pip install pywin32Normal run via wrappers (recommended):
run_focus.bat # main tube — C8, via Device Hub
run_focus_guide.bat # guide tube — 50ED, direct ASCOMOr directly via Python:
python focus_sequencer.py # main tube (default)
python focus_sequencer.py --ascom-id "ASCOM.EAF_2.Focuser" \ # guide tube (direct)
--state-json "..\sharpcap-focus-temperature\sharpcap_focus_state_guide.json"Dry run (connects to the driver, reads real position and temperature, calculates the target, but does not move the focuser and does not update the state JSON):
run_focus.bat --dry-run
run_focus_guide.bat --dry-runDry run with temperature override:
run_focus.bat --dry-run --temp 18.5Custom backlash value (or disable entirely):
run_focus.bat --backlash 300
run_focus.bat --backlash 0 # disable backlash compensationCustom minimum correction threshold for backlash direction:
run_focus.bat --min-correction 50 # default
run_focus.bat --min-correction 0 # always move in both directions| Option | Default | Description |
|---|---|---|
--state-json |
auto-detected | Path to the JSON state file produced by sharpcap_focuser.py. If omitted, uses the canonical sibling-repository path. |
--ascom-id |
ASCOM.DeviceHub.Focuser |
ASCOM ProgID of the focuser driver. |
--dry-run |
off | Connect to the driver, read real position and temperature, calculate the target, but do not move the focuser, do not refresh the state JSON, and do not update last_temp_applied. Use --temp to override the sensor reading. |
--temp |
(from sensor) | Override the temperature read from the EAF sensor (°C). Requires an ASCOM connection to read the real focuser position. Useful with --dry-run to simulate a specific temperature scenario. |
--backlash |
500 |
Backlash compensation in steps. The focuser always arrives at the target from below; if the target is below the current position, it first overshoots to (target − backlash) then moves up. Set to 0 to disable. |
--min-correction |
50 |
Minimum correction (steps) required to trigger a move only when a backlash overshoot is needed (target < current position). Moves in the favourable direction (target ≥ current, no overshoot) are always applied regardless of size. With TCF = −61.59 steps/°C, 50 steps ≈ 0.81 °C. Set to 0 to always move in both directions. |
--move-timeout |
60 |
Seconds to wait for each focuser move to complete. |
The ZWO EAF ASCOM driver reports the commanded position, not the physical encoder position. This means the driver's built-in backlash compensation and any optical measurement (double V-curve) are the only reliable methods.
To avoid double-compensation, use only one layer:
| Layer | Recommended setting | Notes |
|---|---|---|
| ZWO EAF ASCOM driver | 0 | Disable — let the script handle it |
| SharpCap backlash | 0 | Disable — only affects SharpCap autofocus moves |
--backlash (this script) |
500 (default, adjust after measurement) | Script overshoots then approaches from below |
Measuring backlash: Use the optical double V-curve method in SharpCap (run autofocus twice approaching from opposite directions and compare the best-focus positions). The ZWO EAF ASCOM driver reports commanded position only, so software step-counting tools yield 0 and are not useful.
Current configuration: ASCOM driver backlash = 0 (both EAFs), SharpCap backlash = 0. The script uses
--backlash 500by default. Measure the real backlash of your setup and update this value. Typical values for a well-adjusted EAF on a C8 are 100–300 steps.
The script checks IsMoving immediately after connecting. If the focuser is
already moving (e.g. SharpCap is running an autofocus), the script logs a
warning and exits cleanly without touching the focuser:
2026-08-25 23:21:00 | INFO | START | ...
2026-08-25 23:21:01 | WARN | SKIP | Focuser busy (IsMoving=True) — skipping this cycle, retry in 7 min
2026-08-25 23:21:01 | INFO | END | pos=24911 | reason=busy
The next scheduled execution (7 minutes later by default in the SharpCap sequencer) will retry normally.
Small thermal corrections in the unfavourable direction (focuser must move
inward, requiring a backlash overshoot) are skipped when the correction
is smaller than --min-correction. This avoids unnecessary double moves
(overshoot + return) for insignificant corrections.
Corrections in the favourable direction (focuser moves outward — the normal direction during a cooling night) are always applied, regardless of size. This keeps the focus continuously well-corrected with small, frequent adjustments instead of accumulating drift.
--min-correction |
Thermal equivalent (TCF = −61.59 steps/°C) | When to use |
|---|---|---|
| 20 | ~0.32 °C | Very sensitive, moves almost always |
| 50 (default) | ~0.81 °C | Balanced — recommended |
| 100 | ~1.62 °C | Conservative, only large corrections |
If you have more than one ZWO EAF connected (e.g. main tube + guide tube), the ZWO ASCOM driver registers each unit under a different ProgID:
| ProgID | Tube | Firmware | Physical travel | Focus position | Driver backlash |
|---|---|---|---|---|---|
ASCOM.EAF.Focuser |
First EAF — main tube (C8 + ASI2600MC Pro) | 3.3.8A | 0 – 64 264 steps | ~25 000 steps | 0 |
ASCOM.EAF_2.Focuser |
Second EAF — guide tube (50ED + ASI224MC) | 3.3.8C | 0 – 520 000 steps | ~335 000 steps | 0 |
Note on ProgID assignment: ASCOM ProgIDs are assigned by the ZWO driver in USB enumeration order and can change when USB hubs or adapters are added or removed (e.g. adding a CH341T USB-serial adapter). Always verify with
detect_focusers.pyafter any USB topology change.
Note: The guide tube EAF (50ED) must have Max Steps set to 520 000 in ASICap → Focuser → Advanced. After a firmware update or power cycle that resets the counter, use Set Current Position (without moving the focuser) to restore the correct step count before any automated run.
| Tube | ProgID | Access mode | Reason |
|---|---|---|---|
| Main (C8 + ASI2600MC Pro) | ASCOM.DeviceHub.Focuser |
Via ASCOM Device Hub | SharpCap and this script both need simultaneous access. Configure Device Hub to proxy ASCOM.EAF.Focuser. |
| Guide (50ED + ASI224MC) | ASCOM.EAF_2.Focuser |
Direct | SharpCap does not access the guide tube EAF. Device Hub is not needed. |
With both EAF units connected, run:
python detect_focusers.pyThis probes ASCOM.EAF.Focuser through ASCOM.EAF_5.Focuser and prints
the Name, Position, and Temperature of each unit that responds:
Probing ASCOM focuser ProgIDs...
[OK] ASCOM.EAF.Focuser
Name : ZWO Focuser
Description : ZWO Focuser (1)
Position : 25,018 steps <-- main tube
Temperature : 27.16 °C
[OK] ASCOM.EAF_2.Focuser
Name : ZWO Focuser
Description : ZWO Focuser (2)
Position : 335,675 steps <-- guide tube
Temperature : 26.60 °C
If detect_focusers.py does not find your device, use the ASCOM Chooser
directly to discover the exact ProgID registered on your system:
import win32com.client
chooser = win32com.client.Dispatch("ASCOM.Utilities.Chooser")
chooser.DeviceType = "Focuser"
prog_id = chooser.Choose("")
print(f"ProgID: '{prog_id}'")
if prog_id:
f = win32com.client.Dispatch(prog_id)
f.Connected = True
print(f"Name : {f.Name}")
print(f"Position : {f.Position}")
print(f"Temperature: {f.Temperature}")
f.Connected = FalseThis opens the ASCOM Chooser window — select the desired EAF and click OK (not the X button). The ProgID, current position, and temperature will be printed to the console. Repeat for each EAF to map all connected units.
- Run
detect_focusers.pyonce (with both EAFs connected) to identify the correct ProgIDs. - Guide tube: at the start of each night, run
run_focus_guide.batonce to apply the thermally compensated starting position for the 50ED. - Main tube: in your nightly sequencer (SharpCap Advanced Sequencer, NINA, SGP'Pro),
add a Script step after each dither block pointing to
run_focus.bat. The wrapper uses%~dp0so it works from any installation path. - The script auto-detects the state JSON, calls
sharpcap_focuser.pyto refresh it, reads the real EAF position and temperature, calculates the correction, and moves the focuser. - SharpCap's
PERIODIC Refocus WHEN TEMP CHANGES BY 1triggers a full autofocus when needed — the next cycle offocus_sequencer.pypicks up the new reference automatically.
This script reads and updates the state JSON files in-place
in ..\sharpcap-focus-temperature\.
Main tube example:
{
"timestamp_ref": "2026-08-24 23:11:32",
"temp_ref": 18.4,
"focus_ref": 25342,
"last_temp_applied": 17.1,
"last_focus_applied": 25422,
"model_tcf": -61.59,
"model_inv_tcf": -0.016237,
"model_intercept_c": 432.939
}After each run, last_temp_applied and last_focus_applied are updated to
reflect the correction just applied, while focus_ref and temp_ref remain
unchanged as the original reference point.
For diagnostics or a forced refresh without triggering a thermal correction,
call sharpcap_focuser.py directly using the sibling repository's own Python:
# Main tube
..\sharpcap-focus-temperature\.venv\Scripts\python.exe ..\sharpcap-focus-temperature\sharpcap_focuser.py
# Guide tube
..\sharpcap-focus-temperature\.venv\Scripts\python.exe ..\sharpcap-focus-temperature\sharpcap_focuser.py --tube guide- sharpcap-focus-temperature — extracts autofocus data from SharpCap logs and fits the thermal regression model.
This project is licensed under the GNU General Public License v3.0 or later.
See the LICENSE.txt file for the full license text.
David González López-Tercero
Website: https://dragonit.es
Email: davidglt@dragonit.es