ESP-NOW는 ESP32에서 지원하는 ESP32간의 통신 방식이다. WiFi 방식을 기반하고, 실제로 소스에도 WiFi Station 모드를 명시하는데 정확한 내부 로직은 더 알아봐야 할 것 같다. ESP32가 직접 AP(공유기 역할)을 할 수도 있고 공유기에 연결되어 인터넷과 ESP-NOW 통신을 병행할 수 있어서, 아마 WiFi 방식을 활용한 ESP32만의 기술이 있는 것 같다.
 
공유기에 연결하지 않고 ESP-NOW를 사용하는 방식과 공유기 연결 + ESP-NOW 방식을 둘 다 사용해봤을 땐, ESP-NOW만 사용하는 방식이 패킷 손실이 적고, 통신 거리가 비교적 멀리까지 되어서 이 부분은 조금 더 공부해야할 듯하다..
 
공유기를 함께 사용할 경우 특별히 통신할 모든 ESP32 소스에 채널을 고정해야 통신이 가능하다.

#define CHANNEL 6

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  Serial.println(WiFi.localIP());

  esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE);

  initESPNow();

  server.on("/", handleRoot);
  server.on("/send", HTTP_POST, handleSend);
  server.begin();
}

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.


WEB에서 특정 ID를 입력받으면 해당 정보를 노드에 브로드캐스팅하는 방식으로 만들었는데, WEB도 몇번 입력을 넣다보면 로딩이 걸려서 해결이 필요하다.. 우선 브로드캐스팅을 할 ESP32와 점등할 ESP32를 1m정도 거리를 두고 점등 테스트를 해보았다.
 
구조

서버가 되는 ESP32(공유기에 연결, WEB 호스팅)

클라이언트(몇번 노드가 점멸할지 요청)

노드 ESP32



 
브로드캐스팅용 소스 전문(Master)

#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <WebServer.h>


const char* ssid = "와이파이명";
const char* password = "비밀번호";

#define CHANNEL 6  // 모두 동일하게 설정해야 함

WebServer server(80);

#pragma pack(push, 1)

typedef struct struct_message {
  int ids[30];          
  int count;            
  char label[20];        
  bool ledState;        
} struct_message;
#pragma pack(pop)

struct_message outgoingData;

// 브로드캐스팅용 MAC 주소 셋팅
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};


String inputIDs = "";

void sendESPNowMessage() {

  inputIDs.trim();
  inputIDs.replace(" ", "");
  int idIndex = 0;

  int lastIndex = 0;
  while (true) {
    int commaIndex = inputIDs.indexOf(',', lastIndex);
    String token = (commaIndex == -1) ? inputIDs.substring(lastIndex) : inputIDs.substring(lastIndex, commaIndex);
    outgoingData.ids[idIndex++] = token.toInt();
    if (commaIndex == -1 || idIndex >= 30) break;
    lastIndex = commaIndex + 1;
  }
  outgoingData.count = idIndex;

 
  strcpy(outgoingData.label, "HELLO");
  outgoingData.ledState = true;

// 패킷손실을 고려해서 3번 쏘게 만듬
for (int i = 0; i < 3; i++) {
  esp_err_t result = esp_now_send(broadcastAddress, (uint8_t*)&outgoingData, sizeof(outgoingData));
  if (result == ESP_OK) {
    Serial.printf("전송 성공\n", i + 1);
  } else {
    Serial.printf("전송 실패\n", i + 1);
  }
  delay(50);
}

  inputIDs = "";
}

void handleRoot() {
  String html = "<!DOCTYPE html><html><head><meta charset='utf-8'><title>ESP-NOW Sender</title></head><body>";
  html += "<form action='/send' method='POST'>";
  html += "<input name='ids' placeholder='예: 1,2,3,4' style='width: 300px; height: 40px; font-size: 18px;'><br><br>";
  html += "<input type='submit' value='전송' style='width: 150px; height: 40px; font-size: 18px;'>";
  html += "</form></body></html>";

  server.send(200, "text/html", html);
}

