آموزش Espآموزش ESP32آموزش اینترنت اشیاپروژه های Esp

تصویربرداری دوربین حرارتی با برد ESP32

دوربین های حرارتی یه طور گسترده در طیف عظیمی از کاربردهای صنعتی مانند مانیتورینگ و نظارت برعملکرد حرارتی، تشخیص ناهنجاری های دمایی، بنچ مارکینگ یا الگوبرداری دمایی و غیره استفاده میشوند. از تصویربرداری حرارتی حتی در کاربردهای نظامی و دفاعی نیز بهره میگیریم. اما رایج ترین و معمول ترین کاربرد آن در صنعت تعمیرات الکترونیک است. در صنعت تعمیرات، دوربین های تصویربرداری حرارتی یک ابزار بسیار مهم به شمار می آیند که میتوانند پروسه دیباگ یا اشکال زدایی سخت افزاری را مخصوصا در مواقعی که قطعات کوتاه تر دخیل هستند، آسان کنند. اما مشکل و نگرانی اصلی درباره دوربین های تصویربرداری حرارتی این است که آن ها بسیار گران هستند. در این پروژه، برای این مشکل راه حلی پیدا میکنیم. در واقع، تصویربردار حرارتی خود را با قطعات دردسترس و موجود در بازار و با قیمتی بسیار ارزان تر میسازیم. سنسورهای تصویربرداری ارزان تری که برای این منظور پیدا کرده ایم، سنسور AMG8833 از پاناسونیک و MLX90640  و MLX90641 از برند ملکسیس هستند.

در بین این سنسورها، AMG8833 از همه ارزان تر بوده و درعوض رزولوشن ( وضوح صفحه نمایش) تنها 8*8 را به ما میدهد. درحالیکه MLX90640 رزولوشن 24*32 و MLX90641 رزولوشن 12*16 را ارائه میدهند. از آنجایی که در این محدوده قیمت سنسور MLX90640 بهترین رزولوشن را دارد، برای پروژه خود از آن استفاده میکنیم.

ویژگی های دوربین تصویربرداری حرارتی

  1. رزولوشن سنسور تصویربرداری 24*32
  2. میدان دید سنسور: 55 درجه در 35 درجه
  3. محدوده اندازاه گیری دما: 40- درجه سلسیوس تا 300 درجه سلسیوس
  4. محدوده دمای کاری: 40- درجه سلسیوس تا 85 درجه سلسیوس
  5. نرخ تازه سازی یا نرخ تجدید قابل تنظیم : 4 تا 32 هرتز
  6. 10 پالت رنگی متفاوت
  7. پنج مد درون یابی متفاوت
  8. استفاده آسان از GUI
  9. نمایشگر TFT4 اینچی با رزولوشن 240*320
  10. قابلیت ذخیره تصویر حرارتی بر روی کارت حافظه
  11. دارای باتری و مدار شارژ داخلی
  12. متن باز یا open source بودن

قطعات لازم برای ساخت تصویربردار حرارتی

قطعات لازم برای ساخت این دوربین تصویربردار حرارتی در لیست پایین ارائه شده است. مقدار و حالت هر قطعه را میتوانید در شماتیک یا BOM (لیست مواد و قطعات تولید این محصول) بیابید.

  1. ماژول ESP32 Wrover با حافظه فلش 8 مگابایت و PSRAM – 1 عدد
  2. سنسور آرایه دوربین تصویربردار حرارتی MLX90640 IR Array– 1 عدد
  3. نمایشگر TFT4 اینچی با رزولوشن 240*320 و راه انداز ILI9341- 1 عدد
  4. تراشه مبدل USB به UART (سریال) CH340K – 1 عدد
  5. تراشه شارژ لیتیوم-یون TP4056 – 1 عدد
  6. میکروچیپ MIC5219-3.3YM5 LDO با ولتاژ خروجی 3.3 ولت – 1 عدد
  7. ماسفت کانال p A03401- 1 عدد
  8. ماسفت کانال n دوبل 2N7002DW – 1 عدد
  9. ترانزیستور S8050 – ا عدد
  10. دیود SS34 – 1 عدد
  11. رم ریدر – 1 عدد
  12. کانکتور USB تایپ C با 16 پایه – 1 عدد
  13. مقاومت ها و خازن های SMD
  14. LED های SMD
  15. کلیدهای لمسی SMD
  16. کلید کشویی SMD
  17. کانکتور یا رابط الکترونیک
  18. PCB مخصوص پروژه
  19. محفظه و پیچ های نصب چاپ سه بعدی
  20. دیگر ابزار و مواد مصرفی موردنیاز

مدار اتصال دوربین تصویربرداری حرارتی ESP32

مدار اتصال کامل دوربین تصویربردار حرارتی در تصویر پایین نشان داده است که میتوانید آن را در قالب یک فایل PDF نیز دانلود کنید.

مدار اتصال دوربین تصویربرداری حرارتی ESP32

بیایید برای درک بهتر، شماتیک پروژه را بخش به بخش توضیح دهیم و درباره آن ها بحث کنیم.

از یک پورت USB تایپ C برای شارژ و همچنین مقاصد کدنویسی استفاده کرده ایم. تغذیه را از USB به مدار کنترل مسیر تغذیه که در واقع همان مداری است که حول ماسفت کانال p (U2) و دیود (D1) بسته شده است، وصل میکنیم. هنگامی که تغذیه USB در دسترس است، دستگاه از طریق آن تغذیه شده و باتری داخلی را هم شارژ میکند. هنگامی که تغذیه USB قطع شده، دستگاه به طور اتوماتیک و خودکار از باتری داخلی خود استفاده میکند. برای رگوله و تنظیم کردن ولتاژ، از میکروچیپ MIC5219 LDO با ولتاژ خروجی 3.3 ولت استفاده کردیم. این قطعه میتواند تا جریان 500 میلی آمپر و با افت ولتاژ بسیار پایین 500 میلی ولت را در حالت بار کامل تامین کند. یک کلید کشویی با مقاومت پول آپ به پایه enable میکروچیپ MIC5219 متصل است. این کلید برای روشن و خاموش کردن تصویربردار حرارتی استفاده میشود. وقتی این پایه زمین میشود، LDO و درنتیجه دیگر بخش های مدار به جز بخش شارژ باتری خاموش میشوند. برای شارژ باتری داخلی از ماژول شارژ باتری TP4056 استفاده میکنیم که میتواند جریان شارژ حداکثر 1 آمپر را تامین کند.

