From 948f00952a2aeade87b9109ace75f1d652d183b6 Mon Sep 17 00:00:00 2001 From: Kiida Lai Date: Wed, 15 Mar 2023 12:17:07 -0700 Subject: [PATCH 1/2] Another version of "tcp_recvdata" is featured with the timeout version. The name of the new function is "tcp_recvdata_timeout". --- SRC/simApplicationClient/c/tcp_socket.c | 75 ++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/SRC/simApplicationClient/c/tcp_socket.c b/SRC/simApplicationClient/c/tcp_socket.c index f5b41ca9..b0330931 100644 --- a/SRC/simApplicationClient/c/tcp_socket.c +++ b/SRC/simApplicationClient/c/tcp_socket.c @@ -27,6 +27,7 @@ #include #include +#include // added by Kiida Lai for the timeout feature of recvdata #include #include @@ -518,6 +519,78 @@ void CALL_CONV tcp_recvdata(int *socketID, int *dataTypeSize, char data[], int * } } +/* +* tcp_recvdata_timeout() - function to receive data in blocking mode with timeout feature +* Note: this function for now only is tested on windows +* written by Kiida Lai (u93132@gmail.com) +* +* input: int *socketID - socket identifier +* int *dataTypeSize - size of data type +* char *data - pointer to data to receive +* int *lenData - length of data +* double *timeout - if data didn't arrive on time, then return ierr = -3 +* if a timeout < 0 is given, the function will not do the check +* unit: seconds +* +* return: int *ierr - 0 if successfull, negative number if not +*/ +void CALL_CONV tcp_recvdata_timeout(int *socketID, int *dataTypeSize, char data[], int *lenData, int *ierr, double *timeout) +{ + SocketConnection *theSocket = theSockets; + socket_type sockfd; + int nread, nleft; + double timer; + unsigned long nbMode = 0; + char *gMsg = data; + clock_t start_t = clock(); // from time.h + + *ierr = 0; + + // find the socket + while (theSocket != 0 && theSocket->socketID != *socketID) + theSocket=theSocket->next; + if (theSocket == 0) { + fprintf(stderr,"tcp_socket::recvdata() - could not find socket to receive data\n"); + *ierr = -1; + return; + } + sockfd = theSocket->sockfd; + + // turn on blocking mode +#ifdef _WIN32 + if (ioctlsocket(sockfd, FIONBIO, &nbMode) != 0) { + fprintf(stderr,"tcp_socket::recvdata() - could not turn on blocking mode\n"); + *ierr = -2; + return; + } +#else + if (ioctl(sockfd, FIONBIO, &nbMode) != 0) { + fprintf(stderr,"tcp_socket::recvdata() - could not turn on blocking mode\n"); + *ierr = -2; + return; + } +#endif + + // receive the data + // if o.k. get a pointer to the data in the message and + // place the incoming data there + nleft = *lenData * *dataTypeSize; + + while (nleft > 0) { + // While loop will break when data isn't arrive on time + if (*timeout > 0) { + timer = (clock() - start_t) / CLOCKS_PER_SEC; + if (timer > *timeout) { + *ierr = -3; + fprintf(stderr,"tcp_socket::recvdata() - data didn't receive on time\n"); + break; + } + } + nread = recv(sockfd, gMsg, nleft, 0); + nleft -= nread; + gMsg += nread; + } +} /* * tcp_recvnbdata() - function to receive data in nonblocking mode @@ -612,4 +685,4 @@ void CALL_CONV tcp_getsocketid(unsigned int *port, const char machineInetAddr[], } fprintf(stderr,"tcp_socket::getsocketid() - could not find socket to get socketID\n"); *socketID = -2; -} +} \ No newline at end of file From 0a29f9e7014b6bc12f2d95829c95263f8d0a3f22 Mon Sep 17 00:00:00 2001 From: Kiida Lai Date: Wed, 15 Mar 2023 12:25:48 -0700 Subject: [PATCH 2/2] A Python TCP server + Simulink TCP client example using "SFun_TCPSocketSR.mexw64" --- .../simulink/Python_TCPserver.py | 29 ++ .../simulink/SFun_TCPSocketSR.c | 334 ++++++++++++++++++ .../simulink/SFun_TCPSocketSR.mexw64 | Bin 0 -> 19968 bytes 3 files changed, 363 insertions(+) create mode 100644 SRC/simApplicationClient/simulink/Python_TCPserver.py create mode 100644 SRC/simApplicationClient/simulink/SFun_TCPSocketSR.c create mode 100644 SRC/simApplicationClient/simulink/SFun_TCPSocketSR.mexw64 diff --git a/SRC/simApplicationClient/simulink/Python_TCPserver.py b/SRC/simApplicationClient/simulink/Python_TCPserver.py new file mode 100644 index 00000000..28b3fd19 --- /dev/null +++ b/SRC/simApplicationClient/simulink/Python_TCPserver.py @@ -0,0 +1,29 @@ +import socket +import struct + +# STREAM for TCP/IP +portNum = 7200 +serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +serversocket.bind(('localhost',portNum)) +serversocket.listen(5) + +print('Server start on Port '+str(portNum)) +while True: + (connect, address) = serversocket.accept() + print("Connected to client IP: {}".format(address)) + + try: + while True: + msg_recv = connect.recv(24) + if not len(msg_recv): + break + else: + time = struct.unpack(' +#include + +// functions defined in tcp_socket.c +void tcp_setupconnectionserver(unsigned int *port, int *socketID); +void tcp_setupconnectionclient(unsigned int *port, const char machineInetAddr[], int *lengthInet, int *socketID); +void tcp_closeconnection(int *socketID, int *ierr); +void tcp_senddata(const int *socketID, int *dataTypeSize, char data[], int *lenData, int *ierr); +void tcp_recvdata_timeout(const int *socketID, int *dataTypeSize, char data[], int *lenData, int *ierr, double *timeout); + +// generic client parameters +#define ipAddr(S) ssGetSFcnParam(S,0) // ip address of server +#define ipPort(S) ssGetSFcnParam(S,1) // ip port of server +#define sendWidth(S) ssGetSFcnParam(S,2) // number of received data points +#define dataType(S) ssGetSFcnParam(S,3) // receive vector data type +#define recvWidth(S) ssGetSFcnParam(S,4) // number of received data points +#define timeout(S) ssGetSFcnParam(S,5) // number of received data points + +#define NPARAMS 6 + +// ==================== +// S-function methods +// ==================== + +#define MDL_CHECK_PARAMETERS +#if defined(MDL_CHECK_PARAMETERS) && defined(MATLAB_MEX_FILE) +// ============================================================================ +// Function: mdlCheckParameters +// Validate the parameters to verify they are okay + +static void mdlCheckParameters(SimStruct *S) +{ + if (!mxIsChar(ipAddr(S))) { + ssSetErrorStatus(S,"ipAddr must be a string"); + return; + } + if (!mxIsDouble(ipPort(S)) || mxGetPr(ipPort(S))[0] <= 0) { + ssSetErrorStatus(S,"ipPort must be a positive nonzero value"); + return; + } + if (!mxIsChar(dataType(S))) { + ssSetErrorStatus(S,"dataType must be a string"); + return; + } + if (!mxIsDouble(recvWidth(S)) || mxGetPr(recvWidth(S))[0] <= 0) { + ssSetErrorStatus(S,"Receive data points must be a positive nonzero value"); + return; + } + + if (!mxIsDouble(timeout(S))) { + ssSetErrorStatus(S,"Timout is a number with its unit in second"); + return; + } +} +#endif // MDL_CHECK_PARAMETERS + + +// ============================================================================ +// Function: mdlInitializeSizes +// The sizes information is used by Simulink to determine the S-function +// block's characteristics (number of inputs, outputs, states, etc.). + +static void mdlInitializeSizes(SimStruct *S) +{ + char_T *dataType; + int_T sendWidth, recvWidth; + + ssSetNumSFcnParams(S, NPARAMS); // Number of expected parameters +#if defined(MATLAB_MEX_FILE) + if (ssGetNumSFcnParams(S) == ssGetSFcnParamsCount(S)) { + mdlCheckParameters(S); + if (ssGetErrorStatus(S) != NULL) { + return; + } + } else { + return; // Parameter mismatch will be reported by Simulink + } +#endif // MATLAB_MEX_FILE + + ssSetNumContStates(S, 0); + ssSetNumDiscStates(S, 0); + + sendWidth = (int_T)mxGetScalar(sendWidth(S)); + + if (!ssSetNumInputPorts(S, 1)) return; + ssSetInputPortDataType(S, 0, DYNAMICALLY_TYPED); + ssSetInputPortWidth(S, 0, sendWidth); + ssSetInputPortDirectFeedThrough(S, 0, 1); + + if (!ssSetNumOutputPorts(S, 1)) return; + // switch according to data type + dataType = mxArrayToString(dataType(S)); + recvWidth = (int_T)mxGetScalar(recvWidth(S)); + + if (strcmp(dataType,"double") == 0) { + ssSetOutputPortDataType(S, 0, SS_DOUBLE); + } + else if (strcmp(dataType,"single") == 0) { + ssSetOutputPortDataType(S, 0, SS_SINGLE); + } + else if (strcmp(dataType,"int32") == 0) { + ssSetOutputPortDataType(S, 0, SS_INT32); + } + else if (strcmp(dataType,"int16") == 0) { + ssSetOutputPortDataType(S, 0, SS_INT16); + } + else if (strcmp(dataType,"int8") == 0) { + ssSetOutputPortDataType(S, 0, SS_INT8); + } + else { + ssSetErrorStatus(S,"Data type is not supported"); + return; + } + ssSetOutputPortWidth(S, 0, recvWidth); + + ssSetNumSampleTimes(S, 1); + ssSetNumDWork(S, 1); + ssSetNumRWork(S, 0); + ssSetNumIWork(S, 0); + ssSetNumPWork(S, 0); + ssSetNumModes(S, 0); + ssSetNumNonsampledZCs(S, 0); + + // allocate memory for socketID + ssSetDWorkWidth(S, 0, 1); + ssSetDWorkDataType(S, 0, SS_INT32); + + // take care when specifying exception free code - see sfuntmpl_doc.c + ssSetOptions(S, SS_OPTION_EXCEPTION_FREE_CODE); +} + + +// ============================================================================ +// Function: mdlInitializeSampleTimes + +static void mdlInitializeSampleTimes(SimStruct *S) +{ + ssSetSampleTime(S, 0, INHERITED_SAMPLE_TIME); + ssSetOffsetTime(S, 0, 0.0); +} + +#define MDL_START +// ============================================================================ +// Function: mdlStart +// This function is called once at start of model execution. If you +// have states that should be initialized once, this is the place +// to do it. + +static void mdlStart(SimStruct *S) +{ + char_T *ipAddr; + uint_T ipPort; + int_T nleft, sizeAddr; + + // get and initialize parameters + int_T *socketID = (int_T*)ssGetDWork(S,0); + nleft = ssGetInputPortWidth(S,0); + socketID[0] = -1; + + // setup the connection + if (mxIsEmpty(ipAddr(S))) { + ipPort = (int_T)mxGetScalar(ipPort(S)); + tcp_setupconnectionserver(&ipPort, socketID); + if (socketID[0] < 0) { + ssSetErrorStatus(S,"Failed to setup connection with client"); + return; + } + } + else { + ipAddr = mxArrayToString(ipAddr(S)); + sizeAddr = (int_T)mxGetN(ipAddr(S)) + 1; + ipPort = (int_T)mxGetScalar(ipPort(S)); + tcp_setupconnectionclient(&ipPort, ipAddr, &sizeAddr, socketID); + mxFree(ipAddr); + if (socketID[0] < 0) { + ssSetErrorStatus(S,"Failed to setup connection with server"); + return; + } + } +} + + +// ============================================================================ +// Function: mdlOutputs +// Calculate outputs + +static void mdlOutputs(SimStruct *S, int_T tid) +{ + InputPtrsType dataPtrs = ssGetInputPortSignalPtrs(S,0); + + int_T ierr, nleft, dataTypeSize, recvWidth; + char_T *gMsg, *gMsg_recv, *dataType; + real64_T timeout = (real64_T)mxGetScalar(timeout(S)); + + // get parameters + int_T *socketID = (int_T*)ssGetDWork(S,0); + nleft = ssGetInputPortWidth(S,0); + + UNUSED_ARG(tid); // not used in single tasking mode + + // switch according to data type + if (ssGetInputPortDataType(S,0) == SS_DOUBLE) { + real64_T *data = (real64_T *)(*dataPtrs); + gMsg = (char_T *)data; + dataTypeSize = sizeof(real64_T); + } + else if (ssGetInputPortDataType(S,0) == SS_SINGLE) { + real32_T *data = (real32_T *)(*dataPtrs); + gMsg = (char_T *)data; + dataTypeSize = sizeof(real32_T); + } + else if (ssGetInputPortDataType(S,0) == SS_INT32) { + int32_T *data = (int32_T *)(*dataPtrs); + gMsg = (char_T *)data; + dataTypeSize = sizeof(int32_T); + } + else if (ssGetInputPortDataType(S,0) == SS_INT16) { + int16_T *data = (int16_T *)(*dataPtrs); + gMsg = (char_T *)data; + dataTypeSize = sizeof(int16_T); + } + else if (ssGetInputPortDataType(S,0) == SS_INT8) { + int8_T *data = (int8_T *)(*dataPtrs); + gMsg = (char_T *)data; + dataTypeSize = sizeof(int8_T); + } + else { + ssSetErrorStatus(S,"Data type is not supported"); + return; + } + + // send the data + tcp_senddata(socketID, &dataTypeSize, gMsg, &nleft, &ierr); + + // switch according to data type + dataType = mxArrayToString(dataType(S)); + recvWidth = (int_T)mxGetScalar(recvWidth(S)); + + if (strcmp(dataType,"double") == 0) { + real64_T *data_recv = (real64_T *)ssGetOutputPortRealSignal(S,0); + gMsg_recv = (char_T *)data_recv; + dataTypeSize = (int_T)sizeof(real64_T); + } + else if (strcmp(dataType,"single") == 0) { + real32_T *data_recv = (real32_T *)ssGetOutputPortRealSignal(S,0); + gMsg_recv = (char_T *)data_recv; + dataTypeSize = (int_T)sizeof(real32_T); + } + else if (strcmp(dataType,"int32") == 0) { + int32_T *data_recv = (int32_T *)ssGetOutputPortRealSignal(S,0); + gMsg_recv = (char_T *)data_recv; + dataTypeSize = (int_T)sizeof(int32_T); + } + else if (strcmp(dataType,"int16") == 0) { + int16_T *data_recv = (int16_T *)ssGetOutputPortRealSignal(S,0); + gMsg_recv = (char_T *)data_recv; + dataTypeSize = (int_T)sizeof(int16_T); + } + else if (strcmp(dataType,"int8") == 0) { + int8_T *data_recv = (int8_T *)ssGetOutputPortRealSignal(S,0); + gMsg_recv = (char_T *)data_recv; + dataTypeSize = (int_T)sizeof(int8_T); + } + else { + ssSetErrorStatus(S,"Data type is not supported"); + return; + } + + // receive the data + tcp_recvdata_timeout(socketID, &dataTypeSize, gMsg_recv, &recvWidth, &ierr, &timeout); + if (ierr < 0) { + ssSetErrorStatus(S,"Data didn't arrive on time..."); + } +} + + +// ============================================================================ +// Function: mdlTerminate + +static void mdlTerminate(SimStruct *S) +{ + int_T ierr; + + // get socketID + int_T *socketID = (int_T*)ssGetDWork(S,0); + + tcp_closeconnection(socketID, &ierr); +} + +#ifdef MATLAB_MEX_FILE // Is this file being compiled as a MEX-file? +#include "simulink.c" // MEX-file interface mechanism +#else +#include "cg_sfun.h" // Code generation registration function +#endif \ No newline at end of file diff --git a/SRC/simApplicationClient/simulink/SFun_TCPSocketSR.mexw64 b/SRC/simApplicationClient/simulink/SFun_TCPSocketSR.mexw64 new file mode 100644 index 0000000000000000000000000000000000000000..e464e4ae870df8dd9330f46adf112258ea86a678 GIT binary patch literal 19968 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

lKJ0HK3JJ3&O?oS>N(Z+K1IM1%gqh+SZ1|B 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 literal 0 HcmV?d00001