From f4136a4608202a9b2387e967ff8315c2f6d73364 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20Jirman?= <megi@xff.cz>
Date: Sun, 12 Nov 2017 02:10:15 +0100
Subject: [PATCH 172/389] misc: modem-power: Power manager for modems

This driver allows for powering/resetting devices that are otherwise
handled by other subsytems (USB). It also has mechanismsm for polling
from userspace on device->SoC wakeup events via GPIO.

This is mostly useful for controling modems. The supported modems are:

- Quectel EG25
- ZTE MG3732

Signed-off-by: Ondrej Jirman <megi@xff.cz>
---
 drivers/misc/Kconfig       |    7 +
 drivers/misc/Makefile      |    1 +
 drivers/misc/modem-power.c | 1992 ++++++++++++++++++++++++++++++++++++
 3 files changed, 2000 insertions(+)
 create mode 100644 drivers/misc/modem-power.c

diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 358ad56f6524..2c68a55a78fa 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -496,6 +496,13 @@ config VCPU_STALL_DETECTOR
 
 	  If you do not intend to run this kernel as a guest, say N.
 
+config MODEM_POWER
+	tristate "Modem power/wakeup support for EG25, MG3732, etc."
+	depends on OF && SERIAL_DEV_BUS && RFKILL
+	help
+	  This driver provides support for powering up and handling
+	  wakeup signals for various modems.
+
 source "drivers/misc/c2port/Kconfig"
 source "drivers/misc/eeprom/Kconfig"
 source "drivers/misc/cb710/Kconfig"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index ac9b3e757ba1..4ece4e588497 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -62,3 +62,4 @@ obj-$(CONFIG_HI6421V600_IRQ)	+= hi6421v600-irq.o
 obj-$(CONFIG_OPEN_DICE)		+= open-dice.o
 obj-$(CONFIG_GP_PCI1XXXX)	+= mchp_pci1xxxx/
 obj-$(CONFIG_VCPU_STALL_DETECTOR)	+= vcpu_stall_detector.o
