[RFC net-next 3/4] net: enocean: Add ESP3 driver

From: Andreas FÃrber
Date: Tue Jan 29 2019 - 00:02:01 EST


This implements the EnOcean Serial Protocol 3.
Rudimentary sending is prepared. Error handling is lacking and
reception handling is missing.

Tested with EnOcean TCM310 gateway module.

Signed-off-by: Andreas FÃrber <afaerber@xxxxxxx>
---
drivers/net/enocean/Makefile | 4 +
drivers/net/enocean/enocean_esp.c | 277 +++++++++++++++++++++++++++
drivers/net/enocean/enocean_esp.h | 38 ++++
drivers/net/enocean/enocean_esp3.c | 372 +++++++++++++++++++++++++++++++++++++
4 files changed, 691 insertions(+)
create mode 100644 drivers/net/enocean/enocean_esp.c
create mode 100644 drivers/net/enocean/enocean_esp.h
create mode 100644 drivers/net/enocean/enocean_esp3.c

diff --git a/drivers/net/enocean/Makefile b/drivers/net/enocean/Makefile
index efb3cd16c7f2..4492e3d48c0a 100644
--- a/drivers/net/enocean/Makefile
+++ b/drivers/net/enocean/Makefile
@@ -1,2 +1,6 @@
obj-m += enocean-dev.o
enocean-dev-y := enocean.o
+
+obj-m += enocean-esp.o
+enocean-esp-y := enocean_esp.o
+enocean-esp-y += enocean_esp3.o
diff --git a/drivers/net/enocean/enocean_esp.c b/drivers/net/enocean/enocean_esp.c
new file mode 100644
index 000000000000..61bddb77762d
--- /dev/null
+++ b/drivers/net/enocean/enocean_esp.c
@@ -0,0 +1,277 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * EnOcean Serial Protocol
+ *
+ * Copyright (c) 2019 Andreas FÃrber
+ */
+
+#include <linux/bitops.h>
+#include <linux/completion.h>
+#include <linux/enocean/dev.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/serdev.h>
+
+#include "enocean_esp.h"
+
+struct enocean_esp_version {
+ int version;
+ int baudrate;
+ const struct serdev_device_ops *serdev_client_ops;
+ int (*init)(struct enocean_device *edev);
+ int (*send)(struct enocean_device *edev, u32 dest, const void *data, int data_len);
+ void (*cleanup)(struct enocean_device *edev);
+};
+
+struct enocean_esp_priv {
+ struct enocean_dev_priv priv;
+
+ struct enocean_device *edev;
+
+ struct sk_buff *tx_skb;
+ int tx_len;
+
+ struct workqueue_struct *wq;
+ struct work_struct tx_work;
+};
+
+static netdev_tx_t enocean_dev_start_xmit(struct sk_buff *skb, struct net_device *netdev)
+{
+ struct enocean_esp_priv *priv = netdev_priv(netdev);
+
+ netdev_dbg(netdev, "%s\n", __func__);
+
+ if (skb->protocol != htons(ETH_P_ERP1) &&
+ skb->protocol != htons(ETH_P_ERP2)) {
+ kfree_skb(skb);
+ netdev->stats.tx_dropped++;
+ return NETDEV_TX_OK;
+ }
+
+ netif_stop_queue(netdev);
+ priv->tx_skb = skb;
+ queue_work(priv->wq, &priv->tx_work);
+
+ return NETDEV_TX_OK;
+}
+
+static int enocean_esp_tx(struct enocean_device *edev, void *data, int data_len)
+{
+ if (!edev->version->send)
+ return -ENOTSUPP;
+ return edev->version->send(edev, 0xffffffff, data, data_len);
+}
+
+static void enocean_esp_tx_work_handler(struct work_struct *ws)
+{
+ struct enocean_esp_priv *priv = container_of(ws, struct enocean_esp_priv, tx_work);
+ struct enocean_device *edev = priv->edev;
+ struct net_device *netdev = edev->netdev;
+
+ netdev_dbg(netdev, "%s\n", __func__);
+
+ if (priv->tx_skb) {
+ enocean_esp_tx(edev, priv->tx_skb->data, priv->tx_skb->len);
+ priv->tx_len = 1 + priv->tx_skb->len;
+ if (!(netdev->flags & IFF_ECHO) ||
+ priv->tx_skb->pkt_type != PACKET_LOOPBACK ||
+ (priv->tx_skb->protocol != htons(ETH_P_ERP1) &&
+ priv->tx_skb->protocol != htons(ETH_P_ERP2)))
+ kfree_skb(priv->tx_skb);
+ priv->tx_skb = NULL;
+ }
+}
+
+void enocean_esp_tx_done(struct enocean_device *edev)
+{
+ struct net_device *netdev = edev->netdev;
+ struct enocean_esp_priv *priv = netdev_priv(netdev);
+
+ netdev_info(netdev, "TX done.\n");
+ netdev->stats.tx_packets++;
+ netdev->stats.tx_bytes += priv->tx_len - 1;
+ priv->tx_len = 0;
+ netif_wake_queue(netdev);
+}
+
+static int enocean_dev_open(struct net_device *netdev)
+{
+ struct enocean_esp_priv *priv = netdev_priv(netdev);
+ int ret;
+
+ netdev_dbg(netdev, "%s\n", __func__);
+
+ ret = open_enocean_dev(netdev);
+ if (ret)
+ return ret;
+
+ priv->tx_skb = NULL;
+ priv->tx_len = 0;
+
+ priv->wq = alloc_workqueue("enocean_esp_wq", WQ_FREEZABLE | WQ_MEM_RECLAIM, 0);
+ INIT_WORK(&priv->tx_work, enocean_esp_tx_work_handler);
+
+ netif_wake_queue(netdev);
+
+ return 0;
+}
+
+static int enocean_dev_stop(struct net_device *netdev)
+{
+ struct enocean_esp_priv *priv = netdev_priv(netdev);
+
+ netdev_dbg(netdev, "%s\n", __func__);
+
+ close_enocean_dev(netdev);
+
+ destroy_workqueue(priv->wq);
+ priv->wq = NULL;
+
+ if (priv->tx_skb || priv->tx_len)
+ netdev->stats.tx_errors++;
+ if (priv->tx_skb)
+ dev_kfree_skb(priv->tx_skb);
+ priv->tx_skb = NULL;
+ priv->tx_len = 0;
+
+ return 0;
+}
+
+static const struct net_device_ops enocean_esp_netdev_ops = {
+ .ndo_open = enocean_dev_open,
+ .ndo_stop = enocean_dev_stop,
+ .ndo_start_xmit = enocean_dev_start_xmit,
+};
+
+static void enocean_esp_cleanup(struct enocean_device *edev)
+{
+ if (edev->version->cleanup)
+ edev->version->cleanup(edev);
+}
+
+static const struct enocean_esp_version enocean_esp3 = {
+ .version = 3,
+ .baudrate = 57600,
+ .serdev_client_ops = &enocean_esp3_serdev_client_ops,
+ .init = enocean_esp3_init,
+ .send = enocean_esp3_send,
+ .cleanup = enocean_esp3_cleanup,
+};
+
+static const struct of_device_id enocean_of_match[] = {
+ { .compatible = "enocean,esp3", .data = &enocean_esp3 },
+ {}
+};
+MODULE_DEVICE_TABLE(of, enocean_of_match);
+
+static int enocean_probe(struct serdev_device *sdev)
+{
+ struct enocean_esp_priv *priv;
+ struct enocean_device *edev;
+ int ret;
+
+ dev_dbg(&sdev->dev, "Probing");
+
+ edev = devm_kzalloc(&sdev->dev, sizeof(*edev), GFP_KERNEL);
+ if (!edev)
+ return -ENOMEM;
+
+ edev->version = of_device_get_match_data(&sdev->dev);
+ if (!edev->version)
+ return -ENOTSUPP;
+
+ dev_dbg(&sdev->dev, "ESP%d\n", edev->version->version);
+
+ edev->serdev = sdev;
+ INIT_LIST_HEAD(&edev->esp_dispatchers);
+ serdev_device_set_drvdata(sdev, edev);
+
+ ret = serdev_device_open(sdev);
+ if (ret) {
+ dev_err(&sdev->dev, "Failed to open (%d)\n", ret);
+ return ret;
+ }
+
+ serdev_device_set_baudrate(sdev, edev->version->baudrate);
+ serdev_device_set_flow_control(sdev, false);
+ serdev_device_set_client_ops(sdev, edev->version->serdev_client_ops);
+
+ if (edev->version->init) {
+ ret = edev->version->init(edev);
+ if (ret) {
+ serdev_device_close(sdev);
+ return ret;
+ }
+ }
+
+ edev->netdev = devm_alloc_enocean_dev(&sdev->dev, sizeof(struct enocean_esp_priv));
+ if (!edev->netdev) {
+ enocean_esp_cleanup(edev);
+ serdev_device_close(sdev);
+ return -ENOMEM;
+ }
+
+ edev->netdev->netdev_ops = &enocean_esp_netdev_ops;
+ edev->netdev->flags |= IFF_ECHO;
+ SET_NETDEV_DEV(edev->netdev, &sdev->dev);
+
+ priv = netdev_priv(edev->netdev);
+ priv->edev = edev;
+
+ ret = register_enocean_dev(edev->netdev);
+ if (ret) {
+ enocean_esp_cleanup(edev);
+ serdev_device_close(sdev);
+ return ret;
+ }
+
+ dev_dbg(&sdev->dev, "Done.\n");
+
+ return 0;
+}
+
+static void enocean_remove(struct serdev_device *sdev)
+{
+ struct enocean_device *edev = serdev_device_get_drvdata(sdev);
+
+ unregister_enocean_dev(edev->netdev);
+ enocean_esp_cleanup(edev);
+ serdev_device_close(sdev);
+
+ dev_dbg(&sdev->dev, "Removed\n");
+}
+
+static struct serdev_device_driver enocean_serdev_driver = {
+ .probe = enocean_probe,
+ .remove = enocean_remove,
+ .driver = {
+ .name = "enocean-esp",
+ .of_match_table = enocean_of_match,
+ },
+};
+
+static int __init enocean_init(void)
+{
+ int ret;
+
+ enocean_esp3_crc8_populate();
+
+ ret = serdev_device_driver_register(&enocean_serdev_driver);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static void __exit enocean_exit(void)
+{
+ serdev_device_driver_unregister(&enocean_serdev_driver);
+}
+
+module_init(enocean_init);
+module_exit(enocean_exit);
+
+MODULE_DESCRIPTION("EnOcean serdev driver");
+MODULE_AUTHOR("Andreas FÃrber <afaerber@xxxxxxx>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/enocean/enocean_esp.h b/drivers/net/enocean/enocean_esp.h
new file mode 100644
index 000000000000..e02bf5352d61
--- /dev/null
+++ b/drivers/net/enocean/enocean_esp.h
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * EnOcean Serial Protocol
+ *
+ * Copyright (c) 2019 Andreas FÃrber
+ */
+#ifndef ENOCEAN_H
+#define ENOCEAN_H
+
+#include <linux/netdevice.h>
+#include <linux/rculist.h>
+#include <linux/serdev.h>
+
+struct enocean_esp_version;
+
+struct enocean_device {
+ struct serdev_device *serdev;
+ const struct enocean_esp_version *version;
+
+ struct net_device *netdev;
+
+ struct list_head esp_dispatchers;
+
+ void *priv;
+};
+
+extern const struct serdev_device_ops enocean_esp3_serdev_client_ops;
+
+void enocean_esp3_crc8_populate(void);
+
+int enocean_esp3_init(struct enocean_device *edev);
+
+int enocean_esp3_send(struct enocean_device *edev, u32 dest, const void *data, int data_len);
+void enocean_esp_tx_done(struct enocean_device *edev);
+
+void enocean_esp3_cleanup(struct enocean_device *edev);
+
+#endif
diff --git a/drivers/net/enocean/enocean_esp3.c b/drivers/net/enocean/enocean_esp3.c
new file mode 100644
index 000000000000..707c4054ac69
--- /dev/null
+++ b/drivers/net/enocean/enocean_esp3.c
@@ -0,0 +1,372 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * EnOcean Serial Protocol 3
+ *
+ * Copyright (c) 2019 Andreas FÃrber
+ */
+
+#include <asm/unaligned.h>
+#include <linux/completion.h>
+#include <linux/crc8.h>
+#include <linux/rculist.h>
+#include <linux/serdev.h>
+#include <linux/slab.h>
+
+#include "enocean_esp.h"
+
+/* G(x) = x^8 + x^2 + x^1 + x^0 */
+#define ESP3_CRC8_POLY_MSB 0x07
+
+DECLARE_CRC8_TABLE(enocean_esp3_crc8_table);
+
+void enocean_esp3_crc8_populate(void)
+{
+ crc8_populate_msb(enocean_esp3_crc8_table, ESP3_CRC8_POLY_MSB);
+}
+
+static inline u8 enocean_esp3_crc8(u8 *pdata, size_t nbytes)
+{
+ return crc8(enocean_esp3_crc8_table, pdata, nbytes, 0x00);
+}
+
+#define ESP3_SYNC_WORD 0x55
+
+struct enocean_esp3_dispatcher {
+ struct list_head list;
+ u8 packet_type;
+ void (*dispatch)(const u8 *data, u16 data_len, struct enocean_esp3_dispatcher *d);
+};
+
+static void enocean_add_esp3_dispatcher(struct enocean_device *edev,
+ struct enocean_esp3_dispatcher *entry)
+{
+ list_add_tail_rcu(&entry->list, &edev->esp_dispatchers);
+}
+
+static void enocean_remove_esp3_dispatcher(struct enocean_device *edev,
+ struct enocean_esp3_dispatcher *entry)
+{
+ list_del_rcu(&entry->list);
+}
+
+struct enocean_esp3_response {
+ struct enocean_esp3_dispatcher disp;
+
+ u8 code;
+ void *data;
+ u16 data_len;
+
+ struct completion comp;
+};
+
+static void enocean_esp3_response_dispatch(const u8 *data, u16 data_len,
+ struct enocean_esp3_dispatcher *d)
+{
+ struct enocean_esp3_response *resp =
+ container_of(d, struct enocean_esp3_response, disp);
+
+ if (completion_done(&resp->comp))
+ return;
+
+ if (data_len < 1)
+ return;
+
+ resp->code = data[0];
+ if (data_len > 1) {
+ resp->data = kzalloc(data_len - 1, GFP_KERNEL);
+ if (resp->data)
+ memcpy(resp->data, data + 1, data_len - 1);
+ resp->data_len = data_len - 1;
+ } else {
+ resp->data = NULL;
+ resp->data_len = 0;
+ }
+
+ complete(&resp->comp);
+}
+
+struct enocean_esp3_priv {
+ struct enocean_device *edev;
+ struct enocean_esp3_dispatcher radio_erp1_response;
+};
+
+static inline int enocean_esp3_packet_size(u16 data_len, u8 optional_len)
+{
+ return 1 + 4 + 1 + data_len + optional_len + 1;
+}
+
+static int enocean_send_esp3_packet(struct enocean_device *edev, u8 packet_type,
+ const void *data, u16 data_len, const void *optional_data, u8 optional_len,
+ unsigned long timeout)
+{
+ int len = enocean_esp3_packet_size(data_len, optional_len);
+ u8 *buf;
+ int ret;
+
+ buf = kzalloc(len, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ buf[0] = ESP3_SYNC_WORD;
+ put_unaligned_be16(data_len, buf + 1);
+ buf[3] = optional_len;
+ buf[4] = packet_type;
+ buf[5] = enocean_esp3_crc8(buf + 1, 4);
+ dev_dbg(&edev->serdev->dev, "CRC8H = %02x\n", (unsigned int)buf[5]);
+ memcpy(buf + 6, data, data_len);
+ memcpy(buf + 6 + data_len, optional_data, optional_len);
+ buf[6 + data_len + optional_len] = enocean_esp3_crc8(buf + 6, data_len + optional_len);
+ dev_dbg(&edev->serdev->dev, "CRC8D = %02x\n", (unsigned int)buf[6 + data_len + optional_len]);
+
+ ret = serdev_device_write(edev->serdev, buf, len, timeout);
+
+ kfree(buf);
+
+ if (ret < 0)
+ return ret;
+ if (ret > 0 && ret < len)
+ return -EIO;
+ return 0;
+}
+
+#define ESP3_RADIO_ERP1 0x1
+#define ESP3_RESPONSE 0x2
+#define ESP3_COMMON_COMMAND 0x5
+
+static void enocean_esp3_radio_erp1_response_dispatch(const u8 *data, u16 data_len,
+ struct enocean_esp3_dispatcher *d)
+{
+ struct enocean_esp3_priv *priv = container_of(d, struct enocean_esp3_priv, radio_erp1_response);
+ struct enocean_device *edev = priv->edev;
+ int ret;
+
+ enocean_remove_esp3_dispatcher(edev, d);
+
+ if (data_len < 1)
+ return;
+
+ switch (data[0]) {
+ case 0:
+ enocean_esp_tx_done(edev);
+ break;
+ case 2:
+ ret = -ENOTSUPP;
+ break;
+ case 3:
+ ret = -EINVAL;
+ break;
+ case 5:
+ ret = -EIO;
+ break;
+ default:
+ ret = -EIO;
+ break;
+ }
+}
+
+static int enocean_esp3_send_radio_erp1(struct enocean_device *edev, u32 dest,
+ const u8 *data, int data_len, unsigned long timeout)
+{
+ struct enocean_esp3_priv *priv = edev->priv;
+ struct esp3_radio_erp1_optional {
+ u8 sub_tel_num;
+ __be32 destination_id;
+ u8 dbm;
+ u8 security_level;
+ } __packed opt = {
+ .sub_tel_num = 3,
+ .destination_id = cpu_to_be32(dest),
+ .dbm = 0xff,
+ .security_level = 0,
+ };
+ int ret;
+
+ enocean_add_esp3_dispatcher(edev, &priv->radio_erp1_response);
+
+ ret = enocean_send_esp3_packet(edev, ESP3_RADIO_ERP1, data, data_len, &opt, sizeof(opt), timeout);
+ if (ret) {
+ enocean_remove_esp3_dispatcher(edev, &priv->radio_erp1_response);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int enocean_esp3_reset(struct enocean_device *edev, unsigned long timeout)
+{
+ struct enocean_esp3_response resp;
+ const u8 buf[1] = { 0x02 };
+ int ret;
+
+ init_completion(&resp.comp);
+ resp.disp.packet_type = ESP3_RESPONSE;
+ resp.disp.dispatch = enocean_esp3_response_dispatch;
+ enocean_add_esp3_dispatcher(edev, &resp.disp);
+
+ ret = enocean_send_esp3_packet(edev, ESP3_COMMON_COMMAND, buf, sizeof(buf), NULL, 0, timeout);
+ if (ret) {
+ enocean_remove_esp3_dispatcher(edev, &resp.disp);
+ return ret;
+ }
+
+ timeout = wait_for_completion_timeout(&resp.comp, timeout);
+ enocean_remove_esp3_dispatcher(edev, &resp.disp);
+ if (!timeout)
+ return -ETIMEDOUT;
+
+ switch (resp.code) {
+ case 0:
+ return 0;
+ case 1:
+ return -EIO;
+ case 2:
+ return -ENOTSUPP;
+ default:
+ return -EIO;
+ }
+}
+
+struct enocean_esp3_version {
+ u8 app_version[4];
+ u8 api_version[4];
+ __be32 chip_id;
+ __be32 chip_version;
+ char app_desc[16];
+} __packed;
+
+static int enocean_esp3_read_version(struct enocean_device *edev, unsigned long timeout)
+{
+ struct enocean_esp3_response resp;
+ struct enocean_esp3_version *ver;
+ const u8 buf[1] = { 0x03 };
+ int ret;
+
+ init_completion(&resp.comp);
+ resp.disp.packet_type = ESP3_RESPONSE;
+ resp.disp.dispatch = enocean_esp3_response_dispatch;
+ enocean_add_esp3_dispatcher(edev, &resp.disp);
+
+ ret = enocean_send_esp3_packet(edev, ESP3_COMMON_COMMAND, buf, sizeof(buf), NULL, 0, timeout);
+ if (ret) {
+ enocean_remove_esp3_dispatcher(edev, &resp.disp);
+ return ret;
+ }
+
+ timeout = wait_for_completion_timeout(&resp.comp, timeout);
+ enocean_remove_esp3_dispatcher(edev, &resp.disp);
+ if (!timeout)
+ return -ETIMEDOUT;
+
+ switch (resp.code) {
+ case 0:
+ if (!resp.data)
+ return -ENOMEM;
+ break;
+ case 2:
+ if (resp.data)
+ kfree(resp.data);
+ return -ENOTSUPP;
+ default:
+ if (resp.data)
+ kfree(resp.data);
+ return -EIO;
+ }
+
+ ver = resp.data;
+ ver->app_desc[15] = '\0';
+ dev_info(&edev->serdev->dev, "'%s'\n", ver->app_desc);
+ kfree(resp.data);
+
+ return 0;
+}
+
+static int enocean_esp3_receive_buf(struct serdev_device *sdev, const u8 *data, size_t count)
+{
+ struct enocean_device *edev = serdev_device_get_drvdata(sdev);
+ struct enocean_esp3_dispatcher *e;
+ u8 crc8h, crc8d, optional_len;
+ u16 data_len;
+
+ dev_dbg(&sdev->dev, "Receive (%zu)\n", count);
+
+ if (data[0] != ESP3_SYNC_WORD) {
+ dev_warn(&sdev->dev, "not Sync Word (found 0x%02x), skipping\n",
+ (unsigned int)data[0]);
+ return 1;
+ }
+
+ if (count < 6)
+ return 0;
+
+ crc8h = enocean_esp3_crc8((u8*)data + 1, 4);
+ if (data[5] != crc8h) {
+ dev_warn(&sdev->dev, "invalid CRC8H (expected 0x%02x, found 0x%02x), skipping\n",
+ (unsigned int)crc8h, (unsigned int)data[5]);
+ return 1;
+ }
+
+ data_len = be16_to_cpup((__be16 *)(data + 1));
+ optional_len = data[3];
+ if (count < enocean_esp3_packet_size(data_len, optional_len))
+ return 0;
+
+ crc8d = enocean_esp3_crc8((u8*)data + 6, data_len + optional_len);
+ if (data[6 + data_len + optional_len] != crc8d) {
+ dev_warn(&sdev->dev, "invalid CRC8D (expected 0x%02x, found 0x%02x), skipping\n",
+ (unsigned int)crc8d, (unsigned int)data[6 + data_len + optional_len]);
+ return 1;
+ }
+
+ print_hex_dump_debug("received: ", DUMP_PREFIX_NONE, 16, 1, data,
+ enocean_esp3_packet_size(data_len, optional_len), false);
+
+ list_for_each_entry_rcu(e, &edev->esp_dispatchers, list) {
+ if (e->packet_type == data[4])
+ e->dispatch(data + 6, data_len, e);
+ }
+
+ return enocean_esp3_packet_size(data_len, optional_len);
+}
+
+const struct serdev_device_ops enocean_esp3_serdev_client_ops = {
+ .receive_buf = enocean_esp3_receive_buf,
+ .write_wakeup = serdev_device_write_wakeup,
+};
+
+int enocean_esp3_send(struct enocean_device *edev, u32 dest, const void *data, int data_len)
+{
+ return enocean_esp3_send_radio_erp1(edev, dest, data, data_len, HZ / 10);
+}
+
+int enocean_esp3_init(struct enocean_device *edev)
+{
+ struct enocean_esp3_priv *priv;
+ int ret;
+
+ ret = enocean_esp3_reset(edev, HZ / 10);
+ if (ret)
+ return ret;
+
+ msleep(100); /* XXX */
+
+ ret = enocean_esp3_read_version(edev, HZ / 10);
+
+ priv = kzalloc(sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->edev = edev;
+ edev->priv = priv;
+
+ priv->radio_erp1_response.packet_type = ESP3_RESPONSE;
+ priv->radio_erp1_response.dispatch = enocean_esp3_radio_erp1_response_dispatch;
+
+ return 0;
+}
+
+void enocean_esp3_cleanup(struct enocean_device *edev)
+{
+ struct enocean_esp3_priv *priv = edev->priv;
+
+ kfree(priv);
+}
--
2.16.4