Presentación del modelo de controlador simple NodeMCU (SDM): interfaz de usuario dinámica

imagen


NodeMCU es un firmware interactivo que permite ejecutar el intérprete de Lua en el microcontrolador ESP8266 (el soporte ESP32 está en desarrollo). Junto con todas las interfaces de hardware normales, tiene un módulo WiFi y un sistema de archivos SPIFFS .


Este artículo describe el nuevo módulo para NodeMCU - sdm. SDM significa modelo de controlador simple y proporciona abstracción del modelo de controlador de dispositivo para el sistema. En la primera parte de este artículo discutiremos el modelo en sí y en la segunda parte se mostrará una interfaz de usuario web creada dinámicamente usando sdm con algunos comentarios.


Conceptos básicos del modelo de controlador


Dos componentes principales del modelo son dispositivos y controladores . Dispositivo es una representación abstracta de algún hardware o dispositivo virtual. Tiene sentido colocar dispositivos en la jerarquía de árboles, con el microcontrolador en la parte superior, los autobuses en el medio y los sensores como hojas.


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

El controlador de dispositivo es una pieza lógica asociada con un dispositivo dado. Las funciones proporcionadas por el controlador se denominan métodos , los contenedores de datos asociados con el controlador se denominan atributos . Ambos métodos y atributos viven dentro del controlador.


Los atributos tienen dos funciones asociadas con ellos: ganchos getter y setter . Por lo tanto, atribuye la funcionalidad del método de superconjunto, pero también ocupan más memoria (la memoria del microcontrolador es escasa, ¿recuerdas?).


 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 ) 

Dispositivo vinculante


La parte difícil del modelo de controlador es el enlace dispositivo-controlador. El proceso en sí es bastante simple: hacemos coincidir el dispositivo con cada controlador disponible hasta que encaje. Solo faltan dos partes: lógica coincidente y algunos datos con los que coincidir.


En la lógica de coincidencia SDM vive en los controladores bajo el nombre _poll() . Es un método regular que se llama con el identificador del dispositivo como parámetro y devuelve true o false si el dispositivo podría o no conectarse al controlador respectivamente.


 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 ) 

Como se ve en el ejemplo anterior, el controlador coincide con el dispositivo mediante el atributo Pero como se señaló anteriormente, los atributos se asocian solo con el controlador. En general es cierto, pero hay algunos atributos que no se pueden recuperar a través del software. Estas son ID de chip, pines usados, etc. Para ellos, se agregó un tipo especial de atributo al sdm - atributo local . Este atributo está asociado con una instancia del dispositivo y generalmente es inmutable.


Lo único que queda por decir sobre el enlace del controlador. Por lo general, los dispositivos requieren algún tipo de inicialización en el inicio y la limpieza después del uso. Para este propósito, SDM utiliza los _init() y _free() .
Si el controlador tiene el método _init() , se llamará automáticamente después del enlace del dispositivo. Lo mismo con _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 lector atento probablemente pregunte: ¿qué significa "copiar atributo" en el ejemplo anterior? Y tendría razón, porque esto tiene que ver con el tercer tipo de atributo que aún no hemos discutido: el atributo privado . No tiene mucho sentido compartir todos los datos de atributos entre todas las instancias de dispositivos. Para este propósito, SDM proporciona un mecanismo para copiar el atributo del controlador y asociarlo con el dispositivo. Esto hace que el atributo del controlador sea un prototipo o plantilla.


Un resumen rápido:


  • Los atributos locales se utilizan para datos que el software no puede recuperar. Como ID de dispositivo, pines conectados, etc.
  • Los atributos del controlador se utilizan para los datos compartidos entre todas las instancias de dispositivos conectados a este controlador.
  • Los atributos privados se copian de los atributos del controlador y contienen datos asociados con una sola instancia del dispositivo. Este tipo es el más común.

PropiedadAtributo localAtributo privadoAtributo del controlador (público)
Almacenado endispositivodispositivochofer
Accesible usando la manija del conductor--+
Accesible usando el asa del dispositivo+++
Compartido entre dispositivos--+
Persistir en la separación del conductor+-+

Implementación de interfaz de usuario web


Código del servidor


Hay un hermoso proyecto nodemcu-httpserver que implementa el código del servidor para NudeMCU. Lamentablemente parece estar muerto. Fue utilizado como base para el servidor. En primer lugar, las funciones del servidor se movieron a LFS y luego se modificaron ligeramente para servir una página estática para cada llamada. Vue.js es una opción perfecta para páginas web basadas en plantillas. Entonces se usó para frontend . Vale la pena señalar que NodeMCU puede no estar conectado a Internet. Debido a esto, la biblioteca vue.js debe estar presente localmente y servida por el servidor NodeMCU.


Como todos los dispositivos están organizados en estructura de árbol, se accede a ellos como un directorio: /ESP8266/ESP8266_1W/DS18S20-0 . Aquí /ESP8266 es una página NodeMCU, /ESP8266/ESP8266_1W es una página de bus 1Wire y finalmente /ESP8266/ESP8266_1W/DS18S20-0 es un sensor de temperatura.


