项目开发

个人项目


学习成果:


你是否回答了这些问题?


项目开发


我做这项作业时,最终项目已经完成并展示过了。因此,我觉得再编一个计划去收尾一件已经做完的事没有意义。所以我决定在整届 Fab Academy 的收尾中,展示这项作业所要求的能力。


计划


哪些任务已经完成,哪些任务还没有?


完成最终项目的所有任务都已经做完,我也已经把项目展示给 Neil 了。现在只剩下文档和作业,我在下面的表格里列出。原始 Excel 表格在这里。

作业表格


接下来会发生什么,什么时候?


为了完成上面的任务,我提出了下面的时间安排。原始 Excel 表格在这里。

完成计划


哪些做法有效?



哪些没有奏效?



哪些问题需要解决?


我需要改进实施方面。我想,如果我把一个 230 V 插座集成进去给水泵用,用户的安装体验会更简单。


你学到了什么?



下面记录了我的学习过程,从安装 PlatformIO IDE(在 VS Code 上)到提高我的 C++ 编程能力。

日记:最终电路板之前的测试环境日期

06/06/2020 07/06/2020 08/06/2020 09/06/2020 10/06/2020 13/06/2020 14/06/2020 15/06/2020 16/06/2020 17/06/2020

从这里开始我就失去了时间感

设计电路板
铣削电路板
调试电路板
烧录固件
网络代码
铣削外壳
接线
整个系统的总图
搭建测试环境(温室)
成品照(Hero shot)

最终项目进展日记,6 月 6 日至 6 月 17 日

2020 年 6 月 6 日

我写这段的时候,还没有正式的设计文件,只有一份代码草稿,还有面包板上的一个原型!

另一方面,硬件和软件都有一些需求:

软件需求与当前阶段

  1. 通过拆出更多函数来简化代码;
  2. 尝试把 HTML 部分分离出来,让代码更整洁;
  3. 改善代码的外观和界面;
  4. 把读数加到 HTML 里;
  5. 增加事件记录,比如过流;
  6. 给设备增加更多保护;
  7. 增加手机界面(APP);
  8. 增加一个设置当前时间的功能;
  9. 增加一个给每个继电器改名的功能,这样就可以把继电器和关注的区域对应起来,或者至少把继电器改成农场里真实的区域名。

设计需求与现状

  1. 至少需要防水;

2020 年 6 月 7 日

到今天,也就是 2020 年 6 月 7 日,开发情况是这样的。

用 Arduino Uno 的初始装置 我一开始用的是一套在 Uno 上跑得起来的系统……

这套装置上跑的代码在 Fabfarm 的 GitHub 上:Autovalve

Shakeel 是我们的一位志愿者,他做出了一个能在 ESP8266 上跑的版本,就在同一个仓库里,在这里。因为疫情,他匆匆离开了,没时间测试自己的代码,所以它能跑但不稳定,原因是他英勇地尝试把模拟引脚和 WiFi 共用,导致 WiFi 掉线,系统几乎没法用。我就是从这个代码开始的。


2020 年 6 月 8 日

花了好多个小时去理解那份代码之后,我成功地把 Shakeel 的代码移植到了 ESP32 上,并且把文件拆开了,每个函数放在一个单独的文件里。

在这段视频里,我展示到目前为止把代码合并起来、并让它在 ESP32 上跑通的进展。

想看高分辨率的版本,请上 YouTube

对程序员来说,把代码移植到 ESP32 可能很简单,但对我来说花了很多个小时。

步骤:

  1. 把已经是函数的部分都分到单独的文件里,让它更好读;
  2. 然后打开另一个 Arduino IDE 窗口,开始把代码一段一段、一行一行地拷到新窗口里;在这个窗口里,我把主文件重命名为 "esp32irrigation"
  3. 把引脚重新映射到 ESP32 的引脚
  4. 在 HTML 上跑了 beautify,用的是 Brackets
  5. 整理代码,把引脚放在最上面
  6. 为 RTC 的引脚创建了变量
  7. 把那些往串口监视器打印错误的代码注释掉了

代码太大,放在 HTML 里显示不下,所以我留一个链接,指向今天凌晨时分的代码状态:代码在这里。

在这段视频里,你能看到农场当前灌溉系统的测试装置。

想看高分辨率的版本,请上 YouTube


2020 年 6 月 9 日

今天测试完继电器之后,我发现有些继电器能用,有些不能用。直接在实验室电源上、把 5 伏加到信号引脚试过之后,我相信这是因为 ESP32 供的电压太低,或者是信号电压的问题。

然后我看了 GreatScott! 在 YouTube 上关于继电器和光耦的视频。

我要确保继电器至少有 5 伏的供电,这是数据表里要求的电压。信号用 3.3 V 应该没问题。到时候在现场测。

SRD 继电器线圈数据表,5 V 版本已标出

另一个问题是电流读数,完全不对。为了让灌溉系统能工作,我只好先不带电流测量地运行,直到把它调试好。


2020 年 6 月 10 日

测试之后,关于电压问题的结论是:ESP32 不应该给继电器供电;那部分应该由一个外部电源提供,设为 5 伏。信号可以直接从 ESP32 来,不用改,但 ESP32 必须和它共地。

代码里又有几处改动:

  1. 把显示时间的间隔从 3000 ms 增加到 5000 ms(5 s),对应的变量是 RTCtimeInterval
  2. 把显示配置的间隔从 10000 ms 增加到 30000 ms(30 s),对应的变量是 configTimeInterval
  3. 在继电器的 HTML 界面里设置了可以认出来的名字
  4. 又清理了一下串口监视器,把显示配置信息里的秒去掉了

今天上午我会把装置重新跑起来,给每次灌溉设定时间,并在现场测试 ESP32 灌溉模块。

奇怪的是,外部 RTC 时钟走时不准。我会研究一下怎么用内部时钟加一块外部电池,以防断电。

根据 GitHub 上 ESP 仓库里的这个已关闭的 issue 讨论,以及这个关于 ESP 精度的帖子,内部时钟会漂:"Internal RTC clock frequency error is about 5%"(内部 RTC 时钟的频率误差约为 5%)。这可以用一个外部 32.768 kHz 晶振解决,按下面的原理图接:

另一个办法是让 ESP 一直连着互联网,从网上取时间来补偿那个不准。

ESP32 的 RTC 用外部 32.768 kHz 晶振的原理图

睡醒后的更新

今天我去把临时的测试装置装了起来。解决了几个问题。

  1. 装置从电脑上拔下来就不工作。解决办法:供电的接线不对;最后我接了两根 USB 线,一根给 ESP32,一根给继电器,两根共地。也许用同样的办法也把 RTC 时钟的问题解决了。

原型现在长这样:

原型:面包板上的 ESP32 开发板和四个继电器模块 还差把水泵和电磁阀接上去。

原型装在瓦片下面,旁边是一个黄蜂巢 希望这些黄蜂不介意我们闯入

