There are so many different programming languages that Yoctopuce obviously can't offer libraries for each of them. One of our customers happened to have a Yoctopuce module but no official library to use with it. So he did something we weren't expecting: he transformed the C++ programming example into a DLL and he coded calls to this DLL from his favorite programming language. It's really smart. This week, we are going to explain this method to interface a Yoctopuce relay from the GO programming language.
The principle
Starting from a programming example is a great idea because it prevents you from starting from scratch: C++ programming examples already have a good structure to create a DLL. And in the opposite of the yapi.dll DLL, this enables you to take advantage of all of the power of the Yoctopuce high level API. We decided to use this method with the GO language, but you can do so with any programming language able to make calls to a classic DLL.
To make it work in your favorite programming language, you must know how to perform the following actions with this language:
- Load a classic DLL
- Make calls to this DLL
- Pass integers as parameters
- Pass character strings (char*) as parameters
- Retrieve character strings (char*)
For character strings, you can cheat by passing byte arrays.
Creating the DLL
Let's start by opening one of the C++ programming examples with Visual-Studio. Here, we use the Yocto-PowerRelay-V2 example, that is Doc-GettingStarted-Yocto-PowerRelay. The first step is to change the type of target so that Visual-Studio generates a DLL instead of an EXE. We must also check that the DLL architecture corresponds truly to the architecture of the main application: here we work in 64-bit (x64) because we use the 64-bit version of GO. If need be, we can change the output directory so that the DLL lands in a more practical location than bin\debug.
Configuring the project to create a DLL
When the project is configured, you can remove the main() function of the example and start to code the content of the DLL. We need the following functions:
- YRegisterHub : to initialize the Yoctopuce API thanks to YAPI::RegisterHub()
- YFreeAPI : to free the API with YAPI::FreeAPI().
- YRelay_findRelay: To find a relay depending on its name with YRelay::FindRelay()
- YRelay_firstRelay: to find the first available relay (YRelay::FirstRelay())
- YRelay_nextRelay: to list the relays (relay→nextRelay())
- YRelay_set_state: to toggle the state of a relay (relay→set_state()).
- YRelay_isOnline: to know if a relay is connected (relay→isOnline()).
- YRelay_get_hardwareId: to know the hardware name of a relay (relay→get_hardwareId()).
YFreeAPI
It's the easiest function: it doesn't take any parameter
{
YAPI::FreeAPI();
return 1;
}
YRegisterHub
It's a slightly tricky function because it takes a character string (url) as input and returns another one (errMsg) as output. For the string to be returned, we made it as simple as possible: the caller provides a pointer to a buffer (char* errMsg) and its size (int maxlen) and the function fills this buffer, making sure not to overflow.
{
string error;
YAPI::DisableExceptions();
int res = YAPI::RegisterHub(url, error);
strncpy(errMsg, error.c_str(), maxlen);
error[maxlen - 1] = 0;
return res;
}
YRelay_firstRelay
YRelay_firstRelay provides the first available relay. In the Yoctopuce API, relays are objects that we can't naturally pass as such to GO. However, we can perfectly well pass pointers to this objects by casting them as 64 bit integers.
{
YRelay* relay = YRelay::FirstRelay();
if (relay == NULL) return 0;
return (s64)relay;
}
YRelay_set_state
YRelay_set_state toggles the relay. The parameters are thus the 64bit integer referencing the relay (r) and the desired state (state).
{
YRelay* relay = (YRelay*)r;
relay->set_state(state == 0 ? Y_STATE_A : Y_STATE_B);
return 1;
}
The rest
The other calls work according to the same principle, here is the complete code of the DLL:
#include "yocto_relay.h"
#include <iostream>
#include <ctype.h>
#include <stdlib.h>
using namespace std;
extern "C" __declspec(dllexport) int YRegisterHub(char* url, char* errMsg, int maxlen)
{
string error;
YAPI::DisableExceptions();
int res = YAPI::RegisterHub(url, error);
strncpy(errMsg, error.c_str(), maxlen);
error[maxlen - 1] = 0;
return res;
}
extern "C" __declspec(dllexport) int YFreeAPI()
{
YAPI::FreeAPI();
return 1;
}
extern "C" __declspec(dllexport) s64 YRelay_findRelay(char* name)
{
YRelay* relay = YRelay::FindRelay(name);
return (s64)relay;
}
extern "C" __declspec(dllexport) s64 YRelay_firstRelay()
{
YRelay* relay = YRelay::FirstRelay();
if (relay == NULL) return 0;
return (s64)relay;
}
extern "C" __declspec(dllexport) s64 YRelay_nextRelay(s64 r)
{C
if (r == 0) return 0;
YRelay* relay = (YRelay*)r;
relay = relay->nextRelay();
if (relay == NULL) 0;
return (s64) relay;
}
extern "C" __declspec(dllexport) int YRelay_set_state(s64 r, int state)
{
YRelay* relay = (YRelay*)r;
relay->set_state(state == 0 ? Y_STATE_A : Y_STATE_B);
return 1;
}
extern "C" __declspec(dllexport) int YRelay_isOnline(s64 r)
{
YRelay* relay = (YRelay*)r;
if (relay->isOnline()) return 1;
return 0;
}
extern "C" __declspec(dllexport) const int YRelay_get_hardwareId(s64 r, char* res, int maxlen )
{
YRelay* relay = (YRelay*)r;
string name = relay->get_hardwareId();
strncpy(res, name.c_str(), maxlen );
res[maxlen - 1] = 0;
return 1;
}
The GO part
For the GO part, we must start by loading the DLL and finding entry points
var ( myDLL, _ = syscall.LoadLibrary("demo.dll") _YInitAPI, _ = syscall.GetProcAddress(myDLL, "YInitAPI") _YFreeAPI, _ = syscall.GetProcAddress(myDLL, "YFreeAPI") _YRelay_findRelay, _ = syscall.GetProcAddress(myDLL, "YRelay_findRelay") _YRelay_set_state, _ = syscall.GetProcAddress(myDLL, "YRelay_set_state") _YRelay_get_hardwareId , _ = syscall.GetProcAddress(myDLL, "YRelay_get_hardwareId") _YRelay_firstRelay, _ = syscall.GetProcAddress(myDLL, "YRelay_firstRelay") _YRelay_nextRelay, _ = syscall.GetProcAddress(myDLL, "YRelay_nextRelay") )
Then we must implement the calls themselves:
YFreeAPI
Here as well, it's the easiest function:
func YFreeAPI() () { syscall.Syscall(uintptr(_YFreeAPI),0,0,0,0) }
YRegisterHub
Here, it's somewhat more complex because we must convert the character strings into byte buffers.
func YInitAPI( url string) (result int, errMsg string) { Curl := append([]byte(url), 0) var buffer [256] byte ret, _, _ := syscall.Syscall(uintptr(_YInitAPI),3,uintptr(unsafe.Pointer(&Curl[0])), uintptr(unsafe.Pointer(&buffer)), 256) result = int(ret); errMsg = string(buffer[:]) return }
YRelay_firstRelay
Nothing too complex.
func YRelay_firstRelay() (result int64) { ret, _, _ := syscall.Syscall(uintptr(_YRelay_firstRelay),0,0,0,0) result = int64(ret) return }
YRelay_set_state
Easy as well, once you understand the principle.
func YRelay_set_state(relay int64, state int) (result int) { ret, _, _ := syscall.Syscall(uintptr(_YRelay_set_state),2,uintptr(relay), uintptr(state), 0) result = int(ret) return }
Test code
The test code looks like this
func main() { defer syscall.FreeLibrary(myDLL) fmt.Println("start\n") res,errmsg := YInitAPI("usb") if res!=YAPI_SUCCESS { panic(errmsg )} r := YRelay_firstRelay(); if (r==0) { panic("No relay found, check usb cable" )} fmt.Println("using " + YRelay_get_hardwareId(r)); fmt.Println("Switching to B"); YRelay_set_state(r, YRELAY_STATE_B) time.Sleep(2 * time.Second) fmt.Println("Switching to A"); YRelay_set_state(r, YRELAY_STATE_A) YFreeAPI() fmt.Println("done\n") }
The full code
Here is the full GO code.
package main import ("syscall" "unsafe" "fmt" "time" ) var ( myDLL, _ = syscall.LoadLibrary("demo.dll") _YInitAPI, _ = syscall.GetProcAddress(myDLL, "YInitAPI") _YFreeAPI, _ = syscall.GetProcAddress(myDLL, "YFreeAPI") _YRelay_findRelay, _ = syscall.GetProcAddress(myDLL, "YRelay_findRelay") _YRelay_set_state, _ = syscall.GetProcAddress(myDLL, "YRelay_set_state") _YRelay_get_hardwareId , _ = syscall.GetProcAddress(myDLL, "YRelay_get_hardwareId") _YRelay_firstRelay, _ = syscall.GetProcAddress(myDLL, "YRelay_firstRelay") _YRelay_nextRelay, _ = syscall.GetProcAddress(myDLL, "YRelay_nextRelay") ) const (YAPI_SUCCESS =0 YRELAY_STATE_A = 0 YRELAY_STATE_B = 1 YRELAY_STATE_INVALID = -1 ) func YInitAPI( url string) (result int, errMsg string) { Curl := append([]byte(url), 0) var buffer [256] byte ret, _, _ := syscall.Syscall(uintptr(_YInitAPI),3,uintptr(unsafe.Pointer(&Curl[0])), uintptr(unsafe.Pointer(&buffer)), 256) result = int(ret); errMsg = string(buffer[:]) return } func YFreeAPI() () { syscall.Syscall(uintptr(_YFreeAPI),0,0,0,0) } func YRelay_firstRelay() (result int64) { ret, _, _ := syscall.Syscall(uintptr(_YRelay_firstRelay),0,0,0,0) result = int64(ret) return } func YRelay_nextRelay(relay int64) (result int64) { ret, _, _ := syscall.Syscall(uintptr(_YRelay_nextRelay),1,uintptr(relay),0,0) result = int64(ret) return } func YRelay_findRelay(name string) (result int64) { Cname := append([]byte(name), 0) ret, _, _ := syscall.Syscall(uintptr(_YRelay_findRelay), 1, uintptr(unsafe.Pointer(&Cname[0])) ,0, 0) result = int64(ret) return } func YRelay_set_state(relay int64, state int) (result int) { ret, _, _ := syscall.Syscall(uintptr(_YRelay_set_state), 2, uintptr(relay), uintptr(state), 0) result = int(ret) return } func YRelay_get_hardwareId(relay int64) (result string) { var buffer [32] byte syscall.Syscall(uintptr(_YRelay_get_hardwareId), 3, uintptr(relay), uintptr(unsafe.Pointer(&buffer)), 32) result = string(buffer[:]) return } func main() { defer syscall.FreeLibrary(myDLL) fmt.Println("start\n") res,errmsg := YInitAPI("usb") if res!=YAPI_SUCCESS { panic(errmsg )} r := YRelay_firstRelay(); if (r==0) { panic("No relay found, check usb cable" )} fmt.Println("using " + YRelay_get_hardwareId(r)); fmt.Println("Switching to B"); YRelay_set_state(r, YRELAY_STATE_B) time.Sleep(2 * time.Second) fmt.Println("Switching to A"); YRelay_set_state(r, YRELAY_STATE_A) YFreeAPI() fmt.Println("done\n") }
Conclusion
Actually, if you get a closer look, you don't need much code to create a DLL which enables you to interface a Yoctopuce function into a non-supported language. Once you have understood the principle, it goes quite fast because all the calls are based on the same principle.
Finally, a few comments:
- In order to keep this example as concise as possible, error management was reduced to a minimum. For example, it would be wiseto check in the DLL that the pointers passed as parameters are valid.
- If you need to interface sensors, rather than trying to carry around floating point values, multiply them by 1'000, pass them to the other side as int64, and divide by 1'000 on arrival. You'll avoid many representation issues and won't change anything to the accuracy: internally, Yoctopuce sensors work in fixed point with three decimals.
- If the GO part of this example seems a bit shaky, don't be too surprised. At Yoctopuce nobody masters this language <:o)