티스토리 뷰

[목차]

  • Python Code
  • Arduino Code

* 소스 코드 : OTA264V011_Pyduino260408W.zip

[Python]


# pip3 install Flask flask-sqlalchemy apscheduler waitress requests python-dotenv --break-system-packages

import os
from flask import Flask, request, jsonify, render_template, send_file, redirect, url_for, session
from werkzeug.utils import secure_filename
from models import db, Device, Firmware, OtaTask, AutoRule, CompileJob
from compiler import background_compile
from concurrent.futures import ThreadPoolExecutor
from apscheduler.schedulers.background import BackgroundScheduler
import time
import shutil
import sys
from dotenv import load_dotenv

FOLDERPATH_COMMONLIBRARIES = "/root/commonlibs"

sys.path.append(FOLDERPATH_COMMONLIBRARIES)
envPath = FOLDERPATH_COMMONLIBRARIES + "/.env"
load_dotenv(envPath)
from BMessenger import bMessenger
from BTime import bTime

BASE_DIR = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
# 보안: 실제 환경에서는 랜덤 키 사용
app.config['SECRET_KEY'] = 'termux-ota-key264'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(BASE_DIR, 'ota.db')
app.config['UPLOAD_FOLDER_BIN'] = os.path.join(BASE_DIR, 'firmwares', 'uploads')
app.config['UPLOAD_FOLDER_ZIP'] = os.path.join(BASE_DIR, 'firmwares', 'sources')

DIR_BUILDS = os.path.join(BASE_DIR, 'firmwares', 'builds')
DIR_COMPILED = os.path.join(BASE_DIR, 'firmwares', 'compiled')

for folder in [app.config['UPLOAD_FOLDER_BIN'], app.config['UPLOAD_FOLDER_ZIP'], DIR_BUILDS, DIR_COMPILED]:
    os.makedirs(folder, exist_ok=True)


db.init_app(app)
with app.app_context():
    db.create_all()


# ==== 빌드된 파일 쓰레기 수집기 (Garbage Collector) ====
def cleanup_old_files():
    print("[Garbage Collector] 30분 이상 경과된 임시 폴더 및 bin 정리 스캔...")
    now = time.time()
    for directory in [DIR_BUILDS, DIR_COMPILED]:
        if not os.path.exists(directory): continue
        for filename in os.listdir(directory):
            filepath = os.path.join(directory, filename)
            # 3600초 (1시간) 초과 시 삭제
            if os.path.getmtime(filepath) < now - 1800:
                try:
                    if os.path.isdir(filepath):
                        shutil.rmtree(filepath)
                    else:
                        os.remove(filepath)
                    print(f"[Garbage Collector] 삭제됨: {filepath}")
                except Exception as e:
                    print(f"[Garbage Collector] 삭제 실패 {filepath}: {e}")


# 스케쥴러 생성 및 시작 (프로세스 종료 시까지 백그라운드 구동)
scheduler = BackgroundScheduler()
scheduler.add_job(func=cleanup_old_files, trigger="interval", seconds=600) # 10분마다 스캔
scheduler.start()


# 스마트폰 과부하 방지를 위해 컴파일 스레드 최대 개수 제한 (2개)
compile_executor = ThreadPoolExecutor(max_workers=2)


def login_required(f):
    def wrapper(*args, **kwargs):
        if not session.get('is_admin'): return redirect(url_for('login'))
        return f(*args, **kwargs)
    wrapper.__name__ = f.__name__
    return wrapper

