Skip to content

Commit e9afc91

Browse files
mrosseelclaude
andauthored
feat: add manual date and location entry screens (#402)
* feat: add manual date and location entry screens Add UIDateEntry screen that automatically follows UITimeEntry, allowing users to set both time and date when GPS is unavailable. Add UILocationEntry with three-step lat/lon/alt flow for manual coordinate entry, using the same chaining pattern. Key changes: - New UIDateEntry: 3-box date entry (YYYY-MM-DD) with pre-fill - New UILocationEntry: 3-step coordinate entry (lat → lon → alt) - UITimeEntry chains to UIDateEntry on confirm - Location.make_fix() helper shared between UI and web server - Consistent navigation: Right=confirm, Left=back/cancel - Menu restructured: "Set Time/Date", location submenu with "Enter Coords" and "Saved" options Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: force manual time update and show date in GPS status - set_datetime() ignored manual times earlier than current time due to GPS clock-drift guard. Add force parameter to bypass the guard. - Manual time/date setting now sends "time_force" message to GPS queue. - GPS status detailed view now shows the date alongside time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: protect manual time from GPS overwrite fakeGPS sends time updates every 0.5s with datetime.now(), immediately overwriting any manually set time. Add __datetime_manual flag that blocks GPS time updates once time is manually set. Reset clears it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: separate location and time/date reset Location reset should not clear manual time. Add separate "Reset Time/Date" menu item and reset_datetime GPS message. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: stay on last box instead of wrapping to first Auto-advance and key_right were using modulo wrap, sending cursor back to box 0 after filling the last box. Now stays on last box so the user can press Right to confirm. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove unnecessary F821 per-file-ignore TYPE_CHECKING stubs for gettext `_` already satisfy ruff's F821 check, so the blanket ignore for ui/*.py is not needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: improve location menu with load/save and empty state - Rename "Saved" to "Load Location", show "No saved locations" message when list is empty instead of blank screen - Add "Save Location" menu item that prompts for name, shows "No location lock" if no GPS/manual fix is set - Use Location.make_fix() in location list load action - Menu: Enter Coords / Load Location / Save Location Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: shorten messages to fit 128px screen - "No saved locations" -> "No locations" for bold font fit - "Set: date time" -> "date\ntime" two-line format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use two-line format for save location popup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4aafd89 commit e9afc91

10 files changed

Lines changed: 819 additions & 57 deletions

File tree

python/PiFinder/main.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ def main(
571571
if (
572572
not location.source == "WEB"
573573
and not location.source.startswith("CONFIG:")
574+
and not location.source == "MANUAL"
574575
and (
575576
location.error_in_m == 0
576577
or float(gps_content["error_in_m"])
@@ -611,18 +612,22 @@ def main(
611612
location.lon,
612613
location.altitude,
613614
)
614-
if gps_msg == "time":
615+
if gps_msg in ("time", "time_force"):
615616
if isinstance(gps_content, datetime.datetime):
616617
gps_dt = gps_content
617618
else:
618619
gps_dt = gps_content["time"]
619-
shared_state.set_datetime(gps_dt)
620+
shared_state.set_datetime(
621+
gps_dt, force=(gps_msg == "time_force")
622+
)
620623
if log_time:
621624
logger.info("GPS Time (logged only once): %s", gps_dt)
622625
log_time = False
623626
if gps_msg == "reset":
624627
location.reset()
625628
shared_state.set_location(location)
629+
if gps_msg == "reset_datetime":
630+
shared_state.reset_datetime()
626631
if gps_msg == "satellites":
627632
# logger.debug("Main: GPS nr sats seen: %s", gps_content)
628633
shared_state.set_sats(gps_content)

python/PiFinder/server.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pydeepskylog as pds
1010
from PIL import Image
1111
from PiFinder import utils, calc_utils, config
12+
from PiFinder.state import Location
1213
from PiFinder.db.observations_db import (
1314
ObservationsDatabase,
1415
)
@@ -938,18 +939,7 @@ def serve_pil_image():
938939

939940
@auth_required
940941
def gps_lock(lat: float = 50, lon: float = 3, altitude: float = 10):
941-
msg = (
942-
"fix",
943-
{
944-
"lat": lat,
945-
"lon": lon,
946-
"altitude": altitude,
947-
"error_in_m": 0,
948-
"source": "WEB",
949-
"lock": True,
950-
},
951-
)
952-
self.gps_queue.put(msg)
942+
self.gps_queue.put(Location.make_fix(lat, lon, altitude, "WEB"))
953943
logger.debug("Putting location msg on gps_queue: {msg}")
954944

955945
@auth_required

python/PiFinder/state.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,24 @@ def __str__(self):
206206
f"{f', last_lock={self.last_gps_lock}' if self.last_gps_lock else ''})"
207207
)
208208

209+
@staticmethod
210+
def make_fix(
211+
lat: float, lon: float, altitude: float = 0, source: str = "MANUAL"
212+
) -> tuple:
213+
"""Build a GPS fix message tuple for the gps_queue."""
214+
return (
215+
"fix",
216+
{
217+
"lat": lat,
218+
"lon": lon,
219+
"altitude": altitude,
220+
"error_in_m": 0,
221+
"source": source,
222+
"lock": True,
223+
"lock_type": 2,
224+
},
225+
)
226+
209227
def reset(self):
210228
self.lat = 0.0
211229
self.lon = 0.0
@@ -264,6 +282,7 @@ def __init__(self) -> None:
264282
self.__sqm_details: dict = {} # Full SQM calculation details for calibration
265283
self.__datetime = None
266284
self.__datetime_time = None
285+
self.__datetime_manual = False # True when manually set, blocks GPS overrides
267286
self.__screen = None
268287
self.__solve_pixel = config.Config().get_option("solve_pixel")
269288
self.__arch = None
@@ -416,11 +435,21 @@ def local_datetime(self):
416435
return dt.astimezone(pytz.timezone("UTC"))
417436
return dt.astimezone(pytz.timezone("UTC"))
418437

419-
def set_datetime(self, dt):
438+
def set_datetime(self, dt, force=False):
420439
if dt.tzname() is None:
421440
utc_tz = pytz.timezone("UTC")
422441
dt = utc_tz.localize(dt)
423442

443+
if force:
444+
self.__datetime_time = time.time()
445+
self.__datetime = dt
446+
self.__datetime_manual = True
447+
return
448+
449+
# Skip GPS time updates when time was manually set
450+
if self.__datetime_manual:
451+
return
452+
424453
if self.__datetime is None:
425454
self.__datetime_time = time.time()
426455
self.__datetime = dt
@@ -435,6 +464,12 @@ def set_datetime(self, dt):
435464
self.__datetime_time = time.time()
436465
self.__datetime = dt
437466

467+
def reset_datetime(self):
468+
"""Clear manual datetime override, allowing GPS time updates again."""
469+
self.__datetime = None
470+
self.__datetime_time = None
471+
self.__datetime_manual = False
472+
438473
def screen(self):
439474
return self.__screen
440475

python/PiFinder/ui/callbacks.py

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111
import logging
1212
import gettext
1313
import time
14+
from datetime import datetime
15+
16+
import pytz
1417

1518
from typing import Any, TYPE_CHECKING
1619
from PiFinder import utils, calc_utils
20+
from PiFinder.locations import Location as SavedLocation
21+
from PiFinder.state import Location
1722
from PiFinder.ui.base import UIModule
23+
from PiFinder.ui.textentry import UITextEntry
1824
from PiFinder.catalogs import CatalogFilter
1925
from PiFinder.composite_object import CompositeObject, MagnitudeObject
2026

@@ -250,18 +256,68 @@ def get_wifi_mode(ui_module: UIModule) -> list[str]:
250256
return [wfs.read()]
251257

252258

259+
def set_location(ui_module: UIModule) -> None:
260+
"""
261+
Sets location from the coordinate entry UI.
262+
Reads lat, lon, alt from item_definition (passed through the chain).
263+
"""
264+
lat = ui_module.item_definition.get("lat", 0.0)
265+
lon = ui_module.item_definition.get("lon", 0.0)
266+
alt = ui_module.item_definition.get("alt", 0)
267+
logger.info(f"Setting location to: lat={lat}, lon={lon}, alt={alt}")
268+
269+
ui_module.command_queues["gps"].put(Location.make_fix(lat, lon, alt, "MANUAL"))
270+
ui_module.message(
271+
_("{lat:.2f}, {lon:.2f}\n{alt}m alt").format(lat=lat, lon=lon, alt=alt),
272+
2,
273+
)
274+
275+
253276
def gps_reset(ui_module: UIModule) -> None:
254277
ui_module.command_queues["gps"].put(("reset", {}))
255278
ui_module.message("Location Reset", 2)
256279

257280

281+
def datetime_reset(ui_module: UIModule) -> None:
282+
ui_module.command_queues["gps"].put(("reset_datetime", {}))
283+
ui_module.message("Time/Date Reset", 2)
284+
285+
286+
def save_location(ui_module: UIModule) -> None:
287+
"""Save current location — prompts for name via text entry."""
288+
location = ui_module.shared_state.location()
289+
if not location.lock:
290+
ui_module.message(_("No location lock"), 2)
291+
return
292+
293+
def _save(name):
294+
new_loc = SavedLocation(
295+
name=name,
296+
latitude=location.lat,
297+
longitude=location.lon,
298+
height=location.altitude,
299+
error_in_m=location.error_in_m,
300+
source=location.source,
301+
)
302+
ui_module.config_object.locations.add_location(new_loc)
303+
ui_module.config_object.save_locations()
304+
ui_module.message(_("Saved\n{name}").format(name=name), 2)
305+
306+
num = len(ui_module.config_object.locations.locations) + 1
307+
item_definition = {
308+
"name": _("Location Name"),
309+
"class": UITextEntry,
310+
"mode": "text_entry",
311+
"initial_text": _("Loc {number}").format(number=num),
312+
"callback": _save,
313+
}
314+
ui_module.add_to_stack(item_definition)
315+
316+
258317
def set_time(ui_module: UIModule, time_str: str) -> None:
259318
"""
260319
Sets the time from the time entry UI
261320
"""
262-
from datetime import datetime
263-
import pytz
264-
265321
logger.info(f"Setting time to: {time_str}")
266322

267323
timezone_str = ui_module.shared_state.location().timezone
@@ -278,10 +334,28 @@ def set_time(ui_module: UIModule, time_str: str) -> None:
278334
dt_with_date = datetime(now.year, now.month, now.day, dt.hour, dt.minute, dt.second)
279335
dt_with_timezone = timezone.localize(dt_with_date)
280336

281-
ui_module.command_queues["gps"].put(("time", {"time": dt_with_timezone}))
337+
ui_module.command_queues["gps"].put(("time_force", {"time": dt_with_timezone}))
282338
ui_module.message(_("Time: {time}").format(time=time_str), 2)
283339

284340

341+
def set_datetime(ui_module: UIModule, date_str: str) -> None:
342+
"""
343+
Sets both date and time from the date entry UI.
344+
Reads the time_str from the item_definition (passed from UITimeEntry).
345+
"""
346+
time_str = ui_module.item_definition.get("time_str", "00:00:00")
347+
logger.info(f"Setting datetime to: {date_str} {time_str}")
348+
349+
timezone_str = ui_module.shared_state.location().timezone
350+
timezone = pytz.timezone(timezone_str)
351+
352+
dt = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M:%S")
353+
dt_with_timezone = timezone.localize(dt)
354+
355+
ui_module.command_queues["gps"].put(("time_force", {"time": dt_with_timezone}))
356+
ui_module.message(_("{date}\n{time}").format(date=date_str, time=time_str), 2)
357+
358+
285359
def handle_radec_entry(ui_module: UIModule, ra_deg: float, dec_deg: float) -> None:
286360
"""
287361
Handles RA/DEC coordinate entry from the coordinate input UI

0 commit comments

Comments
 (0)