Présentation du modèle de pilote simple (SDM) NodeMCU: interface utilisateur dynamique

image


NodeMCU est un firmware interactif , qui permet d'exécuter l'interpréteur Lua sur le microcontrôleur ESP8266 (le support ESP32 est en cours de développement). En plus de toutes les interfaces matérielles habituelles, il dispose d'un module WiFi et d'un système de fichiers SPIFFS .


Cet article décrit le nouveau module pour le NodeMCU - sdm. SDM signifie modèle de pilote simple et fournit une abstraction de modèle de pilote de périphérique pour le système. Dans la première partie de cet article, nous discuterons du modèle lui-même et dans la deuxième partie, nous présenterons une interface utilisateur Web créée dynamiquement à l'aide de sdm avec quelques commentaires.


Principes de base du modèle de pilote


Les deux principaux composants du modèle sont les périphériques et les pilotes . Le périphérique est une représentation abstraite d'un matériel ou d'un périphérique virtuel. Il est logique de placer les appareils dans une hiérarchie arborescente, avec le microcontrôleur en haut, les bus au milieu et les capteurs en feuilles.


DEVICES + DRIVERS | +-----+ | +-----+ |1WIRE<----------------------+1WIRE| ++-+-++ | +-----+ | | | | +---------+ | +--------+ | +------+ | | | +------+DS1820| +---v----+ +---v----+ +---v----+ | | +------+ |DS1820|0| |DS1820|1| |DS1822|0| | | +---^----+ +---^----+ +---^----+ | | +------+ | | +--------------+DS1822| | | | | +------+ +-----------+------------------+ + 

Le pilote de périphérique est un élément logique associé au périphérique donné. Les fonctions fournies par le pilote sont appelées méthodes , les conteneurs de données associés au pilote sont appelés attributs . Les méthodes et les attributs vivent dans le pilote.


Les attributs ont deux fonctions qui leur sont associées: les crochets getter et setter . Attribue donc des fonctionnalités de méthode de sur-ensemble, mais elles prennent également plus de mémoire (la mémoire du microcontrôleur est rare, vous vous souvenez?).


 sdm.attr_add(drv, -- device handle "ref", -- attribute name "Reference voltage", -- attribute description 5, function(dev) -- this is a getter function return sdm.attr_data(sdm.attr_handle(dev, "ref")) end, function(dev, value) -- this is a setter function sdm.attr_set(sdm.attr_handle(dev, "ref"), value) end ) 

Liaison de périphérique


La partie délicate du modèle de pilote est la liaison de pilote de périphérique. Le processus lui-même est assez simple: nous adaptons le périphérique à chaque pilote disponible jusqu'à ce qu'il corresponde. Il ne manque que deux parties: la logique de correspondance et certaines données auxquelles correspondre.


Dans sdm, la logique de correspondance réside dans les pilotes sous le nom _poll() . Il s'agit d'une méthode régulière qui est appelée avec le descripteur de périphérique en tant que paramètre et renvoie true ou false si le périphérique peut ou ne peut pas être attaché au pilote respectivement.


 sdm.method_add(drv, "_poll", nil, function(dev, drv, par) local attr = sdm.attr_data(sdm.local_attr_handle(dev, "id")) -- get device attribute "id" if attr == nil then return false end -- if it does not have one, driver does not match -- parent name must be "ESP8266_1W" and first byte of "id" must be "0x28" return (sdm.device_name(par) == "ESP8266_1W") and (attr:byte(1) == 0x28) end ) 

Comme vu dans l'exemple ci-dessus, le pilote fait correspondre le périphérique à l'aide de l'attribut. Mais comme indiqué ci-dessus, les attributs ne s'associent qu'au pilote. En général, c'est vrai, mais certains attributs ne peuvent pas être récupérés via un logiciel. Ce sont des identifiants de puce, des broches utilisées, etc. Pour ceux-ci, un type d'attribut spécial a été ajouté à l'attribut sdm- local . Cet attribut est associé à une instance de l'appareil et généralement immuable.