از یک تقسم کننده ولتاژ کلاسیک برای سنجش ولتاژ باتری استفاده کرده ایم تا آن را برای ادازه گیری تا یک سطح امن کاهش دهد.

مدار تغذیه ESP32 دوربین حرارتی

در قسمت بعدی، خود ESP32 SoC  را به همراه مدار برنامه نویسی و سنسور دوربین تصویربرداری حرارتی MLX90640 IR Array داریم. مدار برنامه نویسی شامل یک مبدل یا کنترلر USB به سریال CH340K به همراه یک ماسفت کانال n دوبل 2N7002DW از برند ON Semi است. ما CH340K را به دلیل کوچکی و قیمت پایینش انتخاب کرده ایم. ماسفت دوبل کوچک، به عنوان یک مدار ریست خودکار برای ESP32 عمل میکند. درنتیجه حین کار نیازی نیست به صورت دستی مدار را ریست کرده یا کلید بوت را فشار دهیم. اگرچه که مدار ریست اتوماتیک، به خوبی و بدون اشکال عمل میکند، اما همچنان محض احتیاط دکمه های بوت و ریست را در مدار درنظر میگیریم. مداری که به ESP32 متصل میشود استاندارد است. شما تنها کافی است مقاومت های پول آپ و خازن های بای پس را متصل کنید.

برای اینکه دستگاه کل را فشرده سازیم، تصمیم گرفتیم که سنسور تصویربردارر را مستقیما به PCB لحیم کنیم. سنسور از طریق پایه های I2C به ESP32 متصل شده و تنها 4 پایه از جمله پایه تغذیه و پایه زمین دارد. اگر بخواهیم به جای لحیم کردن سنسور به PCB از ماژول سنسور تصویربردار استفاده کرده یا دستگاه I2C دیگری را به مدار اضافه کنیم، میتوانیم یک کانکتور I2C دیگر به PCB اضافه کنیم.

تصویر برداری حرارتی با ESP32

در بخش پایانی، نمایشگر TFT را به همراه دکمه های ناوبری و اسلات کارت حافظه  MICRO SDداریم. از یک نمایشگر TFT 2.4 اینچی با رزولوشن 240*320 استفاده کرده ایم. نمایشگر از تراشه راه انداز ILI9341 استفاده میکند که از طریق ارتباط SPI به ESP32 متصل شده و ارتباط SPI را تا سرعت 65 مگاهرتز پشتیبانی میکند. نمایشگر نیز مستقیما به PCB لحیم میشود.

برای کنترل نور پس زمینه، از یک ترانزیستور S8050 استفاده کرده ایم. به کمک سیگنال های PWM میتوانیم روشنایی پس زمینه را کنترل کنیم. نمایشگر به  VSPI میکروکنترلر ESP32 متصل شده درحالیکه اسلات کارت حافظه به HSPI  آن. (VSPI و HSPI برای ارتباط میکروکنترلر ESP32 با دستگاه های دیگر استفاده میشوند.)

با اینکاراطمینان حاصل میکنیم که ESP32 در صورت نیاز میتواند به صورت همزمان هم به نمایشگر و هم به کارت حافظه دسترسی پیدا کرده و آن ها را کنترل کند.

اگر در مورد این مطلب سوالی دارید در قسمت نظرات بپرسید

نمایشگر تصویر حرارتی با برد ESP32

PCB دوربین تصویربرداری حرارتی

تصمیم گرفتیم که برای این پروژه، PCB مخصوص خودمان را بسازیم تا مطمئن شویم که محصول نهایی به اندازه کافی فشرده و جمع و جور شده و همچنین مونتاژ و استفاده از آن نیز آسان است. ابعاد PCB، 50*50 میلی متر است. در تصاویر پایین لایه بالایی و لایه پایینی PCB را مشاهده میکنید.

PCB دوربین تصویربرداری حرارتی

و در اینجا تصویری از نمای سه بعدی PCB داریم.

PCB دوربین تصویربرداری حرارتی

در تصویر پایین هم، میتوانید PCB را بعد از مونتاژ کامل قطعات مشاهده کنید.

PCB دوربین تصویربرداری حرارتی

قطعات چاپ سه بعدی

ما یک محفظه برای دوربین تصویربردار حرارتی طراحی کرده ایم که ظاهری جذاب دارد. فایل های مربوط به تمام قطعات چاپ سه بعدی به همراه کد آردوینو و فایل بیت مپ (.bmp) را میتوانید از طریق لینک GitHub موجود در انتهای مطلب دانلود کنید.

قطعات چاپ سه بعدی

در تصویر بالا، دوربین تصویربرداری حرارتی را بعد از مونتاژ کامل مشاهده میکنید.

قطعات چاپ سه بعدی

بررسی اجمالی دوربین حرارتی

تصویر پایین یک دید کلی از دوربین تصویربرداری حرارتی بعد از مونتاژ کامل به شما میدهد.

بررسی اجمالی دوربین حرارتی

کاربرد دکمه های مختلف دوربین در اینجا شرح داده شده است.

کلید بالا/ + :

  • حالت پیش فرض : تغییر یا چرخش از طریق مدهای مختلف دورن یابی
  • در تنظیمات : تغییر مقدار انتخاب شده

کلید میانی/ OK:

  • حالت پیش فرض : اگر کلید به مدت کوتاه فشرده شود، تصاویر در کارت حافظه ذخیره میشوند و اگر کلید به مدت طولانی فشرده شود، به منو تنظیمات وارد میشویم.
  • در تنظیمات : اگر کلید مدت کوتاهی فشرده شود، گزینه انتخاب شده در منو تغییر کرده و اگر طولانی مدت فشرده شود از منو تنظیمات خارج میشویم.

کلید پایین/- :

  • حالت پیش فرض : تغییر پالت رنگی
  • در تنظیمات: تغییر مقدار انتخاب شده

همانگونه که در بالا اشاره شد، میتوانید با استفاده از کلیدهای بالا و یا پایین به ترتیب مدهای دورن یابی و پالت های رنگی را درحالیکه در صفحه اصلی هستید، تغییر دهید.

