eg25-control: support power-on/off via GPIO control instead of modem-power

This commit is contained in:
2024-09-20 08:06:58 +00:00
parent c81a6f51e2
commit f1d05af377

View File

@@ -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
# - <https://lupyuen.github.io/articles/lte>
# - 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()