Máy cho cá ăn tự động bằng ESP32: servo SG90, DS3231 giữ giờ khi mất điện, đặt lịch trên web

Làm máy cho cá ăn tự động bằng ESP32: servo SG90 quay đĩa có lỗ, DS3231 giữ giờ khi mất điện, đồng bộ giờ Internet, trang web đặt 4 giờ cho ăn, không cho ăn lặp sau mất điện.

Đi vắng vài ngày mà bể cá không ai cho ăn là lý do nhiều người tự làm máy cho cá ăn. Bài này làm máy cho cá ăn bằng ESP32: servo SG90 quay một đĩa có lỗ dưới hộp đựng cám, DS3231 giữ giờ kể cả khi mất điện hay mất Wi-Fi, một trang web để đặt 4 giờ cho ăn và số phần mỗi lần, cùng nút bấm cho ăn ngay. Chương trình nhớ giờ hẹn nào đã chạy trong ngày, nên mất điện rồi có lại cũng không cho ăn lặp.

1. Cơ cấu cho ăn

Hai hình cắt hộp đựng thức ăn đặt trên đĩa xoay có một lỗ: khi lỗ lệch cửa ra thì đóng, khi servo quay 110 độ thì lỗ trùng cửa ra và cám rơi xuống; hình nhìn từ trên đĩa có lỗ ở 0 độ và cửa ra ở 110 độ
Đĩa có một lỗ quay qua lại trên cửa ra.
  • Hộp đựng cám (lọ nhựa, ống nhựa) đặt trên một đĩa tròn có một lỗ. Bên dưới đĩa là tấm đế có cửa ra ở vị trí 110°.
  • Servo quay đĩa tới 110°: lỗ trùng cửa ra, cám rơi xuống bể. Quay về 0°: lỗ lệch, cám giữ lại trong hộp. Mỗi lần mở rồi đóng là một phần.
  • Lượng mỗi phần tùy cỡ lỗ và loại cám (viên, mảnh). Cân thử 10 phần để biết một phần bao nhiêu gam, rồi chỉnh số phần mỗi lần.
  • Đặt máy ở chỗ khô, hơi nước từ bể làm cám ẩm và kẹt; có thể thêm gói hút ẩm vào hộp.

2. Chuẩn bị

  • ESP32 DevKit V1 và dây USB, cục sạc USB 5 V từ 1 A
  • Servo SG90 (hoặc MG90S bánh răng kim loại cho bền hơn)
  • Module DS3231 có pin CR2032
  • Nút nhấn; hộp nhựa, đĩa nhựa hoặc mica cắt tròn
  • Breadboard và dây cắm

3. Nối dây

Sơ đồ nối: servo dây cam tín hiệu D26, dây đỏ 5 V vào VIN, dây nâu GND; DS3231 VCC 3V3, GND, SDA D21, SCL D22; nút nhấn giữa D4 và GND
Servo ở D26, DS3231 qua I2C, nút ở D4.
  • Servo chạy 5 V, lấy từ chân VIN khi ESP32 được cấp bằng cục sạc USB. Tín hiệu điều khiển 3,3 V của ESP32 servo vẫn nhận được.
  • Nếu servo giật làm ESP32 khởi động lại, thêm tụ 470 µF giữa 5 V và GND gần servo.

4. Vì sao cần DS3231

ESP32 lấy được giờ từ Internet (NTP), nhưng máy cho ăn phải đúng giờ cả khi mạng nhà mất, router khởi động lại, hay cúp điện cả khu. DS3231 là đồng hồ thời gian thực có pin riêng, sai số khoảng vài giây mỗi tháng nhờ thạch anh bù nhiệt. Chương trình:

  • đọc giờ từ DS3231 mỗi giây để chạy lịch;
  • mỗi ngày một lần, khi có Wi-Fi, chỉnh DS3231 theo giờ Internet (múi giờ Việt Nam);
  • nếu DS3231 báo từng mất nguồn nuôi (cờ OSF, ví dụ hết pin CR2032) mà chưa có giờ Internet thì tạm dừng lịch thay vì cho ăn sai giờ, và trang web hiện "Chưa có giờ đúng".

5. Điều khiển servo

Bảng góc và độ rộng xung: 0 độ 500 micro giây giá trị 1638, 90 độ 1450 micro giây 4751, 110 độ 1661 micro giây 5442, 180 độ 2400 micro giây 7864; đồ thị góc theo thời gian khi cho ăn 2 phần: mở 110 độ giữ 350 ms, đóng 500 ms, lặp lại, rồi ngừng phát xung
Xung PWM 50 Hz và chu trình cho ăn 2 phần.

