Yoctopuce et Golang

Yoctopuce et Golang

Il existe une foultitude de langages de programmation différents, et Yoctopuce ne peut évidement pas proposer des librairies pour chacun d'entre eux. Un de nos clients s'est retrouvé avec un module Yoctopuce mais pas de librairie officielle pour l'utiliser, et il a fait un truc auquel on ne s'attendait pas: il a transformé l'exemple de programmation C++ en DLL et il a codé des appels dans cette DLL depuis son langage de programmation préféré, c'est plutôt malin. Cette semaine, on se propose de vous expliquer cette méthode pour interfacer un relais Yoctopuce depuis le langage de programmation GO.

Le principe

Partir d'un exemple de programmation est une super idée parce que ça évite de partir de zéro: les exemples de programmation C++ ont déjà la bonne structure pour créer une DLL, et contrairement à la DLL yapi.dll, cela permet de profiter de toute la puissance de l'API haut niveau de Yoctopuce. On a décidé d'utiliser cette méthode avec le langage GO, mais c'est applicable à n'importe quel langage de programmation capable de faire des appels dans une DLL classique.

Pour que ça marche dans votre langage de programmation préféré, vous devez savoir avec ce langage:

  • Charger une DLL classique
  • Faire des appels dans cette DLL
  • Passer des entiers en paramètre
  • Passer des chaînes de caractères (char*) en paramètre
  • Récupérer des chaînes de caractères (char*)

Pour les chaînes de caractères, vous pouvez tricher en passant par des tableaux de bytes.


Création de la DLL

Commençons par ouvrir un des exemples de programmation C++ avec Visual-Studio. Ici, on va utiliser celui du Yocto-PowerRelay-V2 c'est-à-dire Doc-GettingStarted-Yocto-PowerRelay. La première chose à faire consiste à changer le type de cible pour que Visual-Studio génère une DLL à la place d'un EXE. Il faut aussi bien vérifier que l'architecture de la DLL corresponde bien à l'architecture du programme principal, ici on va travailler en 64 bit (x64) parce qu'on utilise la version 64 bits de GO. Au besoin, on peut modifier le répertoire de sortie pour que la DLL atterrisse à un endroit plus pratique que bin\debug.

Configuration du projet pour créer une DLL
Configuration du projet pour créer une DLL



Une fois le projet configuré, on peut supprimer la fonction main() de l'exemple et commencer à coder le contenu de la DLL. On a besoin des fonctions suivantes:



YFreeAPI

C'est la fonction la plus facile: elle ne prend pas de paramètre

extern "C"  __declspec(dllexport) int YFreeAPI()
{
  YAPI::FreeAPI();
  return 1;
}




YRegisterHub

C'est une fonction un peu délicate parce qu'elle prend une chaîne de caractères en paramètre (url) et en retourne une autre (errMsg). Pour la chaîne à retourner, on a fait au plus simple: l'appelant fournit un pointeur sur un buffer (char* errMsg) et sa taille (int maxlen) et la fonction remplit ce buffer, en faisant bien attention à ne pas déborder.

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 fournit le premier relais disponible. Dans l'API Yoctopuce, les relais sont des objets qu'on ne peut bien évidement pas passer tels quels à GO. Par contre, on peut parfaitement passer les pointeurs sur ces objets en les castant en entiers 64 bits.

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 fait basculer le relais. Les paramètres sont donc l'entier 64bit qui référence le relais (r) et l'état désiré (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;
}



Le reste

Les autres appels fonctionnent selon le même principe, voici le code complet de la 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;
}



La partie GO

Pour la partie GO, il faut commencer par charger la DLL et trouver les points d'entrée

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") )


Il faut ensuite implémenter les appels proprement dits:

YFreeAPI

Ici aussi, c'est la fonction la plus facile:

func YFreeAPI() () { syscall.Syscall(uintptr(_YFreeAPI),0,0,0,0) }



YRegisterHub

Là, c'est un peu plus compliqué parce qu'il faut convertir les chaînes de caractères en buffer de bytes.

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

Rien de bien compliqué.

func YRelay_firstRelay() (result int64) { ret, _, _ := syscall.Syscall(uintptr(_YRelay_firstRelay),0,0,0,0) result = int64(ret) return }



YRelay_set_state

Facile aussi, une fois qu'on a compris le principe.

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 }



Code de test

La partie test ressemble à ça.

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") }



Code complet

Voici le code GO complet.

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

En fait, quand on y regarde de plus près, il n'y a pas besoin de beaucoup de code pour construire une DLL qui permette d'interfacer une fonction Yoctopuce dans un langage non supporté. Une fois qu'on a compris le principe, ça va assez vite parce que tous les appels sont basés sur le même principe.
Pour finir quelques remarques:

  • Afin de garder cet exemple le plus concis possible, la gestion des erreurs a été réduite au minimum. Il serait par exemple prudent de vérifier dans la DLL que les pointeurs passés en paramètre sont valides.
  • Si vous avez besoin d'interfacer des capteurs, plutôt que d'essayer de trimballer des valeurs en virgule flottante, multipliez les 1000, passez-les de l'autre côté sous forme d'int64 et divisez par 1000 à l'arrivée. Ça vous évitera bien des problèmes de représentation et ça ne changera rien à la précision: en interne les capteurs Yoctopuce fonctionnent en virgule fixe avec 3 décimales.
  • Si la partie GO de cet exemple vous parait un peu bancale, ne soyez pas trop surpris, chez Yoctopuce personne ne maîtrise ce langage <:o)

Commenter aucun commentaire Retour au blog












Yoctopuce, get your stuff connected.