From d76ec1c6b158fbf9e000bfe94baf49874d7ebc77 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Thu, 30 Apr 2026 22:27:14 +0200 Subject: [PATCH 01/27] Fix: Horizontal coordinate orientation (use -parallactic_angle) --- python/PiFinder/calc_utils.py | 19 +++++++++++++++++++ python/PiFinder/integrator.py | 11 ++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/python/PiFinder/calc_utils.py b/python/PiFinder/calc_utils.py index 112271a3d..af4741436 100644 --- a/python/PiFinder/calc_utils.py +++ b/python/PiFinder/calc_utils.py @@ -388,6 +388,25 @@ def ra_to_ha(self, ra_deg, dt): return ha_hrs * 180 / 12 # Hour angle [deg] + def radec_to_pa(self, ra_deg, dec_deg, dt): + """ + Returns the parallactic angle of an object at (ra, dec) as seen from an + observer at latitiude, lat and time, dt. See hadec_to_pa() for how + parallactic angle is defined. + + INPUTS: + ra_deg: Right ascension [deg] + dec_deg: Declination [deg] + dt: Python datetime object (must be timezone-aware) + + RETURNS: + pa_deg: Parallactic angle [deg] + """ + ha_deg = self.ra_to_ha(ra_deg, dt) # Note that HA is in deg + lat_deg = self._observer_geoid.latitude.degrees + pa_deg = hadec_to_pa(ha_deg, dec_deg, lat_deg) + return pa_deg # Parallactic angle [deg] + def radec_to_roll(self, ra_deg, dec_deg, dt): """ Returns the roll (field rotation) of an object at (ra, dec) as diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 2457f4872..eac6a1602 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -319,6 +319,7 @@ def get_roll_by_mount_type( if mount_type == "Alt/Az": # Altaz mounts: Display chart in horizontal coordinates if location and dt: + """ # We have location and time/date (and assume that location has been set) # Roll at the target RA/Dec in the horizontal frame roll_deg = calc_utils.sf_utils.radec_to_roll(ra_deg, dec_deg, dt) @@ -332,12 +333,16 @@ def get_roll_by_mount_type( roll_deg - np.sign(ha_deg) * 180 ) # In essence, gives: roll_deg = -pa_deg # End of HACK + """ + # chart.py uses roll to rotate the chart around the target center + # by roll in anti-clockwise direction. Use -parallactic_angle + roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) else: # No position or time/date available, so set roll to 0.0 - roll_deg = 0.0 + roll_deg = 0.0 # NCP up elif mount_type == "EQ": # EQ-mounts: Display chart with NCP up so roll = 0.0 - roll_deg = 0.0 + roll_deg = 0.0 # NCP up else: logger.error(f"Unknown mount type: {mount_type}. Cannot set roll.") roll_deg = 0.0 @@ -347,6 +352,6 @@ def get_roll_by_mount_type( # EQ mounts: NCP up in northern hemisphere, SCP up in southern hemisphere if location: if location.lat < 0.0: - roll_deg += 180.0 # Southern hemisphere + roll_deg += 180.0 # Southern hemisphere TODO: Verify this return roll_deg From 848ef57e61cb3858608b68eed681a20863fdadad Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Fri, 1 May 2026 22:56:22 +0100 Subject: [PATCH 02/27] Remove duplicate definition of dt --- python/PiFinder/integrator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index eac6a1602..18e6bbf99 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -161,7 +161,6 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # Set Alt/Az because it's needed for the catalogs for the # Alt/Az mount type. TODO: Can this be moved to the catalog? - dt = shared_state.datetime() if location and dt: solved["Alt"], solved["Az"] = calc_utils.sf_utils.radec_to_altaz( solved["RA"], solved["Dec"], dt From 29cd601224077aa2d2d353a1fb2e920dd8f34fa0 Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Fri, 1 May 2026 23:45:41 +0100 Subject: [PATCH 03/27] Encapsulate functionality to set Alt, Az, Roll in solved dict to a func --- python/PiFinder/integrator.py | 66 ++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 18e6bbf99..264692005 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -75,6 +75,7 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa pass if type(next_image_solve) is dict: + # TODO: Refactor this bit: # For camera solves, always start from last successful camera solve # NOT from shared_state (which may contain IMU drift) # This prevents IMU noise accumulation during failed solves @@ -114,6 +115,7 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # We have a new image solve: Use plate-solving for RA/Dec update_plate_solve_and_imu(imu_dead_reckoning, solved) else: + # TODO: In this case, it should run update_imu() # Failed solve - clear constellation solved["solve_source"] = "CAM_FAILED" solved["constellation"] = "" @@ -130,9 +132,9 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa if imu: update_imu(imu_dead_reckoning, solved, last_image_solve, imu) - # Push IMU updates only if newer than last push + # Update Alt, Az, Roll only if newer than last push if ( - solved["RA"] and solved["solve_time"] > last_solve_time + solved["RA"] is not None and solved["solve_time"] > last_solve_time # and solved["solve_source"] == "IMU" ): last_solve_time = time.time() # TODO: solve_time is ambiguous because it's also used for IMU dead-reckoning @@ -141,31 +143,10 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # TODO: Is it necessary to set location? # TODO: Altaz doesn't seem to be required for catalogs when in # EQ mode? Could be disabled in future when in EQ mode? - location = shared_state.location() - dt = shared_state.datetime() - if location: - calc_utils.sf_utils.set_location( - location.lat, location.lon, location.altitude - ) - - # Set the roll so that the chart is displayed appropriately for the mount type - solved["Roll"] = get_roll_by_mount_type( - solved["RA"], solved["Dec"], location, dt, mount_type - ) - - # Update remaining solved keys - # Calculate constellation for current position - solved["constellation"] = calc_utils.sf_utils.radec_to_constellation( - solved["RA"], solved["Dec"] - ) # TODO: Can the outer brackets be omitted? - - # Set Alt/Az because it's needed for the catalogs for the - # Alt/Az mount type. TODO: Can this be moved to the catalog? - if location and dt: - solved["Alt"], solved["Az"] = calc_utils.sf_utils.radec_to_altaz( - solved["RA"], solved["Dec"], dt - ) - + update_solved_coords(solved, + location=shared_state.location(), + dt=shared_state.datetime(), + mount_type=mount_type) # Push IMU update shared_state.set_solution(solved) shared_state.set_solve_state(True) @@ -176,7 +157,6 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # ======== Wrapper and helper functions =============================== - def update_plate_solve_and_imu(imu_dead_reckoning: ImuDeadReckoning, solved: dict): """ Wrapper for ImuDeadReckoning.update_plate_solve_and_imu() to @@ -274,6 +254,36 @@ def update_imu( ) +def update_solved_coords(solved, location, dt, mount_type): + """ + Based on RA/Dec, update the following in dictionary 'solved': + solved["Alt"], solved["Az"], solved["Roll"], solved["constellation"] + """ + if solved["RA"] is None or solved["Dec"] is None: + solved["constellation"] = None + solved["Alt"], solved["Az"] = None, None + solved["Roll"] = None + return + + # Calculate constellation for current position + solved["constellation"] = calc_utils.sf_utils.radec_to_constellation( + solved["RA"], solved["Dec"] + ) + + if location and dt: + # Location needs to be set for Alt/Az and roll calculations + calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) + # Set Alt/Az + solved["Alt"], solved["Az"] = calc_utils.sf_utils.radec_to_altaz( + solved["RA"], solved["Dec"], dt) + # Set the roll so that the chart is displayed appropriately for the mount type + solved["Roll"] = get_roll_by_mount_type( + solved["RA"], solved["Dec"], location, dt, mount_type) + else: + solved["Alt"], solved["Az"] = None, None + solved["Roll"] = None + + def set_cam2scope_alignment(imu_dead_reckoning: ImuDeadReckoning, solved: dict): """ Set alignment. From f53401a19de923be7f865d476a2f9d41f5e2205a Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 2 May 2026 00:35:00 +0100 Subject: [PATCH 04/27] 1) Ensure that AltAz is calculated when plate solved. 2) Fall back to IMU dead-reckoning if plate-solve fails. --- python/PiFinder/integrator.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 264692005..5c3dfd1ad 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -62,9 +62,10 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # This holds the last image solve position info # so we can delta for IMU updates last_image_solve = None - last_solve_time = time.time() + #last_solve_time = time.time() while True: + pointing_updated = False # Flag to track if pointing was updated in this loop state_utils.sleep_for_framerate(shared_state) # Check for new camera solve in queue @@ -101,19 +102,15 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # For failed solves, preserve ALL position data from previous solve # Don't recalculate from GPS (causes drift from GPS noise) - # Set solve_source and push camera solves immediately if solved["RA"] is not None: + # Successfully plate-solved: last_image_solve = copy.deepcopy(solved) solved["solve_source"] = "CAM" - # Calculate constellation for successful solve - solved["constellation"] = ( - calc_utils.sf_utils.radec_to_constellation( - solved["RA"], solved["Dec"] - ) - ) shared_state.set_solve_state(True) # We have a new image solve: Use plate-solving for RA/Dec update_plate_solve_and_imu(imu_dead_reckoning, solved) + #last_solve_time = solved["solve_time"] + pointing_updated = True else: # TODO: In this case, it should run update_imu() # Failed solve - clear constellation @@ -125,20 +122,18 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa shared_state.set_solution(solved) shared_state.set_solve_state(False) - elif imu_dead_reckoning.tracking: + if imu_dead_reckoning.tracking and not pointing_updated: # Previous plate-solve exists so use IMU dead-reckoning from # the last plate solved coordinates. imu = shared_state.imu() if imu: + #last_solve_time = time.time() update_imu(imu_dead_reckoning, solved, last_image_solve, imu) + pointing_updated = True # Update Alt, Az, Roll only if newer than last push - if ( - solved["RA"] is not None and solved["solve_time"] > last_solve_time - # and solved["solve_source"] == "IMU" - ): - last_solve_time = time.time() # TODO: solve_time is ambiguous because it's also used for IMU dead-reckoning - + if pointing_updated: + #last_solve_time = time.time() # Set location for roll and altaz calculations. # TODO: Is it necessary to set location? # TODO: Altaz doesn't seem to be required for catalogs when in From b898825f5db6cf2d11f91757f11bc6df0addb496 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 12:00:44 +0200 Subject: [PATCH 05/27] Fix: No need to add 180 deg to -parallactic_angle for horizontal coordinate chart display in the Southern hemisphere --- python/PiFinder/integrator.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 5c3dfd1ad..a08299b25 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -342,20 +342,17 @@ def get_roll_by_mount_type( # by roll in anti-clockwise direction. Use -parallactic_angle roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) else: - # No position or time/date available, so set roll to 0.0 + # No position or time/date available. Default to display in equatorial coordinate roll_deg = 0.0 # NCP up elif mount_type == "EQ": - # EQ-mounts: Display chart with NCP up so roll = 0.0 + # EQ-mounts: Display chart in equatorial coordinates roll_deg = 0.0 # NCP up + # If location is available, adjust roll for hemisphere: + if location: + if location.lat < 0.0: + roll_deg = 180.0 # SCP up (for southern hemisphere) else: logger.error(f"Unknown mount type: {mount_type}. Cannot set roll.") - roll_deg = 0.0 - - # If location is available, adjust roll for hemisphere: - # Altaz: North up in northern hemisphere, South up in southern hemisphere - # EQ mounts: NCP up in northern hemisphere, SCP up in southern hemisphere - if location: - if location.lat < 0.0: - roll_deg += 180.0 # Southern hemisphere TODO: Verify this + roll_deg = 0.0 # NCP up return roll_deg From ba6668598f68344c4c95ed5a7803e07a3441e1a3 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 12:18:45 +0200 Subject: [PATCH 06/27] Add "Coordinate Sys." to settings/Chart. (TODO need to update other languages) --- python/PiFinder/ui/menu_structure.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/python/PiFinder/ui/menu_structure.py b/python/PiFinder/ui/menu_structure.py index 52c3bad7a..385c79a58 100644 --- a/python/PiFinder/ui/menu_structure.py +++ b/python/PiFinder/ui/menu_structure.py @@ -863,6 +863,30 @@ def _(key: str) -> Any: }, ], }, + { + "name": _("Coordinate Sys."), + "class": UITextMenu, + "select": "single", + "config_option": "chart_coord", + "items": [ + { + "name": _("Horizontal"), + "value": "horiz", + }, + { + "name": _("EQ (auto)"), + "value": "eq_auto", + }, + { + "name": _("EQ (NCP up)"), + "value": "eq_ncp", + }, + { + "name": _("EQ (SCP up)"), + "value": "eq_scp", + }, + ], + }, ], }, { From ef04677b84bacb20d515b8463c57401c16429201 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 12:27:20 +0200 Subject: [PATCH 07/27] Rename settings --- python/PiFinder/ui/menu_structure.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/PiFinder/ui/menu_structure.py b/python/PiFinder/ui/menu_structure.py index 385c79a58..ece253114 100644 --- a/python/PiFinder/ui/menu_structure.py +++ b/python/PiFinder/ui/menu_structure.py @@ -874,15 +874,15 @@ def _(key: str) -> Any: "value": "horiz", }, { - "name": _("EQ (auto)"), + "name": _("EQ (Auto)"), "value": "eq_auto", }, { - "name": _("EQ (NCP up)"), + "name": _("EQ (North-up)"), "value": "eq_ncp", }, { - "name": _("EQ (SCP up)"), + "name": _("EQ (South-up)"), "value": "eq_scp", }, ], From 265a2a321e22a8b6c42fc14d7c33cf913285a8d8 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 12:31:01 +0200 Subject: [PATCH 08/27] Add default config --- default_config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/default_config.json b/default_config.json index 7ce509784..585e6d4ad 100644 --- a/default_config.json +++ b/default_config.json @@ -16,6 +16,7 @@ "chart_dso": 128, "chart_reticle": 128, "chart_constellations": 64, + "chart_coord": "horiz", "solve_pixel": [256, 256], "gps_type": "ublox", "gps_baud_rate": 9600, From b918b4d417b767bb05ffb4fb5dd48633cdd9125e Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 16:53:12 +0200 Subject: [PATCH 09/27] Rename --> chart_coord_sys --- default_config.json | 2 +- python/PiFinder/ui/menu_structure.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/default_config.json b/default_config.json index 585e6d4ad..e16948919 100644 --- a/default_config.json +++ b/default_config.json @@ -16,7 +16,7 @@ "chart_dso": 128, "chart_reticle": 128, "chart_constellations": 64, - "chart_coord": "horiz", + "chart_coord_sys": "horiz", "solve_pixel": [256, 256], "gps_type": "ublox", "gps_baud_rate": 9600, diff --git a/python/PiFinder/ui/menu_structure.py b/python/PiFinder/ui/menu_structure.py index ece253114..5d8790119 100644 --- a/python/PiFinder/ui/menu_structure.py +++ b/python/PiFinder/ui/menu_structure.py @@ -867,7 +867,7 @@ def _(key: str) -> Any: "name": _("Coordinate Sys."), "class": UITextMenu, "select": "single", - "config_option": "chart_coord", + "config_option": "chart_coord_sys", "items": [ { "name": _("Horizontal"), From ae5cb306d2b13c38dbc9f661ec65b55de9a97c1c Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 17:11:23 +0200 Subject: [PATCH 10/27] Use chart_coord_sys configuration to determine roll --- python/PiFinder/integrator.py | 71 ++++++++++++++--------------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index a08299b25..d5a0e35f8 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -52,9 +52,6 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa solved = get_initialized_solved_dict() cfg = config.Config() - mount_type = cfg.get_option("mount_type") - logger.debug(f"mount_type = {mount_type}") - # Set up dead-reckoning tracking by the IMU: imu_dead_reckoning = ImuDeadReckoning(cfg.get_option("screen_direction")) # imu_dead_reckoning.set_cam2scope_alignment(q_scope2cam) # TODO: Enable when q_scope2cam is available from alignment @@ -138,10 +135,10 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # TODO: Is it necessary to set location? # TODO: Altaz doesn't seem to be required for catalogs when in # EQ mode? Could be disabled in future when in EQ mode? - update_solved_coords(solved, + convert_solved_coords(solved, location=shared_state.location(), dt=shared_state.datetime(), - mount_type=mount_type) + chart_coord_sys=cfg.get_option("chart_coord_sys")) # Push IMU update shared_state.set_solution(solved) shared_state.set_solve_state(True) @@ -249,9 +246,9 @@ def update_imu( ) -def update_solved_coords(solved, location, dt, mount_type): +def convert_solved_coords(solved, location, dt, chart_coord_sys): """ - Based on RA/Dec, update the following in dictionary 'solved': + Convert RA/Dec coordinate to the following in the dictionary 'solved': solved["Alt"], solved["Az"], solved["Roll"], solved["constellation"] """ if solved["RA"] is None or solved["Dec"] is None: @@ -271,9 +268,9 @@ def update_solved_coords(solved, location, dt, mount_type): # Set Alt/Az solved["Alt"], solved["Az"] = calc_utils.sf_utils.radec_to_altaz( solved["RA"], solved["Dec"], dt) - # Set the roll so that the chart is displayed appropriately for the mount type - solved["Roll"] = get_roll_by_mount_type( - solved["RA"], solved["Dec"], location, dt, mount_type) + # Set the roll for the chart depending on the configuration + solved["Roll"] = get_roll_by_chart_coord_sys( + solved["RA"], solved["Dec"], chart_coord_sys, location=location, dt=dt) else: solved["Alt"], solved["Az"] = None, None solved["Roll"] = None @@ -301,58 +298,48 @@ def set_cam2scope_alignment(imu_dead_reckoning: ImuDeadReckoning, solved: dict): imu_dead_reckoning.set_cam2scope_alignment(solved_cam, solved_scope) -def get_roll_by_mount_type( +def get_roll_by_chart_coord_sys( ra_deg: float, # Right Ascension of the target in degrees dec_deg: float, # Declination of the target in degrees - location, # astropy EarthLocation object or None - dt: datetime.datetime, # datetime.datetime object or None - mount_type: str, # "Alt/Az" or "EQ" + chart_coord_sys: str, # "Alt/Az" or "EQ" + location=None, # astropy EarthLocation object or None + dt: datetime.datetime | None = None, # datetime.datetime object or None ) -> float: """ - Returns the roll (in degrees) depending on the mount type so that the chart - is displayed appropriately for the mount type. The RA and Dec of the target - should be provided (in degrees). + Returns the roll (in degrees) depending on the configured chart coordinate + system. The RA and Dec of the target should be provided (in degrees). - * Alt/Az mount: Display the chart in the horizontal coordinate so that up - in the chart points to the Zenith. - * EQ mount: Display the chart in the equatorial coordinate system with the - NCP up so roll = 0. + * horiz: Display the chart in the horizontal coordinate so that up in the + chart points to the Zenith. + * EQ (Auto): Display the chart in the equatorial coordinate system. + Automatically select NCP or SCP-up based on location. + * EQ (North-up), EQ (South-up): Display chart in the equatorial coordinate + system with NCP or SCP up. Assumes that location has already been set in calc_utils.sf_utils. """ - if mount_type == "Alt/Az": - # Altaz mounts: Display chart in horizontal coordinates + if chart_coord_sys == "horiz": + # Horizontal coordinates (alt/az): if location and dt: - """ - # We have location and time/date (and assume that location has been set) - # Roll at the target RA/Dec in the horizontal frame - roll_deg = calc_utils.sf_utils.radec_to_roll(ra_deg, dec_deg, dt) - - # HACK: - # The IMU direction flips at a certaint point. Could due to a - # an issue in the formula in calc_utils.sf_utils.hadec_to_roll() - # This is a temperary hack for testing. - ha_deg = calc_utils.sf_utils.ra_to_ha(ra_deg, dt) - roll_deg = ( - roll_deg - np.sign(ha_deg) * 180 - ) # In essence, gives: roll_deg = -pa_deg - # End of HACK - """ # chart.py uses roll to rotate the chart around the target center # by roll in anti-clockwise direction. Use -parallactic_angle roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) else: # No position or time/date available. Default to display in equatorial coordinate roll_deg = 0.0 # NCP up - elif mount_type == "EQ": - # EQ-mounts: Display chart in equatorial coordinates - roll_deg = 0.0 # NCP up + elif chart_coord_sys == "EQ (Auto)": + # Equatorial coordinates: (North-up/south-up depending on latitude) + roll_deg = 0.0 # Default (NCP up) # If location is available, adjust roll for hemisphere: if location: if location.lat < 0.0: roll_deg = 180.0 # SCP up (for southern hemisphere) + elif chart_coord_sys == "EQ (North-up)": + roll_deg = 0.0 + elif chart_coord_sys == "EQ (South-up)": + roll_deg = 180.0 else: - logger.error(f"Unknown mount type: {mount_type}. Cannot set roll.") + logger.error(f"Unknown chart coordinate system: {chart_coord_sys}. Defaulting to EQ North-up.") roll_deg = 0.0 # NCP up return roll_deg From 1a8c4b166fc7c4da66b6d21cebb1f72cdfe6fdd7 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 17:55:33 +0200 Subject: [PATCH 11/27] Rationalize solve_time. Record solve_time and push updates when solve_time > last_solve_time. Remove commented out solve_time. --- python/PiFinder/integrator.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index d5a0e35f8..fb6dd9203 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -59,7 +59,7 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # This holds the last image solve position info # so we can delta for IMU updates last_image_solve = None - #last_solve_time = time.time() + last_solve_time = time.time() while True: pointing_updated = False # Flag to track if pointing was updated in this loop @@ -106,14 +106,11 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa shared_state.set_solve_state(True) # We have a new image solve: Use plate-solving for RA/Dec update_plate_solve_and_imu(imu_dead_reckoning, solved) - #last_solve_time = solved["solve_time"] pointing_updated = True else: - # TODO: In this case, it should run update_imu() # Failed solve - clear constellation solved["solve_source"] = "CAM_FAILED" - solved["constellation"] = "" - + solved["constellation"] = "" # NOTE: This gets over-written by IMU dead-reckoning # Push failed solved immediately # This ensures auto-exposure sees Matches=0 for failed solves shared_state.set_solution(solved) @@ -124,13 +121,11 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # the last plate solved coordinates. imu = shared_state.imu() if imu: - #last_solve_time = time.time() update_imu(imu_dead_reckoning, solved, last_image_solve, imu) pointing_updated = True # Update Alt, Az, Roll only if newer than last push - if pointing_updated: - #last_solve_time = time.time() + if pointing_updated and solved["solve_time"] > last_solve_time: # Set location for roll and altaz calculations. # TODO: Is it necessary to set location? # TODO: Altaz doesn't seem to be required for catalogs when in @@ -139,6 +134,8 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa location=shared_state.location(), dt=shared_state.datetime(), chart_coord_sys=cfg.get_option("chart_coord_sys")) + last_solve_time = solved["solved_time"] + # Push IMU update shared_state.set_solution(solved) shared_state.set_solve_state(True) @@ -196,6 +193,7 @@ def update_imu( imu["quat"], quaternion.quaternion ), "Expecting quaternion.quaternion type" # TODO: Can be removed later q_x2imu = imu["quat"] # Current IMU measurement (quaternion) + imu_time = time.time() # When moving, switch to tracking using the IMU angle_moved = qt.get_quat_angular_diff(last_image_solve["imu_quat"], q_x2imu) @@ -227,8 +225,7 @@ def update_imu( # Store the current scope pointing estimate scope_eq = imu_dead_reckoning.get_scope_radec() solved["RA"], solved["Dec"], solved["Roll"] = scope_eq.get_deg(use_none=True) - - solved["solve_time"] = time.time() + solved["solve_time"] = imu_time solved["solve_source"] = "IMU" # Logging for states updated in solved: @@ -250,6 +247,8 @@ def convert_solved_coords(solved, location, dt, chart_coord_sys): """ Convert RA/Dec coordinate to the following in the dictionary 'solved': solved["Alt"], solved["Az"], solved["Roll"], solved["constellation"] + + TODO: This functionality should be moved to chart.py """ if solved["RA"] is None or solved["Dec"] is None: solved["constellation"] = None From 27033ab11380d3bd7198241c0a9f5e60bbbcbf0b Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 22:08:47 +0200 Subject: [PATCH 12/27] Re-order functions --- python/PiFinder/integrator.py | 46 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index fb6dd9203..7257e5700 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -275,28 +275,6 @@ def convert_solved_coords(solved, location, dt, chart_coord_sys): solved["Roll"] = None -def set_cam2scope_alignment(imu_dead_reckoning: ImuDeadReckoning, solved: dict): - """ - Set alignment. - TODO: Do this once at alignment - """ - # RA, Dec of camera center:: - solved_cam = RaDecRoll() - solved_cam.set_from_deg( - solved["camera_center"]["RA"], - solved["camera_center"]["Dec"], - solved["camera_center"]["Roll"], - ) - - # RA, Dec of target (where scope is pointing): - solved["Roll"] = 0 # Target roll isn't calculated by Tetra3. Set to zero here - solved_scope = RaDecRoll() - solved_scope.set_from_deg(solved["RA"], solved["Dec"], solved["Roll"]) - - # Set alignment in imu_dead_reckoning - imu_dead_reckoning.set_cam2scope_alignment(solved_cam, solved_scope) - - def get_roll_by_chart_coord_sys( ra_deg: float, # Right Ascension of the target in degrees dec_deg: float, # Declination of the target in degrees @@ -316,6 +294,8 @@ def get_roll_by_chart_coord_sys( system with NCP or SCP up. Assumes that location has already been set in calc_utils.sf_utils. + + # TODO: Move this to chart.py """ if chart_coord_sys == "horiz": # Horizontal coordinates (alt/az): @@ -342,3 +322,25 @@ def get_roll_by_chart_coord_sys( roll_deg = 0.0 # NCP up return roll_deg + + +def set_cam2scope_alignment(imu_dead_reckoning: ImuDeadReckoning, solved: dict): + """ + Set alignment. + TODO: Do this once at alignment + """ + # RA, Dec of camera center:: + solved_cam = RaDecRoll() + solved_cam.set_from_deg( + solved["camera_center"]["RA"], + solved["camera_center"]["Dec"], + solved["camera_center"]["Roll"], + ) + + # RA, Dec of target (where scope is pointing): + solved["Roll"] = 0 # Target roll isn't calculated by Tetra3. Set to zero here + solved_scope = RaDecRoll() + solved_scope.set_from_deg(solved["RA"], solved["Dec"], solved["Roll"]) + + # Set alignment in imu_dead_reckoning + imu_dead_reckoning.set_cam2scope_alignment(solved_cam, solved_scope) From 507c2bcc0412d68903d66226402a58b1441ee5f6 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 23:03:31 +0200 Subject: [PATCH 13/27] Refactor: Explicitly return values rather than changing the solved dictionary in place --- python/PiFinder/integrator.py | 78 +++++++++++++++++------------------ 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 7257e5700..c7d2d5fca 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -126,19 +126,24 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # Update Alt, Az, Roll only if newer than last push if pointing_updated and solved["solve_time"] > last_solve_time: - # Set location for roll and altaz calculations. - # TODO: Is it necessary to set location? - # TODO: Altaz doesn't seem to be required for catalogs when in - # EQ mode? Could be disabled in future when in EQ mode? - convert_solved_coords(solved, - location=shared_state.location(), - dt=shared_state.datetime(), - chart_coord_sys=cfg.get_option("chart_coord_sys")) - last_solve_time = solved["solved_time"] + solved["constellation"] = get_constellation(solved["RA"], solved["Dec"]) + + # TODO: Altaz doesn't seem to be required for catalogs when in + # EQ mode? Could be disabled in future when in EQ mode? + solved["Alt"], solved["Az"] = get_alt_az(solved["RA"], solved["Dec"], + shared_state.location(), + shared_state.datetime()) + + solved["Roll"] = get_roll_by_chart_coord_sys(solved["RA"], solved["Dec"], + chart_coord_sys=cfg.get_option("chart_coord_sys"), + location=shared_state.location(), + dt=shared_state.datetime()) - # Push IMU update - shared_state.set_solution(solved) - shared_state.set_solve_state(True) + if (solved["RA"] is not None) and (solved["Dec"] is not None): + # Push new solved to shared state + shared_state.set_solution(solved) + shared_state.set_solve_state(True) + last_solve_time = solved["solved_time"] except EOFError: logger.error("Main no longer running for integrator") @@ -243,36 +248,26 @@ def update_imu( ) -def convert_solved_coords(solved, location, dt, chart_coord_sys): +def get_constellation(RA_deg, Dec_deg) -> str: """ - Convert RA/Dec coordinate to the following in the dictionary 'solved': - solved["Alt"], solved["Az"], solved["Roll"], solved["constellation"] - - TODO: This functionality should be moved to chart.py + Get constellation name from the current RA/Dec position. """ - if solved["RA"] is None or solved["Dec"] is None: - solved["constellation"] = None - solved["Alt"], solved["Az"] = None, None - solved["Roll"] = None - return - - # Calculate constellation for current position - solved["constellation"] = calc_utils.sf_utils.radec_to_constellation( - solved["RA"], solved["Dec"] - ) + if RA_deg is None or Dec_deg is None: + return "" + else: + return calc_utils.sf_utils.radec_to_constellation(RA_deg, Dec_deg) - if location and dt: - # Location needs to be set for Alt/Az and roll calculations - calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) - # Set Alt/Az - solved["Alt"], solved["Az"] = calc_utils.sf_utils.radec_to_altaz( - solved["RA"], solved["Dec"], dt) - # Set the roll for the chart depending on the configuration - solved["Roll"] = get_roll_by_chart_coord_sys( - solved["RA"], solved["Dec"], chart_coord_sys, location=location, dt=dt) + +def get_alt_az(RA_deg, Dec_deg, location, dt) -> tuple[float | None, float | None]: + """ + Get Alt/Az from RA/Dec, location and datetime. + RETURNS: alt_deg, az_deg + """ + if RA_deg is None or Dec_deg is None: + return None, None else: - solved["Alt"], solved["Az"] = None, None - solved["Roll"] = None + calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) + return calc_utils.sf_utils.radec_to_altaz(RA_deg, Dec_deg, dt) def get_roll_by_chart_coord_sys( @@ -280,7 +275,7 @@ def get_roll_by_chart_coord_sys( dec_deg: float, # Declination of the target in degrees chart_coord_sys: str, # "Alt/Az" or "EQ" location=None, # astropy EarthLocation object or None - dt: datetime.datetime | None = None, # datetime.datetime object or None + dt: datetime.datetime | None=None, # datetime.datetime object or None ) -> float: """ Returns the roll (in degrees) depending on the configured chart coordinate @@ -294,12 +289,15 @@ def get_roll_by_chart_coord_sys( system with NCP or SCP up. Assumes that location has already been set in calc_utils.sf_utils. - # TODO: Move this to chart.py """ + if (ra_deg is None) or (dec_deg is None): + return None # Can't calculate roll without RA/Dec + if chart_coord_sys == "horiz": # Horizontal coordinates (alt/az): if location and dt: + calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) # chart.py uses roll to rotate the chart around the target center # by roll in anti-clockwise direction. Use -parallactic_angle roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) From 7c53700d82f9aefbbcba788dd02f8d5d6e312a9c Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sun, 3 May 2026 13:10:07 +0100 Subject: [PATCH 14/27] import __future__ to support | for type hints --- python/PiFinder/integrator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index c7d2d5fca..1f8432933 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -13,6 +13,7 @@ - Refactor into class PointingTracker """ +from __future__ import annotations # To support | in typehints (remove this for Python 3.10+) import datetime import queue @@ -275,7 +276,7 @@ def get_roll_by_chart_coord_sys( dec_deg: float, # Declination of the target in degrees chart_coord_sys: str, # "Alt/Az" or "EQ" location=None, # astropy EarthLocation object or None - dt: datetime.datetime | None=None, # datetime.datetime object or None + dt: datetime.datetime | None = None, # datetime object or None ) -> float: """ Returns the roll (in degrees) depending on the configured chart coordinate From 6a0c89228e5d0c31b31329bbdd810e5de33c3d5e Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Thu, 7 May 2026 19:28:11 +0100 Subject: [PATCH 15/27] Fix incorrect setting value. Also rename settings for clarity --- python/PiFinder/integrator.py | 7 ++++--- python/PiFinder/ui/menu_structure.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 1f8432933..ab110cf15 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -144,6 +144,7 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # Push new solved to shared state shared_state.set_solution(solved) shared_state.set_solve_state(True) + print(solved) last_solve_time = solved["solved_time"] except EOFError: @@ -305,16 +306,16 @@ def get_roll_by_chart_coord_sys( else: # No position or time/date available. Default to display in equatorial coordinate roll_deg = 0.0 # NCP up - elif chart_coord_sys == "EQ (Auto)": + elif chart_coord_sys == "eq_auto": # Equatorial coordinates: (North-up/south-up depending on latitude) roll_deg = 0.0 # Default (NCP up) # If location is available, adjust roll for hemisphere: if location: if location.lat < 0.0: roll_deg = 180.0 # SCP up (for southern hemisphere) - elif chart_coord_sys == "EQ (North-up)": + elif chart_coord_sys == "eq_north_up": roll_deg = 0.0 - elif chart_coord_sys == "EQ (South-up)": + elif chart_coord_sys == "eq_south_up": roll_deg = 180.0 else: logger.error(f"Unknown chart coordinate system: {chart_coord_sys}. Defaulting to EQ North-up.") diff --git a/python/PiFinder/ui/menu_structure.py b/python/PiFinder/ui/menu_structure.py index 5d8790119..c6e69a90a 100644 --- a/python/PiFinder/ui/menu_structure.py +++ b/python/PiFinder/ui/menu_structure.py @@ -879,11 +879,11 @@ def _(key: str) -> Any: }, { "name": _("EQ (North-up)"), - "value": "eq_ncp", + "value": "eq_north_up", }, { "name": _("EQ (South-up)"), - "value": "eq_scp", + "value": "eq_south_up", }, ], }, From 2ca22c127a69fe97999d341224bb9d6e1aafb01b Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Thu, 7 May 2026 19:34:01 +0100 Subject: [PATCH 16/27] Fix typo bug --- python/PiFinder/integrator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index ab110cf15..2e416541b 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -144,8 +144,7 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # Push new solved to shared state shared_state.set_solution(solved) shared_state.set_solve_state(True) - print(solved) - last_solve_time = solved["solved_time"] + last_solve_time = solved["solve_time"] except EOFError: logger.error("Main no longer running for integrator") From 25d4f6e9e20b1931d9bbcad47bfb345ee51a5416 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Fri, 8 May 2026 23:49:44 +0200 Subject: [PATCH 17/27] Fix: Gave error when location or dt weren't defined --- python/PiFinder/integrator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 2e416541b..f905ef4ef 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -264,7 +264,7 @@ def get_alt_az(RA_deg, Dec_deg, location, dt) -> tuple[float | None, float | Non Get Alt/Az from RA/Dec, location and datetime. RETURNS: alt_deg, az_deg """ - if RA_deg is None or Dec_deg is None: + if RA_deg is None or Dec_deg is None or location is None or dt is None: return None, None else: calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) From e50cea3a7fbb567e47e0e0278b515e0f234851ab Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 9 May 2026 12:36:44 +0200 Subject: [PATCH 18/27] Move get_roll_by_chart_coord_sys() to chart.py --- python/PiFinder/integrator.py | 57 ---------------------------------- python/PiFinder/ui/chart.py | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index f905ef4ef..7713d35a4 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -135,11 +135,6 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa shared_state.location(), shared_state.datetime()) - solved["Roll"] = get_roll_by_chart_coord_sys(solved["RA"], solved["Dec"], - chart_coord_sys=cfg.get_option("chart_coord_sys"), - location=shared_state.location(), - dt=shared_state.datetime()) - if (solved["RA"] is not None) and (solved["Dec"] is not None): # Push new solved to shared state shared_state.set_solution(solved) @@ -271,58 +266,6 @@ def get_alt_az(RA_deg, Dec_deg, location, dt) -> tuple[float | None, float | Non return calc_utils.sf_utils.radec_to_altaz(RA_deg, Dec_deg, dt) -def get_roll_by_chart_coord_sys( - ra_deg: float, # Right Ascension of the target in degrees - dec_deg: float, # Declination of the target in degrees - chart_coord_sys: str, # "Alt/Az" or "EQ" - location=None, # astropy EarthLocation object or None - dt: datetime.datetime | None = None, # datetime object or None -) -> float: - """ - Returns the roll (in degrees) depending on the configured chart coordinate - system. The RA and Dec of the target should be provided (in degrees). - - * horiz: Display the chart in the horizontal coordinate so that up in the - chart points to the Zenith. - * EQ (Auto): Display the chart in the equatorial coordinate system. - Automatically select NCP or SCP-up based on location. - * EQ (North-up), EQ (South-up): Display chart in the equatorial coordinate - system with NCP or SCP up. - - Assumes that location has already been set in calc_utils.sf_utils. - # TODO: Move this to chart.py - """ - if (ra_deg is None) or (dec_deg is None): - return None # Can't calculate roll without RA/Dec - - if chart_coord_sys == "horiz": - # Horizontal coordinates (alt/az): - if location and dt: - calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) - # chart.py uses roll to rotate the chart around the target center - # by roll in anti-clockwise direction. Use -parallactic_angle - roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) - else: - # No position or time/date available. Default to display in equatorial coordinate - roll_deg = 0.0 # NCP up - elif chart_coord_sys == "eq_auto": - # Equatorial coordinates: (North-up/south-up depending on latitude) - roll_deg = 0.0 # Default (NCP up) - # If location is available, adjust roll for hemisphere: - if location: - if location.lat < 0.0: - roll_deg = 180.0 # SCP up (for southern hemisphere) - elif chart_coord_sys == "eq_north_up": - roll_deg = 0.0 - elif chart_coord_sys == "eq_south_up": - roll_deg = 180.0 - else: - logger.error(f"Unknown chart coordinate system: {chart_coord_sys}. Defaulting to EQ North-up.") - roll_deg = 0.0 # NCP up - - return roll_deg - - def set_cam2scope_alignment(imu_dead_reckoning: ImuDeadReckoning, solved: dict): """ Set alignment. diff --git a/python/PiFinder/ui/chart.py b/python/PiFinder/ui/chart.py index 670e9c61e..d5e16f0cc 100644 --- a/python/PiFinder/ui/chart.py +++ b/python/PiFinder/ui/chart.py @@ -157,6 +157,13 @@ def update(self, force=False): self.plot_no_solve() elif self.solution_is_new(last_solve_time): # Solution is new so plot the updated chart + self.solution["Roll"] = self._get_roll_by_chart_coord_sys( + self.solution["RA"], self.solution["Dec"], + chart_coord_sys=self.config_object.get_option("chart_coord_sys"), + location=self.shared_state.location(), + dt=self.shared_state.datetime() # TODO used solution["solve_time"]? + ) + # This needs to be called first to set RA/DEC/ROLL image_obj, _visible_stars = self.starfield.plot_starfield( self.solution["RA"], @@ -263,3 +270,54 @@ def key_square(self): self.fov_index = 1 self.set_fov(self.fov_list[self.fov_index]) self.update() + + def _get_roll_by_chart_coord_sys( + ra_deg: float, # Right Ascension of the target in degrees + dec_deg: float, # Declination of the target in degrees + chart_coord_sys: str, # "Alt/Az" or "EQ" + location=None, # astropy EarthLocation object or None + dt: datetime.datetime | None = None, # datetime object or None + ) -> float: + """ + Returns the roll (in degrees) depending on the configured chart coordinate + system. The RA and Dec of the target should be provided (in degrees). + + * horiz: Display the chart in the horizontal coordinate so that up in the + chart points to the Zenith. + * EQ (Auto): Display the chart in the equatorial coordinate system. + Automatically select NCP or SCP-up based on location. + * EQ (North-up), EQ (South-up): Display chart in the equatorial coordinate + system with NCP or SCP up. + + Assumes that location has already been set in calc_utils.sf_utils. + # TODO: Move this to chart.py + """ + if (ra_deg is None) or (dec_deg is None): + return None # Can't calculate roll without RA/Dec + + if chart_coord_sys == "horiz": + # Horizontal coordinates (alt/az): + if location and dt: + calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) + # chart.py uses roll to rotate the chart around the target center + # by roll in anti-clockwise direction. Use -parallactic_angle + roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) + else: + # No position or time/date available. Default to display in equatorial coordinate + roll_deg = 0.0 # NCP up + elif chart_coord_sys == "eq_auto": + # Equatorial coordinates: (North-up/south-up depending on latitude) + roll_deg = 0.0 # Default (NCP up) + # If location is available, adjust roll for hemisphere: + if location: + if location.lat < 0.0: + roll_deg = 180.0 # SCP up (for southern hemisphere) + elif chart_coord_sys == "eq_north_up": + roll_deg = 0.0 + elif chart_coord_sys == "eq_south_up": + roll_deg = 180.0 + else: + logger.error(f"Unknown chart coordinate system: {chart_coord_sys}. Defaulting to EQ North-up.") + roll_deg = 0.0 # NCP up + + return roll_deg \ No newline at end of file From 4ffcb8f7bc1b550f8c6b79780c08ab5c1a38f629 Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 9 May 2026 20:50:38 +0100 Subject: [PATCH 19/27] Fix bug --- python/PiFinder/ui/chart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/PiFinder/ui/chart.py b/python/PiFinder/ui/chart.py index d5e16f0cc..a89f1e9bd 100644 --- a/python/PiFinder/ui/chart.py +++ b/python/PiFinder/ui/chart.py @@ -271,7 +271,7 @@ def key_square(self): self.set_fov(self.fov_list[self.fov_index]) self.update() - def _get_roll_by_chart_coord_sys( + def _get_roll_by_chart_coord_sys(self, ra_deg: float, # Right Ascension of the target in degrees dec_deg: float, # Declination of the target in degrees chart_coord_sys: str, # "Alt/Az" or "EQ" From a4e71e3458427781056668310e9e95c961d418f6 Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 9 May 2026 20:51:16 +0100 Subject: [PATCH 20/27] Refactor --- python/PiFinder/integrator.py | 1 - python/PiFinder/ui/chart.py | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 7713d35a4..c0424880e 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -15,7 +15,6 @@ """ from __future__ import annotations # To support | in typehints (remove this for Python 3.10+) -import datetime import queue import time import copy diff --git a/python/PiFinder/ui/chart.py b/python/PiFinder/ui/chart.py index a89f1e9bd..3b6ae42eb 100644 --- a/python/PiFinder/ui/chart.py +++ b/python/PiFinder/ui/chart.py @@ -5,7 +5,9 @@ This module contains the chart (starfield + constellation lines) UI Module class """ +from __future__ import annotations # To support | in typehints (remove this for Python 3.10+) +import datetime import time from PIL import ImageChops, Image @@ -163,7 +165,6 @@ def update(self, force=False): location=self.shared_state.location(), dt=self.shared_state.datetime() # TODO used solution["solve_time"]? ) - # This needs to be called first to set RA/DEC/ROLL image_obj, _visible_stars = self.starfield.plot_starfield( self.solution["RA"], @@ -274,9 +275,9 @@ def key_square(self): def _get_roll_by_chart_coord_sys(self, ra_deg: float, # Right Ascension of the target in degrees dec_deg: float, # Declination of the target in degrees - chart_coord_sys: str, # "Alt/Az" or "EQ" - location=None, # astropy EarthLocation object or None - dt: datetime.datetime | None = None, # datetime object or None + chart_coord_sys: str, + location=None, + dt: datetime.datetime | None = None, ) -> float: """ Returns the roll (in degrees) depending on the configured chart coordinate @@ -290,7 +291,6 @@ def _get_roll_by_chart_coord_sys(self, system with NCP or SCP up. Assumes that location has already been set in calc_utils.sf_utils. - # TODO: Move this to chart.py """ if (ra_deg is None) or (dec_deg is None): return None # Can't calculate roll without RA/Dec @@ -320,4 +320,4 @@ def _get_roll_by_chart_coord_sys(self, logger.error(f"Unknown chart coordinate system: {chart_coord_sys}. Defaulting to EQ North-up.") roll_deg = 0.0 # NCP up - return roll_deg \ No newline at end of file + return roll_deg From 790fc24add93fb8813177a8d4e683579736792f6 Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 9 May 2026 20:59:24 +0100 Subject: [PATCH 21/27] Make get_roll_by_chart_coord_sys() a func --- python/PiFinder/ui/chart.py | 94 +++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/python/PiFinder/ui/chart.py b/python/PiFinder/ui/chart.py index 3b6ae42eb..1f101fd73 100644 --- a/python/PiFinder/ui/chart.py +++ b/python/PiFinder/ui/chart.py @@ -159,7 +159,7 @@ def update(self, force=False): self.plot_no_solve() elif self.solution_is_new(last_solve_time): # Solution is new so plot the updated chart - self.solution["Roll"] = self._get_roll_by_chart_coord_sys( + self.solution["Roll"] = get_roll_by_chart_coord_sys( self.solution["RA"], self.solution["Dec"], chart_coord_sys=self.config_object.get_option("chart_coord_sys"), location=self.shared_state.location(), @@ -272,52 +272,54 @@ def key_square(self): self.set_fov(self.fov_list[self.fov_index]) self.update() - def _get_roll_by_chart_coord_sys(self, - ra_deg: float, # Right Ascension of the target in degrees - dec_deg: float, # Declination of the target in degrees - chart_coord_sys: str, - location=None, - dt: datetime.datetime | None = None, - ) -> float: - """ - Returns the roll (in degrees) depending on the configured chart coordinate - system. The RA and Dec of the target should be provided (in degrees). - * horiz: Display the chart in the horizontal coordinate so that up in the - chart points to the Zenith. - * EQ (Auto): Display the chart in the equatorial coordinate system. - Automatically select NCP or SCP-up based on location. - * EQ (North-up), EQ (South-up): Display chart in the equatorial coordinate - system with NCP or SCP up. - Assumes that location has already been set in calc_utils.sf_utils. - """ - if (ra_deg is None) or (dec_deg is None): - return None # Can't calculate roll without RA/Dec - - if chart_coord_sys == "horiz": - # Horizontal coordinates (alt/az): - if location and dt: - calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) - # chart.py uses roll to rotate the chart around the target center - # by roll in anti-clockwise direction. Use -parallactic_angle - roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) - else: - # No position or time/date available. Default to display in equatorial coordinate - roll_deg = 0.0 # NCP up - elif chart_coord_sys == "eq_auto": - # Equatorial coordinates: (North-up/south-up depending on latitude) - roll_deg = 0.0 # Default (NCP up) - # If location is available, adjust roll for hemisphere: - if location: - if location.lat < 0.0: - roll_deg = 180.0 # SCP up (for southern hemisphere) - elif chart_coord_sys == "eq_north_up": - roll_deg = 0.0 - elif chart_coord_sys == "eq_south_up": - roll_deg = 180.0 +def get_roll_by_chart_coord_sys( + ra_deg: float, # Right Ascension of the target in degrees + dec_deg: float, # Declination of the target in degrees + chart_coord_sys: str, + location=None, + dt: datetime.datetime | None = None, +) -> float: + """ + Returns the roll (in degrees) depending on the configured chart coordinate + system. The RA and Dec of the target should be provided (in degrees). + + * horiz: Display the chart in the horizontal coordinate so that up in the + chart points to the Zenith. + * EQ (Auto): Display the chart in the equatorial coordinate system. + Automatically select NCP or SCP-up based on location. + * EQ (North-up), EQ (South-up): Display chart in the equatorial coordinate + system with NCP or SCP up. + + Assumes that location has already been set in calc_utils.sf_utils. + """ + if (ra_deg is None) or (dec_deg is None): + return None # Can't calculate roll without RA/Dec + + if chart_coord_sys == "horiz": + # Horizontal coordinates (alt/az): + if location and dt: + calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) + # chart.py uses roll to rotate the chart around the target center + # by roll in anti-clockwise direction. Use -parallactic_angle + roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) else: - logger.error(f"Unknown chart coordinate system: {chart_coord_sys}. Defaulting to EQ North-up.") + # No position or time/date available. Default to display in equatorial coordinate roll_deg = 0.0 # NCP up - - return roll_deg + elif chart_coord_sys == "eq_auto": + # Equatorial coordinates: (North-up/south-up depending on latitude) + roll_deg = 0.0 # Default (NCP up) + # If location is available, adjust roll for hemisphere: + if location: + if location.lat < 0.0: + roll_deg = 180.0 # SCP up (for southern hemisphere) + elif chart_coord_sys == "eq_north_up": + roll_deg = 0.0 + elif chart_coord_sys == "eq_south_up": + roll_deg = 180.0 + else: + logger.error(f"Unknown chart coordinate system: {chart_coord_sys}. Defaulting to EQ North-up.") + roll_deg = 0.0 # NCP up + + return roll_deg From 79762267b6225afbc48072b4afd74a8aab636d0c Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 9 May 2026 21:11:38 +0100 Subject: [PATCH 22/27] Rename roll --> chart_rotation_angle --- python/PiFinder/ui/chart.py | 38 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/python/PiFinder/ui/chart.py b/python/PiFinder/ui/chart.py index 1f101fd73..c3a6bb2d8 100644 --- a/python/PiFinder/ui/chart.py +++ b/python/PiFinder/ui/chart.py @@ -159,17 +159,17 @@ def update(self, force=False): self.plot_no_solve() elif self.solution_is_new(last_solve_time): # Solution is new so plot the updated chart - self.solution["Roll"] = get_roll_by_chart_coord_sys( + chart_rot_angle = get_chart_rotation_angle( self.solution["RA"], self.solution["Dec"], chart_coord_sys=self.config_object.get_option("chart_coord_sys"), location=self.shared_state.location(), dt=self.shared_state.datetime() # TODO used solution["solve_time"]? ) - # This needs to be called first to set RA/DEC/ROLL + # This needs to be called first to set RA/DEC/rot_angle image_obj, _visible_stars = self.starfield.plot_starfield( self.solution["RA"], self.solution["Dec"], - self.solution["Roll"], + chart_rot_angle, constellation_brightness, ) image_obj = ImageChops.multiply( @@ -274,7 +274,7 @@ def key_square(self): -def get_roll_by_chart_coord_sys( +def get_chart_rotation_angle( ra_deg: float, # Right Ascension of the target in degrees dec_deg: float, # Declination of the target in degrees chart_coord_sys: str, @@ -282,8 +282,10 @@ def get_roll_by_chart_coord_sys( dt: datetime.datetime | None = None, ) -> float: """ - Returns the roll (in degrees) depending on the configured chart coordinate - system. The RA and Dec of the target should be provided (in degrees). + Returns angle (in degrees) to rotate the chart by depending on the + configured chart coordinate system. This was previously called "roll". + Chart will be plotted rotated around (RA, Dec); +ve means anti-clockwise + rotation. The RA and Dec of the target should be provided (in degrees). * horiz: Display the chart in the horizontal coordinate so that up in the chart points to the Zenith. @@ -291,35 +293,31 @@ def get_roll_by_chart_coord_sys( Automatically select NCP or SCP-up based on location. * EQ (North-up), EQ (South-up): Display chart in the equatorial coordinate system with NCP or SCP up. - - Assumes that location has already been set in calc_utils.sf_utils. """ if (ra_deg is None) or (dec_deg is None): - return None # Can't calculate roll without RA/Dec + return None # Can't calculate without RA/Dec if chart_coord_sys == "horiz": # Horizontal coordinates (alt/az): if location and dt: calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) - # chart.py uses roll to rotate the chart around the target center - # by roll in anti-clockwise direction. Use -parallactic_angle - roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) + # Use -parallactic_angle + rot_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) else: # No position or time/date available. Default to display in equatorial coordinate - roll_deg = 0.0 # NCP up + rot_deg = 0.0 # NCP up elif chart_coord_sys == "eq_auto": # Equatorial coordinates: (North-up/south-up depending on latitude) - roll_deg = 0.0 # Default (NCP up) - # If location is available, adjust roll for hemisphere: + rot_deg = 0.0 # NCP up (default & northern hemisphere) if location: if location.lat < 0.0: - roll_deg = 180.0 # SCP up (for southern hemisphere) + rot_deg = 180.0 # SCP up (for southern hemisphere) elif chart_coord_sys == "eq_north_up": - roll_deg = 0.0 + rot_deg = 0.0 elif chart_coord_sys == "eq_south_up": - roll_deg = 180.0 + rot_deg = 180.0 else: logger.error(f"Unknown chart coordinate system: {chart_coord_sys}. Defaulting to EQ North-up.") - roll_deg = 0.0 # NCP up + rot_deg = 0.0 # NCP up - return roll_deg + return rot_deg From c49c8f2916398dbfdef28509998573b1a3d9e67d Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 9 May 2026 21:13:58 +0100 Subject: [PATCH 23/27] Roll is not needed --- python/PiFinder/ui/chart.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/PiFinder/ui/chart.py b/python/PiFinder/ui/chart.py index c3a6bb2d8..c08bb91ac 100644 --- a/python/PiFinder/ui/chart.py +++ b/python/PiFinder/ui/chart.py @@ -243,8 +243,7 @@ def solution_is_new(self, last_solve_time): if last_solve_time <= self.last_update: return False if ( - self.solution["Roll"] is None - or self.solution["RA"] is None + self.solution["RA"] is None or self.solution["Dec"] is None ): return False From 610c9ebca1c8b4ea0b381b2d1e75e84e306e6a6d Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 9 May 2026 21:17:28 +0100 Subject: [PATCH 24/27] Edit comments --- python/PiFinder/integrator.py | 2 +- python/PiFinder/ui/chart.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index c0424880e..598d8cb9f 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -124,7 +124,7 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa update_imu(imu_dead_reckoning, solved, last_image_solve, imu) pointing_updated = True - # Update Alt, Az, Roll only if newer than last push + # Update Alt, Az only if newer than last push if pointing_updated and solved["solve_time"] > last_solve_time: solved["constellation"] = get_constellation(solved["RA"], solved["Dec"]) diff --git a/python/PiFinder/ui/chart.py b/python/PiFinder/ui/chart.py index c08bb91ac..b2ed3024b 100644 --- a/python/PiFinder/ui/chart.py +++ b/python/PiFinder/ui/chart.py @@ -163,9 +163,9 @@ def update(self, force=False): self.solution["RA"], self.solution["Dec"], chart_coord_sys=self.config_object.get_option("chart_coord_sys"), location=self.shared_state.location(), - dt=self.shared_state.datetime() # TODO used solution["solve_time"]? + dt=self.shared_state.datetime() ) - # This needs to be called first to set RA/DEC/rot_angle + # This needs to be called first to set RA/DEC/chart_rot_angle image_obj, _visible_stars = self.starfield.plot_starfield( self.solution["RA"], self.solution["Dec"], From 68b6d788a2f511e4d67353e6c898bc4fda89c8fd Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 9 May 2026 21:27:09 +0100 Subject: [PATCH 25/27] align.py: Re-use get_chart_rotation_angle() --- python/PiFinder/ui/align.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/python/PiFinder/ui/align.py b/python/PiFinder/ui/align.py index 976ea37c5..71f9aa8b1 100644 --- a/python/PiFinder/ui/align.py +++ b/python/PiFinder/ui/align.py @@ -14,6 +14,7 @@ from PiFinder.ui.marking_menus import MarkingMenuOption, MarkingMenu from PiFinder import plot from PiFinder.ui.base import UIModule +from PiFinder.ui.chart import get_chart_rotation_angle def align_on_radec(ra, dec, command_queues, config_object, shared_state) -> bool: @@ -215,26 +216,25 @@ def update(self, force=False): last_solve_time = self.solution["solve_time"] if self.solution_is_new(last_solve_time) or force: - # This needs to be called first to set RA/DEC/ROLL if self.align_mode: # We want to use the CAMERA solve as # it's not updated by the IMU and we'll be moving # the reticle to the star - image_obj, self.visible_stars = self.starfield.plot_starfield( - self.solution["camera_solve"]["RA"], - self.solution["camera_solve"]["Dec"], - self.solution["camera_solve"]["Roll"], - constellation_brightness, - shade_frustrum=True, - ) + chart_center = self.solution["camera_solve"] else: - image_obj, self.visible_stars = self.starfield.plot_starfield( - self.solution["camera_center"]["RA"], - self.solution["camera_center"]["Dec"], - self.solution["camera_center"]["Roll"], - constellation_brightness, - shade_frustrum=True, - ) + chart_center = self.solution["camera_center"] + + chart_rot_angle = get_chart_rotation_angle( + chart_center["RA"], chart_center["Dec"], + chart_coord_sys=self.config_object.get_option("chart_coord_sys"), + location=self.shared_state.location(), + dt=self.shared_state.datetime() + ) + # This needs to be called first to set RA/DEC/chart_rot_angle + image_obj, self.visible_stars = self.starfield.plot_starfield( + chart_center["RA"], chart_center["Dec"], chart_rot_angle, + constellation_brightness, shade_frustrum=True, + ) image_obj = ImageChops.multiply( image_obj.convert("RGB"), self.colors.red_image @@ -454,8 +454,7 @@ def solution_is_new(self, last_solve_time): if last_solve_time <= self.last_update: return False if ( - self.solution["Roll"] is None - or self.solution["RA"] is None + self.solution["RA"] is None or self.solution["Dec"] is None ): return False From 0206f1ccef8c57c081c0a9339f739a3086d8ebcf Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 9 May 2026 21:29:32 +0100 Subject: [PATCH 26/27] menu: Move chart_coordinate_sys further up --- python/PiFinder/ui/menu_structure.py | 48 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/python/PiFinder/ui/menu_structure.py b/python/PiFinder/ui/menu_structure.py index c6e69a90a..6a4f15bd9 100644 --- a/python/PiFinder/ui/menu_structure.py +++ b/python/PiFinder/ui/menu_structure.py @@ -771,6 +771,30 @@ def _(key: str) -> Any: "select": "single", "label": "chart_settings", "items": [ + { + "name": _("Coordinate Sys."), + "class": UITextMenu, + "select": "single", + "config_option": "chart_coord_sys", + "items": [ + { + "name": _("Horizontal"), + "value": "horiz", + }, + { + "name": _("EQ (Auto)"), + "value": "eq_auto", + }, + { + "name": _("EQ (North-up)"), + "value": "eq_north_up", + }, + { + "name": _("EQ (South-up)"), + "value": "eq_south_up", + }, + ], + }, { "name": _("Reticle"), "class": UITextMenu, @@ -863,30 +887,6 @@ def _(key: str) -> Any: }, ], }, - { - "name": _("Coordinate Sys."), - "class": UITextMenu, - "select": "single", - "config_option": "chart_coord_sys", - "items": [ - { - "name": _("Horizontal"), - "value": "horiz", - }, - { - "name": _("EQ (Auto)"), - "value": "eq_auto", - }, - { - "name": _("EQ (North-up)"), - "value": "eq_north_up", - }, - { - "name": _("EQ (South-up)"), - "value": "eq_south_up", - }, - ], - }, ], }, { From bce95905266aae0a3870b01670bd25890dae569f Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 9 May 2026 22:03:41 +0100 Subject: [PATCH 27/27] Fix logger issue - flagged up by nox lint check --- python/PiFinder/ui/chart.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/PiFinder/ui/chart.py b/python/PiFinder/ui/chart.py index b2ed3024b..55349f689 100644 --- a/python/PiFinder/ui/chart.py +++ b/python/PiFinder/ui/chart.py @@ -8,6 +8,7 @@ from __future__ import annotations # To support | in typehints (remove this for Python 3.10+) import datetime +import logging import time from PIL import ImageChops, Image @@ -18,6 +19,9 @@ from PiFinder import calc_utils +logger = logging.getLogger("Chart") + + class UIChart(UIModule): __title__ = "CHART" __help_name__ = "chart"