diff --git a/pkgs/additional/eg25-control/eg25-control b/pkgs/additional/eg25-control/eg25-control index 461693a24..b87958281 100755 --- a/pkgs/additional/eg25-control/eg25-control +++ b/pkgs/additional/eg25-control/eg25-control @@ -1,5 +1,6 @@ #!/usr/bin/env nix-shell -#!nix-shell -i python3 -p curl -p modemmanager-split.mmcli -p python3 +#!nix-shell -i python3 -p curl -p modemmanager-split.mmcli -p python3 -p python3.pkgs.libgpiod +# vim: set filetype=python : # this script should run after ModemManager.service is started. # typical invocation is `eg25-control --power-on --enable-gps`. @@ -56,10 +57,14 @@ # - Galileo (EU) # - BeiDou (CN) # ^ these are all global systems, usable outside the country that owns them +# +# eg25 modem/power docs +# [EG25-HW]: https://wiki.pine64.org/images/2/20/Quectel_EG25-G_Hardware_Design_V1.4.pdf import argparse import datetime +import gpiod import logging import os import subprocess @@ -289,6 +294,8 @@ class Phy: raise NotImplementedError() def power_off(self) -> None: raise NotImplementedError() + def dump_debug_info(self) -> None: + pass class MegiPhy(Phy): """ @@ -304,6 +311,96 @@ class MegiPhy(Phy): def power_off(self) -> None: self.executor.write_file(self.power_endpoint, b'0') +class GpioPhy: + # power-on/off sequence documented in a few places: + # - EG25-HW 3.7 + # - + # - modem-power.c in megi's kernel tree + # - eg25-manager source code + # + # PLEASE NOTE THAT THIS REQUIRES THE AXP REGULATOR TO BE POWERING VBAT-BB. + # as of 2024-09-19, that's not mainline; it still requires a handful of patches. + # see postmarketOS' or mobian's kernel to cherry-pick the necessary patches. + + # GPIO indices ("lines") + # DTR = "Data Terminal Ready", controls sleep/wakeup. high means modem is allowed to sleep when requested. high -> low forces the modem awake. internal pull-up. + # WAKEUP = 6 + DTR = 34 # PB2 + PWRKEY = 35 # PB3 + RESET = 68 # PC4 + APREADY = 231 # PH7, a.k.a. "host ready" + DISABLE = 232 # PH8, a.k.a. "enable"; active-low + # STATUS line: HIGH means powered off + STATUS = 233 # PH9 + def __init__(self): + self.lines = gpiod.request_lines( + "/dev/gpiochip1", + consumer="eg25-control", + config={ + GpioPhy.DTR: gpiod.LineSettings(direction=gpiod.line.Direction.OUTPUT), + GpioPhy.PWRKEY: gpiod.LineSettings(direction=gpiod.line.Direction.OUTPUT), + GpioPhy.RESET: gpiod.LineSettings(direction=gpiod.line.Direction.OUTPUT), + GpioPhy.APREADY: gpiod.LineSettings(direction=gpiod.line.Direction.OUTPUT), + GpioPhy.DISABLE: gpiod.LineSettings(direction=gpiod.line.Direction.OUTPUT), + GpioPhy.STATUS: gpiod.LineSettings(direction=gpiod.line.Direction.INPUT, bias=gpiod.line.Bias.PULL_UP), + }, + ) + + def power_toggle(self, disable = gpiod.line.Value.INACTIVE) -> None: + # power-on is signalled by toggling the PWRKEY, and the modem interprets that as either a power-up OR a power-down request. + self.dump_debug_info() + + if self.lines.get_value(self.STATUS) == disable: + # i wouldn't be surprised if the modem can get "stuck" in some state such that this early return always hits, + # if that's the case add some `--force` flag or verify against `mmcli -m any` report, etc. + logger.info("modem appears physically to already be in the desired state: not changing") + return + + self.lines.set_value(self.APREADY, gpiod.line.Value.ACTIVE) + self.lines.set_value(self.DISABLE, disable) + self.lines.set_value(self.RESET, gpiod.line.Value.INACTIVE) + self.lines.set_value(self.PWRKEY, gpiod.line.Value.INACTIVE) + self.lines.set_value(self.DTR, gpiod.line.Value.INACTIVE) + + # Megi's modem-power sleeps 50ms, cites datasheet claim to 30ms power-on + time.sleep(0.050) + + self.dump_debug_info() + + self.lines.set_value(self.PWRKEY, gpiod.line.Value.ACTIVE) + # Megi's modem-power sleeps 200ms; eg25-manager sleeps 1.0s; EG25-HW 3.7 says 500ms+ for power-on, 650ms+ for power-off + time.sleep(1.0) + self.lines.set_value(self.PWRKEY, gpiod.line.Value.INACTIVE) + + # TODO: switch 'status' key to input (megi's modem-power claims it can be multiplexed with other stuff, so shouldn't be actively driven when possible) + for i in range(10): + self.dump_debug_info() + if self.lines.get_value(self.STATUS) == disable: + break + else: + logger.info("modem hasn't pulled STATUS low: sleeping for 1s") + time.sleep(1.0) + else: + logger.info("modem didn't pull STATUS low after 10s: giving up and continuing") + + def power_on(self) -> None: + self.power_toggle() + + def power_off(self) -> None: + self.power_toggle(disable=gpiod.line.Value.ACTIVE) + + def dump_debug_info(self) -> None: + vals = self.lines.get_values() + dtr, pwrkey, reset, apready, disable, status = vals + logger.debug( + "gpio states:\n" + f" DTR: {dtr}\n" + f" PWRKEY: {pwrkey}\n" + f" RESET: {reset}\n" + f" APREADY: {apready}\n" + f" DISABLE: {disable}\n" + f" STATUS: {status}" + ) class Sequencer: def __init__(self, executor: Executor, modem: str, modem_phy: Phy, state_fs: Filesystem): @@ -464,6 +561,7 @@ class Sequencer: assert 'EG25GGBR07A08M2G' in hw or self.executor.dry_run, hw def dump_debug_info(self) -> None: + self.modem_phy.dump_debug_info() logger.debug('checking if AGPS is enabled (1) or not (0)') self._at_structured_cmd('QGPSXTRA?') # see if the GPS assistance data is still within valid range @@ -581,8 +679,8 @@ def main(): logging.getLogger().setLevel(logging.INFO) parser = argparse.ArgumentParser(description="initialize the eg25 Pinephone modem for GPS tracking") - parser.add_argument('--modem', default='any', help='name of modem to configure (see mmcli --list-modems)') - parser.add_argument('--power-endpoint', default='/sys/class/modem-power/modem-power/device/powered', help='sysfs endpoint that can turn the modem on/off') + parser.add_argument('--modem', default='any', help="name of modem to configure (see mmcli --list-modems)") + parser.add_argument('--power-endpoint', default='', help="sysfs endpoint that can turn the modem on/off (if using megi's modem-power), e.g. /sys/class/modem-power/modem-power/device/powered") parser.add_argument('--state-dir', default='') parser.add_argument("--dry-run", action='store_true', help="print commands instead of executing them") @@ -608,7 +706,15 @@ def main(): executor = Executor(dry_run=args.dry_run) state_fs = Filesystem(executor, root=state_dir) - sequencer = Sequencer(executor, modem=args.modem, modem_phy=MegiPhy(executor, args.power_endpoint), state_fs=state_fs) + + modem_phy = Phy() + if args.power_endpoint: + modem_phy = MegiPhy(executor, args.power_endpoint) + elif args.power_on or args.power_off: + # don't initialize the Gpio PHY unless absolutely necessary, since it has to (re-)configure GPIOs just to show debug info + modem_phy = GpioPhy() + + sequencer = Sequencer(executor, modem=args.modem, modem_phy=modem_phy, state_fs=state_fs) if args.power_on: sequencer.power_on()