میتوانید تصویر نمایشگر را با فشردن کلید میانی به مدت کوتاه در قالب یک فایل BMP در کارت حافظه ذخیره کنید. اگر تصویر با موفقیت ذخیره شده باشد، انیمیشن ذخیره موفق اجرا شده و اگر خطایی در ذخیره رخ داده باشد، برای مثال کارت حافظه وارد نشده باشد یا پشتیبانی نشود، یک پیام انیمیشنی مبنی بر خطای کارت حافظه را خواهیم دید. اگر کارت حافظه را هنگامی که دوربین روشن است، وارد دستگاه کرده اید، مطمئن شوید که کارت حافظه را به FAT32 فرمت کرده و سپس دوربین را خاموش و روشن کنید. اگر پس از وارد کردن کارت حافظه عمل ریبوت یا بازراه اندازی را انجام ندهید، ممکن است دوربین قادر به شناسایی آن نباشد. پس کارت حافظه را یا هنگامی که دوربین خاموش است وارد کرده و یا دستگاه را ریبوت کنید.

در تصویر پایین، تصویر صفحه اصلی را مشاهده میکنید. در صفحه نمایش اصلی، تصویر حرارتی را به همراه آیکون باتری و دمای حداکثر، حداقل و دمای نقطه میانی را میبینید.

دوربین حرارتی ESP32

تصویر پایین، صفحه تنظیمات دوربین تصویربردار حرارتی را نشان میدهد. در منو تنظیمات 7 گزینه داریم. گزینه انتخاب شده در منو به رنگ سبز، درحالیکه دیگر گزینه ها به رنگ سفید دیده میشوند. میتوانید گزینه انتخاب شده را با فشردن کوتاه کلید میانی تغییر دهید. همچنین مقدار گزینه انتخاب شده با کلیدهای بالا/+ یا پایین/- تنظیم میشود.

تنظیمات دوربین حرارتی با ESP32

در اینجا گزینه های منو تنظیمات و مقادیر هر کدام را توضیح داده ایم.

Auto Scale:

  • ON: حالت Auto Scale را فعال میکند و رنگ متناسب با دمای حداقل و حداکثری که سنسور خوانده است را نشان میدهد.
  • OFF : حالت Auto Scale را غیرفعال میکند و رنگ متناسب با دمای حداقل و حداکثری که به صورت دستی تنظیم شده را نشان میدهد.

Min Temperature : برای تعیین دمای حداقل به صورت دستی وقتی حالت Auto Scale غیرفعال است.

Max Temperature : برای تعیین دمای حداکثر به صورت دستی وقتی حالت Auto Scale غیرفعال است.

Interpolation : روش درون یابی. این گزینه به الگوریتم درون یابی که برای افزایش رزولوشن تصویر استفاده میشود اشاره دارد. در واقع با این روش، تصویر خروجی سنسور با رزولوشن پایین 24*32 را به تصویر با رزولوشن بالا 240*320 که در نهایت روی صفحه نمایش نشان داده خواهد شد، تبدیل میکند. کیفیت تصویر و زمان پردازش آن متناسب با روش درون یابی انتخاب شده تغییر میکند.

روش های درون یابی پشتیبانی شده در این دستگاه در یک لیست نام برده شده اند:

  1. درون یابی نزدیک ترین همسایه یا Nearest Neighbor
  2. درون یابی با میانگین گیری یا Average
  3. درون یابی دوخطی یا Bilinear
  4. درون یابی دوخطی سریع Bilinear Fast
  5. درون یابی مثلثی یا Triangle

Palette : پالت های رنگی اشاره به حالت ها یا مدهای مختلف نمایش دارند. در این دستگاه 10 پالت رنگی متفاوتی موجود است که میتوان از میان آن ها پالت موردنظر را انتخاب کرد.

Refresh Rate : نرخ تازه سازی یا تجدید سنسور تصویربردار. سنسور MLX90640 دارای نرخ تازه سازی 0.5 تا 64 هرتز است. اما هنگام آزمایش، نرخ های 4، 8، 16 و 32 هرتز پراستفاده ترین نرخ ها بودند پس ما هم در دوربین خود، تنها از این نرخ های تازه سازی استفاده میکنیم.

Backlight : روشنایی نور پس زمینه که میتواند از 10% تا 100% تنظیم شود

کد آردوینو دوربین تصویربردار حرارتی

حال بیایید نگاهی به کد آردوینو بیندازیم. طبق معمول، ابتدا تمام کتابخانه های لازم برای استفاده از توابع خاصی را در کد فراخوانی کرده ایم. این کتابخانه ها، TFT_eSPI، Adafruit_MLX90640، Preferences، AnimatedGIF و دیگر کتابخانه های استاندارد هستند. همچنین، داده تصاویر متحرک به همراه فایل های فونت را هم فراخوانی کرده ایم. میتوانید تمام فایل های ضروری را از لینک مخرن GitHub که در انتهای همین مطلب قرار داده شده است، دانلود کنید.

سپس تمام متغیرهای سراسری را تعریف کرده و کمی بعدتر، برای هر کدام نمونه هایی ساخته ایم. از این نمونه ها برای دسترسی به تابع مناسب استفاده میکنیم.

