623 lines
15 KiB
C
Executable File
623 lines
15 KiB
C
Executable File
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (c) 2021, The Linux Foundation. All rights reserved.
|
|
* Copyright (c) 2022-2024, Qualcomm Innovation Center, Inc. All rights reserved.
|
|
*/
|
|
|
|
#define pr_fmt(msg) "slatersb: %s: " msg, __func__
|
|
#include "slatersb.h"
|
|
#include <linux/remoteproc/qcom_rproc.h>
|
|
|
|
struct slatersb_priv {
|
|
void *handle;
|
|
struct mutex glink_mutex;
|
|
struct mutex rsb_state_mutex;
|
|
enum slatersb_state slatersb_current_state;
|
|
void *lhndl;
|
|
char rx_buf[SLATERSB_GLINK_INTENT_SIZE];
|
|
struct work_struct slate_up_work;
|
|
struct work_struct slate_down_work;
|
|
struct work_struct rsb_up_work;
|
|
struct work_struct rsb_down_work;
|
|
struct work_struct rsb_calibration_work;
|
|
struct work_struct bttn_configr_work;
|
|
struct workqueue_struct *slatersb_wq;
|
|
void *slate_subsys_handle;
|
|
struct completion wrk_cmplt;
|
|
struct completion slate_lnikup_cmplt;
|
|
struct completion tx_done;
|
|
struct device *ldev;
|
|
struct wakeup_source *slatersb_ws;
|
|
wait_queue_head_t link_state_wait;
|
|
uint32_t calbrtion_intrvl;
|
|
uint32_t calbrtion_cpi;
|
|
uint8_t bttn_configs;
|
|
bool slate_resp_cmplt;
|
|
bool rsb_rpmsg;
|
|
bool is_in_twm;
|
|
bool calibration_needed;
|
|
bool is_calibrd;
|
|
bool is_cnfgrd;
|
|
bool blk_rsb_cmnds;
|
|
bool pending_enable;
|
|
};
|
|
|
|
static void *slatersb_drv;
|
|
static int slatersb_enable(struct slatersb_priv *dev, bool enable);
|
|
|
|
static void slatersb_slatedown_work(struct work_struct *work)
|
|
{
|
|
struct slatersb_priv *dev = container_of(work, struct slatersb_priv,
|
|
slate_down_work);
|
|
mutex_lock(&dev->rsb_state_mutex);
|
|
if (dev->slatersb_current_state == SLATERSB_STATE_RSB_ENABLED)
|
|
dev->slatersb_current_state = SLATERSB_STATE_RSB_CONFIGURED;
|
|
|
|
if (dev->slatersb_current_state == SLATERSB_STATE_RSB_CONFIGURED)
|
|
dev->slatersb_current_state = SLATERSB_STATE_INIT;
|
|
|
|
dev->is_cnfgrd = false;
|
|
dev->blk_rsb_cmnds = false;
|
|
pr_debug("RSB current state is : %d\n", dev->slatersb_current_state);
|
|
|
|
if (dev->slatersb_current_state == SLATERSB_STATE_INIT) {
|
|
if (dev->is_calibrd)
|
|
dev->calibration_needed = true;
|
|
}
|
|
mutex_unlock(&dev->rsb_state_mutex);
|
|
}
|
|
|
|
static int slatersb_tx_msg(struct slatersb_priv *dev, void *msg, size_t len)
|
|
{
|
|
int rc = 0;
|
|
uint8_t resp = 0;
|
|
|
|
mutex_lock(&dev->glink_mutex);
|
|
__pm_stay_awake(dev->slatersb_ws);
|
|
|
|
if (!dev->rsb_rpmsg) {
|
|
pr_err("slatersb-rpmsg is not probed yet, waiting for it to be probed\n");
|
|
goto err_ret;
|
|
}
|
|
rc = slatersb_rpmsg_tx_msg(msg, len);
|
|
|
|
/* wait for sending command to SLATE */
|
|
rc = wait_event_timeout(dev->link_state_wait,
|
|
(rc == 0), msecs_to_jiffies(TIMEOUT_MS));
|
|
if (rc == 0) {
|
|
pr_err("failed to send command to SLATE %d\n", rc);
|
|
goto err_ret;
|
|
}
|
|
|
|
/* wait for getting response from SLATE */
|
|
rc = wait_event_timeout(dev->link_state_wait,
|
|
dev->slate_resp_cmplt,
|
|
msecs_to_jiffies(TIMEOUT_MS));
|
|
if (rc == 0) {
|
|
pr_err("failed to get SLATE response %d\n", rc);
|
|
goto err_ret;
|
|
}
|
|
|
|
dev->slate_resp_cmplt = false;
|
|
/* check SLATE response */
|
|
resp = *(uint8_t *)dev->rx_buf;
|
|
if (resp == 0x01) {
|
|
pr_err("Bad SLATE response\n");
|
|
rc = -EINVAL;
|
|
goto err_ret;
|
|
}
|
|
|
|
rc = 0;
|
|
|
|
err_ret:
|
|
__pm_relax(dev->slatersb_ws);
|
|
mutex_unlock(&dev->glink_mutex);
|
|
return rc;
|
|
}
|
|
|
|
static int slatersb_enable(struct slatersb_priv *dev, bool enable)
|
|
{
|
|
struct slatersb_msg req = {0};
|
|
|
|
req.cmd_id = SLATERSB_ENABLE;
|
|
req.data = enable ? 0x01 : 0x00;
|
|
|
|
pr_debug("req.data = %d, req.cmd_id = %d\n", req.data, req.cmd_id);
|
|
return slatersb_tx_msg(dev, &req, SLATERSB_MSG_SIZE);
|
|
}
|
|
|
|
static int slatersb_configr_rsb(struct slatersb_priv *dev, bool enable)
|
|
{
|
|
struct slatersb_msg req = {0};
|
|
|
|
req.cmd_id = SLATERSB_CONFIGR_RSB;
|
|
req.data = enable ? 0x01 : 0x00;
|
|
|
|
pr_debug("req.data = %d, req.cmd_id = %d\n", req.data, req.cmd_id);
|
|
return slatersb_tx_msg(dev, &req, SLATERSB_MSG_SIZE);
|
|
}
|
|
|
|
void slatersb_notify_glink_channel_state(bool state)
|
|
{
|
|
struct slatersb_priv *dev =
|
|
container_of(slatersb_drv, struct slatersb_priv, lhndl);
|
|
|
|
pr_debug("%s: RSB-CTRL channel state: %d\n", __func__, state);
|
|
dev->rsb_rpmsg = state;
|
|
}
|
|
|
|
void slatersb_rx_msg(void *data, int len)
|
|
{
|
|
struct slatersb_priv *dev =
|
|
container_of(slatersb_drv, struct slatersb_priv, lhndl);
|
|
|
|
if (len > SLATERSB_GLINK_INTENT_SIZE) {
|
|
pr_err("Invalid slatersb glink intent size\n");
|
|
return;
|
|
}
|
|
dev->slate_resp_cmplt = true;
|
|
wake_up(&dev->link_state_wait);
|
|
memcpy(dev->rx_buf, data, len);
|
|
}
|
|
|
|
static void slatersb_slateup_work(struct work_struct *work)
|
|
{
|
|
int ret = 0;
|
|
struct slatersb_priv *dev =
|
|
container_of(work, struct slatersb_priv, slate_up_work);
|
|
|
|
mutex_lock(&dev->rsb_state_mutex);
|
|
if (!dev->rsb_rpmsg)
|
|
pr_err("slatersb-rpmsg is not probed yet\n");
|
|
|
|
ret = wait_event_timeout(dev->link_state_wait,
|
|
dev->rsb_rpmsg, msecs_to_jiffies(TIMEOUT_MS_GLINK_OPEN));
|
|
if (ret == 0) {
|
|
pr_err("channel connection time out %d\n",
|
|
ret);
|
|
goto unlock;
|
|
}
|
|
pr_debug("slatersb-rpmsg is probed\n");
|
|
ret = slatersb_configr_rsb(dev, true);
|
|
if (ret != 0) {
|
|
pr_err("SLATE failed to configure RSB %d\n", ret);
|
|
dev->slatersb_current_state = SLATERSB_STATE_INIT;
|
|
goto unlock;
|
|
}
|
|
dev->is_cnfgrd = true;
|
|
dev->slatersb_current_state = SLATERSB_STATE_RSB_CONFIGURED;
|
|
pr_debug("RSB Cofigured\n");
|
|
if (dev->pending_enable)
|
|
queue_work(dev->slatersb_wq, &dev->rsb_up_work);
|
|
|
|
unlock:
|
|
mutex_unlock(&dev->rsb_state_mutex);
|
|
}
|
|
|
|
/**
|
|
* ssr_slate_cb(): callback function is called.
|
|
* @arg1: a notifier_block.
|
|
* @arg2: opcode that defines the event.
|
|
* @arg3: void pointer.
|
|
*
|
|
* by ssr framework when SLATE goes down, up and during
|
|
* ramdump collection. It handles SLATE shutdown and
|
|
* power up events.
|
|
*
|
|
* Return: NOTIFY_DONE.
|
|
*/
|
|
static int ssr_slatersb_cb(struct notifier_block *this,
|
|
unsigned long opcode, void *data)
|
|
{
|
|
struct slatersb_priv *dev = container_of(slatersb_drv,
|
|
struct slatersb_priv, lhndl);
|
|
|
|
switch (opcode) {
|
|
case QCOM_SSR_BEFORE_SHUTDOWN:
|
|
if (dev->slatersb_current_state == SLATERSB_STATE_RSB_ENABLED)
|
|
dev->pending_enable = true;
|
|
queue_work(dev->slatersb_wq, &dev->slate_down_work);
|
|
break;
|
|
case QCOM_SSR_AFTER_POWERUP:
|
|
if (dev->slatersb_current_state == SLATERSB_STATE_INIT)
|
|
queue_work(dev->slatersb_wq, &dev->slate_up_work);
|
|
break;
|
|
}
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
static struct notifier_block ssr_slate_nb = {
|
|
.notifier_call = ssr_slatersb_cb,
|
|
.priority = 0,
|
|
};
|
|
|
|
/**
|
|
* slatersb_ssr_register(): callback function is called.
|
|
* @arg1: pointer to slatersb_priv structure.
|
|
*
|
|
* ssr_register checks that domain id should be in range
|
|
* and register SSR framework for value at domain id.
|
|
*
|
|
* Return: 0 for success and -ENODEV otherwise.
|
|
*/
|
|
static int slatersb_ssr_register(struct slatersb_priv *dev)
|
|
{
|
|
struct notifier_block *nb;
|
|
|
|
if (!dev)
|
|
return -ENODEV;
|
|
|
|
nb = &ssr_slate_nb;
|
|
dev->slate_subsys_handle =
|
|
qcom_register_ssr_notifier(SLATERSB_SLATE_SUBSYS, nb);
|
|
|
|
if (!dev->slate_subsys_handle) {
|
|
dev->slate_subsys_handle = NULL;
|
|
return -ENODEV;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void slatersb_enable_rsb(struct work_struct *work)
|
|
{
|
|
int rc = 0;
|
|
struct slatersb_priv *dev =
|
|
container_of(work, struct slatersb_priv, rsb_up_work);
|
|
|
|
mutex_lock(&dev->rsb_state_mutex);
|
|
if (dev->slatersb_current_state == SLATERSB_STATE_RSB_ENABLED) {
|
|
pr_debug("RSB is already enabled\n");
|
|
goto unlock;
|
|
}
|
|
if (dev->slatersb_current_state != SLATERSB_STATE_RSB_CONFIGURED) {
|
|
pr_err("SLATE is not yet configured for RSB\n");
|
|
dev->pending_enable = true;
|
|
goto unlock;
|
|
}
|
|
rc = slatersb_enable(dev, true);
|
|
if (rc != 0) {
|
|
pr_err("Failed to send enable command to SLATE%d\n", rc);
|
|
dev->slatersb_current_state = SLATERSB_STATE_RSB_CONFIGURED;
|
|
goto unlock;
|
|
}
|
|
|
|
dev->slatersb_current_state = SLATERSB_STATE_RSB_ENABLED;
|
|
dev->pending_enable = false;
|
|
pr_debug("RSB Enabled\n");
|
|
if (dev->calibration_needed) {
|
|
dev->calibration_needed = false;
|
|
queue_work(dev->slatersb_wq, &dev->rsb_calibration_work);
|
|
}
|
|
pr_debug("RSB Enabled\n");
|
|
unlock:
|
|
mutex_unlock(&dev->rsb_state_mutex);
|
|
|
|
}
|
|
|
|
static void slatersb_disable_rsb(struct work_struct *work)
|
|
{
|
|
int rc = 0;
|
|
struct slatersb_priv *dev = container_of(work, struct slatersb_priv,
|
|
rsb_down_work);
|
|
mutex_lock(&dev->rsb_state_mutex);
|
|
dev->pending_enable = false;
|
|
if (dev->slatersb_current_state == SLATERSB_STATE_RSB_ENABLED) {
|
|
rc = slatersb_enable(dev, false);
|
|
if (rc != 0) {
|
|
pr_err("Failed to send disable command to SLATE\n");
|
|
goto unlock;
|
|
}
|
|
dev->slatersb_current_state = SLATERSB_STATE_RSB_CONFIGURED;
|
|
pr_debug("RSB Disabled\n");
|
|
}
|
|
unlock:
|
|
mutex_unlock(&dev->rsb_state_mutex);
|
|
}
|
|
|
|
static void slatersb_calibration(struct work_struct *work)
|
|
{
|
|
int rc = 0;
|
|
struct slatersb_msg req = {0};
|
|
struct slatersb_priv *dev =
|
|
container_of(work, struct slatersb_priv,
|
|
rsb_calibration_work);
|
|
|
|
mutex_lock(&dev->rsb_state_mutex);
|
|
if (!dev->is_cnfgrd) {
|
|
pr_err("RSB is not configured\n");
|
|
goto unlock;
|
|
}
|
|
|
|
req.cmd_id = SLATERSB_CALIBRATION_RESOLUTION;
|
|
req.data = dev->calbrtion_cpi;
|
|
|
|
rc = slatersb_tx_msg(dev, &req, 5);
|
|
if (rc != 0) {
|
|
pr_err("Failed to send resolution value to SLATE %d\n", rc);
|
|
goto unlock;
|
|
}
|
|
|
|
req.cmd_id = SLATERSB_CALIBRATION_INTERVAL;
|
|
req.data = dev->calbrtion_intrvl;
|
|
|
|
rc = slatersb_tx_msg(dev, &req, 5);
|
|
if (rc != 0) {
|
|
pr_err("Failed to send interval value to SLATE %d\n", rc);
|
|
goto unlock;
|
|
}
|
|
dev->is_calibrd = true;
|
|
pr_debug("RSB Calibrated\n");
|
|
|
|
unlock:
|
|
mutex_unlock(&dev->rsb_state_mutex);
|
|
}
|
|
|
|
static void slatersb_buttn_configration(struct work_struct *work)
|
|
{
|
|
int rc = 0;
|
|
struct slatersb_msg req = {0};
|
|
struct slatersb_priv *dev =
|
|
container_of(work, struct slatersb_priv,
|
|
bttn_configr_work);
|
|
|
|
mutex_lock(&dev->rsb_state_mutex);
|
|
if (!dev->is_cnfgrd) {
|
|
pr_err("RSB is not configured\n");
|
|
goto unlock;
|
|
}
|
|
|
|
req.cmd_id = SLATERSB_BUTTN_CONFIGRATION;
|
|
req.data = dev->bttn_configs;
|
|
|
|
rc = slatersb_tx_msg(dev, &req, 5);
|
|
if (rc != 0) {
|
|
pr_err("configuration cmnd failed %d\n",
|
|
rc);
|
|
goto unlock;
|
|
}
|
|
|
|
dev->bttn_configs = 0;
|
|
pr_debug("RSB Button configured\n");
|
|
|
|
unlock:
|
|
mutex_unlock(&dev->rsb_state_mutex);
|
|
}
|
|
|
|
static int slatersb_handle_cmd_in_ssr(struct slatersb_priv *dev, char *str)
|
|
{
|
|
long val;
|
|
int ret = 0;
|
|
char *tmp;
|
|
|
|
tmp = strsep(&str, ":");
|
|
if (!tmp)
|
|
return -EINVAL;
|
|
|
|
ret = kstrtol(tmp, 10, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
if (val == SLATERSB_POWER_ENABLE)
|
|
dev->pending_enable = true;
|
|
else if (val == SLATERSB_POWER_DISABLE)
|
|
dev->pending_enable = false;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int split_slate_work(struct slatersb_priv *dev, char *str)
|
|
{
|
|
long val;
|
|
int ret = 0;
|
|
char *tmp;
|
|
|
|
tmp = strsep(&str, ":");
|
|
if (!tmp)
|
|
return -EINVAL;
|
|
|
|
ret = kstrtol(tmp, 10, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
switch (val) {
|
|
case SLATERSB_IN_TWM:
|
|
dev->is_in_twm = true;
|
|
queue_work(dev->slatersb_wq, &dev->rsb_down_work);
|
|
break;
|
|
case SLATERSB_POWER_DISABLE:
|
|
queue_work(dev->slatersb_wq, &dev->rsb_down_work);
|
|
break;
|
|
case SLATERSB_OUT_TWM:
|
|
dev->is_in_twm = false;
|
|
queue_work(dev->slatersb_wq, &dev->rsb_up_work);
|
|
break;
|
|
case SLATERSB_POWER_ENABLE:
|
|
queue_work(dev->slatersb_wq, &dev->rsb_up_work);
|
|
break;
|
|
case SLATERSB_POWER_CALIBRATION:
|
|
tmp = strsep(&str, ":");
|
|
if (!tmp)
|
|
return -EINVAL;
|
|
|
|
ret = kstrtol(tmp, 10, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
dev->calbrtion_intrvl = (uint32_t)val;
|
|
|
|
tmp = strsep(&str, ":");
|
|
if (!tmp)
|
|
return -EINVAL;
|
|
|
|
ret = kstrtol(tmp, 10, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
dev->calbrtion_cpi = (uint32_t)val;
|
|
|
|
queue_work(dev->slatersb_wq, &dev->rsb_calibration_work);
|
|
break;
|
|
case SLATERSB_BTTN_CONFIGURE:
|
|
tmp = strsep(&str, ":");
|
|
if (!tmp)
|
|
return -EINVAL;
|
|
|
|
ret = kstrtol(tmp, 10, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
dev->bttn_configs = (uint8_t)val;
|
|
queue_work(dev->slatersb_wq, &dev->bttn_configr_work);
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t store_enable(struct device *pdev, struct device_attribute *attr,
|
|
const char *buff, size_t count)
|
|
{
|
|
int rc;
|
|
struct slatersb_priv *dev = dev_get_drvdata(pdev);
|
|
char *arr;
|
|
|
|
if (dev->blk_rsb_cmnds) {
|
|
pr_err("Device is in TWM state\n");
|
|
return count;
|
|
}
|
|
arr = kstrdup(buff, GFP_KERNEL);
|
|
if (!arr)
|
|
return -ENOMEM;
|
|
|
|
rc = split_slate_work(dev, arr);
|
|
if (!dev->is_cnfgrd) {
|
|
slatersb_handle_cmd_in_ssr(dev, arr);
|
|
kfree(arr);
|
|
return -ENOMEDIUM;
|
|
}
|
|
|
|
if (rc != 0)
|
|
pr_err("Not able to process request\n");
|
|
|
|
kfree(arr);
|
|
return count;
|
|
}
|
|
|
|
static ssize_t show_enable(struct device *dev, struct device_attribute *attr,
|
|
char *buff)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static struct device_attribute dev_attr_rsb = {
|
|
.attr = {
|
|
.name = "enable",
|
|
.mode = 00660,
|
|
},
|
|
.show = show_enable,
|
|
.store = store_enable,
|
|
};
|
|
|
|
static int slatersb_init(struct slatersb_priv *dev)
|
|
{
|
|
slatersb_drv = &dev->lhndl;
|
|
mutex_init(&dev->glink_mutex);
|
|
mutex_init(&dev->rsb_state_mutex);
|
|
|
|
dev->slatersb_wq =
|
|
create_singlethread_workqueue("slate-work-queue");
|
|
if (!dev->slatersb_wq) {
|
|
pr_err("Failed to init SLATE-RSB work-queue\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
init_waitqueue_head(&dev->link_state_wait);
|
|
|
|
/* set default slatersb state */
|
|
dev->slatersb_current_state = SLATERSB_STATE_INIT;
|
|
|
|
/* Init all works */
|
|
INIT_WORK(&dev->slate_up_work, slatersb_slateup_work);
|
|
INIT_WORK(&dev->slate_down_work, slatersb_slatedown_work);
|
|
INIT_WORK(&dev->rsb_up_work, slatersb_enable_rsb);
|
|
INIT_WORK(&dev->rsb_down_work, slatersb_disable_rsb);
|
|
INIT_WORK(&dev->rsb_calibration_work, slatersb_calibration);
|
|
INIT_WORK(&dev->bttn_configr_work, slatersb_buttn_configration);
|
|
|
|
slatersb_channel_init(&slatersb_notify_glink_channel_state, &slatersb_rx_msg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int slate_rsb_probe(struct platform_device *pdev)
|
|
{
|
|
struct slatersb_priv *dev;
|
|
struct device_node *node;
|
|
int rc = 0;
|
|
|
|
node = pdev->dev.of_node;
|
|
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
|
|
if (!dev)
|
|
return -ENOMEM;
|
|
/* Add wake lock for PM suspend */
|
|
dev->slatersb_ws = wakeup_source_register(&pdev->dev, "slate_rsb");
|
|
dev->slatersb_current_state = SLATERSB_STATE_UNKNOWN;
|
|
rc = slatersb_init(dev);
|
|
if (rc)
|
|
pr_err("init failed\n");
|
|
/* register device for slate ssr */
|
|
rc = slatersb_ssr_register(dev);
|
|
if (rc)
|
|
pr_err("Failed to register for slate ssr\n");
|
|
rc = device_create_file(&pdev->dev, &dev_attr_rsb);
|
|
if (rc)
|
|
pr_err("Not able to create the file slate-rsb/enable\n");
|
|
|
|
dev_set_drvdata(&pdev->dev, dev);
|
|
pr_debug("RSB probe successfully\n");
|
|
return 0;
|
|
|
|
}
|
|
|
|
static int slate_rsb_remove(struct platform_device *pdev)
|
|
{
|
|
struct slatersb_priv *dev = platform_get_drvdata(pdev);
|
|
|
|
destroy_workqueue(dev->slatersb_wq);
|
|
wakeup_source_unregister(dev->slatersb_ws);
|
|
return 0;
|
|
}
|
|
|
|
static int slate_rsb_resume(struct device *pldev)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static int slate_rsb_suspend(struct device *pldev)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id slate_rsb_of_match[] = {
|
|
{ .compatible = "qcom,slate-rsb" },
|
|
{ },
|
|
};
|
|
|
|
static const struct dev_pm_ops pm_rsb = {
|
|
.resume = slate_rsb_resume,
|
|
.suspend = slate_rsb_suspend,
|
|
};
|
|
|
|
static struct platform_driver slate_rsb_driver = {
|
|
.driver = {
|
|
.name = "slate-rsb",
|
|
.of_match_table = slate_rsb_of_match,
|
|
.pm = &pm_rsb,
|
|
},
|
|
.probe = slate_rsb_probe,
|
|
.remove = slate_rsb_remove,
|
|
};
|
|
module_platform_driver(slate_rsb_driver);
|
|
|
|
MODULE_DESCRIPTION("SoC SLATE RSB driver");
|
|
MODULE_LICENSE("GPL");
|