All of lore.kernel.org
 help / color / mirror / Atom feed
From: Sebastian Reichel <sebastian.reichel@collabora.com>
To: servpol@tuta.io
Cc: "Rob Herring" <robh@kernel.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Conor Dooley" <conor+dt@kernel.org>,
	linux-pm@vger.kernel.org, devicetree@vger.kernel.org,
	linux-kernel@vger.kernel.org,
	"Uwe Kleine-König" <u.kleine-koenig@baylibre.com>
Subject: Re: [PATCH v2 2/2] power: supply: Add GL.iNet GL-E750 MCU battery driver
Date: Mon, 9 Nov 2026 00:35:58 +0100	[thread overview]
Message-ID: <avEHPVdx0NlX4dhy@venus> (raw)
In-Reply-To: <20261021-gl-e750-mcu-v2-2-9bd7e7c2e0e1@tuta.io>

[-- Attachment #1: Type: text/plain, Size: 14991 bytes --]

Hi,

On Wed, Oct 21, 2026 at 07:33:44PM +0000, Serv Pol via B4 Relay wrote:
> From: Serv Pol <servpol@tuta.io>
> 
> The status MCU of the GL.iNet GL-E750 (Mudi) travel router sits on the
> SoC UART and talks in text lines: the host sends JSON objects whose values
> are all strings, and a request containing "mcu_status": "1" is answered
> with
> 
>   {OK},<percent>,<temperature>,<charging>,<cycles>
> 
> Poll that every 30 seconds and expose the result as a power_supply.
> 
> When the power switch is turned off, the MCU sends {"shut_down": "1"},
> without a line ending, and cuts the power a few seconds later. Shut the
> system down cleanly in that case.
> 
> The MCU also renders the status screen from JSON fields (SSIDs, clients,
> cellular state...). That vendor-specific content is not handled here.
> 
> Assisted-by: Claude:claude-opus-5-5
> Signed-off-by: Serv Pol <servpol@tuta.io>
> ---

The Sashiko analysis looks correct:

https://sashiko.dev/#/patchset/20261005-gl-e750-mcu-v1-0-499d2e31f786%40tuta.io

Greetings,

-- Sebastian

>  MAINTAINERS                        |   1 +
>  drivers/power/supply/Kconfig       |  13 ++
>  drivers/power/supply/Makefile      |   1 +
>  drivers/power/supply/gl-e750-mcu.c | 369 +++++++++++++++++++++++++++++++++++++
>  4 files changed, 384 insertions(+)
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 247432449..505aa205c 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -11110,6 +11110,7 @@ M:	Serv Pol <servpol@tuta.io>
>  L:	linux-pm@vger.kernel.org
>  S:	Maintained
>  F:	Documentation/devicetree/bindings/power/supply/glinet,gl-e750-mcu.yaml
> +F:	drivers/power/supply/gl-e750-mcu.c
>  
>  GNSS SUBSYSTEM
>  M:	Johan Hovold <johan@kernel.org>
> diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
> index ee7dc5f26..e87c76c18 100644
> --- a/drivers/power/supply/Kconfig
> +++ b/drivers/power/supply/Kconfig
> @@ -1203,4 +1203,17 @@ config MACSMC_POWER
>  
>  	  Say Y or M here if you have an Apple Silicon based Mac.
>  
> +config BATTERY_GL_E750_MCU
> +	tristate "GL.iNet GL-E750 status MCU battery driver"
> +	depends on SERIAL_DEV_BUS
> +	depends on OF || COMPILE_TEST
> +	help
> +	  Say Y or M here to enable the battery driver for the status
> +	  microcontroller of the GL.iNet GL-E750 (Mudi) travel router.
> +	  It reports the battery state and shuts the system down when
> +	  the power switch is turned off.
> +
> +	  To compile this driver as a module, choose M here: the module
> +	  will be called gl-e750-mcu.
> +
>  endif # POWER_SUPPLY
> diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile
> index f3c0d1006..5d405556e 100644
> --- a/drivers/power/supply/Makefile
> +++ b/drivers/power/supply/Makefile
> @@ -78,6 +78,7 @@ obj-$(CONFIG_CHARGER_TWL4030)	+= twl4030_charger.o
>  obj-$(CONFIG_CHARGER_TWL6030)	+= twl6030_charger.o
>  obj-$(CONFIG_CHARGER_LP8727)	+= lp8727_charger.o
>  obj-$(CONFIG_CHARGER_LP8788)	+= lp8788-charger.o
> +obj-$(CONFIG_BATTERY_GL_E750_MCU)	+= gl-e750-mcu.o
>  obj-$(CONFIG_CHARGER_GPIO)	+= gpio-charger.o
>  obj-$(CONFIG_CHARGER_MANAGER)	+= charger-manager.o
>  obj-$(CONFIG_CHARGER_LT3651)	+= lt3651-charger.o
> diff --git a/drivers/power/supply/gl-e750-mcu.c b/drivers/power/supply/gl-e750-mcu.c
> new file mode 100644
> index 000000000..37541915e
> --- /dev/null
> +++ b/drivers/power/supply/gl-e750-mcu.c
> @@ -0,0 +1,369 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * GL.iNet GL-E750 (Mudi) status MCU battery driver
> + *
> + * The MCU sits on the SoC UART. It drives the OLED screen, monitors the
> + * battery and watches the power switch. The host sends one JSON object per
> + * line, where every value is a string. When a line contains
> + * "mcu_status": "1", the MCU answers with
> + *
> + *	{OK},<percent>,<temperature>,<charging>,<cycles>
> + *
> + * When the power switch is turned off, the MCU sends {"shut_down": "1"} and
> + * cuts the power a few seconds later.
> + *
> + * Copyright (C) 2026 Serv Pol <servpol@tuta.io>
> + */
> +
> +#include <linux/completion.h>
> +#include <linux/ctype.h>
> +#include <linux/devm-helpers.h>
> +#include <linux/math.h>
> +#include <linux/module.h>
> +#include <linux/power_supply.h>
> +#include <linux/property.h>
> +#include <linux/reboot.h>
> +#include <linux/serdev.h>
> +#include <linux/spinlock.h>
> +#include <linux/string.h>
> +#include <linux/workqueue.h>
> +
> +#define MCU_BAUDRATE		115200
> +#define MCU_RX_LINE_MAX		128
> +#define MCU_POLL_INTERVAL_MS	30000
> +#define MCU_REPLY_TIMEOUT_MS	1500
> +
> +static const u8 mcu_status_req[] = "{ \"mcu_status\": \"1\" }\n";
> +
> +struct gl_e750_mcu {
> +	struct serdev_device *serdev;
> +	struct power_supply *psy;
> +	struct delayed_work poll_work;
> +	struct work_struct poweroff_work;
> +	struct completion reply;
> +
> +	/* receive buffer and battery state, filled from the serdev callback */
> +	spinlock_t lock;
> +	char rx_line[MCU_RX_LINE_MAX];
> +	size_t rx_len;
> +	bool valid;
> +	int capacity;		/* percent */
> +	int temp;		/* tenths of a degree Celsius */
> +	bool charging;
> +	int cycles;
> +};
> +
> +/* "37.4", "40" or "-1.5" -> tenths of a degree */
> +static int mcu_parse_tenths(const char *s, int *out)
> +{
> +	const char *dot = strchr(s, '.');
> +	char whole_str[8];
> +	int whole, frac = 0;
> +
> +	if (!dot) {
> +		if (kstrtoint(s, 10, &whole))
> +			return -EINVAL;
> +		*out = whole * 10;
> +		return 0;
> +	}
> +
> +	if (dot == s || dot - s >= sizeof(whole_str))
> +		return -EINVAL;
> +	memcpy(whole_str, s, dot - s);
> +	whole_str[dot - s] = '\0';
> +	if (kstrtoint(whole_str, 10, &whole))
> +		return -EINVAL;
> +	if (isdigit(dot[1]))
> +		frac = dot[1] - '0';
> +	*out = whole * 10 + (*s == '-' ? -frac : frac);
> +	return 0;
> +}
> +
> +/*
> + * "{OK},<percent>,<temperature>,<charging>,<cycles>", called with the lock
> + * held. Returns true if the line was a valid status reply.
> + */
> +static bool mcu_parse_status(struct gl_e750_mcu *mcu, char *line)
> +{
> +	char *p = line + 4, *f[4];
> +	int capacity, temp, charging, cycles, i;
> +	bool changed;
> +
> +	if (*p++ != ',')
> +		return false;
> +	for (i = 0; i < ARRAY_SIZE(f); i++) {
> +		f[i] = strsep(&p, ",");
> +		if (!f[i] || !*f[i])
> +			return false;
> +	}
> +
> +	if (kstrtoint(f[0], 10, &capacity) || mcu_parse_tenths(f[1], &temp) ||
> +	    kstrtoint(f[2], 10, &charging) || kstrtoint(f[3], 10, &cycles))
> +		return false;
> +	if (capacity < 0 || capacity > 100)
> +		return false;
> +
> +	changed = !mcu->valid || mcu->capacity != capacity ||
> +		  mcu->charging != !!charging || mcu->cycles != cycles ||
> +		  abs(mcu->temp - temp) >= 10;
> +	mcu->capacity = capacity;
> +	mcu->temp = temp;
> +	mcu->charging = !!charging;
> +	mcu->cycles = cycles;
> +	mcu->valid = true;
> +
> +	complete(&mcu->reply);
> +	if (changed)
> +		power_supply_changed(mcu->psy);
> +	return true;
> +}
> +
> +/* called with the lock held */
> +static void mcu_handle_line(struct gl_e750_mcu *mcu, char *line)
> +{
> +	if (!strncmp(line, "{OK}", 4))
> +		mcu_parse_status(mcu, line);
> +	else if (strstr(line, "\"shut_down\""))
> +		schedule_work(&mcu->poweroff_work);
> +	else
> +		dev_dbg_ratelimited(&mcu->serdev->dev, "unhandled: %s\n", line);
> +}
> +
> +/* hand the buffered message over, called with the lock held */
> +static void mcu_flush_line(struct gl_e750_mcu *mcu)
> +{
> +	mcu->rx_line[mcu->rx_len] = '\0';
> +	mcu_handle_line(mcu, mcu->rx_line);
> +	mcu->rx_len = 0;
> +}
> +
> +/* a status reply is waiting in the buffer, called with the lock held */
> +static bool mcu_status_pending(struct gl_e750_mcu *mcu)
> +{
> +	return mcu->rx_len > 4 && !strncmp(mcu->rx_line, "{OK}", 4);
> +}
> +
> +static size_t mcu_receive_buf(struct serdev_device *serdev, const u8 *data,
> +			      size_t count)
> +{
> +	struct gl_e750_mcu *mcu = serdev_device_get_drvdata(serdev);
> +	unsigned long flags;
> +	size_t i;
> +
> +	spin_lock_irqsave(&mcu->lock, flags);
> +	for (i = 0; i < count; i++) {
> +		char c = data[i];
> +
> +		if (c == '\r' || c == '\n') {
> +			if (mcu->rx_len)
> +				mcu_flush_line(mcu);
> +			continue;
> +		}
> +
> +		/*
> +		 * Status replies and JSON events may come without a line
> +		 * ending. Every message starts with '{', so a new one ends
> +		 * the previous message, and a JSON event ends with '}'.
> +		 */
> +		if (c == '{' && mcu->rx_len)
> +			mcu_flush_line(mcu);
> +		if (mcu->rx_len < MCU_RX_LINE_MAX - 1)
> +			mcu->rx_line[mcu->rx_len++] = c;
> +		if (c == '}' && mcu->rx_line[0] == '{' &&
> +		    strncmp(mcu->rx_line, "{OK}", 4))
> +			mcu_flush_line(mcu);
> +	}
> +	spin_unlock_irqrestore(&mcu->lock, flags);
> +
> +	return count;
> +}
> +
> +static const struct serdev_device_ops mcu_serdev_ops = {
> +	.receive_buf = mcu_receive_buf,
> +	.write_wakeup = serdev_device_write_wakeup,
> +};
> +
> +static void mcu_poll_work(struct work_struct *work)
> +{
> +	struct gl_e750_mcu *mcu = container_of(to_delayed_work(work),
> +					       struct gl_e750_mcu, poll_work);
> +	unsigned long timeout = msecs_to_jiffies(MCU_REPLY_TIMEOUT_MS);
> +	unsigned long flags;
> +	int ret;
> +
> +	/*
> +	 * Only an earlier status reply is consumed here; a partially received
> +	 * event stays in the buffer.
> +	 */
> +	spin_lock_irqsave(&mcu->lock, flags);
> +	if (mcu_status_pending(mcu))
> +		mcu_flush_line(mcu);
> +	reinit_completion(&mcu->reply);
> +	spin_unlock_irqrestore(&mcu->lock, flags);
> +
> +	ret = serdev_device_write(mcu->serdev, mcu_status_req,
> +				  sizeof(mcu_status_req) - 1, timeout);
> +	if (ret < 0) {
> +		dev_warn_ratelimited(&mcu->serdev->dev, "write failed: %d\n", ret);
> +	} else if (!wait_for_completion_timeout(&mcu->reply, timeout)) {
> +		/* the reply may come without a line ending */
> +		spin_lock_irqsave(&mcu->lock, flags);
> +		if (mcu_status_pending(mcu))
> +			mcu_flush_line(mcu);
> +		else
> +			dev_dbg(&mcu->serdev->dev, "no status reply\n");
> +		spin_unlock_irqrestore(&mcu->lock, flags);
> +	}
> +
> +	schedule_delayed_work(&mcu->poll_work,
> +			      msecs_to_jiffies(MCU_POLL_INTERVAL_MS));
> +}
> +
> +static void mcu_poweroff_work(struct work_struct *work)
> +{
> +	struct gl_e750_mcu *mcu = container_of(work, struct gl_e750_mcu,
> +					       poweroff_work);
> +
> +	/* the MCU cuts the power a few seconds after reporting the switch */
> +	dev_info(&mcu->serdev->dev, "power switch turned off, shutting down\n");
> +	orderly_poweroff(true);
> +}
> +
> +static const enum power_supply_property mcu_battery_props[] = {
> +	POWER_SUPPLY_PROP_PRESENT,
> +	POWER_SUPPLY_PROP_STATUS,
> +	POWER_SUPPLY_PROP_TECHNOLOGY,
> +	POWER_SUPPLY_PROP_CAPACITY,
> +	POWER_SUPPLY_PROP_TEMP,
> +	POWER_SUPPLY_PROP_CYCLE_COUNT,
> +};
> +
> +static int mcu_battery_get_property(struct power_supply *psy,
> +				    enum power_supply_property psp,
> +				    union power_supply_propval *val)
> +{
> +	struct gl_e750_mcu *mcu = power_supply_get_drvdata(psy);
> +	unsigned long flags;
> +	int ret = 0;
> +
> +	spin_lock_irqsave(&mcu->lock, flags);
> +	switch (psp) {
> +	case POWER_SUPPLY_PROP_PRESENT:
> +		val->intval = 1;
> +		break;
> +	case POWER_SUPPLY_PROP_TECHNOLOGY:
> +		val->intval = POWER_SUPPLY_TECHNOLOGY_LION;
> +		break;
> +	case POWER_SUPPLY_PROP_STATUS:
> +		if (!mcu->valid)
> +			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
> +		else if (mcu->charging)
> +			val->intval = mcu->capacity >= 100 ?
> +				      POWER_SUPPLY_STATUS_FULL :
> +				      POWER_SUPPLY_STATUS_CHARGING;
> +		else
> +			val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
> +		break;
> +	case POWER_SUPPLY_PROP_CAPACITY:
> +		if (mcu->valid)
> +			val->intval = mcu->capacity;
> +		else
> +			ret = -ENODATA;
> +		break;
> +	case POWER_SUPPLY_PROP_TEMP:
> +		if (mcu->valid)
> +			val->intval = mcu->temp;
> +		else
> +			ret = -ENODATA;
> +		break;
> +	case POWER_SUPPLY_PROP_CYCLE_COUNT:
> +		if (mcu->valid)
> +			val->intval = mcu->cycles;
> +		else
> +			ret = -ENODATA;
> +		break;
> +	default:
> +		ret = -EINVAL;
> +	}
> +	spin_unlock_irqrestore(&mcu->lock, flags);
> +
> +	return ret;
> +}
> +
> +static const struct power_supply_desc mcu_battery_desc = {
> +	.name = "gl-e750-battery",
> +	.type = POWER_SUPPLY_TYPE_BATTERY,
> +	.properties = mcu_battery_props,
> +	.num_properties = ARRAY_SIZE(mcu_battery_props),
> +	.get_property = mcu_battery_get_property,
> +};
> +
> +static int gl_e750_mcu_probe(struct serdev_device *serdev)
> +{
> +	struct device *dev = &serdev->dev;
> +	struct power_supply_config psy_cfg = {};
> +	struct gl_e750_mcu *mcu;
> +	int ret;
> +
> +	mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL);
> +	if (!mcu)
> +		return -ENOMEM;
> +
> +	mcu->serdev = serdev;
> +	spin_lock_init(&mcu->lock);
> +	init_completion(&mcu->reply);
> +	serdev_device_set_drvdata(serdev, mcu);
> +	serdev_device_set_client_ops(serdev, &mcu_serdev_ops);
> +
> +	/*
> +	 * devm teardown runs in reverse: the poll work is cancelled before the
> +	 * port is closed, the power-off work (queued from the receive path)
> +	 * and the battery only after it.
> +	 */
> +	psy_cfg.drv_data = mcu;
> +	psy_cfg.fwnode = dev_fwnode(dev);
> +	mcu->psy = devm_power_supply_register(dev, &mcu_battery_desc, &psy_cfg);
> +	if (IS_ERR(mcu->psy))
> +		return dev_err_probe(dev, PTR_ERR(mcu->psy),
> +				     "failed to register battery\n");
> +
> +	ret = devm_work_autocancel(dev, &mcu->poweroff_work, mcu_poweroff_work);
> +	if (ret)
> +		return ret;
> +
> +	ret = devm_serdev_device_open(dev, serdev);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "failed to open serdev\n");
> +
> +	serdev_device_set_baudrate(serdev, MCU_BAUDRATE);
> +	serdev_device_set_flow_control(serdev, false);
> +	ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "failed to set parity\n");
> +
> +	ret = devm_delayed_work_autocancel(dev, &mcu->poll_work, mcu_poll_work);
> +	if (ret)
> +		return ret;
> +
> +	schedule_delayed_work(&mcu->poll_work, 0);
> +	return 0;
> +}
> +
> +static const struct of_device_id gl_e750_mcu_of_match[] = {
> +	{ .compatible = "glinet,gl-e750-mcu" },
> +	{ }
> +};
> +MODULE_DEVICE_TABLE(of, gl_e750_mcu_of_match);
> +
> +static struct serdev_device_driver gl_e750_mcu_driver = {
> +	.probe = gl_e750_mcu_probe,
> +	.driver = {
> +		.name = "gl-e750-mcu",
> +		.of_match_table = gl_e750_mcu_of_match,
> +	},
> +};
> +module_serdev_device_driver(gl_e750_mcu_driver);
> +
> +MODULE_AUTHOR("Serv Pol <servpol@tuta.io>");
> +MODULE_DESCRIPTION("GL.iNet GL-E750 status MCU battery driver");
> +MODULE_LICENSE("GPL");
> 
> -- 
> 2.47.3
> 
> 

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

  reply	other threads:[~2026-11-08 23:36 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-21 19:33 [PATCH v2 0/2] power: supply: Add GL.iNet GL-E750 MCU battery driver Serv Pol
2026-10-21 19:33 ` Serv Pol via B4 Relay
2026-10-21 19:33 ` [PATCH v2 1/2] dt-bindings: power: supply: Add GL.iNet GL-E750 MCU Serv Pol via B4 Relay
2026-10-21 19:33   ` Serv Pol
2026-10-21 19:33 ` [PATCH v2 2/2] power: supply: Add GL.iNet GL-E750 MCU battery driver Serv Pol via B4 Relay
2026-10-21 19:33   ` Serv Pol
2026-11-08 23:35   ` Sebastian Reichel [this message]
2026-11-12 20:24     ` servpol

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=avEHPVdx0NlX4dhy@venus \
    --to=sebastian.reichel@collabora.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=krzk+dt@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-pm@vger.kernel.org \
    --cc=robh@kernel.org \
    --cc=servpol@tuta.io \
    --cc=u.kleine-koenig@baylibre.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.