Add the missing Save button to the web UI for OV5640 (#136)

* Add the missing Save button to the web UI for OV5640

* Add MDNS feature that allows the cameras to be found

* Add mDNS Camera Query
ov5640-save-button^2
Me No Dev 2020-03-27 10:47:07 +02:00 committed by GitHub
parent 7937d37e14
commit 67a57377cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 370 additions and 30 deletions

View File

@ -1,4 +1,4 @@
set(COMPONENT_SRCS "app_main.c" "app_wifi.c" "app_camera.c" "app_httpd.c") set(COMPONENT_SRCS "app_main.c" "app_wifi.c" "app_camera.c" "app_httpd.c" "app_mdns.c")
set(COMPONENT_ADD_INCLUDEDIRS "include") set(COMPONENT_ADD_INCLUDEDIRS "include")
set(COMPONENT_REQUIRES set(COMPONENT_REQUIRES

View File

@ -20,6 +20,7 @@
#include "driver/ledc.h" #include "driver/ledc.h"
//#include "camera_index.h" //#include "camera_index.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#include "app_mdns.h"
#if defined(ARDUINO_ARCH_ESP32) && defined(CONFIG_ARDUHAL_ESP_LOG) #if defined(ARDUINO_ARCH_ESP32) && defined(CONFIG_ARDUHAL_ESP_LOG)
#include "esp32-hal-log.h" #include "esp32-hal-log.h"
@ -705,8 +706,12 @@ static esp_err_t cmd_handler(httpd_req_t *req)
int res = 0; int res = 0;
if (!strcmp(variable, "framesize")) { if (!strcmp(variable, "framesize")) {
if (s->pixformat == PIXFORMAT_JPEG) if (s->pixformat == PIXFORMAT_JPEG) {
res = s->set_framesize(s, (framesize_t)val); res = s->set_framesize(s, (framesize_t)val);
if (res == 0) {
app_mdns_update_framesize(val);
}
}
} }
else if (!strcmp(variable, "quality")) else if (!strcmp(variable, "quality"))
res = s->set_quality(s, val); res = s->set_quality(s, val);
@ -878,6 +883,15 @@ static esp_err_t status_handler(httpd_req_t *req)
return httpd_resp_send(req, json_response, strlen(json_response)); return httpd_resp_send(req, json_response, strlen(json_response));
} }
static esp_err_t mdns_handler(httpd_req_t *req)
{
size_t json_len = 0;
const char * json_response = app_mdns_query(&json_len);
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
return httpd_resp_send(req, json_response, json_len);
}
static esp_err_t xclk_handler(httpd_req_t *req) static esp_err_t xclk_handler(httpd_req_t *req)
{ {
char *buf = NULL; char *buf = NULL;
@ -1137,6 +1151,12 @@ void app_httpd_main()
.handler = win_handler, .handler = win_handler,
.user_ctx = NULL}; .user_ctx = NULL};
httpd_uri_t mdns_uri = {
.uri = "/mdns",
.method = HTTP_GET,
.handler = mdns_handler,
.user_ctx = NULL};
ra_filter_init(&ra_filter, 20); ra_filter_init(&ra_filter, 20);
#if CONFIG_ESP_FACE_DETECT_ENABLED #if CONFIG_ESP_FACE_DETECT_ENABLED
@ -1179,6 +1199,8 @@ void app_httpd_main()
httpd_register_uri_handler(camera_httpd, &greg_uri); httpd_register_uri_handler(camera_httpd, &greg_uri);
httpd_register_uri_handler(camera_httpd, &pll_uri); httpd_register_uri_handler(camera_httpd, &pll_uri);
httpd_register_uri_handler(camera_httpd, &win_uri); httpd_register_uri_handler(camera_httpd, &win_uri);
httpd_register_uri_handler(camera_httpd, &mdns_uri);
} }
config.server_port += 1; config.server_port += 1;

View File

@ -23,10 +23,12 @@
#include "app_camera.h" #include "app_camera.h"
#include "app_wifi.h" #include "app_wifi.h"
#include "app_httpd.h" #include "app_httpd.h"
#include "app_mdns.h"
void app_main() void app_main()
{ {
app_wifi_main(); app_wifi_main();
app_camera_main(); app_camera_main();
app_httpd_main(); app_httpd_main();
app_mdns_main();
} }

View File

