Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/skills/docs/references/product-knowledge-base.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ The PiFinder has a digital alignment system that maps where within its 10° fiel
- **Physical alignment vs digital alignment**: When a customer reports alignment problems, first verify they understand the alignment procedure (especially the SQUARE button press). If the camera is physically pointed too far from the telescope's optical axis (>5 degrees), no amount of digital alignment will help — the star won't be in the camera's FOV at all.

### Finding Objects & Push-To Guidance
- Access objects via main menu → **Objects** → choose **By Catalog**, **All Filtered**, **Recent** (session history), or **Name Search** (T9-style text entry).
- Access objects via main menu → **Objects** → choose **By Catalog**, **All Filtered**, **Recent** (session history), or **Name Search** (keypad text entry).
- In object lists, pressing **SQUARE** cycles through info displays: catalog designation → common names → magnitude/size with observation checkmarks.
- **Push-to guidance** (from Object Details screen):
- **Top number**: Rotational direction (CW/CCW) and degrees to move
Expand Down Expand Up @@ -403,7 +403,7 @@ The PiFinder connects to SkySafari (and other planetarium apps) via WiFi using t
- PiFinder initially sends 0° RA/DEC until the first plate solve completes
- Works with **SkySafari 5 Plus, 6, and 7** (7 is most reliable)
- SkySafari can lock the view to the scope's position
- SkySafari can send objects to PiFinder's observing list (useful alternative to T9 keypad entry)
- SkySafari can send objects to PiFinder's observing list (useful alternative to keypad text entry)
- **Single connection limit**: Only one device/app can connect to the PiFinder's LX200 server at a time. If SkySafari is connected on an iPhone, it must be disconnected before connecting from an iPad (or vice versa).
- **SkySafari cannot connect to PiFinder and a GoTo mount simultaneously** — choose one
- **Sleep mode warning**: If PiFinder enters sleep mode, it stops sending position updates. Extend or disable the sleep timer if using SkySafari continuously.
Expand Down
1 change: 0 additions & 1 deletion default_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"auto_exposure_zero_star_handler": "sweep",
"menu_anim_speed": 0.1,
"text_scroll_speed": "Med",
"t9_search": false,
"screen_direction": "right",
"mount_type": "Alt/Az",
"solver_debug": 0,
Expand Down
40 changes: 12 additions & 28 deletions docs/ax/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ At a high level:
└─► Catalogs object (single instance shared across the app)
├── CatalogFilter (one shared instance set on every catalog)
├── T9 / text search caches
└── Iterates as List[Catalog], each holding List[CompositeObject]
```

Expand Down Expand Up @@ -119,7 +118,7 @@ External code is expected to read through `get_objects()` (returns a
### 2.5 `Catalogs`

A container that holds a `List[Catalog]` plus the singleton
`CatalogFilter` and the T9 search cache. It exposes:
`CatalogFilter`. It exposes:

- `filter_catalogs()` — runs `filter_objects()` on every catalog.
- `set_catalog_filter(filter)` — installs one filter object on every
Expand All @@ -131,10 +130,9 @@ A container that holds a `List[Catalog]` plus the singleton
- `get_catalog_by_code(code)` / `get_object(code, sequence)` — direct
lookup.
- `search_by_text(s)` — substring match against all names (selected or
not, filtered or not).
- `search_by_t9(digits)` — see §5.
not, filtered or not). See §5.
- `add(catalog)` / `remove(code)` / `set(catalogs)` — mutate the
collection and invalidate the T9 cache.
collection.
- `is_loading()` — reports whether the background loader thread is
still alive (UI uses this to show a "still loading" indicator).
- `__iter__` — yields only selected catalogs.
Expand Down Expand Up @@ -254,31 +252,17 @@ filter is restored on app start.

## 5. Search

### 5.1 Text search

`Catalogs.search_by_text(s)` does a substring lower-case match against
each name on each object. Returns a `List[CompositeObject]`. No indexing
— it's O(n_names) per call but fine in practice.

### 5.2 T9 (keypad) search

PiFinder's hardware keypad uses a non-standard digit-to-letter mapping
(`KEYPAD_DIGIT_TO_CHARS` at the top of `catalogs.py` — note `7→abc`,
`1→tuv`, `3→'-+/`, etc.). `Catalogs.search_by_t9(digits)`:

