diff mbox series

generic: platform/mikrotik: implement multi caldata

Message ID 20200824103840.38920-1-hacks@slashdirt.org
State New
Headers show
Series generic: platform/mikrotik: implement multi caldata | expand

Commit Message

Thibaut Aug. 24, 2020, 10:38 a.m. UTC
MikroTik recently changed again the way they store wlan calibration data
on devices. Prior to this change, ERD calibration data for all available
radios was stored within a single identifier node ("tag" in RouterBoot
parlance).

Recent devices have been seen with calibration (and BDF) data stored in
separate identifiers within LZOR packing for each radio: this patch
addresses this by:
1) ensuring that both variants are properly supported,
2) preserving backward compatibility with existing data consumers,
3) allowing for more than 2 calibration blobs to be exposed via sysfs.

Specifically, before this patch, the driver would provide a single sysfs
file named /sys/firmware/mikrotik/hard_config/wlan_data that contained
whatever calibration data found on the device's flash. After this patch,
when executed on a device that uses the old style storage, this behavior
is unchanged, but when executed on a device that uses new style storage
(for either traditional "ERD" packing or "LZOR" packing), the driver
replaces that single file with a folder containing one or more files
each containing the data encoded within individual identifiers.

As far as OpenWRT is concerned, this means that for devices which are
known to exist with both styles of data storage, a suitable hotplug stub
could look like this for e.g. the second radio:

wdata="/sys/firmware/mikrotik/hard_config/wlan_data"
( [ -f "$wdata" ] && caldata_sysfsload_from_file "$wdata" 0x8000 0x2f20 ) || \
( [ -d "$wdata" ] && caldata_sysfsload_from_file "$wdata/data_2" 0x0 0x2f20 )

This patch has been tested with LZOR old and new style packing on ipq4019,
and with old style on ath79.

Tested-by: John Thomson <git@johnthomson.fastmail.com.au>
Tested-by: Шебанов Алексей <admin@ublaze.ru>
Tested-by: Alen Opačić <subixonfire@gmail.com>
Signed-off-by: Thibaut VARÈNE <hacks@slashdirt.org>
---
 .../drivers/platform/mikrotik/rb_hardconfig.c | 139 +++++++++++++-----
 1 file changed, 106 insertions(+), 33 deletions(-)

Comments

Thibaut Sept. 25, 2020, 7:09 p.m. UTC | #1
Ping?