@ -0,0 +1,242 @@
/*
* ESPRESSIF MIT License
*
* Copyright (c) 2020 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
*
* Permission is hereby granted for use on ESPRESSIF SYSTEMS products only, in which case,
* it is free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
#include <string.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_camera.h"
#include "mdns.h"
#include "app_camera.h"
static const char *TAG = "camera mdns";
static const char * service_name = "_esp-cam";
static const char * proto = "_tcp";
static mdns_result_t * found_cams = NULL;
static SemaphoreHandle_t query_lock = NULL;
static char iname[64];
static char hname[64];
static char framesize[4];
static char pixformat[4];
static const char * model = NULL;
static void mdns_query_for_cams()
{
mdns_result_t * new_cams = NULL;
esp_err_t err = mdns_query_ptr(service_name, proto, 5000, 4, &new_cams);
if(err){
ESP_LOGE(TAG, "MDNS Query Failed: %s", esp_err_to_name(err));
return;
}
xSemaphoreTake(query_lock, portMAX_DELAY);
if (found_cams != NULL) {
mdns_query_results_free(found_cams);
}
found_cams = new_cams;
xSemaphoreGive(query_lock);
}
static void mdns_task(void * arg)
{
for (;;) {
mdns_query_for_cams();
//delay 55 seconds
vTaskDelay((55 * 1000) / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
/*
* Public Functions
*/
const char * app_mdns_query(size_t * out_len)
{
//build JSON
static char json_response[2048];
char *p = json_response;
*p++ = '[';
//add own data first
tcpip_adapter_ip_info_t ip;
if (strlen(CONFIG_ESP_WIFI_SSID)) {
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip);
} else {
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip);
}
*p++ = '{';
p += sprintf(p, "\"instance\":\"%s\",", iname);
p += sprintf(p, "\"host\":\"%s.local\",", hname);
p += sprintf(p, "\"port\":80,");
p += sprintf(p, "\"txt\":{");
p += sprintf(p, "\"pixformat\":\"%s\",", pixformat);
p += sprintf(p, "\"framesize\":\"%s\",", framesize);
p += sprintf(p, "\"stream_port\":\"81\",");
p += sprintf(p, "\"board\":\"%s\",", CAM_BOARD);
p += sprintf(p, "\"model\":\"%s\",", model);
*p++ = '}';
*p++ = ',';
p += sprintf(p, "\"ip\":\"" IPSTR "\",", IP2STR(&(ip.ip)));
p += sprintf(p, "\"id\":\"" IPSTR ":80\",", IP2STR(&(ip.ip)));
p += sprintf(p, "\"service\":\"%s\",", service_name);
p += sprintf(p, "\"proto\":\"%s\"", proto);
*p++ = '}';
xSemaphoreTake(query_lock, portMAX_DELAY);
if (found_cams) {
*p++ = ',';
}
mdns_result_t * r = found_cams;
mdns_ip_addr_t * a = NULL;
int t;
while(r){
*p++ = '{';
if(r->instance_name){
p += sprintf(p, "\"instance\":\"%s\",", r->instance_name);
}
if(r->hostname){
p += sprintf(p, "\"host\":\"%s.local\",", r->hostname);
p += sprintf(p, "\"port\":%u,", r->port);
}
if(r->txt_count){
p += sprintf(p, "\"txt\":{");
for(t=0; t<r->txt_count; t++){
if (t > 0) {
*p++ = ',';
}
p += sprintf(p, "\"%s\":\"%s\"", r->txt[t].key, r->txt[t].value?r->txt[t].value:"NULL");
}
*p++ = '}';
*p++ = ',';
}
a = r->addr;
while(a){
if(a->addr.type != IPADDR_TYPE_V6){
p += sprintf(p, "\"ip\":\"" IPSTR "\",", IP2STR(&(a->addr.u_addr.ip4)));
p += sprintf(p, "\"id\":\"" IPSTR ":%u\",", IP2STR(&(a->addr.u_addr.ip4)), r->port);
break;
}
a = a->next;
}
p += sprintf(p, "\"service\":\"%s\",", service_name);
p += sprintf(p, "\"proto\":\"%s\"", proto);
*p++ = '}';
r = r->next;
if (r) {
*p++ = ',';
}
}
xSemaphoreGive(query_lock);
*p++ = ']';
*out_len = (uint32_t)p - (uint32_t)json_response;
*p++ = '\0';
ESP_LOGI(TAG, "JSON: %uB", *out_len);
return (const char *)json_response;
}
void app_mdns_update_framesize(int size)
{
snprintf(framesize, 4, "%d", size);
if(mdns_service_txt_item_set(service_name, proto, "framesize", (char*)framesize)){
ESP_LOGE(TAG, "mdns_service_txt_item_set() framesize Failed");
}
}
void app_mdns_main()
{
uint8_t mac[6];
query_lock = xSemaphoreCreateBinary();
if (query_lock == NULL) {
ESP_LOGE(TAG, "xSemaphoreCreateMutex() Failed");
return;
}
xSemaphoreGive(query_lock);
if (esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) {
ESP_LOGE(TAG, "esp_read_mac() Failed");
return;
}
sensor_t * s = esp_camera_sensor_get();
switch(s->id.PID){
case OV2640_PID: model = "OV2640"; break;
case OV3660_PID: model = "OV3660"; break;
case OV5640_PID: model = "OV5640"; break;
case OV7725_PID: model = "OV7725"; break;
default: model = "UNKNOWN"; break;
}
snprintf(iname, 64, "%s-%s-%02X%02X%02X", CAM_BOARD, model, mac[3], mac[4], mac[5]);
snprintf(framesize, 4, "%d", s->status.framesize);
snprintf(pixformat, 4, "%d", s->pixformat);
char * src = iname, * dst = hname, c;
while (*src) {
c = *src++;
if (c >= 'A' && c <= 'Z') {
c -= 'A' - 'a';
}
*dst++ = c;
}
*dst++ = '\0';
if(mdns_init() != ESP_OK){
ESP_LOGE(TAG, "mdns_init() Failed");
return;
}
if(mdns_hostname_set(hname) != ESP_OK){
ESP_LOGE(TAG, "mdns_hostname_set(\"%s\") Failed", hname);
return;
}
if(mdns_instance_name_set(iname) != ESP_OK){
ESP_LOGE(TAG, "mdns_instance_name_set(\"%s\") Failed", iname);
return;
}
if(mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0) != ESP_OK){
ESP_LOGE(TAG, "mdns_service_add() HTTP Failed");
return;
}
mdns_txt_item_t camera_txt_data[] = {
{(char*)"board" ,(char*)CAM_BOARD},
{(char*)"model" ,(char*)model},
{(char*)"stream_port" ,(char*)"81"},
{(char*)"framesize" ,(char*)framesize},
{(char*)"pixformat" ,(char*)pixformat}
};
if(mdns_service_add(NULL, service_name, proto, 80, camera_txt_data, 5)) {
ESP_LOGE(TAG, "mdns_service_add() ESP-CAM Failed");
return;
}
xTaskCreate(mdns_task, "mdns-cam", 2048, NULL, 2, NULL);
}

