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
|
#!/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.
|
# this script should run after ModemManager.service is started.
|
||||||
# typical invocation is `eg25-control --power-on --enable-gps`.
|
# typical invocation is `eg25-control --power-on --enable-gps`.
|
||||||
@@ -56,10 +57,14 @@
|
|||||||
# - Galileo (EU)
|
# - Galileo (EU)
|
||||||
# - BeiDou (CN)
|
# - BeiDou (CN)
|
||||||
# ^ these are all global systems, usable outside the country that owns them
|
# ^ 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 argparse
|
||||||
import datetime
|
import datetime
|
||||||
|
import gpiod
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -289,6 +294,8 @@ class Phy:
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
def power_off(self) -> None:
|
def power_off(self) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
def dump_debug_info(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
class MegiPhy(Phy):
|
class MegiPhy(Phy):
|
||||||
"""
|
"""
|
||||||
@@ -304,6 +311,96 @@ class MegiPhy(Phy):
|
|||||||
def power_off(self) -> None:
|
def power_off(self) -> None:
|
||||||
self.executor.write_file(self.power_endpoint, b'0')
|
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:
|
class Sequencer:
|
||||||
def __init__(self, executor: Executor, modem: str, modem_phy: Phy, state_fs: Filesystem):
|
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
|
assert 'EG25GGBR07A08M2G' in hw or self.executor.dry_run, hw
|
||||||
|
|
||||||
def dump_debug_info(self) -> None:
|
def dump_debug_info(self) -> None:
|
||||||
|
self.modem_phy.dump_debug_info()
|
||||||
logger.debug('checking if AGPS is enabled (1) or not (0)')
|
logger.debug('checking if AGPS is enabled (1) or not (0)')
|
||||||
self._at_structured_cmd('QGPSXTRA?')
|
self._at_structured_cmd('QGPSXTRA?')
|
||||||
# see if the GPS assistance data is still within valid range
|
# see if the GPS assistance data is still within valid range
|
||||||
@@ -581,8 +679,8 @@ def main():
|
|||||||
logging.getLogger().setLevel(logging.INFO)
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="initialize the eg25 Pinephone modem for GPS tracking")
|
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('--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('--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('--state-dir', default='')
|
||||||
|
|
||||||
parser.add_argument("--dry-run", action='store_true', help="print commands instead of executing them")
|
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)
|
executor = Executor(dry_run=args.dry_run)
|
||||||
state_fs = Filesystem(executor, root=state_dir)
|
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:
|
if args.power_on:
|
||||||
sequencer.power_on()
|
sequencer.power_on()
|
||||||
|
Reference in New Issue
Block a user