给系统通电 给系统通电

测试中……测试中……

测试完之后我回到代码,继续多做些定制。

我先看了一系列YouTube 视频,讲函数的,也真正上手写函数,然后我开始写出更多的函数。

基本上就是去看怎么减少代码重复,以及怎么把一个值传给函数。

在原有的代码基础上,我写了这个函数:

void turnOffRelay(int valveHere){
  // wait then turn valve relay OFF
  Serial.print("Waiting ");
  Serial.print(waitTimeValveOff / 1000);
  Serial.println("s before deactivating Valve Relay 3.");
  delay(waitTimeValveOff);
  digitalWrite(valveHere, LOW);
  valveHere = 0;
  Serial.print("*** Valve Relay ");
  Serial.print(valveHere);
  Serial.println("3 turned OFF ***");
}

注意:valveHere = 0; 在 Serial.print(valveHere); 之前执行,所以不管操作的是哪个阀,消息里总是 "Valve Relay 03 turned OFF"。那两条消息里的 "3" 也是写死的。应该在清零之前打印 valveHere(或者删掉那一行),并把文本里写死的 "3" 去掉。

每次我想调用这个函数,就这样写:

      {
        int valveHere = valveRelay3;
        turnOffRelay (valveHere);
      }

用 {} 的原因是:我那时必须声明局部变量 valveHere;这样每次调用函数时我只需替换那一小段代码。

从技术上来说,调用这个函数只要这一行代码就行:turnOffRelay (valveHere);,但它自己跑不起来,因为我总是在用 int valveHere = valveRelay3;

注意:那个 {} 和局部变量都不需要:引脚本来就作为参数传进去了,所以只写 turnOffRelay(valveRelay3); 效果一样。

我还没测试它的行为,但至少能编译通过 :-)


2020 年 6 月 11 日

我把它烧录到 ESP32 里,行为出乎意料。这个函数不停地打印。我觉得我需要返回某个变量的状态,但我没这么做。我会试着用 return variable; 这段代码来做,然后再测试,但先去重看一遍关于函数的视频,把道理弄明白些。


2020 年 6 月 13 日

哎……好吧,我很久没更新了。嗯,我最后传上去的代码能用,也写了几个函数,但我还是不满意,因为它在 Arduino 里能跑,在 PlatformIO 里不行。到目前为止我学到的是:Arduino IDE 在“背后”帮你做了很多事,而在 PlatformIO 里你得把一切都配得更细。

读读下面这些:

最后我用 PlatformIO 把一个文件烧进了 SPIFFS。用 Arduino 的时候我已经会做这件事了,只是不太明白是怎么做到的。现在我觉得可能弄懂了。为了做到这一点,我必须按一系列步骤配置 platformio.ini 文件和目录树,那就来看看:

  1. 我在 PlatformIO 的 "projects" 标签页的区域里新建了一个项目文件
  2. 然后我在 src 文件夹的同一层创建了一个 data 文件夹。另一个办法是在 platformio.ini 文件里给 data 文件夹指定别的路径。
  3. 然后,板子接上以后,我运行了 pio run -t uploadfs
  4. 我不得不把 Time 库从 PlatformIO 里去掉,因为它和 ESPAsyncWebServer 冲突,这个消息是在这里找到的。

成功了。

这样一来,我开始明白自己以前其实并不知道自己在做什么。我一开始用的代码把 HTML 放在 Arduino 的 sketch 里。这让我很不舒服。现在有了这个新的理解,我可以让 HTML 和 sketch 文件分开了。

然后我找到了这个教程:ESP32 Web Server using SPIFFS

还有这个,万一我想用 Arduino IDE 的插件来上传。

真希望我能早点把这件事弄明白!我在这上面花了那么多天!