Servo đọc độ rộng xung trong mỗi chu kỳ 20 ms: khoảng 0,5 ms là 0°, 2,4 ms là 180°. Cách tính giống bài servo SG90 trên website. Sau mỗi lần cho ăn, chương trình gửi ledcWrite(CHAN_SERVO, 0) để ngừng phát xung: servo đứng yên, không kêu rè rè, không tốn điện giữ góc.

6. Chương trình

// PYWORLD.VN - Máy cho cá ăn tự động: ESP32 quay servo SG90 theo lịch; giờ lấy từ DS3231 (giữ giờ khi mất điện), đồng bộ qua Internet
// PYWORLD.VN - Servo tín hiệu D26, nguồn 5 V; DS3231 SDA D21, SCL D22; nút "cho ăn ngay" D4 xuống GND; đèn D2 sáng khi đang cho ăn
// PYWORLD.VN - Trang web đặt 4 giờ cho ăn và số phần mỗi lần; mỗi giờ hẹn chỉ chạy một lần trong ngày, khởi động lại không cho ăn lặp

#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Preferences.h>
#include <time.h>
#include <stdarg.h>

const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";
const int CHAN_SERVO = 26, CHAN_NUT = 4, CHAN_DEN = 2;
const uint8_t DIA_CHI_DS3231 = 0x68;
const int GOC_DONG = 0, GOC_MO = 110;      // góc của đĩa: lỗ đóng / lỗ mở trên cửa ra thức ăn (chỉnh theo hộp)
const uint32_t GIU_MO_MS = 350;            // giữ lỗ mở bao lâu cho mỗi phần
const int TRE_TOI_DA_PHUT = 10;            // mất điện rồi có lại trong 10 phút sau giờ hẹn vẫn cho ăn
const int SO_LICH = 4, SO_SU_KIEN = 10;

// Các struct khai báo trước mọi hàm.
struct Lich { uint8_t gio, phut, phan; bool bat; };
struct ThoiGian { uint16_t nam; uint8_t thang, ngay, gio, phut, giay; };
struct SuKien { char noiDung[64]; };

Lich lich[SO_LICH] = {{7, 0, 2, true}, {17, 30, 2, true}, {12, 0, 1, false}, {21, 0, 1, false}};
long daChoAn[SO_LICH];                     // ngày (yyyymmdd) mà giờ hẹn đó đã chạy
long daBaoLo[SO_LICH];                     // ngày đã ghi "bỏ lỡ" cho giờ hẹn đó
WebServer may(80);
Preferences boNho;
SuKien suKien[SO_SU_KIEN];
int soSuKien = 0, viTriSuKien = -1, phanHomNay = 0;
long homNay = 0, ngayDongBo = 0;
ThoiGian bay;
bool gioHopLe = false;

// ---- DS3231 (như bài DS3231) ----
uint8_t sangBcd(uint8_t v) { return ((v / 10) << 4) | (v % 10); }
uint8_t tuBcd(uint8_t v) { return (v >> 4) * 10 + (v & 0x0F); }

bool ghiThanhGhi(uint8_t tg, const uint8_t *du, size_t n) {
  Wire.beginTransmission(DIA_CHI_DS3231);
  Wire.write(tg);
  Wire.write(du, n);
  return Wire.endTransmission() == 0;
}

bool docThanhGhi(uint8_t tg, uint8_t *du, size_t n) {
  Wire.beginTransmission(DIA_CHI_DS3231);
  Wire.write(tg);
  if (Wire.endTransmission(false) != 0) return false;
  if (Wire.requestFrom(DIA_CHI_DS3231, (uint8_t)n) != n) return false;
  for (size_t i = 0; i < n; i++) du[i] = Wire.read();
  return true;
}

bool datGio(const struct tm &t) {
  uint8_t du[7] = {sangBcd(t.tm_sec), sangBcd(t.tm_min), sangBcd(t.tm_hour), (uint8_t)(t.tm_wday + 1),
                   sangBcd(t.tm_mday), sangBcd(t.tm_mon + 1), sangBcd(t.tm_year - 100)};
  uint8_t trangThai;
  if (!ghiThanhGhi(0x00, du, 7) || !docThanhGhi(0x0F, &trangThai, 1)) return false;
  trangThai &= ~0x80;                      // xóa cờ OSF: giờ đã hợp lệ
  return ghiThanhGhi(0x0F, &trangThai, 1);
}

