diff --git a/default_config.json b/default_config.json index 7684bff0f..d5d3db949 100644 --- a/default_config.json +++ b/default_config.json @@ -21,6 +21,7 @@ "chart_coord_sys": "horiz", "target_pixel": [256, 256], "gps_type": "ublox", + "gps_port": "auto", "gps_baud_rate": 9600, "filter.selected_catalogs": [ "NGC", diff --git a/python/PiFinder/board_config.py b/python/PiFinder/board_config.py new file mode 100644 index 000000000..6e4ad1aac --- /dev/null +++ b/python/PiFinder/board_config.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from pathlib import Path + + +MODEL_PATH = Path("/proc/device-tree/model") + + +@dataclass(frozen=True) +class BoardProfile: + name: str + gps_device: str + uart_overlay: str + + +PI5_CLASS = BoardProfile( + name="pi5_class", + gps_device="/dev/ttyAMA2", + uart_overlay="dtoverlay=uart2-pi5", +) +PI4 = BoardProfile( + name="pi4", + gps_device="/dev/ttyAMA3", + uart_overlay="dtoverlay=uart3", +) +LEGACY = BoardProfile( + name="legacy", + gps_device="/dev/ttyAMA1", + uart_overlay="dtoverlay=uart3", +) + + +def read_board_model(model_path: Path = MODEL_PATH) -> str: + try: + return model_path.read_bytes().decode(errors="ignore").strip("\x00") + except OSError: + return "" + + +def get_board_profile(model: str | None = None) -> BoardProfile: + model = read_board_model() if model is None else model + if "Raspberry Pi 5" in model or "Compute Module 5" in model: + return PI5_CLASS + if "Raspberry Pi 4" in model: + return PI4 + return LEGACY + + +def get_default_gpsd_device(model: str | None = None) -> str: + return get_board_profile(model).gps_device + + +def get_uart_overlay(model: str | None = None) -> str: + return get_board_profile(model).uart_overlay diff --git a/python/PiFinder/sys_utils.py b/python/PiFinder/sys_utils.py index a3094dc08..687e64efc 100644 --- a/python/PiFinder/sys_utils.py +++ b/python/PiFinder/sys_utils.py @@ -9,6 +9,7 @@ from sh import wpa_cli, unzip, passwd import socket +from PiFinder import board_config from PiFinder import utils import logging @@ -362,42 +363,71 @@ def switch_cam_imx462() -> None: sh.sudo("python", "-m", "PiFinder.switch_camera", "imx462") -def check_and_sync_gpsd_config(baud_rate: int) -> bool: +def get_default_gpsd_device() -> str: + return board_config.get_default_gpsd_device() + + +DEFAULT_GPSD_DEVICE = get_default_gpsd_device() + + +def resolve_gpsd_device(device: str | None) -> str: + if not device or device == "auto": + return get_default_gpsd_device() + return device + + +def _gpsd_options_line(baud_rate: int) -> str: + if baud_rate == 115200: + # NOTE: the space before -s in the next line is really needed + return 'GPSD_OPTIONS=" -s 115200"' + return 'GPSD_OPTIONS=""' + + +def _gpsd_devices_line(device: str) -> str: + return f'DEVICES="{device}"' + + +def check_and_sync_gpsd_config( + baud_rate: int, device: str = DEFAULT_GPSD_DEVICE +) -> bool: """ - Checks if GPSD configuration matches the desired baud rate, + Checks if GPSD configuration matches the desired serial device and baud rate, and updates it only if necessary. Args: baud_rate: The desired baud rate (9600 or 115200) + device: The serial device path to configure for gpsd Returns: True if configuration was updated, False if already correct """ - logger.info(f"SYS: Checking GPSD config for baud rate {baud_rate}") + device = resolve_gpsd_device(device) + logger.info(f"SYS: Checking GPSD config for device {device}, baud rate {baud_rate}") try: # Read current config with open("/etc/default/gpsd", "r") as f: content = f.read() - # Determine expected GPSD_OPTIONS - if baud_rate == 115200: - # NOTE: the space before -s in the next line is really needed - expected_options = 'GPSD_OPTIONS=" -s 115200"' - else: - expected_options = 'GPSD_OPTIONS=""' + expected_devices = _gpsd_devices_line(device) + expected_options = _gpsd_options_line(baud_rate) # Check if update is needed current_match = re.search(r"^GPSD_OPTIONS=.*$", content, re.MULTILINE) - if current_match: - current_options = current_match.group(0) - if current_options == expected_options: - logger.info("SYS: GPSD config already correct, no update needed") - return False + current_options = current_match.group(0) if current_match else "" + current_match = re.search(r"^DEVICES=.*$", content, re.MULTILINE) + current_devices = current_match.group(0) if current_match else "" + if current_options == expected_options and current_devices == expected_devices: + logger.info("SYS: GPSD config already correct, no update needed") + return False # Update is needed - logger.info(f"SYS: GPSD config mismatch, updating to {expected_options}") - update_gpsd_config(baud_rate) + logger.info( + "SYS: GPSD config mismatch, updating to %s, %s", + expected_devices, + expected_options, + ) + update_gpsd_config(baud_rate, device) return True except Exception as e: @@ -405,32 +435,43 @@ def check_and_sync_gpsd_config(baud_rate: int) -> bool: return False -def update_gpsd_config(baud_rate: int) -> None: +def update_gpsd_config(baud_rate: int, device: str = DEFAULT_GPSD_DEVICE) -> None: """ - Updates the GPSD configuration file with the specified baud rate + Updates the GPSD configuration file with the specified device and baud rate and restarts the GPSD service. Args: baud_rate: The baud rate to configure (9600 or 115200) + device: The serial device path to configure for gpsd """ - logger.info(f"SYS: Updating GPSD config with baud rate {baud_rate}") + device = resolve_gpsd_device(device) + logger.info(f"SYS: Updating GPSD config with device {device}, baud rate {baud_rate}") try: # Read the current config with open("/etc/default/gpsd", "r") as f: lines = f.readlines() - # Update GPSD_OPTIONS line + expected_devices = _gpsd_devices_line(device) + expected_options = _gpsd_options_line(baud_rate) + + # Update DEVICES and GPSD_OPTIONS lines updated_lines = [] + saw_devices = False + saw_options = False for line in lines: - if line.startswith("GPSD_OPTIONS="): - if baud_rate == 115200: - # NOTE: the space before -s in the next line is really needed - updated_lines.append('GPSD_OPTIONS=" -s 115200"\n') - else: - updated_lines.append('GPSD_OPTIONS=""\n') + if line.startswith("DEVICES="): + updated_lines.append(f"{expected_devices}\n") + saw_devices = True + elif line.startswith("GPSD_OPTIONS="): + updated_lines.append(f"{expected_options}\n") + saw_options = True else: updated_lines.append(line) + if not saw_devices: + updated_lines.append(f"{expected_devices}\n") + if not saw_options: + updated_lines.append(f"{expected_options}\n") # Write the updated config to a temporary file with open("/tmp/gpsd.conf", "w") as f: diff --git a/python/PiFinder/ui/callbacks.py b/python/PiFinder/ui/callbacks.py index dcdb35fb9..65734ee60 100644 --- a/python/PiFinder/ui/callbacks.py +++ b/python/PiFinder/ui/callbacks.py @@ -501,16 +501,19 @@ def telemetry_record_toggle(ui_module: UIModule) -> None: def update_gpsd_baud_rate(ui_module: UIModule) -> None: """ - Updates the GPSD configuration with the current baud rate setting. + Updates the GPSD configuration with the current serial port and baud rate. Always updates GPSD config regardless of current GPS type. """ baud_rate = ui_module.config_object.get_option("gps_baud_rate") + gps_port = ui_module.config_object.get_option( + "gps_port", sys_utils.DEFAULT_GPSD_DEVICE + ) ui_module.message(_("Checking GPS\nconfig..."), 2) - logger.info(f"Checking GPSD baud rate {baud_rate}") + logger.info("Checking GPSD port %s baud rate %s", gps_port, baud_rate) try: - if sys_utils.check_and_sync_gpsd_config(baud_rate): + if sys_utils.check_and_sync_gpsd_config(baud_rate, gps_port): ui_module.message(_("GPS config\nupdated"), 2) else: ui_module.message(_("GPS config\nOK"), 2) diff --git a/python/PiFinder/ui/menu_structure.py b/python/PiFinder/ui/menu_structure.py index 5a8b97f66..73658a69b 100644 --- a/python/PiFinder/ui/menu_structure.py +++ b/python/PiFinder/ui/menu_structure.py @@ -1147,6 +1147,56 @@ def _(key: str) -> Any: }, ], }, + { + "name": _("GPS Port"), + "class": UITextMenu, + "select": "single", + "config_option": "gps_port", + "label": "gps_port", + "post_callback": callbacks.update_gpsd_baud_rate, + "items": [ + { + "name": _("Auto"), + "value": "auto", + }, + { + "name": _("ttyAMA1"), + "value": "/dev/ttyAMA1", + }, + { + "name": _("ttyAMA2"), + "value": "/dev/ttyAMA2", + }, + { + "name": _("ttyAMA3"), + "value": "/dev/ttyAMA3", + }, + { + "name": _("serial0"), + "value": "/dev/serial0", + }, + { + "name": _("ttyAMA0"), + "value": "/dev/ttyAMA0", + }, + { + "name": _("ttyAMA10"), + "value": "/dev/ttyAMA10", + }, + { + "name": _("ttyS0"), + "value": "/dev/ttyS0", + }, + { + "name": _("ttyACM0"), + "value": "/dev/ttyACM0", + }, + { + "name": _("ttyUSB0"), + "value": "/dev/ttyUSB0", + }, + ], + }, ], }, ], diff --git a/python/tests/test_sys_utils.py b/python/tests/test_sys_utils.py index ce9b1f1b2..b8c37a9d0 100644 --- a/python/tests/test_sys_utils.py +++ b/python/tests/test_sys_utils.py @@ -1,6 +1,7 @@ import pytest try: + from PiFinder import board_config from PiFinder import sys_utils @pytest.mark.unit @@ -67,6 +68,51 @@ def test_wpa_supplicant_parsing(): result = sys_utils.Network._parse_wpa_supplicant(wpa_list) assert result[1]["psk"] == "1234@===!!!" + @pytest.mark.unit + @pytest.mark.parametrize( + ("model", "profile", "gps_device", "uart_overlay"), + [ + ( + "Raspberry Pi 5 Model B Rev 1.0", + "pi5_class", + "/dev/ttyAMA2", + "dtoverlay=uart2-pi5", + ), + ( + "Raspberry Pi Compute Module 5 Rev 1.0", + "pi5_class", + "/dev/ttyAMA2", + "dtoverlay=uart2-pi5", + ), + ( + "Raspberry Pi 4 Model B Rev 1.5", + "pi4", + "/dev/ttyAMA3", + "dtoverlay=uart3", + ), + ( + "Raspberry Pi 3 Model B Plus Rev 1.3", + "legacy", + "/dev/ttyAMA1", + "dtoverlay=uart3", + ), + ], + ) + def test_board_profile_by_model(model, profile, gps_device, uart_overlay): + board_profile = board_config.get_board_profile(model) + + assert board_profile.name == profile + assert board_profile.gps_device == gps_device + assert board_profile.uart_overlay == uart_overlay + + @pytest.mark.unit + def test_resolve_gpsd_device_uses_board_default(monkeypatch): + monkeypatch.setattr(sys_utils, "get_default_gpsd_device", lambda: "/dev/ttyAMA3") + + assert sys_utils.resolve_gpsd_device(None) == "/dev/ttyAMA3" + assert sys_utils.resolve_gpsd_device("auto") == "/dev/ttyAMA3" + assert sys_utils.resolve_gpsd_device("/dev/ttyACM0") == "/dev/ttyACM0" + @pytest.mark.unit def test_rewrite_hosts_standard_line(): contents = (