La seule chose qui reste à dire sur la liaison du pilote. Habituellement, les appareils nécessitent une sorte d'initialisation au démarrage et au nettoyage après utilisation. À cette fin, sdm utilise les _init() et _free() .
Si le pilote a la méthode _init() , il sera appelé automatiquement après la liaison du périphérique. Même chose avec _free() .


 sdm.method_add(drv, "_init", nil, function(dev, drv, par) sdm.device_rename(dev, sdm.request_name("DS18B20")) -- rename device sdm.attr_copy(dev, "temp") -- copy attribute sdm.attr_copy(dev, "precision") -- copy attribute local met = sdm.method_dev_handle(par, "setup") -- get 1Wire bus pin init function .. local func = sdm.method_func(met) -- .. and .. func(par, dev) -- .. call it end ) sdm.method_add(drv, "_free", nil, function(dev, drv, par) local met = sdm.method_dev_handle(par, "free") -- get 1Wire bus pin free function .. local func = sdm.method_func(met) -- .. and .. func(par, dev) -- .. call it end ) 

Un lecteur attentif demanderait probablement: que signifie «copier l'attribut» dans l'exemple ci-dessus? Et il aurait raison, car cela a à voir avec le troisième type d'attribut dont nous n'avons pas encore discuté - l'attribut privé . Il n'est pas très logique de partager toutes les données d'attribut entre toutes les instances de périphérique. À cet effet, sdm fournit un mécanisme de copie d'attribut du pilote et de l'associer à l'appareil. Cela fait que le pilote attribue un prototype ou un modèle.


Un résumé rapide:


  • les attributs locaux sont utilisés pour les données qui ne peuvent pas être récupérées par le logiciel. Comme les ID d'appareil, les broches connectées, etc.
  • les attributs de pilote sont utilisés pour les données partagées entre toutes les instances de périphériques connectés à ce pilote.
  • les attributs privés sont copiés à partir des attributs du pilote et contiennent des données associées à une seule instance de périphérique. Ce type est le plus courant.

BiensAttribut localAttribut privéAttribut de pilote (public)
Stocké dansappareilappareilchauffeur
Accessible à l'aide de la poignée du conducteur--+
Accessible à l'aide de la poignée de l'appareil+++
Partagé entre les appareils--+
Persister à détacher le conducteur+-+

Implémentation de l'interface utilisateur Web


Code serveur


Il y a un joli projet nodemcu-httpserver qui implémente le code serveur pour NudeMCU. Malheureusement, il semble être mort. Il a été utilisé comme base pour le serveur. Tout d'abord, les fonctions du serveur ont été déplacées vers LFS puis légèrement modifiées pour servir une page statique pour chaque appel. Vue.js est un choix parfait pour les pages Web basées sur des modèles. Il a donc été utilisé pour le frontend . Il convient de noter que NodeMCU n'est peut-être pas connecté à Internet. Pour cette raison, la bibliothèque vue.js doit être présente localement et servie par le serveur NodeMCU.


Étant donné que tous les périphériques sont organisés en arborescence, ils sont accessibles comme un répertoire: /ESP8266/ESP8266_1W/DS18S20-0 . Ici /ESP8266 est une page NodeMCU, /ESP8266/ESP8266_1W est une page de bus 1Wire et enfin /ESP8266/ESP8266_1W/DS18S20-0 est un capteur de température.


Comme mentionné précédemment, toutes les pages de l'appareil sont construites à partir d'une page de modèle qui est servie à chaque appel. Le code JS à l' intérieur de cette page fait ensuite la demande à la même URL, précédée de /api . Pour l'exemple ci-dessus, l'URL d'appel serait /api/ESP8266/ESP8266_1W/DS18S20-0 . Sur ces demandes, le serveur répond avec des données spécifiques au périphérique encodées JSON, qui remplissent la page. Bien sûr, la demande de page HTML peut être ignorée si seules des données brutes sont nécessaires.


Arborescence des appareils


