Skip to content
Draft
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
1 change: 1 addition & 0 deletions default_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 53 additions & 0 deletions python/PiFinder/board_config.py
Original file line number Diff line number Diff line change
@@ -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
93 changes: 67 additions & 26 deletions python/PiFinder/sys_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from sh import wpa_cli, unzip, passwd

import socket
from PiFinder import board_config
from PiFinder import utils
import logging

Expand Down Expand Up @@ -362,75 +363,115 @@ 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:
logger.error(f"SYS: Error checking/syncing GPSD config: {e}")
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:
Expand Down
9 changes: 6 additions & 3 deletions python/PiFinder/ui/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
50 changes: 50 additions & 0 deletions python/PiFinder/ui/menu_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
],
},
],
},
],
Expand Down
46 changes: 46 additions & 0 deletions python/tests/test_sys_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

try:
from PiFinder import board_config
from PiFinder import sys_utils

@pytest.mark.unit
Expand Down Expand Up @@ -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 = (
Expand Down
Loading