我对lib_chan的注释

在《programming erlang》中,lib_chan是比较复杂的代码,至少我是读了好多遍才算明白。
我不知道是这段代码太复杂,还是我的智商有问题。至少,我不太习惯erlang的这种方式。不过,我很喜欢这个lib_chan的思路和erlang程序的风格。这种方式非常的明确,直接。我不知道为什么erlang没有一个类似的成熟的库。这个lib_chan好像是joe armstrong为这本书写的,难道erlang根本不需要这样的东西?
最好结合书里的那个聊天程序来理解lib_chan。这确实是个很精巧的东西(lib_chan还谈不上精巧,那个chat程序才精巧)。

注意:我只解释重要的地方。

lib_chan是一个类似总外壳的东西,定义接口,连接起诸多细节,这里我去掉了一些琐碎的函数。

%% ---
%% Excerpted from "Programming Erlang",
%% published by The Pragmatic Bookshelf.
%% Copyrights apply to this code. It may not be used to create training material,
%% courses, books, articles, and the like. Contact us if you are in doubt.
%% We make no guarantees that this code is fit for any purpose.
%% Visit http://www.pragmaticprogrammer.com/titles/jaerlang for more book information.
%%---
-module(lib_chan).
-export([cast/2, start_server/0, start_server/1,
connect/5, disconnect/1, rpc/2]).
-import(lists, [map/2, member/2, foreach/2]).
-import(lib_chan_mm, [send/2, close/1]).

%%----------------------------------------------------------------------
%% Server code
start_server1(ConfigData) ->
register(lib_chan, spawn(fun() -> start_server2(ConfigData) end)).

start_server2(ConfigData) ->
[Port] = [ P || {port,P} <- ConfigData],
start_port_server(Port, ConfigData).

start_port_server(Port, ConfigData) ->
%% lib_chan_cs:start_raw_server 函数会启动端口监听,并且,一旦接受连接,
%% 也就是说,拿到一个Socket的时候(一个Channel),就给现在传进来的fun(Socket)。
%% 
%% 也就是说,得到Socket就是lib_chan_cs:start_raw_server的第一阶段的成果,
%% 之后,它的任务就是把拿到的Socket给fun(Socket),就这么简单。
%%
%% 而fun(Socket)做的事情就是,start_port_instance(Socket,ConfigData)。
%% 看见了吗?我们现在有了Socket(先不必考虑监听,accept之类的事),并且,
%% 我们可以从ConfigData里拿到要运行的目标(一个函数,或者说是一个入口)。
%% 现在,你需要的都全了吧?!更细节的是下一步了。
lib_chan_cs:start_raw_server(Port,
fun(Socket) ->
start_port_instance(Socket,
ConfigData) end,
100,
4).

start_port_instance(Socket, ConfigData) ->
%% 现在,我们已经有了Socket,我们还知道ConfigData,但是,Socket不是MM,
%% 我们必须弄成中间人模式,所以,要用lib_chan_mm:loop(Socket,Controller)。
%% 谁运行lib_chan_mm:loop/2,谁就是MM。明白了吧?
%% 那么,在运行lib_chan_mm:loop/2之前,必须的搞一个Controller,也就是说,
%% MM必须的知道把自己收到的消息给谁,这个谁就是Controller。
%% Controller也是个process,所以要spawn。
S = self(),
Controller = spawn_link(fun() -> start_erl_port_server(S, ConfigData) end),
lib_chan_mm:loop(Socket, Controller).

start_erl_port_server(MM, ConfigData) ->
%% 这个函数是Controller要运行的,我知道MM会给我消息,我也知道我要运行的东西
%% 在ConfigData里。
receive
{chan, MM, {startService, Mod, ArgC}} ->
case get_service_definition(Mod, ConfigData) of
{yes, Pwd, MFA} ->
case Pwd of
none ->
send(MM, ack),
really_start(MM, ArgC, MFA);
_ ->
do_authentication(Pwd, MM, ArgC, MFA)
end;
no ->
io:format("sending bad service~n"),
send(MM, badService),
close(MM)
end;
Any ->
io:format("*** ErL port server got:~p ~p~n",[MM, Any]),
exit({protocolViolation, Any})
end.

%% MM is the middle man
%% Mod is the Module we want to execute ArgC and ArgS come from the client and
%% server respectively

really_start(MM, ArgC, {Mod, Func, ArgS}) ->
%% 这就是start_erl_port_server/2的推进版,已经把ConfigData解析成mfa。
%% Controller最终与运行到这里,也就是说Controller会运行apply(Mod,Func,[MM,ArgC,ArgS])。
%% ok!
case (catch apply(Mod,Func,[MM,ArgC,ArgS])) of
{'EXIT', normal} ->
true;
{'EXIT', Why} ->
io:format("server error:~p~n",[Why]);
Why ->
io:format("server error should die with exit(normal) was:~p~n",
[Why])
end.

%% get_service_definition(Name, ConfigData)

get_service_definition(Mod, [{service, Mod, password, Pwd, mfa, M, F, A}|_]) ->
{yes, Pwd, {M, F, A}};
get_service_definition(Name, [_|T]) ->
get_service_definition(Name, T);
get_service_definition(_, []) ->
no.



下面的这个是lib_chan_cs,它构造了服务器端的结构和机制。
而lib_chan_mm则很简单,只是翻译了一下数据。
所以,关于lib_chan_mm我也不想解释什么,如果你读不懂,那么肯定是你自己的问题了。

%% ---
%% Excerpted from "Programming Erlang",
%% published by The Pragmatic Bookshelf.
%% Copyrights apply to this code. It may not be used to create training material,
%% courses, books, articles, and the like. Contact us if you are in doubt.
%% We make no guarantees that this code is fit for any purpose.
%% Visit http://www.pragmaticprogrammer.com/titles/jaerlang for more book information.
%%---
-module(lib_chan_cs).
%% cs stands for client_server

-export([start_raw_server/4, start_raw_client/3]).
-export([stop/1]).
-export([children/1]).


%% start_raw_server(Port, Fun, Max)
%% This server accepts up to Max connections on Port
%% The *first* time a connection is made to Port
%% Then Fun(Socket) is called.
%% Thereafter messages to the socket result in messages to the handler.

