Files
wlan-ap/patches/0089-MorseMicro-iwinfo.patch
ian77_chen f39339564a mediatek-sdk: Support HaLow on EAP112
1. porting MorseMicro HaLow driver to support HaLow on EAP112
2. Only support FCC regulation because of hardware limitation
3. Add /etc/init.d/halow-gpio-reset to initialize HaLow chip in early stage
4. Add /etc/uci-defaults/aaa-fix-phy0-to-morse to correct the default uci for HaLow radio.

Signed-off-by: Ian Chen <ian77_chen@accton.com>
2025-05-12 10:45:38 +02:00

1996 lines
55 KiB
Diff

From 67adfbc5b4831ecb62a169370c102e0939ae884e Mon Sep 17 00:00:00 2001
From: ian77_chen <ian77_chen@accton.com>
Date: Mon, 10 Mar 2025 15:21:57 +0800
Subject: [PATCH] patch for Morse Micro iwinfo
---
.../utils/iwinfo/patches/MMiwinfo.patch | 1976 +++++++++++++++++
1 file changed, 1976 insertions(+)
create mode 100644 package/network/utils/iwinfo/patches/MMiwinfo.patch
diff --git a/package/network/utils/iwinfo/patches/MMiwinfo.patch b/package/network/utils/iwinfo/patches/MMiwinfo.patch
new file mode 100644
index 0000000000..c10d4bf881
--- /dev/null
+++ b/package/network/utils/iwinfo/patches/MMiwinfo.patch
@@ -0,0 +1,1976 @@
+diff --git a/.gitignore b/.gitignore
+new file mode 100644
+index 0000000..7f4922f
+--- /dev/null
++++ b/.gitignore
+@@ -0,0 +1,17 @@
++iwinfo_cli.o
++iwinfo_lib.o
++iwinfo_lua.o
++iwinfo_nl80211.o
++iwinfo_utils.o
++iwinfo_wext_scan.o
++iwinfo_wext.o
++iwinfo.so
++libiwinfo.so
++libiwinfo.so.0
++iwinfo
++
++*.so
++*.so.*
++*.o
++
++.vscode/
+diff --git a/Makefile b/Makefile
+index adb9e73..fac0abf 100644
+--- a/Makefile
++++ b/Makefile
+@@ -37,6 +37,7 @@ ifneq ($(filter nl80211,$(IWINFO_BACKENDS)),)
+ IWINFO_CLI_LDFLAGS += -lnl-tiny
+ IWINFO_LIB_LDFLAGS += -lnl-tiny
+ IWINFO_LIB_OBJ += iwinfo_nl80211.o
++ IWINFO_LIB_OBJ += iwinfo_morsecli.o dot11ah_channel.o
+ endif
+
+
+diff --git a/devices.txt b/devices.txt
+index eded184..397a1cf 100644
+--- a/devices.txt
++++ b/devices.txt
+@@ -192,6 +192,9 @@
+ 0x02d0 0xa9a6 0x0000 0x0000 0 0 "Cypress" "CYW43455"
+ 0x02d0 0x4345 0x0000 0x0000 0 0 "Cypress" "CYW43455"
+ 0x1ae9 0x0310 0x1ae9 0x0000 0 0 "Wilocity" "Wil6210"
++0x325B 0x0206 0x0000 0x0000 0 0 "Morse Micro" "HaLow WiFi"
++0x325B 0x0306 0x0000 0x0000 0 0 "Morse Micro" "HaLow WiFi"
++
+
+ # USB devices
+ # 0x0000 | 0x0000 | vendor id | product id | ...
+@@ -263,3 +266,4 @@
+ "ralink,rt3883-wmac" 0 0 "Ralink" "Rt3883"
+ "ralink,rt5350-wmac" 0 0 "Ralink" "Rt5350"
+ "ralink,rt7620-wmac" 0 0 "MediaTek" "MT7620"
++"morse,mm610x-spi" 0 0 "Morse Micro" "HaLow WiFi"
+diff --git a/dot11ah_channel.c b/dot11ah_channel.c
+new file mode 100644
+index 0000000..24dfb0e
+--- /dev/null
++++ b/dot11ah_channel.c
+@@ -0,0 +1,444 @@
++/*
++ * Copyright 2022 Morse Micro
++ *
++ * The iwinfo library is free software: you can redistribute it and/or
++ * modify it under the terms of the GNU General Public License version 2
++ * as published by the Free Software Foundation.
++ *
++ * The iwinfo library is distributed in the hope that it will be useful,
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
++ * See the GNU General Public License for more details.
++ *
++ * You should have received a copy of the GNU General Public License along
++ * with the iwinfo library. If not, see http://www.gnu.org/licenses/.
++*/
++
++#include <stdio.h>
++#include <string.h>
++#include <stdlib.h>
++
++#include "dot11ah_channel.h"
++
++static const channel_to_halow_freq_t kNullAhValue = {0, 0, 0, 0};
++
++static const country_channel_map_t us_channel_map = {
++ .country = "US",
++ .num_mapped_channels = 48,
++ .ah_vals = {
++ /* 1Mhz */
++ {132, 1, 902.5, 0},
++ {136, 3, 903.5, 0},
++ {36, 5, 904.5, 0},
++ {40, 7, 905.5, 0},
++ {44, 9, 906.5, 0},
++ {48, 11, 907.5, 0},
++ {52, 13, 908.5, 0},
++ {56, 15, 909.5, 0},
++ {60, 17, 910.5, 0},
++ {64, 19, 911.5, 0},
++ {100, 21, 912.5, 0},
++ {104, 23, 913.5, 0},
++ {108, 25, 914.5, 0},
++ {112, 27, 915.5, 0},
++ {116, 29, 916.5, 0},
++ {120, 31, 917.5, 0},
++ {124, 33, 918.5, 0},
++ {128, 35, 919.5, 0},
++ {149, 37, 920.5, 0},
++ {153, 39, 921.5, 0},
++ {157, 41, 922.5, 0},
++ {161, 43, 923.5, 0},
++ {165, 45, 924.5, 0},
++ {169, 47, 925.5, 0},
++ {173, 49, 926.5, 0},
++ {177, 51, 927.5, 0},
++ /* 2MHz */
++ {134, 2, 903, 1},
++ {38, 6, 905, 1},
++ {46, 10, 907, 1},
++ {54, 14, 909, 1},
++ {62, 18, 911, 1},
++ {102, 22, 913, 1},
++ {110, 26, 915, 1},
++ {118, 30, 917, 1},
++ {126, 34, 919, 1},
++ {151, 38, 921, 1},
++ {159, 42, 923, 1},
++ {167, 46, 925, 1},
++ {175, 50, 927, 1},
++ /* 4MHz */
++ {42, 8, 906, 2},
++ {58, 16, 910, 2},
++ {106, 24, 914, 2},
++ {122, 32, 918, 2},
++ {155, 40, 922, 2},
++ {171, 48, 926, 2},
++ /* 8MHz */
++ {50, 12, 908, 3},
++ {114, 28, 916, 3},
++ {163, 44, 924, 3},
++ }
++};
++
++static const country_channel_map_t au_channel_map = {
++ .country = "AU",
++ .num_mapped_channels = 23,
++ .ah_vals = {
++ /* 1Mhz */
++ {112, 27, 915.5, 0},
++ {116, 29, 916.5, 0},
++ {120, 31, 917.5, 0},
++ {124, 33, 918.5, 0},
++ {128, 35, 919.5, 0},
++ {149, 37, 920.5, 0},
++ {153, 39, 921.5, 0},
++ {157, 41, 922.5, 0},
++ {161, 43, 923.5, 0},
++ {165, 45, 924.5, 0},
++ {169, 47, 925.5, 0},
++ {173, 49, 926.5, 0},
++ {177, 51, 927.5, 0},
++ /* 2Mhz */
++ {118, 30, 917, 1},
++ {126, 34, 919, 1},
++ {151, 38, 921, 1},
++ {159, 42, 923, 1},
++ {167, 46, 925, 1},
++ {175, 50, 927, 1},
++ /* 4 Mhz */
++ {122, 32, 918, 2},
++ {155, 40, 922, 2},
++ {171, 48, 926, 2},
++ /* 8 Mhz */
++ {163, 44, 924, 3}
++ }
++};
++
++static const country_channel_map_t nz_channel_map = {
++ .country = "NZ",
++ .num_mapped_channels = 23,
++ .ah_vals = {
++ /* 1Mhz */
++ {112, 27, 915.5, 0},
++ {116, 29, 916.5, 0},
++ {120, 31, 917.5, 0},
++ {124, 33, 918.5, 0},
++ {128, 35, 919.5, 0},
++ {149, 37, 920.5, 0},
++ {153, 39, 921.5, 0},
++ {157, 41, 922.5, 0},
++ {161, 43, 923.5, 0},
++ {165, 45, 924.5, 0},
++ {169, 47, 925.5, 0},
++ {173, 49, 926.5, 0},
++ {177, 51, 927.5, 0},
++ /* 2Mhz */
++ {118, 30, 917, 1},
++ {126, 34, 919, 1},
++ {151, 38, 921, 1},
++ {159, 42, 923, 1},
++ {167, 46, 925, 1},
++ {175, 50, 927, 1},
++ /* 4 Mhz */
++ {122, 32, 918, 2},
++ {155, 40, 922, 2},
++ {171, 48, 926, 2},
++ /* 8 Mhz */
++ {163, 44, 924, 3}
++ }
++};
++
++static const country_channel_map_t eu_channel_map = {
++ .country = "EU",
++ .num_mapped_channels = 8,
++ .ah_vals = {
++ /* 1Mhz */
++ {132, 1, 863.5, 0},
++ {136, 3, 864.5, 0},
++ {36, 5, 865.5, 0},
++ {40, 7, 866.5, 0},
++ {44, 9, 867.5, 0},
++ {120, 31, 916.9, 0},
++ {124, 33, 917.9, 0},
++ {128, 35, 918.9, 0},
++ }
++};
++
++static const country_channel_map_t in_channel_map = {
++ .country = "IN",
++ .num_mapped_channels = 3,
++ .ah_vals = {
++ /* 1Mhz */
++ {36, 5, 865.5, 0},
++ {40, 7, 866.5, 0},
++ {44, 9, 867.5, 0},
++ }
++};
++
++static const country_channel_map_t jp_channel_map = {
++ .country = "JP",
++ .num_mapped_channels = 11,
++ .ah_vals = {
++ /* 1 Mhz */
++ {36, 13, 923, 0},
++ {40, 15, 924, 0},
++ {44, 17, 925, 0},
++ {48, 19, 926, 0},
++ {64, 21, 927, 0},
++ /* 2Mhz */
++ {38, 2, 923.5, 1},
++ {46, 6, 925.5, 1},
++ {54, 4, 924.5, 1},
++ {62, 8, 926.5, 1},
++ /* 4Mhz */
++ {42, 36, 924.5, 2},
++ {58, 38, 925.5, 2},
++ }
++};
++
++static const country_channel_map_t kr_channel_map = {
++ .country = "KR",
++ .num_mapped_channels = 10,
++ .ah_vals = {
++ /* 1 Mhz */
++ {132, 1, 918, 0},
++ {136, 3, 919, 0},
++ {36, 5, 920, 0},
++ {40, 7, 921, 0},
++ {44, 9, 922, 0},
++ {48, 11, 923, 0},
++ /* 2Mhz */
++ {134, 2, 918.5, 1},
++ {38, 6, 920.5, 1},
++ {46, 10, 922.5, 1},
++ /* 4Mhz */
++ {42, 8, 921.5, 2},
++ }
++};
++
++static const country_channel_map_t sg_channel_map = {
++ .country = "SG",
++ .num_mapped_channels = 12,
++ .ah_vals = {
++ /* 1 Mhz */
++ {40, 7, 866.5, 0},
++ {44, 9, 867.5, 0},
++ {48, 11, 868.5, 0},
++ {149, 37, 920.5, 0},
++ {153, 39, 921.5, 0},
++ {157, 41, 922.5, 0},
++ {161, 43, 923.5, 0},
++ {165, 45, 924.5, 0},
++ /* 2Mhz */
++ {46, 10, 868, 1},
++ {151, 38, 921, 1},
++ {159, 42, 923, 1},
++ /* 4Mhz */
++ {155, 40, 922, 2}
++ }
++};
++
++static const country_channel_map_t channel_map_terminate = {
++ .country = {0,0,0},
++ .num_mapped_channels = 0,
++ .ah_vals = {}
++};
++
++static const country_channel_map_t *mapped_channel[] = {
++ &us_channel_map,
++ &au_channel_map,
++ &nz_channel_map,
++ &eu_channel_map,
++ &in_channel_map,
++ &jp_channel_map,
++ &kr_channel_map,
++ &sg_channel_map,
++ &channel_map_terminate
++};
++
++#define CHANNEL_MAP_SIZE (sizeof(mapped_channel) / sizeof(*mapped_channel))
++
++static void morse_get_country(country_channel_map_t *halow_vals)
++{
++ FILE *country_parameter;
++
++ country_parameter = fopen("/sys/module/morse/parameters/country", "r");
++ fscanf(country_parameter, "%2s", halow_vals->country);
++ fclose(country_parameter);
++}
++
++country_channel_map_t *set_s1g_channel_map(void)
++{
++ country_channel_map_t halow_vals;
++
++ morse_get_country(&halow_vals);
++ if (strlen(halow_vals.country) != 0)
++ {
++ for (int i = 0; i < CHANNEL_MAP_SIZE; i++)
++ {
++ if (!strncmp(halow_vals.country, mapped_channel[i]->country, strlen(mapped_channel[i]->country)))
++ {
++ return mapped_channel[i];
++ }
++ }
++ }
++
++ return NULL;
++}
++
++
++channel_to_halow_freq_t *get_s1g(country_channel_map_t *map, int channel)
++{
++ if(map == NULL)
++ return &kNullAhValue;
++
++ for(int i=0;i<map->num_mapped_channels;i++)
++ {
++ if(map->ah_vals[i].channel==channel)
++ return &map->ah_vals[i];
++ }
++ return &kNullAhValue;
++}
++
++float get_freq(country_channel_map_t *map, int channel)
++{
++ if(map == NULL)
++ return 0;
++
++ for(int i=0; i< map->num_mapped_channels; i++)
++ {
++ if(map->ah_vals[i].halow_channel==channel)
++ return map->ah_vals[i].halow_freq;
++ }
++
++ return 0;
++
++}
++
++int s1g_rate(int fiveG_rate, int frq_mhz)
++{
++ int sc_map_5g[][2] = {
++ {20 , 52},
++ {40 , 108},
++ {80 , 234},
++ {160 , 468}};
++ int sc_map_s1g[][2] = {
++ {20 , 24},
++ {40 , 52},
++ {80 , 108},
++ {160 , 234}};
++
++ int index=-1;
++ for (int i = 0; i < sizeof(sc_map_5g) / sizeof(int[2]); i++)
++ {
++ if(sc_map_5g[i][0] == frq_mhz)
++ {
++ index = i;
++ break;
++ }
++ }
++ int scale = 20; // for s1g we need to scale the reported shim layer values. if not exist approximate.
++ if (index != -1)
++ {
++ scale = 10 * sc_map_5g[index][1] / sc_map_s1g[index][1];
++ }
++ return fiveG_rate / scale;
++}
++
++int s1g_freq2channel(country_channel_map_t *map,int freq)//frq in khz
++{
++ if(map == NULL)
++ return 0;
++
++ for(int i=0; i< map->num_mapped_channels; i++)
++ {
++ if ((int)(map->ah_vals[i].halow_freq * 1000) == freq)
++ return map->ah_vals[i].halow_channel;
++ }
++
++ return 0;
++}
++
++int s1g_chan2bw(country_channel_map_t *map,int channel)//bw in MHz
++{
++ if(map == NULL)
++ return 0;
++
++ for(int i=0; i< map->num_mapped_channels; i++)
++ {
++ if ((map->ah_vals[i].halow_channel) == channel)
++ return map->ah_vals[i].bw;
++ }
++
++ return 0;
++}
++
++const country_channel_map_t** s1g_mapped_channel()
++{
++ return mapped_channel;
++}
++
++void s1g_get_country(char *buf)
++{
++ country_channel_map_t halow_vals;
++ morse_get_country(&halow_vals);
++ memcpy(buf,halow_vals.country,2);
++}
++
++
++// returns true if this raw is the selected rate.
++int mmrc_table_active_raw(const char* line)
++{
++ //check if it has MHz and MCS and GI keywords.
++ if (strstr(line, "MCS") == NULL)
++ return 0;
++ if (strstr(line, "MHz") == NULL)
++ return 0;
++ if ((strstr(line, "SGI") == NULL) && (strstr(line, "LGI") == NULL))
++ return 0;
++ //start search from 17th character.
++ if (strstr(line+17,"A"))
++ return 1;
++return 0;
++
++}
++//returns the avg tp from the selected line.
++int get_mmrc_table_raw_throughput_avg(const char* line)
++{
++ float tp_avg;
++ sscanf(line + 55, "%f", &tp_avg);
++ return tp_avg*1000;
++}
++
++int get_mmrc_throughput(const char* phyname)
++{
++ FILE *file;
++ char * line = NULL;
++ size_t len = 0;
++ ssize_t read;
++ char table_path[64];
++ int rate_kbps=-1;
++
++ sprintf (table_path,"/sys/kernel/debug/ieee80211/%s/morse/mmrc_table",phyname);
++ file = fopen(table_path, "r");
++ if (file == NULL)
++ {
++ return -1;
++ }
++
++ while ((read = getline(&line, &len, file)) != -1) {
++ if(mmrc_table_active_raw(line))
++ {
++ rate_kbps = get_mmrc_table_raw_throughput_avg(line);
++ break;
++ }
++ }
++ fclose(file);
++ if (line)
++ free(line);
++
++ if(rate_kbps == 0)
++ rate_kbps+=1; //to make sure that assoc list doesn't show "unknown" when there's no traffic.
++ return rate_kbps;
++}
+\ No newline at end of file
+diff --git a/dot11ah_channel.h b/dot11ah_channel.h
+new file mode 100644
+index 0000000..676906d
+--- /dev/null
++++ b/dot11ah_channel.h
+@@ -0,0 +1,105 @@
++/*
++ * Copyright 2022 Morse Micro
++ *
++ * The iwinfo library is free software: you can redistribute it and/or
++ * modify it under the terms of the GNU General Public License version 2
++ * as published by the Free Software Foundation.
++ *
++ * The iwinfo library is distributed in the hope that it will be useful,
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
++ * See the GNU General Public License for more details.
++ *
++ * You should have received a copy of the GNU General Public License along
++ * with the iwinfo library. If not, see http://www.gnu.org/licenses/.
++*/
++
++#ifndef __DOT11AH_CHANNELS__
++#define __DOT11AH_CHANNELS__
++
++typedef struct {
++ /*5G channel*/
++ int channel;
++ /*80211ah channel*/
++ int halow_channel;
++ /*80211ah freq*/
++ float halow_freq;
++ /*80211ah bandwidth*/
++ int bw;
++} channel_to_halow_freq_t;
++
++typedef struct {
++ char country[3];
++ int num_mapped_channels;
++ channel_to_halow_freq_t ah_vals[];
++} country_channel_map_t;
++
++
++/**
++ * @brief gets the channel 5g to s1g channel map for a given region.
++ * country setting is read from /sys/module/morse/parameters/country
++ */
++country_channel_map_t *set_s1g_channel_map(void);
++
++
++/**
++ * retrieves the map entry for a given 5g channel
++ * @param map a map of 5g to s1g channels retrieved by set_s1g_channel_map
++ * @param channel a 5g channel value
++ * @return The entry in the map corresponding to the given 5g channel and configured region
++ */
++channel_to_halow_freq_t *get_s1g(country_channel_map_t *map, int channel);
++
++/**
++ * retrieves the s1g frequency for a given s1g channel
++ * @param map a map of 5g to s1g channels retrieved by set_s1g_channel_map
++ * @param channel an s1g channel value
++ * @return a floating point value corresponding to the s1g frequency, in mhz, for the given channel.
++ */
++float get_freq(country_channel_map_t *map, int channel);
++
++/**
++ * a conversion helper for calculating the correct s1g mcs data rate for a given bandwidth and existing 5g mcs rate
++ * @param fiveG_rate a 5g mcs data rate value
++ * @param frq_mhz the 5g bandwidth corresponding to the 5g rate
++ * @return the s1g mcs data rate
++ */
++int s1g_rate(int fiveG_rate, int frq_mhz);
++
++/**
++ * converts s1g frequency to s1g channel
++ * @param map a map of 5g to s1g channels retrieved by set_s1g_channel_map
++ * @param freq s1g frequency in KHz
++ * @return s1g channel
++ */
++int s1g_freq2channel(country_channel_map_t *map,int freq);
++
++/**
++ * converts s1g channel to s1g bandwidth
++ * @param map a map of 5g to s1g channels retrieved by set_s1g_channel_map
++ * @param channel s1g channel
++ * @return s1g channel
++ */
++int s1g_chan2bw(country_channel_map_t *map,int channel);
++
++/**
++ * gives a pointer to the country separated channels map
++ * @return a pointer to the country separated channels map.
++ */
++const country_channel_map_t** s1g_mapped_channel();
++
++
++/**
++ * returns the currently set country.
++ * @param buf a pointer to the result buffer.
++ */
++void s1g_get_country(char *buf);
++
++/**
++ * gets mmrc average throughput from the mmrc_table.
++ * @param phyname phyname of the halow device.
++ * @return average throughput in kbps
++ */
++int get_mmrc_throughput(const char* phyname);
++
++#endif /* __DOT11AH_CHANNELS__ */
+diff --git a/include/iwinfo.h b/include/iwinfo.h
+index b50de69..0a9f157 100644
+--- a/include/iwinfo.h
++++ b/include/iwinfo.h
+@@ -31,6 +31,7 @@ enum iwinfo_80211 {
+ IWINFO_80211_AC,
+ IWINFO_80211_AD,
+ IWINFO_80211_AX,
++ IWINFO_80211_AH,
+
+ /* keep last */
+ IWINFO_80211_COUNT
+@@ -43,6 +44,9 @@ enum iwinfo_80211 {
+ #define IWINFO_80211_AC (1 << IWINFO_80211_AC)
+ #define IWINFO_80211_AD (1 << IWINFO_80211_AD)
+ #define IWINFO_80211_AX (1 << IWINFO_80211_AX)
++#define IWINFO_80211_AH (1 << IWINFO_80211_AH)
++
++#define S1G_CHAN_WIDTH_OFFSET (2)
+
+ extern const char * const IWINFO_80211_NAMES[IWINFO_80211_COUNT];
+
+@@ -52,7 +56,7 @@ enum iwinfo_band {
+ IWINFO_BAND_5,
+ IWINFO_BAND_6,
+ IWINFO_BAND_60,
+-
++ IWINFO_BAND_900,
+ /* keep last */
+ IWINFO_BAND_COUNT
+ };
+@@ -61,6 +65,7 @@ enum iwinfo_band {
+ #define IWINFO_BAND_5 (1 << IWINFO_BAND_5)
+ #define IWINFO_BAND_6 (1 << IWINFO_BAND_6)
+ #define IWINFO_BAND_60 (1 << IWINFO_BAND_60)
++#define IWINFO_BAND_900 (1 << IWINFO_BAND_900)
+
+ extern const char * const IWINFO_BAND_NAMES[IWINFO_BAND_COUNT];
+
+@@ -290,6 +295,15 @@ struct iwinfo_crypto_entry {
+ uint16_t pair_ciphers;
+ uint8_t auth_suites;
+ uint8_t auth_algs;
++
++ /* RSNXE data */
++ uint8_t prot_twt:1;
++ uint8_t sae_h2e:1;
++ uint8_t sae_pk:1;
++ uint8_t secure_ltf:1;
++ uint8_t secure_rtt:1;
++ uint8_t prot_range_neg:1;
++ uint8_t pad0:2;
+ };
+
+ struct iwinfo_scanlist_ht_chan_entry {
+@@ -304,6 +318,19 @@ struct iwinfo_scanlist_vht_chan_entry {
+ uint8_t center_chan_2;
+ };
+
++struct iwinfo_scanlist_ah_chan_entry {
++ uint8_t primary_chan;
++ uint8_t chan_width;
++};
++
++static uint16_t ah_chan_width[] = {
++ 1, /* 1 MHz*/
++ 2, /* 2 MHz*/
++ 4, /* 4 MHz*/
++ 8, /* 8 MHz*/
++ 16, /* 16 MHz*/
++};
++
+ extern const char * const ht_secondary_offset[4];
+ /* 0 = 20 MHz
+ 1 = 40 MHz or higher (refer to vht if supported) */
+@@ -327,6 +354,7 @@ struct iwinfo_scanlist_entry {
+ struct iwinfo_crypto_entry crypto;
+ struct iwinfo_scanlist_ht_chan_entry ht_chan_info;
+ struct iwinfo_scanlist_vht_chan_entry vht_chan_info;
++ struct iwinfo_scanlist_ah_chan_entry ah_chan_info;
+ };
+
+ struct iwinfo_country_entry {
+@@ -412,6 +440,7 @@ extern const struct iwinfo_ops wext_ops;
+ extern const struct iwinfo_ops madwifi_ops;
+ extern const struct iwinfo_ops nl80211_ops;
+ extern const struct iwinfo_ops wl_ops;
++extern const struct iwinfo_ops dot11ah_ops;
+
+ #include "iwinfo/utils.h"
+
+diff --git a/include/iwinfo/utils.h b/include/iwinfo/utils.h
+index 7b8ceea..d2a2788 100644
+--- a/include/iwinfo/utils.h
++++ b/include/iwinfo/utils.h
+@@ -66,6 +66,7 @@ int iwinfo_hardware_id_from_mtd(struct iwinfo_hardware_id *id);
+
+ void iwinfo_parse_rsn(struct iwinfo_crypto_entry *c, uint8_t *data, uint8_t len,
+ uint16_t defcipher, uint8_t defauth);
++void iwinfo_parse_rsnxe(struct iwinfo_crypto_entry *c, uint8_t *data, uint8_t len);
+
+ struct uci_section *iwinfo_uci_get_radio(const char *name, const char *type);
+ void iwinfo_uci_free(void);
+diff --git a/iwinfo_cli.c b/iwinfo_cli.c
+index 5dcee9a..790803c 100644
+--- a/iwinfo_cli.c
++++ b/iwinfo_cli.c
+@@ -69,12 +69,12 @@ static char * format_channel(int ch)
+
+ static char * format_frequency(int freq)
+ {
+- static char buf[11];
++ static char buf[15];
+
+ if (freq <= 0)
+ snprintf(buf, sizeof(buf), "unknown");
+ else
+- snprintf(buf, sizeof(buf), "%.3f GHz", ((float)freq / 1000.0));
++ snprintf(buf, sizeof(buf), "%.3f %s", ((float)freq / 1000.0), freq > 500000 ? "MHz" : "GHz");
+
+ return buf;
+ }
+@@ -351,6 +351,19 @@ static const char* format_chan_width(bool vht, uint8_t width)
+ return "unknown";
+ }
+
++static const char* format_ah_chan_width(uint8_t width)
++{
++ if (width < ARRAY_SIZE(ah_chan_width))
++ switch (ah_chan_width[width]) {
++ case 1: return "1 MHz";
++ case 2: return "2 MHz";
++ case 4: return "4 MHz";
++ case 8: return "8 MHz";
++ case 16: return "16 MHz";
++ }
++
++ return "unknown";
++}
+
+ static const char * print_type(const struct iwinfo_ops *iw, const char *ifname)
+ {
+@@ -689,13 +702,16 @@ static void print_scanlist(const struct iwinfo_ops *iw, const char *ifname)
+ format_quality_max(e->quality_max));
+ printf(" Encryption: %s\n",
+ format_encryption(&e->crypto));
+- printf(" HT Operation:\n");
+- printf(" Primary Channel: %d\n",
+- e->ht_chan_info.primary_chan);
+- printf(" Secondary Channel Offset: %s\n",
+- ht_secondary_offset[e->ht_chan_info.secondary_chan_off]);
+- printf(" Channel Width: %s\n",
+- format_chan_width(false, e->ht_chan_info.chan_width));
++
++ if (e->ht_chan_info.primary_chan) {
++ printf(" HT Operation:\n");
++ printf(" Primary Channel: %d\n",
++ e->ht_chan_info.primary_chan);
++ printf(" Secondary Channel Offset: %s\n",
++ ht_secondary_offset[e->ht_chan_info.secondary_chan_off]);
++ printf(" Channel Width: %s\n",
++ format_chan_width(false, e->ht_chan_info.chan_width));
++ }
+
+ if (e->vht_chan_info.center_chan_1) {
+ printf(" VHT Operation:\n");
+@@ -707,6 +723,14 @@ static void print_scanlist(const struct iwinfo_ops *iw, const char *ifname)
+ format_chan_width(true, e->vht_chan_info.chan_width));
+ }
+
++ if (e->ah_chan_info.primary_chan) {
++ printf(" AH Operation:\n");
++ printf(" Channel Width: %s\n",
++ format_ah_chan_width(e->ah_chan_info.chan_width));
++ printf(" Primary Channel: %d\n",
++ e->ah_chan_info.primary_chan);
++ }
++
+ printf("\n");
+ }
+ }
+@@ -912,7 +936,7 @@ int main(int argc, char **argv)
+ char *p;
+ const struct iwinfo_ops *iw;
+ glob_t globbuf;
+-
++
+ if (argc > 1 && argc < 3)
+ {
+ fprintf(stderr,
+@@ -1031,7 +1055,6 @@ int main(int argc, char **argv)
+ }
+ }
+ }
+-
+ iwinfo_finish();
+
+ return rv;
+diff --git a/iwinfo_lib.c b/iwinfo_lib.c
+index 579efc4..02fbd9c 100644
+--- a/iwinfo_lib.c
++++ b/iwinfo_lib.c
+@@ -30,6 +30,7 @@ const char * const IWINFO_80211_NAMES[IWINFO_80211_COUNT] = {
+ "ac",
+ "ad",
+ "ax",
++ "ah",
+ };
+
+ const char * const IWINFO_BAND_NAMES[IWINFO_BAND_COUNT] = {
+@@ -37,6 +38,7 @@ const char * const IWINFO_BAND_NAMES[IWINFO_BAND_COUNT] = {
+ "5 GHz",
+ "6 GHz",
+ "60 GHz",
++ "900 MHz",
+ };
+
+ const char * const IWINFO_CIPHER_NAMES[IWINFO_CIPHER_COUNT] = {
+@@ -383,6 +385,7 @@ const struct iwinfo_iso3166_label IWINFO_ISO3166_NAMES[] = {
+
+ static const struct iwinfo_ops *backends[] = {
+ #ifdef USE_NL80211
++ &dot11ah_ops,
+ &nl80211_ops,
+ #endif
+ #ifdef USE_MADWIFI
+diff --git a/iwinfo_lua.c b/iwinfo_lua.c
+index ecf257d..9def386 100644
+--- a/iwinfo_lua.c
++++ b/iwinfo_lua.c
+@@ -554,6 +554,9 @@ static int iwinfo_L_hwmodelist(lua_State *L, int (*func)(const char *, int *))
+ lua_pushboolean(L, hwmodes & IWINFO_80211_AX);
+ lua_setfield(L, -2, "ax");
+
++ lua_pushboolean(L, hwmodes & IWINFO_80211_AH);
++ lua_setfield(L, -2, "ah");
++
+ return 1;
+ }
+
+diff --git a/iwinfo_morsecli.c b/iwinfo_morsecli.c
+new file mode 100644
+index 0000000..7156bd8
+--- /dev/null
++++ b/iwinfo_morsecli.c
+@@ -0,0 +1,122 @@
++/*
++ * iwinfo - Wireless Information Library - morsecli interface
++ *
++ * Copyright (C) 2023 Morse Micro
++ *
++ * The iwinfo library is free software: you can redistribute it and/or
++ * modify it under the terms of the GNU General Public License version 2
++ * as published by the Free Software Foundation.
++ *
++ * The iwinfo library is distributed in the hope that it will be useful,
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
++ * See the GNU General Public License for more details.
++ *
++ * You should have received a copy of the GNU General Public License along
++ * with the iwinfo library. If not, see http://www.gnu.org/licenses/.
++ */
++
++#include <stdarg.h>
++#include <stdlib.h>
++#include <stdio.h>
++#include <string.h>
++#include <unistd.h>
++
++
++/* popen using exec (rather than like system()).
++ *
++ * Since I have an irrational dislike of using /bin/sh unnecessarily,
++ * and we're potentially calling this quite a lot :(
++ */
++static FILE *execvp_popen_read(const char *file, const char *const argv[])
++{
++ int pipefd[2];
++
++ if (pipe(pipefd)) return NULL;
++
++ switch (fork())
++ {
++ case 0: /* child */
++ close(pipefd[0]);
++ dup2(pipefd[1], STDOUT_FILENO);
++ freopen("/dev/null", "w", stderr); /* throw away stderr, to avoid noise in output */
++ execvp(file, (char * const*)argv);
++ exit(1);
++ break;
++ default: /* parent */
++ close(pipefd[1]);
++ return fdopen(pipefd[0], "r");
++ case -1:
++ close(pipefd[1]);
++ return NULL;
++ }
++}
++
++/* Query particular stats from morse_cli.
++ *
++ * Style of function echos nl80211_hostapd_query.
++ *
++ * This is the YAGNI version of this, since hopefully
++ * it will be replaced by netlink queries in time.
++ *
++ * I don't use morse_cli's filter, since it's hard to construct
++ * a regex covering all the possible options (and filter
++ * doesn't do anything smart and still queries all the stats,
++ * so we may as well not use the regex engine at all).
++ *
++ * Notably:
++ * - uses the plain text output rather than JSON
++ * (which means it can't handle nested stats)
++ * - only handles integer stats
++ */
++int __morse_cli_stats_query(const char *ifname, ...)
++{
++ va_list ap, ap_cur;
++ int *dest;
++ char *search, *key, *val, buf[128];
++ int found = 0;
++ FILE *fp;
++ /* For now, the only stat we want is noise,
++ * so restrict to PHY core.
++ */
++ const char *const argv[] = {"morse_cli", "-i", ifname, "stats", "-u", NULL};
++
++ fp = execvp_popen_read("morse_cli", argv);
++
++ if (!fp)
++ return 0;
++
++ va_start(ap, ifname);
++
++ /* iterate applicable lines and copy found values into dest buffers */
++ while (fgets(buf, sizeof(buf), fp))
++ {
++ key = strtok(buf, ":\n");
++ val = strtok(NULL, "\n");
++
++ if (!key || !val || !*key)
++ continue;
++
++ va_copy(ap_cur, ap);
++
++ while ((search = va_arg(ap_cur, char *)) != NULL)
++ {
++ dest = va_arg(ap_cur, int *);
++
++ if (!strcmp(search, key))
++ {
++ *dest = atoi(val);
++ found++;
++ break;
++ }
++ }
++
++ va_end(ap_cur);
++ }
++
++ va_end(ap);
++
++ fclose(fp);
++
++ return found;
++}
+\ No newline at end of file
+diff --git a/iwinfo_morsecli.h b/iwinfo_morsecli.h
+new file mode 100644
+index 0000000..320f433
+--- /dev/null
++++ b/iwinfo_morsecli.h
+@@ -0,0 +1,27 @@
++/*
++ * iwinfo - Wireless Information Library - morsecli headers
++ *
++ * Copyright (C) 2023 Morse Micro
++ *
++ * The iwinfo library is free software: you can redistribute it and/or
++ * modify it under the terms of the GNU General Public License version 2
++ * as published by the Free Software Foundation.
++ *
++ * The iwinfo library is distributed in the hope that it will be useful,
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
++ * See the GNU General Public License for more details.
++ *
++ * You should have received a copy of the GNU General Public License along
++ * with the iwinfo library. If not, see http://www.gnu.org/licenses/.
++ */
++
++#ifndef __IWINFO_MORSECLI_H_
++#define __IWINFO_MORSECLI_H_
++
++#define morse_cli_stats_query(ifname, ...) \
++ __morse_cli_stats_query(ifname, ##__VA_ARGS__, NULL)
++
++int __morse_cli_stats_query(const char *ifname, ...);
++
++#endif
+\ No newline at end of file
+diff --git a/iwinfo_nl80211.c b/iwinfo_nl80211.c
+index 2200249..dcb29b3 100644
+--- a/iwinfo_nl80211.c
++++ b/iwinfo_nl80211.c
+@@ -2,6 +2,7 @@
+ * iwinfo - Wireless Information Library - NL80211 Backend
+ *
+ * Copyright (C) 2010-2013 Jo-Philipp Wich <xm@subsignal.org>
++ * Copyright 2022 Morse Micro
+ *
+ * The iwinfo library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License version 2
+@@ -30,6 +31,7 @@
+ #include <stdlib.h>
+
+ #include "iwinfo_nl80211.h"
++#include "iwinfo_morsecli.h"
+
+ #define min(x, y) ((x) < (y)) ? (x) : (y)
+
+@@ -408,8 +410,11 @@ static int nl80211_phy_idx_from_uci(const char *name)
+ int idx = -1;
+
+ s = iwinfo_uci_get_radio(name, "mac80211");
+- if (!s)
+- goto out;
++ if (!s){
++ s = iwinfo_uci_get_radio(name, "morse");
++ if(!s)
++ goto out;
++ }
+
+ opt = uci_lookup_option_string(uci_ctx, s, "path");
+ idx = nl80211_phy_idx_from_path(opt);
+@@ -1331,15 +1336,16 @@ static int nl80211_get_bssid(const char *ifname, char *buf)
+
+ res = nl80211_phy2ifname(ifname);
+
+- /* try to obtain mac address via NL80211_CMD_GET_INTERFACE */
+- nl80211_request(res ? res : ifname, NL80211_CMD_GET_INTERFACE, 0,
+- nl80211_get_macaddr_cb, &sb);
++ /* try to find bssid from scan dump results (for AP, this fails
++ * and it goes to NL80211_CMD_GET_INTERFACE)*/
++ nl80211_request(res ? res : ifname,
++ NL80211_CMD_GET_SCAN, NLM_F_DUMP,
++ nl80211_get_ssid_bssid_cb, &sb);
+
+- /* failed, try to find bssid from scan dump results */
++ /* failed, try to obtain mac address via NL80211_CMD_GET_INTERFACE */
+ if (sb.bssid[0] == 0)
+- nl80211_request(res ? res : ifname,
+- NL80211_CMD_GET_SCAN, NLM_F_DUMP,
+- nl80211_get_ssid_bssid_cb, &sb);
++ nl80211_request(res ? res : ifname, NL80211_CMD_GET_INTERFACE, 0,
++ nl80211_get_macaddr_cb, &sb);
+
+ /* failed, try to find mac from hostapd info */
+ if ((sb.bssid[0] == 0) &&
+@@ -1771,7 +1777,8 @@ static const struct {
+ { "EAP-SUITE-B-192", 4, IWINFO_KMGMT_8021x },
+ { "EAP-SUITE-B", 4, IWINFO_KMGMT_8021x },
+ { "EAP-SHA384", 4, IWINFO_KMGMT_8021x },
+- { "EAP-SHA256", 0, IWINFO_KMGMT_8021x },
++ /* SHA256 counts as WPA3 as long as pmf is enabled; we check this below. */
++ { "EAP-SHA256", 4, IWINFO_KMGMT_8021x },
+ { "PSK-SHA256", 0, IWINFO_KMGMT_PSK },
+ { "NONE", 0, IWINFO_KMGMT_NONE },
+ { "None", 0, IWINFO_KMGMT_NONE },
+@@ -1782,7 +1789,7 @@ static const struct {
+ };
+
+ static void parse_wpa_suites(const char *str, int defversion,
+- uint8_t *versions, uint8_t *suites)
++ uint8_t *versions, uint8_t *suites, int pmf)
+ {
+ size_t l;
+ int i, version;
+@@ -1817,6 +1824,35 @@ static void parse_wpa_suites(const char *str, int defversion,
+
+ p = q + strspn(q, sep);
+ }
++
++ /* Handle ieee80211w/pmf (management frame protection).
++ *
++ * Strictly:
++ * ieee80211w=2 && wpa_key_mgmt=WPA-EAP-SHA256
++ * => WPA3-Enterprise
++ * ieee80211w=1 && wpa_key_mgmt=WPA-EAP WPA-EAP-SHA256
++ * => WPA3-Enterprise transition
++ *
++ * Here we just try to aggressively downgrade (i.e. if no pmf,
++ * not WPA3-Enterprise, and if not required then WPA2/WPA3).
++ * This _will_ allow some invalid configurations through
++ * and count certain undefined configurations as WPA2/WPA3
++ * (e.g. WPA-EAP-SHA256 only and ieee80211=1).
++ */
++ if ((*suites & IWINFO_KMGMT_8021x) && (*versions & 4))
++ switch(pmf)
++ {
++ case 0: /* if disabled, it's not WPA3 */
++ *versions &= ~4;
++ *versions |= defversion;
++ break;
++ case 1: /* if not required, not only WPA3 */
++ *versions |= defversion;
++ break;
++ case 2: /* pmf required */
++ default: /* if no pmf info - e.g. from scan */
++ break;
++ }
+ }
+
+ static const struct {
+@@ -1871,6 +1907,7 @@ static int nl80211_get_encryption(const char *ifname, char *buf)
+ uint8_t wpa_version = 0;
+ char wpa[2], wpa_key_mgmt[64], wpa_pairwise[16], wpa_groupwise[16];
+ char auth_algs[2], wep_key0[27], wep_key1[27], wep_key2[27], wep_key3[27];
++ char ieee80211w[2], pmf[2];
+ char mode[16];
+
+ struct iwinfo_crypto_entry *c = (struct iwinfo_crypto_entry *)buf;
+@@ -1880,6 +1917,7 @@ static int nl80211_get_encryption(const char *ifname, char *buf)
+ "pairwise_cipher", wpa_pairwise, sizeof(wpa_pairwise),
+ "group_cipher", wpa_groupwise, sizeof(wpa_groupwise),
+ "key_mgmt", wpa_key_mgmt, sizeof(wpa_key_mgmt),
++ "pmf", pmf, sizeof(pmf),
+ "mode", mode, sizeof(mode)))
+ {
+ /* WEP or Open */
+@@ -1928,7 +1966,7 @@ static int nl80211_get_encryption(const char *ifname, char *buf)
+ wpa_version = 1;
+ }
+
+- parse_wpa_suites(p, wpa_version, &c->wpa_version, &c->auth_suites);
++ parse_wpa_suites(p, wpa_version, &c->wpa_version, &c->auth_suites, atoi(pmf));
+
+ c->enabled = !!(c->wpa_version && c->auth_suites);
+ }
+@@ -1941,6 +1979,7 @@ static int nl80211_get_encryption(const char *ifname, char *buf)
+ "wpa", wpa, sizeof(wpa),
+ "wpa_key_mgmt", wpa_key_mgmt, sizeof(wpa_key_mgmt),
+ "wpa_pairwise", wpa_pairwise, sizeof(wpa_pairwise),
++ "ieee80211w", ieee80211w, sizeof(ieee80211w),
+ "auth_algs", auth_algs, sizeof(auth_algs),
+ "wep_key0", wep_key0, sizeof(wep_key0),
+ "wep_key1", wep_key1, sizeof(wep_key1),
+@@ -1959,7 +1998,7 @@ static int nl80211_get_encryption(const char *ifname, char *buf)
+ if (!strncmp(p, "FT-", 3))
+ p += 3;
+
+- parse_wpa_suites(p, atoi(wpa), &c->wpa_version, &c->auth_suites);
++ parse_wpa_suites(p, atoi(wpa), &c->wpa_version, &c->auth_suites, atoi(ieee80211w));
+ }
+
+ c->enabled = c->wpa_version ? 1 : 0;
+@@ -2531,7 +2570,7 @@ static void nl80211_get_scancrypto(char *spec, struct iwinfo_crypto_entry *c)
+
+ c->enabled = 1;
+
+- parse_wpa_suites(suites, wpa_version, &c->wpa_version, &c->auth_suites);
++ parse_wpa_suites(suites, wpa_version, &c->wpa_version, &c->auth_suites, -1);
+ parse_wpa_ciphers(suites, &c->pair_ciphers);
+ }
+ }
+@@ -2588,6 +2627,9 @@ static void nl80211_get_scanlist_ie(struct nlattr **bss,
+ e->vht_chan_info.center_chan_2 = ie[4];
+ }
+ break;
++ case 244: /* RSNXE */
++ iwinfo_parse_rsnxe(&e->crypto, ie + 2, ie[1]);
++ break;
+ }
+
+ ielen -= ie[1] + 2;
+@@ -3634,7 +3676,7 @@ const struct iwinfo_ops nl80211_ops = {
+ .mbssid_support = nl80211_get_mbssid_support,
+ .hwmodelist = nl80211_get_hwmodelist,
+ .htmodelist = nl80211_get_htmodelist,
+- .htmode = nl80211_get_htmode,
++ .htmode = nl80211_get_htmode,
+ .mode = nl80211_get_mode,
+ .ssid = nl80211_get_ssid,
+ .bssid = nl80211_get_bssid,
+@@ -3653,3 +3695,723 @@ const struct iwinfo_ops nl80211_ops = {
+ .phy_path = nl80211_phy_path,
+ .close = nl80211_close
+ };
++
++
++/*
++ * Morse Micro Shim Layer
++ *
++ */
++
++#include "dot11ah_channel.h"
++
++country_channel_map_t *g_map = NULL;
++
++static bool nl80211_is_halow(const char *ifname)
++{
++ const struct iwinfo_hardware_entry *e = nl80211_get_hardware_entry(ifname);
++ if (!e)
++ return false;
++
++ if (strcmp(e->device_name, "HaLow WiFi"))
++ return false;
++
++ return true;
++}
++
++static inline void _sanitise_rate_entry(struct iwinfo_rate_entry *re){
++ if(re == NULL)
++ return;
++ re->rate = s1g_rate(re->rate, re->mhz);
++ re->mhz /= 20;
++ re->is_vht = 0;
++ re->is_ht = 1;
++}
++
++
++/*
++ * These fill_signal handlers need to be modified for s1g as they work per station.
++ * And it's more correct to handle the per station values in the per station handler
++ */
++
++static uint8_t nl80211_get_bandwidth(struct nlattr **ri, uint32_t size){
++ if ((NL80211_RATE_INFO_5_MHZ_WIDTH < size)
++ && (ri[NL80211_RATE_INFO_5_MHZ_WIDTH]))
++ return 5;
++ else if ((NL80211_RATE_INFO_5_MHZ_WIDTH < size)
++ && (ri[NL80211_RATE_INFO_10_MHZ_WIDTH]))
++ return 10;
++ else if ((NL80211_RATE_INFO_5_MHZ_WIDTH < size)
++ && (ri[NL80211_RATE_INFO_40_MHZ_WIDTH]))
++ return 40;
++ else if ((NL80211_RATE_INFO_5_MHZ_WIDTH < size)
++ && (ri[NL80211_RATE_INFO_80_MHZ_WIDTH]))
++ return 80;
++ else if ((NL80211_RATE_INFO_5_MHZ_WIDTH < size)
++ && (ri[NL80211_RATE_INFO_80P80_MHZ_WIDTH] ||
++ ri[NL80211_RATE_INFO_160_MHZ_WIDTH]))
++ return 160;
++ else
++ return 20;
++}
++
++static int dot11ah_fill_signal_cb(struct nl_msg *msg, void *arg)
++{
++ int8_t dbm;
++ int16_t mbit;
++ struct nl80211_rssi_rate *rr = arg;
++ struct nlattr **attr = nl80211_parse(msg);
++ struct nlattr *sinfo[NL80211_STA_INFO_MAX + 1];
++ struct nlattr *rinfo[NL80211_RATE_INFO_MAX + 1];
++
++ static struct nla_policy stats_policy[NL80211_STA_INFO_MAX + 1] = {
++ [NL80211_STA_INFO_INACTIVE_TIME] = { .type = NLA_U32 },
++ [NL80211_STA_INFO_RX_BYTES] = { .type = NLA_U32 },
++ [NL80211_STA_INFO_TX_BYTES] = { .type = NLA_U32 },
++ [NL80211_STA_INFO_RX_PACKETS] = { .type = NLA_U32 },
++ [NL80211_STA_INFO_TX_PACKETS] = { .type = NLA_U32 },
++ [NL80211_STA_INFO_SIGNAL] = { .type = NLA_U8 },
++ [NL80211_STA_INFO_TX_BITRATE] = { .type = NLA_NESTED },
++ [NL80211_STA_INFO_LLID] = { .type = NLA_U16 },
++ [NL80211_STA_INFO_PLID] = { .type = NLA_U16 },
++ [NL80211_STA_INFO_PLINK_STATE] = { .type = NLA_U8 },
++ };
++
++ static struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1] = {
++ [NL80211_RATE_INFO_BITRATE] = { .type = NLA_U16 },
++ [NL80211_RATE_INFO_MCS] = { .type = NLA_U8 },
++ [NL80211_RATE_INFO_40_MHZ_WIDTH] = { .type = NLA_FLAG },
++ [NL80211_RATE_INFO_SHORT_GI] = { .type = NLA_FLAG },
++ };
++
++ if (attr[NL80211_ATTR_STA_INFO])
++ {
++ if (!nla_parse_nested(sinfo, NL80211_STA_INFO_MAX,
++ attr[NL80211_ATTR_STA_INFO], stats_policy))
++ {
++ if (sinfo[NL80211_STA_INFO_SIGNAL])
++ {
++ dbm = nla_get_u8(sinfo[NL80211_STA_INFO_SIGNAL]);
++ rr->rssi = (rr->rssi * rr->rssi_samples + dbm) / (rr->rssi_samples + 1);
++ rr->rssi_samples++;
++ }
++
++ if (sinfo[NL80211_STA_INFO_TX_BITRATE])
++ {
++ if (!nla_parse_nested(rinfo, NL80211_RATE_INFO_MAX,
++ sinfo[NL80211_STA_INFO_TX_BITRATE],
++ rate_policy))
++ {
++ if (rinfo[NL80211_RATE_INFO_BITRATE])
++ {
++ uint8_t mhz = nl80211_get_bandwidth(rinfo, NL80211_RATE_INFO_MAX);
++ mbit = nla_get_u16(rinfo[NL80211_RATE_INFO_BITRATE]);
++ mbit = s1g_rate(mbit, mhz);
++ rr->rate = (rr->rate * rr->rate_samples + mbit) / (rr->rate_samples + 1);
++ rr->rate_samples++;
++ }
++ }
++ }
++ }
++ }
++
++ return NL_SKIP;
++}
++
++static void dot11ah_fill_signal(const char *ifname, struct nl80211_rssi_rate *r)
++{
++ DIR *d;
++ struct dirent *de;
++
++ memset(r, 0, sizeof(*r));
++
++ if ((d = opendir("/sys/class/net")) != NULL)
++ {
++ while ((de = readdir(d)) != NULL)
++ {
++ if (!strncmp(de->d_name, ifname, strlen(ifname)) &&
++ (!de->d_name[strlen(ifname)] ||
++ !strncmp(&de->d_name[strlen(ifname)], ".sta", 4)))
++ {
++ nl80211_request(de->d_name, NL80211_CMD_GET_STATION,
++ NLM_F_DUMP, dot11ah_fill_signal_cb, r);
++ }
++ }
++
++ closedir(d);
++ }
++}
++
++static int dot11ah_probe(const char *ifname)
++{
++ if (!nl80211_ifname2phy(ifname))
++ return 0;
++
++ if (!nl80211_is_halow(ifname))
++ return 0;
++
++ g_map = set_s1g_channel_map();
++ return 1;
++}
++
++static int dot11ah_get_center_chan2(const char *ifname, int *buf)
++{
++ if(nl80211_get_center_chan2(ifname, buf) < 0)
++ return -1;
++
++ channel_to_halow_freq_t *ch_entry = get_s1g(g_map, *buf);
++ if(ch_entry == NULL)
++ return -1;
++
++ *buf = ch_entry->halow_channel;
++
++ return 0;
++}
++
++static int dot11ah_get_center_chan1(const char *ifname, int *buf)
++{
++ if(nl80211_get_center_chan1(ifname, buf) < 0)
++ return -1;
++
++ channel_to_halow_freq_t *ch_entry = get_s1g(g_map, *buf);
++ if(ch_entry == NULL)
++ return -1;
++
++ *buf = ch_entry->halow_channel;
++
++ return 0;
++
++}
++
++static int dot11ah_get_channel(const char *ifname, int *buf)
++{
++ if(dot11ah_get_center_chan1(ifname, buf) == 0)
++ return 0;
++
++ if(nl80211_get_channel(ifname, buf) < 0)
++ return -1;
++
++ channel_to_halow_freq_t *ch_entry = get_s1g(g_map, *buf);
++ if(ch_entry == NULL)
++ return -1;
++
++ *buf = ch_entry->halow_channel;
++
++ return 0;
++
++}
++
++static int dot11ah_get_frequency(const char *ifname, int *buf)
++{
++ *buf = 0;
++
++ if (dot11ah_get_center_chan1(ifname, buf) < 0)
++ if (dot11ah_get_channel(ifname, buf) < 0)
++ return -1;
++
++ *buf = (int) (get_freq(g_map, *buf)*1000);
++
++ return (*buf == 0) ? -1 : 0;
++}
++
++static int dot11ah_get_bitrate(const char *ifname, int *buf)
++{
++ struct nl80211_rssi_rate rr;
++
++ dot11ah_fill_signal(ifname, &rr);
++
++ if (rr.rate_samples)
++ {
++ *buf = (rr.rate * 100);
++ return 0;
++ }
++
++ return -1;
++}
++
++static int dot11ah_get_hwmodelist(const char *ifname, int *buf)
++{
++ *buf = IWINFO_80211_AH;
++ return 0;
++}
++
++static int dot11ah_get_htmodelist(const char *ifname, int *buf)
++{
++ *buf = IWINFO_HTMODE_NOHT;
++ return 0;
++}
++
++static int dot11ah_get_htmode(const char *ifname, int *buf)
++{
++ int chan;
++ dot11ah_get_channel(ifname, &chan);
++ if(!g_map || !chan)
++ return 0;
++ *buf = s1g_chan2bw(g_map, chan);
++ return 0;
++}
++
++static int dot11ah_get_country(const char *ifname, char *buf)
++{
++ s1g_get_country(buf);
++ return 0;
++}
++
++static int dot11ah_get_noise(const char *ifname, int *buf)
++{
++ if (!morse_cli_stats_query(ifname, "Noise dBm", buf))
++ return -1;
++ else
++ return 0;
++}
++
++static int dot11ah_get_assoclist(const char *ifname, char *buf, int *len)
++{
++ struct iwinfo_assoclist_entry *ae;
++ int noise = 0;
++
++ if (nl80211_get_assoclist(ifname, buf, len) < 0)
++ return -1;
++
++ dot11ah_get_noise(ifname, &noise);
++
++ for (char *p = buf; p < (buf + *len); p += sizeof(struct iwinfo_assoclist_entry))
++ {
++ ae = (struct iwinfo_assoclist_entry *) p;
++ ae->noise = noise;
++ _sanitise_rate_entry(&ae->rx_rate);
++ _sanitise_rate_entry(&ae->tx_rate);
++ }
++
++ return 0;
++}
++
++static int dot11ah_get_scanlist(const char *ifname, char *buf, int *len)
++{
++ struct iwinfo_scanlist_entry *se;
++ channel_to_halow_freq_t *ch_entry, *prim_chan;
++ if(nl80211_get_scanlist(ifname, buf, len) < 0)
++ return -1;
++
++ for(char *p = buf; p < (buf + *len); p += sizeof(struct iwinfo_scanlist_entry)){
++ se = (struct iwinfo_scanlist_entry *) p;
++
++ ch_entry = get_s1g(g_map, se->channel);
++ prim_chan = get_s1g(g_map, se->ht_chan_info.primary_chan);
++ se->channel = ch_entry->halow_channel;
++ se->band = IWINFO_BAND_900;
++ se->mhz = get_freq(g_map, se->channel)*1000;
++
++ if (se->vht_chan_info.center_chan_1)
++ {
++ ch_entry = get_s1g(g_map, se->vht_chan_info.center_chan_1);
++ se->channel = ch_entry->halow_channel;
++ se->vht_chan_info.center_chan_1 = 0;
++ }else if(se->ht_chan_info.secondary_chan_off == 1)
++ {
++ se->channel += 1;
++ }else if(se->ht_chan_info.secondary_chan_off == 3)
++ {
++ se->channel -= 1;
++ }
++ se->ht_chan_info.secondary_chan_off=0;
++ se->ht_chan_info.primary_chan=0;
++ se->ah_chan_info.primary_chan=prim_chan->halow_channel;
++ se->ah_chan_info.chan_width=s1g_chan2bw(g_map, se->channel);
++
++ se->crypto.wpa_version |= 4;
++ if(se->crypto.sae_h2e == 1)
++ se->crypto.auth_suites |= IWINFO_KMGMT_SAE;
++ else
++ se->crypto.auth_suites |= IWINFO_KMGMT_OWE;
++ se->crypto.group_ciphers |= IWINFO_CIPHER_CCMP;
++ se->crypto.group_ciphers &= ~(IWINFO_CIPHER_WEP40 | IWINFO_CIPHER_WEP104);
++ se->crypto.pair_ciphers |= IWINFO_CIPHER_CCMP;
++ se->crypto.pair_ciphers &= ~(IWINFO_CIPHER_WEP40 | IWINFO_CIPHER_WEP104);
++ }
++
++ return 0;
++}
++
++int dot11ah_freq_compare(const void *a, const void *b)
++{
++ const struct iwinfo_freqlist_entry *fe1 = (const struct iwinfo_freqlist_entry *) a;
++ const struct iwinfo_freqlist_entry *fe2 = (const struct iwinfo_freqlist_entry *) b;
++ return ( fe1->mhz - fe2->mhz );
++}
++
++static int dot11ah_get_freqlist(const char *ifname, char *buf, int *len)
++{
++ struct iwinfo_freqlist_entry *fe;
++ channel_to_halow_freq_t *ch_entry;
++ const size_t fe_size = sizeof(struct iwinfo_freqlist_entry);
++ if(nl80211_get_freqlist(ifname, buf, len) < 0)
++ return -1;
++
++ for(char *p = buf; p < (buf + *len); p += fe_size){
++ fe = (struct iwinfo_freqlist_entry *) p;
++ ch_entry = get_s1g(g_map, fe->channel);
++ fe->channel = ch_entry->halow_channel;
++ fe->mhz = get_freq(g_map, fe->channel)*1000;
++ fe->band = IWINFO_BAND_900;
++ fe->flags = 0;
++ }
++
++ qsort(buf, *len/fe_size, fe_size, dot11ah_freq_compare);
++ return 0;
++}
++
++static int dot11ah_get_countrylist(const char *ifname, char *buf, int *len)
++{
++ int count;
++ struct iwinfo_country_entry *e = (struct iwinfo_country_entry *)buf;
++
++ const country_channel_map_t **halow_map = s1g_mapped_channel();
++ char higher='0',lower='0';
++ e->iso3166 = (int)higher*256+lower;
++ e->ccode[0] = '0';
++ e->ccode[1] = '0';
++ e->ccode[2] = 0;
++ e++;
++ count=1;
++ while((*halow_map)->country[0])
++ {
++ higher = (*halow_map)->country[0];
++ lower = (*halow_map)->country[1];
++ e->iso3166 = higher * 256 + lower;
++ e->ccode[0] = higher;
++ e->ccode[1] = lower;
++ e->ccode[2] = 0;
++ e++;
++ halow_map++;
++ count++;
++ }
++ *len = (count * sizeof(struct iwinfo_country_entry));
++ return 0;
++}
++
++/*
++ * modified function to look for the interface in wpa_supplicant_s1g
++ */
++static int dot11ah_wpactl_connect(const char *ifname, struct sockaddr_un *local)
++{
++ struct sockaddr_un remote = { 0 };
++ size_t remote_length, local_length;
++
++ int sock = socket(PF_UNIX, SOCK_DGRAM, 0);
++ if (sock < 0)
++ return sock;
++
++ remote.sun_family = AF_UNIX;
++ remote_length = sizeof(remote.sun_family) +
++ sprintf(remote.sun_path, "/var/run/wpa_supplicant-%s/%s",
++ ifname, ifname);
++
++ if (fcntl(sock, F_SETFD, fcntl(sock, F_GETFD) | FD_CLOEXEC) < 0)
++ {
++ close(sock);
++ return -1;
++ }
++
++ if (connect(sock, (struct sockaddr *)&remote, remote_length))
++ {
++ remote_length = sizeof(remote.sun_family) +
++ sprintf(remote.sun_path, "/var/run/wpa_supplicant_s1g/%s", ifname);
++
++ if (connect(sock, (struct sockaddr *)&remote, remote_length))
++ {
++ close(sock);
++ return -1;
++ }
++ }
++
++ local->sun_family = AF_UNIX;
++ local_length = sizeof(local->sun_family) +
++ sprintf(local->sun_path, "/var/run/iwinfo-%s-%d", ifname, getpid());
++
++ if (bind(sock, (struct sockaddr *)local, local_length) < 0)
++ {
++ close(sock);
++ return -1;
++ }
++
++ return sock;
++}
++
++/* function copied as is from nl80211 version*/
++static int __dot11ah_wpactl_query(const char *ifname, ...)
++{
++ va_list ap, ap_cur;
++ struct sockaddr_un local = { 0 };
++ int len, mode, found = 0, sock = -1;
++ char *search, *dest, *key, *val, *line, *pos, buf[512];
++
++ if (nl80211_get_mode(ifname, &mode))
++ return 0;
++
++ if (mode != IWINFO_OPMODE_CLIENT &&
++ mode != IWINFO_OPMODE_ADHOC &&
++ mode != IWINFO_OPMODE_MESHPOINT)
++ return 0;
++
++ sock = dot11ah_wpactl_connect(ifname, &local);
++
++ if (sock < 0)
++ return 0;
++
++ va_start(ap, ifname);
++
++ /* clear all destination buffers */
++ va_copy(ap_cur, ap);
++
++ while ((search = va_arg(ap_cur, char *)) != NULL)
++ {
++ dest = va_arg(ap_cur, char *);
++ len = va_arg(ap_cur, int);
++
++ memset(dest, 0, len);
++ }
++
++ va_end(ap_cur);
++
++ send(sock, "STATUS", 6, 0);
++
++ while (true)
++ {
++ if (nl80211_wpactl_recv(sock, buf, sizeof(buf)) <= 0)
++ break;
++
++ if (buf[0] == '<')
++ continue;
++
++ for (line = strtok_r(buf, "\n", &pos);
++ line != NULL;
++ line = strtok_r(NULL, "\n", &pos))
++ {
++ key = strtok(line, "=");
++ val = strtok(NULL, "\n");
++
++ if (!key || !val)
++ continue;
++
++ va_copy(ap_cur, ap);
++
++ while ((search = va_arg(ap_cur, char *)) != NULL)
++ {
++ dest = va_arg(ap_cur, char *);
++ len = va_arg(ap_cur, int);
++
++ if (!strcmp(search, key))
++ {
++ strncpy(dest, val, len - 1);
++ found++;
++ break;
++ }
++ }
++
++ va_end(ap_cur);
++ }
++
++ break;
++ }
++
++ va_end(ap);
++
++ close(sock);
++ unlink(local.sun_path);
++
++ return found;
++}
++
++#define dot11ah_wpactl_query(ifname, ...) \
++ __dot11ah_wpactl_query(ifname, ##__VA_ARGS__, NULL)
++
++/* function copied as is from nl80211 version*/
++static int dot11ah_get_encryption(const char *ifname, char *buf)
++{
++ char *p;
++ int opmode;
++ uint8_t wpa_version = 0;
++ char wpa[2], wpa_key_mgmt[64], wpa_pairwise[16], wpa_groupwise[16];
++ char auth_algs[2], wep_key0[27], wep_key1[27], wep_key2[27], wep_key3[27];
++ char ieee80211w[2], pmf[2];
++ char mode[16];
++
++ struct iwinfo_crypto_entry *c = (struct iwinfo_crypto_entry *)buf;
++
++ /* WPA supplicant */
++ if (dot11ah_wpactl_query(ifname,
++ "pairwise_cipher", wpa_pairwise, sizeof(wpa_pairwise),
++ "group_cipher", wpa_groupwise, sizeof(wpa_groupwise),
++ "key_mgmt", wpa_key_mgmt, sizeof(wpa_key_mgmt),
++ "pmf", pmf, sizeof(pmf),
++ "mode", mode, sizeof(mode)))
++ {
++ /* WEP or Open */
++ if (!strcmp(wpa_key_mgmt, "NONE"))
++ {
++ parse_wpa_ciphers(wpa_pairwise, &c->pair_ciphers);
++ parse_wpa_ciphers(wpa_groupwise, &c->group_ciphers);
++
++ if (c->pair_ciphers != 0 && c->pair_ciphers != IWINFO_CIPHER_NONE) {
++ c->enabled = 1;
++ c->auth_suites = IWINFO_KMGMT_NONE;
++ c->auth_algs = IWINFO_AUTH_OPEN | IWINFO_AUTH_SHARED;
++ }
++ else {
++ c->pair_ciphers = 0;
++ c->group_ciphers = 0;
++ }
++ }
++
++ /* MESH with SAE */
++ else if (!strcmp(mode, "mesh") && !strcmp(wpa_key_mgmt, "UNKNOWN"))
++ {
++ c->enabled = 1;
++ c->wpa_version = 4;
++ c->auth_suites = IWINFO_KMGMT_SAE;
++ c->pair_ciphers = IWINFO_CIPHER_CCMP;
++ c->group_ciphers = IWINFO_CIPHER_CCMP;
++ }
++
++ /* WPA */
++ else
++ {
++ parse_wpa_ciphers(wpa_pairwise, &c->pair_ciphers);
++ parse_wpa_ciphers(wpa_groupwise, &c->group_ciphers);
++
++ p = wpa_key_mgmt;
++
++ if (!strncmp(p, "WPA2-", 5) || !strncmp(p, "WPA2/", 5))
++ {
++ p += 5;
++ wpa_version = 2;
++ }
++ else if (!strncmp(p, "WPA-", 4))
++ {
++ p += 4;
++ wpa_version = 1;
++ }
++
++ parse_wpa_suites(p, wpa_version, &c->wpa_version, &c->auth_suites, atoi(pmf));
++
++ c->enabled = !!(c->wpa_version && c->auth_suites);
++ }
++
++ return 0;
++ }
++
++ /* Hostapd */
++ else if (nl80211_hostapd_query(ifname,
++ "wpa", wpa, sizeof(wpa),
++ "wpa_key_mgmt", wpa_key_mgmt, sizeof(wpa_key_mgmt),
++ "wpa_pairwise", wpa_pairwise, sizeof(wpa_pairwise),
++ "ieee80211w", ieee80211w, sizeof(ieee80211w),
++ "auth_algs", auth_algs, sizeof(auth_algs),
++ "wep_key0", wep_key0, sizeof(wep_key0),
++ "wep_key1", wep_key1, sizeof(wep_key1),
++ "wep_key2", wep_key2, sizeof(wep_key2),
++ "wep_key3", wep_key3, sizeof(wep_key3)))
++ {
++ c->wpa_version = 0;
++
++ if (wpa_key_mgmt[0])
++ {
++ for (p = strtok(wpa_key_mgmt, " \t"); p != NULL; p = strtok(NULL, " \t"))
++ {
++ if (!strncmp(p, "WPA-", 4))
++ p += 4;
++
++ if (!strncmp(p, "FT-", 3))
++ p += 3;
++
++ parse_wpa_suites(p, atoi(wpa), &c->wpa_version, &c->auth_suites, atoi(ieee80211w));
++ }
++
++ c->enabled = c->wpa_version ? 1 : 0;
++ }
++
++ if (wpa_pairwise[0])
++ parse_wpa_ciphers(wpa_pairwise, &c->pair_ciphers);
++
++ if (auth_algs[0])
++ {
++ switch (atoi(auth_algs))
++ {
++ case 1:
++ c->auth_algs |= IWINFO_AUTH_OPEN;
++ break;
++
++ case 2:
++ c->auth_algs |= IWINFO_AUTH_SHARED;
++ break;
++
++ case 3:
++ c->auth_algs |= IWINFO_AUTH_OPEN;
++ c->auth_algs |= IWINFO_AUTH_SHARED;
++ break;
++ }
++
++ c->pair_ciphers |= nl80211_check_wepkey(wep_key0);
++ c->pair_ciphers |= nl80211_check_wepkey(wep_key1);
++ c->pair_ciphers |= nl80211_check_wepkey(wep_key2);
++ c->pair_ciphers |= nl80211_check_wepkey(wep_key3);
++
++ c->enabled = (c->auth_algs && c->pair_ciphers) ? 1 : 0;
++ }
++
++ c->group_ciphers = c->pair_ciphers;
++
++ return 0;
++ }
++
++ /* Ad-Hoc or Mesh interfaces without wpa_supplicant are open */
++ else if (!nl80211_get_mode(ifname, &opmode) &&
++ (opmode == IWINFO_OPMODE_ADHOC ||
++ opmode == IWINFO_OPMODE_MESHPOINT))
++ {
++ c->enabled = 0;
++
++ return 0;
++ }
++
++
++ return -1;
++}
++
++const struct iwinfo_ops dot11ah_ops = {
++ .name = "dot11ah",
++ .probe = dot11ah_probe,
++ .channel = dot11ah_get_channel,
++ .center_chan1 = dot11ah_get_center_chan1,
++ .center_chan2 = dot11ah_get_center_chan2,
++ .frequency = dot11ah_get_frequency,
++ .frequency_offset = nl80211_get_frequency_offset,
++ .txpower = nl80211_get_txpower,
++ .txpower_offset = nl80211_get_txpower_offset,
++ .bitrate = dot11ah_get_bitrate,
++ .signal = nl80211_get_signal,
++ .noise = dot11ah_get_noise,
++ .quality = nl80211_get_quality,
++ .quality_max = nl80211_get_quality_max,
++ .mbssid_support = nl80211_get_mbssid_support,
++ .hwmodelist = dot11ah_get_hwmodelist,
++ .htmodelist = dot11ah_get_htmodelist,
++ .htmode = dot11ah_get_htmode,
++ .mode = nl80211_get_mode,
++ .ssid = nl80211_get_ssid,
++ .bssid = nl80211_get_bssid,
++ .country = dot11ah_get_country,
++ .hardware_id = nl80211_get_hardware_id,
++ .hardware_name = nl80211_get_hardware_name,
++ .encryption = dot11ah_get_encryption,
++ .phyname = nl80211_get_phyname,
++ .assoclist = dot11ah_get_assoclist,
++ .txpwrlist = nl80211_get_txpwrlist,
++ .scanlist = dot11ah_get_scanlist,
++ .freqlist = dot11ah_get_freqlist,
++ .countrylist = dot11ah_get_countrylist,
++ .survey = nl80211_get_survey,
++ .lookup_phy = nl80211_lookup_phyname,
++ .phy_path = nl80211_phy_path,
++ .close = nl80211_close
++};
+\ No newline at end of file
+diff --git a/iwinfo_utils.c b/iwinfo_utils.c
+index d96cbb3..a7c8abe 100644
+--- a/iwinfo_utils.c
++++ b/iwinfo_utils.c
+@@ -2,6 +2,7 @@
+ * iwinfo - Wireless Information Library - Shared utility routines
+ *
+ * Copyright (C) 2010 Jo-Philipp Wich <xm@subsignal.org>
++ * Copyright 2022 Morse Micro
+ *
+ * The iwinfo library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License version 2
+@@ -151,8 +152,8 @@ uint8_t iwinfo_ghz2band(uint32_t ghz)
+
+ size_t iwinfo_format_hwmodes(int modes, char *buf, size_t len)
+ {
+- // bit numbers as per IWINFO_80211_*: ad ac ax a b g n
+- const int order[IWINFO_80211_COUNT] = { 5, 4, 6, 0, 1, 2, 3 };
++ // bit numbers as per IWINFO_80211_*: ad ac ax ah a b g n
++ const int order[IWINFO_80211_COUNT] = { 5, 4, 6, 7, 0, 1, 2, 3 };
+ size_t res = 0;
+ int i;
+
+@@ -599,6 +600,25 @@ void iwinfo_parse_rsn(struct iwinfo_crypto_entry *c, uint8_t *data, uint8_t len,
+ len -= 2 + (count * 4);
+ }
+
++void iwinfo_parse_rsnxe(struct iwinfo_crypto_entry *c, uint8_t *data, uint8_t len)
++{
++ if (len > 2)
++ return;
++
++ c->prot_twt = (data[0] >> 4) & 0x1;
++ c->sae_h2e = (data[0] >> 5) & 0x1;
++ c->sae_pk = (data[0] >> 6) & 0x1;
++ len -= 1;
++ data += 1;
++
++ if (len == 0)
++ return;
++
++ c->secure_ltf = (data[0]) & 0x1;
++ c->secure_rtt = (data[0] >> 1) & 0x1;
++ c->prot_range_neg = (data[0] >> 2) & 0x1;
++}
++
+ struct uci_section *iwinfo_uci_get_radio(const char *name, const char *type)
+ {
+ struct uci_ptr ptr = {
--
2.34.1