diff --git a/qosmngr/files/common/lib/qos/classify.sh b/qosmngr/files/common/lib/qos/classify.sh index 862628291..cf3578432 100755 --- a/qosmngr/files/common/lib/qos/classify.sh +++ b/qosmngr/files/common/lib/qos/classify.sh @@ -112,10 +112,10 @@ configure_classify() { sh /tmp/qos/classify.ip6tables sh /tmp/qos/classify.iprule - # Sync DSCP/PCP mappings to mapcontroller for WiFi QoS - if [ -f /lib/qos/mapcontroller_sync.sh ]; then - . /lib/qos/mapcontroller_sync.sh - sync_mapcontroller_qos + # Sync DSCP/PCP mappings to WiFi QoS Map on local AP interfaces + if [ -f /lib/qos/wifi_qos.sh ]; then + . /lib/qos/wifi_qos.sh + sync_wifi_qos fi } diff --git a/qosmngr/files/common/lib/qos/mapcontroller_sync.sh b/qosmngr/files/common/lib/qos/mapcontroller_sync.sh deleted file mode 100644 index f881bb767..000000000 --- a/qosmngr/files/common/lib/qos/mapcontroller_sync.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/sh -# Sync DSCP/PCP mappings from qosmngr classify rules to mapcontroller -# for WiFi QoS (EasyMesh QoS Map) integration. -# -# When device priority classify rules set dscp_mark/pcp_mark, this script -# generates a mapcontroller qos_rule (type dscp_pcp) so all EasyMesh agents -# apply the correct DSCP-to-WMM AC mapping on WiFi interfaces. - -MAPCNTLR_CFG="mapcontroller" -AUTO_RULE_ID="1000" -AUTO_RULE_NAME="qos_auto_dscp_pcp" - -# Collect dscp_mark/pcp_mark pairs from enabled classify sections. -# Stores results in /tmp/qos/dscp_pcp_map (sorted by order, first wins). -_collect_dscp_pcp_mapping() { - local cid="$1" - local is_enable dscp_mark pcp_mark c_order - - config_get is_enable "$cid" "enable" "1" - [ "$is_enable" = "0" ] && return - - config_get dscp_mark "$cid" "dscp_mark" - config_get pcp_mark "$cid" "pcp_mark" - - # Skip rules without both dscp_mark and pcp_mark - [ -z "$dscp_mark" ] && return - [ -z "$pcp_mark" ] && return - - # Skip negative values (means "no change" in TR-181) - [ "$dscp_mark" -lt 0 ] 2>/dev/null && return - [ "$pcp_mark" -lt 0 ] 2>/dev/null && return - - # Validate ranges - [ "$dscp_mark" -gt 63 ] 2>/dev/null && return - [ "$pcp_mark" -gt 7 ] 2>/dev/null && return - - config_get c_order "$cid" "order" "999" - - # Format: order dscp pcp (sorted later, first occurrence per dscp wins) - echo "$c_order $dscp_mark $pcp_mark" >> /tmp/qos/dscp_pcp_map -} - -# Remove previously auto-generated qos_rule sections from mapcontroller config -_cleanup_auto_rules() { - local section auto_gen - - # Find all qos_rule sections with auto_generated='1' - for section in $(uci -q show "$MAPCNTLR_CFG" | grep '\.auto_generated=.1.' | sed "s/\.auto_generated=.*//;s/${MAPCNTLR_CFG}\.//"); do - uci -q delete "${MAPCNTLR_CFG}.${section}" - done -} - -# Main sync function - called after configure_classify() -sync_mapcontroller_qos() { - # Guard: skip if mapcontroller/EasyMesh is not installed - [ -f "/etc/config/$MAPCNTLR_CFG" ] || return 0 - - rm -f /tmp/qos/dscp_pcp_map - - # Collect mappings from all classify rules - config_load qos - config_foreach _collect_dscp_pcp_mapping classify - - # Remove old auto-generated rules - _cleanup_auto_rules - - # If no mappings found, just commit cleanup and signal - if [ ! -s /tmp/qos/dscp_pcp_map ]; then - uci -q commit "$MAPCNTLR_CFG" - killall -HUP mapcontroller 2>/dev/null - return 0 - fi - - # Deduplicate: sort by order (ascending), keep first occurrence per DSCP - sort -n -k1 /tmp/qos/dscp_pcp_map | awk '!seen[$2]++ {print $2 "," $3}' > /tmp/qos/dscp_pcp_list - - if [ ! -s /tmp/qos/dscp_pcp_list ]; then - uci -q commit "$MAPCNTLR_CFG" - killall -HUP mapcontroller 2>/dev/null - return 0 - fi - - # Ensure QoS is enabled in mapcontroller - uci -q set "${MAPCNTLR_CFG}.qos=qos" - uci -q set "${MAPCNTLR_CFG}.qos.enabled=1" - - # Create auto-generated qos_rule - uci -q set "${MAPCNTLR_CFG}.${AUTO_RULE_NAME}=qos_rule" - uci -q set "${MAPCNTLR_CFG}.${AUTO_RULE_NAME}.id=${AUTO_RULE_ID}" - uci -q set "${MAPCNTLR_CFG}.${AUTO_RULE_NAME}.type=dscp_pcp" - uci -q set "${MAPCNTLR_CFG}.${AUTO_RULE_NAME}.precedence=200" - uci -q set "${MAPCNTLR_CFG}.${AUTO_RULE_NAME}.output=0" - uci -q set "${MAPCNTLR_CFG}.${AUTO_RULE_NAME}.always_match=1" - uci -q set "${MAPCNTLR_CFG}.${AUTO_RULE_NAME}.auto_generated=1" - - # Clear any existing dscp_pcp list entries and add new ones - uci -q delete "${MAPCNTLR_CFG}.${AUTO_RULE_NAME}.dscp_pcp" - while read -r mapping; do - uci -q add_list "${MAPCNTLR_CFG}.${AUTO_RULE_NAME}.dscp_pcp=${mapping}" - done < /tmp/qos/dscp_pcp_list - - # Commit and signal mapcontroller to reload - uci -q commit "$MAPCNTLR_CFG" - killall -HUP mapcontroller 2>/dev/null - - rm -f /tmp/qos/dscp_pcp_map /tmp/qos/dscp_pcp_list -} diff --git a/qosmngr/files/common/lib/qos/wifi_qos.sh b/qosmngr/files/common/lib/qos/wifi_qos.sh new file mode 100644 index 000000000..81d0811ab --- /dev/null +++ b/qosmngr/files/common/lib/qos/wifi_qos.sh @@ -0,0 +1,186 @@ +#!/bin/sh +# Sync DSCP/PCP mappings from qosmngr classify rules to WiFi QoS Map. +# +# When device priority or QoS profile classify rules set dscp_mark/pcp_mark, +# this script builds an IEEE 802.11 QoS Map and applies it directly to all +# local WiFi AP interfaces via ubus (wifi.ap.* set_qos_map). +# +# QoS Map format (IEEE 802.11-2020, 9.4.2.93): +# [exception pairs...] [8 UP ranges] +# Exception pair: DSCP_value, User_Priority (2 bytes each) +# UP range: DSCP_low, DSCP_high (2 bytes each, 16 bytes total for UP 0-7) +# If DSCP_low=255 and DSCP_high=255, that UP is unused. + +# Collect dscp_mark/pcp_mark pairs from enabled classify sections. +_collect_dscp_pcp_mapping() { + local cid="$1" + local is_enable dscp_mark pcp_mark c_order + + config_get is_enable "$cid" "enable" "1" + [ "$is_enable" = "0" ] && return + + config_get dscp_mark "$cid" "dscp_mark" + config_get pcp_mark "$cid" "pcp_mark" + + [ -z "$dscp_mark" ] && return + [ -z "$pcp_mark" ] && return + + # Skip negative values (means "no change" in TR-181) + [ "$dscp_mark" -lt 0 ] 2>/dev/null && return + [ "$pcp_mark" -lt 0 ] 2>/dev/null && return + + # Validate ranges + [ "$dscp_mark" -gt 63 ] 2>/dev/null && return + [ "$pcp_mark" -gt 7 ] 2>/dev/null && return + + config_get c_order "$cid" "order" "999" + + echo "$c_order $dscp_mark $pcp_mark" >> /tmp/qos/dscp_pcp_map +} + +# Build IEEE 802.11 QoS Map byte array from DSCP->PCP mappings. +# Input: /tmp/qos/dscp_pcp_list (lines of "dscp,pcp") +# Output: prints space-separated byte array for ubus json +_build_qos_map() { + local dscp pcp default_up i + + # Initialize 64-entry DSCP->PCP table with defaults (IP precedence: dscp/8) + # Using a file since shell arrays aren't portable + i=0 + while [ $i -lt 64 ]; do + echo "$i $((i / 8))" + i=$((i + 1)) + done > /tmp/qos/dscp_pcp_full + + # Override with our explicit mappings + while IFS=',' read -r dscp pcp; do + sed -i "s/^${dscp} .*/${dscp} ${pcp}/" /tmp/qos/dscp_pcp_full + done < /tmp/qos/dscp_pcp_list + + # For each PCP (0-7), find the largest contiguous range of DSCPs + # and collect exceptions (DSCPs outside the range) + local exc="" + local ranges="" + + i=0 + while [ $i -lt 8 ]; do + # Get all DSCPs mapped to this PCP, sorted numerically + local dscp_list + dscp_list=$(awk -v p="$i" '$2 == p {print $1}' /tmp/qos/dscp_pcp_full | sort -n) + + if [ -z "$dscp_list" ]; then + # No DSCPs map to this PCP - mark range as unused + ranges="$ranges 255 255" + i=$((i + 1)) + continue + fi + + # Find largest contiguous range + local best_start="" best_end="" best_len=0 + local cur_start="" cur_end="" cur_len=0 + local prev=-2 + + for d in $dscp_list; do + if [ $((prev + 1)) -eq "$d" ]; then + cur_end=$d + cur_len=$((cur_len + 1)) + else + cur_start=$d + cur_end=$d + cur_len=1 + fi + if [ $cur_len -gt $best_len ]; then + best_start=$cur_start + best_end=$cur_end + best_len=$cur_len + fi + prev=$d + done + + ranges="$ranges $best_start $best_end" + + # Exceptions: DSCPs outside the best range for this PCP + for d in $dscp_list; do + if [ "$d" -lt "$best_start" ] || [ "$d" -gt "$best_end" ]; then + exc="$exc $d $i" + fi + done + + i=$((i + 1)) + done + + # Output: exceptions first, then 8 UP ranges (16 bytes) + echo "$exc $ranges" | tr -s ' ' | sed 's/^ //' + + rm -f /tmp/qos/dscp_pcp_full +} + +# Apply QoS Map to all local WiFi AP interfaces via ubus +_apply_qos_map_to_aps() { + local qos_map_bytes="$1" + local ap json_array + + # Build JSON array string from space-separated bytes + json_array=$(echo "$qos_map_bytes" | tr ' ' ',' | sed 's/^/[/;s/$/]/') + + for ap in $(ubus list 2>/dev/null | grep '^wifi\.ap\.'); do + ubus call "$ap" set_qos_map "{\"set\": $json_array}" 2>/dev/null + done +} + +# Send QoS Map Configuration frame to all connected STAs +_notify_stas() { + local ap sta_list sta + + for ap in $(ubus list 2>/dev/null | grep '^wifi\.ap\.'); do + # Get list of connected STAs + sta_list=$(ubus call "$ap" stations 2>/dev/null | \ + jsonfilter -e '@.stations[*].macaddr' 2>/dev/null) + for sta in $sta_list; do + ubus call "$ap" send_qos_map_conf "{\"sta\": \"$sta\"}" 2>/dev/null + done + done +} + +# Clear QoS Map from all WiFi AP interfaces +_clear_qos_map() { + local ap + + for ap in $(ubus list 2>/dev/null | grep '^wifi\.ap\.'); do + ubus call "$ap" set_qos_map '{"set": []}' 2>/dev/null + done +} + +# Main sync function - called after configure_classify() +sync_wifi_qos() { + rm -f /tmp/qos/dscp_pcp_map + + # Collect mappings from all classify rules + config_load qos + config_foreach _collect_dscp_pcp_mapping classify + + # If no mappings found, clear QoS Map + if [ ! -s /tmp/qos/dscp_pcp_map ]; then + _clear_qos_map + rm -f /tmp/qos/dscp_pcp_map /tmp/qos/dscp_pcp_list + return 0 + fi + + # Deduplicate: sort by order (ascending), keep first occurrence per DSCP + sort -n -k1 /tmp/qos/dscp_pcp_map | awk '!seen[$2]++ {print $2 "," $3}' > /tmp/qos/dscp_pcp_list + + if [ ! -s /tmp/qos/dscp_pcp_list ]; then + _clear_qos_map + rm -f /tmp/qos/dscp_pcp_map /tmp/qos/dscp_pcp_list + return 0 + fi + + # Build and apply QoS Map + local qos_map + qos_map=$(_build_qos_map) + + _apply_qos_map_to_aps "$qos_map" + _notify_stas + + rm -f /tmp/qos/dscp_pcp_map /tmp/qos/dscp_pcp_list +}