diff --git a/data/eg25-manager.service.in b/data/eg25-manager.service.in index fe050c1..7e1e6fc 100644 --- a/data/eg25-manager.service.in +++ b/data/eg25-manager.service.in @@ -4,7 +4,7 @@ Before=ModemManager.service [Service] Type=simple -ExecStart=@bindir@/eg25manager +ExecStart=@bindir@/eg25-manager Restart=on-failure ProtectControlGroups=true ProtectHome=true diff --git a/data/pine64,pinephone-1.0.toml b/data/pine64,pinephone-1.0.toml index 10dbc1e..52df87d 100644 --- a/data/pine64,pinephone-1.0.toml +++ b/data/pine64,pinephone-1.0.toml @@ -1,4 +1,5 @@ [manager] +monitor_udev = true need_libusb = true usb_vid = 0x2c7c usb_pid = 0x0125 diff --git a/data/pine64,pinephone-1.1.toml b/data/pine64,pinephone-1.1.toml index 9242a91..acd8751 100644 --- a/data/pine64,pinephone-1.1.toml +++ b/data/pine64,pinephone-1.1.toml @@ -1,4 +1,5 @@ [manager] +monitor_udev = true need_libusb = true usb_vid = 0x2c7c usb_pid = 0x0125 diff --git a/data/pine64,pinephone-1.2.toml b/data/pine64,pinephone-1.2.toml index 3754274..6f06084 100644 --- a/data/pine64,pinephone-1.2.toml +++ b/data/pine64,pinephone-1.2.toml @@ -1,4 +1,7 @@ [manager] +monitor_udev = true +usb_vid = 0x2c7c +usb_pid = 0x0125 # Delay between setting GPIO and PWRKEY sequence, set in microseconds poweron_delay = 100000 @@ -60,9 +63,9 @@ configure = [ { cmd = "QSCLK", value = "1" }, # GNSS configuration: # * Enable A-GPS data upload support (XTRA) -# * Disable On-Demand-Positioning (ODP) mode +# * Disable On-Demand-Positioning (ODP) mode # to avoid running the GNSS system in the background, even when not enabled. -# * Enable Dynamic Power Optimizations (DPO) mode to turn off GNSS RF radios +# * Enable Dynamic Power Optimizations (DPO) mode to turn off GNSS RF radios # when they are not in use. # * Only enable GPS and GLONASS, disable other GNSS systems. # A-GPS data upload doesn't work for Galileo anyway. diff --git a/data/pine64,pinephone-pro.toml b/data/pine64,pinephone-pro.toml index 492de96..6b1348f 100644 --- a/data/pine64,pinephone-pro.toml +++ b/data/pine64,pinephone-pro.toml @@ -1,4 +1,7 @@ [manager] +monitor_udev = false +usb_vid = 0x2c7c +usb_pid = 0x0125 # Delay between setting GPIO and PWRKEY sequence, set in microseconds poweron_delay = 100000 @@ -35,7 +38,7 @@ configure = [ # Print software version { cmd = "QGMR" }, # Configure audio - { cmd = "QDAI", expect = "3,0,0,4,0,0,1,1" }, + { cmd = "QDAI", expect = "3,0,0,4,0,1,1,1" }, # RI signaling using physical RI pin { cmd = "QCFG", subcmd = "risignaltype", expect = "\"physical\"" }, # Enable VoLTE support @@ -60,9 +63,9 @@ configure = [ { cmd = "QSCLK", value = "1" }, # GNSS configuration: # * Enable A-GPS data upload support (XTRA) -# * Disable On-Demand-Positioning (ODP) mode +# * Disable On-Demand-Positioning (ODP) mode # to avoid running the GNSS system in the background, even when not enabled. -# * Enable Dynamic Power Optimizations (DPO) mode to turn off GNSS RF radios +# * Enable Dynamic Power Optimizations (DPO) mode to turn off GNSS RF radios # when they are not in use. # * Only enable GPS and GLONASS, disable other GNSS systems. # A-GPS data upload doesn't work for Galileo anyway. diff --git a/doc/eg25-manager.5.scd b/doc/eg25-manager.5.scd new file mode 100644 index 0000000..3d08fde --- /dev/null +++ b/doc/eg25-manager.5.scd @@ -0,0 +1,106 @@ +eg25-manager(5) + +# NAME +eg25-manager configuration file format + +# SYNOPSIS +eg25-manager uses toml formatted files for configuration. + +Configurations are loaded from: + - *@eg25_confdir@/.toml*: User-provided overrides (optional) + - *@eg25_datadir@/.toml*: Default configuration (required) + +# SECTION: manager +General settings for eg25-manager. + +*poweron_delay* int (microseconds) + Delay between de-asserting RESET and starting the PWRKEY sequence. + +# SECTION: suspend +Settings for how to handle suspend on the system where eg25-manager is running. + +*boot_timeout* int (seconds) + Prevent the system from suspending for boot_timeout seconds to allow the + modem to fully boot. + + Default: 120 if unset or zero. + +*recovery_timeout* int (seconds) + Amount of time to wait for the modem to reappear after suspend. If the + timeout is reached the modem's USB connection will be reset. + + Default: 9 if unset or zero. + +# SECTION: at +AT commands to send when different events happen, and where to send them to. + +Each command has 4 possible elements: + - *cmd*: the AT command itself, which will be translated to "AT+`cmd`" + - *subcmd*: the subcommand in case a single AT command can be used to + change multiple parameters, such as QCFG + - *value*: the command's argument(s), usually used to set the value of a + specific parameter + - *expect*: the expected return value; the command is first executed + without any value in order to query the current state. This state is + then compared to the *expect* string; if they don't match, the command + is then executed with value *expect* in order to set the parameter to + the expected value +A command can have *expect* OR *value* configured, but it shouldn't have both + +*NOTE:* If a command sequence is configured in an override file, the default +commands won't be loaded from the system configuration. The default commands +should be copied into the override file when changing them. + +*uart* string + The serial port to use for sending AT commands to the modem. + +*configure* List of commands + AT commands to send to the modem when it is first started. + +*suspend* List of commands + AT commands to send to the modem before the system suspends. + +*resume* List of commands + AT commands to send to the modem after the system resumes from suspend. + +*reset* List of commands + AT commands to send to the modem if resetting the usb port fails. + +# SECTION: gnss +Settings for uploading AGPS assistance data to the modem. + +*enabled* boolean + Enable or disable uploading AGPS data to the modem + +*url* string + The directory on the server that contains the assistance files + + Example: https://xtrapath4.izatcloud.net + +*file* string + The name of the assistance file on the server. + + Example: xtra2.bin + +# SECTION: gpio +The *gpio* section defines the GPIO pins to use for different modem functions. +These settings should only be changed when porting eg25-manager to a new device; +for this reason they aren't documented here. + +# EXAMPLES +Print the firmware version every time the phone wakes from suspend: +``` +[at] +resume = [ + { cmd = "QGMR" }, +] +``` + +Disable uploading AGPS data to the modem: +``` +[gnss] +enabled = false +``` + +# SEE AlSO +*eg25-manager*(8) diff --git a/doc/eg25-manager.8.scd b/doc/eg25-manager.8.scd new file mode 100644 index 0000000..095c9e4 --- /dev/null +++ b/doc/eg25-manager.8.scd @@ -0,0 +1,37 @@ +eg25-manager(8) + +# NAME +eg25-manager - a daemon for managing the Quectel EG25 modem found on the +Pine64 PinePhone. + +# SYNOPSIS +*eg25-manager* [-v] [-c config_file] + +# OPTIONS +*-v* + Show the version number and quit. +*-c* + User configuration file, defaults to the device configuration file in + /etc/eg25-manager. + +# FILES +Configurations are loaded from: + - *@eg25_confdir@/.toml*: User-provided overrides (optional) + - *@eg25_datadir@/.toml*: Default configuration (required) + +eg25-manager will search these folders for files named after the value of the +compatible device-tree property (with the .toml file extension) and use the +first matching file in each directory. If no matching default configuration is +found, eg25-manager will exit with an error message. + +Values from the user-provided overrides will take priority over values stored in +the default configuration. Only changed values must be stored as user overrides, +so eg25-manager can fall back to the default configuration as often as possible. + +The file names eg25-manager will check can be listed using: +``` +xargs -0 printf '%s.toml\\n' < /proc/device-tree/compatible +``` + +# SEE ALSO +*eg25-manager*(5) *ModemManager*(8) *ofono*(8) diff --git a/doc/meson.build b/doc/meson.build new file mode 100644 index 0000000..a86005a --- /dev/null +++ b/doc/meson.build @@ -0,0 +1,33 @@ +# +# Copyright (C) 2020 Arnaud Ferraris +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +scdoc = dependency('scdoc', native: true, required: false) +if scdoc.found() + scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) + + foreach section: [5, 8] + name = 'eg25-manager' + out = '@0@.@1@'.format(name, section) + + preprocessed = configure_file( + input: '@0@.scd'.format(out), + output: '@BASENAME@.preprocessed', + configuration: { + 'eg25_confdir': eg25_confdir, + 'eg25_datadir': eg25_datadir, + } + ) + + custom_target( + out, + output: out, + input: preprocessed, + command: ['sh', '-c', '@0@ < @INPUT@'.format(scdoc_prog.full_path())], + capture: true, + install: true, + install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section))) + endforeach +endif diff --git a/meson.build b/meson.build index 535927a..85d11ab 100644 --- a/meson.build +++ b/meson.build @@ -8,9 +8,9 @@ project ( 'eg25-manager', 'c', - version : '0.4.2', + version : '0.4.3', license : 'GPLv3+', - meson_version : '>= 0.50.0', + meson_version : '>= 0.58.0', default_options : [ 'warning_level=1', @@ -66,5 +66,6 @@ mgr_deps = [ ] subdir('data') +subdir('doc') subdir('src') subdir('udev') diff --git a/src/gpio.c b/src/gpio.c index 49bee0d..ad74f34 100644 --- a/src/gpio.c +++ b/src/gpio.c @@ -144,7 +144,7 @@ int gpio_init(struct EG25Manager *manager, toml_table_t *config[]) if (!gpio_config[EG25_CONFIG_SYS]) g_error("Default config file lacks the 'gpio' section!"); - /* + /* * The system config could have the `chips` key, but the user one * could still use the old format! In order to avoid problems, we * should use the new format only if: @@ -196,8 +196,13 @@ int gpio_init(struct EG25Manager *manager, toml_table_t *config[]) for (i = 0; i < GPIO_IN_COUNT; i++) { toml_table_t *table; toml_datum_t chip, line; - if (!config_get_table(gpio_config, gpio_in_names[i], &table)) + + if (!config_get_table(gpio_config, gpio_in_names[i], &table)) { + // BH edition don't have the STATUS line connected, ignore it + if (manager->use_libusb && g_strcmp0(gpio_in_names[i], "status") == 0) + continue; g_error("Unable to get config for input GPIO '%s'", gpio_in_names[i]); + } chip = toml_int_in(table, "chip"); if (!chip.ok || chip.u.i < 0 || chip.u.i > 2) diff --git a/src/manager.c b/src/manager.c index cba4e72..47cbc11 100644 --- a/src/manager.c +++ b/src/manager.c @@ -38,6 +38,9 @@ #define EG25_VERSION "0.0.0" #endif +#define EG25_DEFAULT_VENDOR_ID 0x2c7c +#define EG25_DEFAULT_PRODUCT_ID 0x0125 + #define POWERON_DELAY_US 100000UL static gboolean quit_app(struct EG25Manager *manager) @@ -141,7 +144,7 @@ void modem_configure(struct EG25Manager *manager) static gboolean modem_reset_done(struct EG25Manager* manager) { manager->modem_state = EG25_STATE_RESUMING; - manager->reset_timer = 0; + manager->complete_reset_timer = 0; return FALSE; } @@ -149,7 +152,18 @@ gboolean modem_reset(struct EG25Manager *manager) { int fd, ret, len; - if (manager->reset_timer) { + /* reset sequence started, cannot be canceled anymore */ + if (manager->schedule_reset_timer) { + g_source_remove(manager->schedule_reset_timer); + manager->schedule_reset_timer = 0; + } + + if (manager->modem_recovery_timer) { + g_source_remove(manager->modem_recovery_timer); + manager->modem_recovery_timer = 0; + } + + if (manager->complete_reset_timer) { g_message("modem_reset: timer already setup, skipping..."); return G_SOURCE_REMOVE; } @@ -164,11 +178,6 @@ gboolean modem_reset(struct EG25Manager *manager) return G_SOURCE_REMOVE; } - if (manager->modem_recovery_timer) { - g_source_remove(manager->modem_recovery_timer); - manager->modem_recovery_timer = 0; - } - if (!manager->modem_usb_id) { g_warning("Empty modem USB ID"); goto error; @@ -210,7 +219,7 @@ gboolean modem_reset(struct EG25Manager *manager) * 3s is long enough to make sure the modem has been bound back and * short enough to ensure it hasn't been acquired by ModemManager */ - manager->reset_timer = g_timeout_add_seconds(3, G_SOURCE_FUNC(modem_reset_done), manager); + manager->complete_reset_timer = g_timeout_add_seconds(3, G_SOURCE_FUNC(modem_reset_done), manager); return G_SOURCE_REMOVE; @@ -228,7 +237,7 @@ error: at_sequence_reset(manager); // Setup timer for making sure we don't queue other reset commands - manager->reset_timer = g_timeout_add_seconds(30, G_SOURCE_FUNC(modem_reset_done), manager); + manager->complete_reset_timer = g_timeout_add_seconds(30, G_SOURCE_FUNC(modem_reset_done), manager); return G_SOURCE_REMOVE; } @@ -314,6 +323,7 @@ int main(int argc, char *argv[]) struct EG25Manager manager; gchar *config_file = NULL; gboolean show_version = FALSE; + gboolean monitor_udev = TRUE; toml_table_t *toml_config[EG25_CONFIG_COUNT]; toml_table_t *manager_config[EG25_CONFIG_COUNT]; const GOptionEntry options[] = { @@ -363,10 +373,13 @@ int main(int argc, char *argv[]) if (!manager_config[EG25_CONFIG_SYS]) g_error("Default config file lacks the 'manager' section!"); + config_get_bool(manager_config, "monitor_udev", &monitor_udev); config_get_bool(manager_config, "need_libusb", &manager.use_libusb); - config_get_uint(manager_config, "usb_vid", &manager.usb_vid); - config_get_uint(manager_config, "usb_pid", &manager.usb_pid); config_get_uint(manager_config, "poweron_delay", &manager.poweron_delay); + if (!config_get_uint(manager_config, "usb_vid", &manager.usb_vid)) + manager.usb_vid = EG25_DEFAULT_VENDOR_ID; + if (!config_get_uint(manager_config, "usb_pid", &manager.usb_pid)) + manager.usb_pid = EG25_DEFAULT_PRODUCT_ID; at_init(&manager, toml_config); gpio_init(&manager, toml_config); @@ -375,7 +388,8 @@ int main(int argc, char *argv[]) #endif ofono_iface_init(&manager, toml_config); suspend_init(&manager, toml_config); - udev_init(&manager, toml_config); + if (monitor_udev) + udev_init(&manager, toml_config); gnss_init(&manager, toml_config); for (int i = 0; i < EG25_CONFIG_COUNT; i++) { diff --git a/src/manager.h b/src/manager.h index 2c36348..43d44bd 100644 --- a/src/manager.h +++ b/src/manager.h @@ -53,6 +53,7 @@ enum EG25State { EG25_STATE_SUSPENDING, // System is going into suspend EG25_STATE_RESUMING, // System is being resumed, waiting for modem to come back EG25_STATE_RESETTING, // Something went wrong, we're restarting the modem + EG25_STATE_UPDATING, // Modem is present but being updated EG25_STATE_FINISHING }; @@ -70,7 +71,8 @@ enum EG25Config { struct EG25Manager { GMainLoop *loop; - guint reset_timer; + guint complete_reset_timer; + guint schedule_reset_timer; gboolean use_libusb; guint usb_vid; guint usb_pid; diff --git a/src/meson.build b/src/meson.build index cc13d2f..6f9e767 100644 --- a/src/meson.build +++ b/src/meson.build @@ -25,7 +25,7 @@ if mmglib_dep.found() endif executable ( - 'eg25manager', + 'eg25-manager', src, dependencies : mgr_deps, link_with: gdbofono_lib, diff --git a/src/suspend.c b/src/suspend.c index 4bace47..9485598 100644 --- a/src/suspend.c +++ b/src/suspend.c @@ -18,10 +18,24 @@ #define SD_PATH "/org/freedesktop/login1" #define SD_INTERFACE "org.freedesktop.login1.Manager" +static void resume_ok(struct EG25Manager *manager) +{ + manager->modem_state = EG25_STATE_CONFIGURED; + modem_resume_post(manager); +} + static gboolean check_modem_resume(struct EG25Manager *manager) { - g_message("Modem wasn't probed in time, restart it!"); manager->modem_recovery_timer = 0; + +#ifdef HAVE_MMGLIB + if (manager->mm_modem) { + resume_ok(manager); + return FALSE; + } +#endif + + g_message("Modem wasn't probed in time, restart it!"); modem_reset(manager); return FALSE; @@ -184,8 +198,7 @@ static void signal_cb(GDBusProxy *proxy, * If modem is managed by ofono, we also do resume sequence immediately * as ofono handles resuming from sleep itself. */ - manager->modem_state = EG25_STATE_CONFIGURED; - modem_resume_post(manager); + resume_ok(manager); } else { manager->modem_state = EG25_STATE_RESUMING; manager->modem_recovery_timer = g_timeout_add_seconds(manager->modem_recovery_timeout, diff --git a/src/udev.c b/src/udev.c index 9790c15..cc452b1 100644 --- a/src/udev.c +++ b/src/udev.c @@ -11,18 +11,47 @@ static void udev_event_cb(GUdevClient *client, gchar *action, GUdevDevice *device, gpointer data) { struct EG25Manager *manager = data; + const gchar *prop; + long vid = 0, pid = 0; + + /* + * Act only if the device is the one identified as a modem by MM/ofono + */ + if (!manager->modem_usb_id || + strcmp(g_udev_device_get_name(device), manager->modem_usb_id) != 0) { + return; + } + + prop = g_udev_device_get_property(device, "ID_VENDOR_ID"); + if (prop) + vid = strtol(prop, NULL, 16); + + prop = g_udev_device_get_property(device, "ID_MODEL_ID"); + if (prop) + pid = strtol(prop, NULL, 16); + + if (strcmp(action, "bind") == 0 && vid != manager->usb_vid && pid != manager->usb_pid) { + /* + * Modem is probably executing a FW upgrade, make sure we don't interrupt it + */ + if (manager->schedule_reset_timer != 0) { + g_message("Modem re-appeared with different VID/PID, cancel reset."); + g_source_remove(manager->schedule_reset_timer); + manager->schedule_reset_timer = 0; + } + manager->modem_state = EG25_STATE_UPDATING; + } if (strcmp(action, "unbind") != 0 || + manager->modem_state == EG25_STATE_UPDATING || manager->modem_state == EG25_STATE_RESETTING || - !manager->modem_usb_id) { + manager->complete_reset_timer != 0 || + manager->schedule_reset_timer != 0) { return; - } - - if (strcmp(g_udev_device_get_name(device), manager->modem_usb_id) == 0 && - manager->reset_timer == 0) { - g_message("Lost modem, resetting..."); - g_timeout_add_seconds(2, G_SOURCE_FUNC(modem_reset), manager); } + + g_message("Lost modem, resetting..."); + manager->schedule_reset_timer = g_timeout_add_seconds(3, G_SOURCE_FUNC(modem_reset), manager); } void udev_init (struct EG25Manager *manager, toml_table_t *config[])