Yoctopuce and Golang

Yoctopuce and Golang

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
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:

YFreeAPI

It's the easiest function: it doesn't take any parameter

extern "C"  __declspec(dllexport) int YFreeAPI()
{
  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.

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;
 }



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.

extern "C" __declspec(dllexport) s64 YRelay_firstRelay()
{
  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).

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;
}



The rest

The other calls work according to the same principle, here is the complete code of the DLL:

#include "yocto_api.h"
#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)

Add a comment No comment yet Back to blog












Yoctopuce, get your stuff connected.