View File

@ -33,6 +33,8 @@
#include "lwip/err.h" #include "lwip/err.h"
#include "lwip/sys.h" #include "lwip/sys.h"
#include "mdns.h"
/* The examples use WiFi configuration that you can set via 'make menuconfig'. /* The examples use WiFi configuration that you can set via 'make menuconfig'.
If you'd rather not, just change the below entries to strings with If you'd rather not, just change the below entries to strings with
@ -83,6 +85,7 @@ static esp_err_t event_handler(void *ctx, system_event_t *event)
default: default:
break; break;
} }
mdns_handle_system_event(ctx, event);
return ESP_OK; return ESP_OK;
} }
@ -169,4 +172,5 @@ void app_wifi_main()
wifi_init_sta(); wifi_init_sta();
} }
ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
} }

View File

@ -25,6 +25,7 @@
#define _APP_CAMERA_H_ #define _APP_CAMERA_H_
#if CONFIG_CAMERA_MODEL_WROVER_KIT #if CONFIG_CAMERA_MODEL_WROVER_KIT
#define CAM_BOARD "WROVER-KIT"
#define PWDN_GPIO_NUM -1 #define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1 #define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 21 #define XCLK_GPIO_NUM 21
@ -44,6 +45,7 @@
#define PCLK_GPIO_NUM 22 #define PCLK_GPIO_NUM 22
#elif CONFIG_CAMERA_MODEL_ESP32_CAM_BOARD #elif CONFIG_CAMERA_MODEL_ESP32_CAM_BOARD
#define CAM_BOARD "ESP-DEVCAM"
#define PWDN_GPIO_NUM 32 #define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM 33 #define RESET_GPIO_NUM 33
#define XCLK_GPIO_NUM 4 #define XCLK_GPIO_NUM 4
@ -63,6 +65,7 @@
#define PCLK_GPIO_NUM 25 #define PCLK_GPIO_NUM 25
#elif CONFIG_CAMERA_MODEL_ESP_EYE #elif CONFIG_CAMERA_MODEL_ESP_EYE
#define CAM_BOARD "ESP-EYE"
#define PWDN_GPIO_NUM -1 #define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1 #define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 4 #define XCLK_GPIO_NUM 4
@ -82,6 +85,7 @@
#define PCLK_GPIO_NUM 25 #define PCLK_GPIO_NUM 25
#elif CONFIG_CAMERA_MODEL_M5STACK_PSRAM #elif CONFIG_CAMERA_MODEL_M5STACK_PSRAM
#define CAM_BOARD "M5CAM"
#define PWDN_GPIO_NUM -1 #define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM 15 #define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM 27 #define XCLK_GPIO_NUM 27
@ -101,6 +105,7 @@
#define PCLK_GPIO_NUM 21 #define PCLK_GPIO_NUM 21
#elif CONFIG_CAMERA_MODEL_M5STACK_WIDE #elif CONFIG_CAMERA_MODEL_M5STACK_WIDE
#define CAM_BOARD "M5CAMW"
#define PWDN_GPIO_NUM -1 #define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM 15 #define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM 27 #define XCLK_GPIO_NUM 27
@ -120,6 +125,7 @@
#define PCLK_GPIO_NUM 21 #define PCLK_GPIO_NUM 21
#elif CONFIG_CAMERA_MODEL_AI_THINKER #elif CONFIG_CAMERA_MODEL_AI_THINKER
#define CAM_BOARD "AI-THINKER"
#define PWDN_GPIO_NUM 32 #define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1 #define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0 #define XCLK_GPIO_NUM 0
@ -140,23 +146,24 @@
#elif CONFIG_CAMERA_MODEL_CUSTOM #elif CONFIG_CAMERA_MODEL_CUSTOM
#define PWDN_GPIO_NUM CONFIG_CAMERA_PIN_PWDN #define CAM_BOARD "CUSTOM"
#define RESET_GPIO_NUM CONFIG_CAMERA_PIN_RESET #define PWDN_GPIO_NUM CONFIG_CAMERA_PIN_PWDN
#define XCLK_GPIO_NUM CONFIG_CAMERA_PIN_XCLK #define RESET_GPIO_NUM CONFIG_CAMERA_PIN_RESET
#define SIOD_GPIO_NUM CONFIG_CAMERA_PIN_SIOD #define XCLK_GPIO_NUM CONFIG_CAMERA_PIN_XCLK
#define SIOC_GPIO_NUM CONFIG_CAMERA_PIN_SIOC #define SIOD_GPIO_NUM CONFIG_CAMERA_PIN_SIOD
#define SIOC_GPIO_NUM CONFIG_CAMERA_PIN_SIOC
#define Y9_GPIO_NUM CONFIG_CAMERA_PIN_Y9 #define Y9_GPIO_NUM CONFIG_CAMERA_PIN_Y9
#define Y8_GPIO_NUM CONFIG_CAMERA_PIN_Y8 #define Y8_GPIO_NUM CONFIG_CAMERA_PIN_Y8
#define Y7_GPIO_NUM CONFIG_CAMERA_PIN_Y7 #define Y7_GPIO_NUM CONFIG_CAMERA_PIN_Y7
#define Y6_GPIO_NUM CONFIG_CAMERA_PIN_Y6 #define Y6_GPIO_NUM CONFIG_CAMERA_PIN_Y6
#define Y5_GPIO_NUM CONFIG_CAMERA_PIN_Y5 #define Y5_GPIO_NUM CONFIG_CAMERA_PIN_Y5
#define Y4_GPIO_NUM CONFIG_CAMERA_PIN_Y4 #define Y4_GPIO_NUM CONFIG_CAMERA_PIN_Y4
#define Y3_GPIO_NUM CONFIG_CAMERA_PIN_Y3 #define Y3_GPIO_NUM CONFIG_CAMERA_PIN_Y3
#define Y2_GPIO_NUM CONFIG_CAMERA_PIN_Y2 #define Y2_GPIO_NUM CONFIG_CAMERA_PIN_Y2
#define VSYNC_GPIO_NUM CONFIG_CAMERA_PIN_VSYNC #define VSYNC_GPIO_NUM CONFIG_CAMERA_PIN_VSYNC
#define HREF_GPIO_NUM CONFIG_CAMERA_PIN_HREF #define HREF_GPIO_NUM CONFIG_CAMERA_PIN_HREF
#define PCLK_GPIO_NUM CONFIG_CAMERA_PIN_PCLK #define PCLK_GPIO_NUM CONFIG_CAMERA_PIN_PCLK
#endif #endif
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -0,0 +1,31 @@
// Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _CAMERA_MDNS_H_
#define _CAMERA_MDNS_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
void app_mdns_main();
void app_mdns_update_framesize(int size);
const char * app_mdns_query(size_t * out_len);
#ifdef __cplusplus
}
#endif
#endif /* _CAMERA_MDNS_H_ */

