Contributing to OWM

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, &notified, 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_Config table

  • Mutator pattern - Pushes configuration through ow_conf to osw_drv

  • Separate OSW module - Implements logic as an independent module using OSW_MODULE macro

  • State 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 base policy pointer

  • Create and free functions

  • recalc_cb that modifies candidate list preferences

  • sigusr1_dump_cb for debugging via SIGUSR1 signal

  • Uses CALLOC, FREE, ASSERT from memutil.h

  • Gets 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

  1. Registration (Build Time):

    MODULE(osw_module_example, init_cb, fini_cb)

    Module is registered with the framework.

  2. Initialization (Module Load):

    static void *osw_module_example_load_cb(void) { // Initialize module // Register observers // Return module data return &module; }
  3. 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 _init functions independently (no framework dependencies)

  • Don't test _attach in unit tests (requires full framework)

  • Use stack allocation or static structures

  • See src/lib/ow/src/ow_radar_next_channel.c for 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_snr

3. 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 --fix

Run 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: #123

4. 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 frees

Pattern 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 of calloc()

  • Use FREE() instead of free()

  • Use REALLOC() instead of realloc()

  • Use STRDUP() instead of strdup()

  • Use MEMZERO() instead of memzero()

  • Use MEMNDUP() instead of memndup()

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_timer for delayed/periodic operations in OSW-related code

  • Use 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:

  • _init function can be tested in unit tests without framework dependencies

  • Clear 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_disable patterns and ow_radar_next_channel.c.

  • Working with modules: Use OSW_MODULE macro, separate _init/_attach for testability, load dependencies with OSW_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.