Showcase model driver sederhana (SDM) NodeMCU: antarmuka pengguna yang dinamis

gambar


NodeMCU adalah firmware interaktif , yang memungkinkan menjalankan interpreter Lua pada mikrokontroler ESP8266 (dukungan ESP32 sedang dalam pengembangan). Bersamaan dengan semua antarmuka perangkat keras biasa, ia memiliki modul WiFi dan sistem file SPIFFS .


Artikel ini menjelaskan modul baru untuk NodeMCU - sdm. SDM merupakan kependekan dari model driver sederhana dan menyediakan abstraksi model driver-perangkat untuk sistem. Pada bagian pertama artikel ini kita akan membahas model itu sendiri dan pada bagian kedua akan menjadi karya antarmuka pengguna web yang dibuat secara dinamis menggunakan sdm dengan beberapa komentar.


Dasar-dasar model driver


Dua komponen utama dari model ini adalah perangkat dan driver . Perangkat adalah representasi abstrak dari beberapa perangkat keras atau perangkat virtual. Masuk akal untuk menempatkan perangkat ke hierarki pohon, dengan mikrokontroler di atas, bus di tengah dan sensor sebagai dedaunan.


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

Driver perangkat adalah sepotong logika yang terkait dengan perangkat yang diberikan. Fungsi yang disediakan oleh driver disebut metode , wadah data yang terkait dengan driver disebut atribut . Baik metode dan atribut tinggal di dalam driver.


Atribut memiliki dua fungsi yang terkait dengannya: kait pengambil dan penyetel . Jadi atribut fungsionalitas metode superset, tetapi mereka juga mengambil lebih banyak memori (memori mikrokontroler langka, ingat?).


 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 ) 

Mengikat perangkat


Bagian rumit dari model driver adalah pengikatan device-driver. Prosesnya sendiri cukup sederhana: kami mencocokkan perangkat dengan masing-masing driver yang tersedia hingga cocok. Hanya dua bagian yang hilang - logika yang cocok dan beberapa data untuk dicocokkan.


Dalam sdm, logika yang cocok tinggal di driver dengan nama _poll() . Ini adalah metode biasa yang disebut dengan gagang perangkat sebagai parameter dan mengembalikan true atau false jika perangkat dapat atau tidak dapat dilampirkan ke driver masing-masing.


 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 ) 

Seperti terlihat pada contoh di atas, driver mencocokkan perangkat menggunakan atribut. Tetapi seperti disebutkan di atas, atribut hanya diasosiasikan dengan driver. Secara umum memang benar, tetapi ada beberapa atribut yang tidak dapat diambil melalui perangkat lunak. Ini adalah chip ID, pin bekas dll. Bagi mereka jenis atribut khusus telah ditambahkan ke atribut sdm - local . Atribut ini dikaitkan dengan satu instance perangkat dan biasanya tidak berubah.


Satu-satunya hal yang tersisa untuk dikatakan tentang pengikatan driver. Biasanya perangkat memerlukan semacam inisialisasi pada startup dan pembersihan setelah digunakan. Untuk tujuan ini sdm menggunakan _init() dan _free() .
Jika driver memiliki metode _init() maka itu akan dipanggil secara otomatis setelah perangkat mengikat. Sama dengan _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 ) 

Pembaca yang penuh perhatian mungkin akan bertanya: apa artinya "menyalin atribut" pada contoh di atas? Dan dia akan benar, karena ini ada hubungannya dengan atribut jenis ketiga yang belum kita diskusikan - atribut pribadi . Tidak masuk akal untuk memiliki semua data atribut yang dibagikan di antara semua perangkat contoh. Untuk keperluan ini sdm menyediakan mekanisme menyalin atribut dari driver dan menghubungkannya dengan perangkat. Ini membuat atribut driver prototipe atau templat.


Ringkasan cepat:


  • atribut lokal digunakan untuk data yang tidak dapat diambil oleh perangkat lunak. Seperti ID perangkat, pin yang terhubung, dll.
  • atribut driver digunakan untuk data yang dibagikan di antara semua instance perangkat yang terlampir pada driver ini.
  • atribut pribadi disalin dari atribut driver dan menyimpan data yang terkait dengan hanya satu perangkat contoh. Jenis ini adalah yang paling umum.

PropertiAtribut lokalAtribut pribadiAtribut driver (publik)
Disimpan dalamperangkatperangkatpengemudi
Dapat diakses menggunakan pegangan pengemudi--+
Dapat diakses menggunakan pegangan perangkat+++
Dibagi antar perangkat--+
Bertahan saat pengemudi lepas+-+

Implementasi antarmuka pengguna web


Kode server


Ada proyek nodemcu-httpserver yang indah yang mengimplementasikan kode server untuk NudeMCU. Sayangnya sepertinya sudah mati. Itu digunakan sebagai dasar untuk server. Pertama, fungsi server dipindahkan ke LFS dan kemudian sedikit dimodifikasi untuk melayani satu halaman statis untuk setiap panggilan. Vue.js adalah pilihan sempurna untuk halaman web berbasis template. Jadi itu digunakan untuk frontend . Perlu dicatat bahwa NodeMCU mungkin tidak terhubung ke Internet. Karena itu, pustaka vue.js harus hadir secara lokal dan dilayani oleh server NodeMCU.


Karena semua perangkat disusun dalam struktur pohon, mereka diakses seperti direktori: /ESP8266/ESP8266_1W/DS18S20-0 . Di sini /ESP8266 adalah halaman NodeMCU, /ESP8266/ESP8266_1W adalah halaman bus 1Wire dan akhirnya /ESP8266/ESP8266_1W/DS18S20-0 adalah sensor suhu.


