#include <linux/types.h>
#include <linux/list_sort.h>
#include <linux/slab.h>
#include <linux/mutex.h>
#include <linux/workqueue.h>
#include <linux/usb/typec_altmode.h>
#include "class.h"
struct mode_state {
u16 svid;
u8 priority;
int error;
struct list_head list;
};
struct mode_selection {
struct list_head mode_list;
struct mutex lock;
struct delayed_work work;
struct typec_partner *partner;
u16 active_svid;
unsigned int timeout;
unsigned int delay;
};
struct mode_order {
u16 svid;
int enter;
int result;
};
static int activate_altmode(struct device *dev, void *data)
{
if (is_typec_partner_altmode(dev)) {
struct typec_altmode *alt = to_typec_altmode(dev);
struct mode_order *order = (struct mode_order *)data;
if (order->svid == alt->svid) {
if (alt->ops && alt->ops->activate)
order->result = alt->ops->activate(alt, order->enter);
else
order->result = -EOPNOTSUPP;
return 1;
}
}
return 0;
}
static int mode_selection_activate(struct mode_selection *sel,
const u16 svid, const int enter)
__must_hold(&sel->lock)
{
struct mode_order order = {.svid = svid, .enter = enter, .result = -ENODEV};
mutex_unlock(&sel->lock);
device_for_each_child(&sel->partner->dev, &order, activate_altmode);
mutex_lock(&sel->lock);
return order.result;
}
static void mode_list_clean(struct mode_selection *sel)
{
struct mode_state *ms, *tmp;
list_for_each_entry_safe(ms, tmp, &sel->mode_list, list) {
list_del(&ms->list);
kfree(ms);
}
}
static void mode_selection_work_fn(struct work_struct *work)
{
struct mode_selection *sel = container_of(work,
struct mode_selection, work.work);
struct mode_state *ms;
unsigned int delay = sel->delay;
int result;
guard(mutex)(&sel->lock);
ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
if (!ms)
return;
if (sel->active_svid == ms->svid) {
dev_dbg(&sel->partner->dev, "%x altmode is active\n", ms->svid);
mode_list_clean(sel);
} else if (sel->active_svid != 0) {
result = mode_selection_activate(sel, sel->active_svid, 0);
if (result)
mode_list_clean(sel);
else
sel->active_svid = 0;
} else if (ms->error) {
dev_err(&sel->partner->dev, "%x: entry error %pe\n",
ms->svid, ERR_PTR(ms->error));
mode_selection_activate(sel, ms->svid, 0);
list_del(&ms->list);
kfree(ms);
} else {
result = mode_selection_activate(sel, ms->svid, 1);
if (result) {
dev_err(&sel->partner->dev, "%x: activation error %pe\n",
ms->svid, ERR_PTR(result));
list_del(&ms->list);
kfree(ms);
} else {
delay = sel->timeout;
ms->error = -ETIMEDOUT;
}
}
if (!list_empty(&sel->mode_list))
schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
}
void typec_altmode_state_update(struct typec_partner *partner, const u16 svid,
const int error)
{
struct mode_selection *sel = partner->sel;
struct mode_state *ms;
if (sel) {
mutex_lock(&sel->lock);
ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
if (ms && ms->svid == svid) {
ms->error = error;
if (cancel_delayed_work(&sel->work))
schedule_delayed_work(&sel->work, 0);
}
if (!error)
sel->active_svid = svid;
else
sel->active_svid = 0;
mutex_unlock(&sel->lock);
}
}
EXPORT_SYMBOL_GPL(typec_altmode_state_update);
static int compare_priorities(void *priv,
const struct list_head *a, const struct list_head *b)
{
const struct mode_state *msa = container_of(a, struct mode_state, list);
const struct mode_state *msb = container_of(b, struct mode_state, list);
if (msa->priority < msb->priority)
return -1;
return 1;
}
static int altmode_add_to_list(struct device *dev, void *data)
{
if (is_typec_partner_altmode(dev)) {
struct list_head *list = (struct list_head *)data;
struct typec_altmode *altmode = to_typec_altmode(dev);
const struct typec_altmode *pdev = typec_altmode_get_partner(altmode);
struct mode_state *ms;
if (pdev && altmode->ops && altmode->ops->activate) {
ms = kzalloc_obj(*ms);
if (!ms)
return -ENOMEM;
ms->svid = pdev->svid;
ms->priority = pdev->priority;
INIT_LIST_HEAD(&ms->list);
list_add_tail(&ms->list, list);
}
}
return 0;
}
int typec_mode_selection_start(struct typec_partner *partner,
const unsigned int delay, const unsigned int timeout)
{
struct mode_selection *sel;
int ret;
if (partner->usb_mode == USB_MODE_USB4)
return -EBUSY;
if (partner->sel)
return -EALREADY;
sel = kzalloc_obj(*sel);
if (!sel)
return -ENOMEM;
INIT_LIST_HEAD(&sel->mode_list);
ret = device_for_each_child(&partner->dev, &sel->mode_list,
altmode_add_to_list);
if (ret || list_empty(&sel->mode_list)) {
mode_list_clean(sel);
kfree(sel);
return ret;
}
list_sort(NULL, &sel->mode_list, compare_priorities);
sel->partner = partner;
sel->delay = delay;
sel->timeout = timeout;
mutex_init(&sel->lock);
INIT_DELAYED_WORK(&sel->work, mode_selection_work_fn);
schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
partner->sel = sel;
return 0;
}
EXPORT_SYMBOL_GPL(typec_mode_selection_start);
void typec_mode_selection_delete(struct typec_partner *partner)
{
struct mode_selection *sel = partner->sel;
if (sel) {
partner->sel = NULL;
cancel_delayed_work_sync(&sel->work);
mode_list_clean(sel);
mutex_destroy(&sel->lock);
kfree(sel);
}
}
EXPORT_SYMBOL_GPL(typec_mode_selection_delete);