미니프로젝트 - ESPNOW LED

DongHee Lim·2022년 5월 22일
0

미니프로젝트

목록 보기
1/1
post-thumbnail

서론


학교 동기가 Universal Robots 의 로봇 팔을 이용한
Opencv + 파이썬 스크립트로
글씨를 쓰는 프로젝트를 한다고 들었다.

그래서, 프로젝트에서 글씨를 쓰기 위한
원격 LED 다중제어가 필요하다는 의뢰를 받았다.

원격으로 제어하기 위해 WiFi, Bluetooth, RF 중에 하나를 선택해야했다.
최근, 아두이노 OTA를 사용했던 경험이 있어서 WiFi로 선택하였다.
LED를 카페의 진동벨처럼 충전만하는 완전 독립식으로 두고 싶었다.

사용 보드는 가장 많이 보유하고 있는 ESP32를 사용하기로 했다.
사실, ESP8266 또는 ESP8285 정도로도 충분하지만 기간이 일주일밖에 없었기에
크기와 스펙이 커질 수 밖에 없었다.


사용 보드 : WEMOS D1 Mini ESP32
개발 환경 : Arduino IDE

<부품>
1. SK6812 5050 LED
2. 0.96 inch OLED [ I2C ]

<라이브러리>
1. ESP-NOW (WiFi)
2. AsyncElegantOTA
3. SPIFFS
4. Adafruit Neopixel
5. Adafruit OLED
6. TelnetStream


라이브러리 설치


1. ElegantOTA

사용하는 기능이 생각외로 많이 들어가 있어서 추가해야할 라이브러리가 많다.
예전에 사용하던 ArduinoOTA는 작동이 잘 안되어서 ElegantOTA 라이브러리를 사용하기로 했다.

[사용 방법 Tutorial 링크] <- 사용 방법이 자세하게 나와있다.

  1. AsyncElegantOTA
  2. ESPAsyncWebServer -> [ZIP 다운로드하려면 누르세요]
  3. AsyncTCP ->[ZIP 다운로드하려면 누르세요]

OTA 를 사용하기 위해서는 이 세가지를 추가해야한다.


Arduino IDE 메뉴바에서
스케치 -> 라이브러리 포함하기 -> 라이브러리 관리 -> AsyncElegantOTA
를 입력하면 다운 받을 수 있다.

나머지 2개는 위에서 ZIP파일을 다운받거나 Tutorial 링크에서 받아도 된다.
다운 받은 후에는 알집을 풀지 않고

스케치 -> 라이브러리 포함하기 -> .ZIP 라이브러리 추가
를 하나씩 하면 된다.

// ESP32 전용 
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>

const char* ssid = "........";
const char* password = "........";

AsyncWebServer server(80);		// 80 번 포트


void setup(void) {
  Serial.begin(115200);					
  WiFi.mode(WIFI_STA);					// Station 모드
  WiFi.begin(ssid, password);			// 와이피이 연결하기
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {	// 연결될때까지 대기
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());		// IP 주소


  //  서버 준비 <- 이 부분은 HTML, CSS, JS 파일을 이용해 제어용으로 꾸밀 수 있다.
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(200, "text/plain", "Hi! This is a sample response.");
  });

  AsyncElegantOTA.begin(&server);    // Start AsyncElegantOTA
  server.begin();				 	// 서버 시작
  Serial.println("HTTP server started");
}

void loop(void) {
}

2. WiFiManager

스케치 -> 라이브러리 포함하기 -> 라이브러리 관리 -> WiFiManager

#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager


void setup() {
    WiFi.mode(WIFI_STA); 
    Serial.begin(115200);
    
    WiFiManager wm;  							
    wm.resetSettings();							// 세팅 초기화
    bool res;

    res = wm.autoConnect("원하는 이름");	 		// 주석을 바꿔줘야함
    if(!res) {
        Serial.println("Failed to connect");
        // ESP.restart();						// ESP 재시작 코드 필요하면 사용
    } 
    else {
        //if you get here you have connected to the WiFi    
        Serial.println("connected...yeey :)");
    }

}

void loop() {
    
}

3. SPIFFS

SPI + Flash File System
즉, ESP 보드가 가지고 있는 Flash 메모리에 접근하는 것이다.