#include <Preferences.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include "FS.h"
#include <Adafruit_MLX90640.h>
#include <TFT_eSPI.h>
#include <AnimatedGIF.h>
#include <Fonts/GFXFF/gfxfont.h>  //Include a library of Fonts
#include "Open_Sans_ExtraBold_10.h"
#include "BootAnimation.h"
#include "success.h"
#include "Error.h"
SPIClass spiSD(HSPI);
#define SD_CS 15
//#define USE_DMA
#define NORMAL_SPEED
#define TFT_WIDTH 320
#define TFT_HEIGHT 240
#define BUFFER_SIZE 320
uint16_t usTemp[BUFFER_SIZE];
#define VBAT_PIN 33
#define BATTV_MAX 4.2  // maximum voltage of battery
#define BATTV_MIN 3.2  // what we regard as an empty battery
#define GIF_IMAGE BootAnimationIMG
#define SGIF_IMAGE success_GIF
#define EGIF_IMAGE Error_GIF
bool dmaBuf = 0;
Adafruit_MLX90640 mlx;
Preferences preferences;
AnimatedGIF gif;
TFT_eSPI tft = TFT_eSPI();
File bmpFile;
float frame[32 * 24];
float batv;
const int upButton = 35;
const int middleButton = 36;
const int downButton = 39;
const int backlightPin = 4;
int interpolationMode = 0;
int AutoScale = 0;
volatile bool MenuChange = false;
volatile bool _upShort = false;
volatile bool _downShort = false;
volatile bool middleShort = false;
volatile bool middlePressed = false;
unsigned long middlePressStartTime = 0;
int Menu = 0, Menuitem = 0;
int BLPWM = 0;
int RefreshRate = 0;
float MinT, MaxT;
String menuItems[7] = { "Auto Scale    : ", "Min Temp      : ", "Max Temp      : ", "Interpolation : ", "Palette       : ", "Refresh Rate  : ", "BackLight     : " };
String ASindex[2] = { "Off", "On" };
String IPindex[6] = { "Nearest Neighbor", "Average", "Bilinear", "Bilinear Fast", "Triangle" };
String RRindex[4] = { "4 Hz", "8 Hz", "16 Hz", "32 Hz" };
float xRatios[320];
float yRatios[240];
float xOppositeRatios[320];
float yOppositeRatios[240];
#define PALETTE_COUNT 10
uint16_t colorPalettes[PALETTE_COUNT][6] = {
  { TFT_BLUE, TFT_CYAN, TFT_GREEN, TFT_YELLOW, TFT_RED, TFT_MAGENTA },
  { TFT_BLACK, TFT_DARKGREY, TFT_LIGHTGREY, TFT_WHITE, TFT_ORANGE, TFT_PINK },
  { TFT_NAVY, TFT_OLIVE, TFT_DARKGREEN, TFT_DARKCYAN, TFT_MAROON, TFT_PURPLE },
  { TFT_BLUE, TFT_GREEN, TFT_DARKGREEN, TFT_ORANGE, TFT_MAROON, TFT_RED },
  { TFT_NAVY, TFT_DARKGREEN, TFT_GREEN, TFT_YELLOW, TFT_ORANGE, TFT_RED },
  { TFT_CYAN, TFT_BLUE, TFT_MAGENTA, TFT_YELLOW, TFT_GREEN, TFT_RED },
  { TFT_WHITE, TFT_ORANGE, TFT_RED, TFT_BLUE, TFT_GREEN, TFT_BLACK },
  { TFT_PURPLE, TFT_MAGENTA, TFT_RED, TFT_ORANGE, TFT_YELLOW, TFT_GREEN },
  { TFT_YELLOW, TFT_PINK, TFT_WHITE, TFT_BLUE, TFT_DARKCYAN, TFT_DARKGREEN },
  { TFT_RED, TFT_YELLOW, TFT_GREEN, TFT_CYAN, TFT_BLUE, TFT_MAGENTA }
};
int paletteIndex = 0;
float tempMin = 20.0;  // Minimum temperature
float tempMax = 32.0;  // Maximum temperature
TFT_eSprite sprite = TFT_eSprite(&tft);

توابع upButton_ISR، downButton_ISR و middleButton_ISR برای تشخیص فشرده شدن کلیدها و پردازش آن ها هستند. این توابع از وقفه های سخت افزاری برای تشخیص تغییرات در پایه هایی که به کلیدها متصل هستند، استفاده میکنند. از دیبونس نرم افزاری برای تشخیص درست فشرده شدن کلیدها و همچنین اجتناب از هر نویزی بهره میگیریم. تابع براساس کلیدی که فشار داده شده و مدت زمان فشرده شدن آن، متغیر مناسب ( که در برنامه اصلی پردازش میشود) را true میکند.

void IRAM_ATTR upButton_ISR() {
  static unsigned long last_interrupt_time = 0;
  unsigned long interrupt_time = millis();
  if (interrupt_time - last_interrupt_time > 200) {  // simple debounce
    _upShort = true;
  }
  last_interrupt_time = interrupt_time;
}
void IRAM_ATTR downButton_ISR() {
  static unsigned long last_interrupt_time = 0;
  unsigned long interrupt_time = millis();
  if (interrupt_time - last_interrupt_time > 200) {  // simple debounce
    _downShort = true;
  }
  last_interrupt_time = interrupt_time;
}
void IRAM_ATTR middleButton_ISR() {
  if (digitalRead(middleButton) == LOW) {
    // Button press event
    if (!middlePressed) {  // If button was not already being pressed
      middlePressed = true;
      middlePressStartTime = millis();  // Save the start time of button press
    }
  } else {
    // Button release event
    if (middlePressed) {                             // If the button was being pressed
      if (millis() - middlePressStartTime < 1000) {  // If the button was pressed for less than 1 second
        middleShort = true;
      }
      middlePressed = false;
      middlePressStartTime = 0;  // Reset the start time of button press
    }
  }
}

توابع ConfigRefreshrate  وMLXInit و WriteCongif  و ReadConfig برای برقراری ارتباط و همچنین پیکربندی سنسور MLX90640 استفاده میشوند. تابع MLXInit برای راه اندازی سنسور در ابتدای کار و تابع ConfigRereshrate همانطور که از نامش پیداست، برای تنظیم نرخ تازه سازی سنسور تصویربردار حرارتی استفاده میشود. توابع دیگر نیز برای خواندن و نوشتن پیکربندی های انجام شده در رجیسترهای مربوطه  MLX90640 به کار گرفته میشوند.

void MLXInit() {
  Wire.begin();
  Wire.setClock(1000000);
  if (!mlx.begin()) {
    Serial.println("Failed to initialize MLX90640!");
  }
  mlx.setResolution(MLX90640_ADC_18BIT);
  ConfigRefreshrate();
}
void ConfigRefreshrate() {
  switch (RefreshRate) {
    case 1:
      mlx.setRefreshRate(MLX90640_4_HZ);
      break;
    case 2:
      mlx.setRefreshRate(MLX90640_8_HZ);
      break;
    case 3:
      mlx.setRefreshRate(MLX90640_16_HZ);
      break;
    case 4:
      mlx.setRefreshRate(MLX90640_32_HZ);
      break;
    default:
      break;
  }
}
void ReadConfig() {
  preferences.begin("Config", false);
  String temp;
  temp = preferences.getString("AutoScale", "");
  AutoScale = temp.toInt();
  temp = preferences.getString("MinTe", "");
  MinT = temp.toFloat();
  temp = preferences.getString("MaxTe", "");
  MaxT = temp.toFloat();
  temp = preferences.getString("Imode", "");
  interpolationMode = temp.toInt();
  temp = preferences.getString("PIndex", "");
  paletteIndex = temp.toInt();
  temp = preferences.getString("RefreshRate", "");
  RefreshRate = temp.toInt();
  temp = preferences.getString("BLPWM", "");
  BLPWM = temp.toInt();
  preferences.end();
}
void WriteConfig() {
  preferences.begin("Config", false);
  preferences.putString("AutoScale", String(AutoScale));
  preferences.putString("MinTe", String(MinT));
  preferences.putString("MaxTe", String(MaxT));
  preferences.putString("Imode", String(interpolationMode));
  preferences.putString("PIndex", String(paletteIndex));
  preferences.putString("RefreshRate", String(RefreshRate));
  preferences.putString("BLPWM", String(BLPWM));
  preferences.end();
}