@app.route('/')
def index():
    return redirect(url_for('admin'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        adminPW = os.getenv('PW_OTA264_ADMIN')
        # 아주 단순한 하드코딩 인증 (보안 요구사항 충족)
        if request.form['id'] == 'admin' and request.form['pw'] == adminPW:
            session['is_admin'] = True
            return redirect(url_for('admin'))
        return "인증 실패"
    return '''<form method="post">ID: <input name="id"><br>PW: <input type="password" name="pw"><br><button>로그인</button></form>'''


@app.route('/admin')
@login_required
def admin():
    devices = Device.query.all()
    return render_template('admin.html', devices=devices) # Admin UI를 분리된 템플릿으로 제공


# ==== [방식 1] 관리자 전용 bin 파일 업로드 (보안) ====
@app.route('/admin/upload_bin', methods=['POST'])
@login_required
def upload_bin():
    file = request.files.get('file')
    if not file: return "파일 없음", 400
    fname = secure_filename(file.filename)
    if not fname.endswith('.bin'): return "bin 확장자만 허용", 400
   
    path = os.path.join(app.config['UPLOAD_FOLDER_BIN'], fname)
    file.save(path)
   
    # DB 저장
    fw_name = request.form.get('name', fname)
    new_fw = Firmware(name=fw_name, file_path=path)
    db.session.add(new_fw)
    db.session.commit()
    
    # deviceType이 기입된 경우, 관련된 모든 등록 장치에 수동 할당 큐(Task)를 자동 생성
    device_type = request.form.get('device_type')
    if device_type and device_type.strip() != "":
        devices = Device.query.filter_by(type=device_type).all()
        for d in devices:
            old_tasks = OtaTask.query.filter(OtaTask.device_mac==d.mac, OtaTask.status.in_(['WAITING', 'IN_PROGRESS'])).all()
            for ot in old_tasks: ot.status = 'CANCELED'
            
            task = OtaTask(device_mac=d.mac, firmware_id=new_fw.id, status='WAITING')
            db.session.add(task)
        db.session.commit()
        return f"방식1 펌웨어({fw_name}) 업로드 및 {len(devices)}대({device_type})에 일괄 배포 예약 완료!"

    return f"방식1 펌웨어({fw_name}) 수동 업로드 완료 (기본: 패널 3에서 수동 배정)"


# ==== [방식 1] 사용자가 기기에 특정 펌웨어 매핑 (Task Queue 생성) ====
@app.route('/user/assign', methods=['POST'])
def assign_firmware():
    # 사용자가 웹에서 자신의 기기 MAC과 원하는 기능 번호를 입력했다 가정
    mac = request.form['mac']
    fw_id = request.form['fw_id']
    
    old_tasks = OtaTask.query.filter(OtaTask.device_mac==mac, OtaTask.status.in_(['WAITING', 'IN_PROGRESS'])).all()
    for ot in old_tasks: ot.status = 'CANCELED'
    
    task = OtaTask(device_mac=mac, firmware_id=fw_id, status='WAITING')
    db.session.add(task)
    db.session.commit()
    return "펌웨어가 할당 완료되었습니다. (장치가 접속 시 다운로드됩니다)"


# ==== [방식 2] 관리자 전용 ino 소스코드 zip 패키지 업로드 ====
@app.route('/admin/upload_zip', methods=['POST'])
@login_required
def upload_zip():
    file = request.files.get('file')
    if not file: return "파일 없음", 400
    fname = secure_filename(file.filename)
    if not fname.endswith('.zip'): return "zip 확장자만 허용", 400
   
    path = os.path.join(app.config['UPLOAD_FOLDER_ZIP'], fname)
    file.save(path)
   
    device_type = request.form['device_type']
    target_version = request.form['version']
   
    # Rule 생성 (새 버전으로 갱신)
    rule = AutoRule.query.filter_by(device_type=device_type).first()
    if rule:
        rule.latest_version = target_version
        rule.source_zip_path = path
    else:
        db.session.add(AutoRule(device_type=device_type, latest_version=target_version, source_zip_path=path))
    db.session.commit()
    return f"방식2 규칙({device_type} -> {target_version}) 생성 완료!"


# ==== [API] 아두이노 접속용 엔드포인트 ====

DEVICE_API_KEY = "SECRET_DEVICE_KEY_2026"

def check_device_auth(req):
    return req.headers.get('x-api-key') == DEVICE_API_KEY

def register_device_db(mac, dtype=None):
    if not mac: return
    device = Device.query.filter_by(mac=mac).first()
    if not device:
        db.session.add(Device(mac=mac, type=dtype))
        db.session.commit()
    elif dtype and device.type != dtype:
        device.type = dtype
        db.session.commit()

@app.route('/api/ota/manual', methods=['GET'])
def ota_manual():
    if not check_device_auth(request):
        return "Unauthorized", 401
        
    mac = request.args.get('mac')
    register_device_db(mac)
    
    # WAITING (1번째 접근 - 헤더 확인) 또는 IN_PROGRESS (재시도 및 다운로드)
    task = OtaTask.query.filter(OtaTask.device_mac==mac, OtaTask.status.in_(['WAITING', 'IN_PROGRESS'])).first()
    if task:
        fw = db.session.get(Firmware, task.firmware_id)
        if fw and os.path.exists(fw.file_path):
            if task.status == 'WAITING':
                # 첫 접속 시도 시 IN_PROGRESS 로만 변경. (장부를 여기서 닫지 않음)
                task.status = 'IN_PROGRESS'
                db.session.commit()
                
            # IN_PROGRESS 상태에서는 언제든, 몇 번이든 실패 후 재시도해도 파일을 보내줌
            safe_path = os.path.abspath(os.path.join(BASE_DIR, fw.file_path) if not os.path.isabs(fw.file_path) else fw.file_path)
            return send_file(safe_path, as_attachment=True)
            
    return "No manual task", 304 


@app.route('/api/ota/success', methods=['GET'])
def ota_success():
    """ 아두이노가 플래시 쓰기를 완벽히 성공하고 재부팅 직전에 호출하여 장고를 DONE 마감하는 곳 """
    if not check_device_auth(request): return "Unauthorized", 401
    
    mac = request.args.get('mac')
    task = OtaTask.query.filter_by(device_mac=mac, status='IN_PROGRESS').first()
    if task:
        task.status = 'DONE'
        db.session.commit()
        return "Complete", 200
    return "None", 304


@app.route('/api/ota/auto', methods=['GET'])
def ota_auto():
    if not check_device_auth(request):
        return "Unauthorized", 401
        
    mac = request.args.get('mac')
    dtype = request.args.get('type')
    version = request.args.get('version')
    register_device_db(mac, dtype)
   
    rule = AutoRule.query.filter_by(device_type=dtype).first()
    if not rule or version == rule.latest_version:
        return "No auto update needed", 304


    # 현재 장치/버전에 매칭되는 컴파일 작업이 있는지 점검
    job = CompileJob.query.filter_by(device_mac=mac, target_version=rule.latest_version).first()
   
    if not job:
        # DB에 컴파일 작업 큐 생성 후 백그라운드 구동!
        new_job = CompileJob(device_mac=mac, target_version=rule.latest_version)
        db.session.add(new_job)
        db.session.commit()
       
        # ThreadPool을 사용한 비동기 백그라운드 컴파일 (Waitress 블로킹 회피)
        compile_executor.submit(
            background_compile,
            app, db, CompileJob, new_job.id, rule.source_zip_path, mac, rule.latest_version
        )
        return jsonify({"status": "compiling", "retry_after": 30}), 202
       
    elif job.status == 'COMPILING':
        # 아직 Waitress 스레드 외부에서 컴파일이 도는 중
        return jsonify({"status": "progress", "retry_after": 30}), 202
       
    elif job.status == 'DONE':
        if job.bin_path and os.path.exists(job.bin_path):
            return send_file(job.bin_path, as_attachment=True)
        return "Bin file lost", 404
       
    elif job.status == 'FAIL':
        return "Compile failed", 500


if __name__ == '__main__':
    from waitress import serve
    # Termux 환경의 비루트(Non-Root) 계정 실행을 고려하여 80포트 대신 26410 바인딩
    print("Waitress Server is walking on localhost:26410")
    message1 = "[!] OTA264V01 Started. @ " + bTime.GetTimeString()
    bMessenger.SendTelegram(message1)
    serve(app, host='0.0.0.0', port=26410)

 

[Arduino]

#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <Update.h>
#include <WiFiManager.h> // ESP32 AP Provisioning (캡티브 포털용)
#include <Preferences.h> // 비휘발성 플래시 메모리(NVS) 저장소

// 분리된 서드파티 라이브러리 및 소스를 테스트하기 위한 헤더
#include "sensor.h"

// ==== [보안구역] 초기 비상용 핫스팟 (배포 시 라인 전체 삭제 가능) ====
//#define DEFAULT_SSID "default-ssid"
//#define DEFAULT_PW "default-pw"
// =======================================================
#define FLAG_FACTORY_RESET false

// 서버 통신 설정
const String ota_server_domain = "http://192.168.0.87:26410";
const String ota_server_auto = ota_server_domain + "/api/ota/auto";
const String ota_server_manual = ota_server_domain + "/api/ota/manual";

String deviceMAC = "";
String deviceType = "BiliMon264";
String currentVersion = "V1.5"; // 서버에 상위 버전을 등록하면 변경을 감지합니다.
const String API_KEY = "SECRET_DEVICE_KEY_2026"; 

// [환경 변수 복원 플래그] true: 과거 NVS 메모리 유지 / false: 이하의 새 코드값으로 강제 덮어쓰기
bool reloadFlag = true;
int myStateVal = 0; // 테스트용: 업데이트 이력 보존용 카운터

// [하드웨어 공장초기화 플래그] (주의!) true로 두고 코드를 업로드하면 부팅 직후 모든 기억을 포맷시킵니다.
bool DO_FORMAT_NVS = FLAG_FACTORY_RESET; 

unsigned long lastPollTime = 0;
unsigned long pollInterval = (300L*1000); 

Sensor MySensor; 
Preferences preferences; 
WiFiMulti wifiMulti;

#define BOOT_BUTTON 0 // ESP32 기본 시스템 BOOT 버튼 (GPIO 0)


int outCount = 1;
int outMultiplier = 5;

void setup() {
  Serial.begin(115200);
  delay(1000);
  
  pinMode(BOOT_BUTTON, INPUT_PULLUP);
  
  // 1. Preferences(NVS) 초기화
  preferences.begin("ota_app", false);
  
  // ---------------- [2. 공장초기화 감지 트리거 (수동/자동)] ----
  // [A] 코딩으로 강제 지시 (DO_FORMAT_NVS)
  if (DO_FORMAT_NVS == true) {
    clearAllNVSData(); 
    // clearAllNVSData() 내부에서 무한 루프에 빠지므로 이후 코드 진행 안 됨.
  }

  // [B] 물리 버튼으로 강제 지시 (BOOT 3초간 누름 -> AP모드 전환용)
  Serial.println("\n[시스템] 공유기 비밀번호를 바꾸고 싶으시다면(AP모드 전환) 지금 BOOT 버튼을 3초간 누르세요...");
  delay(100); 
  if (digitalRead(BOOT_BUTTON) == LOW) {
    long startTime = millis();
    bool resetTriggered = true;
    while(millis() - startTime < 3000) {
      if (digitalRead(BOOT_BUTTON) == HIGH) {
        resetTriggered = false;
        break; 
      }
      delay(10);
    }
    if (resetTriggered) {
      switchToAPMode(); // 내부 데이터는 살리고 WiFi만 지우는 전용 함수 호출
    }
  }
  // --------------------------------------------------------

  // ---------------- [3. 변수 리로드 로직] --------------------
  if (reloadFlag == true) {
    myStateVal = preferences.getInt("my_state", myStateVal);
    Serial.printf("\n[NVS] 예전 데이터 복원 완료 (유지 모드 작동): %d\n", myStateVal);
  } else {
    preferences.putInt("my_state", myStateVal);
    Serial.printf("\n[NVS] 새로운 코드값으로 데이터 덮어쓰기 완료 (초기화 모드 작동): %d\n", myStateVal);
  }
  // --------------------------------------------------------

  // ---------------- [4. 무선망 신원정보 로드 및 매크로 갱신] --
  String fb_ssid = preferences.getString("fb_ssid", "");
  String fb_pw = preferences.getString("fb_pw", "");
  String user_ssid = preferences.getString("user_ssid", "");
  String user_pw = preferences.getString("user_pw", "");

  // 전처리기를 통한 삭제 가능형 하드코딩 와이파이 투입
#ifdef DEFAULT_SSID
  String code_ssid = DEFAULT_SSID;
#else
  String code_ssid = "";
#endif

#ifdef DEFAULT_PW
  String code_pw = DEFAULT_PW;
#else
  String code_pw = "";
#endif

  // 코드에 기본값이 적혀있다면 (삭제되지 않았다면) 플래시의 기존 정보를 이것으로 강제 갱신합니다.
  if (code_ssid != "") {
    fb_ssid = code_ssid;
    fb_pw = code_pw;
    preferences.putString("fb_ssid", fb_ssid);
    preferences.putString("fb_pw", fb_pw);
    Serial.println("[NVS] 소스코드 내 고정 핫스팟이 감지되어 플래시에 영구 내재화(오버라이드) 되었습니다.");
  } else {
    Serial.println("[NVS] 소스코드에서 고정 핫스팟이 지워진 상태입니다. 플래시 메모리에서만 가져옵니다.");
  }
  // --------------------------------------------------------

  // ---------------- [5. WiFiMulti 다중 후보군 등록 및 스캔] ---
  Serial.println("\n[WiFiMulti] 접속 후보들을 배터리에 장전합니다...");
  if (fb_ssid != "") {
    wifiMulti.addAP(fb_ssid.c_str(), fb_pw.c_str());
    Serial.println(" -> 고정/비상망 장전 완료: " + fb_ssid);
  }
  if (user_ssid != "") {
    wifiMulti.addAP(user_ssid.c_str(), user_pw.c_str());
    Serial.println(" -> 사용자 동적망 장전 완료: " + user_ssid);
  }

  // 연결 시도 (약 10초 대기하며 후보 중 가장 강한 놈 탐색)
  Serial.println("[WiFiMulti] 주변 AP 스캐닝 및 통신망 연결 시도 중... (최대 10초)");
  bool connected = false;
  int attempts = 0;
  while(attempts < 10) { // 1초씩 10번 = 약 10초
      if(wifiMulti.run() == WL_CONNECTED) {
          connected = true;
          break;
      }
      delay(1000);
      attempts++;
      Serial.print(".");
  }
  // --------------------------------------------------------

  // ---------------- [6. 연결 실패 시 AP 설정 모드 비상 탈출] -
  if (!connected) {
    Serial.println("\n\n[장애] 등록된 두 와이파이를 모두 방어하지 못했거나 암호가 틀렸습니다!");
    Serial.println("[WiFiManager] 스마트폰 설정을 위한 AP(BiliMon_Setup) 강제 진입합니다.");
    
    WiFiManager wm;
    // 만약 사용자가 웹 캡티브 포털에서 새로운 AP를 던져주면 접속 시도
    if (!wm.startConfigPortal("BiliMon_Setup")) {
      Serial.println("[WiFiManager] 세팅창 켜짐 대기시간 초과 (보드 재시작)");
      delay(3000);
      ESP.restart();
    }
    
    // 이 구문에 도달했다면 무사히 웹 포털에서 암호를 입력받고 붙은 상태임
    String new_user_ssid = WiFi.SSID();
    String new_user_pw = WiFi.psk();
    
    // (선택 사항) WiFiManager가 내부적으로 오만가지 잡동사니 AP 이력을 쌓는 것을 방지하기 위해 
    // 여기서 수동으로 1개만 user_ssid 공간에 발라버리고 나머지는 관리에서 배제합니다.
    if (new_user_ssid != "") {
       preferences.putString("user_ssid", new_user_ssid);
       preferences.putString("user_pw", new_user_pw);
       Serial.println("\n[NVS] 스마트폰으로 전송받은 새로운 와이파이 단일 등록 완료: " + new_user_ssid);
    }
  }
  // --------------------------------------------------------

  deviceMAC = WiFi.macAddress();
  Serial.println("\n===== BiliMon Nano ESP32 =====");
  Serial.println("네트워크 정상 접속 완료!");
  Serial.println("현재 연결망: " + WiFi.SSID());
  Serial.println("내부 IP: " + WiFi.localIP().toString());
  Serial.println("MAC 주소: " + deviceMAC);
  Serial.println("현재 버전: " + currentVersion);
 
  MySensor.init();
}

void loop() {
  MySensor.readData();
 
  if (millis() - lastPollTime > pollInterval) {
    lastPollTime = millis();
    Serial.print(outCount);
    Serial.print(", ");
    Serial.println(outMultiplier);
    outCount += 1;
    
    checkManualOTA();
    checkAutoOTA();
  }
}

// ----------------------------------------------------
// 원격 바이너리 스트림 다운로드 및 플래시 쓰기 공통 함수
// ----------------------------------------------------
void performOTA(String downloadUrl) {
  Serial.println("\n[OTA Agent] 플래시 다운로드를 시작합니다 (경로: " + downloadUrl + ")");
  WiFiClient client;
  HTTPClient http;
 
  http.begin(client, downloadUrl);
  http.addHeader("x-api-key", API_KEY); 
  int httpCode = http.GET();
 
  if (httpCode == HTTP_CODE_OK) { 
    int contentLength = http.getSize();
    Serial.printf("[OTA Agent] 파일 수신 완료. 크기: %d bytes\n", contentLength);
   
    bool canBegin = Update.begin(contentLength);
    if (canBegin) {
      Serial.println("[OTA Agent] 메모리 확보 성공. 쓰기를 시작합니다...");
      size_t written = Update.writeStream(client);
     
      if (written == contentLength) {
        Serial.println("[OTA Agent] 스트림 100% 작성을 완료했습니다.");
      }
     
      if (Update.end()) {
        Serial.println("[OTA Agent] 성공! 새 버전 적용을 위해 2초 뒤 기기를 재부팅합니다.");
        
        Serial.println("[OTA Agent] 서버에 완료 콜백 전송 중...");
        HTTPClient httpAck;
        httpAck.begin(ota_server_domain + "/api/ota/success?mac=" + deviceMAC);
        httpAck.addHeader("x-api-key", API_KEY);
        httpAck.GET();
        httpAck.end();
        
        // ----- [NVS] 데이터 보존 테스트 백업 -----
        myStateVal++; 
        preferences.putInt("my_state", myStateVal);
        Serial.printf("[NVS] 업데이트 누적 성공 횟수(%d) 등 중요 변수를 플래시에 강제 백업합니다.\n", myStateVal);
        // ---------------------------------------

        delay(2000);
        ESP.restart(); 
      } else {
        Serial.println("[OTA Agent] 체크섬 에러 등 플래싱 실패. (코드:" + String(Update.getError()) + ")");
      }
    } else {
      Serial.println("[OTA Agent] 저장 공간이 부족합니다.");
    }
  } else {
    Serial.println("[OTA Agent] 서버 통신 불능 상태 (코드:" + String(httpCode) + ")");
  }
  http.end();
}

void checkManualOTA() {
  String url = ota_server_manual + "?mac=" + deviceMAC;
  HTTPClient http;
  http.begin(url);
  http.addHeader("x-api-key", API_KEY);
  int httpCode = http.GET();
  http.end(); 
 
  if (httpCode == 200) {
    Serial.println("[CLI 상태창] 사용자 지정 펌웨어가 대기중입니다! OTA 진입.");
    performOTA(url);
  }
}

void checkAutoOTA() {
  String url = ota_server_auto + "?mac=" + deviceMAC + "&type=" + deviceType + "&version=" + currentVersion;
  HTTPClient http;
  http.begin(url);
  http.addHeader("x-api-key", API_KEY);
  int httpCode = http.GET();
  http.end();
 
  if (httpCode == 202) {
    Serial.println("[Auto-OTA] 서버가 컴파일을 진행하고 있습니다. 30초 딜레이...");
    pollInterval = 30000; 
  } else if (httpCode == 200) {
    Serial.println("[Auto-OTA] 최신 펌웨어가 발견되었습니다. 즉시 다운로드 합니다.");
    performOTA(url);
  }
}

// ----------------------------------------------------
// 사용자 와이파이 설정 지우기 (데이터 보존 + AP모드 전환용)
// ----------------------------------------------------
void switchToAPMode() {
  Serial.println("\n[시스템] 버튼 요청에 따라 와이파이 기록만 삭제하고 AP 모드로 진입합니다.");
  
  WiFiManager wm;
  wm.resetSettings(); // 기존 WiFi 캐시 전부 소실
  
  preferences.begin("ota_app", false);
  preferences.putString("user_ssid", ""); // 유저가 등록했던 핫스팟 기록만 지움
  preferences.putString("user_pw", "");
  // 주의: preferences.clear()를 쓰지 않으므로 my_state 등 다른 기기 데이터는 보존됩니다!
  
  Serial.println("[안내] 와이파이 기록 부분 소거 완료! 보드를 재시작합니다.");
  delay(1000);
  ESP.restart(); 
}

// ----------------------------------------------------
// 물리적 혹은 소프트웨어적 공장 초기화 통합 함수
// ----------------------------------------------------
void clearAllNVSData() {
  Serial.println("\n[시스템] 🚨 펌웨어 코드 지시에 의한 <기기 전체 공장 포맷>을 시작합니다!");
  
  WiFiManager wm;
  wm.resetSettings(); 
  
  preferences.begin("ota_app", false);
  preferences.clear(); // 내부 중요 변수들까지 완전 삭제
  
  Serial.println("[NVS포맷] 모든 데이터가 소멸되었습니다. 기기가 초기 생산 상태로 굳어졌습니다.");
  
  if (DO_FORMAT_NVS) {
    Serial.println("[안내] 코드로 포맷이 지시된 상태입니다! 코드 상단 DO_FORMAT_NVS를 거꾸로 false로 바꾸고 다시 업로드 하십시오.");
    while(1) { delay(1000); } 
  }
}

 

 

 

 

 

 

반응형
반응형
250x250
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함