La configuration initiale du périphérique est effectuée à l'aide d' une structure arborescente de périphérique simple . C'est comme l' arborescence des appareils , mais plus simple. Il décrit la configuration du matériel, y compris les attributs locaux du périphérique.


 local root={ -- local_attributes={}, children={ { name="ESP8266_1W", -- local_attributes={}, children = { { name="DS18S20-0", -- static declaration alternative to 1Wire poll method local_attributes={ { name="id", desc=nil, -- empty description to save space data=string.char(16) .. string.char(221) .. string.char(109) .. string.char(104) .. string.char(3) .. string.char(8) .. string.char(0) .. string.char(150) -- ugly way to create byte array }, { datapin=2 } } }, } }, { name="ESP8266_SPI", -- local_attributes={}, children = { { name="MCP3208-0" }, } }, } } 

Configuration matérielle


Ici commence la vitrine. À cet effet, un tas de capteurs ont été connectés au NodeMCU:



1Les capteurs à fil sont connectés à la même broche.



Pages Web et pilotes


périphérique racine


Le principal objectif du périphérique racine (alias ESP8266) est de fournir un espace de connexion à ses enfants. Cependant, il n'est pas limité d'avoir des méthodes ou des attributs qui lui sont associés.


Cet extrait de code est d' ici :


 sdm.method_add(drv, "_init", nil, function(dev, drv, par) local attr = sdm.attr_handle(dev, "id") -- get device "id" attribute sdm.attr_set(attr, node.chipid()) -- set "id" value attr = sdm.attr_handle(dev, "float") -- get device "float" attribute sdm.attr_set(attr, 3 / 2 ~= 1) -- set to true if firmware supports floating point instructions end ) sdm.attr_add(drv, "float", "Floating point build", false, function(drv) -- attribute value is set inside "_init" function local attr = sdm.attr_drv_handle(drv, "float") return sdm.attr_data(attr) -- just return stored value end, nil ) 

Ce code ajoute un attribut float qui est utilisé pour contenir le type de construction du firmware. Sa valeur est initialisée dans le _init() qui est une fonction spéciale, qui s'exécute une fois lorsque le pilote se connecte au périphérique.


Il s'agit de la page générée pour le périphérique racine.



Ici, nous pouvons voir que le périphérique racine a un heap méthode, deux attributs de pilote float et id . Enfin, il est connecté à deux appareils : les bus SPI et 1Wire .


SPI


Le pilote SPI n'est pas très intéressant. Il mappe simplement les fonctions NodeMCU SPI .



Mcp3208


Le MCP3208 est une puce ADC . Il mesure les tensions de zéro à ref et renvoie du code 12 bits. Ce qui est intéressant dans cette implémentation de pilote, c'est que l'attribut ref ne serait présent que si le micrologiciel prend en charge l'arithmétique à virgule flottante. S'il n'est pas pris en charge, au lieu de la tension absolue, le code de tension est renvoyé par differential méthodes single et differential .


 sdm.method_add(drv, "single", "Single ended measure 0|1|2|3|4|5|6|7", function(dev, channel) -- ... if ref ~= nil then -- this part is executed only if floating point arithmetic is enabled rv = ref * rv / 4096 end return rv end ) if 3/2~=1 then -- other alternative is to access ESP8266 "float" method sdm.attr_add(drv, "ref", "Reference voltage", 5, function(dev) return sdm.attr_data(sdm.attr_handle(dev, "ref")) end, function(dev, value) sdm.attr_set(sdm.attr_handle(dev, "ref"), value) end ) end 


Notez également que cet appareil a l'attribut ref marqué comme privé . Il est défini par périphérique.


1 fil


1 Le pilote pilote implémente la méthode d' poll - recherche dynamique de périphériques .


