lkml.org 
[lkml]   [2019]   [Jan]   [29]   [last100]   RSS Feed
Views: [wrap][no wrap]   [headers]  [forward] 
 
Messages in this thread
Patch in this message
/
From
Subject[RFC net-next 3/4] net: enocean: Add ESP3 driver
Date
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@suse.de>
---
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@suse.de>");
+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
\
 
 \ /
  Last update: 2019-01-29 06:02    [W:0.133 / U:16.264 seconds]
©2003-2020 Jasper Spaans|hosted at Digital Ocean and TransIP|Read the blog|Advertise on this site