void handleSend() {
  if (server.hasArg("ids")) {
    inputIDs = server.arg("ids");
    Serial.print("ID List : ");
    Serial.println(inputIDs);
    sendESPNowMessage();
  }
  server.sendHeader("Location", "/");
  server.send(303);
}

void initESPNow() {
  if (esp_now_init() != ESP_OK) {
    Serial.println("초기화 실패");
    ESP.restart();
  }

  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.channel = CHANNEL;
  peerInfo.encrypt = false;

  if (!esp_now_is_peer_exist(broadcastAddress)) {
    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
      Serial.println("브로드캐스트 피어 등록 fail");
      return;
    }
  }
}

#define CHANNEL 6

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  Serial.println(WiFi.localIP());

  esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE);

  initESPNow();

  server.on("/", handleRoot);
  server.on("/send", HTTP_POST, handleSend);
  server.begin();
}

void loop() {
  server.handleClient();
}

 
 

DAS Node 구조 설계에 고려해야할 점

> Master는 현재 관리 중인 모든 Node 로부터 통신 정상 수신을 받았는지 여부 체크해야 함

  • Master의 노드 테이블에 100개 노드 ID가 등록되었다면, Sub-Master들은 Slave로부터 peer등록 여부를 100개 모두 받아서 Master에 보고해야 함.
  • 1개 Slave에 N개의 Sub-Master를 Peer등록할 필요가 없으므로 이미 Peer가 등록됐다면 Sub-Master로부터 받는 추가적인 Peer 등록 요청 브로드캐스팅 통신을 무시하도록 로직 필요함(Peer == True : END / Peer == False : Update peer)
  • 클라이언트가 작업 요청을 주면, Master↔Sub-Master↔Slave 점검 절차를 한 사이클 돌게 설계(중간에 노드가 초기화되서 등록된 Peer가 사라진다면? => 점검절차를 매 Job data 송신 때마다 수행? 이런식의 대응책 고민 필요)
  • N개의 브로드캐스팅 및 peer 유니캐스팅이 동시다발적일 경우 통신에 장애가 생기는지 실제로 해봐야 함

> 큐방식으로 한번에 몇개 상품까지 분류할 것인지와 상품들이 분류가되면 추가 큐를 넣어줄 수 있게 설계해야 함

> sub-Master는 ESP-NOW + WIFI 둘다 사용할 수 있어야 함(Flask 서버와 통신은 WIFI, Slave들과는 ESP-NOW)

> Slave Node Status 초기화(작업 마감)

 

기록

> ESP-NOW 브로드캐스팅으로 10m 정도 통신이 됨(사이에 벽은 3개 정도) . 15m까진 될 것 같음

 

Main Server, DAS Node 설계

■ Master : Flask Server

  • Data
    • Order data
      • Order Number
      • Item
      • Item Qty
      • Order SEQ
      • ETC
    • Node data
      • Node ID
      • Node Attribute(Sub-Master/Slave)
      • Location
      • Slave Node Routing Info
    • Func
      • Managing Node Data
      • Receiving order data(From client)
      • Sending Job Queue(To Sub-Master) <= api 호출 or Mqtt 서버 발행 고민 중
      • Receiving Job Status(From Sub-Master)
      • Printing Label

  Sub-Master : ESP32 Node

  • Func
    • Receiving job Queue(From Master)
    • Broadcasting Sub-Master data(MAC addr)
    • Receiving  Peer Status(From Slave)
    • Broadcasting  job data(To Slave)
    • Receiving  job status(From Slave)

  Slave : ESP Node(8 Digits LED, Button)

  • Func
    • Receiving Sub-Master MAC addr
    • Update Sub-Master MAC addr 
    • Receiving job data(From Sub-Master)
    • Update job status
    • Unicasting job status(To Sub-Master)

 

ESP32를 통해 LED를 켜거나, 센서 값을 받기 위해선 아래 각 Pin 마다 역할을 확인하고 사용해야한다.

접지, 전원을 제외하면 모두 GPIO로 되어 있으어 ESP32 ↔ 장치 간 데이터 통신에 필요한 기능들을 사용할 수 있다.

