Skip to content
Snippets Groups Projects
acs.c 47.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • 			}
    		} else if (bw) {
    			cntlr_get_current_acs_params(r->radio_el, &cur_acs_params);
    			acs_params.bw = bw;
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    
    
    			if (cur_acs_params.bw == bw) {
    				acs_params.best_channel = cur_acs_params.best_channel;
    				acs_params.best_bw = cur_acs_params.best_bw;
    			}
    		} else {
    			if (cntlr_get_current_acs_params(r->radio_el, &cur_acs_params)) {
    				cntlr_dbg(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " - current channel not known\n",
    					  MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr));
    				continue;
    			}
    
    			acs_params.opclass = cur_acs_params.opclass;
    			acs_params.best_channel = cur_acs_params.best_channel;
    			acs_params.best_bw = cur_acs_params.best_bw;
    		}
    
    		ret = cntlr_acs_radio_channel_recalc(node, r, &acs_params);
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    		if (WARN_ON(ret))
    			continue;
    
    
    		cntlr_trace(LOG_CHANNEL, "acs node " MACFMT " radio " MACFMT " new %d/%d opclass %d vs old %d/%d opclass %d cac_time %u\n",
    
    		    MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr), acs_params.best_channel,
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    		    acs_params.best_bw, acs_params.best_opclass, cur_acs_params.best_channel,
    
    		    cur_acs_params.bw, cur_acs_params.opclass, acs_params.best_cac_time);
    
    		/* Pick up control channel */
    		if (acs_params.best_bw > 40) {
    			ctrl_channel = cntrl_acs_radio_ctrl_channel(r->radio_el,
    							acs_params.best_opclass,
    							acs_params.best_channel);
    		} else {
    			ctrl_channel = acs_params.best_channel;
    		}
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    
    
    		/* Build opclass we would like to send */
    		memcpy(&req_opclass, &r->radio_el->supp_opclass, sizeof(req_opclass));
    		wifi_opclass_set_preferences(&req_opclass, 0x0);
    
    		if (wifi_radio_opclass_update_channel(&req_opclass,
    						      wifi_band_to_freqband(r->radio_el->band),
    						      ctrl_channel, acs_params.best_bw, 15)) {
    
    			cntlr_dbg(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " - update opclass failed\n",
    				  MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr));
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    			continue;
    
    		}
    
    		wifi_opclass_dump_ex(&req_opclass, "acs_selection", r->radio_el->macaddr, false);
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    
    
    		ret = cntlr_send_channel_selection(node->cntlr, node->almacaddr,
    
    						   r->radio_el->macaddr, &req_opclass);
    		if (ret == 0xffff) {
    			cntlr_warn(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " send channel switch request failed\n",
    				   MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr));
    			continue;
    		}
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    
    
    		cntlr_warn(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " switch to best channel %d/%d ctrl %d mid %u\n",
    			   MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr), acs_params.best_channel,
    			   acs_params.best_bw, ctrl_channel, ret);
    
    		last_acs->mid = ret;
    		last_acs->status = ACS_RECALC_STATUS_BEST_REQUESTED;
    		timestamp_update(&last_acs->entry_time);
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    	}
    }
    
    static void cntlr_radio_acs_timer(atimer_t *t)
    {
    	struct acs *acs = container_of(t, struct acs, acs_timer);
    	struct netif_radio *r = acs->radio;
    	struct controller *c = acs->cntlr;
    	struct node *n = acs->node;
    	struct acs_params *last_acs;
    
    	last_acs = cntlr_radio_get_last_acs(r);
    	if (!last_acs)
    		return;
    
    	if (cntlr_acs_radio_is_bsta_connected(c, r)) {
    		cntlr_dbg(LOG_CHANNEL, "acs timer recalc (bsta connected) " MACFMT " " MACFMT "\n",
    			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    		last_acs->status = ACS_RECALC_STATUS_BSTA_CONNECTED;
    		timestamp_update(&last_acs->entry_time);
    		return;
    	}
    
    	if (last_acs->recalc) {
    		cntlr_dbg(LOG_CHANNEL, "acs timer recalc (recalc already set) " MACFMT " " MACFMT "\n",
    			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    		return;
    	}
    
    	if (c->cfg.acs_scan_before_recalc == false) {
    		cntlr_dbg(LOG_CHANNEL, "acs timer recalc node (don't scan) " MACFMT " " MACFMT "\n",
    			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    
    		last_acs->scan_mid = 0xffff;
    		last_acs->recalc = true;
    
    		/* Send node preference request */
    		cntlr_acs_send_pref_request(n);
    	} else {
    
    		struct acs_radio_metrics *m;
    		bool fast = false;
    
    
    		cntlr_dbg(LOG_CHANNEL, "acs timer recalc node (scan) " MACFMT " " MACFMT "\n",
    			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    
    
    		m = cntlr_radio_get_metrics(r);
    		if (m && (m->tx || m->rx))
    			fast = true;
    
    
    		/* Send scan request */
    		last_acs->recalc = true;
    
    		last_acs->scan_mid = cntlr_acs_send_scan_request(n, r, fast);
    
    		if (last_acs->scan_mid != 0xffff) {
    			last_acs->status = ACS_RECALC_STATUS_SCAN_REQUESTED;
    			timestamp_update(&last_acs->entry_time);
    		}
    	}
    }
    
    void cntlr_acs_recalc(struct controller *c)
    
    {
    	struct node *n = NULL;
    
    	uint32_t delay = 10;
    
    	/* Handle main timer - prevent scan/switch at same time */
    
    	list_for_each_entry(n, &c->nodelist, list) {
    
    		struct netif_radio *r = NULL;
    
    		/* Run fresh scan before preference recalc */
    		list_for_each_entry(r, &n->radiolist, list) {
    			cntlr_dbg(LOG_CHANNEL, "acs setup recalc timer %u seconds " MACFMT " " MACFMT "\n",
    				  delay, MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    
    			/* In the future check more params - possible recalc postpond */
    			cntlr_radio_acs_timer_set(r, delay * 1000);
    			delay += 60;
    		}
    
    
    /* DFS background CAC cleanup */
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    static uint8_t cntlr_dfs_cac_method(uint32_t methods)
    {
    	uint8_t cac_method = 0;
    
    	/* allow only bgcac methods */
    	if (methods & (1 << WIFI_CAC_MIMO_REDUCED))
    		cac_method = CAC_METHOD_MIMO_DIM_REDUCED;
    	else if (methods & (1 << WIFI_CAC_DEDICATED))
    		cac_method = CAC_METHOD_DEDICATED_RADIO;
    	else if (methods & (1 << WIFI_CAC_TIME_SLICED))
    		cac_method = CAC_METHOD_TIME_SLICED;
    
    	return cac_method;
    }
    
    
    static bool cntlr_dfs_cac_ongoing(struct wifi_radio_element *radio)
    {
    	struct wifi_radio_opclass_entry *entry;
    	struct wifi_radio_opclass *opclass;
    	int i, j;
    
    	opclass = &radio->pref_opclass;
    
    	for (i = 0; i < opclass->num_opclass; i++) {
    		entry = &opclass->opclass[i];
    
    		for (j = 0; j < entry->num_channel; j++) {
    			if (entry->channel[j].dfs == WIFI_RADIO_OPCLASS_CHANNEL_DFS_CAC)
    				return true;
    		}
    	}
    
    	return false;
    }
    
    static bool cntlr_dfs_get_usable(struct wifi_radio_opclass_entry *entry, struct acs_cac_data *cac)
    
    	for (i = 0; i < entry->num_channel; i++) {
    
    		reas = entry->channel[i].preference & CHANNEL_PREF_REASON;
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    		if (reas != CHANNEL_PREF_REASON_DFS_USABLE)
    			continue;
    
    
    		/* Check if background CAC supported */
    		cac->data.cac_method = cntlr_dfs_cac_method(entry->channel[i].cac_methods);
    		if (!cac->data.cac_method)
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    			continue;
    
    
    		cac->data.channel = entry->channel[i].channel;
    		cac->data.opclass = entry->id;
    		cac->cac_time = entry->channel[i].cac_time;
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    		return true;
    
    static bool cntlr_radio_is_ap_iface(struct netif_radio *radio)
    
    Saurabh Verma's avatar
    Saurabh Verma committed
    	struct netif_iface *iface = NULL;
    
    
    	list_for_each_entry(iface, &radio->iflist, list) {
    		/* Check if AP iface connected */
    
    		if (iface->bss->is_fbss)
    
    		if (iface->bss->is_bbss)
    
    static bool cntlr_dfs_get_cac_data(struct wifi_radio_element *radio, struct acs_cac_data *cac, uint16_t bw)
    
    	struct wifi_radio_opclass_entry *entry;
    	struct wifi_radio_opclass *opclass;
    
    	uint32_t cur_bw = 0;
    	uint32_t cur_chan = 0;
    
    	if (!bw) {
    		cur_bw = ctrl_radio_cur_opclass_max_bw(radio);
    		cur_chan = ctrl_radio_cur_opclass_chan(radio);
    	} else {
    		cur_bw = bw;
    		cur_chan = 0;
    	}
    
    	if (!cur_bw)
    		return false;
    
    	opclass = &radio->pref_opclass;
    
    	for (i = 0; i < opclass->num_opclass; i++) {
    		entry = &opclass->opclass[i];
    
    
    		if (entry->bandwidth != cur_bw)
    
    		if (!cntlr_dfs_get_usable(entry, cac))
    
    		if (cac->data.channel == cur_chan)
    
    		memcpy(cac->data.radio, radio->macaddr, sizeof(cac->data.radio));
    
    
    		return true;
    	}
    
    	return false;
    }
    
    void cntlr_dfs_radio_cleanup(struct node *node, struct netif_radio *radio)
    {
    
    	struct acs_cac_data cac_data = {};
    	struct acs_cac_data *last_cac_data;
    
    	last_cac_data = cntlr_radio_get_last_cac(radio);
    	if (!last_cac_data)
    		return;
    
    	if (!radio->radio_el->pref_opclass.num_opclass ||
    	    !radio->radio_el->cur_opclass.num_opclass) {
    		/* We don't know preferences and channels DFS state */
    		cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC not yet " MACFMT "\n", MAC2STR(radio->radio_el->macaddr));
    		timestamp_update(&last_cac_data->entry_time);
    		last_cac_data->status = ACS_CLEANUP_STATUS_SKIPPED;
    		return;
    	}
    
    	if (!radio->radio_el->bgcac_supported) {
    		/* Some nodes don't even support it */
    		cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC not supported " MACFMT "\n", MAC2STR(radio->radio_el->macaddr));
    		timestamp_update(&last_cac_data->entry_time);
    		last_cac_data->status = ACS_CLEANUP_STATUS_NOT_SUPPORTED;
    		return;
    	}
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    	cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC cleanup " MACFMT "\n", MAC2STR(radio->radio_el->macaddr));
    
    	if (!cntlr_radio_is_ap_iface(radio)) {
    
    		/* Wait controller get proper AP list */
    
    Janusz Dziedzic's avatar
    Janusz Dziedzic committed
    		cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC no AP ifaces, skip radio\n");
    
    		timestamp_update(&last_cac_data->entry_time);
    		last_cac_data->status = ACS_CLEANUP_STATUS_NO_APS;
    
    	/* So far keep it simple - don't start new CAC request if one ongoing */
    	if (cntlr_dfs_cac_ongoing(radio->radio_el)) {
    		/* Could be device run own background CAC */
    		cntlr_dbg(LOG_CHANNEL, "dfs radio CAC ongoing " MACFMT "\n", MAC2STR(radio->radio_el->macaddr));
    		timestamp_update(&last_cac_data->entry_time);
    		last_cac_data->status = ACS_CLEANUP_STATUS_ALREADY_ONGOING;
    
    	if (!cntlr_dfs_get_cac_data(radio->radio_el, &cac_data, 0)) {
    		/* First use current bandwidth for clearing. Finally switch to 20MHz for left channels */
    		cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC cleanup no channels left check 20MHz\n");
    		if (!cntlr_dfs_get_cac_data(radio->radio_el, &cac_data, 20)) {
    			cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC cleanup no channels left\n");
    			timestamp_update(&last_cac_data->entry_time);
    			last_cac_data->status = ACS_CLEANUP_STATUS_ALL_CLEAN;
    			return;
    		}
    	}
    
    	cntlr_warn(LOG_CHANNEL, "dfs node " MACFMT " radio " MACFMT " preCAC run chan %d opclass %d\n",
    		   MAC2STR(node->almacaddr), MAC2STR(radio->radio_el->macaddr),
    		   cac_data.data.channel, cac_data.data.opclass);
    	memcpy(last_cac_data, &cac_data, sizeof(*last_cac_data));
    	timestamp_update(&last_cac_data->entry_time);
    	last_cac_data->status = ACS_CLEANUP_STATUS_SELECTED;
    
    	cac_data.mid = cntlr_send_cac_req(node->cntlr, node->almacaddr, 1, &cac_data.data);
    
    	last_cac_data->mid = cac_data.mid;
    	if (cac_data.mid != 0xffff)
    		last_cac_data->status = ACS_CLEANUP_STATUS_REQUESTED;
    
    }
    
    void cntlr_dfs_node_cleanup(struct node *node)
    {
    
    Saurabh Verma's avatar
    Saurabh Verma committed
    	struct netif_radio *radio = NULL;
    
    	cntlr_trace(LOG_CHANNEL, "dfs node preCAC cleanup " MACFMT "\n", MAC2STR(node->almacaddr));
    
    
    	list_for_each_entry(radio, &node->radiolist, list) {
    
    		if (radio->radio_el->band != BAND_5)
    			continue;
    
    		cntlr_dfs_radio_cleanup(node, radio);
    	}
    }
    
    
    void cntlr_dfs_cleanup(struct controller *c)
    {
    	struct node *n = NULL;
    
    	list_for_each_entry(n, &c->nodelist, list) {
    		cntlr_dfs_node_cleanup(n);
    	}
    }
    
    
    /* CMDU callbacks we expect to receive from nodes */
    void cntlr_acs_channel_sel_response(struct netif_radio *r, uint16_t mid, uint8_t status)
    {
    	struct acs_params *acs;
    
    	cntlr_dbg(LOG_CHANNEL, "new channel selection response radio " MACFMT " mid %u status %u\n",
    		  MAC2STR(r->radio_el->macaddr), mid, status);
    
    	acs = cntlr_radio_get_last_acs(r);
    	if (!acs)
    		return;
    
    	if (acs->status != ACS_RECALC_STATUS_BEST_REQUESTED)
    		return;
    
    	if (acs->mid != mid)
    		return;
    
    	if (status == 0)
    		acs->status = ACS_RECALC_STATUS_BEST_ACCEPTED;
    	else
    		acs->status = ACS_RECALC_STATUS_BEST_REJECTED;
    
    	timestamp_update(&acs->entry_time);
    }
    
    void cntlr_acs_oper_channel_report(struct netif_radio *r)
    {
    	struct acs_params *acs;
    	struct wifi_radio_opclass_entry *entry;
    	struct wifi_radio_opclass *cur;
    	int i, j;
    
    	cntlr_dbg(LOG_CHANNEL, "new oper channel report radio " MACFMT " %u/%u\n",
    		  MAC2STR(r->radio_el->macaddr),
    		  ctrl_radio_cur_opclass_ctrl_chan(r->radio_el),
    		  ctrl_radio_cur_opclass_max_bw(r->radio_el));
    
    	acs = cntlr_radio_get_last_acs(r);
    	if (!acs)
    		return;
    
    	if (acs->status != ACS_RECALC_STATUS_BEST_REQUESTED &&
    	    acs->status != ACS_RECALC_STATUS_BEST_ACCEPTED)
    		return;
    
    	cur = &r->radio_el->cur_opclass;
    	for (i = 0; i < cur->num_opclass; i++) {
    		entry = &cur->opclass[i];
    
    		if (entry->bandwidth != acs->best_bw)
    			continue;
    
    		for (j = 0; j < entry->num_channel; j++) {
    			if (entry->channel[j].channel == acs->best_channel) {
    				acs->status = ACS_RECALC_STATUS_BEST_SET;
    				timestamp_update(&acs->entry_time);
    				return;
    			}
    		}
    	}
    
    	/* timeout check - worst case when continous CAC required */
    	if (timestamp_elapsed_sec(&acs->entry_time) > 30 + acs->best_cac_time) {
    		acs->status = ACS_RECALC_STATUS_BEST_SET_TIMEOUT;
    		timestamp_update(&acs->entry_time);
    	}
    }
    
    void cntlr_acs_cac_completion(struct netif_radio *r, uint8_t classid,
    			      uint8_t channel, uint8_t status)
    {
    	struct acs_params *acs;
    	struct acs_cac_data *cac;
    
    	cntlr_dbg(LOG_CHANNEL, "new cac completion radio " MACFMT " opclass %u channel %u status %u\n",
    		  MAC2STR(r->radio_el->macaddr), classid, channel, status);
    
    	cac = cntlr_radio_get_last_cac(r);
    	if (!cac)
    		return;
    
    	acs = cntlr_radio_get_last_acs(r);
    
    	if (cac->status != ACS_CLEANUP_STATUS_REQUESTED)
    		return;
    
    	if (cac->data.channel != channel)
    		return;
    	if (cac->data.opclass != classid)
    		return;
    
    
    	switch (status) {
    	case CAC_COMP_REPORT_STATUS_SUCCESSFUL:
    		cac->status = ACS_CLEANUP_STATUS_DONE;
    
    		/* Kick ACS recalc - we have new available channel */
    		if (acs && acs->scan_mid == 0xffff)
    			cntlr_radio_acs_timer_set(r, 5 * 1000);
    		break;
    	case CAC_COMP_REPORT_STATUS_CAC_NOT_SUPPORTED:
    	case CAC_COMP_REPORT_STATUS_TOO_BUSY:
    	case CAC_COMP_REPORT_STATUS_NON_CONFORMANT:
    	case CAC_COMP_REPORT_STATUS_OTHER:
    		cac->status = ACS_CLEANUP_STATUS_REJECTED;
    		break;
    	case CAC_COMP_REPORT_STATUS_RADAR_DETECTED:
    		cac->status = ACS_CLEANUP_STATUS_RADAR;
    		break;
    	default:
    		break;
    	}
    }
    
    void cntlr_acs_channel_pref_report(struct node *n, struct netif_radio *r)
    {
    	struct acs_params *acs;
    	struct acs_cac_data *cac;
    
    	cntlr_dbg(LOG_CHANNEL, "new pref report node " MACFMT " radio " MACFMT "\n",
    		  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    
    	cac = cntlr_radio_get_last_cac(r);
    	if (!cac)
    		return;
    
    	acs = cntlr_radio_get_last_acs(r);
    	if (!acs)
    		return;
    
    	/* ACS status update */
    	if (acs->status == ACS_RECALC_STATUS_BEST_REQUESTED ||
    	    acs->status == ACS_RECALC_STATUS_BEST_ACCEPTED) {
    		struct wifi_radio_opclass_channel *chan;
    
    		chan = wifi_opclass_get_channel(&r->radio_el->pref_opclass,
    						acs->best_opclass,
    						acs->best_channel);
    		if (chan) {
    			switch (chan->dfs) {
    			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_CAC:
    				acs->status = ACS_RECALC_STATUS_BEST_CAC;
    				timestamp_update(&acs->entry_time);
    				break;
    			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_NOP:
    				acs->status = ACS_RECALC_STATUS_BEST_CAC_RADAR;
    				timestamp_update(&acs->entry_time);
    				break;
    			default:
    				break;
    			}
    		}
    	}
    
    	/* Background CAC status update */
    	if (cac->status == ACS_CLEANUP_STATUS_REQUESTED ||
    	    cac->status == ACS_CLEANUP_STATUS_ACCEPTED ||
    	    cac->status == ACS_CLEANUP_STATUS_TIMEOUT) {
    		struct wifi_radio_opclass_channel *chan;
    
    		chan = wifi_opclass_get_channel(&r->radio_el->pref_opclass,
    						cac->data.opclass,
    						cac->data.channel);
    		if (chan) {
    			switch (chan->dfs) {
    			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_AVAILABLE:
    				cac->status = ACS_CLEANUP_STATUS_DONE;
    				break;
    			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_CAC:
    				cac->status = ACS_CLEANUP_STATUS_ACCEPTED;
    				break;
    			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_NOP:
    				cac->status = ACS_CLEANUP_STATUS_RADAR;
    				break;
    			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_USABLE:
    				/* change CAC -> USABLE - abort possible */
    				if (cac->status == ACS_CLEANUP_STATUS_ACCEPTED)
    					cac->status = ACS_CLEANUP_STATUS_REJECTED;
    				if (timestamp_elapsed_sec(&cac->entry_time) > 30 + cac->cac_time)
    					cac->status = ACS_CLEANUP_STATUS_TIMEOUT;
    				break;
    			default:
    				break;
    			}
    		}
    	}
    
    	/* Kick CAC background clearing */
    	if (n->cntlr->cfg.dfs_cleanup &&
    	    r->radio_el->band == BAND_5 &&
    	    r->radio_el->bgcac_supported &&
    	    cac->status != ACS_CLEANUP_STATUS_REQUESTED &&
    	    cac->status != ACS_CLEANUP_STATUS_ACCEPTED) {
    		cntlr_dbg(LOG_CHANNEL, "kick dfs cleanup " MACFMT " radio " MACFMT "\n",
    			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    
    		cntlr_dfs_radio_cleanup(n, r);
    	}
    
    	/* Kick ACS recalc code */
    	if (acs->recalc && n->cntlr->cfg.acs && acs->scan_mid == 0xffff) {
    		cntlr_dbg(LOG_CHANNEL, "kick acs reclac " MACFMT " radio " MACFMT "\n",
    			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    
    		acs->recalc = false;
    		cntlr_acs_node_channel_recalc(n, r->radio_el->band, 0, 0,
    				n->cntlr->cfg.acs_skip_dfs,
    				n->cntlr->cfg.acs_prevent_cac,
    				n->cntlr->cfg.acs_highest_bandwidth);
    	}
    }
    
    static uint8_t ewma(uint8_t cur, uint8_t prev, uint8_t alpha)
    {
    	uint8_t new;
    
    	if (alpha > 100)
    		alpha = 100;
    
    	new = (alpha * cur)/100 + ((100 - alpha) * prev) / 100;
    
    	return new;
    }
    
    
    static bool cntlr_acs_radio_add_and_check_metrics(struct netif_radio *r, uint8_t anpi, uint8_t obss,
    						  uint8_t tx, uint8_t rx)
    
    {
    	struct acs_radio_metrics *m;
    	struct acs_radio_metrics_entry *last, *next;
    
    	m = cntlr_radio_get_metrics(r);
    	if (!m)
    		return false;
    
    	timestamp_update(&m->time);
    	m->anpi = ewma(anpi, m->anpi, 60);
    	m->obss = ewma(obss, m->obss, 60);
    
    	m->tx = ewma(tx, m->tx, 60);
    	m->rx = ewma(rx, m->rx, 60);
    
    
    	last = &m->entry[m->idx];
    
    
    	cntlr_trace(LOG_CHANNEL, "radio ewma metrics " MACFMT " anpi %u obss %u tx %u rx %u\n",
    		    MAC2STR(r->radio_el->macaddr), m->anpi, m->obss, m->tx, m->rx);
    
    
    	/* Update ring buffer */
    	if (timestamp_invalid(&last->time)) {
    		timestamp_update(&last->time);
    		last->anpi = m->anpi;
    		last->obss = m->obss;
    
    		last->tx = m->tx;
    		last->rx = m->rx;
    
    	}
    
    	/* Save/check data to ring buffer each 300 seconds */
    	if (timestamp_elapsed_sec(&last->time) >= 300) {
    		m->idx++;
    		m->idx %= ARRAY_SIZE(m->entry);
    
    		next = &m->entry[m->idx];
    
    		timestamp_update(&next->time);
    		next->anpi = m->anpi;
    		next->obss = m->obss;
    
    		next->tx = m->tx;
    		next->rx = m->rx;
    
    		cntlr_dbg(LOG_CHANNEL, "metrics[%u] radio " MACFMT " anpi %u (%d) obss %u (%d) tx %u (%d) rx %u (%d)\n",
    
    			  m->idx, MAC2STR(r->radio_el->macaddr), next->anpi,
    
    			  next->anpi - last->anpi, next->obss, next->obss - last->obss,
    			  next->tx, next->tx - last->tx,
    			  next->rx, next->rx - last->rx);
    
    
    		/* If other BSS busy increase - worth to check it (100% == 255) */
    		if (next->obss - last->obss > 30)
    			return true;
    
    		/* If noise level worst - worth to check it (0.5 dBm) */
    		if (next->anpi - last->anpi > 20)
    			return true;
    	}
    
    	return false;
    }
    
    void cntlr_acs_radio_metrics(struct node *n, struct netif_radio *r,
    
    			     uint8_t anpi, uint8_t obss,
    			     uint8_t tx, uint8_t rx)
    
    {
    	struct acs_params *acs;
    	struct acs_cac_data *cac;
    
    
    	cntlr_trace(LOG_CHANNEL, "new radio metrics node " MACFMT " radio " MACFMT " anpi %u busy %u tx %u rx %u\n",
    		    MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr), anpi, obss, tx, rx);
    
    
    	cac = cntlr_radio_get_last_cac(r);
    	if (!cac)
    		return;
    
    	acs = cntlr_radio_get_last_acs(r);
    	if (!acs)
    		return;
    
    	/* Kick CAC background clearing - check AP apears */
    	if (n->cntlr->cfg.dfs_cleanup &&
    	    r->radio_el->band == BAND_5 &&
    	    r->radio_el->bgcac_supported &&
    	    cac->status == ACS_CLEANUP_STATUS_NO_APS &&
    	    timestamp_elapsed_sec(&cac->entry_time) >= 30) {
    		cntlr_dbg(LOG_CHANNEL, "kick dfs cleanup (NO_APS before) " MACFMT " radio " MACFMT "\n",
    			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    		cntlr_dfs_radio_cleanup(n, r);
    	}
    
    	if (!n->cntlr->cfg.acs)
    		return;
    
    	/* Store data for future use/compare */
    
    	if (!cntlr_acs_radio_add_and_check_metrics(r, anpi, obss, tx, rx))
    
    		return;
    
    	/* Smth changed in current channel noise/busy */
    	cntlr_dbg(LOG_CHANNEL, "issue ACS recalc due to metrics (request fresh pref) " MACFMT " radio " MACFMT "\n",
    		  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    
    	/* Trigger scan/pref_req and get fresh preference report */
    	cntlr_radio_acs_timer_set(r, 5 * 1000);
    }
    
    void cntlr_acs_scan_report(struct node *n, struct netif_radio *r, uint16_t mid)
    {
    	struct acs_params *acs;
    
    	cntlr_dbg(LOG_CHANNEL, "new scan report node " MACFMT " radio " MACFMT " mid %u\n",
    		  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr), mid);
    
    	acs = cntlr_radio_get_last_acs(r);
    	if (!acs)
    		return;
    
    	if (!n->cntlr->cfg.acs)
    		return;
    
    	/* Scan done for this radio, safe to switch channel */
    	if (acs->recalc && acs->scan_mid != 0xffff) {
    		acs->status = ACS_RECALC_STATUS_SCAN_DONE;
    		timestamp_update(&acs->entry_time);
    	}
    
    	acs->scan_mid = 0xffff;
    
    	/* Request fresh preferences if required */
    	if (cntlr_node_pref_opclass_expired(n, 10) || acs->recalc)
    		/* Preference responose with new data will trigger actions */
    		cntlr_acs_send_pref_request(n);
    }
    
    void cntlr_acs_dev_supp_opclass(struct node *n, struct netif_radio *r)
    {
    	cntlr_trace(LOG_CHANNEL, "new dev supp opclass " MACFMT " radio " MACFMT "\n",
    		    MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
    
    	if (r->radio_el->pref_opclass.num_opclass == 0)
    		cntlr_acs_send_pref_request(n);
    }