From 61da76db2f2fc8647e19699107e552f07eb5d539 Mon Sep 17 00:00:00 2001
From: Ondrej Jirman <megi@xff.cz>
Date: Fri, 1 Apr 2022 22:00:11 +0200
Subject: [PATCH 401/463] misc: ppkb-manager: Pinephone Keyboard power manager

This commit adds support for in-kernel power management of Pinephone
Keyboard for Pinephone and Pinephone Pro.

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

diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 41c54051347ab..787a524fac129 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -517,6 +517,13 @@ config NTSYNC
 
 	  If unsure, say N.
 
+config PPKB_POWER_MANAGER
+	tristate "Power manager for Pinephone keyboard."
+	depends on OF
+	help
+	  This driver coordinates Pinephone keyboard power use between Pinephone
+	  keyboard battery and Pinephone battery.
+
 config VCPU_STALL_DETECTOR
 	tristate "Guest vCPU stall detector"
 	depends on OF && HAS_IOMEM
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index c2f990862d2bb..af459c2dfacaa 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -62,6 +62,7 @@ obj-$(CONFIG_HISI_HIKEY_USB)	+= hisi_hikey_usb.o
 obj-$(CONFIG_NTSYNC)		+= ntsync.o
 obj-$(CONFIG_HI6421V600_IRQ)	+= hi6421v600-irq.o
 obj-$(CONFIG_OPEN_DICE)		+= open-dice.o
+obj-$(CONFIG_PPKB_POWER_MANAGER)+= ppkb-manager.o
 obj-$(CONFIG_GP_PCI1XXXX)	+= mchp_pci1xxxx/
 obj-$(CONFIG_VCPU_STALL_DETECTOR)	+= vcpu_stall_detector.o
 obj-$(CONFIG_TMR_MANAGER)      += xilinx_tmr_manager.o