// Đọc giờ; trả về false nếu không đọc được hoặc DS3231 từng mất nguồn nuôi (cờ OSF) mà chưa đặt lại giờ.
bool docGio(ThoiGian &tg) {
  uint8_t du[7], trangThai;
  if (!docThanhGhi(0x00, du, 7) || !docThanhGhi(0x0F, &trangThai, 1)) return false;
  tg.giay = tuBcd(du[0] & 0x7F);
  tg.phut = tuBcd(du[1] & 0x7F);
  tg.gio = tuBcd(du[2] & 0x3F);
  tg.ngay = tuBcd(du[4] & 0x3F);
  tg.thang = tuBcd(du[5] & 0x1F);
  tg.nam = 2000 + tuBcd(du[6]);
  return !(trangThai & 0x80);
}

// ---- Sự kiện ----
void ghiSuKien(const char *dang, ...) {
  viTriSuKien = (viTriSuKien + 1) % SO_SU_KIEN;
  if (soSuKien < SO_SU_KIEN) soSuKien++;
  char noiDung[48];
  va_list ds;
  va_start(ds, dang);
  vsnprintf(noiDung, sizeof(noiDung), dang, ds);
  va_end(ds);
  snprintf(suKien[viTriSuKien].noiDung, sizeof(suKien[0].noiDung), "%02u/%02u %02u:%02u %s", bay.ngay, bay.thang, bay.gio, bay.phut, noiDung);
  Serial.println(suKien[viTriSuKien].noiDung);
}

// ---- Servo và cho ăn ----
void datGoc(int goc) {                     // xung 500-2400 us trong chu kỳ 20 ms (như bài servo SG90)
  uint32_t us = 500 + (uint32_t)1900 * constrain(goc, 0, 180) / 180;
  ledcWrite(CHAN_SERVO, (uint32_t)((uint64_t)us * 65536 / 20000));
}

void choAn(int phan, const char *nguon) {
  digitalWrite(CHAN_DEN, HIGH);
  for (int k = 0; k < phan; k++) {
    datGoc(GOC_MO);
    delay(GIU_MO_MS);
    datGoc(GOC_DONG);
    delay(GIU_MO_MS + 150);
  }
  ledcWrite(CHAN_SERVO, 0);                // ngừng phát xung: servo nghỉ, không rung, đỡ tốn điện
  digitalWrite(CHAN_DEN, LOW);
  phanHomNay += phan;
  ghiSuKien("Cho an %d phan (%s)", phan, nguon);
}

// Giờ hẹn nào đã tới (trễ không quá TRE_TOI_DA_PHUT) mà hôm nay chưa chạy thì cho ăn.
void kiemTraLich() {
  long ngay = bay.nam * 10000L + bay.thang * 100 + bay.ngay;
  if (ngay != homNay) {
    homNay = ngay;
    phanHomNay = 0;
  }
  int phutNgay = bay.gio * 60 + bay.phut;
  for (int i = 0; i < SO_LICH; i++) {
    int hen = lich[i].gio * 60 + lich[i].phut;
    if (!lich[i].bat || daChoAn[i] == homNay || phutNgay < hen) continue;
    if (phutNgay > hen + TRE_TOI_DA_PHUT) {  // quá trễ (mất điện lâu): không cho ăn bù, chỉ ghi lại để người nuôi biết
      if (daBaoLo[i] != homNay) {
        daBaoLo[i] = homNay;
        ghiSuKien("Bo lo lich %02u:%02u", lich[i].gio, lich[i].phut);
      }
      continue;
    }
    daChoAn[i] = homNay;
    char khoa[4] = {'d', (char)('0' + i), 0};
    boNho.putLong(khoa, homNay);           // ghi trước khi quay: lỡ mất điện giữa chừng cũng không cho ăn lặp
    char nguon[16];
    snprintf(nguon, sizeof(nguon), "lich %02u:%02u", lich[i].gio, lich[i].phut);
    choAn(lich[i].phan, nguon);
  }
}

