/****************************************************************** Example for reading modbus registers over TCP in Pico-C Not perfect but working Check how your modbus device behaves when more than one client is connecting... Andrej Konkow, 02/2026, v23 ...Pico-C is in somekind very special in syntax and possibilities... ******************************************************************/ // Supported modbus datatypes enum registerDatatype { DT_NONE = 0, UINT16 = 1, INT16 = 2, UINT32 = 3, INT32 = 4, FLOAT16 = 5, FLOAT32 = 6, STRING16 = 7, STRING32 = 8, COIL = 9 }; // Holds the corresponding bytesizes for the above datatypes static int BYTESIZEMAP[10] = { 2, 2, 2, 4, 4, 2, 4, 16, 32, 1 }; // To run in debug mode and print some more messages activate the #define by removing the comment. // Due to efficiency we will call the debug functions only in debugmode //#define DEBUGMODE #define MODBUS_INPUT_IP_PORT getinputtext(0) #define MODBUS_INPUT_ADDRESS_HEX getinputtext(1) #define MODBUS_INPUT_RESULTS_DT getinputtext(2) #define MODBUS_INPUT_BIGENDIAN getinput(0) #define REPEATER_SLEEP_MS getinput(1) // Supported modbus functioncodes #define MODBUS_FC_READCOILS 0x01 #define MODBUS_FC_READHOLDINGREGISTERS 0x03 // byte-size of message being sent to modbus-device #define MODBUS_MSG_SIZE 12 // Payload bytes which can be ignored when getting the values from the response #define MODBUS_RESPONSE_OFFSET 9 // Our own transaction id, as we like but unique #define MODBUS_TRANSACTIONID 140 // Size of char-array holding the connectionstring for target device #define MODBUS_CONNECTIONSTRING_SIZE 60 // Time spent in reading the responsestream/waiting for data, timeout in ms #define MODBUS_DEFAULT_READTIME_MS 1000 // Sleeptime before repeating request in milliseconds #define REPEATER_DEFAULT_SLEEP_MS 10000 // Blocksize upper limit for reading response data #define RD_BLOCK_SIZE 300 // Size of String to be transferred to next component #define TRANSFER_SIZE 500 // Define how many Output channels are available before handing over // the remaining results. In Loxone programs currently can address // 13 output channels for number-values in maximum #define MAX_OUTPUT_CHANNELS 13 // Maximum of sum of result datatypes to be defined in input component #define MAX_DATATYPES_DEFINED_EXT 150 // Definition of delimiters for datainput, defining // datatypes and the number of results #define DELIMITER_DT_BLOCKS "," #define DELIMITER_DT_NUMBER "x" // Pseudovalue for identfying a "no param" #define STRING_NOPARAM -99999 // Map for converting asc hex-values efficiently static char MapHex2Int[127] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // values for 0-9 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 14, // values for A-F 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, // values for a-f 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; struct registerData { int datatype; // one of "enum registerDatatype" int idxResponse; // Holds the index to the value in the responsestream; int registerAddress; // Real address in the device }; /* Holds information about the specified userdata in the textgenerator component */ struct dataConfig { struct registerData szRegisters [MAX_DATATYPES_DEFINED_EXT]; // List of all user-defined datatypes int szRegisterIdxView[MAX_DATATYPES_DEFINED_EXT]; // Points to the Idx in szRegisters to be displayed int byteSizeListOfDatatypes; // Bytesize of list of all datatypes int no_resultvalues; // Number of total values specified int no_printValues; // Number of values to be printed char modbusRequest[MODBUS_MSG_SIZE]; // Bytesequence for modbus request char connectionString[MODBUS_CONNECTIONSTRING_SIZE]; // tcp connection string int modbusId; // Modbus device id int deviceLittleEndian; // Are we expecting the data in little endian format? int modbus_readtime_ms; // Timeout waiting for response int readCoils; // Are we in the mode of reading coils? int repeater_sleep_ms; // sleep time between iterations char szStartAddress[10]; // Starting address as specified hex-String char szPrintedAdresses[RD_BLOCK_SIZE]; // Adresses printed on outputchannel (first component only) }; // Keeping the configuration global makes things easier static struct dataConfig Config; /* Convert characters/bytes to readable hex-string Caution: Don't use strlen or other string-oriented functions as byte-characters don't behave like normal alphabetical characters Due to efficiency reasons the parameter firstfill tells us with "0" that "dest" is used for the first time. Else Non-"0". Otherwise we would have to use strlen() which can cause time delays when used excessively. */ void convertToReadableHex(char *bytes, int byteLen, char *dest, int hasContent) { int i; for (i=0; i hexlen) // corrupt bytestring, unhandled errorcase break; if (hexString[i] == ' ') // Ignore whitespaces { i--; continue; } // stick together the two 4bit values resultBytes[resultcounter] = (MapHex2Int[hexString[i]] << 4) | (MapHex2Int[hexString[i+1]] & 0xF); resultcounter++; } return resultcounter; } /* The bytes represent ascii-encoded values. Decode them... */ void decodeBytes2Ascii(char *src, char *dest, int len) { char hlp[33], tmpString[35]; init(hlp, 33, 0); int tmpLen = strlen(src); if (tmpLen < len) len = tmpLen; if (len == 0) return; strncpy(hlp, src, len); if (dest[0] == 0) sprintf(dest, "%s", hlp); else sprintf(dest, "%s, %s", dest, hlp); } /* Swap all bytes according to their datatype */ void swapAllBytes(char *bytes) { int i, result_type, offsetCnt=0; struct registerData hlpRegisterData; if (Config.deviceLittleEndian || Config.readCoils) return; for (i=0; i> 7; exponent = ((flt16_bits[0] & 0x7C) >> 2); significand = ((flt16_bits[0] & 0x03) << 8) | (flt16_bits[1]); return (float)(pow(-1,sign_bit) * (1 + significand/pow(2,10)) * pow(2,(exponent - 31))); } /* calc float32 value */ float calc_float32(char *flt32_bits) { char sign_bit; char exponent; unsigned int significand; // IEEE 754 single precision sign_bit = (flt32_bits[1] & 0x80) >> 7; exponent = ((flt32_bits[1] & 0x7F) << 1) | ((flt32_bits[0] & 0x80) >> 7); significand = (((flt32_bits[0] & 0x7F) << 16) | (flt32_bits[3] << 8) | (flt32_bits[2])); return (float)(pow(-1,sign_bit) * (1 + significand/pow(2,23)) * pow(2,(exponent - 127))); } /* Build connection string from an ip address including the port. No errorhandling is done as we assume a correct address */ void buildConnectionString(char *targetaddress, char *connectionString, int *modbusId) { char ip[25], port[10], mbId[5]; int colon, comma; init(mbId, 5, 0); colon = strfind(targetaddress,":",0); comma = strfind(targetaddress,",",0); strncpy(ip, targetaddress, colon); if (comma == -1) { strncpy(port, &targetaddress[colon+1], strlen(targetaddress)-colon-1); *modbusId = 1; } else { strncpy(port, &targetaddress[colon+1], comma-colon-1); strncpy(mbId, &targetaddress[comma+1], strlen(targetaddress)-comma-1); *modbusId = batoi(mbId); } sprintf(connectionString,"/dev/tcp/%s/%s", ip, port); #ifdef DEBUGMODE debugMsg(connectionString); #endif } /* The structure of a Modbus TCP message is: Transaction Id(2 bytes), Protocol(2 bytes), Length(2 bytes), Unit Address(1 byte), Message(N bytes) Where: The Transaction Id field identifies the transaction. The Protocol field is zero to indicate Modbus protocol. The Length field is the number of following bytes. The Unit Address field is the PLC Address encoded as single byte. The Message field is a Modbus PDU. The maximum length of the Message field is is 253 bytes. The maximum Modbus TCP message length is 260 bytes. */ void buildModbusRequest() { int functioncode = MODBUS_FC_READHOLDINGREGISTERS, modbus_readregister_count = Config.byteSizeListOfDatatypes/2; if (Config.readCoils) { functioncode = MODBUS_FC_READCOILS; modbus_readregister_count = Config.no_resultvalues; } Config.modbusRequest[0] = 0x00; Config.modbusRequest[1] = MODBUS_TRANSACTIONID; Config.modbusRequest[2] = 0x00; Config.modbusRequest[3] = 0x00; Config.modbusRequest[4] = 0x00; // Bytelength of Unit-Id and PDU-Message hi Config.modbusRequest[5] = 0x06; // Bytelength of Unit-Id and PDU-Message lo // Beginning of PDU-Message Config.modbusRequest[6] = Config.modbusId; Config.modbusRequest[7] = functioncode; // Set register startaddresses hi an low hex // modbusMsg[8] and [9] are filled up convertHexToBytes(Config.szStartAddress, &Config.modbusRequest[8]); Config.modbusRequest[10] = 0x00; Config.modbusRequest[11] = modbus_readregister_count; } /* Check for errors in receiving the modbus response */ int checkForErrors(int bytesReceived, char *modbusResp) { int errorCode=0, gotResponseError=0, expectedByteSize; char szErrorMsg[100]; expectedByteSize = MODBUS_RESPONSE_OFFSET + Config.byteSizeListOfDatatypes; szErrorMsg[0] = 0; if (bytesReceived > 8) { // Check for Modbus-Error message. Means: Highest bit of byte is set. The following byte // then holds the exception code errorCode = (int)modbusResp[7]; gotResponseError = (errorCode & (1 << 7)); } if ( bytesReceived == 0) sprintf(szErrorMsg,"%s", "Corrupt request or response. Received 0 bytes."); else if ( bytesReceived == -1) sprintf(szErrorMsg,"%s", "No connection to device."); else if (gotResponseError) sprintf(szErrorMsg,"Reading nok. Exceptioncode hex: %02x", modbusResp[8]); else if (bytesReceived != expectedByteSize) sprintf(szErrorMsg, "Received %d bytes don't match expected length of %d", bytesReceived, expectedByteSize); if (szErrorMsg[0] != 0) { setoutputtext(0, szErrorMsg); setoutputtext(1, ""); return 1; } return 0; } /* Get the datatype per outputchannel and store it in an array Keep your data clean as errorhandling is not good Returns the total number of results (not bytes) to read */ void parseInput() { int delim_block=0, delim_number=0, time_colon=0, dtSumView=0, modbusId=0, i, noResults, resultIdx, datatype, currentAddress; char szResults[10], szDatatype[2], handledAddressChar[3], *ipPort, *dtString, *startAddressChar; struct registerData *hlpRegisterData; resultIdx = MODBUS_RESPONSE_OFFSET; startAddressChar = MODBUS_INPUT_ADDRESS_HEX; dtString = MODBUS_INPUT_RESULTS_DT; ipPort = MODBUS_INPUT_IP_PORT; // Some initialization of config structure initInt(Config.szRegisterIdxView, MAX_DATATYPES_DEFINED_EXT, -1); Config.no_resultvalues = 0; Config.readCoils = 0; Config.byteSizeListOfDatatypes = 0; Config.deviceLittleEndian = ! MODBUS_INPUT_BIGENDIAN; Config.modbus_readtime_ms = MODBUS_DEFAULT_READTIME_MS; Config.repeater_sleep_ms = REPEATER_SLEEP_MS; if (Config.repeater_sleep_ms <= 0) Config.repeater_sleep_ms = REPEATER_DEFAULT_SLEEP_MS; // Convert the specified hex-address into a real value strncpy(Config.szStartAddress, startAddressChar, strlen(startAddressChar)); convertHexToBytes(Config.szStartAddress, &handledAddressChar[0]); currentAddress = (handledAddressChar[1]) | (handledAddressChar[0] << 8); // Now lets go to evaulating the datatypes do { time_colon = strfind(&dtString[delim_block], ":", 0); if (time_colon > 0) { strncpy(szResults, &dtString[0], time_colon); Config.modbus_readtime_ms = batoi(szResults); delim_block = time_colon+1; } delim_number = strfind(&dtString[delim_block], DELIMITER_DT_NUMBER, 0); strncpy(szResults, &dtString[delim_block] , delim_number); strncpy(szDatatype, &dtString[delim_block+delim_number+1], 1); noResults = batoi(szResults); datatype = batoi(szDatatype); for (i=0; idatatype = datatype; hlpRegisterData->idxResponse = resultIdx; hlpRegisterData->registerAddress = currentAddress; resultIdx += BYTESIZEMAP[datatype]; if (datatype == COIL) currentAddress += BYTESIZEMAP[datatype]; else currentAddress += BYTESIZEMAP[datatype] / 2; if (datatype != DT_NONE) { Config.szRegisterIdxView[dtSumView] = Config.no_resultvalues; dtSumView++; } Config.no_resultvalues++; } delim_block = strfind(dtString, DELIMITER_DT_BLOCKS, delim_block+1); // Have we reached the last datatype block? if (delim_block == -1) break; delim_block++; } while (TRUE); Config.no_printValues = dtSumView; Config.byteSizeListOfDatatypes = resultIdx - MODBUS_RESPONSE_OFFSET; // Post-handling: Calculate some values on the basis of the given data // Decide whether we are working on coils if (Config.no_resultvalues > 0) { hlpRegisterData = &Config.szRegisters[0]; if (hlpRegisterData->datatype == COIL) { Config.readCoils = 1; Config.byteSizeListOfDatatypes = (Config.byteSizeListOfDatatypes + 7) / 8; Config.no_printValues = Config.byteSizeListOfDatatypes; } } buildConnectionString(ipPort, Config.connectionString, &modbusId); Config.modbusId = modbusId; buildModbusRequest(); for (i=0; iregisterAddress, i); } free(startAddressChar); free(ipPort); free(dtString); } /* Print addresses in a char-array */ void printValue(char *dest, int value, int hasContent) { if (! hasContent) sprintf(dest, "%d", value); else sprintf(dest,"%s, %d", dest, value); } /* Hexify message for handover Only handover results to be displayed and already byte-swapped */ void msgMarshalling(char *hexOutput, int hexOutputSize, char *modbusResponse) { int i, viewIndex; char szResultHelper[3]; short shortHelper, totalBytesSent=0; struct registerData *hlpRegisterData; // First hexify the amount of results being sent shortHelper = (short)(Config.no_printValues - MAX_OUTPUT_CHANNELS); memcpy(szResultHelper, &shortHelper, 2); convertToReadableHex(szResultHelper, 2, hexOutput, 0); totalBytesSent+=2; // Hexify the remaining result datatypes i=MAX_OUTPUT_CHANNELS; while (TRUE) { viewIndex = Config.szRegisterIdxView[i]; if (viewIndex == -1) break; hlpRegisterData = &Config.szRegisters[viewIndex]; // Hexify datatype szResultHelper[0] = (char)hlpRegisterData->datatype; // Downcasting in sizes is ok here convertToReadableHex(szResultHelper, 1, hexOutput, 1); totalBytesSent+=1; // Hexify address shortHelper = (short)hlpRegisterData->registerAddress; memcpy(szResultHelper, &shortHelper, 2); convertToReadableHex(szResultHelper, 2, hexOutput, 1); totalBytesSent+=2; // Hexify value convertToReadableHex(&modbusResponse[hlpRegisterData->idxResponse], BYTESIZEMAP[hlpRegisterData->datatype], hexOutput, 1); totalBytesSent+=BYTESIZEMAP[hlpRegisterData->datatype]; i++; } // Finalize the hexstring by putting the total bytes in front totalBytesSent+=2; memcpy(szResultHelper, &totalBytesSent, 2); sprintf(hexOutput, "%02x %02x %s", szResultHelper[0], szResultHelper[1], hexOutput); } /* Helperfunction for formatting output-value */ void padValue(char *retValue, int value) { if (value <10) sprintf(retValue, "0%d", value); else sprintf(retValue, "%d", value); } /* Gets the current time and returns it formatted. */ char *getFormattedTime(char *buffer) { int now = getcurrenttime(); int year = getyear(now, 1); char s_month[3], s_day[3]; char s_hour[3], s_minute[3], s_second[3]; padValue(s_month, getmonth(now, 1)); padValue(s_day, getday(now, 1)); padValue(s_hour, gethour(now, 1)); padValue(s_minute, getminute(now, 1)); padValue(s_second, getsecond(now, 1)); sprintf (buffer, "%s.%s.%d %s:%s:%s", s_day, s_month, year, s_hour, s_minute, s_second); return buffer; } /* Helperfunctions for initialization */ void init (char *array, int size, int value) { int x; for (x=0; x 0) sleep(sleepMs); } /* Print status and readable information */ void printReadingStatus(int clearError, char *results, int bytesReceived) { char szStatus[RD_BLOCK_SIZE], szInfo[70]; if (strlen(results) > 0) sprintf(szStatus,"Reading ok, %s, %s", results, Config.szPrintedAdresses); else sprintf(szStatus,"Reading ok, %s", Config.szPrintedAdresses); sprintf(szInfo, "Last update: %d bytes at %s", bytesReceived, getFormattedTime(szInfo)); // Remove existing errormsg/refresh output channel if (clearError == 1) setoutputtext(0, ""); setoutputtext(1, szStatus); setoutputtext(2, szInfo); } /************************************************************************ Now lets do the main work Mainfunctionality encapsulated in a function Due to picoC issues a little awkward */ void main() { char modbusResponse[RD_BLOCK_SIZE], modbusStrings[RD_BLOCK_SIZE], hexOutput[TRANSFER_SIZE], handledAddresses[RD_BLOCK_SIZE], szResultHelper[3]; int baseIndex, nBytesReceived, errorInLastIteration=0, errorInCurrentIteration, result_type, result_bytesize, channel, virtChannel, i; unsigned short us_resultValues[MAX_OUTPUT_CHANNELS]; signed short ss_resultValues[MAX_OUTPUT_CHANNELS]; unsigned int ui_resultValues[MAX_OUTPUT_CHANNELS]; signed int si_resultValues[MAX_OUTPUT_CHANNELS]; float f_resultValues[MAX_OUTPUT_CHANNELS]; struct registerData *hlpRegisterData; STREAM* pTcpStream; parseInput(); // Keep polling while (TRUE) { modbusStrings[0] = 0; errorInCurrentIteration = 0; virtChannel = 0; channel = 0; messageParamTimedChannel("Reading Modbus-TCP. Waiting ms: ", Config.modbus_readtime_ms, 0, 1); /****************/ // TCP Request/Response. Putting this in a function leads to instability on // the miniserver when deploying during stream_read() #ifdef DEBUGMODE debugMsg("Creating stream"); #endif pTcpStream = stream_create(Config.connectionString, 0, 0); #ifdef DEBUGMODE debugMsg("Writing on stream"); #endif stream_write(pTcpStream, Config.modbusRequest, MODBUS_MSG_SIZE); #ifdef DEBUGMODE debugMsg("Flushing stream"); #endif stream_flush(pTcpStream); #ifdef DEBUGMODE debugMsg("Reading stream"); #endif nBytesReceived = stream_read(pTcpStream, modbusResponse, RD_BLOCK_SIZE, Config.modbus_readtime_ms); #ifdef DEBUGMODE debugMsgParam("Received bytes on stream: ", nBytesReceived); convertToReadableHex(modbusResponse, nBytesReceived, hexOutput, 0); debugMsg("Printing received bytes on channel 2"); messageParamTimedChannel(hexOutput, STRING_NOPARAM, 3000, 2); #endif stream_close(pTcpStream); /****************/ if (checkForErrors(nBytesReceived, modbusResponse)) { errorInLastIteration = 1; sleep(Config.repeater_sleep_ms); continue; } swapAllBytes(&modbusResponse[MODBUS_RESPONSE_OFFSET]); // Resulthandling // Buffers are big enough. So take the bytes from the responsestream and put them // into the result types/values for (i=0; idatatype; baseIndex = hlpRegisterData->idxResponse; channel = i-virtChannel; // Decode Bits/Bytes to values switch (result_type) { case UINT16: // sizeof(short) == 2 == 16bit us_resultValues[channel] = (modbusResponse[baseIndex]) | (modbusResponse[baseIndex + 1] << 8); break; case INT16: // sizeof(short) == 2 == 16bit ss_resultValues[channel] = (modbusResponse[baseIndex]) | (modbusResponse[baseIndex + 1] << 8); break; case UINT32: // sizeof(int) == 4 == 32bit ui_resultValues[channel] = (modbusResponse[baseIndex] << 16) | (modbusResponse[baseIndex + 1] << 24) | (modbusResponse[baseIndex + 2]) | (modbusResponse[baseIndex + 3] << 8); break; case INT32: // sizeof(int) == 4 == 32bit si_resultValues[channel] = (modbusResponse[baseIndex] << 16) | (modbusResponse[baseIndex + 1] << 24) | (modbusResponse[baseIndex + 2]) | (modbusResponse[baseIndex + 3] << 8); break; case FLOAT16: // sizeof(float) == 8 == 64bit f_resultValues[channel] = calc_float16(&modbusResponse[baseIndex]); break; case FLOAT32: // sizeof(float) == 8 == 64bit f_resultValues[channel] = calc_float32(&modbusResponse[baseIndex]); break; case STRING16: case STRING32: result_bytesize = BYTESIZEMAP[result_type]; decodeBytes2Ascii(&modbusResponse[baseIndex], modbusStrings, result_bytesize); virtChannel++; channel--; break; case COIL: // In case of coils the number of results are coded in bits. Means: 8 coils == 1 Byte setoutput(channel, modbusResponse[baseIndex]); // sizeof(char) == 1 == 8bit break; } channel++; } if (errorInCurrentIteration) { messageParamTimedChannel("Unexpected data error. Received bytes: ", nBytesReceived, 0, 1); sleep(Config.repeater_sleep_ms); continue; } // Handover remaining results. Due to computing delays first do this. if (channel+virtChannel < Config.no_printValues && !Config.readCoils) { msgMarshalling(hexOutput, TRANSFER_SIZE, modbusResponse); messageParamTimedChannel(hexOutput, STRING_NOPARAM, 0, 0); } // Print results on output virtChannel = 0; channel = 0; for (i=0; idatatype; channel = i-virtChannel; // Display all values in a rush switch (result_type) { case UINT16: setoutput(channel, us_resultValues[channel]); break; case INT16: setoutput(channel, ss_resultValues[channel]); break; case UINT32: setoutput(channel, ui_resultValues[channel]); break; case INT32: setoutput(channel, si_resultValues[channel]); break; case FLOAT16: case FLOAT32: setoutput(channel, f_resultValues[channel]); break; case STRING16: case STRING32: virtChannel++; break; } channel++; } printReadingStatus(errorInLastIteration, modbusStrings, nBytesReceived); errorInLastIteration = 0; sleep(Config.repeater_sleep_ms); } } main();