تابع initSDcard برای راه اندازی کارت حافظه و همچنین تشخیص وجود آن در دستگاه پیش از ذخیره تصویر استفاده میشود. اگر کارت حافظه تشخیص داده نشد و یا پشتیبانی نشد، تابع، خطای مناسب را برمیگرداند.

void initSDcard() {
  //spiSD.begin(14, 12, 13, SD_CS);  //CLK,MISO,MOIS,SS
  if (!SD.begin(SD_CS, spiSD)) {
    Serial.println("Card Mount Failed");
    return;
  } else {
    Serial.println("Card Mount Successful");
  }
  uint8_t cardType = SD.cardType();
  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }
}

تابع generateFilename برای تولید نام تصاویر استفاده میشود. این تابع نام تصاویر را به صورت افزایشی تولید میکند. یعنی  در کارت حافظه به دنبال فایل هایی میگردد که درگذشته با همین نام ذخیره شده باشند. در صورت یافتن فایلی که پیش از این با همین نام در کارت حافظه ذخیره شده، یک نام جدید را به همراه عدد افزایش یافته تولید میکند. برای مثال اگر پیش از این تصویری به نام filename1 ذخیره کرده بودیم، این بار تصویر جدید را با نام filename2 ذخیره خواهد کرد. هنگامی که دکمه ذخیره فشار داده میشود، تابع writeBMP فایل را در کارت حافظه مینویسد.

این تابع یک تصویر در قالب BMP  تولید میکند که به هدر داده نیز نیاز دارد. تابع، بافر صفحه را در این قسمت ذخیره کرده و به این شکل تصویر صفحه نمایش را در کارت حافظه ذخیره میکند.

String generateFilename(fs::FS &fs) {
  for (int i = 0; i <= 9999; i++) {
    char filename[23];                               // Allocate the char array
    sprintf(filename, "/ThermalCamera%04d.bmp", i);  // Print formatted string into char array
    if (!fs.exists(filename)) {
      return String(filename);  // Return a String object
    }
  }
  return "";  // return empty string if all filenames are taken
}
int writeBMP(fs::FS &fs, const char *path, TFT_eSprite *sprite) {
  const int width = sprite->width();
  const int height = sprite->height();
  // BMP file header (14 bytes)
  uint8_t bmpFileHeader[14] = { 'B', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0 };
  // The size of the BMP file in bytes
  uint32_t fileSize = 54 + width * height * 2;
  bmpFileHeader[2] = (uint8_t)(fileSize);
  bmpFileHeader[3] = (uint8_t)(fileSize >> 8);
  bmpFileHeader[4] = (uint8_t)(fileSize >> 16);
  bmpFileHeader[5] = (uint8_t)(fileSize >> 24);
  // BMP info header (40 bytes)
  uint8_t bmpInfoHeader[40] = { 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 16, 0 };
  bmpInfoHeader[4] = (uint8_t)(width);
  bmpInfoHeader[5] = (uint8_t)(width >> 8);
  bmpInfoHeader[6] = (uint8_t)(width >> 16);
  bmpInfoHeader[7] = (uint8_t)(width >> 24);
  bmpInfoHeader[8] = (uint8_t)(height);
  bmpInfoHeader[9] = (uint8_t)(height >> 8);
  bmpInfoHeader[10] = (uint8_t)(height >> 16);
  bmpInfoHeader[11] = (uint8_t)(height >> 24);
  File file = fs.open(path, FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file for writing");
    return 0;
  } else {
    //showSavingImageMessage();
    // Write headers
    file.write(bmpFileHeader, 14);
    file.write(bmpInfoHeader, 40);
    // Write pixel data
    for (int y = height - 1; y >= 0; y--) {  // BMP is stored bottom-top
      for (int x = 0; x < width; x++) {
        uint16_t pixel = sprite->readPixel(x, y);
        // Swap red and green channels
        uint16_t r = (pixel >> 11) & 0x1F;
        uint16_t g = (pixel >> 5) & 0x3F;
        uint16_t b = pixel & 0x1F;
        pixel = (b << 11) | (r << 5) | g;
        file.write(pixel >> 8);  // high byte
        file.write(pixel);       // low byte
      }
    }
    file.close();
    return 1;
  }
}

تابع navigationUpdate مسئول تمام روتین های مربوط به کلیدهای لمسی و منو ناوبری است. این تابع فشار کلیدها را براساس اینکه در صفحه اصلی هستیم یا منو تنظیمات، پردازش میکند. (همانطور که پیش تر دیدیم، هر کلید باتوجه به اینکه در کدام صفحه قرار داشته باشیم، کاربرد متفاوتی از خود نشان خواهد داد.)