다만 일부 핀들은 입력만 받는다거나, 터치패드에 사용한다던지 하는데, 우선 기본적인 것들만 알면 될 것 같다.

 

핀 기본

GPIO(General Purpose Input/Output) : 범용 입출력 핀

VCC(Voltage common collector) : 전원

GND(GROUND) : 접지

 

INPUT/OUTPUT 통신방식과 신호

:GPIO 핀에서 어떤 역할을 할지 소스코드 단에서 아래와 같은 역할을 셋팅해줘야 한다. 주로 라이브러리에서 각 역할을 어떻게 수행할지 소스가 짜져 있고, 우린 역할마다 어떤 핀에 역할을 배부해줄지 핀맵을 보고 결정한다고 보면 됨

(예 : #define STB 18 <= 18번 핀에 STB 신호를 쏠거니깐, 변수로 저장해둬라)

 

- CLK(CLOCK) : ESP32와 장치 간 정보를 주고받을 때 사용하는 박자를 보내는 신호

  • CLK의 Low → High 또는 High → Low 상하강에 맞춰서 DIO(딸깍 할 때 Data Input/Ouput)

- RTC(Real-Time Clock) : 전원이 연결되어 있지 않더라도, ESP32 내장 배터리로 시간을 추적할 수 있음

  • 내장된 RTC 성능을 신뢰하긴 어려울 것 같고, 외부 RTC 모듈을 사용할 수 있다.
  • 위에 핀맵 상 RTC_GPIO00로 적혀 있음

- STB(STROBE) : 전자 신호에서 특정 타이밍을 알려주는 신호

  • EPS32가 STB 핀에 Low(0V)를 주면, 통신시작.
  • EPS32가 STB 핀에 High(3.3V)를 주면, 통신 종료.

- DIO(Data Input Output) : 데이터를 주거나 받거나할 PIN 정보를 표시할 때 씀

- ADC(Analog to Digital Converter) : 아날로그 신호를 디지털로 변환해줄 수 있는 핀. 아날로그만 할 수 있는건 아니고 아날로그도 가능하다는거라, 디지털 신호도 가능

  • WIFI모듈과 ADC2 블록이 ESP32 칩 내부 자원을 공유해서, WIFI쓸 때 ADC2채널로 된 핀을 쓰면 오류가 날 수 있음
  • WIFI를 쓴다면 ADC1 채널을 활용해야 함

 

CLK, STB 를 통해 장치와 흐름을 맞추고, DIO 핀을 통해 데이터를 주고 받는다고 보면 될 것 같다.

흐름 STB CLK DIO
통신 시작 Low Low 통신 시작 신호
1박 Low Low → (DIO) → High 데이터 입력/출력
2박 Low Low → (DIO) → High 데이터 입력/출력
3박 Low Low → (DIO) → High 데이터 입력/출력
4박 Low Low → (DIO) → High 데이터 입력/출력
통신 종료 High 상관없음 통신 종료 신호

CLK 딸깍한다 치면 딸(0) → 정보가져가거나 줌(0110110) → 깍(1) 이런 식

 

위처럼 Low High로 신호를 보내는 방식은 디지털 방식이라고 한다. 물리적으로 0V(Low)를 주면 논리값이 0이고, 3.3V(High)를 주면 1인 방식. 즉 STB, CLK는 디지털 신호이고 DIO는 상황에 따라 아날로그일 수 있겠다.

 

- I2C(Inter-Integrated Circuit) : 버스 방식으로 한개의 포트에 여러 개의 장치(마스터-N개 슬레이브)를 연결할 때 사용한다. 핀맵을 보면 I2C_SDA, I2C_SCL이 있다.

출처 : 처음 배우는 Arm Cortex-M 마이크로컨트롤러:STM32F103

 

앞서 설명했던 STB, CLK, DIO 3가지로 통신과 데이터 정보를 주고 받았던 것과 다르게, I2C방식은 CLK과 DIO 로만 통신과 데이터를 주고 받는 것으로 보인다.

출처 : 처음 배우는 Arm Cortex-M 마이크로컨트롤러:STM32F103

 

- SCL(Serial Clock) : 마스터, 슬레이브 간 CLK역할을 수행하면서, SDA와 박자가 맞을 때 START, END 상태를 만들 수 있다.

- SDA(Serial Data) : 시리얼 데이터로 통신하고자 하는 데이터. 각 통신에는 ACK가 포함된다.

  • SCL이 High일 때, SDA가 High → LOW로 내려가면 START, SCL이 High일 때 SDA가 LOW → High로 올라가면 STOP
  • 마찬가지로 START와 STOP 사이에 SDA에서 데이터를 전송한다. 8개 데이터를 보내며 1개의 ACK를 수신한다.

- SPI(Serial Peripheral Interface) : 순서대로 각 장치들과 통신하는 방식으로 MOIS(송신), MISO(응답), SCK(STB와 비슷) SS(어떤 장치와 통신할지)로 이루어짐

- UART(Universal Asynchronous Receiver/Transmitter) : TX(전송), RX(수신) 핀으로 1:1 통신. 한개 장치와 간단한 통신을 할 때 쓴다. 

 

핀 번호 별 참고

용도 비고
GPIO 0 입력/출력 부트모드 결정용, 외부 풀업 필요
GPIO 1 TX0 기본 UART TX (USB 통신 시 사용됨)
GPIO 3 RX0 기본 UART RX (USB 통신 시 사용됨)
GPIO 2 LED_BUILTIN 기본 내장 LED 핀 (모듈에 따라 다름)
GPIO 4~19 일반 I/O 대부분 자유롭게 사용 가능
GPIO 21, 22 I2C (SDA, SCL) 기본 I2C로 많이 사용
GPIO 23, 18, 19, 5 SPI용 SPI 버스 기본 설정으로 많이 사용됨
GPIO 34~39 입력 전용 출력 불가 센서 입력 등에 사용
GPIO 6~11 내부 플래시 사용 중 사용 주의 (SPI 플래시와 연결됨)

 

 
 
1. 아두이노 IDE 설치(2.3.6 버전)
: ESP32에 소스코드를 주입하기 위한 에디터(vscode와 같은 역할, vscode로도 컴파일 가능하나 셋팅이 번거로움)
: 장치와 시리얼 통신 기능을 지원해서 디버깅에 용이하다(이 부분은 vscode에서 지원하는지는 모르겠음)
https://www.arduino.cc/en/software/

 

Software

Open-source electronic prototyping platform enabling users to create interactive electronic objects.

www.arduino.cc

윈도우를 사용한다면, "Windows Win 10 and newer, 64 bits"를 선택
 

 
후원할 생각이 없다면 JUST DOWNLOAD를 클릭
 

동의함 선택
 

 
환경변수를 내가 사용하는 PC의 유저 상관없이 전체 Path에 적용한다면 "이 컴퓨터를 사용하는 모든사람(모든 사용자)"를 선택하고, 특정 유저에서만 Path가 적용되려면 "전용(유저명)"을 선택. 복잡하다면 그냥 "이 컴퓨터를 사용하는 모든사람(모든 사용자)" 체크
 

 
저장공간을 10GB정도 여유 생각하고 폴더를 선택(IDE용량은 작으나 보드매니저 등을 설치하는데 용량을 많이 차지한다) 설치를 누르면 아두이노IDE가 설치가 완료된다. 경로에 한글이 있으면 오류가 날 수 있으니, 한글 경로가 있다면 다른 위치에 설치한다.
 
 
2. ESP32 보드매니저 다운로드

ESP32 셋팅을 위한 Github url에 접속하여, 아래로 스크롤 다운
 

Installing (windows, Linux and mac OS)를 클릭하여 들어간다.
 

Stable release link: 에 있는 url을 복사한다(3.3.0 버전)

https://espressif.github.io/arduino-esp32/package_esp32_index.json

아래 url은 3.2.0 버전(복사할 필요 없다)

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json

 
 
 

설치했던 아두이노 IDE에 들어가서, 화면 상단에 File > Preferences를 클릭한다.
 

Additional boards manager URLs 옆에 파란 버튼을 클릭한다.
 

아까 복사했던  url을 붙여넣기 하고 OK버튼 클릭 & OK버튼 클릭
 

 화면 상단에 Tools > Board > Boards Manager 클릭

왼쪽에 검색창이 뜨면, esp32 를 검색한다. 검색결과를 보면 "esp32 by Espressif Systems"로 된 파일의 INSTALL버튼을 누른다.
최신버전을 선택했는데, 호환성에 따라 과거버전을 받아야할 수도 있는지 버전 선택이 가능하다. 설치까지 꽤 긴 시간이 소요된다.

 

화면 상단에 Tools > Board > esp32 > WEMOS LOLIN32를 선택한다. 현재 내가 사용 중인 보드에 맞는 것을 고르면 된다.

 

다시 화면 상단에 Tools > Port > 본인의 ESP32와 연결된 port 선택("COM3"). 어떤 포트에 연결되어 있는지 확인하려면, 장치관리자에서 연결된 포트를 확인하면 된다.

 


 
 
사용한 제품 : ESP32 WIFI + 블루투스 듀얼 모드 WROOM 32 USB C타입 CH340 드라이버
참고 영상 : https://www.youtube.com/watch?v=fYm324KpllQ&list=PLJG0s98ECJWV-frrBS9biVHlVt2kI5rdI

DAS, DPS 등 노드를 활용하여 상품을 분류하는 솔루션을 판매하는 업체는 많이 있다. 근데 시스템 개발비부터, 장치설치, 출장비 등등 너무 많은 비용이 발생한다. 이를 좀 저렴하게 셀프로 제작해서 물류센터에 도입하고 비용절감 해보고자 프로젝트를 시작.

 

혼자하기엔 챙길게 많다보니(돈도 많이 들고) 지난 달에 사내 코딩 동호회 개설 신청하여, 코딩 스터디 + IoT 기기 실습에 관심이 있는 동료들과 활동을 하고 있다(직장인들이라 속도가 많이 더딘 편;;)

 

지금은 문법 기초를 진행하다보니, 동료들과 프로젝트를 같이하긴 힘들고, 혼자 C++과 ESP32보드를 학습하는 중.

구상한 방법으로 최종 결과물이 나올지는 아직 미지수이지만, 지금은 아래처럼 준비를 하고 있다.

 

이 프로젝트를 실현하기 위해 가장 큰 고민이 각각의 노드들이 통신하기 위한 수단을 어떤 것을 사용할지였다.

WiFi를 사용하려면, 노드들이 다 IP주소를 가지고 있어야 하나, 노드 숫자가 255개가 넘어가게 되면 어려움이 있을 것으로 보였다.

 

그래서 찾은 것이 ESP32라는 보드인데, ESP-NOW라는 WiFi기반으로 고정 IP대역 없이 MAC주소를 기준으로 통신할 수 있다.

ESP32로 노드와 Gateway를 구현하고, 상위와 소통은 MQTT를 활용해서 지시와 결과를 통신하려고 생각 중에 있다(IoT에서 MQTT방식을 많이 쓴다고 하는데, api 호출 방식으로 대체할 수 있을지 프로젝트를 해보며 알아볼 예정)

다 구현하고나서, 전력 공급도 문제인데 이 부분은 사내 설비팀 분들께 조언을 좀 구해볼 예정이다.

 

 

 

 

 

 

 

지금까지 들어온 주문 데이터를 업로드하면, 오늘 총 예상 주문수량 그래프를 만들어주는 WEB


 
※ 대시보드 로직
0) 현재까지 주문 인입된 정보 excel 파일을 업로드
1) DB에 축적된 고객 별 시간 대 별 주문인입량을 조회
2) 조회한 시간까지 기준으로 가장 주문인입량이 비슷한 날을 찾음
3) 고객 별로 찾은 값들을 모두 합산함(시간 별로)
4) 그래프로 시간 별 주문 인입량을 표시 함
 
