... This is just one way to do, please let me know, if you have other idea ...
It is quite a common demand - especially when developing low level function blocks, drivers - to easily and safely generate meaningful log messages. However, using the standard libraries, CONCAT, TO_STRING, the amount of code used for logging might easily exceed the actual code. It just looks silly, especially for people coming from the C, C++ or C# world... They want to do something like this:
PLCLog.DEBUG('Got param ID:%d; Type:%s Flags:%04X; Len:%d; ', dwID, strTypeClass, dwFlags, dwDataLen);
The goal here is the small footprint, flexibility and not really the performance. The problem is however, that the printf like calls available in CODESYS libraries are only handling one placeholder (I can't see a way to map **args, or va_list to IEC code...), and start to behave crazy if there are more placeholders than arguments. So, here comes a workaround:
// Just a tricky wrapper call, to allow sprintf like functionality with multiple placeholders and
// arguments of ANY type. I Mostly use it for debug log generation - it was not intended
// for fast performance or 100% compliance. Not a very nice implementation, and in my experiance
// can cause access violation ...
//
// The purpose is to implement building string with a very small foorptint in the code.
// For example:
//
// PLCLog.DEBUG( PE6(
// 'Param ID:%d; Type:%s Flags:%04X; pData:%016X; Len:%d; DrvSpec:%016X',
// dwID, strTypeClass, dwFlags, pData, dwDataLen, dwDriverSpec
// ));
//
// As illustrated - due to the limitations of ANY inputs (can't be optional),
// a set of helper functions is provided PE1..PE9 for the given numver of parameters.
//
// Limitations:
// - It's based on CODESYS String Utilities... (STU)... I_Strings ? Ergh...
// ... Could use IECStringutils.Printf - see in the code
// - String length 255
// - Placeholders are from standard implementation (not same as in visualization)
// - Due to ANY parameters, only parameters with Read/write access can be used...
// means : no properties (??? Should be), function call results, or any
// ad-hoc calculated values.
//
// Just take it easy, let me know if you have a better way for this feature
FUNCTION _SPrintf_Segments : STRING(255);
VAR_INPUT
strFormat : STRING(255);
END_VAR
VAR_IN_OUT
aArgs : ARRAY[*] OF __SYSTEM.AnyType;
END_VAR
VAR_OUTPUT
lBound : DINT := LOWER_BOUND(aArgs, 1);
uBound : DINT := UPPER_BOUND(aArgs, 1);
END_VAR
VAR
iLenFormat : INT := 0;
iSegmentCnt : INT := 0;
iSegmentStart : INT := 1;
strSegment : STRING(255);
iPos : INT := 0;
iRes : INT;
END_VAR
VAR_STAT CONSTANT
byPc : BYTE := 37; // '%'
END_VAR
_SPrintf_Segments := UTF8#'';
iPos := 0;
iLenFormat := STU.LEN(strFormat);
iSegmentStart := 1;
iSegmentCnt := 0;
lBound := LOWER_BOUND(aArgs, 1);
uBound := UPPER_BOUND(aArgs, 1);
FOR iPos := 0 TO iLenFormat DO
IF strFormat[iPos] = byPC OR iPos >= iLenFormat THEN // Hit %, or end of format string
IF strFormat[iPos+1] <> byPC THEN // skip %%
IF iSegmentCnt > 0 THEN // The first segment extends to the 2nd % char....
strSegment := STU.MID(strFormat, iPos + 1 - iSegmentStart, iSegmentStart);
iSegmentStart := iPos + 1;
IF iSegmentCnt > UPPER_BOUND(aArgs, 1) THEN
EXIT;
END_IF
iRes := STU.StuSprintf(
pstFormat := ADR(strSegment),
pVarAdr := aArgs[iSegmentCnt].pValue,
udiVarType := aArgs[iSegmentCnt].TypeClass,
pBuffer := ADR(_SPrintf_Segments) + TO_UDINT(LEN(_SPrintf_Segments)),
dwBufferSize := SIZEOF(_SPrintf_Segments) - TO_UDINT(LEN(_SPrintf_Segments))
);
END_IF
iSegmentCnt := iSegmentCnt + 1;
ELSE
iPos := iPos +1;
END_IF
END_IF
END_FOR
IF iPos < iLenFormat THEN
// If more placeholders than arguments, just add the remaining of the format string....
_SPrintf_Segments := STU.CONCAT(_SPrintf_Segments, STU.MID(strFormat,iLenFormat,iPos+1));
END_IF
Well, this is not too bad... so far, but it still requires an array as a parameter, what can bot be done in a compact way within the code (?). So here comes a helper function:
UNCTION PE4 : STRING(255);
VAR_INPUT
strFormat : STRING(255); // Format string
Value1 : ANY; // Value for 1st placeholder
Value2 : ANY; // Value for 2nd placeholder
Value3 : ANY; // Value for 3rd placeholder
Value4 : ANY; // Value for 4th placeholder
END_VAR
VAR
aArgs : ARRAY[1..4] OF __SYSTEM.AnyType := [
Value1, Value2, Value3, Value4
];
END_VAR
PE4 := _Sprintf_Segments(strFormat, aArgs);
Yes... Unfortunately, it is not possible to make input variables of ANY type optional... So unfortunately, there is a different helper function for 2, 3, 4 ... 9 parameters. :( Have a better idea?
This way, the final call looks like:
PLCLog.DEBUG( PE4('Got param ID:%d; Type:%s Flags:%04X; Len:%d;', dwID, strTypeClass, dwFlags, dwDataLen) );
Unfortunately I cannot edit this ticket... So sorry for the layout issues...
P.