Aperçu du langage de script Lua

Présentation de l'utilisation de Lua avec l'eGauge

  • La programmation en Lua est un sujet avancé. Le support eGauge ne peut pas examiner le code et offre une assistance limitée pour le dépannage des scripts Lua.

Introduction

La prise en charge d'eGauge Script (eScript) a été introduite dans la version 1.1 du firmware, permettant une programmation limitée via l'utilisation de registres de formules. eScript a été conçu pour calculer des formules mathématiques de base et courantes telles que les opérations arithmétiques élémentaires, les comparaisons et les conditions, et permet également d'appeler des routines (fonctions) de bibliothèque.

Le firmware v4.1 introduit la prise en charge de Lua v5.3 . Afin de garantir un fonctionnement sûr, l'exécution de Lua est protégée par des délais d'expiration pour éviter les boucles infinies ou les temps d'exécution excessivement longs. De plus, un ensemble restreint de bibliothèques standard est pris en charge.

Pour maintenir la rétrocompatibilité, le firmware traduit de manière transparente les expressions eScript en scripts Lua avant l'exécution et l'environnement d'exécution Lua fournit toutes les fonctions disponibles dans eScript.


Introduction à Lua

Conventions lexicales

Les noms peuvent contenir des lettres, des chiffres ou des traits de soulignement, mais ne peuvent pas commencer par un chiffre. Les valeurs numériques suivent les mêmes conventions que dans la plupart des autres langages, notamment la possibilité d'écrire des valeurs hexadécimales avec le préfixe 0x . Les deux valeurs littérales booléennes sont vrai et faux . Une variable sans valeur définie est `nil` . Les chaînes de caractères peuvent être placées entre guillemets doubles ou simples. Les caractères d'une chaîne peuvent être échappés avec une barre oblique inverse. Les commentaires commencent par un double tiret ( `--` ) et s'étendent jusqu'à la fin de la ligne. Les instructions peuvent se terminer par un point-virgule, mais celui-ci n'est généralement pas obligatoire (même lorsqu'il y a plusieurs instructions sur une même ligne). La seule exception est le point-virgule obligatoire si l'instruction suivante commence par une parenthèse ouvrante.

Variables

Les variables sont globales sauf si elles sont explicitement déclarées comme locales. Par exemple :

x = 10        -- global variable
local y = 42   -- local variable

Opérateurs

Lua propose un ensemble classique d'opérateurs pour les opérations arithmétiques et les comparaisons. L'opérateur d'inégalité est quelque peu inhabituel : il s'agit de `~=` au lieu du plus courant `!=` . Les opérateurs logiques utilisent des mots-clés comme en Python : `and` , `or` et `not` . La longueur d'une chaîne de caractères ou d'un tableau (voir la section « Données structurées » ci-dessous) s'obtient en faisant précéder le nom de la chaîne ou du tableau d'un dièse (`#`). Par exemple, `#'hi'` renverra 2. Les chaînes de caractères peuvent être concaténées avec l'opérateur point (`..`). Par exemple , `'yell'..'ow'` donnera `'yellow'` .

Structures de contrôle

La syntaxe des différentes structures de contrôle est la suivante :

for index=initial,step,final do block end -- numeric for loop

for var in iterator do block end   -- iterator for loop

while cond do block end    -- while loop

repeat block until cond    -- do while loop

if cond then block else block end  -- conditional statement

Vous pouvez utiliser break pour sortir d'une boucle, mais contrairement au C et à d'autres langages, il n'existe pas de fonction continue permettant de passer à l'itération suivante.

Données structurées

Lua utilise des tables pour représenter à la fois les tableaux et les tables de hachage (dictionnaires Python, objets Javascript). Par exemple :

local a = {'a', 'b', 'c'}

assigne à la variable locale a un tableau contenant les chaînes de caractères 'a' , 'b' et 'c' . Contrairement à la plupart des autres langages, l'indice de base du tableau est 1, donc a[1] renverrait le premier élément, soit 'a' dans notre exemple.

Les valeurs littérales d'un dictionnaire peuvent s'écrire ainsi :

local d = {['k1']='v1', ['k2']='v2'}

et indexés avec des crochets, de sorte que d['k2'] renvoie 'v2' , par exemple. Si les clés d'un dictionnaire sont des noms Lua valides, les crochets et les guillemets autour des chaînes de caractères des clés peuvent être omis. Par exemple, l'exemple ci-dessus pourrait être simplifié comme suit :

