Contributing to OWM
Overview
This guide explains how to contribute features to OWM (One Wifi Manager), including adding new functionality like DPP and working with OWM modules.
1. Adding New Features
Example: Adding Maximum Station Limit Support
This example demonstrates adding support for limiting the maximum number of connected stations per VAP. When the limit is reached, the AP stops responding to probe requests from new clients. This will use hostapd's no_probe_resp_if_max_sta option.
Step 1: Add OVSDB Schema Field
First, add the new field to the OVSDB schema:
File: interfaces/opensync.ovsschema
"Wifi_VIF_Config": {
"columns": {
...
"no_probe_resp_if_max_sta": {
"type": "integer"
}
}
}Note: The value 0 means disabled, any other value means enabled. see: hostapd.conf
Step 2: OVSDB to ow_conf Layer
Read from OVSDB and set in ow_conf (similar to max_sta at line 3796):
File: src/lib/ow/src/ow_ovsdb.c
static void
ow_ovsdb_vconf_to_ow_conf_ap(struct schema_Wifi_VIF_Config *vconf, bool is_new)
{
// ... existing OVSDB reading code ...
if (is_new == true || vconf->no_probe_resp_if_max_sta_changed == true) {
if (vconf->no_probe_resp_if_max_sta_exists == true) {
const int x = vconf->no_probe_resp_if_max_sta;
ow_conf_vif_set_ap_no_probe_resp_if_max_sta(vconf->if_name, &x);
}
else {
ow_conf_vif_set_ap_no_probe_resp_if_max_sta(vconf->if_name, NULL);
}
}
}Also add state reporting back to OVSDB (similar to max_sta at line 1611):
static void
ow_ovsdb_vifstate_to_schema(const struct osw_state_vif_info *vif)
{
// ... existing state reporting ...
const struct osw_drv_vif_state *drv = &vif->drv_state;
const struct osw_drv_vif_state_ap *ap = &drv->u.ap;
SCHEMA_SET_INT(schema->no_probe_resp_if_max_sta, ap->no_probe_resp_if_max_sta);
}Step 3: ow_conf Layer
Add field storage and mutate function (similar to max_sta at lines 114, 806, 2291-2292, 2425):
File: src/lib/ow/src/ow_conf.c
struct ow_conf_vif {
// ... existing fields ...
int *ap_no_probe_resp_if_max_sta;
};
static void
ow_conf_conf_mutate_vif_ap(struct ow_conf *self,
struct ow_conf_vif *ow_vif,
struct osw_drv_vif_config *osw_vif)
{
// ... existing mutations ...
if (ow_vif->ap_no_probe_resp_if_max_sta != NULL) {
osw_vif->u.ap.no_probe_resp_if_max_sta = *ow_vif->ap_no_probe_resp_if_max_sta;
}
}
// Add format macros and setter using DEFINE_VIF_FIELD macro
#define FMT_vif_ap_no_probe_resp_if_max_sta "%d"
#define ARG_vif_ap_no_probe_resp_if_max_sta(x) x
DEFINE_VIF_FIELD(ap_no_probe_resp_if_max_sta);Step 4: osw_drv Layer
Add fields to driver config and state structures (similar to max_sta at lines 198-199, 306):
File: src/lib/osw/inc/osw_drv.h
struct osw_drv_vif_config_ap {
// ... existing fields ...
int no_probe_resp_if_max_sta;
bool no_probe_resp_if_max_sta_changed;
};
struct osw_drv_vif_state_ap {
// ... existing fields ...
int no_probe_resp_if_max_sta;
};Step 5: osw_confsync Layer
Add comparison logic (similar to max_sta at lines 492-495, 1251, 1294, 1541):
File: src/lib/osw/src/osw_confsync.c
static void
osw_confsync_build_vif_ap_debug(const char *phy, const char *vif,
const struct osw_drv_vif_state_ap *state,
const struct osw_drv_vif_config_ap *conf,
const struct osw_drv_vif_config_ap *cmd,
bool *notified)
{
// ... existing comparisons ...
if (cmd->no_probe_resp_if_max_sta_changed) {
LOGI("osw: confsync: %s/%s: no_probe_resp_if_max_sta: %d -> %d",
phy, vif, state->no_probe_resp_if_max_sta, conf->no_probe_resp_if_max_sta);
*notified = true;
}
}
static void
osw_confsync_vif_ap_mark_changed(const struct osw_drv_vif_state_ap *svif,
const struct osw_drv_vif_config_ap *cvif,
struct osw_drv_vif_config_ap *dvif,
bool all)
{
// ... existing comparisons ...
// Mark as changed if values differ
dvif->no_probe_resp_if_max_sta_changed = all ||
(svif->no_probe_resp_if_max_sta != cvif->no_probe_resp_if_max_sta);
// Accumulate into overall changed flag
dvif->changed |= dvif->no_probe_resp_if_max_sta_changed;
}
static void
osw_confsync_build_drv_conf_vif_ap(const struct osw_drv_vif_config_ap *cvif,
struct osw_drv_vif_config_ap *dvif)
{
// ... existing copies ...
// Copy value to driver command
dvif->no_probe_resp_if_max_sta = cvif->no_probe_resp_if_max_sta;
}Step 6: hostapd Configuration
Generate and read hostapd config (similar to proxy_arp at lines 1294-1298, 1511, 1700, 2054):
File: src/lib/osw/src/osw_hostap_conf.c
void
osw_hostap_conf_no_probe_resp_if_max_sta(const struct osw_drv_vif_config_ap *ap,
struct osw_hostap_conf_ap_config *conf)
{
OSW_HOSTAP_CONF_SET_VAL(conf->no_probe_resp_if_max_sta, ap->no_probe_resp_if_max_sta);
}
static void
osw_hostap_conf_fill_ap(const struct osw_drv_vif_config_ap *ap,
struct osw_hostap_conf_ap_config *conf)
{
// ... existing config generation ...
osw_hostap_conf_no_probe_resp_if_max_sta(ap, conf);
}
static void
osw_hostap_conf_generate_ap(const struct osw_hostap_conf_ap_config *conf, FILE *f)
{
// ... existing generation ...
CONF_APPEND(no_probe_resp_if_max_sta, "%d");
}
static void
osw_hostap_conf_fill_ap_state(const struct osw_hostap *hostap,
struct osw_drv_vif_state_ap *ap)
{
// ... existing state reading ...
STATE_GET_INT(ap->no_probe_resp_if_max_sta, config, "no_probe_resp_if_max_sta");
}Critical for Confsync: The state reading in step 6 ensures confsync can verify that:
Config was written to hostapd
State reflects what was configured
No mismatch triggers continuous retries
Confsync watchdog stays happy
Step 7: Add Unit Tests
Add tests for the new functionality:
File: src/lib/ow/ut/ow_conf_ut.c or src/lib/osw/ut/osw_confsync_ut.c
#include <osw_ut.h>
OSW_UT(osw_confsync_no_probe_resp_if_max_sta)
{
// Test that confsync properly compares config vs state
struct osw_drv_vif_state_ap state = {
.no_probe_resp_if_max_sta = 0, // Disabled
};
struct osw_drv_vif_config_ap config = {
.no_probe_resp_if_max_sta = 1, // Enabled
};
struct osw_drv_vif_config_ap cmd = {0};
bool notified = false;
osw_confsync_vif_ap_changed("phy0", "wlan0", &state, &config, &cmd, ¬ified, false);
// Verify change was detected
OSW_UT_EXPECT(cmd.no_probe_resp_if_max_sta_changed == true);
OSW_UT_EXPECT(cmd.no_probe_resp_if_max_sta == 1);
OSW_UT_EXPECT(notified == true);
}Step 8: Test with OVSDB
# Enable no_probe_resp_if_max_sta (any non-zero value enables it)
ovsh u Wifi_VIF_Config -w if_name==wlan0 \
no_probe_resp_if_max_sta:=1
# Verify configuration was set
ovsh s Wifi_VIF_Config -w if_name==wlan0 no_probe_resp_if_max_sta
# Check state was reported correctly (critical for confsync!)
ovsh s Wifi_VIF_State -w if_name==wlan0 no_probe_resp_if_max_sta
# Verify confsync is happy (no continuous retries)
logread | grep confsync | grep -i settle
# Check hostapd config was updated
cat /var/run/hostapd-wlan0.conf | grep "no_probe_resp_if_max_sta"
# Disable the feature
ovsh u Wifi_VIF_Config -w if_name==wlan0 \
no_probe_resp_if_max_sta:=0
# Monitor logs for config updates
logread -f | grep "ow:"Real-World Example
For a complete, production-quality example of adding a new feature, see ow_radar_next_channel implementation:
File: src/lib/ow/src/ow_radar_next_channel.c
This module demonstrates the full pattern:
OVSDB configuration - Reads from
Wifi_Radio_ConfigtableMutator pattern - Pushes configuration through
ow_conftoosw_drvSeparate OSW module - Implements logic as an independent module using
OSW_MODULEmacroState management - Properly handles state reporting and confsync
Complete integration - Shows all layers working together
Excellent unit tests - Contains comprehensive unit tests in the same file demonstrating how to test the feature
This is an excellent reference for understanding how to implement a complete feature from OVSDB all the way to the driver layer, including proper testing.
2. OWM Modules Explained
What are OWM Modules?
OWM uses a modular architecture where functionality is divided into modules. Modules are independent components that can be loaded/unloaded and tested in isolation.
Module System
OWM modules use the OSW_MODULE macro from the OSW module framework to register and initialize components.
Creating a New Module
Example: Custom Steering Policy
File: src/lib/ow/src/ow_steer_policy_custom.c
This example shows how to create a steering policy similar to ow_steer_policy_btm_response.c:
#include <log.h>
#include <memutil.h>
#include <osw_types.h>
#include <osw_diag.h>
#include <const.h>
#include "ow_steer_candidate_list.h"
#include "ow_steer_policy.h"
#define LOG_PREFIX(fmt, ...) "ow: steer: " fmt, ##__VA_ARGS__
#define LOG_GET_PREFIX(policy, fmt, ...) \
"%s " fmt, \
ow_steer_policy_get_prefix(policy), \
##__VA_ARGS__
static const char *g_policy_name = "custom";
struct ow_steer_policy_custom {
struct ow_steer_policy *base;
int rssi_threshold; // Example: minimum RSSI for candidate
};
struct ow_steer_policy*
ow_steer_policy_custom_get_base(struct ow_steer_policy_custom *custom_policy)
{
ASSERT(custom_policy != NULL, "");
return custom_policy->base;
}
void
ow_steer_policy_custom_free(struct ow_steer_policy_custom *custom_policy)
{
if (custom_policy == NULL)
return;
ow_steer_policy_free(custom_policy->base);
FREE(custom_policy);
}
static void
ow_steer_policy_custom_recalc_cb(struct ow_steer_policy *policy,
struct ow_steer_candidate_list *candidate_list)
{
ASSERT(policy != NULL, "");
ASSERT(candidate_list != NULL, "");
struct ow_steer_policy_custom *custom_policy = ow_steer_policy_get_priv(policy);
// Example: Filter candidates based on RSSI threshold
size_t i;
for (i = 0; i < ow_steer_candidate_list_get_length(candidate_list); i++) {
struct ow_steer_candidate *candidate = ow_steer_candidate_list_get(candidate_list, i);
const struct osw_hwaddr *bssid = ow_steer_candidate_get_bssid(candidate);
// Your custom logic here
// Example: mark candidates with low RSSI as out of scope
const bool meets_threshold = true; // Replace with actual RSSI check
if (meets_threshold == false) {
const char *reason = ow_steer_policy_get_name(policy);
ow_steer_candidate_set_preference(candidate, reason,
OW_STEER_CANDIDATE_PREFERENCE_OUT_OF_SCOPE);
}
LOGD(LOG_GET_PREFIX(policy,
"bssid: "OSW_HWADDR_FMT" preference: %s",
OSW_HWADDR_ARG(bssid),
ow_steer_candidate_preference_to_cstr(ow_steer_candidate_get_preference(candidate))));
}
}
static void
ow_steer_policy_custom_sigusr1_dump_cb(osw_diag_pipe_t *pipe,
struct ow_steer_policy *policy)
{
ASSERT(policy != NULL, "");
struct ow_steer_policy_custom *custom_policy = ow_steer_policy_get_priv(policy);
osw_diag_pipe_writef(pipe, LOG_PREFIX(" rssi_threshold: %d", custom_policy->rssi_threshold));
}
struct ow_steer_policy_custom*
ow_steer_policy_custom_create(const char *name,
const struct osw_hwaddr *sta_addr,
const struct ow_steer_policy_mediator *mediator,
const char *log_prefix,
int rssi_threshold)
{
ASSERT(name != NULL, "");
ASSERT(sta_addr != NULL, "");
ASSERT(mediator != NULL, "");
const struct ow_steer_policy_ops policy_ops = {
.sigusr1_dump_fn = ow_steer_policy_custom_sigusr1_dump_cb,
.recalc_fn = ow_steer_policy_custom_recalc_cb,
};
struct ow_steer_policy_custom *custom_policy = CALLOC(1, sizeof(*custom_policy));
custom_policy->base = ow_steer_policy_create(strfmta("%s_%s", g_policy_name, name),
sta_addr,
&policy_ops,
mediator,
log_prefix,
custom_policy);
custom_policy->rssi_threshold = rssi_threshold;
return custom_policy;
}
// If this policy needs to be instantiated as a module:
OSW_MODULE(ow_steer_policy_custom)
{
// This is optional - only needed if you want the policy to be
// automatically instantiated. Most steering policies are created
// on-demand by ow_steer_bm based on OVSDB configuration.
// Example: Create policy for a specific STA
// static struct osw_hwaddr sta_addr = { .octet = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55} };
// static struct ow_steer_policy_mediator *mediator = get_mediator_from_somewhere();
// static struct ow_steer_policy_custom *policy;
// policy = ow_steer_policy_custom_create("example", &sta_addr, mediator, "custom", -70);
// return policy;
return NULL; // Most policies are created dynamically
}Key patterns matching ow_steer_policy_btm_response.c:
Proper includes and LOG_PREFIX macros
Structure with
basepolicy pointerCreate and free functions
recalc_cbthat modifies candidate list preferencessigusr1_dump_cbfor debugging via SIGUSR1 signalUses
CALLOC,FREE,ASSERTfrom memutil.hGets private data via
ow_steer_policy_get_priv()OSW_MODULE registration (optional for steering policies, as they're usually created dynamically)
3. Common OWM Modules
1. osw_confsync
Configuration synchronization engine.
Purpose: Keeps config and state synchronized
Key Functions:
// Register for state changes
osw_confsync_register_changed_fn(cs, "name", callback, priv);
// Get current state
enum osw_confsync_state state = osw_confsync_get_state(cs);2. osw_stats
Statistics collection and distribution.
Purpose: Collect and publish wireless statistics
Key Functions:
// Publish stats
void osw_drv_report_stats(const struct osw_tlv *stats);
// Subscribe to stats
struct osw_stats_subscriber *sub = osw_stats_subscriber_alloc(fn, priv);
osw_stats_subscriber_register(sub);3. osw_state
Current hardware state.
Purpose: Maintain current state of radios, VIFs, STAs
Key Functions:
// Register observer
struct osw_state_observer obs = { ... };
osw_state_register_observer(&obs);
// Get state (family of osw_state_phy_lookup functions)
const struct osw_state_phy_info *phy = osw_state_phy_lookup(phy_name);
const struct osw_state_vif_info *vif = osw_state_vif_lookup(phy_name, vif_name);
const struct osw_state_sta_info *sta = osw_state_sta_lookup(sta_addr);4. ow_ovsdb
OVSDB integration.
Purpose: Bridge OVSDB and OSW
Module Lifecycle
Registration (Build Time):
MODULE(osw_module_example, init_cb, fini_cb)Module is registered with the framework.
Initialization (Module Load):
static void *osw_module_example_load_cb(void) { // Initialize module // Register observers // Return module data return &module; }Operation:
Module handles events
Processes state changes
Executes logic
Note: There is no provision for unloading OSW modules. Once a module is loaded, it remains active for the lifetime of the process.
Unit Testing Modules
Unit tests are added at the end of each module's source file.
#include <osw_ut.h>
OSW_UT(ow_steer_policy_custom_basic)
{
// Test basic functionality
// Create test structures on stack or use static allocation
struct ow_steer_policy_custom policy;
// Test init function (no framework dependencies)
ow_steer_policy_custom_init(&policy);
const bool result = test_custom_logic(&policy);
OSW_UT_EXPECT(result == true);
}
OSW_UT(ow_steer_policy_custom_recalc)
{
// Test recalc callback
struct ow_steer_candidate_list *list = create_test_candidate_list();
ow_steer_policy_custom_recalc_cb(NULL, list);
// Verify preferences were set correctly
OSW_UT_EXPECT(verify_preferences(list) == true);
}Best practices:
Test
_initfunctions independently (no framework dependencies)Don't test
_attachin unit tests (requires full framework)Use stack allocation or static structures
See
src/lib/ow/src/ow_radar_next_channel.cfor a complete example with unit tests
Running unit tests (OWM only)
Unit tests are compiled and executed in the build environment we call native (TARGET=native resolves to native-cc-ubuntu20.04-x86_64).
# Compile owm
./docker/dock-run make TARGET=native src/owm/ -j$(nproc)
# Execute unit tests for compiled owm (-h prints help)
./docker/dock-run work/native-cc-ubuntu20.04-x86_64/bin/owm -t
# In GDB execute all unit tests for compiled owm
./docker/dock-run gdb -args work/native-cc-ubuntu20.04-x86_64/bin/owm -g
# In GDB execute specific unit test for compiled OWM
./docker/dock-run gdb -args work/native-cc-ubuntu20.04-x86_64/bin/owm -g -u osw_ut_func_ow_steer_bm_stats_disconnect_snr3. Submitting Features
Code Style
General Rule: Match the style of the 5 most recently created files in the same directory.
Formatting: Use clang-format to automatically format your code:
# Format all modified files
./docker/dock-run ./scripts/clang-format-dir.sh --fixRun this before committing to ensure consistent code formatting across the codebase.
Testing Requirements
Before submitting, ensure:
[ ] All unit tests pass:
owm -t[ ] Code compiles without warnings
[ ] Manual testing completed
[ ] Code review
Example Pull Request
Title: Add no_probe_resp_if_max_sta support
Description:
This PR adds support for the no_probe_resp_if_max_sta hostapd option to limit
the maximum number of connected stations per VAP.
Changes:
- Add no_probe_resp_if_max_sta field to OVSDB schema
- Implement OVSDB to ow_conf translation in ow_ovsdb.c
- Add field to osw_drv.h structures
- Implement confsync comparison logic
- Add hostapd config generation and state reading
- Add unit tests for confsync logic
Testing:
- Unit tests: Pass ✓
- Confsync verification: No watchdog triggers ✓
- Manual testing with OVSDB: Pass ✓
Design Rationale:
Why this approach:
- Leverages hostapd's native no_probe_resp_if_max_sta option
- No kernel changes or custom patches required
- Works consistently across all platforms that use hostapd
- Follows the established pattern for hostapd configuration fields (proxy_arp, dgaf_disable)
- hostapd handles the feature efficiently at the appropriate layer
Why not alternative approaches:
- Linux-level probe rejection: Would require additional netfilter rules, adds complexity,
and creates dependencies on Linux-specific networking stack
- Direct WLAN driver configuration: Would require platform-specific code for each vendor
(Broadcom, Qualcomm, MediaTek, etc.), making maintenance difficult and implementation
inconsistent across platforms
Related Issues: #1234. Best Practices
1. Logging
Use appropriate log levels with module-specific prefixes:
// Define a prefix for your module
#define LOG_PREFIX(fmt, ...) "my_module: " fmt, ##__VA_ARGS__
// Define prefixes for PHY context (builds on LOG_PREFIX)
#define LOG_PREFIX_PHY(phy_name, fmt, ...) \
LOG_PREFIX("%s: " fmt, phy_name, ##__VA_ARGS__)
// Define prefixes for VIF context (builds on LOG_PREFIX_PHY)
#define LOG_PREFIX_VIF(phy_name, vif_name, fmt, ...) \
LOG_PREFIX_PHY(phy_name, "%s: " fmt, vif_name, ##__VA_ARGS__)
// Use appropriate log levels
LOGE(LOG_PREFIX("failed to configure: %s", error)); // Error - action failed
LOGW(LOG_PREFIX("unexpected value: %d", value)); // Warning - unexpected but ok
LOGN(LOG_PREFIX("client connected: "MAC_FMT, MAC_ARG)); // Notice - important event
LOGI(LOG_PREFIX("started successfully")); // Info - significant event
LOGD(LOG_PREFIX("processing state change")); // Debug - detailed debugging
LOGT(LOG_PREFIX("entering function: %s", __func__)); // Trace - very verbose
// With PHY/VIF context
LOGI(LOG_PREFIX_PHY(phy->phy_name, "channel changed to %d", channel));
LOGW(LOG_PREFIX_VIF(phy->phy_name, vif->vif_name, "client rejected: "MAC_FMT, MAC_ARG));2. Memory Management
Ownership Model:
Follow clear memory ownership patterns to prevent leaks and double-frees:
Pattern 1: Caller Owns Memory (Preferred)
Functions should fill in memory that has been previously allocated by the caller:
// Caller allocates and owns memory
struct my_stats *stats = CALLOC(1, sizeof(*stats));
my_module_get_stats(stats); // Function fills in the data
// ... use stats ...
FREE(stats); // Caller frees
// Function implementation
void my_module_get_stats(struct my_stats *stats)
{
// Fill in caller's memory
stats->count = get_count();
stats->value = get_value();
// Do NOT free - caller owns the memory
}Pattern 2: Function Allocates, Caller Frees
If a function must allocate memory, document it clearly:
// Returns allocated memory - caller must free
char *my_module_get_name(void)
{
char *name = STRDUP("example");
return name; // Caller must FREE(name)
}
// Usage
char *name = my_module_get_name();
// ... use name ...
FREE(name); // Caller freesPattern 3: Paired Alloc/Free Functions
For complex objects, provide paired allocation and deallocation functions:
// Allocate and initialize
struct my_object *my_object_alloc(void)
{
struct my_object *obj = CALLOC(1, sizeof(*obj));
// ... initialize ...
return obj;
}
// Free and cleanup
void my_object_free(struct my_object *obj)
{
if (obj == NULL) return;
// ... cleanup internal resources ...
FREE(obj);
}
// Usage
struct my_object *obj = my_object_alloc();
// ... use obj ...
my_object_free(obj);Key Principles:
The entity that allocates memory is responsible for freeing it (or providing a free function)
Always document ownership in function comments when not obvious
Prefer caller-allocated memory for simple structures
Use paired alloc/free functions for complex objects with internal state
3. System Function Calls
Use uppercase wrappers for system functions instead of standard library functions:
Use
CALLOC()instead ofcalloc()Use
FREE()instead offree()Use
REALLOC()instead ofrealloc()Use
STRDUP()instead ofstrdup()Use
MEMZERO()instead ofmemzero()Use
MEMNDUP()instead ofmemndup()
These wrappers provide consistent error handling and memory management across the codebase:
// Good
void *ptr = CALLOC(1, sizeof(struct my_struct));
FREE(ptr);
// Avoid
void *ptr = calloc(1, sizeof(struct my_struct));
free(ptr);4. Event Loop and Concurrency
OWM uses libev for a single-threaded event loop. All operations run in the main event loop thread, so you don't need to worry about thread synchronization for normal OWM code:
// All operations run in the event loop
void
my_callback(EV_P_ ev_timer *w, int revents)
{
// This runs in the main event loop thread
// No locks needed - everything is already synchronized
do_some_work();
}
// Schedule work using timers, io watchers, etc.
ev_timer_init(&my_timer, my_callback, delay, 0.0);
ev_timer_start(EV_DEFAULT, &my_timer);Important:
All OWM code runs in the main event loop
Operations are sequential within the event loop
Prefer
osw_timerfor delayed/periodic operations in OSW-related codeUse libev watchers for I/O events
5. Testing
Write separate _init and _attach functions to allow unit testing. The _init function can be tested independently without framework dependencies. Only use _init in unit tests; _attach requires the full framework (libev, observers, etc.) and cannot be used in OSW_UT.
Write tests for new features:
OSW_UT(my_feature_basic)
{
// Arrange
struct my_feature f;
my_feature_init(&f); // Test init without framework dependencies
// Act
const bool result = my_feature_do_something(&f);
// Assert
OSW_UT_EXPECT(result == true);
// Note: no free needed - struct is on stack
}Benefits of separating init and attach:
_initfunction can be tested in unit tests without framework dependenciesClear separation between data initialization and framework integration
Easier to test module logic in isolation
Summary
Adding features: Follow the 8-layer pattern (OVSDB → ow_ovsdb → ow_conf → osw_drv → osw_confsync → hostapd → tests), ensure confsync state reporting is correct, reference
proxy_arp/dgaf_disablepatterns andow_radar_next_channel.c.Working with modules: Use
OSW_MODULEmacro, separate_init/_attachfor testability, load dependencies withOSW_MODULE_LOAD, add unit tests in source files.Submitting: Create PR with "Why this approach" and "Why not alternatives" sections, verify unit tests pass and confsync is happy (no watchdog triggers).
The modular architecture, clear patterns, and comprehensive examples make OWM extensible and maintainable for new contributors.