라이브러리

### <라이브러리 임포트>

from dash import Dash, dcc, html, dash_table
from dash.dependencies import Input, Output, State
import dash_bootstrap_components as dbc
import dash_uploader as du
import plotly.graph_objs as go
import datetime
import pymysql
import pandas as pd
import xlwings as xw
import os
import math
import numpy as np
import time

 
이상치를 제거하는 함수

def get_outlier(df=None, column=None, weight=1.5):
  # target 값과 상관관계가 높은 열을 우선적으로 진행
    quantile_25 = np.percentile(df[column].values, 25)
    quantile_75 = np.percentile(df[column].values, 75)
    IQR = quantile_75 - quantile_25
    IQR_weight = IQR*weight
    lowest = quantile_25 - IQR_weight
    highest = quantile_75 + IQR_weight
    outlier_idx = df[column][ (df[column] < lowest) | (df[column] > highest) ].index
    return outlier_idx

def delect_out(df):
	# 고객별로 루프돌기 위함
    for i in df.columns:
        out = get_outlier(df = df, column=i,weight=1.5)
        df.drop(out, axis = 0, inplace = True)
    return df

 

col = ['고객', '날자', '합계', '00:00~01:00', '01:00~02:00','02:00~03:00', '03:00~04:00', '04:00~05:00', '05:00~06:00','06:00~07:00', '07:00~08:00', '08:00~09:00', '09:00~10:00','10:00~11:00', '11:00~12:00', '12:00~13:00', '13:00~14:00','14:00~15:00', '15:00~16:00', '16:00~17:00', '17:00~18:00','18:00~19:00', '19:00~20:00', '20:00~21:00', '21:00~22:00','22:00~23:00', '23:00~24:00', '요일', '주차', '달', '홀짝']
list_col = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]
col_time = ['00:00~01:00', '01:00~02:00', '02:00~03:00', '03:00~04:00',
       '04:00~05:00', '05:00~06:00', '06:00~07:00', '07:00~08:00',
       '08:00~09:00', '09:00~10:00', '10:00~11:00', '11:00~12:00',
       '12:00~13:00', '13:00~14:00', '14:00~15:00', '15:00~16:00',
       '16:00~17:00', '17:00~18:00', '18:00~19:00', '19:00~20:00',
       '20:00~21:00', '21:00~22:00', '22:00~23:00', '23:00~24:00']