void navigationUpdate() {
  if (_upShort == true) {
    _upShort = false;
    if (Menu == 0) {
      interpolationMode++;
      if (interpolationMode > 4) interpolationMode = 0;
    } else {
      if (Menuitem == 0) {
        AutoScale++;
        if (AutoScale > 1) AutoScale = 1;
      } else if (Menuitem == 1) {
        MinT++;
        if (MinT > 300) MinT = 300;
      } else if (Menuitem == 2) {
        MaxT++;
        if (MaxT > 300) MaxT = 300;
      } else if (Menuitem == 3) {
        interpolationMode++;
        if (interpolationMode > 4) interpolationMode = 4;
      } else if (Menuitem == 4) {
        paletteIndex++;
        if (paletteIndex > 9) paletteIndex = 9;
      } else if (Menuitem == 5) {
        RefreshRate++;
        if (RefreshRate > 3) RefreshRate = 3;
        ConfigRefreshrate();
      } else if (Menuitem == 6) {
        BLPWM = BLPWM + 10;
        if (BLPWM > 100) BLPWM = 100;
        analogWrite(backlightPin, map(BLPWM, 0, 100, 0, 255));  // Turn on backlight
      }
      MenuChange = true;
    }
    WriteConfig();
    Serial.println("Up Short Press");
  }
  if (_downShort == true) {
    _downShort = false;
    if (Menu == 0) {
      paletteIndex = (paletteIndex + 1) % PALETTE_COUNT;
    } else {
      if (Menuitem == 0) {
        AutoScale--;
        if (AutoScale < 0) AutoScale = 0;
      } else if (Menuitem == 1) {
        MinT--;
        if (MinT < 0) MinT = 0;
      } else if (Menuitem == 2) {
        MaxT--;
        if (MaxT < 5) MaxT = 5;
      } else if (Menuitem == 3) {
        interpolationMode--;
        if (interpolationMode < 0) interpolationMode = 0;
      } else if (Menuitem == 4) {
        paletteIndex--;
        if (paletteIndex < 0) paletteIndex = 0;
      } else if (Menuitem == 5) {
        RefreshRate--;
        if (RefreshRate < 0) RefreshRate = 0;
        ConfigRefreshrate();
      } else if (Menuitem == 6) {
        BLPWM = BLPWM - 10;
        if (BLPWM < 10) BLPWM = 10;
        analogWrite(backlightPin, map(BLPWM, 0, 100, 0, 255));  // Turn on backlight
      }
      MenuChange = true;
    }
    WriteConfig();
    Serial.println("down Short Press");
  }
  if (middleShort == true) {
    middleShort = false;
    if (Menu == 0) {
      if (!SD.begin(SD_CS, spiSD)) {
        Serial.println("Card Mount Failed");
        if (gif.open((uint8_t *)EGIF_IMAGE, sizeof(EGIF_IMAGE), GIFDraw1)) {
          Serial.printf("Successfully opened GIF; Canvas size = %d x %d\n", gif.getCanvasWidth(), gif.getCanvasHeight());
          while (gif.playFrame(true, NULL)) {
            sprite.pushSprite(0, 0);  // Push the sprite to screen after every frame
            yield();
          }
          gif.close();
        }
        return;
      } else {
        Serial.println("Card Mount Successful");
      }
      uint8_t cardType = SD.cardType();
      if (cardType == CARD_NONE) {
        Serial.println("No SD card attached");
        if (gif.open((uint8_t *)EGIF_IMAGE, sizeof(EGIF_IMAGE), GIFDraw1)) {
          Serial.printf("Successfully opened GIF; Canvas size = %d x %d\n", gif.getCanvasWidth(), gif.getCanvasHeight());
          while (gif.playFrame(true, NULL)) {
            sprite.pushSprite(0, 0);  // Push the sprite to screen after every frame
            yield();
          }
          gif.close();
        }
        return;
      } else {
        String filename = generateFilename(SD);
        if (filename != "") {
          if (writeBMP(SD, filename.c_str(), &sprite)) {
            if (gif.open((uint8_t *)SGIF_IMAGE, sizeof(SGIF_IMAGE), GIFDraw1)) {
              Serial.printf("Successfully opened GIF; Canvas size = %d x %d\n", gif.getCanvasWidth(), gif.getCanvasHeight());
              while (gif.playFrame(true, NULL)) {
                sprite.pushSprite(0, 0);  // Push the sprite to screen after every frame
                yield();
              }
              gif.close();
            }
          } else {
            if (gif.open((uint8_t *)EGIF_IMAGE, sizeof(EGIF_IMAGE), GIFDraw1)) {
              Serial.printf("Successfully opened GIF; Canvas size = %d x %d\n", gif.getCanvasWidth(), gif.getCanvasHeight());
              while (gif.playFrame(true, NULL)) {
                sprite.pushSprite(0, 0);  // Push the sprite to screen after every frame
                yield();
              }
              gif.close();
            }
          }
        } else {
          Serial.println("Failed to create filename.");
          if (gif.open((uint8_t *)EGIF_IMAGE, sizeof(EGIF_IMAGE), GIFDraw1)) {
            Serial.printf("Successfully opened GIF; Canvas size = %d x %d\n", gif.getCanvasWidth(), gif.getCanvasHeight());
            while (gif.playFrame(true, NULL)) {
              sprite.pushSprite(0, 0);  // Push the sprite to screen after every frame
              yield();
            }
            gif.close();
          }
        }
      }
    } else {
      Menuitem++;
      if (Menuitem > 6) {
        Menuitem = 0;
      }
      MenuChange = true;
    }
    Serial.println("Middle Short Press");
    Serial.print(Menu);
    Serial.print("");
    Serial.println(Menuitem);
  }
  if (middlePressed && !middleShort && millis() - middlePressStartTime > 1000 && middlePressed && !middleShort && millis() - middlePressStartTime < 1500) {
    // If the button is being pressed, no short press has been registered, and it has been over 1 second
    Serial.println("Middle Long Press");
    middlePressed = false;     // Reset the pressed flag
    middlePressStartTime = 0;  // Reset the press start time
    if (Menu == 0) {
      Menu = 1;
      MenuChange = true;
      Menuitem = 0;
    } else {
      Menu = 0;
    }
  }
}

تابع displayUpdate مسئول تمام روتین های گرافیک و به دست آوردن تصویر از سنسور MLX90640 و پردازش آن است. این تابع هنگامی که داده جدیدی در دسترس باشد، آن را از سنسور تصویربرداری خوانده و سپس با استفاده از روش درون یابی انتخاب شده، رزولوشن آن را افزایش میدهد. پس از افزایش رزولوشن، تابع drawPixel را برای رسم پیکسل به پیکسل تصویر فراخوانی میکند. تابع drawPixel با استفاده از پالت رنگی انتخاب شده، رنگ هر پیکسل را تعیین میکند. تابع displayUpdate همچنین مسئول چاپ دمای حداکثر، دمای حداقل و دمای نقطه میانی به همراه آیکون باتری بر روی صفحه نمایش اصلی با استفاده از تابع drawPixel است. همین تابع برای رسم و پردازش  منو تنظیمات نیز به کار می آید.