local d = {k1='v1', k2='v2'}

Pour ces valeurs clés, il est également possible d'accéder à leurs valeurs en utilisant la syntaxe nom-membre. Par exemple, d.k1 renverra 'v1' , tout comme d['k1'] .

Migration d'eScript vers Lua

La plupart des fonctionnalités d'eScript ont un équivalent direct en Lua. eScript prend entièrement en charge un seul type numérique (flottant double précision IEEE 754) et offre une prise en charge limitée des chaînes de caractères. Tel que configuré pour le firmware eGauge, Lua prend en charge le même type numérique, mais aussi les entiers 32 bits, ainsi que les chaînes de caractères, les valeurs booléennes et les tableaux.

Les principales différences entre eScript et Lua sont les suivantes :

  • En eScript, la valeur d'un registre est obtenue avec $"register_name" , tandis qu'en Lua, l'expression équivalente est __r("register_name") .
  • Les valeurs booléennes Lua ne peuvent pas être utilisées directement comme valeurs numériques, tandis qu'eScript utilise 0 pour représenter faux et toute valeur non nulle pour vrai.
  • Lua ne fournit pas d'équivalent direct à l'opérateur conditionnel

cond ? if_true_expr : if_false_expr

Lua utilise plutôt l'expression logique suivante :

cond and if_true_expr or if_false_expr

Cela fonctionne de manière très similaire à la conditionnelle eScript en raison de la définition des opérateurs `and` et `or`. Plus précisément, `and` renvoie `false` si le membre de gauche est `nil` ou `false` , sinon la valeur du membre de droite. L'opérateur `or` renvoie la valeur du membre de gauche s'il n'est ni `nil` ni ` false` , sinon la valeur du membre de droite. Ces deux opérateurs interrompent l'évaluation, et l'opérateur ` and` est prioritaire sur `or` .


eScript propage automatiquement les valeurs NaN (Not-a-Number). Par exemple, si une fonction est appelée avec une valeur NaN, le résultat renvoyé est également NaN. De même, si la condition d'un opérateur conditionnel est NaN, le résultat de l'expression conditionnelle est également NaN.

Compte tenu des similarités entre eScript et Lua, la plupart des expressions eScript se traduisent aisément en Lua. Les traductions plus complexes sont présentées dans le tableau ci-dessous :

Expression eScript Équivalent Lua
a < b __lt(a, b)
a <= b __le(a, b)
a > b __gt(a,b)
a >= b __ge(a,b)
a = b __eq(a,b)
taxi

(fonction(__c)

si __c ~= __c alors retourner __c fin

renvoyer __c~=0 et (a) ou (b)

fin)(c)

En d'autres termes, les opérateurs de comparaison sont traduits en appels aux fonctions auxiliaires `__lt()` , `__le()` , etc. Ces fonctions vérifient si ` a` ou ` b` est `NaN` et renvoient `NaN` le cas échéant. Sinon, elles effectuent la comparaison et renvoient 0 si la valeur est fausse et 1 si elle est vraie. La traduction de l'opérateur conditionnel est plus complexe car il faut veiller à gérer correctement les valeurs `NaN` et à évaluer `a` et `b` uniquement lorsque cela est nécessaire. En Lua, cela est réalisé grâce à une fonction anonyme en ligne qui vérifie si la condition est `NaN` et renvoie `NaN` si c'est le cas. Sinon, la fonction vérifie si la condition a une valeur non nulle et, si c'est le cas, renvoie la valeur de `a` . Sinon, elle renvoie la valeur de `b` .

Environnement Lua fourni par le firmware eGauge

Environnement standard

Pour des raisons de sécurité, le firmware eGauge fournit un environnement Lua restreint (le bac à sable Lua). Les fonctions et variables de base sont limitées au sous-ensemble suivant (voir le manuel Lua 5.3 pour une description détaillée) :

_VERSION, assert, error, getmetatable, ipairs, load, next, pairs, pcall, print, rawequal, rawget, rawlen, rawset, select, setmetatable, tonumber, tostring, type, xpcall

Les bibliothèques Lua standard suivantes sont disponibles :

chaîne

mathématiques

tableau

Ajouts fournis par le micrologiciel eGauge

Fonctions de base et d'alerte

Toutes les fonctions disponibles pour eScript le sont également pour les scripts Lua. Consultez la documentation en ligne d'un compteur eGauge pour obtenir la liste complète ( Aide → Fonctions de base ou Aide → Fonctions d'alerte ). Lorsqu'elles sont appelées depuis Lua, ces fonctions propagent également les valeurs NaN, comme pour eScript. Autrement dit, si l'une d'elles est appelée avec un argument NaN, la valeur de retour sera également NaN.