到目前为止的 C++ 代码。


                    /****************************************************************************
                    *                                  Aknowledments                           * 
                    *                                  by LucioPGN                             * 
                    ****************************************************************************/
                   /*  Up to this date: 07th of June 2020 I don't consider myself a programer 
                    *  so I need to stand on top of giants sholders for my programing projects:
                    *  A Portion of this code was based on Rui Santos Code;
                    *  A Portion of this code was based on losely based Shakeels code for ESP8266;
                    *  A Portion of this code was based on several websites I lost track of....!
                    *  My contributions: 
                    *     -So far I made it work on platformio :), that took me quite a lot of time
                    *     -That means:
                    *        +created a new project;
                    *        +created a folder named data under the main folder (fabfarm_irrigation)
                    *        +linked the platformio.ini to the folder of the project + the data folder
                    *        +linked the needed libraries to their github repo in the platformio.ini
                    *        +found a conflict with time library and ESPAsyncWebserverLibrary can be solved by renaming time library or by removing it
                    *        +Made it work with a separate HTML file under the data folder using SPIFFS
                    *        +I can load the data to Spiffs inside platformio
                    *  Things I still want to program for my final project:
                    *    -so far I ported Shakeels code into ESP32;
                    *    -simplify the code creating functions rather than repeating the code;
                    *    -try to separate the HTML part for a cleaner code (this applies to maybe mix up the other previous code to this);
                    *    -Improve the appearance/Interface of the code
                    *    -Add readings to HTML
                    *    -Add a log of occurrences like over current
                    *    -Add more safety for the equipment
                    *    -Add a phone interface (APP)
                    *    -Add function to set current time
                    *    -Add renaming function to each relay so one can relate the relay to the area of interest or at least rename relays to actual areas of the farm.
                    * 
                    ****************************************************************************/
                    
                   
                   //Required Libraries
                   #include "WiFi.h"
                   #include "ESPAsyncWebServer.h"
                   #include "SPIFFS.h"
                   #include <AsyncTCP.h>
                   
                   // Network Credentials
                   const char* ssid = "rato";
                   const char* password = "imakestuff";
                   
                   //Start the Async Web Server listening on port 80
                   AsyncWebServer server(80);
                   
                   // Set to true to define Relay as Normally Open (NO)
                   #define RELAY_NO false
                   
                   // Set number of relays, will be used in the array
                   #define NUM_RELAYS  4
                   
                   // Assign each GPIO to a relay
                   int relayGPIOs[NUM_RELAYS] = {26, 25, 33, 32};
                   
                   //
                   const char* PARAM_INPUT_1 = "relay";  
                   const char* PARAM_INPUT_2 = "state";
                   
                   // Replaces placeholder with button section in your web page
                   String processor(const String& var){
                     //Serial.println(var);
                     if(var == "BUTTONPLACEHOLDER"){
                       String buttons ="";
                       for(int i=1; i<=NUM_RELAYS; i++){
                         String relayStateValue = relayState(i);
                         //Here parts of the HTML will be parsed to index.html like Relay # followed by its value in variable for the GPIO numbers
                         buttons+= "<h4>Turn on water on " + String(i) + "</h4><h4>Valve (relay) #" + String(i) + " - GPIO " + relayGPIOs[i-1] + "</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"" + String(i) + "\" "+ relayStateValue +"><span class=\"slider\"></span></label>";
                       }
                       return buttons;
                     }
                     return String();
                   }
                   
                   String relayState(int valveRelayNum){
                     if(RELAY_NO){
                       if(digitalRead(relayGPIOs[valveRelayNum-1])){
                         return "";
                       }
                       else {
                         return "checked";
                       }
                     }
                     else {
                       if(digitalRead(relayGPIOs[valveRelayNum-1])){
                         return "checked";
                       }
                       else {
                         return "";
                       }
                     }
                     return "";
                   }
                   
                   void setup(){
                     // Serial port for debugging purposes
                     Serial.begin(9600);
                     // Initialize SPIFFS
                     if(!SPIFFS.begin(true)){
                       Serial.println("An Error has occurred while mounting SPIFFS");
                       return;
                     }
                     // Set all relays to off when the program starts - if set to Normally Open (NO), the relay is off when you set the relay to HIGH
                     for(int i=1; i<=NUM_RELAYS; i++){
                       pinMode(relayGPIOs[i-1], OUTPUT);
                       if(RELAY_NO){
                         digitalWrite(relayGPIOs[i-1], HIGH);
                       }
                       else{
                         digitalWrite(relayGPIOs[i-1], LOW);
                       }
                     }
                     
                     // Connect the ESP to the Wi-Fi using the credentials entered before
                     WiFi.begin(ssid, password);
                     while (WiFi.status() != WL_CONNECTED) {
                       delay(1000);
                       Serial.println("Connecting to WiFi..");
                     }
                   
                     // Print ESP32 Local IP Address
                     Serial.println(WiFi.localIP());
                   
                   /*
                   *Now we are going to configure the route where server will be listening for incoming HTTP requests 
                   and a function that will be executed when a request is received on that route.
                   We specify this by calling the "on" method on the server object. With server.on(){}; 
                   As first input, this method receives a string with the path where it will be listening. 
                   We are going to set it to listen for requests on the "/" route. This could be anything. 
                   It is basically what you write after the ip adress when in the browser or an APP.
                   This website has a great explanation of the ESP32 Arduino: Asynchronous HTTP web server
                   https://techtutorialsx.com/2017/12/01/esp32-arduino-asynchronous-http-webserver/
                   So... 
                   - First parameter here is: "/" thats the root directory.
                   - Second parameter is HTTP_GET thats an enum of type WebRequestMethod a method defined in the library here --> https://github.com/me-no-dev/ESPAsyncWebServer/blob/63b5303880023f17e1bca517ac593d8a33955e94/src/ESPAsyncWebServer.h
                   - Third parameter is a the function AsyncWebServerRequest
                   So there is this c++ lambda function used here. My litle understanding is that they are locally declared unamed function this means they dont have a name and are declared locally :-)
                    I don't grasp the concept fully haha.
                    the syntax is [captures](params){body} where in here [] is empity
                   */
                   
                     // Route for root / web page
                     server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
                       request->send(SPIFFS, "/index.html", String(), false, processor);
                     });
                   
            // Send a GET request to <ESP_IP>/update?relay=<inputMessage>&state=<inputMessage2>
             server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) {
               String inputMessage;
               String inputParam;
               String inputMessage2;
               String inputParam2;
               // GET input1 value on <ESP_IP>/update?relay=<inputMessage>
               if (request->hasParam(PARAM_INPUT_1) & request->hasParam(PARAM_INPUT_2)) {
                 inputMessage = request->getParam(PARAM_INPUT_1)->value();
                 inputParam = PARAM_INPUT_1;
                 inputMessage2 = request->getParam(PARAM_INPUT_2)->value();
                 inputParam2 = PARAM_INPUT_2;
                 if(RELAY_NO){
                   Serial.print("NO ");
                   digitalWrite(relayGPIOs[inputMessage.toInt()-1], !inputMessage2.toInt());
                 }
                 else{
                   Serial.print("NC ");
                   digitalWrite(relayGPIOs[inputMessage.toInt()-1], inputMessage2.toInt());
                 }
               }
               else {
                 inputMessage = "No message sent";
                 inputParam = "none";
               }
               Serial.println(inputMessage + inputMessage2);
           
               //This is the last part of the lambda function. 
               //This method receives as first input the HTTP response code, which will be 200 in our case.  This is the HTTP response code for "OK".
               request->send_P(200, "text/plain", "OK");
             });
           
             // Start server here
             server.begin();
           }
           
           void loop(){
           
           }
        

注意:hasParam(PARAM_INPUT_1) & hasParam(PARAM_INPUT_2) 用的是按位的 &;逻辑与应该是 &&。两个 bool 的情况下结果碰巧一样,但它不会短路,正确的运算符是 &&。

这段代码的当前版本(GitHub 上的 E-SPlash)

到目前为止的 HTML 代码。


<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h2 {font-size: 3.0rem;}
    p {font-size: 3.0rem;}
    body {max-width: 600px; margin:0px auto; padding-bottom: 25px;}
    .switch {position: relative; display: inline-block; width: 120px; height: 68px} 
    .switch input {display: none}
    .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 34px}
    .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 68px}
    input:checked+.slider {background-color: #2196F3}
    input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
  </style>
</head>
<body>
  <h2>Fab Farm Irrigation System</h2>
  %BUTTONPLACEHOLDER%
<script>function toggleCheckbox(element) {
  var xhr = new XMLHttpRequest();
  if(element.checked){ xhr.open("GET", "/update?relay="+element.id+"&state=1", true); }
  else { xhr.open("GET", "/update?relay="+element.id+"&state=0", true); }
  xhr.send();
}</script>
</body>
</html>
                

HTML 文件必须放在一个名叫 "data" 的文件夹里,或者在 platformio.ini 里指定别的地址。

这段代码的当前版本(GitHub 上的 E-SPlash)

它现在长这样:

带继电器按钮的网页界面

现在的界面和代码都已经很像 Random Nerd Tutorials 上那个用 ESP32 控制继电器的页面了。

和原来代码的主要区别是:它跑在 VS Code 的 PlatformIO 里,而且给 HTML 文件用了单独的文件夹结构,那个文件会被烧进 ESP 的 SPIFFS。


2020 年 6 月 14 日

经过这一番自我发现之后,我决定得从头开始写代码,只保留农场现在那套灌溉系统的总体算法。原因是那段代码太乱、重复太多;昨天在那些资料里看到的方法让我意识到,把功能一个接一个加到最后那版代码上,比拿今天在农场里跑的代码重新写一遍要快。

今天我会试着加上实时的信息,比如温度和湿度。我又一次基于 Rui Santos 的工作,就是这一页关于气象站的。用这个思路,我相信我也能把一天中的时间用 RTC 时钟加进去。

接 DHT22 的接线图来自他的网站:

ESP32 与 DHT22 在面包板上的接线

它用一个 4.7k 欧姆的电阻做上拉。

费了一点功夫,页面现在长这样:

显示温度和湿度的网页

我觉得温度现在是负数,是因为温湿度传感器坏了,或者我的接线有点松。我想,等我自己铣一块电路板、把东西焊上去,而不是插在面包板上,应该会更可靠。不管怎样,我能拿到数值,也能在网页界面上显示它们。

在串口监视器里我已经能显示网络时间和网络信息了;现在我得想办法把时间的值取出来,显示到网页界面上。目前我在代码里发的是 TESTE 这个词,出现在时间位置的就是它。

串口监视器显示网络时间和网络信息


2020 年 6 月 15 日

今天我会集中精力把时间显示到网页界面上,也许再写一个按设定时间打开继电器的函数。

20:44 的更新:

- 折腾了一整天之后,我成功地把机器的时间和灌溉系统(ESP32)的时间显示出来了。

现在界面长这样:

网页界面显示机器的时间和 ESP32 的时间

等我长大了我想当程序员 :-),而不是把一整天花在一件别人几行代码就能搞定的事情上!

多亏了一位圣人,我的苦难结束了,这个回答就在 Stack Exchange 上。

这里是那边贴的原函数:


void printLocalTime()
{
  time_t rawtime;
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo))
  {
    Serial.println("Failed to obtain time");
   return;
  }
  char timeStringBuff[50]; //50 chars should be enough
  strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
  //print like "const char*"
  Serial.println(timeStringBuff);

  //Optional: Construct String object 
  String asString(timeStringBuff);
}

做了一点小改动,把一个 void 函数改成一个 String 函数,并加了一个我叫做 "timeAsAString" 的 return:


//this function was found here https://arduino.stackexchange.com/questions/52676/how-do-you-convert-a-formatted-print-statement-into-a-string-variable
//I did a minor change so instead of a void function it now returns a string to be used to show time in the webinterface
String printFarmTime()
{
  time_t rawtime;
  struct tm timeinfo;
  getLocalTime(&timeinfo);
  char timeStringBuff[50]; //50 chars should be enough
  strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
  //print like "const char*"
  Serial.println(timeStringBuff);

  //Construct to create the String object
  String timeAsAString(timeStringBuff);
  return timeAsAString;
}

注意:这一版去掉了 if(!getLocalTime(&timeinfo)) 这个检查,所以如果 NTP 还没回应,它会格式化出一个空的时间(垃圾值)。保留这个检查,失败时返回类似 "time not set" 的东西。

那么,这个问题解决了,接下来我要试着写按设定时间打开和关闭继电器的函数。看看我今天能不能做完……


2020 年 6 月 16 日

没有,我没做那件事,我累了,所以今天画了一些草图。

盒子和网页界面的草图

在这张草图里,我画了盒子会是什么形状,以及网页界面会是什么样。

我也在读关于 String 及其操作的内容。

我可以走这条路:把日期当 String 拆开,再转换成我需要的数据类型,用来做我想做的运算。另一种可能是从一开始就拿到我需要的那种类型的数据;虽然我更喜欢第二种,但我还没找到办法,因为我还没真正弄懂 getLocalTime() 这个函数。所以我现在要多读一点关于它的资料。

等一下……这次很快!我找到一个能解决我问题的帖子。在这里:

Arduino 论坛里关于 getLocalTime 的帖子 用户 Abhay 发在 Arduino 论坛的帖子

于是我就照着上面那个帖子写了下面这个函数:


          
      int gimeTime(char what) {

          int OnlyYear;
          int onlyMonth;
          int onlyDay;
          int onlyHour;
          int onlyMin;
          int onlySec;
      
          struct tm timeinfo;
          if(!getLocalTime(&timeinfo)){
            Serial.println("Failed to obtain time");
            //return;
            }
          onlyHour = timeinfo.tm_hour;
          onlyMin  = timeinfo.tm_min;
          onlySec  = timeinfo.tm_sec;
          onlyDay = timeinfo.tm_mday;
          onlyMonth = timeinfo.tm_mon + 1;
          OnlyYear = timeinfo.tm_year +1900;
      
          switch (what) {
            case 1:
            return onlyHour;
            break;
            case 2:
            return onlyMin;
            break;
            case 3:
            return onlySec;
            break;
            default:
          // if nothing else matches, do the default
          // default is optional
          break;
          }
      }
    
    

注意:那个 default 结尾没有 return,所以对于 1、2、3 以外的任何值,函数会返回一个未定义的值(在那里加上 return -1;)。getLocalTime() 失败时也一样:既然 return 被注释掉了,它就会继续去读一个没有有效时间的 timeinfo。

就这样,现在终于可以写定时器的时间安排代码了。顺带说一句,我几乎花了一整天想找一个更简单的取时间的办法,但就是没做成,因为我还在费劲理解结构体(struct)这个类型。嗯,也许以后我会回头做这件事,但目前这样就行了。


2020 年 6 月 17 日

凌晨更新:

我非常沮丧;我得转到原子那边,把我项目的比特那部分收尾。

深夜时分,我最多只写了一个关闭继电器的小函数;在这里:


        int zoneStartStop (int zone, int startStop){
        int waitTime = 1000;
        if (startStop == 1){
        digitalWrite(relayGPIOs[zone], startStop);
        Serial.print("*** Zone Valve");
        Serial.print(zone);
        Serial.println("ON ***");
        delay(waitTime);
        digitalWrite(pumpGpio, startStop);
        Serial.print("*** Pump turned ON ***");
        }
        else{
        digitalWrite(pumpGpio, startStop);
        Serial.println("*** Pump turned OFF ***");
        delay(waitTime);
        digitalWrite(relayGPIOs[zone], startStop);
        Serial.print("*** Zone Valve");
        Serial.print(zone);
        Serial.println("OFF ***");
        }
        }

(后面改正过的代码:停机时,现在水泵先关,阀后关,这样永远不会顶着关着的阀推水,消息里也写 OFF。)

这是个开头,但还差得远,而且我还没测试过。

早上的更新:

那么现在我要看另一个定时器编程的例子,又是 Rui Santos 的;在这里。也许我可以改动一下,把它集成到我的代码里,再加上一个开始时间,然后用滑块设定它跑多少分钟。目前这样就够用了!

到目前为止,我让代码兼容了 PlatformIO,还把 index.html 文件从 C++ 代码里分出来,放到一个单独的文件夹,通过我在 13/06/2020 描述的 SPIFFS 烧录流程写进去。


那天之后,我转到了界面与应用编程周,在那里我展示了自己是怎么设计最终电路板的,以及界面上的最后一些调整。


设计我的灌溉系统的电路板

我拿 ESP32 开发板玩了一段时间之后,对我板子上需要什么已经有个大概想法了。那就来看看我想要的这些功能,列一个清单:

  1. 控制:
    1. 3 个阀用继电器;
    2. 1 个水泵继电器;
  2. 传感:
    1. 温湿度传感器;
    2. 电流表(水泵的)扩展接针;
    3. 电池充电和测量;
    4. RTC 时钟的扩展接针或焊接焊盘;

需求齐了,我就去找一个 Eagle 的 ESP-WROOM-32 库,找到了好几个:

  1. MacroYau/MacroYau-EAGLE-Libraries
  2. lpodkalicki/eagle-libraries
  3. sparkfun/ESP32_Miscellany

全部评估之后,我决定用 MacroYau/MacroYau 那个。

Eagle 里 MacroYau 库的 ESP-WROOM-32

因为我想要尽量兼容自己用惯的 ESP32 开发板,我去找了一下,找到了一份 ESP32 WROOM 的引脚图,就用它来给引脚命名。

ESP-WROOM-32 的引脚图又是在 Random Nerd Tutorials 网站上找到的 :-)

下一步是在每个引脚上接网络标签。要是我按现在展示的顺序来做就好了,那样就能省掉重画很多第一次接错的线的功夫。嗯,教训是:引脚的逻辑编号和物理编号并不总是一样;所以这次带着这个认识,我把它们都接对了。

在 Eagle 里接网络标签

这是最终结果:

所有网络标签都接好的 ESP32

我在 SnapEDA 上找到的继电器不是我手上那一款,但封装是一样的。

继电器的电路是我在网上找到的,在这里,就在 web.archive 上;原理图如下。

网上找到的继电器电路原理图

有原理图在手,我在 Eagle 里画了下面这些:

Eagle 里的继电器原理图

电池电路我照的是 Random Nerd Tutorials 页面上的一个电路。

name太阳能供电并监测电池电量的 ESP32 电路

因为目前我还没有 TP4056 模块,我的电路只包含测量部分。

供电和电池测量的原理图

分压器里的电阻是用这里的在线计算器算的。

分压计算器:5 V、1.2 MOhm 和 1.5 MOhm 得出 2.778 V

为以后做准备,我在这儿找到了 TP4056 的库,就在这里,多亏我的远程导师 Luis Carvão 给的提示。

湿度传感器的库在这里;有了它,只需要把几根网络标签接到 ESP32 的 32 号引脚。

湿度和温度传感器的原理图

电流传感器我选择用 ACS712;库是在这个 GitHub 上找到的。画好的原理图如下:

ACS712 电流传感器的原理图

我还把实时时钟的库从 SnapEDA 拿下来了,但暂时不用,所以我把完整的原理图画在下面:

完整原理图,第一版

盯着原理图看了一会儿,我发现了几个错误:我忘了把继电器 LED 的电阻接到 VCC,也忘了加一个复位按钮和一个编程按钮,这两个是把 ESP 放进编程模式所必需的,还忘了串口模式和扩展用的接针;最后,我决定加一个晶振,让计时更准。

然后我画了复位和编程按钮:

复位和编程按钮的原理图

这里我修正了继电器,加上了 VCC,并且把光耦放在信号和继电器之间,以避免从继电器过来的任何反馈。

带光耦的继电器原理图

这里我加了 TX RX 接针、一个让内部 RTC 计时更准的晶振,还有与 ESP32 DevKit 兼容的接针。

接针和晶振的原理图

那么,希望这就是我最终的原理图……

最终原理图

现在我要开始给板子布线了。我得说,我有点紧张,因为我从来没做过这么复杂的板子!

看,我在 Eagle 里切到 board 模式时看到的东西多让人振奋!!!

Eagle 的 board 模式下还没布线的板子

我做的第一件事,是把 DevKit 的封装和 ESP32 一起移到中间。

移到中间的 DevKit 和 ESP32 封装

手工布了一会儿之后,我给自动布线器一次机会。几分钟后我就不得不停下来,结果很可笑。我会试着把 ESP32 往上移再试一次,如果不行,我就放弃和 ESP32 DevKit 的兼容。

自动布线器的结果

于是我放弃了和 DevKit 的兼容,决定这次慢慢来。我备份了自己的工作,在另一个 Eagle 窗口里打开它,然后删掉所有我当时不处理的元件;原理图和电路板就成了那样。我的想法是每完成一步,就把元件从备份里拷回来。下面是第一步的样子。

第一步的原理图

在板子上可以看到 #1,继电器;#2,电流传感器电路;还有 #3,板子上的 LED。

第一步的板子:继电器、电流传感器和 LED

那么现在我把第一个阀继电器拷了进来,等我切到板子时,Eagle 把元件贴在一起给我看。我觉得这样方便多了。

贴入第一个阀继电器的板子

到目前为止板子是这样的。我还没布任何电源线,包括 GND、3.3V 和 5V。我现在会试一下自动布线器,看看大概样子,也许再手工布。整个布线过程在输入设备周里描述得更详细。

布电源线之前的板子

电路板布局完成后,我决定加上 logo。点击 File、Import 和 Bitmap(目前 Eagle 只支持 .bmp)。

Eagle 的 File、Import、Bitmap 菜单

选择要导入的颜色。

选择要导入的颜色

然后选择单位,我这里是 mm,还有尺寸,然后 OK。

选择 logo 的单位和尺寸

因为我想把 logo 铣出来而不是印上去,我不得不把它移到顶层。

移到顶层的 logo

最终的电路板布局,带钻孔和铣削轮廓,就像输出设备周里描述的那样。

带钻孔和铣削轮廓的最终电路板布局

铣削

这里展示 G-code 刀路的可视化,红色是靠近将要保留的铜箔的那条路径。

G-code 刀路的可视化

和往常一样,而且在输入设备周里有更详细的描述,这里看一眼自动找平(autolevel)软件。

自动找平软件

铣削电路板的轮廓

焊接与测试、调试

对着光检查板子。

为了检查所有走线都没问题,我把灯放在板子下面,把一切审查一遍,找短路或者缺失的走线。

把灯放在板子下面检查

正在装配的背面。

我把大元件放在最后,免得影响小 SMD 元件的焊接。

正在装配的板子背面

看串口监视器和原理图,以烧录固件

全部焊完之后,我把板子接上我的串口适配器。原理图如下。

用串口适配器烧录固件的接线原理图

