Урок 5. Карта памяти. Markdown веб-сервер



  • Цель урока

    Сегодня мы научимся работать с TF-картой. Однажды в одном из уроков мы научились поднимать веб-сервер, но сегодня мы хотим больше, чем просто отдавать текст клиенту. Markdown. Облегчённый язык разметки, созданный с целью написания максимально читаемого и удобного для правки текста, но пригодного для преобразования в языки для продвинутых публикаций (HTML, Rich Text и других). Мы напишем Markdown Web-сервер на базе M5STACK (рис. 1).

    Рисунок 1.

    При включении устройства пользователю будет предложено вставить карту памяти. На карте памяти будет находится следующие файлы:

    • файл с известными Wi-Fi-сетями (wifi.ini);
    • файл стилей (style.css);
    • JS-библиотека, обрабатывающая Markdown текст (markdown.js);
    • в корне карты памяти будут находиться пользовательские markdown-файлы (*.md), которые будут доступны клиентам (рис. 1.1).

    Рисунок 1.1. Markdown редактор

    После того, как пользователь вставит карту памяти необходимо перезагрузить устройство.
    При запуске устройство загрузит с карты памяти настройки Wi-Fi и попробует подключиться к первой доступной сети. Далее устройство отобразит на экране IP-адрес. Для того, чтобы просматривать веб-станицы необходимо использовать современный браузер с поддержкой JS и CSS.

    Краткая справка

    MicroSD — формат карт памяти (рис. 2) предазначенный для использования в портативных устройствах. На сегодняшний день широко используется в цифровых фотоаппаратах и видеокамерах, мобильных телефонах, смартфонах, электронных книгах, GPS-навигаторах и M5STACK.

    Рисунок 2. Карта памяти MicroSD

    Более подробная информация доступна на Wikipedia https://en.wikipedia.org/wiki/Secure_Digital

    Перечень компонентов для урока

    • M5STACK;
    • кабель USB-C из стандартного набора ;
    • карта памяти MicroSD на 4 ГБайт.

    Начнём!

    Шаг 1. Нарисуем картинки

    Нам потребуются изображения для визуализации процессов. Используйте любой удобный для Вас графический редактор. Мы будем использовать Paint (рис. 3, 3.1).

    Рисунок 3.

    Рисунок 3.1

    Аналогичным образом нарисуем значки: "вставить карту памяти", "просмотры", "сбой", "таймер" (рис. 3.2). Далее сделаем из них массивы пикселей при помощи конвертера (ссылка приведена ниже в разделе Скачать).

    Рисунок 3.2. Коллекция рисунков для проекта

    В дальнейшем подключим изображения к нашему новому проекту:

    extern unsigned char timer_logo[];
    extern unsigned char insertsd_logo[];
    extern unsigned char error_logo[];
    extern unsigned char wifi_logo[];
    extern unsigned char views_logo[];
    

    Шаг 2. Wi-Fi клиент

    Однажды, в одном из уроков мы научились поднимать Wi-Fi точку доступа с веб-сервером http://forum.m5stack.com/topic/60/lesson-3-1-wi-fi-access-point
    Отличительной особенностью сегодняшнего урока является режим работы Wi-Fi - мы будем использовать режим "клиент" (рис. 4).

    Рисунок 4. Wi-Fi в режиме клиента

    WiFi.begin(char* ssid, char* password);
    

    Шаг 3. Подготовка карты памяти к работе

    Инициализируем экземпляр класса SD, а заодно проверим - установлена ли карта памяти в слот или нет.

    if (!SD.begin())
    {
    	M5.Lcd.fillRoundRect(0, 0, 320, 240, 7, 0xffff);
    	M5.Lcd.drawBitmap(50, 70, 62, 115, (uint16_t *)insertsd_logo);
    	M5.Lcd.setCursor(130, 70);
    	M5.Lcd.print("INSERT");
    	M5.Lcd.setCursor(130, 90);
    	M5.Lcd.print("THE TF-CARD");
    	M5.Lcd.setCursor(130, 110);
    	M5.Lcd.print("AND TAP");
    	M5.Lcd.setCursor(130, 130);
    	M5.Lcd.setTextColor(0xe8e4);
    	M5.Lcd.print("POWER");
    	M5.Lcd.setTextColor(0x7bef);
    	M5.Lcd.print(" BUTTON"); 
    	while(true);
    }
    

    Шаг 4. Читаем файл с карты памяти

    Для того, чтобы прочесть данные с карты памяти необходимо вызвать метод open класса SD, предварительно передав ему в качестве аргумента char* с адресом файла.

    String TFReadFile(String path) {
    	File file = SD.open(strToChar(path));
    	String buf = "";
    	if (file)
    	{
        	while (file.available())
        	{
          		buf += (char)file.read();
        	}
        	file.close();
      	}
      	return buf;
    }
    

    Работать с указателями не совсем удобно для нас, поэтому мы напишем простенькую функцию для "конвертации" String в char*:

    char* strToChar(String str) {
    	int len = str.length() + 1;
    	char* buf = new char[len];
    	strcpy(buf, str.c_str());
    	return buf;
    }
    

    Функция TFReadFile принимает в качестве аргумента String адрес файла, пытается прочесть его и возвращает содержимое файла в виде String, если файл прочесть не получится, то функция вернёт пустую строку.

    Шаг 5. Пишем в файл на карту памяти

    Для того, чтобы произвести запись в файл необходимо открыть его дополнительно сообщив методу open аргумент FILE_WRITE, если метод вернёт true, то можно производить перезапись данных с помощью метода print класса File.

    bool TFWriteFile(String path, String str) {
    	File file = SD.open(strToChar(path), FILE_WRITE);
    	bool res = false;
    	if (file)
    	{
    		if (file.print(str)) res = true;
    	}
    	file.close();
    	return false;
    }
    

    Шаг 6. Сделаем настройку Wi-Fi с помощью wifi.ini

    Было бы неплохо, если записать в файл в каждую строку по известной Wi-Fi-сети (рис. 5, 5.1) и устройство смогло бы подключаться к первой доступной.


    Рисунок 5. Содержимое папки system

    Рисунок 5.1. wifi.ini

    Так и сделаем! Отслеживать состояние соединения во время подключения будем при помощи метода status класса WiFi. Напишем таймаут 10 секунд на одну сеть, думаю - будет достаточно:

    bool configWifi() {
    	/* Get WiFi SSID & password from wifi.ini from TF-card */
    	String file = TFReadFile("/system/wifi.ini");
    	if (file != "")
    	{	
    		for (int i = 0; i < cntChrs(file, '\n'); i++)
    		{
    			String wifi = parseString(i, '\n', file);
    			wifi = wifi.substring(0, (wifi.length() - 1)); // remove last char '\r'
    			String ssid = parseString(0, ' ', wifi);
    			String pswd = parseString(1, ' ', wifi);
    			char* ssid_ = strToChar(ssid);
    			char* pswd_ = strToChar(pswd);
    			if (WiFi.begin(ssid_, pswd_))
    			{
    				delay(10);
    				unsigned long timeout = 10000;
    				unsigned long previousMillis = millis();
    				while (true)
    				{
    					unsigned long currentMillis = millis();
    					if (currentMillis - previousMillis > timeout) break;
    				  	if (WiFi.status() == WL_CONNECTED) return true;
    					delay(100);
    				}
    			  }
    			}
      		}
      	return false;
    }
    

    Шаг 7. Посчитаем просмотры

    При каждом открытии любой страницы клиентом будем увеличивать счётчик просмотров на единицу.
    Для того, чтобы хранить счётчик автоматически создадим файл views в папке system. Почему без расширения файл? Он для устройства, а не для компьютера. Так будет лучше.

    int getViews() {
    	String file = TFReadFile("/system/views");
    	if (file != "") return file.toInt();
    	return -1;
    }
    
    bool increaseViews() {
    	int total = getViews();
    	if (total != -1)
    	{
    		total++;
    		if (TFWriteFile("/system/views", (String)(total))) return true;
    	}
    	else
    	{
    		if (TFWriteFile("/system/views", (String)(1))) return true;
    	}
    	return false;
    }
    

    Шаг 8. Принимаем клиентские запросы. Классика жанра

    Тут всё предельно просто! Скрипт и стили получим с карты памяти, икноку с внешнего сайта, а контент с *.md-файлов. Проще простого! Кстати, взгляните на функцию openPage.

    String openPage(String page) {
    	page += ".md";
    	String content = TFReadFile(page);
    	if (content != "")
    	{
    		increaseViews();
    		drawViews();
    		return content;
    	}
    	return "# 404 NOT FOUND #\n### MARKDOWN WEB SERVER ON M5STACK  ###"; // if not found 404
    }
    
    void loop() {
    	String currentString = "";
    	bool readyResponse = false;
    	WiFiClient client = server.available();
    	while (client.connected())
    	{
        	if (client.available())
        	{
          		char c = client.read();
         		if ((c != '\r') && (c != '\n'))
          			currentString += c;
          		else
            		readyResponse = true;
            	
    			if (readyResponse)
    	      	{
    				String GET = parseGET(currentString);
    				String mrkdwnContent = openPage(GET);
    				client.flush();
    				client.println("HTTP/1.1 200 OK");
    				client.println("Content-type:text/html");
    				client.println();
    				client.println("<html>");
    				client.println("<head>");
    				client.println("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"/>");
    				client.println("<title>Markdown page | M5STACK</title>");
    				client.println("<link rel=\"icon\" type=\"image/x-icon\" href=\"http://m5stack.com/favicon.ico\">");
    				client.println("<script type=\"text/javascript\">" + TFReadFile("/system/markdown.js") + "</script>");
    				client.println("<style type=\"text/css\">" + TFReadFile("/system/style.css") + "</style>"); 
    				client.println("</head>");
    				client.println("<body>");
    				client.println("<article></article>");
    				client.println("<script type=\"text/javascript\">");
    				client.println("const message = `" + mrkdwnContent + "`;");
    				client.println("const article = document.querySelector('article');");
    				client.println("article.innerHTML = markdown.toHTML(message);");
    				client.println("</script>");
    				client.println("</body>");
    				client.print("</html>");       
    				client.println();
    				client.println();
    				readyResponse = false;
    				currentString = "";
    				client.stop();
    	      	}
    		}
    	}
    }
    

    Шаг 9. Запускаем

    Отлично - работает! Карта памяти лежит на столе и ждёт когда её установят в слот, а устройство тем временем напоминает нам об этом (рис. 6).

    Рисунок 6. Вставьте карту памяти

    И наконец мы установили карту памяти и нажали на кнопку перезагрузки устройства. Ждём... В этот момент устройство ищет доступную известную беспроводную сеть и пытается к ней подключиться (рис. 6.1).

    Рисунок 6.1. Ждём...

    И вот оно! Заработало! Устройство показывает на своём дисплее адрес. Надо бы скорее подключиться! (рис. 6.2).

    Рисунок 6.2. Готов к работе! Первый запуск

    Страница 404, которая радует глаз (рис. 6.3). Не пугайтесь - мы ведь не делали index :)

    Рисунок 6.3. Страница 404

    А теперь откроем то, ради чего мы всё это делали - страница factorial.

    Обратите внимание: мы не используем расширение файла (*.md) в адресной строке браузера, см. функцию openPage (рис. 6.4).

    Рисунок 6.4. Прекрасно!

    Скачать