Module JSON

Ce module permet d'encoder un objet Lua en une chaîne de caractères et de reconvertir en toute sécurité une chaîne de caractères en objet.

string = json.encode ( value ):

Cette fonction accepte une valeur Lua et la sérialise en une chaîne JSON correspondante, qu'elle renvoie. Les tables contenant des références cycliques ne peuvent pas être encodées en JSON et généreront une erreur. La taille maximale de la chaîne JSON est actuellement limitée à 4 095 octets. Les tables Lua peuvent être indexées par une combinaison de valeurs numériques, booléennes et de chaînes de caractères, tandis que les objets JSON sont toujours indexés par des chaînes de caractères. Cette fonction convertit les tables Lua non vides et dont les indices sont exclusivement composés de nombres compris entre 1 et N (où N est la longueur de la table) en tableaux JSON, et toutes les autres en objets JSON. Dans ce dernier cas, les indices numériques et booléens sont convertis en leurs chaînes de caractères équivalentes.

value = json.decode ( string ):

Cette fonction accepte une chaîne de caractères et la désérialise en l'objet Lua correspondant. Seuls les guillemets doubles sont autorisés. Les espaces et les tabulations sont ignorés.


Module persistant

Ce module fournit des variables dont les valeurs persistent malgré les coupures de courant et les redémarrages des appareils.

obj = persistent:new( name , initial , description ):

Déclare une variable persistante avec le nom spécifié. Contrairement aux noms Lua, ce nom peut être une chaîne de caractères quelconque. Ce nom doit être unique, car tout script Lua déclarant une variable persistante du même nom accédera au même objet sous-jacent. Si c'est la première fois que la variable persistante est déclarée, sa valeur est initialisée . Le rôle de la variable doit être décrit par la chaîne de caractères passée à l' argument `description` . La valeur de retour est un objet représentant la variable persistante.

obj :get()

Renvoie la valeur actuelle de la variable persistante représentée par l'objet obj .

obj :set( value )

Définit la valeur de la variable persistante représentée par l'objet obj sur la valeur spécifiée . Toute valeur acceptable par json.encode() peut être définie.

Environnement Lua pour les scripts de contrôle

Les scripts de contrôle ont accès à l'environnement standard décrit dans la section précédente. Ils ont également accès à toutes les fonctions de base et d'alerte, ainsi qu'à la bibliothèque de coroutines . Plusieurs fonctions de bas niveau et modules pratiques sont également disponibles, comme décrit ci-dessous.

Fonctions de bas niveau

La plupart de ces fonctions ne sont généralement pas utilisées directement. Elles fournissent les mécanismes de bas niveau nécessaires à la mise en œuvre des abstractions de plus haut niveau proposées par les modules décrits dans les sections ci-dessous.

tid , err = __ctrl_submit( attrs , method , args… )

Soumettez un appel à la méthode nommée ` method` sur le périphérique identifié par `attrs` , en lui passant les arguments `args` . Le nom de la méthode doit être une chaîne de caractères, `attrs` un tableau de paires nom/valeur, et `args` une séquence de zéro ou plusieurs valeurs d'arguments Lua compatibles avec les types d'arguments attendus par la méthode nommée. Le nom de la méthode peut être un nom de méthode pleinement qualifié, composé d'un nom d'interface, immédiatement suivi d'un point (.) , puis du nom de la méthode lui-même, ou un nom de méthode seul. Dans ce dernier cas, la méthode est appelée via la première interface enregistrée pour le périphérique qui implémente la méthode nommée. Sinon, la méthode est appelée via l'interface nommée.

La fonction `__ctrl_submit` renvoie deux valeurs : un identifiant de transaction `tid` et une chaîne d'erreur `err` . En cas de succès, `tid` est un nombre positif ou nul qui identifie de manière unique l'objet d'appel nouvellement créé, et `err` est nul . En cas d'erreur, `tid` est un code d'erreur négatif (voir `ctrl.Error` ci-dessous) et `err` est une chaîne facultative pouvant expliquer l'échec de l'appel. Si elle est fournie, la chaîne d'erreur est généralement traduite selon les paramètres régionaux définis sur le compteur.

status , result = __ctrl_result( tid ):

