Monitor for central heating system (e.g. 2zones+hw) Supports up to 15 temp probes (DS18B20/DS18S20) 3 valve monitors Gas pulse meter recording Use stand-alone or with nodeEnergyServer See http://robdobson.com/2015/09/central-heating-monitor
Dependencies: EthernetInterfacePlusHostname NTPClient Onewire RdWebServer SDFileSystem-RTOS mbed-rtos mbed-src
main.cpp
00001 // Gas usage monitor 00002 // Counts pulses from a gas meter 00003 // Monitors temperature sensors 00004 // Monitors valve/pump activity 00005 // Web interface and UDP broadcast 00006 // Rob Dobson 2015 00007 00008 #include "mbed.h" 00009 #include "EthernetInterface.h" 00010 #include "NTPClient.h" 00011 #include "RdWebServer.h" 00012 #include "GasUseCounter.h" 00013 #include "Thermometers.h" 00014 #include "VoltAlerter.h" 00015 #include "Watchdog.h" 00016 #include "Logger.h" 00017 00018 // System name (used for hostname) 00019 char systemName[20] = "RdGasUseMonitor"; 00020 00021 // Web and UDB ports 00022 const int WEBPORT = 80; // Port for web server 00023 const int BROADCAST_PORT = 42853; // Arbitrarily chosen port number 00024 00025 // Main loop delay between data collection passes 00026 const int LOOP_DELAY_IN_MS = 250; 00027 00028 // Debugging and status 00029 RawSerial pc(USBTX, USBRX); 00030 DigitalOut led1(LED1); //ticking (flashes) 00031 DigitalOut led2(LED2); //state of the 1st voltage alerter 00032 DigitalOut led3(LED3); //socket connecting status 00033 DigitalOut led4(LED4); //server status 00034 00035 // Web server 00036 UDPSocket sendUDPSocket; 00037 Endpoint broadcastEndpoint; 00038 00039 // Network Time Protocol (NTP) 00040 NTPClient ntp; 00041 const int NTP_REFRESH_INTERVAL_HOURS = 1; 00042 00043 // Mutex for SD card access 00044 Mutex sdCardMutex; 00045 00046 // File system for SD card 00047 SDFileSystem sd(p5, p6, p7, p8, "sd"); 00048 00049 // Base folder for web file system 00050 char* baseWebFolder = "/sd/"; 00051 00052 // Log file names 00053 const char* gasPulseFileName1 = "/sd/curPulse.txt"; 00054 const char* gasPulseFileName2 = "/sd/curPulse2.txt"; 00055 const char* eventLogFileName = "/sd/log.txt"; 00056 const char* dataLogFileBase = "/sd/"; 00057 00058 // Logger 00059 Logger logger(eventLogFileName, dataLogFileBase, sdCardMutex); 00060 00061 // Gas use counter 00062 DigitalIn gasPulsePin(p21); 00063 GasUseCounter gasUseCounter(gasPulseFileName1, gasPulseFileName2, gasPulsePin, logger, sdCardMutex); 00064 00065 // Thermometers - DS18B20 OneWire Thermometer connections 00066 const PinName tempSensorPins[] = { p22 }; 00067 Thermometers thermometers(sizeof(tempSensorPins)/sizeof(PinName), tempSensorPins, LOOP_DELAY_IN_MS, logger); 00068 00069 // Voltage Sensors / Alerters 00070 const int NUM_VOLT_ALERTERS = 3; 00071 VoltAlerter voltAlerter1(p23); 00072 VoltAlerter voltAlerter2(p24); 00073 VoltAlerter voltAlerter3(p25); 00074 00075 // Watchdog 00076 Watchdog watchdog; 00077 00078 // Broadcast message format 00079 // Data format of the broadcast message - senml - https://tools.ietf.org/html/draft-jennings-senml-08 00080 // { 00081 // "e": [ 00082 // {"n":"gasCount","v":%d}, 00083 // {"n":"gasPulseRateMs","v":%d,"u":"ms"}, 00084 // {"n":"temp_%s","v":%0.1f,"u":"degC"}, 00085 // ... 00086 // {"n":"pump_%d","v":%d}, 00087 // ... 00088 // ], 00089 // "bt": %d 00090 // } 00091 const char broadcastMsgPrefix[] = "{\"e\":["; 00092 const char broadcastMsgGasFormat[] = "{\"n\":\"gasCount\",\"v\":%d,\"u\":\"count\"},{\"n\":\"gasPulseRateMs\",\"v\":%d,\"u\":\"ms\"}"; 00093 const char broadcastTemperatureFormat[] = "{\"n\":\"temp_%s_%s\",\"v\":%0.1f,\"u\":\"degC\"}"; 00094 const char broadcastVoltAlerterFormat[] = "{\"n\":\"pump_%d_%s\",\"bv\":%d}"; 00095 const char broadcastMsgSuffix[] = "],\"bt\":%d}"; 00096 00097 // Broadcast message length and buffer 00098 const int MAX_DEVICE_NAME_LEN = 20; 00099 const int broadcastMsgLen = sizeof(broadcastMsgPrefix) + 00100 sizeof(broadcastMsgGasFormat) + 00101 ((sizeof(broadcastTemperatureFormat)+MAX_DEVICE_NAME_LEN)*Thermometers::MAX_THERMOMETERS) + 00102 ((sizeof(broadcastVoltAlerterFormat)+MAX_DEVICE_NAME_LEN)*NUM_VOLT_ALERTERS) + 00103 sizeof(broadcastMsgSuffix) + 00104 60; 00105 char broadcastMsgBuffer[broadcastMsgLen]; 00106 00107 // Handling of SD card file delete and file upload commands 00108 const int MAX_FNAME_LENGTH = 127; 00109 char fileNameForWrite[MAX_FNAME_LENGTH+1] = ""; 00110 int fileRemainingForWrite = 0; 00111 00112 // Thermometer and pump names 00113 char thermometerAddrs[][MAX_DEVICE_NAME_LEN] = 00114 { 00115 "28b1b1e0050000d0", 00116 "289dd7c705000060", 00117 "28b3b0c60500000a" 00118 }; 00119 char thermometerNames[][MAX_DEVICE_NAME_LEN] = 00120 { 00121 "HWCylinder", 00122 "Inflow2", 00123 "Outflow2" 00124 }; 00125 int numNamedThermometers = 3; 00126 00127 char voltAlerterNames[][MAX_DEVICE_NAME_LEN] = 00128 { 00129 "PumpUpper", 00130 "PumpLower", 00131 "PumpHW" 00132 }; 00133 00134 // Get names of thermometers 00135 char* getThermometerName(char* thermAddr) 00136 { 00137 for (int i = 0; i < numNamedThermometers; i++) 00138 { 00139 if (strcmp(thermometerAddrs[i], thermAddr) == 0) 00140 return thermometerNames[i]; 00141 } 00142 return "Unknown"; 00143 } 00144 00145 // Get names of volt alerters 00146 char* getVoltAlerterName(int idx) 00147 { 00148 if (idx >= 0 && idx < NUM_VOLT_ALERTERS) 00149 { 00150 return voltAlerterNames[idx]; 00151 } 00152 return "Unknown"; 00153 } 00154 00155 // Format broadcast message 00156 void GenBroadcastMessage() 00157 { 00158 // Get temperature values 00159 TemperatureValue tempValues[Thermometers::MAX_THERMOMETERS]; 00160 int numTempValues = thermometers.GetTemperatureValues(Thermometers::MAX_THERMOMETERS, tempValues, 100); 00161 // for (int tempIdx = 0; tempIdx < numTempValues; tempIdx++) 00162 // { 00163 // printf("Temp: %.1f, Addr: %s, Time: %d\r\n", tempValues[tempIdx].tempInCentigrade, tempValues[tempIdx].address, tempValues[tempIdx].timeStamp); 00164 // } 00165 00166 // Format the broadcast message 00167 time_t timeNow = time(NULL); 00168 strcpy(broadcastMsgBuffer, broadcastMsgPrefix); 00169 sprintf(broadcastMsgBuffer+strlen(broadcastMsgBuffer), broadcastMsgGasFormat, gasUseCounter.GetCount(), gasUseCounter.GetPulseRateMs()); 00170 strcpy(broadcastMsgBuffer+strlen(broadcastMsgBuffer), ","); 00171 for (int tempIdx = 0; tempIdx < numTempValues; tempIdx++) 00172 { 00173 sprintf(broadcastMsgBuffer+strlen(broadcastMsgBuffer), broadcastTemperatureFormat, tempValues[tempIdx].address, 00174 getThermometerName(tempValues[tempIdx].address), 00175 tempValues[tempIdx].tempInCentigrade); 00176 strcpy(broadcastMsgBuffer+strlen(broadcastMsgBuffer), ","); 00177 } 00178 sprintf(broadcastMsgBuffer+strlen(broadcastMsgBuffer), broadcastVoltAlerterFormat, 1, getVoltAlerterName(0), voltAlerter1.GetState()); 00179 strcpy(broadcastMsgBuffer+strlen(broadcastMsgBuffer), ","); 00180 sprintf(broadcastMsgBuffer+strlen(broadcastMsgBuffer), broadcastVoltAlerterFormat, 2, getVoltAlerterName(1), voltAlerter2.GetState()); 00181 strcpy(broadcastMsgBuffer+strlen(broadcastMsgBuffer), ","); 00182 sprintf(broadcastMsgBuffer+strlen(broadcastMsgBuffer), broadcastVoltAlerterFormat, 3, getVoltAlerterName(2), voltAlerter3.GetState()); 00183 sprintf(broadcastMsgBuffer+strlen(broadcastMsgBuffer), broadcastMsgSuffix, timeNow); 00184 } 00185 00186 // Send broadcast message with current data 00187 void SendInfoBroadcast() 00188 { 00189 led3 = true; 00190 00191 // Init the sending socket 00192 sendUDPSocket.init(); 00193 sendUDPSocket.set_broadcasting(); 00194 broadcastEndpoint.set_address("255.255.255.255", BROADCAST_PORT); 00195 00196 // Format the message 00197 GenBroadcastMessage(); 00198 00199 // Send 00200 int bytesToSend = strlen(broadcastMsgBuffer); 00201 int rslt = sendUDPSocket.sendTo(broadcastEndpoint, broadcastMsgBuffer, bytesToSend); 00202 if (rslt == bytesToSend) 00203 { 00204 logger.LogDebug("Broadcast (len %d) Sent ok %s", bytesToSend, broadcastMsgBuffer); 00205 } 00206 else if (rslt == -1) 00207 { 00208 logger.LogDebug("Broadcast Failed to send %s", broadcastMsgBuffer); 00209 } 00210 else 00211 { 00212 logger.LogDebug("Broadcast Didn't send all of %s", broadcastMsgBuffer); 00213 } 00214 00215 // Log the data 00216 logger.LogData(broadcastMsgBuffer); 00217 00218 led3 = false; 00219 } 00220 00221 char* getCurDataCallback(int method, char* cmdStr, char* argStr, char* msgBuffer, int msgLen, 00222 int contentLen, unsigned char* pPayload, int payloadLen, int splitPayloadPos) 00223 { 00224 // Format message 00225 GenBroadcastMessage(); 00226 return broadcastMsgBuffer; 00227 } 00228 00229 char* setGasUseCallback(int method, char* cmdStr, char* argStr, char* msgBuffer, int msgLen, 00230 int contentLen, unsigned char* pPayload, int payloadLen, int splitPayloadPos) 00231 { 00232 logger.LogDebug("Setting gas use count %s", argStr); 00233 int newGasUse = 0; 00234 char* eqStr = strchr(argStr, '='); 00235 if (eqStr == NULL) 00236 return "SetGasValue FAILED"; 00237 sscanf(eqStr+1, "%d", &newGasUse); 00238 gasUseCounter.SetCount(newGasUse); 00239 return "SetGasValue OK"; 00240 } 00241 00242 // Handle SD Card File Delete 00243 char* sdCardDelete(int method, char* cmdStr, char* argStr, char* msgBuffer, int msgLen, 00244 int contentLen, unsigned char* pPayload, int payloadLen, int splitPayloadPos) 00245 { 00246 printf("Delete file %s\r\n", argStr); 00247 sdCardMutex.lock(); 00248 char* baseWebFolder = "/sd/"; 00249 char filename[MAX_FNAME_LENGTH+1]; 00250 sprintf(filename, "%s%s", baseWebFolder, argStr); 00251 printf("Full filename for delete is %s\r\n", filename); 00252 bool delOk = (remove(filename) == 0); 00253 sdCardMutex.unlock(); 00254 if (delOk) 00255 return "Delete OK"; 00256 return "Delete Failed"; 00257 } 00258 00259 // Handle SD Card File Upload 00260 char* sdCardUpload(int method, char* cmdStr, char* argStr, char* msgBuffer, int msgLen, 00261 int contentLen, unsigned char* pPayload, int payloadLen, int splitPayloadPos) 00262 { 00263 printf("Upload file %s PayloadLen %d ContentLen %d MsgLen %d\r\n", argStr, payloadLen, contentLen, msgLen); 00264 if (payloadLen == 0) 00265 { 00266 printf("Upload file - payload len == 0 - quitting\r\n"); 00267 return "OK"; 00268 } 00269 00270 bool writeSuccess = false; 00271 if (splitPayloadPos == 0) 00272 { 00273 // Get the filename 00274 fileNameForWrite[0] = '\0'; 00275 fileRemainingForWrite = 0; 00276 char* filenameText = " filename=\""; 00277 char* pFilename = strstr((char*)pPayload, filenameText); 00278 if ((pFilename != NULL) && (pFilename - ((char*)pPayload) < 200)) 00279 { 00280 pFilename += strlen(filenameText); 00281 char* pEndFileName = strstr(pFilename, "\""); 00282 if (pEndFileName != NULL) 00283 { 00284 int fileNameLen = pEndFileName - pFilename; 00285 if (fileNameLen + strlen(baseWebFolder) < MAX_FNAME_LENGTH) 00286 { 00287 strcpy(fileNameForWrite, baseWebFolder); 00288 strncpy(fileNameForWrite+strlen(baseWebFolder), pFilename, fileNameLen); 00289 fileNameForWrite[fileNameLen+strlen(baseWebFolder)] = '\0'; 00290 printf("Upload file - filename %s\r\n", fileNameForWrite); 00291 } 00292 else 00293 { 00294 printf("Upload file - filename too long - quitting\r\n"); 00295 } 00296 } 00297 else 00298 { 00299 printf("Upload file - end of filename not found - quitting\r\n"); 00300 } 00301 } 00302 else 00303 { 00304 printf("Upload file - filename not found (or not near start of message) - quitting\r\n"); 00305 } 00306 // Write first chunk to file 00307 if (strlen(fileNameForWrite) > 0) 00308 { 00309 // Find the chunk start 00310 char* chunkStartText = "\r\n\r\n"; 00311 char* pChunk = strstr((char*)pPayload, chunkStartText); 00312 if ((pChunk != NULL) && (pChunk - ((char*)pPayload) < 300)) 00313 { 00314 pChunk += strlen(chunkStartText); 00315 // Find chunk len 00316 int headerLen = pChunk - ((char*)pPayload); 00317 int chunkLen = payloadLen - headerLen; 00318 fileRemainingForWrite = contentLen - headerLen; 00319 int numBytesToWrite = chunkLen; 00320 if (fileRemainingForWrite > 0) 00321 { 00322 // Check for end boundary of data 00323 if ((fileRemainingForWrite - chunkLen < 100) && (payloadLen > 50)) 00324 { 00325 char* pEndForm = strstr(((char*)pPayload) + payloadLen - 50, "\r\n------"); 00326 if (pEndForm != NULL) 00327 { 00328 numBytesToWrite = pEndForm - pChunk; 00329 printf("Upload file - payload end found writing %d bytes\r\n", numBytesToWrite); 00330 } 00331 } 00332 // Write chunk to file 00333 sdCardMutex.lock(); 00334 FILE* fp = fopen(fileNameForWrite, "w+"); 00335 if (fp == NULL) 00336 { 00337 printf("Upload file - Failed to open output file\r\n"); 00338 } 00339 else 00340 { 00341 fwrite(pChunk, sizeof(char), numBytesToWrite, fp); 00342 fclose(fp); 00343 printf("Upload file - written %d bytes\r\n", numBytesToWrite); 00344 writeSuccess = true; 00345 } 00346 sdCardMutex.unlock(); 00347 // Reduce remaining bytes 00348 fileRemainingForWrite -= chunkLen; 00349 } 00350 else 00351 { 00352 printf("Upload file - file remaining for write <= 0 - quitting\r\n"); 00353 } 00354 } 00355 else 00356 { 00357 printf("Upload file - can't find chunk start - quitting\r\n"); 00358 } 00359 } 00360 else 00361 { 00362 printf("Upload file - filename blank - quitting\r\n"); 00363 } 00364 00365 } 00366 else 00367 { 00368 if (strlen(fileNameForWrite) != 0) 00369 { 00370 if (fileRemainingForWrite > 0) 00371 { 00372 int numBytesToWrite = payloadLen; 00373 // Check for end boundary of data 00374 if ((fileRemainingForWrite - payloadLen < 100) && (payloadLen > 50)) 00375 { 00376 char* pEndForm = strstr(((char*)pPayload) + payloadLen - 50, "\r\n------"); 00377 if (pEndForm != NULL) 00378 { 00379 numBytesToWrite = pEndForm - ((char*)pPayload); 00380 printf("Upload file - payload end found writing %d bytes\r\n", numBytesToWrite); 00381 } 00382 } 00383 sdCardMutex.lock(); 00384 FILE* fp = fopen(fileNameForWrite, "a"); 00385 if (fp == NULL) 00386 { 00387 printf("Failed to open output file\r\n"); 00388 } 00389 else 00390 { 00391 fwrite(pPayload, sizeof(char), numBytesToWrite, fp); 00392 fclose(fp); 00393 writeSuccess = true; 00394 printf("Upload file - written %d bytes\r\n", numBytesToWrite); 00395 } 00396 sdCardMutex.unlock(); 00397 // Reduce remaining bytes 00398 fileRemainingForWrite -= payloadLen; 00399 } 00400 else 00401 { 00402 printf("Upload file - file remaining for write <= 0 - quitting\r\n"); 00403 } 00404 } 00405 else 00406 { 00407 printf("Upload file - filename blank - quitting\r\n"); 00408 } 00409 } 00410 00411 // Return results 00412 if (writeSuccess) 00413 return "Write OK"; 00414 return "Write Failed"; 00415 } 00416 00417 // Create, configure and run the web server 00418 void http_thread(void const* arg) 00419 { 00420 RdWebServer webServer(&sdCardMutex); 00421 webServer.addCommand("", RdWebServerCmdDef::CMD_SDORUSBFILE, NULL, "index.htm", false); 00422 webServer.addCommand("gear-gr.png", RdWebServerCmdDef::CMD_SDORUSBFILE, NULL, NULL, true); 00423 webServer.addCommand("listfiles", RdWebServerCmdDef::CMD_SDORUSBFILE, NULL, "/", false); 00424 webServer.addCommand("getcurdata", RdWebServerCmdDef::CMD_CALLBACK, &getCurDataCallback); 00425 webServer.addCommand("setgascount", RdWebServerCmdDef::CMD_CALLBACK, &setGasUseCallback); 00426 webServer.addCommand("delete", RdWebServerCmdDef::CMD_CALLBACK, &sdCardDelete); 00427 webServer.addCommand("upload", RdWebServerCmdDef::CMD_CALLBACK, &sdCardUpload); 00428 webServer.init(WEBPORT, &led4, baseWebFolder); 00429 webServer.run(); 00430 } 00431 00432 // Network time protocol (NTP) thread to get time from internet 00433 void ntp_thread(void const* arg) 00434 { 00435 while (1) 00436 { 00437 logger.LogDebug("Trying to update time..."); 00438 if (ntp.setTime("0.pool.ntp.org") == NTP_OK) 00439 { 00440 osDelay(1000); // This delay is simply to try to improve printf output 00441 logger.LogDebug("Set time successfully"); 00442 time_t ctTime; 00443 ctTime = time(NULL); 00444 logger.LogDebug("Time is set to (UTC): %s", ctime(&ctTime)); 00445 } 00446 else 00447 { 00448 logger.LogDebug("Cannot set from NTP"); 00449 } 00450 00451 // Refresh time every K hours 00452 for (int k = 0; k < NTP_REFRESH_INTERVAL_HOURS; k++) 00453 { 00454 // 1 hour 00455 for (int i = 0; i < 60; i++) 00456 { 00457 for (int j = 0; j < 60; j++) 00458 { 00459 osDelay(1000); 00460 } 00461 logger.LogDebug("%d mins to next NTP time refresh", (NTP_REFRESH_INTERVAL_HOURS-k-1)*60 + (59-i)); 00462 } 00463 } 00464 } 00465 } 00466 00467 // #define TEST_WATCHDOG 1 00468 #ifdef TEST_WATCHDOG 00469 int watchdogTestLoopCount = 0; 00470 #endif 00471 00472 // Main 00473 int main() 00474 { 00475 pc.baud(115200); 00476 logger.SetDebugDest(true, true); 00477 logger.LogDebug("\r\n\r\nGas Monitor V2 - Rob Dobson 2015"); 00478 00479 // Initialise thermometers 00480 thermometers.Init(); 00481 00482 // Get the current count from the SD Card 00483 gasUseCounter.Init(); 00484 00485 // Setup ethernet interface 00486 char macAddr[6]; 00487 mbed_mac_address(macAddr); 00488 logger.LogDebug("Ethernet MAC address: %02x:%02x:%02x:%02x:%02x:%02x", macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]); 00489 logger.LogDebug("Connecting to ethernet ..."); 00490 00491 // Init ethernet 00492 EthernetInterface::init(); 00493 00494 // Using code described here https://developer.mbed.org/questions/1602/How-to-set-the-TCPIP-stack-s-hostname-pr/ 00495 // to setName on the ethernet interface 00496 EthernetInterface::setName(systemName); 00497 00498 // Connect ethernet 00499 EthernetInterface::connect(); 00500 logger.LogDebug("IP Address: %s HostName %s", EthernetInterface::getIPAddress(), EthernetInterface::getName()); 00501 00502 // NTP Time setter 00503 Thread ntpTimeSetter(&ntp_thread); 00504 00505 // Web Server 00506 Thread httpServer(&http_thread, NULL, osPriorityNormal, (DEFAULT_STACK_SIZE * 3)); 00507 00508 // Store reason for restart 00509 bool watchdogCausedRestart = watchdog.WatchdogCausedRestart(); 00510 bool restartCauseRecorded = false; 00511 00512 // Setup the watchdog for reset 00513 watchdog.SetTimeoutSecs(300); 00514 00515 // Time of last broadcast 00516 time_t timeOfLastBroadcast = time(NULL); 00517 const int TIME_BETWEEN_BROADCASTS_IN_SECS = 60; 00518 while(true) 00519 { 00520 // Check if we can record the reason for restart (i.e. if time is now set) 00521 if (!restartCauseRecorded) 00522 { 00523 time_t nowTime = time(NULL); 00524 if (nowTime > 1000000000) 00525 { 00526 // Record the reason for restarting in the log file 00527 if (watchdogCausedRestart) 00528 { 00529 logger.LogEvent("Watchdog Restart"); 00530 logger.LogDebug("Watchdog Restart"); 00531 } 00532 else 00533 { 00534 logger.LogEvent("Normal Restart"); 00535 logger.LogDebug("Normal Restart"); 00536 } 00537 restartCauseRecorded = true; 00538 } 00539 } 00540 00541 // Loop delay 00542 osDelay(LOOP_DELAY_IN_MS); 00543 00544 // Feed the watchdog and show the flashing LED 00545 led1 = !led1; 00546 watchdog.Feed(); 00547 00548 // Service gas count 00549 if (gasUseCounter.Service()) 00550 { 00551 SendInfoBroadcast(); 00552 timeOfLastBroadcast = time(NULL); 00553 } 00554 00555 // Service thermometers 00556 thermometers.Service(); 00557 00558 // Check if ready for a broadcast 00559 if ((time(NULL) - timeOfLastBroadcast) >= TIME_BETWEEN_BROADCASTS_IN_SECS) 00560 { 00561 SendInfoBroadcast(); 00562 timeOfLastBroadcast = time(NULL); 00563 } 00564 00565 // Service volt alerters 00566 voltAlerter1.Service(); 00567 voltAlerter2.Service(); 00568 voltAlerter3.Service(); 00569 00570 // Set LED2 to the state of the first volt alerter 00571 led2 = voltAlerter1.GetState(); 00572 00573 #ifdef TEST_WATCHDOG 00574 // After about 20 seconds of operation we'll hang to test the watchdog 00575 if (watchdogTestLoopCount++ > 80) 00576 { 00577 // This should cause watchdog to kick in and reset 00578 osDelay(20000); 00579 } 00580 #endif 00581 } 00582 } 00583
Generated on Tue Jul 12 2022 18:43:11 by 1.7.2