Juste après la découverte de l'appareil, son type n'est pas connu. Ainsi, son adresse unique 1Wire est utilisée comme nouveau nom de périphérique (octets représentés par des nombres séparés par un caractère _ ).


 sdm.method_add(drv, "poll", "Poll for devices", function(bus, pin) local children = sdm.device_children(bus) or {} -- already attached local ids = {} -- get IDs of attached devices for name, handle in pairs(children) do local dpin = sdm.attr_data(sdm.local_attr_handle(handle, "pin")) if dpin == pin then ids[sdm.attr_data(sdm.local_attr_handle(handle, "id"))] = true end end ow.reset_search(pin) -- reset previous search while true do -- for all found devices local id = ow.search(pin) if id == nil then break end if ids[id] == nil then -- if not already present local name = "" for i=1,#id do name = name .. tostring(id:byte(i)) .. "_" end name = name:sub(1,-2) -- add to system with their ID used as name local device = sdm.device_add(name, bus) -- add "pin" attribute local rv = sdm.local_attr_add(device, "datapin", nil, pin, nil, nil) -- add "id" attribute local rv = sdm.local_attr_add(device, "id", nil, id, nil, nil) -- poll for driver local rv = sdm.device_poll(device) end end end ) 

Ceci est la page initiale du pilote 1Wire .



Après avoir émis un poll avec l'argument 2 et une page rafraîchissante, la section enfants apparaît. Notez que les noms d'enfants sont lisibles par l'homme. En effet, la fonction device_rename() été appelée lors de leur _init .



DS18S20


Lors de l'initialisation, le pilote DS18S20 vérifie que l' ID de périphérique commence par 0x10 , qui est un code de famille de périphériques. Lorsque le périphérique est connecté au pilote, il est renommé DS18S20-X , où DS18S20 est un nom de base et X est un numéro d'instance.


 sdm.method_add(drv, "_poll", nil, function(dev, drv, par) local attr = sdm.attr_data(sdm.local_attr_handle(dev, "id")) if attr == nil then return false end return (sdm.device_name(par) == "ESP8266_1W") and (attr:byte(1) == 0x10) -- check family ID end ) sdm.method_add(drv, "_init", nil, function(dev, drv, par) sdm.device_rename(dev, sdm.request_name("DS18S20")) -- rename device sdm.attr_copy(dev, "temp") -- copy attribute to device local met = sdm.method_dev_handle(par, "setup") local func = sdm.method_func(met) -- use parent "setup" method on the device func(par, dev) end ) 


Les attributs locaux id et datapin n'ont pas de crochets getter et setter , donc seuls leurs noms sont visibles.


DS18B20


Le pilote DS18B20 est presque le même que le pilote DS18S20 . La seule différence est la méthode de precision . Les deux pilotes DS18? 20 supposent une construction entière et n'utilisent pas la division en virgule flottante.


 sdm.attr_add(drv, "precision", "Precision (9|10|11|12)", 12, function(dev, precision) local attr = sdm.attr_dev_handle(dev, "precision") return sdm.attr_data(attr) end, function(dev, precision) local par = sdm.device_parent(dev) local attr = sdm.attr_dev_handle(dev, "precision") local ex = sdm.method_func(sdm.method_dev_handle(par, "exchange")) local modes = {[9]=0x1f, [10]=0x3f, [11]=0x5f, [12]=0x7f} if modes[precision] ~= nil then ex(par, dev, {0x4e, 0, 0, modes[precision]}) sdm.attr_set(attr, precision) end end ) 


Utilisation de la mémoire


La mémoire libre de l' ESP8266 est d'environ 40k . Le code du serveur est déplacé vers LFS , il ne prend donc pas d'espace RAM au moment de l'initialisation ( le code d'origine prenait environ 10k ).


SDM prend environ 10k pour 5 pilotes de périphérique et 5 périphériques. Légèrement moindre pour la construction de firmware non flottant. Il est donc préférable de ne sélectionner dans le manifeste du pilote que les pilotes nécessaires à la tâche à accomplir. La tâche la plus consommatrice de mémoire consiste à servir la bibliothèque vue.js



En cas de demande de données brutes codées JSON (en utilisant curl ), la consommation de mémoire de pointe peut être considérablement réduite.



Au lieu d'un épilogue


L'une des premières méthodes que j'ai implémentées avec sdm a été la liaison pour
node.restart() .
L'essayer en utilisant l'interface utilisateur Web a produit un résultat curieux. Juste après que le navigateur Web ait émis la demande, la puce a redémarré comme prévu. Mais parce que NodeMCU n'a pas répondu correctement à la demande HTTP, le navigateur Web a réessayé la même demande. Lorsque le serveur NodeMCU a redémarré et était de nouveau opérationnel, le navigateur s'y est connecté, réinitialise le compteur de node.restart() interne et a appelé la méthode node.restart() , commençant ainsi une boucle infinie de redémarrage de NodeMCU.

Source: https://habr.com/ru/post/fr449992/


All Articles