eg25-control: support power-on/off via GPIO control instead of modem-power
This commit is contained in:
@@ -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()
|
||||
|
Reference in New Issue
Block a user