[PATCH 4/5] ath79: mikrotik: add poe driver

Oskari Lemmela oskari at lemmela.net
Tue Jan 25 22:38:44 PST 2022


Add hwmon based driver for mikrotik POE controllers.

Signed-off-by: Oskari Lemmela <oskari at lemmela.net>
---
 .../linux/ath79/files/drivers/hwmon/rbpoe.c   | 256 ++++++++++++++
 .../linux/ath79/files/drivers/hwmon/rbpoe.h   |  25 ++
 .../ath79/files/drivers/hwmon/rbpoeport.c     | 311 ++++++++++++++++++
 target/linux/ath79/mikrotik/config-default    |   3 +
 .../902-hwmon-support-for-mikrotik-poe.patch  |  51 +++
 5 files changed, 646 insertions(+)
 create mode 100644 target/linux/ath79/files/drivers/hwmon/rbpoe.c
 create mode 100644 target/linux/ath79/files/drivers/hwmon/rbpoe.h
 create mode 100644 target/linux/ath79/files/drivers/hwmon/rbpoeport.c
 create mode 100644 target/linux/ath79/patches-5.10/902-hwmon-support-for-mikrotik-poe.patch

diff --git a/target/linux/ath79/files/drivers/hwmon/rbpoe.c b/target/linux/ath79/files/drivers/hwmon/rbpoe.c
new file mode 100644
index 0000000000..fb5de6e6d7
--- /dev/null
+++ b/target/linux/ath79/files/drivers/hwmon/rbpoe.c
@@ -0,0 +1,256 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+/*
+ * Mikrotik POE driver
+ *
+ * Based on https://github.com/adron-s/mtpoe_ctrl
+ */
+
+#include <linux/crc8.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/hwmon.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/jiffies.h>
+#include <linux/spi/spi.h>
+#include <linux/of_platform.h>
+#include <linux/version.h>
+
+#include "rbpoe.h"
+
+DECLARE_CRC8_TABLE(rbpoe_crc_table);
+
+#define MAX_PORTS 4
+
+int rb_poe_get_port_idx(struct rb_poe_data *poe, int reg)
+{
+	if (poe->data->reverse)
+		return (MAX_PORTS-reg);
+	else
+		return reg-1;
+}
+EXPORT_SYMBOL(rb_poe_get_port_idx);
+
+int rb_poe_write_cmd(struct rb_poe_data *poe, u16 *resp, u8 cmd, u8 arg1, u8 arg2)
+{
+	int ret;
+	u8 tx[4];
+	u8 rx[6];
+	u8 crc, retries;
+
+	struct spi_transfer xfers[] = {
+		{
+			.tx_buf = tx,
+			.len = 4,
+			.delay.value = 10,
+			.delay.unit = SPI_DELAY_UNIT_USECS,
+		},
+		{
+			.rx_buf = rx,
+			.len = 6,
+		},
+	};
+
+	mutex_lock(&poe->lock);
+
+	tx[0] = cmd;
+	tx[1] = arg1;
+	tx[2] = arg2;
+	tx[3] = crc8(rbpoe_crc_table, tx, 3, 0);
+
+	for (retries = 0; retries < MAX_RETRIES; retries++) {
+		ret = spi_sync_transfer(poe->spi, xfers, ARRAY_SIZE(xfers));
+		if (ret < 0) {
+			dev_err(&poe->spi->dev, "SPI transfer error");
+			goto out;
+		}
+
+		if (rx[1] != cmd) {
+			ndelay(13);
+			continue;
+		}
+
+		crc = crc8(rbpoe_crc_table, rx+1, 3, 0);
+
+		if (rx[4] != crc && rx[5] != crc)
+			continue;
+
+		resp[0] = rx[2] << 8 | rx[3];
+		goto out;
+	}
+out:
+	mutex_unlock(&poe->lock);
+	return ret;
+}
+EXPORT_SYMBOL(rb_poe_write_cmd);
+
+static int rb_poe_read_version(struct rb_poe_data *data)
+{
+	int ret;
+	u16 vers;
+
+	ret = rb_poe_write_cmd(data, &vers, 0x41, 0, 0);
+	if (ret < 0)
+		return ret;
+	return vers;
+}
+
+int rb_poe_read_voltage(struct rb_poe_data *data)
+{
+	int ret;
+	u16 val;
+
+	ret = rb_poe_write_cmd(data, &val, 0x42, 0, 0);
+	if (ret < 0)
+		return ret;
+	return val * data->data->volt_lsb;
+}
+EXPORT_SYMBOL(rb_poe_read_voltage);
+
+static int rb_poe_read_temperature(struct rb_poe_data *data)
+{
+	int ret;
+	u16 val;
+
+	ret = rb_poe_write_cmd(data, &val, 0x43, 0, 0);
+	if (ret < 0)
+		return ret;
+	return (val * data->data->temp_lsb) - data->data->temp_offset;
+}
+
+/* sysfs attributes */
+static ssize_t temp_input_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	struct rb_poe_data *data = dev_get_drvdata(dev);
+	int val;
+
+	if (IS_ERR(data))
+		return -ENODATA;
+
+	val = rb_poe_read_temperature(data);
+	if (val < 0)
+		return -ENODATA;
+
+	return sprintf(buf, "%d\n", val);
+}
+
+static ssize_t in_input_show(struct device *dev,
+			     struct device_attribute *attr, char *buf)
+{
+	struct rb_poe_data *data = dev_get_drvdata(dev);
+	int val;
+
+	if (IS_ERR(data))
+		return -ENODATA;
+
+	val = rb_poe_read_voltage(data);
+	if (val < 0)
+		return -ENODATA;
+
+	return sprintf(buf, "%d\n", val);
+}
+
+static SENSOR_DEVICE_ATTR_RO(temp1_input, temp_input, 0);
+static SENSOR_DEVICE_ATTR_RO(in0_input, in_input, 4);
+
+static struct attribute *rb_poe_attrs[] = {
+	&sensor_dev_attr_temp1_input.dev_attr.attr,
+	&sensor_dev_attr_in0_input.dev_attr.attr,
+	NULL
+};
+
+ATTRIBUTE_GROUPS(rb_poe);
+
+static int rb_poe_probe(struct spi_device *spi)
+{
+	struct device *dev = &spi->dev;
+	struct device *hwmon_dev;
+	struct rb_poe_data *data;
+	int ret;
+
+	data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	data->data = (struct rb_poe_model *)of_device_get_match_data(dev);
+	data->spi = spi;
+
+	spi->mode = SPI_MODE_0;
+	ret = spi_setup(spi);
+	if (ret)
+		return ret;
+
+	crc8_populate_lsb(rbpoe_crc_table, 0x8C);
+	dev_set_drvdata(dev, data);
+	mutex_init(&data->lock);
+
+	ret = rb_poe_read_version(data);
+	if (ret < 0)
+		return ret;
+
+	dev_info(dev, "firmware: %d.%d", ret & 0xff, ret >> 8);
+
+	ret = of_platform_populate(dev->of_node, NULL, NULL, dev);
+	if (ret < 0) {
+		dev_err(dev, "failed to populate DT children\n");
+		return ret;
+	}
+
+	hwmon_dev = devm_hwmon_device_register_with_groups(dev, "rbpoe", data, rb_poe_groups);
+
+	if (IS_ERR(hwmon_dev))
+		dev_dbg(dev, "unable to register hwmon device\n");
+
+	return PTR_ERR_OR_ZERO(hwmon_dev);
+}
+
+static const struct rb_poe_model v2_data = {
+	.reverse = 1,
+	.volt_lsb = 35,
+	.temp_lsb = 1000,
+	.temp_offset = 273000,
+};
+
+/*
+ * ATSAMD20J15 temperature sensor factory calibration is done at 25C 667mV with Vddana=3.3V
+ * Typical sensor slope 2.4mV/C. 667mV/2.4mV/C = 277.917C - 25C = 253.917C
+ * Measured Vddana is 3.29V, reduce slope a bit and tune calibration vdd.
+ * 680mV/2.375mV/C = 286.315C - 25C = 261.315C
+ * 1000/2.375 = 421
+ */
+static const struct rb_poe_model v3_data = {
+	.reverse = 0,
+	.volt_lsb = 10,
+	.temp_lsb = 421,
+	.temp_offset = 261315,
+};
+
+static const struct of_device_id rb_poe_dt_match[] = {
+	{
+		.compatible = "mikrotik,poe-v2",
+		.data = (void *)&v2_data,
+	}, {
+		.compatible = "mikrotik,poe-v3",
+		.data = (void *)&v3_data,
+	}, { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, rb_poe_dt_match);
+
+static struct spi_driver rb_poe_driver = {
+	.probe = rb_poe_probe,
+	.driver = {
+		.name = "rb-poe-driver",
+		.bus = &spi_bus_type,
+		.of_match_table = of_match_ptr(rb_poe_dt_match),
+	},
+};
+
+module_spi_driver(rb_poe_driver);
+
+MODULE_AUTHOR("Oskari Lemmela <oskari at lemmela.net>");
+MODULE_DESCRIPTION("Mikrotik POE driver");
+MODULE_LICENSE("GPL");
diff --git a/target/linux/ath79/files/drivers/hwmon/rbpoe.h b/target/linux/ath79/files/drivers/hwmon/rbpoe.h
new file mode 100644
index 0000000000..af1f59329f
--- /dev/null
+++ b/target/linux/ath79/files/drivers/hwmon/rbpoe.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0-only
+ *
+ * POE driver for the MikroTik RouterBoard series
+ */
+#include <linux/spi/spi.h>
+
+#define MAX_RETRIES 10
+
+struct rb_poe_model {
+	bool reverse;
+	u8 volt_lsb;
+	u16 temp_lsb;
+	u32 temp_offset;
+};
+
+struct rb_poe_data {
+	struct spi_device *spi;
+	struct rb_poe_model *data;
+
+	struct mutex lock;
+};
+
+int rb_poe_write_cmd(struct rb_poe_data *data, u16 *resp, u8 cmd, u8 arg1, u8 arg2);
+int rb_poe_get_port_idx(struct rb_poe_data *data, int reg);
+int rb_poe_read_voltage(struct rb_poe_data *data);
diff --git a/target/linux/ath79/files/drivers/hwmon/rbpoeport.c b/target/linux/ath79/files/drivers/hwmon/rbpoeport.c
new file mode 100644
index 0000000000..1b9852a0e7
--- /dev/null
+++ b/target/linux/ath79/files/drivers/hwmon/rbpoeport.c
@@ -0,0 +1,311 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+/*
+ * Mikrotik POE port driver
+ */
+
+#include <linux/err.h>
+#include <linux/delay.h>
+#include <linux/hwmon.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/jiffies.h>
+#include <linux/spi/spi.h>
+#include <linux/platform_device.h>
+#include <linux/of_platform.h>
+
+#include "rbpoe.h"
+
+#define PORT_DISABLED 0x0
+#define PORT_FORCE    0x1
+#define PORT_ENABLED  0x2
+
+struct poeport_data {
+	struct rb_poe_data *poe;
+	struct device *dev;
+
+	u8 state;
+	u8 force;
+	u32 reg;
+};
+
+static int read_state(struct poeport_data *port)
+{
+	int ret;
+	u16 status;
+
+	ret = rb_poe_write_cmd(port->poe, &status, 0x45, 0, 0);
+	if (ret < 0)
+		return ret;
+	return status >> rb_poe_get_port_idx(port->poe, port->reg)*4 & 0xF;
+}
+
+static int write_state(struct poeport_data *port, u8 state, u8 force)
+{
+	int ret;
+	u16 status;
+
+	if (state)
+		if (force)
+			state = PORT_FORCE;
+		else
+			state = PORT_ENABLED;
+	else
+		state = PORT_DISABLED;
+
+	ret = rb_poe_write_cmd(port->poe, &status, 0x44, port->reg, state);
+	if (ret < 0) {
+		usleep_range(100, 150);
+		ret = rb_poe_write_cmd(port->poe, &status, 0x44, port->reg, state);
+	}
+	return ret;
+}
+
+static int read_current(struct poeport_data *port)
+{
+	int ret;
+	u16 curr;
+
+	ret = rb_poe_write_cmd(port->poe, &curr, 0x58+port->reg, 0, 0);
+	if (ret < 0)
+		return ret;
+	return curr;
+}
+
+/* sysfs attributes */
+static ssize_t in_input_show(struct device *dev,
+			     struct device_attribute *attr, char *buf)
+{
+	struct poeport_data *data = dev_get_drvdata(dev);
+	int state, curr;
+
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	state = read_state(data);
+	if (state < 0 || state == PORT_DISABLED)
+		return -ENODATA;
+
+	curr = read_current(data);
+	if (curr < 0 || (state == PORT_ENABLED && curr >> 15))
+		return -ENODATA;
+
+	return sprintf(buf, "%d\n", rb_poe_read_voltage(data->poe));
+}
+
+static ssize_t force_enable_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	struct poeport_data *data = dev_get_drvdata(dev);
+
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	return sprintf(buf, "%d\n", data->force);
+}
+
+static ssize_t force_enable_store(struct device *dev,
+				  struct device_attribute *attr,
+				  const char *buf,
+				  size_t count)
+{
+	struct poeport_data *data = dev_get_drvdata(dev);
+	int ret;
+	int state;
+
+	ret = kstrtoint(buf, 0, &state);
+	if (ret)
+		return ret;
+
+	data->force = state;
+	ret = write_state(data, data->state, state);
+	if (ret)
+		return ret;
+	return count;
+}
+
+static ssize_t port_state_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	struct poeport_data *data = dev_get_drvdata(dev);
+	int curr;
+
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	if (!data->state)
+		return sprintf(buf, "disabled\n");
+
+	curr = read_current(data);
+	if (curr == 0x800A)
+		return sprintf(buf, "short circuit\n");
+	if (!data->force && curr >> 15)
+		return sprintf(buf, "searching\n");
+	if (curr < 3)
+		return sprintf(buf, "no load\n");
+
+	return sprintf(buf, "delivering\n");
+
+}
+
+static ssize_t in_enable_show(struct device *dev,
+			      struct device_attribute *attr, char *buf)
+{
+	struct poeport_data *data = dev_get_drvdata(dev);
+
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	return sprintf(buf, "%d\n", data->state);
+}
+
+static ssize_t in_enable_store(struct device *dev,
+			       struct device_attribute *attr,
+			       const char *buf,
+			       size_t count)
+{
+	struct poeport_data *data = dev_get_drvdata(dev);
+	int ret;
+	int state;
+
+	ret = kstrtoint(buf, 0, &state);
+	if (ret)
+		return ret;
+
+	data->state = state;
+	ret = write_state(data, state, data->force);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t curr_input_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	struct poeport_data *data = dev_get_drvdata(dev);
+	int curr;
+
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	curr = read_current(data);
+	if (curr < 0 || curr >> 15)
+		return -ENODATA;
+
+	return sprintf(buf, "%d\n", curr);
+}
+
+static ssize_t power_input_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	struct poeport_data *data = dev_get_drvdata(dev);
+	int volt, curr;
+
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	curr = read_current(data);
+	if (curr < 0 || curr >> 15)
+		return -ENODATA;
+
+	volt = rb_poe_read_voltage(data->poe);
+	if (volt < 0)
+		return -ENODATA;
+
+	return sprintf(buf, "%d\n", curr*volt);
+}
+
+static SENSOR_DEVICE_ATTR_RO(curr1_input, curr_input, 0);
+static SENSOR_DEVICE_ATTR_RO(power1_input, power_input, 0);
+static SENSOR_DEVICE_ATTR_RW(in1_enable, in_enable, 0);
+static SENSOR_DEVICE_ATTR_RO(in1_input, in_input, 0);
+static SENSOR_DEVICE_ATTR_RW(force_enable, force_enable, 0);
+static SENSOR_DEVICE_ATTR_RO(port_state, port_state, 0);
+
+static struct attribute *rb_poeport_attrs[] = {
+	&sensor_dev_attr_curr1_input.dev_attr.attr,
+	&sensor_dev_attr_power1_input.dev_attr.attr,
+	&sensor_dev_attr_in1_enable.dev_attr.attr,
+	&sensor_dev_attr_in1_input.dev_attr.attr,
+	&sensor_dev_attr_force_enable.dev_attr.attr,
+	&sensor_dev_attr_port_state.dev_attr.attr,
+	NULL
+};
+ATTRIBUTE_GROUPS(rb_poeport);
+
+static int rb_poeport_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device *hwmon_dev;
+	struct poeport_data *data;
+	const char *label;
+	int ret, val;
+
+	data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	data->dev = dev;
+
+	ret = of_property_read_u32(dev->of_node, "reg", &val);
+	if (ret < 0) {
+		dev_err(dev, "missing 'reg' property (%d)\n", ret);
+		return ret;
+	}
+
+	data->reg = val;
+
+	if (of_property_read_string(dev->of_node, "label", &label) < 0)
+		label = "poeport";
+
+	if (!dev->parent) {
+		dev_err(dev, "no ctrl device\n");
+		return -ENODEV;
+	}
+
+	data->poe = dev_get_drvdata(dev->parent);
+
+	switch (read_state(data)) {
+	case PORT_FORCE:
+		data->force = 1;
+		data->state = 1;
+		break;
+	case PORT_ENABLED:
+		data->force = 0;
+		data->state = 1;
+		break;
+	case PORT_DISABLED:
+		data->state = 0;
+		data->force = 0;
+	}
+
+	hwmon_dev = devm_hwmon_device_register_with_groups(dev, label, data, rb_poeport_groups);
+
+	if (IS_ERR(hwmon_dev))
+		dev_dbg(dev, "unable to register hwmon device\n");
+
+	return PTR_ERR_OR_ZERO(hwmon_dev);
+}
+
+static const struct of_device_id rb_poeport_of_match[] = {
+	{ .compatible = "mikrotik,poeport" },
+	{ /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, rb_poeport_of_match);
+
+static struct platform_driver rb_poeport_driver = {
+	.probe = rb_poeport_probe,
+	.driver = {
+		.name = "rb-poeport-driver",
+		.of_match_table = of_match_ptr(rb_poeport_of_match),
+	},
+};
+
+module_platform_driver(rb_poeport_driver);
+
+MODULE_AUTHOR("Oskari Lemmela <oskari at lemmela.net>");
+MODULE_DESCRIPTION("Mikrotik poeport driver");
+MODULE_LICENSE("GPL");
diff --git a/target/linux/ath79/mikrotik/config-default b/target/linux/ath79/mikrotik/config-default
index 175c4fac1f..8348998421 100644
--- a/target/linux/ath79/mikrotik/config-default
+++ b/target/linux/ath79/mikrotik/config-default
@@ -7,6 +7,7 @@ CONFIG_GPIO_RB91X_KEY=y
 CONFIG_GPIO_RB4XX=y
 CONFIG_GPIO_WATCHDOG=y
 CONFIG_GPIO_WATCHDOG_ARCH_INITCALL=y
+CONFIG_HWMON=y
 CONFIG_LEDS_RESET=y
 CONFIG_LZO_DECOMPRESS=y
 CONFIG_MDIO_GPIO=y
@@ -36,6 +37,8 @@ CONFIG_PCI_AR71XX=y
 CONFIG_PHY_AR7100_USB=y
 CONFIG_PHY_AR7200_USB=y
 CONFIG_REGULATOR_FIXED_VOLTAGE=y
+CONFIG_SENSORS_MIKROTIK_POE=y
+CONFIG_SENSORS_MIKROTIK_POEPORT=y
 CONFIG_SPI_RB4XX=y
 CONFIG_UBIFS_FS=y
 CONFIG_WATCHDOG_CORE=y
diff --git a/target/linux/ath79/patches-5.10/902-hwmon-support-for-mikrotik-poe.patch b/target/linux/ath79/patches-5.10/902-hwmon-support-for-mikrotik-poe.patch
new file mode 100644
index 0000000000..ac33e2061c
--- /dev/null
+++ b/target/linux/ath79/patches-5.10/902-hwmon-support-for-mikrotik-poe.patch
@@ -0,0 +1,51 @@
+From beb4c6b6e4c186ffaec860a7c5f83ff582c37413 Mon Sep 17 00:00:00 2001
+From: Oskari Lemmela <oskari at lemmela.net>
+Date: Fri, 3 Dec 2021 15:28:49 +0200
+Subject: [PATCH] hwmon: support for mikrotik poe
+
+Signed-off-by: Oskari Lemmela <oskari at lemmela.net>
+---
+ drivers/hwmon/Kconfig  | 13 +++++++++++++
+ drivers/hwmon/Makefile |  2 ++
+ 2 files changed, 15 insertions(+)
+
+diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
+index c4578e8f34bb..9bcd1df93565 100644
+--- a/drivers/hwmon/Kconfig
++++ b/drivers/hwmon/Kconfig
+@@ -1541,6 +1541,19 @@ config SENSORS_RASPBERRYPI_HWMON
+ 	  This driver can also be built as a module. If so, the module
+ 	  will be called raspberrypi-hwmon.
+ 
++config SENSORS_MIKROTIK_POE
++	tristate "Mikrotik POE driver"
++	select CRC8
++	depends on SPI
++        help
++          If you say yes here you get support for Mikrotik POE
++
++config SENSORS_MIKROTIK_POEPORT
++	tristate "Mikrotik POE port driver"
++	depends on SENSORS_MIKROTIK_POE
++        help
++          If you say yes here you get support for Mikrotik POE
++
+ config SENSORS_SL28CPLD
+ 	tristate "Kontron sl28cpld hardware monitoring driver"
+ 	depends on MFD_SL28CPLD || COMPILE_TEST
+diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
+index 162940270661..2778b2562ee1 100644
+--- a/drivers/hwmon/Makefile
++++ b/drivers/hwmon/Makefile
+@@ -162,6 +162,8 @@ obj-$(CONFIG_SENSORS_PCF8591)	+= pcf8591.o
+ obj-$(CONFIG_SENSORS_POWR1220)  += powr1220.o
+ obj-$(CONFIG_SENSORS_PWM_FAN)	+= pwm-fan.o
+ obj-$(CONFIG_SENSORS_RASPBERRYPI_HWMON)	+= raspberrypi-hwmon.o
++obj-$(CONFIG_SENSORS_MIKROTIK_POE)	+= rbpoe.o
++obj-$(CONFIG_SENSORS_MIKROTIK_POEPORT)	+= rbpoeport.o
+ obj-$(CONFIG_SENSORS_S3C)	+= s3c-hwmon.o
+ obj-$(CONFIG_SENSORS_SBTSI)	+= sbtsi_temp.o
+ obj-$(CONFIG_SENSORS_SBRMI)	+= sbrmi.o
+-- 
+2.25.1
+
-- 
2.25.1




More information about the openwrt-devel mailing list