// Mỗi ngày đồng bộ DS3231 với giờ Internet một lần (khi có Wi-Fi).
void dongBoGio() {
  struct tm t;
  if (WiFi.status() != WL_CONNECTED || !getLocalTime(&t, 0)) return;
  long ngay = (t.tm_year + 1900) * 10000L + (t.tm_mon + 1) * 100 + t.tm_mday;
  if (ngay == ngayDongBo) return;
  if (datGio(t)) {
    ngayDongBo = ngay;
    gioHopLe = docGio(bay);
    ghiSuKien("Dong bo DS3231 voi gio Internet");
  }
}

// ---- Trang web ----
void trangChu() {
  String s;
  s.reserve(3000);
  s += F("<!doctype html><html lang='vi'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
         "<title>Cho cá ăn</title><style>body{font-family:sans-serif;margin:16px;background:#f4f6f9}"
         "input{font-size:16px;margin:4px}button{font-size:18px;padding:10px 16px}li{margin:4px 0}</style></head><body>"
         "<h2>Máy cho cá ăn</h2>");
  char dong[120];
  if (gioHopLe) snprintf(dong, sizeof(dong), "<p>Bây giờ: %02u:%02u ngày %02u/%02u/%u. Hôm nay đã cho %d phần.</p>",
                         bay.gio, bay.phut, bay.ngay, bay.thang, bay.nam, phanHomNay);
  else snprintf(dong, sizeof(dong), "<p><b>Chưa có giờ đúng</b>: kết nối Wi-Fi để đồng bộ, lịch tạm dừng.</p>");
  s += dong;
  s += F("<form action='/luu'><h3>Lịch cho ăn</h3>");
  for (int i = 0; i < SO_LICH; i++) {
    snprintf(dong, sizeof(dong), "<div><input type='checkbox' name='b%d'%s> <input type='time' name='g%d' value='%02u:%02u'>",
             i, lich[i].bat ? " checked" : "", i, lich[i].gio, lich[i].phut);
    s += dong;
    snprintf(dong, sizeof(dong), " <input type='number' name='p%d' min='1' max='5' value='%u' style='width:50px'> phần</div>", i, lich[i].phan);
    s += dong;
  }
  s += F("<p><button>Lưu lịch</button></p></form><form action='/cho-an' method='post'><button>Cho ăn ngay 1 phần</button></form><h3>Gần đây</h3><ul>");
  for (int k = 0; k < soSuKien; k++) {
    s += "<li>";
    s += suKien[(viTriSuKien - k + SO_SU_KIEN) % SO_SU_KIEN].noiDung;
    s += "</li>";
  }
  s += F("</ul></body></html>");
  may.send(200, "text/html; charset=utf-8", s);
}

void chuyenVeTrangChu() {
  may.sendHeader("Location", "/");
  may.send(303);
}

void luuLich() {
  for (int i = 0; i < SO_LICH; i++) {
    int g, p;
    char ten[4] = {'g', (char)('0' + i), 0};
    if (sscanf(may.arg(ten).c_str(), "%d:%d", &g, &p) == 2 && g >= 0 && g < 24 && p >= 0 && p < 60) {
      lich[i].gio = g;
      lich[i].phut = p;
    }
    ten[0] = 'p';
    lich[i].phan = constrain(may.arg(ten).toInt(), 1, 5);
    ten[0] = 'b';
    lich[i].bat = may.hasArg(ten);
  }
  boNho.putBytes("lich", lich, sizeof(lich));
  ghiSuKien("Da luu lich moi");
  chuyenVeTrangChu();
}

void choAnNgay() {
  choAn(1, "web");
  chuyenVeTrangChu();
}

void setup() {
  Serial.begin(115200);
  pinMode(CHAN_NUT, INPUT_PULLUP);
  pinMode(CHAN_DEN, OUTPUT);
  ledcAttach(CHAN_SERVO, 50, 16);
  datGoc(GOC_DONG);
  delay(500);
  ledcWrite(CHAN_SERVO, 0);
  Wire.begin(21, 22);
  boNho.begin("choca", false);
  boNho.getBytes("lich", lich, sizeof(lich));
  for (int i = 0; i < SO_LICH; i++) {
    char khoa[4] = {'d', (char)('0' + i), 0};
    daChoAn[i] = boNho.getLong(khoa, 0);
  }
  gioHopLe = docGio(bay);
  Serial.println(gioHopLe ? "DS3231: gio hop le" : "DS3231: chua co gio dung (mat nguon nuoi hoac chua dat), cho dong bo Internet");
  WiFi.mode(WIFI_STA);
  WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
  configTzTime("ICT-7", "pool.ntp.org", "time.google.com");
  may.on("/", trangChu);
  may.on("/luu", luuLich);
  may.on("/cho-an", HTTP_POST, choAnNgay);
  may.begin();
}