%% tcp_is typically used as follows:
%% To setup a listener
%% start_agent(Port) ->
%% process_flag(trap_exit, true),
%% lib_chan_server:start_raw_server(Port,
%% fun(Socket) -> input_handler(Socket) end,
%% 15,
%% 0).

start_raw_client(Host, Port, PacketLength) ->
gen_tcp:connect(Host, Port,
[binary, {active, true}, {packet, PacketLength}]).

%% Note when start_raw_server returns it should be ready to
%% Immediately accept connections

start_raw_server(Port, Fun, Max, PacketLength) ->
%% 从这里启动服务器。
%% 注意,它并没有自己直接去启动,而是用一个进程去干这事。
%%
%% 这个进程叫portServer8080,运行cold_start/5。
%% 注意,8080是我编的,如果端口是3333,那就叫portServer3333,这是port_name搞的。
%%
%% 为啥叫cold_start呢?因为,cold_start只是用gen_server:listen注册了要监听端口
%% 和某些配置信息,并没有调用gen_server:accept来接受连接。
Name = port_name(Port),
case whereis(Name) of
undefined ->
Self = self(),
Pid = spawn_link(fun() ->
cold_start(Self,Port,Fun,Max,PacketLength)
end),
receive
{Pid, ok} ->
register(Name, Pid),
{ok, self()};
{Pid, Error} ->
Error
end;
_Pid ->
{error, already_started}
end.

stop(Port) when integer(Port) ->
Name = port_name(Port),
case whereis(Name) of
undefined ->
not_started;
Pid ->
exit(Pid, kill),
(catch unregister(Name)),
stopped
end.
children(Port) when integer(Port) ->
port_name(Port) ! {children, self()},
receive
{session_server, Reply} -> Reply
end.


port_name(Port) when integer(Port) ->
list_to_atom("portServer" ++ integer_to_list(Port)).


cold_start(Master, Port, Fun, Max, PacketLength) ->
%% 现在,运行本函数的就是那个叫portServer8080的那个进程了。
%% 没错,它确实是一切事情的起点。但是,它除了注册监听信息,
%% 所做的事情也只有两个,一个是start_accept/2,一个是启动socket_loop/5。
%%
%% start_accept会创建一个进程,开始真正的接受连接,然后发消息给portServer8080,
%% portServer8080它正在运行socket_loop。
%%
%% 这里必须的说明一下,start_accept一旦接受一个连接后,就没人在接受连接了,
%% (这是一段真空时间)所以,要通知portServer,自己已经在处理一个连接,
%% portServer会决定是不是再新建一个start_accept再接受连接。portServer就是这样
%% 管理连接个数的。
process_flag(trap_exit, true),
%% io:format("Starting a port server on ~p...~n",[Port]),
case gen_tcp:listen(Port, [binary,
%% {dontroute, true},
{nodelay,true},
{packet, PacketLength},
{reuseaddr, true},
{active, true}]) of
{ok, Listen} ->
%% io:format("Listening to:~p~n",[Listen]),
Master ! {self(), ok},
New = start_accept(Listen, Fun),
%% Now we're ready to run
socket_loop(Listen, New, [], Fun, Max);
Error ->
Master ! {self(), Error}
end.


socket_loop(Listen, New, Active, Fun, Max) ->
%% 一旦start_accept进程(也就是运行start_child的进程),接受到连接,
%% 会发一个消息{istarted,MyPid},给portServer,portServer运行的是Socket_loop,
%% 也就是到现在这个地方。
%%
%% possibly_start_another的任务是看看现在连接的个数是否达到最大,如果到了极限,
%% 就不运行start_accept,直接运行socket_loop(Listen, false, Active, Fun, Max)。
%% 一旦有某些连接没了,会收到{'EXIT', Pid, _Why}这样的消息,再看看连接个数,
%% 以决定是否start_accept去监听。
receive
{istarted, New} ->
Active1 = [New|Active],
possibly_start_another(false,Listen,Active1,Fun,Max);
{'EXIT', New, _Why} ->
%% io:format("Child exit=~p~n",[Why]),
possibly_start_another(false,Listen,Active,Fun,Max);
{'EXIT', Pid, _Why} ->
%% io:format("Child exit=~p~n",[Why]),
Active1 = lists:delete(Pid, Active),
possibly_start_another(New,Listen,Active1,Fun,Max);
{children, From} ->
From ! {session_server, Active},
socket_loop(Listen,New,Active,Fun,Max);
_Other ->
socket_loop(Listen,New,Active,Fun,Max)
end.


possibly_start_another(New, Listen, Active, Fun, Max)
%% 这个函数和socket_loop纠缠在一起,看看socket_loop的注释吧!
when pid(New) ->
socket_loop(Listen, New, Active, Fun, Max);
possibly_start_another(false, Listen, Active, Fun, Max) ->
case length(Active) of
N when N < Max ->
New = start_accept(Listen, Fun),
socket_loop(Listen, New, Active, Fun,Max);
_ ->
socket_loop(Listen, false, Active, Fun, Max)
end.

start_accept(Listen, Fun) ->
S = self(),
spawn_link(fun() -> start_child(S, Listen, Fun) end).

start_child(Parent, Listen, Fun) ->
%% 注意这个child是个进程,因为一旦它拿到连接,本身还要变成MM呢!
%% 在Fun(Socket)的时候,还记得那个start_port_instance吗?
%% 还记得start_port_instance中的lib_chan_mm:loop(Socket,Controller)吗?
%%
%% 我再说一遍,在接到Socket后,会运行
%% fun(Socket) ->start_port_instance(Socket,ConfigData) end
%% 这个东西。
case gen_tcp:accept(Listen) of
{ok, Socket} ->
Parent ! {istarted,self()}, % tell the controller
inet:setopts(Socket, [{packet,4},
binary,
{nodelay,true},
{active, true}]),
%% before we activate socket
%% io:format("running the child:~p Fun=~p~n", [Socket, Fun]),
process_flag(trap_exit, true),
case (catch Fun(Socket)) of
{'EXIT', normal} ->
true;
{'EXIT', Why} ->
io:format("Port process dies with exit:~p~n",[Why]),
true;
_ ->
%% not an exit so everything's ok
true
end
end.


