Extend offloaded ACS QCA vendor command to support VHT

Update ACS driver offload feature for VHT configuration. In addition,
this allows the chanlist parameter to be used to specify which channels
are included as options for the offloaded ACS case.

Signed-off-by: Jouni Malinen <jouni@qca.qualcomm.com>
diff --git a/hostapd/config_file.c b/hostapd/config_file.c
index 53143f7..cae9fd3 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -775,6 +775,24 @@
 }
 
 
+static int hostapd_parse_chanlist(struct hostapd_config *conf, char *val)
+{
+	char *pos;
+
+	/* for backwards compatibility, translate ' ' in conf str to ',' */
+	pos = val;
+	while (pos) {
+		pos = os_strchr(pos, ' ');
+		if (pos)
+			*pos++ = ',';
+	}
+	if (freq_range_list_parse(&conf->acs_ch_list, val))
+		return -1;
+
+	return 0;
+}
+
+
 static int hostapd_parse_intlist(int **int_list, char *val)
 {
 	int *list;
@@ -2542,12 +2560,15 @@
 				   line);
 			return 1;
 #else /* CONFIG_ACS */
+			conf->acs = 1;
 			conf->channel = 0;
 #endif /* CONFIG_ACS */
-		} else
+		} else {
 			conf->channel = atoi(pos);
+			conf->acs = conf->channel == 0;
+		}
 	} else if (os_strcmp(buf, "chanlist") == 0) {
-		if (hostapd_parse_intlist(&conf->chanlist, pos)) {
+		if (hostapd_parse_chanlist(conf, pos)) {
 			wpa_printf(MSG_ERROR, "Line %d: invalid channel list",
 				   line);
 			return 1;
diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf
index 9e81e9e..90d1523 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -170,8 +170,11 @@
 
 # Channel list restriction. This option allows hostapd to select one of the
 # provided channels when a channel should be automatically selected.
-# Default: not set (allow any enabled channel to be selected)
+# Channel list can be provided as range using hyphen ('-') or individual
+# channels can be specified by space (' ') seperated values
+# Default: all channels allowed in selected hw_mode
 #chanlist=100 104 108 112 116
+#chanlist=1 6 11-13
 
 # Beacon interval in kus (1.024 ms) (default: 100; range 15..65535)
 beacon_int=100
diff --git a/src/ap/acs.c b/src/ap/acs.c
index ae7f6c3..652d020 100644
--- a/src/ap/acs.c
+++ b/src/ap/acs.c
@@ -479,16 +479,10 @@
 static int is_in_chanlist(struct hostapd_iface *iface,
 			  struct hostapd_channel_data *chan)
 {
-	int *entry;
-
-	if (!iface->conf->chanlist)
+	if (!iface->conf->acs_ch_list.num)
 		return 1;
 
-	for (entry = iface->conf->chanlist; *entry != -1; entry++) {
-		if (*entry == chan->chan)
-			return 1;
-	}
-	return 0;
+	return freq_range_list_includes(&iface->conf->acs_ch_list, chan->chan);
 }
 
 
diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c
index 76011dc..cccbfab 100644
--- a/src/ap/ap_config.c
+++ b/src/ap/ap_config.c
@@ -181,6 +181,8 @@
 	conf->corrupt_gtk_rekey_mic_probability = 0.0;
 #endif /* CONFIG_TESTING_OPTIONS */
 
+	conf->acs = 0;
+	conf->acs_ch_list.num = 0;
 #ifdef CONFIG_ACS
 	conf->acs_num_scans = 5;
 #endif /* CONFIG_ACS */
@@ -579,7 +581,7 @@
 	os_free(conf->bss);
 	os_free(conf->supported_rates);
 	os_free(conf->basic_rates);
-	os_free(conf->chanlist);
+	os_free(conf->acs_ch_list.range);
 	os_free(conf->driver_params);
 #ifdef CONFIG_ACS
 	os_free(conf->acs_chan_bias);
diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h
index 961d2dd..b9d6832 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -568,7 +568,8 @@
 	int fragm_threshold;
 	u8 send_probe_response;
 	u8 channel;
-	int *chanlist;
+	u8 acs;
+	struct wpa_freq_range_list acs_ch_list;
 	enum hostapd_hw_mode hw_mode; /* HOSTAPD_MODE_IEEE80211A, .. */
 	enum {
 		LONG_PREAMBLE = 0,
diff --git a/src/ap/ap_drv_ops.c b/src/ap/ap_drv_ops.c
index e16306c..aae544f 100644
--- a/src/ap/ap_drv_ops.c
+++ b/src/ap/ap_drv_ops.c
@@ -715,13 +715,66 @@
 int hostapd_drv_do_acs(struct hostapd_data *hapd)
 {
 	struct drv_acs_params params;
+	int ret, i, acs_ch_list_all = 0;
+	u8 *channels = NULL;
+	unsigned int num_channels = 0;
+	struct hostapd_hw_modes *mode;
 
 	if (hapd->driver == NULL || hapd->driver->do_acs == NULL)
 		return 0;
+
 	os_memset(&params, 0, sizeof(params));
 	params.hw_mode = hapd->iface->conf->hw_mode;
+
+	/*
+	 * If no chanlist config parameter is provided, include all enabled
+	 * channels of the selected hw_mode.
+	 */
+	if (!hapd->iface->conf->acs_ch_list.num)
+		acs_ch_list_all = 1;
+
+	mode = hapd->iface->current_mode;
+	if (mode == NULL)
+		return -1;
+	channels = os_malloc(mode->num_channels);
+	if (channels == NULL)
+		return -1;
+
+	for (i = 0; i < mode->num_channels; i++) {
+		struct hostapd_channel_data *chan = &mode->channels[i];
+		if (!acs_ch_list_all &&
+		    !freq_range_list_includes(&hapd->iface->conf->acs_ch_list,
+					      chan->chan))
+			continue;
+		if (!(chan->flag & HOSTAPD_CHAN_DISABLED))
+			channels[num_channels++] = chan->chan;
+	}
+
+	params.ch_list = channels;
+	params.ch_list_len = num_channels;
+
 	params.ht_enabled = !!(hapd->iface->conf->ieee80211n);
 	params.ht40_enabled = !!(hapd->iface->conf->ht_capab &
 				 HT_CAP_INFO_SUPP_CHANNEL_WIDTH_SET);
-	return hapd->driver->do_acs(hapd->drv_priv, &params);
+	params.vht_enabled = !!(hapd->iface->conf->ieee80211ac);
+	params.ch_width = 20;
+	if (hapd->iface->conf->ieee80211n && params.ht40_enabled)
+		params.ch_width = 40;
+
+	/* Note: VHT20 is defined by combination of ht_capab & vht_oper_chwidth
+	 */
+	if (hapd->iface->conf->ieee80211ac && params.ht40_enabled) {
+		if (hapd->iface->conf->vht_oper_chwidth == VHT_CHANWIDTH_80MHZ)
+			params.ch_width = 80;
+		else if (hapd->iface->conf->vht_oper_chwidth ==
+			 VHT_CHANWIDTH_160MHZ ||
+			 hapd->iface->conf->vht_oper_chwidth ==
+			 VHT_CHANWIDTH_80P80MHZ)
+			params.ch_width = 160;
+	}
+
+	ret = hapd->driver->do_acs(hapd->drv_priv, &params);
+	os_free(channels);
+
+	return ret;
 }
diff --git a/src/ap/dfs.c b/src/ap/dfs.c
index d458a42..715f19b 100644
--- a/src/ap/dfs.c
+++ b/src/ap/dfs.c
@@ -165,16 +165,10 @@
 static int is_in_chanlist(struct hostapd_iface *iface,
 			  struct hostapd_channel_data *chan)
 {
-	int *entry;
-
-	if (!iface->conf->chanlist)
+	if (!iface->conf->acs_ch_list.num)
 		return 1;
 
-	for (entry = iface->conf->chanlist; *entry != -1; entry++) {
-		if (*entry == chan->chan)
-			return 1;
-	}
-	return 0;
+	return freq_range_list_includes(&iface->conf->acs_ch_list, chan->chan);
 }
 
 
diff --git a/src/ap/drv_callbacks.c b/src/ap/drv_callbacks.c
index a0adc67..507053e 100644
--- a/src/ap/drv_callbacks.c
+++ b/src/ap/drv_callbacks.c
@@ -532,9 +532,8 @@
 
 #ifdef CONFIG_ACS
 static void hostapd_acs_channel_selected(struct hostapd_data *hapd,
-					 u8 pri_channel, u8 sec_channel)
+					 struct acs_selected_channels *acs_res)
 {
-	int channel;
 	int ret;
 
 	if (hapd->iconf->channel) {
@@ -543,29 +542,55 @@
 		return;
 	}
 
-	hapd->iface->freq = hostapd_hw_get_freq(hapd, pri_channel);
+	hapd->iface->freq = hostapd_hw_get_freq(hapd, acs_res->pri_channel);
 
-	channel = pri_channel;
-	if (!channel) {
+	if (!acs_res->pri_channel) {
 		hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
 			       HOSTAPD_LEVEL_WARNING,
 			       "driver switched to bad channel");
 		return;
 	}
 
-	hapd->iconf->channel = channel;
+	hapd->iconf->channel = acs_res->pri_channel;
+	hapd->iconf->acs = 1;
 
-	if (sec_channel == 0)
+	if (acs_res->sec_channel == 0)
 		hapd->iconf->secondary_channel = 0;
-	else if (sec_channel < pri_channel)
+	else if (acs_res->sec_channel < acs_res->pri_channel)
 		hapd->iconf->secondary_channel = -1;
-	else if (sec_channel > pri_channel)
+	else if (acs_res->sec_channel > acs_res->pri_channel)
 		hapd->iconf->secondary_channel = 1;
 	else {
 		wpa_printf(MSG_ERROR, "Invalid secondary channel!");
 		return;
 	}
 
+	if (hapd->iface->conf->ieee80211ac) {
+		/* set defaults for backwards compatibility */
+		hapd->iconf->vht_oper_centr_freq_seg1_idx = 0;
+		hapd->iconf->vht_oper_centr_freq_seg0_idx = 0;
+		hapd->iconf->vht_oper_chwidth = VHT_CHANWIDTH_USE_HT;
+		if (acs_res->ch_width == 80) {
+			hapd->iconf->vht_oper_centr_freq_seg0_idx =
+				acs_res->vht_seg0_center_ch;
+			hapd->iconf->vht_oper_chwidth = VHT_CHANWIDTH_80MHZ;
+		} else if (acs_res->ch_width == 160) {
+			if (acs_res->vht_seg1_center_ch == 0) {
+				hapd->iconf->vht_oper_centr_freq_seg0_idx =
+					acs_res->vht_seg0_center_ch;
+				hapd->iconf->vht_oper_chwidth =
+					VHT_CHANWIDTH_160MHZ;
+			} else {
+				hapd->iconf->vht_oper_centr_freq_seg0_idx =
+					acs_res->vht_seg0_center_ch;
+				hapd->iconf->vht_oper_centr_freq_seg1_idx =
+					acs_res->vht_seg1_center_ch;
+				hapd->iconf->vht_oper_chwidth =
+					VHT_CHANWIDTH_80P80MHZ;
+			}
+		}
+	}
+
 	ret = hostapd_acs_completed(hapd->iface, 0);
 	if (ret) {
 		wpa_printf(MSG_ERROR,
@@ -1248,9 +1273,8 @@
 		break;
 #ifdef CONFIG_ACS
 	case EVENT_ACS_CHANNEL_SELECTED:
-		hostapd_acs_channel_selected(
-			hapd, data->acs_selected_channels.pri_channel,
-			data->acs_selected_channels.sec_channel);
+		hostapd_acs_channel_selected(hapd,
+					     &data->acs_selected_channels);
 		break;
 #endif /* CONFIG_ACS */
 	default:
diff --git a/src/ap/hostapd.c b/src/ap/hostapd.c
index 3e4e16b..6cdb6d3 100644
--- a/src/ap/hostapd.c
+++ b/src/ap/hostapd.c
@@ -179,6 +179,7 @@
 		hapd = iface->bss[j];
 		hapd->iconf = newconf;
 		hapd->iconf->channel = oldconf->channel;
+		hapd->iconf->acs = oldconf->acs;
 		hapd->iconf->secondary_channel = oldconf->secondary_channel;
 		hapd->iconf->ieee80211n = oldconf->ieee80211n;
 		hapd->iconf->ieee80211ac = oldconf->ieee80211ac;
diff --git a/src/ap/hw_features.c b/src/ap/hw_features.c
index 05431d3..96744c4 100644
--- a/src/ap/hw_features.c
+++ b/src/ap/hw_features.c
@@ -510,7 +510,11 @@
 		return 0;
 	}
 
-	if ((conf & HT_CAP_INFO_SUPP_CHANNEL_WIDTH_SET) &&
+	/*
+	 * Driver ACS chosen channel may not be HT40 due to internal driver
+	 * restrictions.
+	 */
+	if (!iface->conf->acs && (conf & HT_CAP_INFO_SUPP_CHANNEL_WIDTH_SET) &&
 	    !(hw & HT_CAP_INFO_SUPP_CHANNEL_WIDTH_SET)) {
 		wpa_printf(MSG_ERROR, "Driver does not support configured "
 			   "HT capability [HT40*]");
diff --git a/src/common/hw_features_common.c b/src/common/hw_features_common.c
index 309215e..8d83de6 100644
--- a/src/common/hw_features_common.c
+++ b/src/common/hw_features_common.c
@@ -363,8 +363,6 @@
 			    int vht_oper_chwidth, int center_segment0,
 			    int center_segment1, u32 vht_caps)
 {
-	int tmp;
-
 	os_memset(data, 0, sizeof(*data));
 	data->mode = mode;
 	data->freq = freq;
@@ -404,13 +402,34 @@
 			return -1;
 		if (!sec_channel_offset)
 			return -1;
-		/* primary 40 part must match the HT configuration */
-		tmp = (30 + freq - 5000 - center_segment0 * 5) / 20;
-		tmp /= 2;
-		if (data->center_freq1 != 5000 +
-		    center_segment0 * 5 - 20 + 40 * tmp)
-			return -1;
-		data->center_freq1 = 5000 + center_segment0 * 5;
+		if (!center_segment0) {
+			if (channel <= 48)
+				center_segment0 = 42;
+			else if (channel <= 64)
+				center_segment0 = 58;
+			else if (channel <= 112)
+				center_segment0 = 106;
+			else if (channel <= 128)
+				center_segment0 = 122;
+			else if (channel <= 144)
+				center_segment0 = 138;
+			else if (channel <= 161)
+				center_segment0 = 155;
+			data->center_freq1 = 5000 + center_segment0 * 5;
+		} else {
+			/*
+			 * Note: HT/VHT config and params are coupled. Check if
+			 * HT40 channel band is in VHT80 Pri channel band
+			 * configuration.
+			 */
+			if (center_segment0 == channel + 6 ||
+			    center_segment0 == channel + 2 ||
+			    center_segment0 == channel - 2 ||
+			    center_segment0 == channel - 6)
+				data->center_freq1 = 5000 + center_segment0 * 5;
+			else
+				return -1;
+		}
 		break;
 	case VHT_CHANWIDTH_160MHZ:
 		data->bandwidth = 160;
@@ -424,13 +443,21 @@
 			return -1;
 		if (!sec_channel_offset)
 			return -1;
-		/* primary 40 part must match the HT configuration */
-		tmp = (70 + freq - 5000 - center_segment0 * 5) / 20;
-		tmp /= 2;
-		if (data->center_freq1 != 5000 +
-		    center_segment0 * 5 - 60 + 40 * tmp)
+		/*
+		 * Note: HT/VHT config and params are coupled. Check if
+		 * HT40 channel band is in VHT160 channel band configuration.
+		 */
+		if (center_segment0 == channel + 14 ||
+		    center_segment0 == channel + 10 ||
+		    center_segment0 == channel + 6 ||
+		    center_segment0 == channel + 2 ||
+		    center_segment0 == channel - 2 ||
+		    center_segment0 == channel - 6 ||
+		    center_segment0 == channel - 10 ||
+		    center_segment0 == channel - 14)
+			data->center_freq1 = 5000 + center_segment0 * 5;
+		else
 			return -1;
-		data->center_freq1 = 5000 + center_segment0 * 5;
 		break;
 	}
 
diff --git a/src/common/qca-vendor.h b/src/common/qca-vendor.h
index 2117ee7..5ff6817 100644
--- a/src/common/qca-vendor.h
+++ b/src/common/qca-vendor.h
@@ -195,6 +195,11 @@
 	QCA_WLAN_VENDOR_ATTR_ACS_HW_MODE,
 	QCA_WLAN_VENDOR_ATTR_ACS_HT_ENABLED,
 	QCA_WLAN_VENDOR_ATTR_ACS_HT40_ENABLED,
+	QCA_WLAN_VENDOR_ATTR_ACS_VHT_ENABLED,
+	QCA_WLAN_VENDOR_ATTR_ACS_CHWIDTH,
+	QCA_WLAN_VENDOR_ATTR_ACS_CH_LIST,
+	QCA_WLAN_VENDOR_ATTR_ACS_VHT_SEG0_CENTER_CHANNEL,
+	QCA_WLAN_VENDOR_ATTR_ACS_VHT_SEG1_CENTER_CHANNEL,
 	/* keep last */
 	QCA_WLAN_VENDOR_ATTR_ACS_AFTER_LAST,
 	QCA_WLAN_VENDOR_ATTR_ACS_MAX =
diff --git a/src/drivers/driver.h b/src/drivers/driver.h
index 5566201..6a9cd74 100644
--- a/src/drivers/driver.h
+++ b/src/drivers/driver.h
@@ -1588,6 +1588,16 @@
 
 	/* Indicates whether HT40 is enabled */
 	int ht40_enabled;
+
+	/* Indicates whether VHT is enabled */
+	int vht_enabled;
+
+	/* Configured ACS channel width */
+	u16 ch_width;
+
+	/* ACS channel list info */
+	unsigned int ch_list_len;
+	const u8 *ch_list;
 };
 
 
@@ -4546,10 +4556,18 @@
 	 * struct acs_selected_channels - Data for EVENT_ACS_CHANNEL_SELECTED
 	 * @pri_channel: Selected primary channel
 	 * @sec_channel: Selected secondary channel
+	 * @vht_seg0_center_ch: VHT mode Segment0 center channel
+	 * @vht_seg1_center_ch: VHT mode Segment1 center channel
+	 * @ch_width: Selected Channel width by driver. Driver may choose to
+	 *	change hostapd configured ACS channel width due driver internal
+	 *	channel restrictions.
 	 */
 	struct acs_selected_channels {
 		u8 pri_channel;
 		u8 sec_channel;
+		u8 vht_seg0_center_ch;
+		u8 vht_seg1_center_ch;
+		u16 ch_width;
 	} acs_selected_channels;
 };
 
diff --git a/src/drivers/driver_nl80211.c b/src/drivers/driver_nl80211.c
index 023823f..7b3dc51 100644
--- a/src/drivers/driver_nl80211.c
+++ b/src/drivers/driver_nl80211.c
@@ -8382,12 +8382,24 @@
 	    (params->ht_enabled &&
 	     nla_put_flag(msg, QCA_WLAN_VENDOR_ATTR_ACS_HT_ENABLED)) ||
 	    (params->ht40_enabled &&
-	     nla_put_flag(msg, QCA_WLAN_VENDOR_ATTR_ACS_HT40_ENABLED))) {
+	     nla_put_flag(msg, QCA_WLAN_VENDOR_ATTR_ACS_HT40_ENABLED)) ||
+	    (params->vht_enabled &&
+	     nla_put_flag(msg, QCA_WLAN_VENDOR_ATTR_ACS_VHT_ENABLED)) ||
+	    nla_put_u16(msg, QCA_WLAN_VENDOR_ATTR_ACS_CHWIDTH,
+			params->ch_width) ||
+	    (params->ch_list_len &&
+	     nla_put(msg, QCA_WLAN_VENDOR_ATTR_ACS_CH_LIST, params->ch_list_len,
+		     params->ch_list))) {
 		nlmsg_free(msg);
 		return -ENOBUFS;
 	}
 	nla_nest_end(msg, data);
 
+	wpa_printf(MSG_DEBUG,
+		   "nl80211: ACS Params: HW_MODE: %d HT: %d HT40: %d VHT: %d BW: %d CH_LIST_LEN: %u",
+		   params->hw_mode, params->ht_enabled, params->ht40_enabled,
+		   params->vht_enabled, params->ch_width, params->ch_list_len);
+
 	ret = send_and_recv_msgs(drv, msg, NULL, NULL);
 	if (ret) {
 		wpa_printf(MSG_DEBUG,
diff --git a/src/drivers/driver_nl80211_event.c b/src/drivers/driver_nl80211_event.c
index 87e412d..a5b3230 100644
--- a/src/drivers/driver_nl80211_event.c
+++ b/src/drivers/driver_nl80211_event.c
@@ -1500,6 +1500,25 @@
 		nla_get_u8(tb[QCA_WLAN_VENDOR_ATTR_ACS_PRIMARY_CHANNEL]);
 	event.acs_selected_channels.sec_channel =
 		nla_get_u8(tb[QCA_WLAN_VENDOR_ATTR_ACS_SECONDARY_CHANNEL]);
+	if (tb[QCA_WLAN_VENDOR_ATTR_ACS_VHT_SEG0_CENTER_CHANNEL])
+		event.acs_selected_channels.vht_seg0_center_ch =
+			nla_get_u8(tb[QCA_WLAN_VENDOR_ATTR_ACS_VHT_SEG0_CENTER_CHANNEL]);
+	if (tb[QCA_WLAN_VENDOR_ATTR_ACS_VHT_SEG0_CENTER_CHANNEL])
+		event.acs_selected_channels.vht_seg1_center_ch =
+			nla_get_u8(tb[QCA_WLAN_VENDOR_ATTR_ACS_VHT_SEG1_CENTER_CHANNEL]);
+	if (tb[QCA_WLAN_VENDOR_ATTR_ACS_CHWIDTH])
+		event.acs_selected_channels.ch_width =
+			nla_get_u16(tb[QCA_WLAN_VENDOR_ATTR_ACS_CHWIDTH]);
+
+	wpa_printf(MSG_INFO,
+		   "nl80211: ACS Results: PCH: %d SCH: %d BW: %d VHT0: %d VHT1: %d",
+		   event.acs_selected_channels.pri_channel,
+		   event.acs_selected_channels.sec_channel,
+		   event.acs_selected_channels.ch_width,
+		   event.acs_selected_channels.vht_seg0_center_ch,
+		   event.acs_selected_channels.vht_seg1_center_ch);
+
+	/* Ignore ACS channel list check for backwards compatibility */
 
 	wpa_supplicant_event(drv->ctx, EVENT_ACS_CHANNEL_SELECTED, &event);
 }