엑셀파일 형식
 
가장 비슷한 날을 찾는 함수

def run_data(search_time=9, spread_per=0.05, db_data=None, target_data=None): 


    search_time = search_time
    spread_per = spread_per
    order_tm_df = db_data
    sample_df=target_data
    sample_df.iloc[-1,2] = '전체'
    print(sample_df.iloc[-1,:])

    time_col_list = col_time[:search_time]
    print(time_col_list)
    print('조회 time', col_time[search_time-1])
    predict_seller = pd.DataFrame(columns=['예측량'])
    predict_time_seller = pd.DataFrame()

    for seller in sample_df['고객']:
        
        try :
            ## <고객사 별로 해당 시간대에 물량 gap이 가장 적은 날을 찾음>
            cj_test = abs(order_tm_df.loc[order_tm_df['고객'] == seller, time_col_list].sum(axis =1 ) - sample_df.loc[sample_df['고객']==seller, time_col_list].sum(axis= 1).values[0])
            test = cj_test.sort_values()[:math.ceil(len(cj_test)*spread_per)].index
            raw_df = order_tm_df.loc[:,col_time]
            a = raw_df.iloc[test,:]
            ## </고객사 별로 해당 시간대에 물량 gap이 가장 적은 날을 찾음>



            a = delect_out(a)
            target_val = sample_df.loc[sample_df['고객']==seller,col_time].values[0].sum()
            predict_val = a.sum(axis=1).mean()
            predict_seller.loc[seller, :] = predict_val
            predict_now = (a.loc[:,time_col_list]).sum(axis=1).mean()
            predict_time_seller[seller] = a.mean()
            print(seller,'모수',len(cj_test),'대상 : ', len(test),'예측값',predict_val,'측정 시간까지 예측:',predict_now,'/ 실값', target_val,'정확도:',((target_val-abs(target_val-predict_now))/target_val)*100,'%')
        except:
            print('아직 데이터가 없습니다.')
            predict_seller.loc[seller, :] = '데이터 부족'
            pass
    return predict_time_seller[predict_time_seller.columns.difference(['전체'])].sum(axis=1)

 