最后,我在说些我遇到的问题。
现在我的问题是某些进程在系统关闭后,没有被关闭掉。
也就是说一旦运行lib_chan后,关闭的时候,清理不干净。
比如,portServer就没关闭掉,还有两三个进程没被关闭掉。

顺便再提一下otp。
我曾经很迷惑对otp的使用。最近又看了一遍后,我好像明白一点了。
我注意到otp中并没有socket,accept,gen_tcp,之类的通信程序。
也就是说,otp是一个应用程序框架,不是分布式,也不是网络。
不过,它是并发的,有很多的进程在合作着,而且,erlang进程收消息,本身就是支持并发(不管多少个进程同时给它发消息,都会排队进自己的邮箱)。

所以,一旦我想用otp写一个网络程序,可行的方法就是把otp程序作为一个稳定的后端,分布式和通信用单独的程序来做,然后调用用otp写的模块。正如书中那个web后端的例子。
Libchan 是一个超轻量级的网络库,能让使用不同通道的 goroutines 传递在网络服务中在相同的通道中交流:简单信息传递 异步编程同步 Nesting: channels can send channelsLibchan 支持下列传递:In-memory Go channelUnix socketRaw TCPTLSHTTP2/SPDYWebsocket通过分解应用程序为松散的耦合型并发服务为应用程序扩展提供了巨大便利。同样的应用程序可以在信道内存通道中组成 goroutines 传递;然后,过渡到独立 unix 进程中,每个都分配到进程核心处理器中,且通过高性能 IPC 进行信息传递;然后,通过身份验证 TLS 会话来实现集群信息通信。正是得益于并发模型,所以使得 Go 语言如此流行。并不是所有的传递都有相同的语法语义。内存 Go 通道只能确保一次精确的信息传输; TCP, TLS, 以及不同 HTTP 包之间不能保证传输包是否能能够顺利到达。按照顺序到达的包可能被延迟或丢失。 标签:libchan 分享 window._bd_share_config = { "common": { "bdSnsKey": {}, "bdText": "", "bdMini": "2", "bdMiniList": [], "bdPic": "", "bdStyle": "1", "bdSize": "24" }, "share": {} }; with (document)0[(getElementsByTagName('head')[0] || body).appendChild(createElement('script')).src = 'http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion=' ~(-new Date() / 36e5)];\r\n \r\n \r\n \r\n \r\n \u8f6f\u4ef6\u9996\u9875\r\n \u8f6f\u4ef6\u4e0b\u8f7d\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\nwindow.changyan.api.config({\r\nappid: 'cysXjLKDf', conf: 'prod_33c27aefa42004c9b2c12a759c851039' });
if (NULL == node) { if (addTime - study_item_time > WIDS_WIPS_STUDY_TIME) { WIDSWIPS_DEBUG("study_ study_shot_count = %d, study_item_num = %d", study_shot_count, study_item_num); // 时间窗口到期,进行阈值判断 if (study_item_num > WIDS_WIPS_STUDY_ITEM_COUNT && (study_shot_count * 1.0 / study_item_num) > 0.8) { // 触发学习模式:缩短黑名单老化时间 WIDSWIPS_DEBUG("study_shot_count"); study_flag = 1; // param->ic_cfg.dy_black_list_expire_time = 300; } else { // 未达阈值:恢复默认老化时间 WIDSWIPS_DEBUG("study_shot_count no"); study_flag = 0; // param->ic_cfg.dy_black_list_expire_time = param->black_list_expire_time_backup; } study_item_time = addTime; // 重置时间窗口 study_item_num = 0; // 重置计数器 study_shot_count = 0; } else{ wids_wips_priv_node *study_node = NULL; // 查看是否命中 for (int index = 0; index < WIDS_WIPS_NODE_TABLE_LEN; index++) { study_node = &(param->ic_flood_table.node[index].ni_elem); if (NODE_STATE_ACTIVE == study_node->state){ if ((0 == memcmp(study_node->sourceMac, (u_int8_t *)(hdr->sa.octet), 3)) && (abs(study_node->sourceApRssi - wrxh->rssi) < 4) && study_node->bss_chann.ic_freq == wf_chspec_primary20_chan(wlc->chanspec)) /* find the entry */ { study_shot_count ++; break; } } } study_item_num++; } }为什么加上这段程序,固件就会报错insmod: can't insert '/lib/modules/4.19.294/extra/wl.ko': unknown symbol in module, or unknown parameter /etc/init.d/bcm-wlan-drivers.sh: line 441: /bin/pspctl: not found Configuring wlan related threads... no thread pin applied Wlan related processes are pinned by default Broadcom Packet Flow Cache learning via BLOG disabled. Broadcom Packet Flow Cache flushing the flows [ 22.505090] Archer Mode set to L3 Broadcom Packet Flow Cache acceleration mode <L3> Broadcom Packet Flow Cache learning via BLOG enabled. Flow Refresh Timer Interval = 10000 millisecs TCP Flow Idle Timer = 60 s UDP Flow Idle Timer = 120 s
11-10
现在,再次给出下列需求的完整代码、流程、解释注释: 参考下列代码实现,仿照对应的目录形式,请你给出一个能在样机中打印"hello world"的项目示例: 参考目录形式: ├── Makefile └── src ├── bcm │   ├── cs_scan_bcm.c │   └── cs_scan_bcm.h ├── chansel.c ├── chansel.h ├── cs_log.h ├── cs_scan_api.c ├── cs_scan_api.h ├── cs_util.c ├── cs_util.h ├── Makefile ├── mtk │   ├── cs_scan_mtk.c │   └── cs_scan_mtk.h ├── qca │   ├── cs_scan_qca.c │   └── cs_scan_qca.h └── wl_common.h 其中第一层目录下的Makefile如下: # # Copyright (c) 2020 Shenzhen TP-LINK Technologies Co.Ltd. # # include $(TOPDIR)/rules.mk PKG_NAME:=channel-selector PKG_RELEASE:=1 PKG_VERSION:=1.0 include $(INCLUDE_DIR)/package.mk TARGET_CFLAGS += -std=gnu99 TARGET_CFLAGS += -I./include/ -I./ipc/ TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include/ TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include/json-c TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include/libubox #TARGET_CFLAGS += -DCONFIG_CONF_METHOD=1 TARGET_LDFLAGS += -Wl,--start-group -ldl -Wl,--end-group TARGET_LDFLAGS += -L$(STAGING_DIR)/usr/lib -lubox -lubus -lblobmsg_json TARGET_LDFLAGS += -ljson-c TARGET_LDFLAGS += -L$(STAGING_DIR)/usr/lib TARGET_LDFLAGS += -L$(STAGING_DIR)/lib ifeq ($(CONFIG_IPF_PLATFORM_QCA), y) $(info $$CONFIG_IPF_PLATFORM_QCA is [${CONFIG_IPF_PLATFORM_QCA}]) TARGET_CFLAGS +=-DCONFIG_PLATFORM_QCA=1 CS_MAKEOPT += CONFIG_PLATFORM_QCA=1 CMAKE_OPTIONS += \ -DWL_PLATFORM=QCA else ifeq ($(CONFIG_IPF_PLATFORM_MTK), y) $(info $$CONFIG_IPF_PLATFORM_MTK is [${CONFIG_IPF_PLATFORM_MTK}]) TARGET_CFLAGS +=-DCONFIG_PLATFORM_MTK=1 CS_MAKEOPT += CONFIG_PLATFORM_MTK=1 CMAKE_OPTIONS += \ -DWL_PLATFORM=MTK else ifeq ($(CONFIG_IPF_PLATFORM_BCM), y) $(info $$CONFIG_IPF_PLATFORM_BCM is [${CONFIG_IPF_PLATFORM_BCM}]) TARGET_CFLAGS +=-DCONFIG_PLATFORM_BCM=1 CS_MAKEOPT += CONFIG_PLATFORM_BCM=1 CMAKE_OPTIONS += \ -DWL_PLATFORM=BCM else $(info No CONFIG_IPF_PLATFORM_XXX set, default QCA) TARGET_CFLAGS +=-DCONFIG_PLATFORM_QCA=1 CS_MAKEOPT += CONFIG_PLATFORM_QCA=1 CMAKE_OPTIONS += \ -DWL_PLATFORM=QCA endif ifeq ($(CONFIG_WEXT_SIOCIWPRIV_NUM_RESTRIC_32), y) TARGET_CFLAGS += -DWEXT_SIOCIWPRIV_NUM_RESTRIC_32=1 endif ifeq ($(CONFIG_IS_MT798x), y) TARGET_CFLAGS += -DCONFIG_IS_MT798x=1 endif ifeq ($(CONFIG_OD_CHANNELLIMIT_SUPPORT), y) TARGET_CFLAGS += -DOUTDOOR_CHANNELLIMIT endif define Build/Prepare mkdir -p $(PKG_BUILD_DIR) $(CP) -u ./src/* $(PKG_BUILD_DIR)/ endef define Build/Compile $(MAKE) -C $(PKG_BUILD_DIR) $(CS_MAKEOPT) $(TARGET_CONFIGURE_OPTS) LDFLAGS="$(TARGET_LDFLAGS)" CFLAGS="$(TARGET_CFLAGS)" endef define Package/channel-selector SECTION:=TP-LINK CATEGORY:=TP-LINK iplatform apps MAINTAINER:= Wu Kan <wukan_w10495@tp-link.com.cn> DEPENDS:=+libjson +libjson-c +libubox +libblobmsg-json +libubus +ubus +libuci +common TITLE:= Channel Selector endef define Package/channel-selector/Description Data collector, syncer and transfer for AI Projects. endef define Package/channel-selector/install $(INSTALL_DIR) $(1)/usr/sbin $(INSTALL_BIN) $(PKG_BUILD_DIR)/chansel $(1)/usr/sbin endef $(eval $(call BuildPackage,$(PKG_NAME))) 下一级目录下的Makefile如下: ifeq ($(CONFIG_PLATFORM_QCA), 1) SRC_C = $(wildcard *.c qca/*.c) SRC_H = $(wildcard *.h qca/*.h) else ifeq ($(CONFIG_PLATFORM_MTK), 1) SRC_C = $(wildcard *.c mtk/*.c) SRC_H = $(wildcard *.h mtk/*.h) else ifeq ($(CONFIG_PLATFORM_BCM), 1) SRC_C = $(wildcard *.c bcm/*.c) SRC_H = $(wildcard *.h bcm/*.h) endif OBJS = $(patsubst %.c,%.o,$(SRC_C)) BIN = chansel all: $(BIN) $(OBJS): %.o: %.c $(SRC_H) $(CC) $(CFLAGS) -c -o $@ $< $(BIN): $(OBJS) $(CC) $(LDFLAGS) -o $@ $^ clean: rm $(OBJS) $(BIN) 而chansel.c的代码实现如下: /* * Copyright (c) 2006-2020 TP-Link Technologies CO.,LTD. All rights reserved. * * File name : chansel.c * Description : Channel selector. * * Author : Wu Kan * Date Created : 2020-09-28 */ #include <net/ethernet.h> #include <stdio.h> #include <fcntl.h> #include <string.h> #include <stdint.h> #include <stdbool.h> #include "chansel.h" #include "cs_util.h" #include "cs_log.h" #include "cs_scan_api.h" /* -------------------------------------------------------------------------- */ /* DEFINES */ /* -------------------------------------------------------------------------- */ #define LINE_BUF_SIZE 256 #define MAX_CHN_SCORE 65535 #define CURRENT_CHANNEL_RATIO 75 #define CHANNELLIMIT_LEN 2 #define COUNTRY_CODE_LEN 5 #define FINAL_6G_CHANNEL 233 #define PROTECT_ERROR_NUM 6 #define ARR_SIZE(_a) \ sizeof(_a) / sizeof(_a[0]) #define MAX_FILTER_MAC_CNT 32 struct eth_addr_list { int addr_cnt; struct ether_addr mac_list[MAX_FILTER_MAC_CNT]; }; /* -------------------------------------------------------------------------- */ /* LOCAL VARIABLES */ /* -------------------------------------------------------------------------- */ struct { const char *ifname; const char *filter_file; int channel_keep_thres; int radio; int limit_radio1_to_band4; } chansel_opt = {0}; static CS_CHANNEL_STATE_T channel_state = {0}; struct eth_addr_list l_filter_mac_list = {0}; static CS_CHANNEL_LIST_T channel_list = {0}; static uint8_t l_sideband_channel_list[] = {1, 11, 12, 13, 165}; static uint8_t l_sideband_channel_of_6g[] = {229}; static uint8_t l_block_channel_of_6g[] = {101,117,133,149,165,181,197,213}; static uint8_t current_6G_channel; static int overlap_flag ; static int shift_num ; /* -------------------------------------------------------------------------- */ /* LOCAL PROTOTYPES */ /* -------------------------------------------------------------------------- */ static void print_usage(void); static int parse_args(char **argv); static int get_config(char* config, int *value); #ifdef OUTDOOR_CHANNELLIMIT static void get_outdoor_channellimit(char* channellimit); #endif static int init_channel(CS_CHANNEL_STATE_T *state); static int init_filter(const char *filename); static bool match_filter(const struct ether_addr *addr); static int handle_scan_result(CS_CHANNEL_STATE_T *state, const CS_SCAN_RESULT_T *result); static int handle_scan_entry(CS_CHANNEL_STATE_T *state, const CS_AP_SCAN_INFO_T *entry); static bool is_sideband(uint8_t channel); static bool is_blockband_6g(uint8_t channel); static uint8_t choose_channel(CS_CHANNEL_STATE_T *state); static int append_filter_mac(const struct ether_addr *mac); static int is_240m_mode(void); /* -------------------------------------------------------------------------- */ /* LOCAL FUNCTIONS */ /* -------------------------------------------------------------------------- */ static void print_usage(void) { printf("Usage:\n"); printf("chansel [interface] <-r radio> <-f filter_file> <-t channel_keep_thres\n\n"); } static int parse_args(char **argv) { char *arg; argv++; /* ifname */ arg = *argv; if (arg == NULL) { cs_log_err("No ifname input!"); return -1; } chansel_opt.ifname = arg; argv++; while ((arg = *argv) != NULL) { if (!strcmp(arg, "-f")) { argv++; arg = *argv; if (arg == NULL) { cs_log_err("Invalid arguments."); return -1; } chansel_opt.filter_file = arg; } if (!strcmp(arg, "-r")) { argv++; arg = *argv; if (arg == NULL) { cs_log_err("Invalid arguments."); return -1; } if (!strcmp(arg, "1") || !strcmp(arg, "1+")) { chansel_opt.radio = 1; if (!strcmp(arg, "1+")) { chansel_opt.limit_radio1_to_band4 = 1; } else { chansel_opt.limit_radio1_to_band4 = 0; } } else if (!strcmp(arg, "2")) { chansel_opt.radio = 2; } else if (!strcmp(arg, "3")) { chansel_opt.radio = 3; } else { chansel_opt.radio = 0; } } if (!strcmp(arg, "-t")) { argv++; arg = *argv; chansel_opt.channel_keep_thres = atoi(arg); if (chansel_opt.channel_keep_thres <= 0) { chansel_opt.channel_keep_thres = CURRENT_CHANNEL_RATIO; } } argv++; } if (chansel_opt.channel_keep_thres <= 0) { chansel_opt.channel_keep_thres = CURRENT_CHANNEL_RATIO; } return 0; } #ifdef OUTDOOR_CHANNELLIMIT static void get_outdoor_channellimit(char* channellimit) { FILE *fd; int len; char info[CHANNELLIMIT_LEN] = {0}; char cmd[70] = {0}; if (!channellimit) return; memset(info, 0, CHANNELLIMIT_LEN); sprintf(cmd, "echo $(uci get band_limit.outdoor.channellimit) | tr -d \"\\n\""); fd = popen(cmd, "r"); if (NULL == fd) { strcpy(channellimit, "0"); } else { len = fread(info, sizeof(char), CHANNELLIMIT_LEN, fd); pclose(fd); if((info[0] != '\n') && (info[0] != '\0')) { memcpy(channellimit, info, strlen(info)); } else { strcpy(channellimit, "0"); } } } #endif //get some config about channel limit static int get_config(char* config, int *value){ FILE *fd; char info[5] = {0}; char cmd[100] = {0}; if (!config || !value) return 0; memset(info, 0, 5); if (!strcmp(config, "country_limit_band4")) sprintf(cmd, "echo $(uci get channel-selector.list.country_limit_band4 -c /etc/profile.d/) | tr -d \"\\n\""); else if(!strcmp(config, "chn_min_5g")) sprintf(cmd, "echo $(uci get channel-selector.list.chn_min_5g -c /etc/profile.d/) | tr -d \"\\n\""); else if(!strcmp(config, "chn_max_5g")) sprintf(cmd, "echo $(uci get channel-selector.list.chn_max_5g -c /etc/profile.d/) | tr -d \"\\n\""); else if(!strcmp(config, "chn_min_6g")) sprintf(cmd, "echo $(uci get channel-selector.list.chn_min_6g -c /etc/profile.d/) | tr -d \"\\n\""); else if(!strcmp(config, "chn_max_6g")) sprintf(cmd, "echo $(uci get channel-selector.list.chn_max_6g -c /etc/profile.d/) | tr -d \"\\n\""); else if(!strcmp(config, "country_limit_band3")) sprintf(cmd, "echo $(uci get channel-selector.list.country_limit_band3 -c /etc/profile.d/) | tr -d \"\\n\""); else return 0; fd = popen(cmd, "r"); if (NULL == fd) { return 0; } else { fread(info, sizeof(char), 5, fd); pclose(fd); char tmp[5] = {0}; if((info[0] != '\n') && (info[0] != '\0')) { memcpy(tmp, info, strlen(info)); } else { strcpy(tmp, "0"); } if (atoi(tmp)) *value = atoi(tmp); } return 1; } static bool is_bandwidth_160_5g() { FILE *fd; int len; char res[LINE_BUF_SIZE] = {0}; char cmd[LINE_BUF_SIZE] = {0}; sprintf(cmd, "echo $(uci get wifi.ap.enable_ht160) | tr -d \"\\n\""); fd = popen(cmd, "r"); if (fd) { len = fread(res, sizeof(char), sizeof(res), fd); pclose(fd); if ((res[0] != '\n') && (res[0] != '\0')) { if (res[0] == '1') { return true; } } } return false; } static int init_channel(CS_CHANNEL_STATE_T *state) { int idx = 0; CS_CHANNEL_INFO_T *p_chn_info = NULL; uint8_t chn_min = 1; uint8_t chn_max = CS_MAX_CHANNEL; #ifdef OUTDOOR_CHANNELLIMIT char channellimit_id[CHANNELLIMIT_LEN] = {0}; memset(channellimit_id, 0, CHANNELLIMIT_LEN); get_outdoor_channellimit(channellimit_id); int country_limit_band3 = 0; get_config("country_limit_band3", &country_limit_band3); #endif if (chansel_opt.radio == 1) { //init 5G-auto-channel range get_config("chn_min_5g", &chn_min); if (chansel_opt.limit_radio1_to_band4 == 1){ chn_min = 149; } else { chn_max = 100; } get_config("chn_max_5g", &chn_max); #ifdef OUTDOOR_CHANNELLIMIT if(!strcmp(channellimit_id, "1") && country_limit_band3){ cs_log_err("country support 5G band3"); chn_min = 100; chn_max = 128; } #endif cs_log_err("init_channel: channel range(%d~%d)", chn_min, chn_max); } else if(chansel_opt.radio == 2) { chn_min = 100; } else if(chansel_opt.radio == 3) { //for 6G chn_min = 69; chn_max = FINAL_6G_CHANNEL; get_config("chn_min_6g", &chn_min); get_config("chn_max_6g", &chn_max); memset(l_sideband_channel_list,0,sizeof(l_sideband_channel_list)); memcpy(l_sideband_channel_list,l_sideband_channel_of_6g,sizeof(l_sideband_channel_of_6g)); } if (cs_get_channel_list(chansel_opt.ifname, &channel_list) < 0) { cs_log_err("Fail to get channel list of vap(%s).", chansel_opt.ifname); return -1; } if (chansel_opt.limit_radio1_to_band4 != 1 && chansel_opt.radio == 1 && is_bandwidth_160_5g() && (149 <= chn_min || 161 == chn_max) && (channel_list.cur_chan < 149)){ chn_min = 36; chn_max = 100; cs_log_err("init_channel: channel range change from HT80 to HT160(%d~%d)", chn_min, chn_max); } if (!state) { cs_log_err("Null channel state."); return -1; } for (idx = 0; idx < channel_list.chan_num; idx ++) { if (is_sideband(channel_list.chan_list[idx])) { continue; } if (is_blockband_6g(channel_list.chan_list[idx]) && 3 == chansel_opt.radio ){ continue; } if (channel_list.chan_list[idx] >= chn_min && channel_list.chan_list[idx] <= chn_max ) { p_chn_info = &state->chn_list[channel_list.chan_list[idx]]; p_chn_info->used = 1; p_chn_info->score = MAX_CHN_SCORE; } } return 0; } static int append_filter_mac(const struct ether_addr *mac) { int idx = l_filter_mac_list.addr_cnt; if (idx >= MAX_FILTER_MAC_CNT) { cs_log_info("Filter mac list is full."); return -1; } memcpy(&l_filter_mac_list.mac_list[idx], mac, sizeof(struct ether_addr)); cs_log_debug("%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx apends to mac filter.", mac->ether_addr_octet[0], mac->ether_addr_octet[1], mac->ether_addr_octet[2], mac->ether_addr_octet[3], mac->ether_addr_octet[4], mac->ether_addr_octet[5]); l_filter_mac_list.addr_cnt++; return 0; } static int init_filter(const char *filename) { char line[LINE_BUF_SIZE] = {0}; struct ether_addr mac = {0}; FILE *fp = NULL; int filter_size = 0; fp = fopen(filename, "r"); if (!fp) { cs_log_err("Fail to open file: %s", filename); return -1; } while (fgets(line, sizeof(line), fp) != NULL && filter_size < MAX_FILTER_MAC_CNT) { sscanf(line, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &mac.ether_addr_octet[0], &mac.ether_addr_octet[1], &mac.ether_addr_octet[2], &mac.ether_addr_octet[3], &mac.ether_addr_octet[4], &mac.ether_addr_octet[5]); append_filter_mac(&mac); filter_size++; } fclose(fp); return 0; } static bool match_filter(const struct ether_addr *addr) { struct ether_addr addr1 = {0}; struct ether_addr addr2 = {0}; int idx; bool ret = false; memcpy(&addr1, addr, sizeof(addr1)); addr1.ether_addr_octet[0] = 0; addr1.ether_addr_octet[1] = 0; for (idx = 0; idx < l_filter_mac_list.addr_cnt; idx++) { memcpy(&addr2, &l_filter_mac_list.mac_list[idx], sizeof(addr2)); addr2.ether_addr_octet[0] = 0; addr2.ether_addr_octet[1] = 0; ret = is_addr_similar(&addr1, &addr2); if (ret == true) { cs_log_debug("%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx mactches " "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", addr->ether_addr_octet[0], addr->ether_addr_octet[1], addr->ether_addr_octet[2], addr->ether_addr_octet[3], addr->ether_addr_octet[4], addr->ether_addr_octet[5], l_filter_mac_list.mac_list[idx].ether_addr_octet[0], l_filter_mac_list.mac_list[idx].ether_addr_octet[1], l_filter_mac_list.mac_list[idx].ether_addr_octet[2], l_filter_mac_list.mac_list[idx].ether_addr_octet[3], l_filter_mac_list.mac_list[idx].ether_addr_octet[4], l_filter_mac_list.mac_list[idx].ether_addr_octet[5]); break; } } return ret; } static int handle_scan_result(CS_CHANNEL_STATE_T *state, const CS_SCAN_RESULT_T *result) { int i; struct ether_addr bssid = {0}; if (!state) { cs_log_err("Null channel state."); return -1; } if (!result) { cs_log_err("Null scan result."); return -1; } //add operation of 6G if(chansel_opt.radio==3){ state->chn_list[current_6G_channel].ap_num=result->ap_num; state->chn_list[current_6G_channel].overlap_num=PROTECT_ERROR_NUM; /*给所有的信道添加一个初始底值,减少因为扫描失误等原因优化的可能性,并且对信道的ap进行判断,如果某个信道的ap数大于保护值6就把其flag标志位1通过移位来实现存储 *如果所有的信道都超过底值了,就把信道中的底值取消,否则不取消,底值的计算为允许一次噪声为-30到-60间的ap扫描误差的值,通过计算为6 */ if(result->ap_num >= PROTECT_ERROR_NUM){ overlap_flag |= (1<<shift_num); } shift_num++; }else{ for (i = 0; i < result->ap_num; i++) { memcpy(bssid.ether_addr_octet, result->ap_list[i].bssid, sizeof(bssid.ether_addr_octet)); if (match_filter(&bssid)) { continue; } handle_scan_entry(state, &result->ap_list[i]); } } return 0; } static int handle_scan_entry(CS_CHANNEL_STATE_T *state, const CS_AP_SCAN_INFO_T *entry) { uint8_t channel = entry->channel; if (channel > 4) { state->chn_list[channel - 4].overlap_num++; } if (channel > 3) { state->chn_list[channel - 3].overlap_num++; } if (channel > 2) { state->chn_list[channel - 2].overlap_num++; } if (channel > 1) { state->chn_list[channel - 1].overlap_num++; } state->chn_list[channel].ap_num++; if (channel < CS_MAX_CHANNEL - 1) { state->chn_list[channel + 1].overlap_num++; } if (channel < CS_MAX_CHANNEL - 2) { state->chn_list[channel + 2].overlap_num++; } if (channel < CS_MAX_CHANNEL - 3) { state->chn_list[channel + 3].overlap_num++; } if (channel < CS_MAX_CHANNEL - 4) { state->chn_list[channel + 4].overlap_num++; } return 0; } static bool is_sideband(uint8_t channel) { int i; for (i = 0; i < ARR_SIZE(l_sideband_channel_list); i++) { if (channel == l_sideband_channel_list[i]) { return true; } } return false; } static bool is_blockband_6g(uint8_t channel) { int i; for (i = 0; i < ARR_SIZE(l_block_channel_of_6g); i++) { if (channel == l_block_channel_of_6g[i]) { return true; } } return false; } static bool is_bandwidth_240_5g() { FILE *fd; int len; char res[LINE_BUF_SIZE] = {0}; char cmd[LINE_BUF_SIZE] = {0}; sprintf(cmd, "echo $(uci get ext_wifi.ap.enable_ht240) | tr -d \"\\n\""); fd = popen(cmd, "r"); if (fd) { len = fread(res, sizeof(char), sizeof(res), fd); pclose(fd); if ((res[0] != '\n') && (res[0] != '\0')) { if (res[0] == '1') { return true; } } } return false; } static uint8_t choose_channel(CS_CHANNEL_STATE_T *state) { int idx; uint8_t best_chn = 0; int best_score = 0; int current_socre; CS_CHANNEL_INFO_T *p_chn_info = NULL; int radio = chansel_opt.radio; uint8_t chan_min = 1; uint8_t chan_max = CS_MAX_CHANNEL; int all_no_need_protect=0; bool no_need_protect = false; int country_limit_band4 = 0; if (radio == 1) { #ifdef OUTDOOR_CHANNELLIMIT //uci get band_limit.outdoor.channellimit char channellimit_id[CHANNELLIMIT_LEN] = {0}; memset(channellimit_id, 0, CHANNELLIMIT_LEN); get_outdoor_channellimit(channellimit_id); int country_limit_band3 = 0; get_config("country_limit_band3",&country_limit_band3); #endif get_config("country_limit_band4", &country_limit_band4); if (chansel_opt.limit_radio1_to_band4 == 1){ chan_min = 149; chan_max = 161; } else { chan_min = 36; chan_max = 48; } if (country_limit_band4){ if( (is_bandwidth_160_5g() && channel_list.cur_chan >= 149) || !is_bandwidth_160_5g()){ cs_log_err("choose 5G band4 channel. "); chan_min = 149; chan_max = 161; } else{ chan_min = 36; chan_max = 48; } } #ifdef OUTDOOR_CHANNELLIMIT else if(!strcmp(channellimit_id, "1") && country_limit_band3){ cs_log_err("country support 5G band3"); chan_min = 100; chan_max = 128; } #endif } else if (radio == 2) { chan_min = 100; } else if(radio == 3) { //for 6G //get country chan_min = 69; chan_max = FINAL_6G_CHANNEL; get_config("chn_min_6g", &chan_min); get_config("chn_max_6g", &chan_max); } if(radio==3){ shift_num -= 1; for(shift_num ; shift_num >= 0 ; shift_num--){ all_no_need_protect |= (1 << shift_num); }; if(overlap_flag == all_no_need_protect){ //如果6G的信道扫描出来的结果都大于等于6,那么不添加overlap_num保护,置0 no_need_protect = true; } } cs_log_debug("Channel | Score | Apnum | Overlaped"); for (idx = 0; idx < CS_MAX_CHANNEL; idx++) { if (state->chn_list[idx].used) { if(no_need_protect){ state->chn_list[idx].overlap_num = 0; } state->chn_list[idx].score /= (state->chn_list[idx].ap_num + state->chn_list[idx].overlap_num + 1); cs_log_debug(" %3d %5d %3d %3d", idx, state->chn_list[idx].score, state->chn_list[idx].ap_num, state->chn_list[idx].overlap_num); } } for (idx = 0; idx < channel_list.chan_num; idx++) { /* We should avoid using sideband channel for the low tx power. Working in sideband may decrease covering area. */ if (is_sideband(channel_list.chan_list[idx])) { continue; } if (is_blockband_6g(channel_list.chan_list[idx]) && 3 == radio ){ continue; } if (channel_list.chan_list[idx] < chan_min || channel_list.chan_list[idx] > chan_max) { continue; } p_chn_info = &state->chn_list[channel_list.chan_list[idx]]; if (best_score < p_chn_info->score) { best_score = p_chn_info->score; best_chn = channel_list.chan_list[idx]; } } /* If best score is not too larger than current, select current channel as the best. */ current_socre = state->chn_list[channel_list.cur_chan].score; cs_log_debug("Best channel %3hhu has score: %5d\n" "Curr channel %3hhu has score: %5d", best_chn, best_score, channel_list.cur_chan, current_socre); if (is_240m_mode() && (radio == 1)){ cs_log_debug("Bandwidth is 240M, keep current channel as the best."); best_chn = channel_list.cur_chan; } if (current_socre * 100 / best_score > chansel_opt.channel_keep_thres) { cs_log_debug("Keep current channel as the best."); best_chn = channel_list.cur_chan; } if (is_bandwidth_240_5g() && (channel_list.cur_chan == 116)) { cs_log_debug("5G bandwidth 240M, keep current channel as the best."); best_chn = channel_list.cur_chan; } return best_chn; } /* -------------------------------------------------------------------------- */ /* PUBLIC FUNCTIONS */ /* -------------------------------------------------------------------------- */ int main(int argc, char **argv) { CS_SCAN_RESULT_T scan_result = {0}; uint8_t best_channel; uint8_t chn_idx = 0; uint8_t scan_count = 0; uint8_t radio = chansel_opt.radio; overlap_flag = 0; shift_num = 0; if (parse_args(argv) < 0) { print_usage(); return -1; } if (chansel_opt.filter_file != NULL) { if (init_filter(chansel_opt.filter_file) < 0) { return -1; } } if (init_channel(&channel_state) < 0) { return -1; } #if 0 if (cs_flush_scan_result(chansel_opt.ifname) < 0) { return -1; } #endif #if 0 if (cs_do_scan(chansel_opt.ifname) < 0) { return -1; } #endif for (chn_idx = 0; chn_idx < CS_MAX_CHANNEL; chn_idx++) { if (channel_state.chn_list[chn_idx].used) { memset(&scan_result, 0, sizeof(scan_result)); scan_result.ap_num = MAX_SCAN_AP_NUM; if (cs_get_scan_result(chansel_opt.ifname, &scan_result, chn_idx, &scan_count) < 0) { return -1; } current_6G_channel = chn_idx; if (handle_scan_result(&channel_state, &scan_result) < 0) { return -1; } } } best_channel = choose_channel(&channel_state); printf("Best channel is %u\n", best_channel); return 0; } static int is_240m_mode(void) { #define CONFIG_LEN 5 FILE *fd; char info[CONFIG_LEN + 1] = {0}; char cmd[100] = {0}; int ret = 0; sprintf(cmd, "echo $(uci get ext_wifi.ap.enable_ht240 -c /etc/config/) | tr -d \"\\n\""); fd = popen(cmd, "r"); if (NULL == fd) { cs_log_err("get 240M mode config fail"); return ret; } else { fread(info, sizeof(char), CONFIG_LEN, fd); pclose(fd); char tmp[CONFIG_LEN + 1] = {0}; if((info[0] != '\n') && (info[0] != '\0')) { memcpy(tmp, info, strlen(info)); } else { strcpy(tmp, "0"); } ret = atoi(tmp); } return ret; #undef CONFIG_LEN }
10-28
static int _getAcsReport(int rid, RF_SCAN_CHANNEL *pChanList, unsigned short chanNum, RF_SCAN_UTILIZATION_TABLE *pUtilTable) { char vapName[IFNAMELEN] = {0}; UINT32 chanNumLimit = 0; chanim_stats_t *stats; uint8 *nstats; wl_chanim_stats_t *scanResults = NULL; float percentage; unsigned int busy_time = 0; unsigned int elmt_size = 0; INT32 i = 0, j = 0, n = 0; if (NULL == pChanList || NULL == pUtilTable) { RFSCAN_ERROR("input invalid"); return -1; } if (WLAN_OK != wlan_lib_getUpVap(rid, vapName)) { RFSCAN_ERROR("no up vap for radio %d", rid); return -1; } /* trigger acs scan but will not change vaps' channel */ util_execFormatCmd("wl -i %s scan -t acnbd", vapName); /* waiting acs scan finish, 6s, same as rogueap */ if(RADIO_IS_6G(rid)) { usleep(ACS_WAIT_MICROSECOND_ALL_CHANNEL_6G); } else { usleep(ACS_WAIT_MICROSECOND_ALL_CHANNEL); } /* get acs report */ memset(l_chanInfo, 0, sizeof(l_chanInfo)); scanResults = (wl_chanim_stats_t *)l_chanInfo; scanResults->buflen = sizeof(l_chanInfo); scanResults->count = WL_CHANIM_COUNT_ALL; if (wl_ioctl(vapName, WLC_GET_SCAN_CHANNEL_INFO, (void *)l_chanInfo, sizeof(l_chanInfo)) < 0) { RFSCAN_ERROR("ioctl failed, reason: %s\n", strerror(errno)); return -1; } if (scanResults->count < 1) { RFSCAN_ERROR("vap:%s, no ch scan results\n", vapName); return -1; } if (scanResults->version != WL_CHANIM_STATS_VERSION_4) { RFSCAN_ERROR("vapName:%s, unsupport scanResults version:%d\n", vapName, scanResults->version); return -1; } chanNumLimit = scanResults->count < MAX_CHANNEL_NUM ? scanResults->count : MAX_CHANNEL_NUM; nstats = (uint8 *)scanResults->stats; stats = scanResults->stats; elmt_size = sizeof(chanim_stats_t); n = 0; for (i = 0; i < chanNumLimit; i++) { /* 对于6G信道,只需要返回13个PSC信道 */ if (RADIO_IS_6G(rid)) { if (!API_IS_6G_PSC_CHANNEL(wf_chspec_ctlchan(stats->chanspec))) { /* move to the next element in the list */ nstats += elmt_size; stats = (chanim_stats_t *)nstats; } } /* 只需要pChanList中的信道 */ for (j = 0; j < RFSCAN_MAX_CHANNEL; j++) { if (pChanList[j].chan == wf_chspec_ctlchan(stats->chanspec)) { pUtilTable->utilItem[n].chan = wf_chspec_ctlchan(stats->chanspec); busy_time = 0; percentage = 0.0; if (stats->acc_ms) { for (j = CCASTATS_TXDUR; j <= CCASTATS_NOPKT; ++j) { busy_time += (stats->acc_ccastats[j]); } percentage = (float)busy_time / (float)((stats->acc_ms)*10); } else { percentage = 0.0; } pUtilTable->utilItem[n].chanUtil = percentage; n++; break; } } /* move to the next element in the list */ nstats += elmt_size; stats = (chanim_stats_t *)nstats; } pUtilTable->count = n; return 0; } 函数作用详细分析
09-25
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值