Como se mencionó anteriormente, todas las páginas del dispositivo se crean a partir de una página de plantilla que se sirve en cada llamada. El código JS dentro de esta página realiza una solicitud a la misma URL, antepuesta con /api . Para el ejemplo anterior, la URL de la llamada sería /api/ESP8266/ESP8266_1W/DS18S20-0 . En tales solicitudes, el servidor responde con datos específicos del dispositivo codificados con JSON , que pueblan la página. Por supuesto, la solicitud de página HTML puede omitirse si solo se necesitan datos sin procesar.


Árbol de dispositivos


La configuración inicial del dispositivo se realiza utilizando una estructura de árbol de dispositivo simple . Es como el árbol de dispositivos , pero más simple. Describe la configuración del hardware, incluidos los atributos locales del dispositivo.


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

Configuración de hardware


Aquí comienza el escaparate. Para este propósito, se conectaron un grupo de sensores al NodeMCU:



1Los sensores de cable están conectados al mismo pin.



Páginas web y controladores


dispositivo raíz


El objetivo principal del dispositivo raíz (también conocido como ESP8266) es proporcionar un lugar para que sus hijos se conecten. Sin embargo, no está restringido tener métodos o atributos asociados con él.


Este fragmento de código es de aquí :


 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 ) 

Este código agrega float atributo que se utiliza para contener el tipo de compilación de firmware. Su valor se inicializa en el _init() que es una función especial, que se ejecuta una vez cuando el controlador se conecta al dispositivo.


Esta es la página generada para el dispositivo raíz.



Aquí podemos ver que el dispositivo raíz tiene un heap métodos, dos atributos de controlador float e id . Finalmente, tiene dos dispositivos conectados: los buses SPI y 1Wire .


SPI


El controlador SPI no es muy interesante. Simplemente asigna funciones NodeMCU SPI .



Mcp3208


MCP3208 es un chip ADC . Mide voltajes de cero a ref y devuelve un código de 12 bits. Lo interesante de esta implementación de controlador es que la ref atributo estaría presente compilación solo si el firmware admite aritmética de punto flotante. Si no es compatible, en lugar de voltaje absoluto, el código de voltaje se devuelve por métodos single y 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 


También tenga en cuenta que este dispositivo tiene el atributo ref marcado como privado . Se establece por dispositivo.


1 cable


1 Driver driver implementa el método de poll : búsqueda dinámica de dispositivos .


Inmediatamente después del descubrimiento del dispositivo, se desconoce su tipo. Por lo tanto, su dirección única 1Wire se usa como un nuevo nombre de dispositivo (bytes representados como números separados por _ caracteres).


 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 ) 

Esta es la página inicial para el controlador 1Wire .



Después de emitir una llamada de poll con el argumento 2 y actualizar la página, aparece la sección secundaria. Tenga en cuenta que los nombres de los niños son legibles por humanos. Esto se debe a que la función device_rename() fue llamada durante su _init .



DS18S20


Tras la inicialización, el controlador DS18S20 comprueba que la ID del dispositivo comienza con 0x10 , que es un código de familia del dispositivo. Cuando el dispositivo se conecta al controlador, se renombra a DS18S20-X , donde DS18S20 es un nombre base y X es un número de instancia.


 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 ) 


Los atributos locales id y datapin no tienen ganchos getter y setter , por lo que solo sus nombres son visibles.


DS18B20


El controlador DS18B20 es casi lo mismo que el controlador DS18S20 . La única diferencia es el método de precision . Ambos controladores DS18-20 asumen la construcción de enteros y no utilizan la división de punto flotante.


 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 ) 


Uso de la memoria


La memoria libre ESP8266 es de aproximadamente 40k . El código del servidor se mueve a LFS , por lo que no ocupa espacio RAM en el momento de la inicialización (el código original tardó aproximadamente 10k ).


SDM ocupa aproximadamente 10k para 5 controladores de dispositivo y 5 dispositivos. Ligeramente menor para la compilación de firmware no flotante. Por lo tanto, es preferible seleccionar en el manifiesto del controlador solo los controladores necesarios para la tarea en cuestión. La tarea que consume más memoria es servir la biblioteca vue.js



En caso de solicitar datos sin codificar JSON (usando curl ), el consumo máximo de memoria puede reducirse significativamente.



En lugar de un epílogo


Uno de los primeros métodos que implementé con sdm fue el enlace para
node.restart() .
Probarlo usando la interfaz de usuario web produjo un resultado curioso. Justo después de que el navegador web emitió la solicitud, el chip se reinició como se esperaba. Pero debido a que NodeMCU no respondió correctamente a la solicitud HTTP, el navegador web intentó la misma solicitud nuevamente. Cuando el servidor NodeMCU se reinició y volvió a funcionar, el navegador se conectó a él, restableció el contador interno de prueba nuevamente y llamó al método node.restart() , comenzando así un bucle infinito de reinicio de NodeMCU.

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


All Articles