/****************************************************************** 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, 11/2025 ...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, values 0 or 1 #define CODECONFIG_BY_TEXTGENERATOR 1 // Following 4 lines can be ignored when using the textgenerator #define MODBUS_INPUT_IP_PORT "192.168.13.xx:1502" #define MODBUS_INPUT_ADDRESS_HEX "9D0E" #define MODBUS_INPUT_NO_RESULTS 20 #define MODBUS_INPUT_DATATYPE INT16 // Specify whether the modbus-device being read delivers its data big endian(1) or not(0) #define MODBUS_DEVICE_BIGENDIAN 1 // Shall we run in debugmode and print some message on the outputchannels? 0 or 1 #define DEBUGMODE 0 /* ****************************************************/ // supported modbus datatypes enum registerDatatype { DT_NONE = 0, UINT16, INT16, UINT32, INT32, FLOAT16, FLOAT32, STRING16, STRING32, COIL }; // 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 // Sleeptime before repeating request in milliseconds #define REPEATER_SLEEP_MS 10000 /* 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) { // In these cases return unmodified if (!MODBUS_DEVICE_BIGENDIAN || 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 } /* calc float16 value */ float calc_float16(char *flt16_bits) { char sign_bit; char exponent; unsigned int significand; // IEEE 754 half precision sign_bit = (flt16_bits[0] & 0x80) >> 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 - MODBUS_RESPONSE_OFFSET) % resultregister_type_bytesize) != 0 || 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; } /* Print status and readable information */ void printReadingStatus(int clearError, char *status, char *results) { sprintf(status,"%s", "Reading ok"); if (strlen(results) > 0) sprintf(status,"%s, %s", status, results); // Remove existing errormsg/refresh output channel if (clearError == 1) setoutputtext(0, ""); setoutputtext(1, status); } /* In case of debugmode print a message and have a short break to assure that the message can be read */ void debugMsg(char *msg) { debugMsgParam(msg, -9999); } void debugMsgParam(char *msg, int val) { if (!DEBUGMODE) return; char debugmsg[150]; if (val == -9999) sprintf(debugmsg,"[DEBUG] %s", msg); else sprintf(debugmsg,"[DEBUG] %s %d", msg, val); setoutputtext(0, debugmsg); sleep(1000); } /************************************************************************ Now lets do the main work Mainfunctionality encapsulated in a function Due to picoC issues a little awkward */ void main() { char modbusRequest[MODBUS_MSG_SIZE], modbusResponse[RD_BLOCK_SIZE], connectionString[MODBUS_CONNECTIONSTRING_SIZE], okString[RD_BLOCK_SIZE], modbusStrings[RD_BLOCK_SIZE], hexOutput[RD_BLOCK_SIZE]; int baseIndex, nBytesReceived, errorInLastIteration, no_resultvalues, no_printValues, result_type, modbus_resultregister_type_bytesize, channel, i; char szResultHelper[2]; unsigned short us_resultValue; signed short ss_resultValue; unsigned int ui_resultValue; signed int si_resultValue; if (CODECONFIG_BY_TEXTGENERATOR) { no_resultvalues = getinput(0); result_type = getinput(1); buildConnectionString(getinputtext(0), connectionString); } else { no_resultvalues = MODBUS_INPUT_NO_RESULTS; result_type = MODBUS_INPUT_DATATYPE; buildConnectionString(MODBUS_INPUT_IP_PORT, connectionString); } // Byte-size of result registers // For the ease of use all resultsizes are assumed to be the same size modbus_resultregister_type_bytesize = getBytesizeByType(result_type); if (result_type == COIL) { buildModbusRequest(modbusRequest, no_resultvalues, MODBUS_FC_READCOILS); no_printValues = (no_resultvalues + 7) / 8; } else { buildModbusRequest(modbusRequest, no_resultvalues*(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 (no_printValues > MAX_OUTPUT_CHANNELS) no_printValues = MAX_OUTPUT_CHANNELS; // Keep polling while (TRUE) { init(modbusStrings, RD_BLOCK_SIZE, 0); init(okString, RD_BLOCK_SIZE, 0); setoutputtext(1, "Reading Modbus-TCP. Do not interrupt!"); nBytesReceived = doModbusRequest(connectionString, modbusRequest, MODBUS_MSG_SIZE, modbusResponse, RD_BLOCK_SIZE, no_resultvalues, result_type); setoutputtext(1, "Finished reading Modbus-TCP"); if (checkForErrors(nBytesReceived, modbusResponse, 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 && result_type != COIL) { // Handover the remaining results as hex-string init(hexOutput, RD_BLOCK_SIZE, 0); init(szResultHelper, 2, 0); // Hexify result datatype and then the remaining result data memcpy(szResultHelper, &result_type, 1); convertToReadableHex(szResultHelper, 1, hexOutput); convertToReadableHex(&modbusResponse[baseIndex + modbus_resultregister_type_bytesize], (no_resultvalues - MAX_OUTPUT_CHANNELS) * modbus_resultregister_type_bytesize, hexOutput); setoutputtext(0, hexOutput); } errorInLastIteration = 0; sleep(REPEATER_SLEEP_MS); } } main();