엑셀 업로드한 파일을 찾는 함수

def first_data(path_dir):
    files_Path = path_dir
    file_name_and_time_lst = []
    for f_name in os.listdir(f"{files_Path}"):
        written_time = os.path.getctime(f"{files_Path}{f_name}")
        file_name_and_time_lst.append((f_name, written_time))

    sorted_file_lst = sorted(file_name_and_time_lst, key=lambda x: x[1], reverse=True)

    recent_file = sorted_file_lst[0]
    stock_path = recent_file[0]

    return path_dir + stock_path

 
엑셀파일을 조회하는 함수(DRM때문에 바로 접근하는 것은 안되고 엑셀을 한번 물리적으로 켜는 행위)

def open_xls(csv_file):
    app = xw.App(visible=False)
    open_file = xw.Book(csv_file)
    file_to_df = open_file.sheets(1).used_range.options(pd.DataFrame).value.reset_index()
    app.kill()
    return file_to_df

 
WEB 화면(PYTHON PLOTLY DASH)

#### <화면>


external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets =[dbc.themes.BOOTSTRAP])


du.configure_upload(app, "C:\\Users\\cjl\\Desktop\\dev\\time_predcit\\upload")


page_size = 20

app.layout = html.Div(style={'backgroundColor': "#F9F7F7"}, 
 children=[
    
    html.H4(children ="주문인입 TREND 예측 CHART",
           style = {
               "background" : '#112D4E',
               'padding': '30px 30px',
               "color" : '#FFFFFF',
               "font-weight": "bold"
           }),
    html.Br(),
    html.Div(children = '오차 범위 설정(%)',
            style = {
                "font-weight" : "bold"
            }),
    dcc.Input(
        id='my-input',
        value='0.05',
        type = 'text'
    ),
    du.Upload(text = '시간별주문실적 엑셀 업로드',
            text_completed = '업로드 완료 :'),
    
    html.Button(id='submit-button-state', n_clicks=0, children='조회',
               style = {
                   'padding' : '8px 20px',
                   'background' : '#112D4E',
                   'color' : 'white'
               }),
    html.Br(),
    html.Br(),
    html.Div(children = '결과 조회',
            style = {
                'font-weight' : 'bold'
            }),

    dcc.Graph(id='my-graph'),
    html.Div(id='output-total')
     
    
])

 
화면 연동

