/****************************************************************** 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, 01/2026 ...Pico-C is in somekind very special in syntax and possibilities... ******************************************************************/ /********** Main configuration block **************** */ // Do we configure this code from inside ourself or is a // Textgenerator-component connected. Either keep the line or // outcomment the next line like this: // #define CODE... #define CODECONFIG_BY_TEXTGENERATOR // If not using the textgenerator specify your data here #ifndef CODECONFIG_BY_TEXTGENERATOR #define MODBUS_INPUT_IP_PORT "192.168.13.xx:1502" #define MODBUS_INPUT_ADDRESS_HEX "9D0E" #define MODBUS_INPUT_RESULTS_DT "20x2" #define MODBUS_INPUT_BIGENDIAN 1 #endif /* ****************************************************/ // 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 }; // Shall we run in debugmode and print some message on the outputchannels? 0 or 1 #define DEBUGMODE 0 // Inputchannels when using the textgenerator component #ifdef CODECONFIG_BY_TEXTGENERATOR #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) #endif // 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 // Mostly a default value but configurable #define MODBUS_DEVICE_ID 1 // 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 40 // Time spent in reading the responsestream/waiting for data, timeout in ms // This timeout is a critical one. Redeployments on the miniserver during streamreading // results in a miniserver crash. To my knowledge there is no workaround but minimizing // this value to a minimum, but high enough dealing with your device. The stream reading // implementation is provided by Loxone itself. As this error is existing for over 10 years // lets hope the best for the next 10 years. :-) #define MODBUS_READTIME_MS 1000 // Blocksize upper limit for reading response data #define RD_BLOCK_SIZE 300 // 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 displayable results over all components #define MAX_OUTPUT_CHANNELS_TOTAL 2*MAX_OUTPUT_CHANNELS // Sleeptime before repeating request in milliseconds #define REPEATER_SLEEP_MS 10000 // Definition of delimiters for datainput, defining // datatypes and the number of results #define DELIMITER_DT_BLOCKS "," #define DELIMITER_DT_NUMBER "x" /* 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 */ void convertToReadableHex(char *bytes, int byteLen, char *dest) { int i; if (byteLen > 0) { if (strlen(dest) == 0) sprintf(dest,"%02x", bytes[0]); else sprintf(dest,"%s %02x",dest, bytes[0]); } for (i=1; i hexlen) break; bit4_1 = hexString[i]; bit4_2 = hexString[i+1]; // Ignore whitespaces if (bit4_1 == ' ') { i++; continue; } // Do the transformation: 1 Byte is 2x 4bits to be converted // Read pairwise if (bit4_1 >= '0' && bit4_1 <= '9') bit4_1 = bit4_1 - '0'; else if (bit4_1 >= 'a' && bit4_1 <='f') bit4_1 = bit4_1 - 'a' + 10; else if (bit4_1 >= 'A' && bit4_1 <='F') bit4_1 = bit4_1 - 'A' + 10; if (bit4_2 >= '0' && bit4_2 <= '9') bit4_2 = bit4_2 - '0'; else if (bit4_2 >= 'a' && bit4_2 <='f') bit4_2 = bit4_2 - 'a' + 10; else if (bit4_2 >= 'A' && bit4_2 <='F') bit4_2 = bit4_2 - 'A' + 10; // stick together the two 4bit values resultBytes[resultcounter] = (bit4_1 << 4) | (bit4_2 & 0xF); i = i+2; resultcounter++; } while (i < hexlen); return resultcounter; } /* The bytes represent ascii-encoded values. Decode them... */ void decodeBytes2Ascii(char *src, char *dest, int len) { char hlp[32]; init(hlp, 32, 0); int tmpLen = strlen(src); if (tmpLen < len) len = tmpLen; if (len == 0) return; strncpy(hlp, src, len); if (strlen(dest) > 0) sprintf(dest,"%s, %s", dest, hlp); else sprintf(dest,"%s", hlp); } /* PicoC works little endian Supported values are arraylengths of 2 and 4 */ void swapBytes(char *bytes, int result_type, int littleendian) { // In these cases return unmodified if (littleendian || result_type == STRING16 || result_type == STRING32 || result_type == COIL) return; // Maximum size possible char tmpBytes[4]; int len = 0; int i=0; len = getBytesizeByType(result_type); for (i=0; i < len; i++) tmpBytes[i] = bytes[i]; if (len == 2) { bytes[0] = tmpBytes[1]; bytes[1] = tmpBytes[0]; } else if (len == 4) { bytes[0] = tmpBytes[3]; bytes[1] = tmpBytes[2]; bytes[2] = tmpBytes[1]; bytes[3] = tmpBytes[0]; } } /* Helper function for retrieving bytesizes */ int getBytesizeByType(int type) { if (type == UINT16 || type == INT16 || type == FLOAT16) return 2; else if (type == UINT32 || type == INT32 || type == FLOAT32) return 4; else if (type == STRING16) return 8; else if (type == STRING32) return 16; else if (type == COIL) return 1; return 0; // Unknown } /* Helper function for retrieving bytesizes */ int getBytesizeByList(int *types, int sizeOfList) { int bytesize=0, i; 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))); } /* Helperfunction for initialization */ char* init(char *array, int size, int value) { int x; for (x=0; x 8 && modbusResp[7] >= 129 && modbusResp[7] <= 134; if ( bytesReceived - MODBUS_RESPONSE_OFFSET < 0 || bytesReceived != expected_bytesize || error_code == 1 ) { if (error_code == 0) sprintf(errorMsg,"%s", "Reading nok or no connection to device."); else sprintf(errorMsg,"%s Exceptioncode hex: %02x", "Reading nok.", modbusResp[8]); setoutputtext(0, errorMsg); 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 */ int parseDatatypes(char *dtString, int *listOfDatatypes, int listSize) { int delim_block=0, delim_number=0, dtSum=0, i, noResults, datatype; char szResults[3], szDatatype[2]; for (i=0; i 0) sprintf(status,"%s, %s", status, results); // Remove existing errormsg/refresh output channel if (clearError == 1) setoutputtext(0, ""); setoutputtext(1, status); } /* Hexify message for handover */ void msgMarshalling(char *hexOutput, int hexOutputSize, int endian, int *szListOfDatatypes, char *modbusResponse, int baseIndex) { int i, result_type, result_bytesize; char szResultHelper[2]; // Handover the remaining results as hex-string init(hexOutput, hexOutputSize, 0); init(szResultHelper, 2, 0); // Hexify info about endian szResultHelper[0] = (char)(!endian); convertToReadableHex(szResultHelper, 1, hexOutput); // Hexify the remaining result datatypes for (i=0; i 0 && szListOfDatatypes[0] == COIL) bReadCoils = 1; modbus_resultregister_type_bytesize = getBytesizeByList(szListOfDatatypes, MAX_OUTPUT_CHANNELS_TOTAL); // Coils are handled different if (bReadCoils) { buildModbusRequest(modbusRequest, no_resultvalues, MODBUS_FC_READCOILS); no_printValues = modbus_resultregister_type_bytesize; } else { buildModbusRequest(modbusRequest, modbus_resultregister_type_bytesize/2, MODBUS_FC_READHOLDINGREGISTERS); no_printValues = no_resultvalues; } // if we have more than MAX_OUTPUT_CHANNELS results (limited outputchannels...) // then hand over results after MAX_OUTPUT_CHANNELS-1 outputs if (!bReadCoils && no_printValues > MAX_OUTPUT_CHANNELS) no_printValues = MAX_OUTPUT_CHANNELS; buildConnectionString(MODBUS_INPUT_IP_PORT, connectionString); // Keep polling while (TRUE) { init(modbusStrings, RD_BLOCK_SIZE, 0); init(okString, RD_BLOCK_SIZE, 0); baseIndex = MODBUS_RESPONSE_OFFSET; setoutputtext(1, "Reading Modbus-TCP. Do not interrupt!"); nBytesReceived = doModbusRequest(connectionString, modbusRequest, MODBUS_MSG_SIZE, modbusResponse, RD_BLOCK_SIZE); setoutputtext(1, "Finished reading Modbus-TCP"); if (checkForErrors(nBytesReceived, modbusResponse, MODBUS_RESPONSE_OFFSET+modbus_resultregister_type_bytesize)) { errorInLastIteration = 1; sleep(REPEATER_SLEEP_MS); continue; } // Resulthandling // Buffers are big enough. So take the bytes from the responsestream and put them // into the result types/values for (i=0; i MAX_OUTPUT_CHANNELS && szListOfDatatypes[0] != COIL) { msgMarshalling(hexOutput, RD_BLOCK_SIZE, bLittle_endian, szListOfDatatypes, modbusResponse, baseIndex); setoutputtext(0, hexOutput); } errorInLastIteration = 0; sleep(REPEATER_SLEEP_MS); } } main();