diff --git a/drivers/misc/ppkb-manager.c b/drivers/misc/ppkb-manager.c
new file mode 100644
index 0000000000000..39409c9c0f758
--- /dev/null
+++ b/drivers/misc/ppkb-manager.c
@@ -0,0 +1,943 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Pinephone keyboard power manager driver.
+ *
+ * Ondrej Jirman <megi@xff.cz>
+ */
+
+#define DEBUG
+
+#include <linux/wait.h>
+#include <linux/device.h>
+#include <linux/debugfs.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/leds.h>
+#include <linux/slab.h>
+#include <linux/time.h>
+#include <linux/reboot.h>
+#include <linux/power_supply.h>
+
+#define DRIVER_NAME "ppkb-power"
+
+enum {
+	KBPWR_F_DISABLED,
+	KBPWR_F_EMERGENCY_SHUTDOWN,
+	KBPWR_F_BLOCKED,
+};
+
+enum {
+	KBPWR_LED_TRIGGER_KB_VOUT_ON,
+	KBPWR_LED_TRIGGER_KB_VIN_PRESENT,
+	KBPWR_LED_TRIGGER_KB_OFFLINE,
+	KBPWR_LED_TRIGGER_CAPACITY,
+
+	KBPWR_LED_TRIGGER_COUNT,
+};
+
+static const char *trig_names[] = {
+	"kbpwr-kb-vout-on",
+	"kbpwr-kb-vin-present",
+	"kbpwr-kb-offline",
+	"kbpwr-capacity",
+};
+
+struct kbpwr_status {
+	int kb_cap; // capacity in %  (when -1, keyboard charger is
+		    // not accessible, and no kb_* properties are valid)
+	int kb_cur; // current + charging, - discharging
+	int kb_vol; // voltage at the battery terminals
+	int kb_vol_ocv; // OCV voltage
+	int kb_chg_behavior; // (writable) kb battery charger auto=0/inhibited=1
+	int kb_cal; // (writable) battery internal resistance calibration value in mOhm
+	int kb_out; // 5V output enabled/disabled
+	int kb_in; // supply to VIN is connected
+	int kb_max_uwh; // kb battery uWh total capacity
+
+	int ph_cap; // capacity in %
+	int ph_cur; // current (direction determined by ph_chg_status)
+	int ph_vol; // voltage at the battery terminals
+	int ph_chg_status; // POWER_SUPPLY_STATUS_CHARGING = charging,
+			   // other statuses = discharging  (use it to
+			   // interpret meaning of abs(ph_cur))
+	int ph_chg_cur_limit; // (writable) max charging current for phone battery
+	int ph_chg_behavior; // (writable) phone charger auto=0/inhibited=1
+
+	int ph_inp_present; // phone USB supply input is present
+	int ph_inp_en; // (writable) phone USB supply input is used for powering the phone
+	int ph_inp_limit; // (writable) input current limit on phone's VBUS
+	int ph_max_uwh; // phone battery uWh total capacity
+
+	ktime_t ts;
+};
+
+// constants based on device type
+struct kbpwr_machine {
+	int inp_limit_normal;
+	int inp_limit_mid;
+	int inp_limit_high;
+	int chg_limit_high;
+	int chg_limit_low;
+	bool (*has_prop)(const char* name);
+};
+
+struct kbpwr_dev {
+	struct device *dev;
+	struct dentry *debug_root;
+
+	unsigned long flags[1];
+	struct mutex lock;
+
+	struct power_supply *phone_battery;
+	struct power_supply *phone_usb;
+
+	struct power_supply *kb_battery;
+	struct power_supply *kb_boost;
+	struct power_supply *kb_usb;
+
+	struct workqueue_struct *wq;
+	struct delayed_work work;
+
+	struct led_trigger trigger[KBPWR_LED_TRIGGER_COUNT];
+
+	struct kbpwr_status last_status;
+	ktime_t ph_low_until;
+	ktime_t shutdown_after;
+
+	// total state of the battery system
+	int capacity_total_uwh;
+	int capacity_uwh;
+	int capacity_pct;
+	int power_uw;
+	int time_left;
+
+	// kb rint calibration
+	ktime_t rint_valid_until;
+	int kb_vol_now, kb_cur_now, rint;
+
+	const struct kbpwr_machine* mach;
+};
+
+static bool kbpwr_has_prop_pp(const char* name)
+{
+	return true;
+}
+
+static bool kbpwr_has_prop_ppp(const char* name)
+{
+	return strcmp(name, "ph_inp_en");
+}
+
+static const struct kbpwr_machine kbpwr_pp = {
+	.inp_limit_normal = 500000,
+	.inp_limit_mid = 1000000,
+	.inp_limit_high = 1500000,
+	.chg_limit_high = 1200000,
+	.chg_limit_low = 200000,
+	.has_prop = kbpwr_has_prop_pp,
+};
+
+static const struct kbpwr_machine kbpwr_ppp = {
+	.inp_limit_normal = 450000,
+	.inp_limit_mid = 850000,
+	.inp_limit_high = 1500000,
+	.chg_limit_high = 1200000,
+	.chg_limit_low = 1000000,
+	.has_prop = kbpwr_has_prop_ppp,
+};
+
+static void kbpwr_uevent(struct kbpwr_dev *kbpwr, const char* name)
+{
+	char *env[] = {
+		"DRIVER=" DRIVER_NAME,
+		NULL,
+		NULL,
+	};
+
+	env[1] = kasprintf(GFP_KERNEL, "POWER_EVENT=%s", name);
+	if (!env[1])
+		return;
+
+	kobject_uevent_env(&kbpwr->dev->kobj, KOBJ_CHANGE, env);
+
+	kfree(env[1]);
+}
+
+#define STATUS_PROP(member, sup, sup_prop) \
+	{ &s->member, #member, kbpwr->sup, sup_prop, },
+
+static int kbpwr_snaphost(struct kbpwr_dev *kbpwr, struct kbpwr_status* s)
+{
+	bool kb_fail = false;
+	int i, j, ret;
+	struct {
+		int *out;
+		const char* name;
+		struct power_supply *psy;
+		enum power_supply_property prop;
+	} props[] = {
+		STATUS_PROP(kb_cap, kb_battery, POWER_SUPPLY_PROP_CAPACITY)
+		STATUS_PROP(kb_cur, kb_battery, POWER_SUPPLY_PROP_CURRENT_NOW)
+		STATUS_PROP(kb_vol, kb_battery, POWER_SUPPLY_PROP_VOLTAGE_NOW)
+		STATUS_PROP(kb_vol_ocv, kb_battery, POWER_SUPPLY_PROP_VOLTAGE_OCV)
+		STATUS_PROP(kb_cal, kb_battery, POWER_SUPPLY_PROP_CALIBRATE)
+		STATUS_PROP(kb_chg_behavior, kb_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)
+		STATUS_PROP(kb_max_uwh, kb_battery, POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN)
+
+		STATUS_PROP(kb_out, kb_boost, POWER_SUPPLY_PROP_ONLINE)
+
+		STATUS_PROP(kb_in, kb_usb, POWER_SUPPLY_PROP_PRESENT)
+
+		STATUS_PROP(ph_cap, phone_battery, POWER_SUPPLY_PROP_CAPACITY)
+		STATUS_PROP(ph_cur, phone_battery, POWER_SUPPLY_PROP_CURRENT_NOW)
+		STATUS_PROP(ph_vol, phone_battery, POWER_SUPPLY_PROP_VOLTAGE_NOW)
+		STATUS_PROP(ph_chg_status, phone_battery, POWER_SUPPLY_PROP_STATUS)
+		STATUS_PROP(ph_chg_cur_limit, phone_battery, POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT)
+		STATUS_PROP(ph_chg_behavior, phone_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)
+		STATUS_PROP(ph_max_uwh, phone_battery, POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN)
+
+		STATUS_PROP(ph_inp_present, phone_usb, POWER_SUPPLY_PROP_PRESENT)
+		STATUS_PROP(ph_inp_en, phone_usb, POWER_SUPPLY_PROP_ONLINE)
+		STATUS_PROP(ph_inp_limit, phone_usb, POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT)
+	};
+
+	dev_dbg(kbpwr->dev, "snapshot:\n");
+
+	for (i = 0; i < ARRAY_SIZE(props); i++) {
+		union power_supply_propval val = {0,};
+
+		if (!kbpwr->mach->has_prop(props[i].name)) {
+			*props[i].out = -1;
+			continue;
+		}
+
+		/*
+		 * Skip reading kb_* properties after the first failure.
+		 */
+		if (strstarts(props[i].name, "kb_") && kb_fail)
+			continue;
+
+		ret = power_supply_get_property(props[i].psy, props[i].prop, &val);
+		if (ret) {
+			/*
+			 * Failure to read kb_* properties is expected and
+			 * common. When it happens, we clear all the kb_
+			 * properties, so that algorithm behaves as if keyboard
+			 * charger is sleeping.
+			 */
+			if (strstarts(props[i].name, "kb_")) {
+				kb_fail = true;
+				for (j = 0; j < ARRAY_SIZE(props); j++)
+					if (strstarts(props[j].name, "kb_"))
+						*props[j].out = -1;
+				continue;
+			} else {
+				/*
+				 * Other properties should never fail to read,
+				 * so make that a fatal issue.
+				 */
+				dev_err(kbpwr->dev, "Can't read %s\n", props[i].name);
+				return -1;
+			}
+		}
+
+		*props[i].out = val.intval;
+
+		dev_dbg(kbpwr->dev, "  %s = %d\n", props[i].name, val.intval);
+	}
+
+	s->ts = ktime_get();
+
+	return 0;
+}
+
+#define UPDATE_PROP(member, sup, sup_prop) \
+	{ &prev->member, &cur->member, #member, kbpwr->sup, sup_prop, },
+
+static int kbpwr_update(struct kbpwr_dev *kbpwr,
+			struct kbpwr_status* prev,
+			struct kbpwr_status* cur)
+{
+	bool updated = false;
+	int i, ret;
+	struct {
+		int *cmp;
+		int *out;
+		const char* name;
+		struct power_supply *psy;
+		enum power_supply_property prop;
+	} props[] = {
+		UPDATE_PROP(kb_chg_behavior, kb_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)
+		UPDATE_PROP(kb_cal, kb_battery, POWER_SUPPLY_PROP_CALIBRATE)
+		UPDATE_PROP(ph_chg_cur_limit, phone_battery, POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT)
+		UPDATE_PROP(ph_chg_behavior, phone_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)
+		UPDATE_PROP(ph_inp_en, phone_usb, POWER_SUPPLY_PROP_ONLINE)
+		UPDATE_PROP(ph_inp_limit, phone_usb, POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT)
+	};
+
+	// check if there are changes
+	for (i = 0; i < ARRAY_SIZE(props); i++) {
+		if (!kbpwr->mach->has_prop(props[i].name))
+			continue;
+
+		if (*props[i].out != *props[i].cmp) {
+			dev_dbg(kbpwr->dev, "updating:\n");
+			break;
+		}
+	}
+
+	for (i = 0; i < ARRAY_SIZE(props); i++) {
+		union power_supply_propval val = {0,};
+
+		if (!kbpwr->mach->has_prop(props[i].name))
+			continue;
+
+		if (*props[i].out == *props[i].cmp)
+			continue;
+
+		val.intval = *props[i].out;
+
+		/*
+		 * Error handling here is "do as much as we can". Any write
+		 * issue will hopefully be corrected on the next iteration
+		 * of the polling algorithm.
+		 */
+		ret = power_supply_set_property(props[i].psy, props[i].prop, &val);
+		if (ret) {
+			dev_warn(kbpwr->dev, "Can't write %s\n", props[i].name);
+			continue;
+		}
+
+		updated = true;
+		dev_dbg(kbpwr->dev, "  %s = %d\n", props[i].name, val.intval);
+	}
+
+	if (updated)
+		kbpwr_uevent(kbpwr, "update");
+
+	return 0;
+}
+
+static int kbpwr_handle_critical(struct kbpwr_dev *kbpwr)
+{
+	kbpwr_uevent(kbpwr, "critical");
+
+	if (!kbpwr->shutdown_after)
+		kbpwr->shutdown_after = ktime_add_ms(ktime_get(), 60000);
+
+	if (ktime_after(ktime_get(), kbpwr->shutdown_after)) {
+		dev_emerg(kbpwr->dev,
+			  "critically low capacity reached\n");
+
+		//hw_protection_shutdown("Critical capacity", 30000);
+		//set_bit(KBPWR_F_BLOCKED, kbpwr->flags);
+		return true;
+	}
+
+	return false;
+}
+
+static void kbpwr_work(struct work_struct *work)
+{
+	struct kbpwr_dev *kbpwr = container_of(work, struct kbpwr_dev, work.work);
+	unsigned long delay_on, delay_off;
+	struct kbpwr_status cur, upd, prev;
+	int ret;
+
+	if (test_bit(KBPWR_F_DISABLED, kbpwr->flags))
+		return;
+	if (test_bit(KBPWR_F_BLOCKED, kbpwr->flags))
+		return;
+
+	mutex_lock(&kbpwr->lock);
+
+	ret = kbpwr_snaphost(kbpwr, &cur);
+	if (ret)
+		goto out_try_later;
+
+	prev = kbpwr->last_status.ts ? kbpwr->last_status : cur;
+	upd = cur;
+	kbpwr->last_status = cur;
+
+	/*
+	 * We calculate keyboard battery internal resistance based on captured
+	 * keyboard current/voltage at two differnt times after the current
+	 * changes by a largish degree.
+	 */
+	if (!kbpwr->rint_valid_until ||
+	    ktime_after(ktime_get(), kbpwr->rint_valid_until)) {
+		kbpwr->kb_vol_now = cur.kb_vol;
+		kbpwr->kb_cur_now = cur.kb_cur;
+		kbpwr->rint_valid_until = ktime_add_ms(ktime_get(),
+						       5 * 60 * 1000);
+	} else {
+		s64 diff_vol = cur.kb_vol - kbpwr->kb_vol_now;
+		s64 diff_cur = cur.kb_cur - kbpwr->kb_cur_now;
+
+		if (abs(diff_cur) > 150000) {
+			s64 rint = diff_vol * 1000 / diff_cur;
+			if (rint > 30 && rint < 1000) {
+				dev_warn(kbpwr->dev,
+					 "calibrating rint=%lld mOhm\n", rint);
+				kbpwr->rint = rint;
+				upd.kb_cal = rint;
+			}
+
+			kbpwr->kb_vol_now = cur.kb_vol;
+			kbpwr->kb_cur_now = cur.kb_cur;
+			kbpwr->rint_valid_until = ktime_add_ms(ktime_get(),
+							       5 * 60 * 1000);
+		}
+	}
+
+	/*
+	 * The algorithm here tries to ensure that:
+	 *
+	 * When the power supply is plugged into the keyboard:
+	 *
+	 * 1) Phone's internal battery is charged as fast as possible
+	 * 2) When the internal battery is fully charged, keyboard battery starts charging, while 
+	 *    still supplying enough power to the phone so that internal battery doesn't start
+	 *    discharging, until both batteries are fully charged.
+	 *
+	 * When it's unplugged:
+	 *
+	 * 1) Keyboard battery discharges first, preserving phone battery
+	 *    as much as possible.
+	 * 2) Phone battery starts discharging after the keyboard battery
+	 *    is emptied.
+	 *
+	 * There are a few corner cases handled:
+	 *
+	 * - It's not a great thing to drain the batteries completely, since Pinephone Pro
+	 *   can't recover from this state gracefully, and keyboard also has some issues
+	 *   with it, requiring prolonged trickle charging, etc.
+	 *   - The driver tries to keep some residual charge in both batteries.
+	 * - On phone power off, keyboard charger output is turned off, so that:
+	 *   - Pinephone Pro can be turned off (it can't with voltage present on VBUS)
+	 *   - Keyboard battery will not keep charging the phone for no reason.
+	 *   - This is only done when the keyboard battery is not plugged in to a power supply.
+	 * - On suspend/resume:
+	 *   - Phone battery charger and keyboard battery output are turned off.
+	 *
+	 * LED trigger:
+	 *
+	 * The driver provides a LED trigger to communicate to the user that keyboard
+	 * power button should be pressed to enable the keyboard charger.
+	 */
+
+	/* check and update the situation */
+
+	if (cur.kb_cap < 0) {
+		// keyboard charger is sleeping or no keyboard is detected
+
+                //XXX: check for lack of keyboard
+
+		// restore sane defaults (only sensible if phone is in the
+		// keyboard)
+		upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
+		upd.ph_chg_cur_limit = kbpwr->mach->chg_limit_high;
+		upd.ph_inp_limit = kbpwr->mach->inp_limit_normal;
+	} else {
+		// keyboard is connected to the phone
+		bool kb_in_change = cur.kb_in != prev.kb_in;
+
+		if (cur.kb_in) {
+			// keyboard is connected to USB PSU (we are charging)
+			bool kb_chg, ph_chg;
+
+			/*
+			 * Make ph_low comparison stick for 5 minutes in low
+			 * postition, once it crosses the threshold, unless
+			 * kb_in just changed.
+			 */
+			bool ph_low = kb_in_change ? false : ktime_before(ktime_get(), kbpwr->ph_low_until);
+                        if (!ph_low) {
+				ph_low = cur.ph_cap < 80;
+				if (ph_low)
+					kbpwr->ph_low_until = ktime_add_ms(ktime_get(), 5 * 60000);
+				else
+					kbpwr->ph_low_until = 0;
+			}
+
+			kb_chg = !ph_low;
+			ph_chg = ph_low || cur.kb_cap > 90;
+
+			upd.kb_out = 1;
+			upd.ph_inp_en = 1;
+
+			if (ph_chg) {
+				// charge the phone
+				upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
+				upd.ph_chg_cur_limit = kbpwr->mach->chg_limit_high;
+				upd.ph_inp_limit = kbpwr->mach->inp_limit_high;
+			} else {
+				// supply the phone, but don't charge it
+				upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
+				upd.ph_chg_cur_limit = kbpwr->mach->chg_limit_low;
+				upd.ph_inp_limit = kbpwr->mach->inp_limit_mid;
+			}
+
+			// charge the keyboard when the KB battery is low or
+			// phone battery is high
+			if (kb_chg) {
+				upd.kb_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
+			} else {
+				upd.kb_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
+			}
+		} else {
+			// keyboard is mobile (we're discharging)
+			//
+			// Generally we want to avoid shifting charge between the phone
+			// and keyboard batteries, so we disable the phone charger in this
+			// situation and set high input current limit, so that the phone
+			// is primarily supplied from the keyboard
+			bool kb_low = cur.kb_vol < 3100000;
+
+			/*
+			 * Make ph_low comparison stick for 5 minutes in low
+			 * postition, once it crosses the threshold, unless
+			 * kb_in just changed.
+			 */
+			bool ph_low = kb_in_change ? false : ktime_before(ktime_get(), kbpwr->ph_low_until);
+                        if (!ph_low) {
+				ph_low = cur.ph_cap < 10;
+				if (ph_low)
+					kbpwr->ph_low_until = ktime_add_ms(ktime_get(), 5 * 60000);
+				else
+					kbpwr->ph_low_until = 0;
+			}
+
+			//                 kb_out
+			// kb_low ph_low | ph_inp_en  ph_chg
+			// 0      0      |    1         0
+			// 0      1      |    1         1
+			// 1      1      |    1         1
+			// 1      0      |    0         0
+
+			upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
+			upd.ph_inp_limit = kbpwr->mach->inp_limit_high;
+			upd.ph_inp_en = 1;
+			upd.kb_out = 1;
+
+			// charge phone battery a little if it's charge is too low (we
+			// need to keep the phone battery somewhat charged at all times,
+			// if possible)
+			if (ph_low) {
+				upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
+			}
+
+			if (kb_low && !ph_low) {
+				upd.ph_inp_en = 0;
+				upd.kb_out = 0;
+				upd.ph_inp_limit = kbpwr->mach->inp_limit_normal;
+			}
+		}
+	}
+
+	kbpwr->capacity_total_uwh = cur.ph_max_uwh +
+		(cur.kb_cap >= 0 ? cur.kb_max_uwh : 0);
+	kbpwr->capacity_uwh = cur.ph_max_uwh * cur.ph_cap / 100 +
+		(cur.kb_cap >= 0 ? cur.kb_max_uwh * cur.kb_cap / 100 : 0);
+	kbpwr->capacity_pct = kbpwr->capacity_uwh /
+		(kbpwr->capacity_total_uwh / 100);
+
+	kbpwr->power_uw = (cur.ph_cur / 1000) * (cur.ph_vol / 1000);
+	if (cur.kb_cap >= 0)
+		kbpwr->power_uw += (cur.kb_cur / 1000) * (cur.kb_vol / 1000);
+
+	kbpwr->time_left = kbpwr->power_uw > 0 ?
+		kbpwr->capacity_total_uwh - kbpwr->capacity_uwh :
+		kbpwr->capacity_uwh;
+	kbpwr->time_left *= 60;
+	kbpwr->time_left /= abs(kbpwr->power_uw);
+
+	// critical shutdown handler
+
+	if (kbpwr->power_uw < 0 && kbpwr->capacity_pct < 5) {
+		if (kbpwr_handle_critical(kbpwr))
+			goto out_unlock;
+	} else {
+		kbpwr->shutdown_after = 0;
+	}
+
+	// update LED triggers
+
+	// capacity
+	if (kbpwr->power_uw > 0) {
+		if (kbpwr->capacity_pct > 95) {
+			delay_on = 500; delay_off = 0;
+		} else {
+			delay_on = delay_off = 500;
+		}
+	} else if (kbpwr->capacity_pct < 5) {
+		delay_on = delay_off = 100;
+	} else if (kbpwr->capacity_pct < 10) {
+		delay_on = 100; delay_off = 400;
+	} else {
+		delay_on = 0; delay_off = 100;
+	}
+
+	led_trigger_blink(&kbpwr->trigger[KBPWR_LED_TRIGGER_CAPACITY],
+			  delay_on, delay_off);
+
+	led_trigger_event(&kbpwr->trigger[KBPWR_LED_TRIGGER_KB_VOUT_ON],
+			  cur.kb_out > 0 ? LED_FULL : LED_OFF);
+
+	led_trigger_event(&kbpwr->trigger[KBPWR_LED_TRIGGER_KB_VIN_PRESENT],
+			  cur.kb_in > 0 ? LED_FULL : LED_OFF);
+
+	led_trigger_event(&kbpwr->trigger[KBPWR_LED_TRIGGER_KB_OFFLINE],
+			  cur.kb_cap < 0 ? LED_FULL : LED_OFF);
+
+	kbpwr_update(kbpwr, &cur, &upd);
+	kbpwr_uevent(kbpwr, "refresh");
+
+out_try_later:
+	queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(10000));
+out_unlock:
+	mutex_unlock(&kbpwr->lock);
+}
+
+static ssize_t help_show(struct device *dev,
+			 struct device_attribute *attr, char *buf)
+{
+	return scnprintf(buf, PAGE_SIZE,
+		"Pinephone Keyboard Power Manager\n"
+		"================================\n"
+		"disabled - enable/disable the power manager\n"
+		"shutdown - enable/disable emergency shutdown on low capacity\n"
+		"help     - this help file\n"
+	);
+}
+
+static ssize_t disabled_store(struct device *dev,
+			      struct device_attribute *attr,
+			      const char *buf, size_t len)
+{
+	struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev));
+	bool val;
+	int ret;
+
+	ret = kstrtobool(buf, &val);
+	if (ret)
+		return ret;
+
+	if (val) {
+		set_bit(KBPWR_F_DISABLED, kbpwr->flags);
+		cancel_delayed_work_sync(&kbpwr->work);
+		kbpwr->ph_low_until = 0;
+		kbpwr->shutdown_after = 0;
+	} else {
+		clear_bit(KBPWR_F_DISABLED, kbpwr->flags);
+		queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(1000));
+		//XXX: do we want to put the system into some particular state?
+	}
+
+	return len;
+}
+
+static ssize_t disabled_show(struct device *dev,
+			     struct device_attribute *attr, char *buf)
+{
+	struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev));
+
+	return scnprintf(buf, PAGE_SIZE, "%d\n",
+			 !!test_bit(KBPWR_F_DISABLED, kbpwr->flags));
+}
+
+static ssize_t emergency_store(struct device *dev,
+			       struct device_attribute *attr,
+			       const char *buf, size_t len)
+{
+	struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev));
+	bool val;
+	int ret;
+
+	ret = kstrtobool(buf, &val);
+	if (ret)
+		return ret;
+
+	if (val)
+		set_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags);
+	else
+		clear_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags);
+
+	return len;
+}
+
+static ssize_t emergency_show(struct device *dev,
+			      struct device_attribute *attr, char *buf)
+{
+	struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev));
+
+	return scnprintf(buf, PAGE_SIZE, "%d\n",
+			 !!test_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags));
+}
+
+static DEVICE_ATTR_RO(help);
+static DEVICE_ATTR_RW(disabled);
+static DEVICE_ATTR_RW(emergency);
+
+static struct attribute *kbpwr_attrs[] = {
+	&dev_attr_help.attr,
+	&dev_attr_disabled.attr,
+	&dev_attr_emergency.attr,
+	NULL,
+};
+
+static const struct attribute_group kbpwr_group = {
+	.attrs = kbpwr_attrs,
+};
+
+static void devm_power_supply_put(struct device *dev, void *res)
+{
+	struct power_supply **psy = res;
+
+	power_supply_put(*psy);
+}
+
+static struct power_supply *devm_power_supply_get_by_name(struct device *dev,
+						   const char *name)
+{
+	struct power_supply **ptr, *psy;
+
+	ptr = devres_alloc(devm_power_supply_put, sizeof(*ptr), GFP_KERNEL);
+	if (!ptr)
+		return ERR_PTR(-ENOMEM);
+
+	psy = power_supply_get_by_name(name);
+	if (IS_ERR_OR_NULL(psy)) {
+		devres_free(ptr);
+	} else {
+		*ptr = psy;
+		devres_add(dev, ptr);
+	}
+
+	return psy;
+}
+
+static int kbpwr_status_show(struct seq_file *s, void *data)
+{
+	struct kbpwr_dev *kbpwr = s->private;
+	struct kbpwr_status st;
+
+	mutex_lock(&kbpwr->lock);
+	st = kbpwr->last_status;
+	mutex_unlock(&kbpwr->lock);
+
+	seq_printf(s, "{\n");
+
+#define SHOW_PROP(name) \
+	seq_printf(s, "\t\"" #name "\": %d,\n", st.name)
+
+	SHOW_PROP(kb_cap);
+	SHOW_PROP(kb_cur);
+	SHOW_PROP(kb_vol);
+	SHOW_PROP(kb_vol_ocv);
+	SHOW_PROP(kb_chg_behavior);
+	SHOW_PROP(kb_cal);
+	SHOW_PROP(kb_out);
+	SHOW_PROP(kb_in);
+	SHOW_PROP(kb_max_uwh);
+	SHOW_PROP(ph_cap);
+	SHOW_PROP(ph_cur);
+	SHOW_PROP(ph_vol);
+	SHOW_PROP(ph_chg_status);
+	SHOW_PROP(ph_chg_cur_limit);
+	SHOW_PROP(ph_chg_behavior);
+	SHOW_PROP(ph_inp_present);
+	SHOW_PROP(ph_inp_en);
+	SHOW_PROP(ph_inp_limit);
+	SHOW_PROP(ph_max_uwh);
+
+	seq_printf(s, "\t\"disabled\": %s,\n",
+		   test_bit(KBPWR_F_DISABLED, kbpwr->flags) ? "true" : "false");
+	seq_printf(s, "\t\"blocked\": %s,\n",
+		   test_bit(KBPWR_F_BLOCKED, kbpwr->flags) ? "true" : "false");
+	seq_printf(s, "\t\"emergency_shutdown_enable\": %s,\n",
+		   test_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags) ? "true" : "false");
+
+	seq_printf(s, "\t\"capacity_total_uwh\": %d,\n", kbpwr->capacity_total_uwh);
+	seq_printf(s, "\t\"capacity_uwh\": %d,\n", kbpwr->capacity_uwh);
+	seq_printf(s, "\t\"capacity_pct\": %d,\n", kbpwr->capacity_pct);
+	seq_printf(s, "\t\"power_uw\": %d,\n", kbpwr->power_uw);
+	seq_printf(s, "\t\"time_left\": %d,\n", kbpwr->time_left);
+
+	seq_printf(s, "\t\"ts\": %lld\n", st.ts / 1000000);
+
+	seq_printf(s, "}\n");
+
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(kbpwr_status);
+
+static int kbpwr_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device_node *np = dev->of_node;
+	struct kbpwr_dev *kbpwr;
+	int ret, i;
+
+	kbpwr = devm_kzalloc(dev, sizeof(*kbpwr), GFP_KERNEL);
+	if (!kbpwr)
+		return -ENOMEM;
+
+	if (of_machine_is_compatible("pine64,pinephone-pro") > 0) {
+		kbpwr->mach = &kbpwr_ppp;
+	} else if (of_machine_is_compatible("pine64,pinephone") > 0) {
+		kbpwr->mach = &kbpwr_pp;
+	} else {
+		return dev_err_probe(dev, -EINVAL, "unsupported machine\n");
+	}
+
+	kbpwr->dev = dev;
+	mutex_init(&kbpwr->lock);
+	INIT_DELAYED_WORK(&kbpwr->work, kbpwr_work);
+	platform_set_drvdata(pdev, kbpwr);
+
+	struct {
+		const char* prop;
+		struct power_supply **psy;
+	} supplies[] = {
+		{ "phone-battery", &kbpwr->phone_battery, },
+		{ "phone-usb", &kbpwr->phone_usb, },
+		{ "kb-battery", &kbpwr->kb_battery, },
+		{ "kb-boost", &kbpwr->kb_boost, },
+		{ "kb-usb", &kbpwr->kb_usb, },
+	};
+
+	for (i = 0; i < ARRAY_SIZE(supplies); i++) {
+		const char* prop = supplies[i].prop;
+		struct power_supply** psy = supplies[i].psy;
+		const char* name;
+
+		ret = of_property_read_string(np, prop, &name);
+		if (ret)
+			return dev_err_probe(dev, ret, "Can't find supply name for %s\n", prop);
+
+		*psy = devm_power_supply_get_by_name(dev, name);
+		if (IS_ERR_OR_NULL(*psy))
+			return dev_err_probe(dev, -EPROBE_DEFER,
+					     "Couldn't get '%s' power supply\n", name);
+	}
+
+	ret = devm_device_add_group(dev, &kbpwr_group);
+	if (ret)
+		return ret;
+
+	for (i = 0; i < KBPWR_LED_TRIGGER_COUNT; i++) {
+		kbpwr->trigger[i].name = trig_names[i];
+
+		ret = devm_led_trigger_register(dev, &kbpwr->trigger[i]);
+		if (ret)
+			return dev_err_probe(dev, ret, "failed to register LED trigger %s\n",
+					     kbpwr->trigger[i].name);
+	}
+
+	kbpwr->wq = alloc_ordered_workqueue("ppkb-power-wq", 0);
+	if (!kbpwr->wq)
+		return dev_err_probe(dev, -ENOMEM, "failed to allocate workqueue\n");
+
+	kbpwr->debug_root = debugfs_create_dir("kbpwr", NULL);
+	debugfs_create_file("state", 0444, kbpwr->debug_root, kbpwr,
+			    &kbpwr_status_fops);
+
+	dev_info(dev, "Pinephone keyboard power manager ready\n");
+
+	set_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags);
+	if (of_property_read_bool(np, "blocked"))
+		set_bit(KBPWR_F_BLOCKED, kbpwr->flags);
+
+	queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(10000));
+
+	return 0;
+}
+
+static void kbpwr_remove(struct platform_device *pdev)
+{
+	struct kbpwr_dev *kbpwr = platform_get_drvdata(pdev);
+
+	cancel_delayed_work_sync(&kbpwr->work);
+
+	mutex_lock(&kbpwr->lock);
+	//XXX: turn off charging from kb? turn off VOUT if possible
+	mutex_unlock(&kbpwr->lock);
+
+	destroy_workqueue(kbpwr->wq);
+
+	debugfs_remove(kbpwr->debug_root);
+}
+
+static void kbpwr_shutdown(struct platform_device *pdev)
+{
+	struct kbpwr_dev *kbpwr = platform_get_drvdata(pdev);
+
+	cancel_delayed_work_sync(&kbpwr->work);
+
+	mutex_lock(&kbpwr->lock);
+	//XXX: turn off charging from kb? turn off VOUT if possible
+	mutex_unlock(&kbpwr->lock);
+}
+
+static int __maybe_unused kbpwr_suspend(struct device *dev)
+{
+	struct kbpwr_dev *kbpwr = dev_get_drvdata(dev);
+	int ret = 0;
+
+	cancel_delayed_work_sync(&kbpwr->work);
+
+	mutex_lock(&kbpwr->lock);
+	//XXX: turn off charging from kb?
+	mutex_unlock(&kbpwr->lock);
+
+	return ret;
+}
+
+static int __maybe_unused kbpwr_resume(struct device *dev)
+{
+	struct kbpwr_dev *kbpwr = dev_get_drvdata(dev);
+	int ret = 0;
+
+	//XXX: during quick suspend/resume cycles the work may never run
+
+	// schedule update soon
+	queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(5000));
+
+	return ret;
+}
+
+static const struct dev_pm_ops kbpwr_pm_ops = {
+	SET_SYSTEM_SLEEP_PM_OPS(kbpwr_suspend, kbpwr_resume)
+};
+
+static const struct of_device_id kbpwr_of_match[] = {
+	{ .compatible = "megi,pinephone-keyboard-power-manager" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, kbpwr_of_match);
+
+static struct platform_driver kbpwr_driver = {
+	.probe = kbpwr_probe,
+	.remove = kbpwr_remove,
+	.shutdown = kbpwr_shutdown,
+	.driver = {
+		.name = DRIVER_NAME,
+		.of_match_table = kbpwr_of_match,
+		.pm = &kbpwr_pm_ops,
+	},
+};
+
+module_platform_driver(kbpwr_driver);
+
+MODULE_DESCRIPTION("Pinephone keyboard power manager");
+MODULE_AUTHOR("Ondrej Jirman <megi@xff.cz>");
+MODULE_LICENSE("GPL v2");
-- 
2.46.0