SPIFFS 를 그냥 사용할 때는 설치가 필요없다.
하지만, 나중에 제어용 WebServer 까지 생각한다면 설치 해야할 것이 있다.
굳이 필요가 없다면 넘어가도 좋다.

[ ESP32 Data Upload Tool 링크 ]
[ ESP8266 Data Upload Tool 링크 ]

[아두이노 SPIFFS 참고 블로그]
위 블로그는 정말 정리가 잘 되어있으니 꼭 확인하는게 좋다.

이 프로젝트에서 핵심으로 알아야하는 것은

Flash 메모리에 어떤 파일이 있는지 확인하는 ListDirectory 함수
WiFi SSID, PASSWORD 를 파일에 저장하는 Write 함수
작성한 파일을 읽어오는 Read 함수

이 3가지이다.

1. ListDirectory

2. Write

3. Read


4. OLED

OLED 를 사용할 때에는

제품에 따라 드라이버가 다르다는 것을 알아야한다.

내가 사용한 0.96 인치 OLED 모듈은
SSD 1306 드라이버 IC 를 사용하고 해상도는 128x64 이다.
일반적으로 SSD1306 OLED I2C 장치의 7bits 주소는 0x3C 이다.
가끔 종종 0x3D 인 것이 있으니 데이터 시트를 참고하는 것이 좋다.

라이브러리가 없다면 데이터시트를 보고
레지스터를 하나하나 입력하고 폰트도 한땀한땀 만들어야하지만
아두이노에는 Wire.h 라는 좋은 I2C 라이브러리와 Adafruit 덕분에
간편하게 사용 가능하다.

#include <SPI.h>		// i2c  통신일 때는 사용 안함
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)

// default pin -> [ SDA : 21 ], [ SCL : 22 ]  ESP32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void initOLED(){
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
    Serial.println(F("SSD1306 allocation failed"));
    for(;;);
  }
  delay(2000);
}

OLED 함수

  display.clearDisplay();			// 화면 초기화
  display.setTextSize(1);			// 텍스트 사이즈 2로하면 글자 몇개 못씀
  display.setTextColor(WHITE);		// 색생은 제품에 따라 다름
  display.setCursor(0, 5);			// 가로 (0~127), 새로(0~63) , 좌측 상단이 0,0
  display.println("Hello, world!");	// String 작성
  display.display(); 				// 출력

만약, I2C 통신용 SDA, SCL 를 설정하고 싶다면
Oled 를 init 하기 전에 아래 코드를 입력하면된다.

Wire.begin( 원하는 SDA 핀번호, 원하는 SCL 핀번호 );

5. Neopixel

내가 아두이노를 사서 처음 해보고 싶었던 것이 바로 Neopixel 이다.

Neopixel 은 다양한 모양과 LED 컨트롤러가 있어서
원하는 용도에 맞게 사용하면 되고 작동 방식은 모두 같다.

네오픽셀의 동작을 직접 구현하는 건 힘들기에

예제에서 원하는 종류를 선택하면 된다.

내가 사용한 LED는 한개 짜리를 6개 이어놓은 것이라서
"strandtest" 예제를 보고 조합하였다.

LED 가 원하는 이상한 색이 되거나 띄엄띄엄 작동하면
이 부분을 잘못 설정한 것이다.
default 로 NEO_GRB 라고 되어있는데 내가 산 제품은 RGBW 라서 바꿔줬다. 하지만, 입력 부분에서도 RGB 순서가 아니라 GRB 였다. 구입할 때는 RGBW 라고 샀는데 테스트 하니까 GRBW가 나왔다.

Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_RGBW + NEO_KHZ800);
// Argument 1 = Number of pixels in NeoPixel strip
// Argument 2 = Arduino pin number (most are valid)
// Argument 3 = Pixel type flags, add together as needed:
//   NEO_KHZ800  800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
//   NEO_KHZ400  400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
//   NEO_GRB     Pixels are wired for GRB bitstream (most NeoPixel products)
//   NEO_RGB     Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
//   NEO_RGBW    Pixels are wired for RGBW bitstream (NeoPixel RGBW products)

라이브러리를 찾아봤는데 예제 이외에 RGB 위치를 조절할 수 있도록 되어있다.
원하는 조합을 찾으면 된다.


3. 블록 다이어그램


어떤 장비든 통신할 수 있도록 Serial로 데이터를 전송하여
무선으로 여러 장치에 Broadcasting 하는 작업이다.


4. 코드 분석


[소스코드 깃허브 링크 - Python]

[소스코드 깃허브 링크 - Arduino]

Python Code

  • 필요 라이브러리
    pip install pyserial
import sys
import time
from ctypes import *			# 구조체 전달을 위한 모듈
from enum import Enum
import serial					# pyserial
import serial.tools.list_ports	# 윈도우에서 USB 장치 검색용

ESP32 쪽으로 Serial 통신하여 전달하는 패킷이다.

class Packet(Structure):

    _pack = 1  # 1byte 정렬

    _fields_ = [("device_led", c_uint8),
                ("RED", c_uint8),
                ("GREEN", c_uint8),
                ("BLUE", c_uint8),
                ("brightness", c_uint8),
                ("style", c_uint8),
                ("wait_time", c_uint8),
                ("checksum", c_uint8)]

보낼 데이터를 출력하는 메서드이다.

def read_packet_data(fields):
    print("------------------packet------------------------")
    print(f"device_led : {fields.device_led}\n" +
          f"RED   : {fields.RED}\n" +
          f"GREEN : {fields.GREEN}\n" +
          f"BLUE  : {fields.BLUE}\n" +
          f"brightness : {fields.brightness}\n" +
          f"style : {fields.style}\n" +
          f"wait_time : {fields.wait_time}\n" +
          f"checksum : {fields.checksum}")
    print("------------------------------------------------")
    print(f"bytes : {bytes(fields)}")

이 부분은 굳이 필요는 없지만
아두이노 Neopixel 예제에서 제공하는 LED 동작 방식을 정할 수 있다.
그 방식을 쉽게 알아보기 위해 일부러 사용하였다.

class STYLE(Enum):
    NONE = 0
    oneColor = 1
    chase = 2
    rainbow = 3
    colorWipe = 4
    chase_rainbow = 5

보낼 데이터를 설정하는 부분이다.

def set_packet(device_led, rgb_list, brightness, style, wait_time):
    data = Packet(device_led, rgb_list[0], rgb_list[1], rgb_list[2], brightness, style, wait_time, 0)
    read_packet_data(data)
    return data

ESP32 보드에 전원이 들어가거나 다른 ESP 보드와 연결이 안되었을 때
보드 자체 Debug 용으로 Serial 데이터가 출력되는데
그 부분을 넘어갈 수 있는 부분이다.

ESP32 에서 Setup_Done 을 읽어오거나
일정 시간동안 데이터를 읽어오는게 없다면 종료된다.

def readUntilString(ser, exitcode=b'Setup_Done'):
    count = 0
    while True:
        data = ser.read_until()
        print(data)
        # print(count)
        if data == b'':
            count = count + 1
        else:
            count = 0

        if exitcode in data or count > 50:
            return print("====Serial Now available====")

혹시 모를 데이터가 남을 경우를 대비한 버퍼 비우기 용 메서드이다.
time.sleep() 대신에 사용한다.
굳이 필요는 없다.

def clear_serial_buffer(ser, delay):
    close_time = time.time() + delay
    while True:
        # if py_serial.readable():
        res = ser.readline()
        # print(res[:len(res)-1].decode('utf-8').rstrip()
        print(res[:len(res) - 1].decode().rstrip())

        if time.time() > close_time:
            break

ESP32 development 보드의 USB to Serial 칩은
CP2102, CH340, CH9102 등 으로 되어있다.
PC에 해당 드라이버 칩을 가지고 있는 보드를 하나만 연결한다는 가정하에
자동으로 포트를 잡아준다.

# 시리얼 포트 검색
def serial_ports(com_port=None):
    ports = serial.tools.list_ports.comports()
    uart_port = ['CP210x', 'CH340', 'CH340K', 'CH9102']
    dic = {}

    if com_port is not None:
        dic[com_port] = "manually"
        return dic

    for port, desc, hwid in sorted(ports):
        # print("{}: {} [{}]".format(port, desc, hwid))
        for uart in uart_port:
            if uart in desc:
                # print(uart)
                dic[port] = uart
                # print(dic)

    if len(dic.items()) > 0:
        return dic
        
# 시리얼 포트 한개 연결        
def connect_port(com_port=None):
    connected_ports = serial_ports(com_port)
    board_port = list(connected_ports.keys())
    # print(board_port[0])
    return board_port[0]
    
    

PC와 연결된 보드가 PC에 데이터를 보낸 경우 보내는 Callback 함수
테스트 할 때는 사용하였으나 프로젝트에서는 사용안함

def serial_receive_callback(ser, data):
    recv_data = ser.read(data)
    recv_data = Packet.from_buffer_copy(recv_data)
    read_packet_data(recv_data)
   # return recv_data

아두이노에서 제공되는 LED 스타일들은 내부에 delay 가 있어서
중간에 끄고 키는 작업이 되질 않는다.
그래서 파이썬 환경에서 직접 RGB 값을 줘 컨트롤하였다.

# 무지개 7색
def set_rainbow_color(ser, address):
    rainbow_list = [[255, 0, 0],  # Red
                    [255, 140, 0],  # Orange
                    [255, 255, 0],  # Yellow
                    [0, 255, 0],  # Green
                    [0, 0, 255],  # Blue
                    [18, 0, 255],  # Indigo
                    [255, 0, 255],  # Purple
                    [255, 255, 255]]

    for led_number in range(7):
        trans = set_packet(address + led_number, rainbow_list[led_number], 50, STYLE.oneColor.value, 10)
        send_data = ser.write(bytes(trans))
        time.sleep(0.02)


# Blink
def on_off_led(ser):
    for led_num in range(0x10, 0x16):
        trans = set_packet(led_num, [255, 43, 123], 50, STYLE.oneColor.value, 20)
        send_data = ser.write(bytes(trans))

        time.sleep(0.1)  # 약간의 딜레이가 없으면 전송이 힘들 수 있음 Serial 인식 시간

        trans = set_packet(led_num, [255, 43, 123], 0, STYLE.oneColor.value, 20)
        send_data = ser.write(bytes(trans))

        time.sleep(0.1)

# 아래 무지개를 위한 RGB 값
def Wheel(WheelPos):
    WheelPos = 255 - WheelPos
    if WheelPos < 85:
        return [255 - WheelPos * 3, 0, WheelPos * 3]    # R, 0, B

    if WheelPos < 170:
        WheelPos = WheelPos - 85
        return [0, WheelPos * 3, 255 - WheelPos * 3]    # 0, G, B

    WheelPos = WheelPos - 170
    return [WheelPos * 3, 255 - WheelPos * 3, 0]        # R, G, 0

# 무지개 순환 
def rainbow_python(ser, address, size, delay, on_off_brightness):
    global pixel_cycle
    global pixel_queue

    for i in range(0, size, 1):
        trans = set_packet(address + i, Wheel((i + pixel_cycle) % 255), on_off_brightness, STYLE.oneColor.value, 1)
        ser.write(bytes(trans))
        time.sleep(delay)
        pixel_cycle = pixel_cycle + 1
        pixel_queue = pixel_queue + 1
        if pixel_cycle >= 256:
            pixel_cycle = 0
        if pixel_queue >= size:
            pixel_queue = 0

실행 부분이다.

if __name__ == '__main__':

	# 시리얼 포트 연결 3가지 방식으로 할 수 있다.
    print(serial_ports())
    py_serial = serial.Serial(port="COM8", baudrate=115200, timeout=0.1)
    # py_serial = serial.Serial(port=connect_port('COM8'), baudrate=115200, timeout=0.1)
    # py_serial = serial.Serial(port=connect_port(), baudrate=115200, timeout=0.1)    # 포트 연결
	
    # ESP32 보드가 준비될 때까지 읽어옴
    readUntilString(py_serial)
	
    # 
    clear_serial_buffer(py_serial, 1)

    # Global value
    pixel_cycle = 0
    pixel_queue = 0

    # led_num, rgb_list, brightness, style, wait
    while True:
        for i in range(10):
            rainbow_python(py_serial, 0x50, 8, 0.01, 10)
            rainbow_python(py_serial, 0x10, 8, 0.01, 10)
            if i == 4:
                trans = set_packet(0x50, [0, 0, 0], 0, STYLE.oneColor.value, 10)
                py_serial.write(bytes(trans))
                time.sleep(0.5)



    # change WiFi
    # trans = set_packet(255, 255, [0, 0, 0], 0, 1, 20)
    # send_data = py_serial.write(bytes(trans))

    # serial_receive_callback(py_serial, send_data)

    py_serial.close()

Arduino Code

Broadcasting Board

변수

#include <esp_now.h>
#include "WiFi.h"
#include <esp_wifi.h> // esp_now.h  포함
//#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager

uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

String success;
uint8_t incomingRGB[3];

unsigned long t = 0;

#pragma pack(push, 1)
typedef struct packet_
{
    uint8_t device_led;
    uint8_t RED;  // 데이터 타입 : 1
    uint8_t GREEN;
    uint8_t BLUE;
    uint8_t brightness;
    uint8_t style;            // 데이터 : 4 
    uint8_t wait;
    uint8_t checksum;  // 체크섬 : 2
}PACKET;
#pragma pack(pop)

typedef enum {
  oneColor = 1,
  CHASE,
  RAINBOW
}STYLE_Typedef;

//STYLE_Typedef _style;

PACKET serial_data = {0, };
PACKET incomingReadings;
PACKET sample_data1 = {0x10, 255, 0, 0, 10, 1, 20, 0};
PACKET sample_data2 = {0x10, 0, 0, 255, 0, 1, 20, 0};

int neopixel_Flag = 0;
int broadcast_Flag = 0;
int compare_Flag = 0;
int ESPNOW_Flag = 0;
char ssid[32] = {0,};
int8_t channel[255] = {0, };
int8_t temp_channel = 0;
esp_now_peer_info_t peerInfo;

char compare_esp[] = "ESP";
char ssid_esp[3] = {0,};

주변 와이파이를 검색하여 브로드캐스팅할 ESP를 찾아내는 부분이다.

void setup() {
  Serial.begin(115200);
  
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);
  Serial.println("Setup done");

  if (esp_now_init() == ESP_OK) {
    // 0(ESP_OK) 일 때 시작
    Serial.println("ESP-NOW initialized");
  }else{
    Serial.println("Error initializing ESP-NOW");
    ESP.restart();
  }

  broadcast_Init();

  
  while (true){
    Serial.println("scan start");

    // WiFi.scanNetworks will return the number of networks found
    int network = WiFi.scanNetworks();
    Serial.println("scan done");
    if (network == 0) 
    {
      Serial.println("no networks found");
    } 
    else 
    {
      Serial.print(network);
      Serial.println(" networks found");
      for (int i = 0; i < network; ++i) 
      {   // 1번 부터
          // Print SSID and RSSI for each network found
          Serial.print(i + 1);
          Serial.print(": ");
          Serial.print(WiFi.SSID(i));
          Serial.print(" (");           
          Serial.print(WiFi.RSSI(i));
          Serial.print(")");
          
          Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN)?"_":"*");

          delay(10);           
       }                 
    }     
    Serial.println("");
      
    for(int i=0; i < network; ++i)
    {
      WiFi.SSID(i).toCharArray(ssid, sizeof(ssid));   // 배열로

      for(int j=0; j<3; j++)
      {
        ssid_esp[j] = ssid[j];
      }
      
      if(!strcmp(compare_esp, ssid_esp))
      {
        compare_Flag = 1;

        Serial.print("compare : ");
        Serial.println(compare_esp);
        Serial.print("ssid[3] : ");
        Serial.println(ssid_esp);
        Serial.print("FLAG : ");
        
        
        channel[i] = getWiFiChannel(ssid);
        Serial.print("Channel : ");
        Serial.println(channel[i]);
        temp_channel = channel[i];
        break;
      }
    }

    if(compare_Flag == 1)
    {
      esp_wifi_set_promiscuous(true);
      esp_wifi_set_channel(temp_channel, WIFI_SECOND_CHAN_NONE);
      esp_wifi_set_promiscuous(false);
      broadcast((uint8_t *) &sample_data1, sizeof(sample_data1));
      delay(500);
        
      if(broadcast_Flag == 1){
        Serial.println("Complete Broadcast");
        break;
      }
    }
    
    // Wait a bit before scanning again
    delay(3000);
    broadcast_Flag = 0;
    compare_Flag = 0;
    
  }

   // Init ESP-NOW
  

  
  broadcast((uint8_t *) &sample_data2, sizeof(sample_data2));
  Serial.write("\r\nSetup_Done");
  
}

Serial통신으로 전달받은 구조체를 그대로 ESP-NOW 브로드캐스팅해서 보낸다.

void loop() {
  if(Serial.available())
  {
      // packet 사이즈만큼 읽어옴
      Serial.readBytes((char*)&serial_data, sizeof(serial_data));
      serial_data.checksum += 1;
      neopixel_Flag = 1;
      Serial.println("-----------------------");      
      //Serial.write((char*)&serial_data, sizeof(serial_data));
      delay(1);
  }


  if( neopixel_Flag == 1 ){
    neopixel_Flag = 0;  
    broadcast((uint8_t *) &serial_data, sizeof(serial_data));
  }
}

정리된 함수 부분이다.

브로드캐스팅 초기화

void broadcast_Init(){
  // Register peer
  esp_now_peer_info_t peerInfo = {};
  //memset(&peerInfo, 0, sizeof(peerInfo));
  memcpy(peerInfo.peer_addr, broadcastAddress, 6); // 6-> address length
  peerInfo.encrypt = false; // 암호화 ID/PW

  // Add peer        
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    ESPNOW_Flag = 0;
    Serial.println("Failed to add peer");
    return;
  }else{
    Serial.println("Succeed to add peer"); 
    ESPNOW_Flag = 1;
    esp_now_register_send_cb(OnDataSent);
//    esp_now_register_recv_cb(OnDataRecv);
  }
}

와이파이 채널을 얻어오는 함수이다.

int32_t getWiFiChannel(const char *ssid) {
  if (int32_t n = WiFi.scanNetworks()) {
      for (uint8_t i=0; i<n; i++) {
          if (!strcmp(ssid, WiFi.SSID(i).c_str())) {
              return WiFi.channel(i);
          }
      }
  }
  return 0;
}

와이파이 스캔

void scan_WiFi(){
  Serial.println("scan start");

  // WiFi.scanNetworks will return the number of networks found
  int network = WiFi.scanNetworks();
  Serial.println("scan done");
  if (network == 0) 
  {
      Serial.println("no networks found");
  } 
  else 
  {
      Serial.print(network);
      Serial.println(" networks found");
      for (int i = 0; i < network; ++i) 
      {   // 1번 부터
          // Print SSID and RSSI for each network found
          Serial.print(i + 1);
          Serial.print(": ");
          Serial.print(WiFi.SSID(i));
          Serial.print(" (");           
          Serial.print(WiFi.RSSI(i));
          Serial.print(")");
          
          Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN)?"_":"*");

          delay(10);           
       }                 
   }     
   Serial.println("");
}

ESP-NOW 에서 전송완료, 수신완료 Callback


// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
    
  char macStr[18];
  formatMacAddress(mac_addr, macStr, 18);
  Serial.print("Last Packet Sent to: ");
  Serial.println(macStr);
  
  Serial.print("Last Packet Send Status: ");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
  if (status ==0){
    success = "Delivery Success :)";
    broadcast_Flag = 1;
  }
  else{
    success = "Delivery Fail :(";
    broadcast_Flag = 0;
  }
}

// Callback when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));
}

void formatMacAddress(const uint8_t *macAddr, char *buffer, int maxLength)
{
  snprintf(buffer, maxLength, "%02x:%02x:%02x:%02x:%02x:%02x", macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);
}


void broadcast(const uint8_t * broadcastData, int dataSize)
{
  // this will broadcast a message to everyone in range
  uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
  esp_now_peer_info_t peerInfo = {};
  memcpy(&peerInfo.peer_addr, broadcastAddress, 6);
  if (!esp_now_is_peer_exist(broadcastAddress))
  {
    esp_now_add_peer(&peerInfo);
  }
  esp_now_send(broadcastAddress, (const uint8_t *)broadcastData, dataSize);
}

LED


5. 설계


한변의 길이가 45mm 인 정육면체 LED 이다.
PCB를 직접 설계했으면 30mm 까지 작아 질 수 있는데
아직은 실력이 부족하여 단기간에는 끝내기 힘들었다.

LED 6개가 일렬로 되어있어서 하단부에 4개 상단에 2개를 끼울 수 있도록 했다.
지금 프로토 타입은 USB 선이 있지만
나중에는 2mm 정도의 단자 2개만 있으면 된다.
나사를 사용하면 디자인이 별로여서
슬라이드 형식으로 공차를 줘서 조립할 예정이다.


6. 프로토타입

1300mA 배터리를 아직 구매하지 못해서 일단은 보조배터리로 전원을 넣어줬다.

oled 는 나중에 0.91 inch 로 바꿔 프레임 내부에 넣을 예정이다.

[영상]

7. 문제해결


- WiFiManger, OTA Webserver 충돌

WiFiManager 와 AsyncElegantOTA
둘다 WebServer를 열어서 사용한다.

하나의 IP 주소를 사용하기 때문에
OTA를 열면 WiFi 를 초기화 할 수 없기 때문에
OTA 서버를 닫아줘야한다.

아두이노를 통해 제공하는 예제에는 서버를 닫는 것이 없어서
깃허브자료를 찾아봐야했다.

[AsyncTCP 깃허브 링크]

헤더 파일을 보면 end() 메서드를 찾을 수 있다.

와이파이를 초기화 할 수 있는 부분에 OTA서버를 닫고 여는 식으로 해결하였다.


- WiFi Scan Error

와이파이 Scan 하던 중에 SSID에 대한 채널을 찾기 위한 작업을 하면
내부의 딜레이 때문에 다음 번 Scan 때 일부가 검색되지 않는다.

그래서,

Scan 하는 부분에서 모든 작업을 하고
if 문을 통해
ESPNOW init Flag
ESP 장치를 찾아 문자열로 비교하는 Flag
데이터가 잘 전송되었는지 확인하는 Flag
를 한번에 처리하지 않고

ESPNOW init Flag 는 ESP.restart (보드 재시작)으로 대체하고
나머지는 2단계로 나눠서 처리했다.

예)
Before

if((compare_Flag == 1) && (broadcast_Flag == 1))

After

if(!strcmp(compare_esp, ssid_esp))
      {
        compare_Flag = 1;

        Serial.print("compare : ");
        Serial.println(compare_esp);
        Serial.print("ssid[3] : ");
        Serial.println(ssid_esp);
        Serial.print("FLAG : ");
        
        
        channel[i] = getWiFiChannel(ssid);
        Serial.print("Channel : ");
        Serial.println(channel[i]);
        temp_channel = channel[i];
        break;
      }
    }

    if(compare_Flag == 1)
    {
      esp_wifi_set_promiscuous(true);
      esp_wifi_set_channel(temp_channel, WIFI_SECOND_CHAN_NONE);
      esp_wifi_set_promiscuous(false);
      broadcast((uint8_t *) &sample_data1, sizeof(sample_data1));
      delay(500);
        
      if(broadcast_Flag == 1){
        Serial.println("Complete Broadcast");
        break;
      }
    }

8. 개선할 점

- STX, ETX

0 과 1로 표현하는 시리얼 통신을 어플리케이션 단에서
Packet 통신으로 만들었지만
데이터의 크기가 달라질 경우 받는 데이터가 무엇인지 모르는 고정형 패킷이다.
그래서
Start, End 를 구분할 수 있는
STX, ETX 0x02, 0x03을 넣어서 데이터의 처음과 끝을 나타내야한다.

- Length 또는 CRC 기능

이 프로젝트는 데이터를 전송 속도만을 위해
Callback 기능과 데이터가 잘 전송되었는지 확인하는 기능을 제거했다.

하지만 제품의 신뢰성을 위해서는
데이터를 잘 주고 받는지 양방향으로 통신하여 확인하는 것이 좋다.

<작성중>

profile
하고 싶은 것, 소유하고 싶은 것, 좋아하는 것

0개의 댓글