1. Translates every object name to its digit-form via a
`str.maketrans` table.
2. Filters out characters that aren't valid T9 digits.
3. Caches `(catalog_code, sequence) → list[digit_string]` in
`_t9_cache`, invalidated when catalogs are added/removed/replaced.
4. Returns any object whose digit string contains the search pattern as
a substring (skipping objects whose digit string is shorter than the
query, since the substring couldn't match).

The cache is rebuilt lazily by `_ensure_t9_cache(objs)` only if the
dirty flag is set **or** the set of object keys has changed since the
last rebuild — so dynamic catalogs (PL, comets) updating their
positions don't trigger a rebuild as long as their key set is stable.
— it's O(n_names) per call but fine in practice. It is the only catalog
search.

On the hardware keypad, `UITextEntry` builds the search string with
multitap entry: each number key cycles through its letters (the keypad
uses a non-standard layout — `7→abc`, `1→tuv`, `3→'-+/`, etc.), and the
resulting text is passed to `search_by_text`. (An earlier `t9_search`
option that matched names against keypad-digit sequences has been
removed.)

---

Expand Down
8 changes: 2 additions & 6 deletions docs/ax/catalog/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ A `CatalogBase` (object list + sequence/id indices) plus filtering: holds the sh
_Avoid_: catalog object, catalog instance — use `Catalog` when the class is meant.

**Catalogs** (the catalog collection):
The container of `Catalog`s. Owns the singleton `CatalogFilter` and the T9/text search caches; this is the runtime API used by the UI and web. In prose, say "the catalog collection"; reserve `Catalogs` (code-style) for the type itself.
The container of `Catalog`s. Owns the singleton `CatalogFilter`; this is the runtime API used by the UI and web. In prose, say "the catalog collection"; reserve `Catalogs` (code-style) for the type itself.
_Avoid_: catalog list, catalog set, the catalogs object.

### Filtering
Expand Down Expand Up @@ -149,13 +149,9 @@ _Avoid_: catalog state (the enum is `CatalogState`; the wrapper is `CatalogStatu
### Search

**Text search** (`search_by_text`):
Lower-case substring match against every name in every catalog. Selection and filter state are ignored.
Lower-case substring match against every name in every catalog. Selection and filter state are ignored. This is the only catalog search; the on-screen keypad in `UITextEntry` uses multitap (cycle through a key's letters) to build the search string.
_Avoid_: name search, full-text search.

**T9 search** (`search_by_t9`):
Substring search after translating names to PiFinder's non-standard keypad-digit form (`KEYPAD_DIGIT_TO_CHARS` — `7→abc`, `1→tuv`, …). Backed by `_t9_cache` keyed on `(catalog_code, sequence)`.
_Avoid_: keypad search, digit search (use T9 search for the public concept).

### UI helpers

**CatalogDesignator**:
Expand Down
2 changes: 1 addition & 1 deletion docs/ax/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ hardware, or network dependency at construct or update time — are:
| `UIObjectList` | `object_list.py:108` | Reads `catalogs` heavily (filtered/by-code lists). Loads marker PNGs from `PiFinder/markers/`. Constructor mutates the passed `item_definition` (`select`/`items`). |
| `UIObjectDetails` | `object_details.py:56`, `:77` | Requires `item_definition["object"]` and `["object_list"]`. Opens an `ObservationsDatabase` (SQLite, `~/PiFinder_data/observations.db`). Reads `catalogs`, `shared_state`. |
| `UILog` | `log.py:30`, `:44` | Requires `item_definition["object"]`. Opens `ObservationsDatabase`. |
| `UITextEntry` | `textentry.py:104` | In catalog-search mode opens `ObjectsDatabase` (the bundled `pifinder_objects.db`) and calls `catalogs.search_by_t9` / `search_by_text`. |
| `UITextEntry` | `textentry.py:104` | In catalog-search mode opens `ObjectsDatabase` (the bundled `pifinder_objects.db`) and calls `catalogs.search_by_text`. |
| `UISQM` | `sqm.py:54` | `update()` calls `self.camera_image.copy()` — needs a real `camera_image` (a `PIL.Image`, **not** `None`). Reads `shared_state.sqm()`, sends camera commands. |
| `UIPreview` | `preview.py:213` | Also `self.camera_image.copy()` — needs a real camera image; reads `shared_state.last_image_metadata()`. |
| `UISoftware` | `software.py:55`–`:74` | Constructor `open()`s `version.txt` and `wifi_status.txt` from `utils.pifinder_dir` (must exist). Update path does `requests.get(...)` to GitHub — network. |
Expand Down
4 changes: 2 additions & 2 deletions docs/source/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ four options:
for observing projects and finding the nearest objects in a particular catalog.
- **Recent**: Starts empty and builds a history of the objects you've checked out during
the current session.
- **Name Search**: Using the number keypad and T9-style text entry, search for objects by
- **Name Search**: Using the number keypad and keypad text entry, search for objects by
name. The Snowball planetary? Cat's Eye? This is the way to find them.

However you build the list, it always displays the same information and offers the same
Expand Down Expand Up @@ -490,7 +490,7 @@ screen, select it from the Objects menu:

.. image:: images/user_guide/name_search_01.png

It uses T9-style text input, like the cellphones from the dawn of text messaging. The
It uses keypad text input, like the cellphones from the dawn of text messaging. The
on-screen keypad shows the letters available by pressing each number key several times in a
row.

Expand Down
86 changes: 0 additions & 86 deletions python/PiFinder/catalogs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# mypy: ignore-errors
import logging
import re
import time
import datetime
import pytz
Expand Down Expand Up @@ -28,31 +27,6 @@

logger = logging.getLogger("Catalog")

# Mapping from keypad numbers to characters (non-conventional layout)
KEYPAD_DIGIT_TO_CHARS = {
"7": "abc",
"8": "def",
"9": "ghi",
"4": "jkl",
"5": "mno",
"6": "pqrs",
"1": "tuv",
"2": "wxyz",
"3": "'-+/",
}

LETTER_TO_DIGIT_MAP: dict[str, str] = {}
for _digit, _chars in KEYPAD_DIGIT_TO_CHARS.items():
# Map the digit to itself so numbers in names still match
LETTER_TO_DIGIT_MAP[_digit] = _digit
for _char in _chars:
LETTER_TO_DIGIT_MAP[_char] = _digit
LETTER_TO_DIGIT_MAP[_char.upper()] = _digit

translator = str.maketrans(LETTER_TO_DIGIT_MAP)
VALID_T9_DIGITS = "".join(KEYPAD_DIGIT_TO_CHARS.keys())
INVALID_T9_DIGITS_RE = re.compile(f"[^{VALID_T9_DIGITS}]")

# collection of all catalog-related classes

# CatalogBase : just the CompositeObjects (imported from catalog_base)
Expand Down Expand Up @@ -366,8 +340,6 @@ class Catalogs:
def __init__(self, catalogs: List[Catalog]):
self.__catalogs: List[Catalog] = catalogs
self.catalog_filter: Union[CatalogFilter, None] = None
self._t9_cache: dict[tuple[str, int], list[str]] = {}
self._t9_cache_dirty = True

def filter_catalogs(self):
"""
Expand Down Expand Up @@ -421,61 +393,6 @@ def get_object(self, catalog_code: str, sequence: int) -> Optional[CompositeObje
if catalog:
return catalog.get_object_by_sequence(sequence)

# this is memory efficient and doesn't hit the sdcard, but could be faster
# also, it could be cached
def _name_to_t9_digits(self, name: str) -> str:
translated_name = name.translate(translator)
return INVALID_T9_DIGITS_RE.sub("", translated_name)

def _object_cache_key(self, obj: CompositeObject) -> tuple[str, int]:
return (obj.catalog_code, obj.sequence)

def _invalidate_t9_cache(self) -> None:
self._t9_cache_dirty = True

def _rebuild_t9_cache(self, objs: list[CompositeObject]) -> None:
self._t9_cache = {}
for obj in objs:
self._t9_cache[self._object_cache_key(obj)] = [
self._name_to_t9_digits(name) for name in obj.names
]
self._t9_cache_dirty = False

def _ensure_t9_cache(self, objs: list[CompositeObject]) -> None:
current_keys = {self._object_cache_key(obj) for obj in objs}
if self._t9_cache_dirty or current_keys != set(self._t9_cache.keys()):
self._rebuild_t9_cache(objs)

def search_by_t9(self, search_digits: str) -> List[CompositeObject]:
"""Search catalog objects using keypad digits.

Uses the existing keypad letter mapping (including its non-conventional
layout) to convert object names to their digit representation and
returns all objects whose digit string contains the search pattern.
"""

objs = self.get_objects(only_selected=False, filtered=False)
result: list[CompositeObject] = []
if not search_digits:
return result

self._ensure_t9_cache(objs)

for obj in objs:
for digits in self._t9_cache.get(self._object_cache_key(obj), []):
if len(digits) < len(search_digits):
continue
if search_digits in digits:
result.append(obj)
logger.debug(
"Found %s in %s %i via T9",
digits,
obj.catalog_code,
obj.sequence,
)
break
return result

def search_by_text(self, search_text: str) -> List[CompositeObject]:
objs = self.get_objects(only_selected=False, filtered=False)
result = []
Expand All @@ -495,14 +412,12 @@ def search_by_text(self, search_text: str) -> List[CompositeObject]:
def set(self, catalogs: List[Catalog]):
self.__catalogs = catalogs
self.select_all_catalogs()
self._invalidate_t9_cache()

def add(self, catalog: Catalog, select: bool = False):
if catalog.catalog_code not in [x.catalog_code for x in self.__catalogs]:
if select:
self.catalog_filter.selected_catalogs.add(catalog.catalog_code)
self.__catalogs.append(catalog)
self._invalidate_t9_cache()
else:
logger.warning(
"Catalog %s already exists, not replaced (in Catalogs.add)",
Expand All @@ -513,7 +428,6 @@ def remove(self, catalog_code: str):
for catalog in self.__catalogs:
if catalog.catalog_code == catalog_code:
self.__catalogs.remove(catalog)
self._invalidate_t9_cache()
return

logger.warning("Catalog %s does not exist, cannot remove", catalog_code)
Expand Down
16 changes: 0 additions & 16 deletions python/PiFinder/ui/menu_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,22 +702,6 @@ def _(key: str) -> Any:
},
],
},
{
"name": _("T9 Search"),
"class": UITextMenu,
"select": "single",
"config_option": "t9_search",
"items": [
{
"name": _("Off"),
"value": False,
},
{
"name": _("On"),
"value": True,
},
],
},
{
"name": _("Az Arrows"),
"class": UITextMenu,
Expand Down
20 changes: 2 additions & 18 deletions python/PiFinder/ui/textentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,6 @@ def __init__(self, *args, **kwargs) -> None:
self._results_updated = False # Flag to trigger UI refresh
self.SEARCH_DEBOUNCE_MS = 250 # milliseconds

@property
def t9_search_enabled(self) -> bool:
return bool(self.config_object.get_option("t9_search", False))

def draw_text_entry(self):
line_text_y = self.text_y + self.bold.height + 2
self.draw.line(
Expand Down Expand Up @@ -175,7 +171,7 @@ def draw_text_entry(self):
)

def draw_keypad(self):
# 3-column x 4-row T9 grid filling the width below the text line; key
# 3-column x 4-row keypad grid filling the width below the text line; key
# size derives from the screen so it scales (38x23 on the 128 panel).
start_x = self.text_x
start_y = self.text_y + self.bold.height
Expand Down Expand Up @@ -286,10 +282,7 @@ def _perform_search(self, search_text, search_version):
# Priority catalogs (NGC, IC, M) are loaded first, WDS loads in background
# So search will work immediately with those, WDS results appear when loading completes
logger.info(f"Starting search for '{search_text}'")
if self.t9_search_enabled:
results = self.catalogs.search_by_t9(search_text)
else:
results = self.catalogs.search_by_text(search_text)
results = self.catalogs.search_by_text(search_text)
logger.info(f"Search for '{search_text}' found {len(results)} results")

# Only update if this search is still current (not superseded by newer search)
Expand Down Expand Up @@ -353,15 +346,6 @@ def key_long_minus(self):
def key_number(self, number):
current_time = time.time()
number_key = str(number)
if not self.text_entry_mode and self.t9_search_enabled:
# In T9 mode we simply append the pressed digit
self.last_key_press_time = current_time
self.last_key = number
if number_key in self.keys:
self.char_index = 0
self.add_char(number_key)
return

# Check if the same key is pressed within a short time
if self.last_key == number and self.within_keypress_window(current_time):
self.char_index = (self.char_index + 1) % self.keys.get_nr_entries(
Expand Down
Loading
Loading