diff --git a/src/Makefile b/src/Makefile
index 48552a89f41e3ec2ad75cbb00e64602ffb62d1b3..656e03d702790df9d52ffcb35c17e39f41d2b418 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -19,12 +19,14 @@ OBJS = \
 	config.o \
 	main.o
 
+OBJS += steer_module.o
+
 LIBS = -lubus -lubox -ljson-c -lblobmsg_json -luci -pthread
 LIBS += -rdynamic -ldl
 LIBS += -leasy
 LIBS += -lieee1905 -lmaputil
 
-plugin_subdirs ?= $(wildcard plugins/*)
+plugin_subdirs ?= $(wildcard plugins/*/*)
 plugin_sofile = $(wildcard $(d)/*.so)
 plugin_files = $(foreach d, $(plugin_subdirs), $(plugin_sofile))
 HOOKS = pre-commit
diff --git a/src/cntlr.c b/src/cntlr.c
index a0482e95e072c800c5e94596cbacab8e6d3bba0d..49f2cf41d01fec51fcf977a843dc1e08aace10a9 100644
--- a/src/cntlr.c
+++ b/src/cntlr.c
@@ -44,6 +44,7 @@
 #include "cntlr_ubus.h"
 #include "cntlr_map.h"
 #include "cntlr_cmdu.h"
+#include "steer_module.h"
 
 #define map_plugin	"ieee1905.map"
 
@@ -352,9 +353,12 @@ static int forall_node_update_neighbors(struct controller *c)
 }
 #endif
 
-void cntlr_add_sta_steer_attempt(struct controller *c, uint8_t *sta_mac,
-		uint8_t *src_bssid, uint8_t *dst_bssid,
-		enum steer_method method, enum steer_trigger trigger)
+void cntlr_update_sta_steer_counters(struct controller *c,
+				     uint8_t *sta_mac,
+				     uint8_t *src_bssid,
+				     uint8_t *dst_bssid,
+				     enum steer_method method,
+				     enum steer_trigger trigger)
 {
 	trace("%s:--->\n", __func__);
 
@@ -402,103 +406,118 @@ void cntlr_add_sta_steer_attempt(struct controller *c, uint8_t *sta_mac,
 	s->num_steer_attempts += 1;
 }
 
-#define STEER_ATTEMPT_MIN_INTV 30000 /* ms */
+static int cntlr_steer_sta(struct controller *c, struct sta *s,
+			   struct steer_target *to, uint32_t mode,
+			   uint32_t reason)
+{
+	struct cmdu_buff *cmdu;
+
+
+	if (!to || hwaddr_is_zero(to->bssid)) {
+		dbg("%s: steer verdict = ok, but target AP = NULL!\n", __func__);
+		return 0;
+	}
+
+	if (!memcmp(to->bssid, s->bssid, 6)) {
+		s->stats.no_candidate_cnt++;
+		dbg("%s: " MACFMT " connected to best AP! No steer needed.\n",
+		    __func__);
+		return 0;
+	}
+
+	dbg("%s: Try to steer " MACFMT " from " MACFMT " to " MACFMT "\n",
+	     __func__, MAC2STR(s->macaddr), MAC2STR(s->bssid), MAC2STR(to->bssid));
+
+	UNUSED(mode);	//TODO
+	UNUSED(reason);
+
+	cmdu = cntlr_gen_client_steer_request(c, s->fh->agent->alid,
+					s->bssid, 0,
+					1, (uint8_t (*)[6])s->macaddr,
+					1, (uint8_t (*)[6])to->bssid,
+					1); /* mandate */
+	if (!cmdu) {
+		warn("%s: Failed to generate cmdu for steering sta!\n", __func__);
+		return -1;
+	}
+
+	cntlr_update_sta_steer_counters(c, s->macaddr, s->bssid, to->bssid,
+					METHOD_BTM_REQ, TRIGGER_LINK_QUALITY);
+	send_cmdu(c, cmdu);
+	cmdu_free(cmdu);
+
+	return 0;
+}
+
 static void cntlr_bcn_metrics_parse(struct uloop_timeout *t)
 {
 	trace("%s:--->\n", __func__);
 
 	struct sta *s = container_of(t, struct sta, bcn_metrics_timer);
-	struct bcn_metrics *best = NULL, *b = NULL;
 	struct node *n = s->fh->agent;
 	struct controller *c = n->cntlr;
 	struct netif_iface *bss = NULL;
+	struct bcn_metrics *b, *tmp;
+	struct steer_sta candidate = {
+		.s = s,
+		.nbrlist = NULL,
+		.bcnlist = &s->bcnlist,
+		.best = NULL,
+	};
+	int ret;
 
-	dbg("|%s:%d| for "MACFMT" attached to bssid " MACFMT "\n",
-	    __func__, __LINE__,
-	    MAC2STR(s->macaddr), MAC2STR(s->bssid));
-	dbg("|%s:%d| node = "MACFMT"\n", __func__, __LINE__,
-	    MAC2STR(n->alid));
 
-	//for (i = 0; i < s->num_bcn_metrics; i++) {
-	list_for_each_entry(b, &s->bcnlist, list) {
-		dbg("|%s:%d| bcn "MACFMT" \n", __func__, __LINE__,
-				MAC2STR(b->bssid));
+	dbg("%s: STA " MACFMT" connected to " MACFMT " in Node " MACFMT"\n",
+	    __func__, MAC2STR(s->macaddr), MAC2STR(s->bssid), MAC2STR(n->alid));
+
+	list_for_each_entry_safe(b, tmp, &s->bcnlist, list) {
+		dbg("bcn-report from " MACFMT "\n", MAC2STR(b->bssid));
 
+		/* Skip entry not in our network */
 		bss = cntlr_iterate_fbss(c, b->bssid);
 		if (!bss) {
-			/* Skip - bss not in the mesh */
-			dbg("|%s:%d| bssid "MACFMT" is not in the mesh\n",
-					__func__, __LINE__, MAC2STR(b->bssid));
-			continue;
-		}
-
-		if (!best) {
-			best = b;
-			continue;
-		}
-
-		dbg("|%s:%d| best rcpi %u this rcpi %u\n", __func__, __LINE__,
-				best->rcpi, b->rcpi);
-
-		if ((b->rcpi - best->rcpi) > 10) {
-			dbg("|%s:%d| new best bcn "MACFMT" with rcpi %d\n",
-					__func__, __LINE__,
-					MAC2STR(b->bssid),
-					b->rcpi);
-			best = b;
+			list_del(&b->list);
+			free(b);
+			dbg("Delete alien entry "MACFMT"\n", MAC2STR(b->bssid));
 		}
 	}
 
-	if (!best) {
-		dbg("|%s:%d| no best bcn!\n", __func__, __LINE__);
+	if (!c->cfg.enable_sta_steer)
 		return;
-	}
-
-	if (!hwaddr_is_zero(best->bssid)) {
-		struct cmdu_buff *cmdu;
 
-		if (!c->cfg.enable_sta_steer) {
-			trace("|%s:%d| better bssid found, but will not steer "MACFMT",\
-			       because the 'enable_sta_steer' is not set!\n",
-			       __func__, __LINE__, MAC2STR(s->macaddr));
-			return;
-		}
+	/* check if sta should be steered? */
+	ret = cntlr_maybe_steer_sta(c, &candidate);
+	if (ret) {
+		fprintf(stderr, "cntlr_maybe_steer_sta() ret = %d\n", ret);
+		return;
+	}
 
-		/* Do not steer too often */
+	switch (candidate.verdict) {
+	case STEER_VERDICT_OK:
 		if (!timestamp_expired(&s->stats.last_steer_time, STEER_ATTEMPT_MIN_INTV)) {
-			dbg("|%s:%d| previous steer request less than 30s ago\n", __func__, __LINE__);
-			return;
-		}
-
-		if (!memcmp(best->bssid, s->bssid, 6)) {
-			/* No better bssid found - update stats */
-			s->stats.no_candidate_cnt++;
-			dbg("|%s:%d| same bssid - do not steer!\n",
-			     __func__, __LINE__);
+			dbg("%s: last steer attempt < %us ago; skip steering\n",
+			    __func__, STEER_ATTEMPT_MIN_INTV / 1000);
 			return;
 		}
 
-		dbg("|%s:%d| new bssid, wow! try to steer sta " \
-		     MACFMT " from " MACFMT " to " MACFMT "\n",
-		     __func__, __LINE__,
-		     MAC2STR(s->macaddr), MAC2STR(s->bssid), MAC2STR(best->bssid));
-
-		cmdu = cntlr_gen_client_steer_request(c, s->fh->agent->alid,
-						s->bssid, 0,
-						1, (uint8_t (*)[6])s->macaddr,
-						1, (uint8_t (*)[6])best->bssid,
-						1); /* mandate */
-		if (cmdu) {
-			cntlr_add_sta_steer_attempt(c, s->macaddr, s->bssid, best->bssid,
-					METHOD_BTM_REQ, TRIGGER_LINK_QUALITY);
-			send_cmdu(c, cmdu);
-			cmdu_free(cmdu);
-		}
+		cntlr_steer_sta(c, s, candidate.best, candidate.mode, candidate.reason);
+		break;
+	case STEER_VERDICT_NOK:
+		return;
+	case STEER_VERDICT_MAYBE:
+		/* TODO: check next steer-control ? */
+		break;
+	case STEER_VERDICT_EXCLUDE:
+		/* TODO: exclude this STA from subsequent steer attempts */
+		break;
+	default:
+		break;
 	}
+
 	dbg("%s exiting\n", __func__);
 }
 
-static void cntlr_init_steer_stats(struct sta *s)
+static void cntlr_init_sta_steer_counters(struct sta *s)
 {
 	memset(&s->stats, 0, sizeof(struct steer_stats));
 
@@ -531,7 +550,7 @@ struct sta *cntlr_add_sta(struct controller *c, uint8_t *macaddr)
 	memcpy(s->macaddr, macaddr, 6);
 	list_add(&s->list, &c->stalist);
 	s->bcn_metrics_timer.cb = cntlr_bcn_metrics_parse;
-	cntlr_init_steer_stats(s);
+	cntlr_init_sta_steer_counters(s);
 
 	return s;
 }
@@ -2441,6 +2460,12 @@ int start_controller(void)
 
 	controller_subscribe_for_cmdus(c);
 
+	/* steer-control */
+	INIT_LIST_HEAD(&c->sclist);
+	cntlr_load_steer_modules(c);
+	if (!list_empty(&c->sclist))
+		cntlr_assign_steer_module_default(c);
+
 	uloop_run();
 out_exit:
 	ubus_unregister_event_handler(ctx, &c->evh);
diff --git a/src/cntlr.h b/src/cntlr.h
index 911ca1eb4fff5766d2d82fffb76435adfedd2622..dd11e35faa13f1c22791796cf7ad43bc29395640 100644
--- a/src/cntlr.h
+++ b/src/cntlr.h
@@ -48,7 +48,6 @@ struct bcn_metrics {
 	uint8_t rcpi;
 	uint8_t rsni;
 	uint8_t bssid[6];
-
 	struct list_head list;
 };
 
@@ -387,6 +386,9 @@ struct controller {
 	mapmodule_cmdu_mask_t cmdu_mask;
 	void *subscriber;
 	bool subscribed;
+
+	struct list_head sclist;	/* steer-control module list */
+	struct steer_control *sctrl;	/* active steer-control module */
 };
 
 struct sta_channel_report {
@@ -435,7 +437,7 @@ struct netif_radio *cntlr_node_add_radio(struct controller *c, struct node *n,
 struct netif_iface *cntlr_radio_add_interface(struct controller *c,
 		struct netif_radio *r, uint8_t *hwaddr);
 struct netif_iface *cntlr_iterate_fbss(struct controller *c, uint8_t *mac);
-void cntlr_add_sta_steer_attempt(struct controller *c, uint8_t *sta_mac,
+void cntlr_update_sta_steer_counters(struct controller *c, uint8_t *sta_mac,
 		uint8_t *src_bssid, uint8_t *dst_bssid,
 		enum steer_method method, enum steer_trigger trigger);
 struct sta *cntlr_add_sta(struct controller *c, uint8_t *macaddr);
@@ -461,4 +463,7 @@ bool cntlr_node_opclass_expired(struct node *node);
 
 int cntlr_sync_dyn_controller_config(struct controller *c, uint8_t *agent);
 
+void cntlr_load_steer_modules(struct controller *c);
+void cntlr_unload_steer_modules(struct controller *c);
+
 #endif /* CNTLR_H */
diff --git a/src/cntlr_ubus.c b/src/cntlr_ubus.c
index a3357742c974a12555cc24af7a6b68eedbc22f8b..de21dc677be252236476c9643d0e91087982163e 100644
--- a/src/cntlr_ubus.c
+++ b/src/cntlr_ubus.c
@@ -1067,25 +1067,31 @@ fail_cmdu:
 	return err;
 }
 
-static void cntlr_register_steer_attempt(struct controller *c,
-		uint8_t *bssid, uint32_t sta_nr, uint8_t sta_id[][6],
-		uint32_t bssid_nr, uint8_t target_bbsid[][6])
+static void cntlr_update_sta_steering_stats(struct controller *c, uint8_t *bssid,
+					    uint32_t sta_nr, uint8_t sta_id[][6],
+					    uint32_t bssid_nr, uint8_t target_bbsid[][6])
 {
 	int i;
 
 	/* Number of STAs and BSSIDs are equal, map STA to BSSID */
 	if (sta_nr == bssid_nr) {
 		for (i = 0; i < sta_nr; i++) {
-			cntlr_add_sta_steer_attempt(c, sta_id[i],
-					bssid, target_bbsid[i],
-					METHOD_BTM_REQ, TRIGGER_UNKNOWN);
+			cntlr_update_sta_steer_counters(c,
+							sta_id[i],
+							bssid,
+							target_bbsid[i],
+							METHOD_BTM_REQ,
+							TRIGGER_UNKNOWN);
 		}
 	/* Multiple STAs and single BSSID - one attempt per STA */
 	} else if (sta_nr > 0 && bssid_nr == 1) {
 		for (i = 0; i < sta_nr; i++) {
-			cntlr_add_sta_steer_attempt(c, sta_id[i],
-					bssid, target_bbsid[0],
-					METHOD_BTM_REQ, TRIGGER_UNKNOWN);
+			cntlr_update_sta_steer_counters(c,
+							sta_id[i],
+							bssid,
+							target_bbsid[0],
+							METHOD_BTM_REQ,
+							TRIGGER_UNKNOWN);
 		}
 	}
 	/* No STA provided, request applies to ALL associated STAs */
@@ -1094,18 +1100,24 @@ static void cntlr_register_steer_attempt(struct controller *c,
 
 		list_for_each_entry(s, &c->stalist, list) {
 			if (!memcmp(s->bssid, bssid, 6)) {
-				cntlr_add_sta_steer_attempt(c, s->macaddr,
-						bssid, target_bbsid[0],
-						METHOD_BTM_REQ, TRIGGER_UNKNOWN);
+				cntlr_update_sta_steer_counters(c,
+								s->macaddr,
+								bssid,
+								target_bbsid[0],
+								METHOD_BTM_REQ,
+								TRIGGER_UNKNOWN);
 			}
 		}
 	}
 	/* No BSSID specified for the STAs - automatic best */
 	else if (sta_nr > 0 && bssid_nr == 0) {
 		for (i = 0; i < sta_nr; i++) {
-			cntlr_add_sta_steer_attempt(c, sta_id[i],
-					bssid, NULL,
-					METHOD_BTM_REQ, TRIGGER_UNKNOWN);
+			cntlr_update_sta_steer_counters(c,
+							sta_id[i],
+							bssid,
+							NULL,
+							METHOD_BTM_REQ,
+							TRIGGER_UNKNOWN);
 		}
 	}
 }
@@ -1246,9 +1258,8 @@ static int cntlr_client_steering(struct ubus_context *ctx, struct ubus_object *o
 	UNUSED(sta_multi_nr);
 #endif
 
-	/* Register steer attempt */
-	cntlr_register_steer_attempt(c, bss_id, sta_nr, sta_id,
-			bssid_nr, target_bbsid);
+	cntlr_update_sta_steering_stats(c, bss_id, sta_nr, sta_id,
+					bssid_nr, target_bbsid);
 
 	cmdu_put_eom(cmdu);
 	send_cmdu(c, cmdu);
diff --git a/src/config.c b/src/config.c
index 1a4bf924573b3aa5c6b53c9e2d14cd211c0c40ab..d77f565a243f8e9151ae40894750f72ff1ce50c3 100644
--- a/src/config.c
+++ b/src/config.c
@@ -524,6 +524,7 @@ int cntlr_config_defaults(struct controller *cntlr, struct controller_config *cf
 	INIT_LIST_HEAD(&cfg->radiolist);
 	INIT_LIST_HEAD(&cfg->nodelist);
 	INIT_LIST_HEAD(&cfg->aplist);
+	INIT_LIST_HEAD(&cfg->sclist);
 	return 0;
 }
 
@@ -539,6 +540,7 @@ static int cntlr_config_get_base(struct controller_config *c,
 		CNTLR_ENABLE_BSTA_STEER,
 		CNTLR_USE_BCN_METRICS,
 		CNTLR_USE_USTA_METRICS,
+		CNTLR_STEER_MODULE,
 		NUM_CNTLR_ATTRS
 	};
 	const struct uci_parse_option opts[] = {
@@ -550,6 +552,7 @@ static int cntlr_config_get_base(struct controller_config *c,
 		{ .name = "enable_bsta_steer", .type = UCI_TYPE_STRING },
 		{ .name = "use_bcn_metrics", .type = UCI_TYPE_STRING },
 		{ .name = "use_usta_metrics", .type = UCI_TYPE_STRING },
+		{ .name = "steer_module", .type = UCI_TYPE_LIST },
 	};
 	struct uci_option *tb[NUM_CNTLR_ATTRS];
 
@@ -606,6 +609,23 @@ static int cntlr_config_get_base(struct controller_config *c,
 		c->use_usta_metrics = atoi(val) == 1 ? true : false;
 	}
 
+	if (tb[CNTLR_STEER_MODULE]) {
+		struct uci_element *e;
+		struct steer_control_config *sc;
+
+		fprintf(stderr, "Steer module: ");
+		uci_foreach_element(&tb[CNTLR_STEER_MODULE]->v.list, e) {
+			fprintf(stderr, "%s ", e->name);
+
+			sc = calloc(1, sizeof(*sc));
+			if (sc) {
+				strncpy(sc->name, e->name, 63);
+				list_add_tail(&sc->list, &c->sclist);
+			}
+		}
+		fprintf(stderr, "\n");
+	}
+
 	return 0;
 }
 
diff --git a/src/config.h b/src/config.h
index 158cd02345c9a2c35e5e3417e03add77e58fc3bd..7a9997a1f5491831dc24d3bde14db37259279a47 100644
--- a/src/config.h
+++ b/src/config.h
@@ -137,6 +137,11 @@ struct radio_policy {
 	struct list_head list;                /* link to next policy */
 };
 
+struct steer_control_config {
+	char name[64];
+	struct list_head list;
+};
+
 struct controller_config {
 	bool enabled;
 	bool has_registrar_5g;
@@ -152,6 +157,7 @@ struct controller_config {
 	struct list_head nodelist;
 	struct list_head aplist;
 	struct list_head radiolist;
+	struct list_head sclist;		/* steer-control list */
 };
 
 struct controller;
diff --git a/src/plugins/steer/rcpi/Makefile b/src/plugins/steer/rcpi/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..89e1e34f1fead6cd51b293c210181facc62a2975
--- /dev/null
+++ b/src/plugins/steer/rcpi/Makefile
@@ -0,0 +1,13 @@
+CC ?= gcc
+CFLAGS += -I. -I../../.. -I../../../utils -O2 -Wall -g -Werror
+
+all: rcpi.so
+
+%.o: %.c
+	$(CC) $(CFLAGS) -fPIC -c $< -o $@
+
+rcpi.so: rcpi.o
+	$(CC) $(CFLAGS) $(LDFLAGS) -shared -Wl,-soname,$@ -o $@ $^
+
+clean:
+	rm -f *.o *.so*
diff --git a/src/plugins/steer/rcpi/rcpi.c b/src/plugins/steer/rcpi/rcpi.c
new file mode 100644
index 0000000000000000000000000000000000000000..907baa00d6368d45f692f19566966e17c15a480a
--- /dev/null
+++ b/src/plugins/steer/rcpi/rcpi.c
@@ -0,0 +1,98 @@
+/*
+ * rcpi.c - RCPI based STA steering.
+ *
+ * Copyright (C) 2022 IOPSYS Software Solutions AB. All rights reserved.
+ *
+ * See LICENSE file for license related information.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+
+#include <easy/easy.h>
+
+#include "utils.h"
+#include "debug.h"
+#include "steer.h"
+
+struct rcpi_steer_control {
+	uint8_t diffsnr;
+	uint8_t low;
+	int8_t hysteresis;
+	uint8_t rcpi_threshold;
+	void *self;
+};
+
+static int rcpi_steer_init(void **priv);
+static int rcpi_steer_exit(void *priv);
+
+int rcpi_steer(void *priv, struct steer_sta *s)
+{
+	struct rcpi_steer_control *p = (struct rcpi_steer_control *)priv;
+	struct steer_target *b;
+
+
+	fprintf(stderr, "%s: --------->\n", __func__);
+	UNUSED(p);	//TODO
+
+	s->verdict = STEER_VERDICT_UNDECIDED;
+	s->reason = 2;	//TRIGGER_LINK_QUALITY
+
+	if (list_empty(s->bcnlist))
+		return 0;
+
+	s->best = list_first_entry(s->bcnlist, struct steer_target, list);
+	list_for_each_entry(b, s->bcnlist, list) {
+		if ((b->rcpi - s->best->rcpi) > p->diffsnr) {
+			dbg("%s: new best bcn from "MACFMT" with rcpi %d\n",
+			    __func__, MAC2STR(b->bssid), b->rcpi);
+			s->best = b;
+		}
+	}
+
+	return 0;
+}
+
+extern struct steer_control rcpi;
+struct steer_control rcpi = {
+	.name = "rcpi",
+	.init = rcpi_steer_init,
+	.exit = rcpi_steer_exit,
+	.steer = rcpi_steer,
+};
+
+
+static int rcpi_steer_init(void **priv)
+{
+	struct rcpi_steer_control *p;
+
+
+	p = calloc(1, sizeof(struct rcpi_steer_control));
+	if (!p)
+		return -1;
+
+	*priv = p;
+	p->self = &rcpi;
+	p->low = 90;
+	p->rcpi_threshold = 100;
+	p->hysteresis = 5;
+	p->diffsnr = 10;
+
+	fprintf(stderr, "%s: ========================>\n", __func__);
+	return 0;
+}
+
+static int rcpi_steer_exit(void *priv)
+{
+	struct rcpi_steer_control *p = (struct rcpi_steer_control *)priv;
+
+	if (p)
+		free(p);
+
+	fprintf(stderr, "%s: <========================\n", __func__);
+	return 0;
+}
+
diff --git a/src/steer.h b/src/steer.h
new file mode 100644
index 0000000000000000000000000000000000000000..2f99fa656ef0348a624918cc2ef75c81d00999e1
--- /dev/null
+++ b/src/steer.h
@@ -0,0 +1,84 @@
+/*
+ * steer.h - header for defining a new steering module
+ *
+ * Copyright (C) 2022 IOPSYS Software Solutions AB. All rights reserved.
+ *
+ * Author: anjan.chanda@iopsys.eu
+ *
+ * See LICENSE file for license related information.
+ *
+ */
+
+#ifndef STEER_H
+#define STEER_H
+
+#include <stdint.h>
+#include <libubox/list.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum steer_verdict {
+	STEER_VERDICT_UNDECIDED,
+	STEER_VERDICT_OK,
+	STEER_VERDICT_MAYBE,
+	STEER_VERDICT_NOK,
+	STEER_VERDICT_EXCLUDE,
+};
+
+enum steer_reason {
+	STEER_REASON_UNDEFINED,
+	STEER_REASON_LOW_RCPI,
+	STEER_REASON_LOW_THPUT,
+	STEER_REASON_HIGH_PER,
+	STEER_REASON_OTHER,
+};
+
+typedef enum steer_verdict steer_verdict_t;
+
+struct steer_config {
+	uint8_t rcpi_threshold;
+	uint8_t rcpi_hysteresis;
+	uint8_t ch_utilization;
+};
+
+struct steer_target {
+	uint8_t opclass;
+	uint8_t channel;
+	uint8_t rcpi;
+	uint8_t rsni;
+	uint8_t bssid[6];
+	struct list_head list;
+};
+
+struct steer_sta {
+	struct sta *s;
+	struct list_head *nbrlist;
+	struct list_head *bcnlist;
+	steer_verdict_t verdict;
+	struct steer_target *best;
+	uint32_t reason;
+	uint32_t mode;
+};
+
+struct steer_control {
+	char name[64];
+	uint8_t rcpi_threshold;
+	uint8_t rcpi_hysteresis;
+	uint8_t cbinterval;
+	void *priv;
+	int (*init)(void **);
+	int (*exit)(void *);
+	int (*steer)(void *, struct steer_sta *candidate);
+	int (*config)(void *, struct steer_config *);
+	void *handle;
+	struct list_head list;
+};
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* STEER_H */
diff --git a/src/steer_module.c b/src/steer_module.c
new file mode 100644
index 0000000000000000000000000000000000000000..04b34ec05089871831a5818204305a1054869dd4
--- /dev/null
+++ b/src/steer_module.c
@@ -0,0 +1,224 @@
+/*
+ * steer_module.c - STA steering module
+ *
+ * Copyright (C) 2022 IOPSYS Software Solutions AB. All rights reserved.
+ *
+ * Author: anjan.chanda@iopsys.eu
+ *
+ * See LICENSE file for license related information.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <dlfcn.h>
+#include <sys/time.h>
+
+#include <json-c/json.h>
+#include <libubox/blobmsg.h>
+#include <libubox/blobmsg_json.h>
+#include <libubox/uloop.h>
+#include <libubox/ustream.h>
+#include <libubox/utils.h>
+#include <libubus.h>
+
+#include <easy/easy.h>
+
+#include <timer_impl.h>
+#include <cmdu.h>
+#include <1905_tlvs.h>
+#include <map2.h>
+#include <map_module.h>
+#include <uci.h>
+
+#include "utils/debug.h"
+#include "utils/utils.h"
+#include "config.h"
+#include "cntlr.h"
+#include "steer_module.h"
+
+#define CNTLR_STEER_MODULE_PATH		"/usr/lib/mapcontroller"
+
+
+static int plugin_load(const char *path, const char *name, void **handle)
+{
+	char abspath[256] = {0};
+	int flags = 0;
+	void *h;
+
+	if (!handle || !name || !path)
+		return -1;
+
+	flags |= RTLD_NOW | RTLD_GLOBAL;
+	snprintf(abspath, sizeof(abspath) - 1, "%s/%s", path, name);
+	h = dlopen(abspath, flags);
+	if (!h) {
+		fprintf(stderr, "%s: Error: %s\n", __func__, dlerror());
+		return -1;
+	}
+
+	*handle = h;
+	return 0;
+}
+
+static int plugin_unload(void *handle)
+{
+	if (!handle)
+		return -1;
+
+	return dlclose(handle);
+}
+
+static int cntlr_unload_steer_module(struct steer_control *sc)
+{
+	int ret;
+
+	ret = plugin_unload(sc->handle);
+	list_del(&sc->list);
+
+	return !ret ? 0 : -1;
+}
+
+int cntlr_load_steer_module(struct controller *priv, const char *name,
+			    struct steer_control **sc)
+{
+	struct steer_control *p, *pp = NULL;
+	char fname[128] = {0};
+	void *handle;
+	int ret;
+
+
+	snprintf(fname, 127, "%s.so", name);
+	ret = plugin_load(CNTLR_STEER_MODULE_PATH, fname, &handle);
+	if (ret)
+		return -1;
+
+	pp = dlsym(handle, name);
+	if (!pp) {
+		fprintf(stderr, "Symbol '%s' not found\n", name);
+		return -1;
+	}
+
+	p = calloc(1, sizeof(struct steer_control));
+	if (!p) {
+		plugin_unload(handle);
+		return -1;
+	}
+
+	memcpy(p, pp, sizeof(struct steer_control));
+	p->handle = handle;
+	*sc = p;
+
+	if (p->init)
+		p->init(&p->priv);
+
+	fprintf(stderr, "Registered %s (priv = 0x%p)\n", name, p->priv);
+	return 0;
+}
+
+static struct steer_control *cntlr_lookup_steer_module(struct controller *c,
+						       const char *name)
+{
+	struct steer_control *sc;
+
+	list_for_each_entry(sc, &c->sclist, list) {
+		if (!strncmp(sc->name, name, strlen(sc->name)))
+			return sc;
+	}
+
+	return NULL;
+}
+
+void cntlr_assign_steer_module(struct controller *c, const char *name)
+{
+	if (!name || name[0] == '\0') {
+		c->sctrl = NULL;
+		return;
+	}
+
+	c->sctrl = cntlr_lookup_steer_module(c, name);
+}
+
+
+void cntlr_load_steer_modules(struct controller *c)
+{
+	struct steer_control_config *e;
+
+
+	list_for_each_entry(e, &c->cfg.sclist, list) {
+		struct steer_control *sc = NULL;
+		int ret = 0;
+
+		if (cntlr_lookup_steer_module(c, e->name))
+			continue;
+
+		dbg("Loading steer module '%s'\n", e->name);
+		ret = cntlr_load_steer_module(c, e->name, &sc);
+		if (!ret)
+			list_add_tail(&sc->list, &c->sclist);
+	}
+}
+
+void cntlr_unload_steer_modules(struct controller *c)
+{
+	struct steer_control *p, *tmp;
+
+	list_for_each_entry_safe(p, tmp, &c->sclist, list) {
+		if (p->exit)
+			p->exit(p->priv);
+
+		list_del(&p->list);
+		plugin_unload(p->handle);
+		free(p);
+	}
+}
+
+int cntlr_register_steer_module(struct controller *c, const char *name)
+{
+	struct steer_control *sc;
+	int ret;
+
+
+	if (!name || name[0] == '\0')
+		return -1;
+
+	if (cntlr_lookup_steer_module(c, name)) {
+		info("Steer module '%s' already registered\n", name);
+		return 0;
+	}
+
+	ret = cntlr_load_steer_module(c, name, &sc);
+	if (!ret) {
+		list_add_tail(&sc->list, &c->sclist);
+		return 0;
+	}
+
+	return -1;
+}
+
+int cntlr_unregister_steer_module(struct controller *c, char *name)
+{
+	struct steer_control *sc;
+
+
+	if (!name || name[0] == '\0')
+		return -1;
+
+	sc = cntlr_lookup_steer_module(c, name);
+	if (!sc)
+		return -1;
+
+	return cntlr_unload_steer_module(sc);
+}
+
+int cntlr_maybe_steer_sta(struct controller *c, struct steer_sta *s)
+{
+	struct steer_control *sc = cntlr_get_steer_control(c);
+
+	if (sc && sc->steer)
+		return sc->steer(sc->priv, s);
+
+	return 0;
+}
diff --git a/src/steer_module.h b/src/steer_module.h
new file mode 100644
index 0000000000000000000000000000000000000000..ef0be16ea0a5dab41619875939e256750dd81416
--- /dev/null
+++ b/src/steer_module.h
@@ -0,0 +1,49 @@
+/*
+ * steer_module.h - header for steering related stuff
+ *
+ * Copyright (C) 2022 IOPSYS Software Solutions AB. All rights reserved.
+ *
+ * Author: anjan.chanda@iopsys.eu
+ *
+ * See LICENSE file for license related information.
+ *
+ */
+
+#ifndef STEER_MODULE_H
+#define STEER_MODULE_H
+
+#include <stdint.h>
+#include <libubox/list.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "steer.h"
+
+#define STEER_ATTEMPT_MIN_INTV		30000 /* ms */
+
+static inline struct steer_control *cntlr_get_steer_control(struct controller *c)
+{
+	return c->sctrl;
+}
+
+static inline void cntlr_assign_steer_module_default(struct controller *c)
+{
+	c->sctrl = !list_empty(&c->sclist) ?
+			list_first_entry(&c->sclist, struct steer_control, list) :
+			NULL;
+}
+
+void cntlr_assign_steer_module(struct controller *c, const char *name);
+
+int cntlr_register_steer_module(struct controller *c, const char *name);
+int cntlr_unregister_steer_module(struct controller *c, char *name);
+int cntlr_maybe_steer_sta(struct controller *c, struct steer_sta *s);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* STEER_MODULE_H */