mirror of
https://gitlab.com/mobian1/eg25-manager.git
synced 2025-08-29 23:32:14 +02:00
`modem_reset()` could previously either fail silently, or fall back to using AT commands without indicating what happened. This commit adds informative messages and makes sure we fall back to resetting using AT commands whenever we encounter an error.
395 lines
11 KiB
C
395 lines
11 KiB
C
/*
|
|
* Copyright (C) 2020 Arnaud Ferraris <arnaud.ferraris@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
#include "at.h"
|
|
#include "config.h"
|
|
#include "gpio.h"
|
|
#include "manager.h"
|
|
|
|
#ifdef HAVE_MMGLIB
|
|
#include "mm-iface.h"
|
|
#endif
|
|
|
|
#include "ofono-iface.h"
|
|
#include "suspend.h"
|
|
#include "udev.h"
|
|
#include "gnss.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <glib-unix.h>
|
|
#include <libusb.h>
|
|
|
|
#ifndef EG25_CONFDIR
|
|
#define EG25_CONFDIR "/etc/eg25-manager"
|
|
#endif
|
|
|
|
#ifndef EG25_DATADIR
|
|
#define EG25_DATADIR "/usr/share/eg25-manager"
|
|
#endif
|
|
|
|
#ifndef EG25_VERSION
|
|
#define EG25_VERSION "0.0.0"
|
|
#endif
|
|
|
|
#define POWERON_DELAY_US 100000UL
|
|
|
|
static gboolean quit_app(struct EG25Manager *manager)
|
|
{
|
|
int i;
|
|
|
|
g_message("Request to quit...");
|
|
|
|
at_destroy(manager);
|
|
#ifdef HAVE_MMGLIB
|
|
mm_iface_destroy(manager);
|
|
#endif
|
|
ofono_iface_destroy(manager);
|
|
suspend_destroy(manager);
|
|
udev_destroy(manager);
|
|
|
|
if (manager->modem_state >= EG25_STATE_STARTED) {
|
|
g_message("Powering down the modem...");
|
|
gpio_sequence_shutdown(manager);
|
|
manager->modem_state = EG25_STATE_FINISHING;
|
|
for (i = 0; i < 30; i++) {
|
|
if (gpio_check_poweroff(manager, TRUE))
|
|
break;
|
|
sleep(1);
|
|
}
|
|
}
|
|
g_message("Modem down, quitting...");
|
|
|
|
g_main_loop_quit(manager->loop);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean modem_start(struct EG25Manager *manager)
|
|
{
|
|
ssize_t i, count;
|
|
gboolean should_boot = TRUE;
|
|
libusb_context *ctx = NULL;
|
|
libusb_device **devices = NULL;
|
|
struct libusb_device_descriptor desc;
|
|
|
|
if (manager->use_libusb) {
|
|
// BH don't have the STATUS line connected, so check if USB device is present
|
|
libusb_init(&ctx);
|
|
|
|
count = libusb_get_device_list(ctx, &devices);
|
|
for (i = 0; i < count; i++) {
|
|
libusb_get_device_descriptor(devices[i], &desc);
|
|
if (desc.idVendor == manager->usb_vid && desc.idProduct == manager->usb_pid) {
|
|
g_message("Found corresponding USB device, modem already powered");
|
|
should_boot = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
libusb_free_device_list(devices, 1);
|
|
libusb_exit(ctx);
|
|
} else if (!gpio_check_poweroff(manager, FALSE)) {
|
|
g_message("STATUS is low, modem already powered");
|
|
should_boot = FALSE;
|
|
}
|
|
|
|
if (should_boot) {
|
|
g_message("Starting modem...");
|
|
// Modem might crash on boot (especially with worn battery) if we don't delay here
|
|
if (manager->poweron_delay > 0)
|
|
g_usleep(manager->poweron_delay);
|
|
gpio_sequence_poweron(manager);
|
|
manager->modem_state = EG25_STATE_POWERED;
|
|
} else {
|
|
manager->modem_state = EG25_STATE_STARTED;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
#ifdef HAVE_MMGLIB
|
|
void modem_update_state(struct EG25Manager *manager, MMModemState state)
|
|
{
|
|
switch (state) {
|
|
case MM_MODEM_STATE_REGISTERED:
|
|
case MM_MODEM_STATE_DISCONNECTING:
|
|
case MM_MODEM_STATE_CONNECTING:
|
|
manager->modem_state = EG25_STATE_REGISTERED;
|
|
break;
|
|
case MM_MODEM_STATE_CONNECTED:
|
|
manager->modem_state = EG25_STATE_CONNECTED;
|
|
break;
|
|
default:
|
|
manager->modem_state = EG25_STATE_CONFIGURED;
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void modem_configure(struct EG25Manager *manager)
|
|
{
|
|
at_sequence_configure(manager);
|
|
}
|
|
|
|
static gboolean modem_reset_done(struct EG25Manager* manager)
|
|
{
|
|
manager->modem_state = EG25_STATE_RESUMING;
|
|
manager->reset_timer = 0;
|
|
return FALSE;
|
|
}
|
|
|
|
void modem_reset(struct EG25Manager *manager)
|
|
{
|
|
int fd, ret, len;
|
|
|
|
if (manager->reset_timer) {
|
|
g_message("modem_reset: timer already setup, skipping...");
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* If we are managing the modem through lets say ofono, we should not
|
|
* reset the modem based on the availability of USB ID
|
|
* TODO: Improve ofono plugin and add support for fetching USB ID
|
|
*/
|
|
if (manager->modem_iface != MODEM_IFACE_MODEMMANAGER) {
|
|
g_message("modem_reset: not using ModemManager, bail out!");
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
g_message("Trying to reset modem with USB ID '%s'", manager->modem_usb_id);
|
|
|
|
len = strlen(manager->modem_usb_id);
|
|
|
|
manager->modem_state = EG25_STATE_RESETTING;
|
|
|
|
fd = open("/sys/bus/usb/drivers/usb/unbind", O_WRONLY);
|
|
if (fd < 0) {
|
|
g_warning("Unable to open /sys/bus/usb/drivers/usb/unbind");
|
|
goto error;
|
|
}
|
|
ret = write(fd, manager->modem_usb_id, len);
|
|
if (ret < len) {
|
|
g_warning("Couldn't unbind modem: wrote %d/%d bytes", ret, len);
|
|
goto error;
|
|
}
|
|
close(fd);
|
|
|
|
fd = open("/sys/bus/usb/drivers/usb/bind", O_WRONLY);
|
|
if (fd < 0) {
|
|
g_warning("Unable to open /sys/bus/usb/drivers/usb/unbind");
|
|
goto error;
|
|
}
|
|
ret = write(fd, manager->modem_usb_id, len);
|
|
if (ret < len) {
|
|
g_warning("Couldn't bind modem: wrote %d/%d bytes", ret, len);
|
|
goto error;
|
|
}
|
|
close(fd);
|
|
|
|
g_message("Successfully reset modem's USB connection");
|
|
|
|
/*
|
|
* 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);
|
|
|
|
return;
|
|
|
|
error:
|
|
// Release blocking sleep inhibitor
|
|
if (manager->suspend_block_fd >= 0)
|
|
suspend_inhibit(manager, FALSE, TRUE);
|
|
if (manager->modem_boot_timer) {
|
|
g_source_remove(manager->modem_boot_timer);
|
|
manager->modem_boot_timer = 0;
|
|
}
|
|
|
|
// Everything else failed, reboot the modem
|
|
g_message("USB reset failed, falling back to AT command");
|
|
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);
|
|
}
|
|
|
|
void modem_suspend_pre(struct EG25Manager *manager)
|
|
{
|
|
at_sequence_suspend(manager);
|
|
}
|
|
|
|
void modem_suspend_post(struct EG25Manager *manager)
|
|
{
|
|
gpio_sequence_suspend(manager);
|
|
g_message("suspend sequence is over, drop inhibitor");
|
|
suspend_inhibit(manager, FALSE, FALSE);
|
|
}
|
|
|
|
void modem_resume_pre(struct EG25Manager *manager)
|
|
{
|
|
gpio_sequence_resume(manager);
|
|
}
|
|
|
|
void modem_resume_post(struct EG25Manager *manager)
|
|
{
|
|
at_sequence_resume(manager);
|
|
}
|
|
|
|
static toml_table_t *parse_config_file(char *config_file, gboolean force_default)
|
|
{
|
|
toml_table_t *toml_config;
|
|
gchar *compatible;
|
|
gchar error[256];
|
|
gsize len;
|
|
FILE *f = NULL;
|
|
|
|
if (config_file) {
|
|
f = fopen(config_file, "r");
|
|
} else if (g_file_get_contents("/proc/device-tree/compatible", &compatible, &len, NULL)) {
|
|
g_autoptr (GPtrArray) compat = g_ptr_array_new();
|
|
gsize pos = 0;
|
|
|
|
/*
|
|
* `compatible` file is a list of NULL-terminated strings, convert it
|
|
* to an array
|
|
*/
|
|
do {
|
|
g_ptr_array_add(compat, &compatible[pos]);
|
|
pos += strlen(&compatible[pos]) + 1;
|
|
} while (pos < len);
|
|
|
|
for (pos = 0; pos < compat->len; pos++) {
|
|
g_autofree gchar *filename = NULL;
|
|
if (force_default)
|
|
filename = g_strdup_printf(EG25_DATADIR "/%s.toml", (gchar *)g_ptr_array_index(compat, pos));
|
|
else
|
|
filename = g_strdup_printf(EG25_CONFDIR "/%s.toml", (gchar *)g_ptr_array_index(compat, pos));
|
|
|
|
if (access(filename, F_OK) == 0) {
|
|
g_message("Opening config file: %s", filename);
|
|
f = fopen(filename, "r");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!f) {
|
|
if (force_default)
|
|
g_error("unable to find a suitable config file!");
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
toml_config = toml_parse_file(f, error, sizeof(error));
|
|
if (!toml_config)
|
|
g_error("unable to parse config file: %s", error);
|
|
|
|
return toml_config;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
g_autoptr(GOptionContext) opt_context = NULL;
|
|
g_autoptr(GError) err = NULL;
|
|
struct EG25Manager manager;
|
|
gchar *config_file = NULL;
|
|
gboolean show_version = FALSE;
|
|
toml_table_t *toml_config[EG25_CONFIG_COUNT];
|
|
toml_table_t *manager_config[EG25_CONFIG_COUNT];
|
|
const GOptionEntry options[] = {
|
|
{ "config", 'c', 0, G_OPTION_ARG_STRING, &config_file, "Config file to use.", NULL },
|
|
{ "version", 'v', 0, G_OPTION_ARG_NONE, &show_version, "Display version information and exit.", NULL },
|
|
{ NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL }
|
|
};
|
|
|
|
memset(&manager, 0, sizeof(manager));
|
|
manager.at_fd = -1;
|
|
manager.poweron_delay = POWERON_DELAY_US;
|
|
manager.suspend_delay_fd = -1;
|
|
manager.suspend_block_fd = -1;
|
|
|
|
opt_context = g_option_context_new ("- Power management for the Quectel EG25 modem");
|
|
g_option_context_add_main_entries (opt_context, options, NULL);
|
|
if (!g_option_context_parse (opt_context, &argc, &argv, &err)) {
|
|
g_warning ("%s", err->message);
|
|
return 1;
|
|
}
|
|
|
|
if (show_version) {
|
|
printf("eg25-manager version %s\n", EG25_VERSION);
|
|
return 0;
|
|
}
|
|
|
|
manager.loop = g_main_loop_new(NULL, FALSE);
|
|
|
|
toml_config[EG25_CONFIG_SYS] = parse_config_file(NULL, TRUE);
|
|
toml_config[EG25_CONFIG_USER] = parse_config_file(config_file, FALSE);
|
|
|
|
/*
|
|
* We need at least one valid config file, and assuming it's
|
|
* EG25_CONFIG_SYS will make the rest easier to implement
|
|
*/
|
|
if (!toml_config[EG25_CONFIG_SYS] && toml_config[EG25_CONFIG_USER]) {
|
|
toml_config[EG25_CONFIG_SYS] = toml_config[EG25_CONFIG_USER];
|
|
toml_config[EG25_CONFIG_USER] = NULL;
|
|
}
|
|
|
|
if (!toml_config[EG25_CONFIG_SYS])
|
|
g_error("Unable to parse config file!");
|
|
|
|
for (int i = 0; i < EG25_CONFIG_COUNT; i++)
|
|
manager_config[i] = toml_config[i] ? toml_table_in(toml_config[i], "manager") : NULL;
|
|
|
|
if (!manager_config[EG25_CONFIG_SYS])
|
|
g_error("Default config file lacks the 'manager' section!");
|
|
|
|
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);
|
|
|
|
at_init(&manager, toml_config);
|
|
gpio_init(&manager, toml_config);
|
|
#ifdef HAVE_MMGLIB
|
|
mm_iface_init(&manager, toml_config);
|
|
#endif
|
|
ofono_iface_init(&manager, toml_config);
|
|
suspend_init(&manager, toml_config);
|
|
udev_init(&manager, toml_config);
|
|
gnss_init(&manager, toml_config);
|
|
|
|
for (int i = 0; i < EG25_CONFIG_COUNT; i++) {
|
|
if (toml_config[i])
|
|
toml_free(toml_config[i]);
|
|
}
|
|
|
|
g_idle_add(G_SOURCE_FUNC(modem_start), &manager);
|
|
|
|
g_unix_signal_add(SIGINT, G_SOURCE_FUNC(quit_app), &manager);
|
|
g_unix_signal_add(SIGTERM, G_SOURCE_FUNC(quit_app), &manager);
|
|
|
|
g_main_loop_run(manager.loop);
|
|
|
|
gpio_destroy(&manager);
|
|
|
|
return 0;
|
|
}
|