Obtenez le résultat de l'appel de méthode identifié par l'identifiant de transaction tid . Le tid doit être non négatif et doit avoir été renvoyé par un appel précédent à __ctrl_submit .

La fonction `__ctrl_result` renvoie deux valeurs : un entier `status` et `result` . En cas de succès, `status` vaut zéro ; dans ce cas, ` result` correspond au résultat renvoyé par l’appel de méthode, converti en une valeur Lua. En cas d’erreur, `status` est un code d’erreur négatif (voir `ctrl.Error` ci-dessous) et `result` est `nil` . Plus précisément, si un appel de méthode est toujours en cours, le code d’erreur `ctrl.Error.AGAIN` (-8) est renvoyé. Dans ce cas, l’appelant doit patienter quelques instants, puis réessayer l’appel à `__ctrl_result` jusqu’à ce qu’il réussisse.

status , err = __ctrl_cancel( tid ):

Tentez d'annuler l'appel de méthode identifié par l'identifiant de transaction tid . Cet identifiant tid doit être non négatif et doit avoir été renvoyé par un appel précédent à __ctrl_submit .

La fonction `__ctrl_cancel` renvoie deux valeurs : un entier `status` et une chaîne d'erreur optionnelle `err` . En cas de succès, ` status` vaut zéro ; dans ce cas, `tid` est forcément un identifiant de transaction invalide jusqu'à sa réutilisation et son renvoi par un autre appel à `__ctrl_submit` . En cas d'erreur, `status` est un code d'erreur négatif (voir `ctrl.Error` ci-dessous) et `err` est une chaîne optionnelle pouvant expliquer l'échec de l'appel. Si elle est fournie, la chaîne d'erreur est généralement traduite selon les paramètres régionaux du compteur.

ret , err = __ctrl_get_devices( attrs ):

Obtenez la liste des périphériques correspondant aux attributs optionnels spécifiés par le tableau attrs . Si attrs est omis ou nul , la liste de tous les périphériques connus (enregistrés) est renvoyée.

La fonction `__ctrl_get_devices` renvoie deux valeurs : une table ( `ret`) et une chaîne d'erreur optionnelle (`err`) . En cas de succès, `ret` est une liste de tables et `err` est nul . Chaque table de la liste renvoyée correspond à un périphérique, sans ordre particulier. La table contient les paires nom/valeur enregistrées pour ce périphérique.

En cas d'erreur, ret est nul et err peut être une chaîne non nulle expliquant ce qui s'est mal passé, généralement traduite dans les paramètres régionaux définis sur le compteur.

ret = __ctrl_get_interface( name ):

Obtenez une interface spécifique ou la liste de toutes les interfaces connues. L'argument `name` doit être une chaîne de caractères désignant l'interface souhaitée ; il peut être omis ou renvoyé à ` nil` pour obtenir la liste de toutes les interfaces.

La fonction `__ctrl_get_interface` renvoie une seule valeur, `ret` . En cas d'erreur ou si l'interface demandée est introuvable, `nil` est renvoyé. Sinon, `ret` représente une interface unique (si un nom a été spécifié) ou une liste d'interfaces (si le nom a été omis ou si `nil` est renvoyé). Chaque interface est décrite par un tableau contenant les éléments suivants :

  • nom : Le nom de l'interface sous forme de chaîne de caractères.
  • méthodes : Liste des méthodes implémentées par l'interface.

Chaque méthode est décrite par un tableau comportant les éléments suivants :

  • nom : Le nom de la méthode sous forme de chaîne de caractères.
  • arg_types : La signature de type DBus des arguments sous forme de chaîne de caractères.
  • ret_type : La signature de type DBus de la valeur de retour sous forme de chaîne de caractères.
  • doc : Description sous forme de chaîne de caractères du fonctionnement de la méthode. Cette chaîne peut contenir des références aux arguments placés entre les balises <arg>/</arg> . Par exemple, la chaîne « <arg>foo</arg> » fait référence à l’argument de méthode nommé « foo » .
  • arg_names : Chaîne de caractères contenant les noms des arguments, séparés par des virgules et classés par ordre d’apparition. Cette chaîne sert uniquement à des fins de documentation, car, en dehors de ce contexte, les noms des arguments n’ont aucune signification. Par exemple, la chaîne « foo,bar » indique que la méthode attend deux arguments, désignés dans la documentation comme foo et bar , respectivement.



ret = __sleep( time ):