> Le 24 août 2020 à 12:38, Thibaut VARÈNE <hacks@slashdirt.org> a écrit :
> 
> MikroTik recently changed again the way they store wlan calibration data
> on devices. Prior to this change, ERD calibration data for all available
> radios was stored within a single identifier node ("tag" in RouterBoot
> parlance).
> 
> Recent devices have been seen with calibration (and BDF) data stored in
> separate identifiers within LZOR packing for each radio: this patch
> addresses this by:
> 1) ensuring that both variants are properly supported,
> 2) preserving backward compatibility with existing data consumers,
> 3) allowing for more than 2 calibration blobs to be exposed via sysfs.
> 
> Specifically, before this patch, the driver would provide a single sysfs
> file named /sys/firmware/mikrotik/hard_config/wlan_data that contained
> whatever calibration data found on the device's flash. After this patch,
> when executed on a device that uses the old style storage, this behavior
> is unchanged, but when executed on a device that uses new style storage
> (for either traditional "ERD" packing or "LZOR" packing), the driver
> replaces that single file with a folder containing one or more files
> each containing the data encoded within individual identifiers.
> 
> As far as OpenWRT is concerned, this means that for devices which are
> known to exist with both styles of data storage, a suitable hotplug stub
> could look like this for e.g. the second radio:
> 
> wdata="/sys/firmware/mikrotik/hard_config/wlan_data"
> ( [ -f "$wdata" ] && caldata_sysfsload_from_file "$wdata" 0x8000 0x2f20 ) || \
> ( [ -d "$wdata" ] && caldata_sysfsload_from_file "$wdata/data_2" 0x0 0x2f20 )
> 
> This patch has been tested with LZOR old and new style packing on ipq4019,
> and with old style on ath79.
> 
> Tested-by: John Thomson <git@johnthomson.fastmail.com.au>
> Tested-by: Шебанов Алексей <admin@ublaze.ru>
> Tested-by: Alen Opačić <subixonfire@gmail.com>
> Signed-off-by: Thibaut VARÈNE <hacks@slashdirt.org>
> ---
> .../drivers/platform/mikrotik/rb_hardconfig.c | 139 +++++++++++++-----
> 1 file changed, 106 insertions(+), 33 deletions(-)
> 
> diff --git a/target/linux/generic/files/drivers/platform/mikrotik/rb_hardconfig.c b/target/linux/generic/files/drivers/platform/mikrotik/rb_hardconfig.c
> index 8861814be4..41dea98b5e 100644
> --- a/target/linux/generic/files/drivers/platform/mikrotik/rb_hardconfig.c
> +++ b/target/linux/generic/files/drivers/platform/mikrotik/rb_hardconfig.c
> @@ -39,7 +39,7 @@
> 
> #include "routerboot.h"
> 
> -#define RB_HARDCONFIG_VER		"0.05"
> +#define RB_HARDCONFIG_VER		"0.06"
> #define RB_HC_PR_PFX			"[rb_hardconfig] "
> 
> /* ID values for hardware settings */
> @@ -76,6 +76,17 @@
> #define RB_HW_OPT_HAS_TS_FOR_ADC	BIT(22)
> #define RB_HW_OPT_HAS_PLC		BIT(29)
> 
> +/*
> + * Tag ID values for ERD data.
> + * Mikrotik used to pack all calibration data under a single tag id 0x1, but
> + * recently switched to a new scheme where each radio calibration gets a
> + * separate tag. The new scheme has tag id bit 15 always set and seems to be
> + * mutually exclusive with the old scheme.
> + */
> +#define RB_WLAN_ERD_ID_SOLO		0x0001
> +#define RB_WLAN_ERD_ID_MULTI_8001	0x8001
> +#define RB_WLAN_ERD_ID_MULTI_8201	0x8201
> +
> static struct kobject *hc_kobj;
> static u8 *hc_buf;		// ro buffer after init(): no locking required
> static size_t hc_buflen;
> @@ -351,10 +362,22 @@ static ssize_t hc_wlan_data_bin_read(struct file *filp, struct kobject *kobj,
> 				     loff_t off, size_t count);
> 
> static struct hc_wlan_attr {
> +	const u16 erd_tag_id;
> 	struct bin_attribute battr;
> 	u16 pld_ofs;
> 	u16 pld_len;
> -} hc_wlandata_battr = {
> +} hc_wd_multi_battrs[] = {
> +	{
> +		.erd_tag_id = RB_WLAN_ERD_ID_MULTI_8001,
> +		.battr = __BIN_ATTR(data_0, S_IRUSR, hc_wlan_data_bin_read, NULL, 0),
> +	}, {
> +		.erd_tag_id = RB_WLAN_ERD_ID_MULTI_8201,
> +		.battr = __BIN_ATTR(data_2, S_IRUSR, hc_wlan_data_bin_read, NULL, 0),
> +	}
> +};
> +
> +static struct hc_wlan_attr hc_wd_solo_battr = {
> +	.erd_tag_id = RB_WLAN_ERD_ID_SOLO,
> 	.battr = __BIN_ATTR(wlan_data, S_IRUSR, hc_wlan_data_bin_read, NULL, 0),
> };
> 
> @@ -426,19 +449,19 @@ static struct hc_attr {
> /*
>  * If the RB_ID_WLAN_DATA payload starts with RB_MAGIC_ERD, then past
>  * that magic number the payload itself contains a routerboot tag node
> - * locating the LZO-compressed calibration data at id 0x1.
> + * locating the LZO-compressed calibration data. So far this scheme is only
> + * known to use a single tag at id 0x1.
>  */
> -static int hc_wlan_data_unpack_erd(const u8 *inbuf, size_t inlen,
> +static int hc_wlan_data_unpack_erd(const u16 tag_id, const u8 *inbuf, size_t inlen,
> 				   void *outbuf, size_t *outlen)
> {
> 	u16 lzo_ofs, lzo_len;
> 	int ret;
> 
> 	/* Find embedded tag */
> -	ret = routerboot_tag_find(inbuf, inlen, 0x1,	// always id 1
> -				  &lzo_ofs, &lzo_len);
> +	ret = routerboot_tag_find(inbuf, inlen, tag_id, &lzo_ofs, &lzo_len);
> 	if (ret) {
> -		pr_debug(RB_HC_PR_PFX "ERD data not found\n");
> +		pr_debug(RB_HC_PR_PFX "no ERD data for id 0x%04x\n", tag_id);
> 		goto fail;
> 	}
> 
> @@ -461,10 +484,10 @@ fail:
>  * that magic number is a payload that must be appended to the hc_lzor_prefix,
>  * the resulting blob is LZO-compressed. In the LZO decompression result,
>  * the RB_MAGIC_ERD magic number (aligned) must be located. Following that
> - * magic, there is a routerboot tag node (id 0x1) locating the RLE-encoded
> + * magic, there is one or more routerboot tag node(s) locating the RLE-encoded
>  * calibration data payload.
>  */
> -static int hc_wlan_data_unpack_lzor(const u8 *inbuf, size_t inlen,
> +static int hc_wlan_data_unpack_lzor(const u16 tag_id, const u8 *inbuf, size_t inlen,
> 				    void *outbuf, size_t *outlen)
> {
> 	u16 rle_ofs, rle_len;
> @@ -492,10 +515,8 @@ static int hc_wlan_data_unpack_lzor(const u8 *inbuf, size_t inlen,
> 	if (ret) {
> 		if (LZO_E_INPUT_NOT_CONSUMED == ret) {
> 			/*
> -			 * The tag length appears to always be aligned (probably
> -			 * because it is the "root" RB_ID_WLAN_DATA tag), thus
> -			 * the LZO payload may be padded, which can trigger a
> -			 * spurious error which we ignore here.
> +			 * The tag length is always aligned thus the LZO payload may be padded,
> +			 * which can trigger a spurious error which we ignore here.
> 			 */
> 			pr_debug(RB_HC_PR_PFX "LZOR: LZO EOF before buffer end - this may be harmless\n");
> 		} else {
> @@ -520,9 +541,9 @@ static int hc_wlan_data_unpack_lzor(const u8 *inbuf, size_t inlen,
> 	templen -= (u8 *)needle - tempbuf;
> 
> 	/* Past magic. Look for tag node */
> -	ret = routerboot_tag_find((u8 *)needle, templen, 0x1, &rle_ofs, &rle_len);
> +	ret = routerboot_tag_find((u8 *)needle, templen, tag_id, &rle_ofs, &rle_len);
> 	if (ret) {
> -		pr_debug(RB_HC_PR_PFX "LZOR: RLE data not found\n");
> +		pr_debug(RB_HC_PR_PFX "LZOR: no RLE data for id 0x%04x\n", tag_id);
> 		goto fail;
> 	}
> 
> @@ -542,7 +563,7 @@ fail:
> 	return ret;
> }
> 
> -static int hc_wlan_data_unpack(const size_t tofs, size_t tlen,
> +static int hc_wlan_data_unpack(const u16 tag_id, const size_t tofs, size_t tlen,
> 			       void *outbuf, size_t *outlen)
> {
> 	const u8 *lbuf;
> @@ -562,23 +583,25 @@ static int hc_wlan_data_unpack(const size_t tofs, size_t tlen,
> 		/* Skip magic */
> 		lbuf += sizeof(magic);
> 		tlen -= sizeof(magic);
> -		ret = hc_wlan_data_unpack_lzor(lbuf, tlen, outbuf, outlen);
> +		ret = hc_wlan_data_unpack_lzor(tag_id, lbuf, tlen, outbuf, outlen);
> 		break;
> 	case RB_MAGIC_ERD:
> 		/* Skip magic */
> 		lbuf += sizeof(magic);
> 		tlen -= sizeof(magic);
> -		ret = hc_wlan_data_unpack_erd(lbuf, tlen, outbuf, outlen);
> +		ret = hc_wlan_data_unpack_erd(tag_id, lbuf, tlen, outbuf, outlen);
> 		break;
> 	default:
> 		/*
> 		 * If the RB_ID_WLAN_DATA payload doesn't start with a
> 		 * magic number, the payload itself is the raw RLE-encoded
> -		 * calibration data.
> +		 * calibration data. Only RB_WLAN_ERD_ID_SOLO makes sense here.
> 		 */
> -		ret = routerboot_rle_decode(lbuf, tlen, outbuf, outlen);
> -		if (ret)
> -			pr_debug(RB_HC_PR_PFX "RLE decoding error (%d)\n", ret);
> +		if (RB_WLAN_ERD_ID_SOLO == tag_id) {
> +			ret = routerboot_rle_decode(lbuf, tlen, outbuf, outlen);
> +			if (ret)
> +				pr_debug(RB_HC_PR_PFX "RLE decoding error (%d)\n", ret);
> +		}
> 		break;
> 	}
> 
> @@ -633,7 +656,7 @@ static ssize_t hc_wlan_data_bin_read(struct file *filp, struct kobject *kobj,
> 	if (!outbuf)
> 		return -ENOMEM;
> 
> -	ret = hc_wlan_data_unpack(hc_wattr->pld_ofs, hc_wattr->pld_len, outbuf, &outlen);
> +	ret = hc_wlan_data_unpack(hc_wattr->erd_tag_id, hc_wattr->pld_ofs, hc_wattr->pld_len, outbuf, &outlen);
> 	if (ret) {
> 		kfree(outbuf);
> 		return ret;
> @@ -655,14 +678,17 @@ static ssize_t hc_wlan_data_bin_read(struct file *filp, struct kobject *kobj,
> 
> int __init rb_hardconfig_init(struct kobject *rb_kobj)
> {
> +	struct kobject *hc_wlan_kobj;
> 	struct mtd_info *mtd;
> -	size_t bytes_read, buflen;
> +	size_t bytes_read, buflen, outlen;
> 	const u8 *buf;
> -	int i, ret;
> +	void *outbuf;
> +	int i, j, ret;
> 	u32 magic;
> 
> 	hc_buf = NULL;
> 	hc_kobj = NULL;
> +	hc_wlan_kobj = NULL;
> 
> 	// TODO allow override
> 	mtd = get_mtd_device_nm(RB_MTD_HARD_CONFIG);
> @@ -713,15 +739,62 @@ int __init rb_hardconfig_init(struct kobject *rb_kobj)
> 		/* Account for skipped magic */
> 		hc_attrs[i].pld_ofs += sizeof(magic);
> 
> -		/* Special case RB_ID_WLAN_DATA to prep and create the binary attribute */
> +		/*
> +		 * Special case RB_ID_WLAN_DATA to prep and create the binary attribute.
> +		 * We first check if the data is "old style" within a single tag (or no tag at all):
> +		 * If it is we publish this single blob as a binary attribute child of hc_kobj to
> +		 * preserve backward compatibility.
> +		 * If it isn't and instead uses multiple ERD tags, we create a subfolder and
> +		 * publish the known ones there.
> +		 */
> 		if ((RB_ID_WLAN_DATA == hc_attrs[i].tag_id) && hc_attrs[i].pld_len) {
> -			hc_wlandata_battr.pld_ofs = hc_attrs[i].pld_ofs;
> -			hc_wlandata_battr.pld_len = hc_attrs[i].pld_len;
> -
> -			ret = sysfs_create_bin_file(hc_kobj, &hc_wlandata_battr.battr);
> -			if (ret)
> -				pr_warn(RB_HC_PR_PFX "Could not create %s sysfs entry (%d)\n",
> -				       hc_wlandata_battr.battr.attr.name, ret);
> +			outlen = RB_ART_SIZE;
> +			outbuf = kmalloc(outlen, GFP_KERNEL);
> +			if (!outbuf) {
> +				pr_warn(RB_HC_PR_PFX "Out of memory parsing WLAN tag\n");
> +				continue;
> +			}
> +
> +			/* Test ID_SOLO first, if found: done */
> +			ret = hc_wlan_data_unpack(RB_WLAN_ERD_ID_SOLO, hc_attrs[i].pld_ofs, hc_attrs[i].pld_len, outbuf, &outlen);
> +			if (!ret) {
> +				hc_wd_solo_battr.pld_ofs = hc_attrs[i].pld_ofs;
> +				hc_wd_solo_battr.pld_len = hc_attrs[i].pld_len;
> +
> +				ret = sysfs_create_bin_file(hc_kobj, &hc_wd_solo_battr.battr);
> +				if (ret)
> +					pr_warn(RB_HC_PR_PFX "Could not create %s sysfs entry (%d)\n",
> +						hc_wd_solo_battr.battr.attr.name, ret);
> +			}
> +			/* Otherwise, create "wlan_data" subtree and publish known data */
> +			else {
> +				hc_wlan_kobj = kobject_create_and_add("wlan_data", hc_kobj);
> +				if (!hc_wlan_kobj) {
> +					kfree(outbuf);
> +					pr_warn(RB_HC_PR_PFX "Could not create wlan_data sysfs folder\n");
> +					continue;
> +				}
> +
> +				for (j = 0; j < ARRAY_SIZE(hc_wd_multi_battrs); j++) {
> +					outlen = RB_ART_SIZE;
> +					ret = hc_wlan_data_unpack(hc_wd_multi_battrs[j].erd_tag_id,
> +								  hc_attrs[i].pld_ofs, hc_attrs[i].pld_len, outbuf, &outlen);
> +					if (ret) {
> +						hc_wd_multi_battrs[j].pld_ofs = hc_wd_multi_battrs[j].pld_len = 0;
> +						continue;
> +					}
> +
> +					hc_wd_multi_battrs[j].pld_ofs = hc_attrs[i].pld_ofs;
> +					hc_wd_multi_battrs[j].pld_len = hc_attrs[i].pld_len;
> +
> +					ret = sysfs_create_bin_file(hc_wlan_kobj, &hc_wd_multi_battrs[j].battr);
> +					if (ret)
> +						pr_warn(RB_HC_PR_PFX "Could not create wlan_data/%s sysfs entry (%d)\n",
> +							hc_wd_multi_battrs[j].battr.attr.name, ret);
> +				}
> +			}
> +
> +			kfree(outbuf);
> 		}
> 		/* All other tags are published via standard attributes */
> 		else {
> -- 
> 2.24.3 (Apple Git-128)
>
Alexander 'lynxis' Couzens Oct. 7, 2020, 8:41 p.m. UTC | #2
On Fri, 25 Sep 2020 21:09:26 +0200
Thibaut <hacks@slashdirt.org> wrote:

> Ping?

LGTM.

What's in the "$wdata/data_0" file? Is it the BDF?

Best,
lynxis
Thibaut Oct. 8, 2020, 9:11 a.m. UTC | #3
Hi,

> On 7 Oct 2020, at 22:41, Alexander 'lynxis' Couzens <lynxis@fe80.eu> wrote:
> 
> On Fri, 25 Sep 2020 21:09:26 +0200
> Thibaut <hacks@slashdirt.org> wrote:
> 
>> Ping?
> 
> LGTM.
> 
> What's in the "$wdata/data_0" file? Is it the BDF?

My understanding is (disclaimer: I don’t own any hardware that exposes these new files) that on old style devices, the calibration (and BDF where applicable, which means it could be dynamically loaded as well) data of all radios was packed into a single payload, which got exposed as the “wlan_data” sysfs file. Userspace then had to apply offsets to access data pertaining to each radio.

New style simply separates each radio into its own payload, so data_0 is calib (+BDF where applicable) for one of the radios and so on, which makes it easier to process (probably for RouterOS as well, hence the change ;)

There are some devices out there which have been shipped with both styles (the hap-ac2 is one of them), hence the patch ensure that 1) previous behavior is unchanged and 2) it’s “easy” to accommodate both styles in userspace (as suggested in the commit log).

HTH,
Thibaut
Thibaut Oct. 19, 2020, 6:01 p.m. UTC | #4
> Le 8 oct. 2020 à 11:11, Thibaut <hacks@slashdirt.org> a écrit :
> 
> Hi,
> 
>> On 7 Oct 2020, at 22:41, Alexander 'lynxis' Couzens <lynxis@fe80.eu> wrote:
>> 
>> On Fri, 25 Sep 2020 21:09:26 +0200
>> Thibaut <hacks@slashdirt.org> wrote:
>> 
>>> Ping?
>> 
>> LGTM.
>> 
>> What's in the "$wdata/data_0" file? Is it the BDF?
> 
> My understanding is (disclaimer: I don’t own any hardware that exposes these new files) that on old style devices, the calibration (and BDF where applicable, which means it could be dynamically loaded as well) data of all radios was packed into a single payload, which got exposed as the “wlan_data” sysfs file. Userspace then had to apply offsets to access data pertaining to each radio.
> 
> New style simply separates each radio into its own payload, so data_0 is calib (+BDF where applicable) for one of the radios and so on, which makes it easier to process (probably for RouterOS as well, hence the change ;)
> 
> There are some devices out there which have been shipped with both styles (the hap-ac2 is one of them), hence the patch ensure that 1) previous behavior is unchanged and 2) it’s “easy” to accommodate both styles in userspace (as suggested in the commit log).

CC’ing Robert Marko who wants to add his Tested-by.

Patch at https://patchwork.ozlabs.org/project/openwrt/patch/20200824103840.38920-1-hacks@slashdirt.org/

Cheers
Robert Marko Oct. 19, 2020, 6:03 p.m. UTC | #5
On Mon, 19 Oct 2020 at 20:01, Thibaut <hacks@slashdirt.org> wrote:
>
>
> > Le 8 oct. 2020 à 11:11, Thibaut <hacks@slashdirt.org> a écrit :
> >
> > Hi,
> >
> >> On 7 Oct 2020, at 22:41, Alexander 'lynxis' Couzens <lynxis@fe80.eu> wrote:
> >>
> >> On Fri, 25 Sep 2020 21:09:26 +0200
> >> Thibaut <hacks@slashdirt.org> wrote:
> >>
> >>> Ping?
> >>
> >> LGTM.
> >>
> >> What's in the "$wdata/data_0" file? Is it the BDF?
> >
> > My understanding is (disclaimer: I don’t own any hardware that exposes these new files) that on old style devices, the calibration (and BDF where applicable, which means it could be dynamically loaded as well) data of all radios was packed into a single payload, which got exposed as the “wlan_data” sysfs file. Userspace then had to apply offsets to access data pertaining to each radio.
> >
> > New style simply separates each radio into its own payload, so data_0 is calib (+BDF where applicable) for one of the radios and so on, which makes it easier to process (probably for RouterOS as well, hence the change ;)
> >
> > There are some devices out there which have been shipped with both styles (the hap-ac2 is one of them), hence the patch ensure that 1) previous behavior is unchanged and 2) it’s “easy” to accommodate both styles in userspace (as suggested in the commit log).
>
> CC’ing Robert Marko who wants to add his Tested-by.
>
> Patch at https://patchwork.ozlabs.org/project/openwrt/patch/20200824103840.38920-1-hacks@slashdirt.org/
>
> Cheers

Works on my hAP ac2 with old style caldata, so:

Tested-by: Robert Marko <robimarko@gmail.com>
diff mbox series

Patch

diff --git a/target/linux/generic/files/drivers/platform/mikrotik/rb_hardconfig.c b/target/linux/generic/files/drivers/platform/mikrotik/rb_hardconfig.c
index 8861814be4..41dea98b5e 100644
--- a/target/linux/generic/files/drivers/platform/mikrotik/rb_hardconfig.c
+++ b/target/linux/generic/files/drivers/platform/mikrotik/rb_hardconfig.c
@@ -39,7 +39,7 @@ 
 
 #include "routerboot.h"
 
-#define RB_HARDCONFIG_VER		"0.05"
+#define RB_HARDCONFIG_VER		"0.06"
 #define RB_HC_PR_PFX			"[rb_hardconfig] "
 
 /* ID values for hardware settings */
@@ -76,6 +76,17 @@ 
 #define RB_HW_OPT_HAS_TS_FOR_ADC	BIT(22)
 #define RB_HW_OPT_HAS_PLC		BIT(29)
 
+/*
+ * Tag ID values for ERD data.
+ * Mikrotik used to pack all calibration data under a single tag id 0x1, but
+ * recently switched to a new scheme where each radio calibration gets a
+ * separate tag. The new scheme has tag id bit 15 always set and seems to be
+ * mutually exclusive with the old scheme.
+ */
+#define RB_WLAN_ERD_ID_SOLO		0x0001
+#define RB_WLAN_ERD_ID_MULTI_8001	0x8001
+#define RB_WLAN_ERD_ID_MULTI_8201	0x8201
+
 static struct kobject *hc_kobj;
 static u8 *hc_buf;		// ro buffer after init(): no locking required
 static size_t hc_buflen;
@@ -351,10 +362,22 @@  static ssize_t hc_wlan_data_bin_read(struct file *filp, struct kobject *kobj,
 				     loff_t off, size_t count);
 
 static struct hc_wlan_attr {
+	const u16 erd_tag_id;
 	struct bin_attribute battr;
 	u16 pld_ofs;
 	u16 pld_len;
-} hc_wlandata_battr = {
+} hc_wd_multi_battrs[] = {
+	{
+		.erd_tag_id = RB_WLAN_ERD_ID_MULTI_8001,
+		.battr = __BIN_ATTR(data_0, S_IRUSR, hc_wlan_data_bin_read, NULL, 0),
+	}, {
+		.erd_tag_id = RB_WLAN_ERD_ID_MULTI_8201,
+		.battr = __BIN_ATTR(data_2, S_IRUSR, hc_wlan_data_bin_read, NULL, 0),
+	}
+};
+
+static struct hc_wlan_attr hc_wd_solo_battr = {
+	.erd_tag_id = RB_WLAN_ERD_ID_SOLO,
 	.battr = __BIN_ATTR(wlan_data, S_IRUSR, hc_wlan_data_bin_read, NULL, 0),
 };
 
@@ -426,19 +449,19 @@  static struct hc_attr {
 /*
  * If the RB_ID_WLAN_DATA payload starts with RB_MAGIC_ERD, then past
  * that magic number the payload itself contains a routerboot tag node
- * locating the LZO-compressed calibration data at id 0x1.
+ * locating the LZO-compressed calibration data. So far this scheme is only
+ * known to use a single tag at id 0x1.
  */
-static int hc_wlan_data_unpack_erd(const u8 *inbuf, size_t inlen,
+static int hc_wlan_data_unpack_erd(const u16 tag_id, const u8 *inbuf, size_t inlen,
 				   void *outbuf, size_t *outlen)
 {
 	u16 lzo_ofs, lzo_len;
 	int ret;
 
 	/* Find embedded tag */
-	ret = routerboot_tag_find(inbuf, inlen, 0x1,	// always id 1
-				  &lzo_ofs, &lzo_len);
+	ret = routerboot_tag_find(inbuf, inlen, tag_id, &lzo_ofs, &lzo_len);
 	if (ret) {
-		pr_debug(RB_HC_PR_PFX "ERD data not found\n");
+		pr_debug(RB_HC_PR_PFX "no ERD data for id 0x%04x\n", tag_id);
 		goto fail;
 	}
 
@@ -461,10 +484,10 @@  fail:
  * that magic number is a payload that must be appended to the hc_lzor_prefix,
  * the resulting blob is LZO-compressed. In the LZO decompression result,
  * the RB_MAGIC_ERD magic number (aligned) must be located. Following that
- * magic, there is a routerboot tag node (id 0x1) locating the RLE-encoded
+ * magic, there is one or more routerboot tag node(s) locating the RLE-encoded
  * calibration data payload.
  */
-static int hc_wlan_data_unpack_lzor(const u8 *inbuf, size_t inlen,
+static int hc_wlan_data_unpack_lzor(const u16 tag_id, const u8 *inbuf, size_t inlen,
 				    void *outbuf, size_t *outlen)
 {
 	u16 rle_ofs, rle_len;
@@ -492,10 +515,8 @@  static int hc_wlan_data_unpack_lzor(const u8 *inbuf, size_t inlen,
 	if (ret) {
 		if (LZO_E_INPUT_NOT_CONSUMED == ret) {
 			/*
-			 * The tag length appears to always be aligned (probably
-			 * because it is the "root" RB_ID_WLAN_DATA tag), thus
-			 * the LZO payload may be padded, which can trigger a
-			 * spurious error which we ignore here.
+			 * The tag length is always aligned thus the LZO payload may be padded,
+			 * which can trigger a spurious error which we ignore here.
 			 */
 			pr_debug(RB_HC_PR_PFX "LZOR: LZO EOF before buffer end - this may be harmless\n");
 		} else {
@@ -520,9 +541,9 @@  static int hc_wlan_data_unpack_lzor(const u8 *inbuf, size_t inlen,
 	templen -= (u8 *)needle - tempbuf;
 
 	/* Past magic. Look for tag node */
-	ret = routerboot_tag_find((u8 *)needle, templen, 0x1, &rle_ofs, &rle_len);
+	ret = routerboot_tag_find((u8 *)needle, templen, tag_id, &rle_ofs, &rle_len);
 	if (ret) {
-		pr_debug(RB_HC_PR_PFX "LZOR: RLE data not found\n");
+		pr_debug(RB_HC_PR_PFX "LZOR: no RLE data for id 0x%04x\n", tag_id);
 		goto fail;
 	}
 
@@ -542,7 +563,7 @@  fail:
 	return ret;
 }
 
-static int hc_wlan_data_unpack(const size_t tofs, size_t tlen,
+static int hc_wlan_data_unpack(const u16 tag_id, const size_t tofs, size_t tlen,
 			       void *outbuf, size_t *outlen)
 {
 	const u8 *lbuf;
@@ -562,23 +583,25 @@  static int hc_wlan_data_unpack(const size_t tofs, size_t tlen,
 		/* Skip magic */
 		lbuf += sizeof(magic);
 		tlen -= sizeof(magic);
-		ret = hc_wlan_data_unpack_lzor(lbuf, tlen, outbuf, outlen);
+		ret = hc_wlan_data_unpack_lzor(tag_id, lbuf, tlen, outbuf, outlen);
 		break;
 	case RB_MAGIC_ERD:
 		/* Skip magic */
 		lbuf += sizeof(magic);
 		tlen -= sizeof(magic);
-		ret = hc_wlan_data_unpack_erd(lbuf, tlen, outbuf, outlen);
+		ret = hc_wlan_data_unpack_erd(tag_id, lbuf, tlen, outbuf, outlen);
 		break;
 	default:
 		/*
 		 * If the RB_ID_WLAN_DATA payload doesn't start with a
 		 * magic number, the payload itself is the raw RLE-encoded
-		 * calibration data.
+		 * calibration data. Only RB_WLAN_ERD_ID_SOLO makes sense here.
 		 */
-		ret = routerboot_rle_decode(lbuf, tlen, outbuf, outlen);
-		if (ret)
-			pr_debug(RB_HC_PR_PFX "RLE decoding error (%d)\n", ret);
+		if (RB_WLAN_ERD_ID_SOLO == tag_id) {
+			ret = routerboot_rle_decode(lbuf, tlen, outbuf, outlen);
+			if (ret)
+				pr_debug(RB_HC_PR_PFX "RLE decoding error (%d)\n", ret);
+		}
 		break;
 	}
 
@@ -633,7 +656,7 @@  static ssize_t hc_wlan_data_bin_read(struct file *filp, struct kobject *kobj,
 	if (!outbuf)
 		return -ENOMEM;
 
-	ret = hc_wlan_data_unpack(hc_wattr->pld_ofs, hc_wattr->pld_len, outbuf, &outlen);
+	ret = hc_wlan_data_unpack(hc_wattr->erd_tag_id, hc_wattr->pld_ofs, hc_wattr->pld_len, outbuf, &outlen);
 	if (ret) {
 		kfree(outbuf);
 		return ret;
@@ -655,14 +678,17 @@  static ssize_t hc_wlan_data_bin_read(struct file *filp, struct kobject *kobj,
 
 int __init rb_hardconfig_init(struct kobject *rb_kobj)
 {
+	struct kobject *hc_wlan_kobj;
 	struct mtd_info *mtd;
-	size_t bytes_read, buflen;
+	size_t bytes_read, buflen, outlen;
 	const u8 *buf;
-	int i, ret;
+	void *outbuf;
+	int i, j, ret;
 	u32 magic;
 
 	hc_buf = NULL;
 	hc_kobj = NULL;
+	hc_wlan_kobj = NULL;
 
 	// TODO allow override
 	mtd = get_mtd_device_nm(RB_MTD_HARD_CONFIG);
@@ -713,15 +739,62 @@  int __init rb_hardconfig_init(struct kobject *rb_kobj)
 		/* Account for skipped magic */
 		hc_attrs[i].pld_ofs += sizeof(magic);
 
-		/* Special case RB_ID_WLAN_DATA to prep and create the binary attribute */
+		/*
+		 * Special case RB_ID_WLAN_DATA to prep and create the binary attribute.
+		 * We first check if the data is "old style" within a single tag (or no tag at all):
+		 * If it is we publish this single blob as a binary attribute child of hc_kobj to
+		 * preserve backward compatibility.
+		 * If it isn't and instead uses multiple ERD tags, we create a subfolder and
+		 * publish the known ones there.
+		 */
 		if ((RB_ID_WLAN_DATA == hc_attrs[i].tag_id) && hc_attrs[i].pld_len) {
-			hc_wlandata_battr.pld_ofs = hc_attrs[i].pld_ofs;
-			hc_wlandata_battr.pld_len = hc_attrs[i].pld_len;
-
-			ret = sysfs_create_bin_file(hc_kobj, &hc_wlandata_battr.battr);
-			if (ret)
-				pr_warn(RB_HC_PR_PFX "Could not create %s sysfs entry (%d)\n",
-				       hc_wlandata_battr.battr.attr.name, ret);
+			outlen = RB_ART_SIZE;
+			outbuf = kmalloc(outlen, GFP_KERNEL);
+			if (!outbuf) {
+				pr_warn(RB_HC_PR_PFX "Out of memory parsing WLAN tag\n");
+				continue;
+			}
+
+			/* Test ID_SOLO first, if found: done */
+			ret = hc_wlan_data_unpack(RB_WLAN_ERD_ID_SOLO, hc_attrs[i].pld_ofs, hc_attrs[i].pld_len, outbuf, &outlen);
+			if (!ret) {
+				hc_wd_solo_battr.pld_ofs = hc_attrs[i].pld_ofs;
+				hc_wd_solo_battr.pld_len = hc_attrs[i].pld_len;
+
+				ret = sysfs_create_bin_file(hc_kobj, &hc_wd_solo_battr.battr);
+				if (ret)
+					pr_warn(RB_HC_PR_PFX "Could not create %s sysfs entry (%d)\n",
+						hc_wd_solo_battr.battr.attr.name, ret);
+			}
+			/* Otherwise, create "wlan_data" subtree and publish known data */
+			else {
+				hc_wlan_kobj = kobject_create_and_add("wlan_data", hc_kobj);
+				if (!hc_wlan_kobj) {
+					kfree(outbuf);
+					pr_warn(RB_HC_PR_PFX "Could not create wlan_data sysfs folder\n");
+					continue;
+				}
+
+				for (j = 0; j < ARRAY_SIZE(hc_wd_multi_battrs); j++) {
+					outlen = RB_ART_SIZE;
+					ret = hc_wlan_data_unpack(hc_wd_multi_battrs[j].erd_tag_id,
+								  hc_attrs[i].pld_ofs, hc_attrs[i].pld_len, outbuf, &outlen);
+					if (ret) {
+						hc_wd_multi_battrs[j].pld_ofs = hc_wd_multi_battrs[j].pld_len = 0;
+						continue;
+					}
+
+					hc_wd_multi_battrs[j].pld_ofs = hc_attrs[i].pld_ofs;
+					hc_wd_multi_battrs[j].pld_len = hc_attrs[i].pld_len;
+
+					ret = sysfs_create_bin_file(hc_wlan_kobj, &hc_wd_multi_battrs[j].battr);
+					if (ret)
+						pr_warn(RB_HC_PR_PFX "Could not create wlan_data/%s sysfs entry (%d)\n",
+							hc_wd_multi_battrs[j].battr.attr.name, ret);
+				}
+			}
+
+			kfree(outbuf);
 		}
 		/* All other tags are published via standard attributes */
 		else {