void displayUpdate() {
  if (!mlx.getFrame(frame)) {
    // Failed to get frame, so reinitialize
    MLXInit();
  }
  float tempMinRead = 1000;   // some high value
  float tempMaxRead = -1000;  // some low value
  float tempCenter = 0.0;     // Temperature at center
  // Update the minimum and maximum temperatures read from the sensor
  for (int i = 0; i < 32 * 24; i++) {
    if (frame[i] < tempMinRead) {
      tempMinRead = frame[i];
    }
    if (frame[i] > tempMaxRead) {
      tempMaxRead = frame[i];
    }
    // Get the temperature at center
    if (i == 32 * 12 + 16) {
      tempCenter = frame[i];
    }
  }
  sprite.fillSprite(TFT_BLACK);
  if (interpolationMode == 0) {
    // Nearest neighbor interpolation
    for (int y = 0; y < 240; y++) {
      int yIndex = (y / 10) * 32;
      for (int x = 0; x < 320; x++) {
        float val = frame[yIndex + (x / 10)];
        drawPixel(319 - x, y, val);
      }
    }
  } else if (interpolationMode == 1) {
    // Average Interpolation
    for (int y = 0; y < 240; y++) {
      int yIndex = (y / 10) * 32;
      int yNextIndex = ((y / 10) + 1) * 32;  // Next row in original data
      for (int x = 0; x < 320; x++) {
        int xIndex = x / 10;
        // Take average of current and next points in x and y
        float val = (frame[yIndex + xIndex] + frame[yIndex + xIndex + 1] + frame[yNextIndex + xIndex] + frame[yNextIndex + xIndex + 1]) / 4.0;
        drawPixel(319 - x, y, val);
      }
    }
  } else if (interpolationMode == 2) {
    // Bilinear interpolation
    for (int y = 0; y < 240; y++) {
      int yIndex = (y / 10) * 32;
      float y_ratio = (y % 10) / 10.0;
      float y_opposite_ratio = 1 - y_ratio;
      for (int x = 0; x < 320; x++) {
        float x_ratio = (x % 10) / 10.0;
        float x_opposite_ratio = 1 - x_ratio;
        int x_over_10 = x / 10;
        float val = y_opposite_ratio * (x_opposite_ratio * frame[yIndex + x_over_10] + x_ratio * frame[yIndex + x_over_10 + 1]) + y_ratio * (x_opposite_ratio * frame[(yIndex + 32) + x_over_10] + x_ratio * frame[(yIndex + 32) + x_over_10 + 1]);
        drawPixel(319 - x, y, val);
      }
    }
  } else if (interpolationMode == 3) {
    // Bilinear interpolation
    int yIndex;
    int xIndex;
    int yNextIndex;
    for (int y = 0; y < 240; y++) {
      yIndex = (y / 10) * 32;
      yNextIndex = yIndex + 32;
      for (int x = 0; x < 320; x++) {
        xIndex = x / 10;
        float val = yOppositeRatios[y] * (xOppositeRatios[x] * frame[yIndex + xIndex] + xRatios[x] * frame[yIndex + xIndex + 1]) + yRatios[y] * (xOppositeRatios[x] * frame[yNextIndex + xIndex] + xRatios[x] * frame[yNextIndex + xIndex + 1]);
        drawPixel(319 - x, y, val);
      }
    }
  }
  else if (interpolationMode == 4) {
    for (int y = 0; y < 240; y++) {
      int yIndex = (y / 10) * 32;
      int yNextIndex = ((y / 10) + 1) * 32;  // Next row in original data
      for (int x = 0; x < 320; x++) {
        int xIndex = x / 10;
        // Determine which triangle the point is in and interpolate accordingly
        float t = (x % 10) / 10.0f;  // Horizontal distance from left pixel center
        float u = (y % 10) / 10.0f;  // Vertical distance from top pixel center
        float val;
        if (t > u) {  // Point is in lower-left triangle
          // Interpolate between bottom-left, top-right, and bottom-right
          val = (1 - t) * frame[yNextIndex + xIndex] + (1 - u) * frame[yIndex + xIndex + 1] + (t + u - 1) * frame[yNextIndex + xIndex + 1];
        } else {  // Point is in upper-right triangle
          // Interpolate between top-left, bottom-right, and top-right
          val = (1 - t) * frame[yIndex + xIndex] + (1 - u) * frame[yNextIndex + xIndex + 1] + (t + u - 1) * frame[yIndex + xIndex + 1];
        }
        drawPixel(319 - x, y, val);
      }
    }
  }
  // Display the maximum, minimum and center temperatures as overlay
  sprite.setTextColor(TFT_BLACK);
  sprite.setTextSize(1);
  sprite.setFreeFont(&Open_Sans_ExtraBold_10);
  sprite.drawString("T Min: " + String(tempMinRead) + " C", 15, 220);
  sprite.drawString("T Max: " + String(tempMaxRead) + " C", 220, 220);
  sprite.drawString("Tc: " + String(tempCenter, 1) + "C", 135, 220);
  // Display a small crosshair at the center
  sprite.drawLine(155, 120, 165, 120, TFT_WHITE);
  sprite.drawLine(160, 115, 160, 125, TFT_WHITE);
  detachInterrupt(digitalPinToInterrupt(downButton));
  batv = ((float)analogRead(VBAT_PIN) / 4095) * 2 * 1.07 * 3.3;
  attachInterrupt(digitalPinToInterrupt(downButton), downButton_ISR, CHANGE);
  int batpc = (uint8_t)(((batv - BATTV_MIN) / (BATTV_MAX - BATTV_MIN)) * 100);
  drawBattery(batpc);
  if (Menu == 1) {
    MenuChange = false;
    sprite.fillRect(60, 40, 200, 120, TFT_BLACK);
    sprite.fillRect(60, 40, 200, 10, TFT_WHITE);
    sprite.setTextFont(0);
    sprite.setTextSize(1);
    sprite.setTextColor(TFT_BLACK);
    sprite.drawString("Settings", 140, 41);
    sprite.setTextColor(TFT_WHITE);
    for (int i = 0; i < 7; i++) {
      if (i == Menuitem) sprite.setTextColor(TFT_GREEN);
      int y = 55 + (15 * i);
      sprite.drawString(menuItems[i], 65, y);
      if (i == 0) {
        sprite.drawString(ASindex[AutoScale], 160, y);
      } else if (i == 1) {
        sprite.drawString(String(MinT) + " C", 160, y);
      } else if (i == 2) {
        sprite.drawString(String(MaxT) + " C", 160, y);
      } else if (i == 3) {
        sprite.drawString(IPindex[interpolationMode], 160, y);
      } else if (i == 4) {
        sprite.drawString("Palette " + String(paletteIndex), 160, y);
      } else if (i == 5) {
        sprite.drawString(RRindex[RefreshRate], 160, y);
      } else if (i == 6) {
        sprite.drawString(String(BLPWM) + " %", 160, y);
      }
      sprite.setTextColor(TFT_WHITE);
    }
  }
  sprite.pushSprite(0, 0);
}
void drawPixel(int x, int y, float val) {
  int colorIndex;
  if (AutoScale == 1) {
    if (val <= tempMin) {
      colorIndex = 0;
    } else if (val >= tempMax) {
      colorIndex = 5;
    } else {
      // Map the value to a color index between 0 and 5
      colorIndex = int(map(val, tempMin, tempMax, 0, 5));
    }
  } else {
    if (val <= MinT) {
      colorIndex = 0;
    } else if (val >= MaxT) {
      colorIndex = 5;
    } else {
      // Map the value to a color index between 0 and 5
      colorIndex = int(map(val, MinT, MaxT, 0, 5));
    }
  }
  int color = colorPalettes[paletteIndex][colorIndex];
  sprite.drawPixel(x, y, color);
}
void drawBattery(int batpc) {
  int x = 290;
  int y = 10;
  int w = 20;
  int h = 10;
  int color = TFT_RED;  // Default color for the lowest level
  // Determine fill level and color based on batpc
  int fillLevel = 0;
  if (batpc > 75) {
    fillLevel = w;  // 100%
    color = TFT_GREEN;
  } else if (batpc > 50) {
    fillLevel = w * 3 / 4;  // 75%
    color = TFT_GREEN;
  } else if (batpc > 25) {
    fillLevel = w / 2;  // 50%
    color = TFT_BLUE;
  } else if (batpc > 0) {
    fillLevel = w / 4;  // 25%
    color = TFT_BLUE;
  }
  // Draw battery outline
  sprite.drawRect(x, y, w, h, TFT_WHITE);
  sprite.drawRect(x + w, y + h / 4, 2, h / 2, TFT_WHITE);
  // Draw fill level
  if (fillLevel > 0) {
    sprite.fillRect(x + 1, y + 1, fillLevel - 2, h - 2, color);
  }
}