Seperti disebutkan sebelumnya, semua halaman perangkat dibuat dari satu halaman template yang disajikan pada setiap panggilan. Kode JS di dalam halaman ini kemudian membuat permintaan ke URL yang sama, diawali dengan /api . Untuk contoh URL panggilan di atas adalah /api/ESP8266/ESP8266_1W/DS18S20-0 . Atas permintaan tersebut, server merespons dengan data spesifik perangkat yang disandikan JSON , yang mengisi halaman tersebut. Tentu saja, permintaan halaman HTML dapat dilewati jika hanya data mentah yang diperlukan.


Pohon perangkat


Konfigurasi perangkat awal dilakukan menggunakan struktur pohon perangkat sederhana . Ini seperti pohon perangkat , tetapi lebih sederhana. Ini menjelaskan konfigurasi perangkat keras termasuk atribut lokal perangkat.


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

Pengaturan perangkat keras


Di sinilah dimulai showcase. Untuk keperluan ini, sekelompok sensor terhubung ke NodeMCU:



1 Sensor kawat terhubung ke pin yang sama.



Halaman web dan driver


perangkat root


Tujuan utama perangkat root (alias ESP8266) adalah untuk menyediakan tempat bagi anak-anak untuk terhubung. Namun tidak terbatas untuk memiliki metode atau atribut yang terkait dengannya.


Cuplikan kode ini dari sini :


 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 ) 

Kode ini menambahkan atribut float yang digunakan untuk menampung tipe build firmware. Nilainya diinisialisasi dalam kait _init() yang merupakan fungsi khusus, yang berjalan satu kali ketika driver menempel ke perangkat.


Ini adalah halaman yang dihasilkan untuk perangkat root.



Di sini kita dapat melihat bahwa perangkat root memiliki satu metode heap , dua atribut driver float dan id . Akhirnya, ia memiliki dua perangkat yang terhubung - SPI dan 1Wire bus.


SPI


Pengemudi SPI tidak terlalu menarik. Itu hanya memetakan fungsi NodeMCU SPI .



Mcp3208


MCP3208 adalah chip ADC . Ini mengukur tegangan dari nol ke ref dan mengembalikan kode 12 bit. Yang menarik dari implementasi driver ini adalah bahwa atribut ref hanya akan hadir jika firmware mendukung aritmatika floating point. Jika tidak didukung maka alih-alih tegangan absolut, kode tegangan dikembalikan oleh metode single dan 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 


Perhatikan juga bahwa perangkat ini memiliki atribut ref ditandai sebagai pribadi . Ini diatur berdasarkan per perangkat.


1wire


1 Driver driver mengimplementasikan metode poll - pencarian dinamis untuk perangkat .


Tepat setelah penemuan perangkat jenisnya tidak diketahui. Jadi alamat unik 1Wire -nya digunakan sebagai nama perangkat baru (byte diwakili sebagai angka yang dipisahkan oleh karakter _ ).


 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 ) 

Ini adalah halaman awal untuk driver 1Wire .



Setelah mengeluarkan panggilan poll dengan argumen 2 dan halaman yang menyegarkan, bagian anak-anak muncul. Perhatikan bahwa nama anak-anak dapat dibaca manusia. Ini karena fungsi device_rename() dipanggil selama _init mereka.



DS18S20


Setelah inisialisasi, driver DS18S20 memeriksa bahwa ID perangkat dimulai dengan 0x10 , yang merupakan kode keluarga perangkat. Ketika perangkat terpasang ke driver, namanya diganti menjadi DS18S20-X , di mana DS18S20 adalah nama dasar dan X adalah nomor instan.


 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 ) 


Atribut lokal id dan datapin tidak memiliki kait getter dan setter , jadi hanya nama mereka yang terlihat.


DS18B20


Driver DS18B20 hampir sama dengan driver DS18S20 . Satu-satunya perbedaan adalah metode precision . Kedua driver DS18? 20 menganggap build integer dan tidak menggunakan divisi floating point.


 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 ) 


Penggunaan memori


Memori bebas ESP8266 sekitar 40k . Kode server dipindahkan ke LFS , sehingga tidak memerlukan ruang RAM saat inisialisasi ( kode asli memakan waktu sekitar 10rb ).


SDM membutuhkan sekitar 10rb untuk 5 driver perangkat dan 5 perangkat. Sedikit lebih rendah untuk versi firmware non-mengambang. Jadi lebih baik untuk memilih driver manifes hanya driver yang diperlukan untuk tugas yang dihadapi. Tugas paling banyak memakan memori adalah untuk melayani perpustakaan vue.js



Dalam hal meminta data mentah JSON- encoded (menggunakan curl ), konsumsi memori puncak dapat dikurangi secara signifikan.



Alih-alih sebuah epilog


Salah satu metode pertama yang saya terapkan dengan sdm adalah pengikatan untuk
node.restart() .
Mencoba menggunakan antarmuka pengguna web menghasilkan hasil yang aneh. Tepat setelah browser web mengeluarkan permintaan, chip dimulai kembali seperti yang diharapkan. Tetapi karena NodeMCU tidak menanggapi dengan baik permintaan HTTP, browser web mencoba lagi permintaan yang sama. Ketika server NodeMCU dihidupkan ulang dan dinyalakan kembali, browser terhubung, reset internal coba lagi dan disebut metode node.restart() , sehingga memulai loop tak terbatas dari restart NodeMCU.

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


All Articles