
处理 MQL5“EA 交易”的 GSM 调制解调器
简介
当前,有相当数量的方式可以对交易账户进行轻松的远程监视:移动终端、推送通知、ICQ 。但都需要互联网连接。本文描述了“EA 交易”的创建程序,即使在移动互联网不可用的情况下,其也允许您通过电话或短信与交易终端保持联系。此外,在与交易服务器失去连接或重新建立连接时,该“EA 交易”将能够通知您。
几乎任何 GSM 调制解调器,以及带调制解调器功能的大多数手机均可实现该目标。例如,我选择 Huawei E1550,该调制解调器是同类产品中应用范围最广的产品之一。此外,在本文结尾处,我们将尝试用旧款手机 Siemens M55(2003 年发布)代替调制解调器,看看会出现什么情况。
但首先讲一下如何从“EA 交易”传送一字节数据到调制解调器。
1. 使用 COM 端口
将调制解调器连接到计算机,并安装所有需要的驱动程序后,您将在系统中看到虚拟 COM 端口。未来所有调制解调器操作将通过该端口进行。因此,为了与调制解调器交换数据,首先需要访问 COM 端口。
图 1. 华为调制解调器连接至 COM3 端口
此时,我们需要一个 DLL 库 TrComPort.dll,在互联网上该库和源文件可一同自由分配 。它用于配置 COM 端口、查询其状态以及收发数据。为此,我们将使用以下函数:
#import "TrComPort.dll" int TrComPortOpen(int portnum); int TrComPortClose(int portid); int TrComPortSetConfig(int portid, TrComPortParameters& parameters); int TrComPortGetConfig(int portid, TrComPortParameters& parameters); int TrComPortWriteArray(int portid, uchar& buffer[], uint length, int timeout); int TrComPortReadArray(int portid, uchar& buffer[], uint length, int timeout); int TrComPortGetQueue(int portid, uint& input_queue, uint& output_queue); #import
必须稍微修改传递数据类型,以与 MQL5 兼容。
TrComPortParameters 结构如下:
struct TrComPortParameters { uint DesiredParams; int BaudRate; // Data rate int DefaultTimeout; // Default timeout (in milliseconds) uchar ByteSize; // Data size(4-8) uchar StopBits; // Number of stop bits uchar CheckParity; // Parity check(0-no,1-yes) uchar Parity; // Parity type uchar RtsControl; // Initial RTS state uchar DtrControl; // Initial DTR state };
大多数设备进行以下设置:8 数据位、无奇偶检验、1 停止位。因此,在所有 COM 端口参数中,仅添加 COM 端口数和数据率到“EA 交易”参数是合理的操作:
input ComPortList inp_com_port_index=COM3; // Choosing the COM port input BaudRateList inp_com_baudrate=_9600bps; // Data rate
COM 端口初始化函数则为如下所示:
//+------------------------------------------------------------------+ //| COM port initialization | //+------------------------------------------------------------------+ bool InitComPort() { rx_cnt=0; tx_cnt=0; tx_err=0; //--- attempt to open the port PortID=TrComPortOpen(inp_com_port_index); if(PortID!=inp_com_port_index) { Print("Error when opening the COM port"+DoubleToString(inp_com_port_index+1,0)); return(false); } else { Print("The COM port"+DoubleToString(inp_com_port_index+1,0)+" opened successfully"); //--- request all parameters, so set all flags com_par.DesiredParams=tcpmpBaudRate|tcpmpDefaultTimeout|tcpmpByteSize|tcpmpStopBits|tcpmpCheckParity|tcpmpParity|tcpmpEnableRtsControl|tcpmpEnableDtrControl; //--- read current parameters if(TrComPortGetConfig(PortID,com_par)==-1) ,bnreturn(false);//read error // com_par.ByteSize=8; //8 bits com_par.Parity=0; //no parity check com_par.StopBits=0; //1 stop bit com_par.DefaultTimeout=100; //100 ms timeout com_par.BaudRate=inp_com_baudrate; //rate - from the parameters of the Expert Advisor //--- if(TrComPortSetConfig(PortID,com_par)==-1) return(false);//write error } return(true); }
成功的初始化之中,PortID 变量将存储 COM 打开端口的标识符。
此处需要注意的是,标识符从零开始编号,所以 COM3 端口标识符为 2。现在端口已打开,我们可以与调制解调器交换数据了。随便提一下,并非只与调制解调器交换数据。从“EA 交易”访问 COM 端口,为擅长用烛的人员带来了巨大创新机遇:您可以将 EA 交易连接到 LED 显示屏或移动文本显示屏,以显示某货币对的净值或市场价格。
使用 TrComPortGetQueue 函数获取 COM 端口接收器和传送器队列中的详细数据。
int TrComPortGetQueue( int portid, // COM port identifier uint& input_queue, // Number of bytes in the input buffer uint& output_queue // Number of bytes in the output buffer );
遇到错误时,其返回错误代码负值。错误代码的详细描述在 TrComPort.dll 库源代码文档中提供。
如果函数在接收缓存中返回非零数据,则需读取。为此,我们使用 TrComPortReadArray 函数:
int TrComPortReadArray( int portid, // Port identifier uchar& buffer[], // Pointer to the buffer to read uint length, // Number of data bytes int timeout // Execution timeout (in ms) );
遇到错误时,其返回错误代码负值。数据字节数量应该与 TrComPortGetQueue 函数返回的值对应。
欲使用默认超时(设置于 COM 端口初始化),需要传递值 -1。
使用 TrComPortWriteArray 函数传送数据到 COM 端口:
int TrComPortWriteArray( int portid, // Port identifier uchar& buffer[], // Pointer to the initial buffer uint length, // Number of data bytes int timeout // Execution timeout (in ms) );
应用示例。对于内容为 "Hello world!"(世界,您好!) 的消息,我们应该发送 "Have a nice day!" (祝您今天过得愉快!)作为回应。
uchar rx_buf[1024]; uchar tx_buf[1024]; string rx_str; int rxn, txn; TrComPortGetQueue(PortID, rxn, txn); if(rxn>0) { //--- data received in the receiving buffer //--- read TrComPortReadArray(PortID, rx_buf, rxn, -1); //--- convert to a string rx_str = CharArrayToString(rx_buf,0,rxn,CP_ACP); //--- check the received message (expected message "Hello world!" if(StringFind(rx_str,"Hello world!",0)!=-1) {//--- if we have a match, prepare the reply string tx_str = "Have a nice day!"; int len = StringLen(tx_str);//get the length in characters //--- convert to uchar buffer StringToCharArray(tx_str, tx_buf, 0, len, CP_ACP); //--- send to the port if(TrComPortWriteArray(PortID, tx_buf, len, -1)<0) Print("Error when writing to the port"); } }
需要特别注意端口关闭函数:
int TrComPortClose( int portid // Port identifier );
该函数必须始终存在于“EA 交易”去初始化程序中。大多数情况下,打开的端口仅在重启系统后再次可用。事实上,即使关闭和开启调制解调器也没用。
2. AT 命令和使用调制解调器
通过 АТ 命令处理调制解调器。曾经通过计算机使用过移动互联网的人应该记得所谓的“调制解调器初始化字符串”,该字符串大致如下:AT+CGDCONT=1、"IP"、"internet"。这就是 AT 命令的一种。它们几乎全部以 AT 前缀开头,以 0x0d 结尾(回车)。
我们将使用执行目标功能所需的最少的 AT 命令集。它会减少确保与不同设备的命令集兼容的工作量。
以下是我们的处理程序使用调制解调器时采用的 AT 命令列表:
命令 | 说明 |
---|---|
ATE1 | 启用回波 |
AT+CGMI | 获取生产商名称 |
AT+CGMM | 获取设备型号 |
AT^SCKS | 获取 SIM 卡状态 |
AT^SYSINFO | 获取系统信息 |
AT+CREG | 获取网络注册状态 |
AT+COPS | 获取当前移动运营商名称 |
AT+CMGF | 在文本/PDU模式之间转换 |
AT+CLIP | 启用调用线路识别 |
AT+CPAS | 获取调制解调器状态 |
AT+CSQ | 获取信号质量 |
AT+CUSD | 发送 USSD 请求 |
AT+CALM | 启用静音模式(手机适用) |
AT+CBC | 获取电池状态(手机适用) |
AT+CSCA | 获取 SMS 服务中心号码 |
AT+CMGL | 获取 SMS 信息列表 |
AT+CPMS | 为 SMS 信息选择内存 |
AT+CMGD | 从内存中删除 SMS 信息 |
AT+CMGR | 从内存中读取 SMS 信息 |
AT+CHUP | 拒绝来电 |
AT+CMGS | 发送 SMS 信息 |
我不会对使用 AT 命令的微妙之处加以描述,以免偏题。在技术论坛上有大量相关信息。此外,万事已备,为了创建可以使用调制解调器的“EA 交易”,我们只需纳入头文件,开始使用准备就绪的函数和结构。我将对此进行详细描述。
2.1. 函数
COM 端口初始化:
bool InitComPort();
返回值:若初始化成功 - true,失败 - false。在调制解调器初始化之前,从 OnInit() 函数调用。
COM 端口去初始化:
void DeinitComPort();
返回值:无。从 OnDeinit() 函数调用。
调制解调器初始化:
void InitModem();
返回值:无。COM 端口成功初始化后,从 OnInit() 函数调用。
调制解调器事件处理程序:
void ModemTimerProc();
返回值:无。以 1 秒间隔从 OnTimer() 函数调用。
从调制解调器内存中按索引读取 SMS 信息:
bool ReadSMSbyIndex( int index, // SMS message index in the modem memory INCOMING_SMS_STR& sms // Pointer to the structure where the message will be moved );
返回值:若读取成功 - true,失败 - false。
从调制解调器内存中按索引删除 SMS 信息:
bool DelSMSbyIndex( int index // SMS message index in the modem memory );
返回值:若删除成功 - true,失败 - false。
连接质量索引转换为字符串:
string rssi_to_str( int rssi // Connection quality index, values 0..31, 99 );
返回值:字符串,例如 "-55 dBm"。
发送 SMS 信息:
bool SendSMS( string da, // Recipient's phone number in international format string text, // Text of the message, Latin characters and numbers, maximum length - 158 characters bool flash // Flash message flag );
返回值:若发送成功 - true,失败 - false。仅可发送拉丁字符编写的 SMS 信息。仅接收 SMS 信息时可支持西里尔字符。如果设置 flash=true,则将发送闪烁信息。
2.2. 事件(调制解调器处理程序调用的函数)
在调制解调器状态结构中更新数据:
void ModemChState();
传递参数:无。当该函数被调制解调器处理程序调用时,说明数据已在调制解调器结构中更新(下面将提供结构描述)。
来电:
void IncomingCall( string number // Caller number );
传递参数:来电号码。当该函数被调制解调器处理程序调用时,说明来自 'number' 的来电被接听和拒绝。
新接收 SMS 信息:
void IncomingSMS( INCOMING_SMS_STR& sms // SMS message structure );
传递参数:SMS 信息结构(下面将提供结构描述)。当该函数被调制解调器处理程序调用时,说明调制解调器内存中有一条或更多未读新 SMS 信息。如果未读信息条数大于 1,最近的信息将传递至该函数。
SMS 内存已满:
void SMSMemoryFull( int n // Number of SMS messages in the modem memory );
传递参数:调制解调器内存中的信息条数。当该函数被调制解调器处理程序调用时,说明 SMS 内存已满,在内存被释放前,调制解调器将不会接收任何新信息。
2.3. 调制解调器参数的状态结构
struct MODEM_STR { bool init_ok; // The required minimum initialized // string manufacturer; // Manufacturer string device; // Model int sim_stat; // SIM status int net_reg; // Network registration state int status; // Modem status string op; // Operator int rssi; // Signal quality string sms_sca; // SMS center number int bat_stat; // Battery state int bat_charge; // Battery charge in percent (applicability depends on bat_stat) // double bal; // Mobile account balance string exp_date; // Mobile number expiration date int sms_free; // Package SMS available int sms_free_cnt; // Counter of package SMS used // int sms_mem_size; // SMS memory size int sms_mem_used; // Used SMS memory size // string incoming; // Caller number }; MODEM_STR modem;
该结构专门由调制解调器事件处理程序填写,在其他函数中使用时为只读结构。
以下为结构元素描述:
元素 | 说明 |
---|---|
modem.init_ok | 表示调制解调器已成功初始化。 初始化完成后,false 初始值变为 true。 |
modem.manufacturer | 调制解调器生产商,例如,“华为”。 初始值为 "n/a"。 |
modem.device | 调制解调器型号,例如,"E1550" 初始值为 "n/a"。 |
modem.sim_stat | SIM 卡状态。可能出现以下值: -1 - 无数据 0 - 卡缺失、被锁或发生故障 1 - 卡可用 |
modem.net_reg | 网络注册状态。可能出现以下值: -1 - 无数据 0 - 未注册 1 - 已注册 2 - 搜索 3 - 禁用 4 - 状态未定义 5 - 漫游注册 |
modem.status | 调制解调器状态。可能出现以下值: -1 - 初始化 0 - 准备就绪 1 - 错误 2 - 错误 3 - 来电 4 - 通话中 |
modem.op | 当前移动运营商。 等同于运营商名称(例如 "MTS UKR"), 或国际运营商代码(例如 "25501")。 初始值为 "n/a"。 |
modem.rssi | 信号质量索引。可能出现以下值: -1 - 无数据 0 - 信号 -113 dBm 或更低 1 - 信号 -111 dBm 2...30 - 信号 -109...-53 dBm 31 - 信号 -51 dBm 或更高 99 - 无数据 。使用 rssi_to_str() 函数转换为字符串。 |
modem.sms_sca | SMS 服务中心号码。其包含于 SIM 卡内存中。 需要生成一个发出的 SMS 信息。 在极少数情况下,如果号码未存储在 SIM 卡内存中,其将被 “EA 交易”输入参数中的指定号码替代。 |
modem.bat_stat | 调制解调器电池状态(仅手机适用)。 可能出现以下值: -1 - 无数据 0 - 电池电源支持设备 1 - 电池可用,但设备未使用电池 2 - 无电池 3 - 错误 |
modem.bat_charge | 以百分比显示电池充电情况。 数值范围为 0 至 100。 |
modem.bal | 移动账户余额。数值 来自运营商对于有关 USSD 请求的响应。 初始值(初始化之前):-10000. |
modem.exp_date | 移动号码失效日期。数值 来自运营商对于有关 USSD 请求的响应。 初始值为 "n/a"。 |
modem.sms_free | SMS 套餐可用数量。 初始数字减去已使用 SMS 套餐数得出可用数量。 |
modem.sms_free_cnt | 已使用 SMS 套餐数。数值 来自运营商对于有关 USSD 请求的响应。初始值为 -1。 |
modem.sms_mem_size | 调制解调器 SMS 内存容量。 |
modem.sms_mem_used | 已用调制解调器 SMS 内存。 |
modem.incoming | 上次来电号码。 初始值为 "n/a"。 |
2.4. SMS 信息结构
//+------------------------------------------------------------------+ //| SMS message structure | //+------------------------------------------------------------------+ struct INCOMING_SMS_STR { int index; //index in the modem memory string sca; //sender's SMS center number string sender; //sender's number INCOMING_CTST_STR scts; //SMS center time label string text; //text of the message };
SMS 中心时间标签为发送者所发出的信息被 SMS 中心接收时的时间。时间标签结构如下:
//+------------------------------------------------------------------+ //| Time label structure | //+------------------------------------------------------------------+ struct INCOMING_CTST_STR { datetime time; // time int gmt; // time zone };
每间隔 15 分钟为一个时区。因此,数值 8 对应 GMT+02:00。
接收到的 SMS 信息文本可用拉丁或西里尔字符编写。接收到的信息支持 7 位和 UCS2 编码。无法合并长信息(鉴于该操作是为短命令而设计)。
仅可发送拉丁字符编写的 SMS 信息。信息最长为 158 个字符。遇到更长的信息,则不会发送超出规定字符的部分。
3. 开发“EA 交易”
首先,需要将 TrComPort.dll 文件复制到 Libraries 文件夹,并将 ComPort.mqh、modem.mqh 和 sms.mqh 放入 Include 文件夹。
然后使用向导创建新的“EA 交易”,添加使用调制解调器所要求的最低配置。它是:
包含 modem.mqh:
#include <modem.mqh>
添加输入参数:
input string str00="COM port settings"; input ComPortList inp_com_port_index=COM3; // Selecting the COM port input BaudRateList inp_com_baudrate=_9600bps; // Data rate // input string str01="Modem"; input int inp_refr_period=3; // Modem query period, sec input int inp_ussd_request_tout=20; // Timeout for response to a USSD request, sec input string inp_sms_service_center=""; // SMS service center number // input string str02="Balance"; input int inp_refr_bal_period=12; // Query period, hr input string inp_ussd_get_balance=""; // Balance USSD request input string inp_ussd_bal_suffix=""; // Balance suffix input string inp_ussd_exp_prefix=""; // Prefix of the number expiration date // input string str03="Number of package SMS"; input int inp_refr_smscnt_period=6; // Query period, hr input string inp_ussd_get_sms_cnt=""; // USSD request for the package service status input string inp_ussd_sms_suffix=""; // SMS counter suffix input int inp_free_sms_daily=0; // Daily SMS limit
调制解调器处理程序调用的函数:
//+------------------------------------------------------------------+ //| Called when a new SMS message is received | //+------------------------------------------------------------------+ void IncomingSMS(INCOMING_SMS_STR& sms) { } //+------------------------------------------------------------------+ //| SMS memory is full | //+------------------------------------------------------------------+ void SMSMemoryFull(int n) { } //+------------------------------------------------------------------+ //| Called upon receiving an incoming call | //+------------------------------------------------------------------+ void IncomingCall(string number) { } //+------------------------------------------------------------------+ //| Called after updating data in the modem status structure | //+------------------------------------------------------------------+ void ModemChState() { static bool init_ok = false; if(modem.init_ok==true && init_ok==false) { Print("Modem initialized successfully"); init_ok = true; } }
COM 端口和调制解调器初始化及设置为 1 秒间隔的计时器需要添加至 OnInit() 函数:
int OnInit() { //---COM port initialization if(InitComPort()==false) { Print("Error when initializing the COM"+DoubleToString(inp_com_port_index+1,0)+" port"); return(INIT_FAILED); } //--- modem initialization InitModem(); //--- setting the timer EventSetTimer(1); //1 second interval // return(INIT_SUCCEEDED); }
OnTimer() 函数中,我们需要调用调制解调器处理程序:
void OnTimer() { //--- ModemTimerProc(); }
在 OnDeinit() 函数中,需要调用 COM 端口去初始化。
void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); DeinitComPort(); }
编译代码并发现:0 错误。
现在,运行“EA 交易”,但记住允许 DLL 导入,并选择与调制解调器相关联的 COM 端口。您将在“EA 交易”选项卡中发现以下信息:
图 2. 成功运行后的“EA 交易”信息
如果获得相同信息,则意味着您的调制解调器(手机)可以使用该“EA 交易”。在此情况下,我们可以继续。
让我们制作一份表格,将调制解调器参数可视化。它将位于终端窗口的左上角,在 OHLC 线下。表格中所使用的文本字体为等宽字体,如 "Courier New"。
//+------------------------------------------------------------------+ //| TextXY | //+------------------------------------------------------------------+ void TextXY(string ObjName,string Text,int x,int y,color TextColor) { //--- display the text string ObjectDelete(0,ObjName); ObjectCreate(0,ObjName,OBJ_LABEL,0,0,0,0,0); ObjectSetInteger(0,ObjName,OBJPROP_XDISTANCE,x); ObjectSetInteger(0,ObjName,OBJPROP_YDISTANCE,y); ObjectSetInteger(0,ObjName,OBJPROP_COLOR,TextColor); ObjectSetInteger(0,ObjName,OBJPROP_FONTSIZE,9); ObjectSetString(0,ObjName,OBJPROP_FONT,"Courier New"); ObjectSetString(0,ObjName,OBJPROP_TEXT,Text); } //+------------------------------------------------------------------+ //| Drawing the table of modem parameters | //+------------------------------------------------------------------+ void DrawTab() { int x=20, //horizontal indent y = 20, //vertical indent dy = 15; //step along the Y-axis //--- draw the background ObjectDelete(0,"bgnd000"); ObjectCreate(0,"bgnd000",OBJ_RECTANGLE_LABEL,0,0,0,0,0); ObjectSetInteger(0,"bgnd000",OBJPROP_XDISTANCE,x-10); ObjectSetInteger(0,"bgnd000",OBJPROP_YDISTANCE,y-5); ObjectSetInteger(0,"bgnd000",OBJPROP_XSIZE,270); ObjectSetInteger(0,"bgnd000",OBJPROP_YSIZE,420); ObjectSetInteger(0,"bgnd000",OBJPROP_BGCOLOR,clrBlack); //--- port parameters TextXY("str0", "Port: ", x, y, clrWhite); y+=dy; TextXY("str1", "Speed: ", x, y, clrWhite); y+=dy; TextXY("str2", "Rx: ", x, y, clrWhite); y+=dy; TextXY("str3", "Tx: ", x, y, clrWhite); y+=dy; TextXY("str4", "Err: ", x, y, clrWhite); y+=(dy*3)/2; //--- modem parameters TextXY("str5", "Modem: ", x, y, clrWhite); y+=dy; TextXY("str6", "SIM: ", x, y, clrWhite); y+=dy; TextXY("str7", "NET: ", x, y, clrWhite); y+=dy; TextXY("str8", "Operator: ", x, y, clrWhite); y+=dy; TextXY("str9", "SMSC: ", x, y, clrWhite); y+=dy; TextXY("str10", "RSSI: ", x, y, clrWhite); y+=dy; TextXY("str11", "Bat: ", x, y, clrWhite); y+=dy; TextXY("str12", "Modem status: ", x, y, clrWhite); y+=(dy*3)/2; //--- mobile account balance TextXY("str13", "Balance: ", x, y, clrWhite); y+=dy; TextXY("str14", "Expiration date: ", x, y, clrWhite); y+=dy; TextXY("str15", "Free SMS: ", x, y, clrWhite); y+=(dy*3)/2; //--- number of the last incoming call TextXY("str16","Incoming: ",x,y,clrWhite); y+=(dy*3)/2; //--- parameters of the last received SMS message TextXY("str17", "SMS mem full: ", x, y, clrWhite); y+=dy; TextXY("str18", "SMS number: ", x, y, clrWhite); y+=dy; TextXY("str19", "SMS date/time: ", x, y, clrWhite); y+=dy; //--- text of the last received SMS message TextXY("str20", " ", x, y, clrGray); y+=dy; TextXY("str21", " ", x, y, clrGray); y+=dy; TextXY("str22", " ", x, y, clrGray); y+=dy; TextXY("str23", " ", x, y, clrGray); y+=dy; TextXY("str24", " ", x, y, clrGray); y+=dy; //--- ChartRedraw(0); }
为刷新表格中的数据,我们将使用 RefreshTab() 函数:
//+------------------------------------------------------------------+ //| Refreshing values in the table | //+------------------------------------------------------------------+ void RefreshTab() { string str; //--- COM port index: str="COM"+DoubleToString(PortID+1,0); ObjectSetString(0,"str0",OBJPROP_TEXT,"Port: "+str); //--- data rate: str=DoubleToString(inp_com_baudrate,0)+" bps"; ObjectSetString(0,"str1",OBJPROP_TEXT,"Speed: "+str); //--- number of bytes received: str=DoubleToString(rx_cnt,0)+" bytes"; ObjectSetString(0,"str2",OBJPROP_TEXT,"Rx: "+str); //--- number of bytes transmitted: str=DoubleToString(tx_cnt,0)+" bytes"; ObjectSetString(0,"str3",OBJPROP_TEXT,"Tx: "+str); //--- number of port errors: str=DoubleToString(tx_err,0); ObjectSetString(0,"str4",OBJPROP_TEXT,"Err: "+str); //--- modem manufacturer and model: str=modem.manufacturer+" "+modem.device; ObjectSetString(0,"str5",OBJPROP_TEXT,"Modem: "+str); //--- SIM card status: string sim_stat_str[2]={"Error","Ok"}; if(modem.sim_stat==-1) str="n/a"; else str=sim_stat_str[modem.sim_stat]; ObjectSetString(0,"str6",OBJPROP_TEXT,"SIM: "+str); //--- network registration: string net_reg_str[6]={"No","Ok","Search...","Restricted","Unknown","Roaming"}; if(modem.net_reg==-1) str="n/a"; else str=net_reg_str[modem.net_reg]; ObjectSetString(0,"str7",OBJPROP_TEXT,"NET: "+str); //--- name of mobile operator: ObjectSetString(0,"str8",OBJPROP_TEXT,"Operator: "+modem.op); //--- SMS service center number ObjectSetString(0,"str9",OBJPROP_TEXT,"SMSC: "+modem.sms_sca); //--- signal level: if(modem.rssi==-1) str="n/a"; else str=rssi_to_str(modem.rssi); ObjectSetString(0,"str10",OBJPROP_TEXT,"RSSI: "+str); //--- battery status (applicable to phones): string bat_stats_str[4]={"Ok, ","Ok, ","No","Err"}; if(modem.bat_stat==-1) str="n/a"; else str=bat_stats_str[modem.bat_stat]; if(modem.bat_stat==0 || modem.bat_stat==1) str+=DoubleToString(modem.bat_charge,0)+"%"; ObjectSetString(0,"str11",OBJPROP_TEXT,"Bat: "+str); //--- modem status: string modem_stat_str[5]={"Ready","Err","Err","Incoming call","Active call"}; if(modem.status==-1) str="init..."; else { if(modem.status>4 || modem.status<0) Print("Unknown modem status: "+DoubleToString(modem.status,0)); else str=modem_stat_str[modem.status]; } ObjectSetString(0,"str12",OBJPROP_TEXT,"Modem status: "+str); //--- mobile account balance: if(modem.bal==-10000) str="n/a"; else str=DoubleToString(modem.bal,2)+" "+inp_ussd_bal_suffix; ObjectSetString(0,"str13",OBJPROP_TEXT,"Balance: "+str); //--- mobile number expiration date: ObjectSetString(0,"str14",OBJPROP_TEXT,"Expiration date: "+modem.exp_date); //--- package SMS available: if(modem.sms_free<0) str="n/a"; else str=DoubleToString(modem.sms_free,0); ObjectSetString(0,"str15",OBJPROP_TEXT,"Free SMS: "+str); //--- SMS memory full: if(sms_mem_full==true) str="Yes"; else str="No"; ObjectSetString(0,"str17",OBJPROP_TEXT,"SMS mem full: "+str); //--- ChartRedraw(0); }
DelTab() 函数删除表格:
//+------------------------------------------------------------------+ //| Deleting the table | //+------------------------------------------------------------------+ void DelTab() { for(int i=0; i<25; i++) ObjectDelete(0,"str"+DoubleToString(i,0)); ObjectDelete(0,"bgnd000"); }
让我们添加处理表格的函数至事件处理程序 OnInit() 和 OnDeinit(),以及 ModemChState() 函数:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- COM port initialization if(InitComPort()==false) { Print("Error when initializing the COM port"+DoubleToString(inp_com_port_index+1,0)); return(INIT_FAILED); } //--- DrawTab(); //--- modem initialization InitModem(); //--- setting the timer EventSetTimer(1);//1 second interval //--- RefreshTab(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); DeinitComPort(); DelTab(); } //+------------------------------------------------------------------+ //| ModemChState | //+------------------------------------------------------------------+ void ModemChState() { static bool init_ok=false; //Print("Modem status changed"); if(modem.init_ok==true && init_ok==false) { Print("Modem initialized successfully"); init_ok=true; } //--- RefreshTab(); }
此外,添加刷新表格中最近来电数量的功能至 IncomingCall() 函数:
//+------------------------------------------------------------------+ //| Called upon receiving an incoming call | //+------------------------------------------------------------------+ void IncomingCall(string number) { //--- update the number of the last incoming call: ObjectSetString(0, "str16",OBJPROP_TEXT, "Incoming: "+number); }
现在,编译代码并运行“EA 交易”。您可以看到终端窗口中的以下报告:
图 3. 调制解调器参数
尝试对调制解调器拨号。来电会被拒绝,您的号码将显示于“来电”行。
4. 使用 USSD 请求
手机账户不按时充值将使“EA 交易”的操作在最不恰当的时候中断。因此,检查账户余额的函数是最重要的函数之一。我们通常使用 USSD 请求检查手机账户余额。此外,还将使用 USSD 请求获得有关 SMS 套餐可用数量的信息。
用于生成请求和处理所接收到的响应的数据存在于输入参数中:
input string str02="=== Balance ======"; input int inp_refr_bal_period=12; //query period, hr input string inp_ussd_get_balance=""; //balance USSD request input string inp_ussd_bal_suffix=""; //balance suffix input string inp_ussd_exp_prefix=""; //prefix of the number expiration date // input string str03="= Number of package SMS =="; input int inp_refr_smscnt_period=6;//query period, hr input string inp_ussd_get_sms_cnt=""; //USSD request for the package service status input string inp_ussd_sms_suffix=""; //SMS counter suffix input int inp_free_sms_daily=0; //daily SMS limit
如果请求数量未指定,请求将得不到处理。或者,请求将正好于调制解调器初始化后发送,并在特定时间间隔之后重复发送。此外,如果调制解调器(手机)不支持相关 IT 命令(涉及到旧的手机型号),请求将得不到处理。
假设在余额请求之后,您从运营商处获得以下响应:
7.13 UAH,失效于 2014 年 05 月 22 日。电话套餐 - Super MTS 3D Null 25。
为确保处理程序正确识别响应,余额后缀必须设置为 "UAH" 以及号码失效日期前缀为“失效于”。
由于准备经常使用 SMS 信息发送“EA 交易”相关信息,所以最好向运营商购买 SMS 套餐,也就是,一种付少量费用便可发送特定数量 SMS 信息的服务。如此情况下,知道 SMS 套餐剩余量就显得非常有用了。可通过 USSD 请求获知剩余量情况。运营商通常会告知已使用的 SMS 条数,而非剩余可用条数。
假设您从运营商处获得以下响应:
余额:今日本地通话时间 69 分钟。今日已用:0 SMS 和 0 MB。
这时,SMS 计数前缀设置为 "SMS",而每日限度应根据 SMS 套餐条款和条件进行设置。例如,如果每日 30 条文本信息,请求返回数值 10,则意味着您有 30-10=20 条 SMS 信息可用。该数字将由处理程序放置于调制解调器状态结构的相应元素中。
注意! 小心处理 USSD 请求数字!发送错误请求可能出现不良后果,例如启用一些不需要的付费服务!
为了让“EA 交易”开始使用 USSD 请求,我们需要指定相关输入参数。
例如,乌克兰移动运营商 MTS Ukraine 的参数如下所示:
图 4. 可用余额 USSD 请求的参数
图 5. SMS 套餐剩余量 USSD 请求的参数
设置与移动运营商相关的数值。之后,您手机账户的可用余额和 SMS 剩余量将在调制解调器状态表格中显示:
图 6. 从 USSD 响应获得的参数
撰写本文时,我的移动运营商向我发送了圣诞节广告,而非失效日期。所以,处理程序无法获得日期值,也这是在“失效日期”行出现 "n/a" 的原因。请注意,所有运营商响应均显示在“EA 交易”选项卡。
图 7. 在“EA 交易”选项卡中显示运营商响应
5. 发送 SMS 信息
我们将开始添加有用函数,例如,发送 SMS 信息说明当前利润、净值和开仓数量。来电将对发送进行初始化。
此类响应仅出现于管理员号码中,因此,我们有另一个输入参数:
input string inp_admin_number="+XXXXXXXXXXXX";//administrator's phone number
号码必须采用国际格式,包括号码前的 "+"。
号码检查,以及 SMS 文本的生成和发送都需要添加来电处理程序:
//+------------------------------------------------------------------+ //| Called upon receiving an incoming call | //+------------------------------------------------------------------+ void IncomingCall(string number) { bool result; if(number==inp_admin_number) { Print("Administrator's phone number. Sending SMS."); // string mob_bal=""; if(modem.bal!=-10000)//mobile account balance mob_bal = "\n(m.bal="+DoubleToString(modem.bal,2)+")"; result = SendSMS(inp_admin_number, "Account: "+DoubleToString(AccountInfoInteger(ACCOUNT_LOGIN),0) +"\nProfit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2) +"\nEquity: "+DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2) +"\nPositions: "+DoubleToString(PositionsTotal(),0) +mob_bal , false); if(result==true) Print("SMS sent successfully"); else Print("Error when sending SMS"); } else Print("Unauthorized number ("+number+")"); //--- update the number of the last incoming call: ObjectSetString(0, "str16",OBJPROP_TEXT, "Incoming: "+number); }
现在,如果有从管理员号码 inp_admin_number 到调制解调器的来电,将发送 SMS 信息作为响应:
图 8. “EA 交易”发送的响应管理员电话号码来电的 SMS 信息
现在,我们可以看到利润和净值的当前值,以及开仓数量和手机账户余额。
6. 监视与交易服务器的连接
让我们在与交易服务器失去连接以及与交易服务器重新连接时添加通知。为此,我们将使用带 TERMINAL_CONNECTED 属性标识符的 TerminalInfoInteger() 每 10 秒钟对交易服务器进行一次检查。
为过滤短时连接损耗,我们将使用需要添加至输入参数列表的延迟:
input int inp_conn_hyst=6; //Hysteresis, х10 sec
数值 6 表示在超过 6*10=60 秒无连接的情况下,认定为失去连接。同样地,如果连接超过 60 秒,将视为重新连接。首次注册的失去连接的本地时间将被视为失去连接时间,连接首次接通的本地时间被视为恢复连接时间。
为此,我们将下述代码添加到 OnTimer() 函数:
static int s10 = 0;//pre-divider by 10 seconds static datetime conn_time; static datetime disconn_time; if(++s10>=10) {//--- once every 10 seconds s10 = 0; // if((bool)TerminalInfoInteger(TERMINAL_CONNECTED)==true) { if(cm.conn_cnt==0) //first successful query in the sequence conn_time = TimeLocal(); //save the time if(cm.conn_cnt<inp_conn_hyst) { if(++cm.conn_cnt>=inp_conn_hyst) {//--- connection has been stabilized if(cm.connected == false) {//--- if there was a long-standing connection loss prior to that cm.connected = true; cm.new_state = true; cm.conn_time = conn_time; } } } cm.disconn_cnt = 0; } else { if(cm.disconn_cnt==0) //first unsuccessful query in the sequence disconn_time = TimeLocal(); //save the time if(cm.disconn_cnt<inp_conn_hyst) { if(++cm.disconn_cnt>=inp_conn_hyst) {//--- long-standing connection loss if(cm.connected == true) {//--- if the connection was stable prior to that cm.connected = false; cm.new_state = true; cm.disconn_time = disconn_time; } } } cm.conn_cnt = 0; } } // if(cm.new_state == true) {//--- connection status changed if(cm.connected == true) {//--- connection is available string str = "Connected "+TimeToString(cm.conn_time,TIME_DATE|TIME_SECONDS); if(cm.disconn_time!=0) str+= ", offline: "+dTimeToString((ulong)(cm.conn_time-cm.disconn_time)); Print(str); SendSMS(inp_admin_number, str, false);//sending message } else {//--- no connection string str = "Disconnected "+TimeToString(cm.disconn_time,TIME_DATE|TIME_SECONDS); if(cm.conn_time!=0) str+= ", online: "+dTimeToString((ulong)(cm.disconn_time-cm.conn_time)); Print(str); SendSMS(inp_admin_number, str, false);//sending message } cm.new_state = false; }
cm 结构如下:
//+------------------------------------------------------------------+ //| Structure of monitoring connection with the terminal | //+------------------------------------------------------------------+ struct CONN_MON_STR { bool new_state; //flag of change in the connection status bool connected; //connection status int conn_cnt; //counter of successful connection queries int disconn_cnt; //counter of unsuccessful connection queries datetime conn_time; //time of established connection datetime disconn_time; //time of lost connection }; CONN_MON_STR cm;//structure of connection monitoring
在 SMS 信息文本中,将声明与交易服务器失去连接的时间(或重新连接),以及连接可用(或不可用)时间(建立连接及失去连接之间的时间差)。为将时间差从秒转换为 dd hh:mm:ss,我们将添加 dTimeToString() 函数:
string dTimeToString(ulong sec) { string str; uint days = (uint)(sec/86400); if(days>0) { str+= DoubleToString(days,0)+" days, "; sec-= days*86400; } uint hour = (uint)(sec/3600); if(hour<10) str+= "0"; str+= DoubleToString(hour,0)+":"; sec-= hour*3600; uint min = (uint)(sec/60); if(min<10) str+= "0"; str+= DoubleToString(min,0)+":"; sec-= min*60; if(sec<10) str+= "0"; str+= DoubleToString(sec,0); // return(str); }
为确保“EA 交易”每次运行时不会发送关于建立连接的文本信息,我们添加 conn_mon_init() 函数,该函数以此方法为 cm 结构元素设置数值,犹如已经建立了连接。这种情况下,将运行“EA 交易”的本地时间视为连接建立时间。必须从 OnInit() function 调用该函数。
void conn_mon_init() { cm.connected = true; cm.conn_cnt = inp_conn_hyst; cm.disconn_cnt = 0; cm.conn_time = TimeLocal(); cm.new_state = false; }
现在,编译并运行“EA 交易”。之后尝试断开计算机的互联网连接。60 秒(上下浮动 10 秒)内,您将收到连接服务器失败的信息。重新连接到互联网。60 秒内,您将收到重新连接的信息,其中包含失去连接的总时间:
图 9. 通知与服务器失去连接和重新建立连接的文本信息
7. 发送开仓和平仓报告
为监视开仓和平仓,让我们将以下代码添加至 OnTradeTransaction() 函数:
void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { //--- if(trans.type==TRADE_TRANSACTION_DEAL_ADD) { if(trans.deal_type==DEAL_TYPE_BUY || trans.deal_type==DEAL_TYPE_SELL) { int i; for(i=0;i<POS_BUF_LEN;i++) { if(ps[i].new_event==false) break; } if(i<POS_BUF_LEN) { ps[i].new_event = true; ps[i].deal_type = trans.deal_type; ps[i].symbol = trans.symbol; ps[i].volume = trans.volume; ps[i].price = trans.price; ps[i].deal = trans.deal; } } } }
ps 是 POS_STR 结构的缓冲区:
struct POS_STR { bool new_event; string symbol; ulong deal; ENUM_DEAL_TYPE deal_type; double volume; double price; }; #define POS_BUF_LEN 3 POS_STR ps[POS_BUF_LEN];
短时间内平仓(或开仓)超过一次时,缓冲区是必需的。开仓或平仓时,交易添加至历史数据后,可获得所有所需参数并设置 new_event 标志。
以下是添加至 OnTimer() 函数的代码,用以查找 new_event 标志和生成 SMS 报告:
//--- processing of the opening/closing of positions string posstr=""; for(int i=0;i<POS_BUF_LEN;i++) { if(ps[i].new_event==true) { string str; if(ps[i].deal_type==DEAL_TYPE_BUY) str+= "Buy "; else if(ps[i].deal_type==DEAL_TYPE_SELL) str+= "Sell "; str+= DoubleToString(ps[i].volume,2)+" "+ps[i].symbol; int digits = (int)SymbolInfoInteger(ps[i].symbol,SYMBOL_DIGITS); str+= ", price="+DoubleToString(ps[i].price,digits); // long deal_entry; HistorySelect(TimeCurrent()-3600,TimeCurrent());//retrieve the history for the last hour if(HistoryDealGetInteger(ps[i].deal,DEAL_ENTRY,deal_entry)==true) { if(((ENUM_DEAL_ENTRY)deal_entry)==DEAL_ENTRY_IN) str+= ", entry: in"; else if(((ENUM_DEAL_ENTRY)deal_entry)==DEAL_ENTRY_OUT) { str+= ", entry: out"; double profit; if(HistoryDealGetDouble(ps[i].deal,DEAL_PROFIT,profit)==true) { str+= ", profit = "+DoubleToString(profit,2); } } } posstr+= str+"\r\n"; ps[i].new_event=false; } } if(posstr!="") { Print(posstr+"pos: "+DoubleToString(PositionsTotal(),0)); SendSMS(inp_admin_number, posstr+"pos: "+DoubleToString(PositionsTotal(),0), false); }
现在,编译并运行“EA 交易”。让我们尝试购买 0.14 手 AUDCAD。“EA 交易”将发送以下 SMS 信息:“购买 0.14 AUDCAD,价格=0.96538,条目:买入”。稍后,进行平仓并收到有关平仓的文本信息:
图 10. 有关开仓(条目:买入)和平仓(条目:卖出)的文本信息
8. 处理开仓管理的 SMS 信息接收
直到现在,“EA 交易”仅发送过信息到管理员的手机号码。现在,让我们教它接收和执行 SMS 命令。这非常有用,例如,对于平所有或部分持仓仓位而言。如我们所知,没什么比按时平仓更重要。
但我们首先需要确认正确收到了 SMS 信息。为此,我们将最近接收到的信息显示添加至 IncomingSMS() 函数:
//+------------------------------------------------------------------+ //| Called when a new SMS message is received | //+------------------------------------------------------------------+ void IncomingSMS(INCOMING_SMS_STR& sms) { string str, strtmp; //Number from which the last received SMS message was sent: ObjectSetString(0, "str18", OBJPROP_TEXT, "SMS number: "+sms.sender); //Date and time of sending the last received SMS message: str = TimeToString(sms.scts.time,TIME_DATE|TIME_SECONDS); ObjectSetString(0, "str19", OBJPROP_TEXT, "SMS date/time: "+str); //Text of the last received SMS message: strtmp = StringSubstr(sms.text, 0, 32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str20", OBJPROP_TEXT, str); strtmp = StringSubstr(sms.text,32, 32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str21", OBJPROP_TEXT, str); strtmp = StringSubstr(sms.text,64, 32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str22", OBJPROP_TEXT, str); strtmp = StringSubstr(sms.text,96, 32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str23", OBJPROP_TEXT, str); strtmp = StringSubstr(sms.text,128,32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str24", OBJPROP_TEXT, str); }
如果现在发送 SMS 信息到调制解调器,它将显示于表格中:
图 11. 终端窗口中显示的 SMS 信息接收
请注意,“EA 交易”选项卡中的 SMS 信息接收以如下方式显示:<index_in_modem_memory>text_of_the_message:
图 12. 显示在“EA 交易”选项卡中的 SMS 信息接收
"close" 用作完成交易命令。以空格和参数(需要平仓的交易品种)跟随其后,或者以 "all" 对全部品种进行平仓。在处理信息文本之前,此事并不重要,我们将使用 StringToUpper() 函数。分析信息时,务必检查发送者的手机号码是否与所设的管理员号码匹配。
此外,需要注意会出现在延迟较长时间之后收到 SMS 信息的情况(由运营商方面的技术故障引起等)。在此情形下,您不能考虑信息中所收到的命令,因为市场行情可能已经改变。鉴于此,我们将介绍另一个输入参数:
input int inp_sms_max_old=600; //SMS command expiration, sec
数值 600 表示待发送的耗时 600 秒(10 分钟)以上的命令将被忽略。请注意,该示例中所使用的检查发送时间的方法表明,SMS 服务中心和运行”EA 交易“的设备处于同一时区。
为处理 SMS 命令,让我们将以下代码添加至 IncomingSMS() 函数:
if(sms.sender==inp_admin_number) { Print("SMS from the administrator"); datetime t = TimeLocal(); //--- message expiration check if(t-sms.scts.time<=inp_sms_max_old) {//--- check if the message is a command string cmdstr = sms.text; StringToUpper(cmdstr);//convert everything to upper case int pos = StringFind(cmdstr, "CLOSE", 0); cmdstr = StringSubstr(cmdstr, pos+6, 6); if(pos>=0) {//--- command. send it for processing ClosePositions(cmdstr); } } else Print("The SMS command has expired"); }
如果 SMS 信息从管理员处发出,则其未失效,并代表一个命令(包含关键字 "Close"),我们将发送参数通过 ClosePositions() 函数进行处理:
uint ClosePositions(string sstr) {//--- close the specified positions bool all = false; if(StringFind(sstr, "ALL", 0)>=0) all = true; uint res = 0; for(int i=0;i<PositionsTotal();i++) { string symbol = PositionGetSymbol(i); if(all==true || sstr==symbol) { if(PositionSelect(symbol)==true) { long pos_type; double pos_vol; if(PositionGetInteger(POSITION_TYPE,pos_type)==true) { if(PositionGetDouble(POSITION_VOLUME,pos_vol)==true) { if(OrderClose(symbol, (ENUM_POSITION_TYPE)pos_type, pos_vol)==true) res|=0x01; else res|=0x02; } } } } } return(res); }
该函数将检查是否有与命令中所接收的参数(交易品种)相匹配的任何开仓。使用 OrderClose() 函数对符合该条件的仓位进行平仓:
bool OrderClose(string symbol, ENUM_POSITION_TYPE pos_type, double vol) { MqlTick last_tick; MqlTradeRequest request; MqlTradeResult result; double price = 0; // ZeroMemory(request); ZeroMemory(result); // if(SymbolInfoTick(Symbol(),last_tick)) { price = last_tick.bid; } else { Print("Error when getting current prices"); return(false); } // if(pos_type==POSITION_TYPE_BUY) {//--- closing a BUY position - SELL request.type = ORDER_TYPE_SELL; } else if(pos_type==POSITION_TYPE_SELL) {//--- closing a SELL position - BUY request.type = ORDER_TYPE_BUY; } else return(false); // request.price = NormalizeDouble(price, _Digits); request.deviation = 20; request.action = TRADE_ACTION_DEAL; request.symbol = symbol; request.volume = NormalizeDouble(vol, 2); if(request.volume==0) return(false); request.type_filling = ORDER_FILLING_FOK; // if(OrderSend(request, result)==true) { if(result.retcode==TRADE_RETCODE_DONE || result.retcode==TRADE_RETCODE_DONE_PARTIAL) { Print("Order executed successfully"); return(true); } } else { Print("Order parameter error: ", GetLastError(),", Trade server return code: ", result.retcode); return(false); } // return(false); }
成功处理订单后,监视仓位变化的函数将生成并发送 SMS 通知。
9. 从调制解调器内存中删除信息
请注意,调制解调器处理程序不会自己删除正在接收的 SMS 信息。因此,当期间 SMS 内存不足时,处理程序将调用 SMSMemoryFull() 函数并将当前调制解调器内存中的信息数量传递给它。您可以全部删除或选择性删除。内存释放前,调制解调器不会接收任何新信息。
//+------------------------------------------------------------------+ //| SMS memory is full | //+------------------------------------------------------------------+ void SMSMemoryFull(int n) { sms_mem_full = true; for(int i=0; i<n; i++) {//delete all SMS messages if(DelSMSbyIndex(i)==false) break; else sms_mem_full = false; } }
您也可以在 SMS 信息被处理后立即删除。当调制解调器处理程序调用 IncomingSMS() 函数时,INCOMING_SMS_STR 结构传递调制解调器内存中的信息索引,其允许在信息得到处理后用 DelSMSbyIndex() 函数立即删除信息:
总结
本文讲述了可使用 GSM 调制解调器远程监视交易终端的“EA 交易”的开发。我们讨论了开仓、当前利润和使用 SMS 通知的其他数据等相关消息的获取方法。我们还凭借 SMS 命令在开仓管理中应用了基本函数。示例中提供的命令为英文命令,但您同样也可以俄文命令灵活使用(不用浪费时间在您的手机上转换不同的键盘布局)。
最后,让我们以上市超过 10 年的旧手机来检查”EA 交易“的行为。设备 - Siemens M55。我们来连接它:
图 13. 连接 Siemens M55
图 14. 成功初始化 Siemens M55,”EA 交易“选项卡
您会发现已获得所有需要的参数。唯一的问题是来自 USSD 请求的数据。Siemens M55 不支持处理 USSD 请求的 AT 命令。除此之外,其功能性不输于当前的任何调制解调器。因此,其可用于处理”EA 交易“。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/797
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



这就是为什么我一直尊重真正的程序员--因为他们不愿意寻找电路解决方案。
买一个 GSM 交换机,然后关掉电脑。;)
感谢您的文章--信息量很大!
CDMA 呢,老兄?原理一样吗?有什么需要修改的?
我以前有一个 SMS 发送器独立软件,只能在 GSM 上使用,但不能在 CDMA 上使用。
哈哈!真不错。
我也喜欢你的评论"欢迎来到外汇--财务独立的世界"。我还不相信这...