void loop() {
  may.handleClient();
  static uint32_t lanGiay = 0, lanIp = 0;
  if (millis() - lanGiay >= 1000) {
    lanGiay = millis();
    dongBoGio();
    gioHopLe = docGio(bay);
    if (gioHopLe) kiemTraLich();
  }
  if (WiFi.status() == WL_CONNECTED && lanIp == 0) {
    lanIp = millis();
    Serial.printf("Mo trinh duyet: http://%s\n", WiFi.localIP().toString().c_str());
  }
  static bool truoc = HIGH;
  static uint32_t lucBam = 0;
  bool doc = digitalRead(CHAN_NUT);
  if (doc == LOW && truoc == HIGH && millis() - lucBam > 500) {
    lucBam = millis();
    choAn(1, "nut");
  }
  truoc = doc;
}

Giải thích

  • kiemTraLich() chạy mỗi giây. Với mỗi giờ hẹn đang bật: nếu đã tới giờ, chưa quá 10 phút và hôm nay chưa chạy thì cho ăn. Ngày đã chạy được ghi vào Preferences trước khi quay servo, nên mất điện giữa lúc quay, có điện lại cũng không cho ăn lặp.
  • Trễ tối đa 10 phút: mất điện ngắn quanh giờ hẹn thì có điện lại vẫn cho ăn bù. Mất điện lâu hơn thì bỏ qua và ghi "Bo lo lich", tránh cho ăn quá gần bữa sau. Lần đầu nạp chương trình vào buổi tối cũng sẽ thấy các dòng "bỏ lỡ" cho giờ hẹn đã qua trong ngày, đó là bình thường.
  • Trang web: / hiện giờ, lịch, số phần đã cho hôm nay và 10 sự kiện gần nhất; /luu nhận lịch mới từ biểu mẫu và lưu vào Preferences; /cho-an cho ăn ngay 1 phần.
  • Nút D4: bấm để cho ăn ngay 1 phần, tiện thử cơ cấu hoặc cho thêm khi đang ở nhà.

7. Kết quả

Trang web máy cho cá ăn: giờ hiện tại, hôm nay đã cho 3 phần; lịch 4 dòng với ô bật tắt, giờ và số phần; nút lưu lịch, nút cho ăn ngay; danh sách sự kiện gần đây
Trang web đặt lịch (số liệu theo kịch bản giả định).
Trục thời gian ngày 26/10 với hai giờ hẹn 07:00 và 17:30; mất điện 06:55 tới 07:06 vẫn cho ăn lúc 07:06; mất điện 17:20 tới 17:45 thì ghi bỏ lỡ lịch 17:30; 12:15 cho ăn 1 phần từ web
Lịch cho ăn khi có hai lần mất điện (kịch bản giả định).
Serial Monitor: DS3231 giờ hợp lệ, cho ăn 2 phần lúc 07:06, địa chỉ trang web, đồng bộ DS3231 với giờ Internet, cho ăn 1 phần từ web, lần có điện lại lúc 17:45 ghi bỏ lỡ lịch 17:30, ngày hôm sau cho ăn đúng 07:00
Serial Monitor theo cùng kịch bản.

8. Lưu ý khi nuôi cá

  • Cho cá ăn quá nhiều làm bẩn nước nhanh hơn là để cá đói vài ngày. Lượng mỗi bữa nên vừa đủ cá ăn hết trong vài phút.
  • Chạy thử máy vài ngày khi đang ở nhà, quan sát lượng cám rơi và cá ăn hết hay không, rồi mới để máy tự chạy khi đi vắng.
  • Trang web không có mật khẩu: ai cùng mạng Wi-Fi cũng bấm "Cho ăn ngay" được. Với mạng gia đình thường không sao; nếu cần, thêm kiểm tra mật khẩu trong luuLich() và choAnNgay().

9. Lỗi hay gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
"DS3231: chua co gio dung"Mới lắp pin, pin hết, chưa từng đặt giờCho ESP32 vào Wi-Fi để đồng bộ; thay pin CR2032
Servo rung, ESP32 khởi động lạiNguồn yếu, servo kẹt cơ khíCục sạc từ 1 A, thêm tụ 470 µF; kiểm tra đĩa quay trơn
Cám không rơi hoặc rơi quá nhiềuGóc mở chưa đúng vị trí cửa ra, lỗ quá nhỏ hay quá toChỉnh GOC_MO, GIU_MO_MS; khoan lại lỗ
Cám bị kẹt, vón cụcHơi ẩm từ bểĐặt hộp cao hơn mặt nước, thêm gói hút ẩm, dùng cám viên
Đổi lịch trên web không có tác dụngBỏ trống ô giờ, ô bật chưa đánh dấuKiểm tra ô đánh dấu; xem dòng "Da luu lich moi" trên Serial

