621 lines
22 KiB
Python
621 lines
22 KiB
Python
#The MIT License (MIT)
|
|
#
|
|
#Copyright (c) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua )
|
|
#
|
|
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
#of this software and associated documentation files (the "Software"), to deal
|
|
#in the Software without restriction, including without limitation the rights
|
|
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
#copies of the Software, and to permit persons to whom the Software is
|
|
#furnished to do so, subject to the following conditions:
|
|
#
|
|
#The above copyright notice and this permission notice shall be included in all
|
|
#copies or substantial portions of the Software.
|
|
#
|
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
#SOFTWARE.
|
|
|
|
"""
|
|
This file is part of sim-module package. Can be used for HTTP requests making.
|
|
|
|
sim-module package allows to communicate with SIM 900 modules: send SMS, make HTTP requests and use other
|
|
functions of SIM 900 modules.
|
|
|
|
Copyright (C) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) All Rights Reserved.
|
|
"""
|
|
|
|
from lib.sim900.gsm import *
|
|
|
|
class SimInetGSMConnection:
|
|
inetUnknown = -1
|
|
inetConnecting = 0
|
|
inetConnected = 1
|
|
inetClosing = 2
|
|
inetClosed = 3
|
|
|
|
class SimInetGSM(SimGsm):
|
|
def __init__(self, port, logger):
|
|
SimGsm.__init__(self, port, logger)
|
|
|
|
self.__ip = None
|
|
|
|
#user agent
|
|
self.__userAgent = "Aminis SIM-900 module client (version 0.1)"
|
|
self.__connectionState = SimInetGSMConnection.inetUnknown
|
|
self.__httpResult = 0
|
|
self.__httpResponse = None
|
|
|
|
@property
|
|
def connectionState(self):
|
|
return self.__connectionState
|
|
|
|
@property
|
|
def httpResult(self):
|
|
return self.__httpResult
|
|
|
|
@property
|
|
def httpResponse(self):
|
|
return self.__httpResponse
|
|
|
|
@property
|
|
def ip(self):
|
|
return self.__ip
|
|
|
|
@property
|
|
def userAgent(self):
|
|
return self.__userAgent
|
|
|
|
@userAgent.setter
|
|
def userAgent(self, value):
|
|
self.__userAgent = value
|
|
|
|
def checkGprsBearer(self, bearerNumber = 1):
|
|
"""
|
|
Checks GPRS connection. After calling of this method
|
|
|
|
:param bearerNumber: bearer number
|
|
:return: True if checking was without mistakes, otherwise returns False
|
|
"""
|
|
self.logger.debug("checking GPRS bearer connection")
|
|
|
|
ret = self.commandAndStdResult(
|
|
"AT+SAPBR=2,{0}".format(bearerNumber),
|
|
1000,
|
|
["OK"]
|
|
)
|
|
if (ret is None) or (self.lastResult != "OK"):
|
|
self.setError("{0}: error, lastResult={1}, ret={2}".format(inspect.stack()[0][3], self.lastResult, ret))
|
|
return False
|
|
|
|
ret = str(ret).strip()
|
|
self.logger.debug("{0}: result = {1}".format(inspect.stack()[0][3], ret))
|
|
|
|
response = str(ret).split(":")
|
|
|
|
if len(response) < 2:
|
|
self.setError("{0}:error, wrong response length, ret = {1}".format(inspect.stack()[0][3], ret))
|
|
return False
|
|
|
|
#parsing string like:
|
|
# +SAPBR: 1,1,"100.80.75.124" - when connected (channel 1)
|
|
# +SAPBR: 1,3,"0.0.0.0" - when disconnected (channel 1)
|
|
|
|
if response[0] != "+SAPBR":
|
|
self.setWarn("{0}: warning, response is not '+SAPBR', response = {1}".format(inspect.stack()[0][3], response[0]))
|
|
return False
|
|
|
|
response = splitAndFilter(response[1], ",")
|
|
self.logger.debug("{0}: sapbr result = \"{1}\"".format(inspect.stack()[0][3], response))
|
|
|
|
if len(response) < 3:
|
|
self.setError("{0}: wrong SAPBR result length, (sapbr result = '{1}')".format(inspect.stack()[0][3], response[1]))
|
|
return False
|
|
|
|
if response[0] != str(bearerNumber):
|
|
return
|
|
|
|
self.__ip = None
|
|
if response[1] == "0":
|
|
self.__connectionState = SimInetGSMConnection.inetConnecting
|
|
elif response[1] == "1":
|
|
self.__connectionState = SimInetGSMConnection.inetConnected
|
|
self.__ip = response[2].strip("\"").strip()
|
|
elif response[1] == "2":
|
|
self.__connectionState = SimInetGSMConnection.inetClosing
|
|
elif response[1] == "3":
|
|
self.__connectionState = SimInetGSMConnection.inetClosed
|
|
else:
|
|
self.__connectionState = SimInetGSMConnection.inetUnknown
|
|
|
|
return True
|
|
|
|
def attachGPRS(self, apn, user=None, password=None, bearerNumber = 1):
|
|
"""
|
|
Attaches GPRS connection for SIM module
|
|
|
|
:param apn: Access Point Name
|
|
:param user: User name (Login)
|
|
:param password: Password
|
|
:param bearerNumber: Bearer number
|
|
:return: True if everything was OK, otherwise returns False
|
|
"""
|
|
|
|
#checking current connection state
|
|
if not self.checkGprsBearer(bearerNumber):
|
|
return False
|
|
|
|
#going out if already connected
|
|
if self.connectionState == SimInetGSMConnection.inetConnected:
|
|
return True
|
|
|
|
#Closing the GPRS PDP context. We dont care of result
|
|
self.execSimpleOkCommand("AT+CIPSHUT", 500)
|
|
|
|
#initialization sequence for GPRS attaching
|
|
commands = [
|
|
["AT+SAPBR=3,{0},\"CONTYPE\",\"GPRS\"".format(bearerNumber), 1000 ],
|
|
["AT+SAPBR=3,{0},\"APN\",\"{1}\"".format(bearerNumber, apn), 500 ],
|
|
["AT+SAPBR=3,{0},\"USER\",\"{1}\"".format(bearerNumber, user), 500 ],
|
|
["AT+SAPBR=3,{0},\"PWD\",\"{1}\"".format(bearerNumber, password), 500 ],
|
|
["AT+SAPBR=1,{0}".format(bearerNumber), 10000 ]
|
|
]
|
|
|
|
#executing commands sequence
|
|
if not self.execSimpleCommandsList(commands):
|
|
return False
|
|
|
|
#returning GPRS checking sequence
|
|
return self.checkGprsBearer()
|
|
|
|
def disconnectTcp(self):
|
|
"""
|
|
Disconnects TCP connection
|
|
:return:
|
|
"""
|
|
|
|
return self.commandAndStdResult("AT+CIPCLOSE", 1000, ["OK"])
|
|
|
|
def dettachGPRS(self, bearerNumber = 1):
|
|
"""
|
|
Detaches GPRS connection
|
|
:param bearerNumber: bearer number
|
|
:return: True if de
|
|
"""
|
|
|
|
#Disconnecting TCP. Ignoring result
|
|
self.disconnectTcp()
|
|
|
|
#checking current GPRS connection state
|
|
if self.checkGprsBearer(bearerNumber):
|
|
if self.connectionState == SimInetGSMConnection.inetClosed:
|
|
return True
|
|
|
|
#disconnecting GPRS connection for given bearer number
|
|
return self.execSimpleOkCommand("AT+SAPBR=0,{0}".format(bearerNumber), 1000)
|
|
|
|
def terminateHttpRequest(self):
|
|
"""
|
|
Terminates current HTTP request.
|
|
|
|
:return: True if when operation processing was without errors, otherwise returns False
|
|
"""
|
|
return self.execSimpleOkCommand("AT+HTTPTERM", 500)
|
|
|
|
def __parseHttpResult(self, httpResult, bearerChannel = None):
|
|
"""
|
|
Parses http result string.
|
|
:param httpResult: string to parse
|
|
:param bearerChannel: bearer channel
|
|
:return: returns http result code and response length
|
|
"""
|
|
self.logger.debug("{0}: dataLine = {1}".format(inspect.stack()[0][3], httpResult))
|
|
|
|
response = splitAndFilter(httpResult, ":")
|
|
if len(response) < 2:
|
|
self.setWarn("{0}: wrong HTTP response length, length = {1}".format(inspect.stack()[0][3], len(response)))
|
|
return None
|
|
|
|
if response[0] != "+HTTPACTION":
|
|
self.setWarn("{0}: http response is not a '+HTTPACTION', response = '{1}'".format(inspect.stack()[0][3], response[0]))
|
|
return None
|
|
|
|
response = splitAndFilter(response[1], ",")
|
|
|
|
if len(response) < 3:
|
|
self.setWarn("{0}: wrong response length".format(inspect.stack()[0][3]))
|
|
return None
|
|
|
|
#checking bearer channel if necessary
|
|
if bearerChannel is not None:
|
|
if response[0] != str(bearerChannel):
|
|
self.setWarn("{0}: bad bearer number".format(inspect.stack()[0][3]))
|
|
return None
|
|
|
|
httpResultCode = str(response[1])
|
|
if not httpResultCode.isnumeric():
|
|
self.setWarn("{0}: response code is not numeric!".format(inspect.stack()[0][3]))
|
|
return None
|
|
|
|
httpResultCode = int(httpResultCode)
|
|
if httpResultCode != 200:
|
|
return [httpResultCode, 0]
|
|
|
|
responseLength = str(response[2])
|
|
if not responseLength.isnumeric():
|
|
self.setWarn("{0}: response length is not numeric".format(inspect.stack()[0][3]))
|
|
return False
|
|
|
|
return [httpResultCode, int(responseLength)]
|
|
|
|
def __readHttpResponse(self, httpMethodCode, responseLength):
|
|
"""
|
|
Reads http response data from SIM module buffer
|
|
|
|
:param httpMethodCode: ?
|
|
:param responseLength: response length
|
|
:return: True if reading was successful, otherwise returns false
|
|
"""
|
|
self.logger.debug("asking for http response (length = {0})".format(responseLength))
|
|
|
|
#trying to read HTTP response data
|
|
ret = self.commandAndStdResult(
|
|
"AT+HTTPREAD={0},{1}".format(httpMethodCode, responseLength),
|
|
10000,
|
|
["OK"]
|
|
)
|
|
|
|
if (ret is None) or (self.lastResult != "OK"):
|
|
self.setError("{0}: error reading http response data".format(inspect.stack()[0][3]))
|
|
return False
|
|
|
|
#removing leading \n symbols
|
|
#TODO: we must remove only 1 \n, not all! Fix it!
|
|
ret = str(ret).strip()
|
|
|
|
#reading first string in response (it must be "+HTTPREAD")
|
|
httpReadResultString = ""
|
|
while True:
|
|
if len(ret) == 0:
|
|
break
|
|
|
|
httpReadResultString += ret[0]
|
|
ret = ret[1:]
|
|
|
|
if "\n" in httpReadResultString:
|
|
break
|
|
|
|
httpReadResultString = str(httpReadResultString).strip()
|
|
if len(httpReadResultString) == 0:
|
|
self.setError("{0}: wrong http response. Result is empty".format(inspect.stack()[0][3]))
|
|
return False
|
|
|
|
httpReadResult = str(httpReadResultString).strip()
|
|
self.logger.debug("{0}: httpReadResult = {1}".format(inspect.stack()[0][3], httpReadResult))
|
|
|
|
httpReadResult = splitAndFilter(httpReadResult, ":")
|
|
if (len(httpReadResult) < 2) or (httpReadResult[0] != "+HTTPREAD"):
|
|
self.setError("{0}: bad response (cant find '+HTTPREAD'".format(inspect.stack()[0][3]))
|
|
return False
|
|
|
|
if int(httpReadResult[1]) != responseLength:
|
|
self.setWarn("{0}: bad response, wrong responseLength = {1}".format(inspect.stack()[0][3], responseLength))
|
|
return False
|
|
|
|
self.__httpResponse = ret
|
|
return True
|
|
|
|
@staticmethod
|
|
def ___isOkHttpResponseCode(code):
|
|
"""
|
|
Checks that given HTTP return code is successful result code
|
|
|
|
:param code: http result code for checking
|
|
:return: true if given code is HTTP operation successful
|
|
"""
|
|
return code in [200, 201, 202, 203, 204, 205, 206, 207, 226]
|
|
|
|
@staticmethod
|
|
def __isNoContentResponse(code):
|
|
"""
|
|
Checks that HTTP result code is 'NO CONTENT' result code
|
|
:param code: code for analysis
|
|
:return: true when code is 'NO CONTENT' code, otherwise returns false
|
|
"""
|
|
return code == 204
|
|
|
|
@staticmethod
|
|
def ___isHttpResponseCodeReturnsData(code):
|
|
"""
|
|
Checks that http operation returns data by given http result code
|
|
|
|
:param code: given http call result code
|
|
:return: true if http request must return data, otherwise returns false
|
|
"""
|
|
|
|
return code in [200, 206]
|
|
|
|
def httpGet(self, server, port = 80, path = "/", bearerChannel = 1):
|
|
"""
|
|
Makes HTTP GET request to the given server and script
|
|
|
|
:param server: server (host) address
|
|
:param port: http port
|
|
:param path: path to the script
|
|
:param bearerChannel: bearer channel number
|
|
:return: true if operation was successfully finished. Otherwise returns false
|
|
"""
|
|
self.__clearHttpResponse()
|
|
|
|
#TODO: close only when opened
|
|
self.terminateHttpRequest()
|
|
|
|
#HTTP GET request sequence
|
|
simpleCommands = [
|
|
[ "AT+HTTPINIT", 2000 ],
|
|
[ "AT+HTTPPARA=\"CID\",\"{0}\"".format(bearerChannel), 1000 ],
|
|
[ "AT+HTTPPARA=\"URL\",\"{0}:{2}{1}\"".format(server, path,port), 500 ],
|
|
[ "AT+HTTPPARA=\"UA\",\"{0}\"".format(self.userAgent), 500 ],
|
|
[ "AT+HTTPPARA=\"REDIR\",\"1\"", 500 ],
|
|
[ "AT+HTTPPARA=\"TIMEOUT\",\"45\"", 500 ],
|
|
[ "AT+HTTPACTION=0", 10000 ]
|
|
]
|
|
|
|
#executing http get sequence
|
|
if not self.execSimpleCommandsList(simpleCommands):
|
|
self.setError("error executing HTTP GET sequence")
|
|
return False
|
|
|
|
#reading HTTP request result
|
|
dataLine = self.readDataLine(10000)
|
|
|
|
if dataLine is None:
|
|
return False
|
|
|
|
#parsing string like this "+HTTPACTION:0,200,15"
|
|
httpResult = self.__parseHttpResult(dataLine, 0)
|
|
if httpResult is None:
|
|
return False
|
|
|
|
#assigning HTTP result code
|
|
self.__httpResult = httpResult[0]
|
|
|
|
#it's can be bad http code, let's check it
|
|
if not self.___isOkHttpResponseCode(self.httpResult):
|
|
self.terminateHttpRequest()
|
|
return True
|
|
|
|
#when no data from server we just want go out, everything if OK
|
|
if not self.___isHttpResponseCodeReturnsData(self.httpResult):
|
|
self.terminateHttpRequest()
|
|
return True
|
|
|
|
responseLength = httpResult[1]
|
|
if responseLength == 0:
|
|
self.terminateHttpRequest()
|
|
return True
|
|
|
|
self.logger.debug("reading http response data")
|
|
if not self.__readHttpResponse(0, responseLength):
|
|
return False
|
|
|
|
return True
|
|
|
|
def __clearHttpResponse(self):
|
|
self.__httpResponse = None
|
|
self.__httpResult = 0
|
|
|
|
def httpPOST(self, server, port, path, parameters, bearerChannel = 1):
|
|
"""
|
|
Makes HTTP POST request to the given server and script
|
|
|
|
:param server: server (host) address
|
|
:param port: server port
|
|
:param path: path to the script
|
|
:param parameters: POST parameters
|
|
:param bearerChannel: bearer channel number
|
|
:return: true if operation was successfully finished. Otherwise returns false
|
|
"""
|
|
|
|
self.__clearHttpResponse()
|
|
|
|
#TODO: close only when opened
|
|
self.terminateHttpRequest()
|
|
|
|
#HTTP POST request commands sequence
|
|
simpleCommands = [
|
|
[ "AT+HTTPINIT", 2000 ],
|
|
[ "AT+HTTPPARA=\"CID\",\"{0}\"".format(bearerChannel), 1000 ],
|
|
[ "AT+HTTPPARA=\"URL\",\"{0}:{1}{2}\"".format(server, port, path), 500 ],
|
|
[ "AT+HTTPPARA=\"CONTENT\",\"application/x-www-form-urlencoded\"", 500 ],
|
|
[ "AT+HTTPPARA=\"UA\",\"{0}\"".format(self.userAgent), 500 ],
|
|
[ "AT+HTTPPARA=\"REDIR\",\"1\"", 500 ],
|
|
[ "AT+HTTPPARA=\"TIMEOUT\",\"45\"", 500 ]
|
|
]
|
|
|
|
#executing commands sequence
|
|
if not self.execSimpleCommandsList(simpleCommands):
|
|
return False
|
|
|
|
|
|
#uploading data
|
|
self.logger.debug("uploading HTTP POST data")
|
|
ret = self.commandAndStdResult(
|
|
"AT+HTTPDATA={0},10000".format(len(parameters)),
|
|
7000,
|
|
["DOWNLOAD", "ERROR"]
|
|
)
|
|
|
|
if (ret is None) or (self.lastResult != "DOWNLOAD"):
|
|
self.setError("{0}: can't upload HTTP POST data".format(inspect.stack()[0][3]))
|
|
return False
|
|
|
|
self.simpleWriteLn(parameters)
|
|
|
|
dataLine = self.readDataLine(500)
|
|
if (dataLine is None) or (dataLine != "OK"):
|
|
self.setError("{0}: can't upload HTTP POST data".format(inspect.stack()[0][3]))
|
|
return
|
|
|
|
self.logger.debug("actually making request")
|
|
|
|
#TODO: check CPU utilization
|
|
if not self.execSimpleOkCommand("AT+HTTPACTION=1", 15000):
|
|
return False
|
|
|
|
#reading HTTP request result
|
|
dataLine = self.readDataLine(15000)
|
|
|
|
if dataLine is None:
|
|
self.setError("{0}: empty HTTP request result string".format(inspect.stack()[0][3]))
|
|
return False
|
|
|
|
#parsing string like this "+HTTPACTION:0,200,15"
|
|
httpResult = self.__parseHttpResult(dataLine, bearerChannel)
|
|
if httpResult is None:
|
|
return False
|
|
|
|
#assigning HTTP result code
|
|
self.__httpResult = httpResult[0]
|
|
|
|
#it's can be bad http code, let's check it
|
|
if not self.___isOkHttpResponseCode(self.httpResult):
|
|
self.terminateHttpRequest()
|
|
return True
|
|
|
|
#when no data from server we just want go out, everything if OK
|
|
if (
|
|
(self.__isNoContentResponse(self.httpResult)) or
|
|
(not self.___isHttpResponseCodeReturnsData(self.httpResult))
|
|
):
|
|
self.terminateHttpRequest()
|
|
return True
|
|
|
|
responseLength = httpResult[1]
|
|
if responseLength == 0:
|
|
self.terminateHttpRequest()
|
|
return True
|
|
|
|
self.logger.debug("reading http request response data")
|
|
|
|
if not self.__readHttpResponse(0, responseLength):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
# self.disconnectTcp()
|
|
#
|
|
# return True
|
|
|
|
def httpPOST_DATA(self, server, port, path, parameters, bearerChannel = 1):
|
|
"""
|
|
Makes HTTP POST request to the given server and script
|
|
|
|
:param server: server (host) address
|
|
:param port: server port
|
|
:param path: path to the script
|
|
:param parameters: POST parameters
|
|
:param bearerChannel: bearer channel number
|
|
:return: true if operation was successfully finished. Otherwise returns false
|
|
"""
|
|
|
|
self.__clearHttpResponse()
|
|
|
|
#TODO: close only when opened
|
|
self.terminateHttpRequest()
|
|
|
|
#HTTP POST request commands sequence
|
|
simpleCommands = [
|
|
[ "AT+HTTPINIT", 2000 ],
|
|
[ "AT+HTTPPARA=\"CID\",\"{0}\"".format(bearerChannel), 1000 ],
|
|
[ "AT+HTTPPARA=\"URL\",\"{0}:{1}{2}\"".format(server, port, path), 500 ],
|
|
[ "AT+HTTPPARA=\"CONTENT\",\"multipart/form-data\"", 500 ],
|
|
[ "AT+HTTPPARA=\"UA\",\"{0}\"".format(self.userAgent), 500 ],
|
|
[ "AT+HTTPPARA=\"REDIR\",\"1\"", 500 ],
|
|
[ "AT+HTTPPARA=\"TIMEOUT\",\"45\"", 500 ]
|
|
]
|
|
|
|
#executing commands sequence
|
|
if not self.execSimpleCommandsList(simpleCommands):
|
|
return False
|
|
|
|
#uploading data
|
|
self.logger.debug("uploading HTTP POST data")
|
|
ret = self.commandAndStdResult(
|
|
"AT+HTTPDATA={0},10000".format(len(parameters)),
|
|
7000,
|
|
["DOWNLOAD", "ERROR"]
|
|
)
|
|
|
|
if (ret is None) or (self.lastResult != "DOWNLOAD"):
|
|
self.setError("{0}: can't upload HTTP POST data".format(inspect.stack()[0][3]))
|
|
return False
|
|
|
|
self.simpleWriteLn(parameters)
|
|
|
|
dataLine = self.readDataLine(500)
|
|
if (dataLine is None) or (dataLine != "OK"):
|
|
self.setError("{0}: can't upload HTTP POST data".format(inspect.stack()[0][3]))
|
|
return
|
|
|
|
self.logger.debug("actually making request")
|
|
|
|
#TODO: check CPU utilization
|
|
if not self.execSimpleOkCommand("AT+HTTPACTION=1", 15000):
|
|
return False
|
|
|
|
#reading HTTP request result
|
|
dataLine = self.readDataLine(15000)
|
|
|
|
if dataLine is None:
|
|
self.setError("{0}: empty HTTP request result string".format(inspect.stack()[0][3]))
|
|
return False
|
|
|
|
#parsing string like this "+HTTPACTION:0,200,15"
|
|
httpResult = self.__parseHttpResult(dataLine, bearerChannel)
|
|
if httpResult is None:
|
|
return False
|
|
|
|
#assigning HTTP result code
|
|
self.__httpResult = httpResult[0]
|
|
|
|
#it's can be bad http code, let's check it
|
|
if not self.___isOkHttpResponseCode(self.httpResult):
|
|
self.terminateHttpRequest()
|
|
return True
|
|
|
|
#when no data from server we just want go out, everything if OK
|
|
if (
|
|
(self.__isNoContentResponse(self.httpResult)) or
|
|
(not self.___isHttpResponseCodeReturnsData(self.httpResult))
|
|
):
|
|
self.terminateHttpRequest()
|
|
return True
|
|
|
|
responseLength = httpResult[1]
|
|
if responseLength == 0:
|
|
self.terminateHttpRequest()
|
|
return True
|
|
|
|
self.logger.debug("reading http request response data")
|
|
|
|
if not self.__readHttpResponse(0, responseLength):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
# self.disconnectTcp()
|
|
#
|
|
# return True
|
|
|
|
|
|
#
|
|
# int res= gsm.read(result, resultlength);
|
|
# //gsm.disconnectTCP();
|
|
# return res; |