<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Урок 5. Карта памяти. Markdown веб-сервер]]></title><description><![CDATA[<h2>Цель урока</h2>
<p dir="auto">Сегодня мы научимся работать с TF-картой. Однажды в одном из уроков мы научились поднимать веб-сервер, но сегодня мы хотим больше, чем просто отдавать текст клиенту. Markdown. Облегчённый язык разметки, созданный с целью написания максимально читаемого и удобного для правки текста, но пригодного для преобразования в языки для продвинутых публикаций (HTML, Rich Text и других). Мы напишем Markdown Web-сервер на базе M5STACK (рис. 1).</p>
<p dir="auto"><img src="https://pp.userapi.com/c831508/v831508268/8749d/5i-Bmzdklr0.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 1.</p>
<p dir="auto">При включении устройства пользователю будет предложено вставить карту памяти. На карте памяти будет находится следующие файлы:</p>
<ul>
<li>файл с известными Wi-Fi-сетями (wifi.ini);</li>
<li>файл стилей (style.css);</li>
<li>JS-библиотека, обрабатывающая Markdown текст (markdown.js);</li>
<li>в корне карты памяти будут находиться пользовательские markdown-файлы (*.md), которые будут доступны клиентам (рис. 1.1).</li>
</ul>
<p dir="auto"><img src="https://sun1-4.userapi.com/c840634/v840634810/5c425/sLwjvmx4Swk.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 1.1. Markdown редактор</p>
<p dir="auto">После того, как пользователь вставит карту памяти необходимо перезагрузить устройство.<br />
При запуске устройство загрузит с карты памяти настройки Wi-Fi и попробует подключиться к первой доступной сети. Далее устройство отобразит на экране IP-адрес. Для того, чтобы просматривать веб-станицы необходимо использовать современный браузер с поддержкой JS и CSS.</p>
<h2>Краткая справка</h2>
<p dir="auto">MicroSD — формат карт памяти (рис. 2) предазначенный для использования в портативных устройствах. На сегодняшний день широко используется в цифровых фотоаппаратах и видеокамерах, мобильных телефонах, смартфонах, электронных книгах, GPS-навигаторах и M5STACK.</p>
<p dir="auto"><img src="https://pp.userapi.com/c834103/v834103268/c2c9f/GeOblJYGegE.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 2. Карта памяти MicroSD</p>
<p dir="auto">Более подробная информация доступна на Wikipedia <a href="https://en.wikipedia.org/wiki/Secure_Digital" title="https://en.wikipedia.org/wiki/Secure_Digital" target="_blank" rel="noopener noreferrer nofollow ugc">https://en.wikipedia.org/wiki/Secure_Digital</a></p>
<h2><strong>Перечень компонентов для урока</strong></h2>
<ul>
<li>M5STACK;</li>
<li>кабель USB-C из стандартного набора ;</li>
<li>карта памяти MicroSD на 4 ГБайт.</li>
</ul>
<h2>Начнём!</h2>
<h3>Шаг 1. Нарисуем картинки</h3>
<p dir="auto">Нам потребуются изображения для визуализации процессов. Используйте любой удобный для Вас графический редактор. Мы будем использовать Paint (рис. 3, 3.1).</p>
<p dir="auto"><img src="https://pp.userapi.com/c824601/v824601268/c410c/2vaLcTQRLcQ.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 3.</p>
<p dir="auto"><img src="https://pp.userapi.com/c841324/v841324268/71852/DAfN27NK6GA.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 3.1</p>
<p dir="auto">Аналогичным образом нарисуем значки: "вставить карту памяти", "просмотры", "сбой", "таймер" (рис. 3.2). Далее сделаем из них массивы пикселей при помощи конвертера (ссылка приведена ниже в разделе Скачать).</p>
<p dir="auto"><img src="https://pp.userapi.com/c840733/v840733248/5ddeb/EIp2-8NUZzU.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 3.2. Коллекция рисунков для проекта</p>
<p dir="auto">В дальнейшем подключим изображения к нашему новому проекту:</p>
<pre><code>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[];
</code></pre>
<h3>Шаг 2. Wi-Fi клиент</h3>
<p dir="auto">Однажды, в одном из уроков мы научились поднимать Wi-Fi точку доступа с веб-сервером <a href="http://forum.m5stack.com/topic/60/lesson-3-1-wi-fi-access-point" target="_blank" rel="noopener noreferrer nofollow ugc">http://forum.m5stack.com/topic/60/lesson-3-1-wi-fi-access-point</a><br />
Отличительной особенностью сегодняшнего урока является режим работы Wi-Fi - мы будем использовать режим "клиент" (рис. 4).</p>
<p dir="auto"><img src="https://pp.userapi.com/c841622/v841622248/71bbd/2Z3Dq5c35hw.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 4. Wi-Fi в режиме клиента</p>
<pre><code>WiFi.begin(char* ssid, char* password);
</code></pre>
<h3>Шаг 3. Подготовка карты памяти к работе</h3>
<p dir="auto">Инициализируем экземпляр класса SD, а заодно проверим - установлена ли карта памяти в слот или нет.</p>
<pre><code>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);
}
</code></pre>
<h3>Шаг 4. Читаем файл с карты памяти</h3>
<p dir="auto">Для того, чтобы прочесть данные с карты памяти необходимо вызвать метод open класса SD, предварительно передав ему в качестве аргумента char* с адресом файла.</p>
<pre><code>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;
}
</code></pre>
<p dir="auto">Работать с указателями не совсем удобно для нас, поэтому мы напишем простенькую функцию для "конвертации" String в char*:</p>
<pre><code>char* strToChar(String str) {
	int len = str.length() + 1;
	char* buf = new char[len];
	strcpy(buf, str.c_str());
	return buf;
}
</code></pre>
<p dir="auto">Функция TFReadFile принимает в качестве аргумента String адрес файла, пытается прочесть его и возвращает содержимое файла в виде String, если файл прочесть не получится, то функция вернёт пустую строку.</p>
<h3>Шаг 5. Пишем в файл на карту памяти</h3>
<p dir="auto">Для того, чтобы произвести запись в файл необходимо открыть его дополнительно сообщив методу open аргумент FILE_WRITE, если метод вернёт true, то можно производить перезапись данных с помощью метода print класса File.</p>
<pre><code>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;
}
</code></pre>
<h3>Шаг 6. Сделаем настройку Wi-Fi с помощью wifi.ini</h3>
<p dir="auto">Было бы неплохо, если записать в файл в каждую строку по известной Wi-Fi-сети (рис. 5, 5.1) и устройство смогло бы подключаться к первой доступной.</p>
<p dir="auto"><img src="https://pp.userapi.com/c841622/v841622513/70dad/0af0TS9a2-E.jpg" alt="" class=" img-fluid img-markdown" /><br />
Рисунок 5. Содержимое папки system</p>
<p dir="auto"><img src="https://pp.userapi.com/c841622/v841622513/70d70/vp5FGR7WNrU.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 5.1. wifi.ini</p>
<p dir="auto">Так и сделаем! Отслеживать состояние соединения во время подключения будем при помощи метода status класса WiFi. Напишем таймаут 10 секунд на одну сеть, думаю - будет достаточно:</p>
<pre><code>bool configWifi() {
	/* Get WiFi SSID &amp; password from wifi.ini from TF-card */
	String file = TFReadFile("/system/wifi.ini");
	if (file != "")
	{	
		for (int i = 0; i &lt; 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 &gt; timeout) break;
				  	if (WiFi.status() == WL_CONNECTED) return true;
					delay(100);
				}
			  }
			}
  		}
  	return false;
}
</code></pre>
<h3>Шаг 7. Посчитаем просмотры</h3>
<p dir="auto">При каждом открытии любой страницы клиентом будем увеличивать счётчик просмотров на единицу.<br />
Для того, чтобы хранить счётчик автоматически создадим файл views в папке system. Почему без расширения файл? Он для устройства, а не для компьютера. Так будет лучше.</p>
<pre><code>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;
}
</code></pre>
<h3>Шаг 8. Принимаем клиентские запросы. Классика жанра</h3>
<p dir="auto">Тут всё предельно просто! Скрипт и стили получим с карты памяти, икноку с внешнего сайта, а контент с *.md-файлов. Проще простого! Кстати, взгляните на функцию openPage.</p>
<pre><code>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') &amp;&amp; (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("&lt;html&gt;");
				client.println("&lt;head&gt;");
				client.println("&lt;meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"/&gt;");
				client.println("&lt;title&gt;Markdown page | M5STACK&lt;/title&gt;");
				client.println("&lt;link rel=\"icon\" type=\"image/x-icon\" href=\"http://m5stack.com/favicon.ico\"&gt;");
				client.println("&lt;script type=\"text/javascript\"&gt;" + TFReadFile("/system/markdown.js") + "&lt;/script&gt;");
				client.println("&lt;style type=\"text/css\"&gt;" + TFReadFile("/system/style.css") + "&lt;/style&gt;"); 
				client.println("&lt;/head&gt;");
				client.println("&lt;body&gt;");
				client.println("&lt;article&gt;&lt;/article&gt;");
				client.println("&lt;script type=\"text/javascript\"&gt;");
				client.println("const message = `" + mrkdwnContent + "`;");
				client.println("const article = document.querySelector('article');");
				client.println("article.innerHTML = markdown.toHTML(message);");
				client.println("&lt;/script&gt;");
				client.println("&lt;/body&gt;");
				client.print("&lt;/html&gt;");       
				client.println();
				client.println();
				readyResponse = false;
				currentString = "";
				client.stop();
	      	}
		}
	}
}
</code></pre>
<h3>Шаг 9. Запускаем</h3>
<p dir="auto">Отлично - работает! Карта памяти лежит на столе и ждёт когда её установят в слот, а устройство тем временем напоминает нам об этом (рис. 6).</p>
<p dir="auto"><img src="https://pp.userapi.com/c621509/v621509513/6e890/l2id5u0ooZ4.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 6. Вставьте карту памяти</p>
<p dir="auto">И наконец мы установили карту памяти и нажали на кнопку перезагрузки устройства. Ждём... В этот момент устройство ищет доступную известную беспроводную сеть и пытается к ней подключиться (рис. 6.1).</p>
<p dir="auto"><img src="https://pp.userapi.com/c824701/v824701513/c529f/d6BT35m8m0o.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 6.1. Ждём...</p>
<p dir="auto">И вот оно! Заработало! Устройство показывает на своём дисплее адрес. Надо бы скорее подключиться! (рис. 6.2).</p>
<p dir="auto"><img src="https://pp.userapi.com/c824701/v824701161/bac37/m-1SGGMNIIY.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 6.2. Готов к работе! Первый запуск</p>
<p dir="auto">Страница 404, которая радует глаз (рис. 6.3). Не пугайтесь - мы ведь не делали index :)</p>
<p dir="auto"><img src="https://sun1-4.userapi.com/c840634/v840634810/5c42f/F5rfHc2J7_g.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 6.3. Страница 404</p>
<p dir="auto">А теперь откроем то, ради чего мы всё это делали - страница factorial.</p>
<blockquote>
<p dir="auto">Обратите внимание: мы не используем расширение файла (*.md) в адресной строке браузера, см. функцию openPage (рис. 6.4).</p>
</blockquote>
<p dir="auto"><img src="https://sun1-4.userapi.com/c840634/v840634810/5c439/_lWq4my4GN4.jpg" alt="" class=" img-fluid img-markdown" /></p>
<p dir="auto">Рисунок 6.4. Прекрасно!</p>
<h2>Скачать</h2>
<ul>
<li>Файлы, которые необходимо скопировать в корень карты памяти: <a href="https://yadi.sk/d/I7YlKZT-3SdDfx" title="https://yadi.sk/d/I7YlKZT-3SdDfx" target="_blank" rel="noopener noreferrer nofollow ugc">https://yadi.sk/d/I7YlKZT-3SdDfx</a></li>
<li>Конвертер "изображение в массив" для разрешения 59x59 px: <a href="https://yadi.sk/d/Y0w1r1hR3SdTu7" title="https://yadi.sk/d/Y0w1r1hR3SdTu7" target="_blank" rel="noopener noreferrer nofollow ugc">https://yadi.sk/d/Y0w1r1hR3SdTu7</a></li>
<li>Скетч: <a href="https://yadi.sk/d/SCktJBQm3SdD9m" title="https://yadi.sk/d/SCktJBQm3SdD9m" target="_blank" rel="noopener noreferrer nofollow ugc">https://yadi.sk/d/SCktJBQm3SdD9m</a></li>
</ul>
]]></description><link>https://community.m5stack.com/topic/109/урок-5-карта-памяти-markdown-веб-сервер</link><generator>RSS for Node</generator><lastBuildDate>Tue, 10 Mar 2026 22:24:19 GMT</lastBuildDate><atom:link href="https://community.m5stack.com/topic/109.rss" rel="self" type="application/rss+xml"/><pubDate>Thu, 22 Feb 2018 07:18:14 GMT</pubDate><ttl>60</ttl></channel></rss>