/* ============================================================ * Infinite Flash Drive — LEVEL 2 : Cloud-Sync USB * บอร์ด: ESP32-S3 * ============================================================ * * สิ่งที่คลิปโชว์: * เสียบ USB -> ไดรฟ์ขึ้นเหมือน Level 1 * ลากไฟล์ใส่ไดรฟ์ -> บอร์ดแอบเชื่อม WiFi แล้วอัปโหลดไฟล์นั้น * ขึ้น server อัตโนมัติเบื้องหลัง (ดู log ที่ /files ของ server) * * หลักการ: * - ต่อ USB MSC จาก flash partition "ffat" เหมือน Level 1 * - onWrite จับเวลาไว้ พอ PC หยุดเขียนสักพัก (idle) ถือว่าก๊อปเสร็จ * - บอร์ด scan โฟลเดอร์ FAT หาไฟล์ใหม่ แล้ว POST ขึ้น server * * ---------- Board settings (เหมือน Level 1) ---------- * USB Mode = USB-OTG (TinyUSB) , Partition = Default 4MB with ffat * * >>> แก้ 4 บรรทัดด้านล่างก่อนคอมไพล์ <<< * ============================================================ */ #define WIFI_SSID "ใส่ชื่อ_WiFi_ที่นี่" #define WIFI_PASS "ใส่รหัส_WiFi_ที่นี่" #define SERVER_HOST "192.168.1.100" // IP เครื่องที่รัน node server.js #define SERVER_PORT 3000 #include "USB.h" #include "USBMSC.h" #include "wear_levelling.h" #include "esp_vfs_fat.h" #include #include #include #include static const char *PARTITION_LABEL = "ffat"; static const char *MOUNT_POINT = "/disk"; USBMSC msc; wl_handle_t s_wl = WL_INVALID_HANDLE; uint32_t s_sectorSize = 0, s_sectorCount = 0; volatile uint32_t s_lastWriteMs = 0; // เวลาเขียนล่าสุดจาก PC volatile bool s_dirty = false; // มีการเขียนที่ยังไม่ได้ sync std::set s_uploaded; // รายชื่อไฟล์ที่อัปโหลดไปแล้ว (กันซ้ำ) // ---------------- USB MSC callbacks ---------------- static int32_t onRead(uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize) { return wl_read(s_wl, (size_t)lba * s_sectorSize + offset, buffer, bufsize) == ESP_OK ? (int32_t)bufsize : -1; } static int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) { size_t addr = (size_t)lba * s_sectorSize + offset; if (wl_erase_range(s_wl, addr, bufsize) != ESP_OK) return -1; if (wl_write(s_wl, addr, buffer, bufsize) != ESP_OK) return -1; s_lastWriteMs = millis(); // จับเวลาไว้ เดี๋ยว loop จะเช็ค idle s_dirty = true; return (int32_t)bufsize; } // ---------------- อัปโหลดไฟล์ 1 ไฟล์ขึ้น server (POST /upload) ---------------- // ส่งแบบสตรีมทีละก้อน ใช้ RAM น้อย รองรับไฟล์ใหญ่กว่า RAM ได้ static bool uploadFile(const char *fullpath, const char *name, size_t size) { WiFiClient client; if (!client.connect(SERVER_HOST, SERVER_PORT)) { Serial.printf(" [upload] ต่อ server ไม่ได้\n"); return false; } // ส่วนหัว HTTP client.printf("POST /upload?name=%s HTTP/1.1\r\n", name); client.printf("Host: %s:%d\r\n", SERVER_HOST, SERVER_PORT); client.print("Content-Type: application/octet-stream\r\n"); client.printf("Content-Length: %u\r\n", (unsigned)size); client.print("Connection: close\r\n\r\n"); // ส่งเนื้อไฟล์ทีละ 1KB FILE *f = fopen(fullpath, "rb"); if (!f) { client.stop(); return false; } uint8_t buf[1024]; size_t sent = 0, n; while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { client.write(buf, n); sent += n; } fclose(f); // อ่าน response สั้นๆ พอให้ server ทำงานจบ uint32_t t0 = millis(); while (client.connected() && millis() - t0 < 3000) { while (client.available()) client.read(); delay(5); } client.stop(); Serial.printf(" [upload] %s (%u bytes) -> OK\n", name, (unsigned)sent); return true; } // ---------------- scan โฟลเดอร์ FAT หาไฟล์ใหม่ แล้วอัปโหลด ---------------- static void syncNewFiles() { DIR *dir = opendir(MOUNT_POINT); if (!dir) return; struct dirent *ent; while ((ent = readdir(dir)) != NULL) { if (ent->d_type != DT_REG) continue; // เอาเฉพาะไฟล์ std::string nm = ent->d_name; if (nm.empty() || nm[0] == '.') continue; // ข้ามไฟล์ซ่อน/ระบบ if (nm == "System Volume Information") continue; if (s_uploaded.count(nm)) continue; // อัปไปแล้ว ข้าม std::string full = std::string(MOUNT_POINT) + "/" + nm; struct stat st; if (stat(full.c_str(), &st) != 0) continue; Serial.printf("[sync] เจอไฟล์ใหม่: %s (%ld bytes)\n", nm.c_str(), (long)st.st_size); if (uploadFile(full.c_str(), nm.c_str(), st.st_size)) { s_uploaded.insert(nm); } } closedir(dir); } void setup() { Serial.begin(115200); delay(300); Serial.println("\n=== Infinite Flash Drive — LEVEL 2 (Cloud-Sync) ==="); // 1) mount FAT (format ให้ถ้ายังไม่เคย) + ได้ handle wl ไว้ต่อ USB esp_vfs_fat_mount_config_t cfg = { .format_if_mount_failed = true, .max_files = 4, .allocation_unit_size = CONFIG_WL_SECTOR_SIZE }; if (esp_vfs_fat_spiflash_mount_rw_wl(MOUNT_POINT, PARTITION_LABEL, &cfg, &s_wl) != ESP_OK) { Serial.println("!! mount FAT ล้มเหลว — เช็ค Partition Scheme (ต้องมี ffat)"); return; } s_sectorSize = wl_sector_size(s_wl); s_sectorCount = (uint32_t)(wl_size(s_wl) / s_sectorSize); // 2) ต่อ USB Mass Storage msc.vendorID("QLAB"); msc.productID("CloudSyncFD"); msc.productRevision("2.0"); msc.onRead(onRead); msc.onWrite(onWrite); msc.mediaPresent(true); msc.begin(s_sectorCount, s_sectorSize); USB.begin(); Serial.println("[USB] ไดรฟ์พร้อม — ลากไฟล์ใส่ได้เลย"); // 3) ต่อ WiFi (ทำเบื้องหลัง ไม่บล็อกการใช้ไดรฟ์) WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); Serial.printf("[WiFi] กำลังต่อ %s ...\n", WIFI_SSID); } void loop() { // แจ้งสถานะ WiFi ครั้งเดียวตอนต่อติด static bool wifiReady = false; if (!wifiReady && WiFi.status() == WL_CONNECTED) { wifiReady = true; Serial.printf("[WiFi] ต่อแล้ว IP=%s\n", WiFi.localIP().toString().c_str()); } // ถ้ามีการเขียน และ PC หยุดเขียนเกิน 1.5 วิ = ก๊อปเสร็จ -> sync ขึ้น cloud if (s_dirty && (millis() - s_lastWriteMs > 1500)) { s_dirty = false; if (WiFi.status() == WL_CONNECTED) { Serial.println("[sync] PC หยุดเขียนแล้ว เริ่ม sync ขึ้น cloud..."); syncNewFiles(); } else { Serial.println("[sync] ยังไม่มี WiFi — ข้ามรอบนี้ไปก่อน"); s_dirty = true; // ลองใหม่รอบหน้า } } delay(50); }