Suspend l'exécution de l'appel pendant `time` secondes. Cette durée peut être fractionnaire. Si le programme Lua appelant `__sleep` contient d'autres coroutines exécutables, celles-ci sont exécutées. S'il ne reste plus aucune coroutine exécutable, l'exécution du programme est suspendue pendant la durée minimale requise jusqu'à ce que la première coroutine redevienne exécutable.

La fonction renvoie un entier ret qui vaut zéro en cas de succès ou négatif en cas d'erreur.

ret = sleep( time ):

Il s'agit d'un alias pour `__sleep` , utilisable par commodité dans les programmes Lua. Les bibliothèques doivent toujours appeler `__sleep` afin de garantir l'exécution de la fonction prévue, même si un programme Lua redéfinit le nom ` sleep` .

Contrôle du module

Ce module offre une interface de plus haut niveau pour appeler les méthodes de contrôle. Il est généralement préférable d'utiliser ce module plutôt que les fonctions de bas niveau décrites dans la section précédente.

dev = ctrl:dev( attrs , obj ):

Crée un objet de périphérique de contrôle avec les attributs `attrs`. L'argument optionnel `obj` permet d'implémenter une classe de périphérique de contrôle étendue, mais il est généralement omis (ou vous pouvez lui passer `nil` ) pour créer un nouvel objet. Cette opération ne communique pas avec le périphérique distant identifié par `attrs` et retourne donc immédiatement.

iface = dev:interface( name ):

Créez un objet d'interface pour l'interface identifiée par le nom « device dev » . L'objet d'interface retourné contiendra une méthode proxy pour chaque méthode définie par l'interface nommée. Les méthodes proxy peuvent être appelées comme n'importe quelle autre méthode et transmettent automatiquement l'appel au périphérique de contrôle, puis attendent la disponibilité du résultat. Par conséquent, ces opérations peuvent être longues et appelleront la fonction `__sleep()` si nécessaire. De ce fait, d'autres coroutines peuvent être exécutées pendant l'appel d'une méthode proxy.


status , result = dev:call( method , args… )

Appelez la méthode nommée `method` sur le périphérique `dev` , en lui passant les arguments `args` , et retournez le résultat. Le nom de la méthode doit être une chaîne de caractères et `args` doit être une séquence de zéro ou plusieurs valeurs d'arguments Lua compatibles avec les types d'arguments attendus par la méthode nommée. Le nom de la méthode peut être un nom de méthode pleinement qualifié, composé d'un nom d'interface, immédiatement suivi d'un point (`.`) , puis du nom de la méthode lui-même, ou un nom de méthode seul. Dans ce dernier cas, la méthode est appelée via la première interface enregistrée pour le périphérique qui implémente la méthode nommée. Sinon, la méthode est appelée via l'interface nommée.

Deux valeurs sont renvoyées : un entier `status` et `result` . En cas de succès, `status` vaut zéro ; dans ce cas, `result` correspond au résultat renvoyé par la méthode nommée, converti en une valeur Lua. En cas d’erreur, `status` est un code d’erreur négatif (voir `ctrl.Error` ci-dessous) et `result` vaut `nil` .

Error:

Ce tableau répertorie les noms symboliques de diverses erreurs de contrôle, à savoir :

UNSPEC (-1) : Une erreur non spécifiée s'est produite.

INVAL (-2) : Un argument invalide ou incompatible a été passé à une méthode.

NODEV (-3) : Les attributs du périphérique ont spécifié un chemin de périphérique non valide.

ATTRS (-4) : Les attributs ne correspondent pas au périphérique sélectionné.

NOMETHOD (-5) : Le nom de la méthode est introuvable.

NOENT (-6) : L'identifiant de transaction spécifié est introuvable.

OCCUPÉ (-7) : L'appareil est occupé (trop d'appels en attente).

À NOUVEAU (-8) : L'appel est toujours en attente.

Tâches du module

Ce module offre une interface pratique pour créer plusieurs tâches pouvant être exécutées de manière quasi simultanée, puis pour les exécuter jusqu'à ce qu'elles soient toutes terminées.

tasks:add( fun , args… ):

Ajoutez une tâche qui, lors de son exécution, lance la fonction `fun` avec les arguments `args` jusqu'à ce que celle-ci se termine. Cette fonction peut appeler `__sleep` ou `coroutine.yield` pour suspendre temporairement son exécution et permettre à d'autres tâches de s'exécuter.

tasks:run():

Exécutez les tâches précédemment ajoutées jusqu'à ce qu'elles soient toutes terminées.