تابع GIFDraw برای پردازش و نمایش تصویر متحرک است و برای این منظور از کتابخانه AnimatefGIF استفاده میکند.

void GIFDraw(GIFDRAW *pDraw) {
  uint8_t *s;
  uint16_t *d, *usPalette;
  int x, y, iWidth;
  iWidth = pDraw->iWidth;
  if (iWidth + pDraw->iX > TFT_WIDTH)
    iWidth = TFT_WIDTH - pDraw->iX;
  usPalette = pDraw->pPalette;
  y = pDraw->iY + pDraw->y;
  if (y >= TFT_HEIGHT || pDraw->iX >= TFT_WIDTH || iWidth < 1)
    return;
  if (pDraw->ucDisposalMethod == 2) {
    for (x = 0; x < iWidth; x++) {
      if (s[x] == pDraw->ucTransparent)
        s[x] = pDraw->ucBackground;
    }
    pDraw->ucHasTransparency = 0;
  }
  s = pDraw->pPixels;
  if (pDraw->ucHasTransparency) {
    uint8_t *pEnd, c, ucTransparent = pDraw->ucTransparent;
    pEnd = s + iWidth;
    x = 0;
    while (x < iWidth) {
      c = ucTransparent - 1;
      d = &usTemp[0];
      while (c != ucTransparent && s < pEnd) {
        c = *s++;
        if (c == ucTransparent) {
          s--;
        } else {
          *d++ = usPalette[c];
        }
      }
      if (d > &usTemp[0]) {
        sprite.pushImage(pDraw->iX + x, y, d - &usTemp[0], 1, usTemp);  // Push the image to the sprite
        x += d - &usTemp[0];
      }
      c = ucTransparent;
      while (c == ucTransparent && s < pEnd) {
        c = *s++;
        if (c == ucTransparent)
          x++;
        else
          s--;
      }
    }
  } else {
    s = pDraw->pPixels;
    for (x = 0; x < iWidth; x++) {
      usTemp[x] = usPalette[*s++];
    }
    sprite.pushImage(pDraw->iX, y, iWidth, 1, usTemp);  // Push the image to the sprite
  }
}

در نهایت به تابع setup و loop میرسیم. طبق معمول تابع setup برای راه اندازی و فراخوانی تمام کتابخانه ها و تنظیمات پیکربندی در آغاز کار استفاده میشود. تابع setup همچنین تمام پارامترهای پیکربندی را از namespace که در قسمت NVS حافظه فلش ذخیره شده، فراخوانی میکند. تابع loop، توابع navigationUpdate و displayUpdate را به طور مداوم برای عملکرد آرامتر فراخوانی میکند.

void setup() {
  Serial.begin(115200);
  tft.init();
#ifdef USE_DMA
  tft.initDMA();
#endif
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  gif.begin(BIG_ENDIAN_PIXELS);
  sprite.createSprite(320, 240);
  pinMode(upButton, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(upButton), upButton_ISR, CHANGE);
  pinMode(downButton, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(downButton), downButton_ISR, CHANGE);
  pinMode(middleButton, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(middleButton), middleButton_ISR, CHANGE);
  ReadConfig();
  MLXInit();
  if (MinT == 0 && MaxT == 0) {
    MinT = 20.0;
    MaxT = 32.0;
    WriteConfig();
  }
  if (BLPWM == 0) {
    BLPWM = 100;
    WriteConfig();
  }
  // mlx.setRefreshRate(MLX90640_32_HZ);
  pinMode(backlightPin, OUTPUT);
  analogWrite(backlightPin, map(BLPWM, 0, 100, 0, 255));  // Turn on backlight
  if (gif.open((uint8_t *)GIF_IMAGE, sizeof(GIF_IMAGE), GIFDraw)) {
    Serial.printf("Successfully opened GIF; Canvas size = %d x %d\n", gif.getCanvasWidth(), gif.getCanvasHeight());
    while (gif.playFrame(true, NULL)) {
      sprite.pushSprite(0, 0);  // Push the sprite to screen after every frame
      yield();
    }
    gif.close();
  }
  delay(1000);
  initSDcard();
  // Precompute ratios
  for (int x = 0; x < 320; x++) {
    xRatios[x] = (x % 10) / 10.0f;
    xOppositeRatios[x] = 1 - xRatios[x];
  }
  for (int y = 0; y < 240; y++) {
    yRatios[y] = (y % 10) / 10.0f;
    yOppositeRatios[y] = 1 - yRatios[y];
  }
}
void loop() {
  navigationUpdate();
  displayUpdate();
}

موارد موجود در فایل : سورس کامل

5 (1 نفر)

برای دریافت مطالب جدید کانال تلگرام یا پیج اینستاگرام ما را دنبال کنید.

محمد رحیمی

محمد رحیمی هستم. سعی میکنم در آیرنکس مطالب مفید قرار بدهم. سوالات مربوط به این مطلب را در قسمت نظرات همین مطلب اعلام کنید. سعی میکنم در اسرع وقت به نظرات شما پاسخ بدهم.

2 نظر

  1. با سلام و عرض ادب
    پیشنهاد میکنم تمامی قطعات مورد نیاز این نوع آموزشها را در قالب پکیج در سایت قرار دهید تا بشود یکجا آنها را خرید و مونتاژ کرد…
    مخصوصا پی سی بی

    1. سلام عزیز
      ممنون از پیشنهاد شما

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *