/******************** F r i e n d s h i p E n d e r ********************
 * 本程序隶属于 Readguy 开源项目, 请尊重开源开发者, 也就是我FriendshipEnder.
 * 如果有条件请到 extra/artset/reward 中扫描打赏,否则请在 Bilibili 上支持我.
 * 项目交流QQ群: 926824162 (萌新可以进来问问题的哟)
 * 郑重声明: 未经授权还请不要商用本开源项目编译出的程序.
 * 
 * @file 2_wifi_config.ino
 * @author FriendshipEnder (f_ender@163.com), Bilibili: FriendshipEnder
 * @version 1.1
 * @date create: 2023-10-14 last modify: 2024-02-26
 * @note 本版本主要更新了NTP对时机制, 以及扫描wifi时可以在屏幕上显示到底扫描了多少wifi
 * @brief ReadGuy配网服务器 配置并连接附近的WiFi网络演示程序.
 编译烧录后, 本程序将使用AP方式配网并在连接到网络时访问NTP服务器来在墨水屏上显示时间.
 *** 推荐文章   解决2038千年虫: (本程序未使用该文章内容)
 *** https://blog.csdn.net/qdlyd/article/details/131199628
 同时开启在STA上的服务器, 供这个WiFi上的用户访问此墨水屏阅读器.

// 注意, 为了避免此项目占用的flash空间过大, 故库内中不再提供配网的相关功能函数.
// 此示例程序提供了配网相关的方法, 在实际程序中, 请自行编写.

// ******** 在进行此示例之前, 不要将 DYNAMIC_PIN_SETTINGS 和 READGUY_ENABLE_WIFI 注释掉. ********
// ******************************** 此示例需要用到 WiFi 的特性. ********************************
 * 
 * @attention
 * Copyright (c) 2022-2023 FriendshipEnder
 * 
 * Apache License, Version 2.0
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

//在这里包含程序需要用到的库函数

#include <Arduino.h> //arduino功能基础库. 在platformIO平台上此语句不可或缺
#include "readguy.h" //包含readguy_driver 基础驱动库
#include <lwip/apps/sntp.h> // settimeofday 函数 需要

ReadguyDriver guy;//新建一个readguy对象, 用于显示驱动.

typedef ReadguyDriver::ReadguyWebServer* server_t; //类型名太长太繁琐, 使用typedef减短
typedef ReadguyDriver::serveFunc         event_t ; //存储一个WiFi功能事件.

void f1(server_t sv); //服务器响应回调函数. 当启动AP配网服务器时, 这些函数将会被调用
void f2(server_t sv);

/// @brief NTP获取时间的函数, 必须联网才能调用
time_t getNTPTime();

int conf_status = 0;  //标记WiFi配网状态: 当此值为1时, 说明配网程序收到了WiFi SSID和密码信息, 尝试连接.
                      //此变量为2 说明配网成功了. 连接到了WiFi并显示当前时间.
int scanres = 0;      //WiFi扫描结果数量
String recv_ssid = "";//接收到的WiFi SSID
String recv_psk = ""; //接收到的WiFi 密码

void setup(){
  Serial.begin(115200); //初始化串口
  guy.init(); //初始化readguy_driver 基础驱动库. 尽管初始化过程会刷屏, 但此示例不会用到屏幕.
  
  Serial.println(F("[readguy] WiFi Configure demo")); //显示文本 默认是不支持中文显示的.
  guy.println(F("WiFi Configure demo")); //显示文本 默认是不支持中文显示的.
  guy.display();//刷新墨水屏.

  event_t server_event[3]={ //设置一个服务器响应回调函数的容器. 使用方法详见wifi_ap_server示例
    {"配置WiFi","/wificfg",f1}, //配置WiFi的入口链接. 在AP模式下点击此链接将会进入配网功能页面.
    {"","/wifiok",f2}           //收到wifi连接请求后 发送的响应. 此函数调用之后会尝试切换到STA模式, 连接到WiFi.
  }; //本驱动库不会单独保存用户的WiFi SSID和WiFi密码信息

  WiFi.mode(WIFI_AP_STA); //扫描网络需要切换到STA模式进行扫描. 扫描完成之后将会切换到AP模式
  
  guy.server_setup(String(F("WiFi配网示例")),server_event,2); //初始化服务器.
  //这些服务器响应回调函数会打包进入初始化参数列表中.
  //上方的字符串可以在用户访问主页时, 显示在主页的第二行.(作为通知显示, 但并不是通知)

  do{ //没有连接到目标SSID时 一直循环连接.

    conf_status=0; //重置配置状态变量

    scanres = WiFi.scanNetworks(); //开始扫描网络
    
    Serial.printf("[readguy] WiFi Scan %d OK.\n",scanres); //关闭服务器, 尝试连接, 连接成功之后将会在屏幕上显示
    guy.printf("WiFi Scan %d OK.\n",scanres);    //连接失败则会重新进入循环
    guy.display();

    IPAddress local_IP(192,168,4,1); //设置本地AP的IP地址, 网关和子网掩码.
    IPAddress gateway(192,168,4,1);
    IPAddress subnet(255,255,255,0);
    WiFi.softAPConfig(local_IP, gateway, subnet); //注册一个WiFi AP(类比无线路由器), 可以被手机等设备连接
    WiFi.softAP("readguy","12345678"); //初始化WiFi AP模式 (可以理解为路由器模式)

    while(conf_status==0){
      guy.server_loop(); //让服务器一直运行 此函数总是返回true. 因此配网何时完成, 应在程序里设定, 适可而止
    }

    //guy.server_end(); //看来是接收到啊WiFi名称和密码消息了, 现在关闭服务器, 尝试连接到WiFi...

    Serial.println("[readguy] received SSID and PSK info"); //关闭服务器, 尝试连接, 连接成功之后将会在屏幕上显示
    guy.println("received SSID and PSK info");              //连接失败则会重新进入循环
    guy.display(); //显示到墨水屏

    WiFi.scanDelete(); //删除WiFi扫描数据
    scanres = 0; //设置扫描到的网络个数为0

    if(recv_ssid != "-"){ //检测SSID是否为空. 一般的网络SSID应该不会是这个名称吧...
      //WiFi.mode(WIFI_STA);
      WiFi.begin(recv_ssid,recv_psk); //启动STA模式, 然后尝试连接网络(当前维WIFI_AP_STA模式, 可以连接到网络)

      for(int i=0;i<1500;i++){ //等待15秒, 没连上就退出.
        if(WiFi.status() == WL_CONNECTED){ //一旦连接成功就退出循环
          conf_status=2; //标记为连接成功
          guy.print("Connected! IP:"); //连接成功! 显示消息然后退出循环
          guy.println(WiFi.localIP()); 
          Serial.print("[readguy] Connected!IP:"); //连接成功! 显示消息然后退出循环
          Serial.println(WiFi.localIP());
          guy.display();
          break;
        }
        delay(10);
      }
    }
    if(conf_status!=2){ //经过循环之后发现wifi并没连接上 显示连接失败的信息.
      guy.println("WiFi failed! AP and server restarted."); //连接成功! 显示消息然后退出循环
      Serial.println("[readguy] WiFi failed! AP and server restarted."); //连接成功! 显示消息然后退出循环
      guy.display();
    }

  }while(conf_status!=2); // conf_status==2说明连接上了
  
  // WiFi.mode(WIFI_STA); //从WIFI_AP_STA模式切换到WIFI_STA模式, 不再提供readguy热点.

  guy.println("Getting NTP time..."); //连接成功之后尝试获取NTP时间
  Serial.println("[readguy] Getting NTP time..."); //连接成功之后尝试获取NTP时间
  guy.display();

  time_t now = getNTPTime();        //下方的函数演示了如何使用NTP来对时. 此函数必须连接上wifi才能调用
  now=time(nullptr);                //通过Unix API获取时间
  struct tm now_tm;
  gmtime_r(&now,&now_tm);           //转换为GMT时间
  guy.println(asctime(&now_tm));
  Serial.println(asctime(&now_tm));
  localtime_r(&now,&now_tm);        //转换为本地时间(包含了时区数据的)
  guy.println(asctime(&now_tm));
  Serial.println(asctime(&now_tm));
  guy.display();

  guy.server_setup("现在是联网的STA模式."); //如果没有调用server_end函数 连续调用server_setup将自动结束之前的服务器
}

void loop(){
  guy.server_loop();
}

// 以下演示了如何向配网服务器添加回调函数.
//其中, sv 参数指向了一个服务器类型的变量. 当有来自客户端的请求时, 需要通过sv来发送响应消息.

void f1(server_t sv){ //使用PSTR来减少对内存的消耗(不加PSTR()或者F()则字符串会存到.rodata,占用宝贵的内存)
  if(WiFi.status() == WL_CONNECTED) { 
    sv->send_P(200, PSTR("text/html"),PSTR(
      "<html><body><meta charset=\"utf-8\">已连接, 不需要再配网了。</body></html>"));
    return;
  }
  String webpage_html = F(
  "<!DOCTYPE html>"
  "<html lang='zh-cn'>"
  "<head>"
    "<meta charset='UTF-8'>"
    "<title>配置WiFi连接</title>"
  "</head>"
  "<body>"
    "<form name='input' action='/wifiok' method='GET'>"
      "<h2>wifi配置页面</h2>"
      "如需刷新WiFi列表, 请选择选项``重新扫描WiFi``并点击保存, 而不是刷新此页, 刷新此页不会刷新WiFi列表<hr/>"
      "wifi名称:<br/>"
  //    "<input type='text' name='ssid'>"
      "<select name='ssid'>"
  //      "<option value='0'>测试WiFi名称</option>" //在此放置你扫描到的所有WiFi名称
  //      "<option value='1'>1.54寸Lilygo</option>"
  );
  String webpage_html2 = F(
      "</select>"
      "<br/>"
      "wifi密码:<br/>"
      "<input type='text' name='psk'><br/>"
      "<input type='submit' value='保存'>"
    "</form>"
  "</body>"
  "</html>");
  
  for(int i=0;i<=scanres;i++){
    webpage_html += "<option value='";
    webpage_html += i;
    webpage_html += "'>";
    if(i<scanres) webpage_html += WiFi.SSID(i);
    else webpage_html += "重新扫描WiFi";
    webpage_html += "</option>";
  }
  sv->send_P(200, PSTR("text/html"), (webpage_html+webpage_html2).c_str());
} //于此相应, 使用send_P函数而不是send函数来发送数据

void f2(server_t sv){
  if(sv->hasArg("ssid")) {
    if((sv->arg("ssid")).toInt() == scanres){
      recv_ssid = "-"; //空白字符串 退出 重新扫描
    }
    else recv_ssid=WiFi.SSID((sv->arg("ssid")).toInt());
    if(sv->hasArg("psk")) {
      recv_psk=sv->arg("psk");
      if(recv_ssid!="") conf_status=1; //接收到了WiFi SSID信息和密码信息
    }
  }
  if(recv_ssid == "-"){
    sv->send_P(200, PSTR("text/html"), PSTR("<html><body><meta charset=\"utf-8\" http-equiv=\"refresh\""
    " content=\"8;url=/wificfg\">正在扫描WiFi, 8秒后自动跳转.</body></html>"));
  }
  else if(conf_status>=1){
    String s=F("<html><body><meta charset=\"utf-8\">配置成功,正在连接...<br/>WiFi名称:");
    s += recv_ssid;
    s += F("<br/>WiFi密码:");
    s += recv_psk;
    s += F("<br/>连接成功的消息会显示在墨水屏上.</body></html>");
    sv->send_P(200, PSTR("text/html"), s.c_str());
  }
  else sv->send_P(200, PSTR("text/html"), //没有给定SSID, 无法连接到WiFi.
    PSTR("<html><body><meta charset=\"utf-8\">配置失败,缺少信息</body></html>"));
}

    /*----------------- NTP code ------------------*/
#define NTP_SERVERS 4
WiFiUDP udp;
uint8_t packetBuffer[48];
const int16_t timeZone = 8; //Beijing
const int16_t localPort = 1337;
time_t getNTPTime(){
  const char * ntpServerName[NTP_SERVERS] = {
    "ntp1.aliyun.com","time.windows.com","cn.ntp.org.cn","cn.pool.ntp.org"
  };
  time_t _now = 0;
  if(!WiFi.isConnected()) return 0;
  udp.begin(localPort);
  Serial.print("Local port: ");
  Serial.println(localPort);
  for(int i=0;i<NTP_SERVERS;i++){//最多尝试10次对时请求
      _now=0;
      char ntpHost[32];
      IPAddress ntpServerIP; // NTP server's ip address
      
      while (udp.parsePacket() > 0) ; // discard any previously received packets
      Serial.println(F("Transmit NTP Request"));
      // get a random server from the pool
      strncpy_P(ntpHost,ntpServerName[i],31);
      ntpHost[31] = '\0';
      WiFi.hostByName(ntpHost, ntpServerIP);
      Serial.print(FPSTR(ntpServerName[i]));
      Serial.write(':');
      Serial.println(ntpServerIP);

      // set all bytes in the buffer to 0
      memset(packetBuffer, 0, 48);
      // Initialize values needed to form NTP request
      // (see URL above for details on the packets)
      packetBuffer[0] = 0b11100011;   // LI, Version, Mode
      packetBuffer[1] = 0;     // Stratum, or type of clock
      packetBuffer[2] = 6;     // Polling Interval
      packetBuffer[3] = 0xEC;  // Peer Clock Precision
      // 8 bytes of zero for Root Delay & Root Dispersion
      packetBuffer[12] = 49;
      packetBuffer[13] = 0x4E;
      packetBuffer[14] = 49;
      packetBuffer[15] = 52;
      // all NTP fields have been given values, now
      // you can send a packet requesting a timestamp:
      udp.beginPacket(ntpServerIP, 123); //NTP requests are to port 123
      udp.write(packetBuffer, 48);
      udp.endPacket();

      uint32_t beginWait = millis();
      while (millis() - beginWait < 1500) {
        int size = udp.parsePacket();
        if (size >= 48) {
          Serial.println("Receive NTP Response");
          udp.read(packetBuffer, 48);  // read packet into the buffer
          unsigned long secsSince1900;
          // convert four bytes starting at location 40 to a long integer
          secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
          secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
          secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
          secsSince1900 |= (unsigned long)packetBuffer[43];
          _now = secsSince1900 - 2208988800UL; // + timeZone * 3600;
          break;
        }
      }// return 0 if unable to get the time
      if(_now) break; //成功后立即退出
      else Serial.println("No NTP Response :-(");
      yield();
  }
  if(_now){
    if(time(nullptr) < 1577836800){ //时区未设置 (比较时间为2020年1月1日 00:00:00)
      setenv("TZ", "CST-8", 1); //设置时区变量 (当前设置为北京时间)
      tzset();
    }
    timeval tm_now={_now, 0};
    settimeofday(&tm_now,nullptr);
  }
  return _now;
}

/* END OF FILE. ReadGuy project.
Copyright (C) 2023 FriendshipEnder. */