@app.callback(Output('my-graph', 'figure'),Output('output-total','children'),
              Input('submit-button-state', 'n_clicks'),
              State('my-input', 'value'),
              prevent_initial_call = True,
             )
def show_fig(n_clicks, input1):
    print('오차범위:',input1)
    print('파일경로:',first_data(first_data('MY-DIR-PATH')+"\\"))
    os.system("python ./importexcel.py")
    print('대기완료')
    upload_data = pd.read_excel('MY-DIR-PATH + IMPORT EXCEL FILE NAME')
    conn = pymysql.connect(host='127.0.0.1', user='root', password='my-password', db='order_info', charset='utf8')
    cur = conn.cursor()
    qer = "SELECT * FROM time_order"
    cur.execute(qer)
    result = cur.fetchall()
    res = pd.DataFrame(result)
    res.columns = col
    res = res.loc[(res['요일']!=1)|(res['요일']!=2),:]
    db_data = res
    plot_db = run_data(search_time=datetime.datetime.now().hour,spread_per=float(input1),db_data = db_data, target_data=upload_data)
    print(plot_db)
    fig = go.Figure(data=[go.Scatter(x=plot_db.index.values, y=plot_db.values, mode = "lines+markers")])
    total_val = '당일 TOTAL 물량 누계 :{}'.format(plot_db.values.sum())

    return fig, total_val

앱실행

### <앱실행>
if __name__ == '__main__':
    app.run_server(debug=False,host = '0.0.0.0', port = '8070')

 

+ Recent posts