+obj-$(CONFIG_MODEM_POWER)	+= modem-power.o
diff --git a/drivers/misc/modem-power.c b/drivers/misc/modem-power.c
new file mode 100644
index 000000000000..16c731df6e23
--- /dev/null
+++ b/drivers/misc/modem-power.c
@@ -0,0 +1,1992 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Modem power control driver.
+ *
+ * Ondrej Jirman <megi@xff.cz>
+ *
+ * How this works
+ * --------------
+ *
+ * The driver:
+ * - can be registered as a platform or serial device
+ * - will use gpios, regulator and (optionally) serial port to control the modem
+ * - exposes a character device to control the modem power and receive various
+ *   events
+ * - exposes sysfs interface to control modem power and wakeup
+ * - supports multiple modem types and instances
+ *
+ * Power up/power down:
+ * - may take a lot of time (eg. ~13-22s powerup, >22s powerdown)
+ * - happens on a private workqueue under a lock
+ * - may happen from shutdown hook
+ * - prevents suspend when powerup/powerdown is in progress
+ * - is serialized and there's no abort of in-progress operations
+ * - for specific power sequence see comments in the section for each
+ *   supported modem variant
+ * - the driver monitors the power status of the modem (optionally)
+ *   and tries to complete the powerdown initiated via AT command
+ * - the driver tries to detect when the modem is killswitched off
+ *   and updates the driver status to reflect that
+ *
+ * Suspend/resume:
+ * - suspend is blocked if powerup/down is in progress
+ * - modem can wakeup the host over gpio based IRQ (RI signal)
+ * - the driver will assert ap_ready after resume finishes
+ *
+ * Rfkill:
+ * - the driver implements a rfkill interface if rfkill gpio is available
+ */
+
+//#define DEBUG
+
+#include <linux/wait.h>
+#include <linux/interrupt.h>
+#include <linux/device.h>
+#include <linux/cdev.h>
+#include <linux/kfifo.h>
+#include <linux/module.h>
+#include <linux/poll.h>
+#include <linux/spinlock.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/platform_device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/regulator/consumer.h>
+#include <linux/delay.h>
+#include <linux/slab.h>
+#include <linux/serdev.h>
+#include <linux/rfkill.h>
+
+#define DRIVER_NAME "modem-power"
+
+enum {
+	MPWR_REQ_NONE = 0,
+	MPWR_REQ_RESET,
+	MPWR_REQ_PWDN,
+	MPWR_REQ_PWUP,
+};
+
+enum {
+	MPWR_MODE_NORMAL = 1,
+	MPWR_MODE_DUMB,
+	MPWR_MODE_FASTBOOT,
+	MPWR_MODE_ALT1,
+	MPWR_MODE_ALT2,
+};
+
+struct mpwr_dev;
+
+struct mpwr_gpio {
+	const char* name;
+	unsigned desc_off;
+	int flags;
+	bool required;
+	int irq_flags;
+	unsigned irq_off;
+};
+
+#define MPWR_GPIO_DEF(_name, _flags, _req) \
+	{ .name = #_name, \
+	  .desc_off = offsetof(struct mpwr_dev, _name##_gpio), \
+	  .flags = _flags, \
+	  .required = _req, \
+	}
+
+#define MPWR_GPIO_DEF_IRQ(_name, _flags, _req, _irq_flags) \
+	{ .name = #_name, \
+	  .desc_off = offsetof(struct mpwr_dev, _name##_gpio), \
+	  .flags = _flags, \
+	  .required = _req, \
+	  .irq_flags = _irq_flags, \
+	  .irq_off = offsetof(struct mpwr_dev, _name##_irq), \
+	}
+
+struct mpwr_variant {
+	int (*power_init)(struct mpwr_dev* mpwr);
+	int (*power_up)(struct mpwr_dev* mpwr);
+	int (*power_down)(struct mpwr_dev* mpwr);
+	int (*reset)(struct mpwr_dev* mpwr);
+	void (*recv_msg)(struct mpwr_dev *mpwr, const char *msg);
+	int (*suspend)(struct mpwr_dev *mpwr);
+	int (*resume)(struct mpwr_dev *mpwr);
+	const struct mpwr_gpio* gpios;
+	bool regulator_required;
+	bool monitor_wakeup;
+};
+
+struct mpwr_dev {
+	struct device *dev;
+	const struct mpwr_variant* variant;
+
+	wait_queue_head_t wait;
+
+	/* serdev */
+	struct serdev_device *serdev;
+	char rcvbuf[4096];
+	size_t rcvbuf_fill;
+	char msg[4096];
+        int msg_len;
+        int msg_ok;
+	//struct kfifo kfifo;
+	DECLARE_KFIFO(kfifo, unsigned char, 4096);
+
+	/* power */
+	struct regulator *regulator;
+	struct regulator *regulator_vbus;
+
+	/* outputs */
+	struct gpio_desc *enable_gpio;
+	struct gpio_desc *reset_gpio;
+	struct gpio_desc *pwrkey_gpio;
+	struct gpio_desc *sleep_gpio;
+	struct gpio_desc *dtr_gpio;
+	struct gpio_desc *host_ready_gpio;
+	struct gpio_desc *cts_gpio;
+	struct gpio_desc *rts_gpio;
+
+	/* inputs */
+	struct gpio_desc *status_gpio;
+	struct gpio_desc *wakeup_gpio;
+	int wakeup_irq;
+	bool status_pwrkey_multiplexed;
+
+	/* config */
+	struct cdev cdev;
+	dev_t major;
+
+	/* rfkill */
+	struct rfkill *rfkill;
+
+	/* powerup/dn work queue */
+	struct workqueue_struct *wq;
+	struct work_struct power_work;
+	struct work_struct finish_pdn_work;
+        struct mutex modem_lock;
+
+	// change
+	spinlock_t lock; /* protects last_request */
+	int last_request;
+	int powerup_mode;
+	ktime_t last_wakeup;
+
+	struct timer_list wd_timer;
+        struct delayed_work host_ready_work;
+
+	unsigned long flags[1];
+};
+
+enum {
+	/* modem is powered */
+	MPWR_F_POWERED,
+	MPWR_F_POWER_CHANGE_INPROGRESS,
+	MPWR_F_KILLSWITCHED,
+	/* we got a wakeup from the modem */
+	MPWR_F_GOT_WAKEUP,
+        /* serdev */
+        MPWR_F_RECEIVING_MSG,
+        /* eg25 */
+        MPWR_F_GOT_PDN,
+	/* config options */
+        MPWR_F_BLOCKED,
+	/* file */
+	MPWR_F_OPEN,
+	MPWR_F_OVERFLOW,
+};
+
+static struct class* mpwr_class;
+
+static int mpwr_serdev_at_cmd(struct mpwr_dev *mpwr, const char *msg, int timeout_ms);
+static int mpwr_serdev_at_cmd_with_retry(struct mpwr_dev *mpwr, const char *msg,
+					 int timeout_ms, int tries);
+static int mpwr_serdev_at_cmd_with_retry_ignore_timeout(struct mpwr_dev *mpwr, const char *msg,
+							int timeout_ms, int tries);
+
+// {{{ mg2723 variant
+
+static int mpwr_mg2723_power_init(struct mpwr_dev* mpwr)
+{
+	// if the device has power applied or doesn't have regulator
+	// configured (we assume it's always powered) initialize GPIO
+	// to shut it down initially
+	if (!mpwr->regulator || regulator_is_enabled(mpwr->regulator)) {
+		gpiod_set_value(mpwr->enable_gpio, 0);
+		gpiod_set_value(mpwr->reset_gpio, 1);
+	} else {
+		// device is not powered, don't drive the gpios
+		gpiod_direction_input(mpwr->enable_gpio);
+		gpiod_direction_input(mpwr->reset_gpio);
+	}
+
+	return 0;
+}
+
+static int mpwr_mg2723_power_up(struct mpwr_dev* mpwr)
+{
+	int ret;
+
+	// power up
+	if (mpwr->regulator) {
+		ret = regulator_enable(mpwr->regulator);
+		if (ret < 0) {
+			dev_err(mpwr->dev,
+				"can't enable power supply err=%d", ret);
+			return ret;
+		}
+	}
+
+	gpiod_direction_output(mpwr->enable_gpio, 1);
+	gpiod_direction_output(mpwr->reset_gpio, 1);
+	msleep(300);
+	gpiod_set_value(mpwr->reset_gpio, 0);
+
+	return 0;
+}
+
+static int mpwr_mg2723_power_down(struct mpwr_dev* mpwr)
+{
+	gpiod_set_value(mpwr->enable_gpio, 0);
+	msleep(50);
+
+	if (mpwr->regulator) {
+		regulator_disable(mpwr->regulator);
+
+		gpiod_direction_input(mpwr->enable_gpio);
+		gpiod_direction_input(mpwr->reset_gpio);
+	} else {
+		gpiod_set_value(mpwr->reset_gpio, 1);
+	}
+
+	return 0;
+}
+
+static int mpwr_mg2723_reset(struct mpwr_dev* mpwr)
+{
+	gpiod_set_value(mpwr->reset_gpio, 1);
+	msleep(300);
+	gpiod_set_value(mpwr->reset_gpio, 0);
+
+	return 0;
+}
+
+static const struct mpwr_gpio mpwr_mg2723_gpios[] = {
+	MPWR_GPIO_DEF(enable, GPIOD_IN, true),
+	MPWR_GPIO_DEF(reset, GPIOD_IN, true),
+	MPWR_GPIO_DEF_IRQ(wakeup, GPIOD_IN, true, IRQF_TRIGGER_FALLING),
+	{ },
+};
+
+static const struct mpwr_variant mpwr_mg2723_variant = {
+	.power_init = mpwr_mg2723_power_init,
+	.power_up = mpwr_mg2723_power_up,
+	.power_down = mpwr_mg2723_power_down,
+	.reset = mpwr_mg2723_reset,
+	.gpios = mpwr_mg2723_gpios,
+};
+
+// }}}
+// {{{ eg25 variant
+
+static bool mpwr_eg25_qcfg_airplanecontrol_is_ok(const char* v)
+{
+	return strstarts(v, "1,");
+}
+
+struct mpwr_eg25_qcfg {
+	const char* name;
+	const char* val;
+	bool (*is_ok)(const char* val);
+};
+
+#define EG25G_LATEST_KNOWN_FIRMWARE "EG25GGBR07A08M2G_01.002.07"
+
+static const struct mpwr_eg25_qcfg mpwr_eg25_qcfgs[] = {
+	//{ "risignaltype",       "\"respective\"", },
+	{ "risignaltype",       "\"physical\"", },
+	{ "urc/ri/ring",        "\"pulse\",1,1000,5000,\"off\",1", },
+	{ "urc/ri/smsincoming", "\"pulse\",1,1", },
+	{ "urc/ri/other",       "\"off\",1,1", },
+	{ "urc/ri/pin",         "uart_ri", },
+	{ "urc/delay",          "0", },
+
+	//{ "sleep/datactrl",     "0,300,1", },
+
+	{ "sleepind/level",     "0", },
+	{ "wakeupin/level",     "0", },
+
+	{ "ApRstLevel",		"0", },
+	{ "ModemRstLevel",	"0", },
+
+	// in EG25-G this tries to modify file in /etc (read-only)
+	// and fails
+	//{ "dbgctl",		"0", },
+
+	// we don't need AP_READY
+	{ "apready",            "0,0,500", },
+
+	{ "airplanecontrol",    "1",   mpwr_eg25_qcfg_airplanecontrol_is_ok },
+
+	// available since firmware R07A08_01.002.01.002
+	{ "fast/poweroff",	"1" },
+};
+
+static char* mpwr_serdev_get_response_value(struct mpwr_dev *mpwr,
+					    const char* prefix)
+{
+	int off;
+
+	for (off = 0; off < mpwr->msg_len; off += strlen(mpwr->msg + off) + 1)
+		if (strstarts(mpwr->msg + off, prefix))
+			return mpwr->msg + off + strlen(prefix);
+
+	return NULL;
+}
+
+static struct gpio_desc *mpwr_eg25_get_pwrkey_gpio(struct mpwr_dev *mpwr)
+{
+	if (mpwr->status_pwrkey_multiplexed)
+		return mpwr->status_gpio;
+
+	return mpwr->pwrkey_gpio;
+}
+
+/*
+ * Gpio meanings
+ * -------------
+ *
+ * enable_gpio - 1 = enables RF, 0 = disables RF
+ * sleep_gpio  - 1 = puts modem to sleep, 0 = wakes up the modem (must be 0
+ *               during poweron)
+ * reset_gpio  - accepts 150-460ms reset pulse (high __|^|__)
+ * pwrkey_gpio - accepts 100ms-650ms pulse for powerup (high __|^|__)
+ *                       650ms+ pulse for powerdown
+ *                       (initiated after pulse ends, pulse may have indefinite
+ *                        duration)
+ * status_gpio - modem power status 0 = powered  1 = unpowered
+ * wakeup_gpio - "ring indicator" output from the modem
+ * host_ready_gpio - AP_READY pin - host is ready to receive URCs
+ *
+ * (pwrkey may be multiplexed with status_gpio)
+ *
+ * Modem behavior
+ * --------------
+ *
+ * wakeup_gpio (RI):
+ * - goes high shortly after power is applied (~15ms)
+ * - goes low when RDY is sent
+ *
+ * dtr_gpio
+ * - when high, modem can sleep if requested
+ * - H->L will wake up a sleeping modem
+ * - internal pull-up
+ *
+ * ri
+ * - pulled low when there's URC
+ * - modem wakes up on URC automatically
+ *
+ * - AT+QURCCFG
+ * - AT+QINDCFG="csq",1
+ * - AT+QINDCFG="ring",1
+ * - AT+QINDCFG="smsincoming",1
+ * - AT+CGREG=0
+ * - AT+CREG=0
+ *
+ * - AT+QURCCFG="urcport","uart1"
+ */
+static int mpwr_eg25_power_up(struct mpwr_dev* mpwr)
+{
+	struct gpio_desc *pwrkey_gpio = mpwr_eg25_get_pwrkey_gpio(mpwr);
+	bool wakeup_ok, status_ok;
+	bool needs_restart = false;
+	u32 speed = 115200;
+	int ret, i, off;
+	ktime_t start;
+	int mode = mpwr->powerup_mode;
+
+	if (regulator_is_enabled(mpwr->regulator))
+		dev_warn(mpwr->dev,
+			 "regulator was already enabled during powerup");
+
+	/* Enable the modem power. */
+	ret = regulator_enable(mpwr->regulator);
+	if (ret < 0) {
+		dev_err(mpwr->dev,
+			"can't enable power supply err=%d", ret);
+		return ret;
+	}
+
+	ret = regulator_enable(mpwr->regulator_vbus);
+	if (ret < 0) {
+		dev_err(mpwr->dev,
+			"can't enable vbus power supply err=%d", ret);
+		regulator_disable(mpwr->regulator);
+		return ret;
+	}
+
+	/* Drive default gpio signals during powerup */
+	/* host_ready_gpio should be 1 during normal powerup */
+	gpiod_direction_output(mpwr->host_ready_gpio, mode != MPWR_MODE_ALT2);
+	/* #W_DISABLE must be left pulled up during modem power up
+	 * early on, because opensource bootloader uses this signal to enter
+	 * fastboot mode when it's pulled down.
+	 *
+	 * This should be 1 for normal powerup and 0 for fastboot mode with
+	 * special Biktor's firmware.
+	 */
+	gpiod_direction_output(mpwr->enable_gpio, mode != MPWR_MODE_FASTBOOT);
+	gpiod_direction_output(mpwr->sleep_gpio, 0);
+	gpiod_direction_output(mpwr->reset_gpio, 0);
+	gpiod_direction_output(pwrkey_gpio, 0);
+	/* dtr_gpio should be 0 during normal powerup */
+	gpiod_direction_output(mpwr->dtr_gpio, mode == MPWR_MODE_ALT1);
+
+	/* Wait for powerup. (30ms min. according to datasheet) */
+	msleep(50);
+
+	/* Send 200ms pwrkey pulse to initiate poweron */
+	gpiod_set_value(pwrkey_gpio, 1);
+	msleep(200);
+	gpiod_set_value(pwrkey_gpio, 0);
+
+	/* skip modem killswitch status checks in fastboot bootloader entry mode */
+	if (mode != MPWR_MODE_NORMAL)
+		goto open_serdev;
+
+	/* Switch status key to input, in case it's multiplexed with pwrkey. */
+	gpiod_direction_input(mpwr->status_gpio);
+
+	/*
+	 * Wait for status/wakeup change, assume good values, if CTS/status
+	 * signals, are not configured.
+	 */
+	status_ok = mpwr->status_gpio ? false : true;
+	wakeup_ok = mpwr->wakeup_gpio ? false : true;
+
+	/* wait up to 10s for status */
+	start = ktime_get();
+	while (ktime_ms_delta(ktime_get(), start) < 10000) {
+		if (!wakeup_ok && mpwr->wakeup_gpio && gpiod_get_value(mpwr->wakeup_gpio)) {
+			dev_info(mpwr->dev, "wakeup ok\n");
+			wakeup_ok = true;
+		}
+
+		if (!status_ok && mpwr->status_gpio && !gpiod_get_value(mpwr->status_gpio)) {
+			dev_info(mpwr->dev, "status ok\n");
+			status_ok = true;
+		}
+
+		/* modem is ready */
+		if (wakeup_ok && status_ok)
+			break;
+
+		msleep(50);
+	}
+
+	if (!wakeup_ok) {
+		dev_err(mpwr->dev, "The modem looks kill-switched\n");
+		if (!test_and_set_bit(MPWR_F_KILLSWITCHED, mpwr->flags))
+			sysfs_notify(&mpwr->dev->kobj, NULL, "killswitched");
+		goto err_shutdown_noclose;
+	}
+
+	if (!status_ok) {
+		dev_err(mpwr->dev, "The modem didn't report powerup success in time\n");
+		goto err_shutdown_noclose;
+	}
+
+	if (test_and_clear_bit(MPWR_F_KILLSWITCHED, mpwr->flags))
+		sysfs_notify(&mpwr->dev->kobj, NULL, "killswitched");
+
+open_serdev:
+	/* open serial console */
+	ret = serdev_device_open(mpwr->serdev);
+	if (ret) {
+		dev_err(mpwr->dev, "error opening serdev (%d)\n", ret);
+		goto err_shutdown_noclose;
+	}
+
+	of_property_read_u32(mpwr->dev->of_node, "current-speed", &speed);
+	serdev_device_set_baudrate(mpwr->serdev, speed);
+	serdev_device_set_flow_control(mpwr->serdev, false);
+	ret = serdev_device_set_parity(mpwr->serdev, SERDEV_PARITY_NONE);
+	if (ret) {
+		dev_err(mpwr->dev, "error setting serdev parity (%d)\n", ret);
+		goto err_shutdown;
+	}
+
+	if (mode != MPWR_MODE_NORMAL)
+		goto powered_up;
+
+	ret = mpwr_serdev_at_cmd_with_retry_ignore_timeout(mpwr, "AT&FE0", 1000, 30);
+	if (ret)
+		goto err_shutdown;
+
+	/* print firmware version */
+        ret = mpwr_serdev_at_cmd_with_retry(mpwr, "AT+QVERSION;+QSUBSYSVER", 1000, 15);
+        if (ret == 0 && mpwr->msg_len > 0) {
+		bool outdated = false;
+
+		dev_info(mpwr->dev, "===================================================\n");
+		for (off = 0; off < mpwr->msg_len; off += strlen(mpwr->msg + off) + 1) {
+			if (strstr(mpwr->msg + off, "Project Rev") && !strstr(mpwr->msg + off, EG25G_LATEST_KNOWN_FIRMWARE))
+				outdated = true;
+
+			dev_info(mpwr->dev, "%s\n", mpwr->msg + off);
+		}
+		dev_info(mpwr->dev, "===================================================\n");
+
+		if (outdated)
+			dev_warn(mpwr->dev, "Your modem has an outdated firmware. Latest know version is %s. Consider updating.\n", EG25G_LATEST_KNOWN_FIRMWARE);
+	}
+
+	/* print ADB key to dmesg */
+        ret = mpwr_serdev_at_cmd_with_retry(mpwr, "AT+QADBKEY?", 1000, 15);
+        if (ret == 0) {
+		const char *val = mpwr_serdev_get_response_value(mpwr, "+QADBKEY: ");
+		if (val)
+			dev_info(mpwr->dev, "ADB KEY is '%s' (you can use it to unlock ADB access to the modem, see https://xnux.eu/devices/feature/modem-pp.html)\n", val);
+	}
+
+        // check DAI config
+        ret = mpwr_serdev_at_cmd_with_retry(mpwr, "AT+QDAI?", 1000, 15);
+        if (ret == 0) {
+		const char *val = mpwr_serdev_get_response_value(mpwr, "+QDAI: ");
+		const char *needed_val = NULL;
+		char buf[128];
+
+		if (val) {
+			of_property_read_string(mpwr->dev->of_node, "quectel,qdai", &needed_val);
+
+			if (needed_val && strcmp(needed_val, val)) {
+				dev_warn(mpwr->dev, "QDAI is '%s' (changing to '%s')\n", val, needed_val);
+
+				/* update qdai */
+				snprintf(buf, sizeof buf, "AT+QDAI=%s", needed_val);
+				ret = mpwr_serdev_at_cmd(mpwr, buf, 5000);
+				if (ret == 0)
+					needs_restart = true;
+			} else {
+				dev_info(mpwr->dev, "QDAI is '%s'\n", val);
+			}
+		}
+	}
+
+	/* reset the modem, to apply QDAI config if necessary */
+	if (needs_restart) {
+		dev_info(mpwr->dev, "Restarting modem\n");
+
+		/* reboot is broken with fastboot enabled */
+		mpwr_serdev_at_cmd(mpwr, "AT+QCFG=\"fast/poweroff\",0", 5000);
+
+		ret = mpwr_serdev_at_cmd(mpwr, "AT+CFUN=1,1", 5000);
+		if (ret)
+			goto err_shutdown;
+
+		/* wait a bit before starting to probe the modem again */
+		msleep(6000);
+
+		ret = mpwr_serdev_at_cmd_with_retry_ignore_timeout(mpwr, "AT&FE0", 1000, 30);
+		if (ret)
+			goto err_shutdown;
+
+		// wait until QDAI starts succeeding (then the modem is ready
+		// to accept the following QCFGs)
+		ret = mpwr_serdev_at_cmd_with_retry(mpwr, "AT+QDAI?", 1000, 15);
+		if (ret)
+			goto err_shutdown;
+	}
+
+        /* check and update important QCFGs */
+        for (i = 0; i < ARRAY_SIZE(mpwr_eg25_qcfgs); i++) {
+                const char* name = mpwr_eg25_qcfgs[i].name;
+                const char* needed_val = mpwr_eg25_qcfgs[i].val;
+		bool (*is_ok)(const char* val) = mpwr_eg25_qcfgs[i].is_ok;
+		const char *val;
+                char buf[128];
+
+                snprintf(buf, sizeof buf, "AT+QCFG=\"%s\"", name);
+                ret = mpwr_serdev_at_cmd(mpwr, buf, 1000);
+                if (ret)
+			continue;
+
+                snprintf(buf, sizeof buf, "+QCFG: \"%s\",", name);
+		val = mpwr_serdev_get_response_value(mpwr, buf);
+		if (val) {
+			if (needed_val && (is_ok ? !is_ok(val) : strcmp(needed_val, val))) {
+				dev_info(mpwr->dev, "QCFG '%s' is '%s' (changing to '%s')\n", name, val, needed_val);
+
+				/* update qcfg */
+				snprintf(buf, sizeof buf, "AT+QCFG=\"%s\",%s", name, needed_val);
+				ret = mpwr_serdev_at_cmd(mpwr, buf, 1000);
+				if (ret)
+					break; /* go to next QCFG */
+			} else {
+				dev_info(mpwr->dev, "QCFG '%s' is '%s'\n", name, val);
+			}
+		}
+        }
+
+	/* setup URC port */
+	ret = mpwr_serdev_at_cmd(mpwr, "AT+QURCCFG=\"urcport\",\"all\"", 2000);
+        if (ret) {
+		dev_info(mpwr->dev, "Your modem doesn't support AT+QURCCFG=\"urcport\",\"all\", consider upgrading the firmware.\n");
+
+		ret = mpwr_serdev_at_cmd(mpwr, "AT+QURCCFG=\"urcport\",\"usbat\"", 2000);
+		if (ret)
+			dev_err(mpwr->dev, "Modem may not report URCs to the right port!\n");
+	}
+
+	/* enable the modem to go to sleep when DTR is low */
+	ret = mpwr_serdev_at_cmd(mpwr, "AT+QSCLK=1", 2000);
+        if (ret)
+		dev_err(mpwr->dev, "Modem will probably not sleep!\n");
+
+powered_up:
+	// if we're signaling some alternate boot mode via GPIO, we need to
+	// sleep here so that modem's boot script notices the gpio
+	if (mode == MPWR_MODE_ALT1 || mode == MPWR_MODE_FASTBOOT || mode == MPWR_MODE_ALT2)
+		msleep(12000);
+
+	gpiod_direction_output(mpwr->dtr_gpio, 1);
+
+	return 0;
+
+err_shutdown:
+	serdev_device_close(mpwr->serdev);
+err_shutdown_noclose:
+	dev_warn(mpwr->dev,
+		 "Forcibly cutting off power, data loss may occur.\n");
+	gpiod_direction_input(mpwr->enable_gpio);
+	gpiod_direction_input(mpwr->reset_gpio);
+	gpiod_direction_input(mpwr->sleep_gpio);
+	gpiod_direction_input(pwrkey_gpio);
+	gpiod_direction_input(mpwr->host_ready_gpio);
+	gpiod_direction_input(mpwr->dtr_gpio);
+
+	regulator_disable(mpwr->regulator_vbus);
+	regulator_disable(mpwr->regulator);
+	return -ENODEV;
+}
+
+static int mpwr_eg25_power_down_finish(struct mpwr_dev* mpwr)
+{
+	struct gpio_desc *pwrkey_gpio = mpwr_eg25_get_pwrkey_gpio(mpwr);
+	ktime_t start = ktime_get();
+	int ret;
+
+	serdev_device_close(mpwr->serdev);
+
+	/*
+	 * This function is called right after POWERED DOWN message is received.
+	 *
+	 * In case of fast/poweroff == 1, no POWERED DOWN message is sent.
+	 * Fast power off times are around 1s since the end of 800ms
+	 * POK pulse.
+	 *
+	 * When the modem powers down RI (wakeup) goes low and STATUS goes
+	 * high at the same time. Status is not connected on some boards.
+	 * RI should be inactive during poweroff, but we don't know for sure.
+	 *
+	 * Therfore:
+	 * - wait for STATUS going low
+	 * - in case that's not available wait for RI going low
+	 * - in case timings seem off, warn the user
+	 *
+	 * In addition, some boards have PWRKEY multiplexed with STATUS signal.
+	 * In that case we need to switch STATUS to output high level, as soon
+	 * as it goes low in order to prevent a power-up signal being registered
+	 * by the modem.
+	 */
+
+	if (mpwr->status_gpio) {
+		/* wait up to 30s for status going high */
+		while (ktime_ms_delta(ktime_get(), start) < 30000) {
+			if (gpiod_get_value(mpwr->status_gpio)) {
+				if (ktime_ms_delta(ktime_get(), start) < 500)
+					dev_warn(mpwr->dev,
+						 "STATUS signal is high too soon during powerdown. Modem is already off?\n");
+				goto powerdown;
+			}
+
+			msleep(20);
+		}
+
+		dev_warn(mpwr->dev,
+			 "STATUS signal didn't go high during shutdown. Modem is still on?\n");
+		goto force_powerdown;
+	} else {
+		clear_bit(MPWR_F_GOT_WAKEUP, mpwr->flags);
+
+		if (!gpiod_get_value(mpwr->wakeup_gpio)) {
+			dev_warn(mpwr->dev,
+				 "RI signal is low too soon during powerdown. Modem is already off, or spurious wakeup?\n");
+			msleep(2000);
+			goto powerdown;
+		}
+
+		ret = wait_event_timeout(mpwr->wait,
+					 test_bit(MPWR_F_GOT_WAKEUP, mpwr->flags),
+					 msecs_to_jiffies(30000));
+		if (ret <= 0) {
+			dev_warn(mpwr->dev,
+				 "RI signal didn't go low during shutdown, is modem really powering down?\n");
+			goto force_powerdown;
+		}
+
+		if (ktime_ms_delta(ktime_get(), start) < 500) {
+			dev_warn(mpwr->dev,
+				 "RI signal is low too soon during powerdown. Modem is already off, or spurious wakeup?\n");
+			msleep(2000);
+			goto powerdown;
+		}
+	}
+
+powerdown:
+	gpiod_direction_input(mpwr->enable_gpio);
+	gpiod_direction_input(mpwr->reset_gpio);
+	gpiod_direction_input(mpwr->sleep_gpio);
+	gpiod_direction_input(pwrkey_gpio);
+	gpiod_direction_input(mpwr->host_ready_gpio);
+	gpiod_direction_input(mpwr->dtr_gpio);
+
+	regulator_disable(mpwr->regulator_vbus);
+	regulator_disable(mpwr->regulator);
+
+	return 0;
+
+force_powerdown:
+	dev_warn(mpwr->dev,
+		 "Forcibly cutting off power, data loss may occur.\n");
+	goto powerdown;
+}
+
+static int mpwr_eg25_power_down(struct mpwr_dev* mpwr)
+{
+	struct gpio_desc *pwrkey_gpio = mpwr_eg25_get_pwrkey_gpio(mpwr);
+	//int ret;
+
+	/* Send 800ms pwrkey pulse to initiate powerdown. */
+	gpiod_direction_output(pwrkey_gpio, 1);
+	msleep(800);
+	gpiod_set_value(pwrkey_gpio, 0);
+
+	/* Switch status key to input, in case it's multiplexed with pwrkey. */
+	gpiod_direction_input(mpwr->status_gpio);
+
+	msleep(20);
+
+#if 0
+	// wait for POWERED DOWN message
+	clear_bit(MPWR_F_GOT_PDN, mpwr->flags);
+	ret = wait_event_timeout(mpwr->wait,
+				 test_bit(MPWR_F_GOT_PDN, mpwr->flags),
+				 msecs_to_jiffies(7000));
+	if (ret <= 0)
+		dev_warn(mpwr->dev,
+			 "POWERED DOWN message not received, is modem really powering down?\n");
+#endif
+
+	return mpwr_eg25_power_down_finish(mpwr);
+}
+
+static void mpwr_finish_pdn_work(struct work_struct *work)
+{
+	/*
+	struct mpwr_dev *mpwr = container_of(work, struct mpwr_dev, power_work);
+	unsigned long flags;
+
+	spin_lock_irqsave(&mpwr->lock, flags);
+	spin_unlock_irqrestore(&mpwr->lock, flags);
+
+	pm_stay_awake(mpwr->dev);
+
+	mutex_lock(&mpwr->modem_lock);
+
+	mutex_unlock(&mpwr->modem_lock);
+
+	pm_relax(mpwr->dev);
+	*/
+}
+
+static void mpwr_eg25_receive_msg(struct mpwr_dev *mpwr, const char *msg)
+{
+	unsigned int msg_len;
+
+	if (!strcmp(msg, "POWERED DOWN")) {
+		// system is powering down
+                set_bit(MPWR_F_GOT_PDN, mpwr->flags);
+		wake_up(&mpwr->wait);
+
+		/*
+		if (mutex_trylock(&mpwr->modem_lock)) {
+			// if no power op is in progress, this means userspace
+			// tried to shut the modem down via AT command, finish up
+			// the job
+
+			pm_stay_awake(mpwr->dev);
+
+			queue_work(mpwr->wq, &mpwr->power_work);
+			dev_warn(mpwr->dev, "userspace shut down the modem via AT command, finishing the job\n");
+			mpwr_eg25_power_down_finish(mpwr);
+			mutex_unlock(&mpwr->modem_lock);
+
+			pm_relax(mpwr->dev);
+		}
+                  */
+                return;
+	}
+
+	if (!strcmp(msg, "RDY")) {
+		// system is ready after powerup
+                return;
+	}
+
+	if (!test_bit(MPWR_F_OPEN, mpwr->flags))
+		return;
+
+	msg_len = strlen(msg);
+
+	if (msg_len + 1 > kfifo_avail(&mpwr->kfifo)) {
+		if (!test_and_set_bit(MPWR_F_OVERFLOW, mpwr->flags))
+			wake_up(&mpwr->wait);
+		return;
+	}
+
+	kfifo_in(&mpwr->kfifo, msg, msg_len);
+	kfifo_in(&mpwr->kfifo, "\n", 1);
+	wake_up(&mpwr->wait);
+}
+
+static void mpwr_host_ready_work(struct work_struct *work)
+{
+        struct mpwr_dev *mpwr = container_of(work, struct mpwr_dev, host_ready_work.work);
+	int ret;
+
+	mutex_lock(&mpwr->modem_lock);
+	gpiod_direction_output(mpwr->dtr_gpio, 0);
+
+	/*
+	 * We need to give the modem some time to wake up.
+	 */
+	msleep(5);
+
+	ret = mpwr_serdev_at_cmd(mpwr, "AT+QCFG=\"urc/cache\",0", 500);
+	if (ret)
+		dev_warn(mpwr->dev,
+			 "Failed to disable urc/cache, you may not be able to see URCs\n");
+
+	gpiod_direction_output(mpwr->dtr_gpio, 1);
+	mutex_unlock(&mpwr->modem_lock);
+
+	gpiod_direction_output(mpwr->host_ready_gpio, 1);
+}
+
+static int mpwr_eg25_suspend(struct mpwr_dev *mpwr)
+{
+	int ret;
+
+	cancel_delayed_work_sync(&mpwr->host_ready_work);
+
+	gpiod_direction_output(mpwr->host_ready_gpio, 0);
+
+	mutex_lock(&mpwr->modem_lock);
+	gpiod_direction_output(mpwr->dtr_gpio, 0);
+
+	msleep(5);
+
+	ret = mpwr_serdev_at_cmd(mpwr, "AT+QCFG=\"urc/cache\",1", 500);
+	if (ret)
+		dev_warn(mpwr->dev,
+			 "Failed to enable urc/cache, you may lose URCs during suspend\n");
+
+	gpiod_direction_output(mpwr->dtr_gpio, 1);
+	mutex_unlock(&mpwr->modem_lock);
+
+	return 0;
+}
+
+static int mpwr_eg25_resume(struct mpwr_dev *mpwr)
+{
+	//gpiod_direction_output(mpwr->dtr_gpio, 0);
+
+	// delay disabling URC cache until the whole system is hopefully resumed...
+	schedule_delayed_work(&mpwr->host_ready_work, msecs_to_jiffies(1000));
+
+	return 0;
+}
+
+static const struct mpwr_gpio mpwr_eg25_gpios[] = {
+	MPWR_GPIO_DEF(enable, GPIOD_OUT_HIGH, true),
+	MPWR_GPIO_DEF(reset, GPIOD_OUT_LOW, true),
+	MPWR_GPIO_DEF(pwrkey, GPIOD_OUT_LOW, false),
+	MPWR_GPIO_DEF(dtr, GPIOD_OUT_LOW, true),
+	MPWR_GPIO_DEF(status, GPIOD_IN, false),
+	MPWR_GPIO_DEF_IRQ(wakeup, GPIOD_IN, true,
+			  IRQF_TRIGGER_FALLING),
+
+	// XXX: not really needed...
+	MPWR_GPIO_DEF(sleep, GPIOD_OUT_LOW, false),
+	MPWR_GPIO_DEF(host_ready, GPIOD_OUT_HIGH, false),
+	MPWR_GPIO_DEF(cts, GPIOD_IN, false),
+	MPWR_GPIO_DEF(rts, GPIOD_OUT_LOW, false),
+	{ },
+};
+
+static const struct mpwr_variant mpwr_eg25_variant = {
+	.power_up = mpwr_eg25_power_up,
+	.power_down = mpwr_eg25_power_down,
+	.recv_msg = mpwr_eg25_receive_msg,
+	.suspend = mpwr_eg25_suspend,
+	.resume = mpwr_eg25_resume,
+	.gpios = mpwr_eg25_gpios,
+	.regulator_required = true,
+	.monitor_wakeup = true,
+};
+
+// }}}
+// {{{ generic helpers
+
+static void mpwr_reset(struct mpwr_dev* mpwr)
+{
+	struct device *dev = mpwr->dev;
+	int ret;
+
+	if (!test_bit(MPWR_F_POWERED, mpwr->flags)) {
+		dev_err(dev, "reset requested but device is not enabled");
+		return;
+	}
+
+	if (!mpwr->reset_gpio) {
+		dev_err(dev, "reset is not configured for this device");
+		return;
+	}
+
+	if (!mpwr->variant->reset) {
+		dev_err(dev, "reset requested but not implemented");
+		return;
+	}
+
+	dev_info(dev, "resetting");
+	ret = mpwr->variant->reset(mpwr);
+	if (ret) {
+		dev_err(dev, "reset failed");
+	}
+}
+
+static void mpwr_power_down(struct mpwr_dev* mpwr)
+{
+	struct device *dev = mpwr->dev;
+	ktime_t start = ktime_get();
+	int ret;
+
+	if (!test_bit(MPWR_F_POWERED, mpwr->flags))
+		return;
+
+	if (!mpwr->variant->power_down) {
+		dev_err(dev, "power down requested but not implemented");
+		return;
+	}
+
+	dev_info(dev, "powering down");
+
+	ret = mpwr->variant->power_down(mpwr);
+	if (ret) {
+		dev_err(dev, "power down failed");
+	} else {
+		clear_bit(MPWR_F_POWERED, mpwr->flags);
+		sysfs_notify(&mpwr->dev->kobj, NULL, "powered");
+		dev_info(mpwr->dev, "powered down in %lld ms\n",
+			 ktime_ms_delta(ktime_get(), start));
+	}
+}
+
+static void mpwr_power_up(struct mpwr_dev* mpwr)
+{
+	struct device *dev = mpwr->dev;
+	ktime_t start = ktime_get();
+	int ret;
+
+	if (test_bit(MPWR_F_POWERED, mpwr->flags))
+		return;
+
+	if (!mpwr->variant->power_up) {
+		dev_err(dev, "power up requested but not implemented");
+		return;
+	}
+
+	dev_info(dev, "powering up");
+
+	ret = mpwr->variant->power_up(mpwr);
+	if (ret) {
+		dev_err(dev, "power up failed");
+	} else {
+		set_bit(MPWR_F_POWERED, mpwr->flags);
+		sysfs_notify(&mpwr->dev->kobj, NULL, "powered");
+		dev_info(mpwr->dev, "powered up in %lld ms\n",
+			 ktime_ms_delta(ktime_get(), start));
+	}
+}
+
+// }}}
+// {{{ chardev
+
+static int mpwr_release(struct inode *ip, struct file *fp)
+{
+	struct mpwr_dev* mpwr = fp->private_data;
+
+	clear_bit(MPWR_F_OPEN, mpwr->flags);
+
+	return 0;
+}
+
+static int mpwr_open(struct inode *ip, struct file *fp)
+{
+	struct mpwr_dev* mpwr = container_of(ip->i_cdev, struct mpwr_dev, cdev);
+
+	fp->private_data = mpwr;
+
+	if (test_and_set_bit(MPWR_F_OPEN, mpwr->flags))
+		return -EBUSY;
+
+	nonseekable_open(ip, fp);
+	return 0;
+}
+
+static ssize_t mpwr_read(struct file *fp, char __user *buf, size_t len,
+			 loff_t *off)
+{
+	struct mpwr_dev* mpwr = fp->private_data;
+	int non_blocking = fp->f_flags & O_NONBLOCK;
+	unsigned int copied;
+	int ret;
+
+	if (non_blocking && kfifo_is_empty(&mpwr->kfifo))
+		return -EWOULDBLOCK;
+
+	ret = wait_event_interruptible(mpwr->wait,
+				       !kfifo_is_empty(&mpwr->kfifo)
+				       || test_bit(MPWR_F_OVERFLOW, mpwr->flags));
+	if (ret)
+		return ret;
+
+	if (test_and_clear_bit(MPWR_F_OVERFLOW, mpwr->flags)) {
+		if (len < 9)
+			return -E2BIG;
+		if (copy_to_user(buf, "OVERFLOW\n", 9))
+			return -EFAULT;
+		return 9;
+	}
+
+	ret = kfifo_to_user(&mpwr->kfifo, buf, len, &copied);
+
+	return ret ? ret : copied;
+}
+
+static unsigned int mpwr_poll(struct file *fp, poll_table *wait)
+{
+	struct mpwr_dev* mpwr = fp->private_data;
+
+	poll_wait(fp, &mpwr->wait, wait);
+
+	if (!kfifo_is_empty(&mpwr->kfifo))
+		return EPOLLIN | EPOLLRDNORM;
+
+	return 0;
+}
+
+static const struct file_operations mpwr_fops = {
+	.owner		= THIS_MODULE,
+	.open		= mpwr_open,
+	.release	= mpwr_release,
+	.llseek		= noop_llseek,
+	.read		= mpwr_read,
+	.poll		= mpwr_poll,
+};
+
+// }}}
+
+static void mpwr_work_handler(struct work_struct *work)
+{
+	struct mpwr_dev *mpwr = container_of(work, struct mpwr_dev, power_work);
+	unsigned long flags;
+	int last_request;
+
+	spin_lock_irqsave(&mpwr->lock, flags);
+	last_request = mpwr->last_request;
+	mpwr->last_request = 0;
+	spin_unlock_irqrestore(&mpwr->lock, flags);
+
+	pm_stay_awake(mpwr->dev);
+
+	mutex_lock(&mpwr->modem_lock);
+
+	if (last_request == MPWR_REQ_RESET) {
+		mpwr_reset(mpwr);
+	} else if (last_request == MPWR_REQ_PWDN) {
+		mpwr_power_down(mpwr);
+	} else if (last_request == MPWR_REQ_PWUP) {
+		mpwr_power_up(mpwr);
+	}
+
+	mutex_unlock(&mpwr->modem_lock);
+
+	clear_bit(MPWR_F_POWER_CHANGE_INPROGRESS, mpwr->flags);
+	sysfs_notify(&mpwr->dev->kobj, NULL, "is_busy");
+	wake_up(&mpwr->wait);
+
+	pm_relax(mpwr->dev);
+}
+
+static void mpwr_request_power_change(struct mpwr_dev* mpwr, int request, int mode)
+{
+	unsigned long flags;
+
+	set_bit(MPWR_F_POWER_CHANGE_INPROGRESS, mpwr->flags);
+	sysfs_notify(&mpwr->dev->kobj, NULL, "is_busy");
+
+	spin_lock_irqsave(&mpwr->lock, flags);
+	mpwr->last_request = request;
+	if (mode >= 0)
+		mpwr->powerup_mode = mode;
+	spin_unlock_irqrestore(&mpwr->lock, flags);
+
+	queue_work(mpwr->wq, &mpwr->power_work);
+}
+
+static irqreturn_t mpwr_gpio_isr(int irq, void *dev_id)
+{
+	struct mpwr_dev *mpwr = dev_id;
+
+	if (irq == mpwr->wakeup_irq) {
+		dev_dbg(mpwr->dev, "wakeup irq\n");
+
+		if (device_can_wakeup(mpwr->dev))
+			pm_wakeup_event(mpwr->dev, 2000);
+
+		set_bit(MPWR_F_GOT_WAKEUP, mpwr->flags);
+		spin_lock(&mpwr->lock);
+		mpwr->last_wakeup = ktime_get();
+		spin_unlock(&mpwr->lock);
+		wake_up(&mpwr->wait);
+	}
+
+	return IRQ_HANDLED;
+}
+
+static void mpwr_wd_timer_fn(struct timer_list *t)
+{
+	struct mpwr_dev *mpwr = from_timer(mpwr, t, wd_timer);
+
+	if (!mpwr->variant->monitor_wakeup || !test_bit(MPWR_F_POWERED, mpwr->flags))
+		return;
+
+	/*
+	 * Monitor wakeup status:
+	 *
+	 * If RI signal is low for too long we assume the user killswitched
+	 * the modem at runtime.
+	 */
+	spin_lock(&mpwr->lock);
+	if (!gpiod_get_value(mpwr->wakeup_gpio)) {
+		if (ktime_ms_delta(ktime_get(), mpwr->last_wakeup) > 5000) {
+			if (!test_and_set_bit(MPWR_F_KILLSWITCHED, mpwr->flags))
+				sysfs_notify(&mpwr->dev->kobj, NULL, "killswitched");
+			wake_up(&mpwr->wait);
+			dev_warn(mpwr->dev, "modem looks killswitched at runtime!\n");
+		}
+	}
+	spin_unlock(&mpwr->lock);
+
+	mod_timer(t, jiffies + msecs_to_jiffies(1000));
+}
+
+// {{{ sysfs
+
+static ssize_t powered_show(struct device *dev,
+			    struct device_attribute *attr, char *buf)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+
+	return scnprintf(buf, PAGE_SIZE, "%u\n",
+			 !!test_bit(MPWR_F_POWERED, mpwr->flags));
+}
+
+static ssize_t powered_store(struct device *dev,
+			     struct device_attribute *attr,
+			     const char *buf, size_t len)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+	unsigned status;
+	int ret;
+
+	if (test_bit(MPWR_F_BLOCKED, mpwr->flags))
+		return -EPERM;
+
+	ret = kstrtouint(buf, 10, &status);
+	if (ret)
+		return ret;
+
+	mpwr_request_power_change(mpwr, status ? MPWR_REQ_PWUP : MPWR_REQ_PWDN, status);
+
+	return len;
+}
+
+static ssize_t powered_blocking_store(struct device *dev,
+				      struct device_attribute *attr,
+				      const char *buf, size_t len)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+	unsigned status;
+	int ret;
+
+	if (test_bit(MPWR_F_BLOCKED, mpwr->flags))
+		return -EPERM;
+
+	ret = kstrtouint(buf, 10, &status);
+	if (ret)
+		return ret;
+
+	mpwr_request_power_change(mpwr, status ? MPWR_REQ_PWUP : MPWR_REQ_PWDN, status);
+
+	ret = wait_event_interruptible_timeout(mpwr->wait,
+					       !test_bit(MPWR_F_POWER_CHANGE_INPROGRESS, mpwr->flags),
+					       msecs_to_jiffies(60000));
+	if (ret <= 0) {
+		dev_err(mpwr->dev, "Power state change timeout\n");
+		return -EIO;
+	}
+
+	if (!!status != !!test_bit(MPWR_F_POWERED, mpwr->flags))
+		return -EIO;
+
+	return len;
+}
+
+static ssize_t help_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	return scnprintf(buf, PAGE_SIZE,
+			"echo N > powered, where N can be:\n"
+			"0: power off\n"
+			"1: normal powerup\n"
+			"2: dumb powerup (no AT commands and little error checking during powerup)\n"
+			"3: fastboot powerup (with biktor's patched aboot - #W_DISABLE held low during powerup)\n"
+			"4: alternate powerup (megi's userspace - DTR held high during powerup)\n\n"
+			"echo N > powered_blocking can be used for the write to block until power status transition completes\n");
+}
+
+static ssize_t killswitched_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+
+	return scnprintf(buf, PAGE_SIZE, "%u\n",
+			 !!test_bit(MPWR_F_KILLSWITCHED, mpwr->flags));
+}
+
+static ssize_t is_busy_show(struct device *dev,
+			    struct device_attribute *attr, char *buf)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+
+	return scnprintf(buf, PAGE_SIZE, "%u\n",
+			 !!test_bit(MPWR_F_POWER_CHANGE_INPROGRESS, mpwr->flags));
+}
+
+static ssize_t hard_reset_store(struct device *dev,
+				struct device_attribute *attr,
+				const char *buf, size_t len)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+	bool val;
+	int ret;
+
+	if (test_bit(MPWR_F_BLOCKED, mpwr->flags))
+		return -EPERM;
+
+	ret = kstrtobool(buf, &val);
+	if (ret)
+		return ret;
+	if (val)
+		mpwr_request_power_change(mpwr, MPWR_REQ_RESET, -1);
+
+	return len;
+}
+
+static ssize_t debug_pins_store(struct device *dev,
+				struct device_attribute *attr,
+				const char *buf, size_t len)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+	unsigned val;
+	int ret;
+
+	if (test_bit(MPWR_F_BLOCKED, mpwr->flags))
+		return -EPERM;
+
+	ret = kstrtouint(buf, 16, &val);
+	if (ret)
+		return ret;
+
+	gpiod_direction_output(mpwr->host_ready_gpio, val & BIT(0));
+	gpiod_direction_output(mpwr->enable_gpio, val & BIT(1));
+	gpiod_direction_output(mpwr->dtr_gpio, val & BIT(2));
+	gpiod_direction_output(mpwr->rts_gpio, val & BIT(3));
+
+	return len;
+}
+
+static ssize_t debug_pins_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+
+	return scnprintf(buf, PAGE_SIZE, "CTS=%u RI=%u\n",
+			 gpiod_get_value(mpwr->cts_gpio), gpiod_get_value(mpwr->wakeup_gpio));
+}
+
+static DEVICE_ATTR_RW(powered);
+static DEVICE_ATTR_WO(powered_blocking);
+static DEVICE_ATTR_RO(killswitched);
+static DEVICE_ATTR_RO(is_busy);
+static DEVICE_ATTR_RO(help);
+static DEVICE_ATTR_WO(hard_reset);
+static DEVICE_ATTR_RW(debug_pins);
+
+static struct attribute *mpwr_attrs[] = {
+	&dev_attr_powered.attr,
+	&dev_attr_powered_blocking.attr,
+	&dev_attr_killswitched.attr,
+	&dev_attr_is_busy.attr,
+	&dev_attr_help.attr,
+	&dev_attr_hard_reset.attr,
+	&dev_attr_debug_pins.attr,
+	NULL,
+};
+
+static const struct attribute_group mpwr_group = {
+	.attrs = mpwr_attrs,
+};
+
+// }}}
+// {{{ rfkill
+
+static int mpwr_rfkill_set(void *data, bool blocked)
+{
+	struct mpwr_dev *mpwr = data;
+
+	gpiod_set_value(mpwr->enable_gpio, !blocked);
+	return 0;
+}
+
+static void mpwr_rfkill_query(struct rfkill *rfkill, void *data)
+{
+	struct mpwr_dev *mpwr = data;
+
+	rfkill_set_sw_state(rfkill, !gpiod_get_value(mpwr->enable_gpio));
+}
+
+static const struct rfkill_ops mpwr_rfkill_ops = {
+	.set_block = mpwr_rfkill_set,
+	.query = mpwr_rfkill_query,
+};
+
+// }}}
+// {{{ probe
+
+static int mpwr_probe_generic(struct device *dev, struct mpwr_dev **mpwr_out)
+{
+	struct mpwr_dev *mpwr;
+	struct device_node *np = dev->of_node;
+	struct device *sdev;
+	const char* cdev_name = NULL;
+	int ret, i;
+
+	mpwr = devm_kzalloc(dev, sizeof(*mpwr), GFP_KERNEL);
+	if (!mpwr)
+		return -ENOMEM;
+
+	mpwr->variant = of_device_get_match_data(dev);
+	if (!mpwr->variant)
+		return -EINVAL;
+
+	mpwr->dev = dev;
+	init_waitqueue_head(&mpwr->wait);
+        mutex_init(&mpwr->modem_lock);
+        spin_lock_init(&mpwr->lock);
+	INIT_WORK(&mpwr->power_work, &mpwr_work_handler);
+	INIT_WORK(&mpwr->finish_pdn_work, &mpwr_finish_pdn_work);
+        INIT_DELAYED_WORK(&mpwr->host_ready_work, mpwr_host_ready_work);
+	INIT_KFIFO(mpwr->kfifo);
+
+	ret = of_property_read_string(np, "char-device-name", &cdev_name);
+	if (ret) {
+		dev_err(dev, "char-device-name is not configured");
+		return -EINVAL;
+	}
+
+	if (of_property_read_bool(np, "blocked"))
+		set_bit(MPWR_F_BLOCKED, mpwr->flags);
+
+	mpwr->status_pwrkey_multiplexed =
+		of_property_read_bool(np, "status-pwrkey-multiplexed");
+
+	mpwr->regulator = devm_regulator_get_optional(dev, "power");
+	if (IS_ERR(mpwr->regulator)) {
+		ret = PTR_ERR(mpwr->regulator);
+                if (ret != -ENODEV) {
+			dev_err(dev, "can't get power supply err=%d", ret);
+			return ret;
+		}
+
+		mpwr->regulator = NULL;
+	}
+
+	if (!mpwr->regulator && mpwr->variant->regulator_required) {
+		dev_err(dev, "can't get power supply err=%d", -ENODEV);
+		return -ENODEV;
+	}
+
+	mpwr->regulator_vbus = devm_regulator_get(dev, "vbus");
+	if (IS_ERR(mpwr->regulator_vbus))
+		return dev_err_probe(dev, PTR_ERR(mpwr->regulator_vbus),
+				     "can't get vbus power supply\n");
+
+	for (i = 0; mpwr->variant->gpios[i].name; i++) {
+		const struct mpwr_gpio *io = &mpwr->variant->gpios[i];
+		struct gpio_desc **desc = (struct gpio_desc **)((u8*)mpwr +
+								io->desc_off);
+		int *irq = (int*)((u8*)mpwr + io->irq_off);
+		char buf[64];
+
+		if (io->required)
+			*desc = devm_gpiod_get(dev, io->name, io->flags);
+		else
+			*desc = devm_gpiod_get_optional(dev, io->name, io->flags);
+
+		if (IS_ERR(*desc)) {
+			dev_err(dev, "can't get %s gpio err=%ld", io->name,
+				PTR_ERR(*desc));
+			return PTR_ERR(*desc);
+		}
+
+		if (!*desc)
+			continue;
+
+		if (io->irq_flags == 0 || io->irq_off == 0)
+			continue;
+
+		*irq = gpiod_to_irq(*desc);
+		if (*irq <= 0) {
+			dev_err(dev, "error converting %s gpio to irq: %d",
+				io->name, ret);
+			return *irq;
+		}
+
+		snprintf(buf, sizeof buf, "modem-%s-gpio", io->name);
+		ret = devm_request_irq(dev, *irq, mpwr_gpio_isr, io->irq_flags,
+				       devm_kstrdup(dev, buf, GFP_KERNEL), mpwr);
+		if (ret) {
+			dev_err(dev, "error requesting %s irq: %d",
+				io->name, ret);
+			return ret;
+		}
+	}
+
+	if (mpwr->status_pwrkey_multiplexed && mpwr->pwrkey_gpio) {
+		dev_err(dev, "status and pwrkey are multiplexed, but pwrkey defined\n");
+		return -EINVAL;
+	}
+
+	if (mpwr->status_pwrkey_multiplexed && !mpwr->status_gpio) {
+		dev_err(dev, "status and pwrkey are multiplexed, but status is not defined\n");
+		return -EINVAL;
+	}
+
+	ret = devm_device_add_group(dev, &mpwr_group);
+	if (ret)
+		return ret;
+
+	// create char device
+	ret = alloc_chrdev_region(&mpwr->major, 0, 1, "modem-power");
+	if (ret) {
+		dev_err(dev, "can't allocate chrdev region");
+		goto err_disable_regulator;
+	}
+
+	cdev_init(&mpwr->cdev, &mpwr_fops);
+	mpwr->cdev.owner = THIS_MODULE;
+	ret = cdev_add(&mpwr->cdev, mpwr->major, 1);
+	if (ret) {
+		dev_err(dev, "can't add cdev");
+		goto err_unreg_chrev_region;
+	}
+
+	sdev = device_create(mpwr_class, dev, mpwr->major, mpwr, cdev_name);
+	if (IS_ERR(sdev)) {
+		ret = PTR_ERR(sdev);
+		goto err_del_cdev;
+	}
+
+	if (mpwr->wakeup_irq > 0) {
+		ret = device_init_wakeup(dev, true);
+		if (ret) {
+			dev_err(dev, "failed to init wakeup (%d)\n", ret);
+			goto err_free_dev;
+		}
+	}
+
+	if (mpwr->enable_gpio) {
+		mpwr->rfkill = rfkill_alloc("modem", dev, RFKILL_TYPE_WWAN,
+					    &mpwr_rfkill_ops, mpwr);
+		if (!mpwr->rfkill) {
+			dev_err(dev, "failed to alloc rfkill\n");
+			ret = -ENOMEM;
+			goto err_deinit_wakeup;
+		}
+
+                rfkill_init_sw_state(mpwr->rfkill, false);
+
+		ret = rfkill_register(mpwr->rfkill);
+		if (ret) {
+			dev_err(dev, "failed to register rfkill (%d)\n", ret);
+			goto err_free_rfkill;
+		}
+	}
+
+	mpwr->wq = alloc_ordered_workqueue("modem-power", 0);
+	if (!mpwr->wq) {
+		ret = -ENOMEM;
+		dev_err(dev, "failed to allocate workqueue\n");
+		goto err_unreg_rfkill;
+	}
+
+	if (mpwr->variant->power_init)
+		mpwr->variant->power_init(mpwr);
+
+	timer_setup(&mpwr->wd_timer, mpwr_wd_timer_fn, 0);
+	mod_timer(&mpwr->wd_timer, jiffies + msecs_to_jiffies(50));
+
+	dev_info(dev, "modem power manager ready");
+	*mpwr_out = mpwr;
+
+	return 0;
+
+err_unreg_rfkill:
+	if (mpwr->rfkill)
+		rfkill_unregister(mpwr->rfkill);
+err_free_rfkill:
+	if (mpwr->rfkill)
+		rfkill_destroy(mpwr->rfkill);
+err_deinit_wakeup:
+	if (mpwr->wakeup_irq > 0)
+		device_init_wakeup(dev, false);
+err_free_dev:
+	device_destroy(mpwr_class, mpwr->major);
+err_del_cdev:
+	cdev_del(&mpwr->cdev);
+err_unreg_chrev_region:
+	unregister_chrdev(mpwr->major, "modem-power");
+err_disable_regulator:
+	cancel_work_sync(&mpwr->power_work);
+	return ret;
+}
+
+static int mpwr_remove_generic(struct mpwr_dev *mpwr)
+{
+	if (mpwr->rfkill) {
+		rfkill_unregister(mpwr->rfkill);
+		rfkill_destroy(mpwr->rfkill);
+	}
+
+	if (mpwr->wakeup_irq > 0)
+		device_init_wakeup(mpwr->dev, false);
+
+	del_timer_sync(&mpwr->wd_timer);
+        cancel_delayed_work_sync(&mpwr->host_ready_work);
+
+	cancel_work_sync(&mpwr->power_work);
+	destroy_workqueue(mpwr->wq);
+
+	mutex_lock(&mpwr->modem_lock);
+	mpwr_power_down(mpwr);
+	mutex_unlock(&mpwr->modem_lock);
+
+	device_destroy(mpwr_class, mpwr->major);
+	cdev_del(&mpwr->cdev);
+	unregister_chrdev(mpwr->major, "modem-power");
+
+	return 0;
+}
+
+static void mpwr_shutdown_generic(struct mpwr_dev *mpwr)
+{
+	cancel_work_sync(&mpwr->power_work);
+        cancel_delayed_work_sync(&mpwr->host_ready_work);
+
+	mutex_lock(&mpwr->modem_lock);
+	mpwr_power_down(mpwr);
+	mutex_unlock(&mpwr->modem_lock);
+}
+
+// }}}
+// {{{ suspend/resume
+
+static int __maybe_unused mpwr_suspend(struct device *dev)
+{
+	struct mpwr_dev *mpwr = dev_get_drvdata(dev);
+	int ret = 0;
+
+	if (!test_bit(MPWR_F_POWERED, mpwr->flags))
+		return 0;
+
+	//if (mpwr->sleep_gpio)
+		//gpiod_direction_output(mpwr->sleep_gpio, 1);
+
+	if (mpwr->variant->suspend)
+		mpwr->variant->suspend(mpwr);
+
+	if (mpwr->wakeup_irq && device_may_wakeup(mpwr->dev))
+		enable_irq_wake(mpwr->wakeup_irq);
+
+	return ret;
+}
+
+static int __maybe_unused mpwr_resume(struct device *dev)
+{
+	struct mpwr_dev *mpwr = dev_get_drvdata(dev);
+	int ret = 0;
+
+	if (!test_bit(MPWR_F_POWERED, mpwr->flags))
+		return 0;
+
+	//if (mpwr->sleep_gpio)
+		//gpiod_direction_output(mpwr->sleep_gpio, 0);
+
+	if (mpwr->variant->resume)
+		mpwr->variant->resume(mpwr);
+
+	if (mpwr->wakeup_irq && device_may_wakeup(mpwr->dev))
+		disable_irq_wake(mpwr->wakeup_irq);
+
+	return ret;
+}
+
+static const struct dev_pm_ops mpwr_pm_ops = {
+	SET_SYSTEM_SLEEP_PM_OPS(mpwr_suspend, mpwr_resume)
+};
+
+// }}}
+// {{{ serdev
+
+static int mpwr_serdev_send_msg(struct mpwr_dev *mpwr, const char *msg)
+{
+	int ret, len;
+	char buf[128];
+
+	if (!mpwr->serdev)
+		return -ENODEV;
+
+	len = snprintf(buf, sizeof buf, "%s\r\n", msg);
+	if (len >= sizeof buf)
+		return -E2BIG;
+
+	ret = serdev_device_write(mpwr->serdev, buf, len, msecs_to_jiffies(3000));
+	if (ret < len)
+		return -EIO;
+
+	serdev_device_wait_until_sent(mpwr->serdev, msecs_to_jiffies(3000));
+
+	return 0;
+}
+
+static int __mpwr_serdev_at_cmd(struct mpwr_dev *mpwr, const char *msg,
+				int timeout_ms, bool report_error, bool report_timeout)
+{
+        int ret;
+
+        if (test_and_set_bit(MPWR_F_RECEIVING_MSG, mpwr->flags))
+		return -EBUSY;
+
+	mpwr->msg_len = 0;
+
+	dev_dbg(mpwr->dev, "SEND: %s\n", msg);
+
+	ret = mpwr_serdev_send_msg(mpwr, msg);
+        if (ret) {
+		clear_bit(MPWR_F_RECEIVING_MSG, mpwr->flags);
+		dev_err(mpwr->dev, "AT command '%s' can't be sent (%d)\n", msg, ret);
+                return ret;
+        }
+
+	ret = wait_event_interruptible_timeout(mpwr->wait,
+					       !test_bit(MPWR_F_RECEIVING_MSG, mpwr->flags),
+					       msecs_to_jiffies(timeout_ms));
+	if (ret <= 0) {
+		clear_bit(MPWR_F_RECEIVING_MSG, mpwr->flags);
+		if (report_timeout)
+			dev_err(mpwr->dev, "AT command '%s' timed out\n", msg);
+                return ret ? ret : -ETIMEDOUT;
+	}
+
+        if (!mpwr->msg_ok) {
+		if (report_error)
+			dev_err(mpwr->dev, "AT command '%s' returned ERROR\n", msg);
+                return -EINVAL;
+	}
+
+        return 0;
+}
+
+static int mpwr_serdev_at_cmd(struct mpwr_dev *mpwr, const char *msg, int timeout_ms)
+{
+	return __mpwr_serdev_at_cmd(mpwr, msg, timeout_ms, true, true);
+}
+
+static int __mpwr_serdev_at_cmd_with_retry(struct mpwr_dev *mpwr, const char *msg,
+					   int timeout_ms, int tries, bool ignore_timeout)
+{
+	int ret = 0;
+
+	if (tries < 1)
+		tries = 1;
+
+	while (tries-- > 0) {
+		ret = __mpwr_serdev_at_cmd(mpwr, msg, timeout_ms, false, !ignore_timeout);
+		if (ret != -EINVAL && (!ignore_timeout || ret != -ETIMEDOUT))
+			return ret;
+
+		if (ret != -ETIMEDOUT)
+			msleep(1000);
+	}
+
+	dev_err(mpwr->dev, "AT command '%s' returned ERROR\n", msg);
+	return ret;
+}
+
+static int mpwr_serdev_at_cmd_with_retry(struct mpwr_dev *mpwr, const char *msg,
+					 int timeout_ms, int tries)
+{
+	return __mpwr_serdev_at_cmd_with_retry(mpwr, msg, timeout_ms, tries, false);
+}
+
+static int mpwr_serdev_at_cmd_with_retry_ignore_timeout(struct mpwr_dev *mpwr, const char *msg,
+							int timeout_ms, int tries)
+{
+	return __mpwr_serdev_at_cmd_with_retry(mpwr, msg, timeout_ms, tries, true);
+}
+
+static void mpwr_serdev_receive_msg(struct mpwr_dev *mpwr, const char *msg)
+{
+	dev_dbg(mpwr->dev, "RECV: %s\n", msg);
+
+	if (mpwr->variant->recv_msg)
+		mpwr->variant->recv_msg(mpwr, msg);
+
+        if (!test_bit(MPWR_F_RECEIVING_MSG, mpwr->flags))
+                return;
+
+	if (!strcmp(msg, "OK")) {
+                clear_bit(MPWR_F_RECEIVING_MSG, mpwr->flags);
+                mpwr->msg_ok = true;
+		wake_up(&mpwr->wait);
+                return;
+	} else if (!strcmp(msg, "ERROR")) {
+                clear_bit(MPWR_F_RECEIVING_MSG, mpwr->flags);
+                mpwr->msg_ok = false;
+		wake_up(&mpwr->wait);
+                return;
+	} else {
+                int len = strlen(msg);
+
+                if (mpwr->msg_len + len + 1 > sizeof(mpwr->msg)) {
+                        dev_warn(mpwr->dev, "message buffer overflow, ignoring message\n");
+                        return;
+                }
+
+                memcpy(mpwr->msg + mpwr->msg_len, msg, len + 1);
+                mpwr->msg_len += len + 1;
+        }
+}
+
+static int mpwr_serdev_receive_buf(struct serdev_device *serdev,
+				   const unsigned char *buf, size_t count)
+{
+	struct mpwr_dev *mpwr = serdev_device_get_drvdata(serdev);
+	size_t avail = sizeof(mpwr->rcvbuf) - mpwr->rcvbuf_fill;
+	char* p;
+
+	if (avail < count)
+		count = avail;
+
+	if (avail > 0) {
+		memcpy(mpwr->rcvbuf + mpwr->rcvbuf_fill, buf, count);
+		mpwr->rcvbuf_fill += count;
+	}
+
+	while (true) {
+		p = strnstr(mpwr->rcvbuf, "\r\n", mpwr->rcvbuf_fill);
+		if (p) {
+			if (p > mpwr->rcvbuf) {
+				*p = 0;
+				mpwr_serdev_receive_msg(mpwr, mpwr->rcvbuf);
+			}
+
+			mpwr->rcvbuf_fill -= (p - mpwr->rcvbuf) + 2;
+			memmove(mpwr->rcvbuf, p + 2, mpwr->rcvbuf_fill);
+		} else {
+			if (sizeof(mpwr->rcvbuf) - mpwr->rcvbuf_fill == 0) {
+				mpwr->rcvbuf_fill = 0;
+				dev_warn(mpwr->dev, "rcvbuf overflow\n");
+			}
+
+			break;
+		}
+	}
+
+        return count;
+}
+
+static const struct serdev_device_ops mpwr_serdev_ops = {
+	.receive_buf = mpwr_serdev_receive_buf,
+	.write_wakeup = serdev_device_write_wakeup,
+};
+
+static int mpwr_serdev_probe(struct serdev_device *serdev)
+{
+	struct device *dev = &serdev->dev;
+        struct mpwr_dev* mpwr;
+        int ret;
+
+	ret = mpwr_probe_generic(dev, &mpwr);
+	if (ret)
+		return ret;
+
+	serdev_device_set_drvdata(serdev, mpwr);
+	serdev_device_set_client_ops(serdev, &mpwr_serdev_ops);
+	mpwr->serdev = serdev;
+
+        return 0;
+}
+
+static void mpwr_serdev_remove(struct serdev_device *serdev)
+{
+	struct mpwr_dev *mpwr = serdev_device_get_drvdata(serdev);
+
+	mpwr_remove_generic(mpwr);
+}
+
+static const struct of_device_id mpwr_of_match_serdev[] = {
+	{ .compatible = "quectel,eg25",
+	  .data = &mpwr_eg25_variant },
+	{},
+};
+MODULE_DEVICE_TABLE(of, mpwr_of_match_serdev);
+
+static void mpwr_serdev_shutdown(struct device *dev)
+{
+	struct mpwr_dev *mpwr = dev_get_drvdata(dev);
+
+	mpwr_shutdown_generic(mpwr);
+}
+
+static struct serdev_device_driver mpwr_serdev_driver = {
+        .probe  = mpwr_serdev_probe,
+        .remove = mpwr_serdev_remove,
+        .driver = {
+                .name = DRIVER_NAME,
+                .of_match_table = mpwr_of_match_serdev,
+		.pm = &mpwr_pm_ops,
+		.shutdown = mpwr_serdev_shutdown,
+        },
+};
+
+// }}}
+// {{{ platdev
+
+static int mpwr_pdev_probe(struct platform_device *pdev)
+{
+        struct mpwr_dev* mpwr;
+        int ret;
+
+	ret = mpwr_probe_generic(&pdev->dev, &mpwr);
+	if (ret)
+		return ret;
+
+	platform_set_drvdata(pdev, mpwr);
+        return 0;
+}
+
+static int mpwr_pdev_remove(struct platform_device *pdev)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(pdev);
+
+	return mpwr_remove_generic(mpwr);
+}
+
+static void mpwr_pdev_shutdown(struct platform_device *pdev)
+{
+	struct mpwr_dev *mpwr = platform_get_drvdata(pdev);
+
+	mpwr_shutdown_generic(mpwr);
+}
+
+static const struct of_device_id mpwr_of_match_plat[] = {
+	{ .compatible = "zte,mg3732",
+	  .data = &mpwr_mg2723_variant },
+	{},
+};
+MODULE_DEVICE_TABLE(of, mpwr_of_match_plat);
+
+static struct platform_driver mpwr_platform_driver = {
+	.probe = mpwr_pdev_probe,
+	.remove = mpwr_pdev_remove,
+	.shutdown = mpwr_pdev_shutdown,
+	.driver = {
+		.name = DRIVER_NAME,
+		.of_match_table = mpwr_of_match_plat,
+		.pm = &mpwr_pm_ops,
+	},
+};
+
+// }}}
+// {{{ driver init
+
+static int __init mpwr_driver_init(void)
+{
+	int ret;
+
+	mpwr_class = class_create(THIS_MODULE, "modem-power");
+	if (IS_ERR(mpwr_class))
+		return PTR_ERR(mpwr_class);
+
+	ret = serdev_device_driver_register(&mpwr_serdev_driver);
+	if (ret)
+		goto err_class;
+
+	ret = platform_driver_register(&mpwr_platform_driver);
+	if (ret)
+		goto err_serdev;
+
+	return ret;
+
+err_serdev:
+	serdev_device_driver_unregister(&mpwr_serdev_driver);
+err_class:
+	class_destroy(mpwr_class);
+	return ret;
+}
+
+static void __exit mpwr_driver_exit(void)
+{
+	serdev_device_driver_unregister(&mpwr_serdev_driver);
+	platform_driver_unregister(&mpwr_platform_driver);
+	class_destroy(mpwr_class);
+}
+
+module_init(mpwr_driver_init);
+module_exit(mpwr_driver_exit);
+
+MODULE_DESCRIPTION("Modem power manager");
+MODULE_AUTHOR("Ondrej Jirman <megi@xff.cz>");
+MODULE_LICENSE("GPL v2");
+
+// }}}
-- 
2.35.3

