Geppo Posted April 6 Posted April 6 Hello all, I'm trying to use the libmodbus.dll 3.1.10 to connect a modbus device using modbus rtu. I think I succesfully used the modbus_new_rtu function to create the needed context, but although it uses a quite simple syntax, I cannot succeded to use the modbus_set_client function of the dll. It always return a fail code (-1). I cannot understand what is going wrong. Anyone here used the libmodbus dll to connect a device using modbus rtu? Here is the simple code I used to test libmodbus functions: #include <MsgBoxConstants.au3> #include <Array.au3> ; modbus_t *modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit); Local $Port = "\\.\COM7" Local $BaudRate = 19200 Local $Parity = "E" ; N=None, E=Even O=Odd Local $DataBits = 8 Local $StopBits = 1 Local $Modbus_Slave = 5 Local $hDLL = DllOpen("modbus.dll") If @error Then MsgBox($MB_SYSTEMMODAL, "Error", "Error opening modbus.dll, Error code: " & @error) Else Local $RTU_Ptr = DllCall($hDLL, "ptr:cdecl", "modbus_new_rtu", "str", $Port, "int", $BaudRate, "str", $Parity, "int", $DataBits, "int", $StopBits) If @error Then MsgBox($MB_SYSTEMMODAL, "Error", "Error calling function modbus_new_rtu, Error code: " & @error) Else MsgBox($MB_SYSTEMMODAL, "Info", "Exit code: " & $RTU_Ptr[0]) _ArrayDisplay($RTU_Ptr) EndIf Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_slave", "ptr", $RTU_Ptr, "int", $Modbus_Slave) If @error Then MsgBox($MB_SYSTEMMODAL, "Error", "Error calling function modbus_set_slave, Error code: " & @error) Else _ArrayDisplay($RTU_Result) EndIf DllClose ($hDLL) EndIf Thank you in advance for your help.
MattyD Posted April 6 Posted April 6 Hi Geppo, Its a bit difficult without having the dll to play with, but a couple of general things if it helps... DllCall() returns an array, and the return value ends up in element 0. So for the code below, the returned ptr would be in $aCall[0].. $aCall = DllCall($hDll, "ptr", "SomeFunc", "int", $iParam1) Also, the "str" type passes a pointer to a char array. So to pass a single char for the parity param, you'll likely need to use "BYTE"
Geppo Posted April 6 Author Posted April 6 Thank you very much for your suggestions. For what I succeed to understand from the modbus_set_client function documentation here: modbus_set_slave the returned value is an int, while I have to supply a pointer to the RTU data structure created using the modbus_new_rtu function documented here: modbus_new_rtu that instead seems it was correctly created. Also, the Autoit DllCall function, documented here DllCall seems to require as second parameters the type of returned data, that in the specific case of modbus_set_slave function is an int (not a pointer), followed by a pointer to the RTU data structure and finally the slave ID as an int. I suppose you didn't run the code, otherwise you could have seen that the RTU data structure was correctly created and the Autoit DllCall does not return errors (meaning the call was correctly formed) and the return value of the called function modbus_new_rtu is zero, confirming it works. Unfortunately when I try to call the modbus_set_slave function, DllCall return no errors, meaning the call was correctly formed but the modbus_set_slave function return -1 meaning it didn't work. Anyway, for confirmation, I tried to use "byte" instead "str" for the Parity parameter as you suggested. The char "E" after the call was changed to "0" in the RTU data structure, so it doesn't sound good. You can verify it running the code. Here you can find the ready-to-use compiled libmodbus ddl files: libmodbus ready-to-use-dll I attached two screenshots of the array returned after the DllCall to the function modbus_new_rtu and the array returned after the DllCall to the function modbus_set_slave
Solution MattyD Posted April 6 Solution Posted April 6 (edited) Thanks for that link - give this one a go Edit: this was using the 64bit dll! - for the x86 I needed to use :cdecl as you have done in your example above! #AutoIt3Wrapper_UseX64=Y Global $hDLL = DllOpen("modbus.dll") Local $sPort = "\\.\COM7" Local $iBaudRate = 19200 Local $sParity = "E" ; N=None, E=Even O=Odd Local $iDataBits = 8 Local $iStopBits = 1 Local $iModbus_Slave = 5 Local $pRTU, $bSuccess $pRTU = _ModBus_NewRTU($sPort, $iBaudRate, $sParity, $iDataBits, $iStopBits) ConsoleWrite("pRTU = " & $pRTU & @CRLF) If $pRTU Then $bSuccess = _ModBus_SetSlave($pRTU, $iModbus_Slave) ConsoleWrite("Set Slave Success = " & $bSuccess & @CRLF) _ModBus_Close($pRTU) DllClose($hDLL) Func _ModBus_NewRTU($sPort, $iBaudRate, $sParity, $iDataBits, $iStopBits) Local $aCall = DllCall($hDLL, "ptr", "modbus_new_rtu", "str", $sPort, "int", $iBaudRate, "byte", Asc($sParity), "int", $iDataBits, "int", $iStopBits) If @error Then Return SetError(@error, @extended, 0) Return $aCall[0] EndFunc Func _ModBus_SetSlave($pRTU, $iSlave) Local $aCall = DllCall($hDLL, "int", "modbus_set_slave", "ptr", $pRTU, "int", $iSlave) If @error Then Return SetError(@error, @extended, 0) Return $aCall[0] = 0 EndFunc Func _ModBus_Close($pRTU) DllCall($hDLL, "none", "modbus_clode", "ptr", $pRTU) EndFunc Edited April 6 by MattyD
Geppo Posted April 7 Author Posted April 7 Thank you very much for your help. Looking at your code I understood lots of things.... 🙂 Firstly, my bad mistake about Parity, then the reason why the DllCall to the function modbus_set_slave couldn't work. I wrote $RTU_Ptr instead $RTU_Ptr[0] to pass the pointer....... What I don't understand in your code is "Return $aCall[0] = 0" in the Set_Slave function code. Moreover although you code works perfectly with the x86 dll (switching to the cdecl calling convention and disabling the #AutoIt3Wrapper_UseX64=Y directive), I was not able to make it working using the x64 dll. It always returns: pRTU = 0 Set Slave Success =
Geppo Posted April 7 Author Posted April 7 7 minutes ago, Geppo said: What I don't understand in your code is "Return $aCall[0] = 0" in the Set_Slave function code. The correct function name where you wrote the above code is "_ModBus_SetSlave"
MattyD Posted April 7 Posted April 7 No worries mate the $aCall[0] = 0 expression is just a quick way to get the function to return True/False. The dll call returns 0 on success - So if ($aCall[0] = 0) we return True, otherwise we return False. Hope that makes sense! with the 64bit stuff, do you by chance have the full version of SciTE4AutoIt installed? I'm not 100% sure if the directive works without it... (you can verify autoit_64.exe is being run by looking in the console) >Running:(3.3.16.1):C:\Program Files (x86)\AutoIt3\autoit3_x64.exe "C:\Users\matt\Downloads\libmodbus-3.1.10_VS2008_X64\lib&dll\x64\Release\test.au3" +>Setting Hotkeys...--> Press Ctrl+Alt+Break to Restart or Ctrl+BREAK to Stop. pRTU = 0x00000253258D0C40 Set Slave Success = True
Geppo Posted April 7 Author Posted April 7 Thank you again for you help. Unfortunately I discovered it always runs autoit.exe, so I would have to make some change / update of the current Autoit installation. I will do, but it it is not so important at the moment. Instead, although I went forward in testing libmodbus.dll basic functions, I didn't succeded to retrieve the content of some registers using function modbus_read_registers. My intent is to simply test all functions exposed by the dll that I think I will need before coding all the functions to manage them. What sounds odd I verified the correctness of the command issued activating the debug mode, as well as the proper device reply (I can see the requested data in the reply of the device), but I didn't succeed to extract them. This is what I get from the console after activating the debug mode: [01][03][00][00][00][08][44][0C] Waiting for a confirmation... <01><03><10><00><00><00><00><00><00><00><00><00><00><00><00><00><00><03><DD><24><F0> confirming the correctness of the command (SlaveID 1, Function 03, 0 address, 8 registers, CRC16) and of the device reply where 03DD (989) is the content of the last requested device register. This is the code I used to retrieve and to display the contents of the registers (after setting slave ID 1 and successfully connecting): Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "boolean", True) If @error Then MsgBox($MB_SYSTEMMODAL, "Error", "Error calling function modbus_set_debug, Error code: " & @error) Else _ArrayDisplay($RTU_Result, "Modbus Set Debug") EndIf $iRegisterAddr = 0 $iRegisterNum = 8 Local $RegData_t = "struct;word data[" & $iRegisterNum & "];endstruct" Local $tRegisterData = DllStructCreate($RegData_t) Local $aRTU_Result = DllCall($hDLL, "int:cdecl", "modbus_read_registers", "ptr", $pRTU_Ptr[0], "int", $iRegisterAddr, "int", $iRegisterNum, "struct*", $tRegisterData) If @error Then MsgBox($MB_SYSTEMMODAL, "Error", "Error calling function modbus_read_register, Error code: " & @error) Else _ArrayDisplay($aRTU_Result, "Modbus Read Register") MsgBox($MB_SYSTEMMODAL, "Info", "$tRegisterData Struct Size: " & DllStructGetSize($tRegisterData) & @CRLF & _ "Struct pointer: " & DllStructGetPtr($tRegisterData) & @CRLF & _ "Data:" & @CRLF & _ "[1] " & DllStructGetData($tRegisterData, 1) & @CRLF & _ "[2] " & DllStructGetData($tRegisterData, 2) & @CRLF & _ "[3] " & DllStructGetData($tRegisterData, 3) & @CRLF & _ "[4] " & DllStructGetData($tRegisterData, 4) & @CRLF & _ "[5] " & DllStructGetData($tRegisterData, 5) & @CRLF & _ "[6] " & DllStructGetData($tRegisterData, 6) & @CRLF & _ "[7] " & DllStructGetData($tRegisterData, 7) & @CRLF & _ "[8] " & DllStructGetData($tRegisterData, 8)) EndIf ; Alternative to display registers contents ;For $i = 1 To 8 ; ConsoleWrite("[" & $i & "] = " & $tRegisterData.data($i) & @CRLF) ;Next Unfortuanately it doesn't work and I always retrieve zero, also for the last register.
MattyD Posted April 7 Posted April 7 I don't have a device to test on - but I'd check this... That word[] array is all one element in the struct, so you'll need to go by indices with DllStructGetData. "[1] " & DllStructGetData($tRegisterData, 1, 1) & @CRLF & _ "[2] " & DllStructGetData($tRegisterData, 1, 2) & @CRLF & _ This might also help with debugging... #include <WinAPI.au3> at the top, then... $iRegisterAddr = 0 $iRegisterNum = 8 Local $RegData_t = "struct;word data[" & $iRegisterNum & "];endstruct" Local $tRegisterData = DllStructCreate($RegData_t) Local $aRTU_Result = DllCall($hDLL, "int:cdecl", "modbus_read_registers", "ptr", $pRTU_Ptr[0], "int", $iRegisterAddr, "int", $iRegisterNum, "struct*", $tRegisterData) If @error Then ConsoleWrite("Dll Call Failed" & @CRLF) Else If $aRTU_Result[0] = -1 Then $aCall = DllCall($hDLL, "str:cdecl", "modbus_strerror", "int", _WinAPI_GetLastError()) ConsoleWrite($aCall[0] & @CRLF) Else ConsoleWrite("Registers Read:" & $aRTU_Result[0] & @CRLF) EndIf EndIf PS: we got away with it, but just be careful with data typing here: Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "boolean", True) "boolean" is a 1-byte type, but the modbus_set_debug expects a 4-byte "int". Using the 4-byte "bool" type would be a better option ...but to be pedantic we should probably do something like this as its what's documented. Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "int", 1) ;Or to use True/False (AutoIt should cast True to "int"): Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "int", True)
Geppo Posted April 8 Author Posted April 8 Thank you very much. You are an invaluable source of hints. About the modbus_set_debug function, Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "int", 1) was the first syntax I used but it didn't work, so looking at the documentation, it speaks of "flag", so I supposed a flaw in the documentation, I used "boolean" and worked. Anyway, I will verify it. Surely you are right, the bug is in the wrong retrieving of the registers. I should have to use "[1] " & DllStructGetData($tRegisterData, 1, 1) & @CRLF & _ "[2] " & DllStructGetData($tRegisterData, 1, 2) & @CRLF & _ and so on to retrieve the register value. Today I cannot test it, but I will verified as soon as possible. Thank you again.
Geppo Posted April 9 Author Posted April 9 You were right. Now I succeed to read the content of registers using "[1] " & DllStructGetData($tRegisterData, 1, 1) & @CRLF & _ "[2] " & DllStructGetData($tRegisterData, 1, 2) & @CRLF & _ And now also works Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "int", 1) using an int parameter. Evidently I have been mistaken. I will proceed to test other modbus functions, then I will code some Autoit functions to manage them at the best. Than you very much for your help. MattyD and argumentum 2
Geppo Posted April 21 Author Posted April 21 MattyD, I'm sorry to bother you again..... I encountered a new issue. I don't succeed to get the libmodbus functions error codes. I wrote some functions, calling libmodbus internal funtions and they work properly, I get the return value of the functions that in the event of an error is set to -1 but the variable @error is always set to zero, so I don't succed to obtain a more detailed information about the error. Probably I am not doing what needed. Here is an example. Func _Modbus_Write_Single_Register($pRTU, $iAddr, $iRegData) Local $aCall = DllCall($hDLL, "int:cdecl", "modbus_write_register", "ptr", $pRTU, "int", $iAddr, "int", $iRegData) If @error Then Return SetError(@error, @extended, -1) Return $aCall[0] EndFunc The function _Modbus_Write_Single_Register uses modbus function code 0x06. If the device does not support that function number, I get an "illegal function" reply from the device, exactly what I can see in the debug output: <05><86><01><C2><61> ERROR Illegal function Unfortunately I only get the -1 return value, but @error and @extended are always set to zero. Also, I was not able to capture the debug output to try to extract some useful information from it. Any suggestions?
MattyD Posted Monday at 01:57 PM Posted Monday at 01:57 PM no worries at all. @error is only set if DllCall() can't execute the call - so for things like bad dll handles, or if the function name doesn't exist in the dll file etc. You should be able to retrieve the modbus_write_register error with _WinAPI_GetLastError(). Heads up - pretty much anything you do will set that error value (including writing to the console), so grab it before calling anything else
Geppo Posted Monday at 06:20 PM Author Posted Monday at 06:20 PM Thank you for the hint. Just to test it, I changed the code as shown below but I still obtain a 0 error code from the _WinAPI_GetLastError() function (it should be 1 - Illegal modbus function code). Func _Modbus_Write_Single_Register($pRTU, $iAddr, $iRegData) Local $aCall = DllCall($hDLL, "int:cdecl", "modbus_write_register", "ptr", $pRTU, "int", $iAddr, "int", $iRegData) Local $iFunctionErr = _WinAPI_GetLastError() MsgBox($MB_SYSTEMMODAL, "Info", "modbus_write_register, Error code: " & $iFunctionErr) ;If @error Then Return SetError(@error, @extended, -1) Return $aCall[0] EndFunc
Geppo Posted Tuesday at 10:40 AM Author Posted Tuesday at 10:40 AM I also tried to update to the latest Autoit release. Unfortunately it still doesn't work. The return error value is always 0.
Geppo Posted Wednesday at 05:38 AM Author Posted Wednesday at 05:38 AM Any chance to retrieve the errno variable that the libmodbus.dll sets? https://libmodbus.org/reference/modbus_write_register
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now