10. Xem thêm


Tin tức liên quan

ESP32 điều khiển động cơ bước NEMA 17 với A4988: vi bước, chỉnh dòng VREF, tăng tốc mượt
ESP32 điều khiển động cơ bước NEMA 17 với A4988: vi bước, chỉnh dòng VREF, tăng tốc mượt

Hướng dẫn điều khiển động cơ bước NEMA 17 bằng ESP32 qua driver A4988: nối dây, chỉnh dòng VREF, chọn vi bước 1/16, tăng tốc giảm tốc hình thang và thí nghiệm STEM.

RFID RC522 với ESP32: đọc mã thẻ không cần thư viện, lưu thẻ hợp lệ, mở khóa bằng relay
RFID RC522 với ESP32: đọc mã thẻ không cần thư viện, lưu thẻ hợp lệ, mở khóa bằng relay

Nối RC522 với ESP32 qua SPI, hiểu REQA, chống va chạm, SELECT, HLTA, đọc UID 4 và 7 byte không cần thư viện, lưu thẻ bằng Preferences, đóng relay khi quẹt đúng thẻ.

GPS NEO-6M với ESP32: đọc câu NMEA, lấy tọa độ, giờ Việt Nam, tốc độ không cần thư viện
GPS NEO-6M với ESP32: đọc câu NMEA, lấy tọa độ, giờ Việt Nam, tốc độ không cần thư viện

GPS làm việc thế nào, module NEO-6M, nối với ESP32 qua UART2, đọc câu NMEA RMC và GGA, kiểm checksum, đổi tọa độ, giờ Việt Nam, tốc độ, tính khoảng cách, không cần TinyGPS.

Điều khiển relay bằng điện thoại với Blynk IoT và ESP32: template, datastream, đồng bộ trạng thái
Điều khiển relay bằng điện thoại với Blynk IoT và ESP32: template, datastream, đồng bộ trạng thái

Hướng dẫn Blynk IoT với ESP32 từ đầu: tạo template, datastream V0–V5, thiết bị và token, nối relay 4 kênh, chương trình bật tắt bằng điện thoại kèm nút nhấn tại chỗ.

ESP32 làm mạch điều khiển bay cho flycam mini động cơ có chổi than: lọc bù, PID, bộ trộn, giá thử
ESP32 làm mạch điều khiển bay cho flycam mini động cơ có chổi than: lọc bù, PID, bộ trộn, giá thử

Dùng ESP32 làm mạch điều khiển bay cho flycam mini động cơ có chổi than: kênh MOSFET, bộ lọc bù, hai vòng PID, bộ trộn bốn động cơ, điều kiện an toàn và cách chỉnh PID trên giá thử.

ESP32-CAM là gì: sơ đồ chân, cách nạp code, xem video trên trình duyệt, chụp ảnh lưu thẻ nhớ
ESP32-CAM là gì: sơ đồ chân, cách nạp code, xem video trên trình duyệt, chụp ảnh lưu thẻ nhớ

Giới thiệu mạch ESP32-CAM AI Thinker, sơ đồ chân, nạp code bằng đế MB hoặc USB-TTL, chương trình xem video MJPEG trên trình duyệt, chụp ảnh có đèn flash và lưu vào thẻ microSD.

ESPHome cho ESP32: đưa relay 4 kênh vào Home Assistant không cần viết code Arduino
ESPHome cho ESP32: đưa relay 4 kênh vào Home Assistant không cần viết code Arduino

Dùng ESPHome Device Builder khai báo relay 4 kênh, nút nhấn, cảm biến Wi-Fi bằng YAML; nạp lần đầu qua USB, sau đó qua Wi-Fi; so sánh với cách Arduino + MQTT.

Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno
Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno

Hướng dẫn nối màn hình LCD 1602 I2C với Arduino Uno, cài thư viện LiquidCrystal_I2C, chạy code hiển thị và quét địa chỉ I2C 0x27/0x3F.


Bình luận
Đã thêm vào giỏ hàng