我把 ESP32 第一次用出厂固件跑的输出粘了下来。它启动起来了!


            --- Available filters and text transformations: colorize, debug, default, direct, esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time
            --- More details at http://bit.ly/pio-monitor-filters
            --- Miniterm on COM5  115200,8,N,1 ---
            --- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
            ets Jun  8 2016 00:22:57

            rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
            configsip: 0, SPIWP:0x00
            clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
            mode:DIO, clock div:2
            load:0x3fff0008,len:8
            load:0x3fff0010,len:3480
            load:0x40078000,len:7804
            ho 0 tail 12 room 4
            load:0x40080000,len:252
            entry 0x40080034
            ␛[0;32mI (45) boot: ESP-IDF v2.0-3-gbef9896 2nd stage bootloader␛[0m   
            ␛[0;32mI (45) boot: compile time 05:59:45␛[0m
            ␛[0;32mI (45) boot: Enabling RNG early entropy source...␛[0m
            ␛[0;32mI (64) boot: SPI Speed      : 40MHz␛[0m
            ␛[0;32mI (77) boot: SPI Mode       : DIO␛[0m
            ␛[0;32mI (89) boot: SPI Flash Size : 4MB␛[0m
            ␛[0;32mI (101) boot: Partition Table:␛[0m
            ␛[0;32mI (113) boot: ## Label            Usage          Type ST Offset   Length␛[0m
            ␛[0;32mI (136) boot:  0 phy_init         RF data          01 01 0000f000 00001000␛[0m
            ␛[0;32mI (159) boot:  1 otadata          OTA data         01 00 00010000 00002000␛[0m
            ␛[0;32mI (182) boot:  2 nvs              WiFi data        01 02 00012000 0000e000␛[0m
            ␛[0;32mI (205) boot:  3 at_customize     unknown          40 00 00020000 000e0000␛[0m
            ␛[0;32mI (228) boot:  4 ota_0            OTA app          00 10 00100000 00180000␛[0m
            ␛[0;32mI (252) boot:  5 ota_1            OTA app          00 11 00280000 00180000␛[0m
            ␛[0;32mI (275) boot: End of partition table␛[0m
            ␛[0;32mI (288) boot: Disabling RNG early entropy source...␛[0m
            ␛[0;32mI (305) boot: Loading app partition at offset 00100000␛[0m
            ␛[0;32mI (1481) boot: segment 0: paddr=0x00100018 vaddr=0x00000000 size=0x0ffe8 ( 65512) ␛[0m
            ␛[0;32mI (1481) boot: segment 1: paddr=0x00110008 vaddr=0x3f400010 size=0x1c5f0 (116208) map␛[0m
            ␛[0;32mI (1498) boot: segment 2: paddr=0x0012c600 vaddr=0x3ffb0000 size=0x0215c (  8540) load␛[0m
            ␛[0;32mI (1528) boot: segment 3: paddr=0x0012e764 vaddr=0x40080000 size=0x00400 (  1024) load␛[0m
            ␛[0;32mI (1551) boot: segment 4: paddr=0x0012eb6c vaddr=0x40080400 size=0x1b028 (110632) load␛[0m
            ␛[0;32mI (1630) boot: segment 5: paddr=0x00149b9c vaddr=0x400c0000 size=0x00034 (    52) load␛[0m
            ␛[0;32mI (1631) boot: segment 6: paddr=0x00149bd8 vaddr=0x00000000 size=0x06430 ( 25648) ␛[0m
            ␛[0;32mI (1648) boot: segment 7: paddr=0x00150010 vaddr=0x400d0018 size=0x7a56c (501100) map␛[0m
            ␛[0;32mI (1675) heap_alloc_caps: Initializing. RAM available for dynamic allocation:␛[0m
            ␛[0;32mI (1697) heap_alloc_caps: At 3FFBA6B8 len 00025948 (150 KiB): DRAM␛[0m
            ␛[0;32mI (1719) heap_alloc_caps: At 3FFE8000 len 00018000 (96 KiB): D/IRAM␛[0m
            ␛[0;32mI (1740) heap_alloc_caps: At 4009B428 len 00004BD8 (18 KiB): IRAM␛[0m
            ␛[0;32mI (1761) cpu_start: Pro cpu up.␛[0m
            ␛[0;32mI (1773) cpu_start: Single core mode␛[0m
            ␛[0;32mI (1786) cpu_start: Pro cpu start user code␛[0m
            ␛[0;32mI (1846) cpu_start: Starting scheduler on PRO CPU.␛[0m
            ␛[0;32mI (2085) uart: queue free spaces: 10␛[0m
            Bin version:0.10.0
            I (2087) wifi: wifi firmware version: c604573
            I (2087) wifi: config NVS flash: enabled
            I (2088) wifi: config nano formating: disabled
            I (2096) wifi: Init dynamic tx buffer num: 32
            I (2097) wifi: wifi driver task: 3ffc4eac, prio:23, stack:3584
            I (2102) wifi: Init static rx buffer num: 10
            I (2106) wifi: Init dynamic rx buffer num: 0
            I (2110) wifi: Init rx ampdu len mblock:7
            I (2114) wifi: Init lldesc rx ampdu entry mblock:4
            I (2118) wifi: wifi power manager task: 0x3ffca254 prio: 21 stack: 2560
            I (2125) wifi: wifi timer task: 3ffcb2d4, prio:22, stack:3584
            ␛[0;31mE (2130) phy_init: PHY data partition validated␛[0m
            ␛[0;32mI (2152) phy: phy_version: 329, Feb 22 2017, 15:58:07, 0, 0␛[0m
            I (2152) wifi: mode : softAP (3c:71:bf:10:66:e1)
            I (2155) wifi: mode : sta (3c:71:bf:10:66:e0) + softAP (3c:71:bf:10:66:e1)
            I (2159) wifi: mode : softAP (3c:71:bf:10:66:e1)
        

烧录固件:

为了烧录我的固件,我用 Microsoft VS Code 加 PlatformIO。

在开始烧录之前,我首先得把 PlatformIO 的环境配好。我在这里、在项目开发那一周里描述过这件事。

然后我按住 GPIO0 的按钮,再按复位按钮,进入固件烧录模式。接着我点烧录固件的图标,它就烧进去了。成功!


调试电路板。

最后我把所有元件都拆掉,直接分析微处理器产生的信号。

调试的结果是,我发现代码、微处理器的焊接、以及附近的走线都没有问题。

我发现的是:我需要在每个光耦的信号和阳极(1 号引脚)之间放 220 欧姆的电阻。因为光耦里面有一个 LED,驱动它所用的电流必须限住。

怎么计算光耦 LED 的电阻 来自 网站 Electronics Stack Exchange

EL817 光耦的数据表:正向电压和正向电流

于是 R = (3.3 V - 1.2 V) / 20 mA = 105 欧姆,或者

在线 LED 计算器推荐的 120 欧姆电阻

于是我把走线切开,焊上 220 欧姆的电阻,因为我手上只有这个,而且它奏效了,但正确的值是 120 欧姆,就像在线电阻计算器显示的那样。

焊在切开走线上的 220 欧姆电阻

继电器的原理图最后成了这样:

带光耦的继电器电路原理图


固件里的网络代码

我的板子上正在进行两种通信,我可以描述一下:



手机和 ESP32

手机和 ESP32 之间的网络,是由几个库合作建立起来的:

  1. WiFi.h

    它的文档是这样描述它的:

    原文:"Enables network connection (local and Internet) using the ESP32 built-in WiFi. With this library you can initiate Servers, Clients and send/receive UDP packets through WiFi. The shield can connect either to open or encrypted networks (WEP, WPA). The IP address can be assigned statically or through a DHCP. The library can also manage DNS."(用 ESP32 内置的 WiFi 实现网络连接,本地和互联网都可以。用这个库你可以启动服务器、客户端,通过 WiFi 收发 UDP 包。这个 shield 可以连接开放网络或加密网络(WEP、WPA)。IP 地址可以静态分配,也可以通过 DHCP 获取。这个库还能管理 DNS。)

  2. AsyncTCP.h

    它的文档是这样描述它的:

    原文:"This is a fully asynchronous TCP library, aimed at enabling trouble-free, multi-connection network environment for Espressif's ESP32 MCUs. This library is the base for ESPAsyncWebServer"(这是一个完全异步的 TCP 库,目的是给乐鑫的 ESP32 微控制器提供一个无麻烦的、多连接的网络环境。这个库是 ESPAsyncWebServer 的基础。)

  3. ESPAsyncWebServer.h

    它的文档是这样描述它的:

    原文:"Async HTTP and WebSocket Server for ESP8266 Arduino"(ESP8266 Arduino 的异步 HTTP 和 WebSocket 服务器)

    所以基本上,在我的项目里,这几个库负责建立与路由器的网络连接,同时也负责创建一个无线接入点。

    它们还创建了 web 服务器和一个 DHCP 服务器,这样如果有人通过这段代码创建的接入点连进来,它就会给一个 IP 地址:

      //Soft Wifi Access point setup
      WiFi.softAP("softap", "password_here");
      IPAddress IP = WiFi.softAPIP();

    与路由器的连接是用这段代码建立的:

      //start wifi sessions as a client.
      //Wifi client setup
      const char* ssid = doc["data"]["ssid"];
      const char* password = doc["data"]["pass"];
      ssid = "fabfarm";
      password = "password_here";
      WiFi.begin(ssid, password);

    注意:从 JSON 里读到的 ssid 和 password 紧接着就被下两行里写死的值覆盖了,所以 JSON 文件里的凭据从来没被用到。如果想让 JSON 里的值生效,就把那两条固定赋值删掉。

    web 服务器是用这段代码启动的:AsyncWebServer server(81);


用 ArduinoJson 把 C++ 和 HTML 连起来

JSON 是我选的、用于网页界面和微处理器之间通信的格式。

关于 ArduinoJson.h 的文档是这样描述它的:

原文:ArduinoJson is a C++ JSON library for Arduino and IoT (Internet Of Things)(ArduinoJson 是一个用于 Arduino 和 IoT(物联网)的 C++ JSON 库)

一个很好的文档来源,也是很好的助手,可以用来写解析程序 deserializeJson() 和序列化程序 serializeJson(doc, Serial),就是 ArduinoJson 的网站。

为了用这个库,我首先得创建一个 json 文件。这个文件会被助手用来生成 C++ 代码。

手工把 JSON 文件建好之后,就该写 C++ 程序了。助手会生成 C++ 代码,用来序列化,也用来把数据在文件系统里的那个 JSON 文件之间来回解析。

ArduinoJson 助手,第一步

我在左边的对话框里粘上了 json 文件的内容。

粘进 ArduinoJson 助手的 JSON 文件

网站会自动生成代码。

ArduinoJson 助手生成的代码

下面展示助手生成的代码。

解析程序

const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_ARRAY_SIZE(2) + 2*JSON_ARRAY_SIZE(3) + JSON_OBJECT_SIZE(2) + 6*JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + 3*JSON_OBJECT_SIZE(6) + 540;
DynamicJsonDocument doc(capacity);

const char* json = "{\"data\":{\"currentTime\":\"Wednesday, June 24 2020 23:49:23\",\"temperature\":\"22.30\",\"humidity\":\"74.00\",\"override\":0,\"ssid\":\"rato\",\"pass\":\"imakestuff\"},\"relays\":[{\"name\":\"Fruit Tree\",\"pin\":25,\"isRunning\":1,\"isEnabled\":0,\"times\":[{\"startTime\":\"01:01\",\"duration\":0,\"hour\":1,\"min\":1}]},{\"name\":\"Vegie Garden\",\"pin\":26,\"isRunning\":1,\"isEnabled\":0,\"status\":1,\"times\":[{\"startTime\":\"02:02\",\"duration\":32,\"hour\":2,\"min\":2},{\"startTime\":\"02:02\",\"duration\":32,\"hour\":2,\"min\":2}]},{\"name\":\"Cypress Hill\",\"pin\":33,\"isRunning\":1,\"isEnabled\":0,\"status\":1,\"times\":[{\"startTime\":\"03:03\",\"duration\":60,\"hour\":3,\"min\":3},{\"startTime\":\"03:03\",\"duration\":60,\"hour\":3,\"min\":3},{\"startTime\":\"03:03\",\"duration\":60,\"hour\":3,\"min\":3}]}]}";

deserializeJson(doc, json);

JsonObject data = doc["data"];
const char* data_currentTime = data["currentTime"]; // "Wednesday, June 24 2020 23:49:23"
const char* data_temperature = data["temperature"]; // "22.30"
const char* data_humidity = data["humidity"]; // "74.00"
int data_override = data["override"]; // 0
const char* data_ssid = data["ssid"]; // "rato"
const char* data_pass = data["pass"]; // "imakestuff"

JsonArray relays = doc["relays"];

JsonObject relays_0 = relays[0];
const char* relays_0_name = relays_0["name"]; // "Fruit Tree"
int relays_0_pin = relays_0["pin"]; // 25
int relays_0_isRunning = relays_0["isRunning"]; // 1
int relays_0_isEnabled = relays_0["isEnabled"]; // 0

JsonObject relays_0_times_0 = relays_0["times"][0];
const char* relays_0_times_0_startTime = relays_0_times_0["startTime"]; // "01:01"
int relays_0_times_0_duration = relays_0_times_0["duration"]; // 0
int relays_0_times_0_hour = relays_0_times_0["hour"]; // 1
int relays_0_times_0_min = relays_0_times_0["min"]; // 1

JsonObject relays_1 = relays[1];
const char* relays_1_name = relays_1["name"]; // "Vegie Garden"
int relays_1_pin = relays_1["pin"]; // 26
int relays_1_isRunning = relays_1["isRunning"]; // 1
int relays_1_isEnabled = relays_1["isEnabled"]; // 0
int relays_1_status = relays_1["status"]; // 1

JsonObject relays_1_times_0 = relays_1["times"][0];
const char* relays_1_times_0_startTime = relays_1_times_0["startTime"]; // "02:02"
int relays_1_times_0_duration = relays_1_times_0["duration"]; // 32
int relays_1_times_0_hour = relays_1_times_0["hour"]; // 2
int relays_1_times_0_min = relays_1_times_0["min"]; // 2

JsonObject relays_1_times_1 = relays_1["times"][1];
const char* relays_1_times_1_startTime = relays_1_times_1["startTime"]; // "02:02"
int relays_1_times_1_duration = relays_1_times_1["duration"]; // 32
int relays_1_times_1_hour = relays_1_times_1["hour"]; // 2
int relays_1_times_1_min = relays_1_times_1["min"]; // 2

JsonObject relays_2 = relays[2];
const char* relays_2_name = relays_2["name"]; // "Cypress Hill"
int relays_2_pin = relays_2["pin"]; // 33
int relays_2_isRunning = relays_2["isRunning"]; // 1
int relays_2_isEnabled = relays_2["isEnabled"]; // 0
int relays_2_status = relays_2["status"]; // 1

JsonArray relays_2_times = relays_2["times"];

JsonObject relays_2_times_0 = relays_2_times[0];
const char* relays_2_times_0_startTime = relays_2_times_0["startTime"]; // "03:03"
int relays_2_times_0_duration = relays_2_times_0["duration"]; // 60
int relays_2_times_0_hour = relays_2_times_0["hour"]; // 3
int relays_2_times_0_min = relays_2_times_0["min"]; // 3

JsonObject relays_2_times_1 = relays_2_times[1];
const char* relays_2_times_1_startTime = relays_2_times_1["startTime"]; // "03:03"
int relays_2_times_1_duration = relays_2_times_1["duration"]; // 60
int relays_2_times_1_hour = relays_2_times_1["hour"]; // 3
int relays_2_times_1_min = relays_2_times_1["min"]; // 3

JsonObject relays_2_times_2 = relays_2_times[2];
const char* relays_2_times_2_startTime = relays_2_times_2["startTime"]; // "03:03"
int relays_2_times_2_duration = relays_2_times_2["duration"]; // 60
int relays_2_times_2_hour = relays_2_times_2["hour"]; // 3
int relays_2_times_2_min = relays_2_times_2["min"]; // 3

序列化程序

const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_ARRAY_SIZE(2) + 2*JSON_ARRAY_SIZE(3) + JSON_OBJECT_SIZE(2) + 6*JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + 3*JSON_OBJECT_SIZE(6);
DynamicJsonDocument doc(capacity);

JsonObject data = doc.createNestedObject("data");
data["currentTime"] = "Wednesday, June 24 2020 23:49:23";
data["temperature"] = "22.30";
data["humidity"] = "74.00";
data["override"] = 0;
data["ssid"] = "rato";
data["pass"] = "imakestuff";

JsonArray relays = doc.createNestedArray("relays");

JsonObject relays_0 = relays.createNestedObject();
relays_0["name"] = "Fruit Tree";
relays_0["pin"] = 25;
relays_0["isRunning"] = 1;
relays_0["isEnabled"] = 0;

JsonArray relays_0_times = relays_0.createNestedArray("times");

JsonObject relays_0_times_0 = relays_0_times.createNestedObject();
relays_0_times_0["startTime"] = "01:01";
relays_0_times_0["duration"] = 0;
relays_0_times_0["hour"] = 1;
relays_0_times_0["min"] = 1;

JsonObject relays_1 = relays.createNestedObject();
relays_1["name"] = "Vegie Garden";
relays_1["pin"] = 26;
relays_1["isRunning"] = 1;
relays_1["isEnabled"] = 0;
relays_1["status"] = 1;

JsonArray relays_1_times = relays_1.createNestedArray("times");

JsonObject relays_1_times_0 = relays_1_times.createNestedObject();
relays_1_times_0["startTime"] = "02:02";
relays_1_times_0["duration"] = 32;
relays_1_times_0["hour"] = 2;
relays_1_times_0["min"] = 2;

JsonObject relays_1_times_1 = relays_1_times.createNestedObject();
relays_1_times_1["startTime"] = "02:02";
relays_1_times_1["duration"] = 32;
relays_1_times_1["hour"] = 2;
relays_1_times_1["min"] = 2;

JsonObject relays_2 = relays.createNestedObject();
relays_2["name"] = "Cypress Hill";
relays_2["pin"] = 33;
relays_2["isRunning"] = 1;
relays_2["isEnabled"] = 0;
relays_2["status"] = 1;

JsonArray relays_2_times = relays_2.createNestedArray("times");

JsonObject relays_2_times_0 = relays_2_times.createNestedObject();
relays_2_times_0["startTime"] = "03:03";
relays_2_times_0["duration"] = 60;
relays_2_times_0["hour"] = 3;
relays_2_times_0["min"] = 3;

JsonObject relays_2_times_1 = relays_2_times.createNestedObject();
relays_2_times_1["startTime"] = "03:03";
relays_2_times_1["duration"] = 60;
relays_2_times_1["hour"] = 3;
relays_2_times_1["min"] = 3;

JsonObject relays_2_times_2 = relays_2_times.createNestedObject();
relays_2_times_2["startTime"] = "03:03";
relays_2_times_2["duration"] = 60;relays_2_times_2["hour"] = 3;
relays_2_times_2["min"] = 3;

serializeJson(doc, Serial);

在我朋友 Jeff Knight 的帮助下(没有他就做不出来),生成的代码被简化成了我的程序。然后我写了几个函数,下面的图片显示串口监视器正在打印读到的 JSON 文件反序列化后的结果。

串口监视器打印反序列化后的 JSON 文件

我画了这张逻辑图,免得忘了它是怎么工作的:

E-Splash 固件的逻辑图


铣削外壳

我在 SolidWorks 里设计了外壳,还做了板子装在里面的动画,如下。

这里我铣了外壳的内部,螺丝孔和支柱都做成一体。

铣削外壳内部

把外壳在夹具里翻过来之后,我能把它对准得很好,然后就铣了正面。

在夹具里铣削外壳正面

这里是电路板和外壳。

电路板和外壳

这里我在接线之前先试一下板子能不能装进去。

试装:电路板装进外壳


在外壳里给板子接线。

我用了一个防水线缆穿入件把线引进外壳。

外壳上的防水线缆穿入件

这里我试一下板子带着线能不能装进外壳。

带着线试装电路板

这里线已经固定好了,可以封进里面。

固定在外壳里的线

这里外壳已经拧好,可以通电了。

封好、可以通电的外壳


涂环氧树脂。

我在外面贴了胶带,在孔里灌了几层环氧树脂,这样即使这些孔和里面的 LED 不完全对齐,LED 的光也能透出来。

LED 孔里分层涂的环氧树脂

这里是已经固化、还没打磨的环氧树脂。

打磨前已固化的环氧树脂


接线图

这里是整个系统的总图。

整个灌溉系统的接线图


温室里的管道

后来我搭了一些管子用于演示。

装在温室里的灌溉管道

而且一切都运行正常。

温室里正在运行的灌溉

温室里灌溉的演示


成品照(Hero shot)

到最后,它是这样工作的:

← (6月3日) 发明、知识产权与收入 · 所有周 · 最终项目 (6月10日) →