View File

@ -135,7 +135,7 @@
padding: 0 5px padding: 0 5px
} }
button { button, .button {
display: block; display: block;
margin: 5px; margin: 5px;
padding: 0 12px; padding: 0 12px;
@ -335,6 +335,17 @@
display: none display: none
} }
.save {
position: absolute;
right: 25px;
top: 0px;
height: 16px;
line-height: 16px;
padding: 0 4px;
text-decoration: none;
cursor: pointer
}
input[type=text] { input[type=text] {
border: 1px solid #363636; border: 1px solid #363636;
font-size: 14px; font-size: 14px;
@ -391,7 +402,7 @@
<div class="input-group" id="framesize-group"> <div class="input-group" id="framesize-group">
<label for="framesize">Resolution</label> <label for="framesize">Resolution</label>
<select id="framesize" class="default-action"> <select id="framesize" class="default-action">
<!-- 5MP --> <!-- 5MP -->
<option value="21" selected="selected">QSXGA(2560x1920)</option> <option value="21" selected="selected">QSXGA(2560x1920)</option>
<option value="20">P FHD(1080x1920)</option> <option value="20">P FHD(1080x1920)</option>
<option value="19">WQXGA(2560x1600)</option> <option value="19">WQXGA(2560x1600)</option>
@ -895,8 +906,9 @@
</div> </div>
<figure> <figure>
<div id="stream-container" class="image-container hidden"> <div id="stream-container" class="image-container hidden">
<a id="save-still" href="#" class="button save" download="capture.jpg">Save</a>
<div class="close" id="close-stream">×</div> <div class="close" id="close-stream">×</div>
<img id="stream" src=""> <img id="stream" src="" crossorigin>
</div> </div>
</figure> </figure>
</div> </div>
@ -987,15 +999,15 @@ document.addEventListener('DOMContentLoaded', function (event) {
document document
.querySelectorAll('.reg-action') .querySelectorAll('.reg-action')
.forEach(el => { .forEach(el => {
if (el.type === 'text') { if (el.type === 'text') {
el.onkeyup = function(e){ el.onkeyup = function(e){
if(e.keyCode == 13){ if(e.keyCode == 13){
setRegValue(el); setRegValue(el);
} }
} }
} else { } else {
el.onchange = () => setRegValue(el) el.onchange = () => setRegValue(el)
} }
}) })
@ -1124,6 +1136,26 @@ document.addEventListener('DOMContentLoaded', function (event) {
}); });
} }
const saveButton = document.getElementById('save-still');
saveButton.onclick = () => {
var canvas = document.createElement("canvas");
canvas.width = view.width;
canvas.height = view.height;
document.body.appendChild(canvas);
var context = canvas.getContext('2d');
context.drawImage(view,0,0);
try {
var dataURL = canvas.toDataURL('image/jpeg');
saveButton.href = dataURL;
var d = new Date();
saveButton.download = d.getFullYear() + ("0"+(d.getMonth()+1)).slice(-2) + ("0" + d.getDate()).slice(-2) + ("0" + d.getHours()).slice(-2) + ("0" + d.getMinutes()).slice(-2) + ("0" + d.getSeconds()).slice(-2) + ".jpg";
} catch (e) {
console.error(e);
}
canvas.parentNode.removeChild(canvas);
}
const hide = el => { const hide = el => {
el.classList.add('hidden') el.classList.add('hidden')
} }